[
  {
    "path": ".codex",
    "content": ""
  },
  {
    "path": ".gitattributes",
    "content": "*.min.js binary\n*.min.css binary"
  },
  {
    "path": ".github/settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<settings xmlns=\"http://maven.apache.org/SETTINGS/1.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n          xsi:schemaLocation=\"http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd\">\n    <profiles>\n        <profile>\n            <id>gha</id>\n        </profile>\n    </profiles>\n</settings>\n\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: build\n\non:\n  push:\n    branches: [ 'master' ]\n  pull_request:\n    branches: [ 'master' ]\n\nconcurrency:\n  group: ${{ github.ref }}-build\n  cancel-in-progress: true\n\njobs:\n  check-labels:\n    runs-on: ubuntu-latest\n    outputs:\n      skip: ${{ steps.check.outputs.skip }}\n\n    steps:\n      - name: Check PR labels\n        id: check\n        run: |\n          if [[ \"${{ github.event_name }}\" == \"pull_request\" ]]; then\n            labels=$(jq -r '.pull_request.labels | map(.name) | join(\",\")' \"$GITHUB_EVENT_PATH\")\n            if [[ \"$labels\" == *\"[no ci]\"* ]]; then\n              echo \"skip=true\" >> $GITHUB_OUTPUT\n            else\n              echo \"skip=false\" >> $GITHUB_OUTPUT\n            fi\n          else\n            echo \"skip=false\" >> $GITHUB_OUTPUT\n            echo \"skip_tests=false\" >> $GITHUB_OUTPUT\n          fi\n\n  build:\n    needs: check-labels\n    if: needs.check-labels.outputs.skip == 'false'\n    env:\n      WORK: ${{ github.workspace }}/tmp\n      MAVEN_REPO_LOCAL: ${{ github.workspace }}/tmp/.m2/repository\n\n    strategy:\n      matrix:\n        profile: [ 'jdk17', 'jdk17-aarch64' ]\n        include:\n          - jdk_version: '17'\n          - profile: 'jdk17'\n            runs_on: ubuntu-24.04\n          - profile: 'jdk17-aarch64'\n            runs_on: ubuntu-24.04-arm\n      fail-fast: false\n\n    runs-on: ${{ matrix.runs_on }}\n\n    steps:\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0\n        with:\n          driver: docker\n\n      - name: Login to DockerHub\n        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0\n        if: github.event.pull_request.head.repo.full_name == 'walmartlabs/concord'\n        with:\n          username: ${{ secrets.OSS_DOCKERHUB_USERNAME }}\n          password: ${{ secrets.OSS_DOCKERHUB_PASSWORD }}\n\n      - name: Set up JDK\n        uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0\n        with:\n          java-version: '${{ matrix.jdk_version }}'\n          distribution: 'temurin'\n\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Restore Maven cache\n        id: restore-maven-cache\n        uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4\n        with:\n          path: ${{ env.MAVEN_REPO_LOCAL }}\n          key: ${{ runner.os }}-maven-${{ matrix.profile }}-${{ hashFiles('**/pom.xml', '.github/settings.xml') }}\n          restore-keys: |\n            ${{ runner.os }}-maven-${{ matrix.profile }}-\n            ${{ runner.os }}-maven-\n\n      - name: Build and test with Maven\n        env:\n          SKIP_DOCKER_TESTS: \"true\"\n        run: |\n          mkdir -p \"${WORK}\" \"${MAVEN_REPO_LOCAL}\"\n          chmod 1777 \"${WORK}\"\n          ./mvnw -s .github/settings.xml -B clean install \\\n            -Dmaven.repo.local=\"${MAVEN_REPO_LOCAL}\" \\\n            -Djava.io.tmpdir=\"${WORK}\" \\\n            -Pgha -Pdocker -Pit -P${{ matrix.profile }}\n\n      - name: Remove local Concord artifacts from Maven cache\n        if: success()\n        run: rm -rf \"${MAVEN_REPO_LOCAL}/com/walmartlabs/concord\"\n\n      - name: Save Maven cache\n        if: success() && steps.restore-maven-cache.outputs.cache-hit != 'true'\n        uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4\n        with:\n          path: ${{ env.MAVEN_REPO_LOCAL }}\n          key: ${{ steps.restore-maven-cache.outputs.cache-primary-key }}\n"
  },
  {
    "path": ".github/workflows/docker-multiarch.yml",
    "content": "name: docker-multiarch\n\non:\n  workflow_dispatch:\n    inputs:\n      ref:\n        description: Release tag to build, e.g. 2.39.0\n        required: true\n        type: string\n      docker_tag:\n        description: Docker tag to apply to the published images\n        required: true\n        type: string\n      docker_namespace:\n        description: Docker Hub namespace to publish into\n        required: true\n        default: walmartlabs\n        type: string\n  pull_request:\n    branches: [ 'master' ]\n\npermissions:\n  contents: read\n\nconcurrency:\n  group: docker-multiarch-${{ github.event.pull_request.number || github.event.inputs.docker_tag || github.event.inputs.ref || github.ref }}\n  cancel-in-progress: true\n\njobs:\n  check-labels:\n    runs-on: ubuntu-latest\n    outputs:\n      skip: ${{ steps.check.outputs.skip }}\n\n    steps:\n      - name: Check PR labels\n        id: check\n        run: |\n          if [[ \"${{ github.event_name }}\" == \"pull_request\" ]]; then\n            labels=$(jq -r '.pull_request.labels | map(.name) | join(\",\")' \"$GITHUB_EVENT_PATH\")\n            if [[ \"$labels\" == *\"[no ci]\"* ]]; then\n              echo \"skip=true\" >> $GITHUB_OUTPUT\n            else\n              echo \"skip=false\" >> $GITHUB_OUTPUT\n            fi\n          else\n            echo \"skip=false\" >> $GITHUB_OUTPUT\n          fi\n\n  build:\n    needs: check-labels\n    if: needs.check-labels.outputs.skip == 'false'\n    runs-on: ubuntu-24.04\n    timeout-minutes: 120\n    env:\n      WORK: ${{ github.workspace }}/tmp\n      MAVEN_REPO_LOCAL: ${{ github.workspace }}/tmp/.m2/repository\n      IMAGE_PLATFORMS: linux/amd64,linux/arm64\n      LOCAL_REGISTRY: 127.0.0.1:5000\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.ref || github.event.pull_request.head.sha }}\n\n      - name: Resolve build target\n        run: |\n          if [[ \"${{ github.event_name }}\" == 'workflow_dispatch' ]]; then\n            build_ref='${{ github.event.inputs.ref }}'\n            build_tag='${{ github.event.inputs.docker_tag }}'\n            docker_namespace='${{ github.event.inputs.docker_namespace }}'\n            maven_also_make=''\n\n            release_tag=\"${build_ref#refs/tags/}\"\n            if ! [[ \"${release_tag}\" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+$ ]]; then\n              echo \"Workflow dispatch expects a release tag like '2.39.0', got '${build_ref}'.\" >&2\n              exit 1\n            fi\n\n            git fetch --depth=1 origin \"refs/tags/${release_tag}:refs/tags/${release_tag}\"\n            tag_commit=\"$(git rev-parse \"refs/tags/${release_tag}^{commit}\")\"\n            head_commit=\"$(git rev-parse HEAD)\"\n            if [[ \"${tag_commit}\" != \"${head_commit}\" ]]; then\n              echo \"Workflow dispatch ref '${build_ref}' resolved to ${head_commit}, but tag '${release_tag}' points to ${tag_commit}.\" >&2\n              exit 1\n            fi\n          else\n            build_ref='${{ github.event.pull_request.head.sha }}'\n            build_tag=\"pr-${{ github.event.pull_request.number }}-$(git rev-parse --short=12 HEAD)\"\n            docker_namespace=\"${LOCAL_REGISTRY}/walmartlabs\"\n            maven_also_make='-am'\n          fi\n\n          if ! [[ \"${build_tag}\" =~ ^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$ ]]; then\n            echo \"Tag '${build_tag}' is not a valid Docker tag.\" >&2\n            exit 1\n          fi\n\n          if [[ -z \"${docker_namespace}\" ]]; then\n            echo \"Docker namespace must not be empty.\" >&2\n            exit 1\n          fi\n\n          if [[ \"${{ github.event_name }}\" == 'workflow_dispatch' ]] && [[ \"${docker_namespace}\" == */* ]]; then\n            echo \"Workflow dispatch expects a Docker Hub namespace only, e.g. 'walmartlabs'.\" >&2\n            exit 1\n          fi\n\n          echo \"BUILD_TAG=${build_tag}\" >> \"$GITHUB_ENV\"\n          echo \"BUILD_REF=${build_ref}\" >> \"$GITHUB_ENV\"\n          echo \"DOCKER_NAMESPACE=${docker_namespace}\" >> \"$GITHUB_ENV\"\n          echo \"MAVEN_ALSO_MAKE=${maven_also_make}\" >> \"$GITHUB_ENV\"\n          echo \"Using BUILD_REF=${build_ref}\"\n          echo \"Using BUILD_TAG=${build_tag}\"\n          echo \"Using DOCKER_NAMESPACE=${docker_namespace}\"\n          echo \"Using MAVEN_ALSO_MAKE=${maven_also_make}\"\n          git rev-parse HEAD\n\n      - name: Restore Maven cache\n        id: restore-maven-cache\n        uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4\n        with:\n          path: ${{ env.MAVEN_REPO_LOCAL }}\n          key: ${{ runner.os }}-maven-docker-multiarch-${{ hashFiles('**/pom.xml', '.github/settings.xml') }}\n          restore-keys: |\n            ${{ runner.os }}-maven-docker-multiarch-\n            ${{ runner.os }}-maven-\n\n      - name: Set up JDK\n        uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0\n        with:\n          java-version: '17'\n          distribution: 'temurin'\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n        with:\n          platforms: arm64\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0\n        with:\n          driver: docker-container\n          driver-opts: |\n            network=host\n\n      - name: Start local registry\n        if: github.event_name != 'workflow_dispatch'\n        run: |\n          docker run -d -p 5000:5000 --name registry registry:2\n\n          for i in $(seq 1 30); do\n            if curl -fsS \"http://${LOCAL_REGISTRY}/v2/\" >/dev/null; then\n              exit 0\n            fi\n\n            sleep 1\n          done\n\n          docker logs registry\n          exit 1\n\n      - name: Login to DockerHub\n        if: github.event_name == 'workflow_dispatch'\n        uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0\n        with:\n          username: ${{ secrets.OSS_DOCKERHUB_USERNAME }}\n          password: ${{ secrets.OSS_DOCKERHUB_PASSWORD }}\n\n      - name: Prepare Docker image contexts\n        run: |\n          mkdir -p \"${WORK}\" \"${MAVEN_REPO_LOCAL}\"\n          chmod 1777 \"${WORK}\"\n\n          maven_reactor_args=()\n          if [[ -n \"${MAVEN_ALSO_MAKE}\" ]]; then\n            maven_reactor_args+=(\"${MAVEN_ALSO_MAKE}\")\n          fi\n\n          ./mvnw -s .github/settings.xml -B clean package \\\n            -Dmaven.repo.local=\"${MAVEN_REPO_LOCAL}\" \\\n            -Djava.io.tmpdir=\"${WORK}\" \\\n            -DskipTests \\\n            -Pgha \\\n            -pl docker-images/base,docker-images/ansible,docker-images/agent,docker-images/server,docker-images/agent-operator \\\n            \"${maven_reactor_args[@]}\"\n\n      - name: Build Docker images\n        working-directory: docker-images\n        run: |\n          platform_args=()\n          IFS=',' read -ra platforms <<< \"${IMAGE_PLATFORMS}\"\n          for platform in \"${platforms[@]}\"; do\n            platform=\"${platform//[[:space:]]/}\"\n            if [[ -n \"${platform}\" ]]; then\n              platform_args+=(--set \"*.platform=${platform}\")\n            fi\n          done\n\n          docker buildx bake \\\n            --push \\\n            \"${platform_args[@]}\" \\\n            --var \"DOCKER_NAMESPACE=${DOCKER_NAMESPACE}\" \\\n            --var \"DOCKER_TAG=${BUILD_TAG}\" \\\n            --var \"JDK_VERSION=17\"\n\n      - name: Verify multi-arch manifests\n        run: |\n          images=(\n            concord-base\n            concord-ansible\n            concord-agent\n            concord-server\n            concord-agent-operator\n          )\n\n          for image in \"${images[@]}\"; do\n            ref=\"${DOCKER_NAMESPACE}/${image}:${BUILD_TAG}\"\n            echo \"Inspecting ${ref}\"\n            output=$(docker buildx imagetools inspect \"${ref}\")\n            printf '%s\\n' \"${output}\"\n\n            printf '%s\\n' \"${output}\" | grep -q 'linux/amd64'\n            printf '%s\\n' \"${output}\" | grep -q 'linux/arm64'\n          done\n\n      - name: Remove local Concord artifacts from Maven cache\n        if: success()\n        run: rm -rf \"${MAVEN_REPO_LOCAL}/com/walmartlabs/concord\"\n\n      - name: Save Maven cache\n        if: success() && steps.restore-maven-cache.outputs.cache-hit != 'true'\n        uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4\n        with:\n          path: ${{ env.MAVEN_REPO_LOCAL }}\n          key: ${{ steps.restore-maven-cache.outputs.cache-primary-key }}\n"
  },
  {
    "path": ".gitignore",
    "content": "*.iml\n*.ipr\n*.iws\n*.retry\n.*.swo\n.*.swp\n.arcconfig\n.classpath\n.DS_Store\n.idea\n.pmd\n.pmdruleset.xml\n.project\n.settings\n.vagrant\n.vscode\nbuildNumber.properties\nconsole2/build\nflow-typed/\njmeter.log\nnb-configuration.xml\nnode_modules/\nnohup.out\nnpm-debug.log\npom.xml.bak\npom.xml.next\npom.xml.versionsBackup\nrelease.properties\ntarget/\nyarn.lock\n.mvn/wrapper/maven-wrapper.jar\n.java-version\n.attach_pid*\ndependency-reduced-pom.xml\n"
  },
  {
    "path": ".insights.yml",
    "content": "team:\n- id: 20906\nproduct:\n- id: C0AF273F-7470-4A8B-B76E-190F00FFF920\nlanguages:\n- JavaScript\n- JVM\ncloud:\n- autoRestart: false"
  },
  {
    "path": ".looper/render-settings.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\nout=\"${1:-/dev/stdout}\"\n\nrequired_vars=(\n    MAVEN_MIRROR\n    NEXUS_URL\n    MAVEN_SITE_URL\n    NODE_DOWNLOAD_URL\n    NPM_INSTALL_CMD\n    AGENT_IMAGE\n    ANSIBLE_IMAGE\n    CONSOLE_IMAGE\n    DB_IMAGE\n    DIND_IMAGE\n    OLDAP_IMAGE\n    S3MOCK_IMAGE\n    SELENIUM_IMAGE\n    SERVER_IMAGE\n    SOCAT_IMAGE\n)\n\nfor name in \"${required_vars[@]}\"; do\n    if [ -z \"${!name:-}\" ]; then\n        echo \"Missing required environment variable: ${name}\" >&2\n        exit 1\n    fi\ndone\n\nxml_escape() {\n    printf '%s' \"$1\" | sed \\\n        -e 's/&/\\&amp;/g' \\\n        -e 's/\"/\\&quot;/g' \\\n        -e \"s/'/\\&apos;/g\" \\\n        -e 's/</\\&lt;/g' \\\n        -e 's/>/\\&gt;/g'\n}\n\ncat >\"${out}\" <<EOF\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<settings>\n    <mirrors>\n        <mirror>\n            <id>walmart-gec</id>\n            <mirrorOf>external:*</mirrorOf>\n            <url>$(xml_escape \"${MAVEN_MIRROR}\")</url>\n        </mirror>\n    </mirrors>\n\n    <profiles>\n        <profile>\n            <id>looper</id>\n            <activation>\n                <activeByDefault>false</activeByDefault>\n            </activation>\n            <properties>\n                <scm.connection>\\${env.SCM_CONNECTION}</scm.connection>\n            </properties>\n            <repositories>\n                <repository>\n                    <id>central</id>\n                    <url>http://central</url>\n                    <releases>\n                        <enabled>true</enabled>\n                    </releases>\n                    <snapshots>\n                        <enabled>true</enabled>\n                    </snapshots>\n                </repository>\n            </repositories>\n            <pluginRepositories>\n                <pluginRepository>\n                    <id>central</id>\n                    <url>http://central</url>\n                    <releases>\n                        <enabled>true</enabled>\n                    </releases>\n                    <snapshots>\n                        <enabled>true</enabled>\n                    </snapshots>\n                </pluginRepository>\n            </pluginRepositories>\n        </profile>\n\n        <profile>\n            <id>walmart</id>\n            <activation>\n                <activeByDefault>false</activeByDefault>\n            </activation>\n            <properties>\n                <node.downloadRoot>$(xml_escape \"${NODE_DOWNLOAD_URL}\")</node.downloadRoot>\n                <npm.installCmd>$(xml_escape \"${NPM_INSTALL_CMD}\")</npm.installCmd>\n\n                <public.serverId>walmart-gec</public.serverId>\n                <public.nexusUrl>$(xml_escape \"${NEXUS_URL}\")</public.nexusUrl>\n                <public-release.serverId>\\${public.serverId}</public-release.serverId>\n                <public-release.url>\\${public.nexusUrl}/content/repositories/devtools</public-release.url>\n                <public-snapshot.serverId>\\${public.serverId}</public-snapshot.serverId>\n                <public-snapshot.url>\\${public.nexusUrl}/content/repositories/devtools-snapshots</public-snapshot.url>\n\n                <site.id>mvn-site</site.id>\n                <site.url>$(xml_escape \"${MAVEN_SITE_URL}\")</site.url>\n\n                <agent.image>$(xml_escape \"${AGENT_IMAGE}\")</agent.image>\n                <ansible.image>$(xml_escape \"${ANSIBLE_IMAGE}\")</ansible.image>\n                <console.image>$(xml_escape \"${CONSOLE_IMAGE}\")</console.image>\n                <db.image>$(xml_escape \"${DB_IMAGE}\")</db.image>\n                <dind.image>$(xml_escape \"${DIND_IMAGE}\")</dind.image>\n                <oldap.image>$(xml_escape \"${OLDAP_IMAGE}\")</oldap.image>\n                <s3mock.image>$(xml_escape \"${S3MOCK_IMAGE}\")</s3mock.image>\n                <selenium.image>$(xml_escape \"${SELENIUM_IMAGE}\")</selenium.image>\n                <server.image>$(xml_escape \"${SERVER_IMAGE}\")</server.image>\n                <socat.image>$(xml_escape \"${SOCAT_IMAGE}\")</socat.image>\n            </properties>\n        </profile>\n    </profiles>\n</settings>\nEOF\n"
  },
  {
    "path": ".looper/settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<settings>\n    <mirrors>\n        <mirror>\n            <id>walmart-gec</id>\n            <mirrorOf>external:*</mirrorOf>\n            <url>${env.MAVEN_MIRROR}</url>\n        </mirror>\n    </mirrors>\n\n    <profiles>\n        <profile>\n            <id>looper</id>\n            <activation>\n                <activeByDefault>false</activeByDefault>\n            </activation>\n            <properties>\n                <scm.connection>${env.SCM_CONNECTION}</scm.connection>\n            </properties>\n            <repositories>\n                <repository>\n                    <id>central</id>\n                    <url>http://central</url>\n                    <releases>\n                        <enabled>true</enabled>\n                    </releases>\n                    <snapshots>\n                        <enabled>true</enabled>\n                    </snapshots>\n                </repository>\n            </repositories>\n            <pluginRepositories>\n                <pluginRepository>\n                    <id>central</id>\n                    <url>http://central</url>\n                    <releases>\n                        <enabled>true</enabled>\n                    </releases>\n                    <snapshots>\n                        <enabled>true</enabled>\n                    </snapshots>\n                </pluginRepository>\n            </pluginRepositories>\n        </profile>\n\n        <profile>\n            <id>walmart</id>\n            <activation>\n                <activeByDefault>false</activeByDefault>\n            </activation>\n            <properties>\n                <node.downloadRoot>${env.NODE_DOWNLOAD_URL}</node.downloadRoot>\n                <npm.installCmd>${env.NPM_INSTALL_CMD}</npm.installCmd>\n\n                <public.serverId>walmart-gec</public.serverId>\n                <public.nexusUrl>${env.NEXUS_URL}</public.nexusUrl>\n                <public-release.serverId>${public.serverId}</public-release.serverId>\n                <public-release.url>${public.nexusUrl}/content/repositories/devtools</public-release.url>\n                <public-snapshot.serverId>${public.serverId}</public-snapshot.serverId>\n                <public-snapshot.url>${public.nexusUrl}/content/repositories/devtools-snapshots</public-snapshot.url>\n\n                <site.id>mvn-site</site.id>\n                <site.url>${env.MAVEN_SITE_URL}</site.url>\n\n                <agent.image>${env.AGENT_IMAGE}</agent.image>\n                <ansible.image>${env.ANSIBLE_IMAGE}</ansible.image>\n                <console.image>${env.CONSOLE_IMAGE}</console.image>\n                <db.image>${env.DB_IMAGE}</db.image>\n                <dind.image>${env.DIND_IMAGE}</dind.image>\n                <oldap.image>${env.OLDAP_IMAGE}</oldap.image>\n                <s3mock.image>${env.S3MOCK_IMAGE}</s3mock.image>\n                <selenium.image>${env.SELENIUM_IMAGE}</selenium.image>\n                <server.image>${env.SERVER_IMAGE}</server.image>\n                <socat.image>${env.SOCAT_IMAGE}</socat.image>\n            </properties>\n        </profile>\n    </profiles>\n</settings>\n"
  },
  {
    "path": ".looper.yml",
    "content": "inherit: job:///walmart-concord\n"
  },
  {
    "path": ".mvn/wrapper/maven-wrapper.properties",
    "content": "# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#   http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\nwrapperVersion=3.3.2\ndistributionType=only-script\ndistributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip\n"
  },
  {
    "path": ".sentinelpolicy",
    "content": "codescanaccess=j3clark,v0v001r,msprin1\nmaxmedium=1\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# AGENTS\n\nStart here:\n\n- [it/README.md](it/README.md)\n- [README.md](README.md)\n- [NOTES.md](NOTES.md)\n\nWorking approach:\n\n- Read the nearest module `README.md` before editing code in that area.\n- Keep `console2/`, backend modules, and runtime/agent changes separated unless the task is explicitly cross-cutting.\n- When investigating `it/console` UI test failures, inspect screenshots from `it/console/target/screenshots/` and prefer native vision/image tools when available; use OCR or terminal renderers only as fallback.\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Change Log\n\n## [2.40.0] - 2026-04-28\n\n### Added\n\n- runtime-v2: initial support for JSON schema validation for\ntask in/out parameters\n([#1263](https://github.com/walmartlabs/concord/pull/1263));\n- concord-cli: basic support for suspend/resume and forms\n([#1295](https://github.com/walmartlabs/concord/pull/1295));\n- project: build multi-arch Docker images\n([#1299](https://github.com/walmartlabs/concord/pull/1299)).\n\n### Changed\n\n- concord-console2: remove redux-saga dependency\n([#1290](https://github.com/walmartlabs/concord/pull/1290));\n- project: respect PR labels in docker-multiarch flow\n([#1304](https://github.com/walmartlabs/concord/pull/1304));\n- concord-console2: remove redux dependency and dead code\n([#1306](https://github.com/walmartlabs/concord/pull/1306));\n- project: update dependencies\n([#1310](https://github.com/walmartlabs/concord/pull/1310));\n- concord-console2: migrate to react-router v7\n([#1307](https://github.com/walmartlabs/concord/pull/1307));\n- concord-server: assert permissions for process details\n([#1309](https://github.com/walmartlabs/concord/pull/1309)).\n\n\n\n## [2.39.0] - 2026-04-09\n\n### Added\n\n- concord-server, queue-client: include requirements in\nProcessResponse\n([#1287](https://github.com/walmartlabs/concord/pull/1287));\n- concord-server: add EffectiveYamlPolicy to control\nrendering and persisting of effective.concord.yml\n([#1301](https://github.com/walmartlabs/concord/pull/1301)).\n\n### Changed\n\n- project: change label for arm64 GHA runners, fix more\nflaky tests\n([#1268](https://github.com/walmartlabs/concord/pull/1268)).\n- project: update groovy test dependencies to 5.X\n([#1280](https://github.com/walmartlabs/concord/pull/1280));\n- project: upgrade concord-maven-plugin to 0.0.37\n([#1283](https://github.com/walmartlabs/concord/pull/1283));\n- concord-server: add GitHub user mapping for user when\nfound via fallback lookup\n([#1285](https://github.com/walmartlabs/concord/pull/1285));\n- console2: upgrade to Vite 8\n([#1286](https://github.com/walmartlabs/concord/pull/1286));\n- project: add AGENTS.md, update READMEs and notes\n([#1288](https://github.com/walmartlabs/concord/pull/1288));\n- project: enable useNativeGit in git-commit-id-plugin\n([#1289](https://github.com/walmartlabs/concord/pull/1289));\n- project: update Node version\n([#1291](https://github.com/walmartlabs/concord/pull/1291));\n- project: upgrade and pin versions of GHA actions\n([#1292](https://github.com/walmartlabs/concord/pull/1292));\n- concord-server-it: attempt to fix CronIT flakiness\n([#1293](https://github.com/walmartlabs/concord/pull/1293));\n- project: update dependencies\n([#1294](https://github.com/walmartlabs/concord/pull/1294));\n- concord-console2: pass test-ids directly instead of using\nwrappers\n([#1296](https://github.com/walmartlabs/concord/pull/1296));\n- project: improve mvnd (and Maven 4) support\n([#1297](https://github.com/walmartlabs/concord/pull/1297));\n- console2: update dependencies\n([#1302](https://github.com/walmartlabs/concord/pull/1302)).\n\n\n\n## [2.38.0] - 2026-03-11\n\n### Changed\n\n- server: make git allowedSchemes configurable and support\nnon-string values in test server config\n([#12282](https://github.com/walmartlabs/concord/pull/1282));\n- common: use abstract MappingAuthConfig for thenCallRealMethod()\nsupport on mocks\n([#1278](https://github.com/walmartlabs/concord/pull/1278)).\n\n\n\n## [2.37.0] - 2026-03-05\n\n### Added\n\n- cli: add `--target-dir` option to specify custom payload\ntarget directory\n([#1275](https://github.com/walmartlabs/concord/pull/1275));\n- cli: allow tasks to interact with remote during local runs\n([#1269](https://github.com/walmartlabs/concord/pull/1269));\n- repository, server, agent: configurable limit for git cli\noutput\n([#1266](https://github.com/walmartlabs/concord/pull/1266)).\n\n### Changed\n\n- runtime-v2: fix JoinCommand not waiting for threads in\nUNWINDING state\n([#1277](https://github.com/walmartlabs/concord/pull/1277));\n- runtime-v2: fix JoinCommand collecting failed threads from\nunrelated parallel blocks\n([#1276](https://github.com/walmartlabs/concord/pull/1276));\n- console2: fix copyToClipboard is not a function\n([#1274](https://github.com/walmartlabs/concord/pull/1274));\n- server: validate restored payload IDs on process restart\n([#1273](https://github.com/walmartlabs/concord/pull/1273));\n- runtime-v2: mask sensitive data in log segment names\n([#1272](https://github.com/walmartlabs/concord/pull/1272));\n- examples: fix/update runtime-v2 example dependencies\n([#1271](https://github.com/walmartlabs/concord/pull/1271));\n- project: build with postgres 14 image\n([#1148](https://github.com/walmartlabs/concord/pull/1148));\n- console2: reduce calls to trigger API\n([#1251](https://github.com/walmartlabs/concord/pull/1251)).\n\n\n\n## [2.36.0] - 2026-02-04\n\n### Added\n\n- concord-server: add ldap.trustAllCertificates, disable by\ndefault\n([#1261](https://github.com/walmartlabs/concord/pull/1261));\n- concord-cli: add support for .gitignore\n([#1264](https://github.com/walmartlabs/concord/pull/1264)).\n\n### Changed\n\n- runtime-v2: add 'name' attribute to script call in schema\n([#1248](https://github.com/walmartlabs/concord/pull/1248));\n- common, server: skip rate limit metrics for null auth id\n([#1249](https://github.com/walmartlabs/concord/pull/1249));\n- concord-console2: replace some saga usage with hooks\n([#1252](https://github.com/walmartlabs/concord/pull/1252));\n- project: update dependency versions\n([#1253](https://github.com/walmartlabs/concord/pull/1254));\n- concord-noderoster: do not build the tarball anymore\n([#1257](https://github.com/walmartlabs/concord/pull/1257));\n- concord-console-it: add basic tests for team UI\n([#1258](https://github.com/walmartlabs/concord/pull/1258));\n- project: concord-maven-plugin version up\n([#1259](https://github.com/walmartlabs/concord/pull/1259));\n- concord-agent: send segmented logs in order logged\n([#1260](https://github.com/walmartlabs/concord/pull/1260));\n- concord-server: assert user enabled before process restart\nor handler\n([#1262](https://github.com/walmartlabs/concord/pull/1262));\n- project: update testcontainers-concord version\n([#1265](https://github.com/walmartlabs/concord/pull/1265)).\n\n\n\n## [2.35.0] - 2025-12-29\n\n### Changed\n\n- oidc: handle session invalidation errors\n([#1239](https://github.com/walmartlabs/concord/pull/1239));\n- concord-server: sanitize escaped unicode nul characters\nfrom events\n([#1241](https://github.com/walmartlabs/concord/pull/1241));\n- oidc: serialization and mapping fix\n([#1245](https://github.com/walmartlabs/concord/pull/1245));\n- server, agent: github app installation clone support\n([#1242](https://github.com/walmartlabs/concord/pull/1242));\n- concord-server: option to look up GH webhook event sender\nby email\n([#1243](https://github.com/walmartlabs/concord/pull/1243));\n- noderoster: configurable host cache size and eviction duration\n([#1246](https://github.com/walmartlabs/concord/pull/1246));\n- concord-server: inject ProcessKeyCache interface instead of\nimplementation to utilize singleton scope\n([#1247](https://github.com/walmartlabs/concord/pull/1247));\n- concord-db, concord-server: cache external app user mapping\nin database\n([#1244](https://github.com/walmartlabs/concord/pull/1244)).\n\n\n\n## [2.34.0] - 2025-10-29\n\n### Added\n\n- runtime-v2: allow plugins to supply custom EL functions\n([#1225](https://github.com/walmartlabs/concord/pull/1225));\n- runtime-v2: allow paths in SensitiveData keys\n([#1228](https://github.com/walmartlabs/concord/pull/1228));\n- runtime-v2: add EL function to mark strings as sensitive\n([#1230](https://github.com/walmartlabs/concord/pull/1230)).\n\n### Changed\n\n- concord-console2: migrate off CRA to Vite\n([#1231](https://github.com/walmartlabs/concord/pull/1231));\n- concord-server-it: fix testFailedHosts for modern Ansible\nversions\n([#1232](https://github.com/walmartlabs/concord/pull/1232));\n- concord-server: retrieve user ldap groups for form access\nassertion\n([#1233](https://github.com/walmartlabs/concord/pull/1233));\n- project: update dependencies\n([#1234](https://github.com/walmartlabs/concord/pull/1234));\n- concord-console2: update dependencies, README\n([#1235](https://github.com/walmartlabs/concord/pull/1235));\n- concord-server-it: improve test\n([#1236](https://github.com/walmartlabs/concord/pull/1236));\n- concord-console2: update Node and Vite versions\n([#1237](https://github.com/walmartlabs/concord/pull/1237)).\n\n\n\n## [2.33.3] - 2025-10-14\n\n### Changed\n\n- repository: fix uri scheme restriction for SSH repo uris\n([#1226](https://github.com/walmartlabs/concord/pull/1226));\n\n\n\n## [2.33.2] - 2025-10-09\n\n### Changed\n\n- concord-server: fix API key creation for the current user\n([#1222](https://github.com/walmartlabs/concord/pull/1222));\n\n\n\n## [2.33.1] - 2025-10-06\n\n### Changed\n\n- pfed-sso: upgrade nimbus-jose-jwt version\n([#1219](https://github.com/walmartlabs/concord/pull/1219));\n- server: fix usernameSignature generation\n([#1221](https://github.com/walmartlabs/concord/pull/1221));\n\n\n\n## [2.33.0] - 2025-09-22\n\n### Added\n\n-  ansible-tasks: support inventories specified in configFile\n([#1216](https://github.com/walmartlabs/concord/pull/1216)).\n\n### Changed\n\n- project: use JSch fork\n([#1210](https://github.com/walmartlabs/concord/pull/1210));\n- slack-tasks: update readme with required oauth scope info\n([#1213](https://github.com/walmartlabs/concord/pull/1213));\n- it: do not archive deps.dir into payloads\n([#1214](https://github.com/walmartlabs/concord/pull/1214));\n- ansible-tasks: remove ini4j dependency\n([#1215](https://github.com/walmartlabs/concord/pull/1215));\n- concord-server: do not create UserPrincipal for API keys\nwithout userId\n([#1218](https://github.com/walmartlabs/concord/pull/1218)).\n\n### Breaking\n\n- oidc: remove pac4j dependency. Will cause (de)serialization\nissues for SUSPENDED processes with OIDC initiators\n([#1217](https://github.com/walmartlabs/concord/pull/1217)).\n\n\n\n## [2.32.0] - 2025-08-20\n\n### Added\n\n- concord-cli: support for remote secrets, configurable secrets\nproviders\n([#1143](https://github.com/walmartlabs/concord/pull/1143));\n- concord-agent: configurable list of runners\n([#1182](https://github.com/walmartlabs/concord/pull/1182));\n- concord-cli: add self-update command\n([#1198](https://github.com/walmartlabs/concord/pull/1198)).\n\n### Changed\n\n- concord-client2: add Concord-specific media type for\nvalidation errors\n([#1199](https://github.com/walmartlabs/concord/pull/1199));\n- docker-images: use Debian 12\n([#1201](https://github.com/walmartlabs/concord/pull/1201));\n- concord-cli: do not call System.exit in RemoteSecretsProvider\n([#1202](https://github.com/walmartlabs/concord/pull/1202));\n- concord-cli: ignore unknown properties in the CLI config\nfile\n([#1203](https://github.com/walmartlabs/concord/pull/1203));\n- resource-task: Use the shared IOUtils assertInPath method\nfor resource lookups\n([#1204](https://github.com/walmartlabs/concord/pull/1204));\n- project: enable trimHeaderLine in license-maven-plugin\n([#1205](https://github.com/walmartlabs/concord/pull/1205));\n- noderoster: optimize event list query\n([#1206](https://github.com/walmartlabs/concord/pull/1206));\n- concord-common: deprecate IOUtils\n([#1208](https://github.com/walmartlabs/concord/pull/1208)).\n\n\n\n## [2.31.0] - 2025-08-04\n\n### Added\n\n- concord-tasks: add createApiKey action\n([#1194](https://github.com/walmartlabs/concord/pull/1194));\n- concord-server: option to specify API key value\n([#1195](https://github.com/walmartlabs/concord/pull/1195));\n- concord-tasks: add createOrUpdateApiKey action\n([#1196](https://github.com/walmartlabs/concord/pull/1196)).\n\n### Changed\n\n- concord-runtime-model: more flexible interfaces, simplify code\n([#1184](https://github.com/walmartlabs/concord/pull/1184));\n- project: fix typo\n([#1191](https://github.com/walmartlabs/concord/pull/1191));\n- runtime-v2: additional error logging when cloning state fails\n([#1192](https://github.com/walmartlabs/concord/pull/1192));\n- project: update dependencies\n([#1193](https://github.com/walmartlabs/concord/pull/1193)).\n\n\n\n## [2.30.0] - 2025-07-12\n\n### Added\n\n- http-task: support sending int values in multipart\nrequests\n([#1174](https://github.com/walmartlabs/concord/pull/1174));\n- concord-server: config option to disable template script\nprocessing\n([#1176](https://github.com/walmartlabs/concord/pull/1176));\n- common, runtime-v1: add method to assert given path string\nresolves in expected parent\n([#1179](https://github.com/walmartlabs/concord/pull/1179)).\n\n### Changed\n\n- concord-cli: produce an executable binary again\n([#1171](https://github.com/walmartlabs/concord/pull/1171));\n- concord-server: allow get user for concordSystemReader role\n([#1173](https://github.com/walmartlabs/concord/pull/1173));\n- runtime-v2: return copies in ObjectMapperProvider\n([#1175](https://github.com/walmartlabs/concord/pull/1175));\n- concord-server: do not log session token\n([#1177](https://github.com/walmartlabs/concord/pull/1177));\n- concord-server: kill process when resume fails with no\nstate\n([#1178](https://github.com/walmartlabs/concord/pull/1178));\n- runtime-v2: validate form name constraints as documented\n([#1180](https://github.com/walmartlabs/concord/pull/1180));\n- runtime-v1: validate form name constraints as documented\n([#1181](https://github.com/walmartlabs/concord/pull/1181));\n- runtime-v2: remove unused imports, dead code\n([#1185](https://github.com/walmartlabs/concord/pull/1185));\n- concord-console2: sort variables in task call details\n([#1186](https://github.com/walmartlabs/concord/pull/1186));\n- http-task: allow send long multipart with debug enabled\n([#1188](https://github.com/walmartlabs/concord/pull/1188));\n- concord-server: fix tx in process card dao\n([#1189](https://github.com/walmartlabs/concord/pull/1189)).\n\n\n\n## [2.29.0] - 2025-06-13\n\n### Added\n\n- concord-server: refactor WebSocketChannelManager, allow\nmessage sources in plugins\n([#1056](https://github.com/walmartlabs/concord/pull/1056));\n- concord-agent: one-shot mode\n([#1150](https://github.com/walmartlabs/concord/pull/1150));\n- slack-tasks: allow send blocks instead of simple text\n([#1161](https://github.com/walmartlabs/concord/pull/1161));\n- concord-server, concord-console2: add console.cfgFile\nconfig parameter\n([#1166](https://github.com/walmartlabs/concord/pull/1166));\n- runtime-v2: support expressions for output variables in\ncall step\n([#1170](https://github.com/walmartlabs/concord/pull/1170)).\n\n### Changed\n\n- concord-server, concord-console2: use webapp plugin\n([#1154](https://github.com/walmartlabs/concord/pull/1154));\n- runtime-v2: add extra check to testThrowParallelWithPayload\n([#1157](https://github.com/walmartlabs/concord/pull/1157));\n- concord-server-it: reduce agent and server poll delays to\nspeed up tests\n([#1159](https://github.com/walmartlabs/concord/pull/1159));\n- concord-runtime-v1/v2-it: reduce agent and server poll\ndelays to speed up tests\n([#1163](https://github.com/walmartlabs/concord/pull/1163));\n- runtime-v2: improve handling of @SensitiveData on bridge\nmethods\n([#1164](https://github.com/walmartlabs/concord/pull/1164));\n- webapp: fix prefix matching\n([#1167](https://github.com/walmartlabs/concord/pull/1167));\n- oidc: fix role mapping validation\n([#1168](https://github.com/walmartlabs/concord/pull/1168));\n- concord-server-db: use our own implementation of session\nlocks\n([#1169](https://github.com/walmartlabs/concord/pull/1169)).\n\n\n\n## [2.28.0] - 2025-06-04\n\n### Added\n\n- concord-server: add more repository-level GitHub events\n([#1149](https://github.com/walmartlabs/concord/pull/1149));\n- runtime-v2: allow marking nested values as sensitive data\n([#1151](https://github.com/walmartlabs/concord/pull/1151));\n- misc-tasks, runtime-v2: support masking when base64\nencoding/decoding sensitive data\n([#1155](https://github.com/walmartlabs/concord/pull/1155)).\n\n### Changed\n\n- runtime-v1, runtime-v2: refactor loader structure\n([#1147](https://github.com/walmartlabs/concord/pull/1147));\n- examples: update description\n([#1153](https://github.com/walmartlabs/concord/pull/1153));\n- concord-server-it: make dependency resolver timeout same\nas in runtime-v2 ITs\n([#1156](https://github.com/walmartlabs/concord/pull/1156));\n- docker-images: update JDK versions\n([#1158](https://github.com/walmartlabs/concord/pull/1158));\n- project: use central-publishing-maven-plugin\n([#1160](https://github.com/walmartlabs/concord/pull/1160)).\n\n\n\n## [2.27.0] - 2025-05-22\n\n### Added\n\n- concord-server: default Git credentials hostname whitelist\n([#1139](https://github.com/walmartlabs/concord/pull/1139));\n- concord-server: log listening address, starting tasks\n([#1141](https://github.com/walmartlabs/concord/pull/1141));\n\n### Changed\n\n- oidc: validate team and role mappings\n([#1144](https://github.com/walmartlabs/concord/pull/1144));\n- concord-server-db: use session locks in Liquibase\n([#1145](https://github.com/walmartlabs/concord/pull/1145));\n- concord-server: handle SIGTERM\n([#1146](https://github.com/walmartlabs/concord/pull/1146)).\n\n\n\n## [2.26.0] - 2025-05-05\n\n### Added\n\n- resource-task: add fromYamlString method interface\n([#1125](https://github.com/walmartlabs/concord/pull/1125));\n- concord-cli: use jansi, add colors to run output\n([#1138](https://github.com/walmartlabs/concord/pull/1138)).\n\n### Changed\n\n- concord-agent, concord-agent-operator: better shutdown hook,\nhandle SIGTERM\n([#1130](https://github.com/walmartlabs/concord/pull/1130));\n- project: enable full annotation processing\n([#1131](https://github.com/walmartlabs/concord/pull/1131));\n- project: fix concord-runner-v2 test discovery\n([#1133](https://github.com/walmartlabs/concord/pull/1133));\n- project: update testcontainers-concord version\n([#1135](https://github.com/walmartlabs/concord/pull/1135));\n- concord-server: use ProjectLoader interface, bind explicitly\n([#1136](https://github.com/walmartlabs/concord/pull/1136));\n- concord-server: split ProcessSecurityContext\n([#1137](https://github.com/walmartlabs/concord/pull/1137)).\n\n\n\n## [2.25.1] - 2025-04-26\n\n### Changed\n\n- runtime-v2: set lowercase loop mode value in schema\n([#1122](https://github.com/walmartlabs/concord/pull/1122));\n- http-tasks: refactor tests, support for parallel tests\n([#1123](https://github.com/walmartlabs/concord/pull/1123));\n- project: update bouncycastle dependency versions\n([#1127](https://github.com/walmartlabs/concord/pull/1127));\n- concord-server: handle invalid regex in dispatcher\nrequirements\n([#1128](https://github.com/walmartlabs/concord/pull/1128));\n- project: downgrade nimbus-jose-jwt\n([#1129](https://github.com/walmartlabs/concord/pull/1129)).\n\n\n\n## [2.25.0] - 2025-03-23\n\n### Changed\n\n- concord-agent: replace simple immutables with records\n([#1091](https://github.com/walmartlabs/concord/pull/1091));\n- concord-server, concord-server-db: generate UUIDs on\nthe server, use UUID v7\n([#1106](https://github.com/walmartlabs/concord/pull/1106));\n- concord-server: upgrade Shiro to 2.x\n([#1107](https://github.com/walmartlabs/concord/pull/1107));\n- concord-server, concord-client2: uncomment valid code\n([#1108](https://github.com/walmartlabs/concord/pull/1108));\n- concord-server: remove more @Named usage\n([#1110](https://github.com/walmartlabs/concord/pull/1110));\n- concord-server: remove deprecated SecretEntry\n([#1111](https://github.com/walmartlabs/concord/pull/1111));\n- concord-server: remove deprecated /logs/* endpoint\n([#1113](https://github.com/walmartlabs/concord/pull/1113));\n- project: update dependencies\n([#1114](https://github.com/walmartlabs/concord/pull/1114));\n- project: update README\n([#1115](https://github.com/walmartlabs/concord/pull/1115));\n- docker-images: update JDK versions\n([#1116](https://github.com/walmartlabs/concord/pull/1116));\n- project: update dependencies\n([#1118](https://github.com/walmartlabs/concord/pull/1118));\n- project: remove unused dependencies\n([#1119](https://github.com/walmartlabs/concord/pull/1119)).\n\n### Breaking\n\n- project: remove client1\n([#1013](https://github.com/walmartlabs/concord/pull/1013));\n- concord-server: remove deprecated features\n([#1112](https://github.com/walmartlabs/concord/pull/1112)).\n\n\n\n## [2.24.0] - 2025-03-15\n\n### Added\n\n- plugins: new env and collections tasks\n([#1092](https://github.com/walmartlabs/concord/pull/1092));\n- slack-tasks: modern bot token support\n([#1093](https://github.com/walmartlabs/concord/pull/1093));\n- plugins: allow suspend/resume process from concord task\n([#1094](https://github.com/walmartlabs/concord/pull/1094));\n- concord-server: default message for unexpected errors\n([#1096](https://github.com/walmartlabs/concord/pull/1096));\n- concord-server, concord-console2: add support for\nprocessExecMode\n([#1102](https://github.com/walmartlabs/concord/pull/1102)).\n\n### Changed\n\n- runtime-v2: restrict regex github trigger attributes to\nstring value\n([#1097](https://github.com/walmartlabs/concord/pull/1097));\n- mock-tasks: correctly handle mocks in `set variables` step\n([#1098](https://github.com/walmartlabs/concord/pull/1098));\n- concord-console2: handle non-existent root parent\n([#1100](https://github.com/walmartlabs/concord/pull/1100));\n- concord-server: disable deprecated process start endpoints\n([#1101](https://github.com/walmartlabs/concord/pull/1101));\n- concord-plugin: set dry-run mode for subprocesses by\ndefault if the parent process was started in dry-run mode\n([#1104](https://github.com/walmartlabs/concord/pull/1104));\n- project: update Maven plugin versions\n([#1105](https://github.com/walmartlabs/concord/pull/1105)).\n\n\n\n## [2.23.0] - 2025-02-26\n\n### Added\n\n- concord-server: allow handle configuration in custom\nenqueue processor\n([#1088](https://github.com/walmartlabs/concord/pull/1088));\n- concord-agent: allow skipping repository state\n([#1089](https://github.com/walmartlabs/concord/pull/1089)).\n\n### Changed\n\n- runtime-v2: hide stack trace and log location for\nParallelExecutionException\n([#1081](https://github.com/walmartlabs/concord/pull/1081));\n- concord-server: close MultipartInput explicitly\n([#1084](https://github.com/walmartlabs/concord/pull/1084);\n- console2: configure children process columns\n([#1085](https://github.com/walmartlabs/concord/pull/1085));\n- runtimev2: error message for process arguments evaluation\n([#1086](https://github.com/walmartlabs/concord/pull/1086)); \n- concord-tasks: send archive as a file instead of byte\narray\n([#1087](https://github.com/walmartlabs/concord/pull/1087));\n- concord-server: handle request without cookies\n([#1090](https://github.com/walmartlabs/concord/pull/1090)).\n\n\n\n## [2.22.0] - 2025-02-14\n\n### Added\n\n- concord-server: order ui_process_card by a new order_id field\n([#1075](https://github.com/walmartlabs/concord/pull/1075));\n- misc-tasks: add base64 task, shortcut for current ISO timestamp\n([#1078](https://github.com/walmartlabs/concord/pull/1078));\n- http-tasks: use default variables\n([#1080](https://github.com/walmartlabs/concord/pull/1080)).\n\n### Changed\n\n- concord-server: simplify UserInfo, UserInfoProcessor\n([#1061](https://github.com/walmartlabs/concord/pull/1061));\n- project: update to latest Jetty 12.x\n([#1068](https://github.com/walmartlabs/concord/pull/1068));\n- concord-agent, runtime-v2: miscellaneous improvements\n([#1070](https://github.com/walmartlabs/concord/pull/1070));\n- concord-agent-operator: use informers API\n([#1072](https://github.com/walmartlabs/concord/pull/1072));\n- concord-agent-operator: attempt to improve error logging\n([#1073](https://github.com/walmartlabs/concord/pull/1073));\n- runtime-v2: do not log stack trace for EL MethodNotFound\nexception and unify the error messages\n([#1076](https://github.com/walmartlabs/concord/pull/1076));\n- project: do not log into Docker in check runs started in\nforks\n([#1079](https://github.com/walmartlabs/concord/pull/1079));\n- concord-server: proper close queue client\n([#1082](http://github.com/walmartlabs/concord/pull/1082)).\n\n\n\n## [2.21.0] - 2025-01-15\n\n### Added\n\n- concord-repository: support for `mvn://` scheme\n([#1063](https://github.com/walmartlabs/concord/pull/1063)).\n\n### Changed\n\n- concord-server: fix header validation in\nSessionTokenAuthenticationHandler\n([#1044](https://github.com/walmartlabs/concord/pull/1044));\n- concord-console2: add support for redirectTo to the login page\n([#1046](https://github.com/walmartlabs/concord/pull/1046));\n- runtime-v2: allow throw payload with exception\n([#1049](https://github.com/walmartlabs/concord/pull/1049));\n- runtime-v2: use SensitiveDataHolder for task parameter masking\n([#1050](https://github.com/walmartlabs/concord/pull/1050));\n- runtime-v2: add exceptions to ParallelExecutionException\n([#1051](https://github.com/walmartlabs/concord/pull/1051));\n- runtime-v2: mask workDir value in logs by default\n([#1052](https://github.com/walmartlabs/concord/pull/1052));\n- runtime-v2: save out variables for failed process\n([#1053](https://github.com/walmartlabs/concord/pull/1053));\n- concord-server: allow tokens without users, remove user\nfrom default agent token\n([#1054](https://github.com/walmartlabs/concord/pull/1054));\n- concord-targetplatform: update dependencies\n([#1057](https://github.com/walmartlabs/concord/pull/1057));\n- concord-server: replace synchronized with locks\n([#1060](https://github.com/walmartlabs/concord/pull/1060));\n- runtime-v2: fix for exception stack trace logging\n([#1062](https://github.com/walmartlabs/concord/pull/1062));\n- concord-agent-operator: fix ConfigMap creation, update\nexample CRDs\n([#1065](https://github.com/walmartlabs/concord/pull/1065));\n- concord-server: fix PolicyCache reloading loop\n([#1066](https://github.com/walmartlabs/concord/pull/1066));\n- concord-agent-operator: simple exit on watcher error\n([#1067](https://github.com/walmartlabs/concord/pull/1067)).\n\n\n\n## [2.20.0] - 2024-11-20\n\n### Added\n\n- mock-tasks: allow flows to be executed instead of tasks\n([#1042](https://github.com/walmartlabs/concord/pull/1042)).\n\n### Changed\n\n- plugins: fix dependency scopes\n([#1017](https://github.com/walmartlabs/concord/pull/1017));\n- concord-cli: fix dependencies when generating effective yaml\n([#1018](https://github.com/walmartlabs/concord/pull/1018));\n- runtime-v2: remove extraneous error logging\n([#1021](https://github.com/walmartlabs/concord/pull/1021));\n- runtime-v2: script meta options should not override\nthe script step name\n([#1022](https://github.com/walmartlabs/concord/pull/1022));\n- runtime-v2: fix error messages\n([#1023](https://github.com/walmartlabs/concord/pull/1023));\n- mock-tasks: allow matching mocks by meta attributes or\nstep name\n([#1024](https://github.com/walmartlabs/concord/pull/1024));\n- concord-console2: fix zero content-length parsing in makeError\n([#1025](https://github.com/walmartlabs/concord/pull/1025));\n- concord-server: split ConcordAuthenticationHandler into\nseparate handlers\n([#1026](https://github.com/walmartlabs/concord/pull/1026));\n- mocks-task: record task events for mocked tasks\n([#1027](https://github.com/walmartlabs/concord/pull/1027));\n- concord-server: fix session token handling\n([#1032](https://github.com/walmartlabs/concord/pull/1032));\n- concord-server: cleanup control chars from JSONB \n([#1034](https://github.com/walmartlabs/concord/pull/1034));\n- concord-server: add authorization check to User lookup\nendpoint in API\n([#1035](https://github.com/walmartlabs/concord/pull/1035));\n- it: tighten up polling intervals\n([#1036](https://github.com/walmartlabs/concord/pull/1036));\n- project: use concord-maven-plugin\n([#1038](https://github.com/walmartlabs/concord/pull/1038));\n- concord-server: fix process card createOrUpdate\n([#1039](https://github.com/walmartlabs/concord/pull/1039));\n- runtime-v2: allow mark sensitive data for task.execute result\n([#1040](https://github.com/walmartlabs/concord/pull/1040)).\n\n\n\n## [2.19.0] - 2024-11-05\n\n## Added\n\n- runtime-v2: introduce extraDependencies\n([#1014](https://github.com/walmartlabs/concord/pull/1014));\n- runtime-v2: initial support for dry-run mode\n([#1007](https://github.com/walmartlabs/concord/pull/1007));\n- concord-console2: add a full-screen page for process cards\n([#1009](https://github.com/walmartlabs/concord/pull/1009));\n- mock-tasks: support for method mocks in tasks\n([#1010](https://github.com/walmartlabs/concord/pull/1010));\n- mock-tasks: support for task call verify\n([#1012](https://github.com/walmartlabs/concord/pull/1012)).\n\n## Changed\n\n- concord-server: remove resteasy-guice dependency\n([#997](https://github.com/walmartlabs/concord/pull/997));\n- project: update HikariCP version\n([#1000](https://github.com/walmartlabs/concord/pull/1000));\n- runtime-v2: flush events on process error \n([#1001](https://github.com/walmartlabs/concord/pull/1001));\n- concord-server: fix AuthenticationHandler result handling\n([#1003](https://github.com/walmartlabs/concord/pull/1003));\n- server: fix ConcordKey validation regex to 128 character limit\n([#1004](https://github.com/walmartlabs/concord/pull/1004));\n- project: update docker-login action in build flow\n([#1005](https://github.com/walmartlabs/concord/pull/1005));\n- runtime-v2: log task method name in event\n([#1006](https://github.com/walmartlabs/concord/pull/1006));\n- runtime-v2: move tests to separate module\n([#1008](https://github.com/walmartlabs/concord/pull/1008));\n- runtime-v2: use shorter delay while polling status of\nthreads\n([#1011](https://github.com/walmartlabs/concord/pull/1011)).\n\n### Breaking\n\n- runtime-v2: store flow location in process definition.\nNote, this changes the type of\n`context.execution().processDefinition().flows()` object\navailable in runtime-v2 processes.\n([#995](https://github.com/walmartlabs/concord/pull/995));\n- runtime-v2: remove ProjectLoadListener interface\n([#1015](https://github.com/walmartlabs/concord/pull/1015)).\n\n\n\n## [2.18.0] - 2024-10-13\n\n### Added\n\n- concord-console2: status filter for log segments\n([#980](https://github.com/walmartlabs/concord/pull/980));\n- runtime-v2: interface for steps that generate element\nevents\n([#987](https://github.com/walmartlabs/concord/pull/987)).\n\n### Changed\n\n- project: respect PR labels\n([#961](https://github.com/walmartlabs/concord/pull/961));\n- it: re-enable OldAgentIT\n([#962](https://github.com/walmartlabs/concord/pull/962));\n- runtime-v2: add github exclusive trigger to schema\n([#977](https://github.com/walmartlabs/concord/pull/977));\n- concord-server: bind EventEnrichers explicitly\n([#978](https://github.com/walmartlabs/concord/pull/978));\n- oidc, concord-console2: improve error handling\n([#979](https://github.com/walmartlabs/concord/pull/979));\n- runtime-v2: fix the issue when old agents can't parse\nprocess configuration with new attributes\n([#981](https://github.com/walmartlabs/concord/pull/981)).\n- agent-operator: save cookies received from API\n([#984](https://github.com/walmartlabs/concord/pull/984));\n- project: fixes for build-time warnings\n([#985](https://github.com/walmartlabs/concord/pull/985));\n- concord-server: explicitly bind more classes\n([#986](https://github.com/walmartlabs/concord/pull/986));\n- concord-server: more fixes for non auto-wiring\nenvironments\n([#988](https://github.com/walmartlabs/concord/pull/988));\n- project: update frontend-maven-plugin\n([#992](https://github.com/walmartlabs/concord/pull/992));\n- runtime-v2: fix manual trigger exclusive schema\n([#993](https://github.com/walmartlabs/concord/pull/993));\n- concord-server: fix updateWaitConditions when wait\ncondition without processes\n([#994](https://github.com/walmartlabs/concord/pull/994)).\n\n### Breaking\n\n- project: fork ollie-config and make it a submodule.\nServer plugins must be updated to use the new package\n`com.walmartlabs.concord.config` instead of\n`com.walmartlabs.ollie.config`\n([#989](https://github.com/walmartlabs/concord/pull/989)). \n\n\n\n## [2.17.0] - 2024-09-18\n\n### Added\n\n- runtime-v2: option for event batching for runner events\n([#949](https://github.com/walmartlabs/concord/pull/949));\n- runtime-v1: option for event batching for runner events\n([#950](https://github.com/walmartlabs/concord/pull/950));\n- console2, server: simple user info page\n([#952](https://github.com/walmartlabs/concord/pull/952)).\n\n### Changed\n\n- project: update Maven wrapper\n([#967](https://github.com/walmartlabs/concord/pull/967));\n- oidc: redirect back to auth in failed callbacks\n([#969](https://github.com/walmartlabs/concord/pull/969));\n- project: miscellaneous fixes for build-time warnings,\nadd missing @deprecated annotations, remove redundant\ndependencies\n([#970](https://github.com/walmartlabs/concord/pull/970));\n- agent-operator: create agent pod client only for\nRunning pods\n([#973](https://github.com/walmartlabs/concord/pull/973));\n- concord-server: remove GithubTriggerProcessor interface\n([#974](https://github.com/walmartlabs/concord/pull/974));\n- docker: configure safe.directory for git 2.35+\n([#976](https://github.com/walmartlabs/concord/pull/976)).\n\n\n\n### Changed\n\n## [2.16.0] - 2024-09-05\n\n### Added\n\n- runtime-v2: option to update meta only on termination or\nsuspend\n([#948](https://github.com/walmartlabs/concord/pull/948));\n- policy-engine: allow rewriting with multiple values in\n`dependencyRewrite` policies\n([#952](https://github.com/walmartlabs/concord/pull/952));\n- concord-server: allow non-standard runtimes\n([#954](https://github.com/walmartlabs/concord/pull/954));\n- oidc: support \"from\" when logging out\n([#958](https://github.com/walmartlabs/concord/pull/958)).\n\n### Changed\n\n- runtime-v1: update bpm library to fix saving variables\nbefore suspend\n([#955](https://github.com/walmartlabs/concord/pull/955));\n- concord-server: fix DB change set 1580200-a when\n`superuserAvailable` is set to `false`\n([#957](https://github.com/walmartlabs/concord/pull/957));\n- concord-server: skip pull\\_request process start when\nuseEventCommitId is enabled and event is from a different repo\n([#959](https://github.com/walmartlabs/concord/pull/959));\n- docker-images: update ansible galaxy community.docker version\n([#960](https://github.com/walmartlabs/concord/pull/960));\n- cli: fix duplicate step logs\n([#963](https://github.com/walmartlabs/concord/pull/963)).\n\n\n\n## [2.15.0] - 2024-08-07\n\n### Added\n\n- agent: configure host/ip for maintenance-mode endpoint\n([#945](https://github.com/walmartlabs/concord/pull/945));\n- concord-task: new method to wait and check that processes\nhave finished\n([#943](https://github.com/walmartlabs/concord/pull/943));\n- agent-operator: use maintenance mode before terminating\nagent\n([#946](https://github.com/walmartlabs/concord/pull/946));\n\n### Changed\n\n- pfed-sso: fix to not return null for not permanently\ndisabled users\n([#947](https://github.com/walmartlabs/concord/pull/947)).\n - agent-operator: consider pods already marked for deletion\nduring downscaling\n([#951](https://github.com/walmartlabs/concord/pull/951)).\n\n\n\n## [2.14.0] - 2024-07-13\n\n### Added\n\n- concord-server: calculate total process RUNNING time\n([#933](https://github.com/walmartlabs/concord/pull/933));\n- concord-server: expose websocket channels\n([#935](https://github.com/walmartlabs/concord/pull/935));\n- resource-tasks: add versions of writeAs* methods that\naccept destination\n([#937](https://github.com/walmartlabs/concord/pull/937));\n- runtime-v1: option to update meta only on termination\nor suspend\n([#938](https://github.com/walmartlabs/concord/pull/938));\n- project: add JDK 21 profiles\n([#941](https://github.com/walmartlabs/concord/pull/941)).\n\n### Changed\n\n- project: update Groovy to 2.5.23\n([#940](https://github.com/walmartlabs/concord/pull/940));\n- dependency-manager: resolve only unique dependencies \n([#936](https://github.com/walmartlabs/concord/pull/936)); \n- concord-server: move com.walmartlabs.concord.server.ansible.*\ninto ansible plugin\n([#502](https://github.com/walmartlabs/concord/pull/502));\n- concord-server: migrate to PROCESS_META and\nPROCESS_TRIGGER_INFO tables\n([#669](https://github.com/walmartlabs/concord/pull/669));\n- runtime-v2: use draft-07 of JSON Schema for better tool\ncompatibility\n([#939](https://github.com/walmartlabs/concord/pull/939));\n- project: update dependency versions in the parent pom\n([#942](https://github.com/walmartlabs/concord/pull/942)).\n\n\n\n## [2.13.0] - 2024-06-19\n\n### Added\n\n- runtime-v2: \"suspend\" status support for log segments\n([#927](https://github.com/walmartlabs/concord/pull/927));\n- mocks: examples, support for storing the input, add\n`throwError`\n([#928](https://github.com/walmartlabs/concord/pull/928));\n- runtime-v2: add more events to execution listeners\n([#931](https://github.com/walmartlabs/concord/pull/931)).\n\n### Changed\n\n- runtime-v2: rename MultiException and limit stack trace depth\n([#930](https://github.com/walmartlabs/concord/pull/930)).\n\n\n\n## [2.12.0] - 2024-06-12\n\n### Added\n\n- dependency-manager: allow `LATEST` to pull latest version\nfrom remote repositories\n([#913](https://github.com/walmartlabs/concord/pull/913));\n- runtime-v2: additional log segment statuses for error and\nsuspended states\n([#918](https://github.com/walmartlabs/concord/pull/918));\n- runtime-v2: initial support for thread local variables\n([#920](https://github.com/walmartlabs/concord/pull/920));\n-  project: add mock task to parent pom\n([#925](https://github.com/walmartlabs/concord/pull/925));\n- runtime-v2: initial support for finalizers\n([#926](https://github.com/walmartlabs/concord/pull/926)).\n\n### Changed\n\n- docker: fix image build on aarch64 hosts\n([#717](https://github.com/walmartlabs/concord/pull/717));\n- concord-console2: upgrade node version\n([#890](https://github.com/walmartlabs/concord/pull/890));\n- runtime-v2: fix log segment assigment during parallel\nexecution\n([#905](https://github.com/walmartlabs/concord/pull/905));\n- project: agent-operator module re-organization\n([#906](https://github.com/walmartlabs/concord/pull/906));\n- concord-server: fix process metadata values after resume\n([#907](https://github.com/walmartlabs/concord/pull/907));\n- concord-server: process wait conditions in batch mode\n([#910](https://github.com/walmartlabs/concord/pull/910));\n- project: update Liquibase to 4.8.0\n([#912](https://github.com/walmartlabs/concord/pull/912));\n- project: update resteasy to latest 4.x\n([#914](https://github.com/walmartlabs/concord/pull/914));\n- server: exception mapper for InvalidProcessStateException\n([#916](https://github.com/walmartlabs/concord/pull/916));\n- runtime-v2: allow double (floating point) values in YAML\n([#917](https://github.com/walmartlabs/concord/pull/917));\n- project: build both x86 and aarch64 versions\n([#921](https://github.com/walmartlabs/concord/pull/921));\n- concord-server: fix UserDao list method\n([#924](https://github.com/walmartlabs/concord/pull/924)).\n\n\n\n## [2.11.1] - 2024-05-12\n\n### Changed\n\n- concord-server: reduce Shiro usage\n([#889](https://github.com/walmartlabs/concord/pull/889));\n- runtime-v2: fix sensitive data masking in maps\n([#897](https://github.com/walmartlabs/concord/pull/893));\n- concord-server, tasks: disable repos on deleted ref, only\nrefresh repos matching event branch\n([#894](https://github.com/walmartlabs/concord/pull/894));\n- concord-server: fix Jetty metrics\n([#899](https://github.com/walmartlabs/concord/pull/899));\n- concord-server: add some missing GHA event types\n(repository, status, workflow_job, workflow_run)\n([#900](https://github.com/walmartlabs/concord/pull/900));\n- dependency-manager: make it a singleton\n([#901](https://github.com/walmartlabs/concord/pull/901));\n- concord-server: fix initialization of wait conditions\nafter process restart\n([#903](https://github.com/walmartlabs/concord/pull/903));\n- runtime-v2: fix itemIndex in parallel loops\n([#904](https://github.com/walmartlabs/concord/pull/904)).\n\n\n\n## [2.11.0] - 2024-04-30 \n\n### Added\n\n- agent-operator: scaling strategies and configurable\nrequirements\n([#893](https://github.com/walmartlabs/concord/pull/893)).\n\n### Changed\n\n- ansible-tasks: be more helpful when commands are missing.\nCheck if `ansible-playbook` or `virtualenv` exist before\nrunning.\n([#887](https://github.com/walmartlabs/concord/pull/887));\n- project: upgrade dependencies - Jackson to 2.17.0, Jetty\nto 12.0.7, Wiremock to 3.5.2 and others.\n([#861](https://github.com/walmartlabs/concord/pull/861));\n- concord-server: allow plugins to supply their own top-level\nAPI endpoints\n([#891](https://github.com/walmartlabs/concord/pull/891));\n- concord-server: minor improvements to the remember me\ncookie logic \n([#892](https://github.com/walmartlabs/concord/pull/892));\n- concord-server: tone down websocket errors\n([#895](https://github.com/walmartlabs/concord/pull/895));\n- concord-server: do not invalidate sessions in\nonFailedLogin\n([#896](https://github.com/walmartlabs/concord/pull/896)).\n\n\n\n## [2.10.1] - 2024-04-04 \n\n### Changed\n\n- concord-server: fix json serialization of UserActivityResponse\n([#885](https://github.com/walmartlabs/concord/pull/885)).\n\n\n\n## [2.10.0] - 2024-04-01\n\n### Added\n\n- plugins: add new mock-tasks plugin\n([#754](https://github.com/walmartlabs/concord/pull/754));\n- runtime-v2: logYaml step\n([#816](https://github.com/walmartlabs/concord/pull/816));\n- concord-server, concord-console2: add \"process cards\"\n([#808](https://github.com/walmartlabs/concord/pull/808));\n- concord-agent: kill runner child PIDs\n([#880](https://github.com/walmartlabs/concord/pull/880)).\n\n### Changed\n\n- server: fix trigger id calculation for complex args:\nheterogeneous lists, list of maps\n([#882](https://github.com/walmartlabs/concord/pull/882));\n- concord-server: skip validation of disabled repos during\nproject creation\n([#883](https://github.com/walmartlabs/concord/pull/883));\n- concord-server: process wait conditions synchronously\n([#884](https://github.com/walmartlabs/concord/pull/884)).\n\n\n\n## [2.9.0] - 2024-02-28\n\n### Added\n\n- concord-server: option to permanently disable a user\n([#875](https://github.com/walmartlabs/concord/pull/875));\n- tasks: asserts\n([#876](https://github.com/walmartlabs/concord/pull/876));\n- concord-agent, dependency-manager: support for Maven offline\nmode ([#869](https://github.com/walmartlabs/concord/pull/869));\n- concord-server: skip repository refresh when repo is disabled\n([#872](https://github.com/walmartlabs/concord/pull/872));\n- runtime-v2: threadId to task details\n([#874](https://github.com/walmartlabs/concord/pull/874));\n- concord-console2: add more details to trigger list\n([#878](https://github.com/walmartlabs/concord/pull/878));\n\n### Changed\n\n- concord-console: adjust polling frequency based on client\nactivity\n([#634](https://github.com/walmartlabs/concord/pull/634));\n- runtime-v1: fix for resume from same step (bpm version up)\n([#879](https://github.com/walmartlabs/concord/pull/879));\n- cli: api client provider for cli (just to load tasks)\n([#877](https://github.com/walmartlabs/concord/pull/877));\n- ansible: add module_defaults callback, remove deprecated gather_subset in config\n([#873](https://github.com/walmartlabs/concord/pull/873));\n- runtime-v2: ignore empty string as sensitive data\n([#871](https://github.com/walmartlabs/concord/pull/871));\n- project: fix maven compiler source version in parent pom\n([#870](https://github.com/walmartlabs/concord/pull/870)).\n\n\n\n## [2.8.0] - 2024-01-15\n\n### Added\n\n- concord-console2: kv capacity\n([#795](https://github.com/walmartlabs/concord/pull/795));\n- concord-server, concord-console2: ability to restart runtime-v2\nprocesses\n([#850](https://github.com/walmartlabs/concord/pull/850)).\n\n### Changed\n\n- concord-server: invalidate session on failed login\n([#859](https://github.com/walmartlabs/concord/pull/859));\n- runtime-v2: error location for loop, call, parallel, retry commands (v2)\n([#865](https://github.com/walmartlabs/concord/pull/865));\n- runtime-v2: fix incorrect variable merging for set variables step\n([#862](https://github.com/walmartlabs/concord/pull/862)).\n\n\n\n## [2.7.0] - 2024-01-08\n\n### Added\n\n- concord-cli: Add option for default task variables\n([#848](https://github.com/walmartlabs/concord/pull/848)).\n\n### Changed\n\n- runtime-v2: resume event to json serialization fix\n([#860](https://github.com/walmartlabs/concord/pull/860));\n- project: drop siesta-server dependency\n([#826](https://github.com/walmartlabs/concord/pull/826));\n- resource-task: writeYaml: do not split YAML into multiple lines\n([#854](https://github.com/walmartlabs/concord/pull/854));\n- concord-server: logout any session on login failure\n([#858](https://github.com/walmartlabs/concord/pull/858)).\n\n### Breaking\n\n- project: drop siesta-api dependency\n([#857](https://github.com/walmartlabs/concord/pull/857)).\n\n\n\n## [2.6.0] - 2023-12-28\n\n### Added\n\n- concord-server: expose fetch with version\n([#853](https://github.com/walmartlabs/concord/pull/853));\n- server: allow regexp in meta filters\n([#852](https://github.com/walmartlabs/concord/pull/852)).\n\n### Changed\n\n- project: switch to concord-client2\n([#821](https://github.com/walmartlabs/concord/pull/821));\n- concord-server: remove more @Named\n([#839](https://github.com/walmartlabs/concord/pull/839));\n- client2: allow serialize collections\n([#846](https://github.com/walmartlabs/concord/pull/846));\n- runtime-v2: skip annotations for varargs\n([#845](https://github.com/walmartlabs/concord/pull/845));\n- concord-repository: fetch with quiet option\n([#851](https://github.com/walmartlabs/concord/pull/851)).\n\n\n\n## [2.5.0] - 2023-12-10\n\n### Added\n\n- concord-server: support @Priority annotation when binding\nJetty components\n([#841](https://github.com/walmartlabs/concord/pull/841));\n\n### Changed\n\n- runtime-v2: allow \"true|false\" string in if expression\n([#844](https://github.com/walmartlabs/concord/pull/844));\n- docker-images: Upgrade default Ansible installation to 2.14\n([#843](https://github.com/walmartlabs/concord/pull/843));\n- ansible-plugin: callback compatibility for Ansible 2.14\n([#842](https://github.com/walmartlabs/concord/pull/842));\n- concord-server: resume process now returns BAD_REQUEST\nif no event found\n([#838](https://github.com/walmartlabs/concord/pull/838)).\n\n### Breaking\n\n- docker-images: drop CentOS-based images, use Debian by default\n([#843](https://github.com/walmartlabs/concord/pull/843).\n\n\n\n## [2.4.0] - 2023-11-26\n\n### Added\n\n- concord-server: add `EXTRA_CLASSPATH` to start script \n([#836](https://github.com/walmartlabs/concord/pull/836));\n\n### Changed\n\n- concord-agent-operator: use JDK 17 base image\n([#836](https://github.com/walmartlabs/concord/pull/836));\n- concord-common: shared ObjectMapperProvider\n([#836](https://github.com/walmartlabs/concord/pull/836)).\n\n\n\n## [2.3.0] - 2023-11-21\n\n### Added\n\n- testing-concord-server: add getter for the server instance\n([#832](https://github.com/walmartlabs/concord/pull/832));\n- testing-concord-server: add agent wrapper, simple test\n([835](https://github.com/walmartlabs/concord/pull/835)).\n\n### Changed\n\n- project: attach source jars only on release\n([#832](https://github.com/walmartlabs/concord/pull/832));\n- concord-server: auto-wire modules in concord-server/dist\ninstead of impl\n([#834](https://github.com/walmartlabs/concord/pull/834)).\n\n\n\n## [2.2.0] - 2023-11-13\n\n### Added\n\n- pfed-sso: enable bearer token authentication\n([#811](https://github.com/walmartlabs/concord/pull/811)).\n\n### Changed\n\n- runtime-v2: fix exit from parallel loop #830\n([#830](https://github.com/walmartlabs/concord/pull/830));\n- console2: calculate process duration from process last running timestamp\n([#794](https://github.com/walmartlabs/concord/pull/794));\n- console2: do not drop secrets form values on error/password check fail\n([#798](https://github.com/walmartlabs/concord/pull/798));\n- project: attach javadoc jars only on release\n([#823](https://github.com/walmartlabs/concord/pull/823));\n- project: upgrade to source level 17\n([#824](https://github.com/walmartlabs/concord/pull/824));\n- project: remove more @Named usage\n([#828](https://github.com/walmartlabs/concord/pull/828)).\n\n## [2.1.0] - 2023-10-10\n\n### Added\n\n- new concord-client-v2\n([#810](https://github.com/walmartlabs/concord/pull/810));\n- runtime-v2: hasFlow function\n([#813](https://github.com/walmartlabs/concord/pull/813));\n- runtime-v2: uuid function\n([#812](https://github.com/walmartlabs/concord/pull/812));\n- runtime-v2: allow listen to project load events at runtime\n([#785](https://github.com/walmartlabs/concord/pull/785));\n- console2: allow changing JSON store org \n([#790](https://github.com/walmartlabs/concord/pull/790)).\n\n### Changed\n- runtime-v2: automatically convert non serializable map.entry to serializable in exp\n([#815](https://github.com/walmartlabs/concord/pull/815)); \n- server: return 404 when repository is not found\n([#806](https://github.com/walmartlabs/concord/pull/806));\n- runtime-v2: fix global vars update after resume\n([#809](https://github.com/walmartlabs/concord/pull/809));\n- console2: handle procesess with commitId, but without repoUrl\n([#807](https://github.com/walmartlabs/concord/pull/807));\n- runtime-v2: fix initialize of array expression\n([#800](https://github.com/walmartlabs/concord/pull/800));\n- server: only admins can access policies\n([#792](https://github.com/walmartlabs/concord/pull/792));\n- cli: active profiles fix\n([#789](https://github.com/walmartlabs/concord/pull/789)).\n\n\n\n## [2.0.0] - 2023-08-16\n\n# Breaking\n\n- project: drop support for JDK 8 and JDK 11. Make JDK 17\nthe new default version.\n\n## [1.103.0] - 2023-07-16\n\n### Added\n\n- runtime-v2: hide sensitive data in MapELResolver\n([#781](https://github.com/walmartlabs/concord/pull/781));\n- tasks-v2: use debug flag from process configuration\n([#780](https://github.com/walmartlabs/concord/pull/780));\n- concord-console2: show process duration on toolbar\n([#779](https://github.com/walmartlabs/concord/pull/779));\n- concord-console2: allow customizing columns in the main process table\n([#777](https://github.com/walmartlabs/concord/pull/777)); \n- console2: added `last updated at` and `age` to the secret page\n([#775](https://github.com/walmartlabs/concord/pull/775));\n- runtime-v2: hasNonNullVariable function\n([#774](https://github.com/walmartlabs/concord/pull/774));\n- runtime-v2: log call stack on error\n([#761](https://github.com/walmartlabs/concord/pull/761));\n- concord-server: Allow restriction of secrets to multiple projects\n([#688](https://github.com/walmartlabs/concord/pull/688)).\n\n### Changed\n\n- server: fix DB cleanup job\n([#784](https://github.com/walmartlabs/concord/pull/784));\n- runtime-v2: hide stacktrace for UserDefinedException\n([#782](https://github.com/walmartlabs/concord/pull/782));\n- console2: enable save button on repository submit error\n([#771](https://github.com/walmartlabs/concord/pull/771));\n- runtime-v2: handle NPE in expressions\n([#776](https://github.com/walmartlabs/concord/pull/776));\n- concord-ansible-plugin: fix handling of play and task names\nlonger than 1024 chars\n([#772](https://github.com/walmartlabs/concord/pull/772));\n- console2, server: redirect to requested URL after oidc/sso\nauth ([#764](https://github.com/walmartlabs/concord/pull/764)); \n- console2: do not remove project after rename\n([#770](https://github.com/walmartlabs/concord/pull/770));\n- runtime-v2: fix timezone text case in DSL schema\n([#769](https://github.com/walmartlabs/concord/pull/769));\n- docker-images: fix build for Debian 12 based images\n([#767](https://github.com/walmartlabs/concord/pull/767));\n- runtime-v2: serialization fix\n([#758](https://github.com/walmartlabs/concord/pull/758));\n- concord-cli: add no-default-cfg option\n([#763](https://github.com/walmartlabs/concord/pull/763));\n- concord-cli: reduce noise in dependency resolution errors\n([#757](https://github.com/walmartlabs/concord/pull/757));\n- console2: do not remove project after rename;\n([#770](https://github.com/walmartlabs/concord/pull/770)).\n\n\n\n## [1.102.0] - 2023-05-22\n\n### Added\n\n- concord-server: allow any GH event attribute in\n`exclusive.groupBy`\n([#753](https://github.com/walmartlabs/concord/pull/753));\n- concord-server, concord-policy: ability to restrict\n`runtime` type for project processes created after set date\n(e.g. to forbid usage of older runtimes in new projects)\n([#745](https://github.com/walmartlabs/concord/pull/745)).\n\n### Changed\n\n- concord-server, concord-console2: handle empty process\nlists in wait condition\n([#756](https://github.com/walmartlabs/concord/pull/756));\n- concord-task: ignore suspend if no processes provided\n([#755](https://github.com/walmartlabs/concord/pull/755));\n- concord-server: refresh repository triggers synchronously\n([#734](https://github.com/walmartlabs/concord/pull/734));\n- runtime-v2, cli: hide parallel block stacktraces for\nUserDefinedExceptions\n([#751](https://github.com/walmartlabs/concord/pull/751));\n- runtime-v2: hide stacktraces in propertyNotFound exceptions,\nimprove error messages\n([#752](https://github.com/walmartlabs/concord/pull/752));\n- runtime-v2: allow expressions in `parallelism` values\n([#746](https://github.com/walmartlabs/concord/pull/746));\n- server: `created_at` DB field to projects table\n([#744](https://github.com/walmartlabs/concord/pull/744));\n- runtime-v2: allow increment variables in expressions\n([#740](https://github.com/walmartlabs/concord/pull/740)).\n\n\n\n## [1.101.0] - 2023-03-29\n\n### Added\n\n- server: update process policy on process resume\n([#731](https://github.com/walmartlabs/concord/pull/731)).\n\n### Changed\n\n- concord-server: allow auth plugins handle authorization\ntoken ([#737](https://github.com/walmartlabs/concord/pull/737));\n- concord-server: remove more Named usage\n([#729](https://github.com/walmartlabs/concord/pull/729))\n- concord-server: truncate `createdAt` nanoseconds when\ncreating new process keys\n([#736](https://github.com/walmartlabs/concord/pull/736);\n- concord-console: fix rendering of multiple string values in \nforms\n([#735](https://github.com/walmartlabs/concord/pull/735)).\n\n## [1.100.0] - 2023-03-09\n\n### Added\n\n- runtime-v2: mask sensitive data in logs\n([#719](https://github.com/walmartlabs/concord/pull/719));\n- cli: process/project info from variables\n([#727](https://github.com/walmartlabs/concord/pull/727));\n- runtime-v2: support for \"session state\" process attachments\n([#722](https://github.com/walmartlabs/concord/pull/722)).\n\n### Changed\n\n- concord-console: fix v2 log segment spinner after interrupted process\n([#728](https://github.com/walmartlabs/concord/pull/728));\n- cli: log errors from dependency resolver only in verbose mode\n([#723](https://github.com/walmartlabs/concord/pull/723));\n- cli: log flow step name (if provided)\n([#724](https://github.com/walmartlabs/concord/pull/724));\n- agent: log artifact resolve errors only in debug mode\n([#725](https://github.com/walmartlabs/concord/pull/725));\n- runtime-v2: predictable order of process arguments \n([#721](https://github.com/walmartlabs/concord/pull/721));\n- concord-server: remove more @Named usage\n([#650](https://github.com/walmartlabs/concord/pull/650)).\n\n\n\n## [1.99.0] - 2023-02-24\n\n### Added\n\n- concord-server: implement removal of disabled user\naccounts. Old accounts can now be automatically removed\n(the feature is disabled by default)\n([#716](https://github.com/walmartlabs/concord/pull/716));\n- runtime-v2: allow custom JS lang levels in scripts\n([#709](https://github.com/walmartlabs/concord/pull/709));\n- runtime-v2: function for throw exception\n([#712](https://github.com/walmartlabs/concord/pull/712));\n- concord-server: added 'last updated at' field for kv\nrecords\n([#701](https://github.com/walmartlabs/concord/pull/701));\n- policy-engine, server: initial support for the KV store\npolicies\n([#702](https://github.com/walmartlabs/concord/pull/702));\n- concord-server: pass external trigger event ID via\nprocess arguments\n([#715](https://github.com/walmartlabs/concord/pull/715)).\n\n### Changed\n\n- server: use clean directory for each refresh listener\n([#707](https://github.com/walmartlabs/concord/pull/707));\n- concord-server: refactor ConcordLdapContextFactory\nimplementation\n([#695](https://github.com/walmartlabs/concord/pull/695));\n- runtime-v2: improve serialization of `loop` items\n([#714](https://github.com/walmartlabs/concord/pull/714));\n- concord-agent, queue-client: make more delays configurable\n([#705](https://github.com/walmartlabs/concord/pull/705));\n- console2: single vertical scroll for process log page\n([#696](https://github.com/walmartlabs/concord/pull/696));\n- console2: allow expand all log segments\n([#698](https://github.com/walmartlabs/concord/pull/698));\n- dependency-manager: log exception\n([#700](https://github.com/walmartlabs/concord/pull/700));\n- cli: check that state is serializable in checkpoint service\n([#703](https://github.com/walmartlabs/concord/pull/703));\n- runtime-v2: allow expression for form call values and runAs\n([#704](https://github.com/walmartlabs/concord/pull/704));\n- runtime-v2: fix argument passing in forks\n([#708](https://github.com/walmartlabs/concord/pull/708)).\n\n## [1.98.2] - 2023-02-08\n\n### Changed\n\n- concord-server: clean nulls from trigger conditions, args, cfg\n([#713](https://github.com/walmartlabs/concord/pull/713));\n\n## [1.98.1] - 2022-12-22\n\n### Changed\n\n- concord-server: @Inject refactoring (part 1)\n([#658](https://github.com/walmartlabs/concord/pull/658));\n- concord-server, oidc: OIDC team/role mapping. Maps\nOpenID properties (e.g. `groups`) to Concord teams and\nroles\n([#682](https://github.com/walmartlabs/concord/pull/682));\n- concord-server: `process_queue` table split (part 1)\n([#668](https://github.com/walmartlabs/concord/pull/668));\n- runtime-v2: do not create log segments for expressions\nby default. Logs produced by expression blocks without\n`name` will no longer be displayed as a separate log\n\"segment\";\n([#689](https://github.com/walmartlabs/concord/pull/689));\n- concord-console: new compact view for the Log tab\n([#690](https://github.com/walmartlabs/concord/pull/690));\n- concord-server-db: a migration task to update secrets using\nthe updated hashing algorithm\n([#691](https://github.com/walmartlabs/concord/pull/691));\n- concord-task: fix concurrency issue when collecting output\nof processes\n([#693](https://github.com/walmartlabs/concord/pull/693));\n- concord-server-db: pass secret salt as a base64 value\n([#694][https://github.com/walmartlabs/concord/pull/689]).\n\n\n## [1.98.0] - 2022-12-07\n\n### Added\n\n- runtime-v2: provide checkpoint name after restore\n([#677](https://github.com/walmartlabs/concord/pull/677));\n- policy: new policy to restrict raw payload\n([#679](https://github.com/walmartlabs/concord/pull/679));\n- concord-cli: provide default process configuration\n([#649](https://github.com/walmartlabs/concord/pull/649));\n- policy: policy to restrict runtime of process\n([#671](https://github.com/walmartlabs/concord/pull/671));\n- resource-task: add printJson() method\n([#676](https://github.com/walmartlabs/concord/pull/676));\n- server: cleanup agent commands\n([#674](https://github.com/walmartlabs/concord/pull/674));\n- policy-engine: `cron` trigger policy\n([#686](https://github.com/walmartlabs/concord/pull/686)).\n\n### Changed\n\n- runtime-v2: fix parallel loop execution when no out variable defined\n([#659](https://github.com/walmartlabs/concord/pull/659));\n- console2: repository list now with paging\n([#643](https://github.com/walmartlabs/concord/pull/643));\n- server: api for list project repositories with limit/offset\n([#643](https://github.com/walmartlabs/concord/pull/643));\n- runtime-v2: \"throw\" with a string value shouldn't produce a stacktrace\n([#673](https://github.com/walmartlabs/concord/pull/673));\n- concord-server: deprecate `process_queue.commit_msg`\n([#670](https://github.com/walmartlabs/concord/pull/670));\n- runtime-v2: move expression evaluator into sdk\n([#667](https://github.com/walmartlabs/concord/pull/667));\n- cli: log checkpoint instead of throwing Exception\n([#665](https://github.com/walmartlabs/concord/pull/665));\n- http-task: allow any value as json body\n([#675](https://github.com/walmartlabs/concord/pull/675));\n- docker-images: change the default shell to bash in\nDebian-based images\n([#644](https://github.com/walmartlabs/concord/pull/675));\n- runtime-v2: fix `entryPoint` calculation in effective YAML\n([#685](https://github.com/walmartlabs/concord/pull/685)).\n\n\n## [1.97.0] - 2022-10-11\n\n### Added\n\n- github: queryParams condition\n([#663](https://github.com/walmartlabs/concord/pull/663));\n- dependency-manager: allow exclusion artifacts from\ntransitive dependencies\n([#657](https://github.com/walmartlabs/concord/pull/657)).\n\n### Changed\n\n- concord-cli: load deps from active profiles\n([#654](https://github.com/walmartlabs/concord/pull/654));\n- runtime-v2: fix parallel execution of ruby scripts\n([#651](https://github.com/walmartlabs/concord/pull/651));\n- concord-server: termintate process wait watchdog loop on\nbatches less than fetch limit\n([#656](https://github.com/walmartlabs/concord/pull/656));\n- runtime-v2: fix serialization error of flow call command\n([#655](https://github.com/walmartlabs/concord/pull/655));\n- concord-cli: ensure absolute target dir\n([#652](https://github.com/walmartlabs/concord/pull/652));\n- runtime-v2: allow access to current argument when\nargument is evaluated\n([#664](https://github.com/walmartlabs/concord/pull/664)).\n\n## [1.96.1] - 2022-09-06 \n\n### Added\n\n### Changed\n\n- project: initial JDK 17 support\n([#625](https://github.com/walmartlabs/concord/pull/625));\n- concord-console: fix for change visibility and renaming\nof secrets from UI\n([#642](https://github.com/walmartlabs/concord/pull/642));\n- runtime-v2: runtime-v2: fix NPE in flow call step\n([#645](https://github.com/walmartlabs/concord/pull/645));\n- concord-server: remove log call for github event in\nrepository refresh flow \n([#633](https://github.com/walmartlabs/concord/pull/633));\n\n\n## [1.96.0] - 2022-08-10\n\n### Added\n\n- concord-cli: option to show version \n([#615](https://github.com/walmartlabs/concord/pull/615));\n- concord-server: implement endpoints for adding LDAP\ngroups to roles\n([#606](https://github.com/walmartlabs/concord/pull/606));\n- concord-ansile, concord-console: add sort options to the\nAnsible host stats\n([#610](https://github.com/walmartlabs/concord/pull/610);\n- docker-images: support for debian os based docker images\n([#611](https://github.com/walmartlabs/concord/pull/611)).\n\n### Changed\n\n- concord-server: fix out vars processing and restrictions\n([#609](https://github.com/walmartlabs/concord/pull/609);\n- concord-cli: fixed broken JS support\n([#612](https://github.com/walmartlabs/concord/pull/612));\n- concord-repository: use regular repositories in tests\n([#616](https://github.com/walmartlabs/concord/pull/616));\n- concord-server, runtime-v2: fix file upload in forms\n([#623](https://github.com/walmartlabs/concord/pull/623));\n- agent-operator: support for apiextensions.k8s.io/v1 crd\nto support k8s 1.22+\n([#624](https://github.com/walmartlabs/concord/pull/624));\n- concord-server: limit the number of acceptor threads to\n`core count / 4` (min 1)\n([#627](https://github.com/walmartlabs/concord/pull/627));\n- project: update to Groovy 2.5.17 to support JDK 17\n([#639](https://github.com/walmartlabs/concord/pull/639)).\n\n\n\n## [1.95.0] - 2022-04-16\n\n### Added\n\n- concord-server: add API for updating secrets\n([#590](https://github.com/walmartlabs/concord/pull/590));\n- http-tasks: add proxy authentication parameters\n([#597](https://github.com/walmartlabs/concord/pull/597));\n- ansible-tasks: implement stats file for flows with\nmultiple playbook runs\n([#596](https://github.com/walmartlabs/concord/pull/596));\n- runtime-v1, v2: add correlationId to checkpoint events\n([#581](https://github.com/walmartlabs/concord/pull/581));\n- resource-tasks, runtime-v2: support for properties\nfiles ([#593](https://github.com/walmartlabs/concord/pull/593)).\n\n### Changed\n\n- project: improve jdk16 compatibility\n([#592](https://github.com/walmartlabs/concord/pull/592));\n- concord-server: introduce exclusive wait conditions\n([#595](https://github.com/walmartlabs/concord/pull/595));\n- project: improve mvnd support\n([#567](https://github.com/walmartlabs/concord/pull/567));\n- runner: exit JVM on OOM error\n([#594](https://github.com/walmartlabs/concord/pull/594));\n- runtime-v2: fix `currentFlowName()` in error blocks\n([#591](https://github.com/walmartlabs/concord/pull/591));\n- runtime-v2: serialize ignoreErrors only if it is true\n([#588](https://github.com/walmartlabs/concord/pull/588));\n- resource-task: convert relative paths to absolute\n([#589](https://github.com/walmartlabs/concord/pull/589));\n- runtime-v2: redirect script output to logger\n([#587](https://github.com/walmartlabs/concord/pull/587));\n- agent: fix log segments parser\n([#586](https://github.com/walmartlabs/concord/pull/586));\n- resource-task: fix java 8 date/time serialization\n([#584](https://github.com/walmartlabs/concord/pull/584));\n- it: explicitly specify initialBranch for git tests\n([#582](https://github.com/walmartlabs/concord/pull/582)).\n\n\n\n## [1.93.3] - 2022-03-11\n\n### Changed\n\n- agent: fix log segments parse\n([#586](https://github.com/walmartlabs/concord/pull/586)).\n\n## [1.94.0] - 2022-03-07\n\n### Added\n\n- concord-server: add `orgUpdate` permission\n([#552](https://github.com/walmartlabs/concord/pull/552));\n- runtime-v2: add `orDefault` function\n([#557](https://github.com/walmartlabs/concord/pull/557));\n- runtime-v2: add `isDebug` function\n([#558](https://github.com/walmartlabs/concord/pull/558)).\n- agent: option to ignore artifact descriptor repositories\n([#561](https://github.com/walmartlabs/concord/pull/561));\n- runtime-v2: project document support for suspendTimeout\n([#562](https://github.com/walmartlabs/concord/pull/562));\n- concord-server: add process-wait-watchdog metrics\n([#566](https://github.com/walmartlabs/concord/pull/566));\n- runtime-v2: implement `loop` syntax - improved version\nof `(parallel)withItems`\n([#578](https://github.com/walmartlabs/concord/pull/578)).\n\n### Changed\n\n- concord-agent: use a single temporary directory for API clients\n([#544](https://github.com/walmartlabs/concord/pull/544));\n- runtime-v1/v2: remove temporary Docker files\n([#545](https://github.com/walmartlabs/concord/pull/545));\n- runtime-v2: support for runAs (running as another user)\nin cron ([#547](https://github.com/walmartlabs/concord/pull/547));\n- project: update Guava version, remove unneeded usage\n([#550](https://github.com/walmartlabs/concord/pull/550));\n- runtime-v1/v2: use Nashorn compat mode for GraalVM\n([#551](https://github.com/walmartlabs/concord/pull/551));\n- project: upgrade dependencies\n([#554](https://github.com/walmartlabs/concord/pull/554));\n- runtime-v2: remove `configuration.activeProfiles` from\nthe JSON schema (specifying `activeProfiles` in YAML\ndocuments was never supported)\n([#556](https://github.com/walmartlabs/concord/pull/556));\n- runtime-v2: allow arrays for GH trigger conditions\n([#563](https://github.com/walmartlabs/concord/pull/563));\n- ansible: clear host filters after switching to the next stat tab\n([#575](https://github.com/walmartlabs/concord/pull/575));\n- ansible: filter host groups by playbookId\n([#574](https://github.com/walmartlabs/concord/pull/574));\n- runtime-v2: allow null values in configuration.arguments\n([#571](https://github.com/walmartlabs/concord/pull/571));\n- concord-server: implement a better way to kill processes\n([#572](https://github.com/walmartlabs/concord/pull/572));\n- runtime-v2: fix currentFlowName after restoring from a\ncheckpoint\n([#580](https://github.com/walmartlabs/concord/pull/580)).\n\n\n\n## [1.93.2] - 2022-02-17\n\n### Changed\n\n- runtime-v1: do not override process arguments with default variables\n([#569](https://github.com/walmartlabs/concord/pull/569)).\n\n\n\n## [1.93.1] - 2022-02-11\n\n### Added\n\n- concord-server: add orgUpdate permission\n([#552](https://github.com/walmartlabs/concord/pull/552)).\n\n### Changed\n\n- runtime-v2: fix segment status parse\n([#549](https://github.com/walmartlabs/concord/pull/549));\n- graalvm: use nashorn compat mode\n([#551](https://github.com/walmartlabs/concord/pull/551));\n- agent: allow ignore artifact descriptor repositories\n([#561](https://github.com/walmartlabs/concord/pull/561)).\n\n\n\n## [1.93.0] - 2022-01-24\n\n### Added\n\n- runtime-v2: function to retrieve the current flow name \n([#514](https://github.com/walmartlabs/concord/pull/514));\n- runtime-v2: add `evalAsMap` flow function\n([#520](https://github.com/walmartlabs/concord/pull/520));\n- concord-agent: add dependencyResolveTimeout configuration\nparameter ([#522](https://github.com/walmartlabs/concord/pull/522));\n- concord-parent: support for Apple M1 silicon\n([#527](https://github.com/walmartlabs/concord/pull/527));\n- concord-console: add icon for suspended status\n([#530](https://github.com/walmartlabs/concord/pull/530)).\n\n### Changed\n\n- runtime-v2: disable GraalVM runtime compilation warning,\nfix logging ([#517](https://github.com/walmartlabs/concord/pull/517));\n- runtime-v2: use stdout instead of files for logging\n([#518](https://github.com/walmartlabs/concord/pull/518));\n- concord-server, runtime-v2: fix for empty exclusive group\nvalues ([#519](https://github.com/walmartlabs/concord/pull/519);\n- concord-server, lock-tasks: avoid creating a wait\ncondition on each lock aquisition attempt\n([#521](https://github.com/walmartlabs/concord/pull/521));\n- concord-agent: log artifact download errors into process\nlog ([#523](https://github.com/walmartlabs/concord/pull/523));\n- project: migrate to JUnit 5\n([#528](https://github.com/walmartlabs/concord/pull/528));\n- runtime-v1: remove variables from default vars with\nthe same names as task names to avoid conflicts\n([#529](https://github.com/walmartlabs/concord/pull/529));\n- concord-server: remove `/api/service/process_portal`\n([#531](https://github.com/walmartlabs/concord/pull/531));\n- runtime-v2: log errors when `ignoreErrors` specified\n([#532](https://github.com/walmartlabs/concord/pull/532));\n- concord-server: better checkpoint data validation\n([#533](https://github.com/walmartlabs/concord/pull/533));\n- runtime-v2: update JSON schema generation for `ignoreErrors`\nparams ([#536](https://github.com/walmartlabs/concord/pull/536));\n- project: add missing `serialVersionUUID` fields to model\nclasses ([#537](https://github.com/walmartlabs/concord/pull/537));\n- concord-console, runtime-v2: do not automatically open the\nsystem log segment\n([#539](https://github.com/walmartlabs/concord/pull/539));\n- runtime-v2: support for `out` parameters of `script` steps\n([#540](https://github.com/walmartlabs/concord/pull/540)).\n\n\n\n## [1.92.0] - 2021-12-07\n\n### Added\n\n- runtime-v2: `ignoreErrors` mode support for tasks\n([#484](https://github.com/walmartlabs/concord/pull/484));\n- concord-server: record audit log when processes are\ncancelled ([#495](https://github.com/walmartlabs/concord/pull/495));\n- file-tasks: new methods - `move`, `relativize`\n([#498](https://github.com/walmartlabs/concord/pull/498));\n- crypto-v2: allow users to specify `dest` directory when\ncalling `exportAsFile`\n([#499](https://github.com/walmartlabs/concord/pull/499));\n- concord-server: allow users to override timeout for error\nhandlers using new process configuration property\n`handlerProcessTimeout`\n([#501](https://github.com/walmartlabs/concord/pull/501));\n- http-tasks: log invalid JSON bodies and parse errors\n([#505](https://github.com/walmartlabs/concord/pull/505)).\n\n### Changed\n\n- docker: fixed the behavior of `stdout` parameter\n([#472](https://github.com/walmartlabs/concord/pull/472));\n- concord-task: fix `suspendForCompletion` action in the v2\nversion of the task\n([#488](https://github.com/walmartlabs/concord/pull/488));\n- concord-task: return fork IDs in the `ids` variable even for\nsingle-fork calls (v2 only)\n([#488](https://github.com/walmartlabs/concord/pull/488));\n- runtime-v2: fix exception when `MetadataProcessor` called\nafter an `exit` step\n([#491](https://github.com/walmartlabs/concord/pull/491));\n- concord-server: fix concurrent process status update\n([#493](https://github.com/walmartlabs/concord/pull/493));\n- runtime-v2: fixed an issue with variable propagation in\nthe presense of an `error` block in flow calls\n([#496](https://github.com/walmartlabs/concord/pull/496));\n- runtime-v1, runtime-v2: tone down the metadata processor's\nlogs ([#500](https://github.com/walmartlabs/concord/pull/500));\n- smtp-tasks: simplify default variables, allow `host` and\n`port` parameters without nesting into `smtpParams`\n([#503](https://github.com/walmartlabs/concord/pull/503)).\n\n### Breaking\n\n- concord-task: return process IDs as `String` instead of `UUID`\n([#488](https://github.com/walmartlabs/concord/pull/488)).\n- runtime-v2, docker: replace the `logOutput` parameter with\na number of new fine-grained controls - `redirectErrorStream`, `logOut`,\n`logErr`, `saveOut` and `saveErr`\n([#489](https://github.com/walmartlabs/concord/pull/489));\n- concord-server, concord-console: remove the old deprecated\nAnsible UI and related API endpoints\n([#497](https://github.com/walmartlabs/concord/pull/497));\n- concord-server: remove support for `process.defaultConfiguration`\nconfiguration parameter. This effectively removes concord-server's\nsupport of default process variable files (#504).\n\n\n\n## [1.91.0] - 2021-11-05\n\n### Added\n\n- concord-server, concord-console: allow users to disable triggers\nin repositories ([#476](https://github.com/walmartlabs/concord/pull/476));\n- runtime-v2: add feature to record metadata in tasks call\n([#479](https://github.com/walmartlabs/concord/pull/476)).\n\n### Changed\n\n- concord-agent: do not retry logs on 4xx-5xx response\n([#475](https://github.com/walmartlabs/concord/pull/475)).\n- concord-server: throw error on null teams list in bulk access\nupdate ([#477](https://github.com/walmartlabs/concord/pull/477));\n- concord-repository: fix `checkAlreadyFetched` behavior when\nchecking out an older commit using the current branch\n([#480](https://github.com/walmartlabs/concord/pull/480)).\n\n\n\n## [1.90.0] - 2021-09-16\n\n### Added\n\n- runtime-v2: log effective process dependencies in debug mode\n([#467](https://github.com/walmartlabs/concord/pull/467)).\n\n### Changed\n\n- project: switch to AdoptOpenJDK, initial support for JDK 11 and 16.\nThe default Docker images use JDK 8 by default\n([#434](https://github.com/walmartlabs/concord/pull/434));\n- concord-server-db: fix checksum expectations for API key related\nchangesets ([#463](https://github.com/walmartlabs/concord/pull/463));\n- concord-server: remove an old, unused task from the DB\n([#464](https://github.com/walmartlabs/concord/pull/464));\n- runtime-v2: improved parser error message\n([#465](https://github.com/walmartlabs/concord/pull/465));\n- concord-server: fix a potential race condition in the process\ndispatcher ([#466](https://github.com/walmartlabs/concord/pull/466));\n- concord-server: do not create http sessions for api-key auth\n([#471](https://github.com/walmartlabs/concord/pull/471));\n- concord-server: disable servlet sessions for session token\nauthentication ([#473](https://github.com/walmartlabs/concord/pull/473));\n- runtime-v1, runtime-v2: ability to disable events recording\n([#474](https://github.com/walmartlabs/concord/pull/474)).\n\n\n\n## [1.89.2] - 2021-09-14\n\n### Changed\n\n- concord-server: do not create http sessions for api-key auth\n([#471](https://github.com/walmartlabs/concord/pull/471));\n- concord-server: disable http sessions for session token auth\n([#473](https://github.com/walmartlabs/concord/pull/473)).\n\n\n\n## [1.89.1] - 2021-09-03\n\n### Changed\n\n- concord-server-db: fix checksum expectations for API key related\nchangesets ([#463](https://github.com/walmartlabs/concord/pull/463));\n- concord-server: remove unused task from DB\n([#464](https://github.com/walmartlabs/concord/pull/464)).\n\n\n\n## [1.89.0] - 2021-08-22\n\n### Added\n\n- concord-server: ability to load user API keys from a local file\n([#457](https://github.com/walmartlabs/concord/pull/457)).\n\n### Changed\n\n- runtime-v2: sanitize script variables, make sure values are\n`Serializable` ([#458](https://github.com/walmartlabs/concord/pull/458));\n- concord-server: do not mark processes that have been in the `NEW`\nstatus for a long time as failed to start\n([#459](https://github.com/walmartlabs/concord/pull/459));\n- pfed-sso: redirect to login on failure\n([#462](https://github.com/walmartlabs/concord/pull/462)).\n\n\n\n## [1.88.1] - 2021-08-06\n\n### Changed\n\n- runtime-v2: allow step names to be placed anywhere in the step's\ndefinition (i.e. not necessarily as the first element)\n([#452](https://github.com/walmartlabs/concord/pull/452));\n- runtime-v2: fix evaluation of `retry` expressions. Now expressions\nare allowed to return any `Number` (e.g. `Integer`, `Long`, etc)\n([#454](https://github.com/walmartlabs/concord/pull/454));\n- runtime-v2: fix input parameter override on `retry`;\n([#455](https://github.com/walmartlabs/concord/pull/455)).\n\n\n\n## [1.88.0] - 2021-07-29\n\n### Added\n\n- concord-server: a new API endpoint to force sync LDAP groups of\na specific user ([#442](https://github.com/walmartlabs/concord/pull/442));\n- runtime-v1, runtime-v2: add support for `*.yaml` Concord files in\naddition to `*.yml` ([#443](https://github.com/walmartlabs/concord/pull/443));\n- runtime-v2: allow expressions to be used as the `in` block value\nin `task`, `flow` and `script` steps\n([#447](https://github.com/walmartlabs/concord/pull/447)).\n\n### Changed\n\n- concord-server: ignore synthetic methods annotated with `WithTimer`\n([#444](https://github.com/walmartlabs/concord/pull/444));\n- concord-targetplatform: update jackson-databind version to address\n[CVE](https://github.com/advisories/GHSA-288c-cq4h-88gq)\n([#449](https://github.com/walmartlabs/concord/pull/449));\n- concord-server: roll back changes introduced in\n[#390](https://github.com/walmartlabs/concord/pull/390)\n([#450](https://github.com/walmartlabs/concord/pull/450));\n- ansible: fix retry file persistance (`saveRetry` task parameter)\n([#451](https://github.com/walmartlabs/concord/pull/451)).\n\n\n\n## [1.87.0] - 2021-07-12\n\n### Added\n\n- concord-task: `start`, `startExternal` and `fork` actions now\nreturn process IDs ([#427](https://github.com/walmartlabs/concord/pull/427));\n- concord-server: add more metrics for wait conditions\n([#438](https://github.com/walmartlabs/concord/pull/438)).\n\n### Changed\n\n- concord-cli, concord-server, concord-agent: ability to run\nConcord on Java 16\n([#429](https://github.com/walmartlabs/concord/pull/429));\n- concord-server: fix insertion of wait conditions for forked\nprocesses ([#430](https://github.com/walmartlabs/concord/pull/430));\n- concord-console: fix checkpoint color for failed processes\n([#432](https://github.com/walmartlabs/concord/pull/432));\n- concord-console: fix the editor component initialization. Affects\nthe JSON store query and the project configuration editors\n([#433](https://github.com/walmartlabs/concord/pull/433));\n- concord-server: clean up repository cache using a separate thread\n([#436](https://github.com/walmartlabs/concord/pull/436));\n- runtime-v2: add grammar for the `files` condition in GitHub triggers\n([#437](https://github.com/walmartlabs/concord/pull/437));\n- concord-server: use optimistic locking for wait conditions\n([#439](https://github.com/walmartlabs/concord/pull/439));\n- concord-server: send `PROCESS_STATUS` events to listeners\n(fix for the regression in 1.85.0+)\n([#440](https://github.com/walmartlabs/concord/pull/440));\n- concord-server: cancel listeners on exception\n([#441](https://github.com/walmartlabs/concord/pull/441)).\n\n\n\n## [1.86.3] - 2021-06-16\n\n### Changed\n\n- concord-server: update metrics version, use lock free timers\n([#425](https://github.com/walmartlabs/concord/pull/425)).\n\n\n\n## [1.86.2] - 2021-06-02\n\n### Added\n\n- http-tasks: ability to provide a custom trust stores\n([#399](https://github.com/walmartlabs/concord/pull/399));\n- concord-server: new configuration parameter\n`db.changeLogParameters.defaultAgentToken`. Sets the default\nAPI token for the Agent\n([#410](https://github.com/walmartlabs/concord/pull/410));\n- concord-server: add DNS Service Record feature ldap\n(dynamic loading of LDAP server address)\n([#412](https://github.com/walmartlabs/concord/pull/412));\n- smtp-tasks: read default parameters (`defaultProcessCfg`\npolicy) in the runtime-v2 version of the task\n([#419](https://github.com/walmartlabs/concord/pull/419));\n- resource-tasks: parse object from JSON string\n([#420](https://github.com/walmartlabs/concord/pull/420)).\n\n### Changed\n\n- dependency-manager: update the `maven-resolver` version\n([#405](https://github.com/walmartlabs/concord/pull/405));\n- concord-server: the `iam-sso` plugin has been removed\n([#407](https://github.com/walmartlabs/concord/pull/407));\n- concord-repository: remove dependency on`jgit`\n([#414](https://github.com/walmartlabs/concord/pull/414));\n- concord-task: `startExternal` action should ignore\nthe `suspend` parameter\n([#416](https://github.com/walmartlabs/concord/pull/416));\n- concord-console: update dependencies\n([#418](https://github.com/walmartlabs/concord/pull/418)).\n\n### Breaking\n\n- concord-server, concord-agent: the agent's default API\ntoken has been removed. The server now automatically\ngenerates a new API token on the first start\n([#410](https://github.com/walmartlabs/concord/pull/410) and\n([#413](https://github.com/walmartlabs/concord/pull/413)).\n\n\n\n## [1.85.0] - 2021-04-27\n\n### Added\n\n- ansible: option to limit the logging verbosity based on\nthe inventory size ([#384](https://github.com/walmartlabs/concord/pull/384));\n- runtime-v2: support `name` attribute for script calls \n([#402](https://github.com/walmartlabs/concord/pull/402)).\n\n\n### Changed\n\n- concord-server: restoring from a checkpoint now generates a new\n`CHECKPOINT_RESTORE` event instead of a `PROCESS_STATUS` event\nwith custom payload\n([#389](https://github.com/walmartlabs/concord/pull/389));\n- concord-server: use process events to calculate the process\nqueue stats ([#390](https://github.com/walmartlabs/concord/pull/390));\n- runtime-v2: rollback the state cleanup code added in\n[#358](https://github.com/walmartlabs/concord/pull/358). \nFixed in ([#391](https://github.com/walmartlabs/concord/pull/391));\n- concord-server: fix validation of form fields with expressions in \nallowed values ([#392](https://github.com/walmartlabs/concord/pull/392));\n- concord-server: set SameSite=Lax for the session cookie\n([#394](https://github.com/walmartlabs/concord/pull/394));\n- concord-server: only pass enabled repositories to refresh task\n([395](https://github.com/walmartlabs/concord/pull/395));\n- concord-server: do not mark processes as `FAILED` after resuming\nfrom an invalid status\n([#396](https://github.com/walmartlabs/concord/pull/396)).\n- concord-server: use optimistic locking when updating wait\nconditions ([#397](https://github.com/walmartlabs/concord/pull/397));\n- docker-compose: fix agent and dind communication\n([#400](https://github.com/walmartlabs/concord/pull/400));\n- runtime-v2: log exception as a single error line\n([#401](https://github.com/walmartlabs/concord/pull/401));\n- concord-repository: specify path to the `.gitmodules` file when\nreading submodule urls ([#403](https://github.com/walmartlabs/concord/pull/403));\n- runtime-v2: subsequent execution of reentrant tasks now uses\nthe same log segment as for the first call\n([#406](https://github.com/walmartlabs/concord/pull/406)).\n\n\n\n## [1.84.0] - 2021-04-08\n\n### Added\n\n- concord-server: support multiple wait conditions per process\n([#368](https://github.com/walmartlabs/concord/pull/368));\n- concord-server: cleanup job for `wait_conditions`.\nAutomatically remove `wait_conditions` of expunged processes\n([#380](https://github.com/walmartlabs/concord/pull/380));\n- concord-cli: option to generate the effective Concord YAML file\n([#382](https://github.com/walmartlabs/concord/pull/382));\n- runtime-v2: add example of Ansible's `register` statement usage\n([#387](https://github.com/walmartlabs/concord/pull/387)).\n\n### Changed\n\n- pfed-sso: now provides a `LdapPrincipal` for compatibility\nwith legacy authentication providers\n([#376](https://github.com/walmartlabs/concord/pull/376));\n- concord-imports: hide sensitive data in `toString` methods\n([#378](https://github.com/walmartlabs/concord/pull/378));\n- runtime-v2: fix the duration format when generating an\neffective yaml ([#383](https://github.com/walmartlabs/concord/pull/383)).\n- concord-repository: automatically create `repositoryInfo`\ndirectory if not exists\n([#386](https://github.com/walmartlabs/concord/pull/386));\n- pref-sso: do not redirect if refresh token exists\n([#388](https://github.com/walmartlabs/concord/pull/388)).\n\n\n\n## [1.83.0] - 2021-03-25\n\n### Added\n\n- concord-server: log handler process IDs (`onTimeout`,\n`onCancel`, etc) in the parent process log\n([#350](https://github.com/walmartlabs/concord/pull/350));\n- concord-console: on the repository list page, add commit\nID and repository path links;\n([#351](https://github.com/walmartlabs/concord/pull/351));\n- concord-console, runtime-v1, runtime-v2: ability to log\nprocess IDs as links in the UI\n([#356](https://github.com/walmartlabs/concord/pull/356));\n- concord-repository, concord-server, concord-agent: option\nto skip fetching if local commit ID equals remote commit ID\n([#359](https://github.com/walmartlabs/concord/pull/359)).\n- concord-agent: option to redirect process logs to stdout\n([#362](https://github.com/walmartlabs/concord/pull/362));\n- concord-server, runtime-v2: it is now possible to resume\na process waiting for multiple external events\n([#370](https://github.com/walmartlabs/concord/pull/370));\n- concord-server, concord-console: new process status\n`WAITING`. Before this change, the `SUSPENDED` status was\nused for both processes suspended on an event (e.g. on a\nform) and processes waiting for \"external\" conditions (e.g.\nconcurrent execution limits, waiting for another process or\nlock, etc). This PR creates a clear separation in statuses\nfor such cases\n([#371](https://github.com/walmartlabs/concord/pull/371) and\n [#379](https://github.com/walmartlabs/concord/pull/379)).\n\n### Changed\n\n- runtime-v2: implicitly pass variables into scripts\n([#349](https://github.com/walmartlabs/concord/pull/349));\n- concord-repository: disable git auto-maintenance\n([#353](https://github.com/walmartlabs/concord/pull/353));\n- runtime-v2: fix error handling in reentrable tasks\n([#354](https://github.com/walmartlabs/concord/pull/354)).\n- concord-server, concord-console: fix the rendering of\nform dropdown fields with single allowed values\n([#357](https://github.com/walmartlabs/concord/pull/357))\nand ([372](https://github.com/walmartlabs/concord/pull/372));\n- runtime-v2: cleanup `workDir` files after loading\nthe state ([#358](https://github.com/walmartlabs/concord/pull/358));\n- concord-client: skip empty bodies in error responses, use\nthe HTTP status message instead\n([#363](https://github.com/walmartlabs/concord/pull/363));\n- runtime-v2: fixed the `exclusive` mode grammar in triggers\n(cron, generic, oneops)\n([#364](https://github.com/walmartlabs/concord/pull/364));\n- concord-server: set `initiator` for `onCancel`,\n`onFailure` and `onTimeout` handlers\n([#365](https://github.com/walmartlabs/concord/pull/365));\n- concord-console: update node.js to the current LTS (14.16.0)\n([#366](https://github.com/walmartlabs/concord/pull/366));\n- sleep-task: use UUIDs as event names to support parallel\nexecution ([#369](https://github.com/walmartlabs/concord/pull/369));\n- concord-agent: disable preforks by default, use stable\n`workDir` ([#374](https://github.com/walmartlabs/concord/pull/374)).\n\n\n\n## [1.82.0] - 2021-03-08\n\n### Added\n\n- pfed-sso: add README\n([#339](https://github.com/walmartlabs/concord/pull/339));\n- runtime-v2: support for nested variables in `hasVariable`\nfunction ([#343](https://github.com/walmartlabs/concord/pull/343));\n- examples: runtime-v2 demo\n([#344](https://github.com/walmartlabs/concord/pull/344));\n- http-tasks: support for external keystore files\n([#345](https://github.com/walmartlabs/concord/pull/345)).\n\n### Changed\n\n- runtime-v2: fix the detection of script languages based on\nthe script's file name\n([#339](https://github.com/walmartlabs/concord/pull/339));\n- ansible: append values to `PYTHONPATH`, allow users to\nuse their own `PYTHONPATH` in addition to the provided one\n([#341](https://github.com/walmartlabs/concord/pull/341));\n- concord-server: fixed batching of processes with custom\n`branch` or `commitId`. Previously the batching mechanism might\ndetermine the effective branch and/or commitId incorrectly\n([#347](https://github.com/walmartlabs/concord/pull/347));\n- concord-server: fix repository refresh filtering on GitHub\nevent ([#348](https://github.com/walmartlabs/concord/pull/348)).\n\n\n\n## [1.81.0] - 2021-03-01\n\n### Added\n\n- pfed-sso: add a configuration parameter for token signature\nvalidation ([#325](https://github.com/walmartlabs/concord/pull/325));\n- runtime-v1, runtime-v2: optionally expose docker daemon\n([#332](https://github.com/walmartlabs/concord/pull/332));\n- concord-server, concord-console: add log segment duration\n([#335](https://github.com/walmartlabs/concord/pull/335)).\n\n### Changed\n\n- concord-server, pfed-sso: make sso login independent of ldap\n([#327](https://github.com/walmartlabs/concord/pull/327));\n- concord-console: add a redirect on SSO token/session expiration\n([#327](https://github.com/walmartlabs/concord/pull/327));\n- dependency-manager: remove dots from the `resolveFile` log,\nprevent URLs from being mangled in the UI\n([#330](https://github.com/walmartlabs/concord/pull/330));\n- concord-console: show runtime-v1 recorded in-vars\n([#331](https://github.com/walmartlabs/concord/pull/331));\n- concord-repository: fix fetching when both commitId and branch\nspecified ([#329](https://github.com/walmartlabs/concord/pull/329)).\n- runtime-v2: fix Docker passwd generation \n([#332](https://github.com/walmartlabs/concord/pull/332));\n- concord-server, pfed-sso: get user information from the DB\n([#333](https://github.com/walmartlabs/concord/pull/333));\n- project: remove unused dependencies\n([#334](https://github.com/walmartlabs/concord/pull/334));\n- concord-server: allow overriding of default configuration values\nusing environment variables\n([#336](https://github.com/walmartlabs/concord/pull/336));\n- concord-common: skip outside paths when unzipping files\n([#337](https://github.com/walmartlabs/concord/pull/337));\n- concord-server: sort log segments by IDs in addition to their\ntimestamps to make the order more stable (e.g. for `parallel`\nsituations) ([#338](https://github.com/walmartlabs/concord/pull/338)).\n\n\n\n## [1.80.0] - 2021-02-14\n\n### Added\n\n- concord-server: new authentication plugin `pfed-sso`\n([#318](https://github.com/walmartlabs/concord/pull/323)).\n\n### Changed\n\n- docker-images: restrict `cryptography` dependency to 3.3.1\n([#323](https://github.com/walmartlabs/concord/pull/323));\n- concord-server: use `java.time.Duration` for all intervals in\nthe server configuration file\n([#322](https://github.com/walmartlabs/concord/pull/322));\n- concord-server: fix the batch cancellation of processes without\nan agent ([#317](https://github.com/walmartlabs/concord/pull/317));\n- concord-server: if both `orgId` and `orgName` are given,\nthe `/api/v2/process` filter ignores `orgName`\n([#319](https://github.com/walmartlabs/concord/pull/319)).\n\n\n\n## [1.79.0] - 2021-02-04\n\n### Added\n\n- concord-server: support for `configuration.suspendTimeout`. Allows\nusers to specify the maximum amount of time the process can be in \nthe `SUSPENDED` state;\n([#315](https://github.com/walmartlabs/concord/pull/315));\n- runtime-v2: add EL resolver for plain accessor methods, e.g.\n`MyBean#property()`. Allows seamless usage of classes generated by\n[Immutables](https://immutables.github.io/)\n([#316](https://github.com/walmartlabs/concord/pull/316));\n- concord-client: report the base API URL in error messages\n([#312](https://github.com/walmartlabs/concord/pull/312));\n- concord-tasks: add upsertQuery methods to the JSON store task\n([#311](https://github.com/walmartlabs/concord/pull/311)).\n\n### Changed\n\n- concord-repository: fixed the Git CLI command for retrieving commit\ninfo (extraneous `\\n` in commit description)\n([#313](https://github.com/walmartlabs/concord/pull/313));\n- concord-console: make `projectId` and `projectName` optional in\nthe `SecretEntry` definition (minor API usage fix)\n([#306](https://github.com/walmartlabs/concord/pull/306));\n- concord-client, runtime-v1, runtime-v2: make `SecretService` throw\n`SecretNotFoundException` when the requested secret is not found\n([#309](https://github.com/walmartlabs/concord/pull/309));\n- concord-console: fixed linking of organizations and secrets\n([#308](https://github.com/walmartlabs/concord/pull/308))\n- concord-server: fixed linking of projects and secrets\n([#307](https://github.com/walmartlabs/concord/pull/307))\n- concord-console: the project dropdown list on the new secret page\nand on the secret settings page was replaced with a search field\n([#305](https://github.com/walmartlabs/concord/pull/305)).\n\n\n\n## [1.78.0] - 2021-01-22\n\n### Added\n\n- concord-server: new `exclusive.mode` mode `cancelOld`. When a new\nprocess starts using the `cancelOld` mode, all currently running\nprocesses within the same `exclusive.group` are automatically\ncancelled ([#300](https://github.com/walmartlabs/concord/pull/300));\n- concord-cli: notification when copying a large working directory\ninto `target/`\n([#302](https://github.com/walmartlabs/concord/pull/302));\n- concord-server: new `github` trigger parameter - `exclusive.groupBy`.\nCurrently accepts only `branch` value. Provides a way to cancel new or\nalready running processes that were triggered by a `push` into the\nsame Git branch\n([#301](https://github.com/walmartlabs/concord/pull/301));\n- concord-server: when starting a process using a Git repository,\nsave the branch name in `process_queue` (in addition to `commit_id`)\n([#296](https://github.com/walmartlabs/concord/pull/296)).\n\n### Changed\n\n- concord-repository: allow passing branch, tag and/or commit ID as\na single request parameter\n([#304](https://github.com/walmartlabs/concord/pull/304));\n- ansible: use [Apache Kerby](https://directory.apache.org/kerby/)\ninstead of `sun.security.*`\n([#303](https://github.com/walmartlabs/concord/pull/303);\n- concord-server: remove SQL parser, use restricted views for querying\nJSON stores ([#297](https://github.com/walmartlabs/concord/pull/297));\n- concord-console: the secret dropdown list on the repository page\nis replaced with a search field\n([#299](https://github.com/walmartlabs/concord/pull/299)).\n\n\n\n## [1.77.0] - 2021-01-07\n\n### Added\n\n- dependency-manager: support for pre-emptive basic authentication\nwhen talking to Maven repositories\n([#293](https://github.com/walmartlabs/concord/pull/293));\n- runtime-v2: support for the `name` attribute in `try` and `block`\nelements ([#289](https://github.com/walmartlabs/concord/pull/289)); \n- runtime-v2: automatically generate and publish the JSON schema for\n`runtime: concord-v2` syntax\n([#283](https://github.com/walmartlabs/concord/pull/283));\n- concord-console, runtime-v2: show file name in the segment\ninfo popup ([#288](https://github.com/walmartlabs/concord/pull/288)).\n\n### Changed\n\n- runtime-v2: use JDK's `AbstractMap.SimpleImmutableEntry` to iterate\nover `Map` elements in the `withItems` implementation instead of a\ncustom class.\n([#285](https://github.com/walmartlabs/concord/pull/285)).\n\n\n\n## [1.76.1] - 2020-12-22\n\n### Changed\n\n- runtime-v2: fixed a checkpoint serialization issue when classes\nfrom `dependencies` are used as flow variables.\n\n\n\n## [1.76.0] - 2020-12-22\n\n### Added\n\n- concord-console: custom columns in the process list can now\nbe rendered as links;\n- runtime-v2: new annotation `@SensitiveData` can be used to\nprevent task method arguments from being recorded in process\nevents;\n- concord-server, concord-console: audit log for external events,\nthe process status page can now display external events that\ntriggered the process;\n- policy-engine: new policy type `dependencyRewrite`. Can be used\nto override process dependencies (e.g. to force a specific\nversion).\n\n### Changed\n\n- runtime-v2: improved serialization of `lastError` objects that\ncontain circular references (e.g. Guice exceptions);\n- concord-server, concord-console: use `main` as the default Git\nbranch;\n- concord-agent, runtime-v1: make sure temporary directories are\nremoved;\n- policy-engine: rule matching code refactoring, use common map\nmatcher, add more tests;\n- concord-server: assert type of the active profiles collections,\ntrim values;\n- runtime-v1, runtime-v2: update the list of \"retryable\" errors\nfor `docker pull` operations to support recent versions of \ndocker-client.\n\n\n\n## [1.75.0] - 2020-12-05\n\n### Added\n\n- runtime-v2: add grammar for `repositoryInfo` conditions in\nGitHub triggers;\n- concord-server: new GitHub trigger condition\n`repository.enabled`. Allows filtering by the repository's\nenabled/disabled state;\n- runtime-v2, concord-console: record `post` events for failed\ntasks, highlight failed tasks on the events tab.\n\n### Changed\n\n- concord-console: reload the repository list after removing a\nrepository;\n- concord-console: add scrolling to the last error popup, trigger\ninformation popup and the task call details popup;\n- concord-repository: preserve the logger's MDC when logging\nGit client operations;\n- concord-imports: additional logging when processing `imports`;\n- concord-repository: the git client was reworked to better\nsupport partial fetching of repositories;\n- runtime-v2: unwrap runtime exceptions produced by expressions;\n- concord-client: tidy up the error logging - don't log a\nseparate `WARN` message when the server responds with an error;\n- runtime-v2: save original stacktraces when throwing a\n`MultiException` (e.g. in `parallel` situations);\n- concord-console: now users should be correctly redirected to\ntheir destination page after login.\n\n\n\n## [1.74.0] - 2020-11-24\n\n### Added\n\n- concord-server: allow \"any of [list]\" conditions when matching\nprocess `requirements`;\n- concord-console, runtime-v2: a button to launch the form wizard\nfrom the process log tab;\n- concord-console, runtime-v2: link to download individual log\nsegments;\n- concord-console, runtime-v2: individual form links.\n\n### Changed\n\n- concord-server, runtime-v2: correctly remove submitted forms;\n- runtime-v2: `set` now supports references to partially evaluated\nvalues (e.g. variables defined in the same `set` block), including\nnested keys;\n- concord-server: add transaction-aware versions of\n`ProjectManager#get` and `ProjectAccessManager#assertAccess` methods;\n- runtime-v2: now only resuming threads receive the resume event's\npayload. This fixes the issue with multiple `parallel` forms missing\nsubmitted data;\n- concord-agent, runtime-v2: make the runner responsible for log\nsegment creation;\n- runtime-v2: fixed an issue when submitting a form can cause other\nunsubmitted forms to dissappear;\n- runtime-v2: fixed compilation of block steps when `error` is\nused.\n\n### Breaking\n\n- runtime-v2: `set` with nested keys now replaces the top-level\nreference. If `set` is called in a flow, the changes to nested data\nwon't be visible in the caller flow unless `out` is used;\n- policy-engine, concord-server: rename `entity` policy's types for\nconsistency.\n\n\n\n## [1.73.0] - 2020-11-15\n\n### Added\n\n- concord-console: `entryPoint` can now be used as a column in\nthe process list.\n\n### Changed\n\n- runtime-v2: fixed an issue when a parent process checkpoint\nwas incorrectly applied to the process' forks;\n- concord-tasks: 'kill' shouldn't error on empty instanceId lists;\n- concord-tasks: fix common parameters not being inherited by\n`forks` in the runtime-v2 version of the `concord` task;\n- runtime-v1, runtime-v2: avoid reading partially written\n`instanceId` files;\n- concord-server: allow null values when merging policies;\n- runtime-v2: fixed the merging of multiple Concord YAML files\n(e.g. `configuration.events` blocks and others).\n\n\n\n## [1.72.0] - 2020-11-09\n\n### Added\n\n- concord-console: the process list now support boolean values in\ncustom columns and filters;\n- runtime-v2: save `lastError` in process metadata (feature parity\nwith the runtime v1);\n- policy-engine, concord-server: the `entity` policy now supports\nrepository objects;\n- concord-console, concord-repository: support for OAuth (personal)\ntokens for Git authentication.\n\n### Changed\n\n- concord-console: load add log segments on every refresh (temporary\nworkaround);\n- runtime-v2: fix the loop frame being added to the call stack on\nevery step of `withItems`;\n- runtime-v2: fixed an issue preventing form `values` from being\nvisible in `data.js` (custom forms);\n- project: update Maven Wrapper to 0.5.6;\n- runtime-v2: fixed evaluation of literals values for empty\ncollections (e.g. `[]` used with `set` or in expressions);\n- concord-server: improved error messages when processing `imports`.\n\n\n\n## [1.71.0] - 2020-11-03\n\n### Added\n\n- oneops: record incoming events in the audit log;\n- runtime-v2: initial support for `parallelWithItems`;\n- runtime-v2: support for expressions in `out` blocks of task\ncalls, flow calls, `parallel` and `expr` blocks;\n- runtime-v2: save the current thread ID in `TaskResult`;\n- agent-operator: add `requirements` filter;\n- concord-console: the login form now provides a link to the API\nkey login form.\n\n### Changed\n\n- concord-tasks: fix the missing injection annotations in the v2\nversion of the `jsonStore` task (makes it useable in v2 again);\n- concord-server: `ProcessKeyCache` no longer caches misses;\n- concord-server: fixed handling of `any` conditions in\nthe `github` trigger's `files` filter.\n\n### Breaking\n\n- runtime-v2: new version the `TaskResult` structure, now itcan be\nused to tell the runtime to suspend the process.\n\n\n\n## [1.70.0] - 2020-10-23\n\n### Added\n\n- concord-server: expose task failures  in metrics (including error\nmessages);\n- concord-server: new GitHub trigger `condition` - `files.any`.\nContains all `added`, `deleted` or `modified` files in a `push`\nevent.\n\n### Changed\n\n- runtime-v2: add support for expressions in `name` blocks for all\ntypes of steps;\n- concord-server: fixed some edge cases when converting timezones\nin process status history;\n- runtime-v2: automatically convert values of out variables in\nscripts to their Java counterparts - Maps, Lists, etc;\n- runtime-v2: fixed a NPE when tasks return `null` instead of\n`TaskResult`;\n- concord-cli: print out the error's stacktrace if `verbose` is\nenabled.\n\n\n\n## [1.69.0] - 2020-10-15\n\n### Added\n\n- runtime-v2: steps can now specify `slf4j` logging level using\nthe `meta.logLevel` property.\n\n### Changed\n\n- concord-server: timestamp values in process status history are now\nreturned with a correct time zone;\n- concord-server: throw an exception if the resuming process wasn't\nactually `SUSPENDED`;\n- runtime-v2: `SLF4JPrintStreams` messages should no longer appear\nin log segments;\n- concord-server: task scheduler improvements - save the last error\ninto the DB, simplify polling, improved task heartbeat/stall checks;\n- concord-server: escape expressions (`${...}`) in the input data\nwhen resuming processes using\n`/api/v1/process/{id}/resume/{eventName}` endpoint;\n- ansible: fixed an issue preventing Kerberos authentication from\nworking in nested Docker containers (i.e. when `dockerImage` is\nused);\n- concord-server: allow non-admins to search audit logs by `eventId`\n(`/api/v1/audit?eventId=...`);\n- concord-console: fix widths of columns on the secret list page;\n- runtime-v2: `if` steps now treat `null` values as `false`;\n- concord-server: allow teams with no members (i.e. teams with LDAP\ngroups only).\n\n\n\n## [1.68.0] - 2020-10-05\n\n### Added\n\n- concord-server: a PayloadBuilder method to add files to the process'\n`workDir`;\n- file-tasks: initial version.\n\n### Changed\n\n- slack-tasks: read default variables from context instead of\n`@InjectVariables`. This fixes an issue of calling `slack` using\nexpressions;\n- runtime-v2: fixed handling of process state snapshots. Now\nthe runtime correcly resumes from state snapshots and forks.\n\n\n\n## [1.67.0] - 2020-10-01\n\n### Added\n\n- variables-tasks: runtime-v2 support for the `vars` task;\n- runtime-v2: allow profiles to override flows, forms;\n- concord-server: tx aware OrganizationManager's `createOrUpdate` and\n`createOrGet` methods, useful for server-side plugins;\n- concord-server: tx aware SecretManager's `createBinaryData` method;\n- concord-console, runtime-v2: the process events list now includes\nlinks to the source code of steps;\n- resource-tasks: `prettyPrintYaml` method to output data as\nformatted YAML;\n- concord-server, concord-console: option to download \"the\neffective YAML\" - a single YAML document with all Concord resources\nfor a given process.\n\n### Changed\n\n- concord-server: fixed the handling of the `values` field when\n`readonly` form fields are used;\n- runtime-v2: fixed a typo in code that prevented default form field\nvalues from working correctly;\n- concord-tasks: `repositoryRefresh` task was converted to v2;\n- concord-agent: tone down logging when the main thread is\ninterrupted;\n- ansible: allow any Java `Collection` types (list, set, etc) as\ninventory host lists;\n- concord-agent: improved error handling in workers;\n- concord-server: fixed the error message template when validating\nforms;\n- concord-server: fixed a potential race when registering internal\nmetrics. Fixes a `ConcurrentModificationException` when Server is\nrunning in an embedded environment, e.g. testcontainer-concord's\nLOCAL mode.\n\n### Breaking\n\n- runtime-v2: `ProcessDefinition#configuration()` is a separate type\nnow;\n- runtime-v2: move `ProjectInfo` classes to the SDK.\n\n\n## [1.66.0] - 2020-09-17\n\n### Added\n\n- concord-server: optional `startAt` filter in\nthe `/api/v2/process/requirements` endpoint;\n- concord-cli: support for creating new secrets using SecretManager's\n`create*` methods;\n- http-tasks: `followRedirects` now works for `POST` requests too;\n- concord-server: access changes for projects, secrets and JSON\nstores are now recorded in the audit log (e.g. assigning a team to a\nproject);\n- concord-server: configurable out variables mode for projects. Now\nproject owners can restrict who can specify `out` variables in the\nprocess request;\n- slack: full support for runtime-v2. All v1 actions should now be\navailable in the v2 version too;\n- runtime-v2, concord-console: record and display task results (as in\n`configuration.events.recordTaskOutVars`).\n\n### Changed\n\n- concord-server, concord-console: don't send `WWW-Authenticate` for\nunauthorized UI requests to prevent the basic auth popup from\nshowing;\n- concord-server: `dateTime` form fields are now converted into UTC\non submit;\n- agent-operator: when checking the queue's status, filter out\nprocesses with `startAt` in the future;\n- docker-images: python 2 to 3 migration, ansible 2.8 by default;\n- ansible: python 2 to 3 compatibility fixes;\n- concord-server: fixed the checksum of the `170000` changeset;\n- concord-server: fixed an issue of the `runtime` parameter not being\npassed correctly to the process' forks.\n\n\n\n## [1.65.0] - 2020-09-09\n\n### Added\n\n- concord-console: hyperlinks to individual log segments so\nusers can share/access a segment instead of scrolling;\n- concord-server: support for added/removed/modified `files`\nin GitHub push trigger conditions;\n- runtime-v2: support nested variables in\n`configuration.events.inVarsBlacklist`/`outVarsBlacklist`;\n- concord-agent: option to keep the workDir in a configured\ndirectory after the process ends;\n- concord-console: allow changing owners of secrets;\n\n### Changed\n\n- concord-console: keep the \"system\" segment open by default\nso users can see overall progress;\n- concord-server: `USERS.IS_ADMIN` column is deprecated and\nits usage has been removed from the code;\n- runtime-v2: working process checkpoints support.\n\n\n\n## [1.64.0] - 2020-09-02\n\n### Added\n\n- runtime-v2: support for expressions in checkpoint names.\n\n### Changed\n\n- runtime-v2: improved error message when trying to assign a\nnon-serializable value into a `TaskResult`;\n- concord-console: increase the number of visible log segments\nto 100 (was 30);\n- concord-server: change the wording of the maximum number of\ndependencies error message;\n- runtime-v2: merge EventConfiguration when loading multiple\nresources. Allows users to specify the `events` configuration in\nany Concord YAML file available for the process;\n- concord-server: fixed the maximum number of dependencies error\nmessage;\n- runtime-v2: create a single merged `event` configuration from\n`event` sections of all loaded Concord YAML files.\n\n### Breaking\n\n- runtime-v2: rename `SecretParams#name` to `secretName`. Affects\nthe secret creation methods in `SecretService`);\n- runtime-v2: assume `retry.delay` is in seconds (like in v1).\n\n\n\n## [1.63.0] - 2020-08-30\n\n### Added\n\n- concord-imports: support for `dir` imports (external directories),\ndisabled by default;\n- crypto-tasks: an action to create new secrets (runtime-v2 only);\n- concord-server: JSON store data can now be updated by either `POST`\nor `PUT` method;\n- runtime-v2: support for nested `set`;\n- runtime-v2: support for `out` variables in `try` and `block`\nsteps;\n- runtime-v2: support for naming of `log`, `throw` and `expr` steps;\n- runtime-v2: additional `Variables#assert*` methods.\n\n### Changed\n\n- runtime-v2: fixed a potential NPE in the `retry` code when handling\nexceptions w/o message;\n- docker-images: remove `dumb-init` as Docker ships its own init\nimplementation;\n- runtime-v2: when using `withItems`, `out` variables are now\ncollected into lists;\n- concord-server: remove user's LDAP groups from the DB when\nthe account is disabled;\n- runtime-v1: fixed an issue when GitHub triggers without `version`\nwere treated as v1 triggers;\n- concord-console: new look for the runtime v2 log viewer;\n- concord-console: fix layout of the JSON store query editor;\n- iam-sso: mark the SSO cookie as \"secure\";\n- ansible: fixed an issue when `ignore_error` values were saved\n\"as is\" in Ansible events (e.g. unevaluated expressions);\n- concord-agent: do not print out the process' `sessionToken` in\nlogs;\n- concord-console: fix manual refreshing of the process list;\n- concord-server: allow nested metadata filters in\n`GET /api/v2/process` and `GET /api/v2/process/count` endpoints;\n- concord-server: runtime-v2 compatibility for processes started via\nthe browser link (aka \"the process portal\").\n\n### Breaking\n\n- concord-server: the process list entries\n(`GET /api/v2/process` endpoint) no longer include `imports`.\n\n\n\n## [1.62.0] - 2020-08-19\n\n### Added\n\n- concord-server: a way to deploy the DB schema without\n`SUPERUSER` role and/or `CREATE EXTENSION` privileges;\n- concord-console: a way to open a process log at a specific\ncheckpoint by using `#/process/{id}/log#{eventCorrelationId}`;\n- runtime-v2: the v2 SDK now provides the `DependencyManager`\ninterface.\n\n### Changed\n\n- concord-client: `ClientUtils#getHeaders` now correctly assumes\ncase-insensitivity of header names;\n- concord-server: `OrganizationManager#create` and `#update`\nreplaced with a single `createOrUpdate` method;\n- ansible: when using `auth.privateKey.path` don't remove the file\nafter the play's end.\n\n### Breaking\n\n- runtime-v2: `Task#execute` and `ReentrantTask#resume` now return\na new common result type - `com.walmartlabs.concord.runtime.v2.sdk.TaskResult`.\n\n\n\n## [1.61.0] - 2020-08-13\n\n### Added\n\n- concord-agent: `CONCORD_JAVA_OPTS` environment variable can now be\nused to specify additional JVM options.\n\n### Changed\n\n- concord-server: allow `null` values in process event data to\nimprove backward compatibility with older plugins;\n- runtime-v2: warn about `@Singleton` tasks;\n- runtime-v1, runtime-v2: `throw` with a string value doesn't\nproduce a stacktrace anymore;\n- project: JDK 11 compatibility.\n\n### Breaking\n\n- runtime-v2: `ReentrantTask` now accepts `ResumeEvent` instead of\n`Map<String, Object>`;\n- runtime-v2: `@DefaultVariables` annotation was replaced with\n`Context#defaultVariables()` method;\n- runtime-v2: the SDK module no longer shares interfaces/types with\nthe v1 SDK:\n  - `ApiConfiguration`\n  - `DockerContainerSpec`\n\n\n\n## [1.60.1] - 2020-08-05\n\n### Changed\n\n- concord-agent: fixed an issue preventing the Agent from working\nin the `LOCAL` mode of [testcontainers-concord](https://github.com/concord-workflow/testcontainers-concord).\n\n\n\n## [1.60.0] - 2020-08-05\n\n### Added\n\n- runtime-v2: support for `name` attributes in task, flow call and\nexpresion steps;\n- concord-server: store process `dependencies` in\nthe `process_queue.dependencies` column;\n- runtime-v2: `LockService` implementation;\n- concord-server: expose the current number of processes with\n\"wait conditions\" as a new metric - `process_queue_enqueued_wait`.\n\n### Changed\n\n- concord-agent: use process session token to append logs, download\nstate, update status, etc. Previously, agents used an API token from\nthe configuration file for such operations;\n- concord-console: the refresh action of the process list page was\nrewritten to use React Hooks;\n- concord-console: fixed scrolling on the repository list page in\nthe presence of a modal dialog.\n\n\n\n## [1.59.0] - 2020-07-30\n\n### Changed\n\n- concord-console: improved error handling, standartize refresh\nindicators/buttons;\n- concord-server: fixed the parsing of `process.maxStateAge`\nconfiguration parameter.\n\n### Breaking\n\n- concord-server, concord-server-sdk: the `ProcessKey` interface\nwas replaced with a concrete type, moved from the server's `impl`\nmodule;\n- concord-server: remove the support for GitHub triggers v1.\nThe v1 triggers are deprecared since 1.32.0+.\n\n\n## [1.58.1] - 2020-07-28\n\n### Changed\n\n- concord-server: count child processes towards the concurrency\nlimit;\n- agent-operator: now correctly calculates the pod's\nconfiguration hash, fixes an issue when pods where incorrectly\nscheduled for replacement due to the pool's size changes;\n- concord-server: perform all process key lookups via\n`ProcessKeyCache`;\n- concord-console: disable autocomplete for password fields on\nthe new secret page and when encrypting a string.\n\n### Breaking\n\n- concord-agent, concord-server, concord-cli: the configuration\nparameters that previously used integers for duration values\n(e.g. `repositoryCache.maxAge`) now use literal duration values.\n\n\n\n## [1.58.0] - 2020-07-24\n\n### Added\n\n- concord-console: host status filter to the Ansible host list\npage;\n- runtime-v2: support for the `debug` mode.\n\n### Changed\n\n- runtime-v2: improved validation of the `script` step;\n- concord-tasks: log the child process' URL;\n- concord-cli: by default do not remove the `target` directory\nafter the process finishes;\n- concord-cli: trim whitespace when reading single-value (string)\nsecrets;\n- concord-server: fixed an issue preventing JSON store creation\nrequests from being correctly validated (i.e. disallow invalid\nstore names).\n\n### Breaking\n\n- concord-server: `java.util.Date` and `java.sql.Timestamp` used\nin internal and external Java APIs are replaced with\n`java.time.OffsetDateTime`;\n- concord-server-db: `timestamp` columns are migrated to\n`timestamptz`. The migration procedure requires the DB user to\nhave `SUPERUSER` privileges for the duration of the migration.\n\n\n\n## [1.57.0] - 2020-07-22\n\n### Added\n\n- concord-server, concord-repository: an option to ignore\nremote fetch if the local commit ID is the same as the remote\ncommit ID;\n- concord-server: audit logs for API key creation and removal;\n- policy-engine, concord-server: new policy `state`. Allows\ncontrol of various aspects of process state (size, filenames,\netc);\n- runtime-v1: new option configuration option\n`runner.enableSisuIndex`. Enables usage of the Sisu bindings\nindex;\n- concord-server: creation of organizations now requires a\nrole with the `createOrg` permission.\n\n### Changed\n\n- concord-server: fixed an issue preventing the validation of\nthe request data from working correctly in JSON Store\nendpoints;\n- concord-repository: skip fetch if the local copy points to\nthe same commit;\n- concord-repository: correctly release the repository's lock;\n- concord-server: fixed a bug in the \"process portal\" page\ntemplate which was preventing the page from reloading;\n- agent-operator: the API client now saves session cookies to\navoid the need to authenticate on every request;\n- concord-server: allow users with the admin role to create\nnew LDAP-based accounts even if the user creation is disabled\nfor the realm;\n- concord-task: fixed an issue preventing subsequent calls to\nthe task to fail when the suspend/resume feature is used;\n- runtime-v2: now requires `sisu-maven-plugin` to be used in\nall plugins.\n\n\n\n## [1.56.0] - 2020-07-09\n\n### Added\n\n- concord-cli: initial support for `profiles`;\n- concord-cli: option to remove the target directory before\nthe process starts;\n- concord-cli: provide feedback while processing `imports`;\n- concord-cli: the `run` command now supports profiles;\n- dependency-manager: support for proxy servers (http/https);\n- runtime-v2: support for `meta` attributes in `log` steps.\n\n### Changed\n\n- concord-server: fixed an issue with process wait conditions not\nbeing cleared in time;\n- concord-server: fixed a potential NPE in the repository cache\ncleanup code;\n- project: JDK11+ compatibility;\n- concord-server: force all usernames to lower case to avoid issues\nwith AD/LDAP authentication and environments with multiple auth/z\nproviders.\n\n\n\n## [1.55.0] - 2020-07-01\n\n### Added\n\n- runtime-v2: warn when a variable name \"shadows\" a task name;\n- concord-server: lift restrictions on the format of usernames.\nAllow `@` and other special characters;\n- concord-server: a short project cache for GitHub triggers v1 to\navoid multiple DB queries when filtering out triggers;\n- concord-tasks: runtime v2 compatibility;\n- concord-server, agent-operator: fetch only process requirements,\nconfigurable fetch depth;\n- concord-server: new policy to control size of process attachments.\n\n### Changed\n\n- agent-operator: new CR template property `%%preStopHook%%` for\ninjecting the preStop hook script's content;\n- concord-cli: `run` action now copies all files into the `target`\ndirectory in the current directory;\n- concord-client: the error message when the requested secret type\ndoesn't match the result now includes the name of the secret;\n- policy-engine: no longer dependens on `commons-lang3`;\n- concord-console, concord-server: `ENQUEUED` filter now includes\nprocesses with `start_at < now`;\n- concord-server: stop all background and scheduled tasks when\nthe maintenance mode is enabled;\n- concord-repository: move cache directories before removing to\nreduce the time spent while locked;\n- concord-server: fix process status update in the queue batching\ntask;\n- concord-server: optimize the session key validation by reducing\nthe amount of data pulled from the DB.\n\n\n\n## [1.54.0] - 2020-06-24\n\n### Added\n\n- concord-console: an indicator for partially loaded log segments;\n- runtime-v2: initial support for parallel block's in/out;\n- runtime-v2: initial support for in/out variables in `parallel`\nblocks;\n- runtime-v2: initial support for \"reentrant tasks\" (tasks that can\nbe suspended and resumed);\n- agent-operator: add the preStop script to the `dind` container;\n- concord-server: additional metrics for github events - the number\nof processes started/triggers fetched/etc per event;\n- concord-console: new loading indicator for log segments;\n- runtime-v2: add `projectInfo` to `context`;\n- concord-cli: option to run individual Concord YAML files.\n\n### Changed\n\n- concord-server: when updating/moving a project assert name when no\nID given;\n- concord-server: fixed the calculation of\nthe `enqueued-workers-available` metric;\n- concord-console: updated Json Store capacity indicator;\n- concord-console: auto refresh the child process list;\n- concord-console: show open segments correctly when the system\nsegment's visibility changes.\n\n\n\n## [1.53.1] - 2020-06-16\n\n### Changed\n\n- concord-server: fixed an issue preventing `useInitiator` from\nbeing correctly handled by GitHub triggers.\n\n\n\n## [1.53.0] - 2020-06-13\n\n### Added\n\n- concord-server: option to ignore \"empty\" `push` notifications\nfrom GitHub;\n- concord-server: optional batching mechanism for the `NEW` to\n`ENQUEUED` transition;\n- runtime-v2, concord-console: show `meta.segmentName` as the log\nsegment's name;\n- concord-console: show GitHub event details in the \"triggered by\"\npopup;\n- docker-tasks: runtime v2 compatibility;\n- runtime-v2: option to redirect `System.out` and `err` to segmented\nlogs;\n- runtime-v2: support for `error` blocks in `call` steps;\n- runtime-v2: `Context` can now be injected into tasks.\n\n### Changed\n\n- concord-repository: fixed an issue preventing `imports` from \nbeing excluded from the process state;\n- agent-operator: use `preStop` hooks instead of directly\nremoving pods;\n- concord-server: OneOps specific endpoints extracted as a server\nplugin;\n- concord-console: re-load repository data when the process start\npopup opens;\n- ansible: improved error messages in lookup plugins for Concord\nsecrets;\n- concord-console: the team list page and the \"find a team\" dropdown\nare rewritten to use React Hooks instead of `react-redux`;\n- runtime-v1: trim data when recording in/out variables;\n- concord-console: the trigger list moved to the repository page\ninto a separate tab;\n- concord-console: the login form was rewritten to use React Hooks\ninstead of `react-redux`;\n- runtime-v2: `call` can now have multiple `out` variables;\n- concord-server: the endpoint for creation of Inventory queries now\nsupports both `text/plain` and `application/json` content types;\n- concord-server: disable key validation when uploading existing\nSSH key pairs. The library (JSch) used to validate keys doesn't\nsupport keys larger than 2048 bits.\n\n### Breaking\n\n- runtime-v2: the `Task` interface now accepts input `Variables`\ninstead of the context.\n\n\n\n## [1.52.0] - 2020-06-03\n\n### Added\n\n- runtime-v2: `throw` step support;\n- runtime-v2: record pre/post events for task called using\nexpressions;\n- runtime-v2, concord-console: show the YAML file name on\nthe events tab.\n\n### Changed\n\n- concord-server: fix the adding of team members by user IDs;\n- agent-operator: the autoscaler now scales up more rapidly and\nscaled down more gradually;\n- slack: reduce chattiness in the process log by moving some\nof the log statements to `debug`;\n- runtime-v2: replace custom service injector with Guice-based\ninjector;\n- runtime-v2: more details when recording step events;\n- runtime-v2: fix logging for steps without \"proper\"\nsegments - scripts, expressions, etc;\n- runtime-v2: start the heartbeat as soon as possible;\n- runtime-v2: support for the \"short form\" of task calling\nhas been removed;\n- concord-server: re-initialize the process initiator when\ncreating a fork. This fixes an issue when a process fork is\ncreated using an API key that belongs to a user other than\nthe parent process' current user.\n\n### Breaking\n\n- runtime-v2: support `@InjectVariable` annotations has been\nremoved.\n\n\n\n## [1.51.0] - 2020-05-27\n\n### Added\n\n- runtime-v2: implement process heartbeat;\n- concord-console: custom columns in the process list can now\nuse `requirements` as the `source`;\n- runtime-v2: support for `error` blocks in `task` steps;\n- dynamic-tasks: runtime v2 compatibility;\n- concord-console: new \"Duration\" column in the process list.\nShows the amount of time spent `RUNNING` a process;\n- noderoster-tasks: runtime v2 compatibility.\n\n### Changed\n\n- concord-console: `ENQUEUED` processes that are scheduled for\nfuture (using `startAt`) can now be displayed separately from\nregular `ENQUEUED` processes and vice versa;\n- ansible: escape special characters when the command script\nis created;\n- concord-server: fixed an issue preventing the LDAP group\nsynchronizer from working correctly if there are previously\ndisabled user accounts;\n- concord-console: use IDs when adding users to teams to avoid\nissues with duplicate usernames or the need for `userType`\nparameter. Allow `cron` and other system users to be added to\nteams using the UI.\n\n### Breaking\n\n- runtime-v2: make all variables local. Variables must be\nexplictly passed between flows using `in` and `out` parameters;\n- docker: rebase images on top of `centos:8`.\n\n\n\n## [1.50.1] - 2020-05-19\n\n### Changed\n\n- concord-server: fix the \"Unconnected sockets not implemented\"\nissue when connection timeout is used for LDAP connections. \n\n\n\n## [1.50.0] - 2020-05-18\n\n### Added\n\n- concord-server: configurable `connectTimeout` and\n`readTimeout` for LDAP calls;\n- concord-server: log duration of GIT operations;\n- runtime-v2: support for return and exit steps;\n- ansible: new parameters to enable or disable various features:\n`enableEvents`, `enableStats`, `enableOutsVars`;\n- runtime-v2: support for expressions in call step;\n- runtime-v2: grammar support for cron trigger's `timezone`;\n- concord-cli: support for `crypto.exportAsFile`;\n- runtime-v2: `template` support;\n- runtime-v2: support for out variables;\n- repository: support for \"non detached\" checkouts;\n- runtime-v2: support for setting variables using `set`.\n\n### Changed\n\n- iam-sso: re-enable disabled user accounts on successful login;\n- concord-server: fix handling of `Bearer` tokens;\n- runtime-v2: in/out params in task events are now truncated to\nlimit the amount of data saved;\n- runtime-v1: process metadata updates are now automatically\nretried in case of errors;\n- runtime-v2: add `concord/concord.yml`to the list of default\n`resources`;\n- runtime-v2: replace Jackson's `JsonLocation` with a custom type;\n- runtime-v2: log step execution errors with the step's location;\n- runtime-v2: the default expression evaluator now implements\ndifferent evaluation rules for process arguments, task inputs and\n`set` steps.\n\n\n\n## [1.49.0] - 2020-05-06\n\n### Added\n\n- concord-cli: support for `imports`;\n- concord-cli: allow specifying an `entryPoint`;\n- concord-cli: support for `crypto.exportAsString`;\n- concord-cli: initial support for `crypto.decryptString`;\n- concord-cli: initial support for \"default variables\". The built-in\nvariables provide some useful defaults for the Ansible plugin;\n- runtime-v2: support for process metadata;\n- runtime-v2: support for \"processTimeout\", \"exclusive\", \"events\"\nand \"requirements\" elements;\n- runtime-v2: allow process suspension via `Context#suspend` method;\n- concord-server: show a custom 404 page when a form is not found;\n- misc-tasks, datetime-tasks: runtime v2 compatibility;\n- lock-tasks: runtime v2 compatibility;\n- ansible: runtime v2 compatibility;\n- locale-tasks: runtime v2 compatibility;\n- smtp-tasks: runtime v2 compatibility;\n- concord-server, concord-console, concord-agent, runtime-v2:\ninitial implementation of \"segmented\" process logs. Each task now\ngets a separate segment of the process log and can be shown on\nthe UI individually;\n- log-tasks: add `level` parameter to the v2 version of the task;\n- concord-agent: configurable default JVM parameters;\n- concord-console: show `startAt` to the process toolbar;\n- concord-server: make the embedded Jetty aware of proxy headers\nsuch as `X-Forwarded-For`;\n- concord-server: audit log now includes API key IDs.\n\n### Changed\n\n- runtime-v1: fixed an issue preventing process variables from being\npassed into an `onFailure` handler if the parent process fails with\nan unhandled exception;\n- concord-agent: log dependency check results using `WARN` level;\n- runtime-v2: fix github trigger's exclusive attribute definition;\n- ansible: improved detection of the `setup` task type;\n- project: replace ollie dependency with ollie-config;\n- concord-server: fix the \"show only user's organizations\" toggle;\n- concord-console: don't do the initial search when the Node Roster\npage opens;\n- concord-server, concord-console: fixed the behaviour of\nthe \"show only user's organizations\" toggle on the organizations\nlist page.\n\n\n\n## [1.48.1] - 2020-04-22\n\n### Changed\n\n- concord-server: fixed an issue when the `requestInfo` variable\nisn't provided in some cases (regression);\n- concord-server: fixed an issue when the `projectInfo` variable\nis overwritten with an incorrect value when the process is resumed\n(regression).\n\n\n\n## [1.48.0] - 2020-04-21\n\n### Added\n\n- kv-tasks: runtime v2 compatibility;\n- resource-tasks: runtime v2 compatibility;\n- crypto-tasks: runtime v2 compatibility;\n- runtime-v2: optional support for \"segmented logs\" where each\ntask call gets its own log file;\n- log-tasks: runtime v2 compatiblity;\n- throw-tasks: runtime v2 compatibility;\n- runtime-v2: support for `resources`;\n- runtime-v2: support for `script` steps.\n\n### Changed\n\n- concord-server: public organizations are now visible for\neveryone regardless of membership;\n- concord-server: organization owners can now see their\norganizations even if they don't belong to any team in it;\n- concord-server: fixed the even type filter when querying\nrepository events;\n- policy-engine, concord-server: remove deprecated and unused\n`queue` policies - `process`, `processPerOrg` and\n`processPerProject`;\n- runtime-v2: improved \"method not found\" error messages when\nevaluating expressions;\n- runtime-v2, concord-server: improved detection of\nthe `runtime` parameter. Now it can be specified in\nthe `configuration` section or in the request parameters;\n- runtime-v2: the expression evaluator now correctly supports\npartial evaluation of nested data;\n- concord-server: some endpoints that were previously\nautomatically creating users no longer do so. E.g. when\nspecifying an owner of a JSON store, the user record must\nexist beforehand;\n- concord-project-loader: allow `runtime` to be specified\nexternally, e.g. in process request parameters;\n- ansible: improved validation of `inventory` and\n`vaultPassword` parameters;\n- concord-server: make the \"Copying the repository's data\"\nmessage shorter, don't print out the repository's metadata;\n- concord-server, concord-agent, runtime-v1, v2: major\nprocess/runner configuration refactoring. The process' session\ntoken is no longer saved as a file in the working directory,\nbut passed as a process configuration field;\n- repository: removed delay between `fetch` retries.\n\n\n\n## [1.47.1] - 2020-04-10\n\n### Changed\n\n- concord-agent: fix \"preforking\" by removing the process'\nsession key from runner configuration files;\n- concord-server: removed old LDAP user search endpoint\n`/api/service/console/search/users` and all associated code.\n\n\n\n## [1.47.0] - 2020-04-08\n\n### Added\n\n- runtime-v1, runtime-v2, concord-server: support for\n`publicFlows` - a top-level element with a list of public flows.\nOnly public flows are allowed to be used as `entryPoint` values;\n- concord-server: additional audit logging for user\nmanagement - account creation/update, enabling or disabling\nof the account;\n- iam-sso: option to convert user domain names via\n`sso.domainMapping` configuration parameter;\n- http-tasks: initial support for runtime v2.\n\n### Changed\n\n- concord-server: improved validation of API key names, improved\nhandling of duplicates;\n- runtime-v2: now the runtime evaluates only top-level variables\nin task, form and flow call parameters to avoid undesirable\n\"interpolation\" of nested values;\n- concord-server: when executing `ldap.principalSearchFilter`\npass username and domain values separately in additional\narguments;\n- concord-agent: the start script no longer depends on `uuid`\nexecutable.\n\n\n\n## [1.46.0] - 2020-04-02\n\n### Added\n\n- runtime-v2: support for `if` and `switch` steps;\n- runtime-v2: support for lists in `githubOrg` and `githubRepo`\nconditions in `github` triggers;\n- runtime-v2: ability to save all task call results in the process\nstate for later use. Useful for implementing policies that restrict\nflow execution based on results of previously called tasks;\n- concord-server: automatically re-enable AD/LDAP user accounts\nthat were previously disabled by the AD/LDAP synchronization\nprocess;\n- runtime-v2: `DockerService` support;\n- runtime-v2: support for task policies.\n\n### Changed\n\n- concord-server, concord-agent: externalize default git operation\ntimeout duration. Increase default value to ten minutes;\n- concord-server: treat empty project name as null when updating\nsecret. This fixes an issue preventing users from being able to\n\"unlink\" secrets from projects.\n\n\n\n## [1.45.0] - 2020-03-30\n\n### Added\n\n- runtime-v2: initial support for checkpoints;\n- runtime-v2: support for multiple `TaskProvider` instances;\n- runtime-v2: initial implementation of `SecretService` and\n`FileService`;\n- concord-server: option to generate the default admin API token on\nstart;\n- runtime-v2: support for triggers;\n- runtime-v2: process event recording;\n- http-tasks: response headers are now saved into the result\nvariable;\n- concord-console: alternate shading for actionable tables and lists;\n- concord-server, concord-agent: configurable repository locks.\nThe maximum number of concurrent Git operations can now be configured\nin the Server and Agent configuration files;\n- runtime-v2: initial support for task defaults (default task\nvariables).\n\n### Changed\n\n- concord-console: some of user selection fields (e.g. a project\nowner field) now perform search using the DB data, without accessing\nexternal AD/LDAP servers;\n- concord-agent: fixed a potential race condition when\nthe maintenance mode is activated;\n- dependency-manager: disable Aether caching, allows\n`snapshotPolicy.updatePolicy` to work correctly;\n- concord-console: make the scroll up button always visible;\n- concord-tasks: `requirements` are now correctly passed into the API\nrequest;\n- vagrant: fixed the database container startup.\n\n\n\n## [1.44.0] - 2020-03-12\n\n### Added\n\n- concord-server: configurable CORS origin;\n- concord-console: ability to filter processes by a repository\nname;\n- new server plugin: `oidc`. Allows user authentication using an\nOpenID Connect provider;\n- concord-tasks: `meta` field can now be specified when starting or\nforking processes using the `concord` task;\n- concord-cli: initial support for running v2 flows;\n- runtime-v2: support for `imports`;\n- runtime-v2: support for running v1 tasks.\n\n### Changed\n\n- ansible: no longer requires `ujson` Python module. The module is\nstill included into the `concord-ansible` image for backward\ncompatibility with older version of the Ansible plugin;\n- ansible: when recording events make sure that \"unsafe strings\"\n(`AnsibleUnsafeText`) are truncated just like regular strings. This\nprevents large amount of data from getting into the event's payload;\n- ansible: fix `concord_data_secret` lookup when used with Python 3;\n- http-tasks: show `Authorization` header value in `debug` mode;\n- http-tasks: validate the `auth.basic.token` value;\n- concord-console: limit the number of rows returned when running\nJSON store queries;\n- concord-console: fixed the secret move popup's message;\n- concord-console: fixed vertical alignment in the repository events\ntable.\n\n\n\n## [1.43.0] - 2020-03-05\n\n### Added\n\n- concord-tasks: ability to pass `requirements` into the called\nprocess;\n- concord-server: `v2` syntax for generic and OneOps triggers;\n- concord-server, concord-console: ability to move project and\nsecrets across organizations;\n- smtp-tasks: disable debug output by default, add `debug` option;\n- concord-console: show the process status in the window's favicon.\n\n### Changed\n\n- concord-server: fixed the raw payload mode check. Now if the\nproject's raw payload mode is set to \"Only team members\", the server\ncorrectly looks for team members with `READER` access level or\nhigher;\n- http-tasks: honor the `ignoreErrors` value when handling\n\"unauhorized\" (401) responses;\n- ansible: the sensitive data filter is now opt-in by default and\ncan be enabled with the new `enableLogFiltering` parameter;\n- concord-server: fixed handling of GitHub `team` events. Now it\ncorrectly calculates the appropriate `githubRepo` values;\n- concord-server: if `startAt` value is in the past, log the current\nserver time in the error message;\n- concord-console: make `lastUpdatedAt` available as one of\nthe built-in columns again.\n\n\n\n## [1.42.0] - 2020-02-26\n\n### Added\n\n- concord-console: the repository edit page was redesigned to include\nthe new \"Events\" tab. Currently, the \"Events\" tab allows users to see\nincoming GitHub events which can be used to debug trigger conditions\nor monitor the repository traffic;\n- concord-server: new `saveAs` parameter in the process resume\nendpoint. Allows saving the received JSON body as the specified\nprocess `configuration` value;\n- misc-tasks: new `datetime.currentWithZone` methods to get\nthe current date/time for a specific time zone;\n- concord-sdk: new `Context#interpolate(Object, Map<String, Object>)`\nmethod to interpolate values using the specified `Map` as variables.\n\n### Changed\n\n- concord-server: process \"wait conditions\" (locks, waiting for other\nprocesses, etc) are now processed in batches;\n- concord-server: the process queue dispatcher now able to handle\nmultiple processes per projects at the time;\n- ansible, noderoster: trim the data (host names, host groups and\ntask names) before inserting it into the DB;\n- concord-console: the `lastUpdatedAt` column was removed from all\ndefault process lists. It still can be used in custom column\nconfigurations;\n- concord-tasks: fixed a bug causing duplicate entries in the `jobIds`\nvariable when multiple forks start;\n- concord-server: better validation of JSON Store query results.\nThe server expects single column results with valid JSON objects as\nvalues and will report so if the query results don't pass the\nvalidation;\n- smtp-tasks: better validation of input parameters;\n- concord-tasks: make sure the API response's body is closed, prevent\nleaks. \n\n\n\n## [1.41.0] - 2020-02-13\n\n### Added\n\n- kv-tasks: allow calling from `script` environment;\n- project-model: support for expressions in retry parameters;\n- concord-server: support for non-repository GitHub events (e.g.\n`team`, `organization`, etc); \n- concord-console: date/time filters for the audit log.\n\n### Changed\n\n- ansible: improved Python 3 compatibility;\n- concord-server: fixed the `repoId`/`repoName` filter in the\n`/api/v2/process` endpoint;\n- concord-server: fixed a bug in the process queue dispatcher when\na process with `requirements` that cannot be satisfied could block\nother processes in the same project from being dispatched to\nworkers;\n- slack-task: allow sending messages with JSON and support updates;\n- slack-task: make the `action` parameter of the `slackChannel`\ntask case-insensitive;\n- concord-task: multiple process forks are now started in parallel.\n\n\n\n## [1.40.0] - 2020-02-06\n\n### Added\n\n- concord-task: option to disable `debug` logging, including\nthe process arguments;\n- concord-server: support for filtering on repository ID or name in\n`/api/v2/process` and `/api/v2/process/count` endpoints;\n- concord-console: initial version of the Node Roster UI;\n- concord-console: new \"Audit Log\" tab on the organization,\nproject, team, secret and JSON store pages;\n- policies, concord-server: ability to set default configuration\nfor processes using the new `defaultProcessCfg` policy;\n- slack: new parameter `replyBroadcast`. If `true` a reply to a\nthread is also posted to the channel.\n\n### Changed\n\n- concord-console: support for calling processes with arguments\nusing manual triggers;\n- concord-server: improvements in audit logging of team changes.\nNow the audit events contain the change's delta;\n- concord-server: only JSON objects (Java Maps) are now allowed\nas JSON Store items.\n\n\n\n## [1.39.3] - 2020-01-29\n\n### Added\n\n- concord-agent: make the maintenance mode port configurable.\n\n### Changed\n\n- k8s: the example CRDs were updated to include the pod's name into\nthe agent's `capabilities`;\n- concord-server: the process queue dispatcher now sends responses\noutside of the global lock;\n- concord-console: fixed \"flickering\" when switching between\nplaybooks in the new Ansible UI;\n- ansible: callback plugins updated to support both Python 2 and 3;\n- concord-server: the `/api/v1/process/{id}/log` endpoint now\nperforms additional permissions check. Now only initiators, project\n`WRITERS`, admins and \"global readers\" can access process logs.\nDisabled by default.\n\n\n\n## [1.38.2] - 2020-01-23\n\n### Changed\n\n- concord-server: fixed negative maxSize in JSON store capacity\nresponse; \n- concord-server: fixed the existence check when creating or\nupdating an inventory.\n\n\n\n## [1.38.1] - 2020-01-22\n\n### Changed\n\n- concord-server: fixed a potential NPE when handling non-repository\nGitHub events (issues, teams, etc);\n- concord-server: revert changes in `/api/v1/org/${orgName}/inventory/${inventoryName}/data`\nendpoints.\n\n\n\n## [1.38.0] - 2020-01-21\n\n### Added\n\n- concord-server: configurable max request size for the embedded\nJetty server;\n- new JSON Store API, UI and a flow task;\n- concord-server: provide `event.commitId` variable for\n`pull_request` and `push` events in `github` triggers;\n- node-roster: initial version;\n- concord-server, concord-console: disable checkpoint restoration for\nsuspended processes;\n- concord-console: pagination to the process wait conditions page;\n- dependency-manager: support for authentication and release/snapshot\npolicies;\n- ansible: allow mix and match of inline inventories and file paths;\n- concord-console: customizable pages for external resources\npresented as iframes.\n\n### Changed\n\n- concord-server: the Inventory API is deprecated in favor of the\nJSON Store API;\n- concord-console: fixed pagination of the child process list;\n- concord-server: the system trigger (responsible for refreshing\nrepositories) now correctly triggers only on \"push\" events;\n- concord-server: when refreshing trigger definitions in the DB,\nthe server now correctly detects changes and updates/replaces only\nthe changed triggers;\n- concord-console: require an additional confirmation when removing a\nrepository;\n- concord-server: show the `available_worker` metric even if all\nworkers of a specific \"flavor\" are gone;\n- concord-server: escape expressions (`${}`) in external event data.\nAll string values in the `event` variable (which is provided for\nprocesses triggered by external events) will have their `${` escaped\nas `\\${`;\n- concord-cli: fix the build, the standalone JAR is now runnable\nagain;\n- ansible: fixed processing of events that are created by the task\ncalled in an expression.\n\n\n\n## [1.37.1] - 2020-01-06\n\n### Changed\n\n- variables-task: fixed an interpolation issue in the `set`\nimplementation. Now it correctly works for nested values that are\nreferencing variables from the \"outside\" scope.\n\n\n\n## [1.37.0] - 2020-01-02\n\n### Added\n\n- docker: option to use the container's user instead of forcing\nthe default Concord UID;\n- concord-console: project configuration editor;\n- slack: new action `addReaction`;\n- concord-server, plugins: new server plugin `kafka-event-sink`.\nSends process events into a Kafka topic;\n- concord-server-sdk: new interfaces `ProcessEventListener`,\n`AuditLogListener` and `ProcessLogListener`. Allows server plugins to\nlisten for process-level events, process logs and audit events.\n\n### Changed\n\n- slack: make `authToken` and `apiToken` parameters interchangeable;\n- kv-tasks: disallow `null` or empty keys;\n- concord-server: fixed an RBAC issue when loading extended process\nevent data. Now the `/api/v1/process/PROCESS_ID/event?includeAll=true`\ncorrectly checks for org/project permissions and ownership;\n- project-model-v1: merge `dependencies` lists from all loaded\nConcord YAML files;\n- docker: expose the host's `DOCKER_HOST` to the containers running\nin the `docker` task;\n- concord-console: fixed the dropdown behavior in the operation\nconfirmation popup;\n- variables-tasks: fixed an interpolation issue when multiple\ndependant values are `set` simultaneously;\n- http: fixed a potential NPE on empty responses;\n- concord-server, concord-console, ansible: hosts statuses are now\nlimited to `ok`, `failed` and `unreachable`. Fixes the host status\ncalculation and reduces the number of recorded host events;\n- concord-server, concord-console, ansible: fixed handling of retries\nin the new Ansible UI;\n- concord-server, concord-console: fixed the failed hosts/tasks\nrequest for processes with multiple playbook executions;\n- concord-server: fixed a division by zero error when calculating\nAnsible play progress;\n- ansible: correctly handle Jinja2 expressions in host groups when\nrecording events; \n- concord-server: sanitize `\\u0000` in strings when inserting JSONB\ndata (e.g. process events). \n\n### Breaking\n\n- concord-agent: support for the container-per-process execution mode\nis removed. It will be brought back in the future as a a separate\ntype of Agent;\n- concord-agent: `runner` configuration section is renamed to\n`runnerV1` to better support alternative runtimes.\n\n\n\n## [1.36.1] - 2019-12-11\n\n### Changed\n\n- ansible: improved handling of `currentRetryCount` attributes in\nplaybook events. This fixes an issue that was preventing Ansible\nevents from being correctly processed by the server.   \n\n\n\n## [1.36.0] - 2019-12-09\n\n### Added\n\n- concord-console: the about page now shows the date when the\nenvironment was last updated (optional, requires update of `cfg.js`);\n- concord-console: support for multiple profiles in the repository\nrun dialog;\n- server: optional caching of LDAP query results;\n- http: default `User-Agent` value, contains the version of\nthe plugin;\n- imports: ability to `exclude` files when importing external\nresources;\n- concord-console: additional process menu links can now be specified\nusing the `cfg.js` file;\n- ansible, concord-console: new view for Ansible runtime statistics;\n- concord-server: optional rate limit for the process start\nendpoints;\n- slack: new option to `ignoreErrors`;\n- concord-server: cache for policies;\n- concord-server, concord-console: the \"new project\" and the \"new\nsecret\" buttons can now be disabled on the organization level using\nan `entity` policy;\n- concord-server, concord-agent: configurable LRU cache for GIT\nrepositories;\n- concord-console: pagination for the project list;\n- docker, ansible: automatic retries when pulling images;\n- concord-server: configurable hard limit for the process log size;\n- ansible: new parameter `enablePolicy` to apply Concord policies to\nplays;\n- concord-server: new endpoints `/api/v1/agent/all/workers` and\n`/api/v1/agent/all/workersCount` to retrieve the list of currently\navailable agent workers and the number of available workers grouped\nby a property in agent `capabilities`;\n\n### Changed\n\n- concord-server, concord-console: when validating repositories\nreturn errors and warnings separately. Downgrade a missing\n`entryPoint` reference in triggers to a warning;\n- concord-console: project and secret visibility is now private by\ndefault;\n- concord-console: fixed missing support for `activeProfiles` for\nmanual triggers;\n- concord-console: updated look of the process status toolbar;\n- concord-server: enabled support for `onFailure`, `onCancel` and\nother process handlers for forks;\n- slack: better handling of invalid response codes, increase delay\nbetween retries;\n- concord-service: replace `WatchService` with simple polling to\nbetter support the reloading of the default process configuration in\nDocker environment;\n- runtime-v1: move the processing of `imports` to the `ProjectLoader`;\n- concord-console: better handling of undefined process metadata\nvalues;\n- runtime-v1: fixed an issue with nested `retry` blocks;\n- concord-server: handle processes stuck in the `PREPARING` status;\n- concord-server: fixed an issue preventing the `github` trigger's\n`useEventCommitId` from working correctly;\n- concord-server: upgraded to Ollie 0.0.33;\n- concord-server: additional permission checks when downloading\nprocess attachments. Now only process initiators, project owners or\nadmins can download attachments;\n- ansible: disable Concord policies by default;\n- dependency-manager: better handling of partially downloaded\nartifacts;\n- concord-console: show the last error icon on the process status page;\n- concord-server: forks now re-use the parent process' `imports`.\n\n### Breaking\n\n- concord-server: the `/api/v1/process/{id}/kv/{key}/string` endpoint\nnow returns `Content-Type: text/plain` instead of `application/json`.\nThis fixes an issue with non JSON strings;\n- concord-server: make the v2 the default version for `github`\ntriggers. The existing projects must update their `github` trigger\ndefinitions or set the default version in the server configuration\nfile;\n- concord-console: the image is removed. The Console files are\nnow served by the Server itself;\n- concord-runner: remove deprecated process definition attributes:\n    - `__attr_localPath`;\n- concord-sdk: remove deprecated interfaces and annotations:\n    - `com.walmartlabs.concord.common.InjectVariable`;\n    - `com.walmartlabs.concord.common.Task`.\n\n\n\n## [1.35.1] - 2019-10-17\n\n### Changed\n\n- concord-server: additional configuration settings for the handling\nof suspended processes;\n- concord-server: improve the asynchronous processing of external\nevents;\n- concord-server: fix a potential issue when GitHub triggers a\nprocess without a valid user account.\n\n\n\n## [1.35.0] - 2019-10-16\n\n### Added\n\n- concord-console: the repository links that end with `.git` are now\ncorrectly displayed on the process status page;\n- concord-server: new `agent-workers-available` metric, shows how\nmany Agent worker slots are available;\n- docker: support for capturing the command's `stderr` in addition to\n`stdout`;\n- http-tasks: support for `multipart/form-data` requests;\n- concord-agent: support for running processes with custom JVM\nparameters.\n\n### Changed\n\n- concord-runner: sort dependencies before loading to ensure\nconsistent class loading;\n- concord-server, concord-console: fill-in the process status page's\n\"Triggered By\" for processes triggered by `cron`;\n- concord-agent: fix system log messages that may appear out of order\nin the process log. \n\n\n\n## [1.34.3] - 2019-10-07\n\n### Changed\n\n- concord-server: ignore individual errors when refreshing multiple\nrepositories at once;\n- concord-server-db: add missing index on\n`PROCESS_CHECKPOINTS (INSTANCE_ID)`;\n- concord-server: some optimizations for the Ansible event\nprocessing;\n- concord-server: optimize `GET /api/v2/process` endpoint, equality\nfilter on metadata fields is now much faster;\n- concord-server: additional metrics for LDAP and Ansible event\nprocessing.\n\n\n\n## [1.34.2] - 2019-09-27\n\n### Changed\n\n- concord-runner: update BPM to 0.58.2, enables interpolation of\nexpressions in form field labels;\n- concord-server: fixed an issue preventing `useInitiator: false`\nfrom working correctly for GitHub triggers.\n\n\n\n## [1.34.1] - 2019-09-22\n\n### Changed\n\n- concord-server: fixed the system trigger definition. Now it\ncorrectly fires up on changes in all registered repositories.\n\n\n\n## [1.34.0] - 2019-09-19\n\n### Added\n\n- concord-server: new endpoint `/api/v2/process/count`. Returns\nthe number of processes for the specified filters;\n- dependency-manager: automatic retries, improved error reporting;\n- concord-server: the `/api/v2/process` endpoint now supports\ndifferent types of metadata filters (`eq`, `startsWith`, etc);\n- concord-server-sdk: now provides metrics annotations (e.g.\n`@WithTimer`).\n\n### Changed\n\n- concord-agent: dependency resolution logs are now correctly\nsent back to the server;\n- concord-server: fix `STARTING` statuses not being registered in\nthe process history; \n- concord-server: the `ping` endpoint now checks for the DB\nconnection;\n- concord-server: the repository refresh process is updated to use\nGitHub triggers v2.\n\n### Breaking\n\n- concord-server: the `/api/2/process` endpoint now requires a\nproject ID or a project name to be specified when metadata filters\nare used;\n- concord-server: the entity policy's attribute `trigger.name` is\nrenamed to `trigger.eventSource`.\n\n\n\n## [1.33.0] - 2019-09-07\n\n### Added\n\n- concord-runner: additional logging when the process heartbeat is\nrestored;\n- concord-server: configurable key size for generated key pairs;\n- concord-server: expose Jetty Sessions metrics;\n- concord-task: new method `getOutVars` to retrieve out variables of\nalready running or finished processes;\n- concord-task: support for `outVars` for `action: fork` when\n`suspend` is enabled;\n- concord-server: new `exclusive` syntax for triggers and regular\nprocesses.\n\n### Changed\n\n- concord-server: optimize the agent command dispatching;\n- concord-console: fixed overflowing in the Ansible event list popup;\n- concord-server: fixed an issue with incorrect process status\ntransition of forked processes;\n- concord-server: fixed an issue preventing the process wait\ncondition history from being correctly filled in.\n\n\n\n## [1.32.0] - 2019-09-04\n\n### Added\n\n- concord-server: the process resume event name is now exposed as\n`eventName` variable. It can be used to detect when the process is\nrestored from a checkpoint or resumed after suspension;\n- concord-server: regexes are now supported in the process'\n`requirements`;\n- concord-tasks: `suspend` support for `action: fork`;\n- concord-tasks: new method `suspendForCompletion` - suspends\nthe parent process until the specified processes are done;\n- http-tasks: option to disable automatic redirect follow with\n`followRedirects: false`;\n- concord-server, project-model: initial version of the new\nstreamlined GitHub trigger implementation (opt-in).\n\n### Changed\n\n- concord-console: fixed a date-formatting bug preventing `date` and\n`dateTime` process form fields from working correctly; \n- ansible: fixed an issue preventing the host status callback from\nworking correctly when the host is unreachable;\n- ansible: callback plugins now send a custom `User-Agent` header;\n- concord-server: optimize the process dispatching, perform filtering\non the server's side;\n- concord-server: parallel processing of external event triggers; \n- concord-console: hide form actions on the process status page if\nthe process is stopped;\n- concord-agent: move some of the system log messages to the `debug`\nlevel;\n- concord-server: additional logging when the process is enqueued.\n\n\n\n## [1.31.1] - 2019-08-27\n\n### Changed\n\n- concord-console: new icon for the `NEW` process status;\n- concord-console: fix the process status page refresh.\n\n\n\n## [1.31.0] - 2019-08-27\n\n### Added\n\n- concord-runner: additional diagnostic when the process state\ncontains non serializable variables;\n- concord-runner, concord-sdk: expose the `DependencyManager` to\nplugins. Allows plugins to download external artifacts using a\npersistent cache directory;\n- concord-console: add pagination and server-side filtering to\nthe organization list page;\n- concord-console: display trigger information on the process status\npage.\n\n### Changed\n\n- concord-server: allow overriding of `requirements` with `profiles`;\n- inventory: the `query` method now automatically retries in case of\nnetwork or intermittent backend errors; \n- concord-server: fixed an issue preventing processes that were\nscheduled for future (using `startAt`) from resuming correctly;\n- docker-tasks: now `stdout` output is captured without Docker system\nmessages (i.e. without the image download messages);\n- concord-console: relax validation rules for `git+ssh` repository\nURLs. Allows usage of non-GitHub GIT URLs;\n- concord-server: fixed an issue preventing the \"payload archive\"\nendpoint (the one that accepts ZIP archives as\n`application/octet-stream`) from working.\n\n\n\n## [1.30.0] - 2019-08-18\n\n### Added\n\n- concord-server: expose \"the number of enqueued processes that must\nbe executed now\" as a separate metric (i.e. without `startAt` or with\n`startAt` in the past);\n- concord-server: expose `repoBranch` and `repoPath` in\nthe `projectInfo` variable;\n- concord-console: show the process' timeout of the status page;\n- project-model: support for `retry` for flow `call` blocks.\n\n### Changed\n\n- http-tasks: skip certificate validation for all certificates (not only\nfor self-signed);\n- concord-server: process start requests are now handled\nasynchronously. The process queue entry is created as soon as\npossible with the `NEW` status and processes in a separate thread\npool;\n- ansible: fixed passing of `--ssh-extra-args` parameters;\n- concord-agent, queue-client: replace `maxWebSocketInactivity`\nparameter with `websocketPingInterval` and\n`websocketMaxNoActivityPeriod`. Use the latter to detect dead\nconnections.\n\n\n\n## [1.29.0] - 2019-08-06\n\n### Added\n\n- concord-server: process events now can be filtered by their\nsequence ID in the `/api/v1/process/{id}/events` endpoint.\n\n### Changed\n\n- concord-server: allow project `OWNERS` to download state archives\nof the project's processes;\n- concord-server: record `github` events before starting any\nprocesses;\n- http-tasks: use `UTF-8` for string and JSON requests by default;\n- concord-server, concord-ansible: use the event's timestamp instead\nof the DB's timestamp when recording events (if available);\n- concord-console: fix the project payload settings being applied\nwhen the settings page opens;\n- concord-console: use the same process toolbar on all tabs on the\nprocess page.\n\n\n\n## [1.28.1] - 2019-08-01\n\n### Changed\n\n- concord-server: use MDC to log process ID in the pipeline\nprocessors;\n- http-tasks: improved input parameter validation, additional validation\nfor JSON responses;\n- concord-console, concord-runner: fixed displayed duration for\nAnsible and process events;\n- concord-server: allow retrieval of public keys of project-scoped\nkey pairs;\n- ansible: improved calculation of host statuses during playbook\nexecution.\n\n### Breaking\n\n- concord-server: `sync=true` option is removed. Processes can no\nlonger be started in the \"synchronous\" mode, users should poll for\nthe process status updates instead.\n\n\n\n## [1.28.0] - 2019-07-27\n\n### Added\n\n- concord-server: save external `github` events into the audit log;\n- concord-server: save the process' trigger information in the\nprocess queue. The process endpoints now return the new `triggeredBy`\nfield;\n- concord-console: display the process' `startAt` on the process\nstatus page;\n- concord-server: support for multiple `user` entries in the form's\n`runAs` block;\n- sleep-tasks, concord-server: a way to suspend a process until the\nspecified date/time.\n\n### Changed\n\n- concord-server, concord-console: the `acceptsRawPayload` property\nin projects is replaced with `rawPayloadMode`. The old property is\ndeprecated. New projects are created with `rawPayloadMode: DISABLED`\nby default;\n- concord-tasks: fixed an issue when a subprocess is started using an\nAPI key specified by the user with `suspend: true`;\n- ansible: the `limits` parameter now accepts list and array values;\n- concord-server: some optimizations for the process event processing\n(including Ansible). Reduces the contention on the process queue\ntable;\n- ansible: fixed an issue with events not being sent to the server in\nsome cases;\n- concord-server: make organization names optional when using secrets\nin `imports`;\n- concord-console: fixed an issue with the profile selection in\nthe manual trigger popup;\n- concord-console: hide system files in the process attachments list;\n- concord-server: fixed an issue preventing `imports` from working\ncorrectly in `onFailure` and other handlers;\n- concord-console: add process tags to the process status page;\n- concord-server: process `tags` can now be specified using a\ncomma-separated startup argument, e.g. `curl -F tags=x,y,z`;\n- concord-server: store repository info (commit ID, commit message,\netc) early in the pipeline to preserve the data in case or process\nstartup errors (e.g. bad syntax).\n\n\n\n## [1.27.0] - 2019-07-20\n\n### Added\n\n- http-tasks: support for `application/x-www-form-urlencoded`\n(`request: form` type);\n- concord-server, concord-console: pagination for the secret list;\n- concord-server: support for \"exclusive\" triggers and \"exclusive\"\nexecution groups;\n- concord-console: a form to change the secret's project.\n\n### Changed\n\n- bpm: updated to `0.58.1`, resolves an issue with incorrect\n`context#getCurrentFlowName()` value in some cases;\n- project-model: fixed a bug preventing `withItems` and `retry` from\nworking correctly when used together;\n- concord-server: process statup errors now correctly shown when\nthe \"browser link\" is used;\n- concord-server, concord-console: fix the Ansible event status\ncalculation;\n- ansible: fixed an issue with using arrays as the `tags` parameter\nvalues.\n\n\n\n## [1.26.0] - 2019-07-10\n\n### Added\n\n- concord-console: add the UI's version to the About page.\n\n### Changed\n\n- concord-console: fixed the issue with duplicate results in\nthe \"find user\" field;\n- concord-server: fixed potential NPE when searching users in\nAD/LDAP (e.g. by using the Console's \"find user\" field);\n- concord-console: fixed the bug preventing the clear button on the\nprocess list's filter popup from working.\n\n### Breaking\n\n- concord-server: the inventory subsystem now only accepts JSON\nobjects as top-level entries;\n- concord-server: the \"Landing Page\" support is removed.\n\n\n\n## [1.25.0] - 2019-07-02\n\n### Added\n\n- concord-server: support for triggers in `entity` policies.\n\n### Changed\n\n- concord-server: GitHub trigger's `useInitiator` is now correctly\nruns the process using the commit user's security context. This fixes\nthe issue with child processes not inheriting the initiator;\n- concord-server: fixed the handling of `queue.concurrent` policies:\nenqueued processes now track each running process instead of a single\none;\n- concord-console: fixed the issue when a repository refresh error\npersists even after the refresh dialog is closed.\n\n\n\n## [1.24.2] - 2019-06-27\n\n### Changed\n\n- concord-server: use GitHub event's `ldap_dn` to determine the\nevent's initiator.\n\n\n\n## [1.24.1] - 2019-06-27\n\n### Changed\n\n- concord-server: fixed the login when format of usernames\nprovided by users didn't match the data returned by the AD/LDAP\nserver;\n- concord-server: fixed the initial loading of default process\nconfiguration.\n\n\n\n## [1.24.0] - 2019-06-25\n\n### Added\n\n- concord-server: automatic reload of `defaultConfiguration` file\nwithout restart;\n- http-tasks: support for query parameters;\n- concord-server: option to disable automatic user creation for\nthe LDAP realm;\n- concord-console: display error details for `FAILED` processes;\n- concord-server, concord-console: support for AD/LDAP domains,\ncustom username validators;\n- concord-server: API endpoints for role and permission management;\n- concord-console: add \"copy to clipboard\" buttons to entity IDs;\n- concord-server, concord-console: initial implementation of \"manual\"\ntriggers;\n- project-model: `error` blocks support for `script` steps.\n\n### Changed\n\n- http-tasks: make `method: GET` default;\n- concord-server: drop the `process_checkpoint` to `process_queue`\nFK, use a cleaning job instead. Enabled usage of partitioning for\nthe `process_checkpoints` table;\n- docker-tasks: remove dependency on `io.takari.bpm/bpm-engine-api`;\n- concord-runner, docker-images: use `file.encoding=UTF-8` by\ndefault. Fixes the issue with Unicode passwords;\n- concord-server: correctly pass the parent's `requirements` when\nforking a process.\n\n\n\n## [1.23.0] - 2019-06-17\n\n### Added\n\n- smtp: support for attachments.\n\n### Changed\n\n- concord-server: fix symlink handling when importing the process\nstate;\n- concord-tasks: fix the kill action. Now it is correctly accepts\nthe `instanceId` parameter.\n\n\n\n## [1.22.0] - 2019-06-16\n\n### Added\n\n- concord-cli: initial release;\n- project-model: support for configurable resource paths such as\n`./profiles`, `./flows`, etc;\n- project-model: new trigger type - `manual`. Can be used to\nconfigure process entry point available through the UI;\n- ansible: initial support for installing additional pip packages\nusing Python's virtualenv;\n- ansible: support for multiple vault IDs/passwords;\n- concord-runner: configurable time interval without a heartbeat\nbefore the process fails;\n- concord-runner: the current flow name is now available via\n`${context.getCurrentFlowName()}` method;\n- concord-server: return a list of form `fields` in the generated\n`data.js` for custom forms. The list is in the original order of the\nform definition.\n\n### Changed\n\n- concord-server: fixed a potential NPE when handling process\nmetadata;\n- ansible: keep both the head and the tail when trimming long string\nvalues in events;\n- project-model: `withItems` now supports iteration over Java Map\nelements;\n- docker-tasks: make `cmd` optional;\n- concord-server, concord-agents: external `imports` are now\nprocessed without copying into the process state;\n- ansible: send process events asynchronously;\n- concord-client, concord-runner: use a custom `User-Agent` header;\n- concord-server: fixed a NPE when handling optional `file` form\nfields.\n\n### Breaking\n\n- project-model: external `imports` are now a top-level element.\n\n\n\n## [1.21.0] - 2019-05-23\n\n### Added\n\n- concord-server: additional metrics for the process key cache;\n- concord-server: configurable delay when polling for agent commands;\n- concord-console: show the \"so far\" duration of active statuses on\nthe process status history page;\n- concord-runner: new options to enable or disable recording of task\n`in` and `out` variables. Including the option to blacklist specific\nvariable names;\n- concord-console: display task call details in the process log\nviewer;\n- project-model: support for expressions in form calls.\n\n## Changed\n\n- docker: the task can now be called using the regular `task` syntax\n(in addition to the previous `docker` form);\n- concord-server: keep the original values of `readonly` form fields\non submit;\n- concord-server: throttle the AD/LDAP user group caching;\n- concord-console: fixed the log toolbar's flickering issue.\n\n\n\n## [1.20.1] - 2019-05-16\n\n### Changed\n\n- concord-server: fix the `imports` processing - `configuration`\nobjects from the imported resources are now loaded correctly;\n- concord-server: another fix for the issue with symlinks in GIT\nrepositories.\n\n\n\n## [1.20.0] - 2019-05-16\n\n### Added\n\n- http-tasks: new `debug` parameter. Enables additional logging of\nrequest and response data;\n- concord-tasks: log the job's URLs when starting new processes;\n- concord-console: the process log viewer's options are now persisted\nusing the browser's Local Storage.\n\n### Changed\n\n- project-model: fixed an issue with nested objects used in\n`withItems`;\n- concord-server: fixed an issue with circular symlinks in GIT\nrepositories;\n- concord-console: fixed an issue preventing the UI from working\ncorrectly in MS Edge (missing `URLSearchParams` polyfill);\n- concord-agent: ignore subsequent attempts to enable the maintenance\nmode;\n- concord-server: `SUSPENDED` processes are now ignored when\ncalculating the concurrent processes limit;\n- concord-console: fixed an issue preventing checkpoints from being\nrendered correctly in some cases.\n\n\n\n## [1.19.1] - 2019-05-12\n\n### Changed\n\n- concord-console: better handling of log tag parsing errors;\n- concord-runner, concord-console: use a less common log tag name;\n- k8s/agent-operator: simultaneously handle pool size and\nconfiguration changes.\n\n\n\n## [1.19.0] - 2019-05-12\n\n### Added\n\n- concord-console: option to split process logs by task calls;\n- concord-tasks: support for file attachments when starting new\nprocesses;\n- concord-sdk, crypto-tasks: expose `encryptString` method to flows;\n- slack: support for creating and replying to threads;\n- concord-agent, concord-runner: support for a configurable list of\nvolumes to mount into Docker containers created by plugins.\n- concord-tasks: support for file attachments when starting a process\n\n### Changed\n\n- concord-tasks: improved validation of input parameters;\n- ansible: fixed handling of `auth` parameters. The deprecated `user`\nand `privateKey` parameters are working now again;\n- concord-console: the URL parsing in the log viewer is updated to\nbetter handle URLs in quotes.\n\n\n\n## [1.18.1] - 2019-05-07\n\n### Changed\n\n- concord-server: fixed the handling of processes with wait\nconditions.\n\n\n\n## [1.18.0] - 2019-05-05\n\n### Added\n\n- ansible: support for multiple `inventory` and `inventoryFile`\nentries;\n- log-tasks: `logDebug`, `logWarn` and `logError` tasks;\n- concord-server: initial support for RBAC permissions;\n- ansible: initial Kerberos authentication support.\n\n### Changed\n\n- project: initial support for server-side plugins. Ansible-related\nendpoints are moved into `server/plugins/ansible`;\n- concord-console: stop the form wizard when the user navigates away;\n- docker: include the Taurus PIP module in the default Ansible\nimage;\n- concord-server: improve error messages in case of authentication\nfailure due to an invalid input or an internal error.\n\n\n\n## [1.17.0] - 2019-04-24\n\n### Added\n\n- docker: install Ansible's `k8s` dependencies;\n- k8s/agent-operator: initial version;\n- concord-server: return `requirements` when fetching a list of\nprocesses using `/api/v2/process`;\n- concord-server: SSO support for custom forms;\n- concord-console: make the log viewer URLs clicable;\n- ansible: support for non-root paths when fetching external roles;\n- http-tasks: new parameter `requestTimeout`.\n\n### Changed\n\n- http-tasks: make `response` parameter optional;\n- concord-server: fixed the SSO https->http redirect;\n- concord-console: performance optimizations for the log viewer;\n- concord-runner: allow 2 letter checkpoint names.\n\n\n\n## [1.16.1] - 2019-04-18\n\n### Changed\n\n- concord-console: use the configured project metadata to render the\ncheckpoints page;\n- concord-runner: allow whitespaces in checkpoint names.\n\n\n\n## [1.16.0] - 2019-04-17\n\n### Added\n\n- concord-server: new configuration parameter\n`ldap.excludeAttributes` - provides a way to exclude specific\nLDAP attributes from being returned in the user's `attributes`;\n- concord-server, concord-console: JWT-based SSO service support;\n- ansible: existing JSON and YAML extra vars files can now be used\nwith the new `extraVarsFiles` parameter;\n- resource-tasks: new method `prettyPrintJson` - returns formatted\nJSON as a string;\n- concord-server, concord-console: ability to disable a process to\nprevent restoring it from a checkpoint after completion.\n\n### Changed\n\n- concord-server: filter out all non string LDAP attributes;\n- concord-server: support for multivalue LDAP attributes when\nfetching user details from AD/LDAP;\n- concord-console: fixed the page limit dropdown on the checkpoint\nview page;\n- concord-common: do not escape backslashes when creating a ZIP\nachive;\n- project-model, concord-runner: support expressions in checkpoint\nnames.\n\n\n\n## [1.15.0] - 2019-04-03\n\n### Added\n\n- concord-agent, concord-runner: support for the configurable\n`logLevel`.\n\n### Changed\n\n- concord-server: fix the `${initiator}` and `${currentUser}` data\nfetching when dealing with multiple account types.\n\n\n\n## [1.14.0] - 2019-03-31\n\n### Added\n\n- policy-engine, concord-server: support for entity owner-based\npolicies;\n- concord-tasks: new action `startExternal`. Can be used to start a\nnew process on an external Concord instance.\n\n### Changed\n\n- concord-client, concord-tasks: fixed a bug preventing `baseUrl`\nand `apiKey` parameters from being correctly applied;\n- http-tasks: correctly handle empty (204) responses;\n- concord-server: fixed a potential NPE when setting a new owner for\nprojects without owner.\n\n\n\n## [1.13.1] - 2019-03-27\n\n### Changed\n\n- http-task: make `ignoreErrors` work with connection timeouts;\n- concord-server: fixed an authentication issue with passwords that\nend with a `:`;\n\n\n\n## [1.13.0] - 2019-03-26\n\n### Added\n\n- concord-sdk: additional `MapUtils` methods to retrieve Map, List\nand Integer values;\n- concord-console: show user display names on the team member list\npage;\n- concord-server: new user attribute - `displayName`. Automatically\nstored for AD/LDAP users. For local users it can be set using\nthe API;\n- concord-server, concord-console: project owners can now be updated\nusing the API or the Console;\n- concord-server, consord-console: organization owners can now be\nset using the API or the Console;\n- concord-server, concord-runner, concord-tasks: (optionally) suspend\nthe parent process while waiting for a child process (only for\n`start`);\n- project-model: support for arrays in `withItems`;\n- concord-server: an API method to remove an organization\n(admin only).\n\n### Changed\n\n- concord-agent: fix JVM \"pre-forking\". Now the process poll is\ncorrectly shared between all workers;\n- concord-server: more detailed error messages in case of invalid\nencrypted strings;\n- concord-console: fixed a bug when team members could not be\ndeleted.\n\n\n\n## [1.12.0] - 2019-03-13\n\n### Added\n\n- policy-engine, concord-server: support for organization-level max\nconcurrent process limits;\n- concord-console: process metadata filtering for the checkpoint list\npage;\n- concord-server, concord-console: teams can now be associated with\nLDAP groups;\n- concord-server: user accounts can now be disabled via an API call;\n- concord-server: new automatically-provided process\nvariable - `requestInfo`. Contains the request's parameters, headers\nand the user's IP address.\n\n### Changed\n\n- concord-agent: fixed the issue preventing the process startup\nerrors from being logged correctly;\n- concord-console: fixed a login issue with non-ASCII usernames or\npasswords.\n\n\n\n## [1.11.0] - 2019-03-04\n\n### Added\n\n- concord-server, ansible: track `retry` count per host;\n- concord-agent: failover support for the websocket connections; \n- ansible: print a \"No changes will be made\" warning if `check` or\n`syntaxCheck` modes are used.\n\n### Changed\n\n- concord-console: fixed the handling of processes with checkpoints\nbut without history;\n- concord-server: when a repository refresh fails show the error\ncause instead of a wrapped exception;\n- project-model: fixed the behaviour of nested and/or sequential task\ncalls with `retry`;\n- ansible: the task now correcly records both pre- and post-action\nevents;\n- concord-console: fixed the log timestamp pattern, now Ansible log\ntimestamps are correctly converted into the local time;\n- ansible: `{%raw%}` strings are working again.\n\n\n\n## [1.10.0] - 2019-02-27\n\n### Added\n\n- concord-server: support for GitHub webhooks limited to a specific\nrepository;\n- forms: support for `date` and `dateTime` field types;\n- concord-server, concord-console: ability to temporarily disable\nrepositories.\n\n### Changed\n\n- concord-server: return `400 Bad Request` when trying to\n`decryptString` in a process without the project;\n- bpm: updated to `0.54.0` to support expressions in `script` names.\n\n\n\n## [1.9.0] - 2019-02-20\n\n### Added\n\n- concord-server: option to sign the `initiator` and `currentUser`\nusernames. Signatures can be validated using the configured public\nkey.\n\n### Changed\n\n- concord-agent: when resolving dependencies, use `latest` as the\nindicator of an automatically selected version;\n- concord-server: remove keywhiz support. The Concord-Keywhiz\nintegration could be implemented as a separate plugin;\n- concord-server: removed support for archiving process state and\ncheckpoints into an S3 endpoint. The recommended way is to use table\npartitioning or a custom (external) data retention solution;\n- concord-console: fixed the process list filtering.\n\n\n\n## [1.8.1] - 2019-02-18\n\n### Changed\n\n- concord-agent: fix the dependency resolution for plugins with\nnon-default versions;\n\n\n\n## [1.8.0] - 2019-02-17\n\n### Added\n\n- concord-server: option to retrieve a single item in an inventory;\n- concord-sdk: initial implementation of `LockService`; \n- concord-server: support for `activeProfiles` in `cron` triggers;\n- concord-server: new endpoint `GET /api/v2/process`. Returns a list\nof processes with optional filtering (including metadata) and\npagination;\n- concord-sdk: new utility methods to work with the process\nvariables;\n- concord-server: initial support for in-process locks.\n\n### Changed\n\n- concord-server: force logout users on any authentication error.\nFixes the issue with \"remember me\" users with passwords changed\nbetween the server's restarts;\n- repository: clean up and reset locally cached repositories on\ncheckout;\n- concord-server: skip invalid host names when processing Ansible\nevents;\n- concord-runner: more graceful handling of errors while saving \"out\"\nvariables;\n- concord-server: check for permissions when retrieving process\ndetails via `GET /api/v2/process/{id}`.\n- concord-server, concord-agent: load the dependency version list\nfrom the server.\n\n\n\n## [1.7.2] - 2019-02-11\n\n### Changed\n\n- concord-server: forks should keep the original `_main.json` minus\nthe `arguments`. This fixes the issue with forks and\n`onCancel/onFailure` handlers which are using external dependencies;\n- concord-console: fixed the row selection bug in the process list\ncomponent.\n\n\n\n## [1.7.1] - 2019-02-09\n\n### Changed\n\n- concord-server: fix authorization of `cron` triggers.\n\n\n\n## [1.7.0] - 2019-02-07\n\n### Added\n\n- throw-tasks: now capable of throwing exceptions with custom\npayload: maps, lists, etc;\n- concord-console: new \"Wait Conditions\" tab on the process status\npage;\n- concord-server: new process configuration option `exclusiveExec`:\nrestricts the process execution to one process at the time per\nproject;\n- http-tasks: proxy support via `proxy` parameter;\n- concord-server: option to restrict the external events endpoint\n`/api/v1/events/{eventName}` to users with specific roles.\n\n### Changed\n\n- ansible: Concord polices now receive interpolated variable values;\n- concord-console: display empty checkpoint group for suspended\nprocesses;\n- concord-runner: save and restore the last known variables for\nforked processes. This allows forks and onCancel/onError/etc\nhandlers to access the parent process' variables;\n- concord-server: use roles instead of user flags. E.g.\n`concordAdmin` role instead of `USERS.IS_ADMIN`.\n\n\n\n## [1.6.0] - 2019-01-31\n\n### Added\n\n- concord-console: new flow selection dropdown in the process start\npopup.\n\n### Changed\n\n- concord-server: do not copy the parent process' forms when forking\na process;\n- repository: fix the GIT clone bug for repositories without a\n`master` branch;\n- concord-console: fix the checkpoint grouping issue, preventing\ncheckpoints from being correctly rendered;\n- project-model: improve stacktraces in case of YAML parsing errors;\n- concord-runner: fix the timestamp format in the `processLog`\nlogger. Plugins such as Ansible should use the correct timestamp\nformat now.\n\n\n\n## [1.5.0] - 2019-01-30\n\n### Added\n\n- concord-server: new form attribute `submittedBy` - automatically\ncreated for each form after it submitted, contains the form user\ninformation. Can be enabled with `saveSubmittedBy` form call option;\n- concord-console: add option to convert log timestamps into local\ntime;\n- concord-server, concord-runner: use UTC in log timestamps;\n- concord-agent: additional logging while downloading the process'\nrepository data and state;\n- concord-server, concord-console: \"Remember Me\" cookie support;\n- concord-console: list of checkpoints on the process status page;\n- concord-runner: new method `context#form`, allows dynamic creation\nof forms in tasks, scripts and expressions;\n- concord-console: profile selection when starting a process;\n- concord-tasks: new task `concordSecrets`.\n\n### Changed\n\n- concord-server, concord-agent: disable `git.httpLowSpeedLimit` as\nit was causing major performance issues when cloning large\nrepositories;\n- concord-server: reduce the default max session age to 30 minutes.\n\n\n\n## [1.4.0] - 2019-01-23\n\n### Added\n\n- concord-server: return the build's commit id in the server version\nresponse;\n- vars plugin: `get` method can now return nested variables or\nfallback to a specified value;\n- concord-server: new endpoint `/api/v2/process/{id}`, allows\ncustomization of the data included in the response;\n- concord-server: new API endpoint `/api/v2/process/{id}/checkpoint`,\nallows restoring checkpoints with a `GET` request;\n- concord-console: bring back the status column to the child process\nlist;\n\n### Changed\n\n- concord-server, project-model: allow expressions in the form's `runAs`\nparameter;\n- concord-server: close websocket channels when the maintenance mode\nis enabled;\n- ansible: fixed an issue that caused the sensitive data masking\nplugin (`concord_protectdata.py`) to fail on non-ASCII strings.\n\n\n\n## [1.3.0] - 2019-01-15\n\n### Added\n\n- concord-console: list of attachments added to the process status\npage;\n- slack: when sending a message, the task now returns a `result`\nobject.\n\n### Changed\n\n- concord-console: flow events moved to a separate tab on the process\nstatus page;\n- concord-server: copy the parent process' repository info to forked\nprocesses. This fixes an issue preventing process forks from working\ncorrectly.\n\n\n\n## [1.2.2] - 2019-01-14\n\n### Changed\n\n- concord-server: store custom form files in the process state\nregardless of whether they are form a repository or not. This fixes\nan issue preventing custom forms from working correctly.\n\n\n\n## [1.2.1] - 2019-01-13\n\n### Changed\n\n- concord-agent, concord-runner: log additional performance metrics\nwhen running in `debug` mode;\n- dependency-manager: pre-sort dependency URIs to ensure stable\ndependency resolution order. This improves chances of getting a\npre-forked JVM instead of spinning a new one;\n- concord-console: fix potential data race when loading process\ncheckpoints;\n- concord-server: last modified date of process state files is now\ncorrectly calculated when importing the state.\n\n\n\n## [1.2.0] - 2019-01-09\n\n### Added\n\n- http-tasks: support for `PATCH` method.\n\n### Changed\n\n- concord-server: fixed a bug preventing the Ansible events from\nbeing processed correctly;\n- concord-console: fixed the parsing of GIT URLs on the repository\nlist page;\n- concord-server: fixed an issue preventing git submodules using the\ndefault (token-based) auth from working;\n- concord-server: reject flow attachments if \"Allow payload archives\"\nis disabled in the project's settings;\n- concord-console: disable \"New Project\" button if the user is not a\nmember of the organization.\n\n\n\n## [1.1.0] - 2019-01-04\n\n### Added\n\n- concord-console: new \"Checkpoint View\" for projects;\n- concord-console: allow addition of new elements for `type*` and\n`type+` form fields.\n\n### Changed\n\n- concord-console: the process start popup now correctly displays the\nbranch of the selected repository;\n- concord-server: allow project-scoped secrets to be used when\ncloning the project's repositories;\n- concord-agent: improved error logging when cloning repositories;\n- concord-runner: fixed an issue preventing the runner from\nterminating correctly on `java.lang.Error`;\n- concord-server: set the default session timeout to 10 hrs (was\nunlimited);\n- concord-server: accept GitHub events without branch information\n(e.g. archive events).\n\n\n\n## [1.0.0] - 2018-12-26\n\n### Added\n\n- concord-server, concord-console: custom process list filters based\non process metadata;\n- concord-server: new `/api/v2/trigger` endpoint. Currently only the\nlist method is provided;\n- concord-console: externalize extra links in the system menu, allow\nfor environment-specific overrides;\n- concord-console: ability to specify the entry point when starting\na process;\n- ansible: new `syntaxCheck` option to run\n`ansible-playbook --syntax-check`;\n- ansible: new `check` option to run `ansible-playbook --check`.\n\n### Changed\n\n- concord-console: fixed an issue with error messages persisting\nafter navigating out of the page;\n- concord-server: fixed the logic of the Ansible event processor. Now\nit should correctly handle very long-running processes;\n- concord-console: fixed a bug preventing the Ansible host filter\nfrom working correctly;\n- concord-runner: fix the handling of process arguments when\nrestoring a process from a checkpoint;\n- concord-server, concord-agent: perform `git clone` on the Agent,\nkeep only the changes in the DB;\n- bpm: updated to 0.51.0, fixed the resolution order of tasks and\nvariables. Now flow variables can shadow the registered tasks;\n- concord-console: prevent table overflow on process detail table;\n- concord-console: move the Ansible stats table above the flow event\ntable;\n- concord-server: fixed the host status calculation in the Ansible\nevent processor. Now `SKIPPED` is correctly overwritten by other\nstatuses in multi-step plays.\n\n\n\n## [0.99.1] - 2018-12-06\n\n### Changed\n\n- ansible: use `/tmp/${USER}/ansible` as the default `remote_tmp`;\n- concord-server: fixed a bug in `/api/service/process_portal`\npreventing the endpoint from working.\n\n\n\n## [0.99.0] - 2018-12-05\n\n### Added\n\n- project-model: support for programmatically-defined form fields;\n- concord-server: new `useEventCommitId` parameter of `github`\ntriggers;\n- concord-server: `repoBranchOrTag` and `repoCommitId` parameters to\nstart a process with the override of the configured\nbranch/tag/commitId values;\n- concord-console: pagination for the Ansible host list on the\nprocess status page;\n- http-tasks: new parameter `ignoreErrors` - instead of throwing exceptions\non unauthorized requests, the task returns the result object with the\nerror;\n- slack: new `slackChannel` task for creating and archiving channels\nand groups;\n- concord-server: a metric gauge for the number of currently\nconnected websocket clients;\n- misc-tasks: new `datetime` task;\n- concord-server: pagination support for the child process list page;\n- concord-server: support for policy inheritance;\n- concord-server: `offset` and `limit` to the process checkpoint list\nendpoint;\n- concord-server: support for exposing form and nested values as\nprocess metadata;\n- concord-server: support for default metadata values.\n\n### Changed\n\n- docker: set the minimal Ansible version to 2.6.10;\n- concord-project-model: forbid empty flow and form definitions;\n- concord-server: use a single local clone per GIT URL;\n- concord-server: fixed an issue, causing `onFailure` to fire up\nmultiple times in clustered environments.\n- concord-server: `cron` triggers are now using the DB's time to\ncalculate the schedule;\n- concord-console: improved repository validation error messages;\n- concord-console: dropdown menus with optional values now correctly\nrender the empty \"value\";\n- concord-console: fixed a bug preventing the child process list from\nworking correctly;\n- concord-server, concord-console: Ansible events are now\npre-processed and stored on the backend, making the Process Status\npage more responsive when working with large Ansible plays.\n- concord-server: evaluate parsed expression value in custom form field's\n`allow` attribute\n- concord-server: change the (potential) partitioning key in\n`PROCESS_EVENTS` from `EVENT_DATE` to `INSTANCE_CREATED_AT`.\n\n\n\n## [0.98.1] - 2018-11-23\n\n### Changed\n\n- concord-queue-client: fixed potential OOM when handling connection\nerrors.\n\n\n\n## [0.98.0] - 2018-11-18\n\n### Added\n\n- concord-server: add `meta` to the process checkpoint list endpoint.\n- concord-console: pagination support for the process list.\n\n### Changed\n\n- concord-console: fixed a session handling bug. Now the session is\ncorrectly restored on UI reload. \n\n\n## [0.97.0] - 2018-11-13\n\n### Added\n\n- http-task: `connectTimeout` and `socketTimeout` parameters;\n- concord-server: GitHub triggers can now use `payload` field with\nthe original event's data;\n- concord-server: new API endpoint to retrieve a list of processes\nincluding their status history and checkpoints;\n- concord-console: a warning if a password stored as a secret is too\nweak;\n- concord-agent: an endpoint to get the current status of the\nmaintenance mode.\n\n### Changed\n\n- concord-server: removed GitHub webhook registration and repository\ncache;\n- concord-server: fixed a bug preventing relative file upload paths\nfrom working with `/api/v1/process` endpoint;\n- concord-server: the session cookie (`JSESSIONID`) is now marked as\n`HttpOnly`;\n- concord-common: ensure that `CONCORD_TMP_DIR` environment variable\nis defined;\n- concord-server: fixed a bug causing incorrect matching of\nConcord repositories with incoming GitHub events.\n\n\n\n## [0.96.0] - 2018-11-07\n\n### Added\n\n- concord-console: new tab on the process status page - \"Child\nProcesses\";\n- project-model, concord-server: support for `readonly`,\n`placeholder` and `search` form field attributes;\n- project-model: `withItems` now correctly handles `out` variables of\ntasks;\n- concord-server: support for GitHub events other than `push` or\nPR-related events; \n- concord-server: GitHub webhook support for unknown (not registered\nin Concord) repositories.\n\n### Changed\n\n- concord-server: fixed a bug preventing process checkpoints from\nbeing correctly archived;\n- docker: updated Ansible to 2.6.7.\n\n### Breaking\n\n- concord-tasks: IN parameter `jobs` renamed to `forks` to avoid\ncollision with OUT parameter `jobs`.\n\n\n\n## [0.95.0] - 2018-11-03\n\n### Added\n\n- concord-server, concord-runner: store `lastError` in `out`\nvariables in the process' metadata.\n- concord-server: `afterCreatedAt` parameter to the process list API\nendpoint.\n\n### Changed\n\n- concord-server: allow admins to access any form;\n- project-model: checkpoint names must be unique across all loaded\nflow definitions;\n- project-model: fixed a bug preventing nested `withItems` from\nworking correctly;\n- bpm: updated to 0.49.0. Now all context types implement `eval` and\n`interpolate` methods.\n\n### Breaking\n\n- concord-client: `ProcessApi#metadata` renamed to `#updateMetadata`.\n\n\n\n## [0.94.0] - 2018-10-29\n\n### Added\n\n- concord-server: optional \"default filter\" for all GitHub triggers;\n- concord-server: make optional the unknown GitHub webhook removal.\n\n### Changed\n\n- concord-runner: fixed a bug preventing dynamic task registration\nfrom working correctly.\n\n\n\n## [0.93.1] - 2018-10-28\n\n### Changed\n\n- concord-server: fixed a bug preventing the checkpoint archiver\nfrom working correctly.\n\n\n\n## [0.93.0] - 2018-10-28\n\n### Added\n\n- concord-server: add `payload` to `github` trigger events; \n- concord-server: GitHub webhook URLs can now supply additional\nparameters via query parameters;\n- concord-server: configurable period values for cleanup tasks;\n- concord-sdk: support for \"protected\" variables that can be set only\nby allowed tasks;\n- ansible, policy-engine: support for restricting of allowed URLs in\n`maven_artifact`, `uri` and `docker_container`;\n- policy-engine: support for value expressions;\n- concord-console: new process history tab on the status page.\n\n### Changed\n\n- concord-server: allow GitHub events without explicit webhook\nregistration (e.g. organization-level hooks);\n- concord-console: the process filtering list is performed on the\nserver now;\n- concord-server: `task_locks` are replaced with the task schedule\ntable;\n- concord-server: merge the existing process variables with template\nvariables;\n- bpm: updated to 0.48.0, fixed `context#getVariableNames` issue.\n\n### Breaking\n\n- concord-server: `github.enabled` configuration parameters renamed\nto `github.webhookRegistrationEnabled`;\n\n\n\n## [0.92.0] - 2018-10-21\n\n### Added\n\n- concord-server: optional rate limit for process start requests;\n- concord-server: ability to assign a policy to a user;\n- ansible: additional validation for `groupVars`.\n\n### Changed\n\n- concord-server: return `429` then requests are rate-limited or\nrejected by the queue policy;\n- concord-server: fixed an issue preventing organization data from\nbeing correctly updated via REST;\n- project-model: using `withItems` with `null` now skips the block's\nexecution instead of throwing an error.  \n\n\n\n## [0.91.0] - 2018-10-14\n\n### Added\n\n- concord-server: support for `timezone` in `cron` triggers;\n- concord-console: ability to cancel multiple processes;\n- concord-server: the secret decryption error now contains the\nsecret's name;\n- concord-server, concord-console: refresh GitHub webhooks when a\nrepository is refreshed;\n- concord-server: timeout options for GIT's HTTPS and SSH transports; \n- concord-server: a policy rule for setting the maximum allowed\n`processTimeout` value.\n\n### Changed\n\n- concord-server: the \"default variables\" file replaced with the\n\"default configuration\" file. Instead of containing the `arguments`\nsection, it now contains the while `configuration` object.\n\n### Breaking\n\n- concord-server: `process.defaultVariables` configuration file\nparameter renamed to `process.defaultConfiguration`;\n- concord-sdk: `Constants.Request.USE_INITIATOR` renamed to\n`Constants.Trigger.USE_INITIATOR`.\n\n\n\n## [0.90.0] - 2018-10-09\n\n### Added\n\n- concord-server: assign an save a unique \"request ID\" to link audit\nlogs and the process queue data; \n- concord-server: ability to restrict the max number of forks using\npolicies;\n- concord-server: ability to restrict the max number of processes in\nthe queue for a given status using policies;\n- docker: `envFile` parameter to define environment variables using a\nfile;\n- concord-console: show repository names in the project process list;\n- concord-server: ability to overwrite process configuration using\npolicies.\n\n### Changed\n\n- concord-server: fixed an issue preventing process timeouts from\ncorrectly working with multiple running processes; \n- concord-tasks: override the API endpoint with `baseUrl`. \n\n\n\n## [0.89.3] - 2018-10-02\n\n### Changed\n\n- concord-console: fixed an issue preventing the process start\nredirect from working.\n\n\n\n## [0.89.2] - 2018-10-01\n\n### Changed\n\n- concord-server: fixed variabled of the `spec` field in `cron`\ntriggers; \n- concord-server: fixed an issue causing `cron` triggers to use the\ndefault flow.\n\n\n\n## [0.89.1] - 2018-09-30\n\n### Changed\n\n- ansible: fix a bug preventing the task callback from recording task\nstart events.\n\n\n\n## [0.89.0] - 2018-09-30\n\n### Added\n\n- concord-server: user-defined process timeouts support;\n- concord-server: \"initiator passthrough\" support for OneOps and\nGitHub events;\n- concord-server: added support for GitHub PR events;\n- concord-agent, concord-runner: initial support for \"container per\nprocess\" execution model.\n\n### Changed\n\n- ansible: use ANSI colors by default;\n- concord-server: fixed a bug preventing checkpoints from working\nwith `.concord.yml` files.\n\n\n\n## [0.88.0] - 2018-09-19\n\n### Added\n\n- dependency-manager: load plugin versions from a file. Allows\nomitting the version qualifier for the registered plugins.\n\n### Changed\n\n- concord-server: fixed a bug preventing repositories from being\nautomatically refreshed on GitHub push events;\n- concord-console: fixed project process list filtering.\n\n\n\n## [0.87.0] - 2018-09-16\n\n### Added\n\n- concord-console: prevent loading of too much data on the process\nstatus page, show a warning instead;\n- concord-server: support for email form fields\n(`inputType: \"email\"`);\n- concord-server: expose Jetty statistics;\n- concord-console: add filtering to the organization list;\n- concord-console: UI for managing access to projects and secrets;\n- concord-server, concord-console: initial support for process-,\norganization- and project-level metadata.\n\n### Changed\n\n- concord-server: fixed a bug preventing GIT repositories with large\nnumber of tags from working;\n- concord-server: apply RBAC filters when listing secrets.\n\n\n\n## [0.86.0] - 2018-09-05\n\n### Added\n\n- concord-server: new API method to list process checkpoints;\n- concord-agent: option to ignore SSL certificate errors for API\ncalls;\n- concord-console: add filtering to the secret list and the team list\npages;\n- concord-server, concord-console: option to use a service account to\nretrieve GIT repositories instead of user keys;\n- concord-server: store project keys (used in `encrypt/decryptString`\nmethods) in the DB. \n\n### Changed\n\n- concord-console: fix handling of process statup errors;  \n- concord-server: in LDAP auth try `userPrincipalName` first;\n- concord-server: require `READER` access level to refresh triggers\n(instead of `WRITER`);\n- concord-server: process-level variables are now correctly override\nthe system-wide defaults;\n- concord-server: fixed audit logging for AD/LDAP authentication;\n- concord-server: improved audit logging for projects and\nrepositories;\n- concord-server: store `initiatorId` instead of `initiator` username\nin the process queue table;\n- project-model: fix handling of `null` values in `set` step.\n\n\n\n## [0.85.0] - 2018-08-15\n\n### Added\n\n- concord-server: additional metrics, process queue gauges;\n- resource-tasks: new method to read YAML files. Optional support for\nstring interpolation in JSON and YAML files;\n- ansible: initial support for external roles.\n\n### Changed\n\n- runner: upgrade the BPM engine version to `0.47.1` to fix a bug\npreventing correct handling of IN variable evaluation errors.\n\n\n\n## [0.84.1] - 2018-08-12\n\n### Changed\n\n- concord-server: fix missing `@Named` annotations for JOOQ\nconfiguration. Causes the wrong datasource to be injected.\n\n\n\n## [0.84.0] - 2018-08-12\n\n### Added\n\n- concord-server: `${processInfo.activeProfiles}` variable;\n- concord-runner: new utility task `forms` to create links to process\nforms and the form wizard;\n- concord-server, concord-agent, project-model, concord-runner:\ninitial support for process checkpoints;\n- concord-server, policy-engine: support for \"max concurrent\nprocesses\" policy.\n\n### Changed\n\n- docker: increased the default API proxy timeout to 180 seconds;\n- concord-console: fix the custom view link on the process form page;\n- concord-console: fixed retieval of \"Last 10 processes\" on the\nActivity page;\n- concord-server: fixed a bug causing `onFailure` handler processes\nto fail due to missing session keys and `projectInfo` variables.\n\n### Breaking\n\n- concord-server: form API endpoints now accept form names instead of\nIDs. E.g. `/api/v1/process/PROCESS_ID/form/FORM_NAME`.\n\n\n\n## [0.83.1] - 2018-08-06\n\n### Changed\n\n- concord-server: revert inventory RBAC changes, allow publicly\nwritable inventories;\n- concord-console: fix the process status filter on the project\nprocesses page;\n- concord-console: limit `Activity` data to the current user's\norganizations.\n\n\n\n## [0.83.0] - 2018-08-05\n\n### Added\n\n- concord-server: more metrics;\n- concord-console: new process list filters - `Status` and\n`Initiator`;\n- concord-console: new default page `Activity`;\n- concord-console: allow updating of project visibility and\ndescription;\n- concord-server: improved support for multi-select fields in custom\nforms;\n- ansible: option to save Ansible host statistics as a flow variable;\n- http-tasks: support for `DELETE` method;\n- project-model: support for `activeProfiles` in trigger definitions;\n- concord-server: option to disable triggers (specific or all);\n- concord-server: the Prometheus metrics endpoint now proxied by the\nconsole's nginx.\n\n### Changed\n\n- concord-server: fix permissions check when killing a process;\n- concord-console: fix duplicate form events;\n- concord-console: replace automatic redirect to a custom form with a\nlink;\n- concord-console: escape HTML in process logs.\n\n\n\n## [0.82.0] - 2018-07-29\n\n### Added\n\n- concord-server: additional metrics (JVM, memory, JDBC, etc).\n\n\n\n## [0.81.0] - 2018-07-24\n\n### Added\n\n- concord-server: delay before processes are moved into the archive; \n- concord-server: support for custom form validation messages;\n- concord-server: option to disable GIT's \"shallow clone\";\n- concord-server: initial integration with Prometheus;\n- concord-server: option to disable the process state archiving task.\n\n### Changed\n\n- concord-server: make the process state archiving task to pick up\nnext items as soon it's done with the current work.\n\n\n\n## [0.80.0] - 2018-07-22\n\n### Added\n\n- project-model: support for multiple Concord project files in\n`concord/*.yml`;\n- concord-server: support for archiving of process state into\nS3-compatible object stores;\n- concord-server: optionally encrypt sensitive data in the process\nstate;\n- project-model: allow expressions to be used in flow calls;\n- plugins: new built-in task `throw`;\n- concord-console: ability to filter Ansible hosts by their inventory\ngroup;\n- concord-server: RBAC checks for process kill and status update\noperations.\n\n### Changed\n\n- concord-server: return `404` when the secret doesn't exist;\n- concord-server: fix handling of empty inventory query params.\n- concord-server: fixed the repository webhook registration when\nproject or repository is created or updated.\n\n\n\n## [0.79.2] - 2018-07-16\n\n### Changed\n\n- concord-agent: fixed the configuration variable names;\n- concord-agent: increase default timeout values;\n- concord-server: correctly handle table aliases in inventory\nqueries;\n- concord-server: escape special characters in commit messages\n`processInfo.repoCommitMessage`.\n\n\n\n## [0.79.1] - 2018-07-16\n\n### Changed\n\n- concord-server: fixed an issue preventing\n`initiator.attributes.mail` from populating.\n\n\n\n## [0.79.0] - 2018-07-15\n\n### Added\n\n- project-model: additional YAML validations to prevent duplicate\nkeys such as `configuration` or `arguments`;\n- concord-server: an option to perform DB migrations separately;\n- concord-server: periodic audit log clean up;\n- concord-server: option to disable audit logging;\n- concord-server: added limits for \"encrypted strings\" size;\n- concord-server: improved error reporting for non-process related\nerrors;\n- concord-server, concord-console: repository metadata to processes;\n- concord-server: improved inventory query validation;\n- concord-console: display flow and ansible event duration;\n- ansible: record \"pre\" and \"post\" task events separately;\n- concord-console: new API key management UI;\n- concord-server: optional API key expiration;\n- concord-server: methods to list existing inventories and inventory\nqueries;\n- concord-server, concord-console: new method to validate a repo\nwithout starting it;\n- concord-console: new form to encrypt values to use with\n`crypto.decryptString`;\n- concord-server: all secured endpoints will now return appropriate\nauth challenge headers;\n- concord-console: scroll back to top button on the process log page;\n- concord-console: highlighting of errors and warning messages in\nprocess logs;\n\n### Changed\n\n- docker-images: increased nginx's default max request size to 32Mb;\n- concord-server: a single invalid trigger no longer prevents other\ntriggers from firing;\n- concord-agent: now uses the same REST API endpoint to download\nprocess state snapshots as regular users. \n\n\n\n## [0.78.1] - 2018-07-08\n\n### Changed\n\n- concord-server: fixed a bug that prevented the custom forms API\nendpoint from registering correctly.\n\n\n\n## [0.78.0] - 2018-07-08\n\n### Added\n\n- concord-project-model: improved validation of `withItems` values;\n- concord-console: autoscrolling for process logs;\n- concord-console: ability to rename secrets and update their\nvisibility;\n- concord-console: the list of registered triggers for a repository.\n\n# Changed\n\n- concord-server: configuration migrated into a single configuration\nfile.\n- concord-runner: full stacktraces will be printed out for unhandled\nexceptions.\n\n\n\n## [0.77.0] - 2018-07-01\n\n### Added\n\n- concord-server: project-scoped secrets;\n- http-tasks: support for custom headers.\n\n### Changed\n\n- concord-client: better handling of server-side validation errors;\n- concord-server: merge `api` and `impl` modules;\n- concord-server: GitHub triggers will now correctly use the repo's\norganization and project names to match the events;\n- concord-server: now process state snapshots can only be accessed by\nadmins, process owners and \"global readers\".\n\n### Breaking\n\n- concord-server: removed the old project API endpoint\n`/api/v1/project`.\n\n\n\n## [0.76.0] - 2018-06-24\n\n### Added\n\n- concord-server: multiple ldap groups support in forms\n\n### Changed\n\n- concord-server: fixed the repository webhook registration when an\nindividual repository is created or updated;\n- concord-console: project, team members and ansible host filters are\nnow using simple substring search instead of regular expressions;\n- ansible: fixed a bug than prevented `outVars` from working for any\nvariables other than the first in the list.\n\n\n\n## [0.75.0] - 2018-06-17\n\n### Added\n\n- policy-engine: optional rule violation messages;\n- concord-console: the repository start popup now has a link to the\nprocess log if the process failed to start;\n- concord-console: the profile page and the API key form;\n- ansible: new task parameter `outVars` to save specific Ansible\nvariables as Concord flow variables;\n- concord-agent: a local interface to enable maintenance mode;\n- concord-server: check template output for cycles to avoid\nserialization issues;\n- concord-server: size limit for binary data secrets.\n\n### Changed\n\n- concord-console: clean up the error message when users try to\naccess a form without a matching LDAP group;\n- policy-engine: fix calculation of workspace sizes; \n- concord-console: fixed the server error details parsing;\n- concord-rpc: module removed. \n\n\n\n## [0.74.0] - 2018-06-10\n\n### Added\n\n- concord-server: initial support for roles;\n- concord-console: a field to filter the project list by name;\n- concord-project-model: support for `withItems`. Allows iterating\nover a list of elements;\n- concord-server: support for `Bearer <token>` authorization;\n- concord-console: the process page's organization and project links;\n- concord-server: optional git repository check for `concord.yml`\nbeing present.\n\n\n\n## [0.73.0] - 2018-06-04\n\n### Added\n\n- concord-server: new `update` method for secrets. It allows changing\nof secret names and/or visibility;\n- concord-agent: retries for errors during download of process\npayloads;\n- concord-agent: `AGENT_ID` and `USER_AGENT` parameters to support\npersistent agent IDs.\n\n### Changed\n\n- concord-server: fixed the secret access level endpoint not\naccepting team names;\n- concord-server: fixed \"test repository connection\" method.\n\n\n\n## [0.72.0] - 2018-06-03\n\n### Added\n\n- concord-console: add `System` menu with `Documentation` and `About`\nlinks;\n- concord-agent: new `MAX_PREFORK_COUNT` configuration parameter to\nlimit the number of processes in the pool;\n- ansible: new parameter `disableConcordCallbacks` to disable\nConcord-specific Ansible callbacks: stdout filtering, event\nrecording, etc;\n- ansible, concord-console: handle Ansible's `ignore_errors` modifier;\n- ansible: ability to use password-less secrets in the secret\nlookup plugin;\n- concord-client, concord-agent, concord-runner: enable session\ncookies;\n- concord-task: fail the parent process if the subprocess has failed.\nAdded new parameter `ignoreFailures: true` to revert the previous\nbehavior.\n\n### Changed\n\n- concord-project-model: external form definitions must work with and\nwithout a whitespace in the definition.\n\n\n\n## [0.71.3] - 2018-05-31\n\n### Changed\n\n- concord-agent: fixed orphaned docker sweeper bug which caused live\ncontainers to be terminated. \n\n\n\n## [0.71.2] - 2018-05-30\n\n### Changed\n\n- guava downgraded from 21.0 to 20.0 to avoid classpath issues with\nsome of the plugins (e.g. jira).\n\n \n\n## [0.71.1] - 2018-05-29\n\n### Changed\n\n- concord-server: fix form wizard redirect;\n- concord-server: improve `decryptString` error messages;\n- concord-runner: fix export of secrets w/o password;\n- concord-tasks: now uses `concord-client` instead of Resteasy.\n\n\n\n## [0.71.0] - 2018-05-21\n\n### Added\n\n- ansible: limit the saved stdout/stderr size;\n- concord-server: log the agent's IP address when a process starts;\n- concord-server: a method to list all inventory items;\n- concord-server: added optional `replace` query parameter to the\nteam users update operation.\n- concord-console: team management UI.\n\n### Changed\n\n- concord-server-client: renamed to `concord-client`;\n- concord-server: reduced the amount of information on the \"wait\npage\" in the process API endpoint for browsers;\n- concord-server: fixed team management RBAC;\n- concord-console: fixed dropdowns not re-rendering after update;\n- concord-server: fixed incorrect filtering of inventory data.\n\n\n\n## [0.70.0] - 2018-05-17\n\n### Added\n\n- concord-server, concord-agent: initial support for environmental\n`requirements` and agent capabilities.\n\n### Changed\n\n- concord-runner, concord-server: improved error handling when\nworking with secrets;\n- concord-client: renamed to `concord-tasks`;\n- concord-rpc: the KV store, heartbeat and secret gRPC services\nreplaced with the REST API based services.\n\n\n\n## [0.69.0] - 2018-05-09\n\n### Added\n\n- concord-agent: \"maintenance mode\" to suspend job acquisition;\n- resource-tasks: new methods to read/write JSON;\n- concord-server: an \"in progress\" page when the process is started\nusing \"browser\" endpoints;\n- concord-console: a button to download raw process log. \n\n### Changed\n\n- concord-console: new UI layout.\n\n\n\n## [0.68.1] - 2018-05-09\n\n### Changed\n\n- concord-server: fixed the repository connection test method not\ngetting secrets from the UI.\n\n\n\n## [0.68.0] - 2018-05-04\n\n### Added\n\n- http-tasks: support for `PUT` requests;\n- concord-server: new LDAP configuration property `usernameProperty`.\nDefaults to `sAMAccountName`.\nSTRDTORC-507\n\n\n## [0.67.0] - 2018-04-29\n\n### Added\n\n- concord-server-db: `bigserial` columns for forwarding log data;\n- concord-runner: record task `in` parameters in process events;\n- concord-runner, ansible: `correlationId` for task events;\n- concord-server: `runAs` form option.\n\n\n\n## [0.66.0] - 2018-04-22\n\n### Added\n\n- concord-client: support for `activeProfiles`;\n- docker: ability to save `stdout` as a variable;\n- concord-project-model: `retry` support for tasks;\n- smtp: add support for multiple values in `to`, `cc` and `bcc`\nparameters;\n- concord-server: method to cancel a process including its\nsubprocesses;\n- concord-server: method to delete an existing team.\n\n### Changed\n\n- concord-server: skip invalid definitions on trigger activation.  \n\n\n\n## [0.65.3] - 2018-04-16\n\n### Added\n\n- concord-server: additional logging for authentication realms. \n\n### Changed\n\n- concord-server: fixed session key conflicts when sub processes\nare used;\n- concord-console: load correct project processes;\n- concord-server: fixed `platform` filter for OneOps triggers.\n\n\n\n## [0.65.2] - 2018-04-15\n\n### Changed\n\n- docker: fix nginx logging configuration. \n\n\n\n## [0.65.1] - 2018-04-15\n\n### Added\n\n- concord-server: support for `cron` triggers;\n- concord-project-model: better handling of YAML parsing errors;\n- concord-server: process endpoints now return `childrenIds` - array\nof child process IDs.\n\n### Changed\n\n- concord-server: fixed an issue when updating team role for an\nexisting team member.\n\n\n\n## [0.64.0] - 2018-04-11\n\n### Added\n\n- concord-sdk: `Context#suspend` method for suspending processes with\nprogrammatically-defined callback events;\n- concord-server: metadata for organizations;\n- concord-server: initial audit logging implementation;\n- keywhiz: initial support;\n- concord-server: support for symlinks on initial process state\ningestion;\n- ansible: support for exporting secrets as `group_var` files;\n- concord-console: show Ansible host events in a modal popup.\n- ansible: new lookup plugins 'concord_data_secret' and 'concord_public_key_secret' \n\n### Changed\n\n- ansible: deprecate `configuration.ansible`, `inventory` and\n`dynamicInventory` request parameters;\n- http-tasks: parse JSON responses;\n- http-tasks: use `${response}` as the default out variable.\n\n\n\n## [0.63.0] - 2018-04-01\n\n### Added\n\n- concord-console: new UI layout;\n- concord-console: visualization of Ansible stats;\n- concord-server: make `orgName` optional when `teamName` is used\nwhen setting a resource's access level.\n\n### Changed\n\n- ansible: make `privateKey`'s password optional;\n- concord-agent: ship `http-tasks` with the docker image; \n- concord-console: disable TLS 1.0.\n\n\n\n## [0.62.1] - 2018-03-26\n\n### Changed\n\n- concord-server: send empty JSON if a project's cfg is empty;\n- concord-server: use basic auth for the interactive process\nendpoints.\n\n\n\n## [0.61.0] - 2018-03-25\n\n### Added\n\n- concord-server, concord-agent, concord-runner: initial support for\nprocess policies;\n- concord-project-model: `exit` step to terminate execution of\nthe flow w/o throwing an error;\n- concord-client: support for the new `startAt` parameter.\n\n### Changed\n\n- concord-server: OWNER access level is now required to delete a\nproject.\n\n\n\n## [0.60.0] - 2018-03-22\n\n### Added\n\n- concord-server: a method to update access levels of inventories;\n- concord-server: `startAt` process parameter to schedule process\nexecutions;\n- concord-agent: a cleanup job to remove old Docker images (keeps two\nlatest versions).\n- concord-server: an endpoint to download a single state file.\n\n### Changed\n\n- concord-server: fixed an issue, preventing file upload fields from\nworking with custom forms.\n\n\n\n## [0.59.0] - 2018-03-11\n\n### Added\n\n- concord-server: the process event endpoint now accepts `limit` and\n`after` query parameters.\n\n### Changed\n\n- concord-server: allow removal of secrets that are in use;\n- concord-server: trim usernames on login.\n- ansible: the task now prepends user-provided `callback_plugins` and\n`lookup_plugins` values with the default values. \n\n\n\n## [0.58.0] - 2018-02-25\n\n### Added\n\n- new task: `http`. Provides a simple HTTP client with JSON support.\n\n### Changed\n\n- ansible: fixed exporting of key pairs;\n- concord-server: fixed handling of secret requests w/o an\norganization;\n- concord-server: fixed retrieval of team users.\n\n\n\n## [0.57.0] - 2018-02-22\n\n### Added\n\n- slack: support for the `task` syntax, includes new messaging\noptions such as `icon_emoji` and `attachments`;\n- ansible: filter for removing sensitive data from the logs;\n- concord-client: `concord` task's `repo` is an alias for\n`repository` now;\n- concord-client: `project` task now works with organizations other\nthan `Default`;\n- concord-client: `concord` task now accepts `payload` parameter\ninstead of `archive`, which can be either a path to a ZIP archive or\na path to a directory;\n- concord-server: additional logging in case of process statup\nerrors.\n\n### Changed\n\n- concord-server, concord-console: fix handling of multi-select\ndropdowns;\n\n\n\n## [0.56.0] - 2018-02-14\n\n### Added\n\n- concord-server: new user's `type` attribute (`LOCAL` or `LDAP`);\n- crypto: ability to export secrets from organizations other than\nthe current.\n\n### Changed\n\n- concord-docker: fixed the `unable to find user concord: no matching\nentries in passwd file` issue.\n\n\n\n## [0.55.0] - 2018-02-13\n\n### Added\n\n- concord-agent: the new `debug` configuration parameter to log the\nresolved dependencies of a process.\n\n### Changed\n \n- concord-server: fixed incorrect inventory query filtering;\n- concord-client: use polling while waiting for the process to end;\n- ansible: updated to 2.4.3;\n- concord-client: `org` parameter was ignored;\n- concord-server: configurable max state age;\n- ansible, docker: use a non-root user to run all Docker processes\n(including `ansible-playbook`);\n- docker: run Docker containers in the host's network.\n\n### Breaking\n\n- concord-server: the trigger endpoint address is made to conform\npath patterns of the rest of the organization endpoints.\n\n\n\n## [0.54.0] - 2018-02-01\n\n### Added\n\n- ansible, docker: support for `--add-host` in the Ansible and Docker\ntasks.\n\n### Changed\n\n- docker: automatically update `pip` to the latest version;\n- docker: more Walmart-specific CA certificates;\n- concord-server: when a new team is created, automatically add the\ncurrent user as the team's `MAINTAINER`;\n- concord-server: apply RBAC to the process state download endpoint.\n\n\n\n## [0.53.0] - 2018-01-28\n\n### Added\n\n- concord-server: support for nested paths when retrieving\nattachments;\n- docker: Walmart images are now include Walmart's Root CA SSL\ncertificates.\n\n### Changed\n\n- concord-console: embed the Semantic UI resources;\n- concord-server: JGit is replaced with the GIT CLI tool, improving\nthe support for submodules and large repositories.\n\n\n\n## [0.52.0] - 2018-01-23\n\n### Changed\n\n- concord-server: avoid creation of multiple webhooks for the same\nGIT repository urls registered in different projects;\n- docker: add non-root users for the server and agent containers;\n- dependency-manager: ignore checksums, cache the intermediate data.\n\n\n\n## [0.51.0] - 2018-01-17\n\n### Added\n\n- inventory: the ansible wrapper now able to produce inventories with\nper-host variables;\n- crypto: a method to export a single value secret as a file;\n- concord-tasks: make the organization name parameter optional for\nthe inventory task;\n- concord-server: an endpoint to export binary data secrets;\n- ansible: add a lookup plugin for retrieving \"single value\" secrets;\n- ansible: make the org parameter optional for the inventory lookup\nplugin. \n\n### Changed\n\n- concord-server: form `values` are now correctly added to the\nprocess context after the form is submitted submit;\n- ansible: updated to 2.4.2;\n- ansible: `inventoryFile` and `extraEnv` were ignored when the\nplugin was invoked using the task syntax;\n- concord-server: the \"Accept payload archives\" flag now correctly\nupdates;\n- concord-server: fixed the issue preventing the process from being\nmarked as FAILED on YML syntax errors;\n- concord-server: fixed parsing of the `activeProfiles` property when\nthe multipart process endpoint is used.\n\n\n\n## [0.50.0] - 2018-01-10\n\n### Added\n\n- concord-server: limit the maximum allowed size of process files;\n- concord-server: configurable DB connection pool size.\n\n### Changed\n\n- concord-console: fixed the issue with new projects overwriting the\npreviously opened ones. \n\n\n\n## [0.49.0] - 2018-01-07\n\n### Added\n\n- concord-server: allow hyphens and tildes in entity names;\n- concord-server, concord-console: initial support for file upload\nfields.\n\n### Changed\n\n- concord-console: the repository refresh button now opens a pop-up\nwindow.\n\n\n\n## [0.48.2] - 2017-12-28\n\n### Changed\n\n- concord-server: fixed key names in the GitHub configuration file. \n\n\n\n## [0.48.1] - 2017-12-27\n\n### Added\n\n- concord-console: the button to manually refresh a project's\nrepository cache.\n\n### Changed\n\n- concord-server: skip the repository cache if the repository's\nwebhook is not registered (yet);\n- concord-server: fixed an bug preventing startup errors from\nbeing logged in process logs. \n\n\n\n## [0.48.0] - 2017-12-17\n\n### Added\n\n- concord-agent: display the list of dependencies when a process\nstarts;\n- concord-console: new \"visibility\" field on the process and the\nsecret forms;\n- ansible: `skipTags` support;\n- concord-server, concord-console: filter process queue by user\norganizations;\n- concord-console: add \"organization\" field to the process page;\n- concord-server: new provided variables in `projectInfo`:\n`repoCommitId`, `repoCommitAuthor` and `repoCommitMessage`;\n- concord-console: host the swagger-ui app;\n- concord-server: triggers, inventories and landing pages are moved\ninto organizations;\n- concord-server, concord-console: an option to disallow raw payload\narchives for projects;\n- concord-server, concord-console: initial support for Organizations;\n- concord-server: public and private projects and secrets;\n- concord-server: project and secret now has owners;\n- concord-server: new endpoint for managing secrets;\n- concord-client: the `project` task - provides a method to create\nnew projects using flows;\n- concord-server: methods to refresh triggers and LPs for all\nprojects.\n\n### Changed\n\n- concord-client: switched to resteasy-based client;\n- concord-runner: improved stability by using a separate classloader\nto load tasks;\n- concord-server: user permissions are effectively replaced with the\nTeam RBAC feature;\n- concord-console: reworked the form for creating secrets;\n- concord-console: rename \"Kill\" button on the process page to\n\"Cancel\".\n\n\n\n## [0.47.0] - 2017-11-15\n\n### Added\n\n- concord-project-model: `debug` option for the `docker` step;\n- docker: the task now automatically overrides the container's\n`entrypoint`;\n- concord-client: new module;\n- ansible: `ansible` alias for the `ansible2` task; \n- concord-server: new automatically-provided\nvariable - `projectInfo`;\n- concord-runner: `script` task now supports URLs;\n- concord-server: initial support for process triggers.\n\n\n\n## [0.46.0] - 2017-11-04\n\n### Added\n\n- kv: `getLong` and `putLong` methods;\n- concord-console: \"download state\" button to the process status\npage;  \n- concord-agent, concord-server: detect orphaned or stalled\nprocesses;\n- concord-server, concord-console: process landing pages;\n- concord-server: show an error page when a \"portal\" process fails;\n- concord-project-model: alias `::` to `try:`;\n- concord-console: ability to start a new process from the project's\nform;\n- ansible: support for saving and using Ansible's \"limit\" files;\n- concord-server, concord-console: support for boolean form fields;\n- ansible: Inventory lookup plugin.\n\n### Changed\n\n- concord-server: user can now create API keys only for themselves;\n- concord-server: fix empty secret name in project repository\nentries returned by the API;\n- concord-server: added environment variable to override the\nserver's password.\n\n\n\n## [0.45.1] - 2017-10-30\n\n### Added\n\n- concord-console: \"terminated ssl\" option.\n\n\n\n## [0.45.0] - 2017-10-20\n\n### Added\n\n- concord-server: log the repository data when a process starts;\n- concord-server: Inventory API initial support;\n- concord-console: storybook integration;\n- concord-server: retrieve user's LDAP info when API key\nauthentication is used;\n- concord-server: the API keys endpoint now accepts LDAP usernames\nas well as user UUIDs;\n- concord-server, concord-runner: support for process OUT\nvariables;\n- concord-server: log process status updates;\n- concord-server: initial support for Teams;\n- concord-server: an endpoint to retrieve a list of attachments.\n\n### Breaking\n\n- concord-dependency-manager: remove support for `includeOptional`.\n\n### Changed\n\n- concord-dependency-manager: batch resolution of Maven dependencies.\n\n\n\n## [0.44.0] - 2017-10-12\n\n### Added\n\n- concord-runner: Slack task can now be called using the full form;\n- concord-server: automatically remove orphaned data.\n\n### Changed\n\n- concord-rpc: fix dispatching of agent commands when the server is\nrestarted;\n- ansible: throw an exception if a private key file was not found.\n\n### Breaking\n\n- concord-server: `/api/v1/process/{id}/subprocesses` changed to\n`/api/v1/process/{id}/subprocess`.\n\n\n\n## [0.43.0] - 2017-10-05\n\n### Added\n\n- ansible: ability to specify a secret name and password as a\n`privateFile` value;\n- concord-console: a form to create secrets;\n- concord-server: validate uploaded SSH key pairs;\n- concord-server, concord-agent: support for pulling dependencies\nfrom Maven repositories;\n- concord-server: update repositories using GitHub webhooks;\n- concord-agent: automatic cleanup of orphaned Docker containers;\n- ansible: support for additional environment variables.\n\n### Changed\n\n- concord-server: fixed the issue with incorrect credentials\nconfiguration when retrieving GIT submodules.\n\n\n\n## [0.42.0] - 2017-10-01\n\n### Added\n\n- concord-sdk: new provided variable `parentInstanceId`;\n- concord-server: ability to suppress the execution of `onCancel` or\n`onFailure` flows;\n- concord-server: new API method to fork a process as its subprocess;\n- concord-server: support for GIT submodules;\n- concord-server, concord-console: process tags.\n\n\n\n## [0.40.2] - 2017-09-24\n\n### Added\n\n- concord-server: if entry point is not set, use `default`;\n- concord-project-model: alias `variables` to `configuration`;\n- concord-server: pagination support for the process queue list;\n- concord-project-model: support for `switch`;\n- ansible: initial support for Ansible event streaming.\n\n### Changed\n\n- concord-server: fix Jolokia JMX names;\n- concord-runner: fix `InjectVariable` for `JavaDelegate`-style\ntasks;\n- concord-runner: fix `JavaDelegate` handling;\n- concord-server: fix SSH key pair upload/create endpoint;\n- concord-server: process events cleanup;\n- concord-server: fixed a potential NPE while retrieving the process\nqueue data.\n\n\n\n## [0.39.0] - 2017-09-17\n\n### Added\n\n- concord-server, concord-runner: support for `onFailure`, `onCancel`\nflows;\n- concord-server, concord-runner: provide a way to access\npassword-protected secrets from flows;\n- concord-server: allow starting a process with a POST request using\na project, a repository and an entry point specified in\n`.concord.yml` file;\n- concord-server: allow starting a process by sending an empty POST\nrequest;\n- concord-server, concord-console: support for `yield` for\nnon-custom forms;\n- ansible: support for external private key files;\n- ansible: support for external configuration files;\n- ansible: verbosity level can now be set using the task's arguments.\n\n### Changed\n\n- concord-server: include GIT repository name into the cache key;\n- concord-agent: remove the working directory after the process\nfinishes;\n- concord-runner: refresh the value of `${__attr_localPath}` after\nresuming a process;\n- concord-server: fixed variable merging when `activeProfiles` is\nan empty array or `null`.\n\n\n\n## [0.38.3] - 2017-09-10\n\n### Changed\n\n- concord-runner: upgrade to the BPM engine 0.38.2. This fixes\nanother variable interpolation issue.\n\n\n\n## [0.38.0] - 2017-09-07\n\n### Added\n\n- concord-server, concord-console: support for GIT repository paths.\n\n### Changed\n\n- concord-server: fix key pair generation for non-API key users.\n\n\n\n## [0.37.0] - 2017-09-04\n\n### Changed\n\n- concord-agent: normalize dependency URLs, support for Nexus/WARM\nredirects;\n- concord-runner: fix the issue with tasks based on `concord-sdk`;\n- concord-project-model: fix serialization issues when `set` task\nused with nested structures.\n\n\n\n## [0.36.0] - 2017-08-23\n\n### Added\n\n- concord-server: REST API method for exporting process state;\n- concord-sdk: support for full-form tasks;\n- concord-agent: log local IPs before starting a process.\n\n### Changed\n\n- concord-server: fix evaluation of variables when one variable\nreferences another;\n- concord-server: accept any type of attachment as a file, except\n`text/plain`. This fixes the issue with the multipart requests with\nincorrect `Content-Type`.\n\n\n\n## [0.35.0] - 2017-08-19\n\n### Added\n\n- concord-sdk: new module;\n- concord-runner: upgrade the BPM engine version to 0.34. This add\nthe support recursive value interpolation in `variables` or `in`\nblocks;\n- concord-agent, concord-runner: keep a pool of JVM instances\ninstead of starting a new one for each process.\n\n## Changed\n\n- concord-server: preserve user input in `data.js` after a failed\nvalidation.\n\n\n\n## [0.34.1] - 2017-08-14\n\n### Changed\n\n- concord-server: fixed an issue with custom forms and SSL.\n\n\n\n## [0.34.0] - 2017-08-13\n\n### Added\n\n- concord-server: support for `shared` folders for custom forms;\n- concord-server: process files should overwrite template files;\n- ansible: static and dynamic inventory files can now be specified\nusing `inventoryFile` and `dynamicInventoryFile` task parameters.\n\n\n\n## [0.33.0] - 2017-08-10\n\n### Added\n\n- concord-server: add Jolokia agent;\n\n### Changed\n\n- concord-runner: use copyAllCallActivityOutVariables to prevent\nlosing subprocess variables when events are used;\n- concord-agent: slightly improved startup time of process JVMs;\n- concord-server: normalize LDAP usernames;\n- project-model: more robust yml-to-bpmn converter.\n\n\n\n## [0.32.0] - 2017-08-01\n\n### Added\n\n- concord-runner: `@InjectVariable` annotation can now be used to\ninject process context variables as task fields or method arguments.\n\n### Changed\n\n- ansible: fixed potential NPE.\n\n\n\n## [0.31.0] - 2017-07-25\n\n### Added\n\n- project-model: support for in/out variables and `error` blocks for\nprocess calls (aka the full form of `CallActivity`);\n- ansible-tasks: a `JavaDelegate` version of the task. Allows use of\nIN/OUT variables.\n\n### Changed\n\n- concord-server: use the native PostgreSQL UUID type.\n\n\n\n## [0.30.1] - 2017-07-24\n\n### Changed\n\n- concord-server: fixed the merging of arguments while resuming a process.\n\n\n\n## [0.30.0] - 2017-07-23\n\n### Added\n\n- concord-runner: environment variables support for `docker` task.\n- concord-server: allow uploading arbitrary files and override\nrequest parameters using `multipart/form-data` requests.\n\n\n\n## [0.29.0] - 2017-07-23\n\n### Changed\n\n- concord-server: fixed the order of applying variable overrides;\n- concord-runner: docker task now requires `/bin/sh` to be available\nin all images.\n\n### Breaking\n\n- concord-server: storing overrides in `_defaults.json` is not\nsupported anymore.\n\n\n\n## [0.28.3] - 2017-07-19\n\n### Changed\n\n- concord-runner: use `/workspace` instead of `/workplace` in the\ndocker task;\n\n### Breaking\n\n- concord-runner: docker task syntax is changed to\n  ```\n  - docker: image-name\n    cmd: my-cmd\n  ```\n\n\n\n## [0.28.2] - 2017-07-18\n\n### Changed\n\n- concord-server: improve error handling - return error details, add\noptional stacktrace field to error messages;\n- concord-server: fixed NPE on retrieving an empty/non-existing log\nfile;\n- project-model: fixed an issue with passing nested objects in\nIN-parameters of tasks.\n\n\n\n## [0.28.1] - 2017-07-14\n\n### Changed\n\n- concord-server: fixed an data escaping issue with project\nconfiguration migration.\n\n\n\n## [0.28.0] - 2017-07-13\n\n### Added\n\n- concord-server, concord-console: ability to override `type` in\n`<input type=\"...\"/>` for non-branded forms (e.g. for `password`\nfields).\n\n### Changed\n\n- concord-server: fix usage of`${requestInfo}` in non-\"portal\"\ncalls.\n\n\n\n## [0.27.0] - 2017-07-12\n\n### Added\n\n- concord-server: query parameters of the requests made using the\n\"portal\" endpoint are now accessible as `requestInfo.query.param`\nvariables;\n- concord-runner: support for running docker images using `docker`\nflow command;\n- docker-images: new tool image - `ansible`.\n\n### Changed\n\n- concord-server: fixed the issue with inability to use expressions\nas default values of form fields for non-branded forms.\n\n\n\n## [0.26.0] - 2017-07-10\n\n### Added\n\n- concord-server: include all available form data for \"success\"\nand \"process failed\" pages.\n\n### Changed\n\n- concord-server: return project configuration on\n`GET /api/v1/project/{projectName}` calls;\n- concord-server: improve project configuration handling.\n\n\n\n## [0.25.0] - 2017-07-10\n\n### Added\n\n- concord-server: form calls can now override form values and/or\nprovide additional data.\n- concord-server: both `flows` and `processes` directories can now\nbe used to load flows definitions;\n- concord-server: `concord.yml` can now be used instead of\n`.concord.yml`;\n- concord-server: profiles can now be loaded from a `profiles`\ndirectory.\n\n\n\n## [0.24.0] - 2017-07-05\n\n### Added\n\n- new task: `loadTasks`. Allows users to create their own tasks in\nGroovy, store them in process files and load dynamically;\n- concord-server: optional `activeProfiles` parameter can now be\nused in the Portal API.\n\n### Changed\n\n- ansible: dependency URLs in the ansible template are temporary\nchanged to use WARM.\n\n\n\n## [0.23.0] - 2017-06-28\n\n### Added\n\n- concord-server: Slack integration;\n- concord-runner: new `slack` task to send notifications to a Slack\nchannel.\n\n### Changed\n\n- concord-server: templates now are referenced by URLs in project\nconfiguration.\n\n\n\n## [0.22.0] - 2017-06-18\n\n### Added\n\n- concord-server: support for LDAPS;\n- concord-runner, concord-server: record process execution events.\n\n### Changed\n\n- concord-server: store agent commands in the DB;\n- concord-server: store process logs in the DB;\n- concord-server: fixed an issue with `data.js` generation when the\nstore directory does not exist.\n\n### Breaking\n\n- concord-server: remove the support for H2 database.\n\n\n\n## [0.20.1] - 2017-06-08\n\n- concord-server: fix potential connection leak.\n\n\n\n## [0.20.0] - 2017-06-06\n\n### Added\n\n- concord-console: the version information page;\n- concord-server: `/api/v1/server/version` API endpoint;\n- docker-images: optional SSL support for the console's nginx;\n- concord-server: new `PREPARING` process state;\n- concord-runner: upgrade the bpm engine to 0.31.1:\n  - support for EL 3.0 in flow expressions;\n  - form options now can use expressions.\n\n### Changed\n\n- concord-server: `created` flags in the REST API responses are\nreplaced with `actionType: CREATED|UPDATED`. Should be more obvious.\n- concord-server: fixed basic auth using passwords with `:` symbol;\n- concord-console: handle `ENQUEUED` status in the default form\nwizard;\n- concord-server: fixed the issue with custom form redirects when\nusing HTTPS proxy;\n- concord-server: store process state in the DB;\n- concord-server: create the log file of a process as early as\npossible to log startup errors.\n\n\n\n## [0.19.0] - 2017-05-30\n\n### Added\n\n- concord-server: add the first batch of metrics, expose with JMX;\n- concord-console: add \"Test connection\" button to the repository\nform;\n- concord-server: improve error handling for GIT repository cloning;\n- concord-server: add endpoint to encrypt values with a project's key;\n- concord-runner: add `crypto` task to decrypt previously encrypted\nvalues;\n\n### Changed\n\n- remove \"provisio\" builds: use plain .sh startup scripts for the\nserver and the agents;\n- concord-server: improve KV store to work in multi server setups;\n- concord-server: simplify the project configuration handling.\n\n\n\n## [0.18.0] - 2017-05-26\n\n### Added\n\n- concord-console: the project form;\n- concord-server: add form field labels to the generated `data.js`\nfiles.\n\n### Changed\n\n- upgrade the bpm engine to 0.29.0. It changes the form validation\nmessages: now it uses labels instead of field names (when available).\n\n\n\n## [0.17.3] - 2017-05-25\n\n### Changed\n\n- concord-server: fixed a bug preventing LDAP attributes from being\ncollected.\n\n\n\n## [0.17.2] - 2017-05-24\n\n### Changed\n\n- reverted back to jetty 9.2.11.v20150529 due to the issue with\nserving forms using `DefaultServlet`.\n\n\n\n## [0.17.0] - 2017-05-24\n\n### Added\n\n- concord-server: simple KV store backed by the database;\n- concord-runner: `kv` task to use the simple KV store;\n- yaml: optional error code in the `return` command;\n- concord-server: ability to pull a repository using a `commitId`;\n- concord-console: add the project list;\n- concord-server: add project description field;\n- concord-server: return process JSON objects in sync mode regardless\nof success or failure;\n- concord-server: log incoming gRPC connections;\n- concord-server: improve request data validation for the Project REST\nAPI.\n\n### Changed\n\n- concord-server: move the rpc module into the top-level directory;\n- concord-console: fix timestamps on the process page;\n- concord-server: suppress JOOQ banner.\n\n\n\n## [0.16.0] - 2017-05-20\n\n### Added\n\n- concord-runner: add `workDir` variable (same as `__attr_localPath`);\n- ansible-tasks: allow passing a vault password using request variables.\n- upgrade the bpm engine to 0.28.0 which allow flows to access\navailable tasks using expressions `${tasks.get('name')}` or in a script\nblock.\n\n### Changed\n\n- concord-console: wrap long lines in the log viewer;\n- concord-agent: do not cache `file://` and `SNAPSHOT` dependencies;\n- ansible-tasks: move inline inventory processing from the server into the\nplugin.\n\n\n\n## [0.15.0] - 2017-05-19\n\n### Added\n\n- concord-server: process start errors now include `stacktrace` field;\n- concord-server: optional synchronous mode of the process start\nmethods. All forms will be automatically submitted using the provided\nrequest data;\n- ansible: `defaults` replaced with `config` JSON object. Each key\nrepresents a section in the configuration file;\n- concord-runner: additional methods for `LoggingTask`.\n\n### Changed\n\n- concord-console: disable the log button for the processes that\ndon't have a log file;\n- concord-agent: fixed the state transfer for failed processes;\n- concord-server: make all LDAP attributes available in\n`${initiator.attributes}`.\n\n\n\n## [0.14.1] - 2017-05-17\n\n### Changed\n\n- concord-agent: handle early process startup errors.\n\n\n\n## [0.14.0] - 2017-05-17\n\n### Added\n\n- concord-server-db: indexes for the process queue;\n- yaml: `lastError` can now be used to access the last handled\n`BpmnError`.\n\n### Changed\n\n- concord-console: fixed rendering of \"Updated\" and \"Created\"\ncolumns in the queue table.\n- concord-runner: all unhandled exceptions in a `ServiceTask`\n(YAML expressions) or in a `ScriptTask` (YAML script blocks)\nwill be wrapped as `BpmnErrors`.\n\n\n\n## [0.13.0] - 2017-05-16\n\n### Added\n\n- concord-server: process queue;\n- concord-server, concord-agent: support for multiple agents, async\ntransport.\n\n### Changed\n\n- boo, nexus-perf, teamrosters and oneops plugins are moved into a\nseparate repository.\n\n\n\n## [0.12.0] - 2017-05-12\n\n## Added\n\n- concord-console: support for \"int\" and \"decimal\" fields in the\ndefault form renderer;\n- added \"project name\" column to the process history;\n- concord-runner: support for external scripts.\n\n## Changed\n\n- concord-server: fixed LDAP attributes retrieval (including\n`displayName`).\n\n\n\n## [0.11.2] - 2017-05-09\n\n### Changed\n\n- updated oneops-client version, fixing the `NoSuchMethodError` issue with boo-task;\n- oneops-tasks: migrate to the official oneops-client.\n\n\n\n## [0.11.1] - 2017-05-09\n\n### Changed\n\n- boo-task: fixed fatjar creation.\n\n\n\n## [0.11.0] - 2017-05-09\n\n### Added\n\n- ansible: store exit code in the stats file.\n\n### Changed\n\n- concord-agent: more reliable kill command;\n- ansible: allow overriding of connection timeout value.\n\n\n\n## [0.10.0] - 2017-05-04\n\n### Added\n\n- boo-task: tag assembly with a cost center.\n\n\n\n## [0.9.0] - 2017-05-04\n\n### Added\n\n- concord-server: GIT repository shallow cloning and caching.\n\n### Changed\n\n- concord-server: improved error message when creating or updating\na project with non-existing secret;\n- concord-server, concord-agent: fixed logging configuration conflicts;\n- concord-console: reduce wizard polling interval, \"time to first form\"\nimproved;\n- runner.jar moved from the server to the agent, futher reducing the\nprocess statup time.\n\n\n\n## [0.8.2] - 2017-05-01\n\n### Added\n\n- concord-agent: simple caching mechanism for dependencies.\n\n### Changed\n\n- boo-task: bug fixes.\n\n\n\n## [0.8.0] - 2017-05-01\n\n### Added\n\n- support for the form branding: custom forms with user-provided\nHTML/CSS/JS/etc.\n\n\n\n## [0.7.0] - 2017-04-30\n\n### Added\n\n- boo-task: pull more deployment information into the context;\n- teamrosters-task: new task to retrieve Team Rosters data;\n\n### Changed\n\n- fixed: missing line/column numbers when parsing YAML process\ndefinitions and project files;\n- fixed: `value` attribute ignored in form field declarations.\n\n\n\n## [0.6.0] - 2017-04-27\n\n### Added\n\n- the Console now uses LDAP authentication;\n- expose (a configurable set of) LDAP attributes to processes;\n- smtp: support for Mustache templates;\n- yaml: simplify usage of external variables in script steps;\n- boo: return deployment status from the task.\n\n\n\n## [0.5.1] - 2017-04-20\n\n### Changed\n\n- boo: use all provided variables as template variables;\n- fixed: `.concord.yml` variables are ignored when using a GIT\nrepository;\n- fixed: project repositories should be ignored when starting a\nprocess using an archive.\n\n\n\n## [0.5.0] - 2017-04-11\n\n### Added\n\n- concord-runner: support for expressions in variables.\n\n### Breaking\n\n- project and process related constants moved to project-model module.\n\n\n\n## [0.4.1] - 2017-04-10\n\n### Changed\n\n- fixed merging of profile variables with defaults and request's data.\n\n\n\n## [0.4.0] - 2017-04-10\n\n### Added\n\n- support for `.concord.yml` files. Those files can contain flow and\nform definitions, default variables and \"profiles\";\n- allow overriding`dependencies` in a project file, defaults or user\nrequests;\n- concord-console: list of secrets.\n\n### Breaking\n\n- concord-common: removed `Task#getKey`. Value of\n`javax.inject.@Named` annotation is used to resolve a task.\n- removed bpmn-format and yaml-format modules;\n- yaml2-format replaced with a single module: project-model. \n\n\n\n## [0.3.2] - 2017-04-03\n\n### Changed\n\n- concord-server: merge existing process values with form values;\n- concord-server: fix the LDAP mapping update method;\n- concord-server: new method `/api/v1/ldap/query/{username}/group` to\nretrieve a list of LDAP user's groups;\n- concord-agent: fix a race condition in the log creation;\n- upgrade BPM version to 0.2.1.\n\n## Breaking\n\n- concord-server: LDAP mappings methods moved to `/api/v1/ldap/mapping`\npath prefix.\n\n\n\n## [0.3.1] - 2017-04-02\n\n### Changed\n\n- concord-console: host the landing page;\n- concord-console: simple process launcher using\n`/#/portal/start?entryPoint=abc` URLs.\n\n## [0.3.0] - 2017-03-31\n\n### Changed\n\n- fix error logging in nexus-perf tasks.\n- support for single or multiple selection fields\n\n### Breaking\n\n- `_deps` file is replaced with `dependencies` array in a project configuration,\ntemplate, `_defaults.json` or user's request.\n\n\n\n## [0.2.1] - 2017-03-29\n\n### Changed\n\n- boo upgraded to 1.0.3;\n- minor fixes in examples.\n\n\n\n## [0.2.0] - 2017-03-28\n\n### Added\n\n- initial support for forms, including the new UI wizard;\n- the server now uses connection pooling to talk with the agent(s).\n- YAML DSL support for inline JSR-223 scripts;\n- now it's possible to use AD/LDAP fully-qualified usernames, e.g. `user@domain.com`;\n- new endpoint: `/api/v1/role` for managing mappings between roles and permissions;\n- new endpoint: `/api/v1/ldap` for managing mappings between LDAP groups and roles.\n\n### Changed\n\n- logging configuration cleanup;\n- projects now can be created and updated using a single endpoint;\n- \"get user\" method now uses path parameter: `GET /api/v1/user/{username}`;\n- usernames containing backward slashes (\"\\\\\") are forbidden;\n- unauthenticated and unauthorized errors are now returned with `Content-Type: text/plain`.\n\n### Breaking\n\n- removed `PUT /api/v1/user/{username}` method.\n\n\n\n## [0.1.0] - 2017-03-22\n\nFirst release.\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2017-present, Walmart Inc.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\"\n"
  },
  {
    "path": "NOTES.md",
    "content": "# Development Notes\n\n## Code Style\n\nPrefer `var` in new code, but do not mix `var` and explicit local variable types in the same file.\n\n## Git\n\nPrefer commit subjects in the existing `module: short description` style.\nUse comma-separated modules when needed, and keep the body minimal or omit it if the subject is already clear.\n\n## Server Plugins\n\nPrefer explicit binding using `com.google.inject.Module` over `@Named` annotations.\nUse `@Named` for top-level modules and server plugins.\n\nServer and server plugin types that currently require explicit binding using `Multibinder.newSetBinder`:\n- ApiDescriptor\n- AuditLogListener\n- AuthenticationHandler\n- BackgroundTask\n- Component\n- ContextHandlerConfigurator\n- CustomEnqueueProcessor\n- DatabaseChangeLogProvider\n- ExceptionMapper\n- ExternalEventTriggerProcessor\n- Filter\n- FilterChainConfigurator\n- FilterHolder\n- GaugeProvider\n- GithubTriggerProcessor.EventEnricher\n- HttpServlet\n- ModeProcessor\n- PolicyApplier\n- ProcessEventListener\n- ProcessLogListener\n- ProcessStatusListener\n- ProcessWaitHandler\n- ProjectLoader\n- Realm\n- RepositoryRefreshListener\n- RequestErrorHandler\n- ScheduledTask\n- SecretStore\n- ServletContextListener\n- ServletHolder\n- UserInfoProvider\n"
  },
  {
    "path": "README.md",
    "content": "# Concord\n\n![](https://img.shields.io/maven-central/v/com.walmartlabs.concord/parent.svg)\n\n- Website: https://concord.walmartlabs.com\n- [Installation guide](https://concord.walmartlabs.com/docs/getting-started/installation.html)\n- [Core Plugins](./plugins)\n- [Community Plugins](https://github.com/walmartlabs/concord-plugins/)\n\n![](console2/public/images/concord.svg)\n\nConcord is a workflow server. It is the orchestration engine that connects\ndifferent systems together using scenarios and plugins created by users.\n\n- [Building](#building)\n- [Console](#console)\n- [Integration tests](#integration-tests)\n  * [Prerequisites](#prerequisites)\n  * [Running tests](#running-tests)\n- [Repository Docs](#repository-docs)\n- [Examples](#examples)\n- [How To Release New Versions](#how-to-release-new-versions)\n- [Development Notes](#development-notes)\n\n## Building\n\nDependencies:\n- [Git](https://git-scm.com/) 2.18+\n- [Java 17](https://adoptium.net/)\n- [Docker Community Edition](https://www.docker.com/community-edition)\n- [Docker Buildx](https://docs.docker.com/build/buildx/install/)\n- (Optional) [NodeJS and NPM](https://nodejs.org/en/download/) (Node 24 LTS)\n\n```shell\ngit clone https://github.com/walmartlabs/concord.git\ncd concord\n./mvnw clean install -DskipTests\n```\n\nAvailable Maven profiles:\n\n- `docker` - build Docker images;\n- `it` - run integration tests;\n- `jdk17-aarch64` - use a different JDK version for building artifacts and Docker images.\n\nProfiles can be combined, e.g.\n\n```\n./mvnw clean install -Pdocker -Pit -Pjdk17-aarch64\n```\n\n## Console\n\nSee the [console2/README.md](./console2/README.md) file.\n```shell\ncd ./console2\nnpm ci # Install dependencies\n```\n\nStart the console in dev mode by running:\n```shell\nnpm run start\n```\n\n## Integration tests\n\n### Prerequisites\n\nPrerequisites:\n\n- Git 2.18+\n- Docker, listening on `tcp://127.0.0.1:2375`;\n- Ansible 2.6.0+ must be installed and available in `$PATH`.\n  See [the official documentation](http://docs.ansible.com/ansible/intro_installation.html);\n- `requests` python module is required. It can be installed by using `pip install requests`\n  or the system package manager;\n- Java must be available in `$PATH` as `java`;\n- [Chrome WebDriver](http://chromedriver.chromium.org/) available in `$PATH`.\n\n### Running tests\n\nIntegration tests are disabled by default. Use the `it` profile to enable them:\n\n```shell\n./mvnw verify -Pit\n```\n\nThis will run ITs agains the locally running server and the agent.\nTo automatically start and stop the server and the agent using docker, use the\n`docker` profile:\n\n```shell\n./mvnw verify -Pit -Pdocker\n```\n\nTo run UI ITs in an IDE using the UI's dev mode:\n- start the UI's dev mode with `cd console2 && npm start`;\n- set up `IT_CONSOLE_BASE_URL=http://localhost:3000` environment variable before running\nany UI tests.\n\n## Repository Docs\n\nFor repo-specific development entrypoints, see:\n\n- [Integration tests](./it/README.md)\n- [Development notes](./NOTES.md)\n- [Server README](./server/README.md)\n- [Console UI README](./console2/README.md)\n- [Agent operator README](./agent-operator/README.md)\n\n## Examples\n\nSee the [examples](examples) directory.\n\n## How To Release New Versions\n\n- perform a regular Maven release:\n  ```\n  $ ./mvnw release:prepare release:perform\n  ```\n- update and commit the CHANGELOG.md file\n  ```\n  $ git add CHANGELOG.md\n  $ git commit -m 'update changelog'\n  ```\n- push the new tag and the master branch:\n  ```\n  $ git push origin RELEASE_TAG\n  $ git push origin master\n  ```\n- build and push the Docker images:\n  ```\n  $ git checkout RELEASE_TAG\n  $ gh workflow run docker-multiarch.yml --ref master -f ref=RELEASE_TAG -f docker_tag=RELEASE_TAG -f docker_namespace=walmartlabs\n  ```\n- sync to [Sonatype](https://oss.sonatype.org/);\n- check the Central repository if the sync is complete:\n  ```\n  https://repo.maven.apache.org/maven2/com/walmartlabs/concord/parent/RELEASE_TAG\n  ```\n- once the sync is complete, push the `latest` Docker images:\n  ```\n  $ gh workflow run docker-multiarch.yml --ref master -f ref=RELEASE_TAG -f docker_tag=latest -f docker_namespace=walmartlabs\n  ```\n\n## Development Notes\n\nSee [NOTES.md](NOTES.md).\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Reporting a Vulnerability\n\nTo report a vulnerability please use [GitHub Issues](https://github.com/walmartlabs/concord/issues)\nor reach the team using the following email address:\n\n- [concord-team@wal-mart.com](mailto:concord-team@wal-mart.com).\n\n"
  },
  {
    "path": "agent/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-agent</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <properties>\n        <runnerV1.path>\n            ${project.basedir}/../runtime/v1/impl/target/concord-runtime-impl-v1-${project.version}-jar-with-dependencies.jar\n        </runnerV1.path>\n        <runnerV2.path>\n            ${project.basedir}/../runtime/v2/runner/target/concord-runner-v2-${project.version}-jar-with-dependencies.jar\n        </runnerV2.path>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-github-app-installation</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-client2</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-imports</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-dependency-manager</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-policy-engine</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-queue-client</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-repository</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime</groupId>\n            <artifactId>concord-runtime-common</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.sisu</groupId>\n            <artifactId>org.eclipse.sisu.inject</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.inject</groupId>\n            <artifactId>guice</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.guava</groupId>\n            <artifactId>guava</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jdk8</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>ch.qos.logback</groupId>\n            <artifactId>logback-classic</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-compress</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.typesafe</groupId>\n            <artifactId>config</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-config</artifactId>\n        </dependency>\n\n        <!-- Immutables -->\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-guava</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.errorprone</groupId>\n            <artifactId>error_prone_annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-core</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-junit-jupiter</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <!-- dependency tree fix for mvnd -->\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v1</groupId>\n            <artifactId>concord-runtime-impl-v1</artifactId>\n            <version>${project.version}</version>\n            <type>pom</type>\n            <scope>test</scope>\n            <exclusions>\n                <exclusion>\n                    <groupId>*</groupId>\n                    <artifactId>*</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runner-v2</artifactId>\n            <version>${project.version}</version>\n            <type>pom</type>\n            <scope>test</scope>\n            <exclusions>\n                <exclusion>\n                    <groupId>*</groupId>\n                    <artifactId>*</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <resources>\n            <resource>\n                <filtering>false</filtering>\n                <directory>${project.basedir}/src/main/resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n            <resource>\n                <filtering>true</filtering>\n                <directory>${project.basedir}/src/main/filtered-resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n        </resources>\n\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-dependency-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>copy-runner</id>\n                        <phase>process-resources</phase>\n                        <goals>\n                            <goal>copy</goal>\n                        </goals>\n                        <configuration>\n                            <artifactItems>\n                                <artifactItem>\n                                    <groupId>com.walmartlabs.concord.runtime.v1</groupId>\n                                    <artifactId>concord-runtime-impl-v1</artifactId>\n                                    <version>${project.version}</version>\n                                    <classifier>jar-with-dependencies</classifier>\n                                    <destFileName>runner-v1.jar</destFileName>\n                                </artifactItem>\n                                <artifactItem>\n                                    <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n                                    <artifactId>concord-runner-v2</artifactId>\n                                    <version>${project.version}</version>\n                                    <classifier>jar-with-dependencies</classifier>\n                                    <destFileName>runner-v2.jar</destFileName>\n                                </artifactItem>\n                            </artifactItems>\n                            <outputDirectory>${project.build.directory}/runner</outputDirectory>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-assembly-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>dist</id>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>single</goal>\n                        </goals>\n                        <configuration>\n                            <descriptors>\n                                <descriptor>src/assembly/dist.xml</descriptor>\n                            </descriptors>\n                            <tarLongFileMode>posix</tarLongFileMode>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "agent/src/assembly/default.conf",
    "content": "concord-agent {\n}\n"
  },
  {
    "path": "agent/src/assembly/dist.xml",
    "content": "<assembly xmlns=\"http://maven.apache.org/ASSEMBLY/2.0.0\"\n          xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n          xsi:schemaLocation=\"http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd\">\n\n    <id>dist</id>\n\n    <formats>\n        <format>tar.gz</format>\n    </formats>\n\n    <includeBaseDirectory>false</includeBaseDirectory>\n\n    <dependencySets>\n        <dependencySet>\n            <outputDirectory>lib</outputDirectory>\n        </dependencySet>\n    </dependencySets>\n\n    <files>\n        <file>\n            <source>src/assembly/start.sh</source>\n            <outputDirectory>.</outputDirectory>\n            <fileMode>755</fileMode>\n        </file>\n        <file>\n            <source>src/assembly/default.conf</source>\n            <outputDirectory>.</outputDirectory>\n        </file>\n\n        <!-- runners -->\n        <file>\n            <source>${project.build.directory}/runner/runner-v1.jar</source>\n            <outputDirectory>runner</outputDirectory>\n        </file>\n        <file>\n            <source>${project.build.directory}/runner/runner-v2.jar</source>\n            <outputDirectory>runner</outputDirectory>\n        </file>\n    </files>\n</assembly>"
  },
  {
    "path": "agent/src/assembly/start.sh",
    "content": "#!/bin/bash\n\nBASE_DIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\n\nexport RUNNER_V1_PATH=\"${BASE_DIR}/runner/runner-v1.jar\"\nexport RUNNER_V2_PATH=\"${BASE_DIR}/runner/runner-v2.jar\"\n\nif [[ -z \"${CONCORD_TMP_DIR}\" ]]; then\n    export CONCORD_TMP_DIR=\"/tmp\"\nfi\n\nif [[ -z \"${CONCORD_JAVA_OPTS}\" ]]; then\n    CONCORD_JAVA_OPTS=\"-Xmx256m\"\nfi\necho \"CONCORD_JAVA_OPTS: ${CONCORD_JAVA_OPTS}\"\n\nif [[ -z \"${CONCORD_CFG_FILE}\" ]]; then\n    CONCORD_CFG_FILE=\"${BASE_DIR}/default.conf\"\nfi\necho \"CONCORD_CFG_FILE: ${CONCORD_CFG_FILE}\"\n\necho \"Using $(which java)\"\njava -version\n\nJAVA_VERSION=$(java -version 2>&1 \\\n  | head -1 \\\n  | cut -d'\"' -f2 \\\n  | sed 's/^1\\.//' \\\n  | cut -d'.' -f1)\n\nJDK_SPECIFIC_OPTS=\"\"\nif (( $JAVA_VERSION > 8 )); then\n  echo \"Applying JDK 9+ specific options...\"\n  JDK_SPECIFIC_OPTS=\"--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED\"\nfi\n\nexec java \\\n${CONCORD_JAVA_OPTS} \\\n${JDK_SPECIFIC_OPTS} \\\n-Dfile.encoding=UTF-8 \\\n-Djava.net.preferIPv4Stack=true \\\n-Djava.security.egd=file:/dev/./urandom \\\n-Dlogback.configurationFile=com/walmartlabs/concord/agent/logback.xml \\\n-Dconcord.conf=${CONCORD_CFG_FILE} \\\n-cp \"${BASE_DIR}/lib/*\" \\\ncom.walmartlabs.concord.agent.Main \\\n\"$@\"\n"
  },
  {
    "path": "agent/src/main/filtered-resources/com/walmartlabs/concord/agent/cfg/runnerV1.properties",
    "content": "path=${runnerV1.path}"
  },
  {
    "path": "agent/src/main/filtered-resources/com/walmartlabs/concord/agent/cfg/runnerV2.properties",
    "content": "path=${runnerV2.path}"
  },
  {
    "path": "agent/src/main/filtered-resources/com/walmartlabs/concord/agent/executors/runner/default-dependencies",
    "content": "mvn://com.walmartlabs.concord.plugins.basic:concord-tasks:${project.version}\nmvn://com.walmartlabs.concord.plugins.basic:slack-tasks:${project.version}\nmvn://com.walmartlabs.concord.plugins.basic:http-tasks:${project.version}"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/Agent.java",
    "content": "package com.walmartlabs.concord.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Injector;\nimport com.walmartlabs.concord.agent.Worker.CompletionCallback;\nimport com.walmartlabs.concord.agent.cfg.AgentConfiguration;\nimport com.walmartlabs.concord.agent.cfg.DockerConfiguration;\nimport com.walmartlabs.concord.agent.docker.OrphanSweeper;\nimport com.walmartlabs.concord.agent.guice.WorkerModule;\nimport com.walmartlabs.concord.agent.mmode.MaintenanceModeListener;\nimport com.walmartlabs.concord.agent.mmode.MaintenanceModeNotifier;\nimport com.walmartlabs.concord.client2.ProcessEntry.StatusEnum;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.server.queueclient.QueueClient;\nimport com.walmartlabs.concord.server.queueclient.message.ProcessRequest;\nimport com.walmartlabs.concord.server.queueclient.message.ProcessResponse;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\npublic class Agent {\n\n    private static final Logger log = LoggerFactory.getLogger(Agent.class);\n\n    private final Injector injector;\n    private final AgentConfiguration agentCfg;\n    private final DockerConfiguration dockerCfg;\n\n    private final QueueClient queueClient;\n    private final ExecutorService executor;\n\n    private final Map<UUID, Worker> activeWorkers = new ConcurrentHashMap<>();\n    private final AtomicBoolean maintenanceMode = new AtomicBoolean(false);\n\n    // make the reference volatile as we check if for != null in different threads\n    private volatile Semaphore workersAvailable; // NOSONAR\n\n    @Inject\n    public Agent(Injector injector,\n                 AgentConfiguration agentCfg,\n                 DockerConfiguration dockerCfg,\n                 QueueClient queueClient) {\n\n        this.injector = injector;\n\n        this.agentCfg = agentCfg;\n        this.dockerCfg = dockerCfg;\n        this.queueClient = queueClient;\n\n        this.executor = Executors.newCachedThreadPool();\n    }\n\n    public void start() {\n        Runtime.getRuntime().addShutdownHook(new Thread(() -> {\n            log.info(\"Received SIGTERM, stopping...\");\n            Agent.this.stop();\n        }, \"shutdown-hook\"));\n\n        executor.submit(() -> {\n            run();\n            return null;\n        });\n\n        log.info(\"start -> done\");\n    }\n\n    @SuppressWarnings(\"unused\")\n    public void stop() {\n        queueClient.stop();\n        executor.shutdownNow();\n\n        log.info(\"stop -> done\");\n    }\n\n    private void run() throws Exception {\n        int workersCount = agentCfg.getWorkersCount();\n        log.info(\"run -> using {} worker(s)\", workersCount);\n        workersAvailable = new Semaphore(workersCount);\n\n        // listen for maintenance mode requests\n        startMaintenanceModeNotifier(queueClient);\n\n        if (dockerCfg.isOrphanSweeperEnabled()) {\n            executor.submit(new OrphanSweeper(this::isAlive, dockerCfg.getOrphanSweeperPeriod()));\n        }\n\n        // start the command handler in a separate thread\n        CommandHandler commandHandler = new CommandHandler(agentCfg.getAgentId(), queueClient, agentCfg.getPollInterval(), this::cancel);\n        executor.submit(commandHandler);\n\n        // main loop\n        while (!Thread.currentThread().isInterrupted()) {\n            // check if the maintenance mode is enabled. If so, hang there indefinitely\n            validateMaintenanceMode();\n\n            // TODO parallel acquire?\n            // wait for a free \"slot\"\n            workersAvailable.acquire();\n            log.info(\"run -> acquired a slot, {}/{} remains\", workersAvailable.availablePermits(), workersCount);\n\n            // fetch the next job\n            JobRequest jobRequest;\n            try {\n                jobRequest = take(queueClient);\n            } catch (InterruptedException e) {\n                log.info(\"run -> interrupted, exiting...\");\n                return;\n            } catch (Exception e) {\n                log.error(\"run -> error while fetching a job: {}\", e.getMessage(), e);\n\n                workersAvailable.release();\n\n                // wait before retrying\n                // the server is not reachable or unhealthy, no point retrying immediately\n                Utils.sleep(AgentConstants.ERROR_DELAY);\n                continue;\n            }\n\n            if (jobRequest == null) {\n                // can happen on switching to maintenance mode or reconnecting, etc\n                workersAvailable.release();\n                continue;\n            }\n\n            UUID instanceId = jobRequest.getInstanceId();\n\n            // worker will handle the process' lifecycle\n            try {\n                Worker w = injector.createChildInjector(new WorkerModule(agentCfg.getAgentId(), instanceId, jobRequest.getSessionToken()))\n                        .getInstance(WorkerFactory.class)\n                        .create(jobRequest, createStatusCallback(instanceId, workersAvailable));\n\n                // register the worker so we can cancel it later\n                activeWorkers.put(instanceId, w);\n\n                // start a new thread to process the job\n                executor.submit(w);\n            } catch (Exception e) {\n                log.error(\"run -> error while submitting worker: {}\", e.getMessage());\n                workersAvailable.release();\n            }\n        }\n    }\n\n    private void startMaintenanceModeNotifier(QueueClient queueClient) {\n        try {\n            MaintenanceModeNotifier n = new MaintenanceModeNotifier(agentCfg.getMaintenanceModeListenerHost(), agentCfg.getMaintenanceModeListenerPort(), new MaintenanceModeListener() {\n                @Override\n                public Status onMaintenanceMode() {\n                    maintenanceMode.set(true);\n                    queueClient.maintenanceMode();\n                    return getMaintenanceModeStatus();\n                }\n\n                @Override\n                public Status getMaintenanceModeStatus() {\n                    long availableWorkers = (workersAvailable != null)\n                            ? workersAvailable.availablePermits()\n                            : 0L;\n                    long cnt = agentCfg.getWorkersCount() - availableWorkers;\n                    return new Status(maintenanceMode.get(), cnt);\n                }\n            });\n            n.start();\n        } catch (IOException e) {\n            log.warn(\"start -> can't start the maintenance mode notifier: {}\", e.getMessage());\n        }\n    }\n\n    private void validateMaintenanceMode() throws InterruptedException {\n        while (maintenanceMode.get()) {\n            log.info(\"run -> switched to maintenance mode\");\n            synchronized (maintenanceMode) {\n                maintenanceMode.wait();\n            }\n            // TODO option to switch mmode off?\n        }\n    }\n\n    private boolean isAlive(UUID instanceId) {\n        return activeWorkers.containsKey(instanceId);\n    }\n\n    private CompletionCallback createStatusCallback(UUID instanceId, Semaphore workersAvailable) {\n        return new CompletionCallback() {\n\n            // guard against misuse: the callback must be called only once - when\n            // the process reaches its final status (successful or not)\n            private volatile boolean called = false;\n\n            @Override\n            public void onStatusChange(StatusEnum status) {\n                if (called) {\n                    throw new IllegalStateException(\"The completion callback already called once\");\n                }\n                called = true;\n\n                activeWorkers.remove(instanceId);\n                workersAvailable.release();\n            }\n        };\n    }\n\n    private JobRequest take(QueueClient queueClient) throws Exception {\n        Future<ProcessResponse> req = queueClient.request(new ProcessRequest(agentCfg.getCapabilities()));\n\n        ProcessResponse resp = req.get();\n        if (resp == null) {\n            return null;\n        }\n\n        Path workDir = PathUtils.createTempDir(agentCfg.getPayloadDir(), \"workDir\");\n\n        return JobRequest.from(resp, workDir);\n    }\n\n    private void cancel(UUID instanceId) {\n        Worker w = activeWorkers.get(instanceId);\n        if (w == null) {\n            return;\n        }\n\n        w.cancel();\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/AgentAuthTokenProvider.java",
    "content": "package com.walmartlabs.concord.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.typesafe.config.Config;\nimport com.walmartlabs.concord.agent.remote.ApiClientFactory;\nimport com.walmartlabs.concord.common.AuthTokenProvider;\nimport com.walmartlabs.concord.common.ExternalAuthToken;\nimport com.walmartlabs.concord.github.appinstallation.GitHubAppInstallation;\nimport com.walmartlabs.concord.sdk.Secret;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport java.net.URI;\nimport java.util.List;\nimport java.util.Optional;\n\npublic class AgentAuthTokenProvider implements AuthTokenProvider {\n    private final List<AuthTokenProvider> authTokenProviders;\n\n    @Inject\n    public AgentAuthTokenProvider(GitHubAppInstallation githubProvider,\n                                  OauthTokenProvider oauthTokenProvider) {\n\n        this.authTokenProviders = List.of(githubProvider, oauthTokenProvider);\n    }\n\n    @Override\n    public boolean supports(URI repo, @Nullable Secret secret) {\n        return authTokenProviders.stream()\n                .anyMatch(p -> p.supports(repo, secret));\n    }\n\n    public Optional<ExternalAuthToken> getToken(URI repo, @Nullable Secret secret) {\n        for (var tokenProvider : authTokenProviders) {\n            if (tokenProvider.supports(repo, secret)) {\n                return tokenProvider.getToken(repo, secret);\n            }\n        }\n\n        return Optional.empty();\n    }\n\n    public static class ConcordServerTokenProvider implements AuthTokenProvider {\n        private final ApiClientFactory apiClientFactory;\n        private final Config config;\n\n        @Inject\n        public ConcordServerTokenProvider(ApiClientFactory apiClientFactory, Config config) {\n            this.apiClientFactory = apiClientFactory;\n            this.config = config;\n        }\n\n\n        @Override\n        public boolean supports(URI repo, @Nullable Secret secret) {\n            // TODO implement\n            return false;\n        }\n\n        @Override\n        public Optional<ExternalAuthToken> getToken(URI repo, @Nullable Secret secret) {\n            // TODO implement\n            return Optional.empty();\n        }\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/AgentConstants.java",
    "content": "package com.walmartlabs.concord.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic final class AgentConstants {\n\n    public static final long ERROR_DELAY = 5000;\n    public static final int API_CALL_MAX_RETRIES = 3;\n    public static final long API_CALL_RETRY_DELAY = 3000;\n\n    private AgentConstants() {\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/AgentModule.java",
    "content": "package com.walmartlabs.concord.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.typesafe.config.Config;\nimport com.walmartlabs.concord.agent.cfg.*;\nimport com.walmartlabs.concord.agent.executors.runner.DefaultDependencies;\nimport com.walmartlabs.concord.agent.executors.runner.ProcessPool;\nimport com.walmartlabs.concord.agent.remote.ApiClientFactory;\nimport com.walmartlabs.concord.agent.remote.QueueClientProvider;\nimport com.walmartlabs.concord.common.ObjectMapperProvider;\nimport com.walmartlabs.concord.common.cfg.OauthTokenConfig;\nimport com.walmartlabs.concord.config.ConfigModule;\nimport com.walmartlabs.concord.github.appinstallation.cfg.GitHubAppInstallationConfig;\nimport com.walmartlabs.concord.server.queueclient.QueueClient;\n\nimport javax.inject.Named;\n\nimport static com.google.inject.Scopes.SINGLETON;\n\n@Named\npublic class AgentModule implements Module {\n\n    private final Config config;\n\n    public AgentModule() {\n        this(loadDefaultConfig());\n    }\n\n    public AgentModule(Config config) {\n        this.config = config;\n    }\n\n    @Override\n    public void configure(Binder binder) {\n        binder.bind(ObjectMapper.class).toProvider(ObjectMapperProvider.class);\n        binder.bind(Config.class).toInstance(config);\n\n        binder.bind(AgentConfiguration.class).in(SINGLETON);\n        binder.bind(DockerConfiguration.class).in(SINGLETON);\n        binder.bind(RuntimeConfiguration.class).asEagerSingleton();\n        binder.bind(GitConfiguration.class).in(SINGLETON);\n        binder.bind(OauthTokenConfig.class).to(GitConfiguration.class).in(SINGLETON);\n        binder.bind(GitHubConfiguration.class).in(SINGLETON);\n        binder.bind(GitHubAppInstallationConfig.class).to(GitHubConfiguration.class).in(SINGLETON);\n        binder.bind(AgentAuthTokenProvider.ConcordServerTokenProvider.class).in(SINGLETON);\n        binder.bind(ImportConfiguration.class).in(SINGLETON);\n        binder.bind(PreForkConfiguration.class).in(SINGLETON);\n        binder.bind(RepositoryCacheConfiguration.class).in(SINGLETON);\n        binder.bind(ServerConfiguration.class).in(SINGLETON);\n\n        binder.bind(DefaultDependencies.class).in(SINGLETON);\n        binder.bind(ProcessPool.class).in(SINGLETON);\n        binder.bind(ApiClientFactory.class).in(SINGLETON);\n        binder.bind(QueueClient.class).toProvider(QueueClientProvider.class).in(SINGLETON);\n\n        binder.bind(Agent.class).in(SINGLETON);\n    }\n\n    private static Config loadDefaultConfig() {\n        return ConfigModule.load(\"concord-agent\");\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/CommandHandler.java",
    "content": "package com.walmartlabs.concord.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.queueclient.QueueClient;\nimport com.walmartlabs.concord.server.queueclient.message.CommandRequest;\nimport com.walmartlabs.concord.server.queueclient.message.CommandResponse;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.UUID;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\n\npublic class CommandHandler implements Runnable {\n\n    private static final Logger log = LoggerFactory.getLogger(CommandHandler.class);\n\n    private static final long ERROR_DELAY = 5000;\n\n    private final ExecutorService executor;\n\n    private final UUID agentId;\n    private final QueueClient queueClient;\n    private final long pollInterval;\n    private final CancelHandler cancelHandler;\n\n    public CommandHandler(String agentId,\n                          QueueClient queueClient,\n                          long pollInterval,\n                          CancelHandler cancelHandler) {\n\n        this.executor = Executors.newCachedThreadPool();\n\n        this.agentId = UUID.fromString(agentId);\n        this.queueClient = queueClient;\n        this.pollInterval = pollInterval;\n        this.cancelHandler = cancelHandler;\n    }\n\n    @Override\n    public void run() {\n        while (!Thread.currentThread().isInterrupted()) {\n            try {\n                CommandResponse cmd = take();\n                if (cmd == null) {\n                    sleep(pollInterval);\n                    continue;\n                }\n\n                executor.submit(() -> execute(cmd));\n            } catch (Exception e) {\n                log.error(\"run -> error while processing a command: {}\", e.getMessage(), e);\n                sleep(ERROR_DELAY);\n            }\n        }\n    }\n\n    private CommandResponse take() throws Exception {\n        try {\n            return queueClient.<CommandResponse>request(new CommandRequest(agentId)).get();\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n            return null;\n        }\n    }\n\n    private void execute(CommandResponse cmd) {\n        log.info(\"execute -> got a command: {}\", cmd);\n\n        CommandResponse.CommandType type = cmd.getType();\n        if (type == CommandResponse.CommandType.CANCEL_JOB) {\n            UUID instanceId = UUID.fromString((String) cmd.getPayload().get(\"instanceId\"));\n            cancelHandler.cancel(instanceId);\n        } else {\n            log.warn(\"execute -> unsupported command type: {}\", type);\n        }\n    }\n\n    private static void sleep(long millis) {\n        try {\n            Thread.sleep(millis);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    public interface CancelHandler {\n        void cancel(UUID instanceId);\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/ConfiguredJobRequest.java",
    "content": "package com.walmartlabs.concord.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.sdk.Constants;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.Map;\n\n/**\n * A {@link JobRequest} plus the process configuration loaded from the request's\n * payload directory.\n */\npublic class ConfiguredJobRequest extends JobRequest {\n\n    @SuppressWarnings(\"unchecked\")\n    public static ConfiguredJobRequest from(JobRequest req) throws ExecutionException {\n        Map<String, Object> cfg = Collections.emptyMap();\n\n        Path p = req.getPayloadDir().resolve(Constants.Files.CONFIGURATION_FILE_NAME);\n        if (Files.exists(p)) {\n            try (InputStream in = Files.newInputStream(p)) {\n                cfg = new ObjectMapper().readValue(in, Map.class);\n            } catch (IOException e) {\n                throw new ExecutionException(\"Error while reading process configuration\", e);\n            }\n        }\n\n        return new ConfiguredJobRequest(req, cfg);\n    }\n\n    private final Map<String, Object> processCfg;\n\n    private ConfiguredJobRequest(JobRequest src, Map<String, Object> processCfg) {\n        super(src);\n        this.processCfg = processCfg;\n    }\n\n    public Map<String, Object> getProcessCfg() {\n        return processCfg;\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/DefaultStateFetcher.java",
    "content": "package com.walmartlabs.concord.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ClientUtils;\nimport com.walmartlabs.concord.client2.ProcessApi;\nimport com.walmartlabs.concord.common.ZipUtils;\n\nimport javax.inject.Inject;\nimport java.io.InputStream;\nimport java.nio.file.StandardCopyOption;\n\npublic class DefaultStateFetcher implements StateFetcher {\n\n    private final ProcessApi processApi;\n\n    @Inject\n    public DefaultStateFetcher(ProcessApi processApi) {\n        this.processApi = processApi;\n    }\n\n    @Override\n    public void downloadState(JobRequest job) throws Exception {\n        try (InputStream is = ClientUtils.withRetry(AgentConstants.API_CALL_MAX_RETRIES, AgentConstants.API_CALL_RETRY_DELAY, () -> processApi.downloadState(job.getInstanceId()))){\n            ZipUtils.unzip(is, job.getPayloadDir(), StandardCopyOption.REPLACE_EXISTING);\n        }\n    }\n}\n\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/ExecutionException.java",
    "content": "package com.walmartlabs.concord.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic class ExecutionException extends Exception {\n\n    private static final long serialVersionUID = 1L;\n\n    public ExecutionException(String message) {\n        super(message);\n    }\n\n    public ExecutionException(String message, Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/JobInstance.java",
    "content": "package com.walmartlabs.concord.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface JobInstance {\n\n    /**\n     * Wait for the job to finish.\n     * Throws an {@link java.util.concurrent.ExecutionException} if the job finishes unsuccessfully.\n     */\n    void waitForCompletion() throws Exception;\n\n    /**\n     * Cancel the job (unless it's already cancelled or done).\n     */\n    void cancel();\n\n    /**\n     * Returns {@code true} if the job was cancelled using the {@link #cancel()} method.\n     */\n    boolean isCancelled();\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/JobRequest.java",
    "content": "package com.walmartlabs.concord.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.server.queueclient.message.ProcessResponse;\n\nimport java.nio.file.Path;\nimport java.util.UUID;\n\npublic class JobRequest {\n\n    public static JobRequest from(ProcessResponse resp, Path workDir) {\n        return new JobRequest(Type.RUNNER,\n                resp.getProcessId(),\n                workDir,\n                resp.getOrgName(),\n                resp.getRepoUrl(),\n                resp.getRepoPath(),\n                resp.getCommitId(),\n                resp.getRepoBranch(),\n                resp.getSecretName(),\n                resp.getImports(),\n                resp.getSessionToken());\n    }\n\n    private final Type type;\n    private final UUID instanceId;\n    private final Path payloadDir;\n    private final String orgName; // TODO rename to secretOrgName\n    private final String repoUrl;\n    private final String repoPath;\n    private final String commitId;\n    private final String repoBranch;\n    private final String secretName;\n    private final Imports imports;\n    private final String sessionToken;\n\n    protected JobRequest(JobRequest src) {\n        this(src.type,\n                src.instanceId,\n                src.payloadDir,\n                src.orgName,\n                src.repoUrl,\n                src.repoPath,\n                src.commitId,\n                src.repoBranch,\n                src.secretName,\n                src.imports,\n                src.sessionToken);\n    }\n\n    protected JobRequest(Type type,\n                         UUID instanceId,\n                         Path payloadDir,\n                         String orgName,\n                         String repoUrl,\n                         String repoPath,\n                         String commitId,\n                         String repoBranch,\n                         String secretName,\n                         Imports imports,\n                         String sessionToken) {\n\n        this.type = type;\n        this.instanceId = instanceId;\n        this.payloadDir = payloadDir;\n        this.orgName = orgName;\n        this.repoUrl = repoUrl;\n        this.repoPath = repoPath;\n        this.commitId = commitId;\n        this.repoBranch = repoBranch;\n        this.secretName = secretName;\n        this.imports = imports != null ? imports : Imports.builder().build();\n        this.sessionToken = sessionToken;\n    }\n\n    public Type getType() {\n        return type;\n    }\n\n    public UUID getInstanceId() {\n        return instanceId;\n    }\n\n    public Path getPayloadDir() {\n        return payloadDir;\n    }\n\n    public String getOrgName() {\n        return orgName;\n    }\n\n    public String getRepoUrl() {\n        return repoUrl;\n    }\n\n    public String getRepoPath() {\n        return repoPath;\n    }\n\n    public String getRepoBranch() {\n        return repoBranch;\n    }\n\n    public String getCommitId() {\n        return commitId;\n    }\n\n    public String getSecretName() {\n        return secretName;\n    }\n\n    public Imports getImports() {\n        return imports;\n    }\n\n    public String getSessionToken() {\n        return sessionToken;\n    }\n\n    @Override\n    public String toString() {\n        return \"JobRequest{\" +\n                \"type=\" + type +\n                \", instanceId=\" + instanceId +\n                \", payloadDir=\" + payloadDir +\n                \", orgName='\" + orgName + '\\'' +\n                \", repoUrl='\" + repoUrl + '\\'' +\n                \", repoPath='\" + repoPath + '\\'' +\n                \", commitId='\" + commitId + '\\'' +\n                \", repoBranch='\" + repoBranch + '\\'' +\n                \", secretName='\" + secretName + '\\'' +\n                \", imports='\" + imports + '\\'' +\n                '}';\n    }\n\n    public enum Type {\n\n        /**\n         * A concord-runner based job.\n         */\n        RUNNER\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/Main.java",
    "content": "package com.walmartlabs.concord.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport com.google.inject.Guice;\nimport org.eclipse.sisu.space.BeanScanning;\nimport org.eclipse.sisu.space.SpaceModule;\nimport org.eclipse.sisu.space.URLClassSpace;\nimport org.eclipse.sisu.wire.WireModule;\n\npublic class Main {\n\n    public static void main(String[] args) throws Exception {\n        // auto-wire all modules\n        var classLoader = Main.class.getClassLoader();\n        var modules = new WireModule(new SpaceModule(new URLClassSpace(classLoader), BeanScanning.GLOBAL_INDEX));\n        var injector = Guice.createInjector(modules);\n\n        if (args.length == 1) {\n            // one-shot mode - read ProcessResponse directly from the command line, execute the process and exit\n            // the current $PWD will be used as ${workDir}\n            var oneShotRunner = injector.getInstance(OneShotRunner.class);\n            oneShotRunner.run(args[0]);\n        } else if (args.length == 0) {\n            // agent mode - connect to the server's websocket and handle ProcessResponses\n            var agent = injector.getInstance(Agent.class);\n            agent.start();\n        } else {\n            throw new IllegalArgumentException(\"Specify the entire ProcessResponse JSON as the first argument to run in \" +\n                                               \"the one-shot mode or run without arguments for the default (agent) mode.\");\n        }\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/OneShotRunner.java",
    "content": "package com.walmartlabs.concord.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.google.inject.Injector;\nimport com.walmartlabs.concord.agent.cfg.AgentConfiguration;\nimport com.walmartlabs.concord.agent.guice.WorkerModule;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.server.queueclient.message.ProcessResponse;\n\nimport javax.inject.Inject;\n\nimport static java.util.Objects.requireNonNull;\n\npublic class OneShotRunner {\n\n    private final AgentConfiguration agentCfg;\n    private final ObjectMapper objectMapper;\n    private final Injector injector;\n\n    @Inject\n    public OneShotRunner(AgentConfiguration agentCfg,\n                         ObjectMapper objectMapper,\n                         Injector injector) {\n\n        this.agentCfg = requireNonNull(agentCfg);\n        this.objectMapper = requireNonNull(objectMapper);\n        this.injector = requireNonNull(injector);\n    }\n\n    public void run(String processResponseJson) throws Exception {\n        var processResponse = objectMapper.readValue(processResponseJson, ProcessResponse.class);\n        var workDir = PathUtils.createTempDir(agentCfg.getPayloadDir(), \"workDir\");\n        var jobRequest = JobRequest.from(processResponse, workDir);\n\n        var workerModule = new WorkerModule(agentCfg.getAgentId(), jobRequest.getInstanceId(), jobRequest.getSessionToken());\n        var workerFactory = injector.createChildInjector(workerModule).getInstance(WorkerFactory.class);\n        var worker = workerFactory.create(jobRequest, status -> {\n        });\n\n        worker.setThrowOnFailure(true);\n\n        try {\n            worker.run();\n        } catch (Exception e) {\n            System.exit(1);\n        }\n\n        System.exit(0);\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/RepositoryManager.java",
    "content": "package com.walmartlabs.concord.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.agent.cfg.GitConfiguration;\nimport com.walmartlabs.concord.agent.cfg.RepositoryCacheConfiguration;\nimport com.walmartlabs.concord.client2.SecretClient;\nimport com.walmartlabs.concord.imports.Import.SecretDefinition;\nimport com.walmartlabs.concord.repository.*;\nimport com.walmartlabs.concord.sdk.Secret;\nimport com.walmartlabs.concord.dependencymanager.DependencyManager;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.util.List;\n\npublic class RepositoryManager {\n\n    private static final Logger log = LoggerFactory.getLogger(RepositoryManager.class);\n\n    private final SecretClient secretClient;\n    private final RepositoryProviders providers;\n    private final RepositoryCache repositoryCache;\n    private final GitConfiguration gitCfg;\n\n    @Inject\n    public RepositoryManager(SecretClient secretClient,\n                             GitConfiguration gitCfg,\n                             RepositoryCacheConfiguration cacheCfg,\n                             ObjectMapper objectMapper,\n                             DependencyManager dependencyManager,\n                             AgentAuthTokenProvider agentAuthTokenProvider) throws IOException {\n\n        this.secretClient = secretClient;\n        this.gitCfg = gitCfg;\n\n        GitClientConfiguration clientCfg = GitClientConfiguration.builder()\n                .defaultOperationTimeout(gitCfg.getDefaultOperationTimeout())\n                .fetchTimeout(gitCfg.getFetchTimeout())\n                .httpLowSpeedLimit(gitCfg.getHttpLowSpeedLimit())\n                .httpLowSpeedTime(gitCfg.getHttpLowSpeedTime())\n                .sshTimeout(gitCfg.getSshTimeout())\n                .sshTimeoutRetryCount(gitCfg.getSshTimeoutRetryCount())\n                .maxGitCliOutputBytes(gitCfg.maxGitCliOutputBytes())\n                .build();\n\n        this.providers = new RepositoryProviders(List.of(\n                new MavenRepositoryProvider(dependencyManager),\n                new GitCliRepositoryProvider(clientCfg, agentAuthTokenProvider)\n        ));\n        this.repositoryCache = new RepositoryCache(cacheCfg.getCacheDir(),\n                cacheCfg.getInfoDir(),\n                cacheCfg.getLockTimeout(),\n                cacheCfg.getMaxAge(),\n                cacheCfg.getLockCount(),\n                objectMapper);\n    }\n\n    public void export(String repoUrl, String branch, String commitId, String repoPath, Path dest, SecretDefinition secretDefinition, List<String> ignorePatterns) throws ExecutionException {\n        if (gitCfg.isSkip()) {\n            log.info(\"Skipping git export, using local state\");\n            return;\n        }\n\n        Secret secret = getSecret(secretDefinition);\n\n        Path cacheDir = repositoryCache.getPath(repoUrl);\n\n        repositoryCache.withLock(repoUrl, () -> {\n            Repository repo = providers.fetch(\n                    FetchRequest.builder()\n                            .url(repoUrl)\n                            .version(FetchRequest.Version.commitWithBranch(commitId, branch))\n                            .secret(secret)\n                            .destination(cacheDir)\n                            .shallow(gitCfg.isShallowClone())\n                            .checkAlreadyFetched(gitCfg.isCheckAlreadyFetched())\n                            .build(),\n                    repoPath);\n            repo.export(dest, ignorePatterns);\n            return null;\n        });\n        repositoryCache.cleanup();\n    }\n\n    private Secret getSecret(SecretDefinition secret) throws ExecutionException {\n        if (secret == null) {\n            return null;\n        }\n\n        try {\n            return secretClient.getData(secret.org(), secret.name(), secret.password(), null);\n        } catch (Exception e) {\n            throw new ExecutionException(\"Error while retrieving a secret '\" + secret.name() + \"' in org '\" + secret.org() + \"': \" + e.getMessage(), e);\n        }\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/StateFetcher.java",
    "content": "package com.walmartlabs.concord.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface StateFetcher {\n\n    void downloadState(JobRequest jobRequest) throws Exception;\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/Utils.java",
    "content": "package com.walmartlabs.concord.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.List;\n\npublic final class Utils {\n\n    private static final Logger log = LoggerFactory.getLogger(Utils.class);\n\n    public static boolean kill(Process proc) {\n        return kill(proc.toHandle());\n    }\n\n    public static boolean kill(Process proc, boolean killDescendants) {\n        List<ProcessHandle> children = killDescendants ? proc.children().toList() : List.of();\n\n        // kill parent first which may gracefully clean up all descendents\n        boolean killed = kill(proc.toHandle());\n\n        // clean up orphaned processes that are still running\n        children.stream()\n                .flatMap(ProcessHandle::descendants)\n                .forEach(Utils::kill);\n\n        return killed;\n    }\n\n    private static boolean kill(ProcessHandle handle) {\n        if (!handle.isAlive()) {\n            return false;\n        }\n\n        String p = toString(handle);\n\n        log.info(\"kill ['{}'] -> attempting to stop...\", p);\n        handle.destroy();\n\n        if (handle.isAlive()) {\n            sleep(1000);\n        }\n\n        while (handle.isAlive()) {\n            log.warn(\"kill ['{}'] -> waiting for the process to die...\", p);\n            sleep(3000);\n            handle.destroyForcibly();\n        }\n\n        return true;\n    }\n\n    public static void sleep(long ms) {\n        try {\n            Thread.sleep(ms);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    private static String toString(ProcessHandle proc) {\n        try {\n            return \"pid=\" + proc.pid();\n        } catch (UnsupportedOperationException e) {\n            return proc.toString();\n        }\n    }\n\n    private Utils() {\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/Worker.java",
    "content": "package com.walmartlabs.concord.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agent.executors.JobExecutor;\nimport com.walmartlabs.concord.agent.guice.AgentImportManager;\nimport com.walmartlabs.concord.agent.logging.ProcessLog;\nimport com.walmartlabs.concord.agent.remote.ProcessStatusUpdater;\nimport com.walmartlabs.concord.client2.ProcessEntry.StatusEnum;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.imports.Import.SecretDefinition;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.UUID;\n\npublic class Worker implements Runnable {\n\n    private static final Logger log = LoggerFactory.getLogger(Worker.class);\n\n    private final RepositoryManager repositoryManager;\n    private final AgentImportManager importManager;\n    private final JobExecutor executor;\n    private final CompletionCallback completionCallback;\n    private final StateFetcher stateFetcher;\n    private final ProcessStatusUpdater processStatusUpdater;\n    private final ProcessLog processLog;\n    private final JobRequest jobRequest;\n\n    private JobInstance jobInstance;\n    private boolean throwOnFailure;\n\n    @Inject\n    public Worker(RepositoryManager repositoryManager,\n                  AgentImportManager importManager,\n                  JobExecutor executor,\n                  CompletionCallback completionCallback,\n                  StateFetcher stateFetcher,\n                  ProcessStatusUpdater processStatusUpdater,\n                  ProcessLog processLog,\n                  JobRequest jobRequest) {\n\n        this.repositoryManager = repositoryManager;\n        this.importManager = importManager;\n        this.executor = executor;\n        this.completionCallback = completionCallback;\n        this.stateFetcher = stateFetcher;\n        this.processStatusUpdater = processStatusUpdater;\n        this.processLog = processLog;\n        this.jobRequest = jobRequest;\n    }\n\n    @Override\n    public void run() {\n        log.info(\"run -> starting {}\", jobRequest);\n        UUID instanceId = jobRequest.getInstanceId();\n\n        try {\n            // fetch the git repo's data...\n            fetchRepo(jobRequest);\n            // ...and process imports\n            processImports(jobRequest);\n            // ...and download the saved process state from the server\n            downloadState(jobRequest);\n\n            // load the process' configuration\n            ConfiguredJobRequest configuredJobRequest = ConfiguredJobRequest.from(jobRequest);\n\n            // execute the job\n            jobInstance = executor.exec(configuredJobRequest);\n            jobInstance.waitForCompletion();\n\n            // successful completion\n            log.info(\"run -> done with {}\", configuredJobRequest);\n            onStatusChange(instanceId, StatusEnum.FINISHED);\n        } catch (Throwable e) {\n            // unwrap the exception if needed\n            Throwable t = unwrap(e);\n\n            // handle any error during the startup or the execution\n            handleError(instanceId, t);\n        } finally {\n            Path payloadDir = jobRequest.getPayloadDir();\n            try {\n                log.info(\"exec ['{}'] -> removing the payload directory: {}\", instanceId, payloadDir);\n                PathUtils.deleteRecursively(payloadDir);\n            } catch (IOException e) {\n                log.warn(\"exec ['{}'] -> can't remove the payload directory: {}\", instanceId, e.getMessage());\n            }\n        }\n    }\n\n    public void cancel() {\n        if (jobInstance == null) {\n            return;\n        }\n\n        jobInstance.cancel();\n    }\n\n    public void setThrowOnFailure(boolean throwOnFailure) {\n        this.throwOnFailure = throwOnFailure;\n    }\n\n    private void handleError(UUID instanceId, Throwable error) {\n        StatusEnum status = StatusEnum.FAILED;\n\n        if (jobInstance != null && jobInstance.isCancelled()) {\n            log.info(\"handleError ['{}'] -> job cancelled\", instanceId);\n            status = StatusEnum.CANCELLED;\n        } else {\n            log.error(\"handleError ['{}'] -> job failed\", instanceId, error);\n        }\n\n        onStatusChange(instanceId, status);\n        log.info(\"handleError ['{}'] -> done\", instanceId);\n\n        if (throwOnFailure) {\n            if (error instanceof RuntimeException re) {\n                throw re;\n            }\n            throw new RuntimeException(error);\n        }\n    }\n\n    private void onStatusChange(UUID instanceId, StatusEnum status) {\n        try {\n            processStatusUpdater.update(instanceId, status);\n        } finally {\n            completionCallback.onStatusChange(status);\n        }\n    }\n\n    private void fetchRepo(JobRequest r) throws Exception {\n        if (r.getRepoUrl() == null\n                || (r.getCommitId() == null && r.getRepoBranch() == null)\n                || r.getRepoUrl().startsWith(\"classpath://\")) {\n            return;\n        }\n\n        processLog.info(\"Exporting the repository data: {} @ {}:{}, path: {}\",\n                r.getRepoUrl(), r.getRepoBranch() != null ? r.getRepoBranch() : \"*\",\n                r.getCommitId(), r.getRepoPath() != null ? r.getRepoPath() : \"/\");\n\n        long dt;\n        try {\n            dt = withTimer(() -> repositoryManager.export(\n                    r.getRepoUrl(),\n                    r.getRepoBranch(),\n                    r.getCommitId(),\n                    r.getRepoPath(),\n                    r.getPayloadDir(),\n                    getSecret(r),\n                    Collections.emptyList()));\n        } catch (Exception e) {\n            processLog.error(\"Repository export error: {}\", e.getMessage(), e);\n            throw e;\n        }\n\n        processLog.info(\"Repository data export took {}ms\", dt);\n    }\n\n    private static SecretDefinition getSecret(JobRequest r) {\n        if (r.getSecretName() == null) {\n            return null;\n        }\n\n        return SecretDefinition.builder()\n                .org(r.getOrgName())\n                .name(r.getSecretName())\n                .build();\n    }\n\n    private void downloadState(JobRequest r) throws Exception {\n        processLog.info(\"Downloading the process state...\");\n\n        long dt;\n        try {\n            dt = withTimer(() -> stateFetcher.downloadState(r));\n        } catch (Exception e) {\n            processLog.error(\"State download error: {}\", e.getMessage());\n            throw e;\n        }\n\n        processLog.info(\"Process state download took {}ms\", dt);\n    }\n\n    private void processImports(JobRequest r) throws ExecutionException {\n        if (r.getImports().isEmpty()) {\n            return;\n        }\n\n        long dt;\n        try {\n            dt = withTimer(() -> importManager.process(r.getImports(), r.getPayloadDir()));\n        } catch (Exception e) {\n            processLog.error(\"Error while reading the process' imports: \" + e.getMessage());\n            throw new ExecutionException(\"Error while reading the process' imports\", e);\n        }\n\n        processLog.info(\"Import of external resources took {}ms\", dt);\n    }\n\n    private static Throwable unwrap(Throwable t) {\n        if (t instanceof ExecutionException && t.getCause() != null) {\n            t = t.getCause();\n        }\n\n        if (t instanceof RuntimeException && t.getCause() != null) {\n            t = t.getCause();\n        }\n\n        return t;\n    }\n\n    private static long withTimer(Fn f) throws Exception {\n        long t1 = System.currentTimeMillis();\n        f.apply();\n        long t2 = System.currentTimeMillis();\n        return t2 - t1;\n    }\n\n    private interface Fn {\n\n        void apply() throws Exception;\n    }\n\n    public interface CompletionCallback {\n\n        void onStatusChange(StatusEnum status);\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/WorkerFactory.java",
    "content": "package com.walmartlabs.concord.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agent.executors.JobExecutor;\nimport com.walmartlabs.concord.agent.executors.JobExecutorFactory;\nimport com.walmartlabs.concord.agent.guice.AgentImportManager;\nimport com.walmartlabs.concord.agent.logging.ProcessLog;\nimport com.walmartlabs.concord.agent.remote.ProcessStatusUpdater;\n\nimport javax.inject.Inject;\n\npublic class WorkerFactory {\n\n    private final RepositoryManager repositoryManager;\n    private final AgentImportManager importManager;\n    private final JobExecutorFactory jobExecutorFactory;\n    private final StateFetcher stateFetcher;\n    private final ProcessStatusUpdater statusUpdater;\n    private final ProcessLog processLog;\n\n    @Inject\n    public WorkerFactory(RepositoryManager repositoryManager,\n                         AgentImportManager importManager,\n                         JobExecutorFactory jobExecutorFactory,\n                         StateFetcher stateFetcher,\n                         ProcessStatusUpdater statusUpdater,\n                         ProcessLog processLog) {\n\n        this.repositoryManager = repositoryManager;\n        this.importManager = importManager;\n        this.jobExecutorFactory = jobExecutorFactory;\n        this.stateFetcher = stateFetcher;\n        this.statusUpdater = statusUpdater;\n        this.processLog = processLog;\n    }\n\n    public Worker create(JobRequest jobRequest, Worker.CompletionCallback completionCallback) {\n        JobExecutor executor = jobExecutorFactory.create(jobRequest.getType());\n\n        return new Worker(repositoryManager, importManager, executor, completionCallback, stateFetcher, statusUpdater, processLog, jobRequest);\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/cfg/AgentConfiguration.java",
    "content": "package com.walmartlabs.concord.agent.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.typesafe.config.Config;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.nio.file.Path;\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.TimeUnit;\n\nimport static com.walmartlabs.concord.agent.cfg.Utils.getOrCreatePath;\nimport static com.walmartlabs.concord.agent.cfg.Utils.getStringOrDefault;\n\npublic class AgentConfiguration {\n\n    private static final Logger log = LoggerFactory.getLogger(AgentConfiguration.class);\n\n    private final String agentId;\n    private final Map<String, Object> capabilities;\n\n    private final Path dependencyCacheDir;\n    private final Path dependencyListsDir;\n    private final Duration dependencyResolveTimeout;\n    private final boolean dependencyStrictRepositories;\n    private final List<String> dependencyExclusions;\n\n    private final Path payloadDir;\n    private final Path workDirBase;\n\n    private final Path logDir;\n    private final long logMaxDelay;\n    private final boolean workDirMasking;\n\n    private final int workersCount;\n    private final long pollInterval;\n    private final String maintenanceModeListenerHost;\n    private final int maintenanceModeListenerPort;\n\n    private final boolean explicitlyResolveV1Client;\n    private final boolean mavenOfflineMode;\n\n    @Inject\n    public AgentConfiguration(Config cfg) {\n        this.agentId = getStringOrDefault(cfg, \"id\", () -> UUID.randomUUID().toString());\n        log.info(\"Using agent ID: {}\", this.agentId);\n\n        this.capabilities = cfg.hasPath(\"capabilities\") ? cfg.getObject(\"capabilities\").unwrapped() : null;\n        log.info(\"Using the capabilities: {}\", this.capabilities);\n\n        this.dependencyCacheDir = getOrCreatePath(cfg, \"dependencyCacheDir\");\n        this.dependencyListsDir = getOrCreatePath(cfg, \"dependencyListsDir\");\n        this.dependencyResolveTimeout = cfg.hasPath(\"dependencyResolveTimeout\") ? cfg.getDuration(\"dependencyResolveTimeout\") : null;\n        this.dependencyStrictRepositories = cfg.hasPath(\"dependencyStrictRepositories\") && cfg.getBoolean(\"dependencyStrictRepositories\");\n        this.dependencyExclusions = cfg.getStringList(\"dependencyExclusions\");\n\n        this.payloadDir = getOrCreatePath(cfg, \"payloadDir\");\n        this.workDirBase = getOrCreatePath(cfg, \"workDirBase\");\n\n        this.logDir = getOrCreatePath(cfg, \"logDir\");\n        this.logMaxDelay = cfg.getDuration(\"logMaxDelay\", TimeUnit.MILLISECONDS);\n        this.workDirMasking = cfg.getBoolean(\"workDirMasking\");\n\n        this.workersCount = cfg.getInt(\"workersCount\");\n        this.maintenanceModeListenerHost = cfg.getString(\"maintenanceModeListenerHost\");\n        this.maintenanceModeListenerPort = cfg.getInt(\"maintenanceModeListenerPort\");\n\n        this.pollInterval = cfg.getDuration(\"pollInterval\", TimeUnit.MILLISECONDS);\n\n        this.explicitlyResolveV1Client = cfg.getBoolean(\"explicitlyResolveV1Client\");\n        this.mavenOfflineMode = cfg.getBoolean(\"mavenOfflineMode\");\n    }\n\n    public String getAgentId() {\n        return agentId;\n    }\n\n    public Map<String, Object> getCapabilities() {\n        return capabilities;\n    }\n\n    public Path getDependencyCacheDir() {\n        return dependencyCacheDir;\n    }\n\n    public Path getDependencyListsDir() {\n        return dependencyListsDir;\n    }\n\n    public Duration getDependencyResolveTimeout() {\n        return dependencyResolveTimeout;\n    }\n\n    public boolean dependencyStrictRepositories() {\n        return dependencyStrictRepositories;\n    }\n\n    public List<String> dependencyExclusions() {\n        return dependencyExclusions;\n    }\n\n    public Path getPayloadDir() {\n        return payloadDir;\n    }\n\n    public Path getWorkDirBase() {\n        return workDirBase;\n    }\n\n    public Path getLogDir() {\n        return logDir;\n    }\n\n    public long getLogMaxDelay() {\n        return logMaxDelay;\n    }\n\n    public boolean isWorkDirMaskings() {\n        return workDirMasking;\n    }\n\n    public int getWorkersCount() {\n        return workersCount;\n    }\n\n    public long getPollInterval() {\n        return pollInterval;\n    }\n\n    public String getMaintenanceModeListenerHost() {\n        return maintenanceModeListenerHost;\n    }\n\n    public int getMaintenanceModeListenerPort() {\n        return maintenanceModeListenerPort;\n    }\n\n    public boolean isExplicitlyResolveV1Client() {\n        return explicitlyResolveV1Client;\n    }\n\n    public boolean isMavenOfflineMode() {\n        return mavenOfflineMode;\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/cfg/DockerConfiguration.java",
    "content": "package com.walmartlabs.concord.agent.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.typesafe.config.Config;\n\nimport javax.inject.Inject;\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\n\npublic class DockerConfiguration {\n\n    private final String dockerHost;\n    private final boolean orphanSweeperEnabled;\n    private final long orphanSweeperPeriod;\n    private final List<String> extraVolumes;\n    private final boolean exposeDockerDaemon;\n\n    @Inject\n    public DockerConfiguration(Config cfg) {\n        this.dockerHost = cfg.getString(\"docker.host\");\n        this.orphanSweeperEnabled = cfg.getBoolean(\"docker.orphanSweeperEnabled\");\n        this.orphanSweeperPeriod = cfg.getDuration(\"docker.orphanSweeperPeriod\", TimeUnit.MILLISECONDS);\n        this.extraVolumes = cfg.getStringList(\"docker.extraVolumes\");\n        this.exposeDockerDaemon = cfg.getBoolean(\"docker.exposeDockerDaemon\");\n    }\n\n    public String getDockerHost() {\n        return dockerHost;\n    }\n\n    public boolean isOrphanSweeperEnabled() {\n        return orphanSweeperEnabled;\n    }\n\n    public long getOrphanSweeperPeriod() {\n        return orphanSweeperPeriod;\n    }\n\n    public List<String> getExtraVolumes() {\n        return extraVolumes;\n    }\n\n    public boolean exposeDockerDaemon() {\n        return exposeDockerDaemon;\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/cfg/GitConfiguration.java",
    "content": "package com.walmartlabs.concord.agent.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.typesafe.config.Config;\nimport com.walmartlabs.concord.common.cfg.MappingAuthConfig;\nimport com.walmartlabs.concord.common.cfg.OauthTokenConfig;\n\nimport javax.inject.Inject;\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.Optional;\n\nimport static com.walmartlabs.concord.agent.cfg.Utils.getStringOrDefault;\nimport static com.walmartlabs.concord.common.cfg.MappingAuthConfig.assertBaseUrlPattern;\n\npublic class GitConfiguration implements OauthTokenConfig {\n\n    private final String token;\n    private final String oauthUsername;\n    private final String oauthUrlPattern;\n    private final boolean shallowClone;\n    private final boolean checkAlreadyFetched;\n    private final Duration defaultOperationTimeout;\n    private final Duration fetchTimeout;\n    private final int httpLowSpeedLimit;\n    private final Duration httpLowSpeedTime;\n    private final Duration sshTimeout;\n    private final int sshTimeoutRetryCount;\n    private final boolean skip;\n    private final long maxGitCliOutputBytes;\n    private final List<? extends Config> authConfigs;\n\n    @Inject\n    public GitConfiguration(Config cfg) {\n        this.token = getStringOrDefault(cfg, \"git.oauth\", () -> null);\n        this.oauthUsername = getStringOrDefault(cfg, \"git.oauthUsername\", () -> null);\n        this.oauthUrlPattern = getStringOrDefault(cfg, \"git.oauthUrlPattern\", () -> null);\n        this.shallowClone = cfg.getBoolean(\"git.shallowClone\");\n        this.checkAlreadyFetched = cfg.getBoolean(\"git.checkAlreadyFetched\");\n        this.defaultOperationTimeout = cfg.getDuration(\"git.defaultOperationTimeout\");\n        this.fetchTimeout = cfg.getDuration(\"git.fetchTimeout\");\n        this.httpLowSpeedLimit = cfg.getInt(\"git.httpLowSpeedLimit\");\n        this.httpLowSpeedTime = cfg.getDuration(\"git.httpLowSpeedTime\");\n        this.sshTimeout = cfg.getDuration(\"git.sshTimeout\");\n        this.sshTimeoutRetryCount = cfg.getInt(\"git.sshTimeoutRetryCount\");\n        this.skip = cfg.getBoolean(\"git.skip\");\n        this.maxGitCliOutputBytes = cfg.getLong(\"git.maxGitCliOutputBytes\");\n        this.authConfigs = cfg.getConfigList(\"git.systemAuth\");\n    }\n\n    @Override\n    public Optional<String> getOauthToken() {\n        return Optional.ofNullable(token);\n    }\n\n    @Override\n    public Optional<String> getOauthUsername() {\n        return Optional.ofNullable(oauthUsername);\n    }\n\n    @Override\n    public Optional<String> getOauthUrlPattern() {\n        return Optional.ofNullable(oauthUrlPattern);\n    }\n\n    public boolean isShallowClone() {\n        return shallowClone;\n    }\n\n    public boolean isCheckAlreadyFetched() {\n        return checkAlreadyFetched;\n    }\n\n    public Duration getDefaultOperationTimeout() {\n        return defaultOperationTimeout;\n    }\n\n    public Duration getFetchTimeout() {\n        return fetchTimeout;\n    }\n\n    public int getHttpLowSpeedLimit() {\n        return httpLowSpeedLimit;\n    }\n\n    public Duration getHttpLowSpeedTime() {\n        return httpLowSpeedTime;\n    }\n\n    public Duration getSshTimeout() {\n        return sshTimeout;\n    }\n\n    public int getSshTimeoutRetryCount() {\n        return sshTimeoutRetryCount;\n    }\n\n    public boolean isSkip() {\n        return skip;\n    }\n\n    public long maxGitCliOutputBytes() {\n        return maxGitCliOutputBytes;\n    }\n\n    public List<MappingAuthConfig> getSystemAuth() {\n        return authConfigs.stream()\n                .map(o -> {\n                    AuthSource type = AuthSource.valueOf(o.getString(\"type\").toUpperCase());\n\n                    return (AuthConfig) switch (type) {\n                        case OAUTH_TOKEN -> OauthConfig.from(o);\n                    };\n                })\n                .map(AuthConfig::toGitAuth)\n                .toList();\n    }\n\n    enum AuthSource {\n        OAUTH_TOKEN\n    }\n\n    public interface AuthConfig {\n        MappingAuthConfig toGitAuth();\n    }\n\n    public record OauthConfig(String id, String urlPattern, String token) implements AuthConfig {\n\n        static OauthConfig from(Config cfg) {\n            return new OauthConfig(\n                    getStringOrDefault(cfg, \"id\", () -> \"system-oauth-token\"),\n                    cfg.getString(\"urlPattern\"),\n                    cfg.getString(\"token\")\n            );\n        }\n\n        @Override\n        public MappingAuthConfig.OauthAuthConfig toGitAuth() {\n            return MappingAuthConfig.OauthAuthConfig.builder()\n                    .id(this.id())\n                    .urlPattern(assertBaseUrlPattern(this.urlPattern()))\n                    .token(this.token())\n                    .build();\n        }\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/cfg/GitHubConfiguration.java",
    "content": "package com.walmartlabs.concord.agent.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.cfg.MappingAuthConfig;\nimport com.walmartlabs.concord.github.appinstallation.cfg.GitHubAppInstallationConfig;\n\nimport javax.inject.Inject;\nimport java.time.Duration;\nimport java.util.List;\n\npublic class GitHubConfiguration implements GitHubAppInstallationConfig {\n\n    private static final String CFG_APP_INSTALLATION = \"github.appInstallation\";\n\n    private final GitHubAppInstallationConfig appInstallation;\n\n    @Inject\n    public GitHubConfiguration(com.typesafe.config.Config config) {\n        if (config.hasPath(CFG_APP_INSTALLATION)) {\n            var raw = config.getConfig(CFG_APP_INSTALLATION);\n            this.appInstallation = GitHubAppInstallationConfig.fromConfig(raw);\n        } else {\n            this.appInstallation = GitHubAppInstallationConfig.builder()\n                    .authConfigs(List.of())\n                    .build();\n        }\n    }\n\n    @Override\n    public List<MappingAuthConfig> getAuthConfigs() {\n        return appInstallation.getAuthConfigs();\n    }\n\n    @Override\n    public Duration getSystemAuthCacheDuration() {\n        return appInstallation.getSystemAuthCacheDuration();\n    }\n\n    @Override\n    public long getSystemAuthCacheMaxWeight() {\n        return appInstallation.getSystemAuthCacheMaxWeight();\n    }\n\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/cfg/ImportConfiguration.java",
    "content": "package com.walmartlabs.concord.agent.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.typesafe.config.Config;\n\nimport javax.inject.Inject;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.Set;\n\npublic class ImportConfiguration {\n\n    private final Set<String> disabledProcessors;\n\n    @Inject\n    public ImportConfiguration(Config cfg) {\n        this.disabledProcessors = Collections.unmodifiableSet(new HashSet<>(cfg.getStringList(\"imports.disabledProcessors\")));\n    }\n\n    public Set<String> getDisabledProcessors() {\n        return disabledProcessors;\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/cfg/PreForkConfiguration.java",
    "content": "package com.walmartlabs.concord.agent.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.typesafe.config.Config;\n\nimport javax.inject.Inject;\nimport java.util.concurrent.TimeUnit;\n\npublic class PreForkConfiguration {\n\n    private final boolean enabled;\n    private final long maxAge;\n    private final int maxCount;\n\n    @Inject\n    public PreForkConfiguration(Config cfg) {\n        this.enabled = cfg.getBoolean(\"prefork.enabled\");\n        this.maxAge = cfg.getDuration(\"prefork.maxAge\", TimeUnit.MILLISECONDS);\n        this.maxCount = cfg.getInt(\"prefork.maxCount\");\n    }\n\n    public boolean isEnabled() {\n        return enabled;\n    }\n\n    public long getMaxAge() {\n        return maxAge;\n    }\n\n    public int getMaxCount() {\n        return maxCount;\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/cfg/RepositoryCacheConfiguration.java",
    "content": "package com.walmartlabs.concord.agent.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.typesafe.config.Config;\n\nimport javax.inject.Inject;\nimport java.nio.file.Path;\nimport java.time.Duration;\n\nimport static com.walmartlabs.concord.agent.cfg.Utils.getOrCreatePath;\n\npublic class RepositoryCacheConfiguration {\n\n    private final Path cacheDir;\n    private final Duration lockTimeout;\n    private final int lockCount;\n    private final Duration maxAge;\n    private final Path infoDir;\n\n    @Inject\n    public RepositoryCacheConfiguration(Config cfg) {\n        this.cacheDir = getOrCreatePath(cfg, \"repositoryCache.cacheDir\");\n        this.lockTimeout = cfg.getDuration(\"repositoryCache.lockTimeout\");\n        this.lockCount = cfg.getInt(\"repositoryCache.lockCount\");\n        this.maxAge = cfg.getDuration(\"repositoryCache.maxAge\");\n        this.infoDir = getOrCreatePath(cfg, \"repositoryCache.cacheInfoDir\");\n    }\n\n    public Path getCacheDir() {\n        return cacheDir;\n    }\n\n    public Duration getLockTimeout() {\n        return lockTimeout;\n    }\n\n    public int getLockCount() {\n        return lockCount;\n    }\n\n    public Duration getMaxAge() {\n        return maxAge;\n    }\n\n    public Path getInfoDir() {\n        return infoDir;\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/cfg/RuntimeConfiguration.java",
    "content": "package com.walmartlabs.concord.agent.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.typesafe.config.Config;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.agent.cfg.Utils.*;\nimport static java.util.stream.Collectors.joining;\n\npublic class RuntimeConfiguration {\n\n    private static final Logger log = LoggerFactory.getLogger(RuntimeConfiguration.class);\n\n    private final Map<String, Entry> configs;\n\n    @Inject\n    public RuntimeConfiguration(Config config) {\n        var runtimes = config.getObject(\"runtimes\");\n        var configs = new HashMap<String, Entry>();\n        for (var runtime : runtimes.keySet()) {\n            var cfg = Entry.parse(config.getConfig(\"runtimes.\" + runtime));\n            configs.put(runtime, cfg);\n        }\n        log.info(\"Available runtimes: {}\", configs.keySet().stream().sorted().collect(joining(\", \")));\n        this.configs = Collections.unmodifiableMap(configs);\n    }\n\n    public Optional<Entry> getForRuntime(String runtime) {\n        var cfg = configs.get(runtime);\n        return Optional.ofNullable(cfg);\n    }\n\n    public record Entry(Path path,\n                        Path cfgDir,\n                        String javaCmd,\n                        List<String> jvmParams,\n                        String mainClass,\n                        Path persistentWorkDir,\n                        boolean cleanRunnerDescendants,\n                        boolean segmentedLogs) {\n\n        public static Entry parse(Config cfg) {\n            var pathString = getStringOrDefault(cfg, \"path\", () -> {\n                // support local development, use .properties files to get JAR paths\n                // the .properties files are populated during build\n\n                if (!cfg.hasPath(\"propertiesFile\")) {\n                    throw new IllegalStateException(\".path or .propertiesFile are required\");\n                }\n                var fallback = cfg.getString(\"propertiesFile\");\n\n                var props = new Properties();\n                try (var in = Entry.class.getResourceAsStream(fallback)) {\n                    if (in == null) {\n                        throw new IllegalStateException(\"Resource not found: \" + fallback);\n                    }\n                    props.load(in);\n                } catch (IOException e) {\n                    throw new RuntimeException(e);\n                }\n\n                return props.getProperty(\"path\");\n            });\n            var path = Paths.get(pathString);\n            var cfgDir = getOrCreatePath(cfg, \"cfgDir\");\n            var javaCmd = getJavaCmd(cfg);\n            var jvmParams = cfg.getStringList(\"jvmParams\");\n            var mainClass = cfg.getString(\"mainClass\");\n            var persistentWorkDir = getOptionalAbsolutePath(cfg, \"persistentWorkDir\");\n            var cleanRunnerDescendants = cfg.getBoolean(\"cleanRunnerDescendants\");\n            var segmentedLogs = cfg.getBoolean(\"segmentedLogs\");\n            return new Entry(path, cfgDir, javaCmd, jvmParams, mainClass, persistentWorkDir, cleanRunnerDescendants, segmentedLogs);\n        }\n\n        private static String getJavaCmd(Config cfg) {\n            var path = \"javaCmd\";\n\n            if (cfg.hasPath(path)) {\n                var s = cfg.getString(path);\n                if (s != null) {\n                    return s;\n                }\n            }\n\n            var javaHome = System.getProperty(\"java.home\");\n            if (javaHome != null) {\n                return javaHome + \"/bin/java\";\n            }\n\n            return \"java\";\n        }\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/cfg/ServerConfiguration.java",
    "content": "package com.walmartlabs.concord.agent.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.typesafe.config.Config;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.util.concurrent.TimeUnit;\n\nimport static com.walmartlabs.concord.agent.cfg.Utils.getStringOrDefault;\n\npublic class ServerConfiguration {\n\n    private static final Logger log = LoggerFactory.getLogger(ServerConfiguration.class);\n\n    private final String apiBaseUrl;\n    private final String[] websocketUrls;\n    private final String apiKey;\n    private final long pingInterval;\n    private final long maxNoActivityPeriod;\n    private final boolean verifySsl;\n    private final long connectTimeout;\n    private final long readTimeout;\n    private final String userAgent;\n    private final long maxNoHeartbeatInterval;\n    private final long processRequestDelay;\n    private final long reconnectDelay;\n\n    @Inject\n    public ServerConfiguration(Config cfg, AgentConfiguration agentCfg) {\n        this.apiBaseUrl = cfg.getString(\"server.apiBaseUrl\");\n        log.info(\"Using the Server's API address: {}\", apiBaseUrl);\n\n        this.websocketUrls = getWebsocketUrls(cfg);\n        log.info(\"Using the Server's websocket addresses: {}\", (Object[]) websocketUrls);\n\n        this.apiKey = cfg.getString(\"server.apiKey\");\n        if (this.apiKey == null || this.apiKey.trim().isEmpty()) {\n            throw new IllegalArgumentException(\"Configuration is missing value for server.apiKey!\");\n        }\n\n        this.pingInterval = cfg.getDuration(\"server.websocketPingInterval\", TimeUnit.MILLISECONDS);\n        this.maxNoActivityPeriod = cfg.getDuration(\"server.websocketMaxNoActivityPeriod\", TimeUnit.MILLISECONDS);\n\n        this.verifySsl = cfg.getBoolean(\"server.verifySsl\");\n\n        this.connectTimeout = cfg.getDuration(\"server.connectTimeout\", TimeUnit.MILLISECONDS);\n        this.readTimeout = cfg.getDuration(\"server.readTimeout\", TimeUnit.MILLISECONDS);\n        this.userAgent = getStringOrDefault(cfg, \"server.userAgent\", () -> \"Concord-Agent: id=\" + agentCfg.getAgentId());\n\n        this.maxNoHeartbeatInterval = cfg.getDuration(\"server.maxNoHeartbeatInterval\", TimeUnit.MILLISECONDS);\n\n        this.processRequestDelay = cfg.getDuration(\"server.processRequestDelay\", TimeUnit.MILLISECONDS);\n        this.reconnectDelay = cfg.getDuration(\"server.reconnectDelay\", TimeUnit.MILLISECONDS);\n    }\n\n    public String getApiBaseUrl() {\n        return apiBaseUrl;\n    }\n\n    public String[] getWebsocketUrls() {\n        return websocketUrls;\n    }\n\n    public String getApiKey() {\n        return apiKey;\n    }\n\n    public long getPingInterval() {\n        return pingInterval;\n    }\n\n    public long getMaxNoActivityPeriod() {\n        return maxNoActivityPeriod;\n    }\n\n    public boolean isVerifySsl() {\n        return verifySsl;\n    }\n\n    public long getConnectTimeout() {\n        return connectTimeout;\n    }\n\n    public long getReadTimeout() {\n        return readTimeout;\n    }\n\n    public String getUserAgent() {\n        return userAgent;\n    }\n\n    public long getMaxNoHeartbeatInterval() {\n        return maxNoHeartbeatInterval;\n    }\n\n    public long getProcessRequestDelay() {\n        return processRequestDelay;\n    }\n\n    public long getReconnectDelay() {\n        return reconnectDelay;\n    }\n\n    private static String[] getWebsocketUrls(Config cfg) {\n        // we had a silly typo (\"websockeR\") in our configs, so for backward compatibility we must check the old variant first\n        String oldKey = \"server.websockerUrl\";\n        if (cfg.hasPath(oldKey)) {\n            String[] as = getCSV(cfg.getString(oldKey));\n            if (as != null) {\n                return as;\n            }\n        }\n\n        return getCSV(cfg.getString(\"server.websocketUrl\"));\n    }\n\n    private static String[] getCSV(String s) {\n        if (s == null) {\n            return null;\n        }\n\n        String[] as = s.split(\",\");\n        for (int i = 0; i < as.length; i++) {\n            as[i] = as[i].trim();\n        }\n\n        return as;\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/cfg/Utils.java",
    "content": "package com.walmartlabs.concord.agent.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.typesafe.config.Config;\nimport com.walmartlabs.concord.common.PathUtils;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.function.Supplier;\n\npublic final class Utils {\n\n    public static String getStringOrDefault(Config cfg, String key, Supplier<String> defaultValueSupplier) {\n        if (cfg.hasPath(key)) {\n            return cfg.getString(key);\n        }\n        return defaultValueSupplier.get();\n    }\n\n    public static Path getOptionalAbsolutePath(Config cfg, String key) {\n        if (!cfg.hasPath(key)) {\n            return null;\n        }\n\n        String s = cfg.getString(key).trim();\n        if (s.isEmpty()) {\n            return null;\n        }\n\n        if (!s.startsWith(\"/\")) {\n            throw new IllegalArgumentException(key + \" must be an absolute path, got: \" + s);\n        }\n\n        return Paths.get(s);\n    }\n\n    public static Path getOrCreatePath(Config cfg, String key) {\n        try {\n            if (!cfg.hasPath(key)) {\n                return PathUtils.createTempDir(key);\n            }\n\n            String value = cfg.getString(key);\n            if (value.startsWith(\"/\")) {\n                Path p = Paths.get(value)\n                        .normalize()\n                        .toAbsolutePath();\n\n                if (!Files.exists(p)) {\n                    Files.createDirectories(p);\n                }\n\n                return p;\n            }\n            return PathUtils.createTempDir(value);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private Utils() {\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/docker/OrphanSweeper.java",
    "content": "package com.walmartlabs.concord.agent.docker;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.DockerProcessBuilder;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.Function;\n\npublic class OrphanSweeper implements Runnable {\n\n    private static final Logger log = LoggerFactory.getLogger(OrphanSweeper.class);\n\n    private static final String[] PS_CMD = {\"docker\", \"ps\", \"-a\",\n            \"--filter\", \"label=\" + DockerProcessBuilder.CONCORD_TX_ID_LABEL,\n            \"--format\", \"{{.Label \\\"\" + DockerProcessBuilder.CONCORD_TX_ID_LABEL + \"\\\"}} {{.ID}}\"};\n\n    private static final long RETRY_DELAY = TimeUnit.SECONDS.toMillis(30);\n\n    private final StatusChecker statusChecker;\n    private final long period;\n\n    public OrphanSweeper(StatusChecker statusChecker, long period) {\n        this.statusChecker = statusChecker;\n        this.period = period;\n    }\n\n    @Override\n    public void run() {\n        log.info(\"run -> removing orphaned Docker containers...\");\n\n        while (!Thread.currentThread().isInterrupted()) {\n            try {\n                Map<UUID, String> containers = findContainers();\n                log.debug(\"run -> found {} container(s)...\", containers.size());\n\n                for (Map.Entry<UUID, String> c : containers.entrySet()) {\n                    UUID instanceId = c.getKey();\n                    if (statusChecker.isAlive(instanceId)) {\n                        continue;\n                    }\n\n                    String cId = c.getValue();\n                    log.warn(\"run -> found an orphaned container {} (process {}), attempting to kill...\", cId, instanceId);\n                    killContainer(cId);\n                }\n\n                sleep(period);\n            } catch (InterruptedException e) {\n                Thread.currentThread().interrupt();\n            } catch (Exception e) {\n                log.warn(\"run -> error: {}, retrying in {}ms...\", e.getMessage(), RETRY_DELAY);\n                sleep(RETRY_DELAY);\n            }\n        }\n    }\n\n    private static void sleep(long ms) {\n        try {\n            Thread.sleep(ms);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    private static Map<UUID, String> findContainers() throws IOException, InterruptedException {\n        Map<UUID, String> ids = new HashMap<>();\n        exec(PS_CMD, line -> {\n            int idx = line.indexOf(\" \");\n            if (idx < 0 || idx + 1 >= line.length()) {\n                log.warn(\"findContainers -> invalid line: {}\", line);\n                return null;\n            }\n\n            UUID k = UUID.fromString(line.substring(0, idx));\n            String v = line.substring(idx + 1);\n\n            ids.put(k, v);\n\n            return null;\n        });\n        return ids;\n    }\n\n    private static void killContainer(String cId) throws IOException, InterruptedException {\n        Process b = new ProcessBuilder()\n                .command(createKillCommand(cId))\n                .start();\n\n        int code = b.waitFor();\n        if (code != 0) {\n            throw new IOException(\"Error while removing a container \" + cId + \": docker exit code \" + code);\n        }\n\n        log.info(\"killContainer -> done, {} removed\", cId);\n    }\n\n    private static String[] createKillCommand(String cId) {\n        return new String[]{\"docker\", \"rm\", \"-f\", cId};\n    }\n\n    private static void exec(String[] cmd, Function<String, Void> callback) throws IOException, InterruptedException {\n        Process b = new ProcessBuilder()\n                .command(cmd)\n                .redirectErrorStream(true)\n                .start();\n\n        int code = b.waitFor();\n        if (code != 0) {\n            throw new IOException(\"Error while executing a command \" + String.join(\" \", cmd) + \" : docker exit code \" + code);\n        }\n\n        try (BufferedReader reader = new BufferedReader(new InputStreamReader(b.getInputStream()))) {\n            String line;\n            while ((line = reader.readLine()) != null) {\n                if (line.trim().isEmpty()) {\n                    continue;\n                }\n\n                callback.apply(line);\n            }\n        }\n    }\n\n    public interface StatusChecker {\n\n        boolean isAlive(UUID instanceId);\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/executors/JobExecutor.java",
    "content": "package com.walmartlabs.concord.agent.executors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agent.ConfiguredJobRequest;\nimport com.walmartlabs.concord.agent.JobInstance;\n\npublic interface JobExecutor {\n\n    JobInstance exec(ConfiguredJobRequest jobRequest) throws Exception;\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/executors/JobExecutorFactory.java",
    "content": "package com.walmartlabs.concord.agent.executors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agent.ConfiguredJobRequest;\nimport com.walmartlabs.concord.agent.JobRequest;\nimport com.walmartlabs.concord.agent.cfg.*;\nimport com.walmartlabs.concord.agent.executors.runner.DefaultDependencies;\nimport com.walmartlabs.concord.agent.executors.runner.ProcessPool;\nimport com.walmartlabs.concord.agent.executors.runner.RunnerJobExecutor;\nimport com.walmartlabs.concord.agent.logging.ProcessLog;\nimport com.walmartlabs.concord.agent.logging.ProcessLogFactory;\nimport com.walmartlabs.concord.agent.remote.AttachmentsUploader;\nimport com.walmartlabs.concord.dependencymanager.DependencyManager;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\n\nimport javax.inject.Inject;\nimport javax.inject.Singleton;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\n\n@Singleton\npublic class JobExecutorFactory {\n\n    private final AgentConfiguration agentCfg;\n    private final ServerConfiguration serverCfg;\n    private final DockerConfiguration dockerCfg;\n    private final PreForkConfiguration preForkCfg;\n\n    private final RuntimeConfiguration runtimeCfg;\n\n    private final DependencyManager dependencyManager;\n    private final DefaultDependencies defaultDependencies;\n    private final ProcessPool processPool;\n    private final ProcessLog processLog;\n    private final AttachmentsUploader attachmentsUploader;\n    private final ProcessLogFactory processLogFactory;\n\n    private final ExecutorService executor;\n\n    @Inject\n    public JobExecutorFactory(AgentConfiguration agentCfg,\n                              ServerConfiguration serverCfg,\n                              DockerConfiguration dockerCfg,\n                              PreForkConfiguration preForkCfg,\n                              RuntimeConfiguration runtimeCfg,\n                              DependencyManager dependencyManager,\n                              DefaultDependencies defaultDependencies,\n                              ProcessPool processPool,\n                              ProcessLog processLog,\n                              AttachmentsUploader attachmentsUploader,\n                              ProcessLogFactory processLogFactory) {\n\n        this.agentCfg = agentCfg;\n        this.serverCfg = serverCfg;\n        this.dockerCfg = dockerCfg;\n        this.preForkCfg = preForkCfg;\n        this.runtimeCfg = runtimeCfg;\n        this.dependencyManager = dependencyManager;\n        this.defaultDependencies = defaultDependencies;\n        this.processPool = processPool;\n        this.processLog = processLog;\n        this.attachmentsUploader = attachmentsUploader;\n        this.processLogFactory = processLogFactory;\n\n        this.executor = Executors.newCachedThreadPool();\n    }\n\n    public JobExecutor create(JobRequest.Type jobType) {\n        if (jobType != JobRequest.Type.RUNNER) {\n            throw new RuntimeException(\"Unsupported job type: \" + jobType);\n        }\n\n        return jobRequest -> {\n            String runtimeName = getRuntimeName(jobRequest);\n\n            RuntimeConfiguration.Entry runtimeCfg = this.runtimeCfg.getForRuntime(runtimeName)\n                    .orElseThrow(() -> new IllegalStateException(\"Runner configuration for '%s' not found.\".formatted(runtimeName)));\n\n            processLog.info(\"Runtime: {}\", runtimeName);\n\n            RunnerJobExecutor.RunnerJobExecutorConfiguration runnerExecutorCfg = RunnerJobExecutor.RunnerJobExecutorConfiguration.builder()\n                    .agentId(agentCfg.getAgentId())\n                    .serverApiBaseUrl(serverCfg.getApiBaseUrl())\n                    .javaCmd(runtimeCfg.javaCmd())\n                    .jvmParams(runtimeCfg.jvmParams())\n                    .dependencyListDir(agentCfg.getDependencyListsDir())\n                    .dependencyCacheDir(agentCfg.getDependencyCacheDir())\n                    .dependencyResolveTimeout(agentCfg.getDependencyResolveTimeout())\n                    .workDirBase(agentCfg.getWorkDirBase())\n                    .runnerPath(runtimeCfg.path())\n                    .runnerCfgDir(runtimeCfg.cfgDir())\n                    .runnerSecurityManagerEnabled(false) // SecurityManager is deprecated and should not be used\n                    .runnerMainClass(runtimeCfg.mainClass())\n                    .extraDockerVolumes(dockerCfg.getExtraVolumes())\n                    .exposeDockerDaemon(dockerCfg.exposeDockerDaemon())\n                    .maxHeartbeatInterval(serverCfg.getMaxNoHeartbeatInterval())\n                    .segmentedLogs(runtimeCfg.segmentedLogs())\n                    .workDirMasking(agentCfg.isWorkDirMaskings())\n                    .persistentWorkDir(runtimeCfg.persistentWorkDir())\n                    .preforkEnabled(preForkCfg.isEnabled())\n                    .cleanRunnerDescendants(runtimeCfg.cleanRunnerDescendants())\n                    .build();\n\n            JobExecutor delegate = new RunnerJobExecutor(runnerExecutorCfg, dependencyManager, defaultDependencies, attachmentsUploader, processPool, processLogFactory, executor);\n            return delegate.exec(jobRequest);\n        };\n    }\n\n    private static String getRuntimeName(ConfiguredJobRequest req) {\n        return MapUtils.getString(req.getProcessCfg(), Constants.Request.RUNTIME_KEY, \"concord-v1\");\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/executors/runner/DefaultDependencies.java",
    "content": "package com.walmartlabs.concord.agent.executors.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport java.util.stream.Stream;\n\npublic class DefaultDependencies {\n\n    private static final Logger log = LoggerFactory.getLogger(DefaultDependencies.class);\n    private static final String CFG_KEY = \"DEFAULT_DEPS_CFG\";\n\n    private final List<URI> dependencies;\n\n    public DefaultDependencies() {\n        String path = System.getenv(CFG_KEY);\n        if (path != null) {\n            try (Stream<String> lines = Files.lines(Paths.get(path))) {\n                this.dependencies = lines.filter(s -> !s.isBlank())\n                        .map(DefaultDependencies::parseUri)\n                        .toList();\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n\n            log.info(\"init -> using external default dependencies configuration: {}\", path);\n        } else {\n            try (InputStream is = DefaultDependencies.class.getResourceAsStream(\"default-dependencies\")) {\n                if (is == null) {\n                    throw new RuntimeException(\"Can't find com/walmartlabs/concord/agent/executors/runner/default-dependencies. \" +\n                                               \"This is most likely a bug or an issue with the local build and/or classpath.\");\n                }\n                this.dependencies = new BufferedReader(new InputStreamReader(is)).lines()\n                        .filter(s -> !s.isBlank())\n                        .map(DefaultDependencies::parseUri).toList();\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n\n            log.info(\"init -> using classpath default dependencies configuration\");\n        }\n    }\n\n    public List<URI> getDependencies() {\n        return dependencies;\n    }\n\n    private static URI parseUri(String s) {\n        try {\n            return new URI(s);\n        } catch (URISyntaxException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/executors/runner/JobDependencies.java",
    "content": "package com.walmartlabs.concord.agent.executors.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agent.ExecutionException;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.*;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.dependencymanager.DependencyManager.MAVEN_SCHEME;\nimport static com.walmartlabs.concord.policyengine.DependencyVersionsPolicy.Dependency;\n\npublic final class JobDependencies {\n\n    private static final Logger log = LoggerFactory.getLogger(JobDependencies.class);\n\n    public static Collection<URI> get(RunnerJob job) throws ExecutionException {\n        Collection<URI> uris = getDependencyUris(job);\n        if (uris.isEmpty()) {\n            return Collections.emptyList();\n        }\n\n        Map<String, String> versions = getDependencyVersions(job);\n        if (versions.isEmpty()) {\n            return uris;\n        }\n\n        return updateVersions(job, uris, versions);\n    }\n\n    private static Collection<URI> updateVersions(RunnerJob job, Collection<URI> uris, Map<String, String> versions) {\n        List<URI> result = new ArrayList<>();\n        for (URI item : uris) {\n            String scheme = item.getScheme();\n            if (MAVEN_SCHEME.equalsIgnoreCase(scheme)) {\n                IdAndVersion idv = IdAndVersion.parse(item.getAuthority());\n                if (isLatestVersion(idv.version)) {\n                    String version = versions.get(idv.id);\n                    if (version != null) {\n                        item = URI.create(MAVEN_SCHEME + \"://\" + idv.id + \":\" + assertVersion(idv.id, versions));\n                    } else {\n                        job.getLog().warn(\"Can't determine the version of {}, using as-is...\", item);\n                    }\n                }\n            }\n\n            result.add(item);\n        }\n\n        return result;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Collection<URI> getDependencyUris(RunnerJob job) throws ExecutionException {\n        try {\n            Map<String, Object> m = job.getProcessCfg();\n            Collection<String> deps = (Collection<String>) m.get(Constants.Request.DEPENDENCIES_KEY);\n            return normalizeUrls(deps);\n        } catch (URISyntaxException | IOException e) {\n            throw new ExecutionException(\"Error while reading the list of dependencies: \" + e.getMessage(), e);\n        }\n    }\n\n    private static Collection<URI> normalizeUrls(Collection<String> urls) throws IOException, URISyntaxException {\n        if (urls == null || urls.isEmpty()) {\n            return Collections.emptySet();\n        }\n\n        Collection<URI> result = new HashSet<>();\n\n        for (String s : urls) {\n            URI u = new URI(s);\n            String scheme = u.getScheme();\n\n            if (MAVEN_SCHEME.equalsIgnoreCase(scheme)) {\n                result.add(u);\n                continue;\n            }\n\n            if (scheme == null || scheme.trim().isEmpty()) {\n                throw new IOException(\"Invalid dependency URL. Missing URL scheme: \" + s);\n            }\n\n            if (s.endsWith(\".jar\")) {\n                result.add(u);\n                continue;\n            }\n\n            URL url = u.toURL();\n            while (true) {\n                if (\"http\".equalsIgnoreCase(scheme) || \"https\".equalsIgnoreCase(scheme)) {\n                    URLConnection conn = url.openConnection();\n                    if (conn instanceof HttpURLConnection) {\n                        HttpURLConnection httpConn = (HttpURLConnection) conn;\n                        httpConn.setInstanceFollowRedirects(false);\n\n                        int code = httpConn.getResponseCode();\n                        if (code == HttpURLConnection.HTTP_MOVED_TEMP ||\n                                code == HttpURLConnection.HTTP_MOVED_PERM ||\n                                code == HttpURLConnection.HTTP_SEE_OTHER ||\n                                code == 307) {\n\n                            String location = httpConn.getHeaderField(\"Location\");\n                            url = new URL(location);\n                            log.info(\"normalizeUrls -> using: {}\", location);\n\n                            continue;\n                        }\n\n                        u = url.toURI();\n                    } else {\n                        log.warn(\"normalizeUrls -> unexpected connection type: {} (for {})\", conn.getClass(), s);\n                    }\n                }\n\n                break;\n            }\n\n            result.add(u);\n        }\n\n        return result;\n    }\n\n    private static Map<String, String> getDependencyVersions(RunnerJob job) throws ExecutionException {\n        Map<String, String> result = getDependencyVersionsFromFile(job);\n\n        PolicyEngine pe = job.getPolicyEngine();\n        if (pe != null) {\n            result = new HashMap<>(result); // make mutable\n            result.putAll(pe.getDefaultDependencyVersionsPolicy().get().stream()\n                    .collect(Collectors.toMap(Dependency::getArtifact, Dependency::getVersion)));\n        }\n\n\n        return result;\n    }\n\n    private static Map<String, String> getDependencyVersionsFromFile(RunnerJob job) throws ExecutionException {\n        Path workDir = job.getPayloadDir();\n        Path pluginsFile = workDir.resolve(Constants.Files.CONCORD_SYSTEM_DIR_NAME)\n                .resolve(Constants.Files.DEPENDENCY_VERSIONS_FILE_NAME);\n\n        if (!Files.exists(pluginsFile)) {\n            return Collections.emptyMap();\n        }\n\n        try (InputStream is = Files.newInputStream(pluginsFile)) {\n            Map<String, String> result = new HashMap<>();\n            Properties p = new Properties();\n            p.load(is);\n            for (String name : p.stringPropertyNames()) {\n                result.put(name, p.getProperty(name));\n            }\n            return result;\n        } catch (IOException e) {\n            throw new ExecutionException(\"Error while reading default dependency versions: \" + e.getMessage(), e);\n        }\n    }\n\n    private static boolean isLatestVersion(String v) {\n        return v.equalsIgnoreCase(\"latest\");\n    }\n\n    private static String assertVersion(String dep, Map<String, String> versions) {\n        String version = versions.get(dep);\n        if (version != null) {\n            return version;\n        }\n        throw new IllegalArgumentException(\"Unofficial dependency '\" + dep + \"': version is required\");\n    }\n\n    private static class IdAndVersion {\n\n        public static IdAndVersion parse(String s) {\n            int i = s.lastIndexOf(':');\n            if (i >= 0 && i + 1 < s.length()) {\n                String id = s.substring(0, i);\n                String v = s.substring(i + 1);\n                return new IdAndVersion(id, v);\n            }\n\n            throw new IllegalArgumentException(\"Invalid artifact ID format: \" + s);\n        }\n\n        private final String id;\n        private final String version;\n\n        private IdAndVersion(String id, String version) {\n            this.id = id;\n            this.version = version;\n        }\n    }\n\n    private JobDependencies() {\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/executors/runner/ProcessPool.java",
    "content": "package com.walmartlabs.concord.agent.executors.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.hash.HashCode;\nimport com.walmartlabs.concord.agent.ExecutionException;\nimport com.walmartlabs.concord.agent.Utils;\nimport com.walmartlabs.concord.agent.cfg.PreForkConfiguration;\nimport com.walmartlabs.concord.common.PathUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\n\npublic class ProcessPool {\n\n    private static final Logger log = LoggerFactory.getLogger(ProcessPool.class);\n\n    private static final long CLEANUP_PERIOD = 30000;\n\n    private final long maxEntryAge;\n    private final int maxEntryCount;\n    private final Map<HashCode, Queue<ProcessEntry>> pool = new HashMap<>();\n    private final ExecutorService executor = Executors.newCachedThreadPool();\n\n    @Inject\n    public ProcessPool(PreForkConfiguration cfg) {\n        this.maxEntryAge = cfg.getMaxAge();\n        this.maxEntryCount = cfg.getMaxCount();\n        init();\n    }\n\n    public void init() {\n        Thread t = new Thread(() -> {\n            log.info(\"run -> starting cleanup thread, max entry age {}ms, max entry count {}\", maxEntryAge, maxEntryCount);\n\n            while (!Thread.currentThread().isInterrupted()) {\n                Utils.sleep(CLEANUP_PERIOD);\n\n                try {\n                    maintenance();\n                } catch (Exception e) {\n                    log.warn(\"pool -> error while performing maintenance: {}\", e.getMessage());\n                }\n            }\n        }, \"process-pool-cleanup\");\n\n        t.start();\n    }\n\n    public ProcessEntry take(HashCode hc, ProcessLauncher launcher) throws ExecutionException {\n        synchronized (pool) {\n            Queue<ProcessEntry> q = pool.computeIfAbsent(hc, k -> new LinkedList<>());\n\n            ProcessEntry entry = q.poll();\n            if (entry == null) {\n                try {\n                    entry = launcher.start();\n                } catch (IOException e) {\n                    throw new ExecutionException(\"Error while starting a new process\", e);\n                }\n\n                log.info(\"take -> started a new process: {}\", entry.workDir);\n            } else {\n                log.info(\"take -> using a pre-forked instance: {}\", entry.workDir);\n            }\n\n            executor.submit(() -> populate(hc, launcher));\n\n            return entry;\n        }\n    }\n\n    private void populate(HashCode hc, ProcessLauncher launcher) {\n        synchronized (pool) {\n            // calculate the total number of processes in the pool\n            int total = 0;\n            for (Map.Entry<HashCode, Queue<ProcessEntry>> e : pool.entrySet()) {\n                total += e.getValue().size();\n            }\n\n            if (total >= maxEntryCount) {\n                // mark the oldest entry for removal\n                ProcessEntry oldest = null;\n                for (Map.Entry<HashCode, Queue<ProcessEntry>> q : pool.entrySet()) {\n                    for (ProcessEntry e : q.getValue()) {\n                        if (oldest == null || oldest.timestamp > e.timestamp) {\n                            oldest = e;\n                        }\n                    }\n                }\n\n                // it's never null\n                assert oldest != null;\n\n                oldest.remove = true;\n            }\n\n            // add a new pool entry\n            Queue<ProcessEntry> q = pool.computeIfAbsent(hc, k -> new LinkedList<>());\n            try {\n                q.add(launcher.start());\n            } catch (IOException e) {\n                log.error(\"populate -> error while starting a new process\", e);\n            }\n        }\n    }\n\n    private void maintenance() {\n        List<HashCode> queuesToRemove = new ArrayList<>();\n        List<ProcessEntry> processesToKill = new ArrayList<>();\n\n        long t = System.currentTimeMillis();\n\n        synchronized (pool) {\n            pool.forEach((hc, q) -> {\n                q.removeIf(e -> {\n                    if (e.remove || t - e.timestamp >= maxEntryAge) {\n                        processesToKill.add(e);\n                        return true;\n                    }\n                    return false;\n                });\n\n                if (q.isEmpty()) {\n                    queuesToRemove.add(hc);\n                }\n            });\n\n            for (HashCode hc : queuesToRemove) {\n                pool.remove(hc);\n            }\n        }\n\n        log.info(\"maintenance -> removed {} queues\", queuesToRemove.size());\n\n        for (ProcessEntry p : processesToKill) {\n            Utils.kill(p.process);\n            cleanup(p);\n        }\n        log.info(\"maintenance -> killed {} processes\", processesToKill.size());\n    }\n\n    private static void cleanup(ProcessEntry process) {\n        try {\n            PathUtils.deleteRecursively(process.workDir);\n        } catch (IOException e) {\n            log.info(\"cleanup ['{}'] -> error: {}\", process.workDir, e.getMessage());\n            // ignore\n        }\n    }\n\n    public interface ProcessLauncher {\n\n        ProcessEntry start() throws IOException;\n    }\n\n    public static final class ProcessEntry {\n\n        private final long timestamp;\n        private final Process process;\n        private final Path workDir;\n\n        private boolean remove = false;\n\n        public ProcessEntry(Process process, Path workDir) {\n            this.timestamp = System.currentTimeMillis();\n            this.process = process;\n            this.workDir = workDir;\n        }\n\n        public Process getProcess() {\n            return process;\n        }\n\n        public Path getWorkDir() {\n            return workDir;\n        }\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/executors/runner/RunnerCommandBuilder.java",
    "content": "package com.walmartlabs.concord.agent.executors.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class RunnerCommandBuilder {\n\n    private String javaCmd;\n    private Path workDir;\n    private Path runnerPath;\n    private Path runnerCfgPath;\n    private String logLevel;\n    private Path extraDockerVolumesFile;\n    private boolean exposeDockerDaemon;\n    private List<String> extraJvmParams;\n    private String mainClass;\n    private int majorJavaVersion;\n\n    public RunnerCommandBuilder() {\n    }\n\n    public RunnerCommandBuilder javaCmd(String javaCmd) {\n        this.javaCmd = javaCmd;\n        return this;\n    }\n\n    public RunnerCommandBuilder workDir(Path workDir) {\n        this.workDir = workDir;\n        return this;\n    }\n\n    public RunnerCommandBuilder runnerPath(Path runnerPath) {\n        this.runnerPath = runnerPath;\n        return this;\n    }\n\n    public RunnerCommandBuilder runnerCfgPath(Path runnerCfgPath) {\n        this.runnerCfgPath = runnerCfgPath;\n        return this;\n    }\n\n    public RunnerCommandBuilder logLevel(String logLevel) {\n        this.logLevel = logLevel;\n        return this;\n    }\n\n    public RunnerCommandBuilder extraDockerVolumesFile(Path extraDockerVolumesFile) {\n        this.extraDockerVolumesFile = extraDockerVolumesFile;\n        return this;\n    }\n\n    public RunnerCommandBuilder exposeDockerDaemon(boolean exposeDockerDaemon) {\n        this.exposeDockerDaemon = exposeDockerDaemon;\n        return this;\n    }\n\n    public RunnerCommandBuilder jvmParams(List<String> jvmParams) {\n        this.extraJvmParams = jvmParams;\n        return this;\n    }\n\n    public RunnerCommandBuilder mainClass(String mainClass) {\n        this.mainClass = mainClass;\n        return this;\n    }\n\n    public RunnerCommandBuilder majorJavaVersion(int majorJavaVersion) {\n        this.majorJavaVersion = majorJavaVersion;\n        return this;\n    }\n\n    public String[] build() {\n        List<String> l = new ArrayList<>();\n\n        l.add(javaCmd);\n\n        // JVM arguments, can be customized\n        if (extraJvmParams != null) {\n            l.addAll(extraJvmParams);\n        }\n\n        // mandatory JVM parameters\n\n        // speeds up the start, we don't care much about all potential optimizations done by HotSpot\n        l.add(\"-client\");\n        if (majorJavaVersion < 13) {\n            // don't do bytecode verification\n            l.add(\"-noverify\");\n        }\n        // enable support for calling vararg methods in JUEL\n        l.add(\"-Djavax.el.varArgs=true\");\n        // avoid blocking on crypto\n        l.add(\"-Djava.security.egd=file:/dev/./urandom\");\n        // avoid some performance issues by preferring IPv4 instead of IPv6\n        l.add(\"-Djava.net.preferIPv4Stack=true\");\n        // workaround for JDK-8142508\n        l.add(\"-Dsun.zip.disableMemoryMapping=true\");\n\n        // working directory\n        if (workDir != null) {\n            l.add(\"-Duser.dir=\" + workDir);\n        }\n\n        // default to UTF-8\n        l.add(\"-Dfile.encoding=UTF-8\");\n\n        // logback configuration looks for logLevel in JVM properties\n        if (logLevel != null) {\n            l.add(\"-DlogLevel=\" + logLevel);\n        }\n\n        // additional Docker volumes to mount when running containers inside the flow\n        if (extraDockerVolumesFile != null) {\n            // TODO move into RunnerConfiguration\n            l.add(\"-Dconcord.dockerExtraVolumes=\" + extraDockerVolumesFile);\n        }\n\n        l.add(\"-Dconcord.exposeDockerDaemon=\" + exposeDockerDaemon);\n\n        // Java 9+ requires additional add-opens for compatibility\n        if (majorJavaVersion >= 9) {\n            l.add(\"--add-opens\");\n            l.add(\"java.base/java.lang=ALL-UNNAMED\");\n            l.add(\"--add-opens\");\n            l.add(\"java.base/java.util=ALL-UNNAMED\");\n        }\n\n        // classpath\n        l.add(\"-cp\");\n\n        // the runner's runtime is stored somewhere in the agent's libraries\n        String runner = runnerPath.toString();\n        l.add(runner);\n\n        // main class\n        if (mainClass == null) {\n            mainClass = \"com.walmartlabs.concord.runner.Main\";\n        }\n        l.add(mainClass);\n\n        l.add(runnerCfgPath.toString());\n\n        return l.toArray(new String[0]);\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/executors/runner/RunnerJob.java",
    "content": "package com.walmartlabs.concord.agent.executors.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.guava.GuavaModule;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport com.walmartlabs.concord.agent.ExecutionException;\nimport com.walmartlabs.concord.agent.JobRequest;\nimport com.walmartlabs.concord.agent.executors.runner.RunnerJobExecutor.RunnerJobExecutorConfiguration;\nimport com.walmartlabs.concord.agent.logging.ProcessLogFactory;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.policyengine.PolicyEngineRules;\nimport com.walmartlabs.concord.runtime.common.cfg.*;\nimport com.walmartlabs.concord.sdk.Constants;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic class RunnerJob {\n\n    @SuppressWarnings(\"unchecked\")\n    public static RunnerJob from(RunnerJobExecutorConfiguration runnerExecutorCfg, JobRequest jobRequest, ProcessLogFactory processLogFactory) throws ExecutionException, IOException {\n        Map<String, Object> cfg = Collections.emptyMap();\n\n        Path payloadDir = jobRequest.getPayloadDir();\n\n        Path p = payloadDir.resolve(Constants.Files.CONFIGURATION_FILE_NAME);\n        if (Files.exists(p)) {\n            try (InputStream in = Files.newInputStream(p)) {\n                cfg = new ObjectMapper().readValue(in, Map.class);\n            } catch (IOException e) {\n                throw new ExecutionException(\"Error while reading process configuration\", e);\n            }\n        }\n\n        RunnerConfiguration runnerCfg = createRunnerConfiguration(runnerExecutorCfg, cfg);\n        RunnerLog log;\n        try {\n            log = new RunnerLog(\n                    processLogFactory.createRedirectedLog(jobRequest.getInstanceId(), runnerExecutorCfg.segmentedLogs()),\n                    processLogFactory.createRemoteLog(jobRequest.getInstanceId()));\n        } catch (IOException e) {\n            throw new ExecutionException(\"Error while creating the runner's log: \" + e.getMessage(), e);\n        }\n\n        Path policyFile = payloadDir.resolve(Constants.Files.CONCORD_SYSTEM_DIR_NAME)\n                .resolve(Constants.Files.POLICY_FILE_NAME);\n\n        PolicyEngine policyEngine = null;\n        if (Files.exists(policyFile)) {\n            PolicyEngineRules rules = createObjectMapper().readValue(policyFile.toFile(), PolicyEngineRules.class);\n            if (rules != null) {\n                policyEngine = new PolicyEngine(rules);\n            }\n        }\n\n        return new RunnerJob(jobRequest.getInstanceId(), payloadDir, cfg, runnerCfg, log, policyEngine);\n    }\n\n    private final UUID instanceId;\n    private final Path payloadDir;\n    private final Map<String, Object> processCfg;\n    private final RunnerConfiguration runnerCfg;\n    private final boolean debugMode;\n    private final RunnerLog log;\n    private final PolicyEngine policyEngine;\n\n    private RunnerJob(UUID instanceId, Path payloadDir, Map<String, Object> processCfg, RunnerConfiguration runnerCfg, RunnerLog log, PolicyEngine policyEngine) {\n        this.instanceId = instanceId;\n        this.payloadDir = payloadDir;\n        this.processCfg = processCfg;\n        this.runnerCfg = runnerCfg;\n        this.debugMode = debugMode(processCfg);\n        this.log = log;\n        this.policyEngine = policyEngine;\n    }\n\n    public UUID getInstanceId() {\n        return instanceId;\n    }\n\n    public Path getPayloadDir() {\n        return payloadDir;\n    }\n\n    public Map<String, Object> getProcessCfg() {\n        return processCfg;\n    }\n\n    public RunnerConfiguration getRunnerCfg() {\n        return runnerCfg;\n    }\n\n    public boolean isDebugMode() {\n        return debugMode;\n    }\n\n    public RunnerLog getLog() {\n        return log;\n    }\n\n    public PolicyEngine getPolicyEngine() {\n        return policyEngine;\n    }\n\n    private static boolean debugMode(Map<String, Object> processCfg) {\n        Object v = processCfg.get(Constants.Request.DEBUG_KEY);\n        if (v instanceof String) {\n            // allows `curl ... -F debug=true`\n            return Boolean.parseBoolean((String) v);\n        }\n\n        return Boolean.TRUE.equals(v);\n    }\n\n    public RunnerJob withDependencies(Collection<String> resolvedDeps) {\n        if (resolvedDeps == null) {\n            resolvedDeps = Collections.emptyList();\n        }\n\n        RunnerConfiguration cfg = RunnerConfiguration.builder().from(runnerCfg)\n                .dependencies(resolvedDeps)\n                .build();\n\n        // TODO replace with immutables?\n        return new RunnerJob(instanceId, payloadDir, processCfg, cfg, log, policyEngine);\n    }\n\n    @Override\n    public String toString() {\n        return \"RunnerJob{\" +\n                \"instanceId=\" + instanceId +\n                \", debugMode=\" + debugMode +\n                '}';\n    }\n\n    private static RunnerConfiguration createRunnerConfiguration(RunnerJobExecutorConfiguration execCfg, Map<String, Object> processCfg) {\n        ImmutableRunnerConfiguration.Builder b = RunnerConfiguration.builder();\n\n        Object v = processCfg.get(Constants.Request.RUNNER_KEY);\n        if (v != null) {\n            RunnerConfiguration src = createObjectMapper().convertValue(v, RunnerConfiguration.class);\n            b = b.from(src);\n        }\n\n        return b.agentId(execCfg.agentId())\n                .debug(debugMode(processCfg))\n                .api(ApiConfiguration.builder()\n                        .baseUrl(execCfg.serverApiBaseUrl())\n                        .maxNoHeartbeatInterval(execCfg.maxHeartbeatInterval())\n                        .build())\n                .docker(DockerConfiguration.builder()\n                        .extraVolumes(execCfg.extraDockerVolumes())\n                        .exposeDockerDaemon(execCfg.exposeDockerDaemon())\n                        .build())\n                .dependencyManager(DependencyManagerConfiguration.builder()\n                        .cacheDir(execCfg.dependencyCacheDir().toAbsolutePath().toString())\n                        .build())\n                .logging(LoggingConfiguration.builder()\n                        .sendSystemOutAndErrToSLF4J(true)\n                        .segmentedLogs(execCfg.segmentedLogs())\n                        .workDirMasking(execCfg.workDirMasking())\n                        .build())\n                .build();\n    }\n\n    // TODO reuse the same ObjectMapper instance?\n    private static ObjectMapper createObjectMapper() {\n        ObjectMapper om = new ObjectMapper();\n        om.registerModule(new GuavaModule());\n        om.registerModule(new Jdk8Module());\n        return om;\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/executors/runner/RunnerJobExecutor.java",
    "content": "package com.walmartlabs.concord.agent.executors.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.SerializationFeature;\nimport com.google.common.base.Charsets;\nimport com.google.common.hash.HashCode;\nimport com.google.common.hash.HashFunction;\nimport com.google.common.hash.Hasher;\nimport com.google.common.hash.Hashing;\nimport com.walmartlabs.concord.agent.ConfiguredJobRequest;\nimport com.walmartlabs.concord.agent.ExecutionException;\nimport com.walmartlabs.concord.agent.JobInstance;\nimport com.walmartlabs.concord.agent.Utils;\nimport com.walmartlabs.concord.agent.executors.JobExecutor;\nimport com.walmartlabs.concord.agent.executors.runner.ProcessPool.ProcessEntry;\nimport com.walmartlabs.concord.agent.logging.ProcessLog;\nimport com.walmartlabs.concord.agent.logging.ProcessLogFactory;\nimport com.walmartlabs.concord.agent.remote.AttachmentsUploader;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.Posix;\nimport com.walmartlabs.concord.dependencymanager.DependencyEntity;\nimport com.walmartlabs.concord.dependencymanager.DependencyManager;\nimport com.walmartlabs.concord.dependencymanager.ProgressListener;\nimport com.walmartlabs.concord.policyengine.CheckResult;\nimport com.walmartlabs.concord.policyengine.DependencyRule;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport org.immutables.value.Value;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.annotation.Nullable;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.URI;\nimport java.nio.file.AtomicMoveNotSupportedException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.nio.file.StandardOpenOption;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.*;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport static com.walmartlabs.concord.common.DockerProcessBuilder.CONCORD_DOCKER_LOCAL_MODE_KEY;\n\n/**\n * Executes jobs using concord-runner runtime.\n */\npublic class RunnerJobExecutor implements JobExecutor {\n\n    private static final Logger log = LoggerFactory.getLogger(RunnerJobExecutor.class);\n\n    protected final DependencyManager dependencyManager;\n\n    private final RunnerJobExecutorConfiguration cfg;\n    private final DefaultDependencies defaultDependencies;\n    private final AttachmentsUploader attachmentsUploader;\n    private final ProcessPool processPool;\n    private final ProcessLogFactory logFactory;\n    private final ExecutorService executor;\n\n    private final ObjectMapper objectMapper;\n\n    private final int majorJavaVersion;\n\n    public RunnerJobExecutor(RunnerJobExecutorConfiguration cfg,\n                             DependencyManager dependencyManager,\n                             DefaultDependencies defaultDependencies,\n                             AttachmentsUploader attachmentsUploader,\n                             ProcessPool processPool,\n                             ProcessLogFactory processLogFactory,\n                             ExecutorService executor) {\n\n        this.cfg = cfg;\n        this.dependencyManager = dependencyManager;\n        this.defaultDependencies = defaultDependencies;\n        this.attachmentsUploader = attachmentsUploader;\n        this.processPool = processPool;\n        this.logFactory = processLogFactory;\n        this.executor = executor;\n\n        // sort JSON keys for consistency\n        this.objectMapper = new ObjectMapper()\n                .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);\n\n        this.majorJavaVersion = getMajorJavaVersion(cfg.javaCmd());\n    }\n\n    private static int getMajorJavaVersion(String javaCmd) {\n        try {\n            Process process = new ProcessBuilder(javaCmd, \"-version\")\n                    .start();\n\n            int exitCode = process.waitFor();\n            if (exitCode != 0) {\n                throw new RuntimeException(\"`java -version` exited with \" + exitCode);\n            }\n\n            String version;\n            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {\n                version = reader.readLine();\n            }\n\n            int start = version.indexOf(\"\\\"\");\n            int end = version.indexOf(\".\", start + 1);\n            if (start < 0 || start + 1 >= version.length() || end < 0) {\n                throw new RuntimeException(\"Unknown version string: \" + version);\n            }\n\n            int major;\n            try {\n                major = Integer.parseInt(version.substring(start + 1, end));\n            } catch (NumberFormatException e) {\n                throw new RuntimeException(\"Unknown version string: \" + version);\n            }\n\n            return major;\n        } catch (Exception e) {\n            throw new RuntimeException(\"Can't determine the target Java runtime version\", e);\n        }\n    }\n\n    @Override\n    public JobInstance exec(ConfiguredJobRequest jobRequest) throws Exception {\n        RunnerJob job = RunnerJob.from(cfg, jobRequest, logFactory);\n        return exec(job);\n    }\n\n    private JobInstance exec(RunnerJob job) throws Exception {\n        // prepare and start a new JVM of use a pre-forked one\n        ProcessEntry pe;\n        try {\n            // resolve and download the dependencies\n            Collection<String> resolvedDeps;\n            if (cfg.dependencyResolveTimeout() != null) {\n                Duration timeout = Objects.requireNonNull(cfg.dependencyResolveTimeout());\n                RunnerJob finalJob = job;\n\n                Instant startedAt = Instant.now();\n                Future<Collection<String>> future = executor.submit(() -> resolveDeps(finalJob));\n                try {\n                    resolvedDeps = future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);\n                } catch (InterruptedException | TimeoutException e) {\n                    future.cancel(true);\n                    Duration elapsed = Duration.between(startedAt, Instant.now());\n                    throw new RuntimeException(\"Timeout resolving dependencies (timeout=%sms, elapsed=%sms)\".formatted(timeout.toMillis(), elapsed.toMillis()));\n                }\n            } else {\n                resolvedDeps = resolveDeps(job);\n            }\n\n            job = job.withDependencies(resolvedDeps);\n\n            pe = buildProcessEntry(job);\n        } catch (Throwable e) {\n            log.warn(\"exec ['{}'] -> process error: {}\", job.getInstanceId(), e.getMessage(), e);\n\n            job.getLog().error(\"Process startup error: {}\", e.getMessage(), e);\n\n            cleanup(job);\n\n            throw e;\n        }\n\n        // continue the execution in a separate thread to make the process cancellable\n        RunnerJob _job = job;\n        Future<?> f = executor.submit(() -> {\n            boolean uploadAttachmentsOnError = true;\n\n            try {\n                exec(_job, pe);\n\n                uploadAttachmentsOnError = false;\n                uploadAttachments(_job.getInstanceId(), pe);\n            } catch (Throwable t) {\n                if (uploadAttachmentsOnError) {\n                    try {\n                        uploadAttachments(_job.getInstanceId(), pe);\n                    } catch (Exception e) {\n                        // ignore\n                    }\n                }\n\n                throw new RuntimeException(t);\n            } finally {\n                persistWorkDir(_job.getInstanceId(), pe.getWorkDir());\n                cleanup(_job.getInstanceId(), pe);\n                cleanup(_job);\n            }\n        });\n\n        // return a handle that can be used to cancel the process or wait for its completion\n        return new JobInstanceImpl(f, pe.getProcess(), cfg.cleanRunnerDescendants());\n    }\n\n    private void persistWorkDir(UUID instanceId, Path src) {\n        Path persistentWorkDir = cfg.persistentWorkDir();\n\n        if (persistentWorkDir == null) {\n            return;\n        }\n\n        Path dst = persistentWorkDir.resolve(instanceId.toString());\n\n        try {\n            if (!Files.exists(dst)) {\n                Files.createDirectories(dst);\n            }\n\n            log.info(\"exec ['{}'] -> persisting the payload directory into {}...\", instanceId, dst);\n            PathUtils.copy(src, dst);\n\n            // persistentWorkDir is mostly useful when the Agent is running in a container\n            // typically it is running as PID 456 - all files created by the process\n            // are created using PID 456 and won't be readable by the host user\n\n            // therefore, we need to make all files readable by all users\n            // and that's why runner.persistentWorkDir shouldn't be used in prod\n\n            try(Stream<Path> walk = Files.walk(dst)) {\n                walk.forEach(f -> {\n                    try {\n                        if (Files.isDirectory(f)) {\n                            Files.setPosixFilePermissions(f, Posix.posix(0755));\n                        } else if (Files.isRegularFile(f)) {\n                            Files.setPosixFilePermissions(f, Posix.posix(0644));\n                        }\n                    } catch (IOException e) {\n                        log.warn(\"persistWorkDir -> can't update permissions for {}: {}\", f, e.getMessage());\n                    }\n                });\n            }\n        } catch (IOException e) {\n            log.warn(\"persistWorkDir -> failed to copy {} into {}: {}\", src, dst, e.getMessage());\n        }\n    }\n\n    private void uploadAttachments(UUID instanceId, ProcessEntry pe) {\n        try {\n            attachmentsUploader.upload(instanceId, pe.getWorkDir());\n        } catch (Exception e) {\n            log.error(\"uploadAttachments ['{}'] -> error: {}\", instanceId, e.getMessage());\n            throw new RuntimeException(\"Error while uploading attachments: \" + e.getMessage());\n        }\n    }\n\n    private void cleanup(UUID instanceId, ProcessEntry pe) {\n        Path workDir = pe.getWorkDir();\n        try {\n            log.info(\"exec ['{}'] -> removing the working directory: {}\", instanceId, workDir);\n            PathUtils.deleteRecursively(workDir);\n        } catch (IOException e) {\n            log.warn(\"exec ['{}'] -> can't remove the working directory: {}\", instanceId, e.getMessage());\n        }\n    }\n\n    protected ProcessEntry buildProcessEntry(RunnerJob job) throws Exception {\n        List<String> jvmParams = getJvmParams(job.getPayloadDir(), job.getProcessCfg());\n        String[] cmd = createCmd(job, jvmParams);\n\n        boolean prefork = canUsePrefork(job);\n        if (prefork) {\n            log.info(\"start ['{}'] -> using a pre-forked instances\", job.getInstanceId());\n            return fork(job, cmd);\n        } else {\n            return startOneTime(job, cmd);\n        }\n    }\n\n    private void exec(RunnerJob job, ProcessEntry pe) throws Exception {\n        // the actual OS process\n        Process proc = pe.getProcess();\n\n        UUID instanceId = job.getInstanceId();\n        ProcessLog processLog = job.getLog();\n\n        // start the log's maintenance thread (e.g. streaming to the server)\n        LogStream logStream = new LogStream(job, proc);\n        logStream.start();\n\n        try {\n            // save the process' log\n            processLog.log(proc.getInputStream());\n\n            // wait for the process to finish\n            int code;\n            try {\n                code = proc.waitFor();\n            } catch (Exception e) {\n                // wait for the log to finish\n                logStream.waitForCompletion();\n                handleError(job, proc, e.getMessage());\n                throw new ExecutionException(\"Error while executing a job: \" + e.getMessage());\n            }\n\n            // wait for the log to finish\n            logStream.waitForCompletion();\n\n            if (code != 0) {\n                log.warn(\"exec ['{}'] -> finished with {}\", instanceId, code);\n                handleError(job, proc, \"Process exit code: \" + code);\n                throw new ExecutionException(\"Error while executing a job, process exit code: \" + code);\n            }\n\n            log.info(\"exec ['{}'] -> finished with {}\", instanceId, code);\n            processLog.info(\"Process finished with: {}\", code);\n        } finally {\n            // wait for the log to finish\n            logStream.waitForCompletion();\n        }\n    }\n\n    private void handleError(RunnerJob job, Process proc, String error) {\n        job.getLog().error(error);\n\n        if (Utils.kill(proc)) {\n            log.warn(\"handleError ['{}'] -> killed by agent\", job.getInstanceId());\n        }\n    }\n\n    private Collection<String> resolveDeps(RunnerJob job) throws Exception {\n        job.getLog().info(\"Resolving process dependencies...\");\n\n        long t1 = System.currentTimeMillis();\n\n        // combine the default dependencies and the process' dependencies\n        Collection<URI> uris = Stream.concat(defaultDependencies.getDependencies().stream(), JobDependencies.get(job).stream())\n                .collect(Collectors.toList());\n\n        uris = rewriteDependencies(job, uris);\n\n        Collection<DependencyEntity> deps = dependencyManager.resolve(uris, new ProgressListener() {\n\n            private final List<String> errors = Collections.synchronizedList(new ArrayList<>());\n\n            @Override\n            public void onRetry(int retryCount, int maxRetry, long interval, String cause) {\n                synchronized (errors) {\n                    for (String error : errors) {\n                        job.getLog().warn(error);\n                    }\n                }\n\n                job.getLog().warn(\"Error while downloading dependencies: {}\", cause);\n                job.getLog().info(\"Retrying in {}ms\", interval);\n            }\n\n            @Override\n            public void onTransferFailed(String error) {\n                if (job.isDebugMode()) {\n                    job.getLog().warn(error);\n                } else {\n                    errors.add(error);\n                }\n            }\n        });\n\n        // check the resolved dependencies against the current policy\n        validateDependencies(job, deps);\n\n        // sort dependencies to maintain consistency in runner configurations\n        Collection<String> paths = deps.stream()\n                .map(DependencyEntity::getPath)\n                .map(p -> p.toAbsolutePath().toString())\n                .sorted()\n                .collect(Collectors.toList());\n\n        long t2 = System.currentTimeMillis();\n\n        if (job.isDebugMode()) {\n            job.getLog().info(\"Dependency resolution took {}ms\", (t2 - t1));\n            logDependencies(job, paths);\n        } else {\n            logDependencies(job, uris);\n        }\n\n        return paths;\n    }\n\n    private Collection<URI> rewriteDependencies(RunnerJob job, Collection<URI> uris) {\n        PolicyEngine policyEngine = job.getPolicyEngine();\n        if (policyEngine == null) {\n            return uris;\n        }\n\n        return policyEngine.getDependencyRewritePolicy()\n                .rewrite(uris, (msg, from, to) -> {\n                    job.getLog().info(\"Updating dependency from '{}' to '{}'\", from, to);\n                    if (msg != null) {\n                        job.getLog().warn(msg);\n                    }\n                });\n    }\n\n    private void validateDependencies(RunnerJob job, Collection<DependencyEntity> resolvedDepEntities) throws ExecutionException {\n        PolicyEngine policyEngine = job.getPolicyEngine();\n        if (policyEngine == null) {\n            return;\n        }\n\n        ProcessLog processLog = job.getLog();\n\n        processLog.info(\"Checking the dependency policy...\");\n\n        CheckResult<DependencyRule, DependencyEntity> result = policyEngine.getDependencyPolicy().check(resolvedDepEntities);\n        result.getWarn().forEach(d ->\n                processLog.warn(\"Potentially restricted artifact '{}' (dependency policy: {})\", d.getEntity(), d.getRule().msg()));\n        result.getDeny().forEach(d ->\n                processLog.warn(\"Artifact '{}' is forbidden by the dependency policy {}\", d.getEntity(), d.getRule().msg()));\n\n        if (!result.getDeny().isEmpty()) {\n            throw new ExecutionException(\"Found restricted dependencies\");\n        }\n    }\n\n    private void logDependencies(RunnerJob job, Collection<?> deps) {\n        if (deps == null || deps.isEmpty()) {\n            job.getLog().info(\"No external dependencies.\");\n            return;\n        }\n\n        List<String> l = deps.stream()\n                .map(Object::toString)\n                .collect(Collectors.toList());\n\n        StringBuilder b = new StringBuilder();\n        for (String s : l) {\n            b.append(\"\\n\\t\").append(s);\n        }\n\n        job.getLog().info(\"Dependencies: {}\", b);\n    }\n\n    private String[] createCmd(RunnerJob job, List<String> jvmParams) throws IOException {\n        Path runnerCfgFile = storeRunnerCfg(cfg.runnerCfgDir(), job.getRunnerCfg());\n        return new RunnerCommandBuilder()\n                .javaCmd(cfg.javaCmd())\n                .logLevel(getLogLevel(job))\n                .extraDockerVolumesFile(createExtraDockerVolumesFile(job))\n                .exposeDockerDaemon(cfg.exposeDockerDaemon())\n                .runnerPath(cfg.runnerPath().toAbsolutePath())\n                .runnerCfgPath(runnerCfgFile.toAbsolutePath())\n                .mainClass(cfg.runnerMainClass())\n                .jvmParams(jvmParams)\n                .majorJavaVersion(this.majorJavaVersion)\n                .build();\n    }\n\n    private ProcessEntry fork(RunnerJob job, String[] cmd) throws ExecutionException, IOException {\n        long t1 = System.currentTimeMillis();\n\n        HashCode hc = hash(cmd);\n\n        // take a \"pre-forked\" JVM from the pool or start a new one\n        ProcessEntry entry = processPool.take(hc, () -> {\n            // can't use workDirBase, \"preforks\" start before they receive their process payload\n            // create a new temporary directory\n            Path workDir = PathUtils.createTempDir(\"workDir\");\n            return start(workDir, cmd);\n        });\n\n        // the job's payload directory containing all files from the process' state snapshot and/or the repository's data\n        Path src = job.getPayloadDir();\n        // the process' workDir\n        Path dst = entry.getWorkDir();\n        // TODO use move\n        PathUtils.copy(src, dst);\n\n        writeInstanceId(job.getInstanceId(), dst);\n\n        long t2 = System.currentTimeMillis();\n\n        if (job.isDebugMode()) {\n            job.getLog().info(\"Forking a VM took {}ms\", (t2 - t1));\n        }\n\n        return entry;\n    }\n\n    protected ProcessEntry startOneTime(RunnerJob job, String[] cmd) throws IOException {\n        // create the parent directory of the process' ${workDir}\n        Path workDir = cfg.workDirBase().resolve(job.getInstanceId().toString());\n        if (!Files.exists(workDir)) {\n            Files.createDirectories(workDir);\n        }\n\n        // the job's payload directory, contains all files from the state snapshot including imports\n        Path src = job.getPayloadDir();\n\n        try {\n            Files.move(src, workDir, StandardCopyOption.ATOMIC_MOVE);\n        } catch (AtomicMoveNotSupportedException e) {\n            log.error(\"startOneTime ['{}'] -> unable to move {} to {} atomically\", job.getInstanceId(), src, workDir);\n            throw e;\n        }\n\n        writeInstanceId(job.getInstanceId(), workDir);\n\n        return start(workDir, cmd);\n    }\n\n    private ProcessEntry start(Path workDir, String[] cmd) throws IOException {\n        log.info(\"start -> {}, {}\", workDir, String.join(\" \", cmd));\n\n        ProcessBuilder b = new ProcessBuilder()\n                .directory(workDir.toFile())\n                .command(cmd)\n                .redirectErrorStream(true);\n\n        // TODO constants\n        Map<String, String> env = b.environment();\n        env.put(PathUtils.TMP_DIR_KEY, PathUtils.TMP_DIR.toAbsolutePath().toString());\n        env.put(\"_CONCORD_ATTACHMENTS_DIR\", workDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .toAbsolutePath().toString());\n\n        // pass through the docker mode\n        String dockerMode = System.getenv(CONCORD_DOCKER_LOCAL_MODE_KEY);\n        if (dockerMode != null) {\n            log.debug(\"start -> using Docker mode: {}\", dockerMode);\n            env.put(CONCORD_DOCKER_LOCAL_MODE_KEY, dockerMode);\n        }\n\n        Process p = b.start();\n        return new ProcessEntry(p, workDir);\n    }\n\n    protected Path storeRunnerCfg(Path baseDir, RunnerConfiguration runnerCfg) throws IOException {\n        if (!Files.exists(baseDir)) {\n            Files.createDirectories(baseDir);\n        }\n\n        byte[] data = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsBytes(runnerCfg);\n        HashCode hc = Hashing.sha256().hashBytes(data);\n\n        Path cfgFile = baseDir.resolve(hc + \".json\");\n        if (!Files.exists(cfgFile)) {\n            Files.write(cfgFile, data);\n        }\n\n        return cfgFile;\n    }\n\n    @Override\n    public String toString() {\n        return \"RunnerJobExecutor\";\n    }\n\n    private Path createExtraDockerVolumesFile(RunnerJob job) throws IOException {\n        List<String> l = cfg.extraDockerVolumes();\n        if (l.isEmpty()) {\n            return null;\n        }\n\n        Path workDir = job.getPayloadDir();\n        Path p = workDir.resolve(\".extraDockerVolumes\");\n        Files.write(p, l, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);\n        return workDir.relativize(p);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private List<String> getJvmParams(Path workDir, Map<String, Object> processCfg) {\n        // the _agent.json file takes precedence\n        Path p = workDir.resolve(Constants.Agent.AGENT_PARAMS_FILE_NAME);\n        if (Files.exists(p)) {\n            try (InputStream in = Files.newInputStream(p)) {\n                Map<String, Object> m = objectMapper.readValue(in, Map.class);\n                List<String> l = MapUtils.getList(m, Constants.Agent.JVM_ARGS_KEY, null);\n                if (l != null) {\n                    return l;\n                }\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        // check the `configuration.requirements.jvm` next\n        List<String> l = getJvmArgsFromConfig(processCfg);\n        if (l != null) {\n            return l;\n        }\n\n        // fallback to the default parameters\n        return cfg.jvmParams();\n    }\n\n    private boolean canUsePrefork(RunnerJob job) {\n        if (!cfg.preforkEnabled()) {\n            return false;\n        }\n\n        Path workDir = job.getPayloadDir();\n\n        if (Files.exists(workDir.resolve(Constants.Files.LIBRARIES_DIR_NAME))) {\n            // the process supplied its own libraries, can't use preforking\n            return false;\n        }\n\n        // the process supplied its own JVM parameters in concord.yml, can't use preforking\n        List<String> jvmExtraArgs = getJvmArgsFromConfig(job.getProcessCfg());\n        if (jvmExtraArgs != null) {\n            return false;\n        }\n\n        // the process supplied its own JVM parameters in _agent.json, can't use preforking\n        return !Files.exists(workDir.resolve(Constants.Agent.AGENT_PARAMS_FILE_NAME));\n    }\n\n    private static List<String> getJvmArgsFromConfig(Map<String, Object> processCfg) {\n        Map<String, Object> requirements = MapUtils.get(processCfg, Constants.Request.REQUIREMENTS, null);\n        if (requirements == null) {\n            return null;\n        }\n\n        // TODO constants?\n        Map<String, Object> jvm = MapUtils.get(requirements, \"jvm\", null);\n        if (jvm == null) {\n            return null;\n        }\n\n        // TODO constants?\n        List<String> extraArgs = MapUtils.getList(jvm, \"extraArgs\", null);\n        if (extraArgs == null || extraArgs.isEmpty()) {\n            return null;\n        }\n\n        return extraArgs;\n    }\n\n    private static String getLogLevel(RunnerJob job) {\n        RunnerConfiguration cfg = job.getRunnerCfg();\n        if (cfg == null) {\n            return null;\n        }\n\n        String logLevel = cfg.logLevel();\n        if (logLevel == null) {\n            return null;\n        }\n\n        return logLevel.toUpperCase();\n    }\n\n    private static HashCode hash(String[] as) {\n        HashFunction f = Hashing.sha256();\n        Hasher h = f.newHasher();\n        for (String s : as) {\n            h.putString(s, Charsets.UTF_8);\n        }\n        return h.hash();\n    }\n\n    private static void cleanup(RunnerJob job) {\n        try {\n            job.getLog().delete();\n        } catch (Exception e) {\n            log.warn(\"cleanup [{}] -> error while cleaning up the process logs: {}\", job.getInstanceId(), e.getMessage());\n        }\n    }\n\n    /**\n     * A tiny wrapper to simplify working with the log streaming.\n     */\n    private class LogStream {\n\n        private final RunnerJob job;\n        private final Process proc;\n\n        private Future<?> f;\n        private transient boolean doStop = false;\n\n        private LogStream(RunnerJob job, Process proc) {\n            this.job = job;\n            this.proc = proc;\n        }\n\n        /**\n         * Starts the log streaming in a separate thread.\n         */\n        public void start() {\n            RunnerLog processLog = job.getLog();\n\n            f = executor.submit(() -> {\n                try {\n                    processLog.run(() -> doStop);\n                } catch (Exception e) {\n                    handleError(job, proc, e.getMessage());\n                }\n            });\n        }\n\n        /**\n         * Waits for the log stream to finish and removes the log file.\n         */\n        public void waitForCompletion() {\n            this.doStop = true;\n\n            try {\n                f.get(1, TimeUnit.MINUTES);\n            } catch (Exception e) {\n                log.warn(\"waitForCompletion -> timeout waiting for the log stream of {}\", job.getInstanceId());\n            }\n        }\n    }\n\n    private static void writeInstanceId(UUID instanceId, Path dst) throws IOException {\n        Path idPath = dst.resolve(Constants.Files.INSTANCE_ID_FILE_NAME);\n        Files.write(idPath, instanceId.toString().getBytes(), StandardOpenOption.CREATE, StandardOpenOption.SYNC);\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    public interface RunnerJobExecutorConfiguration {\n\n        String agentId();\n\n        String serverApiBaseUrl();\n\n        String javaCmd();\n\n        List<String> jvmParams();\n\n        Path dependencyListDir();\n\n        Path dependencyCacheDir();\n\n        @Nullable\n        Duration dependencyResolveTimeout();\n\n        Path workDirBase();\n\n        Path runnerPath();\n\n        Path runnerCfgDir();\n\n        String runnerMainClass();\n\n        boolean runnerSecurityManagerEnabled();\n\n        boolean segmentedLogs();\n\n        boolean workDirMasking();\n\n        @Value.Default\n        default List<String> extraDockerVolumes() {\n            return Collections.emptyList();\n        }\n\n        @Value.Default\n        default Boolean exposeDockerDaemon() {\n            return true;\n        }\n\n        long maxHeartbeatInterval();\n\n        @Nullable\n        Path persistentWorkDir();\n\n        boolean preforkEnabled();\n\n        boolean cleanRunnerDescendants();\n\n        static ImmutableRunnerJobExecutorConfiguration.Builder builder() {\n            return ImmutableRunnerJobExecutorConfiguration.builder();\n        }\n    }\n\n    private static class JobInstanceImpl implements JobInstance {\n\n        private final Future<?> f;\n        private final Process proc;\n        private final boolean cleanRunnerDescendants;\n\n        private transient boolean cancelled = false;\n\n        private JobInstanceImpl(Future<?> f, Process proc, boolean cleanRunnerDescendants) {\n            this.f = f;\n            this.proc = proc;\n            this.cleanRunnerDescendants = cleanRunnerDescendants;\n        }\n\n        @Override\n        public void waitForCompletion() throws Exception {\n            f.get();\n        }\n\n        @Override\n        public void cancel() {\n            if (f.isCancelled() || f.isDone()) {\n                return;\n            }\n\n            cancelled = true;\n\n            Utils.kill(proc, cleanRunnerDescendants);\n        }\n\n        @Override\n        public boolean isCancelled() {\n            return cancelled;\n        }\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/executors/runner/RunnerLog.java",
    "content": "package com.walmartlabs.concord.agent.executors.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agent.logging.ProcessLog;\nimport com.walmartlabs.concord.agent.logging.RedirectedProcessLog;\nimport com.walmartlabs.concord.agent.logging.RemoteProcessLog;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.function.Supplier;\n\npublic class RunnerLog implements ProcessLog {\n\n    private final RedirectedProcessLog redirectedLog;\n    private final RemoteProcessLog remoteLog;\n\n    public RunnerLog(RedirectedProcessLog redirectedLog, RemoteProcessLog remoteLog) {\n        this.redirectedLog = redirectedLog;\n        this.remoteLog = remoteLog;\n    }\n\n    public void run(Supplier<Boolean> stopCondition) throws Exception {\n        redirectedLog.run(stopCondition);\n    }\n\n    @Override\n    public void delete() {\n        redirectedLog.delete();\n        remoteLog.delete();\n    }\n\n    @Override\n    public void log(InputStream src) throws IOException {\n        redirectedLog.log(src);\n    }\n\n    @Override\n    public void info(String log, Object... args) {\n        remoteLog.info(log, args);\n    }\n\n    @Override\n    public void warn(String log, Object... args) {\n        remoteLog.warn(log, args);\n    }\n\n    @Override\n    public void error(String log, Object... args) {\n        remoteLog.error(log, args);\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/guice/AgentDependencyManagerConfigurationProvider.java",
    "content": "package com.walmartlabs.concord.agent.guice;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agent.cfg.AgentConfiguration;\nimport com.walmartlabs.concord.dependencymanager.DependencyManagerConfiguration;\n\nimport javax.inject.Inject;\nimport javax.inject.Provider;\nimport javax.inject.Singleton;\n\n@Singleton\npublic class AgentDependencyManagerConfigurationProvider implements Provider<DependencyManagerConfiguration> {\n\n    private final AgentConfiguration cfg;\n\n    @Inject\n    public AgentDependencyManagerConfigurationProvider(AgentConfiguration cfg) {\n        this.cfg = cfg;\n    }\n\n    @Override\n    public DependencyManagerConfiguration get() {\n        return DependencyManagerConfiguration.builder()\n                .cacheDir(cfg.getDependencyCacheDir())\n                .strictRepositories(cfg.dependencyStrictRepositories())\n                .exclusions(cfg.dependencyExclusions())\n                .explicitlyResolveV1Client(cfg.isExplicitlyResolveV1Client())\n                .offlineMode(cfg.isMavenOfflineMode())\n                .build();\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/guice/AgentImportManager.java",
    "content": "package com.walmartlabs.concord.agent.guice;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.ImportManager;\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.imports.ImportsListener;\nimport com.walmartlabs.concord.repository.Snapshot;\n\nimport java.nio.file.Path;\nimport java.util.List;\n\n/**\n * A wrapper type to avoid clashes with the Server's instance of a {@link ImportManager}.\n * TODO replace with a common Guice module\n */\npublic class AgentImportManager {\n\n    private final ImportManager delegate;\n\n    public AgentImportManager(ImportManager delegate) {\n        this.delegate = delegate;\n    }\n\n    public List<Snapshot> process(Imports imports, Path dest) throws Exception {\n        return delegate.process(imports, dest, ImportsListener.NOP_LISTENER);\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/guice/AgentImportManagerProvider.java",
    "content": "package com.walmartlabs.concord.agent.guice;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agent.RepositoryManager;\nimport com.walmartlabs.concord.agent.cfg.ImportConfiguration;\nimport com.walmartlabs.concord.dependencymanager.DependencyManager;\nimport com.walmartlabs.concord.imports.ImportManagerFactory;\nimport com.walmartlabs.concord.imports.RepositoryExporter;\n\nimport javax.inject.Inject;\nimport javax.inject.Provider;\nimport javax.inject.Singleton;\nimport java.nio.file.Path;\nimport java.util.Objects;\n\n@Singleton\npublic class AgentImportManagerProvider implements Provider<AgentImportManager> {\n\n    private final ImportManagerFactory factory;\n\n    @Inject\n    public AgentImportManagerProvider(ImportConfiguration cfg, RepositoryManager repositoryManager, DependencyManager dependencyManager) {\n        RepositoryExporter exporter = (entry, workDir) -> {\n            Path dst = workDir;\n\n            String entryDest = entry.dest();\n            if (entry.dest() != null) {\n                dst = dst.resolve(Objects.requireNonNull(entryDest));\n            }\n\n            repositoryManager.export(entry.url(), entry.version(), null, entry.path(), dst, entry.secret(), entry.exclude());\n            return null;\n        };\n\n        this.factory = new ImportManagerFactory(dependencyManager, exporter, cfg.getDisabledProcessors());\n    }\n\n    @Override\n    public AgentImportManager get() {\n        return new AgentImportManager(factory.create());\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/guice/WorkerModule.java",
    "content": "package com.walmartlabs.concord.agent.guice;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.AbstractModule;\nimport com.google.inject.Provides;\nimport com.google.inject.Singleton;\nimport com.google.inject.multibindings.Multibinder;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.agent.DefaultStateFetcher;\nimport com.walmartlabs.concord.agent.StateFetcher;\nimport com.walmartlabs.concord.agent.logging.*;\nimport com.walmartlabs.concord.agent.remote.ApiClientFactory;\nimport com.walmartlabs.concord.agent.remote.ProcessStatusUpdater;\nimport com.walmartlabs.concord.dependencymanager.DependencyManagerConfiguration;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.util.UUID;\n\npublic class WorkerModule extends AbstractModule {\n\n    private static final Logger log = LoggerFactory.getLogger(WorkerModule.class);\n\n    private static final String REDIRECT_PROCESS_LOGS_TO_STDOUT_KEY = \"REDIRECT_PROCESS_LOGS_TO_STDOUT\";\n\n    private final String agentId;\n    private final UUID instanceId;\n    private final String sessionToken;\n\n    public WorkerModule(String agentId, UUID instanceId, String sessionToken) {\n        this.agentId = agentId;\n        this.instanceId = instanceId;\n        this.sessionToken = sessionToken;\n    }\n\n    @Provides\n    @Singleton\n    ApiClient getApiClient(ApiClientFactory factory) throws IOException {\n        return factory.create(sessionToken);\n    }\n\n    @Provides\n    @Singleton\n    ProcessLog getProcessLog(ApiClient apiClient) {\n        return new RemoteProcessLog(instanceId, new RemoteLogAppender(apiClient));\n    }\n\n    @Provides\n    @Singleton\n    SecretClient getSecretClient(ApiClient apiClient) {\n        return new SecretClient(apiClient);\n    }\n\n    @Provides\n    @Singleton\n    ProcessApi getProcessApi(ApiClient apiClient) {\n        return new ProcessApi(apiClient);\n    }\n\n    @Provides\n    @Singleton\n    ProcessStatusUpdater getProcessStatusUpdater(ProcessApi processApi) {\n        return new ProcessStatusUpdater(agentId, processApi);\n    }\n\n    @Override\n    protected void configure() {\n        bind(StateFetcher.class).to(DefaultStateFetcher.class);\n\n        Multibinder<LogAppender> logAppenders = Multibinder.newSetBinder(binder(), LogAppender.class);\n        logAppenders.addBinding().to(RemoteLogAppender.class);\n\n        if (Boolean.parseBoolean(System.getenv(REDIRECT_PROCESS_LOGS_TO_STDOUT_KEY))) {\n            log.info(\"Redirecting process logs into the agent's stdout...\");\n            logAppenders.addBinding().to(StdOutLogAppender.class);\n        }\n\n        bind(LogAppender.class).to(CombinedLogAppender.class);\n\n        bind(AgentImportManager.class).toProvider(AgentImportManagerProvider.class);\n\n        bind(DependencyManagerConfiguration.class).toProvider(AgentDependencyManagerConfigurationProvider.class);\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/logging/AbstractProcessLog.java",
    "content": "package com.walmartlabs.concord.agent.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.LogUtils;\n\npublic abstract class AbstractProcessLog implements ProcessLog {\n\n    @Override\n    public void delete() {\n        // do nothing\n    }\n\n    @Override\n    public void info(String log, Object... args) {\n        log(LogUtils.formatMessage(LogUtils.LogLevel.INFO, log, args));\n    }\n\n    @Override\n    public void warn(String log, Object... args) {\n        log(LogUtils.formatMessage(LogUtils.LogLevel.WARN, log, args));\n    }\n\n    @Override\n    public void error(String log, Object... args) {\n        log(LogUtils.formatMessage(LogUtils.LogLevel.ERROR, log, args));\n    }\n\n    protected abstract void log(String message);\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/logging/CombinedLogAppender.java",
    "content": "package com.walmartlabs.concord.agent.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.inject.Inject;\nimport java.util.Set;\nimport java.util.UUID;\n\npublic class CombinedLogAppender implements LogAppender {\n\n    private final Set<LogAppender> appenders;\n\n    @Inject\n    public CombinedLogAppender(Set<LogAppender> appenders) {\n        this.appenders = appenders;\n    }\n\n    @Override\n    public void appendLog(UUID instanceId, byte[] ab) {\n        appenders.forEach(a -> a.appendLog(instanceId, ab));\n    }\n\n    @Override\n    public boolean appendLog(UUID instanceId, long segmentId, byte[] ab) {\n        boolean result = true;\n        for (LogAppender a : appenders) {\n            boolean done = a.appendLog(instanceId, segmentId, ab);\n            result = result && done;\n        }\n        return result;\n    }\n\n    @Override\n    public boolean updateSegment(UUID instanceId, long segmentId, LogSegmentStats stats) {\n        boolean result = true;\n        for (LogAppender a : appenders) {\n            boolean done = a.updateSegment(instanceId, segmentId, stats);\n            result = result && done;\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/logging/LocalProcessLog.java",
    "content": "package com.walmartlabs.concord.agent.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\n\n/**\n * Local log file. Typically used as a temporary buffer to store process logs\n * before sending them to the server.\n */\npublic class LocalProcessLog extends AbstractProcessLog {\n\n    private static final Logger log = LoggerFactory.getLogger(LocalProcessLog.class);\n\n    private final Path baseDir;\n\n    public LocalProcessLog(Path baseDir) throws IOException {\n        this.baseDir = baseDir;\n        Files.createFile(logFile());\n    }\n\n    @Override\n    public void delete() {\n        Path p = logFile();\n        if (Files.exists(p)) {\n            try {\n                Files.delete(p);\n            } catch (IOException e) {\n                log.warn(\"delete -> error while removing a log file: {}\", p);\n            }\n        }\n    }\n\n    @Override\n    public void log(InputStream src) throws IOException {\n        Path f = logFile();\n        try (OutputStream dst = Files.newOutputStream(f, StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {\n            src.transferTo(dst);\n        }\n    }\n\n    @Override\n    protected void log(String message) {\n        Path f = logFile();\n        try (OutputStream out = Files.newOutputStream(f, StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {\n            out.write(message.getBytes());\n            out.flush();\n        } catch (IOException e) {\n            throw new RuntimeException(\"Error writing to a log file: \" + f, e);\n        }\n    }\n\n    public Path logFile() {\n        return baseDir.resolve(\"system\" + \".log\");\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/logging/LogAppender.java",
    "content": "package com.walmartlabs.concord.agent.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.UUID;\n\npublic interface LogAppender {\n\n    void appendLog(UUID instanceId, byte[] ab);\n\n    boolean appendLog(UUID instanceId, long segmentId, byte[] ab);\n\n    boolean updateSegment(UUID instanceId, long segmentId, LogSegmentStats stats);\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/logging/LogSegmentStats.java",
    "content": "package com.walmartlabs.concord.agent.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentStatus;\n\nimport javax.annotation.Nullable;\n\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\npublic record LogSegmentStats(@Nullable LogSegmentStatus status,\n                              @Nullable Integer errors,\n                              @Nullable Integer warnings) {\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/logging/ProcessLog.java",
    "content": "package com.walmartlabs.concord.agent.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.io.InputStream;\n\npublic interface ProcessLog {\n\n    /**\n     * Removed the associated resources.\n     */\n    void delete();\n\n    /**\n     * Copies the specified stream into the log.\n     */\n    void log(InputStream src) throws IOException;\n\n    void info(String log, Object... args);\n\n    void warn(String log, Object... args);\n\n    void error(String log, Object... args);\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/logging/ProcessLogFactory.java",
    "content": "package com.walmartlabs.concord.agent.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agent.cfg.AgentConfiguration;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.UUID;\nimport java.util.function.Consumer;\n\npublic class ProcessLogFactory {\n\n    private final Path logDir;\n    private final long logStreamMaxDelay;\n    private final LogAppender logAppender;\n\n    @Inject\n    public ProcessLogFactory(AgentConfiguration cfg, LogAppender logAppender) {\n        this.logDir = cfg.getLogDir();\n        this.logStreamMaxDelay = cfg.getLogMaxDelay();\n        this.logAppender = logAppender;\n    }\n\n    public RedirectedProcessLog createRedirectedLog(UUID instanceId, boolean segmented) throws IOException {\n        Path dst = logDir.resolve(instanceId.toString());\n        if (Files.notExists(dst)) {\n            Files.createDirectories(dst);\n        }\n\n        Consumer<RedirectedProcessLog.Chunk> logConsumer;\n        if (segmented) {\n            logConsumer = new SegmentedLogsConsumer(instanceId, logAppender);\n        } else {\n            logConsumer = chunk -> {\n                byte[] ab = new byte[chunk.len()];\n                System.arraycopy(chunk.bytes(), 0, ab, 0, chunk.len());\n                logAppender.appendLog(instanceId, ab);\n            };\n        }\n        return new RedirectedProcessLog(dst, logStreamMaxDelay, logConsumer);\n    }\n\n    public RemoteProcessLog createRemoteLog(UUID instanceId) {\n        return new RemoteProcessLog(instanceId, logAppender);\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/logging/RedirectedProcessLog.java",
    "content": "package com.walmartlabs.concord.agent.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\n\n/**\n * Log that uses a local file as a buffer before sending the data into the specified {@link LogAppender}.\n * Typically, {@link #run(Supplier)} method should be executed in a separate thread.\n */\npublic class RedirectedProcessLog implements ProcessLog {\n\n    protected final long logSteamMaxDelay;\n    private final LocalProcessLog localLog;\n    private final Consumer<Chunk> consumer;\n\n    public RedirectedProcessLog(Path baseDir, long logSteamMaxDelay, Consumer<Chunk> consumer) throws IOException {\n        this.localLog = new LocalProcessLog(baseDir);\n        this.logSteamMaxDelay = logSteamMaxDelay;\n        this.consumer = consumer;\n    }\n\n    public void run(Supplier<Boolean> stopCondition) throws Exception {\n        streamLog(localLog.logFile(), stopCondition, logSteamMaxDelay, consumer);\n    }\n\n    @Override\n    public void delete() {\n        this.localLog.delete();\n    }\n\n    @Override\n    public void log(InputStream src) throws IOException {\n        this.localLog.log(src);\n    }\n\n    @Override\n    public void info(String log, Object... args) {\n        this.localLog.info(log, args);\n    }\n\n    @Override\n    public void warn(String log, Object... args) {\n        this.localLog.warn(log, args);\n    }\n\n    @Override\n    public void error(String log, Object... args) {\n        this.localLog.error(log, args);\n    }\n\n    private static void streamLog(Path p, Supplier<Boolean> stopCondition, long maxDelay, Consumer<Chunk> sink) throws IOException {\n        long total = 0;\n\n        byte[] ab = new byte[8192];\n\n        try (InputStream in = Files.newInputStream(p, StandardOpenOption.READ)) {\n            while (true) {\n                int read = in.read(ab, 0, ab.length);\n                if (read > 0) {\n                    sink.accept(new Chunk(ab, read));\n                    total += read;\n                }\n\n                if (read < ab.length) {\n                    if (stopCondition.get() && total >= Files.size(p)) {\n                        // the log and the job are finished\n                        break;\n                    }\n\n                    // job is still running, wait for more data\n                    try {\n                        Thread.sleep(maxDelay);\n                    } catch (InterruptedException e) {\n                        Thread.currentThread().interrupt();\n                        break;\n                    }\n                }\n            }\n        }\n    }\n\n    public static class Chunk {\n\n        private final byte[] ab;\n        private final int len;\n\n        protected Chunk(byte[] ab, int len) { // NOSONAR\n            this.ab = ab;\n            this.len = len;\n        }\n\n        public byte[] bytes() {\n            return ab;\n        }\n\n        public int len() {\n            return len;\n        }\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/logging/RemoteLogAppender.java",
    "content": "package com.walmartlabs.concord.agent.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agent.AgentConstants;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentStatus;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.io.ByteArrayInputStream;\nimport java.util.UUID;\n\npublic class RemoteLogAppender implements LogAppender {\n\n    private static final Logger log = LoggerFactory.getLogger(RemoteLogAppender.class);\n\n    private final ProcessApi processApi;\n    private final ProcessLogV2Api processLogV2Api;\n\n    @Inject\n    public RemoteLogAppender(ApiClient apiClient) {\n        this.processApi = new ProcessApi(apiClient);\n        this.processLogV2Api = new ProcessLogV2Api(apiClient);\n    }\n\n    @Override\n    public void appendLog(UUID instanceId, byte[] ab) {\n        try {\n            ClientUtils.withRetry(AgentConstants.API_CALL_MAX_RETRIES, AgentConstants.API_CALL_RETRY_DELAY, () -> {\n                processApi.appendProcessLog(instanceId, new ByteArrayInputStream(ab));\n                return null;\n            });\n        } catch (ApiException e) {\n            // TODO handle errors\n            log.warn(\"appendLog ['{}'] -> error: {}\", instanceId, e.getMessage());\n        }\n    }\n\n    @Override\n    public boolean appendLog(UUID instanceId, long segmentId, byte[] ab) {\n        try {\n            ClientUtils.withRetry(AgentConstants.API_CALL_MAX_RETRIES, AgentConstants.API_CALL_RETRY_DELAY, () -> {\n                processLogV2Api.appendProcessLogSegment(instanceId, segmentId, new ByteArrayInputStream(ab));\n                return null;\n            });\n            return true;\n        } catch (ApiException e) {\n            log.warn(\"appendLog ['{}'] -> error: {}\", instanceId, e.getMessage());\n\n            return e.getCode() >= 400 && e.getCode() < 500;\n        }\n    }\n\n    @Override\n    public boolean updateSegment(UUID instanceId, long segmentId, LogSegmentStats stats) {\n        LogSegmentUpdateRequest request = new LogSegmentUpdateRequest()\n                .status(convertStatus(stats.status()))\n                .warnings(stats.warnings())\n                .errors(stats.errors());\n\n        try {\n            ClientUtils.withRetry(AgentConstants.API_CALL_MAX_RETRIES, AgentConstants.API_CALL_RETRY_DELAY,\n                    () -> processLogV2Api.updateProcessLogSegment(instanceId, segmentId, request));\n            return true;\n        } catch (Exception e) {\n            log.warn(\"updateSegment ['{}', '{}', '{}'] -> error: {}\", instanceId, segmentId, stats, e.getMessage());\n        }\n        return false;\n    }\n\n    private static LogSegmentUpdateRequest.StatusEnum convertStatus(LogSegmentStatus status) {\n        if (status == null) {\n            return null;\n        }\n        return switch (status) {\n            case ERROR -> LogSegmentUpdateRequest.StatusEnum.FAILED;\n            case OK -> LogSegmentUpdateRequest.StatusEnum.OK;\n            case RUNNING -> LogSegmentUpdateRequest.StatusEnum.RUNNING;\n            case SUSPENDED -> LogSegmentUpdateRequest.StatusEnum.SUSPENDED;\n        };\n    }\n}"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/logging/RemoteProcessLog.java",
    "content": "package com.walmartlabs.concord.agent.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.InputStream;\nimport java.util.UUID;\n\n/**\n * Simple log implementation that sends all data into a {@link LogAppender} directly.\n */\npublic class RemoteProcessLog extends AbstractProcessLog {\n\n    private final UUID instanceId;\n    private final LogAppender appender;\n\n    public RemoteProcessLog(UUID instanceId, LogAppender appender) {\n        this.instanceId = instanceId;\n        this.appender = appender;\n    }\n\n    @Override\n    public void log(InputStream src) {\n        throw new IllegalStateException(\"Not supported\");\n    }\n\n    @Override\n    protected void log(String message) {\n        appender.appendLog(instanceId, message.getBytes());\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/logging/SegmentHeaderParser.java",
    "content": "package com.walmartlabs.concord.agent.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.common.logger.ImmutableLogSegmentHeader;\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentDeserializer;\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentHeader;\nimport org.immutables.value.Value;\n\nimport java.nio.ByteBuffer;\nimport java.util.List;\n\npublic class SegmentHeaderParser {\n\n    private static final int MAX_FIELD_BYTES = String.valueOf(Long.MAX_VALUE).getBytes().length;\n\n    // msgLength|segmentId|status|warnings|errors|msg\n\n    public static int parse(byte[] ab, List<Segment> segments, List<Position> invalidSegments) {\n        Field field = Field.MSG_LENGTH;\n        StringBuilder fieldData = new StringBuilder();\n        int mark = -1;\n        ImmutableLogSegmentHeader.Builder headerBuilder = LogSegmentHeader.builder();\n\n        boolean continueParse = true;\n        State state = State.FIND_HEADER;\n        ByteBuffer bb = ByteBuffer.wrap(ab);\n        while (continueParse) {\n            switch (state) {\n                case FIND_HEADER: {\n                    if (bb.remaining() <= 0) {\n                        continueParse = false;\n                        break;\n                    }\n\n                    char ch = (char) bb.get();\n                    if (ch == '|') {\n                        if (mark != -1) {\n                            invalidSegments.add(new Position(mark, bb.position() - 1));\n                        }\n\n                        mark = bb.position() - 1;\n                        state = State.FIELD_DATA;\n                    } else {\n                        if (mark == -1) {\n                            mark = bb.position() - 1;\n                        }\n                    }\n                    break;\n                }\n                case FIELD_DATA: {\n                    if (bb.remaining() <= 0) {\n                        continueParse = false;\n                        break;\n                    }\n\n                    char ch = (char)bb.get();\n                    if (ch == '|') {\n                        state = State.END_FIELD;\n                        break;\n                    }\n\n                    if (fieldData.length() > MAX_FIELD_BYTES || !Character.isDigit(ch)) {\n                        // reset\n                        fieldData.setLength(0);\n                        field = Field.MSG_LENGTH;\n                        state = State.FIND_HEADER;\n                        break;\n                    }\n\n                    fieldData.append(ch);\n                    break;\n                }\n                case END_FIELD: {\n                    String fieldValue = fieldData.toString();\n                    if (fieldData.isEmpty()) {\n                        // reset\n                        field = Field.MSG_LENGTH;\n                        state = State.FIND_HEADER;\n                        bb.position(bb.position() - 1);\n                        break;\n                    }\n\n                    field.process(fieldValue, headerBuilder);\n\n                    field = field.next();\n                    if (field == null) {\n                        LogSegmentHeader h = headerBuilder.build();\n                        segments.add(new Segment(h, bb.position()));\n\n                        int actualLength = Math.min(h.length(), bb.remaining());\n                        bb.position(bb.position() + actualLength);\n\n                        // reset\n                        field = Field.MSG_LENGTH;\n                        mark = -1;\n\n                        state = State.FIND_HEADER;\n                    } else {\n                        state = State.FIELD_DATA;\n                    }\n\n                    fieldData.setLength(0);\n\n                    break;\n                }\n            }\n        }\n\n        int result;\n        if (mark != -1) {\n            if (state == State.FIND_HEADER) {\n                invalidSegments.add(new Position(mark, bb.position()));\n                result = bb.position();\n            } else {\n                result = mark;\n            }\n        } else {\n            result = bb.position();\n        }\n\n        return result;\n    }\n\n    public record Position(int start, int end) {\n    }\n\n    public record Segment(LogSegmentHeader header, int msgStart) {\n    }\n\n    enum State {\n        FIND_HEADER,\n        FIELD_DATA,\n        END_FIELD\n    }\n\n    enum Field {\n        MSG_LENGTH {\n            @Override\n            public Field next() {\n                return SEGMENT_ID;\n            }\n\n            @Override\n            public void process(String fieldValue, ImmutableLogSegmentHeader.Builder headerBuilder) {\n                headerBuilder.length(Integer.parseInt(fieldValue));\n            }\n        },\n\n        SEGMENT_ID {\n            @Override\n            public Field next() {\n                return STATUS;\n            }\n\n            @Override\n            public void process(String fieldValue, ImmutableLogSegmentHeader.Builder headerBuilder) {\n                headerBuilder.segmentId(Long.parseLong(fieldValue));\n            }\n        },\n\n        STATUS {\n            @Override\n            public Field next() {\n                return WARNINGS;\n            }\n\n            @Override\n            public void process(String fieldValue, ImmutableLogSegmentHeader.Builder headerBuilder) {\n                headerBuilder.status(LogSegmentDeserializer.deserializeStatus(fieldValue));\n            }\n        },\n\n        WARNINGS {\n            @Override\n            public Field next() {\n                return ERRORS;\n            }\n\n            @Override\n            public void process(String fieldValue, ImmutableLogSegmentHeader.Builder headerBuilder) {\n                headerBuilder.warnCount(Integer.parseInt(fieldValue));\n            }\n        },\n\n        ERRORS {\n            @Override\n            public Field next() {\n                return null;\n            }\n\n            @Override\n            public void process(String fieldValue, ImmutableLogSegmentHeader.Builder headerBuilder) {\n                headerBuilder.errorCount(Integer.parseInt(fieldValue));\n            }\n        };\n\n        public abstract Field next();\n\n        public abstract void process(String fieldValue, ImmutableLogSegmentHeader.Builder headerBuilder);\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/logging/SegmentedLogsConsumer.java",
    "content": "package com.walmartlabs.concord.agent.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentHeader;\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentSerializer;\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentStatus;\n\nimport java.util.*;\nimport java.util.function.Consumer;\n\nimport static com.walmartlabs.concord.agent.logging.SegmentHeaderParser.Position;\nimport static com.walmartlabs.concord.agent.logging.SegmentHeaderParser.Segment;\n\npublic class SegmentedLogsConsumer implements Consumer<RedirectedProcessLog.Chunk> {\n\n    private static final byte[] EMPTY = new byte[0];\n\n    private final UUID instanceId;\n    private final LogAppender logAppender;\n\n    private byte[] unparsed = EMPTY;\n\n    public SegmentedLogsConsumer(UUID instanceId, LogAppender logAppender) {\n        this.instanceId = instanceId;\n        this.logAppender = logAppender;\n    }\n\n    @Override\n    public void accept(RedirectedProcessLog.Chunk chunk) {\n        byte[] ab = new byte[unparsed.length + chunk.len()];\n        if (unparsed.length > 0) {\n            System.arraycopy(unparsed, 0, ab, 0, unparsed.length);\n        }\n        System.arraycopy(chunk.bytes(), 0, ab, unparsed.length, chunk.len());\n        unparsed = EMPTY;\n\n        List<Segment> segments = new ArrayList<>();\n        List<Position> invalidSegments = new ArrayList<>();\n        int pos = SegmentHeaderParser.parse(ab, segments, invalidSegments);\n\n        invalidSegmentsToSystemSegments(invalidSegments, segments);\n        Map<Long, List<Segment>> segmentsById = byId(segments);\n        for (Map.Entry<Long, List<Segment>> e : segmentsById.entrySet()) {\n            int buffLength = e.getValue().stream().mapToInt(h -> actualLength(h, ab.length)).sum();\n            byte[] segmentBuffer = new byte[buffLength];\n            fillBuffer(e.getValue(), ab, segmentBuffer);\n\n            if (segmentBuffer.length > 0) {\n                // TODO: retry?\n                logAppender.appendLog(instanceId, e.getKey(), segmentBuffer);\n            }\n\n            LogSegmentStats stats = findStats(e.getValue());\n            if (stats != null) {\n                logAppender.updateSegment(instanceId, e.getKey(), stats);\n            }\n        }\n\n        Segment partialSegment = findPartialSegment(segments, ab.length);\n        if (partialSegment != null) {\n            unparsed = LogSegmentSerializer.serializeHeader(\n                    partialSegment.header(), partialSegment.header().length() - actualLength(partialSegment, ab.length));\n        }\n\n        if (pos < ab.length) {\n            if (unparsed != EMPTY) {\n                throw new RuntimeException(\"Unexpected partial segment and unparsed tail\");\n            }\n\n            unparsed = Arrays.copyOfRange(ab, pos, ab.length);\n        }\n    }\n\n    private void invalidSegmentsToSystemSegments(List<Position> invalidSegments, List<Segment> segments) {\n        for (Position s : invalidSegments) {\n            LogSegmentHeader header = LogSegmentHeader.builder()\n                    .status(LogSegmentStatus.RUNNING)\n                    .segmentId(0)\n                    .errorCount(0)\n                    .warnCount(0)\n                    .length(s.end() - s.start())\n                    .build();\n            segments.add(new Segment(header, s.start()));\n        }\n    }\n\n    private static int actualLength(Segment segment, int chunkLength) {\n        return Math.min(chunkLength - segment.msgStart(), segment.header().length());\n    }\n\n    private static Map<Long, List<Segment>> byId(List<Segment> segments) {\n        Map<Long, List<Segment>> result = new LinkedHashMap<>();\n        for (Segment s : segments) {\n            result.computeIfAbsent(s.header().segmentId(), id -> new ArrayList<>())\n                    .add(s);\n        }\n        return result;\n    }\n\n    private static void fillBuffer(List<Segment> segments, byte[] from, byte[] to) {\n        int i = 0;\n        for (Segment s : segments) {\n            int actualLength = actualLength(s, from.length);\n            for (int j = 0; j < actualLength; j++) {\n                to[i++] = from[j + s.msgStart()];\n            }\n        }\n    }\n\n    private static Segment findPartialSegment(List<Segment> segments, int chunkLength) {\n        Segment result = null;\n        for (Segment segment : segments) {\n            if (actualLength(segment, chunkLength) != segment.header().length()) {\n                if (result != null) {\n                    throw new RuntimeException(\"Unexpected second partial segment\");\n                }\n                result = segment;\n            }\n        }\n        return result;\n    }\n\n    private static LogSegmentStats findStats(List<Segment> segments) {\n        boolean done = false;\n        LogSegmentStatus status = null;\n        int errorCount = 0;\n        int warnCount = 0;\n        ListIterator<Segment> it = segments.listIterator(segments.size());\n        while (it.hasPrevious()) {\n            Segment segment = it.previous();\n            if (segment.header().status() != LogSegmentStatus.RUNNING) {\n                done = true;\n                status = segment.header().status();\n            }\n            if (warnCount == 0 && segment.header().warnCount() > 0) {\n                warnCount = segment.header().warnCount();\n            }\n            if (errorCount == 0 && segment.header().errorCount() > 0) {\n                errorCount = segment.header().errorCount();\n            }\n        }\n\n        if (done || errorCount > 0 || warnCount > 0) {\n            return new LogSegmentStats(status, errorCount, warnCount);\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/logging/StdOutLogAppender.java",
    "content": "package com.walmartlabs.concord.agent.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.UUID;\n\npublic class StdOutLogAppender implements LogAppender {\n\n    private static final String PREFIX = \"RUNNER: \";\n\n    @Override\n    public void appendLog(UUID instanceId, byte[] ab) {\n        System.out.print(PREFIX + new String(ab));\n    }\n\n    @Override\n    public boolean appendLog(UUID instanceId, long segmentId, byte[] ab) {\n        System.out.print(PREFIX + new String(ab));\n        return true;\n    }\n\n    @Override\n    public boolean updateSegment(UUID instanceId, long segmentId, LogSegmentStats stats) {\n        return true;\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/mmode/MaintenanceModeListener.java",
    "content": "package com.walmartlabs.concord.agent.mmode;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic interface MaintenanceModeListener {\n\n    Status onMaintenanceMode();\n\n    Status getMaintenanceModeStatus();\n\n    class Status {\n\n        private final boolean maintenanceMode;\n        private final long workersAlive;\n\n        public Status(boolean maintenanceMode, long workersAlive) {\n            this.maintenanceMode = maintenanceMode;\n            this.workersAlive = workersAlive;\n        }\n\n        public boolean isMaintenanceMode() {\n            return maintenanceMode;\n        }\n\n        public long getWorkersAlive() {\n            return workersAlive;\n        }\n\n        @Override\n        public String toString() {\n            return \"Status{\" +\n                    \"maintenanceMode=\" + maintenanceMode +\n                    \", workersAlive=\" + workersAlive +\n                    '}';\n        }\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/mmode/MaintenanceModeNotifier.java",
    "content": "package com.walmartlabs.concord.agent.mmode;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.sun.net.httpserver.HttpExchange;\nimport com.sun.net.httpserver.HttpHandler;\nimport com.sun.net.httpserver.HttpServer;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.net.InetSocketAddress;\n\npublic class MaintenanceModeNotifier {\n\n    private static final Logger log = LoggerFactory.getLogger(MaintenanceModeNotifier.class);\n\n    private final HttpServer server;\n\n    public MaintenanceModeNotifier(String host, Integer port, MaintenanceModeListener listener) throws IOException {\n        this.server = HttpServer.create(new InetSocketAddress(host, port), 0);\n        this.server.createContext(\"/maintenance-mode\", new MaintenanceModeHandler(listener));\n    }\n\n    public void start() {\n        server.start();\n        log.info(\"start -> done, listening on {}\", server.getAddress());\n    }\n\n    public void stop() {\n        server.stop(0);\n        log.info(\"stop -> done\");\n    }\n\n    private static class MaintenanceModeHandler implements HttpHandler {\n\n        private static final String NOT_FOUND_RESPONSE = \"404 (Not Found)\\n\";\n\n        private final ObjectMapper objectMapper = new ObjectMapper();\n        private final MaintenanceModeListener listener;\n\n        private MaintenanceModeHandler(MaintenanceModeListener listener) {\n            this.listener = listener;\n        }\n\n        @Override\n        public void handle(HttpExchange httpExchange) throws IOException {\n            MaintenanceModeListener.Status status = null;\n            if (\"GET\".equals(httpExchange.getRequestMethod())) {\n                status = onMaintenanceModeStatus();\n            } else if (\"POST\".equals(httpExchange.getRequestMethod())) {\n                status = onMaintenanceMode();\n            }\n\n            if (status != null) {\n                httpExchange.getResponseHeaders().set(\"Content-Type\", \"application/json\");\n                response(httpExchange, 200, objectMapper.writeValueAsBytes(status));\n                return;\n            }\n\n            response(httpExchange, 404, NOT_FOUND_RESPONSE.getBytes());\n        }\n\n        private void response(HttpExchange httpExchange, int code, byte[] response) throws IOException {\n            httpExchange.sendResponseHeaders(code, response.length);\n            try (OutputStream os = httpExchange.getResponseBody()) {\n                os.write(response);\n            }\n        }\n\n        private MaintenanceModeListener.Status onMaintenanceMode() {\n            MaintenanceModeListener.Status status = listener.onMaintenanceMode();\n\n            log.info(\"onMaintenanceMode -> {}\", status);\n\n            return status;\n        }\n\n        private MaintenanceModeListener.Status onMaintenanceModeStatus() {\n            MaintenanceModeListener.Status status = listener.getMaintenanceModeStatus();\n\n            log.info(\"onMaintenanceModeStatus -> {}\", status);\n\n            return status;\n        }\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/remote/ApiClientFactory.java",
    "content": "package com.walmartlabs.concord.agent.remote;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.agent.cfg.ServerConfiguration;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.time.Duration;\nimport java.time.temporal.ChronoUnit;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic class ApiClientFactory {\n\n    private static final String SESSION_COOKIE_NAME = \"JSESSIONID\";\n\n    private final ServerConfiguration cfg;\n\n    private final DefaultApiClientFactory clientFactory;\n\n    @Inject\n    public ApiClientFactory(ServerConfiguration cfg) throws Exception {\n        this.cfg = cfg;\n        this.clientFactory = new DefaultApiClientFactory(cfg.getApiBaseUrl(), Duration.of(cfg.getConnectTimeout(), ChronoUnit.MILLIS), cfg.isVerifySsl());\n    }\n\n    public ApiClient create(String sessionToken) throws IOException {\n        ImmutableApiClientConfiguration.Builder clientCfgBuilder = ApiClientConfiguration.builder()\n                .baseUrl(cfg.getApiBaseUrl());\n\n        if (sessionToken != null) {\n            clientCfgBuilder.sessionToken(sessionToken);\n        } else {\n            clientCfgBuilder.apiKey(cfg.getApiKey());\n        }\n\n        ApiClient client = clientFactory.create(clientCfgBuilder.build())\n                .setReadTimeout(Duration.of(cfg.getReadTimeout(), ChronoUnit.MILLIS))\n                .setUserAgent(cfg.getUserAgent());\n\n        Map<String, String> cookieJar = new HashMap<>();\n        client.setResponseInterceptor(response -> {\n            List<String> cookies = response.headers().allValues(\"Set-Cookie\");\n            if (cookies.isEmpty()) {\n                return;\n            }\n\n            for (String cookie : cookies) {\n                if (cookie.startsWith(SESSION_COOKIE_NAME)) {\n                    cookieJar.put(SESSION_COOKIE_NAME, cookie);\n                }\n            }\n        });\n\n        client.setRequestInterceptor(builder -> {\n            for (Map.Entry<String, String> cookie : cookieJar.entrySet()) {\n                builder.header(\"Cookie\", cookie.getValue());\n            }\n        });\n\n        return client;\n    }\n}"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/remote/AttachmentsUploader.java",
    "content": "package com.walmartlabs.concord.agent.remote;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agent.AgentConstants;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.TemporaryPath;\nimport com.walmartlabs.concord.common.ZipUtils;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;\n\nimport javax.inject.Inject;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.UUID;\n\npublic class AttachmentsUploader {\n\n    private final ApiClient apiClient;\n\n    @Inject\n    public AttachmentsUploader(ApiClient apiClient) {\n        this.apiClient = apiClient;\n    }\n\n    public void upload(UUID instanceId, Path workDir) throws Exception {\n        Path attachmentsDir = workDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME);\n        if (!Files.exists(attachmentsDir)) {\n            return;\n        }\n\n        try (TemporaryPath tmp = PathUtils.tempFile(\"attachments\", \".zip\")) {\n            try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(Files.newOutputStream(tmp.path()))) {\n                ZipUtils.zip(zip, attachmentsDir);\n            }\n\n            ProcessApi api = new ProcessApi(apiClient);\n            ClientUtils.withRetry(AgentConstants.API_CALL_MAX_RETRIES, AgentConstants.API_CALL_RETRY_DELAY, () -> {\n                api.uploadProcessAttachments(instanceId, Files.newInputStream(tmp.path()));\n                return null;\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/remote/ProcessStatusUpdater.java",
    "content": "package com.walmartlabs.concord.agent.remote;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agent.AgentConstants;\nimport com.walmartlabs.concord.client2.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.UUID;\n\npublic class ProcessStatusUpdater {\n\n    private static final Logger log = LoggerFactory.getLogger(ProcessStatusUpdater.class);\n\n    private final String agentId;\n    private final ProcessApi processApi;\n\n    public ProcessStatusUpdater(String agentId, ProcessApi processApi) {\n        this.agentId = agentId;\n        this.processApi = processApi;\n    }\n\n    public void update(UUID instanceId, ProcessEntry.StatusEnum status) {\n        try {\n            ClientUtils.withRetry(AgentConstants.API_CALL_MAX_RETRIES, AgentConstants.API_CALL_RETRY_DELAY, () -> {\n                processApi.updateStatus(instanceId, agentId, status.name());\n                return null;\n            });\n        } catch (ApiException e) {\n            log.warn(\"updateStatus ['{}'] -> error while updating status of a job: {}\", instanceId, e.getMessage());\n        }\n    }\n}\n"
  },
  {
    "path": "agent/src/main/java/com/walmartlabs/concord/agent/remote/QueueClientProvider.java",
    "content": "package com.walmartlabs.concord.agent.remote;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agent.cfg.AgentConfiguration;\nimport com.walmartlabs.concord.agent.cfg.ServerConfiguration;\nimport com.walmartlabs.concord.server.queueclient.QueueClient;\nimport com.walmartlabs.concord.server.queueclient.QueueClientConfiguration;\n\nimport javax.inject.Inject;\nimport javax.inject.Provider;\nimport java.net.URISyntaxException;\n\npublic class QueueClientProvider implements Provider<QueueClient> {\n\n    private final AgentConfiguration agentCfg;\n    private final ServerConfiguration serverCfg;\n\n    @Inject\n    public QueueClientProvider(AgentConfiguration agentCfg, ServerConfiguration serverCfg) {\n        this.agentCfg = agentCfg;\n        this.serverCfg = serverCfg;\n    }\n\n    @Override\n    public QueueClient get() {\n        try {\n            QueueClient queueClient = new QueueClient(new QueueClientConfiguration.Builder(serverCfg.getWebsocketUrls())\n                    .agentId(agentCfg.getAgentId())\n                    .apiKey(serverCfg.getApiKey())\n                    .userAgent(serverCfg.getUserAgent())\n                    .connectTimeout(serverCfg.getConnectTimeout())\n                    .pingInterval(serverCfg.getPingInterval())\n                    .maxNoActivityPeriod(serverCfg.getMaxNoActivityPeriod())\n                    .processRequestDelay(serverCfg.getProcessRequestDelay())\n                    .reconnectDelay(serverCfg.getReconnectDelay())\n                    .build());\n\n            queueClient.start();\n\n            return queueClient;\n        } catch (URISyntaxException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "agent/src/main/resources/com/walmartlabs/concord/agent/logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] [%-5level] %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "agent/src/main/resources/concord-agent.conf",
    "content": "# Concord Agent\n#\n# Note: most of path parameters accept either absolute paths or a directory\n# name. With the latter, the effective path is ${CONCORD_TMP_DIR}/${value}\n#\n# E.g.\n#\n#   workDirBase = \"workDirs\" # means \"/tmp/workDirs\", created automatically\n#\n#   dependencyCacheDir = \"/data/concord/dependencyCache\" # path to an existing directory\n#\nconcord-agent {\n\n    # unique ID of the agent\n    # a string value, 36 characters max, typically an UUID\n    # generated on start if not specified\n    id = ${?AGENT_ID}\n\n    # agent capabilities, JSON object\n    capabilities = { }\n\n    # directory to cache dependencies\n    dependencyCacheDir = \"dependencyCache\"\n\n    # directory to store process dependency lists\n    dependencyListsDir = \"dependencyLists\"\n\n    # timeout to resolve process dependencies\n    dependencyResolveTimeout = \"10 minutes\"\n\n    # use repositories from `CONCORD_MAVEN_CFG` file only\n    # (allow/ignore repositories from artifact descriptor)\n    dependencyStrictRepositories = false\n\n    # artifact exclude patterns\n    dependencyExclusions = []\n\n    # explicitly resolve v1 version of the concord HTTP client\n    explicitlyResolveV1Client = true\n\n    # resolve Maven artifacts in the offline mode\n    mavenOfflineMode = false\n\n    # base directory to store the process payload\n    # created automatically if not specified\n    payloadDir = \"payload\"\n\n    # base directory for the process' ${workDir}\n    #\n    # Use the same value and use absolute paths when running multiple Agents.\n    # If the process keeps ${workDir} value as a part of another variable,\n    # the value might not longer be valid if the process restarts and\n    # gets a new Agent.\n    workDirBase = \"/tmp/concord-agent/workDirs\"\n    workDirBase = ${?WORK_DIR_BASE}\n\n    # directory to store the process logs\n    # created automatically if not specified\n    logDir = \"logs\"\n\n    # maximum delay between log chunks\n    # determines how ofter the logs are send back to the server\n    logMaxDelay = \"2 seconds\"\n\n    # replace the current process' workDir in logs with literal \"$WORK_DIR\"\n    workDirMasking = true\n\n    # maximum number of concurrent processes\n    workersCount = 3\n    workersCount = ${?WORKERS_COUNT}\n\n    # host/ip of the maintenance mode endpoint\n    maintenanceModeListenerHost = \"localhost\"\n\n    # port of the maintenance mode endpoint\n    maintenanceModeListenerPort = 8010\n    maintenanceModeListenerPort = ${?MM_PORT}\n\n    # interval between new payload requests\n    pollInterval = \"2 seconds\"\n\n    # JVM prefork settings\n    prefork {\n        # enable/disabled the use of \"preforks\"\n        #\n        # When enabled, Agent keeps a copy of the process' JVM as a \"spare\".\n        # If Agent receives another process with the same classpath, JVM and\n        # Concord Runtime parameters, the \"spare\" is used. This can sometimes\n        # minimize the cost of JVM startup and classpath scanning.\n        #\n        # Note, enabling this mechanism can have other side-effects.\n        # The effective ${workDir} might exist before the process ID is known\n        # (in order to keep a \"spare\" running there, with all Java dependencies\n        # loaded).\n        # If any process keeps a copy of ${workDir} value as a part of another\n        # variable, the value might get stale, e.g. if the process restarts\n        # (or resumes after suspend) and, subsequently, gets a \"fresh\" workDir.\n        #\n        # When \"false\", the process' ${workDir} is always ${workDirBase}/${instanceId}\n        enabled = false\n\n        # maximum time to keep a preforked JVM\n        maxAge = \"30 seconds\"\n\n        # maximum number of preforks\n        maxCount = 3\n    }\n\n    # server connection settings\n    server {\n        apiBaseUrl = \"http://localhost:8001\"\n        apiBaseUrl = ${?SERVER_API_BASE_URL}\n\n        # comma-separated list or URLs\n        websocketUrl = \"ws://localhost:8001/websocket\"\n        websocketUrl = ${?SERVER_WEBSOCKET_URL}\n\n        verifySsl = false\n\n        connectTimeout = \"30 seconds\"\n        readTimeout = \"1 minute\"\n\n        retryCount = 5\n        retryInterval = \"30 seconds\"\n\n        # User-Agent header to use with API requests\n        userAgent = null\n        userAgent = ${?USER_AGENT}\n\n        # interval between WS ping requests in case of no other activity\n        websocketPingInterval = \"10 seconds\"\n        # maximum period of no activity before reconnect\n        websocketMaxNoActivityPeriod = \"30 seconds\"\n\n\n        # API key to use\n        # Generated on Server first start or defined in server.conf at db.changeLogParameters.defaultAgentToken\n        # IMPORTANT! After initialization, create a new token via API and delete initial token\n        apiKey = \"\"\n        apiKey = ${?SERVER_API_KEY}\n\n        # maximum time interval without a heartbeat before the process fails\n        maxNoHeartbeatInterval = \"5 minutes\"\n\n        # delay between successful polling attempts\n        processRequestDelay = \"1 seconds\"\n\n        # delay between re-connection attempts if the server is unreachable or unhealthy\n        reconnectDelay = \"5 seconds\"\n    }\n\n    docker {\n        host = \"tcp://127.0.0.1:2375\"\n        host = ${?DOCKER_HOST}\n\n        orphanSweeperEnabled = false\n        orphanSweeperPeriod = \"15 minutes\"\n\n        # list of volumes mounted into the process' containers in addition to the /workspace\n        # affects only the plugins, such as `docker` and `ansible`\n        extraVolumes = []\n\n        # expose docker daemon to containers started by DockerService\n        exposeDockerDaemon = true\n    }\n\n    repositoryCache {\n        # directory to store the local repo cache\n        # created automatically if not specified\n        # cacheDir = \"/tmp/concord/repos\"\n\n        # timeout for checkout operations (ms)\n        lockTimeout = \"3 minutes\"\n\n        # directory to store the local repo cache info\n        # created automatically if not specified\n        #cacheInfoDir = \"/tmp/concord/repos_info\"\n\n        # the allowed concurrency level when pulling Git data\n        lockCount = 8\n\n        # max cached repo age in ms\n        maxAge = \"1 day\"\n    }\n\n    # git clone config\n    git {\n        # if true, skip Git fetch, use workspace state only\n        skip = false\n\n        # GitHub auth token to use when cloning repositories without explicitly\n        # configured authentication. Deprecated in favor of systemAuth list of\n        # tokens or service-specific app config (e.g. github)\n        # oauth = \"...\"\n\n        # specific username to use for auth\n        # oauthUsername = \"\"\n\n        # regex to match against git server's hostname + port + path so oauth\n        # token isn't used for and unexpected host\n        # oauthUrlPattern = \"\"\n\n        # List of system-provided auth token configs\n        # {\n        #   \"token\" = \"...\",\n        #   \"username\" = \"...\", # optional, username to send with auth token\n        #   \"urlPattern\" = \"...\" # required, regex to match against target git host + port + path\n        # }\n        systemAuth = []\n\n        # use GIT's shallow clone\n        shallowClone = true\n\n        # do not execute fetch if the current HEAD is the latest commit ID\n        checkAlreadyFetched = true\n\n        # default timeout duration for any git operation\n        defaultOperationTimeout = \"10 minutes\"\n\n        # fetch timeout duration\n        fetchTimeout = \"10 minutes\"\n\n        # see GIT documentation for GIT_HTTP_LOW_SPEED_LIMIT and GIT_HTTP_LOW_SPEED_TIME\n        # use with caution, can cause performance issues\n        httpLowSpeedLimit = 0\n        httpLowSpeedTime = \"10 minutes\"\n\n        sshTimeoutRetryCount = 1\n        sshTimeout = \"10 minutes\"\n\n        # max bytes to keep from a git cli process output (distinct for stdout and stderr)\n        maxGitCliOutputBytes = 512\n    }\n\n       # github app settings. While this works on the agent, it's preferable to\n       # get auth token from concord-server via externalTokenProvider\n       github {\n           # App installation settings. Multiple auth (private key) definitions are supported,\n           # as each is matched to a particular url pattern.\n           appInstallation {\n               # {\n               #     type = \"GITHUB_APP_INSTALLATION\",\n               #     urlPattern = \"github.com\",  # regex\n               #     username = \"...\",  # optional, defaults to \"x-access-token\"\n               #     apiUrl = \"https://api.github.com\", # github api url, usually *not* the same as the repo url host/path\n               #     clientId = \"...\",\n               #     privateKey = \"/path/to/pk.pem\"\n               # }\n               # or static oauth config. Not exactly a \"GitHub App\", but can do some\n               # API interactions and cloning. Less preferred to actual app.\n               # {\n               #     type = \"OAUTH_TOKEN\",\n               #     token = \"...\",\n               #     username = \"...\",  # optional, usually not necessary\n               #     urlPattern = \"...\" # regex to match against git server's hostname + port + path\n               # }\n               auth = []\n           }\n       }\n\n    imports {\n        # base git url for imports\n        src = \"\"\n\n        # list of disabled import processors\n        # e.g. \"dir\" is useful for local development, but potentially a security issue\n        disabledProcessors = [\n            \"dir\"\n        ]\n    }\n\n    # configuration of \"runners\" -- JARs that are responsible for the actual process execution\n\n    # common configuration for all runners\n    runner {\n        # directory to store process configuration files\n        cfgDir = null\n\n        # reserved for the future use\n        securityManagerEnabled = false\n\n        # command to use to run the runner JAR\n        # '${java.home}/bin/java' by default\n        #javaCmd = \"java\"\n\n        # default JVM parameters\n        jvmParams = [\n            \"-Xmx128m\",\n            \"-XX:+HeapDumpOnOutOfMemoryError\",\n            \"-XX:+ExitOnOutOfMemoryError\",\n            \"-XX:HeapDumpPath=/tmp\"\n        ]\n\n        # if set, the Agent copies all process files into a persistentWorkDir's subdirectory\n        # after the process ends (regardless of the status)\n        # should not be used in production environments\n        # persistentWorkDir = /path/to/dir\n\n        # if true, agent will forcibly kill any remaining child PIDs (i.e. zombies)\n        # of the runner process\n        cleanRunnerDescendants = false\n    }\n\n    # the default v1 runtime configuration\n    runnerV1 = ${runner}\n    runnerV1 {\n        # path to the runner v1 JAR, must be local to the agent\n        path = null\n        path = ${?RUNNER_V1_PATH}\n\n        mainClass = \"com.walmartlabs.concord.runner.Main\"\n\n        # local dev only\n        propertiesFile = \"runnerV1.properties\"\n\n        # use the segmented logs API\n        segmentedLogs = false\n    }\n\n    # the v2 runtime configuration\n    runnerV2 = ${runner}\n    runnerV2 {\n        # path to the runner v2 JAR, must be local to the agent\n        path = null\n        path = ${?RUNNER_V2_PATH}\n\n        mainClass = \"com.walmartlabs.concord.runtime.v2.runner.Main\"\n\n        segmentedLogs = true\n\n        # local dev only\n        propertiesFile = \"runnerV2.properties\"\n    }\n\n    runtimes = {\n        \"concord-v1\" = ${runnerV1},\n        \"concord-v2\" = ${runnerV2},\n    }\n\n    development {\n    }\n\n    production {\n    }\n}\n"
  },
  {
    "path": "agent/src/main/resources/logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] [%-5level] %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "agent/src/test/java/com/walmartlabs/concord/agent/AgentAuthTokenProviderTest.java",
    "content": "package com.walmartlabs.concord.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AuthTokenProvider;\nimport com.walmartlabs.concord.common.ExternalAuthToken;\nimport com.walmartlabs.concord.github.appinstallation.GitHubAppInstallation;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\nimport java.net.URI;\nimport java.time.OffsetDateTime;\nimport java.util.Optional;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.when;\n\n@ExtendWith(MockitoExtension.class)\nclass AgentAuthTokenProviderTest {\n\n    @Mock\n    GitHubAppInstallation ghApp;\n\n    @Mock\n    AuthTokenProvider.OauthTokenProvider oauthTokenProvider;\n\n    @Test\n    void testGitHubApp() {\n        when(ghApp.getToken(any(), any())).\n                thenReturn(Optional.of(ExternalAuthToken.SimpleToken.builder()\n                        .token(\"gh-installation-token\")\n                        .expiresAt(OffsetDateTime.now().plusMinutes(60))\n                        .build()));\n        when(ghApp.supports(any(), any())).thenReturn(true);\n\n        var provider = new AgentAuthTokenProvider(ghApp, oauthTokenProvider);\n\n        // --\n\n        assertTrue(provider.supports(URI.create(\"https://github.local/owner/repo.git\"), null));\n        var o = provider.getToken(URI.create(\"https://github.local/owner/repo.git\"), null);\n\n        // --\n\n        assertTrue(o.isPresent());\n        var result = assertInstanceOf(ExternalAuthToken.class, o.get());\n        assertEquals(\"gh-installation-token\", result.token());\n    }\n\n    @Test\n    void testOauth() {\n        when(oauthTokenProvider.supports(any(), any())).thenReturn(true);\n        when(oauthTokenProvider.getToken(any(), any()))\n                .thenReturn(Optional.of(ExternalAuthToken.StaticToken.builder()\n                        .token(\"oauth-token\")\n                        .build()));\n\n        var provider = new AgentAuthTokenProvider(ghApp, oauthTokenProvider);\n\n        // --\n\n        assertTrue(provider.supports(URI.create(\"https://github.local/owner/repo.git\"), null));\n        var o = provider.getToken(URI.create(\"https://github.local/owner/repo.git\"), null);\n\n        // --\n\n        assertTrue(o.isPresent());\n        var result = assertInstanceOf(ExternalAuthToken.class, o.get());\n        assertEquals(\"oauth-token\", result.token());\n    }\n\n    @Test\n    void testNoAuth() {\n        when(ghApp.supports(any(), any())).thenReturn(false);\n        when(oauthTokenProvider.supports(any(), any())).thenReturn(false);\n\n        var provider = new AgentAuthTokenProvider(ghApp, oauthTokenProvider);\n\n        // --\n\n        assertFalse(provider.supports(URI.create(\"https://github.local/owner/repo.git\"), null));\n        var o = provider.getToken(URI.create(\"https://github.local/owner/repo.git\"), null);\n\n        // --\n\n        assertFalse(o.isPresent());\n    }\n\n}\n"
  },
  {
    "path": "agent/src/test/java/com/walmartlabs/concord/agent/executors/runner/JobDependenciesTest.java",
    "content": "package com.walmartlabs.concord.agent.executors.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.InputStream;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.fail;\nimport static org.mockito.Mockito.*;\n\npublic class JobDependenciesTest {\n\n    @Test\n    public void test() throws Exception {\n        Path payloadDir = Files.createTempDirectory(\"test\");\n\n        Path versionsFile = payloadDir.resolve(Constants.Files.CONCORD_SYSTEM_DIR_NAME)\n                .resolve(Constants.Files.DEPENDENCY_VERSIONS_FILE_NAME);\n\n        Files.createDirectories(versionsFile.getParent());\n\n        try (InputStream in = JobDependenciesTest.class.getResourceAsStream(\"versions.properties\")) {\n            Files.copy(in, versionsFile);\n        }\n\n        Map<String, Object> cfg = new HashMap<>();\n        cfg.put(Constants.Request.DEPENDENCIES_KEY, Arrays.asList(\n                \"file://something.jar\",\n                \"mvn://org.codehaus.groovy:groovy-all:pom:2.5.8\",\n                \"mvn://aaa:aaa:1.0\",\n                \"mvn://bbb:bbb:latest\",\n                \"mvn://ccc:ccc:1.0.1-20190214.203609-21\",\n                \"mvn://ddd:ddd:latest\"\n        ));\n\n        RunnerJob j = mock(RunnerJob.class);\n        when(j.getProcessCfg()).thenReturn(cfg);\n        when(j.getPayloadDir()).thenReturn(payloadDir);\n\n        RunnerLog log = mock(RunnerLog.class);\n        when(j.getLog()).thenReturn(log);\n\n        Collection<URI> uris = JobDependencies.get(j);\n        assertEquals(6, uris.size());\n\n        assertContains(\"mvn://aaa:aaa:1.0\", uris);\n        assertContains(\"mvn://bbb:bbb:1.0\", uris);\n        assertContains(\"mvn://ccc:ccc:1.0.1-20190214.203609-21\", uris);\n        assertContains(\"mvn://ddd:ddd:latest\", uris);\n\n        verify(log, times(1)).warn(anyString(), any());\n    }\n\n    private static void assertContains(String s, Collection<URI> uris) {\n        for (URI u : uris) {\n            if (u.toString().equals(s)) {\n                return;\n            }\n        }\n\n        fail(\"Expected to find \" + s);\n    }\n}\n"
  },
  {
    "path": "agent/src/test/java/com/walmartlabs/concord/agent/executors/runner/SegmentHeaderParserTest.java",
    "content": "package com.walmartlabs.concord.agent.executors.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.primitives.Bytes;\nimport com.walmartlabs.concord.agent.logging.SegmentHeaderParser;\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentHeader;\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentSerializer;\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentStatus;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.charset.StandardCharsets;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport static com.walmartlabs.concord.agent.logging.SegmentHeaderParser.Position;\nimport static com.walmartlabs.concord.agent.logging.SegmentHeaderParser.Segment;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class SegmentHeaderParserTest {\n\n    /**\n     * in: |5|2|1|1|2|hello\n     */\n    @Test\n    public void test1() {\n        String log = \"hello\";\n        byte[] ab = bb(1, log);\n\n        List<Segment> segments = new ArrayList<>();\n        List<Position> invalidSegments = new ArrayList<>();\n        int result = SegmentHeaderParser.parse(ab, segments, invalidSegments);\n        assertEquals(ab.length, result);\n        assertEquals(1, segments.size());\n        assertEquals(log, msg(ab, segments.get(0)));\n        assertEquals(0, invalidSegments.size());\n    }\n\n    /**\n     * in: |7|1|1|1|2|hello-1|8|2|1|1|2|hello-21\n     */\n    @Test\n    public void test1_1() {\n        byte[] ab = Bytes.concat(bb(1, \"hello-1\"), bb(2, \"hello-21\"));\n\n        List<Segment> segments = new ArrayList<>();\n        List<Position> invalidSegments = new ArrayList<>();\n        int result = SegmentHeaderParser.parse(ab, segments, invalidSegments);\n        assertEquals(ab.length, result);\n        assertEquals(2, segments.size());\n        assertEquals(\"hello-1\", msg(ab, segments.get(0)));\n        assertEquals(\"hello-21\", msg(ab, segments.get(1)));\n        assertEquals(0, invalidSegments.size());\n    }\n\n    /**\n     * in: hello\n     */\n    @Test\n    public void test2() {\n        String log = \"hello\";\n        byte[] ab = log.getBytes();\n\n        List<Segment> segments = new ArrayList<>();\n        List<SegmentHeaderParser.Position> invalidSegments = new ArrayList<>();\n        int result = SegmentHeaderParser.parse(ab, segments, invalidSegments);\n        assertEquals(ab.length, result);\n        assertEquals(0, segments.size());\n        assertEquals(1, invalidSegments.size());\n        Position i = invalidSegments.get(0);\n        assertEquals(log, new String(Arrays.copyOfRange(ab, i.start(), i.end())));\n    }\n\n    /**\n     * in: 123|5|2|1|1|2|hello\n     */\n    @Test\n    public void test3() {\n        String log = \"hello\";\n        byte[] ab = {'1', '2', '3'};\n        ab = Bytes.concat(ab, bb(2, log));\n\n        List<Segment> segments = new ArrayList<>();\n        List<Position> invalidSegments = new ArrayList<>();\n        int result = SegmentHeaderParser.parse(ab, segments, invalidSegments);\n\n        assertEquals(ab.length, result);\n        assertEquals(1, segments.size());\n        assertEquals(\"hello\", msg(ab, segments.get(0)));\n\n        assertEquals(1, invalidSegments.size());\n        Position i = invalidSegments.get(0);\n        assertEquals(\"123\", new String(Arrays.copyOfRange(ab, i.start(), i.end())));\n    }\n\n    /**\n     * in: |5|2|1|1|2|hello123\n     */\n    @Test\n    public void test4() {\n        String log = \"hello\";\n        byte[] ab = {'1', '2', '3'};\n        ab = Bytes.concat(bb(2, log), ab);\n\n        List<Segment> segments = new ArrayList<>();\n        List<Position> invalidSegments = new ArrayList<>();\n        int result = SegmentHeaderParser.parse(ab, segments, invalidSegments);\n\n        assertEquals(ab.length, result);\n        assertEquals(1, segments.size());\n\n        assertEquals(1, invalidSegments.size());\n        Position i = invalidSegments.get(0);\n        assertEquals(\"123\", new String(Arrays.copyOfRange(ab, i.start(), i.end())));\n    }\n\n    /**\n     * in: |5|2|1\n     */\n    @Test\n    public void test5() {\n        byte[] ab = {'|', '5', '|', '2', '|', '1'};\n\n        List<Segment> segments = new ArrayList<>();\n        List<Position> invalidSegments = new ArrayList<>();\n        int result = SegmentHeaderParser.parse(ab, segments, invalidSegments);\n\n        assertEquals(0, result);\n        assertEquals(0, segments.size());\n        assertEquals(0, invalidSegments.size());\n    }\n\n    /**\n     * in: abc|5|2|1\n     */\n    @Test\n    public void test6() {\n        byte[] ab = {'a', 'b', 'c', '|', '5', '|', '2', '|', '1'};\n\n        List<Segment> segments = new ArrayList<>();\n        List<Position> invalidSegments = new ArrayList<>();\n        int result = SegmentHeaderParser.parse(ab, segments, invalidSegments);\n\n        assertEquals(3, result);\n        assertEquals(0, segments.size());\n        assertEquals(1, invalidSegments.size());\n        Position i = invalidSegments.get(0);\n        assertEquals(\"abc\", new String(Arrays.copyOfRange(ab, i.start(), i.end())));\n    }\n\n    /**\n     * in: |5|2|1|1|2|he\n     */\n    @Test\n    public void test7() {\n        String log = \"hello\";\n        byte[] full = bb(1, log);\n        byte[] ab = Arrays.copyOfRange(full, 0, full.length - 3);\n\n        List<Segment> segments = new ArrayList<>();\n        List<Position> invalidSegments = new ArrayList<>();\n        int result = SegmentHeaderParser.parse(ab, segments, invalidSegments);\n\n        assertEquals(ab.length, result);\n        assertEquals(1, segments.size());\n        assertEquals(\"he\", msg(ab, segments.get(0)));\n        assertEquals(0, invalidSegments.size());\n    }\n\n    /**\n     *\n     * |0|552|1|0|0|\n     */\n    @Test\n    public void testParseSegmentEndMarker() {\n        String log = \"|0|552|1|0|0|\";\n        byte[] ab = log.getBytes(StandardCharsets.UTF_8);\n\n        List<Segment> segments = new ArrayList<>();\n        List<Position> invalidSegments = new ArrayList<>();\n\n        int result = SegmentHeaderParser.parse(ab, segments, invalidSegments);\n\n        assertEquals(ab.length, result);\n        assertEquals(1, segments.size());\n        Segment s = segments.get(0);\n        assertEquals(0, s.header().length());\n        assertEquals(LogSegmentStatus.OK, s.header().status());\n    }\n\n    private static String msg(byte[] ab, Segment segment) {\n        int to = Math.min(ab.length, segment.msgStart() + segment.header().length());\n        return new String(Arrays.copyOfRange(ab, segment.msgStart(), to));\n    }\n\n    private static byte[] bb(int segmentId, String msg) {\n        byte[] ab = msg.getBytes();\n\n        byte[] header = LogSegmentSerializer.serializeHeader(LogSegmentHeader.builder()\n                .segmentId(segmentId)\n                .length(ab.length)\n                .warnCount(1)\n                .errorCount(2)\n                .status(LogSegmentStatus.RUNNING)\n                .build());\n\n        return Bytes.concat(header, ab);\n    }\n}\n"
  },
  {
    "path": "agent/src/test/java/com/walmartlabs/concord/agent/executors/runner/SegmentedLogsConsumerTest.java",
    "content": "package com.walmartlabs.concord.agent.executors.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.primitives.Bytes;\nimport com.walmartlabs.concord.agent.logging.LogAppender;\nimport com.walmartlabs.concord.agent.logging.LogSegmentStats;\nimport com.walmartlabs.concord.agent.logging.SegmentedLogsConsumer;\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentHeader;\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentSerializer;\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentStatus;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Arrays;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.agent.logging.RedirectedProcessLog.Chunk;\nimport static org.mockito.Mockito.*;\n\npublic class SegmentedLogsConsumerTest {\n\n    private LogAppender logAppender;\n    private SegmentedLogsConsumer consumer;\n\n    @BeforeEach\n    public void init() {\n        this.logAppender = mock(LogAppender.class);\n        consumer = new SegmentedLogsConsumer(UUID.randomUUID(), logAppender);\n    }\n\n    @Test\n    public void test1() {\n        String msg = \"hello\";\n        byte[] ab = bb(1, msg);\n        consumer.accept(toChunk(ab));\n\n        verify(logAppender, times(1)).appendLog(any(), eq(1L), eq(msg.getBytes()));\n        verify(logAppender, times(1)).updateSegment(any(), eq(1L), eq(new LogSegmentStats(null, 2, 1)));\n    }\n\n    /**\n     * in: |7|1|1|1|2|hello1\\n|8|1|1|1|2|hello223\n     */\n    @Test\n    public void test2() {\n        String msg1 = \"hello1\\n\";\n        byte[] s1 = bb(1, msg1);\n\n        String msg2 = \"hello223\";\n        byte[] s2 = bb(1, msg2);\n\n        consumer.accept(toChunk(Bytes.concat(s1, s2)));\n\n        verify(logAppender, times(1)).appendLog(any(), eq(1L), eq(Bytes.concat(msg1.getBytes(), msg2.getBytes())));\n        verify(logAppender, times(1)).updateSegment(any(), eq(1L), eq(new LogSegmentStats(null, 2, 1)));\n    }\n\n    /**\n     * in: |7|1|1|1|2|hello1\\n|8|2|1|1|2|hello223\n     */\n    @Test\n    public void test3() {\n        String msg1 = \"hello1\\n\";\n        byte[] s1 = bb(1, msg1);\n\n        String msg2 = \"hello223\";\n        byte[] s2 = bb(2, msg2);\n\n        consumer.accept(toChunk(Bytes.concat(s1, s2)));\n\n        verify(logAppender, times(1)).appendLog(any(), eq(1L), eq(msg1.getBytes()));\n        verify(logAppender, times(1)).appendLog(any(), eq(2L), eq(msg2.getBytes()));\n        verify(logAppender, times(1)).updateSegment(any(), eq(1L), eq(new LogSegmentStats(null, 2, 1)));\n    }\n\n    /**\n     * in: |7|1|1|1|2|hello1\\n|8|2|1|1|2|hello223\n     */\n    @Test\n    public void test4() {\n        String msg = \"hello\";\n        byte[] s = bb(1, msg);\n\n        consumer.accept(toChunk(Arrays.copyOfRange(s, 0, s.length - 3)));\n\n        byte[] p1 = {'h', 'e'};\n        verify(logAppender, times(1)).appendLog(any(), eq(1L), eq(p1));\n\n        byte[] p2 = {'l', 'l', 'o'};\n        consumer.accept(toChunk(p2));\n        verify(logAppender, times(1)).appendLog(any(), eq(1L), eq(p2));\n        verify(logAppender, times(2)).updateSegment(any(), eq(1L), eq(new LogSegmentStats(null, 2, 1)));\n    }\n\n    /**\n     * in: |5|1|1|0|0|hello\n     */\n    @Test\n    public void test5() {\n        String msg = \"hello\";\n        byte[] ab = bb(1, msg, 0, 0);\n\n        consumer.accept(toChunk(ab));\n\n        verify(logAppender, times(1)).appendLog(any(), eq(1L), eq(msg.getBytes()));\n        verifyNoMoreInteractions(logAppender);\n    }\n\n    /**\n     * in: |5|1|1|0|0|hellotrash|3|2|1|0|0|bye\n     */\n    @Test\n    public void test6() {\n        String msg1 = \"hello\";\n        String msg2 = \"bye\";\n        byte[] s1 = bb(1, msg1, 0, 0);\n        byte[] s2 = bb(2, msg2, 0, 0);\n        byte[] ab = Bytes.concat(s1, \"trash\".getBytes(), s2);\n\n        System.out.println(\">>>\" + new String(ab));\n\n        consumer.accept(toChunk(ab));\n\n        verify(logAppender, times(1)).appendLog(any(), eq(1L), eq(msg1.getBytes()));\n        verify(logAppender, times(1)).appendLog(any(), eq(0L), eq(\"trash\".getBytes()));\n        verify(logAppender, times(1)).appendLog(any(), eq(2L), eq(msg2.getBytes()));\n        verifyNoMoreInteractions(logAppender);\n    }\n\n    private static Chunk toChunk(byte[] ab) {\n        return new Chunk(ab, ab.length) {\n        };\n    }\n\n    private static byte[] bb(int segmentId, String msg) {\n        return bb(segmentId, msg, 2, 1);\n    }\n\n    private static byte[] bb(int segmentId, String msg, int errorCount, int warnCount) {\n        byte[] ab = msg.getBytes();\n\n        byte[] header = LogSegmentSerializer.serializeHeader(LogSegmentHeader.builder()\n                .segmentId(segmentId)\n                .length(ab.length)\n                .warnCount(warnCount)\n                .errorCount(errorCount)\n                .status(LogSegmentStatus.RUNNING)\n                .build());\n\n        return Bytes.concat(header, ab);\n    }\n}\n"
  },
  {
    "path": "agent/src/test/resources/com/walmartlabs/concord/agent/executors/runner/versions.properties",
    "content": "aaa\\:aaa=2.0\nbbb\\:bbb=1.0\n"
  },
  {
    "path": "agent-operator/README.md",
    "content": "# Kubernetes Operator for Concord Agents\n\nTakes care of deploying and scaling [Concord](https://concord.walmartlabs.com) \nAgents based on the current Process Queue usage. \n\n## Prerequisites\n\nBuild the parent Concord repo to install the latest artifacts and Docker\nimages:\n```\n$ cd concord/\n$ ./mvnw clean install -DskipTests\n```\n\n## Running in Minikube\n\nBelow are the steps to deploy the concord agent operator to the `default`\nnamespace in any local/dev k8s cluster (in this case minikube).\n\nBefore deploying the operator's resources, please ensure the \nConcord Server URL and API token fields in the specs files are correctly\npointing  to a running instance on your dev or local machine. \nMake sure the API token used in the `operator.yml` is valid and working.\n\n1. Start the cluster:\n  ```\n  $ minikube start\n  $ minikube \n  ```\n2. Build the operator's image:\n  ```\n  $ eval $(minikube docker-env)\n  $ cd concord/agent-operator\n  $ docker build . -t walmartlabs/concord-agent-operator:latest\n  ```\n3. Build the app's images (might take a while, depending on cached layers\npresent in your minikube instance):\n  ```\n  $ eval $(minikube docker-env)\n  $ cd concord\n  $ ./mvnw -f docker-images clean install -Pdocker\n  ```\n4. Deploy the necessary resources:\n  ```\n  $ minikube kubectl -- create -f deploy/cluster_role.yml -n default\n  $ minikube kubectl -- create -f deploy/service_account.yml -n default\n  $ minikube kubectl -- create -f deploy/cluster_role_binding.yml -n default\n  ```\n5. Create the custom resource:\n  ```\n  $ minikube kubectl -- create -f deploy/crds/crd.yml -n default\n  $ minikube kubectl -- create -f deploy/crds/example.agentpool.yml -n default\n  ```\n6. Start the operator:\n  ```\n  $ minikube kubectl -- create -f deploy/operator.yml -n default\n  ```\n\nIf everything is correct you should see this line in the operator's pod log:\n```\n[INFO ] c.w.concord.agentoperator.Operator - main -> my watch begins... (namespace=default)\n[INFO ] c.w.c.a.p.CreateConfigMapChange - apply -> created a configmap example-agentpool-cfg\n[INFO ] c.w.c.a.planner.CreatePodChange - apply -> created a pod example-agentpool/example-agentpool-00000\n```\nThere should be no other errors or warnings.\n\n## Running in an IDE\n\nRepeat all steps from the [Running in Minikube](#running-in-minikube) section\nexcept for \"Start the operator\".\n\nStart the operator directly in your IDE by using `com.walmartlabs.concord.agentoperator.Operator`\nas the main class. Specify `CONCORD_BASE_URL` and `CONCORD_API_TOKEN` if you\nwish to test the autoscaling feature.\n\n#### How to Verify\n\n1. Check the pods running in the `default` namespace:\n```\nminikube kubectl -- get po -n default\n```\nThe output should show two pods - \nthe concord agent operator pod (with 1 container) and the agentpool pod (with 2 containers).\n\n2. Verify the logs of both these pods using:\n```\nminikube kubectl -- logs -f <pod_name> -c <container_name> -n default\n```\n\n## Running in Production\n\n1. Deploy the service account, the cluster role and the cluster role binding\n(modify the commands according to your namespace):\n  ```\n  $ minikube kubectl -- create -f deploy/cluster_role.yml\n  $ minikube kubectl -- create -f deploy/service_account.yml\n  $ minikube kubectl -- create -f deploy/cluster_role_binding.yml\n  ```\n2. Update the `CONCORD_BASE_URL` and `CONCORD_API_TOKEN` in `deploy/operator.yml`.\n   Verify the image name and tag (version);\n3. Deploy the operator:\n  ```\n  $ minikube kubectl -- create -f deploy/operator.yml\n  ```\n4. Check the operator's pod logs;\n5. Deploy one or more CRs using `deploy/crds/example.agentpool.yml` as a template.\n\n## How To Release New Versions\n\n- build the image;\n- push the image to Docker Hub:\n  ```\n  $ docker push walmartlabs/concord-agent-operator:latest\n  ```\n\n## TODO\n\n- automatically add the necessary labels to pods;\n- add validation rules for CRDs and CRs;\n- use secrets to store Concord API keys;\n- make the queue connection optional if the auto scaling is disabled.\n"
  },
  {
    "path": "agent-operator/deploy/cluster_role.yml",
    "content": "apiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n  name: concord-agent-operator\nrules:\n  - apiGroups:\n      - \"\"\n    resources:\n      - pods\n      - events\n      - configmaps\n      - pods/exec\n    verbs:\n      - '*'\n\n  - apiGroups:\n      - apiextensions.k8s.io\n    resources:\n      - customresourcedefinitions\n    verbs:\n      - '*'\n\n  - apiGroups:\n      - \"\"\n    resources:\n      - namespaces\n    verbs:\n      - get\n\n  - apiGroups:\n      - concord.walmartlabs.com\n    resources:\n      - '*'\n    verbs:\n      - '*'\n"
  },
  {
    "path": "agent-operator/deploy/cluster_role_binding.yml",
    "content": "kind: ClusterRoleBinding\napiVersion: rbac.authorization.k8s.io/v1\nmetadata:\n  name: concord-agent-operator\nsubjects:\n  - kind: ServiceAccount\n    name: concord-agent-operator\n    namespace: 'default'\nroleRef:\n  kind: ClusterRole\n  name: concord-agent-operator\n  apiGroup: rbac.authorization.k8s.io\n"
  },
  {
    "path": "agent-operator/deploy/crds/agentpools.concord.walmartlabs.com-v1.yml",
    "content": "\n# Generated by Fabric8 CRDGenerator, manual edits might get overwritten!\napiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceDefinition\nmetadata:\n  name: agentpools.concord.walmartlabs.com\nspec:\n  group: concord.walmartlabs.com\n  names:\n    kind: AgentPool\n    plural: agentpools\n    shortNames:\n    - aps\n    singular: agentpool\n  scope: Namespaced\n  versions:\n  - name: v1alpha1\n    schema:\n      openAPIV3Schema:\n        properties:\n          spec:\n            properties:\n              autoScale:\n                type: boolean\n              maxSize:\n                type: integer\n              minSize:\n                type: integer\n              size:\n                type: integer\n              queueQueryLimit:\n                type: integer\n              scaleUpDelayMs:\n                type: integer\n              scaleDownDelayMs:\n                type: integer\n              percentIncrement:\n                type: number\n              percentDecrement:\n                type: number\n              incrementThresholdFactor:\n                type: number\n              decrementThresholdFactor:\n                type: number\n              queueSelector:\n                additionalProperties:\n                  type: object\n                  x-kubernetes-preserve-unknown-fields: true\n                type: object\n              configMap:\n                properties:\n                  apiVersion:\n                    type: string\n                  binaryData:\n                    additionalProperties:\n                      type: string\n                    type: object\n                  data:\n                    additionalProperties:\n                      type: string\n                    type: object\n                  immutable:\n                    type: boolean\n                  kind:\n                    type: string\n                  metadata:\n                    properties:\n                      annotations:\n                        additionalProperties:\n                          type: string\n                        type: object\n                      clusterName:\n                        type: string\n                      creationTimestamp:\n                        type: string\n                      deletionGracePeriodSeconds:\n                        type: integer\n                      deletionTimestamp:\n                        type: string\n                      finalizers:\n                        items:\n                          type: string\n                        type: array\n                      generateName:\n                        type: string\n                      generation:\n                        type: integer\n                      labels:\n                        additionalProperties:\n                          type: string\n                        type: object\n                      managedFields:\n                        items:\n                          properties:\n                            apiVersion:\n                              type: string\n                            fieldsType:\n                              type: string\n                            fieldsV1:\n                              type: object\n                            manager:\n                              type: string\n                            operation:\n                              type: string\n                            subresource:\n                              type: string\n                            time:\n                              type: string\n                          type: object\n                        type: array\n                      name:\n                        type: string\n                      namespace:\n                        type: string\n                      ownerReferences:\n                        items:\n                          properties:\n                            apiVersion:\n                              type: string\n                            blockOwnerDeletion:\n                              type: boolean\n                            controller:\n                              type: boolean\n                            kind:\n                              type: string\n                            name:\n                              type: string\n                            uid:\n                              type: string\n                          type: object\n                        type: array\n                      resourceVersion:\n                        type: string\n                      selfLink:\n                        type: string\n                      uid:\n                        type: string\n                    type: object\n                type: object\n              pod:\n                properties:\n                  apiVersion:\n                    type: string\n                  kind:\n                    type: string\n                  metadata:\n                    properties:\n                      annotations:\n                        additionalProperties:\n                          type: string\n                        type: object\n                      clusterName:\n                        type: string\n                      creationTimestamp:\n                        type: string\n                      deletionGracePeriodSeconds:\n                        type: integer\n                      deletionTimestamp:\n                        type: string\n                      finalizers:\n                        items:\n                          type: string\n                        type: array\n                      generateName:\n                        type: string\n                      generation:\n                        type: integer\n                      labels:\n                        additionalProperties:\n                          type: string\n                        type: object\n                      managedFields:\n                        items:\n                          properties:\n                            apiVersion:\n                              type: string\n                            fieldsType:\n                              type: string\n                            fieldsV1:\n                              type: object\n                            manager:\n                              type: string\n                            operation:\n                              type: string\n                            subresource:\n                              type: string\n                            time:\n                              type: string\n                          type: object\n                        type: array\n                      name:\n                        type: string\n                      namespace:\n                        type: string\n                      ownerReferences:\n                        items:\n                          properties:\n                            apiVersion:\n                              type: string\n                            blockOwnerDeletion:\n                              type: boolean\n                            controller:\n                              type: boolean\n                            kind:\n                              type: string\n                            name:\n                              type: string\n                            uid:\n                              type: string\n                          type: object\n                        type: array\n                      resourceVersion:\n                        type: string\n                      selfLink:\n                        type: string\n                      uid:\n                        type: string\n                    type: object\n                  spec:\n                    properties:\n                      activeDeadlineSeconds:\n                        type: integer\n                      affinity:\n                        properties:\n                          nodeAffinity:\n                            properties:\n                              preferredDuringSchedulingIgnoredDuringExecution:\n                                items:\n                                  properties:\n                                    preference:\n                                      properties:\n                                        matchExpressions:\n                                          items:\n                                            properties:\n                                              key:\n                                                type: string\n                                              operator:\n                                                type: string\n                                              values:\n                                                items:\n                                                  type: string\n                                                type: array\n                                            type: object\n                                          type: array\n                                        matchFields:\n                                          items:\n                                            properties:\n                                              key:\n                                                type: string\n                                              operator:\n                                                type: string\n                                              values:\n                                                items:\n                                                  type: string\n                                                type: array\n                                            type: object\n                                          type: array\n                                      type: object\n                                    weight:\n                                      type: integer\n                                  type: object\n                                type: array\n                              requiredDuringSchedulingIgnoredDuringExecution:\n                                properties:\n                                  nodeSelectorTerms:\n                                    items:\n                                      properties:\n                                        matchExpressions:\n                                          items:\n                                            properties:\n                                              key:\n                                                type: string\n                                              operator:\n                                                type: string\n                                              values:\n                                                items:\n                                                  type: string\n                                                type: array\n                                            type: object\n                                          type: array\n                                        matchFields:\n                                          items:\n                                            properties:\n                                              key:\n                                                type: string\n                                              operator:\n                                                type: string\n                                              values:\n                                                items:\n                                                  type: string\n                                                type: array\n                                            type: object\n                                          type: array\n                                      type: object\n                                    type: array\n                                type: object\n                            type: object\n                          podAffinity:\n                            properties:\n                              preferredDuringSchedulingIgnoredDuringExecution:\n                                items:\n                                  properties:\n                                    podAffinityTerm:\n                                      properties:\n                                        labelSelector:\n                                          properties:\n                                            matchExpressions:\n                                              items:\n                                                properties:\n                                                  key:\n                                                    type: string\n                                                  operator:\n                                                    type: string\n                                                  values:\n                                                    items:\n                                                      type: string\n                                                    type: array\n                                                type: object\n                                              type: array\n                                            matchLabels:\n                                              additionalProperties:\n                                                type: string\n                                              type: object\n                                          type: object\n                                        namespaceSelector:\n                                          properties:\n                                            matchExpressions:\n                                              items:\n                                                properties:\n                                                  key:\n                                                    type: string\n                                                  operator:\n                                                    type: string\n                                                  values:\n                                                    items:\n                                                      type: string\n                                                    type: array\n                                                type: object\n                                              type: array\n                                            matchLabels:\n                                              additionalProperties:\n                                                type: string\n                                              type: object\n                                          type: object\n                                        namespaces:\n                                          items:\n                                            type: string\n                                          type: array\n                                        topologyKey:\n                                          type: string\n                                      type: object\n                                    weight:\n                                      type: integer\n                                  type: object\n                                type: array\n                              requiredDuringSchedulingIgnoredDuringExecution:\n                                items:\n                                  properties:\n                                    labelSelector:\n                                      properties:\n                                        matchExpressions:\n                                          items:\n                                            properties:\n                                              key:\n                                                type: string\n                                              operator:\n                                                type: string\n                                              values:\n                                                items:\n                                                  type: string\n                                                type: array\n                                            type: object\n                                          type: array\n                                        matchLabels:\n                                          additionalProperties:\n                                            type: string\n                                          type: object\n                                      type: object\n                                    namespaceSelector:\n                                      properties:\n                                        matchExpressions:\n                                          items:\n                                            properties:\n                                              key:\n                                                type: string\n                                              operator:\n                                                type: string\n                                              values:\n                                                items:\n                                                  type: string\n                                                type: array\n                                            type: object\n                                          type: array\n                                        matchLabels:\n                                          additionalProperties:\n                                            type: string\n                                          type: object\n                                      type: object\n                                    namespaces:\n                                      items:\n                                        type: string\n                                      type: array\n                                    topologyKey:\n                                      type: string\n                                  type: object\n                                type: array\n                            type: object\n                          podAntiAffinity:\n                            properties:\n                              preferredDuringSchedulingIgnoredDuringExecution:\n                                items:\n                                  properties:\n                                    podAffinityTerm:\n                                      properties:\n                                        labelSelector:\n                                          properties:\n                                            matchExpressions:\n                                              items:\n                                                properties:\n                                                  key:\n                                                    type: string\n                                                  operator:\n                                                    type: string\n                                                  values:\n                                                    items:\n                                                      type: string\n                                                    type: array\n                                                type: object\n                                              type: array\n                                            matchLabels:\n                                              additionalProperties:\n                                                type: string\n                                              type: object\n                                          type: object\n                                        namespaceSelector:\n                                          properties:\n                                            matchExpressions:\n                                              items:\n                                                properties:\n                                                  key:\n                                                    type: string\n                                                  operator:\n                                                    type: string\n                                                  values:\n                                                    items:\n                                                      type: string\n                                                    type: array\n                                                type: object\n                                              type: array\n                                            matchLabels:\n                                              additionalProperties:\n                                                type: string\n                                              type: object\n                                          type: object\n                                        namespaces:\n                                          items:\n                                            type: string\n                                          type: array\n                                        topologyKey:\n                                          type: string\n                                      type: object\n                                    weight:\n                                      type: integer\n                                  type: object\n                                type: array\n                              requiredDuringSchedulingIgnoredDuringExecution:\n                                items:\n                                  properties:\n                                    labelSelector:\n                                      properties:\n                                        matchExpressions:\n                                          items:\n                                            properties:\n                                              key:\n                                                type: string\n                                              operator:\n                                                type: string\n                                              values:\n                                                items:\n                                                  type: string\n                                                type: array\n                                            type: object\n                                          type: array\n                                        matchLabels:\n                                          additionalProperties:\n                                            type: string\n                                          type: object\n                                      type: object\n                                    namespaceSelector:\n                                      properties:\n                                        matchExpressions:\n                                          items:\n                                            properties:\n                                              key:\n                                                type: string\n                                              operator:\n                                                type: string\n                                              values:\n                                                items:\n                                                  type: string\n                                                type: array\n                                            type: object\n                                          type: array\n                                        matchLabels:\n                                          additionalProperties:\n                                            type: string\n                                          type: object\n                                      type: object\n                                    namespaces:\n                                      items:\n                                        type: string\n                                      type: array\n                                    topologyKey:\n                                      type: string\n                                  type: object\n                                type: array\n                            type: object\n                        type: object\n                      automountServiceAccountToken:\n                        type: boolean\n                      containers:\n                        items:\n                          properties:\n                            args:\n                              items:\n                                type: string\n                              type: array\n                            command:\n                              items:\n                                type: string\n                              type: array\n                            env:\n                              items:\n                                properties:\n                                  name:\n                                    type: string\n                                  value:\n                                    type: string\n                                  valueFrom:\n                                    properties:\n                                      configMapKeyRef:\n                                        properties:\n                                          key:\n                                            type: string\n                                          name:\n                                            type: string\n                                          optional:\n                                            type: boolean\n                                        type: object\n                                      fieldRef:\n                                        properties:\n                                          apiVersion:\n                                            type: string\n                                          fieldPath:\n                                            type: string\n                                        type: object\n                                      resourceFieldRef:\n                                        properties:\n                                          containerName:\n                                            type: string\n                                          divisor:\n                                            anyOf:\n                                            - type: integer\n                                            - type: string\n                                            x-kubernetes-int-or-string: true\n                                          resource:\n                                            type: string\n                                        type: object\n                                      secretKeyRef:\n                                        properties:\n                                          key:\n                                            type: string\n                                          name:\n                                            type: string\n                                          optional:\n                                            type: boolean\n                                        type: object\n                                    type: object\n                                type: object\n                              type: array\n                            envFrom:\n                              items:\n                                properties:\n                                  configMapRef:\n                                    properties:\n                                      name:\n                                        type: string\n                                      optional:\n                                        type: boolean\n                                    type: object\n                                  prefix:\n                                    type: string\n                                  secretRef:\n                                    properties:\n                                      name:\n                                        type: string\n                                      optional:\n                                        type: boolean\n                                    type: object\n                                type: object\n                              type: array\n                            image:\n                              type: string\n                            imagePullPolicy:\n                              type: string\n                            lifecycle:\n                              properties:\n                                postStart:\n                                  properties:\n                                    exec:\n                                      properties:\n                                        command:\n                                          items:\n                                            type: string\n                                          type: array\n                                      type: object\n                                    httpGet:\n                                      properties:\n                                        host:\n                                          type: string\n                                        httpHeaders:\n                                          items:\n                                            properties:\n                                              name:\n                                                type: string\n                                              value:\n                                                type: string\n                                            type: object\n                                          type: array\n                                        path:\n                                          type: string\n                                        port:\n                                          anyOf:\n                                          - type: integer\n                                          - type: string\n                                          x-kubernetes-int-or-string: true\n                                        scheme:\n                                          type: string\n                                      type: object\n                                    tcpSocket:\n                                      properties:\n                                        host:\n                                          type: string\n                                        port:\n                                          anyOf:\n                                          - type: integer\n                                          - type: string\n                                          x-kubernetes-int-or-string: true\n                                      type: object\n                                  type: object\n                                preStop:\n                                  properties:\n                                    exec:\n                                      properties:\n                                        command:\n                                          items:\n                                            type: string\n                                          type: array\n                                      type: object\n                                    httpGet:\n                                      properties:\n                                        host:\n                                          type: string\n                                        httpHeaders:\n                                          items:\n                                            properties:\n                                              name:\n                                                type: string\n                                              value:\n                                                type: string\n                                            type: object\n                                          type: array\n                                        path:\n                                          type: string\n                                        port:\n                                          anyOf:\n                                          - type: integer\n                                          - type: string\n                                          x-kubernetes-int-or-string: true\n                                        scheme:\n                                          type: string\n                                      type: object\n                                    tcpSocket:\n                                      properties:\n                                        host:\n                                          type: string\n                                        port:\n                                          anyOf:\n                                          - type: integer\n                                          - type: string\n                                          x-kubernetes-int-or-string: true\n                                      type: object\n                                  type: object\n                              type: object\n                            livenessProbe:\n                              properties:\n                                exec:\n                                  properties:\n                                    command:\n                                      items:\n                                        type: string\n                                      type: array\n                                  type: object\n                                failureThreshold:\n                                  type: integer\n                                grpc:\n                                  properties:\n                                    port:\n                                      type: integer\n                                    service:\n                                      type: string\n                                  type: object\n                                httpGet:\n                                  properties:\n                                    host:\n                                      type: string\n                                    httpHeaders:\n                                      items:\n                                        properties:\n                                          name:\n                                            type: string\n                                          value:\n                                            type: string\n                                        type: object\n                                      type: array\n                                    path:\n                                      type: string\n                                    port:\n                                      anyOf:\n                                      - type: integer\n                                      - type: string\n                                      x-kubernetes-int-or-string: true\n                                    scheme:\n                                      type: string\n                                  type: object\n                                initialDelaySeconds:\n                                  type: integer\n                                periodSeconds:\n                                  type: integer\n                                successThreshold:\n                                  type: integer\n                                tcpSocket:\n                                  properties:\n                                    host:\n                                      type: string\n                                    port:\n                                      anyOf:\n                                      - type: integer\n                                      - type: string\n                                      x-kubernetes-int-or-string: true\n                                  type: object\n                                terminationGracePeriodSeconds:\n                                  type: integer\n                                timeoutSeconds:\n                                  type: integer\n                              type: object\n                            name:\n                              type: string\n                            ports:\n                              items:\n                                properties:\n                                  containerPort:\n                                    type: integer\n                                  hostIP:\n                                    type: string\n                                  hostPort:\n                                    type: integer\n                                  name:\n                                    type: string\n                                  protocol:\n                                    type: string\n                                type: object\n                              type: array\n                            readinessProbe:\n                              properties:\n                                exec:\n                                  properties:\n                                    command:\n                                      items:\n                                        type: string\n                                      type: array\n                                  type: object\n                                failureThreshold:\n                                  type: integer\n                                grpc:\n                                  properties:\n                                    port:\n                                      type: integer\n                                    service:\n                                      type: string\n                                  type: object\n                                httpGet:\n                                  properties:\n                                    host:\n                                      type: string\n                                    httpHeaders:\n                                      items:\n                                        properties:\n                                          name:\n                                            type: string\n                                          value:\n                                            type: string\n                                        type: object\n                                      type: array\n                                    path:\n                                      type: string\n                                    port:\n                                      anyOf:\n                                      - type: integer\n                                      - type: string\n                                      x-kubernetes-int-or-string: true\n                                    scheme:\n                                      type: string\n                                  type: object\n                                initialDelaySeconds:\n                                  type: integer\n                                periodSeconds:\n                                  type: integer\n                                successThreshold:\n                                  type: integer\n                                tcpSocket:\n                                  properties:\n                                    host:\n                                      type: string\n                                    port:\n                                      anyOf:\n                                      - type: integer\n                                      - type: string\n                                      x-kubernetes-int-or-string: true\n                                  type: object\n                                terminationGracePeriodSeconds:\n                                  type: integer\n                                timeoutSeconds:\n                                  type: integer\n                              type: object\n                            resources:\n                              properties:\n                                limits:\n                                  additionalProperties:\n                                    anyOf:\n                                    - type: integer\n                                    - type: string\n                                    x-kubernetes-int-or-string: true\n                                  type: object\n                                requests:\n                                  additionalProperties:\n                                    anyOf:\n                                    - type: integer\n                                    - type: string\n                                    x-kubernetes-int-or-string: true\n                                  type: object\n                              type: object\n                            securityContext:\n                              properties:\n                                allowPrivilegeEscalation:\n                                  type: boolean\n                                capabilities:\n                                  properties:\n                                    add:\n                                      items:\n                                        type: string\n                                      type: array\n                                    drop:\n                                      items:\n                                        type: string\n                                      type: array\n                                  type: object\n                                privileged:\n                                  type: boolean\n                                procMount:\n                                  type: string\n                                readOnlyRootFilesystem:\n                                  type: boolean\n                                runAsGroup:\n                                  type: integer\n                                runAsNonRoot:\n                                  type: boolean\n                                runAsUser:\n                                  type: integer\n                                seLinuxOptions:\n                                  properties:\n                                    level:\n                                      type: string\n                                    role:\n                                      type: string\n                                    type:\n                                      type: string\n                                    user:\n                                      type: string\n                                  type: object\n                                seccompProfile:\n                                  properties:\n                                    localhostProfile:\n                                      type: string\n                                    type:\n                                      type: string\n                                  type: object\n                                windowsOptions:\n                                  properties:\n                                    gmsaCredentialSpec:\n                                      type: string\n                                    gmsaCredentialSpecName:\n                                      type: string\n                                    hostProcess:\n                                      type: boolean\n                                    runAsUserName:\n                                      type: string\n                                  type: object\n                              type: object\n                            startupProbe:\n                              properties:\n                                exec:\n                                  properties:\n                                    command:\n                                      items:\n                                        type: string\n                                      type: array\n                                  type: object\n                                failureThreshold:\n                                  type: integer\n                                grpc:\n                                  properties:\n                                    port:\n                                      type: integer\n                                    service:\n                                      type: string\n                                  type: object\n                                httpGet:\n                                  properties:\n                                    host:\n                                      type: string\n                                    httpHeaders:\n                                      items:\n                                        properties:\n                                          name:\n                                            type: string\n                                          value:\n                                            type: string\n                                        type: object\n                                      type: array\n                                    path:\n                                      type: string\n                                    port:\n                                      anyOf:\n                                      - type: integer\n                                      - type: string\n                                      x-kubernetes-int-or-string: true\n                                    scheme:\n                                      type: string\n                                  type: object\n                                initialDelaySeconds:\n                                  type: integer\n                                periodSeconds:\n                                  type: integer\n                                successThreshold:\n                                  type: integer\n                                tcpSocket:\n                                  properties:\n                                    host:\n                                      type: string\n                                    port:\n                                      anyOf:\n                                      - type: integer\n                                      - type: string\n                                      x-kubernetes-int-or-string: true\n                                  type: object\n                                terminationGracePeriodSeconds:\n                                  type: integer\n                                timeoutSeconds:\n                                  type: integer\n                              type: object\n                            stdin:\n                              type: boolean\n                            stdinOnce:\n                              type: boolean\n                            terminationMessagePath:\n                              type: string\n                            terminationMessagePolicy:\n                              type: string\n                            tty:\n                              type: boolean\n                            volumeDevices:\n                              items:\n                                properties:\n                                  devicePath:\n                                    type: string\n                                  name:\n                                    type: string\n                                type: object\n                              type: array\n                            volumeMounts:\n                              items:\n                                properties:\n                                  mountPath:\n                                    type: string\n                                  mountPropagation:\n                                    type: string\n                                  name:\n                                    type: string\n                                  readOnly:\n                                    type: boolean\n                                  subPath:\n                                    type: string\n                                  subPathExpr:\n                                    type: string\n                                type: object\n                              type: array\n                            workingDir:\n                              type: string\n                          type: object\n                        type: array\n                      dnsConfig:\n                        properties:\n                          nameservers:\n                            items:\n                              type: string\n                            type: array\n                          options:\n                            items:\n                              properties:\n                                name:\n                                  type: string\n                                value:\n                                  type: string\n                              type: object\n                            type: array\n                          searches:\n                            items:\n                              type: string\n                            type: array\n                        type: object\n                      dnsPolicy:\n                        type: string\n                      enableServiceLinks:\n                        type: boolean\n                      ephemeralContainers:\n                        items:\n                          properties:\n                            args:\n                              items:\n                                type: string\n                              type: array\n                            command:\n                              items:\n                                type: string\n                              type: array\n                            env:\n                              items:\n                                properties:\n                                  name:\n                                    type: string\n                                  value:\n                                    type: string\n                                  valueFrom:\n                                    properties:\n                                      configMapKeyRef:\n                                        properties:\n                                          key:\n                                            type: string\n                                          name:\n                                            type: string\n                                          optional:\n                                            type: boolean\n                                        type: object\n                                      fieldRef:\n                                        properties:\n                                          apiVersion:\n                                            type: string\n                                          fieldPath:\n                                            type: string\n                                        type: object\n                                      resourceFieldRef:\n                                        properties:\n                                          containerName:\n                                            type: string\n                                          divisor:\n                                            anyOf:\n                                            - type: integer\n                                            - type: string\n                                            x-kubernetes-int-or-string: true\n                                          resource:\n                                            type: string\n                                        type: object\n                                      secretKeyRef:\n                                        properties:\n                                          key:\n                                            type: string\n                                          name:\n                                            type: string\n                                          optional:\n                                            type: boolean\n                                        type: object\n                                    type: object\n                                type: object\n                              type: array\n                            envFrom:\n                              items:\n                                properties:\n                                  configMapRef:\n                                    properties:\n                                      name:\n                                        type: string\n                                      optional:\n                                        type: boolean\n                                    type: object\n                                  prefix:\n                                    type: string\n                                  secretRef:\n                                    properties:\n                                      name:\n                                        type: string\n                                      optional:\n                                        type: boolean\n                                    type: object\n                                type: object\n                              type: array\n                            image:\n                              type: string\n                            imagePullPolicy:\n                              type: string\n                            lifecycle:\n                              properties:\n                                postStart:\n                                  properties:\n                                    exec:\n                                      properties:\n                                        command:\n                                          items:\n                                            type: string\n                                          type: array\n                                      type: object\n                                    httpGet:\n                                      properties:\n                                        host:\n                                          type: string\n                                        httpHeaders:\n                                          items:\n                                            properties:\n                                              name:\n                                                type: string\n                                              value:\n                                                type: string\n                                            type: object\n                                          type: array\n                                        path:\n                                          type: string\n                                        port:\n                                          anyOf:\n                                          - type: integer\n                                          - type: string\n                                          x-kubernetes-int-or-string: true\n                                        scheme:\n                                          type: string\n                                      type: object\n                                    tcpSocket:\n                                      properties:\n                                        host:\n                                          type: string\n                                        port:\n                                          anyOf:\n                                          - type: integer\n                                          - type: string\n                                          x-kubernetes-int-or-string: true\n                                      type: object\n                                  type: object\n                                preStop:\n                                  properties:\n                                    exec:\n                                      properties:\n                                        command:\n                                          items:\n                                            type: string\n                                          type: array\n                                      type: object\n                                    httpGet:\n                                      properties:\n                                        host:\n                                          type: string\n                                        httpHeaders:\n                                          items:\n                                            properties:\n                                              name:\n                                                type: string\n                                              value:\n                                                type: string\n                                            type: object\n                                          type: array\n                                        path:\n                                          type: string\n                                        port:\n                                          anyOf:\n                                          - type: integer\n                                          - type: string\n                                          x-kubernetes-int-or-string: true\n                                        scheme:\n                                          type: string\n                                      type: object\n                                    tcpSocket:\n                                      properties:\n                                        host:\n                                          type: string\n                                        port:\n                                          anyOf:\n                                          - type: integer\n                                          - type: string\n                                          x-kubernetes-int-or-string: true\n                                      type: object\n                                  type: object\n                              type: object\n                            livenessProbe:\n                              properties:\n                                exec:\n                                  properties:\n                                    command:\n                                      items:\n                                        type: string\n                                      type: array\n                                  type: object\n                                failureThreshold:\n                                  type: integer\n                                grpc:\n                                  properties:\n                                    port:\n                                      type: integer\n                                    service:\n                                      type: string\n                                  type: object\n                                httpGet:\n                                  properties:\n                                    host:\n                                      type: string\n                                    httpHeaders:\n                                      items:\n                                        properties:\n                                          name:\n                                            type: string\n                                          value:\n                                            type: string\n                                        type: object\n                                      type: array\n                                    path:\n                                      type: string\n                                    port:\n                                      anyOf:\n                                      - type: integer\n                                      - type: string\n                                      x-kubernetes-int-or-string: true\n                                    scheme:\n                                      type: string\n                                  type: object\n                                initialDelaySeconds:\n                                  type: integer\n                                periodSeconds:\n                                  type: integer\n                                successThreshold:\n                                  type: integer\n                                tcpSocket:\n                                  properties:\n                                    host:\n                                      type: string\n                                    port:\n                                      anyOf:\n                                      - type: integer\n                                      - type: string\n                                      x-kubernetes-int-or-string: true\n                                  type: object\n                                terminationGracePeriodSeconds:\n                                  type: integer\n                                timeoutSeconds:\n                                  type: integer\n                              type: object\n                            name:\n                              type: string\n                            ports:\n                              items:\n                                properties:\n                                  containerPort:\n                                    type: integer\n                                  hostIP:\n                                    type: string\n                                  hostPort:\n                                    type: integer\n                                  name:\n                                    type: string\n                                  protocol:\n                                    type: string\n                                type: object\n                              type: array\n                            readinessProbe:\n                              properties:\n                                exec:\n                                  properties:\n                                    command:\n                                      items:\n                                        type: string\n                                      type: array\n                                  type: object\n                                failureThreshold:\n                                  type: integer\n                                grpc:\n                                  properties:\n                                    port:\n                                      type: integer\n                                    service:\n                                      type: string\n                                  type: object\n                                httpGet:\n                                  properties:\n                                    host:\n                                      type: string\n                                    httpHeaders:\n                                      items:\n                                        properties:\n                                          name:\n                                            type: string\n                                          value:\n                                            type: string\n                                        type: object\n                                      type: array\n                                    path:\n                                      type: string\n                                    port:\n                                      anyOf:\n                                      - type: integer\n                                      - type: string\n                                      x-kubernetes-int-or-string: true\n                                    scheme:\n                                      type: string\n                                  type: object\n                                initialDelaySeconds:\n                                  type: integer\n                                periodSeconds:\n                                  type: integer\n                                successThreshold:\n                                  type: integer\n                                tcpSocket:\n                                  properties:\n                                    host:\n                                      type: string\n                                    port:\n                                      anyOf:\n                                      - type: integer\n                                      - type: string\n                                      x-kubernetes-int-or-string: true\n                                  type: object\n                                terminationGracePeriodSeconds:\n                                  type: integer\n                                timeoutSeconds:\n                                  type: integer\n                              type: object\n                            resources:\n                              properties:\n                                limits:\n                                  additionalProperties:\n                                    anyOf:\n                                    - type: integer\n                                    - type: string\n                                    x-kubernetes-int-or-string: true\n                                  type: object\n                                requests:\n                                  additionalProperties:\n                                    anyOf:\n                                    - type: integer\n                                    - type: string\n                                    x-kubernetes-int-or-string: true\n                                  type: object\n                              type: object\n                            securityContext:\n                              properties:\n                                allowPrivilegeEscalation:\n                                  type: boolean\n                                capabilities:\n                                  properties:\n                                    add:\n                                      items:\n                                        type: string\n                                      type: array\n                                    drop:\n                                      items:\n                                        type: string\n                                      type: array\n                                  type: object\n                                privileged:\n                                  type: boolean\n                                procMount:\n                                  type: string\n                                readOnlyRootFilesystem:\n                                  type: boolean\n                                runAsGroup:\n                                  type: integer\n                                runAsNonRoot:\n                                  type: boolean\n                                runAsUser:\n                                  type: integer\n                                seLinuxOptions:\n                                  properties:\n                                    level:\n                                      type: string\n                                    role:\n                                      type: string\n                                    type:\n                                      type: string\n                                    user:\n                                      type: string\n                                  type: object\n                                seccompProfile:\n                                  properties:\n                                    localhostProfile:\n                                      type: string\n                                    type:\n                                      type: string\n                                  type: object\n                                windowsOptions:\n                                  properties:\n                                    gmsaCredentialSpec:\n                                      type: string\n                                    gmsaCredentialSpecName:\n                                      type: string\n                                    hostProcess:\n                                      type: boolean\n                                    runAsUserName:\n                                      type: string\n                                  type: object\n                              type: object\n                            startupProbe:\n                              properties:\n                                exec:\n                                  properties:\n                                    command:\n                                      items:\n                                        type: string\n                                      type: array\n                                  type: object\n                                failureThreshold:\n                                  type: integer\n                                grpc:\n                                  properties:\n                                    port:\n                                      type: integer\n                                    service:\n                                      type: string\n                                  type: object\n                                httpGet:\n                                  properties:\n                                    host:\n                                      type: string\n                                    httpHeaders:\n                                      items:\n                                        properties:\n                                          name:\n                                            type: string\n                                          value:\n                                            type: string\n                                        type: object\n                                      type: array\n                                    path:\n                                      type: string\n                                    port:\n                                      anyOf:\n                                      - type: integer\n                                      - type: string\n                                      x-kubernetes-int-or-string: true\n                                    scheme:\n                                      type: string\n                                  type: object\n                                initialDelaySeconds:\n                                  type: integer\n                                periodSeconds:\n                                  type: integer\n                                successThreshold:\n                                  type: integer\n                                tcpSocket:\n                                  properties:\n                                    host:\n                                      type: string\n                                    port:\n                                      anyOf:\n                                      - type: integer\n                                      - type: string\n                                      x-kubernetes-int-or-string: true\n                                  type: object\n                                terminationGracePeriodSeconds:\n                                  type: integer\n                                timeoutSeconds:\n                                  type: integer\n                              type: object\n                            stdin:\n                              type: boolean\n                            stdinOnce:\n                              type: boolean\n                            targetContainerName:\n                              type: string\n                            terminationMessagePath:\n                              type: string\n                            terminationMessagePolicy:\n                              type: string\n                            tty:\n                              type: boolean\n                            volumeDevices:\n                              items:\n                                properties:\n                                  devicePath:\n                                    type: string\n                                  name:\n                                    type: string\n                                type: object\n                              type: array\n                            volumeMounts:\n                              items:\n                                properties:\n                                  mountPath:\n                                    type: string\n                                  mountPropagation:\n                                    type: string\n                                  name:\n                                    type: string\n                                  readOnly:\n                                    type: boolean\n                                  subPath:\n                                    type: string\n                                  subPathExpr:\n                                    type: string\n                                type: object\n                              type: array\n                            workingDir:\n                              type: string\n                          type: object\n                        type: array\n                      hostAliases:\n                        items:\n                          properties:\n                            hostnames:\n                              items:\n                                type: string\n                              type: array\n                            ip:\n                              type: string\n                          type: object\n                        type: array\n                      hostIPC:\n                        type: boolean\n                      hostNetwork:\n                        type: boolean\n                      hostPID:\n                        type: boolean\n                      hostname:\n                        type: string\n                      imagePullSecrets:\n                        items:\n                          properties:\n                            name:\n                              type: string\n                          type: object\n                        type: array\n                      initContainers:\n                        items:\n                          properties:\n                            args:\n                              items:\n                                type: string\n                              type: array\n                            command:\n                              items:\n                                type: string\n                              type: array\n                            env:\n                              items:\n                                properties:\n                                  name:\n                                    type: string\n                                  value:\n                                    type: string\n                                  valueFrom:\n                                    properties:\n                                      configMapKeyRef:\n                                        properties:\n                                          key:\n                                            type: string\n                                          name:\n                                            type: string\n                                          optional:\n                                            type: boolean\n                                        type: object\n                                      fieldRef:\n                                        properties:\n                                          apiVersion:\n                                            type: string\n                                          fieldPath:\n                                            type: string\n                                        type: object\n                                      resourceFieldRef:\n                                        properties:\n                                          containerName:\n                                            type: string\n                                          divisor:\n                                            anyOf:\n                                            - type: integer\n                                            - type: string\n                                            x-kubernetes-int-or-string: true\n                                          resource:\n                                            type: string\n                                        type: object\n                                      secretKeyRef:\n                                        properties:\n                                          key:\n                                            type: string\n                                          name:\n                                            type: string\n                                          optional:\n                                            type: boolean\n                                        type: object\n                                    type: object\n                                type: object\n                              type: array\n                            envFrom:\n                              items:\n                                properties:\n                                  configMapRef:\n                                    properties:\n                                      name:\n                                        type: string\n                                      optional:\n                                        type: boolean\n                                    type: object\n                                  prefix:\n                                    type: string\n                                  secretRef:\n                                    properties:\n                                      name:\n                                        type: string\n                                      optional:\n                                        type: boolean\n                                    type: object\n                                type: object\n                              type: array\n                            image:\n                              type: string\n                            imagePullPolicy:\n                              type: string\n                            lifecycle:\n                              properties:\n                                postStart:\n                                  properties:\n                                    exec:\n                                      properties:\n                                        command:\n                                          items:\n                                            type: string\n                                          type: array\n                                      type: object\n                                    httpGet:\n                                      properties:\n                                        host:\n                                          type: string\n                                        httpHeaders:\n                                          items:\n                                            properties:\n                                              name:\n                                                type: string\n                                              value:\n                                                type: string\n                                            type: object\n                                          type: array\n                                        path:\n                                          type: string\n                                        port:\n                                          anyOf:\n                                          - type: integer\n                                          - type: string\n                                          x-kubernetes-int-or-string: true\n                                        scheme:\n                                          type: string\n                                      type: object\n                                    tcpSocket:\n                                      properties:\n                                        host:\n                                          type: string\n                                        port:\n                                          anyOf:\n                                          - type: integer\n                                          - type: string\n                                          x-kubernetes-int-or-string: true\n                                      type: object\n                                  type: object\n                                preStop:\n                                  properties:\n                                    exec:\n                                      properties:\n                                        command:\n                                          items:\n                                            type: string\n                                          type: array\n                                      type: object\n                                    httpGet:\n                                      properties:\n                                        host:\n                                          type: string\n                                        httpHeaders:\n                                          items:\n                                            properties:\n                                              name:\n                                                type: string\n                                              value:\n                                                type: string\n                                            type: object\n                                          type: array\n                                        path:\n                                          type: string\n                                        port:\n                                          anyOf:\n                                          - type: integer\n                                          - type: string\n                                          x-kubernetes-int-or-string: true\n                                        scheme:\n                                          type: string\n                                      type: object\n                                    tcpSocket:\n                                      properties:\n                                        host:\n                                          type: string\n                                        port:\n                                          anyOf:\n                                          - type: integer\n                                          - type: string\n                                          x-kubernetes-int-or-string: true\n                                      type: object\n                                  type: object\n                              type: object\n                            livenessProbe:\n                              properties:\n                                exec:\n                                  properties:\n                                    command:\n                                      items:\n                                        type: string\n                                      type: array\n                                  type: object\n                                failureThreshold:\n                                  type: integer\n                                grpc:\n                                  properties:\n                                    port:\n                                      type: integer\n                                    service:\n                                      type: string\n                                  type: object\n                                httpGet:\n                                  properties:\n                                    host:\n                                      type: string\n                                    httpHeaders:\n                                      items:\n                                        properties:\n                                          name:\n                                            type: string\n                                          value:\n                                            type: string\n                                        type: object\n                                      type: array\n                                    path:\n                                      type: string\n                                    port:\n                                      anyOf:\n                                      - type: integer\n                                      - type: string\n                                      x-kubernetes-int-or-string: true\n                                    scheme:\n                                      type: string\n                                  type: object\n                                initialDelaySeconds:\n                                  type: integer\n                                periodSeconds:\n                                  type: integer\n                                successThreshold:\n                                  type: integer\n                                tcpSocket:\n                                  properties:\n                                    host:\n                                      type: string\n                                    port:\n                                      anyOf:\n                                      - type: integer\n                                      - type: string\n                                      x-kubernetes-int-or-string: true\n                                  type: object\n                                terminationGracePeriodSeconds:\n                                  type: integer\n                                timeoutSeconds:\n                                  type: integer\n                              type: object\n                            name:\n                              type: string\n                            ports:\n                              items:\n                                properties:\n                                  containerPort:\n                                    type: integer\n                                  hostIP:\n                                    type: string\n                                  hostPort:\n                                    type: integer\n                                  name:\n                                    type: string\n                                  protocol:\n                                    type: string\n                                type: object\n                              type: array\n                            readinessProbe:\n                              properties:\n                                exec:\n                                  properties:\n                                    command:\n                                      items:\n                                        type: string\n                                      type: array\n                                  type: object\n                                failureThreshold:\n                                  type: integer\n                                grpc:\n                                  properties:\n                                    port:\n                                      type: integer\n                                    service:\n                                      type: string\n                                  type: object\n                                httpGet:\n                                  properties:\n                                    host:\n                                      type: string\n                                    httpHeaders:\n                                      items:\n                                        properties:\n                                          name:\n                                            type: string\n                                          value:\n                                            type: string\n                                        type: object\n                                      type: array\n                                    path:\n                                      type: string\n                                    port:\n                                      anyOf:\n                                      - type: integer\n                                      - type: string\n                                      x-kubernetes-int-or-string: true\n                                    scheme:\n                                      type: string\n                                  type: object\n                                initialDelaySeconds:\n                                  type: integer\n                                periodSeconds:\n                                  type: integer\n                                successThreshold:\n                                  type: integer\n                                tcpSocket:\n                                  properties:\n                                    host:\n                                      type: string\n                                    port:\n                                      anyOf:\n                                      - type: integer\n                                      - type: string\n                                      x-kubernetes-int-or-string: true\n                                  type: object\n                                terminationGracePeriodSeconds:\n                                  type: integer\n                                timeoutSeconds:\n                                  type: integer\n                              type: object\n                            resources:\n                              properties:\n                                limits:\n                                  additionalProperties:\n                                    anyOf:\n                                    - type: integer\n                                    - type: string\n                                    x-kubernetes-int-or-string: true\n                                  type: object\n                                requests:\n                                  additionalProperties:\n                                    anyOf:\n                                    - type: integer\n                                    - type: string\n                                    x-kubernetes-int-or-string: true\n                                  type: object\n                              type: object\n                            securityContext:\n                              properties:\n                                allowPrivilegeEscalation:\n                                  type: boolean\n                                capabilities:\n                                  properties:\n                                    add:\n                                      items:\n                                        type: string\n                                      type: array\n                                    drop:\n                                      items:\n                                        type: string\n                                      type: array\n                                  type: object\n                                privileged:\n                                  type: boolean\n                                procMount:\n                                  type: string\n                                readOnlyRootFilesystem:\n                                  type: boolean\n                                runAsGroup:\n                                  type: integer\n                                runAsNonRoot:\n                                  type: boolean\n                                runAsUser:\n                                  type: integer\n                                seLinuxOptions:\n                                  properties:\n                                    level:\n                                      type: string\n                                    role:\n                                      type: string\n                                    type:\n                                      type: string\n                                    user:\n                                      type: string\n                                  type: object\n                                seccompProfile:\n                                  properties:\n                                    localhostProfile:\n                                      type: string\n                                    type:\n                                      type: string\n                                  type: object\n                                windowsOptions:\n                                  properties:\n                                    gmsaCredentialSpec:\n                                      type: string\n                                    gmsaCredentialSpecName:\n                                      type: string\n                                    hostProcess:\n                                      type: boolean\n                                    runAsUserName:\n                                      type: string\n                                  type: object\n                              type: object\n                            startupProbe:\n                              properties:\n                                exec:\n                                  properties:\n                                    command:\n                                      items:\n                                        type: string\n                                      type: array\n                                  type: object\n                                failureThreshold:\n                                  type: integer\n                                grpc:\n                                  properties:\n                                    port:\n                                      type: integer\n                                    service:\n                                      type: string\n                                  type: object\n                                httpGet:\n                                  properties:\n                                    host:\n                                      type: string\n                                    httpHeaders:\n                                      items:\n                                        properties:\n                                          name:\n                                            type: string\n                                          value:\n                                            type: string\n                                        type: object\n                                      type: array\n                                    path:\n                                      type: string\n                                    port:\n                                      anyOf:\n                                      - type: integer\n                                      - type: string\n                                      x-kubernetes-int-or-string: true\n                                    scheme:\n                                      type: string\n                                  type: object\n                                initialDelaySeconds:\n                                  type: integer\n                                periodSeconds:\n                                  type: integer\n                                successThreshold:\n                                  type: integer\n                                tcpSocket:\n                                  properties:\n                                    host:\n                                      type: string\n                                    port:\n                                      anyOf:\n                                      - type: integer\n                                      - type: string\n                                      x-kubernetes-int-or-string: true\n                                  type: object\n                                terminationGracePeriodSeconds:\n                                  type: integer\n                                timeoutSeconds:\n                                  type: integer\n                              type: object\n                            stdin:\n                              type: boolean\n                            stdinOnce:\n                              type: boolean\n                            terminationMessagePath:\n                              type: string\n                            terminationMessagePolicy:\n                              type: string\n                            tty:\n                              type: boolean\n                            volumeDevices:\n                              items:\n                                properties:\n                                  devicePath:\n                                    type: string\n                                  name:\n                                    type: string\n                                type: object\n                              type: array\n                            volumeMounts:\n                              items:\n                                properties:\n                                  mountPath:\n                                    type: string\n                                  mountPropagation:\n                                    type: string\n                                  name:\n                                    type: string\n                                  readOnly:\n                                    type: boolean\n                                  subPath:\n                                    type: string\n                                  subPathExpr:\n                                    type: string\n                                type: object\n                              type: array\n                            workingDir:\n                              type: string\n                          type: object\n                        type: array\n                      nodeName:\n                        type: string\n                      nodeSelector:\n                        additionalProperties:\n                          type: string\n                        type: object\n                      os:\n                        properties:\n                          name:\n                            type: string\n                        type: object\n                      overhead:\n                        additionalProperties:\n                          anyOf:\n                          - type: integer\n                          - type: string\n                          x-kubernetes-int-or-string: true\n                        type: object\n                      preemptionPolicy:\n                        type: string\n                      priority:\n                        type: integer\n                      priorityClassName:\n                        type: string\n                      readinessGates:\n                        items:\n                          properties:\n                            conditionType:\n                              type: string\n                          type: object\n                        type: array\n                      restartPolicy:\n                        type: string\n                      runtimeClassName:\n                        type: string\n                      schedulerName:\n                        type: string\n                      securityContext:\n                        properties:\n                          fsGroup:\n                            type: integer\n                          fsGroupChangePolicy:\n                            type: string\n                          runAsGroup:\n                            type: integer\n                          runAsNonRoot:\n                            type: boolean\n                          runAsUser:\n                            type: integer\n                          seLinuxOptions:\n                            properties:\n                              level:\n                                type: string\n                              role:\n                                type: string\n                              type:\n                                type: string\n                              user:\n                                type: string\n                            type: object\n                          seccompProfile:\n                            properties:\n                              localhostProfile:\n                                type: string\n                              type:\n                                type: string\n                            type: object\n                          supplementalGroups:\n                            items:\n                              type: integer\n                            type: array\n                          sysctls:\n                            items:\n                              properties:\n                                name:\n                                  type: string\n                                value:\n                                  type: string\n                              type: object\n                            type: array\n                          windowsOptions:\n                            properties:\n                              gmsaCredentialSpec:\n                                type: string\n                              gmsaCredentialSpecName:\n                                type: string\n                              hostProcess:\n                                type: boolean\n                              runAsUserName:\n                                type: string\n                            type: object\n                        type: object\n                      serviceAccount:\n                        type: string\n                      serviceAccountName:\n                        type: string\n                      setHostnameAsFQDN:\n                        type: boolean\n                      shareProcessNamespace:\n                        type: boolean\n                      subdomain:\n                        type: string\n                      terminationGracePeriodSeconds:\n                        type: integer\n                      tolerations:\n                        items:\n                          properties:\n                            effect:\n                              type: string\n                            key:\n                              type: string\n                            operator:\n                              type: string\n                            tolerationSeconds:\n                              type: integer\n                            value:\n                              type: string\n                          type: object\n                        type: array\n                      topologySpreadConstraints:\n                        items:\n                          properties:\n                            labelSelector:\n                              properties:\n                                matchExpressions:\n                                  items:\n                                    properties:\n                                      key:\n                                        type: string\n                                      operator:\n                                        type: string\n                                      values:\n                                        items:\n                                          type: string\n                                        type: array\n                                    type: object\n                                  type: array\n                                matchLabels:\n                                  additionalProperties:\n                                    type: string\n                                  type: object\n                              type: object\n                            maxSkew:\n                              type: integer\n                            minDomains:\n                              type: integer\n                            topologyKey:\n                              type: string\n                            whenUnsatisfiable:\n                              type: string\n                          type: object\n                        type: array\n                      volumes:\n                        items:\n                          properties:\n                            awsElasticBlockStore:\n                              properties:\n                                fsType:\n                                  type: string\n                                partition:\n                                  type: integer\n                                readOnly:\n                                  type: boolean\n                                volumeID:\n                                  type: string\n                              type: object\n                            azureDisk:\n                              properties:\n                                cachingMode:\n                                  type: string\n                                diskName:\n                                  type: string\n                                diskURI:\n                                  type: string\n                                fsType:\n                                  type: string\n                                kind:\n                                  type: string\n                                readOnly:\n                                  type: boolean\n                              type: object\n                            azureFile:\n                              properties:\n                                readOnly:\n                                  type: boolean\n                                secretName:\n                                  type: string\n                                shareName:\n                                  type: string\n                              type: object\n                            cephfs:\n                              properties:\n                                monitors:\n                                  items:\n                                    type: string\n                                  type: array\n                                path:\n                                  type: string\n                                readOnly:\n                                  type: boolean\n                                secretFile:\n                                  type: string\n                                secretRef:\n                                  properties:\n                                    name:\n                                      type: string\n                                  type: object\n                                user:\n                                  type: string\n                              type: object\n                            cinder:\n                              properties:\n                                fsType:\n                                  type: string\n                                readOnly:\n                                  type: boolean\n                                secretRef:\n                                  properties:\n                                    name:\n                                      type: string\n                                  type: object\n                                volumeID:\n                                  type: string\n                              type: object\n                            configMap:\n                              properties:\n                                defaultMode:\n                                  type: integer\n                                items:\n                                  items:\n                                    properties:\n                                      key:\n                                        type: string\n                                      mode:\n                                        type: integer\n                                      path:\n                                        type: string\n                                    type: object\n                                  type: array\n                                name:\n                                  type: string\n                                optional:\n                                  type: boolean\n                              type: object\n                            csi:\n                              properties:\n                                driver:\n                                  type: string\n                                fsType:\n                                  type: string\n                                nodePublishSecretRef:\n                                  properties:\n                                    name:\n                                      type: string\n                                  type: object\n                                readOnly:\n                                  type: boolean\n                                volumeAttributes:\n                                  additionalProperties:\n                                    type: string\n                                  type: object\n                              type: object\n                            downwardAPI:\n                              properties:\n                                defaultMode:\n                                  type: integer\n                                items:\n                                  items:\n                                    properties:\n                                      fieldRef:\n                                        properties:\n                                          apiVersion:\n                                            type: string\n                                          fieldPath:\n                                            type: string\n                                        type: object\n                                      mode:\n                                        type: integer\n                                      path:\n                                        type: string\n                                      resourceFieldRef:\n                                        properties:\n                                          containerName:\n                                            type: string\n                                          divisor:\n                                            anyOf:\n                                            - type: integer\n                                            - type: string\n                                            x-kubernetes-int-or-string: true\n                                          resource:\n                                            type: string\n                                        type: object\n                                    type: object\n                                  type: array\n                              type: object\n                            emptyDir:\n                              properties:\n                                medium:\n                                  type: string\n                                sizeLimit:\n                                  anyOf:\n                                  - type: integer\n                                  - type: string\n                                  x-kubernetes-int-or-string: true\n                              type: object\n                            ephemeral:\n                              properties:\n                                volumeClaimTemplate:\n                                  properties:\n                                    metadata:\n                                      properties:\n                                        annotations:\n                                          additionalProperties:\n                                            type: string\n                                          type: object\n                                        clusterName:\n                                          type: string\n                                        creationTimestamp:\n                                          type: string\n                                        deletionGracePeriodSeconds:\n                                          type: integer\n                                        deletionTimestamp:\n                                          type: string\n                                        finalizers:\n                                          items:\n                                            type: string\n                                          type: array\n                                        generateName:\n                                          type: string\n                                        generation:\n                                          type: integer\n                                        labels:\n                                          additionalProperties:\n                                            type: string\n                                          type: object\n                                        managedFields:\n                                          items:\n                                            properties:\n                                              apiVersion:\n                                                type: string\n                                              fieldsType:\n                                                type: string\n                                              fieldsV1:\n                                                type: object\n                                              manager:\n                                                type: string\n                                              operation:\n                                                type: string\n                                              subresource:\n                                                type: string\n                                              time:\n                                                type: string\n                                            type: object\n                                          type: array\n                                        name:\n                                          type: string\n                                        namespace:\n                                          type: string\n                                        ownerReferences:\n                                          items:\n                                            properties:\n                                              apiVersion:\n                                                type: string\n                                              blockOwnerDeletion:\n                                                type: boolean\n                                              controller:\n                                                type: boolean\n                                              kind:\n                                                type: string\n                                              name:\n                                                type: string\n                                              uid:\n                                                type: string\n                                            type: object\n                                          type: array\n                                        resourceVersion:\n                                          type: string\n                                        selfLink:\n                                          type: string\n                                        uid:\n                                          type: string\n                                      type: object\n                                    spec:\n                                      properties:\n                                        accessModes:\n                                          items:\n                                            type: string\n                                          type: array\n                                        dataSource:\n                                          properties:\n                                            apiGroup:\n                                              type: string\n                                            kind:\n                                              type: string\n                                            name:\n                                              type: string\n                                          type: object\n                                        dataSourceRef:\n                                          properties:\n                                            apiGroup:\n                                              type: string\n                                            kind:\n                                              type: string\n                                            name:\n                                              type: string\n                                          type: object\n                                        resources:\n                                          properties:\n                                            limits:\n                                              additionalProperties:\n                                                anyOf:\n                                                - type: integer\n                                                - type: string\n                                                x-kubernetes-int-or-string: true\n                                              type: object\n                                            requests:\n                                              additionalProperties:\n                                                anyOf:\n                                                - type: integer\n                                                - type: string\n                                                x-kubernetes-int-or-string: true\n                                              type: object\n                                          type: object\n                                        selector:\n                                          properties:\n                                            matchExpressions:\n                                              items:\n                                                properties:\n                                                  key:\n                                                    type: string\n                                                  operator:\n                                                    type: string\n                                                  values:\n                                                    items:\n                                                      type: string\n                                                    type: array\n                                                type: object\n                                              type: array\n                                            matchLabels:\n                                              additionalProperties:\n                                                type: string\n                                              type: object\n                                          type: object\n                                        storageClassName:\n                                          type: string\n                                        volumeMode:\n                                          type: string\n                                        volumeName:\n                                          type: string\n                                      type: object\n                                  type: object\n                              type: object\n                            fc:\n                              properties:\n                                fsType:\n                                  type: string\n                                lun:\n                                  type: integer\n                                readOnly:\n                                  type: boolean\n                                targetWWNs:\n                                  items:\n                                    type: string\n                                  type: array\n                                wwids:\n                                  items:\n                                    type: string\n                                  type: array\n                              type: object\n                            flexVolume:\n                              properties:\n                                driver:\n                                  type: string\n                                fsType:\n                                  type: string\n                                options:\n                                  additionalProperties:\n                                    type: string\n                                  type: object\n                                readOnly:\n                                  type: boolean\n                                secretRef:\n                                  properties:\n                                    name:\n                                      type: string\n                                  type: object\n                              type: object\n                            flocker:\n                              properties:\n                                datasetName:\n                                  type: string\n                                datasetUUID:\n                                  type: string\n                              type: object\n                            gcePersistentDisk:\n                              properties:\n                                fsType:\n                                  type: string\n                                partition:\n                                  type: integer\n                                pdName:\n                                  type: string\n                                readOnly:\n                                  type: boolean\n                              type: object\n                            gitRepo:\n                              properties:\n                                directory:\n                                  type: string\n                                repository:\n                                  type: string\n                                revision:\n                                  type: string\n                              type: object\n                            glusterfs:\n                              properties:\n                                endpoints:\n                                  type: string\n                                path:\n                                  type: string\n                                readOnly:\n                                  type: boolean\n                              type: object\n                            hostPath:\n                              properties:\n                                path:\n                                  type: string\n                                type:\n                                  type: string\n                              type: object\n                            iscsi:\n                              properties:\n                                chapAuthDiscovery:\n                                  type: boolean\n                                chapAuthSession:\n                                  type: boolean\n                                fsType:\n                                  type: string\n                                initiatorName:\n                                  type: string\n                                iqn:\n                                  type: string\n                                iscsiInterface:\n                                  type: string\n                                lun:\n                                  type: integer\n                                portals:\n                                  items:\n                                    type: string\n                                  type: array\n                                readOnly:\n                                  type: boolean\n                                secretRef:\n                                  properties:\n                                    name:\n                                      type: string\n                                  type: object\n                                targetPortal:\n                                  type: string\n                              type: object\n                            name:\n                              type: string\n                            nfs:\n                              properties:\n                                path:\n                                  type: string\n                                readOnly:\n                                  type: boolean\n                                server:\n                                  type: string\n                              type: object\n                            persistentVolumeClaim:\n                              properties:\n                                claimName:\n                                  type: string\n                                readOnly:\n                                  type: boolean\n                              type: object\n                            photonPersistentDisk:\n                              properties:\n                                fsType:\n                                  type: string\n                                pdID:\n                                  type: string\n                              type: object\n                            portworxVolume:\n                              properties:\n                                fsType:\n                                  type: string\n                                readOnly:\n                                  type: boolean\n                                volumeID:\n                                  type: string\n                              type: object\n                            projected:\n                              properties:\n                                defaultMode:\n                                  type: integer\n                                sources:\n                                  items:\n                                    properties:\n                                      configMap:\n                                        properties:\n                                          items:\n                                            items:\n                                              properties:\n                                                key:\n                                                  type: string\n                                                mode:\n                                                  type: integer\n                                                path:\n                                                  type: string\n                                              type: object\n                                            type: array\n                                          name:\n                                            type: string\n                                          optional:\n                                            type: boolean\n                                        type: object\n                                      downwardAPI:\n                                        properties:\n                                          items:\n                                            items:\n                                              properties:\n                                                fieldRef:\n                                                  properties:\n                                                    apiVersion:\n                                                      type: string\n                                                    fieldPath:\n                                                      type: string\n                                                  type: object\n                                                mode:\n                                                  type: integer\n                                                path:\n                                                  type: string\n                                                resourceFieldRef:\n                                                  properties:\n                                                    containerName:\n                                                      type: string\n                                                    divisor:\n                                                      anyOf:\n                                                      - type: integer\n                                                      - type: string\n                                                      x-kubernetes-int-or-string: true\n                                                    resource:\n                                                      type: string\n                                                  type: object\n                                              type: object\n                                            type: array\n                                        type: object\n                                      secret:\n                                        properties:\n                                          items:\n                                            items:\n                                              properties:\n                                                key:\n                                                  type: string\n                                                mode:\n                                                  type: integer\n                                                path:\n                                                  type: string\n                                              type: object\n                                            type: array\n                                          name:\n                                            type: string\n                                          optional:\n                                            type: boolean\n                                        type: object\n                                      serviceAccountToken:\n                                        properties:\n                                          audience:\n                                            type: string\n                                          expirationSeconds:\n                                            type: integer\n                                          path:\n                                            type: string\n                                        type: object\n                                    type: object\n                                  type: array\n                              type: object\n                            quobyte:\n                              properties:\n                                group:\n                                  type: string\n                                readOnly:\n                                  type: boolean\n                                registry:\n                                  type: string\n                                tenant:\n                                  type: string\n                                user:\n                                  type: string\n                                volume:\n                                  type: string\n                              type: object\n                            rbd:\n                              properties:\n                                fsType:\n                                  type: string\n                                image:\n                                  type: string\n                                keyring:\n                                  type: string\n                                monitors:\n                                  items:\n                                    type: string\n                                  type: array\n                                pool:\n                                  type: string\n                                readOnly:\n                                  type: boolean\n                                secretRef:\n                                  properties:\n                                    name:\n                                      type: string\n                                  type: object\n                                user:\n                                  type: string\n                              type: object\n                            scaleIO:\n                              properties:\n                                fsType:\n                                  type: string\n                                gateway:\n                                  type: string\n                                protectionDomain:\n                                  type: string\n                                readOnly:\n                                  type: boolean\n                                secretRef:\n                                  properties:\n                                    name:\n                                      type: string\n                                  type: object\n                                sslEnabled:\n                                  type: boolean\n                                storageMode:\n                                  type: string\n                                storagePool:\n                                  type: string\n                                system:\n                                  type: string\n                                volumeName:\n                                  type: string\n                              type: object\n                            secret:\n                              properties:\n                                defaultMode:\n                                  type: integer\n                                items:\n                                  items:\n                                    properties:\n                                      key:\n                                        type: string\n                                      mode:\n                                        type: integer\n                                      path:\n                                        type: string\n                                    type: object\n                                  type: array\n                                optional:\n                                  type: boolean\n                                secretName:\n                                  type: string\n                              type: object\n                            storageos:\n                              properties:\n                                fsType:\n                                  type: string\n                                readOnly:\n                                  type: boolean\n                                secretRef:\n                                  properties:\n                                    name:\n                                      type: string\n                                  type: object\n                                volumeName:\n                                  type: string\n                                volumeNamespace:\n                                  type: string\n                              type: object\n                            vsphereVolume:\n                              properties:\n                                fsType:\n                                  type: string\n                                storagePolicyID:\n                                  type: string\n                                storagePolicyName:\n                                  type: string\n                                volumePath:\n                                  type: string\n                              type: object\n                          type: object\n                        type: array\n                    type: object\n                  status:\n                    properties:\n                      conditions:\n                        items:\n                          properties:\n                            lastProbeTime:\n                              type: string\n                            lastTransitionTime:\n                              type: string\n                            message:\n                              type: string\n                            reason:\n                              type: string\n                            status:\n                              type: string\n                            type:\n                              type: string\n                          type: object\n                        type: array\n                      containerStatuses:\n                        items:\n                          properties:\n                            containerID:\n                              type: string\n                            image:\n                              type: string\n                            imageID:\n                              type: string\n                            lastState:\n                              properties:\n                                running:\n                                  properties:\n                                    startedAt:\n                                      type: string\n                                  type: object\n                                terminated:\n                                  properties:\n                                    containerID:\n                                      type: string\n                                    exitCode:\n                                      type: integer\n                                    finishedAt:\n                                      type: string\n                                    message:\n                                      type: string\n                                    reason:\n                                      type: string\n                                    signal:\n                                      type: integer\n                                    startedAt:\n                                      type: string\n                                  type: object\n                                waiting:\n                                  properties:\n                                    message:\n                                      type: string\n                                    reason:\n                                      type: string\n                                  type: object\n                              type: object\n                            name:\n                              type: string\n                            ready:\n                              type: boolean\n                            restartCount:\n                              type: integer\n                            started:\n                              type: boolean\n                            state:\n                              properties:\n                                running:\n                                  properties:\n                                    startedAt:\n                                      type: string\n                                  type: object\n                                terminated:\n                                  properties:\n                                    containerID:\n                                      type: string\n                                    exitCode:\n                                      type: integer\n                                    finishedAt:\n                                      type: string\n                                    message:\n                                      type: string\n                                    reason:\n                                      type: string\n                                    signal:\n                                      type: integer\n                                    startedAt:\n                                      type: string\n                                  type: object\n                                waiting:\n                                  properties:\n                                    message:\n                                      type: string\n                                    reason:\n                                      type: string\n                                  type: object\n                              type: object\n                          type: object\n                        type: array\n                      ephemeralContainerStatuses:\n                        items:\n                          properties:\n                            containerID:\n                              type: string\n                            image:\n                              type: string\n                            imageID:\n                              type: string\n                            lastState:\n                              properties:\n                                running:\n                                  properties:\n                                    startedAt:\n                                      type: string\n                                  type: object\n                                terminated:\n                                  properties:\n                                    containerID:\n                                      type: string\n                                    exitCode:\n                                      type: integer\n                                    finishedAt:\n                                      type: string\n                                    message:\n                                      type: string\n                                    reason:\n                                      type: string\n                                    signal:\n                                      type: integer\n                                    startedAt:\n                                      type: string\n                                  type: object\n                                waiting:\n                                  properties:\n                                    message:\n                                      type: string\n                                    reason:\n                                      type: string\n                                  type: object\n                              type: object\n                            name:\n                              type: string\n                            ready:\n                              type: boolean\n                            restartCount:\n                              type: integer\n                            started:\n                              type: boolean\n                            state:\n                              properties:\n                                running:\n                                  properties:\n                                    startedAt:\n                                      type: string\n                                  type: object\n                                terminated:\n                                  properties:\n                                    containerID:\n                                      type: string\n                                    exitCode:\n                                      type: integer\n                                    finishedAt:\n                                      type: string\n                                    message:\n                                      type: string\n                                    reason:\n                                      type: string\n                                    signal:\n                                      type: integer\n                                    startedAt:\n                                      type: string\n                                  type: object\n                                waiting:\n                                  properties:\n                                    message:\n                                      type: string\n                                    reason:\n                                      type: string\n                                  type: object\n                              type: object\n                          type: object\n                        type: array\n                      hostIP:\n                        type: string\n                      initContainerStatuses:\n                        items:\n                          properties:\n                            containerID:\n                              type: string\n                            image:\n                              type: string\n                            imageID:\n                              type: string\n                            lastState:\n                              properties:\n                                running:\n                                  properties:\n                                    startedAt:\n                                      type: string\n                                  type: object\n                                terminated:\n                                  properties:\n                                    containerID:\n                                      type: string\n                                    exitCode:\n                                      type: integer\n                                    finishedAt:\n                                      type: string\n                                    message:\n                                      type: string\n                                    reason:\n                                      type: string\n                                    signal:\n                                      type: integer\n                                    startedAt:\n                                      type: string\n                                  type: object\n                                waiting:\n                                  properties:\n                                    message:\n                                      type: string\n                                    reason:\n                                      type: string\n                                  type: object\n                              type: object\n                            name:\n                              type: string\n                            ready:\n                              type: boolean\n                            restartCount:\n                              type: integer\n                            started:\n                              type: boolean\n                            state:\n                              properties:\n                                running:\n                                  properties:\n                                    startedAt:\n                                      type: string\n                                  type: object\n                                terminated:\n                                  properties:\n                                    containerID:\n                                      type: string\n                                    exitCode:\n                                      type: integer\n                                    finishedAt:\n                                      type: string\n                                    message:\n                                      type: string\n                                    reason:\n                                      type: string\n                                    signal:\n                                      type: integer\n                                    startedAt:\n                                      type: string\n                                  type: object\n                                waiting:\n                                  properties:\n                                    message:\n                                      type: string\n                                    reason:\n                                      type: string\n                                  type: object\n                              type: object\n                          type: object\n                        type: array\n                      message:\n                        type: string\n                      nominatedNodeName:\n                        type: string\n                      phase:\n                        type: string\n                      podIP:\n                        type: string\n                      podIPs:\n                        items:\n                          properties:\n                            ip:\n                              type: string\n                          type: object\n                        type: array\n                      qosClass:\n                        type: string\n                      reason:\n                        type: string\n                      startTime:\n                        type: string\n                    type: object\n                type: object\n            type: object\n          status:\n            type: object\n        type: object\n    served: true\n    storage: true\n    subresources:\n      status: {}\n"
  },
  {
    "path": "agent-operator/deploy/crds/example.agentpool.yml",
    "content": "## Concord Agent Pool definition\n##\n## \"%%var%%\" are template variables which will be replaces by the operator\n##\n## %%configMapName%% - name of the ConfigMap resource used by the Agent's pod\n## %%podName%% - Agent's pod name\n## %%app%% - the operator's label\n## %%poolName%% - name of the pool, the operator automatically uses the CR's name as the pool name\n## %%concordCfgHash%% - configuration hash\n## %%preStopHook%% - content of the preStop script\n\napiVersion: concord.walmartlabs.com/v1alpha1\nkind: AgentPool\nmetadata:\n  name: example\nspec:\n  queueSelector:\n    agent:\n      flavor: \"k8s-test\"\n\n  autoScale: false\n  minSize: 1\n  maxSize: 10\n  size: 1\n\n  configMap:\n    apiVersion: v1\n    kind: ConfigMap\n    metadata:\n      name: \"%%configMapName%%\"\n    data:\n      mvn.json: |\n        {\n          \"repositories\": [\n            {\n              \"id\": \"central\",\n              \"url\": \"https://repo.maven.apache.org/maven2/\"\n            }\n          ]\n        }\n\n      agent.conf: |\n        concord-agent {\n          capabilities = {\n              flavor = \"k8s-test\"\n              k8s {\n                  cluster = \"minikube\"\n                  namespace = \"default\"\n                  pod = ${MY_POD_NAME}\n              }\n          }\n\n          workersCount = 1\n\n          server {\n              apiBaseUrl = \"http://11.12.13.14:8001\"\n              websocketUrl = \"ws://11.12.13.14:8001/websocket\"\n              readTimeout = \"10 minutes\"\n          }\n        }\n      preStopHook.sh: \"%%preStopHook%%\"\n\n  pod:\n    apiVersion: v1\n    kind: Pod\n    metadata:\n      name: \"%%podName%%\"\n      labels:\n        app: \"%%app%%\"\n        poolName: \"%%poolName%%\"\n        concordCfgHash: \"%%concordCfgHash%%\"\n    spec:\n      terminationGracePeriodSeconds: 3600\n      containers:\n        - name: dind\n          image: \"docker:dind\"\n          args: [ \"-H tcp://0.0.0.0:6666\" ]\n          resources:\n            requests:\n              cpu: 1\n              memory: \"2G\"\n              ephemeral-storage: \"2G\"\n            limits:\n              cpu: 2\n              memory: \"3G\"\n              ephemeral-storage: \"3G\"\n          volumeMounts:\n            - name: \"process-tmp\"\n              mountPath: \"/tmp\"\n            - mountPath: \"/hooks/preStopHook.sh\"\n              name: cfg\n              subPath: preStopHook.sh\n          securityContext:\n            privileged: true\n          lifecycle:\n            preStop:\n              exec:\n                command:\n                  - \"sh\"\n                  - \"/hooks/preStopHook.sh\"\n\n        - name: agent\n          image: \"walmartlabs/concord-agent:latest\"\n          imagePullPolicy: Never\n          volumeMounts:\n            - mountPath: \"/opt/concord/conf/agent.conf\"\n              name: cfg\n              subPath: agent.conf\n            - mountPath: \"/opt/concord/conf/mvn.json\"\n              name: cfg\n              subPath: mvn.json\n            - mountPath: \"/opt/concord/hooks/preStopHook.sh\"\n              name: cfg\n              subPath: preStopHook.sh\n            - mountPath: \"/tmp\"\n              name: process-tmp\n          resources:\n            requests:\n              cpu: 1\n              memory: \"1G\"\n            limits:\n              cpu: 2\n              memory: \"2G\"\n          env:\n            - name: CONCORD_TMP_DIR\n              value: \"/tmp/concord\"\n            - name: CONCORD_DOCKER_LOCAL_MODE\n              value: \"false\"\n            - name: DOCKER_HOST\n              value: \"tcp://localhost:6666\"\n            - name: CONCORD_CFG_FILE\n              value: \"/opt/concord/conf/agent.conf\"\n            - name: CONCORD_MAVEN_CFG\n              value: \"/opt/concord/conf/mvn.json\"\n            - name: MY_POD_NAME\n              valueFrom:\n                fieldRef:\n                  fieldPath: metadata.name\n            - name: MY_POD_NAMESPACE\n              valueFrom:\n                fieldRef:\n                  fieldPath: metadata.namespace\n            - name: MY_POD_IP\n              valueFrom:\n                fieldRef:\n                  fieldPath: status.podIP\n            - name: USER_AGENT\n              value: \"k8s-agent $(MY_POD_NAMESPACE)/$(MY_POD_NAME) @ $(MY_POD_IP)\"\n          lifecycle:\n            preStop:\n              exec:\n                command:\n                  - \"/bin/bash\"\n                  - \"/opt/concord/hooks/preStopHook.sh\"\n      volumes:\n        - name: cfg\n          configMap:\n            name: \"%%configMapName%%\"\n        - name: process-tmp\n          emptyDir: { }\n"
  },
  {
    "path": "agent-operator/deploy/operator.yml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: concord-agent-operator\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      name: concord-agent-operator\n  template:\n    metadata:\n      labels:\n        name: concord-agent-operator\n    spec:\n      serviceAccountName: concord-agent-operator\n      containers:\n        - name: concord-agent-operator\n          image: \"walmartlabs/concord-agent-operator:latest\"\n          imagePullPolicy: Never\n          env:\n            - name: WATCH_NAMESPACE\n              valueFrom:\n                fieldRef:\n                  fieldPath: metadata.namespace\n            - name: POD_NAME\n              valueFrom:\n                fieldRef:\n                  fieldPath: metadata.name\n            - name: SCALE_UP_DELAY_MS\n              value: \"15000\"\n            - name: SCALE_DOWN_DELAY_MS\n              value: \"180000\"\n            - name: INCREMENT_PERCENTAGE\n              value: \"50\"\n            - name: DECREMENT_PERCENTAGE\n              value: \"10\"\n            - name: INCREMENT_THRESHOLD_FACTOR\n              value: \"1.5\"\n            - name: DECREMENT_THRESHOLD_FACTOR\n              value: \"1.0\"\n            - name: OPERATOR_NAME\n              value: \"concord-agent-operator\"\n            - name: CONCORD_BASE_URL\n              value: \"http://host.minikube.internal:8001\" # replace with local IP\n            - name: CONCORD_API_TOKEN\n              value: \"...API token...\" # replace with an actual Concord API token\n"
  },
  {
    "path": "agent-operator/deploy/service_account.yml",
    "content": "apiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: concord-agent-operator\n  namespace: default\n"
  },
  {
    "path": "agent-operator/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <groupId>com.walmartlabs.concord.k8s</groupId>\n    <artifactId>concord-agent-operator</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n        </dependency>\n\n        <!-- k8s stuff -->\n        <dependency>\n            <groupId>io.fabric8</groupId>\n            <artifactId>kubernetes-client</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.fabric8</groupId>\n            <artifactId>kubernetes-model-apiextensions</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.fabric8</groupId>\n            <artifactId>kubernetes-model-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.fabric8</groupId>\n            <artifactId>kubernetes-model-common</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.bouncycastle</groupId>\n            <artifactId>bcprov-jdk18on</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.bouncycastle</groupId>\n            <artifactId>bcpkix-jdk18on</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.bouncycastle</groupId>\n            <artifactId>bcutil-jdk18on</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.guava</groupId>\n            <artifactId>guava</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>ch.qos.logback</groupId>\n            <artifactId>logback-classic</artifactId>\n        </dependency>\n\n        <!-- JDK9 compatibility -->\n        <dependency>\n            <groupId>javax.xml.bind</groupId>\n            <artifactId>jaxb-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.sun.xml.bind</groupId>\n            <artifactId>jaxb-impl</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>compile</scope>\n        </dependency>\n\n        <!-- testing -->\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-shade-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>shade</goal>\n                        </goals>\n                        <configuration>\n                            <finalName>operator</finalName>\n                            <shadedArtifactAttached>true</shadedArtifactAttached>\n                            <shadedClassifierName>uber</shadedClassifierName>\n                            <transformers>\n                                <transformer implementation=\"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer\">\n                                    <mainClass>com.walmartlabs.concord.agentoperator.Operator</mainClass>\n                                </transformer>\n                            </transformers>\n                            <filters>\n                                <filter>\n                                    <artifact>*:*</artifact>\n                                    <excludes>\n                                        <exclude>META-INF/*.SF</exclude>\n                                        <exclude>META-INF/*.RSA</exclude>\n                                    </excludes>\n                                </filter>\n                            </filters>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/HashUtils.java",
    "content": "package com.walmartlabs.concord.agentoperator;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.SerializationFeature;\nimport com.google.common.hash.HashCode;\nimport com.google.common.hash.Hashing;\n\nimport javax.xml.bind.DatatypeConverter;\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\n\npublic final class HashUtils {\n\n    private static final ObjectMapper objectMapper = new ObjectMapper()\n            .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);\n\n    public static String hashAsHexString(Object v) throws IOException {\n        String s = objectMapper.writeValueAsString(v);\n        HashCode hc = Hashing.sha1().hashString(s, StandardCharsets.UTF_8);\n        return DatatypeConverter.printHexBinary(hc.asBytes());\n    }\n\n    private HashUtils() {\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/Operator.java",
    "content": "package com.walmartlabs.concord.agentoperator;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agentoperator.crd.AgentPool;\nimport com.walmartlabs.concord.agentoperator.crd.AgentPoolList;\nimport com.walmartlabs.concord.agentoperator.scheduler.AutoScalerFactory;\nimport com.walmartlabs.concord.agentoperator.scheduler.Scheduler;\nimport io.fabric8.kubernetes.client.DefaultKubernetesClient;\nimport io.fabric8.kubernetes.client.informers.ResourceEventHandler;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.time.Duration;\nimport java.util.concurrent.Executors;\n\nimport static com.walmartlabs.concord.agentoperator.scheduler.Event.Type.DELETED;\nimport static com.walmartlabs.concord.agentoperator.scheduler.Event.Type.MODIFIED;\n\npublic class Operator {\n\n    private static final Logger log = LoggerFactory.getLogger(Operator.class);\n\n    private static final long RESYNC_PERIOD = Duration.ofSeconds(10).toMillis();\n\n    public static void main(String[] args) {\n        var namespace = getEnv(\"WATCH_NAMESPACE\", \"default\");\n        var concordBaseUrl = getEnv(\"CONCORD_BASE_URL\", \"http://192.168.99.1:8001\"); // use minikube/vbox host's default address\n        var concordApiToken = getEnv(\"CONCORD_API_TOKEN\", null);\n        var useMaintenanceMode = Boolean.parseBoolean(getEnv(\"USE_AGENT_MAINTENANCE_MODE\", \"false\"));\n\n        var k8sClient = new DefaultKubernetesClient().inNamespace(namespace);\n        var executor = Executors.newSingleThreadExecutor();\n\n        var autoScalerFactory = new AutoScalerFactory(concordBaseUrl, concordApiToken, k8sClient);\n        var scheduler = new Scheduler(autoScalerFactory, k8sClient, useMaintenanceMode);\n        var handler = new ResourceEventHandler<AgentPool>() {\n            @Override\n            public void onAdd(AgentPool resource) {\n                executor.submit(() -> scheduler.onEvent(MODIFIED, resource));\n            }\n\n            @Override\n            public void onUpdate(AgentPool oldResource, AgentPool newResource) {\n                if (oldResource == newResource) {\n                    return;\n                }\n                executor.submit(() -> scheduler.onEvent(MODIFIED, newResource));\n            }\n\n            @Override\n            public void onDelete(AgentPool resource, boolean deletedFinalStateUnknown) {\n                executor.submit(() -> scheduler.onEvent(DELETED, resource));\n            }\n        };\n\n        var informer = k8sClient.resources(AgentPool.class, AgentPoolList.class)\n                .inAnyNamespace()\n                .inform(handler, RESYNC_PERIOD);\n\n        scheduler.start();\n\n        try {\n            informer.run();\n        } catch (Exception e) {\n            log.error(\"Error while watching for CRs (namespace={})\", namespace, e);\n            System.exit(2);\n        }\n\n        log.info(\"main -> and so my watch begins... (namespace={})\", namespace);\n    }\n\n    private static String getEnv(String key, String defaultValue) {\n        String s = System.getenv(key);\n        if (s == null) {\n            return defaultValue;\n        }\n\n        return s;\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/PodLabels.java",
    "content": "package com.walmartlabs.concord.agentoperator;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.fabric8.kubernetes.api.model.Pod;\nimport io.fabric8.kubernetes.client.KubernetesClient;\nimport io.fabric8.kubernetes.client.KubernetesClientException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Map;\n\npublic final class PodLabels {\n\n    private static final Logger log = LoggerFactory.getLogger(PodLabels.class);\n\n    public static void applyTag(KubernetesClient client, String podName, String tagName, String tagValue) {\n        Pod pod = client.pods().withName(podName).get();\n        if (pod == null) {\n            log.warn(\"['{}']: apply tag ['{}': '{}'] -> pod doesn't exist, nothing to do\", podName, tagName, tagValue);\n            return;\n        }\n\n        Map<String, String> labels = pod.getMetadata().getLabels();\n        if (labels.containsKey(tagName)) {\n            return;\n        }\n\n        try {\n            labels.put(tagName, tagValue);\n            client.pods().withName(podName).patch(pod);\n            log.info(\"['{}']: apply tag ['{}': '{}'] -> done\", podName, tagName, tagValue);\n        } catch (KubernetesClientException e) {\n            if (e.getCode() == 404) {\n                log.warn(\"['{}']: apply tag ['{}': '{}'] -> pod doesn't exist, nothing to do\", podName, tagName, tagValue);\n            } else {\n                log.warn(\"['{}']: apply tag ['{}': '{}'] -> error\", podName, tagName, tagValue, e);\n            }\n        }\n    }\n\n    private PodLabels() {\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/agent/AgentClient.java",
    "content": "package com.walmartlabs.concord.agentoperator.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface AgentClient {\n\n    void enableMaintenanceMode() throws Exception;\n\n    boolean hasBusyWorkers() throws Exception;\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/agent/AgentClientFactory.java",
    "content": "package com.walmartlabs.concord.agentoperator.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.fabric8.kubernetes.api.model.Pod;\n\nimport java.net.http.HttpClient;\nimport java.time.Duration;\n\nimport static java.net.http.HttpClient.Redirect.NEVER;\n\npublic class AgentClientFactory {\n\n    private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(10);\n\n    private final boolean useMaintenanceMode;\n    private final HttpClient httpClient;\n\n    public AgentClientFactory(boolean useMaintenanceMode) {\n        this.useMaintenanceMode = useMaintenanceMode;\n        if (useMaintenanceMode) {\n            this.httpClient = HttpClient.newBuilder()\n                    .version(HttpClient.Version.HTTP_1_1)\n                    .followRedirects(NEVER)\n                    .connectTimeout(CONNECT_TIMEOUT)\n                    .build();\n        } else {\n            httpClient = null;\n        }\n    }\n\n    public AgentClient create(Pod pod) {\n        if (useMaintenanceMode && isRunning(pod) && hasIP(pod)) {\n            return new DefaultAgentClient(httpClient, pod.getStatus().getPodIP());\n        } else {\n            return new NopAgentClient();\n        }\n    }\n\n    private static boolean isRunning(Pod pod) {\n        return pod.getStatus() != null && \"Running\".equals(pod.getStatus().getPhase());\n    }\n\n    private static boolean hasIP(Pod pod) {\n        return pod.getStatus() != null && pod.getStatus().getPodIP() != null;\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/agent/DefaultAgentClient.java",
    "content": "package com.walmartlabs.concord.agentoperator.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\n\npublic class DefaultAgentClient implements AgentClient {\n\n    private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(10);\n\n    private final ObjectMapper objectMapper = new ObjectMapper();\n\n    private final HttpClient client;\n    private final String url;\n\n    public DefaultAgentClient(HttpClient client, String podIp) {\n        this.client = client;\n        this.url = \"http://%s:8010/maintenance-mode\".formatted(podIp);\n    }\n\n    @Override\n    public void enableMaintenanceMode() throws Exception {\n        HttpRequest request = HttpRequest.newBuilder()\n                .uri(new URI(url))\n                .POST(HttpRequest.BodyPublishers.noBody())\n                .timeout(REQUEST_TIMEOUT)\n                .build();\n\n        client.send(request, HttpResponse.BodyHandlers.ofString());\n    }\n\n    @Override\n    public boolean hasBusyWorkers() throws Exception {\n        HttpRequest request = HttpRequest.newBuilder()\n                .uri(new URI(url))\n                .GET()\n                .timeout(REQUEST_TIMEOUT)\n                .build();\n\n        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());\n\n        MaintenanceMode entity = objectMapper.readValue(response.body(), MaintenanceMode.class);\n        return entity.maintenanceMode() && entity.workersAlive() > 0;\n    }\n\n    @Value.Immutable\n    @JsonSerialize(as = ImmutableMaintenanceMode.class)\n    @JsonDeserialize(as = ImmutableMaintenanceMode.class)\n    public interface MaintenanceMode {\n\n        boolean maintenanceMode();\n\n        int workersAlive();\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/agent/NopAgentClient.java",
    "content": "package com.walmartlabs.concord.agentoperator.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class NopAgentClient implements AgentClient {\n\n    @Override\n    public void enableMaintenanceMode() {\n        // do nothing\n    }\n\n    @Override\n    public boolean hasBusyWorkers() {\n        return false;\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/crd/AgentPool.java",
    "content": "package com.walmartlabs.concord.agentoperator.crd;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.fabric8.kubernetes.api.model.KubernetesResource;\nimport io.fabric8.kubernetes.api.model.Namespaced;\nimport io.fabric8.kubernetes.client.CustomResource;\nimport io.fabric8.kubernetes.model.annotation.*;\n\n\n@Version(AgentPool.VERSION)\n@Group(AgentPool.CONCORD_GROUP)\n@Singular(AgentPool.SERVICE_SINGULAR_NAME)\n@Plural(AgentPool.SERVICE_PLURAL_NAME)\n@ShortNames(\"aps\")\n@Kind(AgentPool.SERVICE_KIND)\npublic class AgentPool extends CustomResource<AgentPoolConfiguration, KubernetesResource> implements Namespaced {\n\n    public static final String VERSION = \"v1alpha1\";\n    private static final long serialVersionUID = 1L;\n    public static final String CONCORD_GROUP = \"concord.walmartlabs.com\";\n    public static final String SERVICE_KIND = \"AgentPool\";\n    public static final String SERVICE_LIST_KIND = \"AgentPoolList\";\n    public static final String SERVICE_SINGULAR_NAME = \"agentpool\";\n    public static final String SERVICE_PLURAL_NAME = \"agentpools\";\n    public static final String SERVICE_FULL_NAME = SERVICE_PLURAL_NAME + \".\" + CONCORD_GROUP;\n\n\n}\n\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/crd/AgentPoolConfiguration.java",
    "content": "package com.walmartlabs.concord.agentoperator.crd;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agentoperator.scheduler.DefaultAutoScaler;\nimport io.fabric8.kubernetes.api.model.ConfigMap;\nimport io.fabric8.kubernetes.api.model.Pod;\n\nimport java.io.Serializable;\nimport java.util.Map;\n\npublic class AgentPoolConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private static final long DEFAULT_SCALE_UP_DELAY_MS = 30000;\n    private static final long DEFAULT_SCALE_DOWN_DELAY_MS = 180000;\n\n    private static final String ENV_SCALE_UP_DELAY_MS = \"SCALE_UP_DELAY_MS\";\n    private static final String ENV_SCALE_DOWN_DELAY_MS = \"SCALE_DOWN_DELAY_MS\";\n    private static final String ENV_INCREMENT_PERCENTAGE = \"INCREMENT_PERCENTAGE\";\n    private static final String ENV_DECREMENT_PERCENTAGE = \"DECREMENT_PERCENTAGE\";\n    private static final String ENV_INCREMENT_THRESHOLD_FACTOR = \"INCREMENT_THRESHOLD_FACTOR\";\n    private static final String ENV_DECREMENT_THRESHOLD_FACTOR = \"DECREMENT_THRESHOLD_FACTOR\";\n\n    private static final int DEFAULT_MAX_SIZE = 10;\n    private static final int DEFAULT_MIN_SIZE = 1;\n    private static final int DEFAULT_SIZE = 1;\n    private static final int DEFAULT_SIZE_INCREMENT = 1;\n\n    private static final double DEFAULT_INCREMENT_THRESHOLD_FACTOR = 1.5;\n    private static final double DEFAULT_DECREMENT_THRESHOLD_FACTOR = 1.0;\n    private static final double DEFAULT_INCREMENT_PERCENTAGE = 50;\n    private static final double DEFAULT_DECREMENT_PERCENTAGE = 10;\n\n    private static final int DEFAULT_QUEUE_QUERY_LIMIT = 300;\n\n    private boolean autoScale = true;\n    private int maxSize = DEFAULT_MAX_SIZE;\n    private int minSize = DEFAULT_MIN_SIZE;\n    private int size = DEFAULT_SIZE;\n\n    private String autoScaleStrategy = DefaultAutoScaler.NAME;\n    private int sizeIncrement = DEFAULT_SIZE_INCREMENT;\n\n    private int queueQueryLimit = DEFAULT_QUEUE_QUERY_LIMIT;\n\n    /**\n     * Minimum time that should elapse between one scale up operation to the next\n     */\n    private long scaleUpDelayMs = getLongFromEnv(ENV_SCALE_UP_DELAY_MS, DEFAULT_SCALE_UP_DELAY_MS);\n\n    /**\n     * Minimum time that should elapse between one scale down operation to the next\n     */\n    private long scaleDownDelayMs = getLongFromEnv(ENV_SCALE_DOWN_DELAY_MS, DEFAULT_SCALE_DOWN_DELAY_MS);\n\n    /**\n     * Percentage of current pool size by which the poolsize has to be increased\n     */\n    private double percentIncrement = getDoubleFromEnv(ENV_INCREMENT_PERCENTAGE, DEFAULT_INCREMENT_PERCENTAGE);\n\n    /**\n     * Percentage of current pool size by which the poolsize has to be decreased\n     */\n    private double percentDecrement = getDoubleFromEnv(ENV_DECREMENT_PERCENTAGE, DEFAULT_DECREMENT_PERCENTAGE);\n\n    /**\n     * Factor that determines the threshold above which the operator can scale up the agent pods\n     */\n\n    private double incrementThresholdFactor = getDoubleFromEnv(ENV_INCREMENT_THRESHOLD_FACTOR, DEFAULT_INCREMENT_THRESHOLD_FACTOR);\n\n    /**\n     * Factor that determines the threshold below which the operator can scale down the agent pods.\n     */\n    private double decrementThresholdFactor = getDoubleFromEnv(ENV_DECREMENT_THRESHOLD_FACTOR, DEFAULT_DECREMENT_THRESHOLD_FACTOR);\n\n    private Map<String, Object> queueSelector;\n    private ConfigMap configMap;\n\n\n    private Pod pod;\n\n    public boolean isAutoScale() {\n        return autoScale;\n    }\n\n    public int getSizeIncrement() {\n        return sizeIncrement;\n    }\n\n    public void setSizeIncrement(int sizeIncrement) {\n        this.sizeIncrement = sizeIncrement;\n    }\n\n    public void setAutoScale(boolean autoScale) {\n        this.autoScale = autoScale;\n    }\n\n    public String getAutoScaleStrategy() {\n        return autoScaleStrategy;\n    }\n\n    public void setAutoScaleStrategy(String autoScaleStrategy) {\n        this.autoScaleStrategy = autoScaleStrategy;\n    }\n\n    public long getScaleUpDelayMs() {\n        return scaleUpDelayMs;\n    }\n\n    public void setScaleUpDelayMs(long scaleUpDelayMs) {\n        this.scaleUpDelayMs = scaleUpDelayMs;\n    }\n\n    public long getScaleDownDelayMs() {\n        return scaleDownDelayMs;\n    }\n\n    public void setScaleDownDelayMs(long scaleDownDelayMs) {\n        this.scaleDownDelayMs = scaleDownDelayMs;\n    }\n\n    public int getMaxSize() {\n        return maxSize;\n    }\n\n    public void setMaxSize(int maxSize) {\n        this.maxSize = maxSize;\n    }\n\n    public int getMinSize() {\n        return minSize;\n    }\n\n    public void setMinSize(int minSize) {\n        this.minSize = minSize;\n    }\n\n    public int getSize() {\n        return size;\n    }\n\n    public void setSize(int size) {\n        this.size = size;\n    }\n\n    public Map<String, Object> getQueueSelector() {\n        return queueSelector;\n    }\n\n    public void setQueueSelector(Map<String, Object> queueSelector) {\n        this.queueSelector = queueSelector;\n    }\n\n    public double getPercentIncrement() {\n        return percentIncrement;\n    }\n\n    public void setPercentIncrement(double percentIncrement) {\n        this.percentIncrement = percentIncrement;\n    }\n\n    public double getPercentDecrement() {\n        return percentDecrement;\n    }\n\n    public void setPercentDecrement(double percentDecrement) {\n        this.percentDecrement = percentDecrement;\n    }\n\n    public double getIncrementThresholdFactor() {\n        return incrementThresholdFactor;\n    }\n\n    public void setIncrementThresholdFactor(double incrementThresholdFactor) {\n        this.incrementThresholdFactor = incrementThresholdFactor;\n    }\n\n    public double getDecrementThresholdFactor() {\n        return decrementThresholdFactor;\n    }\n\n    public void setDecrementThresholdFactor(double decrementThresholdFactor) {\n        this.decrementThresholdFactor = decrementThresholdFactor;\n    }\n\n    public int getQueueQueryLimit() {\n        return queueQueryLimit;\n    }\n\n    public void setQueueQueryLimit(int queueQueryLimit) {\n        this.queueQueryLimit = queueQueryLimit;\n    }\n\n    public ConfigMap getConfigMap() {\n        return configMap;\n    }\n\n    public void setConfigMap(ConfigMap configMap) {\n        this.configMap = configMap;\n    }\n\n    public Pod getPod() {\n        return pod;\n    }\n\n    public void setPod(Pod pod) {\n        this.pod = pod;\n    }\n\n    private static long getLongFromEnv(String key, Long defaultValue) {\n        String envValue = System.getenv(key);\n        return envValue != null ? Long.parseLong(envValue) : defaultValue;\n    }\n\n    private static double getDoubleFromEnv(String key, double defaultValue) {\n        String envValue = System.getenv(key);\n        return envValue != null ? Double.parseDouble(envValue) : defaultValue;\n    }\n\n    private static String getStringFromEnv(String key, String defaultValue) {\n        String envValue = System.getenv(key);\n        return envValue != null ? envValue : defaultValue;\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/crd/AgentPoolList.java",
    "content": "package com.walmartlabs.concord.agentoperator.crd;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.fabric8.kubernetes.api.model.DefaultKubernetesResourceList;\n\npublic class AgentPoolList extends DefaultKubernetesResourceList<AgentPool> {\n\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/planner/Change.java",
    "content": "package com.walmartlabs.concord.agentoperator.planner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.fabric8.kubernetes.client.KubernetesClient;\n\npublic interface Change {\n\n    void apply(KubernetesClient client);\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/planner/CreateConfigMapChange.java",
    "content": "package com.walmartlabs.concord.agentoperator.planner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agentoperator.resources.AgentConfigMap;\nimport com.walmartlabs.concord.agentoperator.scheduler.AgentPoolInstance;\nimport io.fabric8.kubernetes.client.KubernetesClient;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\n\npublic class CreateConfigMapChange implements Change {\n\n    private static final Logger log = LoggerFactory.getLogger(CreateConfigMapChange.class);\n\n    private final AgentPoolInstance poolInstance;\n    private final String configMapName;\n\n    public CreateConfigMapChange(AgentPoolInstance poolInstance, String configMapName) {\n        this.poolInstance = poolInstance;\n        this.configMapName = configMapName;\n    }\n\n    @Override\n    public void apply(KubernetesClient client) {\n        try {\n            AgentConfigMap.create(client, poolInstance, configMapName);\n            log.info(\"apply -> created a configmap {}\", configMapName);\n        } catch (IOException e) {\n            log.error(\"apply -> error while creating a configmap {}: {}\", configMapName, e.getMessage());\n        }\n    }\n\n    @Override\n    public String toString() {\n        return \"CreateConfigMapChange{\" +\n                \"configMapName='\" + configMapName + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/planner/CreatePodChange.java",
    "content": "package com.walmartlabs.concord.agentoperator.planner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agentoperator.resources.AgentPod;\nimport com.walmartlabs.concord.agentoperator.scheduler.AgentPoolInstance;\nimport io.fabric8.kubernetes.client.KubernetesClient;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\n\npublic class CreatePodChange implements Change {\n\n    private static final Logger log = LoggerFactory.getLogger(CreatePodChange.class);\n\n    private final AgentPoolInstance poolInstance;\n    private final String podName;\n    private final String configMapName;\n    private final String hash;\n\n    public CreatePodChange(AgentPoolInstance poolInstance, String podName, String configMapName, String hash) {\n        this.poolInstance = poolInstance;\n        this.podName = podName;\n        this.configMapName = configMapName;\n        this.hash = hash;\n    }\n\n    @Override\n    public void apply(KubernetesClient client) {\n        try {\n            AgentPod.create(client, poolInstance, podName, configMapName, hash);\n            log.info(\"apply -> created a pod {}/{}\", poolInstance.getName(), podName);\n        } catch (IOException e) {\n            log.error(\"apply -> error while creating a pod {}/{}: {}\", poolInstance.getName(), podName, e.getMessage());\n        }\n    }\n\n    @Override\n    public String toString() {\n        return \"CreatePodChange{\" +\n                \"podName='\" + podName + '\\'' +\n                \", configMapName='\" + configMapName + '\\'' +\n                \", hash='\" + hash + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/planner/DeleteConfigMapChange.java",
    "content": "package com.walmartlabs.concord.agentoperator.planner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agentoperator.resources.AgentConfigMap;\nimport io.fabric8.kubernetes.client.KubernetesClient;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class DeleteConfigMapChange implements Change {\n\n    private static final Logger log = LoggerFactory.getLogger(DeleteConfigMapChange.class);\n\n    private final String configMapName;\n\n    public DeleteConfigMapChange(String configMapName) {\n        this.configMapName = configMapName;\n    }\n\n    @Override\n    public void apply(KubernetesClient client) {\n        AgentConfigMap.delete(client, configMapName);\n        log.info(\"apply -> removed a configmap {}\", configMapName);\n    }\n\n    @Override\n    public String toString() {\n        return \"DeleteConfigMapChange{\" +\n               \"configMapName='\" + configMapName + '\\'' +\n               '}';\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/planner/Planner.java",
    "content": "package com.walmartlabs.concord.agentoperator.planner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agentoperator.HashUtils;\nimport com.walmartlabs.concord.agentoperator.agent.AgentClient;\nimport com.walmartlabs.concord.agentoperator.agent.AgentClientFactory;\nimport com.walmartlabs.concord.agentoperator.resources.AgentConfigMap;\nimport com.walmartlabs.concord.agentoperator.resources.AgentPod;\nimport com.walmartlabs.concord.agentoperator.scheduler.AgentPoolInstance;\nimport io.fabric8.kubernetes.api.model.ConfigMap;\nimport io.fabric8.kubernetes.api.model.Pod;\nimport io.fabric8.kubernetes.client.KubernetesClient;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\npublic class Planner {\n\n    private static final Logger log = LoggerFactory.getLogger(Planner.class);\n\n    private final KubernetesClient client;\n    private final AgentClientFactory agentClientFactory;\n\n    public Planner(KubernetesClient client, AgentClientFactory agentClientFactory) {\n        this.client = client;\n        this.agentClientFactory = agentClientFactory;\n    }\n\n    public List<Change> plan(AgentPoolInstance poolInstance) throws IOException {\n        String resourceName = poolInstance.getName();\n\n        List<Change> changes = new ArrayList<>();\n\n        // process pods marked for removal first\n        AgentPod.listMarkedForRemoval(client, resourceName)\n                .forEach(n -> changes.add(new TryToDeletePodChange(n.getMetadata().getName(), agentClientFactory.create(n))));\n\n        List<Pod> pods = AgentPod.list(client, resourceName);\n        int currentSize = pods.size();\n        int currentNotMarkedForDeleteSize = (int) pods.stream()\n                .filter(p -> !p.getMetadata().getLabels().containsKey(AgentPod.TAGGED_FOR_REMOVAL_LABEL))\n                .count();\n\n        // hash of the Agent Pod configuration, will be used to determine which resources should be updated\n        String newHash = HashUtils.hashAsHexString(poolInstance.getResource().getSpec().getPod());\n\n        // calculate the configmap changes\n\n        boolean recreateAllPods = false;\n\n        String configMapName = configMapName(resourceName);\n        ConfigMap m = AgentConfigMap.get(client, configMapName);\n\n        int targetSize = poolInstance.getTargetSize();\n\n        log.info(\"plan ['{}'] -> currentSize = {}, currentNotMarkedForDeleteSize = {}, targetSize= {}, configMap = {}\", resourceName, currentSize, currentNotMarkedForDeleteSize, targetSize, m != null);\n\n        AgentPoolInstance.Status poolStatus = poolInstance.getStatus();\n        if (poolStatus == AgentPoolInstance.Status.DELETED) {\n            targetSize = 0;\n        }\n\n        /*\n        Set the flag to recreate all pods to true, when there is a change to\n        Config Map in the AgentPool definition, or when a new ConfigMap is added.\n\n        Delete the Config Map if there are no pods present in the pool.\n         */\n\n        if (m == null) {\n            if (targetSize > 0) {\n                changes.add(new CreateConfigMapChange(poolInstance, configMapName));\n                recreateAllPods = true;\n            }\n        } else {\n            if (targetSize <= 0 && currentSize == 0) {\n                changes.add(new DeleteConfigMapChange(configMapName));\n            } else if (AgentConfigMap.hasChanges(client, poolInstance, m)) {\n                changes.add(new DeleteConfigMapChange(configMapName));\n                changes.add(new CreateConfigMapChange(poolInstance, configMapName));\n                recreateAllPods = true;\n            }\n        }\n\n        /*\n        check all pods for cfg changes.\n\n        Delete the pod only if there is a change in the Hash of Agent Pod,\n        that is change to Pod definition in the AgentPool resource definition.\n\n        Changes include change to the image, mem requirements, env variable changes,\n        docker changes, or anything that requires the Agent Pod to be restarted.\n\n        Do not delete and recreate all pods when there is a change to the\n        agentpool sizes - min, max and current size.\n         */\n\n        for (Pod p : pods) {\n            String currentHash = p.getMetadata().getLabels().get(AgentPod.CONFIG_HASH_LABEL);\n            if (!newHash.equals(currentHash)) {\n                changes.add(new TagForRemovalChange(p.getMetadata().getName(), agentClientFactory.create(p)));\n            }\n        }\n\n        // recreate all pods if the configmap changed\n\n        if (recreateAllPods) {\n            pods.forEach(p -> changes.add(new TagForRemovalChange(p.getMetadata().getName(), agentClientFactory.create(p))));\n        }\n\n        // create or remove pods according to the configured pool size\n        if (targetSize > currentSize) {\n            Set<String> podNames = pods.stream().map(p -> p.getMetadata().getName()).collect(Collectors.toSet());\n            for (int i = 0; i < targetSize - currentSize; i++) {\n                String podName = generatePodName(resourceName, podNames);\n                changes.add(new CreatePodChange(poolInstance, podName, configMapName(resourceName), newHash));\n                podNames.add(podName);\n            }\n        }\n\n        if (currentNotMarkedForDeleteSize > targetSize) {\n            int podsToDelete = currentNotMarkedForDeleteSize - targetSize;\n            for (Pod pod : pods) {\n                if (pod.getMetadata().getLabels().containsKey(AgentPod.TAGGED_FOR_REMOVAL_LABEL)) {\n                    continue;\n                }\n\n                String podName = pod.getMetadata().getName();\n                AgentClient agentClient = agentClientFactory.create(pod);\n                changes.add(new TagForRemovalChange(podName, agentClient));\n                changes.add(new TryToDeletePodChange(podName, agentClient));\n\n                podsToDelete--;\n                if (podsToDelete == 0) {\n                    break;\n                }\n            }\n        }\n\n        if (!changes.isEmpty()) {\n            log.info(\"plan ['{}'] -> changes: {}\", resourceName, changes);\n        }\n\n        return changes;\n    }\n\n    private static String generatePodName(String resourceName, Set<String> podNames) {\n        for (int i = 0; i < podNames.size() + 1; i++) {\n            String podName = podName(resourceName, i);\n\n            if (!podNames.contains(podName)) {\n                return podName;\n            }\n        }\n        throw new RuntimeException(\"Can't generate pod name for '\" + resourceName + \"', current pods: \" + podNames);\n    }\n\n    private static String configMapName(String resourceName) {\n        return resourceName + \"-cfg\";\n    }\n\n    private static String podName(String resourceName, int i) {\n        return String.format(\"%s-%05d\", resourceName, i);\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/planner/TagForRemovalChange.java",
    "content": "package com.walmartlabs.concord.agentoperator.planner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agentoperator.agent.AgentClient;\nimport com.walmartlabs.concord.agentoperator.PodLabels;\nimport com.walmartlabs.concord.agentoperator.resources.AgentPod;\nimport io.fabric8.kubernetes.client.KubernetesClient;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class TagForRemovalChange implements Change {\n\n    private static final Logger log = LoggerFactory.getLogger(TagForRemovalChange.class);\n\n    private final String podName;\n    private final AgentClient agentClient;\n\n    public TagForRemovalChange(String podName, AgentClient agentClient) {\n        this.podName = podName;\n        this.agentClient = agentClient;\n    }\n\n    @Override\n    public void apply(KubernetesClient client) {\n        try {\n            agentClient.enableMaintenanceMode();\n        } catch (Exception e) {\n            log.error(\"Error enabling maintenance mode for pod '{}'\", podName, e);\n            return;\n        }\n\n        PodLabels.applyTag(client, podName, AgentPod.TAGGED_FOR_REMOVAL_LABEL, \"true\");\n    }\n\n    @Override\n    public String toString() {\n        return \"TagForRemovalChange{\" +\n                \"podName='\" + podName + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/planner/TryToDeletePodChange.java",
    "content": "package com.walmartlabs.concord.agentoperator.planner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agentoperator.agent.AgentClient;\nimport com.walmartlabs.concord.agentoperator.PodLabels;\nimport com.walmartlabs.concord.agentoperator.resources.AgentPod;\nimport io.fabric8.kubernetes.api.model.Pod;\nimport io.fabric8.kubernetes.client.KubernetesClient;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Map;\n\npublic class TryToDeletePodChange implements Change {\n\n    private static final Logger log = LoggerFactory.getLogger(TryToDeletePodChange.class);\n\n    private final String podName;\n    private final AgentClient agentClient;\n\n    public TryToDeletePodChange(String podName, AgentClient agentClient) {\n        this.podName = podName;\n        this.agentClient = agentClient;\n    }\n\n    /**\n     * When the agent pod is being deleted, Kubernetes calls the prestop hook configured.\n     * The prestop hook script configured for agent container enables maintenance mode on the agent,\n     * and waits for the number of workers in use to go to 0, before the pod gets terminated.\n     * <p>\n     * Whenever the scheduler calls this `apply` method, the following conditions are checked, and\n     * corresponding actions are executed.\n     * <p>\n     * - If the pod has a label `preStopHookTermination: true`, do nothing and exit, as the pod is being\n     * terminated, waiting for the prestop hook script to complete (that is, last running process on the agent\n     * container to complete).\n     * <p>\n     * - Otherwise if the pod is in `RUNNING` phase, call the kubernetes client `delete` method on the agent pod,\n     * which will put the pod in `Terminating` state and start executing the prestop hook script on the\n     * agent container. Add the label `preStopHookTermination: true` (this will be checked on\n     * subsequent executions).\n     *\n     * @param client instance of Kubernetes client\n     */\n    @Override\n    public void apply(KubernetesClient client) {\n        Pod pod = client.pods().withName(podName).get();\n\n        if (pod == null) {\n            log.warn(\"apply ['{}'] -> pod doesn't exist, nothing to do\", podName);\n            return;\n        }\n\n        Map<String, String> labels = pod.getMetadata().getLabels();\n\n        if (\"true\".equals(labels.getOrDefault(AgentPod.PRE_STOP_HOOK_TERMINATION_LABEL, \"false\"))) {\n            log.debug(\"['{}'] -> has already been marked for termination\", podName);\n            return;\n        }\n\n        try {\n            if (agentClient.hasBusyWorkers()) {\n                return;\n            }\n        } catch (Exception e) {\n            log.error(\"Error while checking agent workers count for pod '{}'\", podName, e);\n            return;\n        }\n\n        // agent pod in maintenance mode and all workers done\n        client.pods().withName(podName).delete();\n        PodLabels.applyTag(client, podName, AgentPod.PRE_STOP_HOOK_TERMINATION_LABEL, \"true\");\n        log.info(\"apply ['{}'] -> Marked for termination (former phase: {})\", podName, pod.getStatus().getPhase());\n    }\n\n    @Override\n    public String toString() {\n        return \"TryToDeletePodChange{\" +\n                \"podName='\" + podName + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/processqueue/ProcessQueueClient.java",
    "content": "package com.walmartlabs.concord.agentoperator.processqueue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.google.common.annotations.VisibleForTesting;\nimport com.google.common.escape.Escaper;\nimport com.google.common.net.UrlEscapers;\nimport com.walmartlabs.concord.agentoperator.scheduler.QueueSelector;\nimport com.walmartlabs.concord.sdk.Constants;\n\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.TrustManager;\nimport javax.net.ssl.X509TrustManager;\nimport java.io.IOException;\nimport java.net.CookieManager;\nimport java.net.CookiePolicy;\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.security.KeyManagementException;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.SecureRandom;\nimport java.security.cert.X509Certificate;\nimport java.util.List;\n\npublic class ProcessQueueClient {\n\n    private static final TypeReference<List<ProcessQueueEntry>> LIST_OF_PROCESS_QUEUE_ENTRIES = new TypeReference<>() {\n    };\n\n    private final String baseUrl;\n    private final String apiToken;\n    private final ObjectMapper objectMapper;\n    private final HttpClient client;\n\n    public ProcessQueueClient(String baseUrl, String apiToken) {\n        this.baseUrl = baseUrl;\n        this.apiToken = apiToken;\n        this.objectMapper = new ObjectMapper();\n        this.client = initClient();\n    }\n\n    public List<ProcessQueueEntry> query(String processStatus, int limit, QueueSelector queueSelector) throws IOException {\n        StringBuilder queryUrl = new StringBuilder(baseUrl + \"/api/v2/process/requirements?status=\" + processStatus + \"&limit=\" + limit + \"&startAt.len=\");\n        String flavor = queueSelector.getFlavor();\n        if (flavor != null) {\n            queryUrl.append(\"&requirements.agent.flavor.eq=\").append(flavor);\n        }\n        List<String> queryParams = queueSelector.getQueryParams();\n        if (queryParams != null) {\n            for (String queryParam : queryParams) {\n                queryUrl.append(\"&\").append(escapeQueryParam(queryParam));\n            }\n        }\n\n        HttpRequest request = HttpRequest.newBuilder()\n                .uri(URI.create(queryUrl.toString()))\n                .header(\"Authorization\", apiToken)\n                .header(\"User-Agent\", \"k8s-agent-operator\")\n                .header(Constants.Headers.ENABLE_HTTP_SESSION, \"true\")\n                .GET()\n                .build();\n\n        HttpResponse<String> response;\n        try {\n            response = client.send(request, HttpResponse.BodyHandlers.ofString());\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n            throw new IOException(\"Interrupted while fetching the queue data\", e);\n        }\n\n        if (response.statusCode() != 200) {\n            throw new IOException(\"Error while fetching the process queue data: \" + response.statusCode());\n        }\n\n        return objectMapper.readValue(response.body(), LIST_OF_PROCESS_QUEUE_ENTRIES);\n    }\n\n    private static HttpClient initClient() {\n        try {\n            TrustManager[] trustAllCerts = new TrustManager[]{\n                    new X509TrustManager() {\n                        public X509Certificate[] getAcceptedIssuers() {\n                            return new X509Certificate[0];\n                        }\n\n                        public void checkClientTrusted(X509Certificate[] certs, String authType) {\n                        }\n\n                        public void checkServerTrusted(X509Certificate[] certs, String authType) {\n                        }\n                    }\n            };\n\n            SSLContext sslContext = SSLContext.getInstance(\"SSL\");\n            sslContext.init(null, trustAllCerts, new SecureRandom());\n\n            CookieManager cookieManager = new CookieManager();\n            cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);\n\n            return HttpClient.newBuilder()\n                    .sslContext(sslContext)\n                    .cookieHandler(cookieManager)\n                    .build();\n        } catch (NoSuchAlgorithmException | KeyManagementException e) {\n            throw new RuntimeException(\"Error while initializing the HTTP client\", e);\n        }\n    }\n\n    @VisibleForTesting\n    static String escapeQueryParam(String s) {\n        Escaper escaper = UrlEscapers.urlPathSegmentEscaper();\n        int i = s.indexOf(\"=\");\n        if (i < 0) {\n            return escaper.escape(s);\n        }\n        String key = s.substring(0, i);\n        String value = s.substring(i + 1);\n        return escaper.escape(key) + \"=\" + escaper.escape(value);\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/processqueue/ProcessQueueEntry.java",
    "content": "package com.walmartlabs.concord.agentoperator.processqueue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.io.Serializable;\nimport java.util.Map;\n\n@JsonIgnoreProperties(ignoreUnknown = true)\npublic class ProcessQueueEntry implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Map<String, Object> requirements;\n\n    @JsonCreator\n    public ProcessQueueEntry(@JsonProperty(\"requirements\") Map<String, Object> requirements) {\n        this.requirements = requirements;\n    }\n\n    public Map<String, Object> getRequirements() {\n        return requirements;\n    }\n\n    @Override\n    public String toString() {\n        return \"ProcessQueueEntry{\" +\n                \"requirements=\" + requirements +\n                '}';\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/resources/AgentConfigMap.java",
    "content": "package com.walmartlabs.concord.agentoperator.resources;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.agentoperator.HashUtils;\nimport com.walmartlabs.concord.agentoperator.crd.AgentPoolConfiguration;\nimport com.walmartlabs.concord.agentoperator.scheduler.AgentPoolInstance;\nimport io.fabric8.kubernetes.api.model.ConfigMap;\nimport io.fabric8.kubernetes.client.KubernetesClient;\nimport io.fabric8.kubernetes.client.KubernetesClientException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\n\npublic final class AgentConfigMap {\n\n    private static final Logger log = LoggerFactory.getLogger(AgentConfigMap.class);\n\n    private static final ObjectMapper objectMapper = new ObjectMapper();\n\n    public static ConfigMap get(KubernetesClient client, String configMapName) {\n        try {\n            return client.configMaps().withName(configMapName).get();\n        } catch (KubernetesClientException e) {\n            log.warn(\"get ['{}'] -> error while getting a configmap: {}\", configMapName, e.getMessage());\n            throw e;\n        }\n    }\n\n    public static void create(KubernetesClient client, AgentPoolInstance poolInstance, String configMapName) throws IOException {\n        try {\n            ConfigMap m = prepare(client, poolInstance, configMapName);\n            client.configMaps().resource(m).create();\n        } catch (KubernetesClientException e) {\n            log.warn(\"create ['{}', '{}'] -> error while creating a configmap: {}\", poolInstance.getName(), configMapName, e.getMessage());\n            throw e;\n        }\n    }\n\n    public static void delete(KubernetesClient client, String configMapName) {\n        try {\n            client.configMaps().withName(configMapName).delete();\n\n            // wait till it's actually removed\n            while (client.configMaps().withName(configMapName).get() != null) {\n                try {\n                    Thread.sleep(1000);\n                } catch (InterruptedException e) {\n                    Thread.currentThread().interrupt();\n                    return;\n                }\n            }\n        } catch (KubernetesClientException e) {\n            log.warn(\"delete ['{}'] -> error while deleting a configmap: {}\", configMapName, e.getMessage());\n            throw e;\n        }\n    }\n\n    public static boolean hasChanges(KubernetesClient client, AgentPoolInstance poolInstance, ConfigMap a) throws IOException {\n        ConfigMap b = prepare(client, poolInstance, a.getMetadata().getName());\n\n        String hashA = HashUtils.hashAsHexString(a.getData());\n        String hashB = HashUtils.hashAsHexString(b.getData());\n\n        return !hashA.equals(hashB);\n    }\n\n    private static ConfigMap prepare(KubernetesClient client, AgentPoolInstance poolInstance, String configMapName) throws IOException {\n        try {\n            AgentPoolConfiguration spec = poolInstance.getResource().getSpec();\n\n            String configMapYaml = objectMapper.writeValueAsString(spec.getConfigMap())\n                    .replaceAll(\"%%configMapName%%\", configMapName)\n                    .replace(\"%%preStopHook%%\", escape(Resources.get(\"/prestop-hook.sh\")));\n\n            return client.configMaps().load(new ByteArrayInputStream(configMapYaml.getBytes())).item();\n        } catch (KubernetesClientException e) {\n            log.warn(\"prepare ['{}', '{}'] -> error while preparing a configmap: {}\", poolInstance.getName(), configMapName, e.getMessage());\n            throw e;\n        }\n    }\n\n    private static String escape(String str) throws JsonProcessingException {\n        String result = objectMapper.writeValueAsString(str);\n        return result.substring(1, result.length() - 1);\n    }\n\n    private AgentConfigMap() {\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/resources/AgentPod.java",
    "content": "package com.walmartlabs.concord.agentoperator.resources;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.agentoperator.crd.AgentPool;\nimport com.walmartlabs.concord.agentoperator.crd.AgentPoolConfiguration;\nimport com.walmartlabs.concord.agentoperator.scheduler.AgentPoolInstance;\nimport io.fabric8.kubernetes.api.model.Pod;\nimport io.fabric8.kubernetes.client.KubernetesClient;\nimport io.fabric8.kubernetes.client.KubernetesClientException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.util.List;\n\npublic final class AgentPod {\n\n    private static final Logger log = LoggerFactory.getLogger(AgentPod.class);\n\n    public static final String TAGGED_FOR_REMOVAL_LABEL = \"concordTaggedForRemoval\";\n    public static final String PRE_STOP_HOOK_TERMINATION_LABEL = \"preStopHookTermination\";\n    public static final String POOL_NAME_LABEL = \"poolName\";\n    public static final String CONFIG_HASH_LABEL = \"concordCfgHash\";\n\n    private static final ObjectMapper objectMapper = new ObjectMapper();\n\n    public static List<Pod> listMarkedForRemoval(KubernetesClient client, String resourceName) {\n        try {\n            return client.pods()\n                    .withLabel(AgentPod.TAGGED_FOR_REMOVAL_LABEL)\n                    .withLabel(AgentPod.POOL_NAME_LABEL, resourceName)\n                    .list()\n                    .getItems();\n        } catch (KubernetesClientException e) {\n            log.warn(\"listMarkedForRemoval ['{}'] -> error while listing marked for removal pods: {}\", resourceName, e.getMessage());\n            throw e;\n        }\n    }\n\n    public static List<Pod> list(KubernetesClient client, String resourceName) {\n        try {\n            return client.pods()\n                    .withLabel(POOL_NAME_LABEL, resourceName)\n                    .list()\n                    .getItems();\n        } catch (KubernetesClientException e) {\n            log.warn(\"list ['{}'] -> error while listing pool pods: {}\", resourceName, e.getMessage());\n            throw e;\n        }\n    }\n\n    public static void create(KubernetesClient client,\n                              AgentPoolInstance poolInstance,\n                              String podName,\n                              String configMapName,\n                              String hash) throws IOException {\n\n        try {\n            AgentPoolConfiguration spec = poolInstance.getResource().getSpec();\n\n            String podYaml = objectMapper.writeValueAsString(spec.getPod())\n                    .replaceAll(\"%%podName%%\", podName)\n                    .replaceAll(\"%%app%%\", AgentPool.SERVICE_FULL_NAME)\n                    .replaceAll(\"%%\" + POOL_NAME_LABEL + \"%%\", poolInstance.getName())\n                    .replaceAll(\"%%configMapName%%\", configMapName)\n                    .replaceAll(\"%%\" + CONFIG_HASH_LABEL + \"%%\", hash);\n\n            client.pods().load(new ByteArrayInputStream(podYaml.getBytes())).create();\n        } catch (KubernetesClientException e) {\n            log.warn(\"create ['{}', '{}', '{}', '{}'] -> error while creating a pod: {}\", poolInstance.getName(), podName, configMapName, hash, e.getMessage());\n            throw e;\n        }\n    }\n\n    private AgentPod() {\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/resources/Resources.java",
    "content": "package com.walmartlabs.concord.agentoperator.resources;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.base.Charsets;\nimport com.google.common.io.CharStreams;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\npublic final class Resources {\n\n    private static final Map<String, String> resources = new ConcurrentHashMap<>();\n\n    public static String get(String name) {\n        return resources.computeIfAbsent(name, Resources::load);\n    }\n\n    private static String load(String name) {\n        try (InputStream in = Resources.class.getResourceAsStream(name)) {\n            if (in == null) {\n                throw new RuntimeException(\"Resource '\" + name + \"' not found\");\n            }\n            return CharStreams.toString(new InputStreamReader(in, Charsets.UTF_8));\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private Resources() {\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/scheduler/AgentPoolInstance.java",
    "content": "package com.walmartlabs.concord.agentoperator.scheduler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agentoperator.crd.AgentPool;\n\npublic class AgentPoolInstance {\n\n    public static AgentPoolInstance updateStatus(AgentPoolInstance i, Status status) {\n        return new AgentPoolInstance(i.name, i.resource, status, i.targetSize, System.currentTimeMillis(), i.getLastScaleUpTimestamp(), i.getLastScaleDownTimeStamp());\n    }\n\n    public static AgentPoolInstance updateTargetSize(AgentPoolInstance i, int targetSize, long scaleUptimeStamp, long scaleDownTimeStamp) {\n        return new AgentPoolInstance(i.name, i.resource, i.status, targetSize, System.currentTimeMillis(), scaleUptimeStamp, scaleDownTimeStamp);\n    }\n\n    private final String name;\n    private final AgentPool resource;\n    private final Status status;\n    private final int targetSize;\n    private final long lastUpdateTimestamp;\n    private final long lastScaleUpTimestamp;\n    private final long lastScaleDownTimeStamp;\n\n    public AgentPoolInstance(String name, AgentPool resource, Status status, int targetSize, long lastUpdateTimestamp,\n                             long lastScaleUpTimestamp, long lastScaleDownTimeStamp) {\n        this.name = name;\n        this.resource = resource;\n        this.status = status;\n        this.targetSize = targetSize;\n        this.lastUpdateTimestamp = lastUpdateTimestamp;\n        this.lastScaleUpTimestamp = lastScaleUpTimestamp;\n        this.lastScaleDownTimeStamp = lastScaleDownTimeStamp;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public AgentPool getResource() {\n        return resource;\n    }\n\n    public Status getStatus() {\n        return status;\n    }\n\n    public int getTargetSize() {\n        return targetSize;\n    }\n\n    public long getLastUpdateTimestamp() {\n        return lastUpdateTimestamp;\n    }\n\n    public long getLastScaleUpTimestamp() {\n        return lastScaleUpTimestamp;\n    }\n\n    public long getLastScaleDownTimeStamp() {\n        return lastScaleDownTimeStamp;\n    }\n\n    public enum Status {\n        ACTIVE,\n        DELETED\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/scheduler/AutoScaler.java",
    "content": "package com.walmartlabs.concord.agentoperator.scheduler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\n\npublic interface AutoScaler {\n\n    AgentPoolInstance apply(AgentPoolInstance i) throws IOException;\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/scheduler/AutoScalerFactory.java",
    "content": "package com.walmartlabs.concord.agentoperator.scheduler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agentoperator.crd.AgentPoolConfiguration;\nimport com.walmartlabs.concord.agentoperator.processqueue.ProcessQueueClient;\nimport com.walmartlabs.concord.agentoperator.resources.AgentPod;\nimport io.fabric8.kubernetes.client.KubernetesClient;\n\nimport java.util.function.Function;\n\npublic class AutoScalerFactory {\n\n    private final Function<String, Integer> podCounter;\n    private final ProcessQueueClient processQueueClient;\n\n    public AutoScalerFactory(String concordBaseUrl, String concordApiToken, KubernetesClient k8sClient) {\n        this.podCounter = n -> AgentPod.list(k8sClient, n).size();\n        this.processQueueClient = new ProcessQueueClient(concordBaseUrl, concordApiToken);\n    }\n\n    public AutoScaler create(AgentPoolInstance poolInstance) {\n        AgentPoolConfiguration cfg = poolInstance.getResource().getSpec();\n        if (LinearAutoScaler.NAME.equals(cfg.getAutoScaleStrategy())) {\n            return new LinearAutoScaler(processQueueClient, podCounter);\n        }\n\n        return new DefaultAutoScaler(processQueueClient, podCounter);\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/scheduler/DefaultAutoScaler.java",
    "content": "package com.walmartlabs.concord.agentoperator.scheduler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agentoperator.crd.AgentPoolConfiguration;\nimport com.walmartlabs.concord.agentoperator.processqueue.ProcessQueueClient;\nimport com.walmartlabs.concord.agentoperator.processqueue.ProcessQueueEntry;\nimport com.walmartlabs.concord.common.Matcher;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Function;\n\npublic class DefaultAutoScaler implements AutoScaler {\n\n    public static final String NAME = \"default\";\n\n    private static final Logger log = LoggerFactory.getLogger(DefaultAutoScaler.class);\n\n    private final ProcessQueueClient processQueueClient;\n    private final Function<String, Integer> podCounter;\n    private final Function<AgentPoolInstance, Boolean> canBeScaledUp;\n    private final Function<AgentPoolInstance, Boolean> canBeScaledDown;\n    private long scaleUpTimeStamp;\n    private long scaleDownTimeStamp;\n\n    public DefaultAutoScaler(ProcessQueueClient processQueueClient, Function<String, Integer> podCounter) {\n        this(processQueueClient, podCounter, i -> {\n            long t = System.currentTimeMillis();\n            return t - i.getLastScaleUpTimestamp() > i.getResource().getSpec().getScaleUpDelayMs();\n        }, i -> {\n            long t = System.currentTimeMillis();\n            return t - i.getLastScaleDownTimeStamp() > i.getResource().getSpec().getScaleDownDelayMs();\n        });\n    }\n\n    public DefaultAutoScaler(ProcessQueueClient processQueueClient,\n                             Function<String, Integer> podCounter, Function<AgentPoolInstance, Boolean> canBeScaledUp,\n                             Function<AgentPoolInstance, Boolean> canBeScaledDown) {\n        this.processQueueClient = processQueueClient;\n        this.podCounter = podCounter;\n        this.canBeScaledUp = canBeScaledUp;\n        this.canBeScaledDown = canBeScaledDown;\n        this.scaleUpTimeStamp = System.currentTimeMillis();\n        this.scaleDownTimeStamp = System.currentTimeMillis();\n    }\n\n    /**\n     * Scale up or Scale down the number of agent pods depending on various conditions\n     * <p>\n     * If enqueued process count is greater than the threshold defined for incrementing to max\n     * pool size, increase the pool size to the maximum size.\n     * Otherwise, if the enqueued process count is greater than the threshold defined for incrementing\n     * the pool size, increase the pool size by the increment percentage defined\n     * <p>\n     * Decrease the pool size by the decrement percentage defined only if,\n     * - enqueued process count is lesser than the minimum pool size threshold defined\n     * - running process count is lesser than the threshold defined (which depends on current size of the pool\n     * running the processes, and a constant factor specified - default to 1. Simplified to\n     * runningCount < podsCount)\n     *\n     * @param i            Agent pool on which the scaling activity is to be performed\n     */\n    public AgentPoolInstance apply(AgentPoolInstance i) throws IOException {\n        int queueQueryLimit = i.getResource().getSpec().getQueueQueryLimit();\n        QueueSelector queueSelector = QueueSelector.parse(i.getResource().getSpec().getQueueSelector());\n        List<ProcessQueueEntry> queueEntries = processQueueClient.query(\"ENQUEUED\", queueQueryLimit, queueSelector);\n\n        scaleUpTimeStamp = i.getLastScaleUpTimestamp();\n        scaleDownTimeStamp = i.getLastScaleDownTimeStamp();\n\n        AgentPoolConfiguration cfg = i.getResource().getSpec();\n\n        boolean canBeScaled = canBeScaledUp.apply(i) || canBeScaledDown.apply(i);\n        if (!canBeScaled) {\n            // was updated recently, skipping\n            return i;\n        }\n\n        // count the currently running pods\n        int podsCount = podCounter.apply(i.getName());\n        log.info(\"['{}']: Current pool size: {}\", i.getName(), podsCount);\n\n        // the number of processes waiting for an agent in the current pool\n        int enqueuedCount = getProcessCount(cfg, queueEntries);\n        log.info(\"['{}']: Enqueued process count: {}\", i.getName(), enqueuedCount);\n\n        if (podsCount < cfg.getMinSize()) {\n            return AgentPoolInstance.updateTargetSize(i, cfg.getMinSize(), System.currentTimeMillis(), System.currentTimeMillis());\n        }\n\n        // The threshold above which the operator can scale up the agent pods to the defined maximum pool size\n        double maxPoolSizeThreshold = cfg.getMaxSize() * cfg.getIncrementThresholdFactor();\n\n        // The threshold above which the pool size can be increased by the increment percentage defined\n        double incrementThreshold = cfg.getIncrementThresholdFactor() * podsCount;\n\n        // The threshold, combined with threshold for running processes determine if the pool size can be\n        // reduced by the decrement percentage defined\n        double minPoolSizeThreshold = cfg.getDecrementThresholdFactor() * cfg.getMinSize();\n\n        // Initial target size of the agent pool before updation\n        int targetSize = i.getTargetSize();\n\n        // Try scaling up if the time elapsed after last scale up operation\n        // is greater than the scale up delay defined (default: 15s)\n        if (canBeScaledUp.apply(i)) {\n            targetSize = tryScaleUp(cfg, i, podsCount, enqueuedCount, targetSize, maxPoolSizeThreshold, incrementThreshold);\n\n            // Reset scaledown delay counter if enqueued count is greater than min threshold.\n            // Scale down should happen only if enqueued count is less than\n            // min threshold consistently for scaledown delay defined (default: 180s)\n            if (enqueuedCount >= minPoolSizeThreshold) {\n                log.info(\"['{}']: Resetting scale down delay counter - (enqueued count({}) >= minimum threshold({}))...\",\n                        i.getName(), enqueuedCount, minPoolSizeThreshold);\n                scaleDownTimeStamp = System.currentTimeMillis();\n            }\n        }\n\n        // Try scaling down if the time elapsed after last scale down operation\n        // is greater than the scale down delay defined (default: 180s)\n        if (canBeScaledDown.apply(i)) {\n            targetSize = tryScaleDown(cfg, i, podsCount, enqueuedCount, targetSize, minPoolSizeThreshold);\n        }\n\n        if (targetSize == i.getTargetSize()) {\n            log.info(\"['{}']: Not changing the pool size.\", i.getName());\n        } else {\n            log.info(\"apply ['{}'] -> updated to {}\", i.getName(), targetSize);\n        }\n        return AgentPoolInstance.updateTargetSize(i, targetSize, scaleUpTimeStamp, scaleDownTimeStamp);\n    }\n\n    private int tryScaleUp(AgentPoolConfiguration cfg, AgentPoolInstance i, int podsCount, int enqueuedCount, int poolSize,\n                           double maxPoolSizeThreshold, double incrementThreshold) {\n\n        // To prevent scale up before previous scale down action is completed\n        podsCount = Math.min(podsCount, i.getTargetSize());\n\n        // Reset scaleup delay counter for every attempt to scale up\n        scaleUpTimeStamp = System.currentTimeMillis();\n\n        if (podsCount < cfg.getMaxSize()) {\n            if (enqueuedCount >= maxPoolSizeThreshold) {\n                poolSize = cfg.getMaxSize();\n                log.info(\"['{}']: Incrementing to max size - {}\", i.getName(), poolSize);\n            } else if (enqueuedCount >= incrementThreshold) {\n                poolSize = (int) Math.round(podsCount * (1 + cfg.getPercentIncrement() / 100));\n\n                // Limit to maximum pool size if the computed target size is more than the max size\n                if (poolSize > cfg.getMaxSize()) {\n                    log.warn(\"['{}']: Target pool size exceeds the allowed maximum: {} > {}. Updating to maximum size - {}\",\n                            i.getName(), poolSize, cfg.getMaxSize(), cfg.getMaxSize());\n                    poolSize = cfg.getMaxSize();\n                }\n\n                log.info(\"['{}']: Scaling up to {}...\", i.getName(), poolSize);\n            }\n        } else {\n            log.warn(\"['{}']: Target pool size already the allowed maximum size: {}. Not updating.\",\n                    i.getName(), cfg.getMaxSize());\n        }\n\n        return poolSize;\n    }\n\n    private int tryScaleDown(AgentPoolConfiguration cfg, AgentPoolInstance i, int podsCount, int enqueuedCount,\n                             int poolSize, double minPoolSizeThreshold) {\n\n        // To prevent scale down before previous scale up action is completed\n        podsCount = Math.max(podsCount, i.getTargetSize());\n\n        // Reset scaledown delay counter for every attempt to scale down\n        scaleDownTimeStamp = System.currentTimeMillis();\n\n        if (podsCount > cfg.getMinSize()) {\n            if (enqueuedCount < minPoolSizeThreshold) {\n                poolSize = (int) Math.floor(podsCount * (1 - cfg.getPercentDecrement() / 100));\n\n                log.info(\"['{}']: Scaling down - (enqueued count({}) < minimum threshold({})) for more than {} seconds...\",\n                        i.getName(), enqueuedCount, minPoolSizeThreshold,\n                        (i.getResource().getSpec().getScaleDownDelayMs() / 1000));\n\n                // Limit to minimum pool size if the computed target size is less than the min size\n                if (poolSize < cfg.getMinSize()) {\n                    log.warn(\"['{}']: Target pool size lesser than the allowed minimum: {} < {}. Updating to minimum size - {}\",\n                            i.getName(), poolSize, cfg.getMinSize(), cfg.getMinSize());\n                    poolSize = cfg.getMinSize();\n                } else {\n                    log.info(\"['{}']: Scaling down to {}...\", i.getName(), poolSize);\n                }\n            }\n        } else {\n            log.warn(\"['{}']: Target pool size already the allowed minimum size: {}. Not updating.\",\n                    i.getName(), cfg.getMinSize());\n        }\n\n        return poolSize;\n    }\n\n    private static int getProcessCount(AgentPoolConfiguration cfg, List<ProcessQueueEntry> processQueueEntries) {\n        return (int) processQueueEntries.stream()\n                .map(ProcessQueueEntry::getRequirements)\n                .filter(a -> isEmpty(cfg.getQueueSelector()) || Matcher.matches(a, cfg.getQueueSelector()))\n                .count();\n    }\n\n    private static boolean isEmpty(Map<String, Object> m) {\n        return m == null || m.isEmpty();\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/scheduler/Event.java",
    "content": "package com.walmartlabs.concord.agentoperator.scheduler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agentoperator.crd.AgentPool;\n\npublic class Event {\n\n    private final Type type;\n    private final AgentPool resource;\n\n    public Event(Type type, AgentPool resource) {\n        this.type = type;\n        this.resource = resource;\n    }\n\n    public Type getType() {\n        return type;\n    }\n\n    public AgentPool getResource() {\n        return resource;\n    }\n\n    public enum Type {\n\n        MODIFIED,\n        DELETED\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/scheduler/LinearAutoScaler.java",
    "content": "package com.walmartlabs.concord.agentoperator.scheduler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agentoperator.crd.AgentPoolConfiguration;\nimport com.walmartlabs.concord.agentoperator.processqueue.ProcessQueueClient;\nimport com.walmartlabs.concord.agentoperator.processqueue.ProcessQueueEntry;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.function.Function;\n\npublic class LinearAutoScaler implements AutoScaler {\n\n    private static final Logger log = LoggerFactory.getLogger(LinearAutoScaler.class);\n\n    public static final String NAME = \"linear\";\n\n    private final ProcessQueueClient processQueueClient;\n    private final Function<String, Integer> podCounter;\n    private final Function<AgentPoolInstance, Boolean> canBeScaledUp;\n    private final Function<AgentPoolInstance, Boolean> canBeScaledDown;\n\n    public LinearAutoScaler(ProcessQueueClient processQueueClient, Function<String, Integer> podCounter) {\n        this(processQueueClient, podCounter, i -> {\n            long t = System.currentTimeMillis();\n            return t - i.getLastScaleUpTimestamp() > i.getResource().getSpec().getScaleUpDelayMs();\n        }, i -> {\n            long t = System.currentTimeMillis();\n            return t - i.getLastScaleDownTimeStamp() > i.getResource().getSpec().getScaleDownDelayMs();\n        });\n    }\n\n    public LinearAutoScaler(ProcessQueueClient processQueueClient,\n                            Function<String, Integer> podCounter, Function<AgentPoolInstance, Boolean> canBeScaledUp,\n                            Function<AgentPoolInstance, Boolean> canBeScaledDown) {\n        this.processQueueClient = processQueueClient;\n        this.podCounter = podCounter;\n        this.canBeScaledUp = canBeScaledUp;\n        this.canBeScaledDown = canBeScaledDown;\n    }\n\n    @Override\n    public AgentPoolInstance apply(AgentPoolInstance i) throws IOException {\n        if (!canBeScaledUp.apply(i) && !canBeScaledDown.apply(i)) {\n            log.info(\"apply [{}] -> not a time. up: {}, down: {}, delay up: {}, delay down: {}\", i.getName(), (System.currentTimeMillis() - i.getLastScaleUpTimestamp()), (System.currentTimeMillis() - i.getLastScaleDownTimeStamp()), i.getResource().getSpec().getScaleUpDelayMs(), i.getResource().getSpec().getScaleDownDelayMs());\n            // was updated recently, skipping\n            return i;\n        }\n\n        long scaleUpTimeStamp = i.getLastScaleUpTimestamp();\n        long scaleDownTimeStamp = i.getLastScaleDownTimeStamp();\n\n        AgentPoolConfiguration cfg = i.getResource().getSpec();\n\n        QueueSelector queueSelector = QueueSelector.parse(cfg.getQueueSelector());\n        List<ProcessQueueEntry> queueEntries = processQueueClient.query(\"ENQUEUED\", cfg.getMaxSize(), queueSelector);\n\n        // count the currently running pods\n        int podsCount = podCounter.apply(i.getName());\n\n        // the number of processes waiting for an agent in the current pool\n        int enqueuedCount = queueEntries.size();\n        int runningCount = processQueueClient.query(\"RUNNING\", cfg.getMaxSize(), queueSelector).size();\n        int freePodsCount = Math.max(podsCount - runningCount, 0);\n\n        int increment = 0;\n        if (enqueuedCount > freePodsCount) {\n            increment = cfg.getSizeIncrement();\n            scaleUpTimeStamp = System.currentTimeMillis();\n            scaleDownTimeStamp = System.currentTimeMillis();\n        } else if (enqueuedCount < freePodsCount) {\n            increment = -cfg.getSizeIncrement();\n            scaleDownTimeStamp = System.currentTimeMillis();\n        }\n\n        int targetSize = Math.max(cfg.getMinSize(), podsCount + increment);\n        if (i.getTargetSize() == targetSize) {\n            log.info(\"apply ['{}'] -> targetSize = {}, enqueuedCount = {}, increment = {}, podsCount = {}\", i.getName(), targetSize, enqueuedCount, increment, podsCount);\n            // no changes needed\n            return i;\n        }\n\n        if (increment > 0 && !canBeScaledUp.apply(i)) {\n            log.info(\"apply ['{}'] -> not a time to scale up to {}\", i.getName(), targetSize);\n            return i;\n        }\n        if (increment < 0 && !canBeScaledDown.apply(i)) {\n            log.info(\"apply ['{}'] -> not a time to scale down to {}\", i.getName(), targetSize);\n            return i;\n        }\n\n        if (targetSize > cfg.getMaxSize()) {\n            log.warn(\"apply ['{}'] -> target pool size exceeds the allowed maximum: {} > {}\", i.getName(), enqueuedCount, cfg.getMaxSize());\n        }\n\n        targetSize = Math.min(targetSize, cfg.getMaxSize());\n        log.info(\"apply ['{}'] -> updated to {}, pods: {}, free: {}, enqueued: {}, running: {}\", i.getName(), targetSize, podsCount, freePodsCount, enqueuedCount, runningCount);\n        return AgentPoolInstance.updateTargetSize(i, targetSize, scaleUpTimeStamp, scaleDownTimeStamp);\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/scheduler/QueueSelector.java",
    "content": "package com.walmartlabs.concord.agentoperator.scheduler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\n\nimport java.util.List;\nimport java.util.Map;\n\npublic class QueueSelector {\n\n    public static QueueSelector parse(Map<String, Object> queueSelector) {\n        String flavor;\n        Object maybeFlavor = ConfigurationUtils.get(queueSelector, \"agent\", \"flavor\");\n        if (maybeFlavor != null && !(maybeFlavor instanceof String)) {\n            throw new IllegalArgumentException(\"Expected a string value as 'agent.flavor', got: \" + maybeFlavor);\n        }\n        flavor = (String) maybeFlavor;\n\n        List<String> queryParams;\n        Object maybeQueryParams = ConfigurationUtils.get(queueSelector, \"queryParams\");\n        if (maybeQueryParams != null) {\n            if (!(maybeQueryParams instanceof List)) {\n                throw new IllegalArgumentException(\"Expected a list value as 'queryParams', got: \" + maybeQueryParams);\n            }\n\n            ((List<?>) maybeQueryParams).forEach(qp -> {\n                if (!(qp instanceof String)) {\n                    throw new IllegalArgumentException(\"Expected a string value as 'queryParams' item, got: \" + qp);\n                }\n            });\n        }\n        //noinspection unchecked\n        queryParams = (List<String>) maybeQueryParams;\n        return new QueueSelector(flavor, queryParams);\n    }\n\n    private final String flavor;\n    private final List<String> queryParams;\n\n    private QueueSelector(String flavor, List<String> queryParams) {\n        this.flavor = flavor;\n        this.queryParams = queryParams;\n    }\n\n    /**\n     * \"Flavor\" of the current agent. Translates to the \"requirements.agent.flavor.eq\"\n     * query parameter when fetching the process queue.\n     */\n    public String getFlavor() {\n        return flavor;\n    }\n\n    /**\n     * Additional query parameters to be used when fetching the process queue. Appended\n     * as-is to the query URL.\n     */\n    public List<String> getQueryParams() {\n        return queryParams;\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/scheduler/Scheduler.java",
    "content": "package com.walmartlabs.concord.agentoperator.scheduler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agentoperator.agent.AgentClientFactory;\nimport com.walmartlabs.concord.agentoperator.crd.AgentPool;\nimport com.walmartlabs.concord.agentoperator.planner.Change;\nimport com.walmartlabs.concord.agentoperator.planner.Planner;\nimport com.walmartlabs.concord.agentoperator.resources.AgentPod;\nimport io.fabric8.kubernetes.api.model.Pod;\nimport io.fabric8.kubernetes.client.KubernetesClient;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.util.*;\n\npublic class Scheduler {\n\n    private static final Logger log = LoggerFactory.getLogger(Scheduler.class);\n\n    private static final long POLL_DELAY = 10000;\n    private static final long ERROR_DELAY = 30000;\n\n    private final AutoScalerFactory autoScalerFactory;\n    private final KubernetesClient k8sClient;\n    private final Planner planner;\n    private final Map<String, AgentPoolInstance> pools;\n    private final List<Event> events;\n\n    public Scheduler(AutoScalerFactory autoScalerFactory, KubernetesClient k8sClient, boolean useMaintenanceMode) {\n        this.autoScalerFactory = autoScalerFactory;\n        this.k8sClient = k8sClient;\n        this.planner = new Planner(k8sClient, new AgentClientFactory(useMaintenanceMode));\n        this.pools = new HashMap<>();\n        this.events = new LinkedList<>();\n    }\n\n    public void onEvent(Event.Type type, AgentPool resource) {\n        log.info(\"onEvent -> handling {} for {}/{}\", type, resource.getMetadata().getNamespace(), resource.getMetadata().getName());\n        synchronized (events) {\n            events.add(new Event(type, resource));\n        }\n    }\n\n    public void start() {\n        new Thread(new Worker(), \"scheduler-worker\").start();\n    }\n\n    /**\n     * Process the recent events and update the cluster state.\n     */\n    private void doRun() {\n        // drain the event queue\n        List<Event> evs;\n        synchronized (events) {\n            evs = new ArrayList<>(events);\n            events.clear();\n        }\n\n        for (Event e : evs) {\n            AgentPool resource = e.getResource();\n            String resourceName = resource.getMetadata().getName();\n\n            switch (e.getType()) {\n                case MODIFIED: {\n                    onAdd(resourceName, resource);\n                    break;\n                }\n                case DELETED: {\n                    onDelete(resourceName);\n                    break;\n                }\n                default:\n                    throw new IllegalArgumentException(\"Unknown event type: \" + e.getType());\n            }\n        }\n\n        // process the pool\n        List<AgentPoolInstance> todo;\n        synchronized (pools) {\n            todo = new ArrayList<>(pools.values());\n        }\n\n        if (todo.isEmpty()) {\n            return;\n        }\n\n        // fetch the process queue status\n\n        todo.parallelStream().forEach(i -> {\n            try {\n                switch (i.getStatus()) {\n                    case ACTIVE: {\n                        AgentPoolInstance updated = updateTargetSize(i);\n                        processActive(updated);\n                        break;\n                    }\n                    case DELETED: {\n                        processDeleted(i);\n                        break;\n                    }\n                    default:\n                        throw new IllegalArgumentException(\"Unknown pool status: \" + i.getStatus());\n                }\n            } catch (IOException e) {\n                log.error(\"doRun -> error while processing a registered pool {} ({}): {}\", i.getName(), i.getStatus(), e.getMessage());\n            }\n        });\n    }\n\n    private void onAdd(String resourceName, AgentPool resource) {\n        int targetSize = resource.getSpec().getSize();\n        synchronized (pools) {\n            long currentTimeStamp = System.currentTimeMillis();\n            pools.put(resourceName, new AgentPoolInstance(resourceName, resource, AgentPoolInstance.Status.ACTIVE,\n                    targetSize, currentTimeStamp, currentTimeStamp, currentTimeStamp));\n        }\n    }\n\n    private void onDelete(String resourceName) {\n        synchronized (pools) {\n            AgentPoolInstance i = pools.get(resourceName);\n            if (i == null) {\n                return;\n            }\n\n            pools.put(resourceName, AgentPoolInstance.updateStatus(i, AgentPoolInstance.Status.DELETED));\n        }\n    }\n\n    private AgentPoolInstance updateTargetSize(AgentPoolInstance i) throws IOException {\n        if (!i.getResource().getSpec().isAutoScale()) {\n            return i;\n        }\n\n        AgentPoolInstance result = autoScalerFactory.create(i).apply(i);\n\n        synchronized (pools) {\n            pools.put(i.getName(), result);\n        }\n\n        return result;\n    }\n\n    private void processActive(AgentPoolInstance i) throws IOException {\n        log.info(\"processActive ['{}']\", i.getName());\n        List<Change> changes = planner.plan(i);\n        apply(changes);\n    }\n\n    private void processDeleted(AgentPoolInstance i) throws IOException {\n        log.info(\"processDeleted ['{}']\", i.getName());\n        String resourceName = i.getName();\n\n        // remove all pool's pods\n        List<Change> changes = planner.plan(i);\n        apply(changes);\n\n        // if no pods left - remove the pool\n        List<Pod> pods = AgentPod.list(k8sClient, resourceName);\n        if (pods.isEmpty()) {\n            synchronized (pools) {\n                pools.remove(resourceName);\n                log.info(\"processDeleted ['{}'] -> no pods left, the pool was removed\", resourceName);\n            }\n        } else {\n            log.info(\"processDeleted ['{}'] -> {} pod(s) left, will be deleted on the next iteration\", resourceName, pods.size());\n        }\n    }\n\n    private void apply(List<Change> changes) {\n        changes.forEach(c -> c.apply(k8sClient));\n    }\n\n    private static void sleep(long ms) {\n        try {\n            Thread.sleep(ms);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    private class Worker implements Runnable {\n\n        @Override\n        public void run() {\n            while (!Thread.currentThread().isInterrupted()) {\n                try {\n                    doRun();\n                    sleep(POLL_DELAY);\n                } catch (Exception e) {\n                    log.error(\"run -> error while running the scheduler: {}\", e.getMessage(), e);\n                    sleep(ERROR_DELAY);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/main/resources/logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] [%-5level] %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "agent-operator/src/main/resources/prestop-hook.sh",
    "content": "#!/usr/bin/env bash\n\necho \"PreStop hook started at $(date)\"\n\nMAX_RETRIES=5\nRETRY_DELAY=1\n\ncurrent_workers=\"1\"\nnum_retries=0\n\nwhile [ \"$current_workers\" != \"0\" ] && [ \"$num_retries\" -lt \"$MAX_RETRIES\" ]\ndo\n  echo \"[$HOSTNAME]: Agent is still executing a process.. enabling maintenance mode and checking the number of current_workers\"\n\n  response=$(exec 3<>/dev/tcp/127.0.0.1/8010;\n             echo -e \"POST /maintenance-mode HTTP/1.1\\r\\nHost: 127.0.0.1:8010\\r\\nContent-Length: 0\\r\\nConnection: close\\r\\n\\r\\n\" >&3;\n             cat <&3;\n             exec 3>&-)\n\n  mmode_response=$(echo \"$response\" | sed -n '/^\\r*$/,$p' | tail -n +2)\n  mmode_enabled=$(echo \"$mmode_response\" | sed -n 's/^.*\\\"maintenanceMode\\\":\\([a-z]*\\).*$/\\1/p')\n\n  if [ \"$mmode_enabled\" == \"true\" ]; then\n    echo \"[$HOSTNAME]: Maintenance mode enabled: $mmode_enabled\"\n    current_workers=$(echo \"$mmode_response\" | sed -n 's/^.*\\\"workersAlive\\\":\\([0-9]*\\).*$/\\1/p')\n    echo \"[$HOSTNAME]: Number of current_workers: $current_workers\"\n  else\n    echo \"[$HOSTNAME]: trouble enabling maintenance mode\"\n    num_retries=$((\"$num_retries\" + 1))\n  fi\n\n  sleep ${RETRY_DELAY}\ndone\n\nif [ \"$num_retries\" -ge \"$MAX_RETRIES\" ]; then\n  echo \"[$HOSTNAME]: Number of retries to enable exceeded $MAX_RETRIES times. Exiting ...\"\n  exit 1\nfi\n\necho \"[$HOSTNAME]: There are no processes running on this agent. Terminating...\"\n"
  },
  {
    "path": "agent-operator/src/test/java/com/walmartlabs/concord/agentoperator/processqueue/ProcessQueueClientTest.java",
    "content": "package com.walmartlabs.concord.agentoperator.processqueue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport static com.walmartlabs.concord.agentoperator.processqueue.ProcessQueueClient.escapeQueryParam;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class ProcessQueueClientTest {\n\n    @Test\n    public void testEscapeQueryParam() {\n        assertEquals(\"foo%20bar\", escapeQueryParam(\"foo bar\"));\n        assertEquals(\"foo=bar\", escapeQueryParam(\"foo=bar\"));\n        assertEquals(\"foo=bar%20baz\", escapeQueryParam(\"foo=bar baz\"));\n        assertEquals(\"foo.bar.baz=.*()%23%2F%2F&++$\", escapeQueryParam(\"foo.bar.baz=.*()#//&++$\"));\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/test/java/com/walmartlabs/concord/agentoperator/scheduler/DefaultAutoScalerTest.java",
    "content": "package com.walmartlabs.concord.agentoperator.scheduler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agentoperator.crd.AgentPool;\nimport com.walmartlabs.concord.agentoperator.crd.AgentPoolConfiguration;\nimport com.walmartlabs.concord.agentoperator.processqueue.ProcessQueueClient;\nimport com.walmartlabs.concord.agentoperator.processqueue.ProcessQueueEntry;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicInteger;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class DefaultAutoScalerTest {\n\n    @Test\n    public void testStill() throws Exception {\n        AtomicInteger podCount = new AtomicInteger(1);\n        List<ProcessQueueEntry> queue = new ArrayList<>();\n\n        DefaultAutoScaler as = new DefaultAutoScaler(mockProcessQueueClient(queue), n -> podCount.get(), i -> true, i -> true);\n\n        AgentPoolConfiguration spec = new AgentPoolConfiguration();\n        spec.setPercentIncrement(50);\n        spec.setDecrementThresholdFactor(1.0);\n        spec.setIncrementThresholdFactor(1.5);\n        spec.setPercentDecrement(10);\n        spec.setQueueSelector(Collections.singletonMap(\"test\", 123));\n\n        AgentPool resource = new AgentPool();\n        resource.setSpec(spec);\n\n        AgentPoolInstance pool = new AgentPoolInstance(\"test\", resource, AgentPoolInstance.Status.ACTIVE, 1, 0, 0, 0);\n\n        // ---\n\n        pool = as.apply(pool);\n        assertEquals(1, pool.getTargetSize());\n\n        pool = as.apply(pool);\n        assertEquals(1, pool.getTargetSize());\n    }\n\n    @Test\n    public void testZeroStart() throws IOException {\n        AtomicInteger podCount = new AtomicInteger(0);\n        List<ProcessQueueEntry> queue = new ArrayList<>();\n\n        DefaultAutoScaler as = new DefaultAutoScaler(mockProcessQueueClient(queue), n -> podCount.get(), i -> true, i -> true);\n\n        AgentPoolConfiguration spec = new AgentPoolConfiguration();\n        spec.setPercentIncrement(50);\n        spec.setDecrementThresholdFactor(1.0);\n        spec.setIncrementThresholdFactor(1.5);\n        spec.setPercentDecrement(10);\n        spec.setQueueSelector(Collections.singletonMap(\"test\", 123));\n\n        AgentPool resource = new AgentPool();\n        resource.setSpec(spec);\n\n        AgentPoolInstance pool = new AgentPoolInstance(\"test\", resource, AgentPoolInstance.Status.ACTIVE, 1, 0, 0, 0);\n\n        // ---\n\n        pool = as.apply(pool);\n        assertEquals(1, pool.getTargetSize());\n\n        podCount.set(2);\n\n        pool = as.apply(pool);\n        assertEquals(1, pool.getTargetSize());\n    }\n\n    private ProcessQueueClient mockProcessQueueClient(List<ProcessQueueEntry> queue) {\n        return new ProcessQueueClient(\"test\", \"test\") {\n            @Override\n            public List<ProcessQueueEntry> query(String processStatus, int limit, QueueSelector queueSelector) throws IOException {\n                return queue;\n            }\n        };\n    }\n\n    @Test\n    public void testQueue() throws IOException {\n        AtomicInteger podCount = new AtomicInteger(1);\n        List<ProcessQueueEntry> queue = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            queue.add(new ProcessQueueEntry(Collections.singletonMap(\"test\", 123)));\n        }\n\n        DefaultAutoScaler as = new DefaultAutoScaler(mockProcessQueueClient(queue), n -> podCount.get(), i -> true, i -> true);\n\n        AgentPoolConfiguration spec = new AgentPoolConfiguration();\n        spec.setPercentIncrement(50);\n        spec.setDecrementThresholdFactor(1.0);\n        spec.setIncrementThresholdFactor(1.5);\n        spec.setPercentDecrement(10);\n        spec.setQueueSelector(Collections.singletonMap(\"test\", 123));\n\n        AgentPool resource = new AgentPool();\n        resource.setSpec(spec);\n\n        AgentPoolInstance pool = new AgentPoolInstance(\"test\", resource, AgentPoolInstance.Status.ACTIVE, 1, 0, 0, 0);\n\n        // ---\n\n        pool = as.apply(pool);\n        assertEquals(2, pool.getTargetSize());\n\n        podCount.set(2);\n\n        pool = as.apply(pool);\n        assertEquals(3, pool.getTargetSize());\n\n        podCount.set(3);\n\n        pool = as.apply(pool);\n        assertEquals(5, pool.getTargetSize());\n\n        podCount.set(5);\n\n        pool = as.apply(pool);\n        assertEquals(8, pool.getTargetSize());\n\n        podCount.set(8);\n\n        // ---\n\n        queue.clear();\n\n        pool = as.apply(pool);\n        assertEquals(7, pool.getTargetSize());\n\n        podCount.set(7);\n\n        pool = as.apply(pool);\n        assertEquals(6, pool.getTargetSize());\n    }\n}\n"
  },
  {
    "path": "agent-operator/src/test/java/com/walmartlabs/concord/agentoperator/scheduler/LinearAutoScalerTest.java",
    "content": "package com.walmartlabs.concord.agentoperator.scheduler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.agentoperator.crd.AgentPool;\nimport com.walmartlabs.concord.agentoperator.crd.AgentPoolConfiguration;\nimport com.walmartlabs.concord.agentoperator.processqueue.ProcessQueueClient;\nimport com.walmartlabs.concord.agentoperator.processqueue.ProcessQueueEntry;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicInteger;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class LinearAutoScalerTest {\n\n    @Test\n    public void testStill() throws Exception {\n        AtomicInteger podCount = new AtomicInteger(1);\n        List<ProcessQueueEntry> queue = new ArrayList<>();\n\n        LinearAutoScaler as = new LinearAutoScaler(mockProcessQueueClient(queue), n -> podCount.get(), i -> true, i -> true);\n\n        AgentPoolConfiguration spec = new AgentPoolConfiguration();\n        spec.setQueueSelector(Collections.singletonMap(\"test\", 123));\n\n        AgentPool resource = new AgentPool();\n        resource.setSpec(spec);\n\n        AgentPoolInstance pool = new AgentPoolInstance(\"test\", resource, AgentPoolInstance.Status.ACTIVE, 1, 0, 0, 0);\n\n        // ---\n\n        pool = as.apply(pool);\n        assertEquals(1, pool.getTargetSize());\n\n        pool = as.apply(pool);\n        assertEquals(1, pool.getTargetSize());\n    }\n\n    @Test\n    public void testZeroStart() throws IOException {\n        AtomicInteger podCount = new AtomicInteger(0);\n        List<ProcessQueueEntry> queue = new ArrayList<>();\n\n        AutoScaler as = new LinearAutoScaler(mockProcessQueueClient(queue), n -> podCount.get(), i -> true, i -> true);\n\n        AgentPoolConfiguration spec = new AgentPoolConfiguration();\n        spec.setMinSize(0);\n        spec.setSize(0);\n        spec.setQueueSelector(Collections.singletonMap(\"test\", 123));\n\n        AgentPool resource = new AgentPool();\n        resource.setSpec(spec);\n\n        AgentPoolInstance pool = new AgentPoolInstance(\"test\", resource, AgentPoolInstance.Status.ACTIVE, 0, 0, 0, 0);\n\n        // ---\n\n        pool = as.apply(pool);\n        assertEquals(0, pool.getTargetSize());\n\n        // 1 enqueued process -> inc pods count\n        queue.add(new ProcessQueueEntry(Collections.singletonMap(\"test\", 123)));\n\n        pool = as.apply(pool);\n        assertEquals(1, pool.getTargetSize());\n\n        podCount.set(1);\n\n        // no processes -> 0 pods\n        queue.clear();\n\n        pool = as.apply(pool);\n        assertEquals(0, pool.getTargetSize());\n    }\n\n    private ProcessQueueClient mockProcessQueueClient(List<ProcessQueueEntry> queue) {\n        return new ProcessQueueClient(\"test\", \"test\") {\n            @Override\n            public List<ProcessQueueEntry> query(String processStatus, int limit, QueueSelector queueSelector) throws IOException {\n                return queue;\n            }\n        };\n    }\n\n    @Test\n    public void testQueue() throws IOException {\n        AtomicInteger podCount = new AtomicInteger(1);\n        List<ProcessQueueEntry> queue = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            queue.add(new ProcessQueueEntry(Collections.singletonMap(\"test\", 123)));\n        }\n\n        DefaultAutoScaler as = new DefaultAutoScaler(mockProcessQueueClient(queue), n -> podCount.get(), i -> true, i -> true);\n\n        AgentPoolConfiguration spec = new AgentPoolConfiguration();\n        spec.setPercentIncrement(50);\n        spec.setDecrementThresholdFactor(1.0);\n        spec.setIncrementThresholdFactor(1.5);\n        spec.setPercentDecrement(10);\n        spec.setQueueSelector(Collections.singletonMap(\"test\", 123));\n\n        AgentPool resource = new AgentPool();\n        resource.setSpec(spec);\n\n        AgentPoolInstance pool = new AgentPoolInstance(\"test\", resource, AgentPoolInstance.Status.ACTIVE, 1, 0, 0, 0);\n\n        // ---\n\n        pool = as.apply(pool);\n        assertEquals(2, pool.getTargetSize());\n\n        podCount.set(2);\n\n        pool = as.apply(pool);\n        assertEquals(3, pool.getTargetSize());\n\n        podCount.set(3);\n\n        pool = as.apply(pool);\n        assertEquals(5, pool.getTargetSize());\n\n        podCount.set(5);\n\n        pool = as.apply(pool);\n        assertEquals(8, pool.getTargetSize());\n\n        podCount.set(8);\n\n        // ---\n\n        queue.clear();\n\n        pool = as.apply(pool);\n        assertEquals(7, pool.getTargetSize());\n\n        podCount.set(7);\n\n        pool = as.apply(pool);\n        assertEquals(6, pool.getTargetSize());\n    }\n}\n"
  },
  {
    "path": "checkstyle.xml",
    "content": "<?xml version=\"1.0\"?>\n<!DOCTYPE module PUBLIC\n        \"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN\"\n        \"https://checkstyle.org/dtds/configuration_1_3.dtd\">\n\n<module name=\"Checker\">\n    <property name=\"charset\" value=\"UTF-8\"/>\n\n    <module name=\"TreeWalker\">\n        <module name=\"IllegalImport\">\n            <property name=\"regexp\" value=\"true\"/>\n\n            <!--\n                Disallow star imports of Guice to avoid conflicts between \"com.google.inject.Module\"\n                and \"java.lang.Module\" from JDK9+\n            -->\n            <property name=\"illegalClasses\" value=\"^com\\.google\\.inject\\.\\*\"/>\n\n            <!--\n                Allow sun.* packages for the Kerberos support in the Ansible plugin\n            -->\n            <property name=\"illegalPkgs\" value=\"\"/>\n        </module>\n    </module>\n</module>\n"
  },
  {
    "path": "cli/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-cli</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <properties>\n        <main.class>com.walmartlabs.concord.cli.Main</main.class>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime</groupId>\n            <artifactId>concord-runtime-common</artifactId>\n        </dependency>\n\n        <!-- v1 dependencies -->\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v1</groupId>\n            <artifactId>concord-runtime-model-v1</artifactId>\n        </dependency>\n\n        <!-- v2 dependencies -->\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-model-v2</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runner-v2</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-vm-v2</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime</groupId>\n            <artifactId>concord-runtime-loader</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-imports</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-repository</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-dependency-manager</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-client2</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.inject</groupId>\n            <artifactId>guice</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.glassfish</groupId>\n            <artifactId>javax.el</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>info.picocli</groupId>\n            <artifactId>picocli</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.dataformat</groupId>\n            <artifactId>jackson-dataformat-yaml</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.fusesource.jansi</groupId>\n            <artifactId>jansi</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.eclipse.jgit</groupId>\n            <artifactId>org.eclipse.jgit</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <resources>\n            <resource>\n                <filtering>false</filtering>\n                <directory>${project.basedir}/src/main/resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n            <resource>\n                <filtering>true</filtering>\n                <directory>${project.basedir}/src/main/filtered-resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n        </resources>\n        <testResources>\n            <testResource>\n                <filtering>false</filtering>\n                <directory>${project.basedir}/src/test/resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </testResource>\n            <testResource>\n                <filtering>true</filtering>\n                <directory>${project.basedir}/src/test/filtered-resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </testResource>\n        </testResources>\n\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-shade-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>shade</goal>\n                        </goals>\n                        <configuration>\n                            <shadedArtifactAttached>true</shadedArtifactAttached>\n                            <shadedClassifierName>executable</shadedClassifierName>\n                            <transformers>\n                                <transformer implementation=\"org.apache.maven.plugins.shade.resource.AppendingTransformer\">\n                                    <resource>META-INF/sisu/javax.inject.Named</resource>\n                                </transformer>\n                                <transformer implementation=\"org.apache.maven.plugins.shade.resource.AppendingTransformer\">\n                                    <resource>META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider</resource>\n                                </transformer>\n                                <transformer implementation=\"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer\">\n                                    <manifestEntries>\n                                        <Main-Class>${main.class}</Main-Class>\n                                        <Add-Opens>java.base/java.lang java.base/java.util</Add-Opens>\n                                    </manifestEntries>\n                                </transformer>\n                            </transformers>\n                            <filters>\n                                <filter>\n                                    <artifact>*:*</artifact>\n                                    <excludes>\n                                        <exclude>META-INF/*.SF</exclude>\n                                        <exclude>META-INF/*.RSA</exclude>\n                                    </excludes>\n                                </filter>\n                            </filters>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.skife.maven</groupId>\n                <artifactId>really-executable-jar-maven-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>really-executable-jar</goal>\n                        </goals>\n                    </execution>\n                </executions>\n                <configuration>\n                    <flags>-Xmx128m -client</flags>\n                    <classifier>executable</classifier>\n                    <allowOtherTypes>true</allowOtherTypes>\n                    <programFile>concord-cli-${project.version}</programFile>\n                    <programFileType>sh</programFileType>\n                    <attachProgramFile>true</attachProgramFile>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "cli/src/main/filtered-resources/defaultCfg.yml",
    "content": "configuration:\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.plugins.basic:http-tasks:${project.version}\"\n    - \"mvn://com.walmartlabs.concord.plugins.basic:slack-tasks:${project.version}\"\n    - \"mvn://com.walmartlabs.concord.plugins.basic:concord-tasks:${project.version}\"\n"
  },
  {
    "path": "cli/src/main/filtered-resources/project.properties",
    "content": "project.version=${project.version}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/AbortException.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class AbortException extends RuntimeException {\n\n    public AbortException() {\n        super(\"Aborted\");\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/App.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport picocli.CommandLine.Command;\nimport picocli.CommandLine.Model.CommandSpec;\nimport picocli.CommandLine.Option;\nimport picocli.CommandLine.Spec;\n\n@Command(name = \"concord\", subcommands = {Lint.class, Run.class, Resume.class, RemoteRun.class, SelfUpdate.class})\npublic class App implements Runnable {\n\n    @Spec\n    private CommandSpec spec;\n\n    @Option(names = {\"-h\", \"--help\"}, usageHelp = true, description = \"display a help message\")\n    boolean helpRequested = false;\n\n    @Option(names = {\"--version\"}, description = \"display version\")\n    boolean versionRequested = false;\n\n    @Override\n    public void run() {\n        if (versionRequested) {\n            System.out.println(Version.getVersion());\n            return;\n        }\n\n        spec.commandLine().usage(System.out);\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/CliConfig.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.DeserializationFeature;\nimport com.fasterxml.jackson.databind.JsonMappingException;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLMapper;\nimport com.google.common.annotations.VisibleForTesting;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.annotation.Nullable;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Map;\nimport java.util.Optional;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static java.util.Objects.requireNonNull;\nimport static org.fusesource.jansi.Ansi.ansi;\n\npublic record CliConfig(Map<String, CliConfigContext> contexts) {\n\n    private static final Logger log = LoggerFactory.getLogger(CliConfig.class);\n\n    public static CliConfig.CliConfigContext load(Verbosity verbosity, String context, Overrides overrides) {\n        try {\n            return loadOrThrow(verbosity, context, overrides);\n        } catch (Exception e) {\n            handleCliConfigErrorAndBail(resolveCliConfigPath().toAbsolutePath().toString(), e);\n            throw new IllegalStateException(\"should be unreachable\");\n        }\n    }\n\n    public static CliConfig.CliConfigContext loadOrThrow(Verbosity verbosity,\n                                                         String context,\n                                                         Overrides overrides) throws IOException {\n\n        var cfgFile = resolveCliConfigPath();\n        if (Files.notExists(cfgFile)) {\n            var cfg = CliConfig.create();\n            return requireCliConfigContext(cfg, context, false).withOverrides(overrides);\n        }\n\n        if (verbosity.verbose()) {\n            log.info(\"Using CLI configuration file: {} (\\\"{}\\\" context)\", cfgFile, context);\n        }\n\n        var cfg = loadConfigFile(cfgFile);\n        return requireCliConfigContext(cfg, context, true).withOverrides(overrides);\n    }\n\n    private static void handleCliConfigErrorAndBail(String cfgPath, Throwable e) {\n        // unwrap runtime exceptions\n        if (e instanceof RuntimeException ex) {\n            if (ex.getCause() instanceof IllegalArgumentException) {\n                e = ex.getCause();\n            }\n        }\n\n        if (e instanceof MissingContextException) {\n            System.out.println(ansi().fgRed().a(e.getMessage()));\n            System.exit(1);\n        }\n\n        // handle YAML errors\n        if (e instanceof IllegalArgumentException) {\n            if (e.getCause() instanceof UnrecognizedPropertyException ex) {\n                System.out.println(ansi().fgRed().a(\"Invalid format of the CLI configuration file \").a(cfgPath).a(\". \").a(ex.getMessage()));\n                System.exit(1);\n            }\n            System.out.println(ansi().fgRed().a(\"Invalid format of the CLI configuration file \").a(cfgPath).a(\". \").a(e.getMessage()));\n            System.exit(1);\n        }\n\n        // all other errors\n        System.out.println(ansi().fgRed().a(\"Failed to read the CLI configuration file \").a(cfgPath).a(\". \").a(e.getMessage()));\n        System.exit(1);\n    }\n\n    private static CliConfig.CliConfigContext requireCliConfigContext(CliConfig config, String context, boolean userConfigLoaded) {\n        var result = config.contexts().get(context);\n        if (result == null) {\n            throw new MissingContextException(context, userConfigLoaded);\n        }\n        return result;\n    }\n\n    static final class MissingContextException extends IllegalArgumentException {\n\n        private MissingContextException(String context, boolean userConfigLoaded) {\n            super(message(context, userConfigLoaded));\n        }\n\n        private static String message(String context, boolean userConfigLoaded) {\n            if (userConfigLoaded) {\n                return \"Configuration context not found: \" + context + \". Check the CLI configuration file.\";\n            }\n            return \"Configuration context not found: \" + context + \". No CLI configuration file was found in ~/.concord; only the built-in 'default' context is available.\";\n        }\n    }\n\n    @VisibleForTesting\n    static CliConfig loadConfigFile(Path path) throws IOException {\n        var mapper = new YAMLMapper()\n                .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);\n\n        JsonNode defaults = mapper.readTree(readDefaultConfig());\n\n        JsonNode cfg;\n        try (var reader = Files.newBufferedReader(path)) {\n            cfg = mapper.readTree(reader);\n        }\n\n        // merge the loaded config file with the default built-in config\n        var cfgWithDefaults = mapper.updateValue(defaults, cfg);\n\n        // merge each non-default context with the default context\n        var contexts = assertContexts(cfgWithDefaults);\n\n        var defaultCtx = contexts.get(\"default\");\n        if (defaultCtx == null) {\n            throw new IllegalArgumentException(\"Missing 'default' context.\");\n        }\n\n        contexts.fieldNames().forEachRemaining(ctxName -> {\n            if (\"default\".equals(ctxName)) {\n                return;\n            }\n\n            var ctx = contexts.get(ctxName);\n            try {\n                var mergedCtx = mapper.updateValue(defaultCtx, ctx);\n                contexts.set(ctxName, mergedCtx);\n            } catch (JsonMappingException e) {\n                throw new RuntimeException(e);\n            }\n        });\n\n        return mapper.convertValue(cfgWithDefaults, CliConfig.class);\n    }\n\n    private static ObjectNode assertContexts(JsonNode cfg) {\n        var maybeContexts = cfg.get(\"contexts\");\n        if (maybeContexts == null) {\n            throw new IllegalArgumentException(\"Missing 'contexts' object.\");\n        }\n        if (!maybeContexts.isObject()) {\n            throw new IllegalArgumentException(\"The 'contexts' field must be an object.\");\n        }\n        return (ObjectNode) maybeContexts;\n    }\n\n    public static CliConfig create() {\n        var mapper = new YAMLMapper();\n        try {\n            return mapper.readValue(readDefaultConfig(), CliConfig.class);\n        } catch (IOException e) {\n            throw new IllegalStateException(\"Can't parse the default CLI config file. \" + e.getMessage());\n        }\n    }\n\n    public record Overrides(@Nullable Path secretStoreDir, @Nullable Path vaultDir, @Nullable String vaultId) {\n    }\n\n    static boolean hasUserConfig() {\n        return Files.exists(Paths.get(System.getProperty(\"user.home\"), \".concord\", \"cli.yaml\"))\n                || Files.exists(Paths.get(System.getProperty(\"user.home\"), \".concord\", \"cli.yml\"));\n    }\n\n    private static Path resolveCliConfigPath() {\n        var baseDir = Paths.get(System.getProperty(\"user.home\"), \".concord\");\n        var cfgFile = baseDir.resolve(\"cli.yaml\");\n        if (Files.exists(cfgFile)) {\n            return cfgFile;\n        }\n        return baseDir.resolve(\"cli.yml\");\n    }\n\n    public record CliConfigContext(@Nullable RemoteRunConfiguration remoteRun, SecretsConfiguration secrets) {\n\n        public CliConfigContext withOverrides(@Nullable Overrides overrides) {\n            if (overrides == null) {\n                return this;\n            }\n            var remoteRun = this.remoteRun();\n            var secrets = this.secrets().withOverrides(overrides);\n            return new CliConfigContext(remoteRun, secrets);\n        }\n    }\n\n    public record SecretRef(String orgName, String secretName) {\n\n        public SecretRef(String orgName, String secretName) {\n            this.orgName = orgName == null ? \"Default\" : orgName;\n            if (this.orgName.isBlank()) {\n                throw new IllegalArgumentException(\"'orgName' is required\");\n            }\n            this.secretName = requireNonNull(secretName);\n            if (this.secretName.isBlank()) {\n                throw new IllegalArgumentException(\"'secretName' is required\");\n            }\n        }\n    }\n\n    public record RemoteRunConfiguration(@Nullable String baseUrl, @Nullable SecretRef apiKeyRef) {\n    }\n\n    public record SecretsConfiguration(VaultConfiguration vault,\n                                       FileSecretsProviderConfiguration local,\n                                       RemoteSecretsProviderConfiguration remote) {\n\n        public SecretsConfiguration withOverrides(@Nullable Overrides overrides) {\n            if (overrides == null) {\n                return this;\n            }\n            var vault = this.vault().withOverrides(overrides);\n            var localFiles = this.local().withOverrides(overrides);\n            return new SecretsConfiguration(vault, localFiles, this.remote);\n        }\n\n        public record VaultConfiguration(Path dir, String id) {\n\n            public VaultConfiguration withOverrides(@Nullable Overrides overrides) {\n                if (overrides == null) {\n                    return this;\n                }\n                return new VaultConfiguration(\n                        Optional.ofNullable(overrides.vaultDir()).orElse(this.dir()),\n                        Optional.ofNullable(overrides.vaultId()).orElse(this.id()));\n            }\n        }\n\n        public record FileSecretsProviderConfiguration(boolean enabled, boolean writable, Path dir) {\n\n            public FileSecretsProviderConfiguration withOverrides(@Nullable Overrides overrides) {\n                if (overrides == null) {\n                    return this;\n                }\n                return new FileSecretsProviderConfiguration(\n                        this.enabled,\n                        this.writable,\n                        Optional.ofNullable(overrides.secretStoreDir()).orElse(this.dir()));\n            }\n        }\n\n        public record RemoteSecretsProviderConfiguration(boolean enabled,\n                                                         boolean writable,\n                                                         @Nullable String baseUrl,\n                                                         @Nullable String apiKey,\n                                                         boolean confirmAccess) {\n        }\n    }\n\n    private static String readDefaultConfig() {\n        try (var in = CliConfig.class.getResourceAsStream(\"defaultCliConfig.yaml\")) {\n            if (in == null) {\n                throw new IllegalStateException(\"defaultCliConfig.yaml resource not found\");\n            }\n            var ab = in.readAllBytes();\n            var s = new String(ab, UTF_8);\n\n            var dotConcordPath = Paths.get(System.getProperty(\"user.home\")).resolve(\".concord\");\n            return s.replace(\"${configDir}\", dotConcordPath.normalize().toAbsolutePath().toString());\n        } catch (IOException e) {\n            throw new IllegalStateException(\"Can't load the default CLI config file. \" + e.getMessage());\n        }\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/CliExitCodes.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nfinal class CliExitCodes {\n\n    static final int SUCCESS = 0;\n    static final int ERROR = 1;\n    static final int USAGE = 2;\n    static final int SUSPENDED = 20;\n    static final int INPUT_REQUIRED = 21;\n    static final int NON_INTERACTIVE_UNSUPPORTED = 22;\n    static final int PROCESS_FAILED = -1;\n\n    private CliExitCodes() {\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/CliPaths.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.nio.file.Path;\n\npublic final class CliPaths {\n\n    public static final String DEFAULT_TARGET_DIR_NAME = \"target\";\n\n    public static Path defaultTargetDir(Path sourceDir) {\n        return sourceDir.resolve(DEFAULT_TARGET_DIR_NAME);\n    }\n\n    public static Path preferredResumeDir(Path sourceDir, Path workDir) {\n        var normalizedSourceDir = sourceDir.normalize().toAbsolutePath();\n        var normalizedWorkDir = workDir.normalize().toAbsolutePath();\n        if (normalizedWorkDir.equals(defaultTargetDir(normalizedSourceDir))) {\n            return normalizedSourceDir;\n        }\n        return normalizedWorkDir;\n    }\n\n    private CliPaths() {\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/Confirmation.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\n\nimport static org.fusesource.jansi.Ansi.ansi;\n\npublic final class Confirmation {\n\n    public static boolean confirm(String message) throws IOException {\n        return confirm(message, false);\n    }\n\n    public static boolean confirm(String message, boolean defaultValue) throws IOException {\n        System.out.println(ansi().fgBrightYellow().bold().a(message).reset());\n\n        var response = new StringBuilder();\n        while (true) {\n            int ch = System.in.read();\n            if (ch == -1 || ch == '\\n') {\n                break;\n            }\n            if (ch != '\\r') {\n                response.append((char) ch);\n            }\n        }\n\n        var value = response.toString().trim().toLowerCase();\n        if (value.isEmpty()) {\n            return defaultValue;\n        }\n\n        return \"y\".equals(value) || \"yes\".equals(value);\n    }\n\n    private Confirmation() {\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/GitIgnoreFilter.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.eclipse.jgit.ignore.IgnoreNode;\n\nimport java.io.IOException;\nimport java.nio.file.*;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.util.*;\n\n/**\n * Utility class for filtering files based on .gitignore patterns.\n * Supports hierarchical .gitignore files in subdirectories per git spec.\n */\npublic class GitIgnoreFilter {\n\n    private final Map<Path, IgnoreNode> ignoreNodes;\n\n    private GitIgnoreFilter(Map<Path, IgnoreNode> ignoreNodes) {\n        this.ignoreNodes = ignoreNodes;\n    }\n\n    /**\n     * Loads all .gitignore files from the given directory and its subdirectories.\n     *\n     * @param baseDir the base directory to scan for .gitignore files\n     * @return a GitIgnoreFilter instance, or null if no .gitignore files exist\n     */\n    public static GitIgnoreFilter load(Path baseDir) throws IOException {\n        var nodes = new HashMap<Path, IgnoreNode>();\n\n        Files.walkFileTree(baseDir, new SimpleFileVisitor<>() {\n            @Override\n            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {\n                var gitignore = dir.resolve(\".gitignore\");\n                if (Files.isRegularFile(gitignore)) {\n                    var node = new IgnoreNode();\n                    try (var in = Files.newInputStream(gitignore)) {\n                        node.parse(in);\n                    }\n                    if (!node.getRules().isEmpty()) {\n                        var relDir = baseDir.relativize(dir);\n                        nodes.put(relDir, node);\n                    }\n                }\n                return FileVisitResult.CONTINUE;\n            }\n        });\n\n        if (nodes.isEmpty()) {\n            return null;\n        }\n\n        return new GitIgnoreFilter(nodes);\n    }\n\n    /**\n     * Checks if the given path should be ignored based on .gitignore rules.\n     *\n     * @param relativePath the path relative to the base directory\n     * @param isDirectory  true if the path is a directory\n     * @return true if the path should be ignored\n     */\n    public boolean isIgnored(Path relativePath, boolean isDirectory) {\n        var pathStr = relativePath.toString().replace('\\\\', '/');\n        Boolean ignored = null;\n\n        // Check from root down to parent directory\n        for (var ancestor : getAncestorPaths(relativePath)) {\n            var node = ignoreNodes.get(ancestor);\n            if (node != null) {\n                // Make path relative to this ignore node's directory\n                String relativeToNode;\n                if (ancestor.toString().isEmpty()) {\n                    relativeToNode = pathStr;\n                } else {\n                    var ancestorStr = ancestor.toString().replace('\\\\', '/');\n                    relativeToNode = pathStr.substring(ancestorStr.length() + 1);\n                }\n\n                var result = node.isIgnored(relativeToNode, isDirectory);\n                if (result == IgnoreNode.MatchResult.IGNORED) {\n                    ignored = true;\n                } else if (result == IgnoreNode.MatchResult.NOT_IGNORED) {\n                    ignored = false;\n                }\n                // CHECK_PARENT means no match, keep current value\n            }\n        }\n\n        return Boolean.TRUE.equals(ignored);\n    }\n\n    /**\n     * Returns all ancestor paths from root (empty path) to the parent of the given path.\n     */\n    private List<Path> getAncestorPaths(Path relativePath) {\n        var ancestors = new ArrayList<Path>();\n        ancestors.add(Paths.get(\"\"));  // root\n\n        var current = Paths.get(\"\");\n        for (var i = 0; i < relativePath.getNameCount() - 1; i++) {\n            current = current.resolve(relativePath.getName(i));\n            ancestors.add(current);\n        }\n\n        return ancestors;\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/Lint.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.cli.lint.*;\nimport com.walmartlabs.concord.cli.lint.LintResult.Type;\nimport com.walmartlabs.concord.cli.runner.CliImportsListener;\nimport com.walmartlabs.concord.imports.ImportManager;\nimport com.walmartlabs.concord.imports.NoopImportManager;\nimport com.walmartlabs.concord.process.loader.DelegatingProjectLoader;\nimport com.walmartlabs.concord.runtime.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.model.SourceMap;\nimport com.walmartlabs.concord.runtime.v1.ProjectLoaderV1;\nimport com.walmartlabs.concord.runtime.v2.ProjectLoaderV2;\nimport org.fusesource.jansi.Ansi;\nimport picocli.CommandLine.Command;\nimport picocli.CommandLine.Model.CommandSpec;\nimport picocli.CommandLine.Option;\nimport picocli.CommandLine.Parameters;\nimport picocli.CommandLine.Spec;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.concurrent.Callable;\n\nimport static org.fusesource.jansi.Ansi.ansi;\n\n@Command(name = \"lint\", description = \"Parse and validate Concord YAML files\")\npublic class Lint implements Callable<Integer> {\n\n    @Spec\n    private CommandSpec spec;\n\n    @Option(names = {\"-h\", \"--help\"}, usageHelp = true, description = \"display the command's help message\")\n    boolean helpRequested = false;\n\n    @Option(names = {\"-v\", \"--verbose\"}, description = \"Verbose output\")\n    boolean verbose = false;\n\n    @Parameters(arity = \"0..1\")\n    Path targetDir = Paths.get(System.getProperty(\"user.dir\"));\n\n    @Override\n    public Integer call() throws Exception {\n        targetDir = targetDir.normalize().toAbsolutePath();\n\n        if (!Files.isDirectory(targetDir)) {\n            throw new IllegalArgumentException(\"Not a directory: \" + targetDir);\n        }\n\n        ImportManager importManager = new NoopImportManager();\n        ProjectLoaderV1 v1 = new ProjectLoaderV1(importManager);\n        ProjectLoaderV2 v2 = new ProjectLoaderV2(importManager);\n        DelegatingProjectLoader loader = new DelegatingProjectLoader(Set.of(v1, v2));\n        ProcessDefinition pd = loader.loadProject(targetDir, new DummyImportsNormalizer(), verbose ? new CliImportsListener() : null).projectDefinition();\n\n        List<LintResult> lintResults = new ArrayList<>();\n        linters().forEach(l -> lintResults.addAll(l.apply(pd)));\n\n        if (!lintResults.isEmpty()) {\n            print(lintResults);\n            println();\n        }\n\n        println(\"Found:\");\n        println(\"  imports: \" + pd.imports().items().size());\n        println(\"  profiles: \" + pd.profiles().size());\n        println(\"  flows: \" + pd.flows().size());\n        println(\"  forms: \" + pd.forms().size());\n        println(\"  triggers: \" + pd.triggers().size());\n        println(\"  (not counting dynamically imported resources)\");\n\n        println();\n        printStats(lintResults);\n\n        println();\n        boolean hasErrors = hasErrors(lintResults);\n        if (hasErrors) {\n            System.out.println(ansi().fgBrightRed().bold().a(\"INVALID\").reset());\n        } else {\n            System.out.println(ansi().fgBrightGreen().bold().a(\"VALID\").reset());\n        }\n\n        return hasErrors ? 10 : 0;\n    }\n\n    private List<Linter> linters() {\n        return Arrays.asList(\n                new ExpressionLinter(verbose),\n                new TaskCallLinter(verbose)\n        );\n    }\n\n    private void print(List<LintResult> results) {\n        for (LintResult r : results) {\n            Ansi msg = ansi();\n            switch (r.getType()) {\n                case ERROR: {\n                    ansi().fgBrightRed().a(\"ERROR:\").reset();\n                    break;\n                }\n                case WARNING: {\n                    ansi().fgBrightYellow().a(\"WARN:\").reset();\n                    break;\n                }\n                default:\n                    throw new IllegalArgumentException(\"Unsupported result type: \" + r.getType());\n            }\n\n            SourceMap sm = r.getSourceMap();\n            if (sm != null) {\n                msg.a(\"@ [\").a(sm.source()).a(\"] line: \").a(sm.line()).a(\", col: \").a(sm.column());\n            }\n\n            msg.append(\"\\n\\t\").append(r.getMessage());\n\n            println(msg);\n            println(\"------------------------------------------------------------\");\n        }\n    }\n\n    private void printStats(List<LintResult> results) {\n        long errors = results.stream().filter(r -> r.getType() == Type.ERROR).count();\n        long warns = results.stream().filter(r -> r.getType() == Type.WARNING).count();\n        println(\"Result: \" + errors + \" error(s), \" + warns + \" warning(s)\");\n    }\n\n    private void println(Object o) {\n        System.out.println(o);\n    }\n\n    private void println() {\n        System.out.println();\n    }\n\n    private static boolean hasErrors(List<LintResult> results) {\n        return results.stream().anyMatch(l -> l.getType() == Type.ERROR);\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/LocalCliRuntime.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Injector;\nimport com.walmartlabs.concord.cli.CliConfig.CliConfigContext;\nimport com.walmartlabs.concord.cli.runner.CliServicesModule;\nimport com.walmartlabs.concord.dependencymanager.DependencyManager;\nimport com.walmartlabs.concord.dependencymanager.DependencyManagerConfiguration;\nimport com.walmartlabs.concord.dependencymanager.DependencyManagerRepositories;\nimport com.walmartlabs.concord.imports.ImportsListener;\nimport com.walmartlabs.concord.imports.NoopImportManager;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.runtime.v2.NoopImportsNormalizer;\nimport com.walmartlabs.concord.runtime.v2.ProjectLoaderV2;\nimport com.walmartlabs.concord.runtime.v2.runner.InjectorFactory;\nimport com.walmartlabs.concord.runtime.v2.runner.guice.ProcessDependenciesModule;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration;\nimport com.walmartlabs.concord.runtime.v2.sdk.WorkingDirectory;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\nfinal class LocalCliRuntime {\n\n    static DependencyManager createDependencyManager(Path depsCacheDir) throws IOException {\n        var cfgFile = Paths.get(System.getProperty(\"user.home\"), \".concord\", \"mvn.json\");\n        if (Files.exists(cfgFile)) {\n            return new DependencyManager(DependencyManagerConfiguration.of(depsCacheDir, DependencyManagerRepositories.get(cfgFile)));\n        }\n        return new DependencyManager(DependencyManagerConfiguration.of(depsCacheDir));\n    }\n\n    static Injector createInjector(Path workDir,\n                                   RunnerConfiguration runnerCfg,\n                                   ProcessConfiguration processCfg,\n                                   CliConfigContext cliConfigContext,\n                                   Path defaultTaskVars,\n                                   DependencyManager dependencyManager,\n                                   Verbosity verbosity) {\n\n        return new InjectorFactory(new WorkingDirectory(workDir),\n                runnerCfg,\n                () -> processCfg,\n                new ProcessDependenciesModule(workDir, runnerCfg.dependencies(), processCfg.debug()),\n                new CliServicesModule(cliConfigContext, workDir, defaultTaskVars, dependencyManager, verbosity))\n                .create();\n    }\n\n    static void notifyProjectLoaded(Path workDir) throws Exception {\n        var loader = new ProjectLoaderV2(new NoopImportManager());\n        loader.load(workDir, new NoopImportsNormalizer(), ImportsListener.NOP_LISTENER);\n    }\n\n    private LocalCliRuntime() {\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/LocalFormInputs.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.forms.DefaultFormValidator;\nimport com.walmartlabs.concord.forms.DefaultFormValidatorLocale;\nimport com.walmartlabs.concord.forms.Form;\nimport com.walmartlabs.concord.forms.FormField;\nimport com.walmartlabs.concord.forms.FormFields;\nimport com.walmartlabs.concord.forms.FormUtils;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.UncheckedIOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.forms.Constants.FORM_FILES;\n\nfinal class LocalFormInputs {\n\n    private static final DefaultFormValidatorLocale LOCALE = new DefaultFormValidatorLocale();\n    private static final DefaultFormValidator VALIDATOR = new DefaultFormValidator(LOCALE);\n\n    static Converted convertAndValidate(Form form, Map<String, Object> rawValues) throws InputException {\n        return convertAndValidate(form, rawValues, false);\n    }\n\n    static Converted convertAndValidate(Form form, Map<String, Object> input, boolean unwrapFormName) throws InputException {\n        var rawValues = unwrapFormName ? unwrapFormValues(form, input) : input;\n        var tmpFiles = new LinkedHashMap<String, String>();\n        var opened = new ArrayList<InputStream>();\n\n        try {\n            var convertedInput = prepareInput(form, rawValues, opened);\n            var converted = new LinkedHashMap<>(FormUtils.convert(LOCALE, form, convertedInput, tmpFiles));\n            var errors = VALIDATOR.validate(form, converted);\n            if (!errors.isEmpty()) {\n                cleanupTempFiles(tmpFiles);\n                throw new InputException(errors.stream().map(e -> e.error()).toList());\n            }\n\n            return new Converted(converted, tmpFiles);\n        } catch (FormUtils.ValidationException e) {\n            cleanupTempFiles(tmpFiles);\n            throw new InputException(List.of(e.getMessage()), e);\n        } catch (IOException e) {\n            cleanupTempFiles(tmpFiles);\n            throw new InputException(List.of(e.getMessage()), e);\n        } finally {\n            close(opened);\n        }\n    }\n\n    private static Map<String, Object> prepareInput(Form form, Map<String, Object> rawValues, List<InputStream> opened) throws IOException {\n        var convertedInput = new LinkedHashMap<String, Object>();\n        for (var field : form.fields()) {\n            var value = rawValues.get(field.name());\n            if (value == null) {\n                continue;\n            }\n\n            convertedInput.put(field.name(), toFormInput(value, field, opened));\n        }\n        return convertedInput;\n    }\n\n    private static Object toFormInput(Object value, FormField field, List<InputStream> opened) throws IOException {\n        if (!FormFields.FileField.TYPE.equals(field.type())) {\n            return value;\n        }\n\n        if (value instanceof Path path) {\n            var in = Files.newInputStream(path);\n            opened.add(in);\n            return in;\n        }\n\n        if (value instanceof Collection<?> values) {\n            var result = new ArrayList<>();\n            for (var item : values) {\n                result.add(toFormInput(item, field, opened));\n            }\n            return result;\n        }\n\n        return value;\n    }\n\n    private static Map<String, Object> unwrapFormValues(Form form, Map<String, Object> input) throws InputException {\n        var value = input.get(form.name());\n        if (value == null) {\n            return input;\n        }\n\n        if (!(value instanceof Map<?, ?> values)) {\n            throw new InputException(List.of(\"Expected an object value for form '\" + form.name() + \"'\"));\n        }\n\n        var result = new LinkedHashMap<String, Object>();\n        for (var e : values.entrySet()) {\n            if (!(e.getKey() instanceof String key)) {\n                throw new InputException(List.of(\"Expected string field names for form '\" + form.name() + \"'\"));\n            }\n            result.put(key, e.getValue());\n        }\n        return result;\n    }\n\n    static void cleanupTempFiles(Map<String, String> tmpFiles) {\n        for (var tmpFile : tmpFiles.values()) {\n            try {\n                Files.deleteIfExists(Path.of(tmpFile));\n            } catch (IOException ignored) {\n                // best effort cleanup for validation retries\n            }\n        }\n    }\n\n    private static void close(List<InputStream> opened) {\n        for (var in : opened) {\n            try {\n                in.close();\n            } catch (IOException e) {\n                throw new UncheckedIOException(e);\n            }\n        }\n    }\n\n    record Converted(Map<String, Object> values, Map<String, String> tmpFiles) {\n\n        Map<String, Object> payload(Form form) {\n            var values = new LinkedHashMap<>(this.values);\n            values.remove(FORM_FILES);\n\n            var payload = new LinkedHashMap<String, Object>();\n            payload.put(form.name(), values);\n            return payload;\n        }\n\n        void cleanupTempFiles() {\n            LocalFormInputs.cleanupTempFiles(tmpFiles);\n        }\n    }\n\n    static final class InputException extends Exception {\n\n        private final List<String> messages;\n\n        private InputException(List<String> messages) {\n            this(messages, null);\n        }\n\n        private InputException(List<String> messages, Throwable cause) {\n            super(String.join(\", \", messages), cause);\n            this.messages = List.copyOf(messages);\n        }\n\n        List<String> messages() {\n            return messages;\n        }\n    }\n\n    private LocalFormInputs() {\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/LocalFormPrompts.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.forms.Form;\nimport com.walmartlabs.concord.forms.FormField;\nimport com.walmartlabs.concord.forms.FormFields;\nimport com.walmartlabs.concord.forms.FormFields.FileField;\n\nimport java.io.BufferedReader;\nimport java.io.Console;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.io.UncheckedIOException;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.forms.Constants.FORM_FILES;\nimport static org.fusesource.jansi.Ansi.ansi;\n\nfinal class LocalFormPrompts {\n\n    private static final String HIDDEN_VALUE = \"<hidden>\";\n\n    private final Path workDir;\n    private final boolean printHeader;\n    private final PromptIo promptIo;\n\n    LocalFormPrompts(Path workDir) {\n        this(workDir, true);\n    }\n\n    LocalFormPrompts(Path workDir, boolean printHeader) {\n        this.workDir = workDir;\n        this.printHeader = printHeader;\n        this.promptIo = new PromptIo();\n    }\n\n    Map<String, Object> prompt(Form form) throws Exception {\n        var rawValues = new LinkedHashMap<String, Object>();\n\n        while (true) {\n            if (printHeader) {\n                printHeader(form);\n            }\n            collectRawValues(form, rawValues);\n\n            try {\n                var converted = LocalFormInputs.convertAndValidate(form, rawValues);\n                try {\n                    moveFormFiles(converted.values());\n                } catch (IOException e) {\n                    converted.cleanupTempFiles();\n                    throw e;\n                }\n                return converted.payload(form);\n            } catch (LocalFormInputs.InputException e) {\n                clearSensitiveValues(form, rawValues);\n                printInputErrors(e);\n            }\n        }\n    }\n\n    private void printHeader(Form form) {\n        System.out.println(\"Pending form input:\");\n        System.out.println(\"  \" + form.name() + \" -> \" + form.eventName());\n        if (form.options().isYield()) {\n            printWarning(\"'yield' is informational only in local CLI.\");\n        }\n        if (form.options().saveSubmittedBy()) {\n            printWarning(\"'saveSubmittedBy' is ignored in local CLI.\");\n        }\n        if (!form.options().runAs().isEmpty()) {\n            printWarning(\"'runAs' restrictions are not enforced in local CLI.\");\n        }\n    }\n\n    private void collectRawValues(Form form, Map<String, Object> rawValues) {\n        for (var field : form.fields()) {\n            if (Boolean.TRUE.equals(field.getOption(FormFields.CommonFieldOptions.READ_ONLY))) {\n                continue;\n            }\n\n            var value = promptField(field, rawValues.get(field.name()));\n            if (value == MissingValue.INSTANCE) {\n                rawValues.remove(field.name());\n            } else {\n                rawValues.put(field.name(), value);\n            }\n        }\n    }\n\n    private Object promptField(FormField field, Object currentValue) {\n        if (isRepeated(field)) {\n            return promptRepeatedField(field, currentValue);\n        }\n\n        while (true) {\n            var line = readValue(field, currentValue);\n            if (line == null) {\n                return currentValue != null ? currentValue : MissingValue.INSTANCE;\n            }\n\n            if (FileField.TYPE.equals(field.type())) {\n                var path = Path.of(line);\n                if (!Files.isRegularFile(path)) {\n                    printError(\"File not found: \" + path);\n                    continue;\n                }\n                return path;\n            }\n\n            return normalizeRawValue(field, line);\n        }\n    }\n\n    private Object promptRepeatedField(FormField field, Object currentValue) {\n        if (currentValue instanceof Collection && !((Collection<?>) currentValue).isEmpty()) {\n            var current = isPasswordField(field) ? HIDDEN_VALUE : currentValue;\n            System.out.println(\"Current values for \" + label(field) + \": \" + current);\n        }\n\n        var values = new ArrayList<>();\n        while (true) {\n            var line = readValue(field, null, values.size() + 1);\n            if (line == null) {\n                if (values.isEmpty()) {\n                    return currentValue != null ? currentValue : MissingValue.INSTANCE;\n                }\n                return values;\n            }\n\n            if (FileField.TYPE.equals(field.type())) {\n                var path = Path.of(line);\n                if (!Files.isRegularFile(path)) {\n                    printError(\"File not found: \" + path);\n                    continue;\n                }\n                values.add(path);\n            } else {\n                values.add(normalizeRawValue(field, line));\n            }\n        }\n    }\n\n    private String readValue(FormField field, Object currentValue) {\n        return readValue(field, currentValue, null);\n    }\n\n    private String readValue(FormField field, Object currentValue, Integer idx) {\n        var prompt = buildPrompt(field, currentValue, idx);\n        var password = \"password\".equals(field.getOption(FormFields.StringField.INPUT_TYPE));\n        var line = password ? promptIo.readPassword(prompt) : promptIo.readLine(prompt);\n        if (line == null || line.trim().isEmpty()) {\n            return null;\n        }\n        return line;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private void moveFormFiles(Map<String, Object> converted) throws IOException {\n        var formFiles = (Map<String, String>) converted.get(FORM_FILES);\n        if (formFiles == null || formFiles.isEmpty()) {\n            return;\n        }\n\n        for (var e : formFiles.entrySet()) {\n            var destination = workDir.resolve(e.getKey());\n            var parent = destination.getParent();\n            if (parent != null && Files.notExists(parent)) {\n                Files.createDirectories(parent);\n            }\n            Files.move(Path.of(e.getValue()), destination, StandardCopyOption.REPLACE_EXISTING);\n        }\n    }\n\n    private static void printInputErrors(LocalFormInputs.InputException e) {\n        for (var message : e.messages()) {\n            printError(message);\n        }\n    }\n\n    private static void printError(String message) {\n        System.err.println(ansi().fgBrightRed().a(\"Error: \").a(message).reset());\n    }\n\n    private static void printWarning(String message) {\n        System.out.println(ansi().fgBrightYellow().a(\"Warning: \").a(message).reset());\n    }\n\n    private static String buildPrompt(FormField field, Object currentValue, Integer idx) {\n        var details = new ArrayList<String>();\n        details.add(field.type());\n        details.add(isOptional(field) ? \"optional\" : \"required\");\n\n        if (field.allowedValue() != null) {\n            details.add(\"allowed: \" + field.allowedValue());\n        }\n\n        if (currentValue != null) {\n            details.add(\"current: \" + promptValue(field, currentValue));\n        } else if (field.defaultValue() != null) {\n            details.add(\"default: \" + promptValue(field, field.defaultValue()));\n        }\n\n        if (FormFields.DateField.TYPE.equals(field.type()) || FormFields.DateTimeField.TYPE.equals(field.type())) {\n            details.add(\"format: ISO-8601\");\n        }\n\n        var name = idx != null ? label(field) + \" [\" + idx + \"]\" : label(field);\n        return name + \" [\" + String.join(\", \", details) + \"]: \";\n    }\n\n    private static String promptValue(FormField field, Object value) {\n        return isPasswordField(field) ? HIDDEN_VALUE : String.valueOf(value);\n    }\n\n    private static boolean isPasswordField(FormField field) {\n        return \"password\".equals(field.getOption(FormFields.StringField.INPUT_TYPE));\n    }\n\n    private static void clearSensitiveValues(Form form, Map<String, Object> rawValues) {\n        for (var field : form.fields()) {\n            if (isPasswordField(field)) {\n                rawValues.remove(field.name());\n            }\n        }\n    }\n\n    private static boolean isRepeated(FormField field) {\n        return field.cardinality() == FormField.Cardinality.ANY || field.cardinality() == FormField.Cardinality.AT_LEAST_ONE;\n    }\n\n    private static boolean isOptional(FormField field) {\n        return field.cardinality() == FormField.Cardinality.ONE_OR_NONE || field.cardinality() == FormField.Cardinality.ANY;\n    }\n\n    private static Object normalizeRawValue(FormField field, String line) {\n        if (FormFields.BooleanField.TYPE.equals(field.type())) {\n            var normalized = line.trim().toLowerCase();\n            if (\"y\".equals(normalized) || \"yes\".equals(normalized)) {\n                return \"true\";\n            }\n            if (\"n\".equals(normalized) || \"no\".equals(normalized)) {\n                return \"false\";\n            }\n            return normalized;\n        }\n\n        if (!FormFields.StringField.TYPE.equals(field.type())) {\n            return line.trim();\n        }\n\n        return line;\n    }\n\n    private static String label(FormField field) {\n        return field.label() != null ? field.label() : field.name();\n    }\n\n    private enum MissingValue {\n        INSTANCE\n    }\n\n    private static final class PromptIo {\n\n        private final Console console = System.console();\n        private final BufferedReader reader = console == null\n                ? new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8))\n                : null;\n\n        String readLine(String prompt) {\n            try {\n                if (console != null) {\n                    return console.readLine(\"%s\", prompt);\n                }\n\n                System.out.print(prompt);\n                System.out.flush();\n                var line = reader.readLine();\n                if (line == null) {\n                    throw new IllegalStateException(\"End of input while reading form values\");\n                }\n                return line;\n            } catch (IOException e) {\n                throw new UncheckedIOException(e);\n            }\n        }\n\n        String readPassword(String prompt) {\n            if (console != null) {\n                var chars = console.readPassword(\"%s\", prompt);\n                return chars != null ? new String(chars) : \"\";\n            }\n\n            return readLine(prompt);\n        }\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/LocalFormSession.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.runner.ProcessSnapshot;\nimport com.walmartlabs.concord.runtime.v2.runner.Runner;\n\nimport java.nio.file.Path;\nimport java.util.Collections;\n\nfinal class LocalFormSession {\n\n    static ProcessSnapshot resumePendingForms(Path workDir,\n                                              Runner runner,\n                                              ProcessSnapshot snapshot,\n                                              LocalSuspendPersistence.ResumeMetadata metadata) throws Exception {\n        return resumePendingForms(workDir, runner, snapshot, metadata, true);\n    }\n\n    static ProcessSnapshot resumePendingForms(Path workDir,\n                                              Runner runner,\n                                              ProcessSnapshot snapshot,\n                                              LocalSuspendPersistence.ResumeMetadata metadata,\n                                              boolean printHeader) throws Exception {\n\n        var formPrompts = new LocalFormPrompts(workDir, printHeader);\n\n        while (LocalSuspendPersistence.isSuspended(snapshot)) {\n            var events = LocalSuspendPersistence.getEvents(snapshot);\n            LocalSuspendPersistence.save(workDir, snapshot, metadata);\n\n            var pendingForms = LocalFormState.syncPendingForms(workDir, events);\n            if (pendingForms.isEmpty()) {\n                return snapshot;\n            }\n\n            LocalFormState.assertSupported(workDir, pendingForms);\n\n            var form = pendingForms.get(0);\n            var input = formPrompts.prompt(form);\n            snapshot = runner.resume(snapshot, Collections.singleton(form.eventName()), input);\n        }\n\n        LocalFormState.syncPendingForms(workDir, Collections.emptySet());\n        return snapshot;\n    }\n\n    private LocalFormSession() {\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/LocalFormState.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.forms.Form;\nimport com.walmartlabs.concord.runtime.common.FormService;\nimport com.walmartlabs.concord.sdk.Constants;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collection;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.TreeSet;\nimport java.util.stream.Collectors;\n\nfinal class LocalFormState {\n\n    static List<Form> syncPendingForms(Path workDir, Set<String> waitingEvents) throws IOException {\n        var formsDir = formsDir(workDir);\n        if (Files.notExists(formsDir)) {\n            return List.of();\n        }\n\n        var forms = new FormService(formsDir).list();\n        for (var form : forms) {\n            if (!waitingEvents.contains(form.eventName())) {\n                Files.deleteIfExists(formPath(workDir, form.name()));\n            }\n        }\n\n        return forms.stream()\n                .filter(form -> waitingEvents.contains(form.eventName()))\n                .sorted(Comparator.comparing(Form::name).thenComparing(Form::eventName))\n                .collect(Collectors.toList());\n    }\n\n    static void assertSupported(Path workDir, Collection<Form> forms) {\n        for (var form : forms) {\n            var customAssets = customAssetsPath(workDir, form.name());\n            if (Files.exists(customAssets)) {\n                throw new IllegalArgumentException(\"Custom form assets are not supported in local CLI resume: \" + customAssets);\n            }\n        }\n    }\n\n    static Set<String> formEvents(Collection<Form> forms) {\n        return forms.stream()\n                .map(Form::eventName)\n                .collect(Collectors.toCollection(TreeSet::new));\n    }\n\n    static Path formFilesDir(Path workDir) {\n        return workDir.resolve(Constants.Files.FORM_FILES);\n    }\n\n    private static Path formsDir(Path workDir) {\n        return workDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(Constants.Files.JOB_STATE_DIR_NAME)\n                .resolve(Constants.Files.JOB_FORMS_V2_DIR_NAME);\n    }\n\n    private static Path formPath(Path workDir, String formName) {\n        return formsDir(workDir).resolve(formName);\n    }\n\n    private static Path customAssetsPath(Path workDir, String formName) {\n        return workDir.resolve(Constants.Files.JOB_FORMS_DIR_NAME).resolve(formName);\n    }\n\n    private LocalFormState() {\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/LocalSuspendPersistence.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.cli.CliConfig.CliConfigContext;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.TemporaryPath;\nimport com.walmartlabs.concord.forms.Form;\nimport com.walmartlabs.concord.runtime.common.StateManager;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.runtime.v2.runner.ProcessSnapshot;\nimport com.walmartlabs.concord.runtime.v2.runner.guice.ObjectMapperProvider;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.svm.ThreadStatus;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collection;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.TreeSet;\n\nfinal class LocalSuspendPersistence {\n\n    private static final String METADATA_FILE_NAME = \"_cliResume.json\";\n\n    static boolean isSuspended(ProcessSnapshot snapshot) {\n        return snapshot.vmState().threadStatus().entrySet().stream()\n                .anyMatch(e -> e.getValue() == ThreadStatus.SUSPENDED);\n    }\n\n    static void save(Path workDir, ProcessSnapshot snapshot, ResumeMetadata metadata) throws IOException {\n        var events = getEvents(snapshot);\n        StateManager.finalizeSuspendedState(workDir, snapshot, events);\n        writeMetadata(workDir, metadata);\n    }\n\n    static ResumeMetadata readMetadata(Path workDir) throws IOException {\n        var metadataPath = metadataPath(workDir);\n        if (Files.notExists(metadataPath)) {\n            return null;\n        }\n\n        try {\n            return objectMapper().readValue(metadataPath.toFile(), ResumeMetadata.class);\n        } catch (IOException e) {\n            throw new IOException(\"Error while reading CLI resume metadata: \" + e.getMessage(), e);\n        }\n    }\n\n    static Set<String> readWaitingEvents(Path workDir) throws IOException {\n        var suspendMarker = suspendMarkerPath(workDir);\n        if (Files.notExists(suspendMarker)) {\n            return null;\n        }\n\n        return new LinkedHashSet<>(Files.readAllLines(suspendMarker));\n    }\n\n    static boolean hasSnapshot(Path workDir) {\n        return Files.exists(snapshotPath(workDir));\n    }\n\n    static boolean hasMetadata(Path workDir) {\n        return Files.exists(metadataPath(workDir));\n    }\n\n    static void cleanup(Path workDir) throws IOException {\n        StateManager.cleanupState(workDir);\n    }\n\n    static void printResumeGuidance(Path resumeDir,\n                                    Set<String> events,\n                                    Collection<Form> pendingForms,\n                                    boolean interactiveAvailable) {\n        LocalSuspendPrinter.printSuspendGuidance(resumeDir, events, pendingForms, interactiveAvailable);\n    }\n\n    static Set<String> getEvents(ProcessSnapshot snapshot) {\n        var eventRefs = snapshot.vmState().getEventRefs().values();\n\n        var events = new TreeSet<>(eventRefs);\n        if (events.size() != eventRefs.size()) {\n            throw new IllegalStateException(\"Non-unique event refs: \" + eventRefs + \". This is most likely a bug.\");\n        }\n\n        return events;\n    }\n\n    private static void writeMetadata(Path workDir, ResumeMetadata metadata) throws IOException {\n        var metadataPath = metadataPath(workDir);\n        var stateDir = metadataPath.getParent();\n        if (Files.notExists(stateDir)) {\n            Files.createDirectories(stateDir);\n        }\n\n        try (TemporaryPath tmp = PathUtils.tempFile(\"cli-resume\", \".json\")) {\n            objectMapper().writerWithDefaultPrettyPrinter().writeValue(tmp.path().toFile(), metadata);\n            Files.move(tmp.path(), metadataPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n        }\n    }\n\n    private static ObjectMapper objectMapper() {\n        return new ObjectMapperProvider().get();\n    }\n\n    private static Path stateDir(Path workDir) {\n        return workDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(Constants.Files.JOB_STATE_DIR_NAME);\n    }\n\n    private static Path metadataPath(Path workDir) {\n        return stateDir(workDir).resolve(METADATA_FILE_NAME);\n    }\n\n    private static Path suspendMarkerPath(Path workDir) {\n        return stateDir(workDir).resolve(Constants.Files.SUSPEND_MARKER_FILE_NAME);\n    }\n\n    private static Path snapshotPath(Path workDir) {\n        return stateDir(workDir).resolve(\"instance\");\n    }\n\n    record ResumeMetadata(ProcessConfiguration processConfiguration,\n                          RunnerConfiguration runnerConfiguration,\n                          List<String> activeProfiles,\n                          String resumeDir,\n                          String workDir,\n                          String defaultTaskVars,\n                          String depsCacheDir,\n                          CliConfigData cliConfig) {\n\n        static ResumeMetadata from(Path workDir,\n                                   Path resumeDir,\n                                   Path defaultTaskVars,\n                                   Path depsCacheDir,\n                                   String contextName,\n                                   CliConfig.Overrides cliConfigOverrides,\n                                   List<String> activeProfiles,\n                                   ProcessConfiguration processConfiguration,\n                                   RunnerConfiguration runnerConfiguration) {\n\n            return new ResumeMetadata(processConfiguration,\n                    runnerConfiguration,\n                    List.copyOf(activeProfiles),\n                    pathToString(resumeDir),\n                    pathToString(workDir),\n                    pathToString(defaultTaskVars),\n                    pathToString(depsCacheDir),\n                    CliConfigData.from(contextName, cliConfigOverrides));\n        }\n\n        CliConfigContext loadCliConfigContext(Verbosity verbosity) throws Exception {\n            return Objects.requireNonNull(cliConfig, \"cliConfig\").load(verbosity, resumeDirPath());\n        }\n\n        Path defaultTaskVarsPath() {\n            return stringToPath(defaultTaskVars, resumeDirPath());\n        }\n\n        Path depsCacheDirPath() {\n            return stringToPath(depsCacheDir, resumeDirPath());\n        }\n\n        Path resumeDirPath() {\n            if (resumeDir != null) {\n                return Path.of(resumeDir);\n            }\n            var path = Path.of(workDir);\n            var parent = path.getParent();\n            if (parent != null && CliPaths.DEFAULT_TARGET_DIR_NAME.equals(path.getFileName().toString())) {\n                return parent;\n            }\n            return path;\n        }\n    }\n\n    record CliConfigData(String contextName,\n                         boolean requiresUserConfig,\n                         String secretStoreDir,\n                         String vaultDir,\n                         String vaultId) {\n\n        static CliConfigData from(String contextName, CliConfig.Overrides overrides) {\n            return new CliConfigData(contextName,\n                    CliConfig.hasUserConfig(),\n                    pathToString(overrides.secretStoreDir()),\n                    pathToString(overrides.vaultDir()),\n                    overrides.vaultId());\n        }\n\n        CliConfigContext load(Verbosity verbosity, Path fallbackBaseDir) throws Exception {\n            try {\n                if (requiresUserConfig && !CliConfig.hasUserConfig()) {\n                    throw new IllegalArgumentException(\"CLI configuration file is missing from ~/.concord. Resume requires the stored '\" + contextName + \"' context.\");\n                }\n                return CliConfig.loadOrThrow(verbosity, contextName, toOverrides(fallbackBaseDir));\n            } catch (Exception e) {\n                throw new IllegalArgumentException(\"Unable to reload CLI configuration context '\" + contextName + \"' for resume: \" + e.getMessage(), e);\n            }\n        }\n\n        private CliConfig.Overrides toOverrides(Path fallbackBaseDir) {\n            return new CliConfig.Overrides(stringToPath(secretStoreDir, fallbackBaseDir), stringToPath(vaultDir, fallbackBaseDir), vaultId);\n        }\n    }\n\n    private static String pathToString(Path path) {\n        return path != null ? path.normalize().toAbsolutePath().toString() : null;\n    }\n\n    private static Path stringToPath(String value, Path fallbackBaseDir) {\n        if (value == null) {\n            return null;\n        }\n\n        var path = Path.of(value);\n        if (path.isAbsolute()) {\n            return path.normalize();\n        }\n\n        return fallbackBaseDir.resolve(path).normalize().toAbsolutePath();\n    }\n\n    private LocalSuspendPersistence() {\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/LocalSuspendPrinter.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLFactory;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;\nimport com.walmartlabs.concord.forms.Form;\nimport com.walmartlabs.concord.forms.FormField;\nimport com.walmartlabs.concord.forms.FormFields;\n\nimport java.io.Serializable;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.TreeSet;\n\nfinal class LocalSuspendPrinter {\n\n    private static final ObjectMapper YAML_OBJECT_MAPPER = new ObjectMapper(new YAMLFactory()\n            .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER));\n\n    static void printSuspendGuidance(Path resumeDir,\n                                     Set<String> events,\n                                     Collection<Form> pendingForms,\n                                     boolean interactiveAvailable) {\n\n        System.out.println(\"Process suspended.\");\n        System.out.println();\n        printResumeContext(resumeDir, System.out);\n        System.out.println();\n        printPendingForms(pendingForms);\n        printNonInteractiveSupport(pendingForms, interactiveAvailable, System.out);\n        printAdditionalEvents(additionalEvents(events, pendingForms));\n        printContinueWith(pendingForms, additionalEvents(events, pendingForms), interactiveAvailable, false);\n    }\n\n    static void printInputRequired(Path resumeDir,\n                                   Set<String> events,\n                                   Collection<Form> pendingForms,\n                                   boolean interactiveAvailable) {\n\n        printResumeContext(resumeDir, System.err);\n        System.err.println();\n        if (pendingForms.size() == 1) {\n            System.err.println(\"Pending form requires input in non-interactive mode.\");\n        } else {\n            System.err.println(\"Pending forms require input or explicit event selection.\");\n        }\n        System.err.println();\n        printPendingForms(pendingForms, System.err);\n        printNonInteractiveSupport(pendingForms, interactiveAvailable, System.err);\n        printAdditionalEvents(additionalEvents(events, pendingForms), System.err);\n        printContinueWith(pendingForms, additionalEvents(events, pendingForms), interactiveAvailable, true);\n    }\n\n    static void printDescribeSelectionRequired(Path resumeDir,\n                                               Set<String> events,\n                                               Collection<Form> pendingForms) {\n\n        printResumeContext(resumeDir, System.err);\n        System.err.println();\n        System.err.println(\"Pending forms require explicit event selection before describing input.\");\n        System.err.println();\n        printPendingForms(pendingForms, System.err);\n        printAdditionalEvents(additionalEvents(events, pendingForms), System.err);\n        System.err.println(\"Continue with:\");\n        System.err.println(\"  Describe input:\");\n        for (var form : pendingForms) {\n            System.err.println(\"    \" + describeInputCommand(form));\n        }\n    }\n\n    static void printEventSelectionRequired(Path resumeDir, Set<String> events) {\n        printResumeContext(resumeDir, System.err);\n        System.err.println();\n        System.err.println(\"Multiple waiting events require explicit event selection.\");\n        System.err.println();\n        printAdditionalEvents(new TreeSet<>(events), System.err);\n        System.err.println(\"Continue with:\");\n        System.err.println(\"  Resume event:\");\n        for (var event : new TreeSet<>(events)) {\n            System.err.println(\"    \" + resumeCommand() + \" --event \" + shellQuote(event));\n        }\n    }\n\n    static void printDescribeInput(Path resumeDir, Form form) throws Exception {\n        printResumeDir(resumeDir);\n        System.out.println(\"Pending form input:\");\n        System.out.println(\"  \" + formMapping(form));\n\n        var requiredFields = userFields(form).stream()\n                .filter(f -> !isOptional(f))\n                .toList();\n        var optionalFields = userFields(form).stream()\n                .filter(LocalSuspendPrinter::isOptional)\n                .toList();\n        var fileFields = userFields(form).stream()\n                .filter(LocalSuspendPrinter::isFileField)\n                .toList();\n\n        printFieldList(\"Required fields:\", requiredFields, System.out);\n        printFieldList(\"Optional fields:\", optionalFields, System.out);\n\n        if (!fileFields.isEmpty()) {\n            System.out.println(\"File-upload fields:\");\n            for (var field : fileFields) {\n                System.out.println(\"  \" + field.name());\n            }\n            System.out.println(\"Non-interactive submission:\");\n            System.out.println(\"  not supported for file-upload fields\");\n        }\n\n        var example = examplePayload(form);\n        if (!example.isEmpty()) {\n            System.out.println(\"Example input file:\");\n            for (var line : YAML_OBJECT_MAPPER.writeValueAsString(example).stripTrailing().split(\"\\\\R\")) {\n                System.out.println(\"  \" + line);\n            }\n        }\n    }\n\n    static void printUnsupportedNonInteractiveForm(Path resumeDir, Form form, boolean interactiveAvailable) {\n        printResumeContext(resumeDir, System.err);\n        System.err.println();\n        System.err.println(\"Pending form cannot be submitted non-interactively because it contains file-upload fields.\");\n        System.err.println();\n        printPendingForms(List.of(form), System.err);\n        System.err.println(\"Continue with:\");\n        System.err.println(\"  Describe input:\");\n        System.err.println(\"    \" + describeInputCommand(form));\n        if (interactiveAvailable) {\n            System.err.println(\"  Fill interactively:\");\n            System.err.println(\"    \" + resumeCommand());\n        }\n    }\n\n    static boolean supportsNonInteractiveInput(Form form) {\n        return userFields(form).stream().noneMatch(LocalSuspendPrinter::isFileField);\n    }\n\n    private static void printContinueWith(Collection<Form> pendingForms,\n                                          Set<String> additionalEvents,\n                                          boolean interactiveAvailable,\n                                          boolean toErr) {\n\n        var out = toErr ? System.err : System.out;\n        out.println(\"Continue with:\");\n\n        if (interactiveAvailable && !pendingForms.isEmpty()) {\n            out.println(\"  Fill interactively:\");\n            out.println(\"    \" + resumeCommand());\n        }\n\n        if (!pendingForms.isEmpty()) {\n            out.println(\"  Describe input:\");\n            for (var form : pendingForms) {\n                out.println(\"    \" + describeInputCommand(form));\n            }\n        }\n\n        if (pendingForms.stream().anyMatch(LocalSuspendPrinter::supportsNonInteractiveInput)) {\n            out.println(\"  Submit input:\");\n        }\n        for (var form : pendingForms) {\n            if (!supportsNonInteractiveInput(form)) {\n                continue;\n            }\n\n            out.println(\"    \" + inputFileCommand(form));\n        }\n\n        if (!additionalEvents.isEmpty()) {\n            out.println(\"  Resume event:\");\n            for (var event : additionalEvents) {\n                out.println(\"    \" + resumeCommand() + \" --event \" + shellQuote(event));\n            }\n        }\n    }\n\n    private static void printResumeContext(Path resumeDir, java.io.PrintStream out) {\n        out.println(\"Resume dir: \" + resumeDir.normalize().toAbsolutePath());\n        out.println(\"Commands below assume you are in that directory.\");\n    }\n\n    private static void printResumeDir(Path resumeDir) {\n        System.out.println(\"Resume dir: \" + resumeDir.normalize().toAbsolutePath());\n    }\n\n    private static void printPendingForms(Collection<Form> pendingForms) {\n        printPendingForms(pendingForms, System.out);\n    }\n\n    private static void printPendingForms(Collection<Form> pendingForms, java.io.PrintStream out) {\n        if (pendingForms.isEmpty()) {\n            return;\n        }\n\n        out.println(\"Pending forms:\");\n        var formKeyWidth = \"Form key\".length();\n        for (var form : pendingForms) {\n            formKeyWidth = Math.max(formKeyWidth, form.name().length());\n        }\n\n        out.printf(\"  %-\" + formKeyWidth + \"s  %s%n\", \"Form key\", \"Event ID\");\n        for (var form : pendingForms) {\n            out.printf(\"  %-\" + formKeyWidth + \"s  %s%n\", form.name(), form.eventName());\n        }\n        out.println();\n    }\n\n    private static void printAdditionalEvents(Set<String> events) {\n        printAdditionalEvents(events, System.out);\n    }\n\n    private static void printAdditionalEvents(Set<String> events, java.io.PrintStream out) {\n        if (events.isEmpty()) {\n            return;\n        }\n\n        out.println(\"Additional waiting events:\");\n        for (var event : events) {\n            out.println(\"  \" + event);\n        }\n        out.println();\n    }\n\n    private static void printFieldList(String header, List<FormField> fields, java.io.PrintStream out) {\n        if (fields.isEmpty()) {\n            return;\n        }\n\n        out.println(header);\n        for (var field : fields) {\n            out.println(\"  \" + field.name());\n        }\n    }\n\n    private static void printNonInteractiveSupport(Collection<Form> pendingForms,\n                                                   boolean interactiveAvailable,\n                                                   java.io.PrintStream out) {\n        if (interactiveAvailable || pendingForms.size() != 1) {\n            return;\n        }\n\n        var form = pendingForms.iterator().next();\n        if (supportsNonInteractiveInput(form)) {\n            return;\n        }\n\n        out.println(\"Non-interactive submission:\");\n        out.println(\"  not supported for file-upload fields\");\n        out.println();\n    }\n\n    private static String formMapping(Form form) {\n        return form.name() + \" -> \" + form.eventName();\n    }\n\n    private static Set<String> additionalEvents(Set<String> events, Collection<Form> pendingForms) {\n        var result = new TreeSet<>(events);\n        result.removeAll(LocalFormState.formEvents(pendingForms));\n        return result;\n    }\n\n    private static String describeInputCommand(Form form) {\n        return resumeCommand() + \" --event \" + shellQuote(form.eventName()) + \" --describe-input\";\n    }\n\n    private static String inputFileCommand(Form form) {\n        return resumeCommand() + \" --event \" + shellQuote(form.eventName()) + \" --input-file \" + shellQuote(exampleInputFileName(form));\n    }\n\n    private static List<FormField> userFields(Form form) {\n        return form.fields().stream()\n                .filter(f -> !Boolean.TRUE.equals(f.getOption(FormFields.CommonFieldOptions.READ_ONLY)))\n                .toList();\n    }\n\n    private static boolean isOptional(FormField field) {\n        return field.cardinality() == FormField.Cardinality.ONE_OR_NONE || field.cardinality() == FormField.Cardinality.ANY;\n    }\n\n    private static boolean isRepeated(FormField field) {\n        return field.cardinality() == FormField.Cardinality.ANY || field.cardinality() == FormField.Cardinality.AT_LEAST_ONE;\n    }\n\n    private static boolean isFileField(FormField field) {\n        return FormFields.FileField.TYPE.equals(field.type());\n    }\n\n    private static String exampleInputFileName(Form form) {\n        return form.name().replaceAll(\"[^A-Za-z0-9._-]+\", \"_\") + \".yml\";\n    }\n\n    private static Object firstAllowedValue(Serializable value) {\n        if (value instanceof List<?> l && !l.isEmpty()) {\n            return l.get(0);\n        }\n        return value;\n    }\n\n    private static Map<String, Object> examplePayload(Form form) {\n        var values = new LinkedHashMap<String, Object>();\n        for (var field : userFields(form)) {\n            values.put(field.name(), exampleValue(field));\n        }\n\n        if (values.isEmpty()) {\n            return Map.of();\n        }\n\n        return Map.of(form.name(), values);\n    }\n\n    private static Object exampleValue(FormField field) {\n        var baseValue = field.defaultValue() != null ? field.defaultValue() : firstAllowedValue(field.allowedValue());\n        if (baseValue == null) {\n            baseValue = switch (field.type()) {\n                case \"boolean\" -> Boolean.TRUE;\n                case \"int\" -> 0;\n                case \"decimal\" -> 0.0d;\n                case \"file\" -> \"path/to/file\";\n                default -> \"\";\n            };\n        }\n\n        if (!isRepeated(field)) {\n            return baseValue;\n        }\n\n        if (baseValue instanceof Collection<?> c) {\n            return new ArrayList<>(c);\n        }\n\n        return List.of(baseValue);\n    }\n\n    private static String resumeCommand() {\n        return \"concord resume\";\n    }\n\n    private static String shellQuote(String value) {\n        if (value.matches(\"[A-Za-z0-9_./:=+-]+\")) {\n            return value;\n        }\n\n        return \"'\" + value.replace(\"'\", \"'\\\"'\\\"'\") + \"'\";\n    }\n\n    private LocalSuspendPrinter() {\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/Main.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.fusesource.jansi.AnsiConsole;\nimport picocli.CommandLine;\n\npublic class Main {\n\n    public static void main(String[] args) {\n        AnsiConsole.systemInstall();\n\n        CommandLine cli = new CommandLine(new App());\n        int code = cli.execute(args);\n        System.exit(code);\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/PromptSupport.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nfinal class PromptSupport {\n\n    static final String ALLOW_STDIN_PROMPTS_PROPERTY = \"concord.cli.allowStdInPrompts\";\n\n    static boolean canPromptInteractively() {\n        return System.console() != null || Boolean.getBoolean(ALLOW_STDIN_PROMPTS_PROPERTY);\n    }\n\n    private PromptSupport() {\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/RemoteRun.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.walmartlabs.concord.cli.secrets.CliSecretService;\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.client2.ApiException;\nimport com.walmartlabs.concord.client2.ProcessApi;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.TemporaryPath;\nimport com.walmartlabs.concord.common.ZipUtils;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;\nimport picocli.CommandLine.Command;\nimport picocli.CommandLine.Option;\nimport picocli.CommandLine.Parameters;\n\nimport java.io.IOException;\nimport java.net.http.HttpClient;\nimport java.nio.file.*;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.concurrent.Callable;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.fusesource.jansi.Ansi.ansi;\n\n@Command(name = \"remote-run\", description = \"Execute flows remotely. Sends the specified <workDir> as the process payload.\")\npublic class RemoteRun implements Callable<Integer> {\n    private static final long LARGE_PAYLOAD_SIZE_BYTES = 16 * 1024 * 1024;\n    private static final String LARGE_PAYLOAD_SIZE_HUMAN = \"16MB\";\n\n    @Option(names = {\"--context\"}, description = \"Configuration context to use\")\n    String context = \"default\";\n\n    @Option(names = {\"-v\", \"--verbose\"}, description = \"Use verbose output\")\n    boolean[] verbosity = new boolean[0];\n\n    @Option(names = {\"--profiles\"}, description = \"A comma-separated list of Concord profiles to use\", split = \",\")\n    String[] activeProfiles;\n\n    @Option(names = {\"--org\"}, description = \"Start the process in the specified Concord organization\")\n    String orgName;\n\n    @Option(names = {\"--project\"}, description = \"Start the process in the specified Concord project\")\n    String projectName;\n\n    @Option(names = {\"--entry-point\"}, description = \"Name of the starting flow\")\n    String entryPoint;\n\n    @Option(names = {\"--args\"}, description = \"Process arguments (flow variables)\")\n    Map<String, String> processArgs = new LinkedHashMap<>();\n\n    @Option(names = {\"--cfg\"}, description = \"Process configuration in JSON format (dependencies, runtime, arguments, etc)\")\n    String processCfg;\n\n    @Option(names = {\"--no-gitignore\"}, description = \"Do not use .gitignore patterns when filtering files\")\n    boolean noGitIgnore = false;\n\n    @Parameters(arity = \"1\", description = \"A path to a single concord.yaml (or .yml) file or a directory with flows\")\n    Path workDir = Paths.get(System.getProperty(\"user.dir\"));\n\n    @Override\n    public Integer call() {\n        var verbosity = new Verbosity(this.verbosity);\n\n        var mapper = new ObjectMapper();\n        if (processCfg != null && !processCfg.isBlank()) {\n            try {\n                mapper.readValue(processCfg, ObjectNode.class);\n            } catch (JsonProcessingException e) {\n                return err(\"Expected a valid JSON object in <cfg>, got: \" + processCfg);\n            }\n        }\n\n        var configContext = CliConfig.load(verbosity, context, null);\n\n        var remoteRunConfig = configContext.remoteRun();\n        if (remoteRunConfig == null) {\n            return missingConfig(context, \"remoteRun\");\n        }\n        if (remoteRunConfig.baseUrl() == null) {\n            return missingConfig(context, \"remoteRun.baseUrl\");\n        }\n        if (remoteRunConfig.apiKeyRef() == null) {\n            return missingConfig(context, \"remoteRun.apiKeyRef\");\n        }\n\n        var secretService = CliSecretService.create(configContext, workDir, verbosity);\n        String apiKey;\n        try {\n            apiKey = secretService.exportAsString(remoteRunConfig.apiKeyRef().orgName(), remoteRunConfig.apiKeyRef().secretName(), null);\n        } catch (Exception e) {\n            return err(\"Unable to fetch the API key. \" + e.getMessage());\n        }\n\n        var client = new ApiClient(HttpClient.newBuilder().build())\n                .setBaseUrl(remoteRunConfig.baseUrl())\n                .setApiKey(apiKey);\n\n        var processApi = new ProcessApi(client);\n\n        info(\"Preparing the payload...\");\n\n        if (!Files.exists(workDir)) {\n            return err(\"<workDir> not found: \" + workDir);\n        }\n\n        try {\n            var payloadSize = getDirectorySize(workDir);\n            if (payloadSize >= LARGE_PAYLOAD_SIZE_BYTES) {\n                if (!Confirmation.confirm(\"The specified <workDir> is larger than %s. Continue? (y/N)\".formatted(LARGE_PAYLOAD_SIZE_HUMAN))) {\n                    return err(\"Aborting.\");\n                }\n            }\n        } catch (IOException e) {\n            return err(\"Unable to determine the payload size: \" + e.getMessage());\n        }\n\n        try (var archive = prepareArchive(workDir, noGitIgnore)) {\n            var input = new HashMap<String, Object>();\n            input.put(\"archive\", archive.path());\n            if (orgName != null) {\n                input.put(Constants.Multipart.ORG_NAME, orgName);\n            }\n            if (projectName != null) {\n                input.put(Constants.Multipart.PROJECT_NAME, projectName);\n            }\n            if (activeProfiles != null) {\n                input.put(Constants.Request.ACTIVE_PROFILES_KEY, activeProfiles);\n            }\n            if (entryPoint != null) {\n                input.put(Constants.Request.ENTRY_POINT_KEY, entryPoint);\n            }\n            if (processArgs != null) {\n                processArgs.forEach((key, value) -> input.put(Constants.Request.ARGUMENTS_KEY + \".\" + key, value));\n            }\n            if (processCfg != null && !processCfg.isBlank()) {\n                input.put(\"request\", processCfg.getBytes(UTF_8));\n            }\n\n            info(\"Starting a new process...\");\n            var response = processApi.startProcess(input);\n            info(\"Started %s/#/process/%s/log\".formatted(remoteRunConfig.baseUrl(), response.getInstanceId()));\n        } catch (ApiException e) {\n            return handleApiException(e);\n        } catch (IOException e) {\n            return err(\"Failed to start a process. \" + e.getMessage());\n        }\n\n        return 0;\n    }\n\n    private static TemporaryPath prepareArchive(Path src, boolean noGitIgnore) throws IOException {\n        var badSrc = false;\n\n        if (Files.isRegularFile(src)) {\n            var fileName = src.getFileName().toString();\n            badSrc = !fileName.equals(\"concord.yml\") && !fileName.equals(\"concord.yaml\") && !fileName.endsWith(\".yml\") && !fileName.endsWith(\".yaml\");\n        } else if (!Files.isDirectory(src)) {\n            badSrc = true;\n        }\n\n        if (badSrc) {\n            throw new IOException(\"Expected a path to a single concord.yaml (or .yml) file or a directory with flows, got \" + src);\n        }\n\n        var dst = PathUtils.tempFile(\"payload\", \".zip\");\n\n        try (var zip = new ZipArchiveOutputStream(dst.path(), StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {\n            if (Files.isRegularFile(src)) {\n                ZipUtils.zipFile(zip, src, src.getFileName().toString());\n            } else if (Files.isDirectory(src)) {\n                GitIgnoreFilter gitIgnoreFilter = noGitIgnore ? null : GitIgnoreFilter.load(src);\n                zipWithGitIgnore(zip, src, gitIgnoreFilter);\n            }\n        }\n\n        return dst;\n    }\n\n    private static void zipWithGitIgnore(ZipArchiveOutputStream zip, Path srcDir,\n                                          GitIgnoreFilter filter) throws IOException {\n        Files.walkFileTree(srcDir, new SimpleFileVisitor<>() {\n            @Override\n            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {\n                if (dir.equals(srcDir)) {\n                    return FileVisitResult.CONTINUE;\n                }\n\n                String name = dir.getFileName().toString();\n                // Always skip .git and .concord directories\n                if (name.equals(\".git\") || name.equals(\".concord\")) {\n                    return FileVisitResult.SKIP_SUBTREE;\n                }\n                // Check gitignore\n                Path rel = srcDir.relativize(dir);\n                if (filter != null && filter.isIgnored(rel, true)) {\n                    return FileVisitResult.SKIP_SUBTREE;\n                }\n                return FileVisitResult.CONTINUE;\n            }\n\n            @Override\n            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {\n                Path rel = srcDir.relativize(file);\n                if (filter != null && filter.isIgnored(rel, false)) {\n                    return FileVisitResult.CONTINUE;\n                }\n                String name = rel.toString();\n                ZipUtils.zipFile(zip, file, name);\n                return FileVisitResult.CONTINUE;\n            }\n        });\n    }\n\n    private static long getDirectorySize(Path src) throws IOException {\n        try (var walker = Files.walk(src)) {\n            return walker.filter(Files::isRegularFile)\n                    .mapToLong(path -> {\n                        try {\n                            return Files.size(path);\n                        } catch (IOException e) {\n                            throw new RuntimeException(e);\n                        }\n                    }).sum();\n        }\n    }\n\n    private static int handleApiException(ApiException apiException) {\n        if (apiException.getCode() != 400) {\n            return err(\"Failed to start a process. \" + apiException.getMessage());\n        }\n\n        var contentType = apiException.getResponseHeaders().firstValue(\"Content-Type\");\n        if (contentType.filter(t -> t.contains(\"vnd.concord-validation-errors-v1+json\")).isEmpty()) {\n            return unexpectedErrorBody(apiException);\n        }\n\n        JsonNode validationErrors;\n        try {\n            var mapper = new ObjectMapper();\n            validationErrors = mapper.readTree(apiException.getResponseBody());\n        } catch (IOException e) {\n            return unexpectedErrorBody(apiException);\n        }\n\n        if (validationErrors == null || !validationErrors.isArray()) {\n            return unexpectedErrorBody(apiException);\n        }\n\n        var text = new StringBuilder();\n        for (var node : validationErrors) {\n            if (!node.isObject()) {\n                return unexpectedErrorBody(apiException);\n            }\n            var message = node.get(\"message\");\n            if (message != null && !message.asText().isBlank()) {\n                text.append(message.asText());\n            }\n        }\n\n        validationErrors.forEach(e -> text.append(e.asText()).append(\". \"));\n        return err(\"Failed to start a process. \" + text);\n    }\n\n    private static void info(String msg) {\n        System.out.println(msg);\n    }\n\n    private static int unexpectedErrorBody(ApiException e) {\n        warn(\"Unable to parse the API error response body: \" + e.getResponseBody());\n        return err(\"Failed to start a process. \" + e.getMessage());\n    }\n\n    private static int missingConfig(String context, String key) {\n        return err(\"Missing '%s' configuration in the '%s' context\".formatted(key, context));\n    }\n\n    private static void warn(String msg) {\n        System.out.println(ansi().fgYellow().a(msg).reset());\n    }\n\n    private static int err(String msg) {\n        System.out.println(ansi().fgRed().a(msg).reset());\n        return -1;\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/Resume.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLFactory;\nimport com.google.inject.Key;\nimport com.google.inject.name.Names;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.forms.Form;\nimport com.walmartlabs.concord.runtime.common.StateManager;\nimport com.walmartlabs.concord.runtime.v2.runner.ProcessSnapshot;\nimport com.walmartlabs.concord.runtime.v2.runner.Runner;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskProviders;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.ParallelExecutionException;\nimport com.walmartlabs.concord.runtime.v2.sdk.UserDefinedException;\nimport picocli.CommandLine.Command;\nimport picocli.CommandLine.Option;\nimport picocli.CommandLine.Parameters;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.Callable;\n\n@Command(name = \"resume\", description = \"Resume a previously suspended local runtime-v2 workspace.\")\npublic class Resume implements Callable<Integer> {\n\n    private static final ObjectMapper INPUT_OBJECT_MAPPER = new ObjectMapper(new YAMLFactory());\n    private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {\n    };\n\n    @Option(names = {\"-h\", \"--help\"}, usageHelp = true, description = \"display the command's help message\")\n    boolean helpRequested = false;\n\n    @Option(names = {\"-v\", \"--verbose\"}, description = {\n            \"Specify multiple -v options to increase verbosity. For example, `-v -v -v` or `-vvv`\",\n            \"-v log flow steps\",\n            \"-vv log task input/output args\",\n            \"-vvv runner debug logs\"})\n    boolean[] verbosity = new boolean[0];\n\n    @Option(names = {\"--event\"}, description = \"Waiting event reference to resume\")\n    String event;\n\n    @Option(names = {\"--input-file\"}, description = \"Resume input payload in JSON or YAML format\")\n    Path inputFile;\n\n    @Option(names = {\"-e\", \"--extra-vars\"}, description = \"inline resume input values (key=value)\")\n    Map<String, String> extraVars = new LinkedHashMap<>();\n\n    @Option(names = {\"--save-as\"}, description = \"Wrap the payload under the specified variable path\")\n    String saveAs;\n\n    @Option(names = {\"--describe-input\"}, description = \"Describe the expected input shape for a pending form\")\n    boolean describeInput = false;\n\n    @Option(names = {\"--no-prompt\"}, description = \"Disable interactive prompts and print recovery commands instead\")\n    boolean noPrompt = false;\n\n    @Parameters(arity = \"0..1\", description = \"Prepared workspace directory containing suspended local state (default: current directory or ./target).\")\n    Path workDir;\n\n    @Override\n    public Integer call() throws Exception {\n        var verbosity = new Verbosity(this.verbosity);\n        var workDir = resolveWorkDir();\n\n        if (saveAs != null && inputFile == null && extraVars.isEmpty()) {\n            return err(CliExitCodes.USAGE, \"--save-as requires --input-file or -e/--extra-vars\");\n        }\n        if (describeInput && hasManualInputFlags()) {\n            return err(CliExitCodes.USAGE, \"--describe-input can't be combined with --input-file, -e/--extra-vars or --save-as\");\n        }\n\n        try {\n            var metadata = loadMetadata(workDir);\n            var waitingEvents = loadWaitingEvents(workDir);\n            var pendingForms = LocalFormState.syncPendingForms(workDir, waitingEvents);\n            var resumeDir = metadata.resumeDirPath();\n            var interactiveAvailable = canPromptInteractively();\n            var formMode = usesFormMode(pendingForms);\n\n            if (describeInput) {\n                return describeInput(resumeDir, waitingEvents, pendingForms);\n            }\n\n            if (formMode && !interactiveAvailable) {\n                if (pendingForms.size() == 1\n                        && waitingEvents.size() == 1\n                        && !LocalSuspendPrinter.supportsNonInteractiveInput(pendingForms.get(0))) {\n                    LocalSuspendPrinter.printUnsupportedNonInteractiveForm(resumeDir, pendingForms.get(0), false);\n                    return CliExitCodes.NON_INTERACTIVE_UNSUPPORTED;\n                }\n\n                LocalSuspendPrinter.printInputRequired(resumeDir, waitingEvents, pendingForms, false);\n                return CliExitCodes.INPUT_REQUIRED;\n            }\n\n            var selectedEvent = formMode ? null : selectEvent(resumeDir, waitingEvents, pendingForms);\n            if (!formMode && selectedEvent.exitCode() != CliExitCodes.SUCCESS) {\n                return selectedEvent.exitCode();\n            }\n\n            var dependencyManager = LocalCliRuntime.createDependencyManager(metadata.depsCacheDirPath());\n            var injector = LocalCliRuntime.createInjector(workDir,\n                    metadata.runnerConfiguration(),\n                    metadata.processConfiguration(),\n                    metadata.loadCliConfigContext(verbosity),\n                    metadata.defaultTaskVarsPath(),\n                    dependencyManager,\n                    verbosity);\n\n            LocalCliRuntime.notifyProjectLoaded(workDir);\n\n            if (metadata.processConfiguration().debug()) {\n                System.out.println(\"Available tasks: \" + injector.getInstance(TaskProviders.class).names());\n            }\n\n            var classLoader = injector.getInstance(Key.get(ClassLoader.class, Names.named(\"runtime\")));\n            var snapshot = loadSnapshot(workDir, classLoader);\n            if (snapshot == null) {\n                return err(CliExitCodes.ERROR, \"Missing suspended snapshot in \" + workDir);\n            }\n\n            var runner = injector.getInstance(Runner.class);\n\n            try {\n                if (formMode) {\n                    LocalFormState.assertSupported(workDir, pendingForms);\n                    snapshot = LocalFormSession.resumePendingForms(workDir, runner, snapshot, metadata);\n                } else {\n                    var selectedForm = findPendingForm(pendingForms, selectedEvent.event());\n                    if (selectedForm != null && !hasManualInputFlags()) {\n                        LocalSuspendPrinter.printInputRequired(resumeDir, waitingEvents, pendingForms, interactiveAvailable);\n                        return CliExitCodes.INPUT_REQUIRED;\n                    }\n                    if (selectedForm != null && !LocalSuspendPrinter.supportsNonInteractiveInput(selectedForm)) {\n                        LocalSuspendPrinter.printUnsupportedNonInteractiveForm(resumeDir, selectedForm, interactiveAvailable);\n                        return CliExitCodes.NON_INTERACTIVE_UNSUPPORTED;\n                    }\n\n                    var input = loadInput();\n                    if (selectedForm != null) {\n                        try {\n                            input = LocalFormInputs.convertAndValidate(selectedForm, input, true).payload(selectedForm);\n                        } catch (LocalFormInputs.InputException e) {\n                            printFormInputErrors(e);\n                            LocalSuspendPrinter.printInputRequired(resumeDir, waitingEvents, pendingForms, interactiveAvailable);\n                            return CliExitCodes.INPUT_REQUIRED;\n                        }\n                    }\n                    snapshot = runner.resume(snapshot, Collections.singleton(selectedEvent.event()), input);\n                }\n            } catch (ParallelExecutionException | UserDefinedException e) {\n                return CliExitCodes.PROCESS_FAILED;\n            } catch (Exception e) {\n                Run.logException(verbosity, e);\n                return CliExitCodes.ERROR;\n            }\n\n            if (LocalSuspendPersistence.isSuspended(snapshot)) {\n                LocalSuspendPersistence.save(workDir, snapshot, metadata);\n                var events = LocalSuspendPersistence.getEvents(snapshot);\n                var nextPendingForms = LocalFormState.syncPendingForms(workDir, events);\n                LocalSuspendPersistence.printResumeGuidance(resumeDir, events, nextPendingForms, interactiveAvailable);\n                return CliExitCodes.SUSPENDED;\n            }\n\n            LocalSuspendPersistence.cleanup(workDir);\n            System.out.println(\"...done!\");\n            return CliExitCodes.SUCCESS;\n        } catch (Exception e) {\n            return err(CliExitCodes.ERROR, e.getMessage());\n        }\n    }\n\n    private int describeInput(Path resumeDir, Set<String> waitingEvents, List<Form> pendingForms) throws Exception {\n        if (pendingForms.isEmpty()) {\n            return err(CliExitCodes.USAGE, \"--describe-input requires a pending form\");\n        }\n\n        if (event == null || event.isBlank()) {\n            if (pendingForms.size() > 1) {\n                LocalSuspendPrinter.printDescribeSelectionRequired(resumeDir, waitingEvents, pendingForms);\n                return CliExitCodes.INPUT_REQUIRED;\n            }\n\n            LocalSuspendPrinter.printDescribeInput(resumeDir, pendingForms.get(0));\n            return CliExitCodes.SUCCESS;\n        }\n\n        var form = findPendingForm(pendingForms, event);\n        if (form == null) {\n            if (waitingEvents.contains(event)) {\n                return err(CliExitCodes.USAGE, \"--describe-input is only available for pending forms\");\n            }\n            return err(CliExitCodes.USAGE, \"Unknown event: \" + event + \". Available events: \" + String.join(\", \", waitingEvents));\n        }\n\n        LocalSuspendPrinter.printDescribeInput(resumeDir, form);\n        return CliExitCodes.SUCCESS;\n    }\n\n    private SelectionResult selectEvent(Path resumeDir, Set<String> waitingEvents, List<Form> pendingForms) {\n        if (event == null || event.isBlank()) {\n            if (waitingEvents.size() == 1) {\n                return SelectionResult.success(waitingEvents.iterator().next());\n            }\n\n            if (!pendingForms.isEmpty()) {\n                LocalSuspendPrinter.printInputRequired(resumeDir, waitingEvents, pendingForms, canPromptInteractively());\n                return SelectionResult.error(CliExitCodes.INPUT_REQUIRED);\n            } else {\n                LocalSuspendPrinter.printEventSelectionRequired(resumeDir, waitingEvents);\n                return SelectionResult.error(CliExitCodes.INPUT_REQUIRED);\n            }\n        }\n\n        if (!waitingEvents.contains(event)) {\n            err(CliExitCodes.USAGE, \"Unknown event: \" + event + \". Available events: \" + String.join(\", \", waitingEvents));\n            return SelectionResult.error(CliExitCodes.USAGE);\n        }\n\n        return SelectionResult.success(event);\n    }\n\n    private Map<String, Object> loadInput() throws Exception {\n        var input = new LinkedHashMap<String, Object>();\n\n        if (inputFile != null) {\n            if (!Files.exists(inputFile)) {\n                throw new IllegalArgumentException(\"Input file not found: \" + inputFile);\n            }\n\n            var node = INPUT_OBJECT_MAPPER.readTree(inputFile.toFile());\n            if (node == null || !node.isObject()) {\n                throw new IllegalArgumentException(\"Expected a JSON or YAML object in \" + inputFile);\n            }\n\n            input.putAll(INPUT_OBJECT_MAPPER.convertValue(node, MAP_TYPE));\n        }\n\n        if (!extraVars.isEmpty()) {\n            input.putAll(ConfigurationUtils.deepMerge(input, loadInlineInput()));\n        }\n\n        if (input.isEmpty()) {\n            return Collections.emptyMap();\n        }\n        if (saveAs == null || saveAs.isBlank()) {\n            return input;\n        }\n\n        return wrapInput(input, saveAs);\n    }\n\n    private Map<String, Object> loadInlineInput() throws Exception {\n        var input = new LinkedHashMap<String, Object>();\n\n        for (var e : extraVars.entrySet()) {\n            var key = e.getKey().trim();\n            if (key.isEmpty()) {\n                throw new IllegalArgumentException(\"Invalid inline input key\");\n            }\n\n            var nested = ConfigurationUtils.toNested(key, parseInlineValue(e.getValue()));\n            input.putAll(ConfigurationUtils.deepMerge(input, nested));\n        }\n\n        return input;\n    }\n\n    private static Object parseInlineValue(String value) throws Exception {\n        return INPUT_OBJECT_MAPPER.readValue(value, Object.class);\n    }\n\n    private static Map<String, Object> wrapInput(Map<String, Object> input, String saveAs) {\n        var segments = saveAs.split(\"\\\\.\");\n        var current = input;\n\n        for (int i = segments.length - 1; i >= 0; i--) {\n            var segment = segments[i].trim();\n            if (segment.isEmpty()) {\n                throw new IllegalArgumentException(\"Invalid --save-as path: \" + saveAs);\n            }\n\n            var wrapper = new LinkedHashMap<String, Object>();\n            wrapper.put(segment, current);\n            current = wrapper;\n        }\n\n        return current;\n    }\n\n    private static int err(int exitCode, String message) {\n        System.err.println(message);\n        return exitCode;\n    }\n\n    private static void printFormInputErrors(LocalFormInputs.InputException e) {\n        for (var message : e.messages()) {\n            System.err.println(\"Invalid form input: \" + message);\n        }\n        System.err.println();\n    }\n\n    private boolean usesFormMode(List<Form> pendingForms) {\n        return !hasGenericManualFlags() && !pendingForms.isEmpty();\n    }\n\n    private boolean hasGenericManualFlags() {\n        return event != null || hasManualInputFlags();\n    }\n\n    private boolean hasManualInputFlags() {\n        return inputFile != null || saveAs != null || !extraVars.isEmpty();\n    }\n\n    private boolean canPromptInteractively() {\n        return !noPrompt && PromptSupport.canPromptInteractively();\n    }\n\n    private static Form findPendingForm(List<Form> pendingForms, String event) {\n        if (event == null) {\n            return null;\n        }\n\n        return pendingForms.stream()\n                .filter(f -> event.equals(f.eventName()))\n                .findFirst()\n                .orElse(null);\n    }\n\n    private Path resolveWorkDir() {\n        var baseDir = workDir != null ? workDir : Paths.get(System.getProperty(\"user.dir\"));\n        var normalized = baseDir.normalize().toAbsolutePath();\n\n        if (hasSuspendedState(normalized)) {\n            return normalized;\n        }\n\n        var targetDir = CliPaths.defaultTargetDir(normalized);\n        if (hasSuspendedState(targetDir)) {\n            return targetDir;\n        }\n\n        return normalized;\n    }\n\n    private static boolean hasSuspendedState(Path workDir) {\n        return LocalSuspendPersistence.hasMetadata(workDir) && LocalSuspendPersistence.hasSnapshot(workDir);\n    }\n\n    private static LocalSuspendPersistence.ResumeMetadata loadMetadata(Path workDir) throws Exception {\n        var metadata = LocalSuspendPersistence.readMetadata(workDir);\n        if (metadata == null) {\n            throw new IllegalArgumentException(\"Missing CLI resume metadata in \" + workDir);\n        }\n        if (!LocalSuspendPersistence.hasSnapshot(workDir)) {\n            throw new IllegalArgumentException(\"Missing suspended snapshot in \" + workDir);\n        }\n        return metadata;\n    }\n\n    private static Set<String> loadWaitingEvents(Path workDir) throws Exception {\n        Set<String> waitingEvents;\n        try {\n            waitingEvents = LocalSuspendPersistence.readWaitingEvents(workDir);\n        } catch (Exception e) {\n            throw new IllegalArgumentException(\"Error while reading waiting events: \" + e.getMessage(), e);\n        }\n\n        if (waitingEvents == null || waitingEvents.isEmpty()) {\n            throw new IllegalArgumentException(\"Missing suspend marker in \" + workDir);\n        }\n        return waitingEvents;\n    }\n\n    private record SelectionResult(String event, int exitCode) {\n\n        static SelectionResult success(String event) {\n            return new SelectionResult(event, CliExitCodes.SUCCESS);\n        }\n\n        static SelectionResult error(int exitCode) {\n            return new SelectionResult(null, exitCode);\n        }\n    }\n\n    private static ProcessSnapshot loadSnapshot(Path workDir, ClassLoader classLoader) {\n        // This branch is the first producer of local CLI suspended state, so direct reads are intentional for now.\n        // Revisit compatibility handling here if the on-disk local suspended-state format changes after release.\n        return StateManager.readProcessState(workDir, classLoader);\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/Run.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLFactory;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;\nimport com.google.inject.Injector;\nimport com.walmartlabs.concord.cli.CliConfig.CliConfigContext;\nimport com.walmartlabs.concord.cli.runner.*;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.common.FileVisitor;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.dependencymanager.DependencyManager;\nimport com.walmartlabs.concord.forms.Form;\nimport com.walmartlabs.concord.imports.*;\nimport com.walmartlabs.concord.runtime.common.cfg.ApiConfiguration;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.runtime.model.EffectiveConfiguration;\nimport com.walmartlabs.concord.runtime.v2.ProjectLoaderV2;\nimport com.walmartlabs.concord.runtime.v2.ProjectSerializerV2;\nimport com.walmartlabs.concord.runtime.v2.model.Flow;\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinitionConfiguration;\nimport com.walmartlabs.concord.runtime.v2.model.Profile;\nimport com.walmartlabs.concord.runtime.v2.runner.ProcessSnapshot;\nimport com.walmartlabs.concord.runtime.v2.runner.Runner;\nimport com.walmartlabs.concord.runtime.v2.sdk.ImmutableProcessConfiguration;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskProviders;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.ParallelExecutionException;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessInfo;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProjectInfo;\nimport com.walmartlabs.concord.runtime.v2.sdk.UserDefinedException;\nimport com.walmartlabs.concord.runtime.v2.wrapper.ProcessDefinitionV2;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport picocli.CommandLine.Command;\nimport picocli.CommandLine.Model.CommandSpec;\nimport picocli.CommandLine.Option;\nimport picocli.CommandLine.Parameters;\nimport picocli.CommandLine.Spec;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.*;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.util.*;\nimport java.util.concurrent.Callable;\nimport java.util.stream.Stream;\n\nimport static org.fusesource.jansi.Ansi.ansi;\n\n@Command(name = \"run\", description = \"Execute flows locally. Sends the specified <workDir> as the process payload.\")\npublic class Run implements Callable<Integer> {\n\n    @Spec\n    private CommandSpec spec;\n\n    @Option(names = {\"-h\", \"--help\"}, usageHelp = true, description = \"display the command's help message\")\n    boolean helpRequested = false;\n\n    @Option(names = {\"--context\"}, description = \"Configuration context to use\")\n    String context = \"default\";\n\n    @Option(names = {\"-e\", \"--extra-vars\"}, description = \"additional process variables\")\n    Map<String, Object> extraVars = new LinkedHashMap<>();\n\n    @Option(names = {\"--default-cfg\"}, description = \"default Concord configuration file\")\n    Path defaultCfg = Paths.get(System.getProperty(\"user.home\")).resolve(\".concord\").resolve(\"defaultCfg.yml\");\n\n    @Option(names = {\"--default-task-vars\"}, description = \"default task variables configuration file\")\n    Path defaultTaskVars = Paths.get(System.getProperty(\"user.home\")).resolve(\".concord\").resolve(\"defaultTaskVars.json\");\n\n    @Option(names = {\"--deps-cache-dir\"}, description = \"process dependencies cache dir\")\n    Path depsCacheDir = Paths.get(System.getProperty(\"user.home\")).resolve(\".concord\").resolve(\"depsCache\");\n\n    @Option(names = {\"--repo-cache-dir\"}, description = \"repository cache dir\")\n    Path repoCacheDir = Paths.get(System.getProperty(\"user.home\")).resolve(\".concord\").resolve(\"repoCache\");\n\n    @Option(names = {\"--secret-dir\"}, description = \"secret store dir\")\n    Path secretStoreDir;\n\n    @Option(names = {\"--vault-dir\"}, description = \"vault dir\")\n    Path vaultDir;\n\n    @Option(names = {\"--vault-id\"}, description = \"vault id\")\n    String vaultId;\n\n    @Option(names = {\"--imports-source\"}, description = \"default imports source\")\n    String importsSource = \"https://github.com\";\n\n    @Option(names = {\"--entry-point\"}, description = \"entry point\")\n    String entryPoint = Constants.Request.DEFAULT_ENTRY_POINT_NAME;\n\n    @Option(names = {\"-p\", \"--profile\"}, description = \"active profile\")\n    List<String> profiles = new ArrayList<>();\n\n    @Option(names = {\"-c\", \"--clean\"}, description = \"remove the target directory before starting the process\")\n    boolean cleanup = false;\n\n    @Option(names = {\"-v\", \"--verbose\"}, description = {\n            \"Specify multiple -v options to increase verbosity. For example, `-v -v -v` or `-vvv`\",\n            \"-v log flow steps\",\n            \"-vv log task input/output args\",\n            \"-vvv runner debug logs\"})\n    boolean[] verbosity = new boolean[0];\n\n    @Option(names = {\"--effective-yaml\"}, description = \"generate the effective YAML (skips execution)\")\n    boolean effectiveYaml = false;\n\n    @Option(names = {\"--default-import-version\"}, description = \"default import version or repo branch\")\n    String defaultVersion = \"main\";\n\n    @Option(names = {\"--no-default-cfg\"}, description = \"Do not load default configuration (including standard dependencies)\")\n    boolean noDefaultCfg = false;\n\n    @Option(names = {\"--no-prompt\"}, description = \"Disable interactive prompts and print recovery commands instead\")\n    boolean noPrompt = false;\n\n    @Option(names = {\"--no-gitignore\"}, description = \"Do not use .gitignore patterns when filtering files\")\n    boolean noGitIgnore = false;\n\n    @Option(names = {\"--target-dir\"}, description = \"Target directory for the assembled payload (default: <sourceDir>/target)\")\n    Path targetDir;\n\n    @Parameters(arity = \"0..1\", description = \"Directory with Concord files or a path to a single Concord YAML file.\")\n    Path sourceDirOrFile = Paths.get(System.getProperty(\"user.dir\"));\n\n    @Option(names = {\"--dry-run\"}, description = \"execute process in dry-run mode?\")\n    boolean dryRunMode = false;\n\n    @Override\n    public Integer call() throws Exception {\n        var verbosity = new Verbosity(this.verbosity);\n        var cliConfigOverrides = new CliConfig.Overrides(secretStoreDir, vaultDir, vaultId);\n        CliConfigContext cliConfigContext = CliConfig.load(verbosity, context, cliConfigOverrides);\n\n        var sourceDir = resolveSourceDir(sourceDirOrFile.normalize().toAbsolutePath());\n        var workDir = prepareWorkDir(sourceDirOrFile.normalize().toAbsolutePath(), verbosity);\n\n        if (!noDefaultCfg) {\n            copyDefaultCfg(workDir, defaultCfg, verbosity.verbose());\n        }\n\n        var dependencyManager = LocalCliRuntime.createDependencyManager(depsCacheDir);\n        var importManager = new ImportManagerFactory(dependencyManager,\n                new CliRepositoryExporter(repoCacheDir), Collections.emptySet())\n                .create();\n\n        ProjectLoaderV2.Result loadResult;\n        try {\n            loadResult = new ProjectLoaderV2(importManager)\n                    .load(workDir, new CliImportsNormalizer(importsSource, verbosity.verbose(), defaultVersion), verbosity.verbose() ? new CliImportsListener() : null);\n        } catch (ImportProcessingException e) {\n            ObjectMapper om = new ObjectMapper();\n            System.err.println(\"Error while processing import \" + om.writeValueAsString(e.getImport()) + \": \" + e.getMessage());\n            return CliExitCodes.PROCESS_FAILED;\n        } catch (Exception e) {\n            System.err.println(\"Error while loading \" + workDir);\n            e.printStackTrace();\n            return CliExitCodes.PROCESS_FAILED;\n        }\n\n        var processDefinition = loadResult.getProjectDefinition();\n        var instanceId = UUID.randomUUID();\n\n        if (verbosity.verbose() && !extraVars.isEmpty()) {\n            System.out.println(\"Additional variables: \" + extraVars);\n        }\n\n        if (verbosity.verbose() && !profiles.isEmpty()) {\n            System.out.println(\"Active profiles: \" + profiles);\n        }\n\n        // \"deps\" are the \"dependencies\" of the last profile in the list of active profiles (if present)\n        var overlayCfg = EffectiveConfiguration.getEffectiveConfiguration(new ProcessDefinitionV2(processDefinition), profiles);\n        List<String> deps = MapUtils.getList(overlayCfg, Constants.Request.DEPENDENCIES_KEY, Collections.emptyList());\n\n        // \"extraDependencies\" are additive: ALL extra dependencies from ALL ACTIVE profiles are added to the list\n        var extraDeps = profiles.stream()\n                .flatMap(profileName -> Stream.ofNullable(processDefinition.profiles().get(profileName)))\n                .flatMap(profile -> profile.configuration().extraDependencies().stream())\n                .toList();\n\n        var allDeps = new ArrayList<String>(deps);\n        allDeps.addAll(extraDeps);\n\n        var resolver = new DependencyResolver(dependencyManager, verbosity.verbose());\n        Collection<String> resolvedDependencies;\n        try {\n            resolvedDependencies = resolver.resolveDeps(allDeps);\n        } catch (Exception e) {\n            System.err.println(e.getMessage());\n            return CliExitCodes.PROCESS_FAILED;\n        }\n\n        var runnerCfg = RunnerConfiguration.builder()\n                .api(buildApiConfiguration(cliConfigContext))\n                .dependencies(resolvedDependencies)\n                .debug(processDefinition.configuration().debug())\n                .build();\n\n        if (verbosity.verbose()) {\n            System.out.println(\"Using '\" + runnerCfg.api().baseUrl() + \"' as API base URL\");\n        }\n\n        Map<String, Object> overlayArgs = MapUtils.getMap(overlayCfg, Constants.Request.ARGUMENTS_KEY, Collections.emptyMap());\n        var args = ConfigurationUtils.deepMerge(processDefinition.configuration().arguments(), overlayArgs, extraVars);\n        args.put(Constants.Context.TX_ID_KEY, instanceId.toString());\n        args.put(Constants.Context.WORK_DIR_KEY, workDir.toAbsolutePath().toString());\n        if (verbosity.verbose()) {\n            dumpArguments(args);\n        }\n\n        if (effectiveYaml) {\n            var flows = new HashMap<String, Flow>(processDefinition.flows());\n            for (var ap : profiles) {\n                var p = processDefinition.profiles().get(ap);\n                if (p != null) {\n                    flows.putAll(p.flows());\n                }\n            }\n\n            var pd = ProcessDefinition.builder().from(processDefinition)\n                    .configuration(ProcessDefinitionConfiguration.builder().from(processDefinition.configuration())\n                            .arguments(args)\n                            .dependencies(allDeps)\n                            .build())\n                    .flows(flows)\n                    .imports(Imports.builder().build())\n                    .profiles(Collections.emptyMap())\n                    .build();\n\n            var serializer = new ProjectSerializerV2();\n            serializer.write(pd, System.out);\n            return CliExitCodes.SUCCESS;\n        }\n\n        System.out.println(ansi().fgBrightGreen().a(\"Starting...\").reset());\n\n        if (dryRunMode) {\n            System.out.println(\"Running in the dry-run mode.\");\n        }\n\n        ProcessInfo processInfo = ProcessInfo.builder()\n                .activeProfiles(profiles)\n                .sessionToken(\"<undefined>\")\n                .build();\n\n        ProcessConfiguration cfg = from(processDefinition.configuration(), processInfo, projectInfo(args))\n                .entryPoint(entryPoint)\n                .instanceId(instanceId)\n                .dryRun(dryRunMode)\n                .build();\n\n        Injector injector = LocalCliRuntime.createInjector(workDir,\n                runnerCfg,\n                cfg,\n                cliConfigContext,\n                defaultTaskVars,\n                dependencyManager,\n                verbosity);\n\n        // Just to notify listeners\n        LocalCliRuntime.notifyProjectLoaded(workDir);\n\n        Runner runner = injector.getInstance(Runner.class);\n\n        if (cfg.debug()) {\n            System.out.println(\"Available tasks: \" + injector.getInstance(TaskProviders.class).names());\n        }\n\n        ProcessSnapshot snapshot;\n        try {\n            snapshot = runner.start(cfg, processDefinition, args);\n        } catch (ParallelExecutionException | UserDefinedException e) {\n            return CliExitCodes.PROCESS_FAILED;\n        } catch (Exception e) {\n            logException(verbosity, e);\n            return CliExitCodes.ERROR;\n        }\n\n        if (LocalSuspendPersistence.isSuspended(snapshot)) {\n            var resumeDir = CliPaths.preferredResumeDir(sourceDir, workDir);\n            var metadata = LocalSuspendPersistence.ResumeMetadata.from(workDir,\n                    resumeDir,\n                    defaultTaskVars,\n                    depsCacheDir,\n                    context,\n                    cliConfigOverrides,\n                    profiles,\n                    cfg,\n                    runnerCfg);\n            LocalSuspendPersistence.save(workDir, snapshot, metadata);\n            Set<String> events = LocalSuspendPersistence.getEvents(snapshot);\n            List<Form> pendingForms = LocalFormState.syncPendingForms(workDir, events);\n            var interactiveAvailable = canPromptInteractively();\n\n            if (pendingForms.isEmpty()) {\n                LocalSuspendPersistence.printResumeGuidance(resumeDir, events, pendingForms, false);\n                return CliExitCodes.SUSPENDED;\n            }\n\n            if (!interactiveAvailable) {\n                LocalSuspendPersistence.printResumeGuidance(resumeDir, events, pendingForms, false);\n                return CliExitCodes.SUSPENDED;\n            }\n\n            if (!Confirmation.confirm(\"Fill pending form now? (Y/n)\", true)) {\n                LocalSuspendPersistence.printResumeGuidance(resumeDir, events, pendingForms, true);\n                return CliExitCodes.SUSPENDED;\n            }\n\n            try {\n                snapshot = LocalFormSession.resumePendingForms(workDir, runner, snapshot, metadata, false);\n            } catch (ParallelExecutionException | UserDefinedException e) {\n                return CliExitCodes.PROCESS_FAILED;\n            } catch (Exception e) {\n                logException(verbosity, e);\n                return CliExitCodes.ERROR;\n            }\n\n            if (!LocalSuspendPersistence.isSuspended(snapshot)) {\n                LocalSuspendPersistence.cleanup(workDir);\n                System.out.println(ansi().fgBrightGreen().a(\"...done!\").reset());\n                return CliExitCodes.SUCCESS;\n            }\n\n            LocalSuspendPersistence.save(workDir, snapshot, metadata);\n            events = LocalSuspendPersistence.getEvents(snapshot);\n            pendingForms = LocalFormState.syncPendingForms(workDir, events);\n            LocalSuspendPersistence.printResumeGuidance(resumeDir, events, pendingForms, true);\n            return CliExitCodes.SUSPENDED;\n        }\n\n        LocalSuspendPersistence.cleanup(workDir);\n        System.out.println(ansi().fgBrightGreen().a(\"...done!\").reset());\n\n        return CliExitCodes.SUCCESS;\n    }\n\n    private boolean canPromptInteractively() {\n        return !noPrompt && PromptSupport.canPromptInteractively();\n    }\n\n    private ApiConfiguration buildApiConfiguration(CliConfigContext cliConfigContext) {\n        CliConfig.RemoteRunConfiguration remoteRun = cliConfigContext.remoteRun();\n        if (remoteRun == null || remoteRun.baseUrl() == null) {\n            return ApiConfiguration.builder().build();\n        }\n\n        return ApiConfiguration.builder()\n                .baseUrl(remoteRun.baseUrl())\n                .build();\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static ProjectInfo projectInfo(Map<String, Object> args) {\n        Object projectInfoObject = args.get(\"projectInfo\");\n        if (projectInfoObject == null) {\n            projectInfoObject = fromExtraVars(\"projectInfo\", args);\n        }\n\n        Map<String, Object> projectInfo = Collections.emptyMap();\n        if (projectInfoObject instanceof Map) {\n            projectInfo = (Map<String, Object>) projectInfoObject;\n        }\n\n        return ProjectInfo.builder()\n                .orgName(MapUtils.getString(projectInfo, \"orgName\"))\n                .projectName(MapUtils.getString(projectInfo, \"projectName\"))\n                .repoName(MapUtils.getString(projectInfo, \"repoName\"))\n                .repoUrl(MapUtils.getString(projectInfo, \"repoUrl\"))\n                .repoBranch(MapUtils.getString(projectInfo, \"repoBranch\"))\n                .repoPath(MapUtils.getString(projectInfo, \"repoPath\"))\n                .repoCommitId(MapUtils.getString(projectInfo, \"repoCommitId\"))\n                .repoCommitAuthor(MapUtils.getString(projectInfo, \"repoCommitAuthor\"))\n                .repoCommitMessage(MapUtils.getString(projectInfo, \"repoCommitMessage\"))\n                .build();\n    }\n\n    private static ImmutableProcessConfiguration.Builder from(ProcessDefinitionConfiguration cfg, ProcessInfo processInfo, ProjectInfo projectInfo) {\n        return ProcessConfiguration.builder()\n                .debug(cfg.debug())\n                .entryPoint(cfg.entryPoint())\n                .arguments(cfg.arguments())\n                .meta(cfg.meta())\n                .events(cfg.events())\n                .processInfo(processInfo)\n                .projectInfo(projectInfo)\n                .out(cfg.out());\n    }\n\n    private static Map<String, Object> fromExtraVars(String key, Map<String, Object> args) {\n        Map<String, Object> result = new HashMap<>();\n        for (String k : args.keySet()) {\n            if (k.startsWith(key + \".\")) {\n                result.put(k.substring(key.length() + 1), args.get(k));\n            }\n        }\n        return result;\n    }\n\n    private static void copyDefaultCfg(Path targetDir, Path defaultCfg, boolean verbose) throws IOException {\n        final Path destDir = targetDir.resolve(\"concord\");\n        final Path destFile = destDir.resolve(\"_defaultCfg.concord.yml\");\n\n        // Don't overwrite existing file is given project dir\n        if (Files.exists(destFile)) {\n            if (verbose) {\n                System.out.println(\"Default configuration already exists: \" + defaultCfg);\n            }\n            return;\n        }\n\n        if (!Files.exists(destDir)) {\n            Files.createDirectory(destDir);\n        }\n\n        if (Files.exists(defaultCfg)) {\n            if (Files.isRegularFile(defaultCfg)) {\n                Files.copy(defaultCfg, destFile);\n            } else {\n                System.err.println(\"Default configuration must be a file!\");\n            }\n        } else {\n            try (InputStream in = Run.class.getClassLoader().getResourceAsStream(\"defaultCfg.yml\")) {\n                if (in == null) {\n                    throw new IllegalStateException(\"Failed to load embedded default concord configuration.\");\n                }\n                Files.copy(in, destFile);\n            }\n        }\n    }\n\n    private Path prepareWorkDir(Path sourceDir, Verbosity verbosity) throws IOException {\n        Path srcDir = resolveSourceDir(sourceDir);\n\n        Path target = targetDir != null\n                ? targetDir.normalize().toAbsolutePath()\n                : CliPaths.defaultTargetDir(srcDir);\n\n        validatePaths(srcDir, target);\n\n        if (cleanup) {\n            cleanTargetDirectory(target, verbosity);\n        }\n\n        if (!Files.exists(target)) {\n            Files.createDirectories(target);\n        }\n\n        if (Files.isRegularFile(sourceDir)) {\n            System.out.println(\"Running a single Concord file: \" + sourceDir);\n            Files.copy(sourceDir, target.resolve(\"concord.yml\"), StandardCopyOption.REPLACE_EXISTING);\n        } else {\n            copySourceToTarget(srcDir, target, verbosity);\n        }\n\n        return target;\n    }\n\n    private static Path resolveSourceDir(Path sourceDir) {\n        if (Files.isRegularFile(sourceDir)) {\n            var parent = sourceDir.getParent();\n            if (parent == null) {\n                throw new IllegalArgumentException(\"Cannot determine parent directory of: \" + sourceDir);\n            }\n            return parent;\n        }\n        if (Files.isDirectory(sourceDir)) {\n            return sourceDir;\n        }\n        throw new IllegalArgumentException(\"Invalid source (not a file or directory): \" + sourceDir);\n    }\n\n    private static void validatePaths(Path src, Path target) {\n        if (target.equals(src)) {\n            throw new IllegalArgumentException(\"Target directory cannot be the same as the source: \" + target);\n        }\n        if (src.startsWith(target)) {\n            throw new IllegalArgumentException(\"Target directory cannot be a parent of the source: \" + target);\n        }\n        if (Files.isSymbolicLink(target)) {\n            throw new IllegalArgumentException(\"Target directory cannot be a symbolic link: \" + target);\n        }\n    }\n\n    private static void cleanTargetDirectory(Path target, Verbosity verbosity) throws IOException {\n        if (Files.exists(target)) {\n            if (verbosity.verbose()) {\n                System.out.println(\"Cleaning target directory: \" + target);\n            }\n            PathUtils.deleteRecursively(target);\n        }\n    }\n\n    private void copySourceToTarget(Path src, Path target, Verbosity verbosity) throws IOException {\n        Path skipDir = target.startsWith(src) ? target : null;\n        GitIgnoreFilter filter = noGitIgnore ? null : GitIgnoreFilter.load(src);\n\n        String targetRelPath = skipDir != null ? src.relativize(target).toString() : null;\n        String displayPath = (targetRelPath != null) ? \"./\" + targetRelPath : target.toString();\n        CopyNotifier notifier = new CopyNotifier(verbosity.verbose() ? 0 : 100, displayPath);\n\n        copyWithGitIgnore(src, target, skipDir, filter, notifier, StandardCopyOption.REPLACE_EXISTING);\n    }\n\n    private static void dumpArguments(Map<String, Object> args) {\n        ObjectMapper om = new ObjectMapper(new YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER));\n        try {\n            System.out.print(ansi().fgYellow().a(\"\\nProcess arguments:\\n\\t\"));\n            System.out.println(om.writerWithDefaultPrettyPrinter().writeValueAsString(args).replace(\"\\n\", \"\\n\\t\"));\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    static void logException(Verbosity verbosity, Exception e) {\n        if (verbosity.verbose()) {\n            System.err.print(ansi().fgBrightRed().a(\"Error: \"));\n            e.printStackTrace(System.err);\n        } else {\n            System.err.println(\"Error: \" + e.getMessage()); // TODO\n        }\n    }\n\n    private static class CopyNotifier implements FileVisitor {\n\n        private final long notifyOnCount;\n        private final String targetDirDisplay;\n\n        private long currentCount = 0;\n\n        public CopyNotifier(long notifyOnCount, String targetDirDisplay) {\n            this.notifyOnCount = notifyOnCount;\n            this.targetDirDisplay = targetDirDisplay;\n        }\n\n        @Override\n        public void visit(Path sourceFile, Path dstFile) {\n            if (currentCount == -1) {\n                return;\n            }\n\n            if (currentCount == notifyOnCount) {\n                System.out.println(ansi().fgBrightBlack().a(\"Copying files into \" + targetDirDisplay + \" directory...\"));\n                currentCount = -1;\n                return;\n            }\n\n            currentCount++;\n        }\n    }\n\n    private static void copyWithGitIgnore(Path src, Path dst, Path skipDir,\n                                          GitIgnoreFilter filter,\n                                          FileVisitor visitor, CopyOption... options) throws IOException {\n        Files.walkFileTree(src, new SimpleFileVisitor<>() {\n            @Override\n            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {\n                if (dir.equals(src)) {\n                    return FileVisitResult.CONTINUE;\n                }\n\n                // Skip the target directory (if it's inside the source directory)\n                if (skipDir != null && Files.isSameFile(dir, skipDir)) {\n                    return FileVisitResult.SKIP_SUBTREE;\n                }\n\n                Path rel = src.relativize(dir);\n                // Check gitignore\n                if (filter != null && filter.isIgnored(rel, true)) {\n                    return FileVisitResult.SKIP_SUBTREE;\n                }\n                return FileVisitResult.CONTINUE;\n            }\n\n            @Override\n            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {\n                Path rel = src.relativize(file);\n                if (filter != null && filter.isIgnored(rel, false)) {\n                    return FileVisitResult.CONTINUE;\n                }\n\n                Path dstFile = dst.resolve(rel);\n                Path parent = dstFile.getParent();\n                if (!Files.exists(parent)) {\n                    Files.createDirectories(parent);\n                }\n\n                if (Files.isSymbolicLink(file)) {\n                    Path link = Files.readSymbolicLink(file);\n                    Path target = file.getParent().resolve(link).normalize();\n\n                    if (!target.startsWith(src)) {\n                        throw new IOException(\"Symlinks outside the base directory are not supported: \" + file + \" -> \" + target);\n                    }\n\n                    if (Files.notExists(target)) {\n                        return FileVisitResult.CONTINUE;\n                    }\n\n                    Files.deleteIfExists(dstFile);\n                    Files.createSymbolicLink(dstFile, link);\n                } else {\n                    Files.copy(file, dstFile, options);\n                }\n\n                if (visitor != null) {\n                    visitor.visit(file, dstFile);\n                }\n\n                return FileVisitResult.CONTINUE;\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/SelfUpdate.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.apache.maven.artifact.versioning.ComparableVersion;\nimport picocli.CommandLine;\nimport picocli.CommandLine.Command;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse.BodyHandlers;\nimport java.nio.file.*;\nimport java.nio.file.attribute.PosixFilePermission;\nimport java.time.Duration;\nimport java.util.HashSet;\nimport java.util.Optional;\nimport java.util.concurrent.Callable;\n\nimport static org.fusesource.jansi.Ansi.ansi;\n\n@Command(name = \"self-update\", description = \"Update the CLI to the latest release version\")\npublic class SelfUpdate implements Callable<Integer> {\n\n    private static final Duration TIMEOUT = Duration.ofSeconds(10);\n    private static final URI GITHUB_RELEASES_ENDPOINT = URI.create(\"https://api.github.com/repos/walmartlabs/concord/releases/latest\");\n    private static final String DOWNLOAD_TEMPLATE = \"https://repo.maven.apache.org/maven2/com/walmartlabs/concord/concord-cli/%1$s/concord-cli-%1$s.sh\";\n\n    @CommandLine.Option(names = {\"-h\", \"--help\"}, usageHelp = true, description = \"display the command's help message\")\n    boolean helpRequested = false;\n\n    @Override\n    public Integer call() {\n        var selfLocation = SelfUpdate.class.getProtectionDomain().getCodeSource().getLocation();\n\n        Path dst;\n        try {\n            dst = Paths.get(selfLocation.getPath());\n        } catch (InvalidPathException e) {\n            return unableToDetermineSelfLocation();\n        }\n\n        if (Files.isDirectory(dst)) {\n            return unableToDetermineSelfLocation();\n        }\n        if (!Files.isWritable(dst)) {\n            return selfLocationIsNotWritable();\n        }\n\n        String latestVersion;\n        try {\n            System.out.println(\"Checking for updates...\");\n            var maybeLatestVersion = getLatestVersion();\n            if (maybeLatestVersion.isEmpty()) {\n                return unableToDetermineLatestReleaseVersion();\n            }\n\n            latestVersion = maybeLatestVersion.get();\n        } catch (IOException | InterruptedException e) {\n            return err(e.getMessage());\n        }\n\n        var currentVersion = Version.getVersion();\n        var comparison = new ComparableVersion(latestVersion).compareTo(new ComparableVersion(currentVersion));\n        if (comparison == 0) {\n            return currentVersionIsLatest();\n        } else if (comparison < 0) {\n            return currentVersionIsMoreRecent();\n        }\n\n        System.out.printf(\"Updating to %s...%n\", latestVersion);\n\n        try {\n            var tmpFile = Files.createTempFile(\"concord-cli-\" + latestVersion, \".sh\");\n            var src = downloadArtifact(latestVersion, tmpFile);\n            Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING);\n        } catch (IOException | InterruptedException e) {\n            return err(e.getMessage());\n        }\n\n        System.out.println(ansi().fgBrightGreen().a(\"Done!\"));\n\n        try {\n            var permissions = new HashSet<>(Files.getPosixFilePermissions(dst));\n            permissions.add(PosixFilePermission.OWNER_EXECUTE);\n            Files.setPosixFilePermissions(dst, permissions);\n        } catch (UnsupportedOperationException | IOException e) {\n            warn(\"Unable to mark the binary as an executable. You might need to manually update the permissions of \" + dst.toAbsolutePath());\n        }\n\n        return 0;\n    }\n\n    private static Optional<String> getLatestVersion() throws IOException, InterruptedException {\n        var client = HttpClient.newBuilder()\n                .connectTimeout(TIMEOUT)\n                .build();\n\n        var request = HttpRequest.newBuilder()\n                .uri(GITHUB_RELEASES_ENDPOINT)\n                .timeout(TIMEOUT)\n                .header(\"Accept\", \"application/vnd.github.v3+json\")\n                .header(\"User-Agent\", \"concord-cli \" + Version.getVersion())\n                .GET()\n                .build();\n\n        var response = client.send(request, BodyHandlers.ofInputStream());\n        if (response.statusCode() == 200) {\n            var mapper = new ObjectMapper();\n            try (var body = response.body()) {\n                var json = mapper.readTree(body);\n                var tagName = json.path(\"tag_name\").asText();\n                if (!tagName.isEmpty()) {\n                    return Optional.of(tagName);\n                }\n            }\n        } else if (response.statusCode() == 404) {\n            throw new IOException(\"Repository not found or no releases available.\");\n        } else {\n            throw new IOException(\"GitHub API returned unexpected status: \" + response.statusCode());\n        }\n        return Optional.empty();\n    }\n\n    private static Path downloadArtifact(String version, Path dst) throws IOException, InterruptedException {\n        var client = HttpClient.newBuilder()\n                .connectTimeout(TIMEOUT)\n                .build();\n\n        var request = HttpRequest.newBuilder()\n                .uri(URI.create(DOWNLOAD_TEMPLATE.formatted(version)))\n                .timeout(TIMEOUT)\n                .header(\"Accept\", \"application/octet-stream\")\n                .header(\"User-Agent\", \"concord-cli \" + Version.getVersion())\n                .GET()\n                .build();\n\n        var response = client.send(request, BodyHandlers.ofFile(dst));\n        if (response.statusCode() == 200) {\n            return response.body();\n        } else if (response.statusCode() == 404) {\n            throw new IOException(\"Release %s not found\".formatted(version));\n        } else {\n            throw new IOException(\"Maven Central returned unexpected status: \" + response.statusCode());\n        }\n    }\n\n    private static int unableToDetermineSelfLocation() {\n        return err(\"Unable to determine the location of the CLI binary, self-update is not possible.\");\n    }\n\n    private static int selfLocationIsNotWritable() {\n        return err(\"Unable to overwrite the CLI binary, self-update is not possible.\");\n    }\n\n    private static int unableToDetermineLatestReleaseVersion() {\n        return err(\"Cannot determine the latest release version.\");\n    }\n\n    private static int currentVersionIsLatest() {\n        System.out.println(\"The current version is the latest release version. Nothing to do.\");\n        return 0;\n    }\n\n    private static int currentVersionIsMoreRecent() {\n        System.out.println(\"The current version is more recent than the latest available release version. Nothing to do.\");\n        return 0;\n    }\n\n    private static int err(String msg) {\n        System.out.println(ansi().fgRed().a(msg));\n        return -1;\n    }\n\n    private static void warn(String msg) {\n        System.out.println(ansi().fgYellow().a(msg));\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/Verbosity.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class Verbosity {\n\n    private final boolean[] verbosity;\n\n    public Verbosity(boolean[] verbosity) {\n        this.verbosity = verbosity;\n    }\n\n    public boolean logFlowSteps() {\n        return verbosity.length > 0;\n    }\n\n    public boolean logTaskParams() {\n        return verbosity.length > 1;\n    }\n\n    public boolean verbose() {\n        return verbosity.length > 2;\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/Version.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.util.Properties;\n\npublic class Version {\n\n    private static final String VERSION;\n\n    static {\n        Properties props = new Properties();\n        try {\n            props.load(Version.class.getClassLoader().getResourceAsStream(\"project.properties\"));\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n        VERSION = props.getProperty(\"project.version\");\n    }\n\n    public static String getVersion() {\n        return VERSION;\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/lint/DummyImportsNormalizer.java",
    "content": "package com.walmartlabs.concord.cli.lint;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.process.loader.ImportsNormalizer;\n\npublic class DummyImportsNormalizer implements ImportsNormalizer {\n\n    @Override\n    public Imports normalize(Imports imports) {\n        if (imports != null && !imports.isEmpty()) {\n            System.out.println(\"WARN: Linting of 'imports' is not supported at the moment.\");\n        }\n\n        return Imports.builder().build();\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/lint/ExpressionLinter.java",
    "content": "package com.walmartlabs.concord.cli.lint;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.ExpressionStep;\nimport com.walmartlabs.concord.runtime.model.SourceMap;\nimport com.walmartlabs.concord.runtime.model.Step;\n\nimport javax.el.ELException;\nimport java.util.Collections;\nimport java.util.List;\n\npublic class ExpressionLinter extends FlowElementLinter {\n\n    public ExpressionLinter(boolean verbose) {\n        super(verbose);\n    }\n\n    @Override\n    protected List<LintResult> apply(Step element) {\n        ExpressionStep task = (ExpressionStep) element;\n\n        String expr = task.expression();\n        notify(\"  Validating expression: \" + expr);\n\n        if (expr == null || expr.trim().isEmpty()) {\n            String msg = \"Empty or null expression\";\n            return Collections.singletonList(LintResult.error(element.location(), msg));\n        }\n\n        LintResult r = validate(expr, element.location());\n        if (r != null) {\n            return Collections.singletonList(r);\n        }\n\n        return null;\n    }\n\n    @Override\n    protected boolean accepts(Step element) {\n        return element instanceof ExpressionStep;\n    }\n\n    @Override\n    protected String getStartMessage() {\n        return \"Validating expressions...\";\n    }\n\n    public static LintResult validate(String expr, SourceMap sourceMap) {\n        try {\n            Utils.compileExpression(expr);\n        } catch (ELException e) {\n            return Utils.toResult(e, sourceMap, null);\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/lint/FlowElementLinter.java",
    "content": "package com.walmartlabs.concord.cli.lint;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.FlowDefinition;\nimport com.walmartlabs.concord.runtime.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.model.Step;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\npublic abstract class FlowElementLinter implements Linter {\n\n    private final boolean verbose;\n\n    public FlowElementLinter(boolean verbose) {\n        this.verbose = verbose;\n    }\n\n    @Override\n    public List<LintResult> apply(ProcessDefinition pd) {\n        notify(\">> \" + getStartMessage());\n\n        Map<String, ? extends FlowDefinition> flows = pd.flows();\n        if (flows == null || flows.isEmpty()) {\n            return Collections.emptyList();\n        }\n\n        List<LintResult> results = new ArrayList<>();\n        flows.forEach((key, value) -> results.addAll(apply(value)));\n\n        notify(\"<< ...done\\n\");\n        return results;\n    }\n\n    private List<LintResult> apply(FlowDefinition pd) {\n        List<LintResult> results = new ArrayList<>();\n\n        for (Step e : pd.steps()) {\n            if (!accepts(e)) {\n                continue;\n            }\n\n            List<LintResult> r = apply(e);\n            if (r != null) {\n                results.addAll(r);\n            }\n        }\n\n        return results;\n    }\n\n    protected abstract boolean accepts(Step element);\n\n    protected abstract List<LintResult> apply(Step element);\n\n    protected abstract String getStartMessage();\n\n    protected void notify(String s) {\n        if (!verbose) {\n            return;\n        }\n\n        System.out.println(s);\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/lint/LintResult.java",
    "content": "package com.walmartlabs.concord.cli.lint;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.SourceMap;\n\npublic class LintResult {\n\n    public static LintResult error(SourceMap sourceMap, String message) {\n        return new LintResult(Type.ERROR, sourceMap, message);\n    }\n\n    private final Type type;\n    private final SourceMap sourceMap;\n    private final String message;\n\n    public LintResult(Type type, SourceMap sourceMap, String message) {\n        this.type = type;\n        this.sourceMap = sourceMap;\n        this.message = message;\n    }\n\n    public Type getType() {\n        return type;\n    }\n\n    public SourceMap getSourceMap() {\n        return sourceMap;\n    }\n\n    public String getMessage() {\n        return message;\n    }\n\n    public enum Type {\n        WARNING,\n        ERROR\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/lint/Linter.java",
    "content": "package com.walmartlabs.concord.cli.lint;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.ProcessDefinition;\n\nimport java.util.List;\n\npublic interface Linter {\n\n    List<LintResult> apply(ProcessDefinition pd);\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/lint/TaskCallLinter.java",
    "content": "package com.walmartlabs.concord.cli.lint;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.SourceMap;\nimport com.walmartlabs.concord.runtime.model.Step;\nimport com.walmartlabs.concord.runtime.model.TaskCallStep;\n\nimport javax.el.ELException;\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\n\npublic class TaskCallLinter extends FlowElementLinter {\n\n    public TaskCallLinter(boolean verbose) {\n        super(verbose);\n    }\n\n    @Override\n    protected List<LintResult> apply(Step element) {\n        List<LintResult> results = new ArrayList<>();\n\n        TaskCallStep task = (TaskCallStep) element;\n\n        String expr = task.name();\n        notify(\"  Validating task call: \" + expr);\n\n        LintResult r = ExpressionLinter.validate(expr, element.location());\n        if (r != null) {\n            results.add(r);\n        }\n\n        Map<String, Serializable> inVars = task.input();\n        for (Map.Entry<String, Serializable> e : inVars.entrySet()) {\n            LintResult lr = validateArgument(e.getKey(), e.getValue(), element.location());\n            if (lr != null) {\n                results.add(lr);\n            }\n        }\n\n        return results;\n    }\n\n    private LintResult validateArgument(String paramName, Object value, SourceMap sourceMap) {\n        if (value != null) {\n            return validateValue(paramName, value, sourceMap);\n        }\n\n        return null;\n    }\n\n    private LintResult validateValue(String paramName, Object value, SourceMap sourceMap) {\n        if (value instanceof String) {\n            String s = (String) value;\n            if (s.contains(\"${\")) {\n                return validateExpression(paramName, s, sourceMap);\n            }\n        } else if (value instanceof Collection) {\n            Collection c = (Collection) value;\n            for (Object vv : c) {\n                LintResult r = validateValue(paramName, vv, sourceMap);\n                if (r != null) {\n                    return r;\n                }\n            }\n        } else if (value instanceof Map) {\n            Map m = (Map) value;\n            for (Object vv : m.values()) {\n                LintResult r = validateValue(paramName, vv, sourceMap);\n                if (r != null) {\n                    return r;\n                }\n            }\n        }\n\n        return null;\n    }\n\n    private LintResult validateExpression(String paramName, String expr, SourceMap sourceMap) {\n        try {\n            Utils.compileExpression(expr);\n        } catch (ELException e) {\n            return Utils.toResult(e, sourceMap, \"Invalid expression in task arguments: \\\"\" + expr + \"\\\" in IN \" + paramName);\n        }\n\n        return null;\n    }\n\n    @Override\n    protected boolean accepts(Step element) {\n        return element instanceof TaskCallStep;\n    }\n\n    @Override\n    protected String getStartMessage() {\n        return \"Validating task calls...\";\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/lint/Utils.java",
    "content": "package com.walmartlabs.concord.cli.lint;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.SourceMap;\n\nimport javax.el.ELContext;\nimport javax.el.ELException;\nimport javax.el.ExpressionFactory;\nimport javax.el.StandardELContext;\n\npublic final class Utils {\n\n    public static void compileExpression(String expr) {\n        ExpressionFactory ef = ExpressionFactory.newInstance();\n        ELContext ctx = new StandardELContext(ef);\n        ef.createValueExpression(ctx, expr, Object.class);\n    }\n\n    public static LintResult toResult(ELException e, SourceMap sm, String message) {\n        // make EL exceptions a bit more compact\n        String error = e.getCause().getMessage().replaceAll(\"\\n\", \"\").replaceAll(\" {4}\", \" \");\n        return LintResult.error(sm, (message != null ? message + \" \" + error : error));\n    }\n\n    private Utils() {\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/runner/ApiKey.java",
    "content": "package com.walmartlabs.concord.cli.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.cli.CliConfig;\nimport com.walmartlabs.concord.cli.Verbosity;\nimport com.walmartlabs.concord.cli.secrets.CliSecretService;\n\nimport java.nio.file.Path;\n\npublic record ApiKey(String value) {\n\n    public static ApiKey create(CliConfig.CliConfigContext cliConfigContext, Path workDir, Verbosity verbosity) {\n        CliConfig.RemoteRunConfiguration remoteRunConfig = cliConfigContext.remoteRun();\n        if (remoteRunConfig == null || remoteRunConfig.apiKeyRef() == null) {\n            return new ApiKey(null);\n        }\n\n        if (verbosity.verbose()) {\n            System.out.println(\"Using '\" + remoteRunConfig.apiKeyRef() + \"' as a secret for API key\");\n        }\n\n        CliSecretService secretService = CliSecretService.create(cliConfigContext, workDir, verbosity);\n        try {\n            return new ApiKey(secretService.exportAsString(remoteRunConfig.apiKeyRef().orgName(), remoteRunConfig.apiKeyRef().secretName(), null));\n        } catch (Exception e) {\n            throw new RuntimeException(\"Unable to fetch the API key. \" + e.getMessage());\n        }\n    }\n\n    @Override\n    public String toString() {\n        return \"***\";\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/runner/CliApiClientProvider.java",
    "content": "package com.walmartlabs.concord.cli.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.client2.ApiClientConfiguration;\nimport com.walmartlabs.concord.client2.ApiClientFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration;\n\nimport javax.inject.Inject;\nimport javax.inject.Provider;\n\npublic class CliApiClientProvider implements Provider<ApiClient> {\n\n    private final ApiClientFactory clientFactory;\n    private final ApiKey apiKey;\n    private final ProcessConfiguration processCfg;\n\n    @Inject\n    public CliApiClientProvider(ApiClientFactory clientFactory, ApiKey apiKey, ProcessConfiguration processCfg) {\n        this.clientFactory = clientFactory;\n        this.apiKey = apiKey;\n        this.processCfg = processCfg;\n    }\n\n    @Override\n    public ApiClient get() {\n        return clientFactory.create(ApiClientConfiguration.builder()\n                .apiKey(apiKey.value())\n                .sessionToken(processCfg.processInfo().sessionToken())\n                .build());\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/runner/CliCheckpointService.java",
    "content": "package com.walmartlabs.concord.cli.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.common.SerializationUtils;\nimport com.walmartlabs.concord.runtime.v2.runner.ProcessSnapshot;\nimport com.walmartlabs.concord.runtime.v2.runner.checkpoints.CheckpointService;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.ThreadId;\n\nimport java.io.ByteArrayOutputStream;\nimport java.util.UUID;\n\npublic class CliCheckpointService implements CheckpointService {\n\n    @Override\n    public void create(ThreadId threadId, UUID correlationId, String name, Runtime runtime, ProcessSnapshot snapshot) {\n        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {\n            SerializationUtils.serialize(baos, snapshot.vmState());\n        } catch (Exception e) {\n            throw new RuntimeException(\"Checkpoint create error\", e);\n        }\n\n        System.out.println(\"Checkpoint '\" +  name +  \"' ignored\");\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/runner/CliDockerService.java",
    "content": "package com.walmartlabs.concord.cli.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport com.walmartlabs.concord.runtime.v2.sdk.DockerContainerSpec;\nimport com.walmartlabs.concord.runtime.v2.sdk.DockerService;\n\npublic class CliDockerService implements DockerService {\n\n    @Override\n    public int start(DockerContainerSpec spec, LogCallback outCallback, LogCallback errCallback) {\n        throw new UnsupportedOperationException(\"Running Docker containers is not supported by the concord-cli yet\");\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/runner/CliImportsListener.java",
    "content": "package com.walmartlabs.concord.cli.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Import;\nimport com.walmartlabs.concord.imports.ImportsListener;\n\nimport java.util.List;\n\npublic class CliImportsListener implements ImportsListener {\n\n    private long startAt;\n\n    @Override\n    public void onStart(List<Import> items) {\n        startAt = System.currentTimeMillis();\n        System.out.println(\"Resolving \" + items.size() + \" import(s)...\");\n    }\n\n    @Override\n    public void onEnd(List<Import> items) {\n        System.out.println(\"Imports resolution took \" + (System.currentTimeMillis() - startAt) + \"ms\");\n    }\n\n    @Override\n    public void beforeImport(Import i) {\n        System.out.println(\"Resolving import: \" + i);\n    }\n\n    @Override\n    public void afterImport(Import i) {\n        System.out.println(\"Import resolved\");\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/runner/CliImportsNormalizer.java",
    "content": "package com.walmartlabs.concord.cli.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Import;\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.runtime.v2.ImportsNormalizer;\n\nimport java.util.stream.Collectors;\n\npublic class CliImportsNormalizer implements ImportsNormalizer {\n\n    private static final String DEFAULT_DEST = \"concord\";\n\n    private final String defaultSource;\n    private final boolean verbose;\n    private final String defaultVersion;\n\n    public CliImportsNormalizer(String defaultSource, boolean verbose, String defaultVersion) {\n        this.defaultSource = defaultSource;\n        this.verbose = verbose;\n        this.defaultVersion = defaultVersion;\n    }\n\n    @Override\n    public Imports normalize(Imports imports) {\n        if (imports == null || imports.isEmpty()) {\n            return Imports.builder().build();\n        }\n\n        if (verbose) {\n            System.out.println(\"Processing imports...\");\n        }\n\n        Imports result = Imports.of(imports.items().stream()\n                .map(this::normalize)\n                .collect(Collectors.toList()));\n\n        if (verbose) {\n            imports.items().forEach(i -> System.out.println(\"import: \" + i));\n        }\n\n        return result;\n    }\n\n    private Import normalize(Import i) {\n        switch (i.type()) {\n            case Import.MvnDefinition.TYPE: {\n                Import.MvnDefinition src = (Import.MvnDefinition) i;\n                return Import.MvnDefinition.builder()\n                        .from(src)\n                        .dest(src.dest() != null ? src.dest() : DEFAULT_DEST)\n                        .build();\n            }\n            case Import.GitDefinition.TYPE: {\n                Import.GitDefinition src = (Import.GitDefinition) i;\n                return normalize(src);\n            }\n            case Import.DirectoryDefinition.TYPE: {\n                return i;\n            }\n            default: {\n                throw new IllegalArgumentException(\"Unsupported import type: '\" + i.type() + \"'\");\n            }\n        }\n    }\n\n    private Import.GitDefinition normalize(Import.GitDefinition e) {\n        String url = e.url();\n        if (url == null) {\n            String name = e.name();\n            url = normalizeUrl(defaultSource) + name;\n        }\n\n        return Import.GitDefinition.builder().from(e)\n                .url(url)\n                .version(e.version() != null ? e.version() : defaultVersion)\n                .dest(e.dest() != null ? e.dest() : DEFAULT_DEST)\n                .secret(e.secret())\n                .build();\n    }\n\n    private static String normalizeUrl(String u) {\n        if (u.endsWith(\"/\")) {\n            return u;\n        }\n        return u + \"/\";\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/runner/CliLockService.java",
    "content": "package com.walmartlabs.concord.cli.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.LockService;\n\n/**\n * Just a stub for the CLI.\n */\npublic class CliLockService implements LockService {\n\n    @Override\n    public void projectLock(String lockName) {\n        // nothing to do\n    }\n\n    @Override\n    public void projectUnlock(String lockName) {\n        // nothing to do\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/runner/CliRepositoryExporter.java",
    "content": "package com.walmartlabs.concord.cli.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AuthTokenProvider;\nimport com.walmartlabs.concord.common.ExternalAuthToken;\nimport com.walmartlabs.concord.imports.Import;\nimport com.walmartlabs.concord.imports.RepositoryExporter;\nimport com.walmartlabs.concord.repository.*;\nimport com.walmartlabs.concord.sdk.Secret;\n\nimport javax.annotation.Nullable;\nimport java.io.UnsupportedEncodingException;\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.nio.file.Path;\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Optional;\n\npublic class CliRepositoryExporter implements RepositoryExporter {\n\n    // TODO: move to configuration\n    private static final Duration DEFAULT_OPERATION_TIMEOUT = Duration.parse(\"PT10M\");\n    private static final Duration FETCH_TIMEOUT = Duration.parse(\"PT10M\");\n    private static final int HTTP_LOW_SPEED_LIMIT = 0;\n    private static final Duration HTTP_LOW_SPEED_TIME = Duration.ofMinutes(10);\n    private static final Duration SSH_TIMEOUT = Duration.ofMinutes(10);\n    private static final int SSH_TIMEOUT_RETRY_COUNT = 1;\n\n    private final Path repoCacheDir;\n\n    private final RepositoryProviders providers;\n\n    public CliRepositoryExporter(Path repoCacheDir) {\n        this.repoCacheDir = repoCacheDir;\n\n        GitClientConfiguration clientCfg = GitClientConfiguration.builder()\n                .defaultOperationTimeout(DEFAULT_OPERATION_TIMEOUT)\n                .fetchTimeout(FETCH_TIMEOUT)\n                .httpLowSpeedLimit(HTTP_LOW_SPEED_LIMIT)\n                .httpLowSpeedTime(HTTP_LOW_SPEED_TIME)\n                .sshTimeout(SSH_TIMEOUT)\n                .sshTimeoutRetryCount(SSH_TIMEOUT_RETRY_COUNT)\n                .build();\n\n        AuthTokenProvider authProvider = new AuthTokenProvider() {\n            @Override\n            public boolean supports(URI repo, @Nullable Secret secret) {\n                return false;\n            }\n\n            @Override\n            public Optional<ExternalAuthToken> getToken(URI repo, @Nullable Secret secret) throws RepositoryException {\n                throw new UnsupportedOperationException(\"Not supported\");\n            }\n        };\n\n        this.providers = new RepositoryProviders(List.of(new GitCliRepositoryProvider(clientCfg, authProvider)));\n    }\n\n    @Override\n    public Snapshot export(Import.GitDefinition entry, Path workDir) throws Exception {\n        Path dest = workDir;\n        if (entry.dest() != null) {\n            dest = dest.resolve(Objects.requireNonNull(entry.dest()));\n        }\n\n        String url = Objects.requireNonNull(entry.url());\n        Path cacheDir = repoCacheDir.resolve(encodeUrl(url));\n        Secret secret = null;\n\n        Repository repo = providers.fetch(\n                FetchRequest.builder()\n                        .url(url)\n                        .version(FetchRequest.Version.from(entry.version()))\n                        .secret(secret)\n                        .destination(cacheDir)\n                .build(),\n                entry.path());\n        return repo.export(dest, entry.exclude());\n    }\n\n    private static String encodeUrl(String url) {\n        String encodedUrl;\n        try {\n            encodedUrl = URLEncoder.encode(url, \"UTF-8\");\n        } catch (UnsupportedEncodingException e) {\n            throw new RepositoryException(\"Url encoding error\", e);\n        }\n\n        return encodedUrl;\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/runner/CliServicesModule.java",
    "content": "package com.walmartlabs.concord.cli.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.google.inject.AbstractModule;\nimport com.google.inject.Singleton;\nimport com.google.inject.multibindings.Multibinder;\nimport com.walmartlabs.concord.cli.CliConfig.CliConfigContext;\nimport com.walmartlabs.concord.cli.Verbosity;\nimport com.walmartlabs.concord.cli.secrets.CliSecretService;\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.dependencymanager.DependencyManager;\nimport com.walmartlabs.concord.runtime.v2.runner.*;\nimport com.walmartlabs.concord.runtime.v2.runner.checkpoints.CheckpointService;\nimport com.walmartlabs.concord.runtime.v2.runner.guice.BaseRunnerModule;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.RunnerLogger;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.SimpleLogger;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallListener;\nimport com.walmartlabs.concord.runtime.v2.sdk.DockerService;\nimport com.walmartlabs.concord.runtime.v2.sdk.LockService;\nimport com.walmartlabs.concord.runtime.v2.sdk.SecretService;\nimport com.walmartlabs.concord.svm.ExecutionListener;\n\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Map;\nimport java.util.function.Supplier;\n\npublic class CliServicesModule extends AbstractModule {\n\n    private final CliConfigContext cliConfigContext;\n    private final Path workDir;\n    private final Path defaultTaskVars;\n    private final DependencyManager dependencyManager;\n    private final Verbosity verbosity;\n\n    public CliServicesModule(CliConfigContext cliConfigContext,\n                             Path workDir,\n                             Path defaultTaskVars,\n                             DependencyManager dependencyManager,\n                             Verbosity verbosity) {\n\n        this.cliConfigContext = cliConfigContext;\n        this.workDir = workDir;\n        this.defaultTaskVars = defaultTaskVars;\n        this.dependencyManager = dependencyManager;\n        this.verbosity = verbosity;\n    }\n\n    @Override\n    protected void configure() {\n        install(new BaseRunnerModule());\n\n        bind(RunnerLogger.class).to(SimpleLogger.class);\n\n        bind(SecretService.class).toInstance(CliSecretService.create(cliConfigContext, workDir, verbosity));\n\n        bind(DockerService.class).to(CliDockerService.class);\n\n        bind(CheckpointService.class).to(CliCheckpointService.class);\n        bind(PersistenceService.class).to(DefaultPersistenceService.class);\n        bind(ProcessStatusCallback.class).toInstance(instanceId -> {\n        });\n\n        bind(ApiKey.class).toInstance(ApiKey.create(cliConfigContext, workDir, verbosity));\n        bind(ApiClient.class).toProvider(CliApiClientProvider.class);\n\n        bind(DefaultTaskVariablesService.class)\n                .toInstance(new MapBackedDefaultTaskVariablesService(readDefaultVars(defaultTaskVars)));\n\n        bind(LockService.class).to(CliLockService.class);\n\n        bind(DependencyManager.class).toInstance(dependencyManager);\n        bind(com.walmartlabs.concord.runtime.v2.sdk.DependencyManager.class).to(DefaultDependencyManager.class).in(Singleton.class);\n\n        Multibinder<ExecutionListener> executionListeners = Multibinder.newSetBinder(binder(), ExecutionListener.class);\n        if (verbosity.logFlowSteps()) {\n            executionListeners.addBinding().to(FlowStepLogger.class);\n        }\n\n        if (verbosity.logTaskParams()) {\n            Multibinder<TaskCallListener> taskCallListeners = Multibinder.newSetBinder(binder(), TaskCallListener.class);\n            taskCallListeners.addBinding().toInstance(new TaskParamsLogger());\n        }\n    }\n\n    private static Map<String, Map<String, Object>> readDefaultVars(Path defaultTaskVars) {\n        if (Files.exists(defaultTaskVars)) {\n            try (InputStream is = Files.newInputStream(defaultTaskVars)) {\n                return parseDefaultVars(() -> is);\n            } catch (Exception e) {\n                System.out.println(\"Error parsing default variables in '\" + defaultTaskVars + \"': \" + e.getMessage());\n            }\n        }\n\n        return parseDefaultVars(() -> CliServicesModule.class.getResourceAsStream(\"/default-vars.json\"));\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Map<String, Map<String, Object>> parseDefaultVars(Supplier<InputStream> isSupplier) {\n        try (InputStream is = isSupplier.get()) {\n            if (is == null) {\n                throw new IllegalStateException(\"Default variables input stream is null.\");\n            }\n\n            return new ObjectMapper().readValue(is, Map.class);\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage());\n        }\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/runner/DependencyResolver.java",
    "content": "package com.walmartlabs.concord.cli.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.dependencymanager.DependencyEntity;\nimport com.walmartlabs.concord.dependencymanager.DependencyManager;\nimport com.walmartlabs.concord.dependencymanager.ProgressListener;\n\nimport java.io.IOException;\nimport java.net.*;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport static com.walmartlabs.concord.dependencymanager.DependencyManager.MAVEN_SCHEME;\n\npublic class DependencyResolver {\n\n    private final DependencyManager dependencyManager;\n    private final List<URI> defaultDependencies = Collections.emptyList();\n    private final boolean verbose;\n\n    public DependencyResolver(DependencyManager dependencyManager, boolean verbose) {\n        this.dependencyManager = dependencyManager;\n        this.verbose = verbose;\n    }\n\n    public Collection<String> resolveDeps(List<String> dependencies) throws Exception {\n        if (verbose) {\n            System.out.println(\"Resolving process dependencies...\");\n        }\n\n        long t1 = System.currentTimeMillis();\n\n        // combine the default dependencies and the process' dependencies\n        Collection<URI> uris = Stream.concat(defaultDependencies.stream(),\n                normalizeUrls(dependencies).stream())\n                .collect(Collectors.toList());\n\n        Collection<DependencyEntity> deps = dependencyManager.resolve(uris, new ProgressListener() {\n\n            @Override\n            public void onRetry(int retryCount, int maxRetry, long interval, String cause) {\n                System.err.println(\"Error while downloading dependencies: \" + cause);\n                System.err.println(\"Retrying in \" + interval + \"ms\");\n            }\n\n            @Override\n            public void onTransferFailed(String error) {\n                // when we have more than one repo in mvn.json we can get transfer error for one repo\n                // but artifact will be resolved with second repo...\n                if (verbose) {\n                    System.err.println(\"Transfer failed: \" + error);\n                }\n            }\n        });\n\n        // sort dependencies to maintain consistency in runner configurations\n        Collection<String> paths = deps.stream()\n                .map(DependencyEntity::getPath)\n                .map(p -> p.toAbsolutePath().toString())\n                .sorted()\n                .collect(Collectors.toList());\n\n        long t2 = System.currentTimeMillis();\n\n        if (verbose) {\n            System.out.println(\"Dependency resolution took \" + ((t2 - t1)) + \"ms\");\n            logDependencies(paths);\n        }\n\n        return paths;\n    }\n\n    private void logDependencies(Collection<?> deps) {\n        if (verbose && deps.isEmpty()) {\n            System.out.println(\"No external dependencies.\");\n            return;\n        }\n\n        List<String> l = deps.stream()\n                .map(Object::toString)\n                .collect(Collectors.toList());\n\n        StringBuilder b = new StringBuilder();\n        for (String s : l) {\n            b.append(\"\\n\\t\").append(s);\n        }\n\n        System.out.println(\"Dependencies: \" + b);\n    }\n\n    private static Collection<URI> normalizeUrls(Collection<String> urls) throws IOException, URISyntaxException {\n        if (urls == null || urls.isEmpty()) {\n            return Collections.emptySet();\n        }\n\n        Collection<URI> result = new HashSet<>();\n\n        for (String s : urls) {\n            URI u = new URI(s);\n            String scheme = u.getScheme();\n\n            if (MAVEN_SCHEME.equalsIgnoreCase(scheme)) {\n                result.add(u);\n                continue;\n            }\n\n            if (scheme == null || scheme.trim().isEmpty()) {\n                throw new IOException(\"Invalid dependency URL. Missing URL scheme: \" + s);\n            }\n\n            if (s.endsWith(\".jar\")) {\n                result.add(u);\n                continue;\n            }\n\n            URL url = u.toURL();\n            while (true) {\n                if (\"http\".equalsIgnoreCase(scheme) || \"https\".equalsIgnoreCase(scheme)) {\n                    URLConnection conn = url.openConnection();\n                    if (conn instanceof HttpURLConnection) {\n                        HttpURLConnection httpConn = (HttpURLConnection) conn;\n                        httpConn.setInstanceFollowRedirects(false);\n\n                        int code = httpConn.getResponseCode();\n                        if (code == HttpURLConnection.HTTP_MOVED_TEMP ||\n                                code == HttpURLConnection.HTTP_MOVED_PERM ||\n                                code == HttpURLConnection.HTTP_SEE_OTHER ||\n                                code == 307) {\n\n                            String location = httpConn.getHeaderField(\"Location\");\n                            url = new URL(location);\n                            System.out.println(\"normalizeUrls -> using: \" + location);\n\n                            continue;\n                        }\n\n                        u = url.toURI();\n                    } else {\n                        System.out.println(\"normalizeUrls -> unexpected connection type: \" + conn.getClass() + \" (for \" + s + \")\");\n                    }\n                }\n\n                break;\n            }\n\n            result.add(u);\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/runner/FlowStepLogger.java",
    "content": "package com.walmartlabs.concord.cli.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.model.*;\nimport com.walmartlabs.concord.runtime.v2.runner.context.ContextFactory;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.SegmentedLogger;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.LogSegmentScopeCommand;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.StepCommand;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\nimport static org.fusesource.jansi.Ansi.ansi;\n\npublic class FlowStepLogger implements ExecutionListener {\n\n    @Override\n    public Result beforeCommand(Runtime runtime, VM vm, State state, ThreadId threadId, Command cmd) {\n        if (!(cmd instanceof StepCommand)) {\n            return Result.CONTINUE;\n        }\n\n        if (cmd instanceof LogSegmentScopeCommand) {\n            return Result.CONTINUE;\n        }\n\n        StepCommand<?> s = (StepCommand<?>) cmd;\n\n        Location loc = s.getStep().getLocation();\n\n        System.out.println(ansi().fgBrightCyan().bold().a(\">>> '\").a(getDescription(runtime, state, threadId, s.getStep())).boldOff()\n                .a(\"' @ \").a(loc.fileName()).a(\":\").a(loc.lineNum()).reset());\n\n        return Result.CONTINUE;\n    }\n\n    private static String getDescription(Runtime runtime, State state, ThreadId threadId, Step step) {\n        if (step instanceof AbstractStep) {\n            ContextFactory contextFactory = runtime.getService(ContextFactory.class);\n            Context ctx = contextFactory.create(runtime, state, threadId, step);\n\n            String rawSegmentName = SegmentedLogger.getSegmentName((AbstractStep<?>) step);\n            String segmentName = ctx.eval(rawSegmentName, String.class);\n            if (segmentName != null) {\n                return segmentName;\n            }\n        }\n\n        return getDefaultDescription(step);\n    }\n\n    private static String getDefaultDescription(Step step) {\n        if (step instanceof FlowCall) {\n            return \"Flow call: \" + ((FlowCall) step).getFlowName();\n        } else if (step instanceof Expression) {\n            return \"Expression: \" + ((Expression) step).getExpr();\n        } else if (step instanceof ScriptCall) {\n            return \"Script: \" + ((ScriptCall) step).getLanguageOrRef();\n        } else if (step instanceof IfStep) {\n            return \"Check: \" + ((IfStep) step).getExpression();\n        } else if (step instanceof SwitchStep) {\n            return \"Switch: \" + ((SwitchStep) step).getExpression();\n        } else if (step instanceof SetVariablesStep) {\n            return \"Set variables\";\n        } else if (step instanceof Checkpoint) {\n            return \"Checkpoint: \" + ((Checkpoint) step).getName();\n        } else if (step instanceof FormCall) {\n            return \"Form call: \" + ((FormCall) step).getName();\n        } else if (step instanceof GroupOfSteps) {\n            return \"Group of steps\";\n        } else if (step instanceof ParallelBlock) {\n            return \"Parallel block\";\n        } else if (step instanceof ExitStep) {\n            return \"Exit\";\n        } else if (step instanceof ReturnStep) {\n            return \"Return\";\n        } else if (step instanceof TaskCall) {\n            return \"Task: \" + ((TaskCall) step).getName();\n        }\n\n        return step.getClass().getName();\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/runner/TaskParamsLogger.java",
    "content": "package com.walmartlabs.concord.cli.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallEvent;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallListener;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\nimport org.fusesource.jansi.Ansi;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static org.fusesource.jansi.Ansi.ansi;\n\npublic class TaskParamsLogger implements TaskCallListener {\n\n    @Override\n    public void onEvent(TaskCallEvent event) {\n        Map<String, Object> inVars = convertInput(event.input());\n        Map<String, Object> outVars = asMapOrNull(event.result());\n\n        if (TaskCallEvent.Phase.PRE.equals(event.phase())) {\n            System.out.println(ansi().fgCyan().a(\"     in: \" + inVars).reset());\n        } else {\n            System.out.println(ansi().fgCyan().a(\"     out: \").a(outVars).reset());\n            System.out.println(ansi().fgCyan().a(\"     duration: \").a(event.duration()).a(\"ms\").reset());\n            if (event.error() != null) {\n                System.out.println(ansi().fgBrightRed().a(\"    error: \").a(event.error()).reset());\n            }\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private Map<String, Object> asMapOrNull(Object v) {\n        if (v instanceof TaskResult.SimpleResult) {\n            return ((TaskResult.SimpleResult) v).toMap();\n        }\n\n        if (v instanceof Map) {\n            return (Map<String, Object>) v;\n        }\n\n        return null;\n    }\n\n    private static Map<String, Object> convertInput(List<Object> input) {\n        if (input.isEmpty()) {\n            return Collections.emptyMap();\n        }\n\n        if (input.size() == 1) {\n            if (input.get(0) instanceof Variables) {\n                return ((Variables) input.get(0)).toMap();\n            }\n        }\n\n        Map<String, Object> result = new HashMap<>();\n        for (int i = 0; i < input.size(); i++) {\n            Object arg = input.get(i);\n            if (arg instanceof Context) {\n                arg = \"context\";\n            }\n            result.put(String.valueOf(i), arg);\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/runner/VaultProvider.java",
    "content": "package com.walmartlabs.concord.cli.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Properties;\n\nimport static org.fusesource.jansi.Ansi.ansi;\n\n/**\n * Implements a simple \"vault\" system.\n * When using {@code crypto.decryptString} in a flow executed by the CLI,\n * instead of decrypting the key, it performs a simple lookup.\n * E.g.\n * <pre>\n * {@code\n * # concord.yml\n * flows:\n *   default:\n *     - log: \"${crypto.decryptString('abc')}\" # prints out \"the_actual_value\"\n * }\n * </pre>\n * <pre>\n * {@code\n * # ~/.concord/vaults/default\n * abc = the_actual_value\n * }\n * </pre>\n */\n// TODO consider implementing support for encrypted Ansible vaults\npublic class VaultProvider {\n\n    private final String id;\n    private final Map<String, String> items;\n\n    public VaultProvider(Path dir, String id) {\n        this.id = id;\n        this.items = load(vaultPath(dir, id));\n    }\n\n    public String getValue(String key) {\n        if (items == null) {\n            System.err.println(\"Vault '\" + id + \"' not found. Can't get value for key '\" + key + \"'\");\n            throw new RuntimeException(\"Vault not configured\");\n        }\n\n        if (!items.containsKey(key)) {\n            System.out.println(ansi().fgRed().a(\"There are no key '\").a(key).a(\"' in vault '\").a(id).a(\"'\").reset());\n        }\n\n        return items.get(key);\n    }\n\n    private static Map<String, String> load(Path file) {\n        if (Files.notExists(file)) {\n            return null;\n        }\n\n        Map<String, String> result = new HashMap<>();\n        try (InputStream is = Files.newInputStream(file)) {\n            Properties props = new Properties();\n            props.load(is);\n            for (String name: props.stringPropertyNames()) {\n                result.put(name, props.getProperty(name));\n            }\n            return result;\n        } catch (IOException e) {\n            System.out.println(ansi().fgBrightRed().a(\"Error loading vault file '\").a(file).a(\"': \").a(e.getMessage()).reset());\n            throw new RuntimeException(e.getMessage());\n        }\n    }\n\n    private static Path vaultPath(Path dir, String vaultId) {\n        return dir.resolve(vaultId);\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/secrets/CliSecretService.java",
    "content": "package com.walmartlabs.concord.cli.secrets;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.cli.CliConfig.CliConfigContext;\nimport com.walmartlabs.concord.cli.Verbosity;\nimport com.walmartlabs.concord.cli.runner.VaultProvider;\nimport com.walmartlabs.concord.runtime.v2.sdk.SecretService;\n\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static java.util.Objects.requireNonNull;\nimport static org.fusesource.jansi.Ansi.ansi;\n\npublic class CliSecretService implements SecretService {\n\n    private final List<SecretsProviderRef> secretsProviders;\n    private final VaultProvider vaultProvider;\n    private final Verbosity verbosity;\n\n    public static CliSecretService create(CliConfigContext cliConfigContext, Path workDir, Verbosity verbosity) {\n        var providers = new ArrayList<SecretsProviderRef>();\n\n        var local = cliConfigContext.secrets().local();\n        if (local.enabled()) {\n            var provider = new FileSecretsProvider(workDir, local.dir());\n            providers.add(new SecretsProviderRef(\"localFile\", provider, local.writable()));\n        }\n\n        var remote = cliConfigContext.secrets().remote();\n        if (remote.enabled()) {\n            var provider = new RemoteSecretsProvider(workDir, remote.baseUrl(), remote.apiKey(), remote.confirmAccess());\n            providers.add(new SecretsProviderRef(\"remote\", provider, remote.writable()));\n        }\n\n        var vault = cliConfigContext.secrets().vault();\n        return new CliSecretService(providers, new VaultProvider(vault.dir(), vault.id()), verbosity);\n    }\n\n    public CliSecretService(List<SecretsProviderRef> secretsProviders, VaultProvider vaultProvider, Verbosity verbosity) {\n        this.secretsProviders = requireNonNull(secretsProviders);\n        this.vaultProvider = requireNonNull(vaultProvider);\n        this.verbosity = requireNonNull(verbosity);\n    }\n\n    @Override\n    public SecretService.KeyPair exportKeyAsFile(String orgName, String secretName, String secretPassword) throws Exception {\n        for (var ref : secretsProviders) {\n            var result = ref.provider().exportKeyAsFile(orgName, secretName, secretPassword);\n            if (result.isPresent()) {\n                reportSecretFound(orgName, secretName, ref);\n                return result.get();\n            }\n        }\n        throw new RuntimeException(\"Secret not found: %s/%s.\".formatted(orgName, secretName));\n    }\n\n    @Override\n    public String exportAsString(String orgName, String secretName, String secretPassword) throws Exception {\n        for (var ref : secretsProviders) {\n            var result = ref.provider().exportAsString(orgName, secretName, secretPassword);\n            if (result.isPresent()) {\n                reportSecretFound(orgName, secretName, ref);\n                return result.get();\n            }\n        }\n        throw new RuntimeException(\"Secret not found: %s/%s.\".formatted(orgName, secretName));\n    }\n\n    @Override\n    public Path exportAsFile(String orgName, String secretName, String secretPassword) throws Exception {\n        for (var ref : secretsProviders) {\n            var result = ref.provider().exportAsFile(orgName, secretName, secretPassword);\n            if (result.isPresent()) {\n                reportSecretFound(orgName, secretName, ref);\n                return result.get();\n            }\n        }\n        throw new RuntimeException(\"Secret not found: %s/%s.\".formatted(orgName, secretName));\n    }\n\n    @Override\n    public UsernamePassword exportCredentials(String orgName, String secretName, String secretPassword) throws Exception {\n        for (var ref : secretsProviders) {\n            var result = ref.provider().exportCredentials(orgName, secretName, secretPassword);\n            if (result.isPresent()) {\n                reportSecretFound(orgName, secretName, ref);\n                return result.get();\n            }\n        }\n        throw new RuntimeException(\"Secret not found: %s/%s.\".formatted(orgName, secretName));\n    }\n\n    @Override\n    public SecretCreationResult createKeyPair(SecretParams secret, KeyPair keyPair) throws Exception {\n        var ref = assertWritableProvider();\n        var result = ref.provider().createKeyPair(secret, keyPair);\n        reportSecretCreated(secret, ref);\n        return result;\n    }\n\n    @Override\n    public SecretCreationResult createUsernamePassword(SecretParams secret, UsernamePassword usernamePassword) throws Exception {\n        var ref = assertWritableProvider();\n        var result = ref.provider().createUsernamePassword(secret, usernamePassword);\n        reportSecretCreated(secret, ref);\n        return result;\n    }\n\n    @Override\n    public SecretCreationResult createData(SecretParams secret, byte[] data) throws Exception {\n        var ref = assertWritableProvider();\n        var result = ref.provider().createData(secret, data);\n        reportSecretCreated(secret, ref);\n        return result;\n    }\n\n    @Override\n    public String decryptString(String encryptedValue) {\n        return vaultProvider.getValue(encryptedValue);\n    }\n\n    @Override\n    public String encryptString(String orgName, String projectName, String value) {\n        throw new UnsupportedOperationException(\"Encrypting secrets is not supported by concord-cli yet\");\n    }\n\n    private SecretsProviderRef assertWritableProvider() {\n        return secretsProviders.stream().filter(SecretsProviderRef::writable)\n                .findFirst()\n                .orElseThrow(() -> new RuntimeException(\"No writable secret providers configured\"));\n    }\n\n    private void reportSecretFound(String orgName, String secretName, SecretsProviderRef ref) {\n        if (verbosity.verbose()) {\n            System.out.println(ansi().fgBlue().a(\"Fetched secret \").a(orgName).a(\"/\").a(secretName).a(\" from the '\").a(ref.name()).a(\"' provider\"));\n        }\n    }\n\n    private void reportSecretCreated(SecretParams params, SecretsProviderRef ref) {\n        if (verbosity.verbose()) {\n            System.out.println(ansi().fgBlue().a(\"Created secret \").a(params.orgName()).a(\"/\").a(params.secretName()).a(\" in the '\").a(ref.name()).a(\"' provider\"));\n        }\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/secrets/FileSecretsProvider.java",
    "content": "package com.walmartlabs.concord.cli.secrets;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.runtime.v2.sdk.SecretService;\n\nimport javax.annotation.Nullable;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.cli.secrets.UncheckedIO.*;\n\n/**\n * Simple file-based secret provider.\n * Secrets stored unencrypted in ${dir}/${orgName}/${secretName}.\n */\npublic class FileSecretsProvider implements SecretsProvider {\n\n    private final Path workDir;\n    private final Path secretStoreDir;\n    private final ObjectMapper objectMapper = new ObjectMapper();\n\n    public FileSecretsProvider(@Nullable Path workDir, Path secretStoreDir) {\n        this.workDir = workDir;\n        this.secretStoreDir = secretStoreDir;\n    }\n\n    @Override\n    public Optional<SecretService.KeyPair> exportKeyAsFile(String orgName, String secretName, String password) throws Exception {\n        var publicKey = toSecretPath(orgName, secretName + \".pub\");\n        var privateKey = toSecretPath(orgName, secretName);\n\n        if (Files.notExists(publicKey) || Files.notExists(privateKey)) {\n            return Optional.empty();\n        }\n\n        var tmpDir = UncheckedIO.assertTmpDir(assertWorkDir());\n        var tmpPublicKey = tmpDir.resolve(secretName + \".pub\");\n        var tmpPrivateKey = tmpDir.resolve(secretName);\n        Files.copy(publicKey, tmpPublicKey, StandardCopyOption.REPLACE_EXISTING);\n        Files.copy(privateKey, tmpPrivateKey, StandardCopyOption.REPLACE_EXISTING);\n\n        var result = SecretService.KeyPair.builder()\n                .privateKey(tmpPrivateKey)\n                .publicKey(tmpPublicKey)\n                .build();\n\n        return Optional.of(result);\n    }\n\n    @Override\n    public Optional<String> exportAsString(String orgName, String secretName, String password) throws IOException {\n        return getSecret(orgName, secretName)\n                .map(UncheckedIO::readAllBytes)\n                .map(String::new)\n                .map(String::trim);\n    }\n\n    @Override\n    public Optional<Path> exportAsFile(String orgName, String secretName, String password) throws IOException {\n        return getSecret(orgName, secretName)\n                .map(path -> {\n                    var tmpDir = assertTmpDir(assertWorkDir());\n                    var dest = createTempFile(tmpDir, \"file\", \".bin\");\n                    copy(path, dest, StandardCopyOption.REPLACE_EXISTING);\n                    return dest;\n                });\n    }\n\n    @Override\n    public Optional<SecretService.UsernamePassword> exportCredentials(String orgName, String secretName, String secretPassword) {\n        return getSecret(orgName, secretName).map(path -> {\n            try {\n                var data = objectMapper.readTree(path.toFile());\n\n                var username = Optional.ofNullable(data.get(\"username\")).map(JsonNode::asText)\n                        .orElseThrow(() -> new IllegalStateException(\"Secret %s/%s is missing the username field\".formatted(orgName, secretName)));\n                var password = Optional.ofNullable(data.get(\"password\")).map(JsonNode::asText)\n                        .orElseThrow(() -> new IllegalStateException(\"Secret %s/%s is missing the password field\".formatted(orgName, secretName)));\n\n                return SecretService.UsernamePassword.of(username, password);\n            } catch (IOException e) {\n                throw new RuntimeException(\"Invalid secret '%s/%s' ('%s') format: %s\".formatted(orgName, secretName, path, e.getMessage()));\n            }\n        });\n    }\n\n    @Override\n    public SecretService.SecretCreationResult createKeyPair(SecretService.SecretParams secret, SecretService.KeyPair keyPair) throws Exception {\n        var publicKey = createSecretFile(secret.orgName(), secret.secretName() + \".pub\");\n        var privateKey = createSecretFile(secret.orgName(), secret.secretName());\n\n        Files.copy(keyPair.publicKey(), publicKey);\n        Files.copy(keyPair.privateKey(), privateKey);\n\n        return SecretService.SecretCreationResult.builder()\n                .id(UUID.randomUUID())\n                .build();\n    }\n\n    @Override\n    public SecretService.SecretCreationResult createUsernamePassword(SecretService.SecretParams secret, SecretService.UsernamePassword usernamePassword) throws Exception {\n        var path = createSecretFile(secret.orgName(), secret.secretName());\n\n        objectMapper.writeValue(path.toFile(), Map.of(\n                \"username\", usernamePassword.username(),\n                \"password\", usernamePassword.password()\n        ));\n\n        return SecretService.SecretCreationResult.builder()\n                .id(UUID.randomUUID())\n                .build();\n    }\n\n    @Override\n    public SecretService.SecretCreationResult createData(SecretService.SecretParams secret, byte[] data) throws Exception {\n        var path = createSecretFile(secret.orgName(), secret.secretName());\n\n        Files.write(path, data);\n\n        return SecretService.SecretCreationResult.builder()\n                .id(UUID.randomUUID())\n                .build();\n    }\n\n    private Optional<Path> getSecret(String orgName, String secretName) {\n        var secretPath = toSecretPath(orgName, secretName);\n        if (Files.notExists(secretPath)) {\n            return Optional.empty();\n        }\n        return Optional.of(secretPath);\n    }\n\n    private Path createSecretFile(String orgName, String secretName) throws IOException {\n        var path = toSecretPath(orgName, secretName);\n\n        if (Files.exists(path)) {\n            throw new RuntimeException(\"Secret '%s/%s' ('%s') already exists\".formatted(orgName, secretName, path));\n        }\n\n        if (Files.notExists(path.getParent())) {\n            Files.createDirectories(path.getParent());\n        }\n\n        return path;\n    }\n\n    private Path toSecretPath(String orgName, String name) {\n        var secretPath = secretStoreDir;\n\n        if (orgName != null) {\n            secretPath = secretStoreDir.resolve(orgName);\n        }\n\n        var result = secretPath.resolve(name);\n\n        if (!result.normalize().startsWith(secretStoreDir)) {\n            throw new IllegalArgumentException(\"Invalid secret name: %s/%s\".formatted(orgName, name));\n        }\n\n        return result;\n    }\n\n    private Path assertWorkDir() {\n        if (this.workDir == null) {\n            throw new RuntimeException(\"The workDir must be specified for the export functions to work\");\n        }\n        return this.workDir;\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/secrets/RemoteSecretsProvider.java",
    "content": "package com.walmartlabs.concord.cli.secrets;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.cli.AbortException;\nimport com.walmartlabs.concord.cli.Confirmation;\nimport com.walmartlabs.concord.cli.Version;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.secret.BinaryDataSecret;\nimport com.walmartlabs.concord.common.secret.KeyPair;\nimport com.walmartlabs.concord.common.secret.UsernamePassword;\nimport com.walmartlabs.concord.runtime.v2.sdk.SecretService;\n\nimport javax.annotation.Nullable;\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.util.Objects;\nimport java.util.Optional;\n\nimport static java.util.Objects.requireNonNull;\n\npublic class RemoteSecretsProvider implements SecretsProvider {\n\n    private final Path workDir;\n    private final SecretClient secretClient;\n    private final boolean confirmAccess;\n\n    public RemoteSecretsProvider(@Nullable Path workDir, String baseUrl, String apiKey, boolean confirmAccess) {\n        this.workDir = workDir;\n        this.confirmAccess = confirmAccess;\n\n        var apiClient = new DefaultApiClientFactory(requireNonNull(baseUrl))\n                .create(ApiClientConfiguration.builder()\n                        .apiKey(requireNonNull(apiKey))\n                        .build());\n        apiClient.setUserAgent(\"Concord-Cli (%s)\".formatted(Version.getVersion()));\n\n        this.secretClient = new SecretClient(apiClient);\n    }\n\n    @Override\n    public Optional<SecretService.KeyPair> exportKeyAsFile(String orgName, String secretName, String secretPassword) throws Exception {\n        return getKeyPair(orgName, secretName, secretPassword)\n                .map(keyPair -> {\n                    var tmpDir = UncheckedIO.assertTmpDir(assertWorkDir());\n\n                    var publicKeyPath = tmpDir.resolve(secretName + \".pub\");\n                    UncheckedIO.write(publicKeyPath, keyPair.getPublicKey(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);\n\n                    var privateKeyPath = tmpDir.resolve(secretName);\n                    UncheckedIO.write(privateKeyPath, keyPair.getPrivateKey(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);\n\n                    return SecretService.KeyPair.builder()\n                            .privateKey(privateKeyPath)\n                            .publicKey(publicKeyPath)\n                            .build();\n                });\n    }\n\n    @Override\n    public Optional<String> exportAsString(String orgName, String secretName, String secretPassword) throws Exception {\n        return getBinaryDataSecret(orgName, secretName, secretPassword)\n                .map(BinaryDataSecret::getData)\n                .map(String::new);\n    }\n\n    @Override\n    public Optional<Path> exportAsFile(String orgName, String secretName, String secretPassword) throws Exception {\n        return getBinaryDataSecret(orgName, secretName, secretPassword)\n                .map(BinaryDataSecret::getData)\n                .map(data -> {\n                    var tmpDir = UncheckedIO.assertTmpDir(assertWorkDir());\n                    var secretPath = tmpDir.resolve(secretName + \".bin\");\n                    UncheckedIO.write(secretPath, data, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);\n                    return secretPath;\n                });\n    }\n\n    @Override\n    public Optional<SecretService.UsernamePassword> exportCredentials(String orgName, String secretName, String secretPassword) throws Exception {\n        return getUsernamePassword(orgName, secretName, secretPassword)\n                .map(secret -> SecretService.UsernamePassword.of(secret.getUsername(), new String(secret.getPassword())));\n    }\n\n    @Override\n    public SecretService.SecretCreationResult createKeyPair(SecretService.SecretParams secret, SecretService.KeyPair keyPair) throws Exception {\n        return toResult(secretClient.createSecret(secretRequest(secret)\n                .keyPair(CreateSecretRequest.KeyPair.builder()\n                        .publicKey(keyPair.publicKey())\n                        .privateKey(keyPair.privateKey())\n                        .build())\n                .build()));\n    }\n\n    @Override\n    public SecretService.SecretCreationResult createUsernamePassword(SecretService.SecretParams secret, SecretService.UsernamePassword usernamePassword) throws Exception {\n        return toResult(secretClient.createSecret(secretRequest(secret)\n                .usernamePassword(CreateSecretRequest.UsernamePassword.of(usernamePassword.username(), usernamePassword.password()))\n                .build()));\n    }\n\n    @Override\n    public SecretService.SecretCreationResult createData(SecretService.SecretParams secret, byte[] data) throws Exception {\n        return toResult(secretClient.createSecret(secretRequest(secret)\n                .data(data)\n                .build()));\n    }\n\n    private Optional<KeyPair> getKeyPair(String orgName, String secretName, String secretPassword) throws Exception {\n        askForAccessConfirmation(orgName, secretName);\n\n        try {\n            return Optional.of(secretClient.getData(orgName, secretName, secretPassword, SecretEntryV2.TypeEnum.KEY_PAIR));\n        } catch (com.walmartlabs.concord.client2.SecretNotFoundException e) {\n            return Optional.empty();\n        }\n    }\n\n    private Optional<BinaryDataSecret> getBinaryDataSecret(String orgName, String secretName, String secretPassword) throws Exception {\n        askForAccessConfirmation(orgName, secretName);\n\n        try {\n            return Optional.of(secretClient.getData(orgName, secretName, secretPassword, SecretEntryV2.TypeEnum.DATA));\n        } catch (com.walmartlabs.concord.client2.SecretNotFoundException e) {\n            return Optional.empty();\n        }\n    }\n\n    private Optional<UsernamePassword> getUsernamePassword(String orgName, String secretName, String secretPassword) throws Exception {\n        askForAccessConfirmation(orgName, secretName);\n\n        try {\n            return Optional.of(secretClient.getData(orgName, secretName, secretPassword, SecretEntryV2.TypeEnum.USERNAME_PASSWORD));\n        } catch (com.walmartlabs.concord.client2.SecretNotFoundException e) {\n            return Optional.empty();\n        }\n    }\n\n    private static SecretService.SecretCreationResult toResult(SecretOperationResponse response) {\n        return SecretService.SecretCreationResult.builder()\n                .id(response.getId())\n                .password(response.getPassword())\n                .build();\n    }\n\n    private ImmutableCreateSecretRequest.Builder secretRequest(SecretService.SecretParams params) {\n        var builder = CreateSecretRequest.builder()\n                .org(params.orgName())\n                .name(params.secretName())\n                .generatePassword(params.generatePassword())\n                .storePassword(params.storePassword());\n\n        var visibility = params.visibility();\n        if (visibility != null) {\n            builder.visibility(SecretEntryV2.VisibilityEnum.fromValue(visibility.name()));\n        }\n\n        if (params.project() != null) {\n            builder.addProjectNames(Objects.requireNonNull(params.project()));\n        }\n\n        return builder;\n    }\n\n    private void askForAccessConfirmation(String orgName, String secretName) throws IOException {\n        if (!confirmAccess) {\n            return;\n        }\n\n        if (!Confirmation.confirm(\"Fetching remote secret %s/%s. Are you sure you want to proceed? (y/N)\".formatted(orgName, secretName))) {\n            throw new AbortException();\n        }\n    }\n\n    private Path assertWorkDir() {\n        if (this.workDir == null) {\n            throw new RuntimeException(\"The workDir must be specified for the export functions to work\");\n        }\n        return this.workDir;\n    }\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/secrets/SecretsProvider.java",
    "content": "package com.walmartlabs.concord.cli.secrets;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.SecretService;\nimport com.walmartlabs.concord.runtime.v2.sdk.SecretService.KeyPair;\nimport com.walmartlabs.concord.runtime.v2.sdk.SecretService.SecretCreationResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.SecretService.UsernamePassword;\n\nimport java.nio.file.Path;\nimport java.util.Optional;\n\npublic interface SecretsProvider {\n\n    Optional<KeyPair> exportKeyAsFile(String orgName, String secretName, String password) throws Exception;\n\n    Optional<String> exportAsString(String orgName, String secretName, String password) throws Exception;\n\n    Optional<Path> exportAsFile(String orgName, String secretName, String password) throws Exception;\n\n    Optional<UsernamePassword> exportCredentials(String orgName, String secretName, String secretPassword) throws Exception;\n\n    SecretCreationResult createKeyPair(SecretService.SecretParams secret, KeyPair keyPair) throws Exception;\n\n    SecretCreationResult createUsernamePassword(SecretService.SecretParams secret, UsernamePassword usernamePassword) throws Exception;\n\n    SecretCreationResult createData(SecretService.SecretParams secret, byte[] data) throws Exception;\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/secrets/SecretsProviderRef.java",
    "content": "package com.walmartlabs.concord.cli.secrets;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic record SecretsProviderRef(String name, SecretsProvider provider, boolean writable) {\n\n}\n"
  },
  {
    "path": "cli/src/main/java/com/walmartlabs/concord/cli/secrets/UncheckedIO.java",
    "content": "package com.walmartlabs.concord.cli.secrets;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.cli.CliPaths;\nimport com.walmartlabs.concord.sdk.Constants;\n\nimport java.io.IOException;\nimport java.nio.file.CopyOption;\nimport java.nio.file.Files;\nimport java.nio.file.OpenOption;\nimport java.nio.file.Path;\n\npublic final class UncheckedIO {\n\n    public static Path assertTmpDir(Path workDir) {\n        var dir = CliPaths.defaultTargetDir(workDir).resolve(Constants.Files.CONCORD_TMP_DIR_NAME);\n        if (Files.notExists(dir)) {\n            try {\n                Files.createDirectories(dir);\n            } catch (IOException e) {\n                throw new RuntimeException(e.getMessage());\n            }\n        }\n        return dir;\n    }\n\n    public static Path write(Path path, byte[] bytes, OpenOption... options) {\n        try {\n            return Files.write(path, bytes, options);\n        } catch (IOException e) {\n            throw new RuntimeException(e.getMessage());\n        }\n    }\n\n    public static byte[] readAllBytes(Path path) {\n        try {\n            return Files.readAllBytes(path);\n        } catch (IOException e) {\n            throw new RuntimeException(e.getMessage());\n        }\n    }\n\n    public static Path createTempFile(Path dir, String prefix, String suffix) {\n        try {\n            return Files.createTempFile(dir, prefix, suffix);\n        } catch (IOException e) {\n            throw new RuntimeException(e.getMessage());\n        }\n    }\n\n    public static Path copy(Path source, Path target, CopyOption... options) {\n        try {\n            return Files.copy(source, target, options);\n        } catch (IOException e) {\n            throw new RuntimeException(e.getMessage());\n        }\n    }\n\n    private UncheckedIO() {\n    }\n}\n"
  },
  {
    "path": "cli/src/main/resources/com/walmartlabs/concord/cli/defaultCliConfig.yaml",
    "content": "contexts:\n  default:\n    secrets:\n      vault:\n        dir: \"${configDir}/vaults\"\n        id: \"default\"\n      local:\n        enabled: true\n        writable: true\n        dir: \"${configDir}/secrets\"\n      remote:\n        enabled: false\n        writable: false\n        confirmAccess: true\n"
  },
  {
    "path": "cli/src/main/resources/default-vars.json",
    "content": "{\n  \"ansible\": {\n    \"enableEvents\": false\n  }\n}"
  },
  {
    "path": "cli/src/main/resources/logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <withJansi>true</withJansi>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} %green([%thread]) %highlight(%msg%n)</pattern>\n        </encoder>\n    </appender>\n\n    <logger name=\"com.walmartlabs.concord.dependencymanager\" level=\"WARN\"/>\n    <logger name=\"com.walmartlabs.concord.repository\" level=\"WARN\"/>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "cli/src/test/filtered-resources/com/walmartlabs/concord/cli/defaultCfg/concord.yml",
    "content": "configuration:\n  debug: true\nflows:\n  default:\n    - log: \"isTrue = ${files.notExists('non-exist-dir')}\"\n"
  },
  {
    "path": "cli/src/test/filtered-resources/com/walmartlabs/concord/cli/defaultCfg/defaults.yml",
    "content": "configuration:\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.plugins.basic:file-tasks:${project.version}\"\n"
  },
  {
    "path": "cli/src/test/java/com/walmartlabs/concord/cli/AbstractTest.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.InputStream;\nimport java.io.PrintStream;\nimport java.nio.charset.StandardCharsets;\nimport java.util.regex.Pattern;\nimport java.util.concurrent.Callable;\n\nimport static org.junit.jupiter.api.Assertions.fail;\n\npublic abstract class AbstractTest {\n    private final PrintStream originalOut = System.out;\n    private final PrintStream originalErr = System.err;\n    private final InputStream originalIn = System.in;\n    private final ByteArrayOutputStream out = new ByteArrayOutputStream();\n    private final ByteArrayOutputStream err = new ByteArrayOutputStream();\n\n    @BeforeEach\n    public void setUpStreams() {\n        out.reset();\n        err.reset();\n        System.setOut(new PrintStream(out));\n        System.setErr(new PrintStream(err));\n    }\n\n    @AfterEach\n    public void restoreStreams() {\n        System.setOut(originalOut);\n        System.setErr(originalErr);\n        System.setIn(originalIn);\n    }\n\n    protected void assertLog(String pattern) {\n        String outStr = out.toString();\n        if (grep(outStr, pattern) != 1) {\n            fail(\"Expected a single log entry: '\" + pattern + \"', got: \\n\" + outStr);\n        }\n    }\n\n    protected void assertLog(String pattern, int times) {\n        String outStr = out.toString();\n        int found = grep(outStr, pattern);\n        if (found != times) {\n            fail(\"Expected [\" + times + \"] log entries: '\" + pattern + \"', got [\" + found + \"]: \\n\" + outStr);\n        }\n    }\n\n    protected String stdOut() {\n        return out.toString();\n    }\n\n    protected String stdErr() {\n        return err.toString();\n    }\n\n    protected void assertOutContainsRegex(String pattern) {\n        assertContainsRegex(stdOut(), pattern, \"stdout\");\n    }\n\n    protected void assertErrContainsRegex(String pattern) {\n        assertContainsRegex(stdErr(), pattern, \"stderr\");\n    }\n\n    protected <T> T withInput(String value, Callable<T> action) throws Exception {\n        var previous = System.getProperty(PromptSupport.ALLOW_STDIN_PROMPTS_PROPERTY);\n        System.setIn(new java.io.ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8)));\n        System.setProperty(PromptSupport.ALLOW_STDIN_PROMPTS_PROPERTY, \"true\");\n        try {\n            return action.call();\n        } finally {\n            if (previous == null) {\n                System.clearProperty(PromptSupport.ALLOW_STDIN_PROMPTS_PROPERTY);\n            } else {\n                System.setProperty(PromptSupport.ALLOW_STDIN_PROMPTS_PROPERTY, previous);\n            }\n        }\n    }\n\n    private static int grep(String str, String pattern) {\n        int cnt = 0;\n\n        String[] lines = str.split(\"\\\\r?\\\\n\");\n        for (String line : lines) {\n            if (line.matches(pattern)) {\n                cnt++;\n            }\n        }\n\n        return cnt;\n    }\n\n    private static void assertContainsRegex(String value, String pattern, String streamName) {\n        if (!Pattern.compile(pattern, Pattern.DOTALL | Pattern.MULTILINE).matcher(value).find()) {\n            fail(\"Expected \" + streamName + \" to match: '\" + pattern + \"', got:\\n\" + value);\n        }\n    }\n}\n"
  },
  {
    "path": "cli/src/test/java/com/walmartlabs/concord/cli/CliConfigTest.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.io.TempDir;\n\nimport java.io.IOException;\nimport java.net.URISyntaxException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\nimport static java.util.Objects.requireNonNull;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class CliConfigTest {\n\n    @TempDir\n    private Path tempDir;\n\n    @Test\n    public void parse() throws Exception {\n        var cfg = load(\"testConfig.yaml\");\n        var defaultCtx = cfg.contexts().get(\"default\");\n        assertNotNull(defaultCtx);\n        assertEquals(\"foo\", defaultCtx.secrets().vault().id());\n    }\n\n    @Test\n    public void checkDefaults() throws Exception {\n        var cfg = load(\"configWithDefaults.yaml\");\n        var defaultCtx = cfg.contexts().get(\"default\");\n        assertNotNull(defaultCtx);\n        assertNotNull(defaultCtx.secrets().vault().dir());\n        assertTrue(defaultCtx.secrets().vault().dir().toString().contains(\"/vaults\"));\n    }\n\n    @Test\n    public void withOverrides() throws Exception {\n        var cfg = load(\"testConfig.yaml\");\n        var defaultCtx = cfg.contexts().get(\"default\");\n        assertNotNull(defaultCtx);\n        var ctxWithOverrides = defaultCtx.withOverrides(new CliConfig.Overrides(Path.of(\"/barbaz\"), Path.of(\"/foobar\"), \"qux\"));\n        assertEquals(\"/barbaz\", ctxWithOverrides.secrets().local().dir().toString());\n        assertEquals(\"/foobar\", ctxWithOverrides.secrets().vault().dir().toString());\n        assertEquals(\"qux\", ctxWithOverrides.secrets().vault().id());\n    }\n\n    @Test\n    public void multiContexts() throws Exception {\n        var cfg = load(\"multiContextConfig.yaml\");\n        var anotherCtx = cfg.contexts().get(\"another\");\n        assertNotNull(anotherCtx);\n        assertEquals(\"bar\", anotherCtx.secrets().vault().id());\n        assertEquals(\"qux\", anotherCtx.secrets().vault().dir().toString());\n    }\n\n    @Test\n    public void missingContextWithoutUserConfig() throws Exception {\n        var homeDir = tempDir.resolve(\"missing-context-home\");\n\n        var e = withUserHome(homeDir, () -> assertThrows(CliConfig.MissingContextException.class,\n                () -> CliConfig.loadOrThrow(new Verbosity(new boolean[0]), \"another\", new CliConfig.Overrides(null, null, null))));\n\n        assertTrue(e.getMessage().contains(\"Configuration context not found: another\"));\n        assertTrue(e.getMessage().contains(\"only the built-in 'default' context is available\"));\n    }\n\n    @Test\n    public void missingContextWithUserConfig() throws Exception {\n        var homeDir = tempDir.resolve(\"configured-home\");\n        var configDir = homeDir.resolve(\".concord\");\n        Files.createDirectories(configDir);\n        Files.writeString(configDir.resolve(\"cli.yaml\"), \"\"\"\n                contexts:\n                  default: {}\n                \"\"\");\n\n        var e = withUserHome(homeDir, () -> assertThrows(CliConfig.MissingContextException.class,\n                () -> CliConfig.loadOrThrow(new Verbosity(new boolean[0]), \"another\", new CliConfig.Overrides(null, null, null))));\n\n        assertEquals(\"Configuration context not found: another. Check the CLI configuration file.\", e.getMessage());\n    }\n\n    private static CliConfig load(String resource) throws IOException, URISyntaxException {\n        var src = Paths.get(requireNonNull(CliConfigTest.class.getResource(resource)).toURI());\n        return CliConfig.loadConfigFile(src);\n    }\n\n    private static <T> T withUserHome(Path userHome, java.util.concurrent.Callable<T> action) throws Exception {\n        var originalUserHome = System.getProperty(\"user.home\");\n        System.setProperty(\"user.home\", userHome.toString());\n        try {\n            return action.call();\n        } finally {\n            if (originalUserHome == null) {\n                System.clearProperty(\"user.home\");\n            } else {\n                System.setProperty(\"user.home\", originalUserHome);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "cli/src/test/java/com/walmartlabs/concord/cli/GitIgnoreFilterTest.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.io.TempDir;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nclass GitIgnoreFilterTest {\n\n    @TempDir\n    Path tempDir;\n\n    @Test\n    void testNoGitignoreReturnsNull() throws IOException {\n        var filter = GitIgnoreFilter.load(tempDir);\n        assertNull(filter);\n    }\n\n    @Test\n    void testEmptyGitignoreReturnsNull() throws IOException {\n        Files.writeString(tempDir.resolve(\".gitignore\"), \"\");\n        var filter = GitIgnoreFilter.load(tempDir);\n        assertNull(filter);\n    }\n\n    @Test\n    void testCommentsOnlyReturnsNull() throws IOException {\n        Files.writeString(tempDir.resolve(\".gitignore\"), \"# This is a comment\\n# Another comment\\n\");\n        var filter = GitIgnoreFilter.load(tempDir);\n        assertNull(filter);\n    }\n\n    @Test\n    void testBasicPatterns() throws IOException {\n        Files.writeString(tempDir.resolve(\".gitignore\"), \"*.log\\nnode_modules/\\n\");\n\n        var filter = GitIgnoreFilter.load(tempDir);\n        assertNotNull(filter);\n\n        assertTrue(filter.isIgnored(Path.of(\"debug.log\"), false));\n        assertTrue(filter.isIgnored(Path.of(\"error.log\"), false));\n        assertFalse(filter.isIgnored(Path.of(\"debug.txt\"), false));\n\n        assertTrue(filter.isIgnored(Path.of(\"node_modules\"), true));\n        assertFalse(filter.isIgnored(Path.of(\"other_modules\"), true));\n    }\n\n    @Test\n    void testGlobPatterns() throws IOException {\n        Files.writeString(tempDir.resolve(\".gitignore\"), \"**/*.tmp\\nbuild/**/output\\n\");\n\n        var filter = GitIgnoreFilter.load(tempDir);\n        assertNotNull(filter);\n\n        assertTrue(filter.isIgnored(Path.of(\"test.tmp\"), false));\n        assertTrue(filter.isIgnored(Path.of(\"sub/dir/test.tmp\"), false));\n        assertFalse(filter.isIgnored(Path.of(\"test.txt\"), false));\n\n        assertTrue(filter.isIgnored(Path.of(\"build/output\"), true));\n        assertTrue(filter.isIgnored(Path.of(\"build/sub/output\"), true));\n    }\n\n    @Test\n    void testNegationPatterns() throws IOException {\n        Files.writeString(tempDir.resolve(\".gitignore\"), \"*.log\\n!important.log\\n\");\n\n        var filter = GitIgnoreFilter.load(tempDir);\n        assertNotNull(filter);\n\n        assertTrue(filter.isIgnored(Path.of(\"debug.log\"), false));\n        assertTrue(filter.isIgnored(Path.of(\"error.log\"), false));\n        assertFalse(filter.isIgnored(Path.of(\"important.log\"), false));\n    }\n\n    @Test\n    void testDirectoryOnlyPatterns() throws IOException {\n        Files.writeString(tempDir.resolve(\".gitignore\"), \"build/\\n\");\n\n        var filter = GitIgnoreFilter.load(tempDir);\n        assertNotNull(filter);\n\n        assertTrue(filter.isIgnored(Path.of(\"build\"), true));\n        // A file named \"build\" should not be ignored when pattern has trailing slash\n        assertFalse(filter.isIgnored(Path.of(\"build\"), false));\n    }\n\n    @Test\n    void testAnchoredPatterns() throws IOException {\n        Files.writeString(tempDir.resolve(\".gitignore\"), \"/config.local\\n\");\n\n        var filter = GitIgnoreFilter.load(tempDir);\n        assertNotNull(filter);\n\n        assertTrue(filter.isIgnored(Path.of(\"config.local\"), false));\n        // Anchored pattern should not match in subdirectories\n        assertFalse(filter.isIgnored(Path.of(\"sub/config.local\"), false));\n    }\n\n    @Test\n    void testNestedGitignore() throws IOException {\n        // Root .gitignore\n        Files.writeString(tempDir.resolve(\".gitignore\"), \"*.log\\n\");\n\n        // Create subdirectory with its own .gitignore\n        var subDir = tempDir.resolve(\"subdir\");\n        Files.createDirectories(subDir);\n        Files.writeString(subDir.resolve(\".gitignore\"), \"*.txt\\n!keep.txt\\n\");\n\n        var filter = GitIgnoreFilter.load(tempDir);\n        assertNotNull(filter);\n\n        // Root patterns apply everywhere\n        assertTrue(filter.isIgnored(Path.of(\"debug.log\"), false));\n        assertTrue(filter.isIgnored(Path.of(\"subdir/debug.log\"), false));\n\n        // Subdir patterns only apply within subdir\n        assertFalse(filter.isIgnored(Path.of(\"test.txt\"), false));\n        assertTrue(filter.isIgnored(Path.of(\"subdir/test.txt\"), false));\n        assertFalse(filter.isIgnored(Path.of(\"subdir/keep.txt\"), false));\n    }\n\n    @Test\n    void testBlankLinesIgnored() throws IOException {\n        Files.writeString(tempDir.resolve(\".gitignore\"), \"*.log\\n\\n\\n*.tmp\\n\");\n\n        var filter = GitIgnoreFilter.load(tempDir);\n        assertNotNull(filter);\n\n        assertTrue(filter.isIgnored(Path.of(\"debug.log\"), false));\n        assertTrue(filter.isIgnored(Path.of(\"test.tmp\"), false));\n    }\n\n    @Test\n    void testMixedPatterns() throws IOException {\n        Files.writeString(tempDir.resolve(\".gitignore\"), \"\"\"\n                # Dependencies\n                node_modules/\n\n                # Build output\n                build/\n                dist/\n                *.class\n\n                # IDE\n                .idea/\n                *.iml\n\n                # Logs\n                *.log\n                !important.log\n\n                # Temp files\n                **/*.tmp\n                \"\"\");\n\n        var filter = GitIgnoreFilter.load(tempDir);\n        assertNotNull(filter);\n\n        assertTrue(filter.isIgnored(Path.of(\"node_modules\"), true));\n        assertTrue(filter.isIgnored(Path.of(\"build\"), true));\n        assertTrue(filter.isIgnored(Path.of(\"dist\"), true));\n        assertTrue(filter.isIgnored(Path.of(\"Main.class\"), false));\n        assertTrue(filter.isIgnored(Path.of(\".idea\"), true));\n        assertTrue(filter.isIgnored(Path.of(\"project.iml\"), false));\n        assertTrue(filter.isIgnored(Path.of(\"app.log\"), false));\n        assertFalse(filter.isIgnored(Path.of(\"important.log\"), false));\n        assertTrue(filter.isIgnored(Path.of(\"deep/nested/file.tmp\"), false));\n    }\n\n    @Test\n    void testSubdirectoryPaths() throws IOException {\n        Files.writeString(tempDir.resolve(\".gitignore\"), \"target/\\n\");\n\n        var filter = GitIgnoreFilter.load(tempDir);\n        assertNotNull(filter);\n\n        assertTrue(filter.isIgnored(Path.of(\"target\"), true));\n        assertTrue(filter.isIgnored(Path.of(\"sub/target\"), true));\n    }\n\n    @Test\n    void testFilesInIgnoredDirectory() throws IOException {\n        Files.writeString(tempDir.resolve(\".gitignore\"), \"build/\\n\");\n\n        var filter = GitIgnoreFilter.load(tempDir);\n        assertNotNull(filter);\n\n        // The directory itself is ignored\n        assertTrue(filter.isIgnored(Path.of(\"build\"), true));\n        // Files inside an ignored directory are NOT directly matched by the pattern.\n        // In practice, the walk would skip the directory entirely, so this case wouldn't occur.\n        // The pattern `build/` only matches directories, not paths within them.\n        assertFalse(filter.isIgnored(Path.of(\"build/output.jar\"), false));\n    }\n}\n"
  },
  {
    "path": "cli/src/test/java/com/walmartlabs/concord/cli/LintTest.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.TemporaryPath;\nimport org.junit.jupiter.api.Test;\nimport picocli.CommandLine;\n\nimport java.net.URI;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nclass LintTest extends AbstractTest {\n\n    @Test\n    void lintV1Test() throws Exception {\n        int exitCode = lint(\"lintV1\");\n        assertEquals(0, exitCode);\n        assertLog(\".*flows: 2.*\");\n    }\n\n    @Test\n    void lintV2Test() throws Exception {\n        int exitCode = lint(\"lintV2\");\n        assertEquals(0, exitCode);\n        assertLog(\".*flows: 2.*\");\n    }\n\n    private static int lint(String payload) throws Exception {\n        URI uri = LintTest.class.getResource(payload).toURI();\n        Path source = Paths.get(uri);\n\n        try (TemporaryPath dst = PathUtils.tempDir(\"cli-tests\")) {\n            PathUtils.copy(source, dst.path());\n\n            App app = new App();\n            CommandLine cmd = new CommandLine(app);\n\n            List<String> args = new ArrayList<>();\n            args.add(\"lint\");\n\n            Path pwd = Paths.get(System.getProperty(\"user.dir\")).toAbsolutePath();\n            Path relative = pwd.relativize(dst.path());\n\n            args.add(relative.toString());\n\n            return cmd.execute(args.toArray(new String[0]));\n        }\n    }\n}\n"
  },
  {
    "path": "cli/src/test/java/com/walmartlabs/concord/cli/ResumeTest.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.io.TempDir;\nimport picocli.CommandLine;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nclass ResumeTest extends AbstractTest {\n\n    @TempDir\n    private Path tempDir;\n\n    @Test\n    void suspendedRunPersistsStateAndPrintsGuidance() throws Exception {\n        var source = preparePayload(\"suspend\");\n        var targetDir = CliPaths.defaultTargetDir(source);\n\n        var exitCode = executeIn(source, runArgs());\n\n        assertEquals(CliExitCodes.SUSPENDED, exitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n        assertLog(\".*before suspend.*\");\n        assertOutContainsRegex(\"Process suspended\\\\.\\\\R\\\\RResume dir: \" + quoted(source) + \"\\\\RCommands below assume you are in that directory\\\\.\\\\R\\\\RAdditional waiting events:\\\\R  ev1\\\\R\\\\RContinue with:\\\\R  Resume event:\\\\R    concord resume --event ev1\");\n        assertFalse(stdOut().contains(\"...done!\"), stdOut());\n        assertTrue(Files.exists(targetDir.resolve(\"_attachments\").resolve(\"_state\").resolve(\"instance\")));\n        assertTrue(Files.exists(targetDir.resolve(\"_attachments\").resolve(\"_state\").resolve(\"_suspend\")));\n        assertTrue(Files.exists(targetDir.resolve(\"_attachments\").resolve(\"_state\").resolve(\"_cliResume.json\")));\n    }\n\n    @Test\n    void resumeConsumesInputFileAndSaveAs() throws Exception {\n        var source = preparePayload(\"suspend\");\n        var targetDir = CliPaths.defaultTargetDir(source);\n        var inputFile = tempDir.resolve(\"payload.yml\");\n\n        Files.writeString(inputFile, \"value: resumed-value\\n\");\n\n        var runExitCode = executeIn(source, runArgs());\n        assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n\n        var afterRun = stdOut();\n\n        var resumeExitCode = executeIn(source, List.of(\"resume\", \"--input-file\", inputFile.toString(), \"--save-as\", \"myForm\"));\n\n        assertEquals(CliExitCodes.SUCCESS, resumeExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n\n        var resumeOutput = stdOut().substring(afterRun.length());\n        assertTrue(resumeOutput.contains(\"after resume: resumed-value\"), resumeOutput);\n        assertTrue(resumeOutput.contains(\"...done!\"), resumeOutput);\n        assertFalse(Files.exists(targetDir.resolve(\"_attachments\").resolve(\"_state\")), targetDir.toString());\n    }\n\n    @Test\n    void resumeConsumesInlineNestedInput() throws Exception {\n        var source = preparePayload(\"suspend\");\n        var targetDir = CliPaths.defaultTargetDir(source);\n\n        var runExitCode = executeIn(source, runArgs());\n        assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n\n        var afterRun = stdOut();\n\n        var resumeExitCode = executeIn(source, List.of(\"resume\", \"-e\", \"myForm.value=resumed-inline\"));\n\n        assertEquals(CliExitCodes.SUCCESS, resumeExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n\n        var resumeOutput = stdOut().substring(afterRun.length());\n        assertTrue(resumeOutput.contains(\"after resume: resumed-inline\"), resumeOutput);\n        assertTrue(resumeOutput.contains(\"...done!\"), resumeOutput);\n        assertFalse(Files.exists(targetDir.resolve(\"_attachments\").resolve(\"_state\")), targetDir.toString());\n    }\n\n    @Test\n    void resumePromptsForPendingStandardForms() throws Exception {\n        var source = preparePayload(\"form\");\n        var targetDir = CliPaths.defaultTargetDir(source);\n\n        var runExitCode = executeIn(source, runArgs());\n        assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n        assertOutContainsRegex(\"Process suspended\\\\.\\\\R\\\\RResume dir: \" + quoted(source) + \"\\\\RCommands below assume you are in that directory\\\\.\\\\R\\\\RPending forms:\\\\R  Form key\\\\s+Event ID\\\\R  myForm\\\\s+[^\\\\n]+\\\\R\\\\RContinue with:\\\\R  Describe input:\\\\R    concord resume --event [^\\\\s]+ --describe-input\\\\R  Submit input:\\\\R    concord resume --event [^\\\\s]+ --input-file myForm\\\\.yml\");\n        assertFalse(stdOut().contains(\"concord resume \" + source), stdOut());\n        assertFalse(stdOut().contains(\"Fill pending form now?\"), stdOut());\n        assertFalse(stdOut().contains(\"Fill interactively:\"), stdOut());\n        assertTrue(Files.exists(targetDir.resolve(\"_attachments\").resolve(\"_state\").resolve(\"V2forms\").resolve(\"myForm\")));\n\n        var afterRun = stdOut();\n\n        var resumeExitCode = withInput(\"John Smith\\n33\\n\", () -> executeIn(source, List.of(\"resume\")));\n\n        assertEquals(CliExitCodes.SUCCESS, resumeExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n\n        var resumeOutput = stdOut().substring(afterRun.length());\n        assertOutContainsRegex(\"Pending form input:\\\\R  myForm -> [^\\\\n]+\");\n        assertTrue(resumeOutput.contains(\"after form: John Smith, 33\"), resumeOutput);\n        assertTrue(resumeOutput.contains(\"...done!\"), resumeOutput);\n        assertFalse(Files.exists(targetDir.resolve(\"_attachments\").resolve(\"_state\")), targetDir.toString());\n    }\n\n    @Test\n    void resumeFailsFastForFormsWithoutInteractiveInput() throws Exception {\n        var source = preparePayload(\"form\");\n\n        var runExitCode = executeIn(source, runArgs());\n        assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n\n        var resumeExitCode = executeIn(source, List.of(\"resume\"));\n\n        assertEquals(CliExitCodes.INPUT_REQUIRED, resumeExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n        assertErrContainsRegex(\"Resume dir: \" + quoted(source) + \"\\\\RCommands below assume you are in that directory\\\\.\\\\R\\\\RPending form requires input in non-interactive mode\\\\.\\\\R\\\\RPending forms:\\\\R  Form key\\\\s+Event ID\\\\R  myForm\\\\s+[^\\\\n]+\\\\R\\\\RContinue with:\\\\R  Describe input:\\\\R    concord resume --event [^\\\\s]+ --describe-input\\\\R  Submit input:\\\\R    concord resume --event [^\\\\s]+ --input-file myForm\\\\.yml\");\n        assertFalse(stdErr().contains(\"Fill interactively:\"), stdErr());\n    }\n\n    @Test\n    void describeInputShowsExpectedFormShape() throws Exception {\n        var source = preparePayload(\"form\");\n\n        var runExitCode = executeIn(source, runArgs());\n        assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n\n        var afterRun = stdOut();\n        var describeExitCode = executeIn(source, List.of(\"resume\", \"--describe-input\"));\n\n        assertEquals(CliExitCodes.SUCCESS, describeExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n\n        var describeOutput = stdOut().substring(afterRun.length());\n        assertOutContainsRegex(\"Resume dir: \" + quoted(source) + \"\\\\RPending form input:\\\\R  myForm -> [^\\\\n]+\\\\RRequired fields:\\\\R  name\\\\R  age\");\n        assertTrue(describeOutput.contains(\"Example input file:\"), describeOutput);\n        assertTrue(describeOutput.contains(\"myForm:\"), describeOutput);\n        assertTrue(describeOutput.contains(\"name: \\\"\\\"\"), describeOutput);\n        assertTrue(describeOutput.contains(\"age: 0\"), describeOutput);\n    }\n\n    @Test\n    void runOffersImmediateFormFill() throws Exception {\n        var source = preparePayload(\"form\");\n        var targetDir = CliPaths.defaultTargetDir(source);\n\n        var exitCode = withInput(\"\\nJohn Smith\\n33\\n\", () -> executeIn(source, runArgs()));\n\n        assertEquals(CliExitCodes.SUCCESS, exitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n        assertTrue(stdOut().contains(\"Fill pending form now? (Y/n)\"), stdOut());\n        assertTrue(stdOut().contains(\"Name [string, required]:\"), stdOut());\n        assertTrue(stdOut().contains(\"after form: John Smith, 33\"), stdOut());\n        assertTrue(stdOut().contains(\"...done!\"), stdOut());\n        assertFalse(stdOut().contains(\"Process suspended.\"), stdOut());\n        assertFalse(Files.exists(targetDir.resolve(\"_attachments\").resolve(\"_state\")), targetDir.toString());\n    }\n\n    @Test\n    void runNoPromptSkipsImmediateFormFill() throws Exception {\n        var source = preparePayload(\"form\");\n\n        var exitCode = withInput(\"\\nJohn Smith\\n33\\n\", () -> executeIn(source, List.of(\"run\", \"--no-default-cfg\", \"--no-prompt\")));\n\n        assertEquals(CliExitCodes.SUSPENDED, exitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n        assertFalse(stdOut().contains(\"Fill pending form now?\"), stdOut());\n        assertOutContainsRegex(\"Process suspended\\\\.\\\\R\\\\RResume dir: \" + quoted(source) + \"\\\\RCommands below assume you are in that directory\\\\.\\\\R\\\\RPending forms:\\\\R  Form key\\\\s+Event ID\\\\R  myForm\\\\s+[^\\\\n]+\");\n    }\n\n    @Test\n    void interruptedImmediateFormFillCanBeResumed() throws Exception {\n        var source = preparePayload(\"parallelForms\");\n        var targetDir = CliPaths.defaultTargetDir(source);\n\n        var runExitCode = withInput(\"\\nfirst-value\\n\", () -> executeIn(source, runArgs()));\n\n        assertEquals(CliExitCodes.ERROR, runExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n        assertTrue(Files.exists(targetDir.resolve(\"_attachments\").resolve(\"_state\").resolve(\"_cliResume.json\")));\n\n        var afterRun = stdOut();\n        var resumeExitCode = withInput(\"second-value\\n\", () -> executeIn(source, List.of(\"resume\")));\n\n        assertEquals(CliExitCodes.SUCCESS, resumeExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n\n        var resumeOutput = stdOut().substring(afterRun.length());\n        assertOutContainsRegex(\"Pending form input:\\\\R  form2 -> [^\\\\n]+\");\n        assertTrue(stdOut().contains(\"parallel forms: form1=first-value\"), stdOut());\n        assertTrue(resumeOutput.contains(\"parallel forms: done one=first-value two=second-value\"), resumeOutput);\n        assertTrue(resumeOutput.contains(\"...done!\"), resumeOutput);\n        assertFalse(Files.exists(targetDir.resolve(\"_attachments\").resolve(\"_state\")), targetDir.toString());\n    }\n\n    @Test\n    void parallelFormsRunShowsMappingsAndAutomationHints() throws Exception {\n        var source = preparePayload(\"parallelForms\");\n\n        var runExitCode = executeIn(source, runArgs());\n\n        assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n        assertOutContainsRegex(\"Process suspended\\\\.\\\\R\\\\RResume dir: \" + quoted(source) + \"\\\\RCommands below assume you are in that directory\\\\.\\\\R\\\\RPending forms:\\\\R  Form key\\\\s+Event ID\\\\R  form1\\\\s+[^\\\\n]+\\\\R  form2\\\\s+[^\\\\n]+\\\\R\\\\RContinue with:\\\\R  Describe input:\\\\R    concord resume --event [^\\\\s]+ --describe-input\\\\R    concord resume --event [^\\\\s]+ --describe-input\\\\R  Submit input:\\\\R    concord resume --event [^\\\\s]+ --input-file form1\\\\.yml\\\\R    concord resume --event [^\\\\s]+ --input-file form2\\\\.yml\");\n        assertFalse(stdOut().contains(\"concord resume \" + source), stdOut());\n    }\n\n    @Test\n    void resumeWithoutPayloadOnMultipleFormsRequiresExplicitEvent() throws Exception {\n        var source = preparePayload(\"parallelForms\");\n\n        var runExitCode = executeIn(source, runArgs());\n        assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n\n        var resumeExitCode = executeIn(source, List.of(\"resume\"));\n\n        assertEquals(CliExitCodes.INPUT_REQUIRED, resumeExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n        assertErrContainsRegex(\"Resume dir: \" + quoted(source) + \"\\\\RCommands below assume you are in that directory\\\\.\\\\R\\\\RPending forms require input or explicit event selection\\\\.\\\\R\\\\RPending forms:\\\\R  Form key\\\\s+Event ID\\\\R  form1\\\\s+[^\\\\n]+\\\\R  form2\\\\s+[^\\\\n]+\");\n        assertTrue(stdErr().contains(\"--describe-input\"), stdErr());\n        assertTrue(stdErr().contains(\"--input-file form1.yml\"), stdErr());\n        assertTrue(stdErr().contains(\"--input-file form2.yml\"), stdErr());\n    }\n\n    @Test\n    void describeInputRequiresEventForMultipleForms() throws Exception {\n        var source = preparePayload(\"parallelForms\");\n\n        var runExitCode = executeIn(source, runArgs());\n        assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n\n        var describeExitCode = executeIn(source, List.of(\"resume\", \"--describe-input\"));\n\n        assertEquals(CliExitCodes.INPUT_REQUIRED, describeExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n        assertErrContainsRegex(\"Resume dir: \" + quoted(source) + \"\\\\RCommands below assume you are in that directory\\\\.\\\\R\\\\RPending forms require explicit event selection before describing input\\\\.\\\\R\\\\RPending forms:\\\\R  Form key\\\\s+Event ID\\\\R  form1\\\\s+[^\\\\n]+\\\\R  form2\\\\s+[^\\\\n]+\\\\R\\\\RContinue with:\\\\R  Describe input:\");\n        assertTrue(stdErr().contains(\"--describe-input\"), stdErr());\n    }\n\n    @Test\n    void mixedFormAndEventGuidanceIncludesBothChoices() throws Exception {\n        var source = preparePayload(\"mixedFormEvent\");\n\n        var runExitCode = executeIn(source, runArgs());\n\n        assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n        assertOutContainsRegex(\"Process suspended\\\\.\\\\R\\\\RResume dir: \" + quoted(source) + \"\\\\RCommands below assume you are in that directory\\\\.\\\\R\\\\RPending forms:\\\\R  Form key\\\\s+Event ID\\\\R  approvalForm\\\\s+[^\\\\n]+\\\\R\\\\RAdditional waiting events:\\\\R  ev_timeout\\\\R\\\\RContinue with:\\\\R  Describe input:\\\\R    concord resume --event [^\\\\s]+ --describe-input\\\\R  Submit input:\\\\R    concord resume --event [^\\\\s]+ --input-file approvalForm\\\\.yml\\\\R  Resume event:\\\\R    concord resume --event ev_timeout\");\n    }\n\n    @Test\n    void fileUploadFormDescribeInputAndResumeFailure() throws Exception {\n        var source = preparePayload(\"fileForm\");\n        var inputFile = tempDir.resolve(\"uploadForm.yml\");\n        Files.writeString(inputFile, \"uploadForm:\\n  attachment: path/to/file\\n\");\n\n        var runExitCode = executeIn(source, runArgs());\n        assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n        assertTrue(stdOut().contains(\"not supported for file-upload fields\"), stdOut());\n\n        var afterRun = stdOut();\n        var describeExitCode = executeIn(source, List.of(\"resume\", \"--describe-input\"));\n\n        assertEquals(CliExitCodes.SUCCESS, describeExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n\n        var describeOutput = stdOut().substring(afterRun.length());\n        assertTrue(describeOutput.contains(\"File-upload fields:\"), describeOutput);\n        assertTrue(describeOutput.contains(\"attachment\"), describeOutput);\n        assertTrue(describeOutput.contains(\"not supported for file-upload fields\"), describeOutput);\n\n        var resumeExitCode = executeIn(source, List.of(\"resume\", \"--input-file\", inputFile.toString()));\n\n        assertEquals(CliExitCodes.NON_INTERACTIVE_UNSUPPORTED, resumeExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n        assertErrContainsRegex(\"Resume dir: \" + quoted(source) + \"\\\\RCommands below assume you are in that directory\\\\.\\\\R\\\\RPending form cannot be submitted non-interactively because it contains file-upload fields\\\\.\\\\R\\\\RPending forms:\\\\R  Form key\\\\s+Event ID\\\\R  uploadForm\\\\s+[^\\\\n]+\");\n    }\n\n    @Test\n    void nonInteractiveFormInputIsConvertedAndValidated() throws Exception {\n        var source = preparePayload(\"validatedForm\");\n        var targetDir = CliPaths.defaultTargetDir(source);\n        var invalidInputFile = tempDir.resolve(\"invalidValidatedForm.yml\");\n        var validInputFile = tempDir.resolve(\"validValidatedForm.yml\");\n\n        Files.writeString(invalidInputFile, \"\"\"\n                validatedForm:\n                  name: Alice\n                  age: 10\n                  choice: green\n                \"\"\");\n        Files.writeString(validInputFile, \"\"\"\n                name: Alice\n                age: 33\n                choice: red\n                \"\"\");\n\n        var runExitCode = executeIn(source, runArgs());\n        assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n\n        var invalidResumeExitCode = executeIn(source, List.of(\"resume\", \"--input-file\", invalidInputFile.toString()));\n        assertEquals(CliExitCodes.INPUT_REQUIRED, invalidResumeExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n        assertTrue(stdErr().contains(\"Invalid form input:\"), stdErr());\n        assertTrue(stdErr().contains(\"Age\"), stdErr());\n        assertTrue(stdErr().contains(\"Choice\"), stdErr());\n        assertTrue(Files.exists(targetDir.resolve(\"_attachments\").resolve(\"_state\").resolve(\"instance\")));\n\n        var afterInvalidResume = stdOut();\n        var validResumeExitCode = executeIn(source, List.of(\"resume\", \"--input-file\", validInputFile.toString()));\n\n        assertEquals(CliExitCodes.SUCCESS, validResumeExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n\n        var resumeOutput = stdOut().substring(afterInvalidResume.length());\n        assertTrue(resumeOutput.contains(\"validated form: name=Alice, age=33, choice=red, note=default-note, readOnly=locked\"), resumeOutput);\n        assertTrue(resumeOutput.contains(\"...done!\"), resumeOutput);\n        assertFalse(Files.exists(targetDir.resolve(\"_attachments\").resolve(\"_state\")), targetDir.toString());\n    }\n\n    @Test\n    void fileUploadFormRetryCleansFailedAttemptTempFiles() throws Exception {\n        var source = preparePayload(\"fileRetryForm\");\n        var uploadFile = tempDir.resolve(\"upload.txt\");\n        Files.writeString(uploadFile, \"upload contents\");\n\n        var beforeTmpFiles = listTmpFiles(\"attachment\");\n        var exitCode = withInput(\"\\n\" + uploadFile + \"\\n10\\n\" + uploadFile + \"\\n33\\n\", () -> executeIn(source, runArgs()));\n\n        assertEquals(CliExitCodes.SUCCESS, exitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n        assertTrue(stdOut().contains(\"after upload retry: 33\"), stdOut());\n        assertEquals(beforeTmpFiles, listTmpFiles(\"attachment\"));\n    }\n\n    @Test\n    void resumeMetadataStoresAbsolutePathsAndResolvesOldRelativePaths() {\n        var metadata = LocalSuspendPersistence.ResumeMetadata.from(Path.of(\"work\"),\n                Path.of(\"resume\"),\n                Path.of(\"defaultTaskVars.json\"),\n                Path.of(\"deps\"),\n                \"default\",\n                new CliConfig.Overrides(Path.of(\"secrets\"), Path.of(\"vault\"), \"test\"),\n                List.of(),\n                null,\n                null);\n\n        assertTrue(Path.of(metadata.workDir()).isAbsolute());\n        assertTrue(Path.of(metadata.resumeDir()).isAbsolute());\n        assertTrue(Path.of(metadata.defaultTaskVars()).isAbsolute());\n        assertTrue(Path.of(metadata.depsCacheDir()).isAbsolute());\n        assertTrue(Path.of(metadata.cliConfig().secretStoreDir()).isAbsolute());\n        assertTrue(Path.of(metadata.cliConfig().vaultDir()).isAbsolute());\n\n        var oldMetadata = new LocalSuspendPersistence.ResumeMetadata(null,\n                null,\n                List.of(),\n                \"resume\",\n                \"resume/target\",\n                \"defaultTaskVars.json\",\n                \"deps\",\n                new LocalSuspendPersistence.CliConfigData(\"default\", false, \"secrets\", \"vault\", \"test\"));\n\n        assertEquals(Path.of(\"resume\", \"defaultTaskVars.json\").normalize().toAbsolutePath(), oldMetadata.defaultTaskVarsPath());\n        assertEquals(Path.of(\"resume\", \"deps\").normalize().toAbsolutePath(), oldMetadata.depsCacheDirPath());\n    }\n\n    @Test\n    void suspendedMetadataOmitsApiKeyAndResumeReloadsStoredContextAndOverrides() throws Exception {\n        var source = preparePayload(\"secretResume\");\n        var targetDir = CliPaths.defaultTargetDir(source);\n        var homeDir = tempDir.resolve(\"home\");\n        var overrideSecretDir = tempDir.resolve(\"override-secrets\");\n        writeSecret(overrideSecretDir, \"Default\", \"resumeSecret\", \"value-from-override\");\n        writeCliConfig(homeDir, \"\"\"\n                contexts:\n                  default:\n                    secrets:\n                      local:\n                        enabled: true\n                        writable: true\n                        dir: \"%s\"\n                      remote:\n                        enabled: true\n                        writable: false\n                        baseUrl: \"http://localhost:8001\"\n                        apiKey: \"resume-api-key\"\n                  another:\n                    secrets:\n                      local:\n                        dir: \"%s\"\n                \"\"\".formatted(tempDir.resolve(\"wrong-default-secrets\"), tempDir.resolve(\"wrong-another-secrets\")));\n\n        var runExitCode = withUserHome(homeDir, () -> executeIn(source, runArgs(\"--context\", \"another\", \"--secret-dir\", overrideSecretDir.toString())));\n        assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n\n        var metadata = Files.readString(targetDir.resolve(\"_attachments\").resolve(\"_state\").resolve(\"_cliResume.json\"));\n        assertFalse(metadata.contains(\"resume-api-key\"), metadata);\n        assertTrue(metadata.contains(\"\\\"contextName\\\" : \\\"another\\\"\"), metadata);\n        assertTrue(metadata.contains(overrideSecretDir.toString()), metadata);\n\n        var afterRun = stdOut();\n        var resumeExitCode = withUserHome(homeDir, () -> executeIn(source, List.of(\"resume\")));\n\n        assertEquals(CliExitCodes.SUCCESS, resumeExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n        var resumeOutput = stdOut().substring(afterRun.length());\n        assertTrue(resumeOutput.contains(\"after resume secret: value-from-override\"), resumeOutput);\n    }\n\n    @Test\n    void passwordRetryPromptsDoNotEchoSecretsAndRunCleanupRemovesSessionFiles() throws Exception {\n        var source = preparePayload(\"passwordRetry\");\n        var targetDir = CliPaths.defaultTargetDir(source);\n\n        var exitCode = withInput(\"\\nentered-secret\\n10\\nentered-secret\\n33\\n\", () -> executeIn(source, runArgs()));\n\n        assertEquals(CliExitCodes.SUCCESS, exitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n        assertTrue(stdOut().contains(\"Fill pending form now? (Y/n)\"), stdOut());\n        assertTrue(stdOut().contains(\"after password form: 33\"), stdOut());\n        assertEquals(2, countMatches(stdOut(), \"Password \\\\[string, required, default: <hidden>\\\\]:\"), stdOut());\n        assertFalse(stdOut().contains(\"seed-secret\"), stdOut());\n        assertFalse(stdOut().contains(\"entered-secret\"), stdOut());\n        assertFalse(stdErr().contains(\"seed-secret\"), stdErr());\n        assertFalse(stdErr().contains(\"entered-secret\"), stdErr());\n        assertFalse(Files.exists(targetDir.resolve(\"_attachments\").resolve(\"_session_files\")), targetDir.toString());\n    }\n\n    @Test\n    void resumeCleanupRemovesSessionFiles() throws Exception {\n        var source = preparePayload(\"passwordSuspend\");\n        var targetDir = CliPaths.defaultTargetDir(source);\n\n        var runExitCode = withInput(\"\\nresume-secret\\n33\\n\", () -> executeIn(source, runArgs()));\n        assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n        var sessionFilesDir = targetDir.resolve(\"_attachments\").resolve(\"_session_files\");\n        Files.createDirectories(sessionFilesDir);\n        Files.writeString(sessionFilesDir.resolve(\"sensitive.json\"), \"test\");\n\n        var afterRun = stdOut();\n        var resumeExitCode = executeIn(source, List.of(\"resume\"));\n\n        assertEquals(CliExitCodes.SUCCESS, resumeExitCode, () -> \"out:\\n\" + stdOut() + \"\\n\\nerr:\\n\" + stdErr());\n        var resumeOutput = stdOut().substring(afterRun.length());\n        assertTrue(resumeOutput.contains(\"after resume: 33\"), resumeOutput);\n        assertFalse(Files.exists(targetDir.resolve(\"_attachments\").resolve(\"_state\")), targetDir.toString());\n        assertFalse(Files.exists(sessionFilesDir), targetDir.toString());\n    }\n\n    @Test\n    void cleanupRemovesStateAndSessionFiles() throws Exception {\n        var workDir = tempDir.resolve(\"cleanup\");\n        var stateDir = workDir.resolve(\"_attachments\").resolve(\"_state\");\n        var sessionFilesDir = workDir.resolve(\"_attachments\").resolve(\"_session_files\");\n        Files.createDirectories(stateDir);\n        Files.createDirectories(sessionFilesDir);\n        Files.writeString(stateDir.resolve(\"instance\"), \"state\");\n        Files.writeString(sessionFilesDir.resolve(\"sensitive.json\"), \"session\");\n\n        LocalSuspendPersistence.cleanup(workDir);\n\n        assertFalse(Files.exists(stateDir), workDir.toString());\n        assertFalse(Files.exists(sessionFilesDir), workDir.toString());\n    }\n\n    private int execute(List<String> args) {\n        var app = new App();\n        var cmd = new CommandLine(app);\n        return cmd.execute(args.toArray(new String[0]));\n    }\n\n    private int executeIn(Path userDir, List<String> args) {\n        var originalUserDir = System.getProperty(\"user.dir\");\n        System.setProperty(\"user.dir\", userDir.toString());\n        try {\n            return execute(args);\n        } finally {\n            if (originalUserDir == null) {\n                System.clearProperty(\"user.dir\");\n            } else {\n                System.setProperty(\"user.dir\", originalUserDir);\n            }\n        }\n    }\n\n    private static List<String> runArgs(String... extraArgs) {\n        var result = new java.util.ArrayList<String>();\n        result.add(\"run\");\n        result.add(\"--no-default-cfg\");\n        result.addAll(List.of(extraArgs));\n        return result;\n    }\n\n    private Path preparePayload(String payload) throws Exception {\n        var uri = ResumeTest.class.getResource(payload).toURI();\n        var source = Paths.get(uri);\n        PathUtils.copy(source, tempDir);\n        return tempDir;\n    }\n\n    private static String quoted(Path path) {\n        return Pattern.quote(path.toString());\n    }\n\n    private <T> T withUserHome(Path userHome, java.util.concurrent.Callable<T> action) throws Exception {\n        var originalUserHome = System.getProperty(\"user.home\");\n        System.setProperty(\"user.home\", userHome.toString());\n        try {\n            return action.call();\n        } finally {\n            if (originalUserHome == null) {\n                System.clearProperty(\"user.home\");\n            } else {\n                System.setProperty(\"user.home\", originalUserHome);\n            }\n        }\n    }\n\n    private static void writeCliConfig(Path homeDir, String contents) throws IOException {\n        var configDir = homeDir.resolve(\".concord\");\n        Files.createDirectories(configDir);\n        Files.writeString(configDir.resolve(\"cli.yaml\"), contents);\n    }\n\n    private static void writeSecret(Path secretDir, String orgName, String secretName, String value) throws IOException {\n        var dir = secretDir.resolve(orgName);\n        Files.createDirectories(dir);\n        Files.writeString(dir.resolve(secretName), value);\n    }\n\n    private static int countMatches(String value, String regex) {\n        var matcher = Pattern.compile(regex).matcher(value);\n        var count = 0;\n        while (matcher.find()) {\n            count++;\n        }\n        return count;\n    }\n\n    private static Set<Path> listTmpFiles(String prefix) throws IOException {\n        try (var files = Files.list(PathUtils.TMP_DIR)) {\n            return files.filter(p -> {\n                        var fileName = p.getFileName().toString();\n                        return fileName.startsWith(prefix) && fileName.endsWith(\".tmp\");\n                    })\n                    .collect(Collectors.toSet());\n        }\n    }\n}\n"
  },
  {
    "path": "cli/src/test/java/com/walmartlabs/concord/cli/RunTest.java",
    "content": "package com.walmartlabs.concord.cli;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.io.TempDir;\nimport picocli.CommandLine;\n\nimport java.net.URI;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.*;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nclass RunTest extends AbstractTest {\n\n    @TempDir\n    private Path tempDir;\n\n    @Test\n    void runTest() throws Exception {\n        Map<String, Object> extraVars = Collections.singletonMap(\"name\", \"Concord\");\n        List<String> args = new ArrayList<>();\n        for (Map.Entry<String, Object> e : extraVars.entrySet()) {\n            args.add(\"-e\");\n            args.add(e.getKey() + \"=\" + e.getValue());\n        }\n\n        int exitCode = run(\"simple\", args);\n        assertExitCode(0, exitCode);\n        assertLog(\".*Hello, Concord.*\");\n        assertEquals(0, exitCode);\n        // default dependencies should be added\n        assertLog(\".*concord-tasks-\" + Version.getVersion() + \".jar.*\");\n        assertLog(\".*http-tasks-\" + Version.getVersion() + \".jar.*\");\n        assertLog(\".*slack-tasks-\" + Version.getVersion() + \".jar.*\");\n    }\n\n    @Test\n    void testResourceTask() throws Exception {\n        int exitCode = run(\"resourceTask\", Collections.emptyList());\n        assertExitCode(0, exitCode);\n        assertLog(\".*\\\"k\\\" : \\\"v\\\".*\");\n    }\n\n    @Test\n    void testDepsFromProfile() throws Exception {\n        int exitCode = run(\"profileDeps\", Arrays.asList(\"-p\", \"test\"));\n        assertExitCode(0, exitCode);\n        assertLog(\".*exists=true.*\");\n    }\n\n    @Test\n    void testCliCheckpointService() throws Exception {\n        int exitCode = run(\"cliCheckpointService\", Collections.emptyList());\n        assertExitCode(0, exitCode);\n        assertLog(\".*Checkpoint.*ignored.*\", 2);\n    }\n\n    @Test\n    void testCustomDefaultConfig() throws Exception {\n        int exitCode = run(\"defaultCfg\", Collections.emptyList(), \"defaults.yml\");\n        assertExitCode(0, exitCode);\n        assertLog(\".*file-tasks-\" + Version.getVersion() + \".jar.*\");\n    }\n\n    @Test\n    void testCustomDefaultTaskVars() throws Exception {\n        int exitCode = run(\"defaultTaskVars\", List.of(\"--default-task-vars\", tempDir.resolve(\"defaultTaskVars.json\").toString()));\n        assertExitCode(0, exitCode);\n        assertLog(\".*Unknown action: 'customInvalidAction'. Available actions.*\");\n    }\n\n    @Test\n    void testProcessProjectInfo() throws Exception {\n        Map<String, Object> extraVars = new HashMap<>();\n        extraVars.put(\"processInfo.sessionToken\", \"test-token\");\n        extraVars.put(\"projectInfo.orgName\", \"test-org\");\n\n        List<String> args = new ArrayList<>();\n        for (Map.Entry<String, Object> e : extraVars.entrySet()) {\n            args.add(\"-e\");\n            args.add(e.getKey() + \"=\" + e.getValue());\n        }\n\n        int exitCode = run(\"processProjectInfo\", args);\n        assertExitCode(0, exitCode);\n        assertLog(\".*processInfo: \\\\{sessionToken=test-token}.*\");\n        assertLog(\".*projectInfo: \\\\{orgName=test-org}.*\");\n    }\n\n    private void assertExitCode(int expected, int current) {\n        assertEquals(expected, current, () -> \"out:\\n\" + stdOut() + \"\\n\\n\" + \"err:\\n\" + stdErr());\n    }\n\n    private int run(String payload, List<String> args) throws Exception {\n        return run(payload, args, null);\n    }\n\n    private int run(String payload, List<String> args, String defaultCfg) throws Exception {\n        URI uri = RunTest.class.getResource(payload).toURI();\n        Path source = Paths.get(uri);\n\n        PathUtils.copy(source, tempDir);\n\n        App app = new App();\n        CommandLine cmd = new CommandLine(app);\n\n        List<String> effectiveArgs = new ArrayList<>();\n        effectiveArgs.add(\"run\");\n        effectiveArgs.addAll(args);\n        effectiveArgs.add(tempDir.toString());\n\n        if (defaultCfg != null) {\n            effectiveArgs.add(\"--default-cfg\");\n            effectiveArgs.add(tempDir.resolve(defaultCfg).toString());\n        }\n\n        return cmd.execute(effectiveArgs.toArray(new String[0]));\n    }\n}\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/cliCheckpointService/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - checkpoint: my-first-checkpoint\n    - checkpoint: my-second-checkpoint\n    - log: \"Checkpoint done!\"\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/configWithDefaults.yaml",
    "content": "contexts:\n  default:\n    secrets:\n      vault:\n        id: \"foo\"\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/defaultTaskVars/concord.yml",
    "content": "flows:\n  default:\n    # execute some task that looks for default (policy-provide) variables\n    # obviously this won't successfully send a message in an IT, however\n    # the error messages are useful enough to make sure the vars work\n    - task: slack\n      in:\n        # action is given via default variables\n        text: mock-message\n      error:\n        - if: \"${not lastError.message.contains('Unknown action')}\"\n          then:\n            - throw: \"Unexpected error: ${lastError}\"\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/defaultTaskVars/defaultTaskVars.json",
    "content": "{\n  \"slack\": {\n    \"action\": \"customInvalidAction\"\n  }\n}\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/fileForm/concord.yml",
    "content": "flows:\n  default:\n    - log: \"before file form\"\n    - form: uploadForm\n    - log: \"after upload: ${uploadForm.attachment}\"\n\nforms:\n  uploadForm:\n    - attachment:\n        label: Attachment\n        type: file\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/fileRetryForm/concord.yml",
    "content": "flows:\n  default:\n    - form: uploadForm\n    - log: \"after upload retry: ${uploadForm.age}\"\n\nforms:\n  uploadForm:\n    - attachment:\n        label: Attachment\n        type: file\n    - age:\n        label: Age\n        type: int\n        min: 21\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/form/concord.yml",
    "content": "flows:\n  default:\n    - log: \"before form\"\n    - form: myForm\n    - log: \"after form: ${myForm.name}, ${myForm.age}\"\n\nforms:\n  myForm:\n    - name:\n        label: Name\n        type: string\n    - age:\n        label: Age\n        type: int\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/lintV1/concord/extra.concord.yml",
    "content": "flows:\n  extraFlow:\n    - log: \"extra\"\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/lintV1/concord.yml",
    "content": "flows:\n  default:\n    - log: \"Hello, world!\"\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/lintV2/concord/extra.concord.yml",
    "content": "flows:\n  extraFlow:\n    - log: \"extra\"\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/lintV2/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n\nflows:\n  default:\n    - log: \"Hello, world!\"\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/mixedFormEvent/concord.yml",
    "content": "flows:\n  default:\n    - log: \"before mixed suspend\"\n    - parallel:\n        - block:\n            - form: approvalForm\n        - block:\n            - suspend: ev_timeout\n      out:\n        - approvalForm\n    - log: \"after mixed suspend: ${approvalForm.decision}\"\n\nforms:\n  approvalForm:\n    - decision:\n        label: Decision\n        type: string\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/multiContextConfig.yaml",
    "content": "contexts:\n  default:\n    secrets:\n      vault:\n        id: \"foo\"\n        dir: \"qux\"\n  another:\n    secrets:\n      vault:\n        id: \"bar\"\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/parallelForms/concord.yml",
    "content": "flows:\n  default:\n    - log: \"parallel forms: before\"\n    - parallel:\n        - block:\n            - form: form1\n            - log: \"parallel forms: form1=${form1.value}\"\n        - block:\n            - form: form2\n            - log: \"parallel forms: form2=${form2.value}\"\n      out:\n        - form1\n        - form2\n    - log: \"parallel forms: done one=${form1.value} two=${form2.value}\"\n\nforms:\n  form1:\n    - value: { label: \"Form One\", type: \"string\" }\n\n  form2:\n    - value: { label: \"Form Two\", type: \"string\" }\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/passwordRetry/concord.yml",
    "content": "flows:\n  default:\n    - form: secureForm\n    - log: \"after password form: ${secureForm.age}\"\n\nforms:\n  secureForm:\n    - password: { label: \"Password\", type: \"string\", inputType: \"password\", value: \"seed-secret\" }\n    - age: { label: \"Age\", type: \"int\", min: 21 }\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/passwordSuspend/concord.yml",
    "content": "flows:\n  default:\n    - form: secureForm\n    - suspend: ev1\n    - log: \"after resume: ${secureForm.age}\"\n\nforms:\n  secureForm:\n    - password: { label: \"Password\", type: \"string\", inputType: \"password\" }\n    - age: { label: \"Age\", type: \"int\", min: 21 }\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/processProjectInfo/concord.yml",
    "content": "configuration:\n  debug: true\n\nflows:\n  default:\n    - log: \"processInfo: ${processInfo}\"\n    - log: \"projectInfo: ${projectInfo}\"\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/profileDeps/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nprofiles:\n  test:\n    configuration:\n      dependencies:\n        - \"mvn://com.walmartlabs.concord.plugins.basic:file-tasks:1.96.0\"\n\nflows:\n  default:\n    - log: \"exists=${files.exists('concord.yml')}\"\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/resourceTask/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  arguments:\n    msg:\n      k: v\n\nflows:\n  default:\n    - log: ${resource.asString(resource.writeAsJson(msg))}\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/secretResume/concord.yml",
    "content": "flows:\n  default:\n    - suspend: ev1\n    - log: \"after resume secret: ${crypto.exportAsString('Default', 'resumeSecret', null)}\"\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/simple/concord.yml",
    "content": "configuration:\n  debug: true\n\nflows:\n  default:\n    - log: \"Hello, ${name}\"\n    - expr: \"${sleep.ms(10)}\""
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/suspend/concord.yml",
    "content": "flows:\n  default:\n    - log: \"before suspend\"\n    - suspend: ev1\n    - log: \"after resume: ${myForm.value}\"\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/testConfig.yaml",
    "content": "contexts:\n  default:\n    secrets:\n      vault:\n        dir: \"/my-vault\"\n        id: \"foo\"\n      local:\n        dir: \"/my-secrets\"\n      remote:\n        enabled: true\n        baseUrl: \"http://localhost:8001\"\n        apiKey: \"foobar\"\n"
  },
  {
    "path": "cli/src/test/resources/com/walmartlabs/concord/cli/validatedForm/concord.yml",
    "content": "flows:\n  default:\n    - form: validatedForm\n    - log: \"validated form: name=${validatedForm.name}, age=${validatedForm.age}, choice=${validatedForm.choice}, note=${validatedForm.note}, readOnly=${validatedForm.readOnlyValue}\"\n\nforms:\n  validatedForm:\n    - name:\n        label: Name\n        type: string\n    - age:\n        label: Age\n        type: int\n        min: 21\n        max: 65\n    - choice:\n        label: Choice\n        type: string\n        allow:\n          - \"red\"\n          - \"blue\"\n    - note:\n        label: Note\n        type: string\n        value: default-note\n    - readOnlyValue:\n        label: Read Only\n        type: string\n        value: locked\n        readOnly: true\n"
  },
  {
    "path": "cli/src/test/resources/logback-test.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <!-- we should not use Jansi in tests, tests override System.out and System.err to capture output of CLI commands -->\n        <withJansi>false</withJansi>\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <logger name=\"com.walmartlabs.concord.dependencymanager\" level=\"WARN\"/>\n    <logger name=\"com.walmartlabs.concord.repository\" level=\"WARN\"/>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "client2/README.md",
    "content": "# concord-client\n\nA Concord API client based on Swagger codegen.\nBuild `server/impl` first to generate Swagger spec.\n"
  },
  {
    "path": "client2/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-client2</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n        </dependency>\n\n        <!-- JSON processing: jackson -->\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jsr310</artifactId>\n        </dependency>\n\n        <!-- Immutables -->\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.wiremock</groupId>\n            <artifactId>wiremock-jetty12</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <resources>\n            <resource>\n                <filtering>false</filtering>\n                <directory>${project.basedir}/src/main/resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n            <resource>\n                <filtering>true</filtering>\n                <directory>${project.basedir}/src/main/filtered-resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n        </resources>\n\n        <plugins>\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>cobertura-maven-plugin</artifactId>\n            </plugin>\n            <plugin>\n                <groupId>org.openapitools</groupId>\n                <artifactId>openapi-generator-maven-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>generate</goal>\n                        </goals>\n                        <configuration>\n                            <inputSpec>${project.basedir}/../server/impl/target/classes/com/walmartlabs/concord/server/swagger/swagger.yaml</inputSpec>\n                            <generatorName>java</generatorName>\n                            <apiPackage>com.walmartlabs.concord.client2</apiPackage>\n                            <modelPackage>com.walmartlabs.concord.client2</modelPackage>\n                            <packageName>com.walmartlabs.concord.client2</packageName>\n                            <invokerPackage>com.walmartlabs.concord.client2</invokerPackage>\n                            <configOptions>\n                                <sourceFolder>src/gen/java/main</sourceFolder>\n                                <dateLibrary>java8</dateLibrary>\n                                <serializableModel>true</serializableModel>\n                                <openApiNullable>false</openApiNullable>\n                                <supportUrlQuery>false</supportUrlQuery>\n                            </configOptions>\n                            <skipValidateSpec>false</skipValidateSpec>\n                            <library>native</library>\n                            <generateApiTests>false</generateApiTests>\n                            <generateModelTests>false</generateModelTests>\n                            <generateApiDocumentation>false</generateApiDocumentation>\n                            <generateModelDocumentation>false</generateModelDocumentation>\n                            <generateSupportingFiles>true</generateSupportingFiles>\n                            <supportingFilesToGenerate>ApiClient.java,ApiResponse.java,ApiException.java,Pair.java</supportingFilesToGenerate>\n                            <templateDirectory>${project.basedir}/src/main/template</templateDirectory>\n                            <cleanupOutput>true</cleanupOutput>\n                            <typeMappings>string+binary=InputStream</typeMappings>\n                            <importMappings>InputStream=java.io.InputStream</importMappings>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.revapi</groupId>\n                <artifactId>revapi-maven-plugin</artifactId>\n                <configuration>\n                    <!-- lots of incompatible changes due to breaking changes in dependencies -->\n                    <!-- re-enable after release -->\n                    <skip>true</skip>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/ApiClientConfiguration.java",
    "content": "package com.walmartlabs.concord.client2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface ApiClientConfiguration {\n\n    /**\n     * Base URL of the API, e.g. {@code http://localhost:8001}\n     */\n    @Nullable\n    String baseUrl();\n\n    /**\n     * The process' session token.\n     */\n    @Nullable\n    String sessionToken();\n\n    /**\n     * The user's API key. If set then the session token will be ignored.\n     */\n    @Nullable\n    String apiKey();\n\n    static ImmutableApiClientConfiguration.Builder builder() {\n        return ImmutableApiClientConfiguration.builder();\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/ApiClientFactory.java",
    "content": "package com.walmartlabs.concord.client2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface ApiClientFactory {\n\n    ApiClient create(ApiClientConfiguration cfg);\n}\n\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/ClientUtils.java",
    "content": "package com.walmartlabs.concord.client2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.Callable;\n\npublic final class ClientUtils {\n\n    private static final Logger log = LoggerFactory.getLogger(ClientUtils.class);\n\n    public static <T> T withRetry(int retryCount, long retryInterval, Callable<T> c) throws ApiException {\n        Exception exception = null;\n        int tryCount = 0;\n        while (!Thread.currentThread().isInterrupted() && tryCount < retryCount + 1) {\n            try {\n                return c.call();\n            } catch (ApiException e) {\n                exception = e;\n\n                if (e.getCode() >= 400 && e.getCode() < 500) {\n                    break;\n                }\n                log.warn(\"call error: '{}'\", getErrorMessage(e));\n            } catch (Exception e) {\n                exception = e;\n                log.error(\"call error\", e);\n            }\n            log.info(\"retry after {} sec\", retryInterval / 1000);\n            sleep(retryInterval);\n            tryCount++;\n        }\n\n        if (exception instanceof ApiException) {\n            throw (ApiException) exception;\n        }\n\n        throw new ApiException(exception);\n    }\n\n    /**\n     * Returns a value of the specified header.\n     * Only the first value is returned.\n     * The header's {@code name} is case-insensitive.\n     */\n    public static String getHeader(String name, ApiResponse<?> resp) {\n        Map<String, List<String>> headers = resp.getHeaders();\n        if (headers == null) {\n            return null;\n        }\n\n        for (Map.Entry<String, List<String>> e : headers.entrySet()) {\n            if (!e.getKey().equalsIgnoreCase(name)) {\n                continue;\n            }\n\n            List<String> values = e.getValue();\n\n            if (values == null || values.isEmpty()) {\n                return null;\n            }\n\n            return values.get(0);\n        }\n\n        return null;\n    }\n\n    private static void sleep(long t) {\n        try {\n            Thread.sleep(t);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    private static String getErrorMessage(ApiException e) {\n        String error = e.getMessage();\n        if (e.getResponseBody() != null && !e.getResponseBody().isEmpty()) {\n            error += \": \" + e.getResponseBody();\n        }\n        return error;\n    }\n\n    private ClientUtils() {\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/ConcordApiClient.java",
    "content": "package com.walmartlabs.concord.client2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.net.http.HttpClient;\n\npublic class ConcordApiClient extends ApiClient {\n\n    public ConcordApiClient(HttpClient httpClient) {\n        super(httpClient);\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/CreateSecretRequest.java",
    "content": "package com.walmartlabs.concord.client2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport com.walmartlabs.concord.client2.ImmutableCreateSecretRequest;\nimport com.walmartlabs.concord.client2.ImmutableKeyPair;\nimport com.walmartlabs.concord.client2.ImmutableUsernamePassword;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.UUID;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface CreateSecretRequest {\n\n    String org();\n\n    String name();\n\n    @Value.Default\n    default boolean generatePassword() {\n        return false;\n    }\n\n    @Nullable\n    String storePassword();\n\n    @Nullable\n    SecretEntryV2.VisibilityEnum visibility();\n\n    @Nullable\n    List<String> projectNames();\n\n    @Nullable\n    List<UUID> projectIds();\n\n    @Nullable\n    byte[] data();\n\n    @Nullable\n    KeyPair keyPair();\n\n    @Nullable\n    UsernamePassword usernamePassword();\n\n    static ImmutableCreateSecretRequest.Builder builder() {\n        return ImmutableCreateSecretRequest.builder();\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface KeyPair {\n\n        long serialVersionUID = 1L;\n\n        Path privateKey();\n\n        Path publicKey();\n\n        static ImmutableKeyPair.Builder builder() {\n            return ImmutableKeyPair.builder();\n        }\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface UsernamePassword {\n\n        long serialVersionUID = 1L;\n\n        String username();\n\n        String password();\n\n        static UsernamePassword of(String username, String password) {\n            return ImmutableUsernamePassword.builder()\n                    .username(username)\n                    .password(password)\n                    .build();\n        }\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/DefaultApiClientFactory.java",
    "content": "package com.walmartlabs.concord.client2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.TrustManager;\nimport javax.net.ssl.X509TrustManager;\nimport java.net.http.HttpClient;\nimport java.security.SecureRandom;\nimport java.security.cert.X509Certificate;\nimport java.time.Duration;\n\npublic class DefaultApiClientFactory {\n\n    private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(30);\n\n    private final HttpClient httpClient;\n    private final String defaultApiUrl;\n\n    public DefaultApiClientFactory(String defaultApiUrl) {\n        this(defaultApiUrl, null, true);\n    }\n\n    public DefaultApiClientFactory(String defaultApiUrl, Duration connectTimeout) {\n        this(defaultApiUrl, connectTimeout, true);\n    }\n\n    public DefaultApiClientFactory(String defaultApiUrl, Duration connectTimeout, boolean verifySsl) {\n        this.defaultApiUrl = defaultApiUrl;\n\n        this.httpClient = HttpClient.newBuilder()\n                .version(HttpClient.Version.HTTP_1_1)\n                .connectTimeout(connectTimeout != null ? connectTimeout : DEFAULT_CONNECT_TIMEOUT)\n                .sslContext(sslContext(verifySsl))\n                .build();\n    }\n\n    public ApiClient create() {\n        return new ConcordApiClient(httpClient)\n                .setBaseUrl(defaultApiUrl);\n    }\n\n    public ApiClient create(ApiClientConfiguration overrides) {\n        String baseUrl = overrides.baseUrl() != null ? overrides.baseUrl() : defaultApiUrl;\n\n        String sessionToken = null;\n        if (overrides.apiKey() == null) {\n            sessionToken = overrides.sessionToken();\n        }\n\n        String apiKey = overrides.apiKey();\n        if (apiKey != null) {\n            sessionToken = null;\n        }\n\n        if (sessionToken == null && apiKey == null) {\n            throw new IllegalArgumentException(\"Session token or an API key is required\");\n        }\n\n        return new ConcordApiClient(httpClient)\n                .setBaseUrl(baseUrl)\n                .setSessionToken(sessionToken)\n                .setApiKey(apiKey)\n                .addDefaultHeader(\"Accept\", \"*/*\");\n    }\n\n    private static SSLContext sslContext(boolean verifySsl) {\n        try {\n            SSLContext sslContext = SSLContext.getInstance(\"TLS\");\n            if (verifySsl) {\n                sslContext.init(null, null, null);\n            } else {\n                TrustManager trustAll = new X509TrustManager() {\n                    @Override\n                    public void checkClientTrusted(X509Certificate[] chain, String authType) {\n                    }\n\n                    @Override\n                    public void checkServerTrusted(X509Certificate[] chain, String authType) {\n                    }\n\n                    @Override\n                    public X509Certificate[] getAcceptedIssuers() {\n                        return null;\n                    }\n                };\n                TrustManager[] trustManagers = new TrustManager[]{trustAll};\n                System.getProperties().setProperty(\"jdk.internal.httpclient.disableHostnameVerification\", Boolean.TRUE.toString());\n\n                sslContext.init(null, trustManagers, new SecureRandom());\n            }\n            return sslContext;\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/ProcessDataInclude.java",
    "content": "package com.walmartlabs.concord.client2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic enum ProcessDataInclude {\n\n    CHECKPOINTS (\"checkpoints\"),\n    CHECKPOINTS_HISTORY (\"checkpointsHistory\"),\n    CHILDREN_IDS (\"childrenIds\"),\n    STATUS_HISTORY (\"history\");\n\n    private final String value;\n\n    ProcessDataInclude(String value) {\n        this.value = value;\n    }\n\n    public String getValue() {\n        return value;\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/ProcessListFilter.java",
    "content": "package com.walmartlabs.concord.client2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ImmutableProcessListFilter;\nimport com.walmartlabs.concord.client2.ProcessDataInclude;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.time.OffsetDateTime;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.UUID;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface ProcessListFilter {\n\n    @Nullable\n    UUID orgId();\n\n    @Nullable\n    String orgName();\n\n    @Nullable\n    UUID projectId();\n\n    @Nullable\n    String projectName();\n\n    @Nullable\n    UUID repoId();\n\n    @Nullable\n    String repoName();\n\n    @Nullable\n    OffsetDateTimeParam afterCreatedAt();\n\n    @Nullable\n    OffsetDateTimeParam beforeCreatedAt();\n\n    @Nullable\n    Set<String> tags();\n\n    @Nullable\n    String status();\n\n    @Nullable\n    String initiator();\n\n    @Nullable\n    UUID parentInstanceId();\n\n    @Nullable\n    Set<String> include();\n\n    @Nullable\n    Integer limit();\n\n    @Nullable\n    Integer offset();\n\n    @Nullable\n    Map<String, String> meta();\n\n    class Builder extends ImmutableProcessListFilter.Builder {\n\n        public Builder status(ProcessEntry.StatusEnum status) {\n            return status(status.getValue());\n        }\n\n        public Builder addInclude(ProcessDataInclude... elements) {\n            for (ProcessDataInclude e : elements) {\n                addInclude(e.getValue());\n            }\n            return this;\n        }\n\n        public Builder afterCreatedAt(OffsetDateTime afterCreatedAt) {\n            if (afterCreatedAt == null) {\n                return this;\n            }\n\n            afterCreatedAt(new OffsetDateTimeParam().value(afterCreatedAt));\n            return this;\n        }\n    }\n\n    static ImmutableProcessListFilter.Builder builder() {\n        return new Builder();\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/ProcessUtils.java",
    "content": "package com.walmartlabs.concord.client2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessEntry.StatusEnum;\n\npublic class ProcessUtils {\n\n    public static boolean isFinal(StatusEnum s) {\n        return s == StatusEnum.FINISHED || s == StatusEnum.FAILED || s == StatusEnum.CANCELLED || s == StatusEnum.SUSPENDED || s == StatusEnum.TIMED_OUT;\n    }\n\n    private ProcessUtils() {\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/SecretClient.java",
    "content": "package com.walmartlabs.concord.client2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.secret.BinaryDataSecret;\nimport com.walmartlabs.concord.common.secret.KeyPair;\nimport com.walmartlabs.concord.common.secret.UsernamePassword;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.Secret;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic class SecretClient {\n\n    private static final int DEFAULT_RETRY_COUNT = 3;\n    private static final long DEFAULT_RETRY_INTERVAL = 5000;\n\n    private final ApiClient apiClient;\n    private final int retryCount;\n    private final long retryInterval;\n\n    public SecretClient(ApiClient apiClient) {\n        this(apiClient, DEFAULT_RETRY_COUNT, DEFAULT_RETRY_INTERVAL);\n    }\n\n    public SecretClient(ApiClient apiClient, int retryCount, long retryInterval) {\n        this.apiClient = apiClient;\n        this.retryCount = retryCount;\n        this.retryInterval = retryInterval;\n    }\n\n    /**\n     * Fetches a decrypted Concord secret from the server.\n     */\n    public <T extends Secret> T getData(String orgName, String secretName, String password, SecretEntryV2.TypeEnum expectedType) throws Exception {\n        SecretsApi.GetSecretDataRequest req = new SecretsApi.GetSecretDataRequest()\n                .storePassword(password);\n\n        SecretsApi api = new SecretsApi(apiClient);\n\n        ApiResponse<InputStream> r = null;\n        try {\n            r = ClientUtils.withRetry(retryCount, retryInterval,\n                    () -> api.getSecretDataWithHttpInfo(orgName, secretName, req.asMap()));\n\n            if (r.getData() == null) {\n                throw new SecretNotFoundException(orgName, secretName);\n            }\n\n            String secretType = ClientUtils.getHeader(Constants.Headers.SECRET_TYPE, r);\n            if (secretType == null) {\n                throw new IllegalStateException(\"Can't determine the secret's expectedType. Server response: code=\" + r.getStatusCode());\n            }\n\n            SecretEntryV2.TypeEnum actualSecretType = SecretEntryV2.TypeEnum.valueOf(secretType);\n\n            if (expectedType != null && expectedType != actualSecretType) {\n                String msg = \"Unexpected type of %s/%s. Expected %s, got %s. \" +\n                        \"Check the secret's expectedType and its usage - some secrets can only be used for specific purposes \" +\n                        \"(e.g. %s is typically used for key-based authentication).\";\n                throw new IllegalArgumentException(String.format(msg, orgName, secretName, expectedType, actualSecretType, SecretEntryV2.TypeEnum.KEY_PAIR));\n            }\n\n            try (InputStream is = r.getData()) {\n                return readSecret(actualSecretType, is.readAllBytes());\n            }\n        } catch (ApiException e) {\n            if (e.getCode() == 404) {\n                throw new SecretNotFoundException(orgName, secretName);\n            }\n            throw e;\n        } finally {\n            if (r != null && r.getData() != null) {\n                r.getData().close();\n            }\n        }\n    }\n\n    /**\n     * Decrypt the provided string using the project's key.\n     */\n    public byte[] decryptString(UUID instanceId, byte[] input) throws Exception {\n        ProcessApi api = new ProcessApi(apiClient);\n        return ClientUtils.withRetry(retryCount, retryInterval, () -> api.decryptString(instanceId, input));\n    }\n\n    /**\n     * Encrypts the provided string using the project's key.\n     */\n    public String encryptString(String orgName, String projectName, String input) throws Exception {\n        ProjectsApi api = new ProjectsApi(apiClient);\n        EncryptValueResponse r = ClientUtils.withRetry(retryCount, retryInterval,\n                () -> api.encrypt(orgName, projectName, input));\n\n        return r.getData();\n    }\n\n    /**\n     * Creates a new Concord secret.\n     */\n    public SecretOperationResponse createSecret(CreateSecretRequest secretRequest) throws ApiException {\n        String path = \"/api/v1/org/\" + secretRequest.org() + \"/secret\";\n\n        Map<String, Object> params = new HashMap<>();\n        params.put(Constants.Multipart.NAME, secretRequest.name());\n        params.put(Constants.Multipart.GENERATE_PASSWORD, secretRequest.generatePassword());\n        if (secretRequest.storePassword() != null) {\n            params.put(Constants.Multipart.STORE_PASSWORD, secretRequest.storePassword());\n        }\n\n        SecretEntryV2.VisibilityEnum visibility = secretRequest.visibility();\n        if (visibility != null) {\n            params.put(Constants.Multipart.VISIBILITY, visibility.getValue());\n        }\n\n        if (secretRequest.projectIds() != null) {\n            params.put(Constants.Multipart.PROJECT_IDS, secretRequest.projectIds().stream().map(UUID::toString).collect(Collectors.joining(\",\")));\n        } else if (secretRequest.projectNames() != null) {\n            params.put(Constants.Multipart.PROJECT_NAMES, String.join(\",\", secretRequest.projectNames()));\n        }\n\n        byte[] data = secretRequest.data();\n        CreateSecretRequest.KeyPair keyPair = secretRequest.keyPair();\n        CreateSecretRequest.UsernamePassword usernamePassword = secretRequest.usernamePassword();\n\n        if (data != null) {\n            params.put(Constants.Multipart.TYPE, SecretEntryV2.TypeEnum.DATA.getValue());\n            params.put(Constants.Multipart.DATA, data);\n        } else if (keyPair != null) {\n            params.put(Constants.Multipart.TYPE, SecretEntryV2.TypeEnum.KEY_PAIR.getValue());\n            params.put(Constants.Multipart.PUBLIC, readFile(keyPair.publicKey()));\n            params.put(Constants.Multipart.PRIVATE, readFile(keyPair.privateKey()));\n        } else if (usernamePassword != null) {\n            params.put(Constants.Multipart.TYPE, SecretEntryV2.TypeEnum.USERNAME_PASSWORD.getValue());\n            params.put(Constants.Multipart.USERNAME, usernamePassword.username());\n            params.put(Constants.Multipart.PASSWORD, usernamePassword.password());\n        } else {\n            throw new IllegalArgumentException(\"Secret data, a key pair or username/password must be specified.\");\n        }\n\n        SecretsApi api = new SecretsApi(apiClient);\n\n        SecretOperationResponse response = ClientUtils.withRetry(retryCount, retryInterval,\n                () -> api.createSecret(secretRequest.org(), params));\n        return response;\n    }\n\n    public void updateSecret(String orgName, String secretName, UpdateSecretRequest request) throws ApiException {\n        String path = \"/api/v2/org/\" + orgName + \"/secret/\" + secretName;\n\n        Map<String, Object> params = new HashMap<>();\n        params.put(Constants.Multipart.ORG_ID, request.newOrgId());\n        params.put(Constants.Multipart.ORG_NAME, request.newOrgName());\n        params.put(\"removeProjectLink\", request.removeProjectLink());\n        params.put(\"ownerId\", request.newOwnerId());\n        params.put(Constants.Multipart.STORE_PASSWORD, request.currentPassword());\n        params.put(\"newStorePassword\", request.newPassword());\n        params.put(Constants.Multipart.NAME, request.newName());\n        params.put(Constants.Multipart.VISIBILITY, request.newVisibility());\n        if (request.newProjectIds() != null) {\n            params.put(Constants.Multipart.PROJECT_IDS, request.newProjectIds().stream().map(UUID::toString).collect(Collectors.joining(\",\")));\n        } else if (request.newProjectNames() != null) {\n            params.put(Constants.Multipart.PROJECT_NAMES, String.join(\",\", request.newProjectNames()));\n        }\n\n        byte[] data = request.data();\n        CreateSecretRequest.KeyPair keyPair = request.keyPair();\n        CreateSecretRequest.UsernamePassword usernamePassword = request.usernamePassword();\n\n        if (data != null) {\n            params.put(Constants.Multipart.TYPE, SecretEntryV2.TypeEnum.DATA.getValue());\n            params.put(Constants.Multipart.DATA, data);\n        } else if (keyPair != null) {\n            params.put(Constants.Multipart.TYPE, SecretEntryV2.TypeEnum.KEY_PAIR.getValue());\n            params.put(Constants.Multipart.PUBLIC, readFile(keyPair.publicKey()));\n            params.put(Constants.Multipart.PRIVATE, readFile(keyPair.privateKey()));\n        } else if (usernamePassword != null) {\n            params.put(Constants.Multipart.TYPE, SecretEntryV2.TypeEnum.USERNAME_PASSWORD.getValue());\n            params.put(Constants.Multipart.USERNAME, usernamePassword.username());\n            params.put(Constants.Multipart.PASSWORD, usernamePassword.password());\n        }\n\n        params.values().removeIf(Objects::isNull);\n\n        SecretsV2Api api = new SecretsV2Api(apiClient);\n\n        ClientUtils.withRetry(retryCount, retryInterval,\n                () -> api.updateSecret(orgName, secretName, params));\n    }\n\n    private static byte[] readFile(Path file) {\n        if (file == null) {\n            return null;\n        }\n\n        if (Files.notExists(file)) {\n            throw new IllegalArgumentException(\"File '\" + file + \"' not found\");\n        }\n\n        try {\n            return Files.readAllBytes(file);\n        } catch (IOException e) {\n            throw new RuntimeException(\"Error while reading \" + file + \": \" + e.getMessage());\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static <T> T readSecret(SecretEntryV2.TypeEnum type, byte[] bytes) {\n        switch (type) {\n            case DATA:\n                return (T) new BinaryDataSecret(bytes);\n            case KEY_PAIR:\n                return (T) KeyPair.deserialize(bytes);\n            case USERNAME_PASSWORD:\n                return (T) UsernamePassword.deserialize(bytes);\n            default:\n                throw new IllegalArgumentException(\"unknown secret type: \" + type);\n        }\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/SecretNotFoundException.java",
    "content": "package com.walmartlabs.concord.client2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class SecretNotFoundException extends IllegalArgumentException {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String orgName;\n\n    private final String secretName;\n\n    public SecretNotFoundException(String orgName, String secretName) {\n        super(\"Secret not found: \" + orgName + \"/\" + secretName);\n        this.orgName = orgName;\n        this.secretName = secretName;\n    }\n\n    public String getOrgName() {\n        return orgName;\n    }\n\n    public String getSecretName() {\n        return secretName;\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/UpdateSecretRequest.java",
    "content": "package com.walmartlabs.concord.client2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.util.List;\nimport java.util.UUID;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface UpdateSecretRequest {\n\n    @Nullable\n    UUID newOrgId();\n\n    @Nullable\n    String newOrgName();\n\n    @Nullable\n    List<String> newProjectNames();\n\n    @Nullable\n    List<UUID> newProjectIds();\n\n    @Value.Default\n    default boolean removeProjectLink() {\n        return false;\n    }\n\n    @Nullable\n    UUID newOwnerId();\n\n    @Nullable\n    String currentPassword();\n\n    @Nullable\n    String newPassword();\n\n    @Nullable\n    String newName();\n\n    @Nullable\n    SecretEntryV2.VisibilityEnum newVisibility();\n\n    @Nullable\n    byte[] data();\n\n    @Nullable\n    CreateSecretRequest.KeyPair keyPair();\n\n    @Nullable\n    CreateSecretRequest.UsernamePassword usernamePassword();\n\n    static ImmutableUpdateSecretRequest.Builder builder() {\n        return ImmutableUpdateSecretRequest.builder();\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/impl/ByteArrayBuffer.java",
    "content": "package com.walmartlabs.concord.client2.impl;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class ByteArrayBuffer {\n\n    private byte[] array;\n    private int len;\n\n    public ByteArrayBuffer(int capacity) {\n        super();\n        this.array = new byte[capacity];\n    }\n\n    public void append(byte[] b) {\n        append(b, 0, b.length);\n    }\n\n    public void append(byte[] b, int off, int len) {\n        if (b == null) {\n            return;\n        }\n        if ((off < 0) || (off > b.length) || (len < 0) ||\n                ((off + len) < 0) || ((off + len) > b.length)) {\n            throw new IndexOutOfBoundsException(\"off: \"+off+\" len: \"+len+\" b.length: \"+b.length);\n        }\n        if (len == 0) {\n            return;\n        }\n        int newlen = this.len + len;\n        if (newlen > this.array.length) {\n            expand(newlen);\n        }\n        System.arraycopy(b, off, this.array, this.len, len);\n        this.len = newlen;\n    }\n\n    private void expand(int newlen) {\n        byte[] newArray = new byte[Math.max(this.array.length << 1, newlen)];\n        System.arraycopy(this.array, 0, newArray, 0, this.len);\n        this.array = newArray;\n    }\n\n    public byte[] array() {\n        return this.array;\n    }\n\n    public byte[] toByteArray() {\n        final byte[] b = new byte[this.len];\n        if (this.len > 0) {\n            System.arraycopy(this.array, 0, b, 0, this.len);\n        }\n        return b;\n    }\n\n    public int length() {\n        return this.len;\n    }\n\n    public void clear() {\n        this.len = 0;\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/impl/ContentType.java",
    "content": "package com.walmartlabs.concord.client2.impl;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\npublic class ContentType {\n\n    public static final ContentType APPLICATION_JSON = create(\n            \"application/json\", StandardCharsets.UTF_8);\n\n    public static final ContentType APPLICATION_OCTET_STREAM = create(\n            \"application/octet-stream\");\n\n    public static final ContentType TEXT_PLAIN = create(\"text/plain\");\n\n    public static final ContentType MULTIPART_FORM = create(\"multipart/form-data\");\n\n    public static ContentType create(String mimeType) {\n        return create(mimeType, null);\n    }\n\n    public static ContentType create(String mimeType, Charset charset) {\n        String normalizedMimeType = mimeType.toLowerCase();\n        return new ContentType(normalizedMimeType, charset);\n    }\n\n    private final String mimeType;\n    private final Charset charset;\n    private final List<NameValuePair> params;\n\n    public ContentType(String mimeType, Charset charset) {\n        this(mimeType, charset, null);\n    }\n\n    public ContentType(String mimeType, Charset charset, List<NameValuePair> params) {\n        this.mimeType = mimeType;\n        this.charset = charset;\n        this.params = params;\n    }\n\n    public ContentType withCharset(Charset charset) {\n        return create(getMimeType(), charset);\n    }\n\n    public ContentType withParameters(List<NameValuePair> params) {\n        return new ContentType(getMimeType(), charset, params);\n    }\n\n    public String getMimeType() {\n        return mimeType;\n    }\n\n    public Charset getCharset() {\n        return charset;\n    }\n\n    @Override\n    public String toString() {\n        StringBuilder buf = new StringBuilder();\n        buf.append(this.mimeType);\n        if (this.params != null) {\n            buf.append(\"; \");\n            formatParameters(buf, this.params);\n        } else if (this.charset != null) {\n            buf.append(\"; charset=\");\n            buf.append(this.charset.name().toLowerCase());\n        }\n        return buf.toString();\n    }\n\n    private static void formatParameters(StringBuilder buf, List<NameValuePair> params) {\n        String s = params.stream()\n                .map(nvp -> nvp.getName() + \"=\" + nvp.getValue())\n                .collect(Collectors.joining(\"; \"));\n\n        buf.append(s);\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/impl/Headers.java",
    "content": "package com.walmartlabs.concord.client2.impl;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Collections;\nimport java.util.List;\n\npublic class Headers {\n\n    private final List<NameValuePair> items;\n\n    public static Headers of(String name, String value) {\n        return new Headers(Collections.singletonList(new NameValuePair(name, value)));\n    }\n\n    public Headers(List<NameValuePair> items) {\n        this.items = items;\n    }\n\n    public String get(String name) {\n        return items.stream()\n                .filter(nvp -> nvp.getName().equalsIgnoreCase(name))\n                .map(NameValuePair::getValue)\n                .findFirst()\n                .orElse(null);\n    }\n\n    public int size() {\n        return items.size();\n    }\n\n    public String name(int index) {\n        return items.get(index).getName();\n    }\n\n    public String value(int index) {\n        return items.get(index).getValue();\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/impl/HttpEntity.java",
    "content": "package com.walmartlabs.concord.client2.impl;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.io.InputStream;\n\npublic interface HttpEntity {\n\n    ContentType contentType();\n\n    long contentLength() throws IOException;\n\n    InputStream getContent() throws IOException;\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/impl/MultipartBuilder.java",
    "content": "package com.walmartlabs.concord.client2.impl;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.SequenceInputStream;\nimport java.nio.ByteBuffer;\nimport java.nio.CharBuffer;\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\nimport java.util.*;\n\npublic class MultipartBuilder {\n\n    private static final byte[] COLONSPACE = {':', ' '};\n    private static final byte[] CRLF = {'\\r', '\\n'};\n    private static final byte[] DASHDASH = {'-', '-'};\n\n    private final List<Headers> partHeaders = new ArrayList<>();\n    private final List<RequestBody> partBodies = new ArrayList<>();\n\n    private final ContentType type = ContentType.MULTIPART_FORM;\n    private final String boundary;\n\n    public MultipartBuilder() {\n        this(UUID.randomUUID().toString());\n    }\n\n    public MultipartBuilder(String boundary) {\n        this.boundary = boundary;\n    }\n\n    public MultipartBuilder addFormDataPart(String name, String value) {\n        return addFormDataPart(name, null, RequestBody.create(null, value));\n    }\n\n    public MultipartBuilder addFormDataPart(String name, String filename, RequestBody value) {\n        Objects.requireNonNull(name, \"name\");\n        StringBuilder disposition = new StringBuilder(\"form-data; name=\");\n        appendQuotedString(disposition, name);\n\n        if (filename != null) {\n            disposition.append(\"; filename=\");\n            appendQuotedString(disposition, filename);\n        }\n\n        return addPart(Headers.of(\"Content-Disposition\", disposition.toString()), value);\n    }\n\n    public MultipartBuilder addPart(Headers headers, RequestBody body) {\n        if (body == null) {\n            throw new NullPointerException(\"body == null\");\n        }\n        if (headers != null && headers.get(\"Content-Type\") != null) {\n            throw new IllegalArgumentException(\"Unexpected header: Content-Type\");\n        }\n        if (headers != null && headers.get(\"Content-Length\") != null) {\n            throw new IllegalArgumentException(\"Unexpected header: Content-Length\");\n        }\n\n        partHeaders.add(headers);\n        partBodies.add(body);\n        return this;\n    }\n\n    public RequestBody build() {\n        return new MultipartRequestBody(type, boundary, partHeaders, partBodies);\n    }\n\n    private static void appendQuotedString(StringBuilder target, String key) {\n        target.append('\"');\n        for (int i = 0, len = key.length(); i < len; i++) {\n            char ch = key.charAt(i);\n            switch (ch) {\n                case '\\n':\n                    target.append(\"%0A\");\n                    break;\n                case '\\r':\n                    target.append(\"%0D\");\n                    break;\n                case '\"':\n                    target.append(\"%22\");\n                    break;\n                default:\n                    target.append(ch);\n                    break;\n            }\n        }\n        target.append('\"');\n    }\n\n    private static final class MultipartRequestBody extends RequestBody {\n        private final String boundary;\n        private final ContentType contentType;\n        private final List<Headers> partHeaders;\n        private final List<RequestBody> partBodies;\n\n        public MultipartRequestBody(ContentType type, String boundary, List<Headers> partHeaders,\n                                    List<RequestBody> partBodies) {\n\n            Objects.requireNonNull(type, \"type\");\n\n            this.boundary = boundary;\n            this.contentType = type.withParameters(Collections.singletonList(new NameValuePair(\"boundary\", boundary)));\n            this.partHeaders = partHeaders;\n            this.partBodies = partBodies;\n        }\n\n        @Override\n        public ContentType contentType() {\n            return contentType;\n        }\n\n        @Override\n        public long contentLength() {\n            return -1;\n        }\n\n        @Override\n        public InputStream getContent() throws IOException {\n            SequenceInputStreamBuilder result = new SequenceInputStreamBuilder();\n            try {\n                write(result);\n                return result.build();\n            } catch (Exception e) {\n                result.close();\n                throw e;\n            }\n        }\n\n        private void write(SequenceInputStreamBuilder result) throws IOException {\n            ByteArrayBuffer boundaryEncoded = encode(StandardCharsets.US_ASCII, this.boundary);\n\n            for (int p = 0, partCount = partHeaders.size(); p < partCount; p++) {\n                Headers headers = partHeaders.get(p);\n                RequestBody body = partBodies.get(p);\n\n                result.write(DASHDASH);\n                result.write(boundaryEncoded);\n                result.write(CRLF);\n\n                if (headers != null) {\n                    for (int h = 0, headerCount = headers.size(); h < headerCount; h++) {\n                        writeHeader(headers.name(h), headers.value(h), result);\n                    }\n                }\n\n                ContentType contentType = body.contentType();\n                if (contentType != null) {\n                    writeHeader(\"Content-Type\", contentType.toString(), result);\n                }\n\n                long contentLength = body.contentLength();\n                if (contentLength != -1) {\n                    writeHeader(\"Content-Length\", String.valueOf(contentLength), result);\n                }\n\n                result.write(CRLF);\n                result.write(body.getContent());\n                result.write(CRLF);\n            }\n\n            result.write(DASHDASH);\n            result.write(boundaryEncoded);\n            result.write(DASHDASH);\n            result.write(CRLF);\n        }\n\n        private void writeHeader(String name, String value, SequenceInputStreamBuilder out) throws IOException {\n            out.write(encodeHeader(name));\n            out.write(COLONSPACE);\n            out.write(encodeHeader(value));\n            out.write(CRLF);\n        }\n\n        private static ByteArrayBuffer encode(Charset charset, String string) {\n            ByteBuffer encoded = charset.encode(CharBuffer.wrap(string));\n            ByteArrayBuffer bab = new ByteArrayBuffer(encoded.remaining());\n            bab.append(encoded.array(), encoded.arrayOffset() + encoded.position(), encoded.remaining());\n            return bab;\n        }\n\n        private static ByteArrayBuffer encodeHeader(String value) {\n            return encode(StandardCharsets.ISO_8859_1, value);\n        }\n    }\n\n    static class SequenceInputStreamBuilder {\n\n        private final Vector<InputStream> streams = new Vector<>();\n        private final ByteArrayBuffer currentBuffer = new ByteArrayBuffer(1024);\n\n        public void write(byte[] buff) {\n            currentBuffer.append(buff);\n        }\n\n        public void write(ByteArrayBuffer buff) {\n            currentBuffer.append(buff.array(), 0, buff.length());\n        }\n\n        public void write(InputStream stream) {\n            flushCurrentBuffer();\n\n            streams.add(stream);\n        }\n\n        public void close() throws IOException {\n            IOException ioe = null;\n            for (InputStream in : streams) {\n                try {\n                    in.close();\n                } catch (IOException e) {\n                    if (ioe == null) {\n                        ioe = e;\n                    } else {\n                        ioe.addSuppressed(e);\n                    }\n                }\n            }\n            if (ioe != null) {\n                throw ioe;\n            }\n        }\n\n        public InputStream build() {\n            flushCurrentBuffer();\n\n            return new SequenceInputStream(streams.elements());\n        }\n\n        private void flushCurrentBuffer() {\n            if (currentBuffer.length() > 0) {\n                streams.add(new ByteArrayInputStream(currentBuffer.toByteArray(), 0, currentBuffer.length()));\n                currentBuffer.clear();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/impl/MultipartRequestBodyHandler.java",
    "content": "package com.walmartlabs.concord.client2.impl;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collection;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic final class MultipartRequestBodyHandler {\n\n    public static HttpEntity handle(ObjectMapper objectMapper, Map<String, Object> data) {\n        return handle(new MultipartBuilder(), objectMapper, data);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static HttpEntity handle(MultipartBuilder b, ObjectMapper objectMapper, Map<String, Object> data) {\n        for (Map.Entry<String, Object> e : data.entrySet()) {\n            String k = e.getKey();\n            Object v = e.getValue();\n            if (v instanceof InputStream) {\n                b.addFormDataPart(k, null, new InputStreamRequestBody((InputStream) v));\n            } else if (v instanceof byte[]) {\n                b.addFormDataPart(k, null, RequestBody.create(ContentType.APPLICATION_OCTET_STREAM, (byte[]) v));\n            } else if (v instanceof String) {\n                b.addFormDataPart(k, (String) v);\n            } else if (v instanceof Path) {\n                b.addFormDataPart(k, null, new PathRequestBody((Path) v));\n            } else if (v instanceof Map) {\n                String json;\n                try {\n                    json = objectMapper.writeValueAsString(v);\n                } catch (JsonProcessingException ex) {\n                    throw new RuntimeException(ex);\n                }\n                b.addFormDataPart(k, null, RequestBody.create(ContentType.APPLICATION_JSON, json));\n            } else if (v instanceof Boolean) {\n                b.addFormDataPart(k, null, RequestBody.create(ContentType.TEXT_PLAIN, v.toString()));\n            } else if (v instanceof String[]) {\n                b.addFormDataPart(k, null, RequestBody.create(ContentType.TEXT_PLAIN, String.join(\",\", (String[]) v)));\n            } else if (v instanceof Collection<?>) {\n                b.addFormDataPart(k, null, RequestBody.create(ContentType.TEXT_PLAIN, String.join(\",\", (Collection) v)));\n            } else if (v instanceof UUID) {\n                b.addFormDataPart(k, v.toString());\n            } else if (v instanceof Enum<?>) {\n                b.addFormDataPart(k, ((Enum<?>)v).name());\n            } else {\n                throw new IllegalArgumentException(\"Unknown input type: \" + k + \"=\" + v + (v != null ? \" (\" + v.getClass() + \")\" : \"\"));\n            }\n        }\n        return b.build();\n    }\n\n    private MultipartRequestBodyHandler() {\n    }\n\n    public static final class InputStreamRequestBody extends RequestBody {\n\n        private final InputStream in;\n\n        public InputStreamRequestBody(InputStream in) {\n            this.in = in;\n        }\n\n        @Override\n        public ContentType contentType() {\n            return ContentType.APPLICATION_OCTET_STREAM;\n        }\n\n        @Override\n        public long contentLength() {\n            return -1;\n        }\n\n        @Override\n        public InputStream getContent() {\n            return in;\n        }\n    }\n\n    public static class PathRequestBody extends RequestBody {\n\n        private final Path path;\n\n        public PathRequestBody(Path path) {\n            this.path = path;\n        }\n\n        @Override\n        public ContentType contentType() {\n            return ContentType.APPLICATION_OCTET_STREAM;\n        }\n\n        @Override\n        public long contentLength() throws IOException {\n            return Files.size(path);\n        }\n\n        @Override\n        public InputStream getContent() throws IOException {\n            return Files.newInputStream(this.path);\n        }\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/impl/NameValuePair.java",
    "content": "package com.walmartlabs.concord.client2.impl;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\nimport java.util.Objects;\n\npublic class NameValuePair implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String name;\n    private final String value;\n\n    public NameValuePair(final String name, final String value) {\n        this.name = name;\n        this.value = value;\n    }\n\n    public String getName() {\n        return this.name;\n    }\n\n    public String getValue() {\n        return this.value;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        NameValuePair that = (NameValuePair) o;\n        return Objects.equals(name, that.name) && Objects.equals(value, that.value);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(name, value);\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/impl/OffsetDateTimeDeserializer.java",
    "content": "package com.walmartlabs.concord.client2.impl;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer;\n\nimport java.time.Instant;\nimport java.time.OffsetDateTime;\n\npublic class OffsetDateTimeDeserializer extends InstantDeserializer<OffsetDateTime> {\n\n    public OffsetDateTimeDeserializer() {\n        super(\n                OffsetDateTime.class, OffsetDateTimeSerializer.FORMATTER,\n                OffsetDateTime::from,\n                a -> OffsetDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId),\n                a -> OffsetDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId),\n                (d, z) -> (d.isEqual(OffsetDateTime.MIN) || d.isEqual(OffsetDateTime.MAX) ? d : d.withOffsetSameInstant(z.getRules().getOffset(d.toLocalDateTime()))),\n                false\n        );\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/impl/OffsetDateTimeSerializer.java",
    "content": "package com.walmartlabs.concord.client2.impl;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.JsonSerializer;\nimport com.fasterxml.jackson.databind.SerializerProvider;\n\nimport java.io.IOException;\nimport java.time.OffsetDateTime;\nimport java.time.format.DateTimeFormatter;\n\npublic class OffsetDateTimeSerializer extends JsonSerializer<OffsetDateTime> {\n\n    public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(\"yyyy-MM-dd'T'HH:mm:ss.SSSX\");\n\n    @Override\n    public void serialize(OffsetDateTime value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {\n        if (value == null) {\n            throw new IOException(\"OffsetDateTime argument is null.\");\n        }\n\n        jsonGenerator.writeString(FORMATTER.format(value));\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/impl/RequestBody.java",
    "content": "package com.walmartlabs.concord.client2.impl;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.ByteArrayInputStream;\nimport java.io.InputStream;\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Objects;\n\npublic abstract class RequestBody implements HttpEntity {\n\n    public static RequestBody create(ContentType contentType, String content) {\n        Charset charset = StandardCharsets.UTF_8;\n        if (contentType != null) {\n            charset = contentType.getCharset();\n            if (charset == null) {\n                charset = StandardCharsets.UTF_8;\n                contentType = contentType.withCharset(charset);\n            }\n        }\n        byte[] bytes = content.getBytes(charset);\n        return create(contentType, bytes);\n    }\n\n    public static RequestBody create(ContentType contentType, byte[] content) {\n        return create(contentType, content, 0, content.length);\n    }\n\n    public static RequestBody create(ContentType contentType, byte[] content, int offset, int byteCount) {\n        Objects.requireNonNull(content, \"content\");\n\n        return new RequestBody() {\n            @Override\n            public ContentType contentType() {\n                return contentType;\n            }\n\n            @Override\n            public long contentLength() {\n                return byteCount;\n            }\n\n            @Override\n            public InputStream getContent() {\n                return new ByteArrayInputStream(content, offset, byteCount);\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/impl/RequestBodyHandler.java",
    "content": "package com.walmartlabs.concord.client2.impl;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.http.HttpRequest;\n\npublic final class RequestBodyHandler {\n\n    public static HttpRequest.BodyPublisher handle(ObjectMapper ignoredObjectMapper, byte[] param) throws IOException {\n        return HttpRequest.BodyPublishers.ofByteArray(param);\n    }\n\n    public static HttpRequest.BodyPublisher handle(ObjectMapper ignoredObjectMapper, InputStream param) throws IOException {\n        return HttpRequest.BodyPublishers.ofInputStream(() -> param);\n    }\n\n    public static HttpRequest.BodyPublisher handle(ObjectMapper objectMapper, Object param) throws IOException {\n        if (param instanceof String) {\n            return HttpRequest.BodyPublishers.ofString((String) param);\n        }\n        byte[] localVarPostBody = objectMapper.writeValueAsBytes(param);\n        return HttpRequest.BodyPublishers.ofByteArray(localVarPostBody);\n    }\n\n    private RequestBodyHandler() {\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/impl/ResponseBodyHandler.java",
    "content": "package com.walmartlabs.concord.client2.impl;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.client2.ApiException;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.lang.reflect.Type;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic final class ResponseBodyHandler {\n\n    @SuppressWarnings(\"unchecked\")\n    public static <T> T handle(ObjectMapper objectMapper,\n                              HttpResponse<InputStream> response,\n                              TypeReference<T> returnTypeRef) throws IOException, ApiException {\n        if (response == null) {\n            return null;\n        }\n\n        Type returnType = returnTypeRef.getType();\n        InputStream is = response.body();\n        if (is == null) {\n            return null;\n        }\n\n        try {\n            if (returnType.equals(byte[].class)) {\n                return (T)is.readAllBytes();\n            } else if (returnType.equals(InputStream.class)) {\n                return (T)is;\n            }\n\n            String contentType = response.headers().firstValue(\"Content-Type\").orElse(\"application/json\");\n            if (isJsonMime(contentType)) {\n                return objectMapper.readValue(is, returnTypeRef);\n            } else if (returnType.equals(String.class)) {\n                return (T) toString(is, charset(response));\n            } else {\n                throw new ApiException(\n                        \"Content type \\\"\" + contentType + \"\\\" is not supported for type: \" + returnType,\n                        response.statusCode(),\n                        response.headers(),\n                        \"skipped\");\n            }\n        } finally {\n            if (!returnType.equals(InputStream.class)) {\n                is.close();\n            }\n        }\n    }\n\n    private static boolean isJsonMime(String mime) {\n        String jsonMime = \"(?i)^(application/json|[^;/ \\t]+/[^;/ \\t]+[+]json)[ \\t]*(;.*)?$\";\n        return mime != null && (mime.matches(jsonMime) || mime.equals(\"*/*\"));\n    }\n\n    private static String toString(InputStream input, Charset charset) throws IOException {\n        return new String(input.readAllBytes(), charset);\n    }\n\n    private static Charset charset(HttpResponse<InputStream> response) {\n        String contentType = response.headers().firstValue(\"Content-Type\").orElse(null);\n        if (contentType == null) {\n            return StandardCharsets.UTF_8;\n        }\n\n        return parseCharset(contentType, StandardCharsets.UTF_8);\n    }\n\n    // TODO: super simple\n    private static Charset parseCharset(String contentTypeHeader, Charset defaultCharset) {\n        Pattern pattern = Pattern.compile(\"charset=([\\\\w-]+)\", Pattern.CASE_INSENSITIVE);\n        Matcher matcher = pattern.matcher(contentTypeHeader);\n\n        if (matcher.find()) {\n            return Charset.forName(matcher.group(1));\n        }\n\n        return defaultCharset;\n    }\n\n    private ResponseBodyHandler() {\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/impl/auth/ApiKey.java",
    "content": "package com.walmartlabs.concord.client2.impl.auth;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.net.http.HttpRequest;\n\npublic class ApiKey implements Authentication {\n\n    private final String key;\n\n    public ApiKey(String key) {\n        this.key = key;\n    }\n\n    @Override\n    public HttpRequest.Builder applyTo(HttpRequest.Builder requesBuilder) {\n        return requesBuilder.setHeader(\"Authorization\", key);\n    }\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/impl/auth/Authentication.java",
    "content": "package com.walmartlabs.concord.client2.impl.auth;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.net.http.HttpRequest;\n\npublic interface Authentication {\n\n    HttpRequest.Builder applyTo(HttpRequest.Builder requesBuilder);\n}\n"
  },
  {
    "path": "client2/src/main/java/com/walmartlabs/concord/client2/impl/auth/SessionToken.java",
    "content": "package com.walmartlabs.concord.client2.impl.auth;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.net.http.HttpRequest;\n\npublic class SessionToken implements Authentication {\n\n    private final String token;\n\n    public SessionToken(String token) {\n        this.token = token;\n    }\n\n    @Override\n    public HttpRequest.Builder applyTo(HttpRequest.Builder requesBuilder) {\n        return requesBuilder.setHeader(\"X-Concord-SessionToken\", token);\n    }\n}\n"
  },
  {
    "path": "client2/src/main/template/README.md",
    "content": "Check the diff between `*.mustache` and `*.orig` to see the introduced customizations."
  },
  {
    "path": "client2/src/main/template/libraries/native/ApiClient.mustache",
    "content": "{{>licenseInfo}}\npackage {{invokerPackage}};\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.DeserializationFeature;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.SerializationFeature;\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\n{{#openApiNullable}}\nimport org.openapitools.jackson.nullable.JsonNullableModule;\n{{/openApiNullable}}\n\nimport java.io.InputStream;\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpConnectTimeoutException;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\nimport java.time.OffsetDateTime;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.HashMap;\nimport java.util.StringJoiner;\nimport java.util.function.Consumer;\nimport java.util.stream.Collectors;\n\nimport com.walmartlabs.concord.client2.impl.auth.Authentication;\nimport com.walmartlabs.concord.client2.impl.auth.ApiKey;\nimport com.walmartlabs.concord.client2.impl.auth.SessionToken;\nimport com.walmartlabs.concord.client2.impl.OffsetDateTimeDeserializer;\nimport com.walmartlabs.concord.client2.impl.OffsetDateTimeSerializer;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\n/**\n * Configuration and utility class for API clients.\n *\n * <p>This class can be constructed and modified, then used to instantiate the\n * various API classes. The API classes use the settings in this class to\n * configure themselves, but otherwise do not store a link to this class.</p>\n *\n * <p>This class is mutable and not synchronized, so it is not thread-safe.\n * The API classes generated from this are immutable and thread-safe.</p>\n *\n * <p>The setter methods of this class return the current object to facilitate\n * a fluent style of configuration.</p>\n */\n{{>generatedAnnotation}}\npublic class ApiClient {\n\n  private final HttpClient httpClient;\n  private ObjectMapper mapper;\n  private String baseUri;\n  private String scheme;\n  private String host;\n  private int port;\n  private String basePath;\n  private Consumer<HttpRequest.Builder> interceptor;\n  private Consumer<HttpResponse<InputStream>> responseInterceptor;\n  private Consumer<HttpResponse<String>> asyncResponseInterceptor;\n  private Duration readTimeout;\n  private Duration connectTimeout;\n  private Authentication auth;\n  private final Map<String, String> defaultHeaderMap = new HashMap<String, String>();\n\n  public Authentication getAuth() {\n    return auth;\n  }\n\n  public void setAuth(Authentication auth) {\n    this.auth = auth;\n  }\n\n  public Map<String, String> defaultHeaderMap() {\n    return defaultHeaderMap;\n  }\n\n  private static String valueToString(Object value) {\n    if (value == null) {\n      return \"\";\n    }\n    if (value instanceof OffsetDateTime) {\n      return ((OffsetDateTime) value).format(OffsetDateTimeSerializer.FORMATTER);\n    }\n    return value.toString();\n  }\n\n  /**\n   * URL encode a string in the UTF-8 encoding.\n   *\n   * @param s String to encode.\n   * @return URL-encoded representation of the input string.\n   */\n  public static String urlEncode(String s) {\n    return URLEncoder.encode(s, UTF_8).replaceAll(\"\\\\+\", \"%20\");\n  }\n\n  /**\n   * Convert a URL query name/value parameter to a list of encoded {@link Pair}\n   * objects.\n   *\n   * <p>The value can be null, in which case an empty list is returned.</p>\n   *\n   * @param name The query name parameter.\n   * @param value The query value, which may not be a collection but may be\n   *              null.\n   * @return A singleton list of the {@link Pair} objects representing the input\n   * parameters, which is encoded for use in a URL. If the value is null, an\n   * empty list is returned.\n   */\n  public static List<Pair> parameterToPairs(String name, Object value) {\n    if (name == null || name.isEmpty() || value == null) {\n      return Collections.emptyList();\n    }\n    return Collections.singletonList(new Pair(urlEncode(name), urlEncode(valueToString(value))));\n  }\n\n  /**\n   * Convert a URL query name/collection parameter to a list of encoded\n   * {@link Pair} objects.\n   *\n   * @param collectionFormat The swagger collectionFormat string (csv, tsv, etc).\n   * @param name The query name parameter.\n   * @param values A collection of values for the given query name, which may be\n   *               null.\n   * @return A list of {@link Pair} objects representing the input parameters,\n   * which is encoded for use in a URL. If the values collection is null, an\n   * empty list is returned.\n   */\n  public static List<Pair> parameterToPairs(\n      String collectionFormat, String name, Collection<?> values) {\n    if (name == null || name.isEmpty() || values == null || values.isEmpty()) {\n      return Collections.emptyList();\n    }\n\n    // get the collection format (default: csv)\n    String format = collectionFormat == null || collectionFormat.isEmpty() ? \"csv\" : collectionFormat;\n\n    // create the params based on the collection format\n    if (\"multi\".equals(format)) {\n      return values.stream()\n          .map(value -> new Pair(urlEncode(name), urlEncode(valueToString(value))))\n          .collect(Collectors.toList());\n    }\n\n    String delimiter;\n    switch(format) {\n      case \"csv\":\n        delimiter = urlEncode(\",\");\n        break;\n      case \"ssv\":\n        delimiter = urlEncode(\" \");\n        break;\n      case \"tsv\":\n        delimiter = urlEncode(\"\\t\");\n        break;\n      case \"pipes\":\n        delimiter = urlEncode(\"|\");\n        break;\n      default:\n        throw new IllegalArgumentException(\"Illegal collection format: \" + collectionFormat);\n    }\n\n    StringJoiner joiner = new StringJoiner(delimiter);\n    for (Object value : values) {\n      joiner.add(urlEncode(valueToString(value)));\n    }\n\n    return Collections.singletonList(new Pair(urlEncode(name), joiner.toString()));\n  }\n\n  /**\n   * Create an instance of ApiClient.\n   */\n  public ApiClient(HttpClient httpClient) {\n    this.httpClient = httpClient;\n    this.mapper = createDefaultObjectMapper();\n    updateBaseUri(getDefaultBaseUri());\n    interceptor = null;\n    readTimeout = null;\n    connectTimeout = null;\n    responseInterceptor = null;\n    asyncResponseInterceptor = null;\n  }\n\n  public static ObjectMapper createDefaultObjectMapper() {\n    ObjectMapper mapper = new ObjectMapper();\n    mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);\n    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);\n    mapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false);\n    mapper.configure(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS, true);\n    mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);\n    mapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);\n    mapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING);\n    mapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);\n    mapper.registerModule(new JavaTimeModule()\n                    .addDeserializer(OffsetDateTime.class, new OffsetDateTimeDeserializer())\n                    .addSerializer(OffsetDateTime.class, new OffsetDateTimeSerializer()));\n    {{#openApiNullable}}\n    mapper.registerModule(new JsonNullableModule());\n    {{/openApiNullable}}\n    return mapper;\n  }\n\n  public ApiClient setUserAgent(String userAgent) {\n    addDefaultHeader(\"User-Agent\", userAgent);\n    return this;\n  }\n\n  public ApiClient addDefaultHeader(String key, String value) {\n    defaultHeaderMap.put(key, value);\n    return this;\n  }\n\n  protected String getDefaultBaseUri() {\n    return \"{{{basePath}}}\";\n  }\n\n  public HttpRequest.Builder requestBuilder() {\n    HttpRequest.Builder result = HttpRequest.newBuilder();\n    for (Map.Entry<String, String> e : defaultHeaderMap.entrySet()) {\n        result.header(e.getKey(), e.getValue());\n    }\n    if (getAuth() != null) {\n        result = getAuth().applyTo(result);\n    }\n    return result;\n  }\n\n  public void updateBaseUri(String baseUri) {\n    this.baseUri = baseUri;\n\n    URI uri = URI.create(baseUri);\n    scheme = uri.getScheme();\n    host = uri.getHost();\n    port = uri.getPort();\n    basePath = uri.getRawPath();\n  }\n\n  public ApiClient setBaseUrl(String baseUrl) {\n    updateBaseUri(baseUrl);\n    return this;\n  }\n\n  public String getBaseUrl() {\n    return this.baseUri;\n  }\n\n  public ApiClient setSessionToken(String token) {\n    if (token == null) {\n        return this;\n    }\n\n    setAuth(new SessionToken(token));\n\n    return this;\n  }\n\n  public ApiClient setApiKey(String key) {\n    if (key == null) {\n        return this;\n    }\n\n    setAuth(new ApiKey(key));\n\n    return this;\n  }\n\n  /**\n   * Get an {@link HttpClient} based on the current {@link HttpClient.Builder}.\n   *\n   * <p>The returned object is immutable and thread-safe.</p>\n   *\n   * @return The HTTP client.\n   */\n  public HttpClient getHttpClient() {\n    return httpClient;\n  }\n\n  /**\n   * Set a custom {@link ObjectMapper} to serialize and deserialize the request\n   * and response bodies.\n   *\n   * @param mapper Custom object mapper.\n   * @return This object.\n   */\n  public ApiClient setObjectMapper(ObjectMapper mapper) {\n    this.mapper = mapper;\n    return this;\n  }\n\n  /**\n   * Get a copy of the current {@link ObjectMapper}.\n   *\n   * @return A copy of the current object mapper.\n   */\n  public ObjectMapper getObjectMapper() {\n    return mapper.copy();\n  }\n\n  /**\n   * Set a custom host name for the target service.\n   *\n   * @param host The host name of the target service.\n   * @return This object.\n   */\n  public ApiClient setHost(String host) {\n    this.host = host;\n    return this;\n  }\n\n  /**\n   * Set a custom port number for the target service.\n   *\n   * @param port The port of the target service. Set this to -1 to reset the\n   *             value to the default for the scheme.\n   * @return This object.\n   */\n  public ApiClient setPort(int port) {\n    this.port = port;\n    return this;\n  }\n\n  /**\n   * Set a custom base path for the target service, for example '/v2'.\n   *\n   * @param basePath The base path against which the rest of the path is\n   *                 resolved.\n   * @return This object.\n   */\n  public ApiClient setBasePath(String basePath) {\n    this.basePath = basePath;\n    return this;\n  }\n\n  /**\n   * Get the base URI to resolve the endpoint paths against.\n   *\n   * @return The complete base URI that the rest of the API parameters are\n   * resolved against.\n   */\n  public String getBaseUri() {\n    return scheme + \"://\" + host + (port == -1 ? \"\" : \":\" + port) + basePath;\n  }\n\n  /**\n   * Set a custom scheme for the target service, for example 'https'.\n   *\n   * @param scheme The scheme of the target service\n   * @return This object.\n   */\n  public ApiClient setScheme(String scheme){\n    this.scheme = scheme;\n    return this;\n  }\n\n  /**\n   * Set a custom request interceptor.\n   *\n   * <p>A request interceptor is a mechanism for altering each request before it\n   * is sent. After the request has been fully configured but not yet built, the\n   * request builder is passed into this function for further modification,\n   * after which it is sent out.</p>\n   *\n   * <p>This is useful for altering the requests in a custom manner, such as\n   * adding headers. It could also be used for logging and monitoring.</p>\n   *\n   * @param interceptor A function invoked before creating each request. A value\n   *                    of null resets the interceptor to a no-op.\n   * @return This object.\n   */\n  public ApiClient setRequestInterceptor(Consumer<HttpRequest.Builder> interceptor) {\n    this.interceptor = interceptor;\n    return this;\n  }\n\n  /**\n   * Get the custom interceptor.\n   *\n   * @return The custom interceptor that was set, or null if there isn't any.\n   */\n  public Consumer<HttpRequest.Builder> getRequestInterceptor() {\n    return interceptor;\n  }\n\n  /**\n   * Set a custom response interceptor.\n   *\n   * <p>This is useful for logging, monitoring or extraction of header variables</p>\n   *\n   * @param interceptor A function invoked before creating each request. A value\n   *                    of null resets the interceptor to a no-op.\n   * @return This object.\n   */\n  public ApiClient setResponseInterceptor(Consumer<HttpResponse<InputStream>> interceptor) {\n    this.responseInterceptor = interceptor;\n    return this;\n  }\n\n /**\n   * Get the custom response interceptor.\n   *\n   * @return The custom interceptor that was set, or null if there isn't any.\n   */\n  public Consumer<HttpResponse<InputStream>> getResponseInterceptor() {\n    return responseInterceptor;\n  }\n\n  /**\n   * Set a custom async response interceptor. Use this interceptor when asyncNative is set to 'true'.\n   *\n   * <p>This is useful for logging, monitoring or extraction of header variables</p>\n   *\n   * @param interceptor A function invoked before creating each request. A value\n   *                    of null resets the interceptor to a no-op.\n   * @return This object.\n   */\n  public ApiClient setAsyncResponseInterceptor(Consumer<HttpResponse<String>> interceptor) {\n    this.asyncResponseInterceptor = interceptor;\n    return this;\n  }\n\n /**\n   * Get the custom async response interceptor. Use this interceptor when asyncNative is set to 'true'.\n   *\n   * @return The custom interceptor that was set, or null if there isn't any.\n   */\n  public Consumer<HttpResponse<String>> getAsyncResponseInterceptor() {\n    return asyncResponseInterceptor;\n  }\n\n  /**\n   * Set the read timeout for the http client.\n   *\n   * <p>This is the value used by default for each request, though it can be\n   * overridden on a per-request basis with a request interceptor.</p>\n   *\n   * @param readTimeout The read timeout used by default by the http client.\n   *                    Setting this value to null resets the timeout to an\n   *                    effectively infinite value.\n   * @return This object.\n   */\n  public ApiClient setReadTimeout(Duration readTimeout) {\n    this.readTimeout = readTimeout;\n    return this;\n  }\n\n  /**\n   * Get the read timeout that was set.\n   *\n   * @return The read timeout, or null if no timeout was set. Null represents\n   * an infinite wait time.\n   */\n  public Duration getReadTimeout() {\n    return readTimeout;\n  }\n}"
  },
  {
    "path": "client2/src/main/template/libraries/native/api.mustache",
    "content": "{{>licenseInfo}}\npackage {{package}};\n\nimport {{invokerPackage}}.ApiClient;\nimport {{invokerPackage}}.ApiException;\nimport {{invokerPackage}}.ApiResponse;\nimport {{invokerPackage}}.Pair;\n\n{{#imports}}\nimport {{import}};\n{{/imports}}\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\n\nimport com.walmartlabs.concord.client2.impl.*;\n\n{{#hasFormParamsInSpec}}\n{{/hasFormParamsInSpec}}\nimport java.io.InputStream;\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.net.http.HttpRequest;\nimport java.nio.channels.Channels;\nimport java.nio.channels.Pipe;\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\n\n{{^fullJavaUtil}}\nimport java.util.ArrayList;\nimport java.util.StringJoiner;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.HashMap;\nimport java.util.function.Consumer;\n{{/fullJavaUtil}}\n{{#asyncNative}}\n\nimport java.util.concurrent.CompletableFuture;\n{{/asyncNative}}\n\n{{>generatedAnnotation}}\n{{#operations}}\npublic class {{classname}} {\n  private final HttpClient memberVarHttpClient;\n  private final ObjectMapper memberVarObjectMapper;\n  private final String memberVarBaseUri;\n  private final {{#fullJavaUtil}}java.util.function.{{/fullJavaUtil}}Consumer<HttpRequest.Builder> memberVarInterceptor;\n  private final Duration memberVarReadTimeout;\n  private final {{#fullJavaUtil}}java.util.function.{{/fullJavaUtil}}Consumer<HttpResponse<InputStream>> memberVarResponseInterceptor;\n  private final {{#fullJavaUtil}}java.util.function.{{/fullJavaUtil}}Consumer<HttpResponse<String>> memberVarAsyncResponseInterceptor;\n\n  private final ApiClient apiClient;\n\n  public {{classname}}(ApiClient apiClient) {\n    memberVarHttpClient = apiClient.getHttpClient();\n    memberVarObjectMapper = apiClient.getObjectMapper();\n    memberVarBaseUri = apiClient.getBaseUri();\n    memberVarInterceptor = apiClient.getRequestInterceptor();\n    memberVarReadTimeout = apiClient.getReadTimeout();\n    memberVarResponseInterceptor = apiClient.getResponseInterceptor();\n    memberVarAsyncResponseInterceptor = apiClient.getAsyncResponseInterceptor();\n\n    this.apiClient = apiClient;\n  }\n\n  public ApiClient getApiClient() {\n    return this.apiClient;\n  }\n\n  {{#asyncNative}}\n\n  private ApiException getApiException(String operationId, HttpResponse<String> response) {\n    String message = formatExceptionMessage(operationId, response.statusCode(), response.body());\n    return new ApiException(response.statusCode(), message, response.headers(), response.body());\n  }\n  {{/asyncNative}}\n  {{^asyncNative}}\n\n  protected ApiException getApiException(String operationId, HttpResponse<InputStream> response) throws IOException {\n    String body = response.body() == null ? null : new String(response.body().readAllBytes());\n    String message = formatExceptionMessage(operationId, response.statusCode(), body);\n    return new ApiException(response.statusCode(), message, response.headers(), body);\n  }\n  {{/asyncNative}}\n\n  private String formatExceptionMessage(String operationId, int statusCode, String body) {\n    if (body == null || body.isEmpty()) {\n      body = \"[no body]\";\n    }\n    return operationId + \" call failed with: \" + statusCode + \" - \" + body;\n  }\n\n  {{#operation}}\n  {{#vendorExtensions.x-group-parameters}}\n  {{#hasParams}}\n  {{#isDeprecated}}\n  @Deprecated\n  {{/isDeprecated}}\n  public {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}}{{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture<Void>{{/asyncNative}}{{^asyncNative}}void{{/asyncNative}}{{/returnType}} {{operationId}}(API{{operationId}}Request apiRequest) throws ApiException {\n    {{#allParams}}\n    {{{dataType}}} {{paramName}} = apiRequest.{{paramName}}();\n    {{/allParams}}\n    {{#returnType}}return {{/returnType}}{{^returnType}}{{#asyncNative}}return {{/asyncNative}}{{/returnType}}{{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}});\n  }\n\n  {{#isDeprecated}}\n  @Deprecated\n  {{/isDeprecated}}\n  public {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} {{operationId}}WithHttpInfo(API{{operationId}}Request apiRequest) throws ApiException {\n    {{#allParams}}\n    {{{dataType}}} {{paramName}} = apiRequest.{{paramName}}();\n    {{/allParams}}\n    return {{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}});\n  }\n\n  {{/hasParams}}\n  {{/vendorExtensions.x-group-parameters}}\n  {{#isDeprecated}}\n  @Deprecated\n  {{/isDeprecated}}\n\n  {{#vendorExtensions.x-concord.groupParams}}\n  public {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}}{{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture<Void>{{/asyncNative}}{{^asyncNative}}void{{/asyncNative}}{{/returnType}} {{operationId}}({{vendorExtensions.x-concord.groupName}} in) throws ApiException {\n      return {{operationId}}({{#isMultipart}}{{#allParams}}{{^isFormParam}}in.{{paramName}}(),{{/isFormParam}}{{/allParams}} multipartInput{{/isMultipart}}{{^isMultipart}}{{#allParams}}in.{{paramName}}(){{^-last}}, {{/-last}} {{/allParams}}{{/isMultipart}});\n  }\n  {{/vendorExtensions.x-concord.groupParams}}\n\n  public {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}}{{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture<Void>{{/asyncNative}}{{^asyncNative}}void{{/asyncNative}}{{/returnType}} {{operationId}}({{#isMultipart}}{{#allParams}}{{^isFormParam}}{{#vendorExtensions.x-concord.customQueryParams}}Map<String, String>{{/vendorExtensions.x-concord.customQueryParams}}{{^vendorExtensions.x-concord.customQueryParams}}{{{dataType}}}{{/vendorExtensions.x-concord.customQueryParams}} {{paramName}},{{/isFormParam}}{{/allParams}} Map<String, Object> multipartInput{{/isMultipart}}{{^isMultipart}}{{#allParams}} {{#vendorExtensions.x-concord.customQueryParams}}Map<String, String>{{/vendorExtensions.x-concord.customQueryParams}}{{^vendorExtensions.x-concord.customQueryParams}}{{{dataType}}}{{/vendorExtensions.x-concord.customQueryParams}} {{paramName}}{{^-last}}, {{/-last}} {{/allParams}}{{/isMultipart}}) throws ApiException {\n    {{^asyncNative}}\n    {{#returnType}}ApiResponse<{{{.}}}> localVarResponse = {{/returnType}}{{operationId}}WithHttpInfo({{#isMultipart}}{{#allParams}}{{^isFormParam}}{{paramName}},{{/isFormParam}}{{/allParams}} multipartInput{{/isMultipart}}{{^isMultipart}}{{#allParams}}{{paramName}}{{^-last}}, {{/-last}} {{/allParams}}{{/isMultipart}});\n    {{#returnType}}\n    return localVarResponse.getData();\n    {{/returnType}}\n    {{/asyncNative}}\n    {{#asyncNative}}\n    try {\n      HttpRequest.Builder localVarRequestBuilder = {{operationId}}RequestBuilder({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}});\n      return memberVarHttpClient.sendAsync(\n          localVarRequestBuilder.build(),\n          HttpResponse.BodyHandlers.ofString()).thenComposeAsync(localVarResponse -> {\n            if (localVarResponse.statusCode()/ 100 != 2) {\n              return CompletableFuture.failedFuture(getApiException(\"{{operationId}}\", localVarResponse));\n            }\n            {{#returnType}}\n            try {\n              String responseBody = localVarResponse.body();\n              return CompletableFuture.completedFuture(\n                  responseBody == null || responseBody.isBlank() ? null : memberVarObjectMapper.readValue(responseBody, new TypeReference<{{{returnType}}}>() {})\n              );\n            } catch (IOException e) {\n              return CompletableFuture.failedFuture(new ApiException(e));\n            }\n            {{/returnType}}\n            {{^returnType}}\n            return CompletableFuture.completedFuture(null);\n            {{/returnType}}\n      });\n    }\n    catch (ApiException e) {\n      return CompletableFuture.failedFuture(e);\n    }\n    {{/asyncNative}}\n  }\n\n  {{#isDeprecated}}\n  @Deprecated\n  {{/isDeprecated}}\n  public {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} {{operationId}}WithHttpInfo({{#isMultipart}}{{#allParams}}{{^isFormParam}}{{#vendorExtensions.x-concord.customQueryParams}}Map<String, String>{{/vendorExtensions.x-concord.customQueryParams}}{{^vendorExtensions.x-concord.customQueryParams}}{{{dataType}}}{{/vendorExtensions.x-concord.customQueryParams}} {{paramName}},{{/isFormParam}}{{/allParams}} Map<String, Object> multipartInput{{/isMultipart}}{{^isMultipart}}{{#allParams}} {{#vendorExtensions.x-concord.customQueryParams}}Map<String, String>{{/vendorExtensions.x-concord.customQueryParams}}{{^vendorExtensions.x-concord.customQueryParams}}{{{dataType}}}{{/vendorExtensions.x-concord.customQueryParams}} {{paramName}}{{^-last}}, {{/-last}} {{/allParams}}{{/isMultipart}}) throws ApiException {\n    {{^asyncNative}}\n    HttpRequest.Builder localVarRequestBuilder = {{operationId}}RequestBuilder({{#isMultipart}}{{#allParams}}{{^isFormParam}}{{paramName}},{{/isFormParam}}{{/allParams}} multipartInput{{/isMultipart}}{{^isMultipart}}{{#allParams}}{{paramName}}{{^-last}}, {{/-last}} {{/allParams}}{{/isMultipart}});\n    try {\n      HttpResponse<InputStream> localVarResponse = memberVarHttpClient.send(\n          localVarRequestBuilder.build(),\n          HttpResponse.BodyHandlers.ofInputStream());\n      if (memberVarResponseInterceptor != null) {\n        memberVarResponseInterceptor.accept(localVarResponse);\n      }\n      try {\n        if (localVarResponse.statusCode()/ 100 != 2) {\n          throw getApiException(\"{{operationId}}\", localVarResponse);\n        }\n        if (localVarResponse.statusCode() == 204) {\n            return new ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>(\n                localVarResponse.statusCode(),\n                localVarResponse.headers().map(),\n                null\n            );\n        }\n\n        return new ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>(\n          localVarResponse.statusCode(),\n          localVarResponse.headers().map(),\n          {{#returnType}}\n          ResponseBodyHandler.handle(memberVarObjectMapper, localVarResponse, new TypeReference<{{{returnType}}}>() {})\n          {{/returnType}}\n          {{^returnType}}\n          null\n          {{/returnType}}\n        );\n      } finally {\n        {{^returnType}}\n        // Drain the InputStream\n        while (localVarResponse.body().read() != -1) {\n            // Ignore\n        }\n        localVarResponse.body().close();\n        {{/returnType}}\n      }\n    } catch (IOException e) {\n      throw new ApiException(e);\n    }\n    catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n      throw new ApiException(e);\n    }\n    {{/asyncNative}}\n    {{#asyncNative}}\n    try {\n      HttpRequest.Builder localVarRequestBuilder = {{operationId}}RequestBuilder({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}});\n      return memberVarHttpClient.sendAsync(\n          localVarRequestBuilder.build(),\n          HttpResponse.BodyHandlers.ofString()).thenComposeAsync(localVarResponse -> {\n            if (memberVarAsyncResponseInterceptor != null) {\n              memberVarAsyncResponseInterceptor.accept(localVarResponse);\n            }\n            if (localVarResponse.statusCode()/ 100 != 2) {\n              return CompletableFuture.failedFuture(getApiException(\"{{operationId}}\", localVarResponse));\n            }\n            {{#returnType}}\n            try {\n              String responseBody = localVarResponse.body();\n              return CompletableFuture.completedFuture(\n                  new ApiResponse<{{{returnType}}}>(\n                      localVarResponse.statusCode(),\n                      localVarResponse.headers().map(),\n                      TODO:)\n              );\n            } catch (IOException e) {\n              return CompletableFuture.failedFuture(new ApiException(e));\n            }\n            {{/returnType}}\n            {{^returnType}}\n            return CompletableFuture.completedFuture(\n                new ApiResponse<Void>(localVarResponse.statusCode(), localVarResponse.headers().map(), null)\n            );\n            {{/returnType}}\n        }\n      );\n    }\n    catch (ApiException e) {\n      return CompletableFuture.failedFuture(e);\n    }\n    {{/asyncNative}}\n  }\n\n\n  private HttpRequest.Builder {{operationId}}RequestBuilder({{#isMultipart}}{{#allParams}}{{^isFormParam}}{{#vendorExtensions.x-concord.customQueryParams}}Map<String, String>{{/vendorExtensions.x-concord.customQueryParams}}{{^vendorExtensions.x-concord.customQueryParams}}{{{dataType}}}{{/vendorExtensions.x-concord.customQueryParams}} {{paramName}},{{/isFormParam}}{{/allParams}} Map<String, Object> multipartInput{{/isMultipart}}{{^isMultipart}}{{#allParams}} {{#vendorExtensions.x-concord.customQueryParams}}Map<String, String>{{/vendorExtensions.x-concord.customQueryParams}}{{^vendorExtensions.x-concord.customQueryParams}}{{{dataType}}}{{/vendorExtensions.x-concord.customQueryParams}} {{paramName}}{{^-last}}, {{/-last}} {{/allParams}}{{/isMultipart}}) throws ApiException {\n    {{#allParams}}\n    {{#required}}\n    // verify the required parameter '{{paramName}}' is set\n    if ({{paramName}} == null) {\n      throw new ApiException(400, \"Missing the required parameter '{{paramName}}' when calling {{operationId}}\");\n    }\n    {{/required}}\n    {{/allParams}}\n\n    HttpRequest.Builder localVarRequestBuilder = apiClient.requestBuilder();\n\n    {{! Switch delimiters for baseName so we can write constants like \"{query}\" }}\n    String localVarPath = \"{{{path}}}\"{{#pathParams}}\n        .replace({{=<% %>=}}\"{<%baseName%>}\"<%={{ }}=%>, ApiClient.urlEncode({{{paramName}}}.toString())){{/pathParams}};\n\n    {{#hasQueryParams}}\n    {{javaUtilPrefix}}List<Pair> localVarQueryParams = new {{javaUtilPrefix}}ArrayList<>();\n    {{javaUtilPrefix}}StringJoiner localVarQueryStringJoiner = new {{javaUtilPrefix}}StringJoiner(\"&\");\n    {{#queryParams}}\n         {{#vendorExtensions.x-concord.customQueryParams}}\n    if ({{paramName}} != null) {\n        for (Map.Entry<String, String> e : {{paramName}}.entrySet()) {\n            localVarQueryParams.addAll(ApiClient.parameterToPairs(e.getKey(), e.getValue()));\n        }\n    }\n         {{/vendorExtensions.x-concord.customQueryParams}}\n      {{#collectionFormat}}\n    localVarQueryParams.addAll(ApiClient.parameterToPairs(\"{{{collectionFormat}}}\", \"{{baseName}}\", {{paramName}}));\n      {{/collectionFormat}}\n      {{^collectionFormat}}\n        {{#isDeepObject}}\n    if ({{paramName}} != null) {\n            {{#isArray}}\n      for (int i=0; i < {{paramName}}.size(); i++) {\n        localVarQueryStringJoiner.add({{paramName}}.get(i).toUrlQueryString(String.format(\"{{baseName}}[%d]\", i)));\n      }\n            {{/isArray}}\n            {{^isArray}}\n      localVarQueryStringJoiner.add({{paramName}}.toUrlQueryString(\"{{baseName}}\"));\n            {{/isArray}}\n    }\n        {{/isDeepObject}}\n        {{^isDeepObject}}\n            {{#isExplode}}\n                {{#hasVars}}\n                    {{#vars}}\n                        {{#isArray}}\n    localVarQueryParams.addAll(ApiClient.parameterToPairs(\"multi\", \"{{baseName}}\", {{paramName}}.{{getter}}()));\n                        {{/isArray}}\n                        {{^isArray}}\n    if ({{paramName}} != null) {\n        localVarQueryParams.addAll(ApiClient.parameterToPairs(\"{{paramName}}\", {{paramName}}.{{getter}}()));\n    }\n                        {{/isArray}}\n                    {{/vars}}\n                {{/hasVars}}\n                {{^hasVars}}\n                {{#isModel}}\n    localVarQueryStringJoiner.add({{paramName}}.toUrlQueryString());\n                {{/isModel}}\n                {{^isModel}}\n    {{^vendorExtensions.x-concord.customQueryParams}}\n    localVarQueryParams.addAll(ApiClient.parameterToPairs(\"{{baseName}}\", {{paramName}}));\n    {{/vendorExtensions.x-concord.customQueryParams}}\n                {{/isModel}}\n                {{/hasVars}}\n            {{/isExplode}}\n            {{^isExplode}}\n    localVarQueryParams.addAll(ApiClient.parameterToPairs(\"{{baseName}}\", {{paramName}}));\n            {{/isExplode}}\n        {{/isDeepObject}}\n      {{/collectionFormat}}\n    {{/queryParams}}\n\n    if (!localVarQueryParams.isEmpty() || localVarQueryStringJoiner.length() != 0) {\n      {{javaUtilPrefix}}StringJoiner queryJoiner = new {{javaUtilPrefix}}StringJoiner(\"&\");\n      localVarQueryParams.forEach(p -> queryJoiner.add(p.getName() + '=' + p.getValue()));\n      if (localVarQueryStringJoiner.length() != 0) {\n        queryJoiner.add(localVarQueryStringJoiner.toString());\n      }\n      localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath + '?' + queryJoiner.toString()));\n    } else {\n      localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath));\n    }\n    {{/hasQueryParams}}\n    {{^hasQueryParams}}\n    localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath));\n    {{/hasQueryParams}}\n\n    {{#headerParams}}\n    if ({{paramName}} != null) {\n      localVarRequestBuilder.header(\"{{baseName}}\", {{paramName}}.toString());\n    }\n    {{/headerParams}}\n    {{#bodyParam}}\n    localVarRequestBuilder.header(\"Content-Type\", \"{{#hasConsumes}}{{#consumes}}{{#-first}}{{mediaType}}{{/-first}}{{/consumes}}{{/hasConsumes}}{{#hasConsumes}}{{^consumes}}application/json{{/consumes}}{{/hasConsumes}}{{^hasConsumes}}application/json{{/hasConsumes}}\");\n    {{/bodyParam}}\n    String acceptHeaderValue = \"{{#hasProduces}}{{#produces}}{{mediaType}}{{^-last}}, {{/-last}}{{/produces}}{{/hasProduces}}{{#hasProduces}}{{^produces}}application/json{{/produces}}{{/hasProduces}}{{^hasProduces}}application/json{{/hasProduces}}\";\n    acceptHeaderValue += \",application/vnd.siesta-validation-errors-v1+json\";\n    acceptHeaderValue += \",application/vnd.concord-validation-errors-v1+json\";\n    localVarRequestBuilder.header(\"Accept\", acceptHeaderValue);\n    {{#bodyParam}}\n    {{#isString}}\n    localVarRequestBuilder.method(\"{{httpMethod}}\", HttpRequest.BodyPublishers.ofString({{paramName}}));\n    {{/isString}}\n    {{^isString}}\n    try {\n      localVarRequestBuilder.method(\"{{httpMethod}}\", RequestBodyHandler.handle(memberVarObjectMapper, {{paramName}}));\n    } catch (IOException e) {\n      throw new ApiException(e);\n    }\n    {{/isString}}\n    {{/bodyParam}}\n    {{^bodyParam}}\n\n    {{#isMultipart}}\n    HttpEntity entity = MultipartRequestBodyHandler.handle(memberVarObjectMapper, multipartInput);\n    localVarRequestBuilder\n        .header(\"Content-Type\", entity.contentType().toString())\n        .method(\"{{httpMethod}}\", HttpRequest.BodyPublishers.ofInputStream(() -> {\n            try {\n            return entity.getContent();\n            } catch (IOException e) {\n            throw new RuntimeException(e);\n            }\n            }));\n    {{/isMultipart}}\n    {{^isMultipart}}\n        {{#hasFormParams}}\n    List<NameValuePair> formValues = new ArrayList<>();\n    {{#formParams}}\n    {{#isArray}}\n    for (int i=0; i < {{paramName}}.size(); i++) {\n        if ({{paramName}}.get(i) != null) {\n            formValues.add(new NameValuePair(\"{{{baseName}}}\", {{paramName}}.get(i).toString()));\n        }\n    }\n    {{/isArray}}\n    {{^isArray}}\n    if ({{paramName}} != null) {\n        formValues.add(new NameValuePair(\"{{{baseName}}}\", {{paramName}}.toString()));\n    }\n    {{/isArray}}\n    {{/formParams}}\n    HttpEntity entity = new UrlEncodedFormEntity(formValues, java.nio.charset.StandardCharsets.UTF_8);\n    ByteArrayOutputStream formOutputStream = new ByteArrayOutputStream();\n    try {\n        entity.writeTo(formOutputStream);\n    } catch (IOException e) {\n        throw new RuntimeException(e);\n    }\n    localVarRequestBuilder\n        .header(\"Content-Type\", entity.getContentType().toString())\n        .method(\"{{httpMethod}}\", HttpRequest.BodyPublishers\n            .ofInputStream(() -> new ByteArrayInputStream(formOutputStream.toByteArray())));\n\n        {{/hasFormParams}}\n        {{^hasFormParams}}\n    localVarRequestBuilder.method(\"{{httpMethod}}\", HttpRequest.BodyPublishers.noBody());\n        {{/hasFormParams}}\n    {{/isMultipart}}\n\n\n    {{/bodyParam}}\n    if (memberVarReadTimeout != null) {\n      localVarRequestBuilder.timeout(memberVarReadTimeout);\n    }\n    if (memberVarInterceptor != null) {\n      memberVarInterceptor.accept(localVarRequestBuilder);\n    }\n    return localVarRequestBuilder;\n  }\n  {{#isMultipart}}\n  {{#hasParams}}\n\n  public static final class {{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request {\n    {{#allParams}}\n        {{#isFormParam}}\n    private {{{dataType}}} {{paramName}};\n        {{/isFormParam}}\n    {{/allParams}}\n\n    {{#allParams}}\n        {{#isFormParam}}\n    public {{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request {{paramName}}({{{dataType}}} {{paramName}}) {\n      this.{{paramName}} = {{paramName}};\n      return this;\n    }\n        {{/isFormParam}}\n    {{/allParams}}\n\n    public Map<String, Object> asMap() {\n        Map<String, Object> result = new HashMap<>();\n\n      {{#allParams}}\n          {{#isFormParam}}\n        if ({{paramName}} != null) {\n          result.put(\"{{baseName}}\", {{paramName}});\n        }\n          {{/isFormParam}}\n      {{/allParams}}\n\n        return result;\n    }\n  }\n\n  {{/hasParams}}\n  {{/isMultipart}}\n  {{/operation}}\n}\n{{/operations}}\n"
  },
  {
    "path": "client2/src/main/template/libraries/native/api.mustache.orig",
    "content": "{{>licenseInfo}}\npackage {{package}};\n\nimport {{invokerPackage}}.ApiClient;\nimport {{invokerPackage}}.ApiException;\nimport {{invokerPackage}}.ApiResponse;\nimport {{invokerPackage}}.Pair;\n\n{{#imports}}\nimport {{import}};\n{{/imports}}\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\n\n{{#hasFormParamsInSpec}}\nimport org.apache.http.HttpEntity;\nimport org.apache.http.NameValuePair;\nimport org.apache.http.entity.mime.MultipartEntityBuilder;\nimport org.apache.http.message.BasicNameValuePair;\nimport org.apache.http.client.entity.UrlEncodedFormEntity;\n\n{{/hasFormParamsInSpec}}\nimport java.io.InputStream;\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.net.http.HttpRequest;\nimport java.nio.channels.Channels;\nimport java.nio.channels.Pipe;\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\n\nimport java.util.ArrayList;\nimport java.util.StringJoiner;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.Consumer;\n{{#asyncNative}}\n\nimport java.util.concurrent.CompletableFuture;\n{{/asyncNative}}\n\n{{>generatedAnnotation}}\n{{#operations}}\npublic class {{classname}} {\n  private final HttpClient memberVarHttpClient;\n  private final ObjectMapper memberVarObjectMapper;\n  private final String memberVarBaseUri;\n  private final Consumer<HttpRequest.Builder> memberVarInterceptor;\n  private final Duration memberVarReadTimeout;\n  private final Consumer<HttpResponse<InputStream>> memberVarResponseInterceptor;\n  private final Consumer<HttpResponse<String>> memberVarAsyncResponseInterceptor;\n\n  public {{classname}}() {\n    this(new ApiClient());\n  }\n\n  public {{classname}}(ApiClient apiClient) {\n    memberVarHttpClient = apiClient.getHttpClient();\n    memberVarObjectMapper = apiClient.getObjectMapper();\n    memberVarBaseUri = apiClient.getBaseUri();\n    memberVarInterceptor = apiClient.getRequestInterceptor();\n    memberVarReadTimeout = apiClient.getReadTimeout();\n    memberVarResponseInterceptor = apiClient.getResponseInterceptor();\n    memberVarAsyncResponseInterceptor = apiClient.getAsyncResponseInterceptor();\n  }\n  {{#asyncNative}}\n\n  private ApiException getApiException(String operationId, HttpResponse<String> response) {\n    String message = formatExceptionMessage(operationId, response.statusCode(), response.body());\n    return new ApiException(response.statusCode(), message, response.headers(), response.body());\n  }\n  {{/asyncNative}}\n  {{^asyncNative}}\n\n  protected ApiException getApiException(String operationId, HttpResponse<InputStream> response) throws IOException {\n    String body = response.body() == null ? null : new String(response.body().readAllBytes());\n    String message = formatExceptionMessage(operationId, response.statusCode(), body);\n    return new ApiException(response.statusCode(), message, response.headers(), body);\n  }\n  {{/asyncNative}}\n\n  private String formatExceptionMessage(String operationId, int statusCode, String body) {\n    if (body == null || body.isEmpty()) {\n      body = \"[no body]\";\n    }\n    return operationId + \" call failed with: \" + statusCode + \" - \" + body;\n  }\n\n  {{#operation}}\n  {{#vendorExtensions.x-group-parameters}}\n  {{#hasParams}}\n  /**\n   * {{summary}}\n   * {{notes}}\n   * @param apiRequest {@link API{{operationId}}Request}\n   {{#returnType}}\n   * @return {{#asyncNative}}CompletableFuture&lt;{{/asyncNative}}{{returnType}}{{#asyncNative}}&gt;{{/asyncNative}}\n   {{/returnType}}\n   {{^returnType}}\n   {{#asyncNative}}\n   * @return CompletableFuture&lt;Void&gt;\n   {{/asyncNative}}\n   {{/returnType}}\n   * @throws ApiException if fails to make API call\n   {{#isDeprecated}}\n   * @deprecated\n   {{/isDeprecated}}\n   {{#externalDocs}}\n   * {{description}}\n   * @see <a href=\"{{url}}\">{{summary}} Documentation</a>\n   {{/externalDocs}}\n   */\n  {{#isDeprecated}}\n  @Deprecated\n  {{/isDeprecated}}\n  public {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}}{{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture<Void>{{/asyncNative}}{{^asyncNative}}void{{/asyncNative}}{{/returnType}} {{operationId}}(API{{operationId}}Request apiRequest) throws ApiException {\n    {{#allParams}}\n    {{{dataType}}} {{paramName}} = apiRequest.{{paramName}}();\n    {{/allParams}}\n    {{#returnType}}return {{/returnType}}{{^returnType}}{{#asyncNative}}return {{/asyncNative}}{{/returnType}}{{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}});\n  }\n\n  /**\n   * {{summary}}\n   * {{notes}}\n   * @param apiRequest {@link API{{operationId}}Request}\n   * @return {{#asyncNative}}CompletableFuture&lt;{{/asyncNative}}ApiResponse&lt;{{returnType}}{{^returnType}}Void{{/returnType}}&gt;{{#asyncNative}}&gt;{{/asyncNative}}\n   * @throws ApiException if fails to make API call\n   {{#isDeprecated}}\n   * @deprecated\n   {{/isDeprecated}}\n   {{#externalDocs}}\n   * {{description}}\n   * @see <a href=\"{{url}}\">{{summary}} Documentation</a>\n   {{/externalDocs}}\n   */\n  {{#isDeprecated}}\n  @Deprecated\n  {{/isDeprecated}}\n  public {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} {{operationId}}WithHttpInfo(API{{operationId}}Request apiRequest) throws ApiException {\n    {{#allParams}}\n    {{{dataType}}} {{paramName}} = apiRequest.{{paramName}}();\n    {{/allParams}}\n    return {{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}});\n  }\n\n  {{/hasParams}}\n  {{/vendorExtensions.x-group-parameters}}\n  /**\n   * {{summary}}\n   * {{notes}}\n   {{#allParams}}\n   * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{^isContainer}}{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/isContainer}}{{/required}}\n   {{/allParams}}\n   {{#returnType}}\n   * @return {{#asyncNative}}CompletableFuture&lt;{{/asyncNative}}{{returnType}}{{#asyncNative}}&gt;{{/asyncNative}}\n   {{/returnType}}\n   {{^returnType}}\n   {{#asyncNative}}\n   * @return CompletableFuture&lt;Void&gt;\n   {{/asyncNative}}\n   {{/returnType}}\n   * @throws ApiException if fails to make API call\n   {{#isDeprecated}}\n   * @deprecated\n   {{/isDeprecated}}\n   {{#externalDocs}}\n   * {{description}}\n   * @see <a href=\"{{url}}\">{{summary}} Documentation</a>\n   {{/externalDocs}}\n   */\n  {{#isDeprecated}}\n  @Deprecated\n  {{/isDeprecated}}\n  public {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}}{{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture<Void>{{/asyncNative}}{{^asyncNative}}void{{/asyncNative}}{{/returnType}} {{operationId}}({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws ApiException {\n    {{^asyncNative}}\n    {{#returnType}}ApiResponse<{{{.}}}> localVarResponse = {{/returnType}}{{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}});\n    {{#returnType}}\n    return localVarResponse.getData();\n    {{/returnType}}\n    {{/asyncNative}}\n    {{#asyncNative}}\n    try {\n      HttpRequest.Builder localVarRequestBuilder = {{operationId}}RequestBuilder({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}});\n      return memberVarHttpClient.sendAsync(\n          localVarRequestBuilder.build(),\n          HttpResponse.BodyHandlers.ofString()).thenComposeAsync(localVarResponse -> {\n            if (localVarResponse.statusCode()/ 100 != 2) {\n              return CompletableFuture.failedFuture(getApiException(\"{{operationId}}\", localVarResponse));\n            }\n            {{#returnType}}\n            try {\n              String responseBody = localVarResponse.body();\n              return CompletableFuture.completedFuture(\n                  responseBody == null || responseBody.isBlank() ? null : memberVarObjectMapper.readValue(responseBody, new TypeReference<{{{returnType}}}>() {})\n              );\n            } catch (IOException e) {\n              return CompletableFuture.failedFuture(new ApiException(e));\n            }\n            {{/returnType}}\n            {{^returnType}}\n            return CompletableFuture.completedFuture(null);\n            {{/returnType}}\n      });\n    }\n    catch (ApiException e) {\n      return CompletableFuture.failedFuture(e);\n    }\n    {{/asyncNative}}\n  }\n\n  /**\n   * {{summary}}\n   * {{notes}}\n   {{#allParams}}\n   * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{^isContainer}}{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/isContainer}}{{/required}}\n   {{/allParams}}\n   * @return {{#asyncNative}}CompletableFuture&lt;{{/asyncNative}}ApiResponse&lt;{{returnType}}{{^returnType}}Void{{/returnType}}&gt;{{#asyncNative}}&gt;{{/asyncNative}}\n   * @throws ApiException if fails to make API call\n   {{#isDeprecated}}\n   * @deprecated\n   {{/isDeprecated}}\n   {{#externalDocs}}\n   * {{description}}\n   * @see <a href=\"{{url}}\">{{summary}} Documentation</a>\n   {{/externalDocs}}\n   */\n  {{#isDeprecated}}\n  @Deprecated\n  {{/isDeprecated}}\n  public {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} {{operationId}}WithHttpInfo({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws ApiException {\n    {{^asyncNative}}\n    HttpRequest.Builder localVarRequestBuilder = {{operationId}}RequestBuilder({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}});\n    try {\n      HttpResponse<InputStream> localVarResponse = memberVarHttpClient.send(\n          localVarRequestBuilder.build(),\n          HttpResponse.BodyHandlers.ofInputStream());\n      if (memberVarResponseInterceptor != null) {\n        memberVarResponseInterceptor.accept(localVarResponse);\n      }\n      try {\n        if (localVarResponse.statusCode()/ 100 != 2) {\n          throw getApiException(\"{{operationId}}\", localVarResponse);\n        }\n        {{#vendorExtensions.x-java-text-plain-string}}\n        // for plain text response\n        if (localVarResponse.headers().map().containsKey(\"Content-Type\") &&\n                \"text/plain\".equalsIgnoreCase(localVarResponse.headers().map().get(\"Content-Type\").get(0).split(\";\")[0].trim())) {\n          java.util.Scanner s = new java.util.Scanner(localVarResponse.body()).useDelimiter(\"\\\\A\");\n          String responseBodyText = s.hasNext() ? s.next() : \"\";\n          return new ApiResponse<String>(\n                  localVarResponse.statusCode(),\n                  localVarResponse.headers().map(),\n                  responseBodyText\n          );\n        } else {\n            throw new RuntimeException(\"Error! The response Content-Type is supposed to be `text/plain` but it's not: \" + localVarResponse);\n        }\n        {{/vendorExtensions.x-java-text-plain-string}}\n        {{^vendorExtensions.x-java-text-plain-string}}\n        return new ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>(\n          localVarResponse.statusCode(),\n          localVarResponse.headers().map(),\n          {{#returnType}}\n          localVarResponse.body() == null ? null : memberVarObjectMapper.readValue(localVarResponse.body(), new TypeReference<{{{returnType}}}>() {}) // closes the InputStream\n          {{/returnType}}\n          {{^returnType}}\n          null\n          {{/returnType}}\n        );\n        {{/vendorExtensions.x-java-text-plain-string}}\n      } finally {\n        {{^returnType}}\n        // Drain the InputStream\n        while (localVarResponse.body().read() != -1) {\n            // Ignore\n        }\n        localVarResponse.body().close();\n        {{/returnType}}\n      }\n    } catch (IOException e) {\n      throw new ApiException(e);\n    }\n    catch (InterruptedException e) {\n      Thread.currentThread().interrupt();\n      throw new ApiException(e);\n    }\n    {{/asyncNative}}\n    {{#asyncNative}}\n    try {\n      HttpRequest.Builder localVarRequestBuilder = {{operationId}}RequestBuilder({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}});\n      return memberVarHttpClient.sendAsync(\n          localVarRequestBuilder.build(),\n          HttpResponse.BodyHandlers.ofString()).thenComposeAsync(localVarResponse -> {\n            if (memberVarAsyncResponseInterceptor != null) {\n              memberVarAsyncResponseInterceptor.accept(localVarResponse);\n            }\n            if (localVarResponse.statusCode()/ 100 != 2) {\n              return CompletableFuture.failedFuture(getApiException(\"{{operationId}}\", localVarResponse));\n            }\n            {{#returnType}}\n            try {\n              String responseBody = localVarResponse.body();\n              return CompletableFuture.completedFuture(\n                  new ApiResponse<{{{returnType}}}>(\n                      localVarResponse.statusCode(),\n                      localVarResponse.headers().map(),\n                      responseBody == null || responseBody.isBlank() ? null : memberVarObjectMapper.readValue(responseBody, new TypeReference<{{{returnType}}}>() {}))\n              );\n            } catch (IOException e) {\n              return CompletableFuture.failedFuture(new ApiException(e));\n            }\n            {{/returnType}}\n            {{^returnType}}\n            return CompletableFuture.completedFuture(\n                new ApiResponse<Void>(localVarResponse.statusCode(), localVarResponse.headers().map(), null)\n            );\n            {{/returnType}}\n        }\n      );\n    }\n    catch (ApiException e) {\n      return CompletableFuture.failedFuture(e);\n    }\n    {{/asyncNative}}\n  }\n\n  private HttpRequest.Builder {{operationId}}RequestBuilder({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws ApiException {\n    {{#allParams}}\n    {{#required}}\n    // verify the required parameter '{{paramName}}' is set\n    if ({{paramName}} == null) {\n      throw new ApiException(400, \"Missing the required parameter '{{paramName}}' when calling {{operationId}}\");\n    }\n    {{/required}}\n    {{/allParams}}\n\n    HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder();\n\n    {{! Switch delimiters for baseName so we can write constants like \"{query}\" }}\n    String localVarPath = \"{{{path}}}\"{{#pathParams}}\n        .replace({{=<% %>=}}\"{<%baseName%>}\"<%={{ }}=%>, ApiClient.urlEncode({{{paramName}}}.toString())){{/pathParams}};\n\n    {{#hasQueryParams}}\n    List<Pair> localVarQueryParams = new ArrayList<>();\n    StringJoiner localVarQueryStringJoiner = new StringJoiner(\"&\");\n    String localVarQueryParameterBaseName;\n    {{#queryParams}}\n    localVarQueryParameterBaseName = \"{{{baseName}}}\";\n      {{#collectionFormat}}\n    localVarQueryParams.addAll(ApiClient.parameterToPairs(\"{{{collectionFormat}}}\", \"{{baseName}}\", {{paramName}}));\n      {{/collectionFormat}}\n      {{^collectionFormat}}\n        {{#isDeepObject}}\n    if ({{paramName}} != null) {\n            {{#isArray}}\n      for (int i=0; i < {{paramName}}.size(); i++) {\n        localVarQueryStringJoiner.add({{paramName}}.get(i).toUrlQueryString(String.format(\"{{baseName}}[%d]\", i)));\n      }\n            {{/isArray}}\n            {{^isArray}}\n      localVarQueryStringJoiner.add({{paramName}}.toUrlQueryString(\"{{baseName}}\"));\n            {{/isArray}}\n    }\n        {{/isDeepObject}}\n        {{^isDeepObject}}\n            {{#isExplode}}\n                {{#hasVars}}\n                    {{#vars}}\n                        {{#isArray}}\n    localVarQueryParams.addAll(ApiClient.parameterToPairs(\"multi\", \"{{baseName}}\", {{paramName}}.{{getter}}()));\n                        {{/isArray}}\n                        {{^isArray}}\n    localVarQueryParams.addAll(ApiClient.parameterToPairs(\"{{baseName}}\", {{paramName}}.{{getter}}()));\n                        {{/isArray}}\n                    {{/vars}}\n                {{/hasVars}}\n                {{^hasVars}}\n                {{#isModel}}\n    localVarQueryStringJoiner.add({{paramName}}.toUrlQueryString());\n                {{/isModel}}\n                {{^isModel}}\n    localVarQueryParams.addAll(ApiClient.parameterToPairs(\"{{baseName}}\", {{paramName}}));\n                {{/isModel}}\n                {{/hasVars}}\n            {{/isExplode}}\n            {{^isExplode}}\n    localVarQueryParams.addAll(ApiClient.parameterToPairs(\"{{baseName}}\", {{paramName}}));\n            {{/isExplode}}\n        {{/isDeepObject}}\n      {{/collectionFormat}}\n    {{/queryParams}}\n\n    if (!localVarQueryParams.isEmpty() || localVarQueryStringJoiner.length() != 0) {\n      StringJoiner queryJoiner = new StringJoiner(\"&\");\n      localVarQueryParams.forEach(p -> queryJoiner.add(p.getName() + '=' + p.getValue()));\n      if (localVarQueryStringJoiner.length() != 0) {\n        queryJoiner.add(localVarQueryStringJoiner.toString());\n      }\n      localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath + '?' + queryJoiner.toString()));\n    } else {\n      localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath));\n    }\n    {{/hasQueryParams}}\n    {{^hasQueryParams}}\n    localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath));\n    {{/hasQueryParams}}\n\n    {{#headerParams}}\n    if ({{paramName}} != null) {\n      localVarRequestBuilder.header(\"{{baseName}}\", {{paramName}}.toString());\n    }\n    {{/headerParams}}\n    {{#bodyParam}}\n    localVarRequestBuilder.header(\"Content-Type\", \"{{#hasConsumes}}{{#consumes}}{{#-first}}{{mediaType}}{{/-first}}{{/consumes}}{{/hasConsumes}}{{#hasConsumes}}{{^consumes}}application/json{{/consumes}}{{/hasConsumes}}{{^hasConsumes}}application/json{{/hasConsumes}}\");\n    {{/bodyParam}}\n    localVarRequestBuilder.header(\"Accept\", \"{{#hasProduces}}{{#produces}}{{mediaType}}{{^-last}}, {{/-last}}{{/produces}}{{/hasProduces}}{{#hasProduces}}{{^produces}}application/json{{/produces}}{{/hasProduces}}{{^hasProduces}}application/json{{/hasProduces}}\");\n\n    {{#bodyParam}}\n    {{#isString}}\n    localVarRequestBuilder.method(\"{{httpMethod}}\", HttpRequest.BodyPublishers.ofString({{paramName}}));\n    {{/isString}}\n    {{^isString}}\n    try {\n      byte[] localVarPostBody = memberVarObjectMapper.writeValueAsBytes({{paramName}});\n      localVarRequestBuilder.method(\"{{httpMethod}}\", HttpRequest.BodyPublishers.ofByteArray(localVarPostBody));\n    } catch (IOException e) {\n      throw new ApiException(e);\n    }\n    {{/isString}}\n    {{/bodyParam}}\n    {{^bodyParam}}\n    {{#hasFormParams}}\n    {{#isMultipart}}\n    MultipartEntityBuilder multiPartBuilder = MultipartEntityBuilder.create();\n    boolean hasFiles = false;\n    {{#formParams}}\n    {{#isArray}}\n    for (int i=0; i < {{paramName}}.size(); i++) {\n        {{#isFile}}\n        multiPartBuilder.addBinaryBody(\"{{{baseName}}}\", {{paramName}}.get(i));\n        hasFiles = true;\n        {{/isFile}}\n        {{^isFile}}\n        multiPartBuilder.addTextBody(\"{{{baseName}}}\", {{paramName}}.get(i).toString());\n        {{/isFile}}\n    }\n    {{/isArray}}\n    {{^isArray}}\n    {{#isFile}}\n    multiPartBuilder.addBinaryBody(\"{{{baseName}}}\", {{paramName}});\n    hasFiles = true;\n    {{/isFile}}\n    {{^isFile}}\n    multiPartBuilder.addTextBody(\"{{{baseName}}}\", {{paramName}}.toString());\n    {{/isFile}}\n    {{/isArray}}\n    {{/formParams}}\n    HttpEntity entity = multiPartBuilder.build();\n    HttpRequest.BodyPublisher formDataPublisher;\n    if (hasFiles) {\n        Pipe pipe;\n        try {\n            pipe = Pipe.open();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n        new Thread(() -> {\n            try (OutputStream outputStream = Channels.newOutputStream(pipe.sink())) {\n                entity.writeTo(outputStream);\n            } catch (IOException e) {\n                e.printStackTrace();\n            }\n        }).start();\n        formDataPublisher = HttpRequest.BodyPublishers.ofInputStream(() -> Channels.newInputStream(pipe.source()));\n    } else {\n        ByteArrayOutputStream formOutputStream = new ByteArrayOutputStream();\n        try {\n            entity.writeTo(formOutputStream);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n        formDataPublisher = HttpRequest.BodyPublishers\n            .ofInputStream(() -> new ByteArrayInputStream(formOutputStream.toByteArray()));\n    }\n    localVarRequestBuilder\n        .header(\"Content-Type\", entity.getContentType().getValue())\n        .method(\"{{httpMethod}}\", formDataPublisher);\n    {{/isMultipart}}\n    {{^isMultipart}}\n    List<NameValuePair> formValues = new ArrayList<>();\n    {{#formParams}}\n    {{#isArray}}\n    for (int i=0; i < {{paramName}}.size(); i++) {\n        if ({{paramName}}.get(i) != null) {\n            formValues.add(new BasicNameValuePair(\"{{{baseName}}}\", {{paramName}}.get(i).toString()));\n        }\n    }\n    {{/isArray}}\n    {{^isArray}}\n    if ({{paramName}} != null) {\n        formValues.add(new BasicNameValuePair(\"{{{baseName}}}\", {{paramName}}.toString()));\n    }\n    {{/isArray}}\n    {{/formParams}}\n    HttpEntity entity = new UrlEncodedFormEntity(formValues, java.nio.charset.StandardCharsets.UTF_8);\n    ByteArrayOutputStream formOutputStream = new ByteArrayOutputStream();\n    try {\n        entity.writeTo(formOutputStream);\n    } catch (IOException e) {\n        throw new RuntimeException(e);\n    }\n    localVarRequestBuilder\n        .header(\"Content-Type\", entity.getContentType().getValue())\n        .method(\"{{httpMethod}}\", HttpRequest.BodyPublishers\n            .ofInputStream(() -> new ByteArrayInputStream(formOutputStream.toByteArray())));\n    {{/isMultipart}}\n    {{/hasFormParams}}\n    {{^hasFormParams}}\n    localVarRequestBuilder.method(\"{{httpMethod}}\", HttpRequest.BodyPublishers.noBody());\n    {{/hasFormParams}}\n    {{/bodyParam}}\n    if (memberVarReadTimeout != null) {\n      localVarRequestBuilder.timeout(memberVarReadTimeout);\n    }\n    if (memberVarInterceptor != null) {\n      memberVarInterceptor.accept(localVarRequestBuilder);\n    }\n    return localVarRequestBuilder;\n  }\n  {{#vendorExtensions.x-group-parameters}}\n  {{#hasParams}}\n\n  public static final class API{{operationId}}Request {\n    {{#requiredParams}}\n    private {{{dataType}}} {{paramName}}; // {{description}} (required)\n    {{/requiredParams}}\n    {{#optionalParams}}\n    private {{{dataType}}} {{paramName}}; // {{description}} (optional{{^isContainer}}{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/isContainer}}\n    {{/optionalParams}}\n\n    private API{{operationId}}Request(Builder builder) {\n      {{#requiredParams}}\n      this.{{paramName}} = builder.{{paramName}};\n      {{/requiredParams}}\n      {{#optionalParams}}\n      this.{{paramName}} = builder.{{paramName}};\n      {{/optionalParams}}\n    }\n    {{#allParams}}\n    public {{{dataType}}} {{paramName}}() {\n      return {{paramName}};\n    }\n    {{/allParams}}\n    public static Builder newBuilder() {\n      return new Builder();\n    }\n\n    public static class Builder {\n      {{#requiredParams}}\n      private {{{dataType}}} {{paramName}};\n      {{/requiredParams}}\n      {{#optionalParams}}\n      private {{{dataType}}} {{paramName}};\n      {{/optionalParams}}\n\n      {{#allParams}}\n      public Builder {{paramName}}({{{dataType}}} {{paramName}}) {\n        this.{{paramName}} = {{paramName}};\n        return this;\n      }\n      {{/allParams}}\n      public API{{operationId}}Request build() {\n        return new API{{operationId}}Request(this);\n      }\n    }\n  }\n\n  {{/hasParams}}\n  {{/vendorExtensions.x-group-parameters}}\n  {{/operation}}\n}\n{{/operations}}\n"
  },
  {
    "path": "client2/src/main/template/libraries/native/pojo.mustache",
    "content": "{{#discriminator}}\nimport {{invokerPackage}}.JSON;\n{{/discriminator}}\n/**\n * {{description}}{{^description}}{{classname}}{{/description}}{{#isDeprecated}}\n * @deprecated{{/isDeprecated}}\n */{{#isDeprecated}}\n@Deprecated{{/isDeprecated}}\n{{#swagger1AnnotationLibrary}}\n{{#description}}\n@ApiModel(description = \"{{{.}}}\")\n{{/description}}\n{{/swagger1AnnotationLibrary}}\n{{#swagger2AnnotationLibrary}}\n{{#description}}\n@Schema(description = \"{{{.}}}\")\n{{/description}}\n{{/swagger2AnnotationLibrary}}\n{{#jackson}}\n@JsonPropertyOrder({\n{{#vars}}\n  {{classname}}.JSON_PROPERTY_{{nameInSnakeCase}}{{^-last}},{{/-last}}\n{{/vars}}\n})\n{{/jackson}}\n{{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{>xmlAnnotation}}\n{{#vendorExtensions.x-class-extra-annotation}}\n{{{vendorExtensions.x-class-extra-annotation}}}\n{{/vendorExtensions.x-class-extra-annotation}}\npublic class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{#-last}} {{/-last}}{{/vendorExtensions.x-implements}}{\n{{#serializableModel}}\n  private static final long serialVersionUID = 1L;\n\n{{/serializableModel}}\n  {{#vars}}\n    {{#isEnum}}\n    {{^isContainer}}\n    {{^vendorExtensions.x-enum-as-string}}\n{{>modelInnerEnum}}\n    {{/vendorExtensions.x-enum-as-string}}\n    {{/isContainer}}\n    {{#isContainer}}\n    {{#mostInnerItems}}\n{{>modelInnerEnum}}\n    {{/mostInnerItems}}\n    {{/isContainer}}\n    {{/isEnum}}\n  {{#gson}}\n  public static final String SERIALIZED_NAME_{{nameInSnakeCase}} = \"{{baseName}}\";\n  {{/gson}}\n  {{#jackson}}\n  public static final String JSON_PROPERTY_{{nameInSnakeCase}} = \"{{baseName}}\";\n  {{/jackson}}\n  {{#withXml}}\n  {{#isXmlAttribute}}\n  @XmlAttribute(name = \"{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}\")\n  {{/isXmlAttribute}}\n  {{^isXmlAttribute}}\n    {{^isContainer}}\n  @XmlElement({{#xmlNamespace}}namespace=\"{{.}}\", {{/xmlNamespace}}name = \"{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}\")\n    {{/isContainer}}\n    {{#isContainer}}\n  // Is a container wrapped={{isXmlWrapped}}\n      {{#items}}\n  // items.name={{name}} items.baseName={{baseName}} items.xmlName={{xmlName}} items.xmlNamespace={{xmlNamespace}}\n  // items.example={{example}} items.type={{dataType}}\n  @XmlElement({{#xmlNamespace}}namespace=\"{{.}}\", {{/xmlNamespace}}name = \"{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}\")\n      {{/items}}\n      {{#isXmlWrapped}}\n  @XmlElementWrapper({{#xmlNamespace}}namespace=\"{{.}}\", {{/xmlNamespace}}name = \"{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}\")\n      {{/isXmlWrapped}}\n    {{/isContainer}}\n  {{/isXmlAttribute}}\n  {{/withXml}}\n  {{#gson}}\n  @SerializedName(SERIALIZED_NAME_{{nameInSnakeCase}})\n  {{/gson}}\n  {{#vendorExtensions.x-field-extra-annotation}}\n  {{{vendorExtensions.x-field-extra-annotation}}}\n  {{/vendorExtensions.x-field-extra-annotation}}\n  {{#vendorExtensions.x-is-jackson-optional-nullable}}\n  {{#isContainer}}\n  private JsonNullable<{{{datatypeWithEnum}}}> {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>undefined();\n  {{/isContainer}}\n  {{^isContainer}}\n  private JsonNullable<{{{datatypeWithEnum}}}> {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>{{#defaultValue}}of({{{.}}}){{/defaultValue}}{{^defaultValue}}undefined(){{/defaultValue}};\n  {{/isContainer}}\n  {{/vendorExtensions.x-is-jackson-optional-nullable}}\n  {{^vendorExtensions.x-is-jackson-optional-nullable}}\n  private {{{datatypeWithEnum}}} {{name}};\n  {{/vendorExtensions.x-is-jackson-optional-nullable}}\n\n  {{/vars}}\n  public {{classname}}() { {{#parent}}{{#parcelableModel}}\n    super();{{/parcelableModel}}{{/parent}}{{#gson}}{{#discriminator}}\n    this.{{{discriminatorName}}} = this.getClass().getSimpleName();{{/discriminator}}{{/gson}}\n  }{{#vendorExtensions.x-has-readonly-properties}}{{^withXml}}\n\n  {{#jackson}}@JsonCreator{{/jackson}}\n  public {{classname}}(\n  {{#readOnlyVars}}\n    @JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}}) {{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}}\n  {{/readOnlyVars}}\n  ) {\n  this();\n  {{#readOnlyVars}}\n    this.{{name}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}};\n  {{/readOnlyVars}}\n  }{{/withXml}}{{/vendorExtensions.x-has-readonly-properties}}\n  {{#vars}}\n\n  {{^isReadOnly}}\n  {{#vendorExtensions.x-enum-as-string}}\n  public static final Set<String> {{{nameInSnakeCase}}}_VALUES = new HashSet<>(Arrays.asList(\n    {{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}\n  ));\n\n  {{/vendorExtensions.x-enum-as-string}}\n  public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) {\n    {{#vendorExtensions.x-enum-as-string}}\n    if (!{{{nameInSnakeCase}}}_VALUES.contains({{name}})) {\n      throw new IllegalArgumentException({{name}} + \" is invalid. Possible values for {{name}}: \" + String.join(\", \", {{{nameInSnakeCase}}}_VALUES));\n    }\n\n    {{/vendorExtensions.x-enum-as-string}}\n    {{#vendorExtensions.x-is-jackson-optional-nullable}}\n    this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{name}});\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n    {{^vendorExtensions.x-is-jackson-optional-nullable}}\n    this.{{name}} = {{name}};\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n    return this;\n  }\n  {{#isArray}}\n\n  public {{classname}} add{{nameInCamelCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) {\n    {{#vendorExtensions.x-is-jackson-optional-nullable}}\n    if (this.{{name}} == null || !this.{{name}}.isPresent()) {\n      this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}});\n    }\n    try {\n      this.{{name}}.get().add({{name}}Item);\n    } catch (java.util.NoSuchElementException e) {\n      // this can never happen, as we make sure above that the value is present\n    }\n    return this;\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n    {{^vendorExtensions.x-is-jackson-optional-nullable}}\n    if (this.{{name}} == null) {\n      this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}};\n    }\n    this.{{name}}.add({{name}}Item);\n    return this;\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n  }\n  {{/isArray}}\n  {{#isMap}}\n\n  public {{classname}} put{{nameInCamelCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) {\n    {{#vendorExtensions.x-is-jackson-optional-nullable}}\n    if (this.{{name}} == null || !this.{{name}}.isPresent()) {\n      this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}});\n    }\n    try {\n      this.{{name}}.get().put(key, {{name}}Item);\n    } catch (java.util.NoSuchElementException e) {\n      // this can never happen, as we make sure above that the value is present\n    }\n    return this;\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n    {{^vendorExtensions.x-is-jackson-optional-nullable}}\n    if (this.{{name}} == null) {\n      this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}};\n    }\n    this.{{name}}.put(key, {{name}}Item);\n    return this;\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n  }\n  {{/isMap}}\n\n  {{/isReadOnly}}\n   /**\n  {{#description}}\n   * {{.}}\n  {{/description}}\n  {{^description}}\n   * Get {{name}}\n  {{/description}}\n  {{#minimum}}\n   * minimum: {{.}}\n  {{/minimum}}\n  {{#maximum}}\n   * maximum: {{.}}\n  {{/maximum}}\n   * @return {{name}}\n   {{#deprecated}}\n   * @deprecated\n   {{/deprecated}}\n  **/\n{{#deprecated}}\n  @Deprecated\n{{/deprecated}}\n// {{#required}}\n// {{#isNullable}}\n//   @{{javaxPackage}}.annotation.Nullable\n// {{/isNullable}}\n// {{^isNullable}}\n//   @{{javaxPackage}}.annotation.Nonnull\n// {{/isNullable}}\n// {{/required}}\n// {{^required}}\n//   @{{javaxPackage}}.annotation.Nullable\n// {{/required}}\n{{#useBeanValidation}}\n{{>beanValidation}}\n{{/useBeanValidation}}\n{{#swagger1AnnotationLibrary}}\n  @ApiModelProperty({{#example}}example = \"{{{.}}}\", {{/example}}{{#required}}required = {{required}}, {{/required}}value = \"{{{description}}}\")\n{{/swagger1AnnotationLibrary}}\n{{#swagger2AnnotationLibrary}}\n  @Schema({{#example}}example = \"{{{.}}}\", {{/example}}requiredMode = {{#required}}Schema.RequiredMode.REQUIRED{{/required}}{{^required}}Schema.RequiredMode.NOT_REQUIRED{{/required}}, description = \"{{{description}}}\")\n{{/swagger2AnnotationLibrary}}\n{{#vendorExtensions.x-extra-annotation}}\n  {{{vendorExtensions.x-extra-annotation}}}\n{{/vendorExtensions.x-extra-annotation}}\n{{#vendorExtensions.x-is-jackson-optional-nullable}}\n  {{!unannotated, Jackson would pick this up automatically and add it *in addition* to the _JsonNullable getter field}}\n  @JsonIgnore\n{{/vendorExtensions.x-is-jackson-optional-nullable}}\n{{^vendorExtensions.x-is-jackson-optional-nullable}}{{#jackson}}{{> jackson_annotations}}{{/jackson}}{{/vendorExtensions.x-is-jackson-optional-nullable}}\n  public {{{datatypeWithEnum}}} {{getter}}() {\n    {{#vendorExtensions.x-is-jackson-optional-nullable}}\n    {{#isReadOnly}}{{! A readonly attribute doesn't have setter => jackson will set null directly if explicitly returned by API, so make sure we have an empty JsonNullable}}\n    if ({{name}} == null) {\n      {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>{{#defaultValue}}of({{{.}}}){{/defaultValue}}{{^defaultValue}}undefined(){{/defaultValue}};\n    }\n    {{/isReadOnly}}\n    return {{name}}.orElse(null);\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n    {{^vendorExtensions.x-is-jackson-optional-nullable}}\n    return {{name}};\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n  }\n\n  {{#vendorExtensions.x-is-jackson-optional-nullable}}\n{{> jackson_annotations}}\n  public JsonNullable<{{{datatypeWithEnum}}}> {{getter}}_JsonNullable() {\n    return {{name}};\n  }\n  {{/vendorExtensions.x-is-jackson-optional-nullable}}{{#vendorExtensions.x-is-jackson-optional-nullable}}\n  @JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}})\n  {{#isReadOnly}}private{{/isReadOnly}}{{^isReadOnly}}public{{/isReadOnly}} void {{setter}}_JsonNullable(JsonNullable<{{{datatypeWithEnum}}}> {{name}}) {\n    {{! For getters/setters that have name differing from attribute name, we must include setter (albeit private) for jackson to be able to set the attribute}}\n    this.{{name}} = {{name}};\n  }\n  {{/vendorExtensions.x-is-jackson-optional-nullable}}\n\n  {{^isReadOnly}}\n{{#vendorExtensions.x-setter-extra-annotation}}  {{{vendorExtensions.x-setter-extra-annotation}}}\n{{/vendorExtensions.x-setter-extra-annotation}}{{#jackson}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{> jackson_annotations}}{{/vendorExtensions.x-is-jackson-optional-nullable}}{{/jackson}}  public void {{setter}}({{{datatypeWithEnum}}} {{name}}) {\n    {{#vendorExtensions.x-enum-as-string}}\n    if (!{{{nameInSnakeCase}}}_VALUES.contains({{name}})) {\n      throw new IllegalArgumentException({{name}} + \" is invalid. Possible values for {{name}}: \" + String.join(\", \", {{{nameInSnakeCase}}}_VALUES));\n    }\n\n    {{/vendorExtensions.x-enum-as-string}}\n    {{#vendorExtensions.x-is-jackson-optional-nullable}}\n    this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{name}});\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n    {{^vendorExtensions.x-is-jackson-optional-nullable}}\n    this.{{name}} = {{name}};\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n  }\n  {{/isReadOnly}}\n\n  {{/vars}}\n{{>libraries/native/additional_properties}}\n  {{#parent}}\n  {{#allVars}}\n  {{#isOverridden}}\n  @Override\n  public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) {\n    {{#vendorExtensions.x-is-jackson-optional-nullable}}\n    this.{{setter}}(JsonNullable.<{{{datatypeWithEnum}}}>of({{name}}));\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n    {{^vendorExtensions.x-is-jackson-optional-nullable}}\n    this.{{setter}}({{name}});\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n    return this;\n  }\n\n  {{/isOverridden}}\n  {{/allVars}}\n  {{/parent}}\n  /**\n   * Return true if this {{name}} object is equal to o.\n   */\n  @Override\n  public boolean equals(Object o) {\n  {{#useReflectionEqualsHashCode}}\n    return EqualsBuilder.reflectionEquals(this, o, false, null, true);\n  {{/useReflectionEqualsHashCode}}\n  {{^useReflectionEqualsHashCode}}\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }{{#hasVars}}\n    {{classname}} {{classVarName}} = ({{classname}}) o;\n    return {{#vars}}{{#vendorExtensions.x-is-jackson-optional-nullable}}equalsNullable(this.{{name}}, {{classVarName}}.{{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{#isByteArray}}Arrays{{/isByteArray}}{{^isByteArray}}Objects{{/isByteArray}}.equals(this.{{name}}, {{classVarName}}.{{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^-last}} &&\n        {{/-last}}{{/vars}}{{#additionalPropertiesType}}&&\n        Objects.equals(this.additionalProperties, {{classVarName}}.additionalProperties){{/additionalPropertiesType}}{{#parent}} &&\n        super.equals(o){{/parent}};{{/hasVars}}{{^hasVars}}\n    return {{#parent}}super.equals(o){{/parent}}{{^parent}}true{{/parent}};{{/hasVars}}\n  {{/useReflectionEqualsHashCode}}\n  }{{#vendorExtensions.x-jackson-optional-nullable-helpers}}\n\n  private static <T> boolean equalsNullable(JsonNullable<T> a, JsonNullable<T> b) {\n    return a == b || (a != null && b != null && a.isPresent() && b.isPresent() && Objects.deepEquals(a.get(), b.get()));\n  }{{/vendorExtensions.x-jackson-optional-nullable-helpers}}\n\n  @Override\n  public int hashCode() {\n  {{#useReflectionEqualsHashCode}}\n    return HashCodeBuilder.reflectionHashCode(this);\n  {{/useReflectionEqualsHashCode}}\n  {{^useReflectionEqualsHashCode}}\n    return Objects.hash({{#vars}}{{#vendorExtensions.x-is-jackson-optional-nullable}}hashCodeNullable({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{^isByteArray}}{{name}}{{/isByteArray}}{{#isByteArray}}Arrays.hashCode({{name}}){{/isByteArray}}{{/vendorExtensions.x-is-jackson-optional-nullable}}{{^-last}}, {{/-last}}{{/vars}}{{#parent}}{{#hasVars}}, {{/hasVars}}super.hashCode(){{/parent}}{{#additionalPropertiesType}}, additionalProperties{{/additionalPropertiesType}});\n  {{/useReflectionEqualsHashCode}}\n  }{{#vendorExtensions.x-jackson-optional-nullable-helpers}}\n\n  private static <T> int hashCodeNullable(JsonNullable<T> a) {\n    if (a == null) {\n      return 1;\n    }\n    return a.isPresent() ? Arrays.deepHashCode(new Object[]{a.get()}) : 31;\n  }{{/vendorExtensions.x-jackson-optional-nullable-helpers}}\n\n  @Override\n  public String toString() {\n    StringBuilder sb = new StringBuilder();\n    sb.append(\"class {{classname}} {\\n\");\n    {{#parent}}\n    sb.append(\"    \").append(toIndentedString(super.toString())).append(\"\\n\");\n    {{/parent}}\n    {{#vars}}\n    sb.append(\"    {{name}}: \").append(toIndentedString({{name}})).append(\"\\n\");\n    {{/vars}}\n    {{#additionalPropertiesType}}\n    sb.append(\"    additionalProperties: \").append(toIndentedString(additionalProperties)).append(\"\\n\");\n    {{/additionalPropertiesType}}\n    sb.append(\"}\");\n    return sb.toString();\n  }\n\n  /**\n   * Convert the given object to string with each line indented by 4 spaces\n   * (except the first line).\n   */\n  private String toIndentedString(Object o) {\n    if (o == null) {\n      return \"null\";\n    }\n    return o.toString().replace(\"\\n\", \"\\n    \");\n  }\n{{#supportUrlQuery}}\n\n  /**\n   * Convert the instance into URL query string.\n   *\n   * @return URL query string\n   */\n  public String toUrlQueryString() {\n    return toUrlQueryString(null);\n  }\n\n  /**\n   * Convert the instance into URL query string.\n   *\n   * @param prefix prefix of the query string\n   * @return URL query string\n   */\n  public String toUrlQueryString(String prefix) {\n    String suffix = \"\";\n    String containerSuffix = \"\";\n    String containerPrefix = \"\";\n    if (prefix == null) {\n      // style=form, explode=true, e.g. /pet?name=cat&type=manx\n      prefix = \"\";\n    } else {\n      // deepObject style e.g. /pet?id[name]=cat&id[type]=manx\n      prefix = prefix + \"[\";\n      suffix = \"]\";\n      containerSuffix = \"]\";\n      containerPrefix = \"[\";\n    }\n\n    StringJoiner joiner = new StringJoiner(\"&\");\n\n    {{#allVars}}\n    // add `{{baseName}}` to the URL query string\n    {{#isArray}}\n    {{#items.isPrimitiveType}}\n    {{#uniqueItems}}\n    if ({{getter}}() != null) {\n      int i = 0;\n      for ({{{items.dataType}}} _item : {{getter}}()) {\n        joiner.add(String.format(\"%s{{baseName}}%s%s=%s\", prefix, suffix,\n            \"\".equals(suffix) ? \"\" : String.format(\"%s%d%s\", containerPrefix, i, containerSuffix),\n            URLEncoder.encode(String.valueOf(_item), StandardCharsets.UTF_8).replaceAll(\"\\\\+\", \"%20\")));\n      }\n      i++;\n    }\n    {{/uniqueItems}}\n    {{^uniqueItems}}\n    if ({{getter}}() != null) {\n      for (int i = 0; i < {{getter}}().size(); i++) {\n        joiner.add(String.format(\"%s{{baseName}}%s%s=%s\", prefix, suffix,\n            \"\".equals(suffix) ? \"\" : String.format(\"%s%d%s\", containerPrefix, i, containerSuffix),\n            URLEncoder.encode(String.valueOf({{getter}}().get(i)), StandardCharsets.UTF_8).replaceAll(\"\\\\+\", \"%20\")));\n      }\n    }\n    {{/uniqueItems}}\n    {{/items.isPrimitiveType}}\n    {{^items.isPrimitiveType}}\n    {{#items.isModel}}\n    {{#uniqueItems}}\n    if ({{getter}}() != null) {\n      int i = 0;\n      for ({{{items.dataType}}} _item : {{getter}}()) {\n        if (_item != null) {\n          joiner.add(_item.toUrlQueryString(String.format(\"%s{{baseName}}%s%s\", prefix, suffix,\n              \"\".equals(suffix) ? \"\" : String.format(\"%s%d%s\", containerPrefix, i, containerSuffix))));\n        }\n      }\n      i++;\n    }\n    {{/uniqueItems}}\n    {{^uniqueItems}}\n    if ({{getter}}() != null) {\n      for (int i = 0; i < {{getter}}().size(); i++) {\n        if ({{getter}}().get(i) != null) {\n          joiner.add({{getter}}().get(i).toUrlQueryString(String.format(\"%s{{baseName}}%s%s\", prefix, suffix,\n          \"\".equals(suffix) ? \"\" : String.format(\"%s%d%s\", containerPrefix, i, containerSuffix))));\n        }\n      }\n    }\n    {{/uniqueItems}}\n    {{/items.isModel}}\n    {{^items.isModel}}\n    {{#uniqueItems}}\n    if ({{getter}}() != null) {\n      int i = 0;\n      for ({{{items.dataType}}} _item : {{getter}}()) {\n        if (_item != null) {\n          joiner.add(String.format(\"%s{{baseName}}%s%s=%s\", prefix, suffix,\n              \"\".equals(suffix) ? \"\" : String.format(\"%s%d%s\", containerPrefix, i, containerSuffix),\n              URLEncoder.encode(String.valueOf(_item), StandardCharsets.UTF_8).replaceAll(\"\\\\+\", \"%20\")));\n        }\n        i++;\n      }\n    }\n    {{/uniqueItems}}\n    {{^uniqueItems}}\n    if ({{getter}}() != null) {\n      for (int i = 0; i < {{getter}}().size(); i++) {\n        if ({{getter}}().get(i) != null) {\n          joiner.add(String.format(\"%s{{baseName}}%s%s=%s\", prefix, suffix,\n              \"\".equals(suffix) ? \"\" : String.format(\"%s%d%s\", containerPrefix, i, containerSuffix),\n              URLEncoder.encode(String.valueOf({{getter}}().get(i)), StandardCharsets.UTF_8).replaceAll(\"\\\\+\", \"%20\")));\n        }\n      }\n    }\n    {{/uniqueItems}}\n    {{/items.isModel}}\n    {{/items.isPrimitiveType}}\n    {{/isArray}}\n    {{^isArray}}\n    {{#isMap}}\n    {{^items.isModel}}\n    if ({{getter}}() != null) {\n      for (String _key : {{getter}}().keySet()) {\n        joiner.add(String.format(\"%s{{baseName}}%s%s=%s\", prefix, suffix,\n            \"\".equals(suffix) ? \"\" : String.format(\"%s%d%s\", containerPrefix, _key, containerSuffix),\n            {{getter}}().get(_key), URLEncoder.encode(String.valueOf({{getter}}().get(_key)), StandardCharsets.UTF_8).replaceAll(\"\\\\+\", \"%20\")));\n      }\n    }\n    {{/items.isModel}}\n    {{#items.isModel}}\n    if ({{getter}}() != null) {\n      for (String _key : {{getter}}().keySet()) {\n        if ({{getter}}().get(_key) != null) {\n          joiner.add({{getter}}().get(_key).toUrlQueryString(String.format(\"%s{{baseName}}%s%s\", prefix, suffix,\n              \"\".equals(suffix) ? \"\" : String.format(\"%s%d%s\", containerPrefix, _key, containerSuffix))));\n        }\n      }\n    }\n    {{/items.isModel}}\n    {{/isMap}}\n    {{^isMap}}\n    {{#isPrimitiveType}}\n    if ({{getter}}() != null) {\n      joiner.add(String.format(\"%s{{{baseName}}}%s=%s\", prefix, suffix, URLEncoder.encode(String.valueOf({{{getter}}}()), StandardCharsets.UTF_8).replaceAll(\"\\\\+\", \"%20\")));\n    }\n    {{/isPrimitiveType}}\n    {{^isPrimitiveType}}\n    {{#isModel}}\n    if ({{getter}}() != null) {\n      joiner.add({{getter}}().toUrlQueryString(prefix + \"{{{baseName}}}\" + suffix));\n    }\n    {{/isModel}}\n    {{^isModel}}\n    if ({{getter}}() != null) {\n      joiner.add(String.format(\"%s{{{baseName}}}%s=%s\", prefix, suffix, URLEncoder.encode(String.valueOf({{{getter}}}()), StandardCharsets.UTF_8).replaceAll(\"\\\\+\", \"%20\")));\n    }\n    {{/isModel}}\n    {{/isPrimitiveType}}\n    {{/isMap}}\n    {{/isArray}}\n\n    {{/allVars}}\n    return joiner.toString();\n  }\n{{/supportUrlQuery}}\n{{#parcelableModel}}\n\n  public void writeToParcel(Parcel out, int flags) {\n{{#model}}\n{{#isArray}}\n    out.writeList(this);\n{{/isArray}}\n{{^isArray}}\n{{#parent}}\n    super.writeToParcel(out, flags);\n{{/parent}}\n{{#vars}}\n    out.writeValue({{name}});\n{{/vars}}\n{{/isArray}}\n{{/model}}\n  }\n\n  {{classname}}(Parcel in) {\n{{#isArray}}\n    in.readTypedList(this, {{arrayModelType}}.CREATOR);\n{{/isArray}}\n{{^isArray}}\n{{#parent}}\n    super(in);\n{{/parent}}\n{{#vars}}\n{{#isPrimitiveType}}\n    {{name}} = ({{{datatypeWithEnum}}})in.readValue(null);\n{{/isPrimitiveType}}\n{{^isPrimitiveType}}\n    {{name}} = ({{{datatypeWithEnum}}})in.readValue({{complexType}}.class.getClassLoader());\n{{/isPrimitiveType}}\n{{/vars}}\n{{/isArray}}\n  }\n\n  public int describeContents() {\n    return 0;\n  }\n\n  public static final Parcelable.Creator<{{classname}}> CREATOR = new Parcelable.Creator<{{classname}}>() {\n    public {{classname}} createFromParcel(Parcel in) {\n{{#model}}\n{{#isArray}}\n      {{classname}} result = new {{classname}}();\n      result.addAll(in.readArrayList({{arrayModelType}}.class.getClassLoader()));\n      return result;\n{{/isArray}}\n{{^isArray}}\n      return new {{classname}}(in);\n{{/isArray}}\n{{/model}}\n    }\n    public {{classname}}[] newArray(int size) {\n      return new {{classname}}[size];\n    }\n  };\n{{/parcelableModel}}\n{{#discriminator}}\nstatic {\n  // Initialize and register the discriminator mappings.\n  Map<String, Class<?>> mappings = new HashMap<String, Class<?>>();\n  {{#mappedModels}}\n  mappings.put(\"{{mappingName}}\", {{modelName}}.class);\n  {{/mappedModels}}\n  mappings.put(\"{{name}}\", {{classname}}.class);\n  JSON.registerDiscriminator({{classname}}.class, \"{{propertyBaseName}}\", mappings);\n}\n{{/discriminator}}\n}\n"
  },
  {
    "path": "client2/src/main/template/libraries/native/pojo.mustache.orig",
    "content": "{{#discriminator}}\nimport {{invokerPackage}}.JSON;\n{{/discriminator}}\n/**\n * {{description}}{{^description}}{{classname}}{{/description}}{{#isDeprecated}}\n * @deprecated{{/isDeprecated}}\n */{{#isDeprecated}}\n@Deprecated{{/isDeprecated}}\n{{#swagger1AnnotationLibrary}}\n{{#description}}\n@ApiModel(description = \"{{{.}}}\")\n{{/description}}\n{{/swagger1AnnotationLibrary}}\n{{#swagger2AnnotationLibrary}}\n{{#description}}\n@Schema(description = \"{{{.}}}\")\n{{/description}}\n{{/swagger2AnnotationLibrary}}\n{{#jackson}}\n@JsonPropertyOrder({\n{{#vars}}\n  {{classname}}.JSON_PROPERTY_{{nameInSnakeCase}}{{^-last}},{{/-last}}\n{{/vars}}\n})\n{{/jackson}}\n{{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{>xmlAnnotation}}\n{{#vendorExtensions.x-class-extra-annotation}}\n{{{vendorExtensions.x-class-extra-annotation}}}\n{{/vendorExtensions.x-class-extra-annotation}}\npublic class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{#-last}} {{/-last}}{{/vendorExtensions.x-implements}}{\n{{#serializableModel}}\n  private static final long serialVersionUID = 1L;\n\n{{/serializableModel}}\n  {{#vars}}\n    {{#isEnum}}\n    {{^isContainer}}\n    {{^vendorExtensions.x-enum-as-string}}\n{{>modelInnerEnum}}\n    {{/vendorExtensions.x-enum-as-string}}\n    {{/isContainer}}\n    {{#isContainer}}\n    {{#mostInnerItems}}\n{{>modelInnerEnum}}\n    {{/mostInnerItems}}\n    {{/isContainer}}\n    {{/isEnum}}\n  {{#gson}}\n  public static final String SERIALIZED_NAME_{{nameInSnakeCase}} = \"{{baseName}}\";\n  {{/gson}}\n  {{#jackson}}\n  public static final String JSON_PROPERTY_{{nameInSnakeCase}} = \"{{baseName}}\";\n  {{/jackson}}\n  {{#withXml}}\n  {{#isXmlAttribute}}\n  @XmlAttribute(name = \"{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}\")\n  {{/isXmlAttribute}}\n  {{^isXmlAttribute}}\n    {{^isContainer}}\n  @XmlElement({{#xmlNamespace}}namespace=\"{{.}}\", {{/xmlNamespace}}name = \"{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}\")\n    {{/isContainer}}\n    {{#isContainer}}\n  // Is a container wrapped={{isXmlWrapped}}\n      {{#items}}\n  // items.name={{name}} items.baseName={{baseName}} items.xmlName={{xmlName}} items.xmlNamespace={{xmlNamespace}}\n  // items.example={{example}} items.type={{dataType}}\n  @XmlElement({{#xmlNamespace}}namespace=\"{{.}}\", {{/xmlNamespace}}name = \"{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}\")\n      {{/items}}\n      {{#isXmlWrapped}}\n  @XmlElementWrapper({{#xmlNamespace}}namespace=\"{{.}}\", {{/xmlNamespace}}name = \"{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}\")\n      {{/isXmlWrapped}}\n    {{/isContainer}}\n  {{/isXmlAttribute}}\n  {{/withXml}}\n  {{#gson}}\n  @SerializedName(SERIALIZED_NAME_{{nameInSnakeCase}})\n  {{/gson}}\n  {{#vendorExtensions.x-field-extra-annotation}}\n  {{{vendorExtensions.x-field-extra-annotation}}}\n  {{/vendorExtensions.x-field-extra-annotation}}\n  {{#vendorExtensions.x-is-jackson-optional-nullable}}\n  {{#isContainer}}\n  private JsonNullable<{{{datatypeWithEnum}}}> {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>undefined();\n  {{/isContainer}}\n  {{^isContainer}}\n  private JsonNullable<{{{datatypeWithEnum}}}> {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>{{#defaultValue}}of({{{.}}}){{/defaultValue}}{{^defaultValue}}undefined(){{/defaultValue}};\n  {{/isContainer}}\n  {{/vendorExtensions.x-is-jackson-optional-nullable}}\n  {{^vendorExtensions.x-is-jackson-optional-nullable}}\n  private {{{datatypeWithEnum}}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};\n  {{/vendorExtensions.x-is-jackson-optional-nullable}}\n\n  {{/vars}}\n  public {{classname}}() { {{#parent}}{{#parcelableModel}}\n    super();{{/parcelableModel}}{{/parent}}{{#gson}}{{#discriminator}}\n    this.{{{discriminatorName}}} = this.getClass().getSimpleName();{{/discriminator}}{{/gson}}\n  }{{#vendorExtensions.x-has-readonly-properties}}{{^withXml}}\n\n  {{#jackson}}@JsonCreator{{/jackson}}\n  public {{classname}}(\n  {{#readOnlyVars}}\n    @JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}}) {{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}}\n  {{/readOnlyVars}}\n  ) {\n  this();\n  {{#readOnlyVars}}\n    this.{{name}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}};\n  {{/readOnlyVars}}\n  }{{/withXml}}{{/vendorExtensions.x-has-readonly-properties}}\n  {{#vars}}\n\n  {{^isReadOnly}}\n  {{#vendorExtensions.x-enum-as-string}}\n  public static final Set<String> {{{nameInSnakeCase}}}_VALUES = new HashSet<>(Arrays.asList(\n    {{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}\n  ));\n\n  {{/vendorExtensions.x-enum-as-string}}\n  public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) {\n    {{#vendorExtensions.x-enum-as-string}}\n    if (!{{{nameInSnakeCase}}}_VALUES.contains({{name}})) {\n      throw new IllegalArgumentException({{name}} + \" is invalid. Possible values for {{name}}: \" + String.join(\", \", {{{nameInSnakeCase}}}_VALUES));\n    }\n\n    {{/vendorExtensions.x-enum-as-string}}\n    {{#vendorExtensions.x-is-jackson-optional-nullable}}\n    this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{name}});\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n    {{^vendorExtensions.x-is-jackson-optional-nullable}}\n    this.{{name}} = {{name}};\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n    return this;\n  }\n  {{#isArray}}\n\n  public {{classname}} add{{nameInCamelCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) {\n    {{#vendorExtensions.x-is-jackson-optional-nullable}}\n    if (this.{{name}} == null || !this.{{name}}.isPresent()) {\n      this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}});\n    }\n    try {\n      this.{{name}}.get().add({{name}}Item);\n    } catch (java.util.NoSuchElementException e) {\n      // this can never happen, as we make sure above that the value is present\n    }\n    return this;\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n    {{^vendorExtensions.x-is-jackson-optional-nullable}}\n    if (this.{{name}} == null) {\n      this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}};\n    }\n    this.{{name}}.add({{name}}Item);\n    return this;\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n  }\n  {{/isArray}}\n  {{#isMap}}\n\n  public {{classname}} put{{nameInCamelCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) {\n    {{#vendorExtensions.x-is-jackson-optional-nullable}}\n    if (this.{{name}} == null || !this.{{name}}.isPresent()) {\n      this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}});\n    }\n    try {\n      this.{{name}}.get().put(key, {{name}}Item);\n    } catch (java.util.NoSuchElementException e) {\n      // this can never happen, as we make sure above that the value is present\n    }\n    return this;\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n    {{^vendorExtensions.x-is-jackson-optional-nullable}}\n    if (this.{{name}} == null) {\n      this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}};\n    }\n    this.{{name}}.put(key, {{name}}Item);\n    return this;\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n  }\n  {{/isMap}}\n\n  {{/isReadOnly}}\n   /**\n  {{#description}}\n   * {{.}}\n  {{/description}}\n  {{^description}}\n   * Get {{name}}\n  {{/description}}\n  {{#minimum}}\n   * minimum: {{.}}\n  {{/minimum}}\n  {{#maximum}}\n   * maximum: {{.}}\n  {{/maximum}}\n   * @return {{name}}\n   {{#deprecated}}\n   * @deprecated\n   {{/deprecated}}\n  **/\n{{#deprecated}}\n  @Deprecated\n{{/deprecated}}\n{{#required}}\n{{#isNullable}}\n  @{{javaxPackage}}.annotation.Nullable\n{{/isNullable}}\n{{^isNullable}}\n  @{{javaxPackage}}.annotation.Nonnull\n{{/isNullable}}\n{{/required}}\n{{^required}}\n  @{{javaxPackage}}.annotation.Nullable\n{{/required}}\n{{#useBeanValidation}}\n{{>beanValidation}}\n{{/useBeanValidation}}\n{{#swagger1AnnotationLibrary}}\n  @ApiModelProperty({{#example}}example = \"{{{.}}}\", {{/example}}{{#required}}required = {{required}}, {{/required}}value = \"{{{description}}}\")\n{{/swagger1AnnotationLibrary}}\n{{#swagger2AnnotationLibrary}}\n  @Schema({{#example}}example = \"{{{.}}}\", {{/example}}requiredMode = {{#required}}Schema.RequiredMode.REQUIRED{{/required}}{{^required}}Schema.RequiredMode.NOT_REQUIRED{{/required}}, description = \"{{{description}}}\")\n{{/swagger2AnnotationLibrary}}\n{{#vendorExtensions.x-extra-annotation}}\n  {{{vendorExtensions.x-extra-annotation}}}\n{{/vendorExtensions.x-extra-annotation}}\n{{#vendorExtensions.x-is-jackson-optional-nullable}}\n  {{!unannotated, Jackson would pick this up automatically and add it *in addition* to the _JsonNullable getter field}}\n  @JsonIgnore\n{{/vendorExtensions.x-is-jackson-optional-nullable}}\n{{^vendorExtensions.x-is-jackson-optional-nullable}}{{#jackson}}{{> jackson_annotations}}{{/jackson}}{{/vendorExtensions.x-is-jackson-optional-nullable}}\n  public {{{datatypeWithEnum}}} {{getter}}() {\n    {{#vendorExtensions.x-is-jackson-optional-nullable}}\n    {{#isReadOnly}}{{! A readonly attribute doesn't have setter => jackson will set null directly if explicitly returned by API, so make sure we have an empty JsonNullable}}\n    if ({{name}} == null) {\n      {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>{{#defaultValue}}of({{{.}}}){{/defaultValue}}{{^defaultValue}}undefined(){{/defaultValue}};\n    }\n    {{/isReadOnly}}\n    return {{name}}.orElse(null);\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n    {{^vendorExtensions.x-is-jackson-optional-nullable}}\n    return {{name}};\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n  }\n\n  {{#vendorExtensions.x-is-jackson-optional-nullable}}\n{{> jackson_annotations}}\n  public JsonNullable<{{{datatypeWithEnum}}}> {{getter}}_JsonNullable() {\n    return {{name}};\n  }\n  {{/vendorExtensions.x-is-jackson-optional-nullable}}{{#vendorExtensions.x-is-jackson-optional-nullable}}\n  @JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}})\n  {{#isReadOnly}}private{{/isReadOnly}}{{^isReadOnly}}public{{/isReadOnly}} void {{setter}}_JsonNullable(JsonNullable<{{{datatypeWithEnum}}}> {{name}}) {\n    {{! For getters/setters that have name differing from attribute name, we must include setter (albeit private) for jackson to be able to set the attribute}}\n    this.{{name}} = {{name}};\n  }\n  {{/vendorExtensions.x-is-jackson-optional-nullable}}\n\n  {{^isReadOnly}}\n{{#vendorExtensions.x-setter-extra-annotation}}  {{{vendorExtensions.x-setter-extra-annotation}}}\n{{/vendorExtensions.x-setter-extra-annotation}}{{#jackson}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{> jackson_annotations}}{{/vendorExtensions.x-is-jackson-optional-nullable}}{{/jackson}}  public void {{setter}}({{{datatypeWithEnum}}} {{name}}) {\n    {{#vendorExtensions.x-enum-as-string}}\n    if (!{{{nameInSnakeCase}}}_VALUES.contains({{name}})) {\n      throw new IllegalArgumentException({{name}} + \" is invalid. Possible values for {{name}}: \" + String.join(\", \", {{{nameInSnakeCase}}}_VALUES));\n    }\n\n    {{/vendorExtensions.x-enum-as-string}}\n    {{#vendorExtensions.x-is-jackson-optional-nullable}}\n    this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{name}});\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n    {{^vendorExtensions.x-is-jackson-optional-nullable}}\n    this.{{name}} = {{name}};\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n  }\n  {{/isReadOnly}}\n\n  {{/vars}}\n{{>libraries/native/additional_properties}}\n  {{#parent}}\n  {{#allVars}}\n  {{#isOverridden}}\n  @Override\n  public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) {\n    {{#vendorExtensions.x-is-jackson-optional-nullable}}\n    this.{{setter}}(JsonNullable.<{{{datatypeWithEnum}}}>of({{name}}));\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n    {{^vendorExtensions.x-is-jackson-optional-nullable}}\n    this.{{setter}}({{name}});\n    {{/vendorExtensions.x-is-jackson-optional-nullable}}\n    return this;\n  }\n\n  {{/isOverridden}}\n  {{/allVars}}\n  {{/parent}}\n  /**\n   * Return true if this {{name}} object is equal to o.\n   */\n  @Override\n  public boolean equals(Object o) {\n  {{#useReflectionEqualsHashCode}}\n    return EqualsBuilder.reflectionEquals(this, o, false, null, true);\n  {{/useReflectionEqualsHashCode}}\n  {{^useReflectionEqualsHashCode}}\n    if (this == o) {\n      return true;\n    }\n    if (o == null || getClass() != o.getClass()) {\n      return false;\n    }{{#hasVars}}\n    {{classname}} {{classVarName}} = ({{classname}}) o;\n    return {{#vars}}{{#vendorExtensions.x-is-jackson-optional-nullable}}equalsNullable(this.{{name}}, {{classVarName}}.{{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{#isByteArray}}Arrays{{/isByteArray}}{{^isByteArray}}Objects{{/isByteArray}}.equals(this.{{name}}, {{classVarName}}.{{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^-last}} &&\n        {{/-last}}{{/vars}}{{#additionalPropertiesType}}&&\n        Objects.equals(this.additionalProperties, {{classVarName}}.additionalProperties){{/additionalPropertiesType}}{{#parent}} &&\n        super.equals(o){{/parent}};{{/hasVars}}{{^hasVars}}\n    return {{#parent}}super.equals(o){{/parent}}{{^parent}}true{{/parent}};{{/hasVars}}\n  {{/useReflectionEqualsHashCode}}\n  }{{#vendorExtensions.x-jackson-optional-nullable-helpers}}\n\n  private static <T> boolean equalsNullable(JsonNullable<T> a, JsonNullable<T> b) {\n    return a == b || (a != null && b != null && a.isPresent() && b.isPresent() && Objects.deepEquals(a.get(), b.get()));\n  }{{/vendorExtensions.x-jackson-optional-nullable-helpers}}\n\n  @Override\n  public int hashCode() {\n  {{#useReflectionEqualsHashCode}}\n    return HashCodeBuilder.reflectionHashCode(this);\n  {{/useReflectionEqualsHashCode}}\n  {{^useReflectionEqualsHashCode}}\n    return Objects.hash({{#vars}}{{#vendorExtensions.x-is-jackson-optional-nullable}}hashCodeNullable({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{^isByteArray}}{{name}}{{/isByteArray}}{{#isByteArray}}Arrays.hashCode({{name}}){{/isByteArray}}{{/vendorExtensions.x-is-jackson-optional-nullable}}{{^-last}}, {{/-last}}{{/vars}}{{#parent}}{{#hasVars}}, {{/hasVars}}super.hashCode(){{/parent}}{{#additionalPropertiesType}}, additionalProperties{{/additionalPropertiesType}});\n  {{/useReflectionEqualsHashCode}}\n  }{{#vendorExtensions.x-jackson-optional-nullable-helpers}}\n\n  private static <T> int hashCodeNullable(JsonNullable<T> a) {\n    if (a == null) {\n      return 1;\n    }\n    return a.isPresent() ? Arrays.deepHashCode(new Object[]{a.get()}) : 31;\n  }{{/vendorExtensions.x-jackson-optional-nullable-helpers}}\n\n  @Override\n  public String toString() {\n    StringBuilder sb = new StringBuilder();\n    sb.append(\"class {{classname}} {\\n\");\n    {{#parent}}\n    sb.append(\"    \").append(toIndentedString(super.toString())).append(\"\\n\");\n    {{/parent}}\n    {{#vars}}\n    sb.append(\"    {{name}}: \").append(toIndentedString({{name}})).append(\"\\n\");\n    {{/vars}}\n    {{#additionalPropertiesType}}\n    sb.append(\"    additionalProperties: \").append(toIndentedString(additionalProperties)).append(\"\\n\");\n    {{/additionalPropertiesType}}\n    sb.append(\"}\");\n    return sb.toString();\n  }\n\n  /**\n   * Convert the given object to string with each line indented by 4 spaces\n   * (except the first line).\n   */\n  private String toIndentedString(Object o) {\n    if (o == null) {\n      return \"null\";\n    }\n    return o.toString().replace(\"\\n\", \"\\n    \");\n  }\n{{#supportUrlQuery}}\n\n  /**\n   * Convert the instance into URL query string.\n   *\n   * @return URL query string\n   */\n  public String toUrlQueryString() {\n    return toUrlQueryString(null);\n  }\n\n  /**\n   * Convert the instance into URL query string.\n   *\n   * @param prefix prefix of the query string\n   * @return URL query string\n   */\n  public String toUrlQueryString(String prefix) {\n    String suffix = \"\";\n    String containerSuffix = \"\";\n    String containerPrefix = \"\";\n    if (prefix == null) {\n      // style=form, explode=true, e.g. /pet?name=cat&type=manx\n      prefix = \"\";\n    } else {\n      // deepObject style e.g. /pet?id[name]=cat&id[type]=manx\n      prefix = prefix + \"[\";\n      suffix = \"]\";\n      containerSuffix = \"]\";\n      containerPrefix = \"[\";\n    }\n\n    StringJoiner joiner = new StringJoiner(\"&\");\n\n    {{#allVars}}\n    // add `{{baseName}}` to the URL query string\n    {{#isArray}}\n    {{#items.isPrimitiveType}}\n    {{#uniqueItems}}\n    if ({{getter}}() != null) {\n      int i = 0;\n      for ({{{items.dataType}}} _item : {{getter}}()) {\n        joiner.add(String.format(\"%s{{baseName}}%s%s=%s\", prefix, suffix,\n            \"\".equals(suffix) ? \"\" : String.format(\"%s%d%s\", containerPrefix, i, containerSuffix),\n            URLEncoder.encode(String.valueOf(_item), StandardCharsets.UTF_8).replaceAll(\"\\\\+\", \"%20\")));\n      }\n      i++;\n    }\n    {{/uniqueItems}}\n    {{^uniqueItems}}\n    if ({{getter}}() != null) {\n      for (int i = 0; i < {{getter}}().size(); i++) {\n        joiner.add(String.format(\"%s{{baseName}}%s%s=%s\", prefix, suffix,\n            \"\".equals(suffix) ? \"\" : String.format(\"%s%d%s\", containerPrefix, i, containerSuffix),\n            URLEncoder.encode(String.valueOf({{getter}}().get(i)), StandardCharsets.UTF_8).replaceAll(\"\\\\+\", \"%20\")));\n      }\n    }\n    {{/uniqueItems}}\n    {{/items.isPrimitiveType}}\n    {{^items.isPrimitiveType}}\n    {{#items.isModel}}\n    {{#uniqueItems}}\n    if ({{getter}}() != null) {\n      int i = 0;\n      for ({{{items.dataType}}} _item : {{getter}}()) {\n        if (_item != null) {\n          joiner.add(_item.toUrlQueryString(String.format(\"%s{{baseName}}%s%s\", prefix, suffix,\n              \"\".equals(suffix) ? \"\" : String.format(\"%s%d%s\", containerPrefix, i, containerSuffix))));\n        }\n      }\n      i++;\n    }\n    {{/uniqueItems}}\n    {{^uniqueItems}}\n    if ({{getter}}() != null) {\n      for (int i = 0; i < {{getter}}().size(); i++) {\n        if ({{getter}}().get(i) != null) {\n          joiner.add({{getter}}().get(i).toUrlQueryString(String.format(\"%s{{baseName}}%s%s\", prefix, suffix,\n          \"\".equals(suffix) ? \"\" : String.format(\"%s%d%s\", containerPrefix, i, containerSuffix))));\n        }\n      }\n    }\n    {{/uniqueItems}}\n    {{/items.isModel}}\n    {{^items.isModel}}\n    {{#uniqueItems}}\n    if ({{getter}}() != null) {\n      int i = 0;\n      for ({{{items.dataType}}} _item : {{getter}}()) {\n        if (_item != null) {\n          joiner.add(String.format(\"%s{{baseName}}%s%s=%s\", prefix, suffix,\n              \"\".equals(suffix) ? \"\" : String.format(\"%s%d%s\", containerPrefix, i, containerSuffix),\n              URLEncoder.encode(String.valueOf(_item), StandardCharsets.UTF_8).replaceAll(\"\\\\+\", \"%20\")));\n        }\n        i++;\n      }\n    }\n    {{/uniqueItems}}\n    {{^uniqueItems}}\n    if ({{getter}}() != null) {\n      for (int i = 0; i < {{getter}}().size(); i++) {\n        if ({{getter}}().get(i) != null) {\n          joiner.add(String.format(\"%s{{baseName}}%s%s=%s\", prefix, suffix,\n              \"\".equals(suffix) ? \"\" : String.format(\"%s%d%s\", containerPrefix, i, containerSuffix),\n              URLEncoder.encode(String.valueOf({{getter}}().get(i)), StandardCharsets.UTF_8).replaceAll(\"\\\\+\", \"%20\")));\n        }\n      }\n    }\n    {{/uniqueItems}}\n    {{/items.isModel}}\n    {{/items.isPrimitiveType}}\n    {{/isArray}}\n    {{^isArray}}\n    {{#isMap}}\n    {{^items.isModel}}\n    if ({{getter}}() != null) {\n      for (String _key : {{getter}}().keySet()) {\n        joiner.add(String.format(\"%s{{baseName}}%s%s=%s\", prefix, suffix,\n            \"\".equals(suffix) ? \"\" : String.format(\"%s%d%s\", containerPrefix, _key, containerSuffix),\n            {{getter}}().get(_key), URLEncoder.encode(String.valueOf({{getter}}().get(_key)), StandardCharsets.UTF_8).replaceAll(\"\\\\+\", \"%20\")));\n      }\n    }\n    {{/items.isModel}}\n    {{#items.isModel}}\n    if ({{getter}}() != null) {\n      for (String _key : {{getter}}().keySet()) {\n        if ({{getter}}().get(_key) != null) {\n          joiner.add({{getter}}().get(_key).toUrlQueryString(String.format(\"%s{{baseName}}%s%s\", prefix, suffix,\n              \"\".equals(suffix) ? \"\" : String.format(\"%s%d%s\", containerPrefix, _key, containerSuffix))));\n        }\n      }\n    }\n    {{/items.isModel}}\n    {{/isMap}}\n    {{^isMap}}\n    {{#isPrimitiveType}}\n    if ({{getter}}() != null) {\n      joiner.add(String.format(\"%s{{{baseName}}}%s=%s\", prefix, suffix, URLEncoder.encode(String.valueOf({{{getter}}}()), StandardCharsets.UTF_8).replaceAll(\"\\\\+\", \"%20\")));\n    }\n    {{/isPrimitiveType}}\n    {{^isPrimitiveType}}\n    {{#isModel}}\n    if ({{getter}}() != null) {\n      joiner.add({{getter}}().toUrlQueryString(prefix + \"{{{baseName}}}\" + suffix));\n    }\n    {{/isModel}}\n    {{^isModel}}\n    if ({{getter}}() != null) {\n      joiner.add(String.format(\"%s{{{baseName}}}%s=%s\", prefix, suffix, URLEncoder.encode(String.valueOf({{{getter}}}()), StandardCharsets.UTF_8).replaceAll(\"\\\\+\", \"%20\")));\n    }\n    {{/isModel}}\n    {{/isPrimitiveType}}\n    {{/isMap}}\n    {{/isArray}}\n\n    {{/allVars}}\n    return joiner.toString();\n  }\n{{/supportUrlQuery}}\n{{#parcelableModel}}\n\n  public void writeToParcel(Parcel out, int flags) {\n{{#model}}\n{{#isArray}}\n    out.writeList(this);\n{{/isArray}}\n{{^isArray}}\n{{#parent}}\n    super.writeToParcel(out, flags);\n{{/parent}}\n{{#vars}}\n    out.writeValue({{name}});\n{{/vars}}\n{{/isArray}}\n{{/model}}\n  }\n\n  {{classname}}(Parcel in) {\n{{#isArray}}\n    in.readTypedList(this, {{arrayModelType}}.CREATOR);\n{{/isArray}}\n{{^isArray}}\n{{#parent}}\n    super(in);\n{{/parent}}\n{{#vars}}\n{{#isPrimitiveType}}\n    {{name}} = ({{{datatypeWithEnum}}})in.readValue(null);\n{{/isPrimitiveType}}\n{{^isPrimitiveType}}\n    {{name}} = ({{{datatypeWithEnum}}})in.readValue({{complexType}}.class.getClassLoader());\n{{/isPrimitiveType}}\n{{/vars}}\n{{/isArray}}\n  }\n\n  public int describeContents() {\n    return 0;\n  }\n\n  public static final Parcelable.Creator<{{classname}}> CREATOR = new Parcelable.Creator<{{classname}}>() {\n    public {{classname}} createFromParcel(Parcel in) {\n{{#model}}\n{{#isArray}}\n      {{classname}} result = new {{classname}}();\n      result.addAll(in.readArrayList({{arrayModelType}}.class.getClassLoader()));\n      return result;\n{{/isArray}}\n{{^isArray}}\n      return new {{classname}}(in);\n{{/isArray}}\n{{/model}}\n    }\n    public {{classname}}[] newArray(int size) {\n      return new {{classname}}[size];\n    }\n  };\n{{/parcelableModel}}\n{{#discriminator}}\nstatic {\n  // Initialize and register the discriminator mappings.\n  Map<String, Class<?>> mappings = new HashMap<String, Class<?>>();\n  {{#mappedModels}}\n  mappings.put(\"{{mappingName}}\", {{modelName}}.class);\n  {{/mappedModels}}\n  mappings.put(\"{{name}}\", {{classname}}.class);\n  JSON.registerDiscriminator({{classname}}.class, \"{{propertyBaseName}}\", mappings);\n}\n{{/discriminator}}\n}\n"
  },
  {
    "path": "client2/src/test/java/com/walmartlabs/concord/client2/ApiClientJsonTest.java",
    "content": "package com.walmartlabs.concord.client2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.junit.jupiter.api.Test;\n\nimport java.text.SimpleDateFormat;\nimport java.time.OffsetDateTime;\nimport java.time.ZoneOffset;\nimport java.util.Collections;\nimport java.util.Date;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class ApiClientJsonTest {\n\n    @Test\n    public void testParseDate() throws Exception {\n        ObjectMapper om = ApiClient.createDefaultObjectMapper();\n\n        Date date = new Date(1587500112000L);\n        OffsetDateTime offsetDateTime = date.toInstant().atOffset(ZoneOffset.UTC);\n\n\n        String toParse = om.writeValueAsString(offsetDateTime);\n        toParse = toParse.substring(1, toParse.length() - 1);\n\n        // format like a sever entries\n        Date parsed = new SimpleDateFormat(\"yyyy-MM-dd'T'HH:mm:ss.SSSX\").\n                parse(toParse);\n\n        assertEquals(date, parsed);\n    }\n\n    @Test\n    public void testObjectSerialize() throws Exception {\n        ProjectEntry project = new ProjectEntry()\n                .name(\"\");\n\n        String str = ApiClient.createDefaultObjectMapper().writeValueAsString(project);\n        assertEquals(\"{\\\"name\\\":\\\"\\\"}\", str);\n    }\n\n    @Test\n    public void testEmptyCollectionSerialize() throws Exception {\n        CreateUserRequest user = new CreateUserRequest()\n                .username(\"test\")\n                .roles(Collections.emptySet());\n\n        String str = ApiClient.createDefaultObjectMapper().writeValueAsString(user);\n        assertEquals(\"{\\\"username\\\":\\\"test\\\",\\\"roles\\\":[]}\", str);\n    }\n}\n"
  },
  {
    "path": "client2/src/test/java/com/walmartlabs/concord/client2/ProcessApiTest.java",
    "content": "package com.walmartlabs.concord.client2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.impl.auth.ApiKey;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport javax.xml.bind.DatatypeConverter;\nimport java.util.UUID;\n\npublic class ProcessApiTest {\n\n    @Test\n    @Disabled\n    public void testDecrypt() throws Exception {\n        ApiClient apiClient = new DefaultApiClientFactory(\"http://localhost:8001\").create();\n        apiClient.setAuth(new ApiKey(\"cTFxMXExcTE=\"));\n\n        ProjectsApi projectsApi = new ProjectsApi(apiClient);\n        EncryptValueResponse encrypted = projectsApi.encrypt(\"org_1692633472807_3d32f7\", \"project_1692633472833_a1a531\", \"123qwe\");\n\n        String encryptedValue = encrypted.getData();\n        System.out.println(\">>>\" + encryptedValue);\n        byte[] input;\n\n        try {\n            input = DatatypeConverter.parseBase64Binary(encryptedValue);\n        } catch (Exception e) {\n            throw new IllegalArgumentException(\"Invalid encrypted string value, please verify that it was specified/copied correctly: \" + e.getMessage());\n        }\n\n        ProcessApi api = new ProcessApi(apiClient);\n        UUID pid = UUID.fromString(\"f891d797-d97e-4724-b0ba-91d48efce6d8\");\n        System.out.println(\">>>'\" + new String(api.decryptString(pid, input)) + \"'\");\n    }\n}\n"
  },
  {
    "path": "client2/src/test/java/com/walmartlabs/concord/client2/SecretClientTest.java",
    "content": "package com.walmartlabs.concord.client2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;\nimport com.github.tomakehurst.wiremock.junit5.WireMockTest;\nimport com.walmartlabs.concord.client2.impl.auth.ApiKey;\nimport com.walmartlabs.concord.common.secret.BinaryDataSecret;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport static com.github.tomakehurst.wiremock.client.WireMock.*;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\n@WireMockTest\npublic class SecretClientTest {\n\n    @Test\n    public void testInvalidSecretType(WireMockRuntimeInfo wmRuntimeInfo) throws Exception {\n        String orgName = \"org_\" + System.currentTimeMillis();\n        String secretName = \"secret_\" + System.currentTimeMillis();\n\n        stubFor(post(urlEqualTo(\"/api/v1/org/\" + orgName + \"/secret/\" + secretName + \"/data\"))\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withHeader(Constants.Headers.SECRET_TYPE, SecretEntryV2.TypeEnum.DATA.name())\n                        .withBody(\"Hello!\")));\n\n        ApiClient apiClient = new DefaultApiClientFactory(\"http://localhost:\" + wmRuntimeInfo.getHttpPort()).create();\n        SecretClient secretClient = new SecretClient(apiClient);\n\n        try {\n            secretClient.getData(orgName, secretName, null, SecretEntryV2.TypeEnum.KEY_PAIR);\n        } catch (IllegalArgumentException e) {\n            assertTrue(e.getMessage().contains(\"Unexpected type of \" + orgName + \"/\" + secretName));\n        }\n    }\n\n    @Test\n    @Disabled\n    public void testGetSecret() throws Exception {\n        ApiClient apiClient = new DefaultApiClientFactory(\"http://localhost:8001\").create();\n        apiClient.setAuth(new ApiKey(\"cTFxMXExcTE\"));\n        SecretClient secretClient = new SecretClient(apiClient);\n\n        BinaryDataSecret result = secretClient.getData(\"Default\", \"test\", null, SecretEntryV2.TypeEnum.DATA);\n        System.out.println(\">>> '\" + new String(result.getData()) + \"'\");\n    }\n}\n"
  },
  {
    "path": "client2/src/test/java/com/walmartlabs/concord/client2/impl/MultipartRequestBodyHandlerTest.java",
    "content": "package com.walmartlabs.concord.client2.impl;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.InputStream;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class MultipartRequestBodyHandlerTest {\n\n    @Test\n    public void test2() throws Exception {\n        Map<String, Object> data = new LinkedHashMap<>();\n        data.put(\"string-field\", \"this stuff\");\n        data.put(\"byte[]-field\", \"byte array\".getBytes());\n        data.put(\"inputstream-field\", new ByteArrayInputStream(\"my input stream\".getBytes()));\n        data.put(\"boolean-field\", true);\n        data.put(\"json-field\", Collections.singletonMap(\"k\", \"v\"));\n        data.put(\"string[]-field\", new String[] {\"one\", \"two\"});\n        data.put(\"uuid-field\", UUID.fromString(\"f8d30c37-4c84-4817-9cb8-23b27a54c459\"));\n\n        MultipartBuilder mpb = new MultipartBuilder(\"e572b648-941a-4648-97ed-0e3c5350f0ad\");\n        HttpEntity entity = MultipartRequestBodyHandler.handle(mpb, new ObjectMapper(), data);\n\n        try (InputStream is = entity.getContent()) {\n            String str = new String(is.readAllBytes(), StandardCharsets.UTF_8);\n\n            assertEquals(body, str);\n            assertEquals(\"multipart/form-data; boundary=e572b648-941a-4648-97ed-0e3c5350f0ad\", entity.contentType().toString());\n        }\n    }\n\n    private static final String body = \"--e572b648-941a-4648-97ed-0e3c5350f0ad\\r\\n\" +\n            \"Content-Disposition: form-data; name=\\\"string-field\\\"\\r\\n\" +\n            \"Content-Length: 10\\r\\n\" +\n            \"\\r\\n\" +\n            \"this stuff\\r\\n\" +\n            \"--e572b648-941a-4648-97ed-0e3c5350f0ad\\r\\n\" +\n            \"Content-Disposition: form-data; name=\\\"byte[]-field\\\"\\r\\n\" +\n            \"Content-Type: application/octet-stream\\r\\n\" +\n            \"Content-Length: 10\\r\\n\" +\n            \"\\r\\n\" +\n            \"byte array\\r\\n\" +\n            \"--e572b648-941a-4648-97ed-0e3c5350f0ad\\r\\n\" +\n            \"Content-Disposition: form-data; name=\\\"inputstream-field\\\"\\r\\n\" +\n            \"Content-Type: application/octet-stream\\r\\n\" +\n            \"\\r\\n\" +\n            \"my input stream\\r\\n\" +\n            \"--e572b648-941a-4648-97ed-0e3c5350f0ad\\r\\n\" +\n            \"Content-Disposition: form-data; name=\\\"boolean-field\\\"\\r\\n\" +\n            \"Content-Type: text/plain; charset=utf-8\\r\\n\" +\n            \"Content-Length: 4\\r\\n\" +\n            \"\\r\\n\" +\n            \"true\\r\\n\" +\n            \"--e572b648-941a-4648-97ed-0e3c5350f0ad\\r\\n\" +\n            \"Content-Disposition: form-data; name=\\\"json-field\\\"\\r\\n\" +\n            \"Content-Type: application/json; charset=utf-8\\r\\n\" +\n            \"Content-Length: 9\\r\\n\" +\n            \"\\r\\n\" +\n            \"{\\\"k\\\":\\\"v\\\"}\\r\\n\" +\n            \"--e572b648-941a-4648-97ed-0e3c5350f0ad\\r\\n\" +\n            \"Content-Disposition: form-data; name=\\\"string[]-field\\\"\\r\\n\" +\n            \"Content-Type: text/plain; charset=utf-8\\r\\n\" +\n            \"Content-Length: 7\\r\\n\" +\n            \"\\r\\n\" +\n            \"one,two\\r\\n\" +\n            \"--e572b648-941a-4648-97ed-0e3c5350f0ad\\r\\n\" +\n            \"Content-Disposition: form-data; name=\\\"uuid-field\\\"\\r\\n\" +\n            \"Content-Length: 36\\r\\n\" +\n            \"\\r\\n\" +\n            \"f8d30c37-4c84-4817-9cb8-23b27a54c459\\r\\n\" +\n            \"--e572b648-941a-4648-97ed-0e3c5350f0ad--\" +\n            \"\\r\\n\";\n}\n"
  },
  {
    "path": "common/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-common</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.takari.bpm</groupId>\n            <artifactId>bpm-engine-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>io.takari.bpm</groupId>\n            <artifactId>bpm-engine-impl</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>javax.validation</groupId>\n            <artifactId>validation-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-compress</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>commons-validator</groupId>\n            <artifactId>commons-validator</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jdk8</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-guava</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jsr310</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <!-- JDK9+ compatibility -->\n        <dependency>\n            <groupId>javax.xml.bind</groupId>\n            <artifactId>jaxb-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.sun.xml.bind</groupId>\n            <artifactId>jaxb-impl</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-core</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-junit-jupiter</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <configuration>\n                    <environmentVariables>\n                        <CONCORD_TMP_DIR>${java.io.tmpdir}</CONCORD_TMP_DIR>\n                    </environmentVariables>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/AllowNulls.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Target;\n\n@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.TYPE_USE})\npublic @interface AllowNulls {}"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/AuthTokenProvider.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.cfg.MappingAuthConfig;\nimport com.walmartlabs.concord.common.cfg.OauthTokenConfig;\nimport com.walmartlabs.concord.common.secret.BinaryDataSecret;\nimport com.walmartlabs.concord.sdk.Secret;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.regex.Pattern;\n\nimport static com.walmartlabs.concord.common.cfg.MappingAuthConfig.assertBaseUrlPattern;\n\npublic interface AuthTokenProvider {\n\n    /**\n     * @return {@code true} if this the given repo URI and secret are compatible\n     *         with this provider's {@link #getToken(URI, Secret)} method,\n     *         {@code false} otherwise.\n     */\n    boolean supports(URI repo, @Nullable Secret secret);\n\n    Optional<ExternalAuthToken> getToken(URI repo, @Nullable Secret secret);\n\n    default URI addUserInfoToUri(URI repo, @Nullable Secret secret) {\n        if (!supports(repo, secret)) {\n            // not compatible with auth provider(s)\n            return repo;\n        }\n\n        return getToken(repo, secret)\n                .map(expiringToken -> {\n                    var token  = expiringToken.token();\n                    var userInfo = expiringToken.username() != null\n                            ? expiringToken.username() + \":\" + token\n                            : token;\n\n                    try {\n                        return new URI(repo.getScheme(), userInfo, repo.getHost(),\n                                repo.getPort(), repo.getPath(), repo.getQuery(), repo.getFragment());\n                    } catch (URISyntaxException e) {\n                        return null;\n                    }\n                })\n                .orElse(repo);\n    }\n\n    @SuppressWarnings(\"ClassCanBeRecord\")\n    class OauthTokenProvider implements AuthTokenProvider {\n        // >0 length, printable ascii (no newlines, etc)\n        private static final Pattern BASIC_STRING_PTN = Pattern.compile(\"[ -~]+\");\n        private final List<MappingAuthConfig> authConfigs;\n\n        @Inject\n        public OauthTokenProvider(OauthTokenConfig config) {\n            this.authConfigs = toConfigList(config);\n        }\n\n        @Override\n        public boolean supports(URI repo, @Nullable Secret secret) {\n            return validateSecret(secret) || systemSupports(repo);\n        }\n\n        @Override\n        public Optional<ExternalAuthToken> getToken(URI repo, @Nullable Secret secret) {\n            if (secret != null) {\n                if (secret instanceof BinaryDataSecret bds) {\n                    return Optional.of(ExternalAuthToken.StaticToken.builder()\n                            .token(new String(bds.getData()))\n                            .build());\n                } else {\n                    return Optional.empty();\n                }\n            }\n\n            return authConfigs.stream()\n                    .filter(auth -> auth.canHandle(repo))\n                    .filter(MappingAuthConfig.OauthAuthConfig.class::isInstance)\n                    .map(MappingAuthConfig.OauthAuthConfig.class::cast)\n                    .findFirst()\n                    .map(auth -> ExternalAuthToken.StaticToken.builder()\n                            .authId(auth.id())\n                            .token(auth.token())\n                            .username(auth.username())\n                            .build());\n        }\n\n        private boolean validateSecret(Secret secret) {\n            if (secret == null) {\n                return false;\n            }\n\n            if (!(secret instanceof BinaryDataSecret bds)) {\n                // this class is not the place for handling key pairs or username/password\n                return false;\n            } else {\n                var data = new String(bds.getData());\n                return BASIC_STRING_PTN.matcher(data).matches();\n            }\n        }\n\n        private boolean systemSupports(URI repoUri) {\n            return authConfigs.stream().anyMatch(auth -> auth.canHandle(repoUri));\n        }\n\n        private static List<MappingAuthConfig> toConfigList(OauthTokenConfig config) {\n            var token = config.getOauthToken().orElse(null);\n\n            if (token == null || token.isBlank() && config.getSystemAuth().isEmpty()) {\n                return config.getSystemAuth();\n            }\n\n            return List.of(MappingAuthConfig.OauthAuthConfig.builder()\n                    .id(\"static-token\")\n                    .token(token)\n                    .username(config.getOauthUsername().orElse(null))\n                    .urlPattern(assertBaseUrlPattern(config.getOauthUrlPattern().orElse(\".*\")))// for backwards compat with git.oauth\n                    .build());\n        }\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/ConfigurationUtils.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.*;\n\npublic final class ConfigurationUtils {\n\n    @SuppressWarnings(\"unchecked\")\n    public static boolean has(Map<String, Object> m, String[] path) {\n        if (m == null) {\n            return false;\n        }\n\n        if (path.length == 0) {\n            return false;\n        }\n\n        for (int i = 0; i < path.length - 1; i++) {\n            Object v = m.get(path[i]);\n            if (!(v instanceof Map)) {\n                return false;\n            }\n\n            m = (Map<String, Object>) v;\n        }\n\n        return m.containsKey(path[path.length - 1]);\n    }\n\n    public static Object get(Map<String, Object> m, String... path) {\n        int depth = path != null ? path.length : 0;\n        return get(m, depth, path);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static Object get(Map<String, Object> m, int depth, String... path) {\n        if (m == null) {\n            return null;\n        }\n\n        if (depth == 0) {\n            return m;\n        }\n\n        for (int i = 0; i < depth - 1; i++) {\n            Object v = m.get(path[i]);\n            if (v == null) {\n                return null;\n            }\n\n            if (!(v instanceof Map)) {\n                throw new IllegalArgumentException(\"Invalid data type, expected JSON object, got: \" + v.getClass());\n            }\n\n            m = (Map<String, Object>) v;\n        }\n\n        return m.get(path[depth - 1]);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static void set(Map<String, Object> a, Object b, String... path) {\n        Object holder = get(a, path.length - 1, path);\n\n        if (holder != null && !(holder instanceof Map)) {\n            throw new IllegalArgumentException(\"Value should be contained in a JSON object: \" + String.join(\"/\", path));\n        }\n\n        Map<String, Object> m = (Map<String, Object>) holder;\n\n        // TODO automatically create the value holder?\n        assert m != null;\n        m.put(path[path.length - 1], b);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static void delete(Map<String, Object> a, String... path) {\n        Object holder = get(a, path.length - 1, path);\n        if (holder == null) {\n            return;\n        }\n\n        if (!(holder instanceof Map)) {\n            throw new IllegalArgumentException(\"Value should be contained in a JSON object: \" + String.join(\"/\", path));\n        }\n\n        Map<String, Object> m = (Map<String, Object>) holder;\n        m.remove(path[path.length - 1]);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static void merge(Map<String, Object> a, Map<String, Object> b, String... path) {\n        Object holder = get(a, path);\n\n        if (holder != null && !(holder instanceof Map)) {\n            throw new IllegalArgumentException(\"Existing value is not a JSON object: \" + holder + \" @ \" + String.join(\"/\", path));\n        }\n\n        Map<String, Object> m = (Map<String, Object>) holder;\n\n        // TODO automatically create the value holder?\n        assert m != null;\n        m.putAll(b);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static Map<String, Object> deepMerge(Map<String, Object> a, Map<String, Object> b) {\n        Map<String, Object> result = new LinkedHashMap<>(a != null ? a : Collections.emptyMap());\n\n        for (String k : b.keySet()) {\n            Object av = result.get(k);\n            Object bv = b.get(k);\n\n            Object o = bv;\n            if (av instanceof Map && bv instanceof Map) {\n                o = deepMerge((Map<String, Object>) av, (Map<String, Object>) bv);\n            }\n\n            // preserve the order of the keys\n            if (result.containsKey(k)) {\n                result.replace(k, o);\n            } else {\n                result.put(k, o);\n            }\n        }\n        return result;\n    }\n\n    @SafeVarargs\n    public static Map<String, Object> deepMerge(Map<String, Object>... maps) {\n        if (maps == null || maps.length == 0) {\n            return Collections.emptyMap();\n        }\n\n        Map<String, Object> result = new LinkedHashMap<>(maps[0]);\n        for (int i = 1; i < maps.length; i++) {\n            result = deepMerge(result, maps[i]);\n        }\n        return result;\n    }\n\n    public static Map<String, Object> toNested(String k, Object v) {\n        String[] as = k.split(\"\\\\.\");\n        if (as.length == 1) {\n            return Collections.singletonMap(k, v);\n        }\n\n        Map<String, Object> m = new LinkedHashMap<>();\n        Map<String, Object> root = m;\n\n        for (int i = 0; i < as.length; i++) {\n            if (i + 1 >= as.length) {\n                m.put(as[i], v);\n            } else {\n                Map<String, Object> mm = new LinkedHashMap<>();\n                m.put(as[i], mm);\n                m = mm;\n            }\n        }\n\n        return root;\n    }\n\n    @SuppressWarnings(\"rawtypes\")\n    public static boolean deepEquals(Object a, Object b) {\n        if (!Objects.deepEquals(a, b)) {\n            return false;\n        }\n\n        if (a instanceof Map && b instanceof Map) {\n            return equals((Map) a, (Map) b);\n        } else if (a instanceof Collection && b instanceof Collection) {\n            return equals((Collection) a, (Collection) b);\n        }\n\n        return true;\n    }\n\n    @SafeVarargs\n    public static <T> Set<T> distinct(Collection<T>... collections) {\n        Set<T> result = new HashSet<>();\n\n        if (collections != null) {\n            for (Collection<T> coll : collections) {\n                if (coll != null) {\n                    result.addAll(coll);\n                }\n            }\n        }\n\n        return result;\n    }\n\n    private static boolean equals(Map<?, ?> a, Map<?, ?> b) {\n        if (a.keySet().size() != b.keySet().size()) {\n            return false;\n        }\n\n        for (Map.Entry<?, ?> aEntry : a.entrySet()) {\n            Object aValue = aEntry.getValue();\n            Object bValue = b.get(aEntry.getKey());\n            if (!deepEquals(aValue, bValue)) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    private static boolean equals(Collection<?> a, Collection<?> b) {\n        if (a.size() != b.size()) {\n            return false;\n        }\n\n        Iterator<?> aIterator = a.iterator();\n        Iterator<?> bIterator = b.iterator();\n        while (aIterator.hasNext()) {\n            Object aValue = aIterator.next();\n            Object bValue = bIterator.next();\n            if (!deepEquals(aValue, bValue)) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    public static boolean isNestedKey(String key) {\n        return key.contains(\".\");\n    }\n\n    private ConfigurationUtils() {\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/CycleChecker.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.*;\n\npublic final class CycleChecker {\n\n    public static class CheckResult {\n        private final boolean hasCycle;\n        private final String node1;\n        private final String node2;\n\n        private CheckResult(boolean hasCycle, String node1, String node2) {\n            this.hasCycle = hasCycle;\n            this.node1 = node1;\n            this.node2 = node2;\n        }\n\n        public static CheckResult noCycle() {\n            return new CheckResult(false, null, null);\n        }\n\n        public static CheckResult cycle(String node1, String node2) {\n            return new CheckResult(true, node1, node2);\n        }\n\n        public boolean isHasCycle() {\n            return hasCycle;\n        }\n\n        public String getNode1() {\n            return node1;\n        }\n\n        public String getNode2() {\n            return node2;\n        }\n\n        @Override\n        public String toString() {\n            return hasCycle ? getNode1() + \" <-> \" + getNode2() : \"no cycle\";\n        }\n    }\n\n    public static CheckResult check(Map<String, Object> m) {\n        return check(\"root\", m);\n    }\n\n    public static CheckResult check(String rootName, Map<String, Object> m) {\n        Deque<N> visited = new ArrayDeque<>();\n        return hasCycle(new N(rootName, m), visited);\n    }\n\n    private static CheckResult hasCycle(N node, Deque<N> visited) {\n        if (node.getObject() == null) {\n            return CheckResult.noCycle();\n        }\n\n        N n = find(visited, node);\n        if (n != null) {\n            return CheckResult.cycle(node.path, n.path);\n        }\n\n        visited.push(node);\n        for (N nextNode : getNeighbours(node)) {\n            CheckResult result = hasCycle(nextNode, visited);\n            if (result.hasCycle) {\n                return result;\n            }\n        }\n        visited.pop();\n        return CheckResult.noCycle();\n    }\n\n    private static N find(Collection<N> s, N n) {\n        for (N v : s) {\n            if (v.equals(n)) {\n                return v;\n            }\n        }\n\n        return null;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static List<N> getNeighbours(N n) {\n        Object element = n.getObject();\n\n        if (element instanceof Map) {\n            Map<String, Object> m = (Map<String, Object>) element;\n\n            List<N> result = new ArrayList<>(m.size());\n            m.forEach((key, value) -> result.add(new N(n.getPath() + \".\" + key, value)));\n            return result;\n        } else if (element instanceof Collection) {\n            Collection<Object> c = (Collection<Object>) element;\n\n            List<N> result = new ArrayList<>(c.size());\n            c.forEach(v -> result.add(new N(n.getPath(), v)));\n            return result;\n        }\n        return Collections.emptyList();\n    }\n\n    private static class N {\n        private final String path;\n        private final Object object;\n\n        public N(String path, Object object) {\n            this.path = path;\n            this.object = object;\n        }\n\n        public String getPath() {\n            return path;\n        }\n\n        public Object getObject() {\n            return object;\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n            N n = (N) o;\n            return object == n.object;\n        }\n\n        @Override\n        public int hashCode() {\n            return Objects.hash(object);\n        }\n\n        @Override\n        public String toString() {\n            return getPath();\n        }\n    }\n\n    private CycleChecker() {\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/DateTimeUtils.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.time.OffsetDateTime;\nimport java.time.format.DateTimeFormatter;\n\npublic final class DateTimeUtils {\n\n    private static final DateTimeFormatter FORMAT = DateTimeFormatter.ISO_OFFSET_DATE_TIME;\n\n    /**\n     * Formats the supplied {@link OffsetDateTime} to an ISO-8601 string\n     * ({@code yyyy-MM-ddTHH:mm:ss.SSSX})\n     */\n    public static String toIsoString(OffsetDateTime t) {\n        return FORMAT.format(t);\n    }\n\n    /**\n     * Parses the supplied {@code text} as an ISO-8601 date/time value\n     * ({@code yyyy-MM-ddTHH:mm:ss.SSSX}).\n     */\n    public static OffsetDateTime fromIsoString(CharSequence text) {\n        return OffsetDateTime.parse(text, FORMAT);\n    }\n\n    private DateTimeUtils() {\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/DockerProcessBuilder.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.DockerContainerSpec;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.*;\n\npublic class DockerProcessBuilder {\n\n    public static DockerProcessBuilder from(UUID txId, DockerContainerSpec spec) {\n        DockerProcessBuilder b = new DockerProcessBuilder(spec.image())\n                .name(spec.name())\n                .user(Optional.ofNullable(spec.user()).orElse(DEFAULT_USER))\n                .workdir(spec.workdir())\n                .entryPoint(spec.entryPoint())\n                .cpu(spec.cpu())\n                .memory(spec.memory())\n                .args(spec.args())\n                .env(spec.env())\n                .envFile(spec.envFile())\n                .labels(spec.labels())\n                .debug(spec.debug())\n                .forcePull(spec.forcePull())\n                .stdOutFilePath(spec.stdOutFilePath())\n                .redirectErrorStream(spec.redirectErrorStream());\n\n        DockerContainerSpec.Options options = spec.options();\n        if (options != null) {\n            DockerOptionsBuilder ob = new DockerOptionsBuilder();\n\n            List<String> hosts = options.hosts();\n            if (hosts != null) {\n                hosts.forEach(ob::etcHost);\n            }\n\n            b.options(ob.build());\n        }\n\n        // system stuff\n        b.addLabel(CONCORD_TX_ID_LABEL, txId.toString());\n\n        return b;\n    }\n\n    public static DockerProcessBuilder from(Context ctx, DockerContainerSpec spec) {\n        String txId = (String) ctx.getVariable(Constants.Context.TX_ID_KEY);\n        return from(UUID.fromString(txId), spec);\n    }\n\n    private static final Logger log = LoggerFactory.getLogger(DockerProcessBuilder.class);\n\n    public static final String CONCORD_DOCKER_LOCAL_MODE_KEY = \"CONCORD_DOCKER_LOCAL_MODE\";\n    public static final String CONCORD_DOCKER_DEFAULT_USER_KEY = \"CONCORD_DOCKER_DEFAULT_USER\";\n    public static final String CONCORD_DOCKER_USE_CONTAINER_USER_KEY = \"CONCORD_DOCKER_USE_CONTAINER_USER\";\n\n    public static final String CONCORD_TX_ID_LABEL = \"concordTxId\";\n\n    private static final String DEFAULT_USER;\n\n    static {\n        String s = System.getenv(CONCORD_DOCKER_DEFAULT_USER_KEY);\n        if (s != null) {\n            DEFAULT_USER = s;\n        } else {\n            DEFAULT_USER = \"456\"; // as in dockerPasswd\n        }\n    }\n\n    private final String image;\n\n    private String name;\n    private String user = DEFAULT_USER;\n    private String workdir;\n    private String entryPoint;\n    private String cpu;\n    private String memory;\n    private String stdOutFilePath;\n\n    private final List<String> args = new ArrayList<>();\n    private Map<String, String> env;\n    private String envFile;\n    private Map<String, String> labels;\n    private Collection<String> volumes = new ArrayList<>();\n    private List<Map.Entry<String, String>> options = new ArrayList<>();\n\n    private boolean cleanup = true;\n    private boolean debug = false;\n    private boolean forcePull = true;\n    private boolean generateUsers = false;\n    private boolean exposeHostUsers = false;\n    private boolean useHostUser = false;\n    private boolean useHostNetwork = true;\n    private boolean useContainerUser;\n\n    private boolean redirectErrorStream = true;\n\n    private final List<Path> tmpPaths = new ArrayList<>();\n\n    public DockerProcessBuilder(String image) {\n        this.image = image;\n\n        if (Boolean.parseBoolean(env(CONCORD_DOCKER_LOCAL_MODE_KEY, \"true\"))) {\n            // in the \"local docker mode\" we run all Docker processes using the current OS user's UID/GID\n            // in order to do that, we need to mount the local /etc/passwd inside of the container\n            log.warn(\"Running in the local Docker mode. Consider setting {}=false in the production environment.\", CONCORD_DOCKER_LOCAL_MODE_KEY);\n\n            this.exposeHostUsers = true;\n            this.useHostUser = true;\n        } else {\n            this.generateUsers = true;\n        }\n\n        this.useContainerUser = Boolean.parseBoolean(env(CONCORD_DOCKER_USE_CONTAINER_USER_KEY, \"false\"));\n    }\n\n    public DockerProcess build() throws IOException {\n        String[] cmd = buildCmd();\n\n        if (debug) {\n            log.info(\"CMD: {}\", (Object) cmd);\n        }\n\n        return new DockerProcess(cmd, redirectErrorStream, tmpPaths);\n    }\n\n    private String[] buildCmd() throws IOException {\n        if (forcePull) {\n            return new String[]{\"/bin/bash\", \"-c\", \"docker pull \" + q(image) + \" && \" + buildDockerCmd()};\n        } else {\n            return new String[]{\"/bin/bash\", \"-c\", buildDockerCmd()};\n        }\n    }\n\n    private String buildDockerCmd() throws IOException {\n        List<String> c = new ArrayList<>();\n        c.add(\"docker\");\n        c.add(\"run\");\n        if (name != null) {\n            c.add(\"--name\");\n            c.add(q(name));\n        }\n        if (user != null && !useHostUser && !useContainerUser) {\n            c.add(\"-u\");\n            c.add(user);\n        }\n        if (cleanup) {\n            c.add(\"--rm\");\n        }\n        c.add(\"-i\");\n        if (volumes != null) {\n            volumes.forEach(v -> {\n                c.add(\"-v\");\n                c.add(q(v));\n            });\n        }\n        if (env != null) {\n            env.forEach((k, v) -> {\n                c.add(\"-e\");\n                c.add(q(k + \"=\" + v));\n            });\n        }\n        if (envFile != null) {\n            c.add(\"--env-file\");\n            c.add(q(envFile));\n        }\n        if (workdir != null) {\n            c.add(\"-w\");\n            c.add(q(workdir));\n        }\n        if (labels != null) {\n            for (Map.Entry<String, String> l : labels.entrySet()) {\n                String k = l.getKey();\n                String v = l.getValue();\n\n                c.add(\"--label\");\n                c.add(q(k + (v != null ? \"=\" + v : \"\")));\n            }\n        }\n        if (entryPoint != null) {\n            c.add(\"--entrypoint\");\n            c.add(entryPoint);\n        }\n        if (generateUsers) {\n            Path tmp = PathUtils.createTempFile(\"passwd\", \".docker\"); // NOSONAR\n            tmpPaths.add(tmp);\n\n            try (InputStream src = Objects.requireNonNull(DockerProcessBuilder.class.getResourceAsStream(\"dockerPasswd\"));\n                 OutputStream dst = Files.newOutputStream(tmp)) {\n                src.transferTo(dst);\n            }\n            c.add(\"-v\");\n            c.add(tmp.toAbsolutePath() + \":/etc/passwd:ro\");\n        }\n        if (exposeHostUsers) {\n            c.add(\"-v\");\n            c.add(\"/etc/passwd:/etc/passwd:ro\");\n        }\n        if (useHostUser && !useContainerUser) {\n            c.add(\"-u\");\n            c.add(\"`id -u`:`id -g`\");\n\n            c.add(\"-e\");\n            c.add(\"HOME=/tmp\");\n        }\n        if (useHostNetwork) {\n            c.add(\"--net=host\");\n        }\n        if (cpu != null) {\n            c.add(\"--cpus\");\n            c.add(cpu);\n        }\n        if (memory != null) {\n            c.add(\"-m\");\n            c.add(memory);\n        }\n        options.forEach(o -> {\n            c.add(o.getKey());\n            if (o.getValue() != null) {\n                c.add(o.getValue());\n            }\n        });\n        c.add(q(image));\n\n        args.forEach(a -> c.add(q(a)));\n\n        if (stdOutFilePath != null) {\n            c.add(0, \"set -o pipefail && \");\n            c.add(\"| tee \");\n            c.add(stdOutFilePath);\n        }\n        return String.join(\" \", c);\n    }\n\n    public DockerProcessBuilder cpu(String cpu) {\n        this.cpu = cpu;\n        return this;\n    }\n\n    public DockerProcessBuilder memory(String memory) {\n        this.memory = memory;\n        return this;\n    }\n\n    public DockerProcessBuilder stdOutFilePath(String stdOutFilePath) {\n        this.stdOutFilePath = stdOutFilePath;\n        return this;\n    }\n\n    public DockerProcessBuilder name(String name) {\n        this.name = name;\n        return this;\n    }\n\n    public DockerProcessBuilder user(String user) {\n        this.user = user;\n        return this;\n    }\n\n    public DockerProcessBuilder labels(Map<String, String> labels) {\n        this.labels = labels;\n        return this;\n    }\n\n    public DockerProcessBuilder addLabel(String k, String v) {\n        if (labels == null) {\n            labels = new HashMap<>();\n        }\n        labels.put(k, v);\n        return this;\n    }\n\n    public DockerProcessBuilder debug(boolean debug) {\n        this.debug = debug;\n        return this;\n    }\n\n    public DockerProcessBuilder workdir(String workdir) {\n        this.workdir = workdir;\n        return this;\n    }\n\n    public DockerProcessBuilder volumes(Collection<String> volumes) {\n        this.volumes = volumes;\n        return this;\n    }\n\n    public DockerProcessBuilder volume(String spec) {\n        volumes.add(spec);\n        return this;\n    }\n\n    public DockerProcessBuilder volume(String hostSrc, String containerDest) {\n        volumes.add(hostSrc + \":\" + containerDest);\n        return this;\n    }\n\n    public DockerProcessBuilder volume(String hostSrc, String containerDest, boolean readOnly) {\n        volumes.add(hostSrc + \":\" + containerDest + (readOnly ? \":ro\" : \":rw\"));\n        return this;\n    }\n\n    public DockerProcessBuilder cleanup(boolean cleanup) {\n        this.cleanup = cleanup;\n        return this;\n    }\n\n    public DockerProcessBuilder args(List<String> args) {\n        if (args == null) {\n            return this;\n        }\n\n        this.args.addAll(args);\n        return this;\n    }\n\n    public DockerProcessBuilder arg(String v) {\n        this.args.add(v);\n        return this;\n    }\n\n    public DockerProcessBuilder arg(String k, String v) {\n        this.args.add(k);\n        this.args.add(v);\n        return this;\n    }\n\n    public DockerProcessBuilder env(Map<String, String> env) {\n        this.env = env;\n        return this;\n    }\n\n    public DockerProcessBuilder envFile(String envFile) {\n        this.envFile = envFile;\n        return this;\n    }\n\n    public DockerProcessBuilder entryPoint(String entryPoint) {\n        this.entryPoint = entryPoint;\n        return this;\n    }\n\n    public DockerProcessBuilder forcePull(boolean forcePull) {\n        this.forcePull = forcePull;\n        return this;\n    }\n\n    public DockerProcessBuilder useHostNetwork(boolean useHostNetwork) {\n        this.useHostNetwork = useHostNetwork;\n        return this;\n    }\n\n    public DockerProcessBuilder options(List<Map.Entry<String, String>> options) {\n        this.options = options;\n        return this;\n    }\n\n    public DockerProcessBuilder option(String k, String v) {\n        this.options.add(new AbstractMap.SimpleEntry<>(k, v));\n        return this;\n    }\n\n    public DockerProcessBuilder redirectErrorStream(boolean redirectErrorStream) {\n        this.redirectErrorStream = redirectErrorStream;\n        return this;\n    }\n\n    public DockerProcessBuilder useContainerUser(boolean useContainerUser) {\n        this.useContainerUser = useContainerUser;\n        return this;\n    }\n\n    private static String q(String s) {\n        if (s == null) {\n            return null;\n        }\n\n        return \"'\" + s + \"'\";\n    }\n\n    private static String env(String k, String defaultValue) {\n        String s = System.getenv(k);\n        return s != null ? s : defaultValue;\n    }\n\n    public static class DockerOptionsBuilder {\n\n        private final List<Map.Entry<String, String>> options = new ArrayList<>();\n\n        public DockerOptionsBuilder etcHost(String host) {\n            this.options.add(new AbstractMap.SimpleEntry<>(\"--add-host\", host));\n            return this;\n        }\n\n        public List<Map.Entry<String, String>> build() {\n            return options;\n        }\n    }\n\n    public static class DockerProcess implements AutoCloseable {\n\n        private final String[] cmd;\n        private final boolean redirectErrorStream;\n        private final List<Path> tmpPaths;\n\n        public DockerProcess(String[] cmd, boolean redirectErrorStream, List<Path> tmpPaths) {\n            this.cmd = cmd;\n            this.redirectErrorStream = redirectErrorStream;\n            this.tmpPaths = tmpPaths;\n        }\n\n        public Process start() throws IOException {\n            return PrivilegedAction.perform(\"docker\", () -> new ProcessBuilder(cmd)\n                    .redirectErrorStream(redirectErrorStream)\n                    .start());\n        }\n\n        public String[] cmd() {\n            return cmd;\n        }\n\n        @Override\n        public void close() {\n            for (Path p : tmpPaths) {\n                try {\n                    PathUtils.deleteRecursively(p);\n                } catch (IOException e) {\n                    log.warn(\"delete '{}' -> error: {}\", p, e.getMessage());\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/DynamicTaskRegistry.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Task;\n\npublic interface DynamicTaskRegistry {\n\n    Task getByKey(String key);\n\n    void register(Class<? extends Task> c);\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/ExceptionUtils.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\n\npublic final class ExceptionUtils {\n\n    public static List<Throwable> getExceptionList(Throwable e) {\n        List<Throwable> list = new ArrayList<>();\n        while (e != null && !list.contains(e)) {\n            list.add(e);\n            e = e.getCause();\n        }\n        return list;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <T extends Throwable> T findLastException(T e, Class<T> clazz) {\n        var exceptions = getExceptionList(e);\n\n        for (int i = exceptions.size() - 1; i >= 0; i--) {\n            var ex = exceptions.get(i);\n            if (clazz.isInstance(ex)) {\n                return (T) ex;\n            }\n        }\n\n        return e;\n    }\n\n    private ExceptionUtils() {\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/ExternalAuthToken.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.time.Duration;\nimport java.time.OffsetDateTime;\n\n@JsonDeserialize(as = ImmutableSimpleToken.class)\npublic interface ExternalAuthToken {\n\n    @Nullable\n    @JsonProperty(\"auth_id\")\n    String authId();\n\n    @JsonProperty(\"token\")\n    String token();\n\n    @Nullable\n    @JsonProperty(\"username\")\n    String username();\n\n    @Nullable\n    @JsonProperty(\"expires_at\")\n    // GitHub gives time in seconds, but most parsers (e.g. jackson) expect milliseconds\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss[.SSS]X\")\n    OffsetDateTime expiresAt();\n\n    @Value.Default\n    @JsonIgnore\n    default long secondsUntilExpiration() {\n        if (expiresAt() == null) {\n            return Long.MAX_VALUE;\n        }\n\n        var d = Duration.between(OffsetDateTime.now(), expiresAt());\n        return d.getSeconds();\n    }\n\n    /**\n     * Basic implementation of an expiring token.\n     */\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface SimpleToken extends ExternalAuthToken {\n        static ImmutableSimpleToken.Builder builder() {\n            return ImmutableSimpleToken.builder();\n        }\n    }\n\n    /**\n     * A token that effectively never expires.\n     */\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface StaticToken extends ExternalAuthToken {\n\n        @Value.Default\n        @Nullable\n        @Override\n        default OffsetDateTime expiresAt() {\n            return null;\n        }\n\n        static ImmutableStaticToken.Builder builder() {\n            return ImmutableStaticToken.builder();\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/FileVisitor.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.nio.file.Path;\n\npublic interface FileVisitor {\n\n    void visit(Path sourceFile, Path dstFile) throws IOException;\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/GrepUtils.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.*;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic final class GrepUtils {\n\n    public static List<String> grep(String pattern, byte[] ab) throws IOException {\n        return grep(pattern, new ByteArrayInputStream(ab));\n    }\n\n    public static List<String> grep(String pattern, InputStream in) throws IOException {\n        List<String> result = new ArrayList<>();\n        try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {\n            String line;\n            while ((line = reader.readLine()) != null) {\n                if (line.matches(pattern)) {\n                    result.add(line);\n                }\n            }\n        }\n        return result;\n    }\n\n    private GrepUtils() {\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/IOUtils.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;\n\nimport javax.validation.constraints.NotNull;\nimport java.io.*;\nimport java.nio.file.CopyOption;\nimport java.nio.file.Path;\nimport java.util.List;\n\n/**\n * @deprecated use the alternatives in {@link PathUtils}, {@link ZipUtils}, {@link GrepUtils}, etc.\n */\n@Deprecated\npublic final class IOUtils {\n\n    /**\n     * @deprecated use {@link PathUtils#TMP_DIR_KEY}\n     */\n    @Deprecated\n    public static final String TMP_DIR_KEY = PathUtils.TMP_DIR_KEY;\n\n    /**\n     * @deprecated use {@link PathUtils#TMP_DIR}\n     */\n    @Deprecated\n    public static final Path TMP_DIR = PathUtils.TMP_DIR;\n\n    /**\n     * @deprecated use {@link PathUtils#tempFile(String, String)}\n     */\n    @Deprecated\n    public static TemporaryPath tempFile(String prefix, String suffix) throws IOException {\n        return PathUtils.tempFile(prefix, suffix);\n    }\n\n    /**\n     * @deprecated use {@link PathUtils#createTempFile(String, String)}\n     */\n    @Deprecated\n    public static Path createTempFile(String prefix, String suffix) throws IOException {\n        return PathUtils.createTempFile(prefix, suffix);\n    }\n\n    /**\n     * @deprecated use {@link PathUtils#createTempDir(Path, String)}\n     */\n    @Deprecated\n    public static Path createTempDir(Path dir, String prefix) throws IOException {\n        return PathUtils.createTempDir(dir, prefix);\n    }\n\n    /**\n     * @deprecated use {@link PathUtils#tempDir(String)}\n     */\n    @Deprecated\n    public static TemporaryPath tempDir(String prefix) throws IOException {\n        return PathUtils.tempDir(prefix);\n    }\n\n    /**\n     * @deprecated use {@link PathUtils#createTempDir(String)}\n     */\n    @Deprecated\n    public static Path createTempDir(String prefix) throws IOException {\n        return PathUtils.createTempDir(prefix);\n    }\n\n    /**\n     * @deprecated use {@link ZipUtils#zipFile(ZipArchiveOutputStream, Path, String)}\n     */\n    @Deprecated\n    public static void zipFile(ZipArchiveOutputStream zip, Path src, String name) throws IOException {\n        ZipUtils.zipFile(zip, src, name);\n    }\n\n    /**\n     * @deprecated use {@link ZipUtils#zip(ZipArchiveOutputStream, Path, String...)}\n     */\n    @Deprecated\n    public static void zip(ZipArchiveOutputStream zip, Path srcDir, String... filters) throws IOException {\n        ZipUtils.zip(zip, srcDir, filters);\n    }\n\n    /**\n     * @deprecated use {@link ZipUtils#zip(ZipArchiveOutputStream, String, Path, String...)}\n     */\n    @Deprecated\n    public static void zip(ZipArchiveOutputStream zip, String dstPrefix, Path srcDir, String... filters) throws IOException {\n        ZipUtils.zip(zip, dstPrefix, srcDir, filters);\n    }\n\n    /**\n     * @deprecated use {@link ZipUtils#unzip(InputStream, Path, CopyOption...)}\n     */\n    @Deprecated\n    public static void unzip(InputStream in, Path targetDir, CopyOption... options) throws IOException {\n        ZipUtils.unzip(in, targetDir, options);\n    }\n\n    /**\n     * @deprecated use {@link ZipUtils#unzip(Path, Path, CopyOption...)}\n     */\n    @Deprecated\n    public static void unzip(Path in, Path targetDir, CopyOption... options) throws IOException {\n        ZipUtils.unzip(in, targetDir, options);\n    }\n\n    /**\n     * @deprecated use {@link ZipUtils#unzip(Path, Path, boolean, CopyOption...)}\n     */\n    @Deprecated\n    public static void unzip(Path in, Path targetDir, boolean skipExisting, CopyOption... options) throws IOException {\n        ZipUtils.unzip(in, targetDir, skipExisting, options);\n    }\n\n    /**\n     * @deprecated use {@link ZipUtils#unzip(InputStream, Path, boolean, FileVisitor, CopyOption...)}\n     */\n    @Deprecated\n    public static void unzip(InputStream in, Path targetDir, boolean skipExisting, FileVisitor visitor, CopyOption... options) throws IOException {\n        ZipUtils.unzip(in, targetDir, skipExisting, visitor, options);\n    }\n\n    /**\n     * @deprecated use {@link ZipUtils#unzip(Path, Path, boolean, FileVisitor, CopyOption...)}\n     */\n    @Deprecated\n    public static void unzip(Path in, Path targetDir, boolean skipExisting, FileVisitor visitor, CopyOption... options) throws IOException {\n        ZipUtils.unzip(in, targetDir, skipExisting, visitor, options);\n    }\n\n    /**\n     * @deprecated use {@link InputStream#transferTo(OutputStream)}\n     */\n    @Deprecated\n    public static void copy(InputStream in, OutputStream out) throws IOException {\n        byte[] ab = new byte[4096];\n        int read;\n        while ((read = in.read(ab)) > 0) {\n            out.write(ab, 0, read);\n        }\n    }\n\n    /**\n     * @deprecated use {@link PathUtils#copy(Path, Path)}\n     */\n    @Deprecated\n    public static void copy(Path src, Path dst) throws IOException {\n        PathUtils.copy(src, dst);\n    }\n\n    /**\n     * @deprecated use {@link PathUtils#copy(Path, Path, CopyOption...)}\n     */\n    @Deprecated\n    public static void copy(Path src, Path dst, CopyOption... options) throws IOException {\n        PathUtils.copy(src, dst, options);\n    }\n\n    /**\n     * @deprecated use {@link PathUtils#copy(Path, Path, String, CopyOption...)}\n     */\n    @Deprecated\n    public static void copy(Path src, Path dst, String ignorePattern, CopyOption... options) throws IOException {\n        PathUtils.copy(src, dst, ignorePattern, options);\n    }\n\n    /**\n     * @deprecated use {@link PathUtils#copy(Path, Path, String, FileVisitor, CopyOption...)}\n     */\n    @Deprecated\n    public static void copy(Path src, Path dst, String skipContents, FileVisitor visitor, CopyOption... options) throws IOException {\n        PathUtils.copy(src, dst, skipContents, visitor, options);\n    }\n\n    /**\n     * @deprecated use {@link PathUtils#copy(Path, Path, List, FileVisitor, CopyOption...)}\n     */\n    @Deprecated\n    public static void copy(Path src, Path dst, List<String> skipContents, FileVisitor visitor, CopyOption... options) throws IOException {\n        PathUtils.copy(src, dst, skipContents, visitor, options);\n    }\n\n    /**\n     * @deprecated use {@link GrepUtils#grep(String, byte[])}\n     */\n    @Deprecated\n    public static List<String> grep(String pattern, byte[] ab) throws IOException {\n        return GrepUtils.grep(pattern, ab);\n    }\n\n    /**\n     * @deprecated use {@link GrepUtils#grep(String, InputStream)}\n     */\n    @Deprecated\n    public static List<String> grep(String pattern, InputStream in) throws IOException {\n        return GrepUtils.grep(pattern, in);\n    }\n\n    /**\n     * @deprecated use {@link PathUtils#deleteRecursively(Path)}\n     */\n    @Deprecated\n    public static boolean deleteRecursively(Path p) throws IOException {\n        return PathUtils.deleteRecursively(p);\n    }\n\n    /**\n     * @deprecated use {@link InputStream#readAllBytes()}\n     */\n    @Deprecated\n    public static byte[] toByteArray(InputStream src) throws IOException {\n        ByteArrayOutputStream dst = new ByteArrayOutputStream();\n        copy(src, dst);\n        return dst.toByteArray();\n    }\n\n    /**\n     * @deprecated use {@link PathUtils#delete(File)}\n     */\n    @Deprecated\n    public static void delete(File f) {\n        PathUtils.delete(f);\n    }\n\n    /**\n     * @deprecated use {@link PathUtils#assertInPath(Path, String)}\n     */\n    @Deprecated\n    public static Path assertInPath(@NotNull Path parent, @NotNull String child) throws IOException {\n        return PathUtils.assertInPath(parent, child);\n    }\n\n    private IOUtils() {\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/LogUtils.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.MDC;\nimport org.slf4j.helpers.FormattingTuple;\nimport org.slf4j.helpers.MessageFormatter;\n\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.concurrent.Callable;\n\npublic final class LogUtils {\n\n    // the UI expects log timestamps in a specific format to be able to convert it to the local time\n    // see also runner/src/main/resources/logback.xml and console2/src/components/molecules/ProcessLogViewer/datetime.tsx\n    private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern(\"yyyy-MM-dd'T'HH:mm:ss.SSSZ\", Locale.US);\n    private static final ZoneId DEFAULT_TIMESTAMP_TZ = ZoneId.of(\"UTC\");\n\n    public enum LogLevel {\n        DEBUG,\n        INFO,\n        WARN,\n        ERROR\n    }\n\n    public static String formatMessage(LogLevel level, String log, Object... args) {\n        String timestamp = ZonedDateTime.now(DEFAULT_TIMESTAMP_TZ).format(TIMESTAMP_FORMAT);\n        FormattingTuple m = MessageFormatter.arrayFormat(log, args);\n        if (m.getThrowable() != null) {\n            return String.format(\"%s [%-5s] %s%n%s%n\", timestamp, level.name(), m.getMessage(), formatException(m.getThrowable()));\n        }\n\n        return String.format(\"%s [%-5s] %s%n\", timestamp, level.name(), m.getMessage());\n    }\n\n    public static Runnable withMdc(Runnable runnable) {\n        Map<String, String> mdc = MDC.getCopyOfContextMap();\n        return () -> {\n            if (mdc != null) {\n                MDC.setContextMap(mdc);\n            }\n            try {\n                runnable.run();\n            } finally {\n                if (mdc != null) {\n                    MDC.clear();\n                }\n            }\n        };\n    }\n\n    public static <T> Callable<T> withMdc(Callable<T> callable) {\n        Map<String, String> mdc = MDC.getCopyOfContextMap();\n        return () -> {\n            if (mdc != null) {\n                MDC.setContextMap(mdc);\n            }\n            try {\n                return callable.call();\n            } finally {\n                if (mdc != null) {\n                    MDC.clear();\n                }\n            }\n        };\n    }\n\n    private static String formatException(Throwable t) {\n        StringWriter sw = new StringWriter();\n        t.printStackTrace(new PrintWriter(sw));\n        return sw.toString();\n    }\n\n    private LogUtils() {\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/Matcher.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Collection;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.regex.Pattern;\n\npublic final class Matcher {\n\n    public static boolean matches(Object data, Object conditions) {\n        return compareNodes(data, conditions);\n    }\n\n    public static boolean matchAny(Object condition, Collection<Object> nodes) {\n        for (Object n : nodes) {\n            boolean result = compareNodes(n, condition);\n            if (result) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    public static <T> boolean matchAny(Collection<T> conditions, T data) {\n        for (T c : conditions) {\n            boolean result = compareNodes(data, c);\n            if (result) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    private static boolean compareNodes(Object data, Object conditions) {\n        data = normalizeNull(data, conditions);\n\n        if (data == null && conditions == null) {\n            return true;\n        } else if ((data == null && !(conditions instanceof Collection)) || conditions == null) {\n            return false;\n        }\n\n        if (conditions instanceof Map && data instanceof Map) {\n            return compareObjectNodes((Map<String, Object>) data, (Map<String, Object>) conditions);\n        } else if (conditions instanceof String && data instanceof UUID) {\n            return compareStringValues(data.toString(), (String)conditions);\n        } else if (conditions instanceof String && data instanceof String) {\n            return compareStringValues((String) data, (String) conditions);\n        } else if (conditions instanceof Collection && data instanceof Collection) {\n            return compareArrayNodes((Collection) data, (Collection) conditions);\n        } else if (conditions instanceof Collection) {\n            return matchAny((Collection) conditions, data);\n        } else if (data instanceof Collection) {\n            return matchAny(conditions, (Collection)data);\n        } else {\n            return compareValues(data, conditions);\n        }\n    }\n\n    private static Object normalizeNull(Object data, Object conditions) {\n        if (data != null) {\n            return data;\n        }\n\n        if (conditions instanceof String) {\n            return \"\";\n        }\n\n        return null;\n    }\n\n    private static boolean compareObjectNodes(Map<String, Object> data, Map<String, Object> conditions) {\n        if (conditions.isEmpty() && !data.isEmpty()) {\n            return false;\n        }\n\n        for (Map.Entry<String, Object> e : conditions.entrySet()) {\n            Object dataItem = data.get(e.getKey());\n            if (!compareNodes(dataItem, e.getValue())) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    private static boolean compareStringValues(String value, String condition) {\n        return Pattern.compile(condition, Pattern.CASE_INSENSITIVE).matcher(value).matches();\n    }\n\n    private static boolean compareArrayNodes(Collection<Object> dataElements, Collection<Object> conditionElements) {\n        if (conditionElements.size() > dataElements.size()) {\n            return false;\n        }\n\n        if (conditionElements.isEmpty() && !dataElements.isEmpty()) {\n            return false;\n        }\n\n        for (Object c : conditionElements) {\n            boolean matched = matchAny(c, dataElements);\n            if (!matched) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    private static boolean compareValues(Object dataValue, Object conditionValue) {\n        return dataValue.equals(conditionValue);\n    }\n\n    private Matcher() {\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/MemoSupplier.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.function.Supplier;\n\n/**\n * A memoizing {@link Supplier}. Not thread-safe.\n */\npublic class MemoSupplier<T> implements Supplier<T> {\n\n    public static <T> Supplier<T> memo(Supplier<T> delegate) {\n        return new MemoSupplier<>(delegate);\n    }\n\n    private final Supplier<T> delegate;\n\n    private volatile boolean initialized;\n\n    T value;\n\n    public MemoSupplier(Supplier<T> delegate) {\n        this.delegate = delegate;\n    }\n\n    @Override\n    public synchronized T get() {\n        if (!initialized) {\n            T t = delegate.get();\n            value = t;\n            initialized = true;\n            return t;\n        }\n        return value;\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/ObjectInputStreamWithClassLoader.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.ObjectInputStream;\nimport java.io.ObjectStreamClass;\nimport java.lang.reflect.Proxy;\n\n/**\n * Almost a carbon copy of <a href=\"https://commons.apache.org/proper/commons-io/apidocs/org/apache/commons/io/input/ClassLoaderObjectInputStream.html\">ClassLoaderObjectInputStream</a>\n * from commons-io.\n */\npublic class ObjectInputStreamWithClassLoader extends ObjectInputStream {\n\n    private final ClassLoader classLoader;\n\n    public ObjectInputStreamWithClassLoader(InputStream in, ClassLoader classLoader) throws IOException {\n        super(in);\n        this.classLoader = classLoader;\n    }\n\n    @Override\n    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {\n        try {\n            return Class.forName(desc.getName(), false, classLoader);\n        } catch (ClassNotFoundException e) {\n            // delegate to super class loader which can resolve primitives\n            return super.resolveClass(desc);\n        }\n    }\n\n    @Override\n    protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException {\n        Class<?>[] interfaceClasses = new Class[interfaces.length];\n        for (int i = 0; i < interfaces.length; i++) {\n            interfaceClasses[i] = Class.forName(interfaces[i], false, classLoader);\n        }\n\n        try {\n            return Proxy.getProxyClass(classLoader, interfaceClasses);\n        } catch (IllegalArgumentException e) {\n            return super.resolveProxyClass(interfaces);\n        }\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/ObjectMapperProvider.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *  \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.DeserializationFeature;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.SerializationFeature;\nimport com.fasterxml.jackson.datatype.guava.GuavaModule;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Provider;\n\npublic class ObjectMapperProvider implements Provider<ObjectMapper> {\n\n    private static final Logger log = LoggerFactory.getLogger(ObjectMapperProvider.class);\n\n    @Override\n    public ObjectMapper get() {\n        log.debug(\"Using concord-common's ObjectMapper...\");\n\n        ObjectMapper mapper = new ObjectMapper()\n                .registerModule(new Jdk8Module())\n                .registerModule(new GuavaModule())\n                .registerModule(new JavaTimeModule());\n\n        // Write dates as ISO-8601\n        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);\n\n        // Ignore unknown properties\n        mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);\n\n        // Ignore nulls\n        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);\n\n        return mapper;\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/PathUtils.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.validation.constraints.NotNull;\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.*;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.util.Collections;\nimport java.util.List;\n\npublic final class PathUtils {\n\n    public static final String TMP_DIR_KEY = \"CONCORD_TMP_DIR\";\n    public static final Path TMP_DIR = Paths.get(getEnv(TMP_DIR_KEY, System.getProperty(\"java.io.tmpdir\")));\n\n    private static final Logger log = LoggerFactory.getLogger(PathUtils.class);\n\n    static {\n        try {\n            if (!Files.exists(TMP_DIR)) {\n                Files.createDirectories(TMP_DIR);\n            }\n            log.debug(\"Using {} as CONCORD_TMP_DIR\", TMP_DIR);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static TemporaryPath tempFile(String prefix, String suffix) throws IOException {\n        return new TemporaryPath(createTempFile(prefix, suffix));\n    }\n\n    public static Path createTempFile(String prefix, String suffix) throws IOException {\n        return Files.createTempFile(TMP_DIR, prefix, suffix);\n    }\n\n    public static Path createTempDir(Path dir, String prefix) throws IOException {\n        return Files.createTempDirectory(dir, prefix);\n    }\n\n    public static TemporaryPath tempDir(String prefix) throws IOException {\n        return new TemporaryPath(createTempDir(prefix));\n    }\n\n    public static Path createTempDir(String prefix) throws IOException {\n        return Files.createTempDirectory(TMP_DIR, prefix);\n    }\n\n    public static boolean deleteRecursively(Path p) throws IOException {\n        if (!Files.exists(p)) {\n            return false;\n        }\n\n        if (!Files.isDirectory(p)) {\n            Files.delete(p);\n            return true;\n        }\n\n        Files.walkFileTree(p, new SimpleFileVisitor<Path>() {\n            @Override\n            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {\n                Files.delete(file);\n                return FileVisitResult.CONTINUE;\n            }\n\n            @Override\n            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {\n                Files.delete(dir);\n                return FileVisitResult.CONTINUE;\n            }\n        });\n\n        return true;\n    }\n\n    public static void delete(File f) {\n        if (f == null || !f.exists()) {\n            return;\n        }\n\n        if (!f.delete()) {\n            log.warn(\"delete ['{}'] -> failed\", f.getAbsolutePath());\n        }\n    }\n\n    /**\n     * Resolves a child path within a parent, asserting the normalized child\n     * starts with the parent path to avoid relative path escaping (e.g.\n     * {@code \"../../not/in/parent\"}).\n     * @param parent parent path within which child must exist when resolved\n     * @param child filename or path to resolve as a child of {@code parent}\n     * @return normalized child path\n     * @throws IOException when the child does not resolve to an absolute path within the parent path\n     */\n    public static Path assertInPath(@NotNull Path parent, @NotNull String child) throws IOException {\n        Path normalizedParent = parent.normalize().toAbsolutePath();\n        Path normalizedChild = normalizedParent.resolve(child).normalize().toAbsolutePath();\n\n        if (!normalizedChild.startsWith(normalizedParent)) {\n            throw new IOException(\"Child path resolves outside of parent path: \" + child);\n        }\n\n        return normalizedChild;\n    }\n\n    private static String getEnv(String key, String defaultValue) {\n        String s = System.getenv(key);\n        if (s == null) {\n            return defaultValue;\n        }\n        return s;\n    }\n\n    private PathUtils() {\n    }\n\n    public static void copy(Path src, Path dst) throws IOException {\n        copy(src, dst, (String) null, null, new CopyOption[0]);\n    }\n\n    public static void copy(Path src, Path dst, CopyOption... options) throws IOException {\n        copy(src, dst, (String) null, null, options);\n    }\n\n    public static void copy(Path src, Path dst, String ignorePattern, CopyOption... options) throws IOException {\n        _copy(src, src, dst, toList(ignorePattern), null, options);\n    }\n\n    public static void copy(Path src, Path dst, String skipContents, FileVisitor visitor, CopyOption... options) throws IOException {\n        _copy(src, src, dst, toList(skipContents), visitor, options);\n    }\n\n    public static void copy(Path src, Path dst, List<String> skipContents, FileVisitor visitor, CopyOption... options) throws IOException {\n        _copy(src, src, dst, skipContents, visitor, options);\n    }\n\n    private static void _copy(Path root, Path src, Path dst, List<String> ignorePattern, FileVisitor visitor, CopyOption... options) throws IOException {\n        Files.walkFileTree(src, new SimpleFileVisitor<Path>() {\n            @Override\n            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {\n                if (dir != src && anyMatch(src.relativize(dir).toString(), ignorePattern)) {\n                    return FileVisitResult.SKIP_SUBTREE;\n                }\n\n                return FileVisitResult.CONTINUE;\n            }\n\n            @Override\n            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {\n                if (file != src && anyMatch(src.relativize(file).toString(), ignorePattern)) {\n                    return FileVisitResult.CONTINUE;\n                }\n\n                Path a = file;\n                Path b = dst.resolve(src.relativize(file));\n\n                Path parent = b.getParent();\n                if (!Files.exists(parent)) {\n                    Files.createDirectories(parent);\n                }\n\n                if (Files.isSymbolicLink(file)) {\n                    Path link = Files.readSymbolicLink(file);\n                    Path target = file.getParent().resolve(link).normalize();\n\n                    if (!target.startsWith(root)) {\n                        throw new IOException(\"Symlinks outside the base directory are not supported: \" + file + \" -> \" + target);\n                    }\n\n                    if (Files.notExists(target)) {\n                        // missing target\n                        return FileVisitResult.CONTINUE;\n                    }\n\n                    Files.createSymbolicLink(b, link);\n                    return FileVisitResult.CONTINUE;\n                }\n\n                Files.copy(a, b, options);\n\n                if (visitor != null) {\n                    visitor.visit(a, b);\n                }\n\n                return FileVisitResult.CONTINUE;\n            }\n        });\n    }\n\n    private static List<String> toList(String entry) {\n        if (entry == null) {\n            return Collections.emptyList();\n        }\n\n        return Collections.singletonList(entry);\n    }\n\n    private static boolean anyMatch(String what, List<String> patterns) {\n        if (patterns == null) {\n            return false;\n        }\n\n        return patterns.stream().anyMatch(what::matches);\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/Posix.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.nio.file.attribute.PosixFilePermission;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.Set;\n\npublic final class Posix {\n\n    public static final int DEFAULT_UNIX_MODE = 420; // 0644\n\n    public static int unixMode(Set<PosixFilePermission> s) {\n        if (s == null || s.isEmpty()) {\n            return DEFAULT_UNIX_MODE;\n        }\n\n        int i = 0;\n        for (PosixFilePermission p : s) {\n            switch (p) {\n                case OWNER_EXECUTE:\n                    i += 64;  // 0100\n                    break;\n                case OWNER_WRITE:\n                    i += 128; // 0200\n                    break;\n                case OWNER_READ:\n                    i += 256; // 0400\n                    break;\n                case GROUP_EXECUTE:\n                    i += 8;   // 0010\n                    break;\n                case GROUP_WRITE:\n                    i += 16;  // 0020\n                    break;\n                case GROUP_READ:\n                    i += 32;  // 0040\n                    break;\n                case OTHERS_EXECUTE:\n                    i += 1;   // 0001\n                    break;\n                case OTHERS_WRITE:\n                    i += 2;   // 0002\n                    break;\n                case OTHERS_READ:\n                    i += 4;   // 0004\n                    break;\n            }\n        }\n        return i;\n    }\n\n    public static Set<PosixFilePermission> posix(int unixMode) {\n        if (unixMode <= 0) {\n            return Collections.emptySet();\n        }\n\n        Set<PosixFilePermission> s = new HashSet<>();\n\n        if ((unixMode & 64) == 64) {   // 0100\n            s.add(PosixFilePermission.OWNER_EXECUTE);\n        }\n        if ((unixMode & 128) == 128) { // 0200\n            s.add(PosixFilePermission.OWNER_WRITE);\n        }\n        if ((unixMode & 256) == 256) { // 0400\n            s.add(PosixFilePermission.OWNER_READ);\n        }\n        if ((unixMode & 8) == 8) {     // 0010\n            s.add(PosixFilePermission.GROUP_EXECUTE);\n        }\n        if ((unixMode & 16) == 16) {   // 0020\n            s.add(PosixFilePermission.GROUP_WRITE);\n        }\n        if ((unixMode & 32) == 32) {   // 0040\n            s.add(PosixFilePermission.GROUP_READ);\n        }\n        if ((unixMode & 1) == 1) {     // 0001\n            s.add(PosixFilePermission.OTHERS_EXECUTE);\n        }\n        if ((unixMode & 2) == 2) {     // 0002\n            s.add(PosixFilePermission.OTHERS_WRITE);\n        }\n        if ((unixMode & 4) == 4) {     // 0004\n            s.add(PosixFilePermission.OTHERS_READ);\n        }\n\n        return s;\n    }\n\n    private Posix() {\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/PrivilegedAction.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\n\npublic final class PrivilegedAction {\n\n    private static final ThreadLocal<String> currentDomain = new ThreadLocal<>();\n\n    public static String getCurrentDomain() {\n        return currentDomain.get();\n    }\n\n    public static <T> T perform(String domain, IOAction<T> f) throws IOException {\n        String prevDomain = currentDomain.get();\n        try {\n            currentDomain.set(domain);\n            return f.call();\n        } finally {\n            if (prevDomain == null) {\n                currentDomain.remove();\n            } else {\n                currentDomain.set(prevDomain);\n            }\n        }\n    }\n\n    public interface IOAction<T> {\n\n        T call() throws IOException;\n    }\n\n    private PrivilegedAction() {\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/ReflectionUtils.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.lang.annotation.Annotation;\n\npublic final class ReflectionUtils {\n\n    public static <A extends Annotation> A findAnnotation(Class<?> clazz, Class<A> annotationType) {\n        A annotation = clazz.getAnnotation(annotationType);\n        if (annotation != null) {\n            return annotation;\n        }\n\n        for (Class<?> ifc : clazz.getInterfaces()) {\n            annotation = findAnnotation(ifc, annotationType);\n            if (annotation != null) {\n                return annotation;\n            }\n        }\n\n        Class<?> superClass = clazz.getSuperclass();\n        if (superClass == null || superClass == Object.class) {\n            return null;\n        }\n\n        return findAnnotation(superClass, annotationType);\n    }\n\n    private ReflectionUtils() {\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/StringUtils.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic final class StringUtils {\n\n    public static String abbreviate(String str, int maxWidth) {\n        if (str == null) {\n            return null;\n        }\n\n        if (maxWidth < 4) {\n            throw new IllegalArgumentException(\"Minimum abbreviation width is 4\");\n        }\n\n        if (str.length() <= maxWidth) {\n            return str;\n        }\n\n        return str.substring(0, maxWidth - 3) + \"...\";\n    }\n\n    private StringUtils() {\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/TemporaryPath.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic class TemporaryPath implements AutoCloseable {\n\n    private static final Logger log = LoggerFactory.getLogger(TemporaryPath.class);\n\n    private final Path path;\n\n    public TemporaryPath(Path path) {\n        this.path = path;\n    }\n\n    public Path path() {\n        return path;\n    }\n\n    @Override\n    public void close() {\n        if (path == null) {\n            return;\n        }\n\n        try {\n            if (Files.isDirectory(path)) {\n                PathUtils.deleteRecursively(path);\n            } else {\n                Files.deleteIfExists(path);\n            }\n        } catch (IOException e) {\n            log.warn(\"cleanup ['{}'] -> error: {}\", path, e.getMessage());\n        }\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/ThreadLocalStack.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.LinkedList;\nimport java.util.List;\n\npublic class ThreadLocalStack<T> {\n\n    private final ThreadLocal<List<T>> localStack = ThreadLocal.withInitial(LinkedList::new);\n\n    public void push(T value) {\n        List<T> stack = localStack.get();\n        if (stack == null) {\n            stack = new LinkedList<>();\n            localStack.set(stack);\n        }\n        stack.add(0, value);\n    }\n\n    public T pop() {\n        List<T> stack = localStack.get();\n        if (stack == null || stack.isEmpty()) {\n            throw new IllegalStateException(\"Stack is empty. This is most likely a bug.\");\n        }\n\n        T result = stack.remove(0);\n        if (stack.isEmpty()) {\n            localStack.remove();\n        }\n        return result;\n    }\n\n    public T peek() {\n        List<T> stack = localStack.get();\n        if (stack == null || stack.isEmpty()) {\n            return null;\n        }\n        return stack.get(0);\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/ToStringHelper.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Collection;\n\npublic class ToStringHelper {\n\n    public static ToStringHelper prefix(String prefix) {\n        return new ToStringHelper(prefix);\n    }\n\n    private final String prefix;\n    private final StringBuilder builder = new StringBuilder();\n    private boolean appendSeparator = false;\n\n    public ToStringHelper(String prefix) {\n        this.prefix = prefix;\n    }\n\n    public ToStringHelper add(String name, Object value) {\n        return addNameValue(name, value);\n    }\n\n    public ToStringHelper add(String name, Number value) {\n        return addNameValue(name, value);\n    }\n\n    public ToStringHelper add(String name, String value) {\n        if (value == null) {\n            return this;\n        }\n\n        return addNameValue(name, \"\\\"\" + value + \"\\\"\");\n    }\n\n    public ToStringHelper add(String name, Collection<?> value) {\n        if (value == null || value.isEmpty()) {\n            return this;\n        }\n\n        return addNameValue(name, value);\n    }\n\n    public String toString() {\n        builder.insert(0, '{');\n        if (prefix != null) {\n            builder.insert(0, prefix);\n        }\n        builder.append('}');\n        return builder.toString();\n    }\n\n    private ToStringHelper addNameValue(String name, Object value) {\n        if (value == null) {\n            return this;\n        }\n\n        if (appendSeparator) {\n            builder.append(\", \");\n        }\n        builder.append(name).append('=').append(value);\n\n        appendSeparator = true;\n\n        return this;\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/TruncBufferedReader.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.Reader;\n\npublic class TruncBufferedReader extends BufferedReader {\n\n    public static final int DEFAULT_MAX_LINE_LENGTH = 4*1024;\n    private static final int CR = '\\r';\n    private static final int LF = '\\n';\n\n    private final int readerMaxLineLen;\n    private final char[] data;\n\n    public TruncBufferedReader(Reader reader) {\n        this(reader, DEFAULT_MAX_LINE_LENGTH);\n    }\n\n    public TruncBufferedReader(Reader reader, int maxLineLen) {\n        super(reader);\n        if (maxLineLen <= 0) {\n            throw new IllegalArgumentException(\"maxLineLen must be greater than 0\");\n        }\n\n        this.readerMaxLineLen = maxLineLen;\n        this.data = new char[readerMaxLineLen];\n    }\n\n    @Override\n    public String readLine() throws IOException {\n        int currentPos = 0;\n        int currentCharVal = super.read();\n\n        while ((currentCharVal != CR) && (currentCharVal != LF) && (currentCharVal >= 0)) {\n            data[currentPos++] = (char) currentCharVal;\n            if (currentPos < readerMaxLineLen) {\n                currentCharVal = super.read();\n            } else {\n                break;\n            }\n        }\n\n        if (currentCharVal < 0) {\n            if (currentPos > 0) {\n                return (new String(data, 0, currentPos));\n            } else {\n                return null;\n            }\n        } else {\n            int skipped = skipTillEndOfLine(currentCharVal);\n            String result = new String(data, 0, currentPos);\n            if (skipped > 0) {\n                result += \"...[skipped \" + skipped + \" bytes]\";\n            }\n            return result;\n        }\n    }\n\n    private int skipTillEndOfLine(int currentCharVal) throws IOException {\n        int skippedCount = 0;\n        while ((currentCharVal != CR) && (currentCharVal != LF) && (currentCharVal >= 0)) {\n            currentCharVal = super.read();\n            skippedCount++;\n        }\n\n        if (currentCharVal < 0) {\n            return skippedCount - 1;\n        }\n\n        if (currentCharVal == CR) {\n            super.mark(1);\n            if (super.read() != LF) {\n                super.reset();\n            }\n        }\n\n        return skippedCount - 1;\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/ZipUtils.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.apache.commons.compress.archivers.zip.ZipArchiveEntry;\nimport org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;\nimport org.apache.commons.compress.archivers.zip.ZipFile;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.*;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.nio.file.attribute.PosixFilePermission;\nimport java.util.Enumeration;\nimport java.util.Set;\n\npublic final class ZipUtils {\n\n    public static void zipFile(ZipArchiveOutputStream zip, Path src, String name) throws IOException {\n        ZipArchiveEntry e = new ZipArchiveEntry(name) {\n            @Override\n            public int getPlatform() {\n                return PLATFORM_UNIX;\n            }\n        };\n\n        Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(src);\n        e.setUnixMode(Posix.unixMode(permissions));\n\n        e.setSize(Files.size(src));\n\n        zip.putArchiveEntry(e);\n        Files.copy(src, zip);\n        zip.closeArchiveEntry();\n    }\n\n    public static void zip(ZipArchiveOutputStream zip, Path srcDir, String... filters) throws IOException {\n        zip(zip, null, srcDir, filters);\n    }\n\n    public static void zip(ZipArchiveOutputStream zip, String dstPrefix, Path srcDir, String... filters) throws IOException {\n        Files.walkFileTree(srcDir, new SimpleFileVisitor<Path>() {\n            @Override\n            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {\n                if (dir.toAbsolutePath().equals(srcDir)) {\n                    return FileVisitResult.CONTINUE;\n                }\n\n                if (matches(dir, filters)) {\n                    return FileVisitResult.SKIP_SUBTREE;\n                }\n\n                return FileVisitResult.CONTINUE;\n            }\n\n            @Override\n            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {\n                if (matches(file, filters)) {\n                    return FileVisitResult.SKIP_SUBTREE;\n                }\n\n                String n = srcDir.relativize(file).toString();\n                if (dstPrefix != null) {\n                    n = dstPrefix + n;\n                }\n\n                zipFile(zip, file, n);\n\n                return FileVisitResult.CONTINUE;\n            }\n        });\n    }\n\n    public static void unzip(InputStream in, Path targetDir, CopyOption... options) throws IOException {\n        try (TemporaryPath tmpZip = new TemporaryPath(PathUtils.createTempFile(\"unzip\", \"zip\"))) {\n            Files.copy(in, tmpZip.path(), StandardCopyOption.REPLACE_EXISTING);\n            unzip(tmpZip.path(), targetDir, options);\n        }\n    }\n\n    public static void unzip(Path in, Path targetDir, CopyOption... options) throws IOException {\n        unzip(in, targetDir, false, null, options);\n    }\n\n    public static void unzip(Path in, Path targetDir, boolean skipExisting, CopyOption... options) throws IOException {\n        unzip(in, targetDir, skipExisting, null, options);\n    }\n\n    public static void unzip(InputStream in, Path targetDir, boolean skipExisting, FileVisitor visitor, CopyOption... options) throws IOException {\n        try (TemporaryPath tmpZip = new TemporaryPath(PathUtils.createTempFile(\"unzip\", \"zip\"))) {\n            Files.copy(in, tmpZip.path(), StandardCopyOption.REPLACE_EXISTING);\n            unzip(tmpZip.path(), targetDir, skipExisting, visitor, options);\n        }\n    }\n\n    public static void unzip(Path in, Path targetDir, boolean skipExisting, FileVisitor visitor, CopyOption... options) throws IOException {\n        targetDir = targetDir.normalize().toAbsolutePath();\n\n        try (ZipFile zip = new ZipFile(in.toFile())) {\n            Enumeration<ZipArchiveEntry> entries = zip.getEntries();\n\n            while (entries.hasMoreElements()) {\n                ZipArchiveEntry e = entries.nextElement();\n\n                Path p = targetDir.resolve(e.getName());\n\n                // skip paths outside of targetDir\n                // (don't log anything to avoid \"log bombing\")\n                if (!p.normalize().toAbsolutePath().startsWith(targetDir)) {\n                    continue;\n                }\n\n                if (skipExisting && Files.exists(p)) {\n                    continue;\n                }\n\n                if (e.isDirectory()) {\n                    Files.createDirectories(p);\n                } else {\n                    Path parent = p.getParent();\n                    if (!Files.exists(parent)) {\n                        Files.createDirectories(parent);\n                    }\n\n                    try (InputStream src = zip.getInputStream(e)) {\n                        Files.copy(src, p, options);\n                    }\n\n                    int unixMode = e.getUnixMode();\n                    if (unixMode <= 0) {\n                        unixMode = Posix.DEFAULT_UNIX_MODE;\n                    }\n\n                    Files.setPosixFilePermissions(p, Posix.posix(unixMode));\n                    if (visitor != null) {\n                        visitor.visit(p, p);\n                    }\n                }\n            }\n        }\n    }\n\n    private static boolean matches(Path p, String... filters) {\n        String n = p.getName(p.getNameCount() - 1).toString();\n        for (String f : filters) {\n            if (n.matches(f)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    private ZipUtils() {\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/cfg/MappingAuthConfig.java",
    "content": "package com.walmartlabs.concord.common.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.net.URI;\nimport java.util.regex.Pattern;\n\n/**\n * Configuration for mapping Git repository URLs to an authentication method.\n * Mapping is based on regex matching (see {@link #urlPattern()}) against the\n * repository URL.\n */\npublic interface MappingAuthConfig {\n\n    /**\n     * Identification for the auth config. Should be unique within the\n     * application config. May be used for identifying source configs in metrics */\n    String id();\n\n    /** Regex matching the host, optional port and path of a Git repository URL. */\n    Pattern urlPattern();\n\n    /**\n     * Username to use for authentication with a provided token. Some services\n     * (e.g. GitHub API for app installation) require a specific username. Others\n     * (e.g. GitHub API for personal access tokens) accept just the token and no username\n     */\n    @Nullable\n    String username();\n\n    /**\n     * For compatibility with a {@link MappingAuthConfig} instance, a URI must match the\n     * {@link #urlPattern()} regex. The regex may match against the path to support\n     * either a Git host behind a reverse proxy or restricting the auth to specific\n     * org/repo patterns.\n     * @return {@code true} if this provider can handle the given repo URI, {@code false} otherwise.\n     */\n    default boolean canHandle(URI repo) {\n        var port = (repo.getPort() == -1 ? \"\" : (\":\" + repo.getPort()));\n        var path = (repo.getPath() == null ? \"\" : repo.getPath());\n        var repoHostPortAndPath = repo.getHost() + port + path;\n\n        return repoHostPortAndPath.matches(urlPattern() + \".*\");\n    }\n\n    static Pattern assertBaseUrlPattern(String pattern) {\n        return pattern.endsWith(\".*\")\n                ? Pattern.compile(pattern)\n                : Pattern.compile(pattern + \".*\");\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    abstract class OauthAuthConfig implements MappingAuthConfig {\n        public abstract String token();\n\n        public static ImmutableOauthAuthConfig.Builder builder() {\n            return ImmutableOauthAuthConfig.builder();\n        }\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface ConcordServerAuthConfig extends MappingAuthConfig {\n        static ImmutableConcordServerAuthConfig.Builder builder() {\n            return ImmutableConcordServerAuthConfig.builder();\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/cfg/OauthTokenConfig.java",
    "content": "package com.walmartlabs.concord.common.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.List;\nimport java.util.Optional;\n\npublic interface OauthTokenConfig {\n\n    Optional<String> getOauthToken();\n\n    Optional<String> getOauthUsername();\n\n    Optional<String> getOauthUrlPattern();\n\n    List<MappingAuthConfig> getSystemAuth();\n\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/form/ConcordFormFields.java",
    "content": "package com.walmartlabs.concord.common.form;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport static io.takari.bpm.model.form.FormField.Option;\n\npublic final class ConcordFormFields {\n\n    public static final class FileField {\n\n        public static final String TYPE = \"file\";\n\n        private FileField() {\n        }\n    }\n\n    public static final class DateField {\n\n        public static final String TYPE = \"date\";\n\n        private DateField() {\n        }\n    }\n\n    public static final class DateTimeField {\n\n        public static final String TYPE = \"dateTime\";\n\n        private DateTimeField() {\n        }\n    }\n\n    public static final class DateFieldOptions {\n        public static final Option<String> POPUP_POSITION = new Option<>(\"popupPosition\", String.class);\n\n        private DateFieldOptions() {\n        }\n    }\n    public static final class FieldOptions {\n\n        public static final Option<String> INPUT_TYPE = new Option<>(\"inputType\", String.class);\n        public static final Option<String> PLACEHOLDER = new Option<>(\"placeholder\", String.class);\n        public static final Option<Boolean> READ_ONLY = new Option<>(\"readOnly\", Boolean.class);\n        public static final Option<Boolean> SEARCH = new Option<>(\"search\", Boolean.class);\n\n        private FieldOptions() {\n        }\n    }\n\n    private ConcordFormFields() {\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/form/ConcordFormValidator.java",
    "content": "package com.walmartlabs.concord.common.form;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.takari.bpm.api.ExecutionException;\nimport io.takari.bpm.form.DefaultFormValidator;\nimport io.takari.bpm.form.FormSubmitResult;\nimport io.takari.bpm.form.FormValidatorLocale;\nimport io.takari.bpm.model.form.FormField;\nimport org.apache.commons.validator.routines.EmailValidator;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\npublic class ConcordFormValidator extends DefaultFormValidator {\n\n    public ConcordFormValidator() {\n        this(new DefaultConcordFormValidatorLocale());\n    }\n\n    public ConcordFormValidator(FormValidatorLocale locale) {\n        super(createValidators(locale), locale);\n    }\n\n    private static Collection<FieldValidator> createValidators(FormValidatorLocale locale) {\n        List<FieldValidator> vs = new ArrayList<>();\n        vs.add(new StringFieldValidator(locale));\n        vs.add(new DefaultFormValidator.IntegerFieldValidator(locale));\n        vs.add(new DefaultFormValidator.DecimalFieldValidator(locale));\n        vs.add(new DefaultFormValidator.BooleanFieldValidator(locale));\n        vs.add(new FileFieldValidator());\n        vs.add(new DateFieldValidator());\n        return vs;\n    }\n\n    public static final class FileFieldValidator implements DefaultFormValidator.FieldValidator {\n\n        private static final String[] TYPES = {ConcordFormFields.FileField.TYPE};\n\n        @Override\n        public String[] allowedTypes() {\n            return TYPES;\n        }\n\n        @Override\n        public FormSubmitResult.ValidationError validate(String formId, FormField f, Integer idx, Object v) {\n            String fieldName = f.getName();\n\n            if (!(v instanceof String)) {\n                throw new IllegalArgumentException(\"Expected a file value: \" + fieldName);\n            }\n\n            return null;\n        }\n    }\n\n    public static final class StringFieldValidator implements DefaultFormValidator.FieldValidator {\n\n        private final DefaultFormValidator.StringFieldValidator delegate;\n\n        public StringFieldValidator(FormValidatorLocale locale) {\n            this.delegate = new DefaultFormValidator.StringFieldValidator(locale);\n        }\n\n        @Override\n        public String[] allowedTypes() {\n            return delegate.allowedTypes();\n        }\n\n        @Override\n        public FormSubmitResult.ValidationError validate(String formId, FormField f, Integer idx, Object v) throws ExecutionException {\n            FormSubmitResult.ValidationError error = delegate.validate(formId, f, idx, v);\n            if (error != null) {\n                return error;\n            }\n            String inputType = f.getOption(new FormField.Option<>(\"inputType\", String.class));\n            if (\"email\".equalsIgnoreCase(inputType)) {\n                boolean valid = EmailValidator.getInstance().isValid((String)v);\n                if (!valid) {\n                    return new FormSubmitResult.ValidationError(f.getName(), \"Invalid email address\");\n                }\n            }\n\n            return null;\n        }\n    }\n\n    public static final class DateFieldValidator implements DefaultFormValidator.FieldValidator {\n\n        private static final String[] TYPES = {ConcordFormFields.DateField.TYPE, ConcordFormFields.DateTimeField.TYPE};\n\n        @Override\n        public String[] allowedTypes() {\n            return TYPES;\n        }\n\n        @Override\n        public FormSubmitResult.ValidationError validate(String formId, FormField f, Integer idx, Object v) {\n            String fieldName = f.getName();\n\n            if (!(v instanceof String)) {\n                throw new IllegalArgumentException(\"Expected a date value: \" + fieldName);\n            }\n\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/form/ConcordFormValidatorLocale.java",
    "content": "package com.walmartlabs.concord.common.form;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.takari.bpm.form.FormValidatorLocale;\nimport io.takari.bpm.model.form.FormField;\n\npublic interface ConcordFormValidatorLocale extends FormValidatorLocale {\n\n    /**\n     * Expected a date value.\n     *\n     * @param formId\n     * @param field\n     * @param idx\n     * @param value\n     * @return\n     */\n    String expectedDate(String formId, FormField field, Integer idx, Object value);\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/form/DefaultConcordFormValidatorLocale.java",
    "content": "package com.walmartlabs.concord.common.form;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.takari.bpm.form.DefaultFormValidatorLocale;\nimport io.takari.bpm.model.form.FormField;\n\npublic class DefaultConcordFormValidatorLocale extends DefaultFormValidatorLocale implements ConcordFormValidatorLocale {\n\n    @Override\n    public String expectedDate(String formId, FormField field, Integer idx, Object value) {\n        return String.format(\"%s: expected a date value, got %s\", fieldName(field, idx), value);\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/secret/BinaryDataSecret.java",
    "content": "package com.walmartlabs.concord.common.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Secret;\n\npublic class BinaryDataSecret implements Secret {\n\n    private static final long serialVersionUID = 1L;\n\n    private final byte[] data;\n\n    public BinaryDataSecret(byte[] data) { // NOSONAR\n        this.data = data;\n    }\n\n    public byte[] getData() {\n        return data;\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/secret/HashAlgorithm.java",
    "content": "package com.walmartlabs.concord.common.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Arrays;\n\npublic enum HashAlgorithm {\n    @Deprecated\n    LEGACY_MD5(\"md5\"),\n    SHA256(\"SHA-256\");\n    private String name;\n\n    HashAlgorithm(String name) {\n        this.name = name;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public static HashAlgorithm getByName(String name) {\n        return Arrays.stream(HashAlgorithm.values()).filter(hashAlgorithm -> hashAlgorithm.getName().equals(name)).findFirst().orElse(LEGACY_MD5);\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/secret/KeyPair.java",
    "content": "package com.walmartlabs.concord.common.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Secret;\n\nimport java.io.*;\n\npublic class KeyPair implements Secret {\n\n    private static final long serialVersionUID = 1L;\n\n    public static KeyPair deserialize(byte[] input) {\n        try {\n            DataInput in = new DataInputStream(new ByteArrayInputStream(input));\n\n            int n1 = assertKeyLength(in.readInt());\n            byte[] ab1 = new byte[n1];\n            in.readFully(ab1);\n\n            int n2 = assertKeyLength(in.readInt());\n            byte[] ab2 = new byte[n2];\n            in.readFully(ab2);\n\n            return new KeyPair(ab1, ab2);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static byte[] serialize(KeyPair k) {\n        try {\n            ByteArrayOutputStream baos = new ByteArrayOutputStream();\n            DataOutput out = new DataOutputStream(baos);\n\n            out.writeInt(k.getPublicKey().length);\n            out.write(k.getPublicKey());\n\n            out.writeInt(k.getPrivateKey().length);\n            out.write(k.getPrivateKey());\n\n            return baos.toByteArray();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static int assertKeyLength(int n) {\n        if (n < 0 || n > 8192) {\n            throw new IllegalArgumentException(\"Invalid key length: \" + n);\n        }\n        return n;\n    }\n\n    private final byte[] publicKey;\n    private final byte[] privateKey;\n\n    public KeyPair(byte[] publicKey, byte[] privateKey) { // NOSONAR\n        this.publicKey = publicKey;\n        this.privateKey = privateKey;\n    }\n\n    public byte[] getPublicKey() {\n        return publicKey;\n    }\n\n    public byte[] getPrivateKey() {\n        return privateKey;\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/secret/SecretEncryptedByType.java",
    "content": "package com.walmartlabs.concord.common.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic enum SecretEncryptedByType {\n\n    SERVER_KEY,\n    PASSWORD\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/secret/SecretUtils.java",
    "content": "package com.walmartlabs.concord.common.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.crypto.BadPaddingException;\nimport javax.crypto.Cipher;\nimport javax.crypto.CipherInputStream;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.security.GeneralSecurityException;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.SecureRandom;\n\npublic final class SecretUtils {\n\n    public static byte[] encrypt(byte[] input, byte[] password, byte[] salt) {\n        return encrypt(input, password, salt, HashAlgorithm.LEGACY_MD5);\n    }\n\n    public static byte[] encrypt(byte[] input, byte[] password, byte[] salt, HashAlgorithm hashAlgorithm) {\n        try {\n            return encrypt(new ByteArrayInputStream(input), password, salt, hashAlgorithm).readAllBytes();\n        } catch (IOException e) {\n            throw new SecurityException(\"Error encrypting a secret: \" + e);\n        }\n    }\n\n    public static InputStream encrypt(InputStream input, byte[] password, byte[] salt) {\n        return encrypt(input, password, salt, HashAlgorithm.LEGACY_MD5);\n    }\n\n    public static InputStream encrypt(InputStream input, byte[] password, byte[] salt, HashAlgorithm hashAlgorithm) {\n        try {\n            Cipher c = init(password, salt, Cipher.ENCRYPT_MODE, hashAlgorithm);\n            return new CipherInputStream(input, c);\n        } catch (GeneralSecurityException e) {\n            throw new SecurityException(\"Error encrypting a secret: \" + e);\n        }\n    }\n\n    public static byte[] decrypt(byte[] input, byte[] password, byte[] salt) {\n        return decrypt(input, password, salt, HashAlgorithm.LEGACY_MD5);\n    }\n\n    public static byte[] decrypt(byte[] input, byte[] password, byte[] salt, HashAlgorithm hashAlgorithm) {\n        try {\n            InputStream out = decrypt(new ByteArrayInputStream(input), password, salt, hashAlgorithm);\n            return out.readAllBytes();\n        } catch (IOException e) {\n            Throwable t = e.getCause() == null ? e : e.getCause();\n            if (t instanceof BadPaddingException) {\n                throw new SecurityException(\"Error decrypting a secret: \" + t.getMessage() + \". Invalid input data and/or a password.\");\n            }\n            throw new SecurityException(\"Error decrypting a secret: \" + e.getMessage(), t);\n        }\n    }\n\n    public static InputStream decrypt(InputStream input, byte[] password, byte[] salt) {\n        return decrypt(input, password, salt, HashAlgorithm.LEGACY_MD5);\n    }\n\n    public static InputStream decrypt(InputStream input, byte[] password, byte[] salt, HashAlgorithm hashAlgorithm) {\n        try {\n            Cipher c = init(password, salt, Cipher.DECRYPT_MODE, hashAlgorithm);\n            return new CipherInputStream(input, c);\n        } catch (BadPaddingException e) {\n            throw new SecurityException(\"Error decrypting a secret: \" + e.getMessage() + \". Invalid input data and/or a password.\");\n        } catch (GeneralSecurityException e) {\n            throw new SecurityException(\"Error decrypting a secret: \" + e.getMessage());\n        }\n    }\n\n    public static byte[] hash(byte[] in, byte[] salt, HashAlgorithm hashAlgorithm) throws NoSuchAlgorithmException {\n        MessageDigest digest = MessageDigest.getInstance(hashAlgorithm.getName());\n        digest.update(salt);\n        return in != null ? digest.digest(in) : digest.digest();\n    }\n\n    private static Cipher init(byte[] password, byte[] salt, int mode, HashAlgorithm hashAlgorithm) throws GeneralSecurityException {\n        Cipher c = Cipher.getInstance(\"AES\");\n\n        byte[] key = hash(password, salt, hashAlgorithm);\n        SecretKeySpec k = new SecretKeySpec(key, \"AES\");\n\n        c.init(mode, k);\n        return c;\n    }\n\n    public static byte[] generateSalt(int size) {\n        SecureRandom sr = new SecureRandom();\n        byte[] bytes = new byte[size];\n        sr.nextBytes(bytes);\n        return bytes;\n    }\n\n    private SecretUtils() {\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/secret/UsernamePassword.java",
    "content": "package com.walmartlabs.concord.common.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Secret;\n\nimport java.io.*;\nimport java.nio.ByteBuffer;\nimport java.nio.CharBuffer;\nimport java.nio.charset.StandardCharsets;\n\npublic class UsernamePassword implements Secret {\n\n    private static final long serialVersionUID = 1L;\n\n    public static byte[] serialize(UsernamePassword input) {\n        try {\n            ByteArrayOutputStream baos = new ByteArrayOutputStream();\n            DataOutput out = new DataOutputStream(baos);\n\n            out.writeUTF(input.getUsername());\n\n            ByteBuffer bb = StandardCharsets.UTF_8.encode(CharBuffer.wrap(input.getPassword()));\n            byte[] ab = new byte[bb.remaining()];\n            bb.get(ab);\n\n            out.writeInt(ab.length);\n            out.write(ab);\n\n            return baos.toByteArray();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static UsernamePassword deserialize(byte[] input) {\n        try {\n            DataInput in = new DataInputStream(new ByteArrayInputStream(input));\n\n            String username = in.readUTF();\n\n            int len = in.readInt();\n            byte[] ab = new byte[len];\n            in.readFully(ab);\n\n            char[] password = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(ab)).array();\n\n            return new UsernamePassword(username, password);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private final String username;\n    private final char[] password;\n\n    public UsernamePassword(String username, char[] password) { // NOSONAR\n        this.username = username;\n        this.password = password;\n    }\n\n    public String getUsername() {\n        return username;\n    }\n\n    public char[] getPassword() {\n        return password;\n    }\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/validation/ConcordId.java",
    "content": "package com.walmartlabs.concord.common.validation;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.validation.Constraint;\nimport javax.validation.Payload;\nimport javax.validation.constraints.Pattern;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.Target;\n\nimport static java.lang.annotation.ElementType.*;\nimport static java.lang.annotation.RetentionPolicy.RUNTIME;\n\n@Target({FIELD, PARAMETER, ANNOTATION_TYPE})\n@Retention(RUNTIME)\n@Pattern(regexp = \"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$\")\n@Constraint(validatedBy = {})\npublic @interface ConcordId {\n\n    String message() default \"{concord.validation.constraints.ConcordKey.message}\";\n\n    Class<?>[] groups() default {};\n\n    Class<? extends Payload>[] payload() default {};\n}\n"
  },
  {
    "path": "common/src/main/java/com/walmartlabs/concord/common/validation/ConcordKey.java",
    "content": "package com.walmartlabs.concord.common.validation;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.validation.Constraint;\nimport javax.validation.Payload;\nimport javax.validation.constraints.Pattern;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.Target;\n\nimport static java.lang.annotation.ElementType.*;\nimport static java.lang.annotation.RetentionPolicy.RUNTIME;\n\n@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})\n@Retention(RUNTIME)\n@Pattern(regexp = ConcordKey.PATTERN)\n@Constraint(validatedBy = {})\npublic @interface ConcordKey {\n\n    String PATTERN = \"^[0-9a-zA-Z][0-9a-zA-Z_@.\\\\-~]{2,127}$\";\n\n    String MESSAGE = \"Must contain only alphanumeric characters, digits, underscore, @, dot (.) or a minus (-). \" +\n            \"Must start with an alphanumeric character or a digit. \" +\n            \"Must be between 2 and 128 characters in length.\";\n\n    String message() default MESSAGE;\n\n    Class<?>[] groups() default {};\n\n    Class<? extends Payload>[] payload() default {};\n}\n"
  },
  {
    "path": "common/src/main/resources/com/walmartlabs/concord/common/dockerPasswd",
    "content": "root:x:0:0:root:/root:/bin/bash\nconcord:x:456:456::/tmp:/sbin/nologin\n"
  },
  {
    "path": "common/src/test/java/com/walmartlabs/concord/common/AuthTokenProviderTest.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.cfg.MappingAuthConfig;\nimport com.walmartlabs.concord.common.cfg.OauthTokenConfig;\nimport com.walmartlabs.concord.common.secret.BinaryDataSecret;\nimport com.walmartlabs.concord.common.secret.UsernamePassword;\nimport org.immutables.value.Value;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\nimport java.net.URI;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Optional;\nimport java.util.regex.Pattern;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.never;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\n@ExtendWith(MockitoExtension.class)\nclass AuthTokenProviderTest {\n\n    private static final byte[] SECRET_BYTES = \"abc123\".getBytes(StandardCharsets.UTF_8);\n    private static final String MOCK_TOKEN = \"mock-token\";\n    private static final String MOCK_USERNAME = \"mock-username\";\n    private static final String VALID_REPO = \"https://github.local/owner/repo.git\";\n\n    @Mock\n    BinaryDataSecret binaryDataSecret;\n\n    @Mock\n    UsernamePassword usernamePassword;\n\n    @Mock\n    MappingAuthConfig.OauthAuthConfig oauth;\n\n    @Mock\n    TestOauthTokenConfig oauthTokenConfig;\n\n    @Test\n    void testSingleOauth() {\n        // the \"old\" config approach\n        when(oauthTokenConfig.getOauthToken()).thenReturn(Optional.of(MOCK_TOKEN));\n        when(oauthTokenConfig.getOauthUrlPattern()).thenReturn(Optional.of(\"github\\\\.local\"));\n        when(oauthTokenConfig.getOauthUsername()).thenReturn(Optional.of(MOCK_USERNAME));\n\n        executeWithoutSecret(oauthTokenConfig);\n\n        verify(oauthTokenConfig, times(1)).getOauthUrlPattern(); // retrieved once and stored\n    }\n\n    @Test\n    void testSystemAuth() {\n        when(oauth.canHandle(any())).thenCallRealMethod();\n        when(oauth.urlPattern()).thenReturn(Pattern.compile(\"github\\\\.local\"));\n        when(oauth.token()).thenReturn(MOCK_TOKEN);\n        when(oauth.username()).thenReturn(MOCK_USERNAME);\n\n        var cfg = TestOauthTokenConfig.builder()\n                .addSystemAuth(oauth)\n                .build();\n\n        executeWithoutSecret(cfg);\n\n        verify(oauth, times(12)).canHandle(any());\n    }\n\n    void executeWithoutSecret(OauthTokenConfig cfg) {\n        var provider = new AuthTokenProvider.OauthTokenProvider(cfg);\n\n        assertTrue(provider.supports(URI.create(\"https://github.local/owner/repo.git\"), null));\n        assertTrue(provider.supports(URI.create(\"https://github.local/owner/repo\"), null));\n        assertTrue(provider.supports(URI.create(\"https://github.local/owner/repo/\"), null));\n        assertFalse(provider.supports(URI.create(\"https://elsewhere.local/owner/repo.git\"), null));\n        assertFalse(provider.supports(URI.create(\"https://elsewhere.local/owner/repo\"), null));\n\n        assertEquals(MOCK_TOKEN, provider.getToken(URI.create(\"https://github.local/owner/repo.git\"), null).map(ExternalAuthToken::token).orElse(null));\n        assertEquals(MOCK_TOKEN, provider.getToken(URI.create(\"https://github.local/owner/repo\"), null).map(ExternalAuthToken::token).orElse(null));\n        assertEquals(MOCK_TOKEN, provider.getToken(URI.create(\"https://github.local/owner/repo/\"), null).map(ExternalAuthToken::token).orElse(null));\n        assertFalse(provider.getToken(URI.create(\"https://elsewhere.local/owner/repo.git\"), null).isPresent());\n        assertFalse(provider.getToken(URI.create(\"https://elsewhere.local/owner/repo\"), null).isPresent());\n\n        var enriched = provider.addUserInfoToUri(URI.create(\"https://github.local/owner/repo.git\"), null);\n        assertEquals(MOCK_USERNAME + \":\" + MOCK_TOKEN, enriched.getUserInfo());\n        assertEquals(\"https://\" + MOCK_USERNAME + \":\" + MOCK_TOKEN + \"@github.local/owner/repo.git\", enriched.toString());\n    }\n\n    @Test\n    void testUsernamePassword() {\n        var cfg = TestOauthTokenConfig.builder().build();\n        var provider = new AuthTokenProvider.OauthTokenProvider(cfg);\n\n        assertFalse(provider.supports(URI.create(VALID_REPO), usernamePassword));\n    }\n\n    @Test\n    void testWithSecret() {\n        var cfg = TestOauthTokenConfig.builder()\n                .addSystemAuth(oauth) // won't be used\n                .build();\n\n        executeWithSecret(cfg);\n    }\n\n    @Test\n    void testWithSecretNoDefault() {\n        var cfg = TestOauthTokenConfig.builder().build();\n\n        executeWithSecret(cfg);\n    }\n\n    private void executeWithSecret(TestOauthTokenConfig cfg) {\n        var provider = new AuthTokenProvider.OauthTokenProvider(cfg);\n\n        when(binaryDataSecret.getData()).thenReturn(SECRET_BYTES);\n        assertTrue(provider.supports(URI.create(\"https://github.local/owner/repo.git\"), binaryDataSecret));\n\n        verify(oauth, never()).token(); // prove it wasn't used\n        verify(binaryDataSecret, times(1)).getData();\n    }\n\n    @Value.Immutable\n    interface TestOauthTokenConfig extends OauthTokenConfig {\n        static ImmutableTestOauthTokenConfig.Builder builder() {\n            return ImmutableTestOauthTokenConfig.builder();\n        }\n    }\n}\n"
  },
  {
    "path": "common/src/test/java/com/walmartlabs/concord/common/ConfigurationUtilsTest.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ConfigurationUtilsTest {\n\n    @Test\n    public void deepMergeTest() {\n        Map<String, Object> m1 = new HashMap<>();\n        m1.put(\"a\", \"a-value1\");\n        m1.put(\"b\", \"b-value1\");\n\n        Map<String, Object> m2 = new HashMap<>();\n        m2.put(\"a\", \"a-value2\");\n        m2.put(\"c\", \"b-value2\");\n\n        Map<String, Object> result = ConfigurationUtils.deepMerge(m1, m2);\n        assertEquals(\"a-value2\", result.get(\"a\"));\n        assertEquals(\"b-value1\", result.get(\"b\"));\n        assertEquals(\"b-value2\", result.get(\"c\"));\n    }\n\n    @Test\n    public void deepEqualsTest() {\n        Object a = Collections.singletonMap(\"x\", Collections.singletonList(\"test1\"));\n        Object b = Collections.singletonMap(\"x\", Collections.singletonList(\"test2\"));\n        assertFalse(ConfigurationUtils.deepEquals(a, b));\n\n        a = Collections.singletonMap(\"x\", Collections.singletonList(\"test\"));\n        b = Collections.singletonMap(\"x\", Collections.singletonList(\"test\"));\n        assertTrue(ConfigurationUtils.deepEquals(a, b));\n    }\n}\n"
  },
  {
    "path": "common/src/test/java/com/walmartlabs/concord/common/CycleCheckerTest.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class CycleCheckerTest {\n\n    /**\n     * m -(m1)- m1 -(mm1)- m2\n     * \\(m2)- m2 -(mm2)- m1\n     */\n    @Test\n    public void test1() throws Exception {\n        Map<String, Object> m = new HashMap<>();\n        Map<String, Object> m1 = new HashMap<>();\n        Map<String, Object> m2 = new HashMap<>();\n\n        m1.put(\"mm1\", m2);\n        m2.put(\"mm2\", m1);\n\n        m.put(\"m1\", m1);\n        m.put(\"m2\", m2);\n\n        System.out.println(CycleChecker.check(m));\n        assertTrue(CycleChecker.check(m).isHasCycle());\n    }\n\n    /**\n     * m -(m2)- \"a1\"\n     * \\(m2)- m\n     */\n    @Test\n    public void test2() throws Exception {\n        Map<String, Object> m = new HashMap<>();\n\n        m.put(\"m2\", Arrays.asList(\"a1\", m));\n\n        System.out.println(CycleChecker.check(m));\n        assertTrue(CycleChecker.check(m).isHasCycle());\n    }\n\n    /**\n     * m -(k)-- \"v\"\n     * \\(k2)- * -(kk2)- \"value\"\n     */\n    @Test\n    public void test3() throws Exception {\n        Map<String, Object> m = new HashMap<>();\n\n        m.put(\"k\", \"v\");\n        m.put(\"k2\", Collections.singletonMap(\"kk2\", \"value\"));\n\n        System.out.println(CycleChecker.check(m));\n        assertFalse(CycleChecker.check(m).isHasCycle());\n        System.out.println(new ObjectMapper().writeValueAsString(m));\n    }\n\n    /**\n     * m -(m2)- \"a1\"\n     * \\(m2)- * -(kk2)- \"value\"\n     */\n    @Test\n    public void test4() throws Exception {\n        Map<String, Object> m = new HashMap<>();\n\n        m.put(\"m2\", Arrays.asList(\"a1\", Collections.singletonMap(\"kk2\", \"value\")));\n\n        System.out.println(new ObjectMapper().writeValueAsString(m));\n        System.out.println(CycleChecker.check(m));\n\n        assertFalse(CycleChecker.check(m).isHasCycle());\n    }\n\n    /**\n     * m -(m1)- m1 -(k1)- \"v1\"\n     * \\(m2)- m2 -(k2)- \"v2\"\n     * \\(m2)- m2 -(k3)- m1\n     */\n    @Test\n    public void test5() throws Exception {\n        Map<String, Object> m = new HashMap<>();\n        Map<String, Object> m1 = new HashMap<>();\n        Map<String, Object> m2 = new HashMap<>();\n\n        m1.put(\"k1\", \"v1\");\n        m2.put(\"k2\", \"v2\");\n        m2.put(\"k3\", m1);\n\n        m.put(\"m1\", m1);\n        m.put(\"m2\", m2);\n\n        System.out.println(new ObjectMapper().writeValueAsString(m));\n\n        System.out.println(CycleChecker.check(m));\n        assertFalse(CycleChecker.check(m).isHasCycle());\n    }\n}\n"
  },
  {
    "path": "common/src/test/java/com/walmartlabs/concord/common/DateTimeUtilsTest.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport javax.xml.bind.DatatypeConverter;\nimport java.time.OffsetDateTime;\nimport java.time.ZoneId;\nimport java.time.temporal.ChronoField;\nimport java.util.Calendar;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class DateTimeUtilsTest {\n\n    @Test\n    public void test() throws Exception {\n        Calendar now1 = Calendar.getInstance();\n        now1.set(Calendar.MILLISECOND, 123);\n\n        String a = DatatypeConverter.printDateTime(now1);\n\n        OffsetDateTime now2 = OffsetDateTime.ofInstant(now1.toInstant(), ZoneId.of(now1.getTimeZone().getID()));\n        String b = DateTimeUtils.toIsoString(now2);\n\n        assertEquals(a, b);\n\n        String src = \"2020-07-16T13:47:51.085-04:00\";\n        OffsetDateTime x = DateTimeUtils.fromIsoString(src);\n        String dst = DateTimeUtils.toIsoString(x);\n        assertEquals(src, dst);\n\n        src = \"2020-07-16T17:13:27.912Z\";\n        x = DateTimeUtils.fromIsoString(src);\n\n        assertEquals(x.toZonedDateTime().getZone().normalized(), ZoneId.of(\"UTC\").normalized());\n        assertEquals(x.get(ChronoField.YEAR), 2020);\n        assertEquals(x.get(ChronoField.MILLI_OF_SECOND), 912);\n    }\n}\n"
  },
  {
    "path": "common/src/test/java/com/walmartlabs/concord/common/ExternalAuthTokenTest.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.OffsetDateTime;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nclass ExternalAuthTokenTest {\n\n    static final String MOCK_TOKEN = \"mock-token\";\n    static final ObjectMapper MAPPER = new ObjectMapperProvider().get();\n\n    @Test\n    void testExpiration() {\n        var externalToken = ExternalAuthToken.SimpleToken.builder()\n                .token(MOCK_TOKEN)\n                .expiresAt(OffsetDateTime.now().minusSeconds((100)))\n                .build();\n\n        assertTrue(externalToken.secondsUntilExpiration() < 0);\n    }\n\n    @Test\n    void testStaticExpiration() {\n        var externalToken = ExternalAuthToken.StaticToken.builder()\n                .token(MOCK_TOKEN)\n                .build();\n\n        assertEquals(MOCK_TOKEN, externalToken.token());\n        assertEquals(Long.MAX_VALUE, externalToken.secondsUntilExpiration());\n    }\n\n    @Test\n    void testMinimalDeserialization() throws JsonProcessingException {\n        var minimalFromJson = MAPPER.readValue(\"\"\"\n                {\n                    \"token\": \"mock-token\"\n                }\n                \"\"\", ExternalAuthToken.class);\n\n        assertEquals(MOCK_TOKEN, minimalFromJson.token());\n        assertEquals(Long.MAX_VALUE, minimalFromJson.secondsUntilExpiration());\n    }\n\n    @Test\n    void testFullDeserialization() throws JsonProcessingException {\n        var fullFromJson = MAPPER.readValue(\"\"\"\n                {\n                    \"token\": \"mock-token\",\n                    \"expires_at\": \"2099-12-31T23:59:59Z\",\n                    \"username\": \"mock-username\"\n                }\n                \"\"\", ExternalAuthToken.class);\n\n        assertEquals(MOCK_TOKEN, fullFromJson.token());\n        assertEquals(\"mock-username\", fullFromJson.username());\n        var dt = fullFromJson.expiresAt();\n        assertNotNull(dt);\n        assertEquals(2099, dt.getYear());\n    }\n\n    @Test\n    void testFullDeserializationMillis() throws JsonProcessingException {\n        var fullFromJson = MAPPER.readValue(\"\"\"\n                {\n                    \"token\": \"mock-token\",\n                    \"expires_at\": \"2099-12-31T23:59:59.123Z\",\n                    \"username\": \"mock-username\"\n                }\n                \"\"\", ExternalAuthToken.class);\n\n        assertEquals(MOCK_TOKEN, fullFromJson.token());\n        var dt = fullFromJson.expiresAt();\n        assertNotNull(dt);\n        assertEquals(2099, dt.getYear());\n        assertEquals(123, dt.getNano() / 1_000_000);\n    }\n\n    @Test\n    void testDateSerializationSecondsToMillis() throws JsonProcessingException {\n        var json = MAPPER.writeValueAsString(ExternalAuthToken.SimpleToken.builder()\n                .token(MOCK_TOKEN)\n                .expiresAt(OffsetDateTime.parse(\"2099-12-31T23:59:59Z\"))\n                .build());\n\n        assertTrue(json.contains(\"23:59:59.000Z\"));\n    }\n\n    @Test\n    void testDateSerializationMillis() throws JsonProcessingException {\n        var json = MAPPER.writeValueAsString(ExternalAuthToken.SimpleToken.builder()\n                .token(MOCK_TOKEN)\n                .expiresAt(OffsetDateTime.parse(\"2099-12-31T23:59:59.123Z\"))\n                .build());\n\n        assertTrue(json.contains(\"23:59:59.123Z\"));\n    }\n}\n"
  },
  {
    "path": "common/src/test/java/com/walmartlabs/concord/common/LogUtilsTest.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\npublic class LogUtilsTest {\n\n    @Test\n    public void test() throws Exception {\n        System.out.println(LogUtils.formatMessage(LogUtils.LogLevel.INFO, \"Hello, {}!\", \"there\"));\n    }\n}\n"
  },
  {
    "path": "common/src/test/java/com/walmartlabs/concord/common/MatcherTest.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport java.util.*;\n\nimport static java.util.Arrays.asList;\nimport static java.util.Collections.emptyList;\nimport static java.util.Collections.emptyMap;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class MatcherTest {\n\n    @Test\n    public void testAllJsonTypes() {\n        Map<String, Object> event = new HashMap<>();\n        event.put(\"a\", \"a-value\");\n        event.put(\"b\", \"b-value\");\n        event.put(\"c\", 123);\n        event.put(\"d\", null);\n        event.put(\"e\", true);\n        event.put(\"f\", asList(\"3\", \"1\", \"4\", \"2\"));\n\n        Map<String, Object> conditions = new HashMap<>();\n        conditions.put(\"a\", \"a-v.*\");\n        conditions.put(\"b\", \"b-value\");\n        conditions.put(\"c\", 123);\n        conditions.put(\"d\", null);\n        conditions.put(\"e\", true);\n        conditions.put(\"f\", asList(\"1\", \"2\"));\n\n        boolean result = Matcher.matches(event, conditions);\n        assertTrue(result);\n    }\n\n    @Test\n    public void testNoConditions() {\n        Map<String, Object> event = new HashMap<>();\n        event.put(\"a\", \"a-value\");\n\n        Map<String, Object> conditions = new HashMap<>();\n\n        boolean result = Matcher.matches(event, conditions);\n        assertFalse(result);\n    }\n\n    @Test\n    public void testNotMatched() {\n        Map<String, Object> event = new HashMap<>();\n        event.put(\"a\", \"a-value\");\n        event.put(\"b\", \"b-value\");\n        event.put(\"c\", 123);\n        event.put(\"d\", null);\n        event.put(\"e\", true);\n        event.put(\"f\", asList(\"3\", \"1\", \"4\", \"2\"));\n\n        Map<String, Object> conditions = new HashMap<>();\n        conditions.put(\"b\", \"XXXX\");\n\n        boolean result = Matcher.matches(event, conditions);\n        assertFalse(result);\n    }\n\n    @Test\n    public void testTypesMismatch() {\n        Map<String, Object> event = new HashMap<>();\n        event.put(\"a\", 100);\n\n        Map<String, Object> conditions = new HashMap<>();\n        conditions.put(\"a\", \"123\");\n\n        boolean result = Matcher.matches(event, conditions);\n        assertFalse(result);\n    }\n\n    @Test\n    public void testObjectsOfObjects() {\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"o1\", \"o1v1\");\n        m.put(\"o2\", \"o2v2\");\n\n        Map<String, Object> event = new HashMap<>();\n        event.put(\"a\", 100);\n        event.put(\"obj\", m);\n\n        Map<String, Object> conditions = new HashMap<>();\n        conditions.put(\"a\", 100);\n        conditions.put(\"obj\", Collections.singletonMap(\"o1\", \"o1v1\"));\n\n        boolean result = Matcher.matches(event, conditions);\n        assertTrue(result);\n    }\n\n    @Test\n    public void testPartialMatch() {\n        Map<String, Object> event = new HashMap<>();\n        event.put(\"unknownRepo\", true);\n\n        Map<String, Object> conditions = new HashMap<>();\n        conditions.put(\"unknownRepo\", asList(true, false));\n\n        boolean result = Matcher.matches(event, conditions);\n        assertTrue(result);\n    }\n\n    @Test\n    public void testPartialNotMatch() {\n        Map<String, Object> event = new HashMap<>();\n        event.put(\"unknownRepo\", true);\n\n        Map<String, Object> conditions = new HashMap<>();\n        conditions.put(\"unknownRepo\", Collections.singletonList(false));\n\n        boolean result = Matcher.matches(event, conditions);\n        assertFalse(result);\n    }\n\n    @Test\n    public void testMatchEmptyCondition() {\n        Map<String, Object> conditions = new HashMap<>();\n        conditions.put(\"params\", emptyMap());\n\n        // --- empty param\n        Map<String, Object> event1 = new HashMap<>();\n        event1.put(\"k\", \"v\");\n        event1.put(\"params\", emptyMap());\n\n        boolean result = Matcher.matches(event1, conditions);\n        assertTrue(result);\n\n        // --- param not present\n        Map<String, Object> event2 = new HashMap<>();\n        event2.put(\"k\", \"v\");\n\n        boolean result2 = Matcher.matches(event2, conditions);\n        assertFalse(result2);\n\n        // --- param present\n        Map<String, Object> event3 = new HashMap<>();\n        event3.put(\"k\", \"v\");\n        event3.put(\"params\", Collections.singletonMap(\"a\", \"a-value\"));\n\n        boolean result3 = Matcher.matches(event3, conditions);\n        assertFalse(result3);\n    }\n\n    @Test\n    public void testMatchNullCondition() {\n        Map<String, Object> conditions = new HashMap<>();\n        conditions.put(\"params\", null);\n\n        // --- empty param\n        Map<String, Object> event1 = new HashMap<>();\n        event1.put(\"k\", \"v\");\n        event1.put(\"params\", emptyMap());\n\n        boolean result = Matcher.matches(event1, conditions);\n        assertFalse(result);\n\n        // --- param not present\n        Map<String, Object> event2 = new HashMap<>();\n        event2.put(\"k\", \"v\");\n\n        boolean result2 = Matcher.matches(event2, conditions);\n        assertTrue(result2);\n\n        // --- param is null\n        Map<String, Object> event3 = new HashMap<>();\n        event3.put(\"k\", \"v\");\n        event3.put(\"params\", null);\n\n        boolean result3 = Matcher.matches(event3, conditions);\n        assertTrue(result3);\n\n        // --- param present\n        Map<String, Object> event4 = new HashMap<>();\n        event4.put(\"k\", \"v\");\n        event4.put(\"params\", Collections.singletonMap(\"a\", \"a-value\"));\n\n        boolean result4 = Matcher.matches(event4, conditions);\n        assertFalse(result4);\n    }\n\n    @Test\n    public void testNulls() {\n        // data   ?    condition\n        //  null == null\n        assertTrue(Matcher.matches(null, null));\n\n        //  null == \".*\"\n        assertTrue(Matcher.matches(null, \".*\"));\n        //  null == \"\"\n        assertTrue(Matcher.matches(null, \"\"));\n\n        //  null != {}\n        assertFalse(Matcher.matches(null, emptyMap()));\n\n        //  null != []\n        assertFalse(Matcher.matches(null, emptyList()));\n\n        //  {} == {}\n        assertTrue(Matcher.matches(emptyMap(), emptyMap()));\n\n        //  [] == []\n        assertTrue(Matcher.matches(emptyList(), emptyList()));\n\n        //  null != 1\n        assertFalse(Matcher.matches(null, 1));\n\n        //  \"\" != null\n        assertFalse(Matcher.matches(\"\", null));\n\n        //  {} != null\n        assertFalse(Matcher.matches(emptyMap(), null));\n\n        //  [] != null\n        assertFalse(Matcher.matches(emptyList(), null));\n    }\n\n    @Test\n    public void testOr() {\n        // null == [null, []]\n        assertTrue(Matcher.matches(null, asList(null, emptyList())));\n\n        // {} == [null, [], {}]\n        assertTrue(Matcher.matches(emptyMap(), asList(null, emptyList(), emptyMap())));\n\n        //! [] == [null, []]\n        assertFalse(Matcher.matches(emptyList(), asList(null, emptyList())));\n    }\n\n    @Test\n    public void testArrayMatch() {\n        List<String> data = Arrays.asList(\"one\", \"two\");\n\n        assertTrue(Matcher.matches(data, \"on.*\"));\n        assertFalse(Matcher.matches(data, \"ono\"));\n    }\n\n    // null == null, \"\", \".*\", [null]\n    // []   == []\n    // {}   == {}, [{}]\n}\n"
  },
  {
    "path": "common/src/test/java/com/walmartlabs/concord/common/PathUtilsTest.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardOpenOption;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class PathUtilsTest {\n\n    @Test\n    public void testResolveNonChild() {\n        Path parent = Paths.get(\"/parent/path\");\n        Exception e = assertThrows(IOException.class, () -> PathUtils.assertInPath(parent, \"../child\"));\n        assertTrue(e.getMessage().contains(\"Child path resolves outside of parent path\"));\n    }\n\n    @Test\n    public void testResolveValidChild() {\n        Path parent = Paths.get(\"/parent/path\");\n        assertDoesNotThrow(() -> PathUtils.assertInPath(parent, \"child\"));\n        assertDoesNotThrow(() -> PathUtils.assertInPath(parent, \"another/child\"));\n\n        Path p = assertDoesNotThrow(() -> PathUtils.assertInPath(parent, \"odd/../but/valid\"));\n        assertEquals(\"/parent/path/but/valid\", p.toString());\n    }\n\n\n    @Test\n    public void testCopy() throws Exception {\n        Path src = Files.createTempDirectory(\"test\");\n        Path dst = Files.createTempDirectory(\"test\");\n\n        // ---\n\n        Path nestedDir = src.resolve(\"a/b\");\n        Files.createDirectories(nestedDir);\n\n        Path srcFile = nestedDir.resolve(\"c.txt\");\n        Files.createFile(srcFile);\n\n        // ---\n\n        PathUtils.copy(src, dst);\n        assertTrue(Files.exists(dst.resolve(\"a/b/c.txt\")));\n    }\n\n    @Test\n    public void testSymlinks() throws Exception {\n        Path src = Files.createTempDirectory(\"test\");\n\n        Path aFile = src.resolve(\"a\");\n        Files.write(aFile, \"hello\".getBytes(), StandardOpenOption.CREATE);\n\n        Path xDir = src.resolve(\"x\");\n        Files.createDirectories(xDir);\n\n        Path bLink = xDir.resolve(\"b\");\n        Files.createSymbolicLink(bLink, aFile);\n\n        // ---\n\n        Path dst = Files.createTempDirectory(\"test\");\n\n        // ---\n\n        PathUtils.copy(src, dst);\n\n        // ---\n\n        assertTrue(Files.isSymbolicLink(dst.resolve(\"x\").resolve(\"b\")));\n        assertTrue(Files.isRegularFile(dst.resolve(\"x\").resolve(\"b\")));\n    }\n\n    @Test\n    public void testExternalSymlinks() throws Exception {\n        Path src = Files.createTempDirectory(\"test\");\n\n        Path link = src.resolve(\"a\");\n        Path target = Paths.get(\"../../../etc/passwd\");\n        Files.createSymbolicLink(link, target);\n\n        // ---\n\n        Path dst = Files.createTempDirectory(\"test\");\n\n        // ---\n\n        Exception e = assertThrows(IOException.class, () -> PathUtils.copy(src, dst));\n        assertTrue(e.getMessage().contains(\"Symlinks outside the base directory are not supported\"));\n    }\n}\n"
  },
  {
    "path": "common/src/test/java/com/walmartlabs/concord/common/StringUtilsTest.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class StringUtilsTest {\n\n    @Test\n    public void test() {\n        try {\n            assertEquals(\"123\", StringUtils.abbreviate(\"123\", 3));\n            fail(\"exception expected\");\n        } catch (Exception e) {\n            // expected\n        }\n\n        assertNull(StringUtils.abbreviate(null, 5));\n\n        assertEquals(\"1234\", StringUtils.abbreviate(\"1234\", 5));\n\n        assertEquals(\"12345\", StringUtils.abbreviate(\"12345\", 5));\n\n        assertEquals(\"12...\", StringUtils.abbreviate(\"123456\", 5));\n    }\n}\n"
  },
  {
    "path": "common/src/test/java/com/walmartlabs/concord/common/TruncBufferedReaderTest.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport java.io.BufferedReader;\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class TruncBufferedReaderTest {\n\n    @Test\n    public void testEmpty() throws Exception {\n        String str = \"\";\n\n        List<String> r = readLines(str);\n        assertEquals(0, r.size());\n    }\n\n    @Test\n    public void test1() throws Exception {\n        String str = \"1\";\n\n        List<String> r = readLines(str);\n        assertEquals(1, r.size());\n        assertEquals(\"1\", r.get(0));\n    }\n\n    @Test\n    public void test2() throws Exception {\n        String str = \"12\";\n\n        List<String> r = readLines(str, 2);\n        assertEquals(1, r.size());\n        assertEquals(\"12\", r.get(0));\n    }\n\n    @Test\n    public void test3() throws Exception {\n        String str = \"123456789\";\n\n        List<String> r = readLines(str, 2);\n        assertEquals(1, r.size());\n        assertEquals(\"12...[skipped 7 bytes]\", r.get(0));\n    }\n\n    @Test\n    public void test4() throws Exception {\n        String str = \"12\\n3\";\n\n        List<String> r = readLines(str, 2);\n        assertEquals(2, r.size());\n        assertEquals(\"12\", r.get(0));\n        assertEquals(\"3\", r.get(1));\n    }\n\n    @Test\n    public void test5() throws Exception {\n        String str = \"123\\n456\";\n\n        List<String> r = readLines(str, 2);\n        assertEquals(2, r.size());\n        assertEquals(\"12...[skipped 1 bytes]\", r.get(0));\n        assertEquals(\"45...[skipped 1 bytes]\", r.get(1));\n    }\n\n    @Test\n    public void test6() throws Exception {\n        String str = \"1\\r\\n23\\n45\\r6\";\n\n        List<String> r = readLines(str, 2);\n        assertEquals(4, r.size());\n        assertEquals(\"1\", r.get(0));\n        assertEquals(\"23\", r.get(1));\n        assertEquals(\"45\", r.get(2));\n        assertEquals(\"6\", r.get(3));\n    }\n\n    @Test\n    public void test7() throws Exception {\n        String str = \"1\\r\\n23\\n45\\r6\\n\";\n\n        List<String> r = readLines(str, 2);\n        assertEquals(4, r.size());\n        assertEquals(\"1\", r.get(0));\n        assertEquals(\"23\", r.get(1));\n        assertEquals(\"45\", r.get(2));\n        assertEquals(\"6\", r.get(3));\n    }\n\n    private List<String> readLines(String str) throws IOException {\n        return readLines(str, TruncBufferedReader.DEFAULT_MAX_LINE_LENGTH);\n    }\n\n    private List<String> readLines(String str, int maxLineLength) throws IOException {\n        List<String> result = new ArrayList<>();\n        BufferedReader reader = new TruncBufferedReader(new InputStreamReader(new ByteArrayInputStream(str.getBytes())), maxLineLength);\n        String line;\n        while ((line = reader.readLine()) != null) {\n            result.add(line);\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "common/src/test/java/com/walmartlabs/concord/common/ZipUtilsTest.java",
    "content": "package com.walmartlabs.concord.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class ZipUtilsTest {\n\n    @Test\n    public void testZipUnzip() throws Exception {\n        Path src = Files.createTempDirectory(\"test-zip\");\n        Files.createFile(src.resolve(\"a.txt\"));\n        Files.createFile(src.resolve(\"b\\\\c.txt\"));\n        Files.createDirectory(src.resolve(\"b\"));\n        Files.createFile(src.resolve(\"b\").resolve(\"c.txt\"));\n\n        Path archive = Files.createTempFile(\"archive\", \"zip\");\n\n        try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(Files.newOutputStream(archive))) {\n            ZipUtils.zip(zip, src);\n        }\n\n        PathUtils.deleteRecursively(src);\n\n        Path dst = Files.createTempDirectory(\"test\");\n        ZipUtils.unzip(archive, dst);\n        assertTrue(Files.exists(dst.resolve(\"a.txt\")));\n        assertTrue(Files.exists(dst.resolve(\"b\\\\c.txt\")));\n        assertTrue(Files.exists(dst.resolve(\"b\").resolve(\"c.txt\")));\n    }\n}\n"
  },
  {
    "path": "config/README.md",
    "content": "# Concord Config\n\nBased on [ollie-config](https://github.com/takari/ollie/tree/master/ollie-config),\nminus the environment handling.\n"
  },
  {
    "path": "config/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-config</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.typesafe</groupId>\n            <artifactId>config</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.inject</groupId>\n            <artifactId>guice</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.reflections</groupId>\n            <artifactId>reflections</artifactId>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "config/src/main/java/com/walmartlabs/concord/config/Config.java",
    "content": "package com.walmartlabs.concord.config;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2018 Takari\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.BindingAnnotation;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.Target;\n\nimport static java.lang.annotation.ElementType.*;\nimport static java.lang.annotation.RetentionPolicy.RUNTIME;\n\n@BindingAnnotation\n@Target({FIELD, PARAMETER, METHOD})\n@Retention(RUNTIME)\npublic @interface Config {\n\n    String value();\n}\n"
  },
  {
    "path": "config/src/main/java/com/walmartlabs/concord/config/ConfigExtractor.java",
    "content": "package com.walmartlabs.concord.config;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2018 Takari\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\ninterface ConfigExtractor {\n\n    Object extractValue(com.typesafe.config.Config config, String path);\n\n    Class<?>[] getMatchingClasses();\n}\n"
  },
  {
    "path": "config/src/main/java/com/walmartlabs/concord/config/ConfigExtractors.java",
    "content": "package com.walmartlabs.concord.config;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2018 Takari\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.typesafe.config.Config;\nimport com.typesafe.config.*;\n\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.Duration;\nimport java.util.Base64;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\n\nenum ConfigExtractors implements ConfigExtractor {\n\n    BOOLEAN(boolean.class, Boolean.class) {\n        @Override\n        public Object extractValue(Config config, String path) {\n            return config.getBoolean(path);\n        }\n    },\n    BYTE(byte.class, Byte.class) {\n        @Override\n        public Object extractValue(Config config, String path) {\n            return (byte) config.getInt(path);\n        }\n    },\n    SHORT(short.class, Short.class) {\n        @Override\n        public Object extractValue(Config config, String path) {\n            return (short) config.getInt(path);\n        }\n    },\n    INTEGER(int.class, Integer.class) {\n        @Override\n        public Object extractValue(Config config, String path) {\n            return config.getInt(path);\n        }\n    },\n    LONG(long.class, Long.class) {\n        @Override\n        public Object extractValue(Config config, String path) {\n            return config.getLong(path);\n        }\n    },\n    FLOAT(float.class, Float.class) {\n        @Override\n        public Object extractValue(Config config, String path) {\n            return (float) config.getDouble(path);\n        }\n    },\n    DOUBLE(double.class, Double.class) {\n        @Override\n        public Object extractValue(Config config, String path) {\n            return config.getDouble(path);\n        }\n    },\n    STRING(String.class) {\n        @Override\n        public Object extractValue(Config config, String path) {\n            return config.getString(path);\n        }\n    },\n    PATH(Path.class) {\n        @Override\n        public Object extractValue(Config config, String path) {\n            return Paths.get(config.getString(path));\n        }\n    },\n    ANY_REF(Object.class) {\n        @Override\n        public Object extractValue(Config config, String path) {\n            return config.getAnyRef(path);\n        }\n    },\n    CONFIG(Config.class) {\n        @Override\n        public Object extractValue(Config config, String path) {\n            return config.getConfig(path);\n        }\n    },\n    CONFIG_OBJECT(ConfigObject.class) {\n        @Override\n        public Object extractValue(Config config, String path) {\n            return config.getObject(path);\n        }\n    },\n    CONFIG_VALUE(ConfigValue.class) {\n        @Override\n        public Object extractValue(Config config, String path) {\n            return config.getValue(path);\n        }\n    },\n    CONFIG_LIST(ConfigList.class) {\n        @Override\n        public Object extractValue(Config config, String path) {\n            return config.getList(path);\n        }\n    },\n    DURATION(Duration.class) {\n        @Override\n        public Object extractValue(Config config, String path) {\n            return config.getDuration(path);\n        }\n    },\n    MEMORY_SIZE(ConfigMemorySize.class) {\n        @Override\n        public Object extractValue(Config config, String path) {\n            return config.getMemorySize(path);\n        }\n    },\n    BYTE_ARRAY(byte[].class) {\n        @Override\n        public Object extractValue(Config config, String path) {\n            return Base64.getDecoder().decode(config.getString(path));\n        }\n    };\n\n    private final Class<?>[] matchingClasses;\n    private static final Map<Class<?>, ConfigExtractor> EXTRACTOR_MAP = new HashMap<>();\n\n    static {\n        for (var extractor : ConfigExtractors.values()) {\n            for (var clazz : extractor.getMatchingClasses()) {\n                EXTRACTOR_MAP.put(clazz, extractor);\n            }\n        }\n    }\n\n    ConfigExtractors(Class<?>... matchingClasses) {\n        this.matchingClasses = matchingClasses;\n    }\n\n    @Override\n    public Class<?>[] getMatchingClasses() {\n        return matchingClasses;\n    }\n\n    static Optional<Object> extractConfigValue(Config config, Class<?> paramClass, String path) {\n        if (config.hasPath(path) && EXTRACTOR_MAP.containsKey(paramClass)) {\n            return Optional.of(EXTRACTOR_MAP.get(paramClass).extractValue(config, path));\n        } else {\n            return Optional.empty();\n        }\n    }\n}\n"
  },
  {
    "path": "config/src/main/java/com/walmartlabs/concord/config/ConfigModule.java",
    "content": "package com.walmartlabs.concord.config;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2018 Takari\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.AbstractModule;\nimport com.google.inject.Key;\nimport com.google.inject.Provider;\nimport com.typesafe.config.*;\nimport org.reflections.Reflections;\nimport org.reflections.scanners.FieldAnnotationsScanner;\nimport org.reflections.scanners.MethodAnnotationsScanner;\nimport org.reflections.scanners.MethodParameterScanner;\nimport org.reflections.scanners.TypeAnnotationsScanner;\nimport org.reflections.util.ClasspathHelper;\nimport org.reflections.util.ConfigurationBuilder;\nimport org.reflections.util.FilterBuilder;\n\nimport java.io.File;\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.Parameter;\nimport java.lang.reflect.ParameterizedType;\nimport java.lang.reflect.Type;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic class ConfigModule extends AbstractModule {\n\n    private static final String CONFIG_FILE = \"concord.conf\";\n    private static final String LEGACY_CONFIG_FILE = \"ollie.conf\";\n\n    private static final Provider<Object> NULL_PROVIDER = () -> null;\n\n    private final com.typesafe.config.Config config;\n    private final Reflections reflections;\n    private final Set<Config> boundAnnotations;\n\n    public ConfigModule(String packageToScan, com.typesafe.config.Config config) {\n        var configBuilder = new ConfigurationBuilder()\n                .filterInputsBy(new FilterBuilder().includePackage(packageToScan))\n                .setUrls(ClasspathHelper.forPackage(packageToScan))\n                .setScanners(\n                        new TypeAnnotationsScanner(),\n                        new MethodParameterScanner(),\n                        new MethodAnnotationsScanner(),\n                        new FieldAnnotationsScanner());\n\n        this.config = config;\n        this.reflections = new Reflections(configBuilder);\n        this.boundAnnotations = new HashSet<>();\n    }\n\n    public static com.typesafe.config.Config load(String name) {\n        var options = ConfigResolveOptions.defaults().setAllowUnresolved(true);\n        var defaultConfig = ConfigFactory.load(name + \".conf\", ConfigParseOptions.defaults(), options);\n        var result = defaultConfig.getConfig(name);\n\n        if (System.getProperty(LEGACY_CONFIG_FILE) != null) {\n            var p = System.getProperty(LEGACY_CONFIG_FILE);\n            var externalConfig = ConfigFactory.parseFile(new File(p)).getConfig(name);\n            result = externalConfig.withFallback(result);\n        }\n\n        if (System.getProperty(CONFIG_FILE) != null) {\n            var p = System.getProperty(CONFIG_FILE);\n            var externalConfig = ConfigFactory.parseFile(new File(p)).getConfig(name);\n            result = externalConfig.withFallback(result);\n        }\n\n        return result.resolve();\n    }\n\n    @Override\n    public void configure() {\n        var annotatedConstructors = reflections.getConstructorsWithAnyParamAnnotated(Config.class);\n        for (var c : annotatedConstructors) {\n            var params = c.getParameters();\n            bindParameters(params);\n        }\n\n        var annotatedMethods = reflections.getMethodsWithAnyParamAnnotated(Config.class);\n        for (var m : annotatedMethods) {\n            var params = m.getParameters();\n            bindParameters(params);\n        }\n\n        var annotatedFields = reflections.getFieldsAnnotatedWith(Config.class);\n        for (var f : annotatedFields) {\n            var annotation = f.getAnnotation(Config.class);\n            bindValue(f.getType(), f.getAnnotatedType().getType(), annotation, isNullable(f.getAnnotations()));\n        }\n    }\n\n    private void bindParameters(Parameter[] params) {\n        for (var p : params) {\n            if (!p.isAnnotationPresent(Config.class)) {\n                continue;\n            }\n\n            var annotation = p.getAnnotation(Config.class);\n            bindValue(p.getType(), p.getAnnotatedType().getType(), annotation, isNullable(p.getAnnotations()));\n        }\n    }\n\n    private void bindValue(Class<?> paramClass, Type paramType, Config annotation, boolean nullable) {\n        if (boundAnnotations.contains(annotation)) {\n            return;\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        var key = (Key<Object>) Key.get(paramType, annotation);\n\n        var path = annotation.value();\n        var value = getConfigValue(paramClass, paramType, path, nullable);\n\n        if (value == null) {\n            if (nullable) {\n                bind(key).toProvider(NULL_PROVIDER);\n            } else {\n                throw new ConfigException.Missing(path);\n            }\n        } else {\n            bind(key).toInstance(value);\n        }\n\n        boundAnnotations.add(annotation);\n    }\n\n    private Object getConfigValue(Class<?> paramClass, Type paramType, String path, boolean nullable) {\n        var extractedValue = ConfigExtractors.extractConfigValue(config, paramClass, path);\n        if (extractedValue.isPresent()) {\n            return extractedValue.get();\n        }\n\n        if (nullable && !config.hasPath(path)) {\n            return null;\n        }\n\n        var value = config.getValue(path);\n        var type = value.valueType();\n        if (type.equals(ConfigValueType.OBJECT) && Map.class.isAssignableFrom(paramClass)) {\n            var object = config.getObject(path);\n            return object.unwrapped();\n        } else if (type.equals(ConfigValueType.OBJECT)) {\n            return ConfigBeanFactory.create(config.getConfig(path), paramClass);\n        } else if (type.equals(ConfigValueType.LIST) && List.class.isAssignableFrom(paramClass)) {\n            var listType = ((ParameterizedType) paramType).getActualTypeArguments()[0];\n\n            var extractedListValue = ListExtractors.extractConfigListValue(config, listType, path);\n\n            if (extractedListValue.isPresent()) {\n                return extractedListValue.get();\n            } else {\n                var configList = config.getConfigList(path);\n                return configList.stream()\n                        .map(cfg -> ConfigBeanFactory.create(cfg, (Class<?>) listType))\n                        .collect(Collectors.toList());\n            }\n        }\n\n        throw new RuntimeException(\"Cannot obtain config value for \" + paramType + \" at path: \" + path);\n    }\n\n    private static boolean isNullable(Annotation[] annotations) {\n        if (annotations == null || annotations.length == 0) {\n            return false;\n        }\n\n        return Arrays.stream(annotations)\n                .anyMatch(a -> \"Nullable\".equals(a.annotationType().getSimpleName()));\n    }\n}\n"
  },
  {
    "path": "config/src/main/java/com/walmartlabs/concord/config/ListExtractor.java",
    "content": "package com.walmartlabs.concord.config;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2018 Takari\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.lang.reflect.Type;\nimport java.util.List;\n\npublic interface ListExtractor {\n\n    List<?> extractListValue(com.typesafe.config.Config config, String path);\n\n    Type getMatchingParameterizedType();\n}\n"
  },
  {
    "path": "config/src/main/java/com/walmartlabs/concord/config/ListExtractors.java",
    "content": "package com.walmartlabs.concord.config;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2018 Takari\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.typesafe.config.Config;\nimport com.typesafe.config.ConfigMemorySize;\nimport com.typesafe.config.ConfigObject;\n\nimport java.lang.reflect.Type;\nimport java.time.Duration;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\n\npublic enum ListExtractors implements ListExtractor {\n    BOOLEAN(Boolean.class) {\n        @Override\n        public List<?> extractListValue(Config config, String path) {\n            return config.getBooleanList(path);\n        }\n    },\n    INTEGER(Integer.class) {\n        @Override\n        public List<?> extractListValue(Config config, String path) {\n            return config.getIntList(path);\n        }\n    },\n    DOUBLE(Double.class) {\n        @Override\n        public List<?> extractListValue(Config config, String path) {\n            return config.getDoubleList(path);\n        }\n    },\n    LONG(Long.class) {\n        @Override\n        public List<?> extractListValue(Config config, String path) {\n            return config.getLongList(path);\n        }\n    },\n    STRING(String.class) {\n        @Override\n        public List<?> extractListValue(Config config, String path) {\n            return config.getStringList(path);\n        }\n    },\n    DURATION(Duration.class) {\n        @Override\n        public List<?> extractListValue(Config config, String path) {\n            return config.getDurationList(path);\n        }\n    },\n    MEMORY_SIZE(ConfigMemorySize.class) {\n        @Override\n        public List<?> extractListValue(Config config, String path) {\n            return config.getMemorySizeList(path);\n        }\n    },\n    OBJECT(Object.class) {\n        @Override\n        public List<?> extractListValue(Config config, String path) {\n            return config.getAnyRefList(path);\n        }\n    },\n    CONFIG(Config.class) {\n        @Override\n        public List<?> extractListValue(Config config, String path) {\n            return config.getConfigList(path);\n        }\n    },\n    CONFIG_OBJECT(ConfigObject.class) {\n        @Override\n        public List<?> extractListValue(Config config, String path) {\n            return config.getObjectList(path);\n        }\n    },\n    CONFIG_VALUE(ConfigObject.class) {\n        @Override\n        public List<?> extractListValue(Config config, String path) {\n            return config.getList(path);\n        }\n    };\n\n    private final Class<?> parameterizedTypeClass;\n    private static final Map<Type, ListExtractor> EXTRACTOR_MAP = new HashMap<>();\n\n    static {\n        for (var extractor : ListExtractors.values()) {\n            EXTRACTOR_MAP.put(extractor.getMatchingParameterizedType(), extractor);\n        }\n    }\n\n    ListExtractors(Class<?> parameterizedTypeClass) {\n        this.parameterizedTypeClass = parameterizedTypeClass;\n    }\n\n    @Override\n    public Type getMatchingParameterizedType() {\n        return parameterizedTypeClass;\n    }\n\n    static Optional<List<?>> extractConfigListValue(Config config, Type listType, String path) {\n        if (EXTRACTOR_MAP.containsKey(listType)) {\n            return Optional.of(EXTRACTOR_MAP.get(listType).extractListValue(config, path));\n        } else {\n            return Optional.empty();\n        }\n    }\n}\n"
  },
  {
    "path": "console2/.gitignore",
    "content": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n\n# testing\n/coverage\n\n# production\n/build\n/dist\n\n# Vite\n*.local\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n.vscode\n.xit\n\n/storybook-static\n.docz\ndoczrc.js\n*.mdx\n"
  },
  {
    "path": "console2/.npmrc",
    "content": "registry=https://registry.npmjs.org\n"
  },
  {
    "path": "console2/README.md",
    "content": "# Concord UI\n\n## Prerequisites\n\n- Node 24 LTS, available in `$PATH`;\n- Java 17, available in `$PATH`. Necessary only to build the package.\n\n## Dependencies\n\nTo install the necessary dependencies for the first time:\n```bash\n$ npm ci\n```\n\nTo update `package-lock.json` run\n```bash\n$ ../mvnw clean package # only for the first run\n$ ../mvnw com.github.eirslett:frontend-maven-plugin:npm -Darguments=install\n```\n\n## Running in Dev Mode\n\nIn the dev mode the UI is served by running `npm start`.\n\nFirst time:\n```bash\n$ npm ci\n$ npm run dev\n```\n\nOpen http://localhost:3000.\n\nThe `ci` step can be skipped for subsequent runs.\n\nThe dev mode has the following limitations:\n- file download (e.g. downloading raw logs) doesn't work;\n- [custom forms](https://concord.walmartlabs.com/docs/getting-started/forms.html#custom) don't work.\n\nIn order to use those features, you need to run the UI in production\nmode (see below).\n\n## Running in Production Mode\n\nIn the production mode the UI is served by concord-server from the JAR file\ncreated during concord-console2 [build](./pom.xml).\n\nWhen running locally, it is available at http://localhost:8001.\n\n## Configuration\n\nSpecify the path to the `cfg.js` file when you start\n[the Server](../server/dist):\n\n```\nCONSOLE_CFG_FILE=/path/to/cfg.js\n```\n\nor using concord-server.conf:\n\n```\nconcord-server {\n  console {\n     cfgFile = \"/path/to/cfg.js\"\n  }\n}\n```\n\nUse [./public/cfg.js](./public/cfg.js) as an example.\n"
  },
  {
    "path": "console2/cfg.d.ts",
    "content": "import {ColumnDefinition} from \"./src/api/org\";\n\nexport {};\n\nexport interface ConcordEnvironment {\n    topBar?: TopBarMeta;\n    loginUrl?: string;\n    logoutUrl?: string;\n    login?: LoginConfiguration;\n    extraProcessMenuLinks?: ExtraProcessMenuLinks;\n    lastUpdated?: string;\n    customResources?: CustomResources;\n    processListColumns?: ColumnDefinition[];\n}\n\nexport interface TopBarMeta {\n    systemLinks?: SystemLinks;\n}\n\nexport type SystemLinks = LinkMeta[];\n\nexport type ExtraProcessMenuLinks = ExtraProcessMenuLink[];\n\nexport interface LinkMeta {\n    text: string;\n    url: string;\n    icon?: string;\n}\n\nexport interface LoginConfiguration {\n    usernameValidator?: (username: string) => string | undefined;\n    usernameHint?: string;\n}\n\nexport interface ExtraProcessMenuLink {\n    url: string;\n    label: string;\n    color: string;\n    icon: string;\n}\n\nexport interface CustomResources {\n    [key: string]: CustomResource;\n}\n\nexport interface CustomResource {\n    title?: string;\n    description?: string;\n    icon?: string;\n    url: string;\n    width?: string;\n    height?: string;\n}\n\ndeclare global {\n    interface Window {\n        concord: ConcordEnvironment;\n    }\n}\n"
  },
  {
    "path": "console2/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n\n  <!-- Load config before app initialization -->\n  <script src=\"/api/service/console/cfg\"></script>\n\n  <link id=\"favicon\" rel=\"shortcut icon\" href=\"/favicon.png\">\n  <base href=\"/\" />\n  <title>Concord</title>\n</head>\n\n<body>\n  <noscript>\n    You need to enable JavaScript to run this app.\n  </noscript>\n  <div id=\"root\"></div>\n\n  <!-- Vite entry point -->\n  <script type=\"module\" src=\"/src/index.tsx\"></script>\n</body>\n\n</html>"
  },
  {
    "path": "console2/npm.sh",
    "content": "#!/usr/bin/env bash\n# run a local version of node installed by Maven\nexport SET NODE_OPTIONS=--openssl-legacy-provider\n./target/node/node ./target/node/node_modules/npm/bin/npm-cli.js \"$@\"\n"
  },
  {
    "path": "console2/package/META-INF/concord/webapp.properties",
    "content": "path=/\nchecksumsFileResourcePath=META-INF/console2.checksums.cvs\nresourceRoot=META-INF/console2/\nindexHtmlRelativePath=index.html\n"
  },
  {
    "path": "console2/package.json",
    "content": "{\n    \"name\": \"concord-console\",\n    \"version\": \"1.0.0\",\n    \"private\": true,\n    \"type\": \"module\",\n    \"devDependencies\": {\n        \"@datasert/cronjs-matcher\": \"^1.4.0\",\n        \"@testing-library/react\": \"12.1.2\",\n        \"@types/jest\": \"27.0.3\",\n        \"@types/lodash\": \"4.14.178\",\n        \"@types/react\": \"^17.0.91\",\n        \"@types/react-dom\": \"^17.0.26\",\n        \"@types/sinon\": \"10.0.6\",\n        \"@types/styled-components\": \"5.1.17\",\n        \"@typescript-eslint/typescript-estree\": \"5.35.1\",\n        \"@vitejs/plugin-react\": \"^6.0.1\",\n        \"esbuild\": \"^0.27.4\",\n        \"eslint\": \"7.32.0\",\n        \"prettier\": \"2.5.1\",\n        \"react-hooks-testing-library\": \"0.6.0\",\n        \"shx\": \"0.3.3\",\n        \"ts-node\": \"10.4.0\",\n        \"typescript\": \"5.3.3\",\n        \"vite\": \"^8.0.5\"\n    },\n    \"dependencies\": {\n        \"@monaco-editor/react\": \"4.3.1\",\n        \"ansi_up\": \"6.0.6\",\n        \"constate\": \"3.3.0\",\n        \"copy-to-clipboard\": \"3.3.1\",\n        \"date-fns\": \"2.27.0\",\n        \"formik\": \"2.2.9\",\n        \"lodash\": \"4.18.1\",\n        \"parse-domain\": \"4.1.0\",\n        \"query-string\": \"7.0.1\",\n        \"react\": \"18.2.0\",\n        \"react-dom\": \"18.2.0\",\n        \"react-hook-form\": \"7.21.2\",\n        \"react-idle-timer\": \"5.4.1\",\n        \"react-json-view\": \"1.21.3\",\n        \"react-router\": \"7.14.1\",\n        \"react-spring\": \"9.3.2\",\n        \"reakit\": \"1.3.11\",\n        \"semantic-ui-calendar-react\": \"0.15.3\",\n        \"semantic-ui-css\": \"2.4.1\",\n        \"semantic-ui-react\": \"2.0.4\",\n        \"styled-components\": \"5.3.3\",\n        \"styled-tools\": \"1.7.2\",\n        \"typeface-lato\": \"1.1.13\",\n        \"url-search-params-polyfill\": \"8.1.1\"\n    },\n    \"scripts\": {\n        \"start\": \"vite\",\n        \"dev\": \"vite\",\n        \"build\": \"vite build\",\n        \"build:check\": \"tsc && vite build\",\n        \"preview\": \"vite preview\",\n        \"test\": \"vitest\",\n        \"pretty\": \"prettier --tab-width 4 --print-width 100 --single-quote --jsx-bracket-same-line --arrow-parens 'always' parser 'typescript' --write 'src/**/*.{ts,tsx}'\"\n    },\n    \"eslintConfig\": {\n        \"extends\": [\n            \"react-app\",\n            \"react-app/jest\"\n        ],\n        \"rules\": {\n            \"import/no-anonymous-default-export\": [\n                2,\n                {\n                    \"allowArrowFunction\": true,\n                    \"allowAnonymousFunction\": true,\n                    \"allowAnonymousClass\": true\n                }\n            ]\n        }\n    },\n    \"browserslist\": [\n        \">0.2%\",\n        \"not dead\",\n        \"not ie <= 11\",\n        \"not op_mini all\"\n    ],\n    \"proxy\": \"http://localhost:8001\"\n}\n"
  },
  {
    "path": "console2/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-console2</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <properties>\n        <skipNpm>false</skipNpm>\n        <node.downloadRoot>https://nodejs.org/dist/</node.downloadRoot>\n        <npm.installCmd>ci</npm.installCmd>\n    </properties>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>license-maven-plugin</artifactId>\n                <configuration>\n                    <roots>\n                        <root>src</root>\n                    </roots>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>com.github.eirslett</groupId>\n                <artifactId>frontend-maven-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>install node and npm</id>\n                        <phase>generate-resources</phase>\n                        <goals>\n                            <goal>install-node-and-npm</goal>\n                        </goals>\n                        <configuration>\n                            <skip>${skipNpm}</skip>\n                        </configuration>\n                    </execution>\n                    <execution>\n                        <id>npm ci</id>\n                        <goals>\n                            <goal>npm</goal>\n                        </goals>\n                        <configuration>\n                            <skip>${skipNpm}</skip>\n                            <arguments>${npm.installCmd} --legacy-peer-deps</arguments>\n                        </configuration>\n                    </execution>\n                    <execution>\n                        <id>build</id>\n                        <phase>generate-resources</phase>\n                        <goals>\n                            <goal>npm</goal>\n                        </goals>\n                        <configuration>\n                            <environmentVariables>\n                                <REACT_APP_CONCORD_VERSION>${project.version}</REACT_APP_CONCORD_VERSION>\n                            </environmentVariables>\n                            <skip>${skipNpm}</skip>\n                            <arguments>run build</arguments>\n                        </configuration>\n                    </execution>\n                </executions>\n                <configuration>\n                    <environmentVariables>\n                        <ADBLOCK>true</ADBLOCK> <!-- disable annoying npm postinstall messages -->\n                    </environmentVariables>\n                    <installDirectory>target</installDirectory>\n                    <nodeDownloadRoot>${node.downloadRoot}</nodeDownloadRoot>\n                    <nodeVersion>v${node.version}</nodeVersion>\n                    <workingDirectory>${basedir}</workingDirectory>\n                </configuration>\n            </plugin>\n            <plugin>\n                <artifactId>maven-resources-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>copy-package-descriptor</id>\n                        <phase>prepare-package</phase>\n                        <goals>\n                            <goal>copy-resources</goal>\n                        </goals>\n                        <configuration>\n                            <outputDirectory>${basedir}/target/classes</outputDirectory>\n                            <resources>\n                                <resource>\n                                    <directory>package</directory>\n                                    <includes>\n                                        <include>**/*</include>\n                                    </includes>\n                                </resource>\n                            </resources>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>net.nicoulaj.maven.plugins</groupId>\n                <artifactId>checksum-maven-plugin</artifactId>\n                <version>1.11</version>\n                <executions>\n                    <execution>\n                        <id>create-checksum-file</id>\n                        <phase>prepare-package</phase>\n                        <goals>\n                            <goal>files</goal>\n                        </goals>\n                        <configuration>\n                            <fileSets>\n                                <fileSet>\n                                    <directory>${project.build.directory}/classes/META-INF/console2</directory>\n                                    <includes>\n                                        <include>**/*.*</include>\n                                    </includes>\n                                </fileSet>\n                            </fileSets>\n                            <algorithms>\n                                <algorithm>SHA-1</algorithm>\n                            </algorithms>\n                            <csvSummaryFile>classes/META-INF/console2.checksums.cvs</csvSummaryFile>\n                            <includeRelativePath>true</includeRelativePath>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "console2/public/cfg.js",
    "content": "// environment specific data\nwindow.concord = {\n    documentationSite: 'https://concord.walmartlabs.com',\n    topBar: {\n        systemLinks: [\n            {\n                text: 'GitHub',\n                url: 'https://github.com/walmartlabs/concord',\n                icon: 'github'\n            }\n        ]\n    }\n};\n\n"
  },
  {
    "path": "console2/public/manifest.json",
    "content": "{\n  \"short_name\": \"React App\",\n  \"name\": \"Create React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"192x192\",\n      \"type\": \"image/png\"\n    }\n  ],\n  \"start_url\": \"./index.html\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "console2/react-json-view.d.ts",
    "content": "// adapted from the original react-json-view/index.d.ts file\n\nimport * as React from 'react';\n\nexport interface ReactJsonViewProps {\n  /**\n   * This property contains your input JSON.\n   *\n   * Required.\n   */\n  src: object;\n  /**\n   * Contains the name of your root node. Use null or false for no name.\n   *\n   * Default: \"root\"\n   */\n  name?: string | null | false;\n  /**\n   * RJV supports base-16 themes. Check out the list of supported themes in the demo.\n   * A custom \"rjv-default\" theme applies by default.\n   *\n   * Default: \"rjv-default\"\n   */\n  theme?: ThemeKeys | ThemeObject;\n  /**\n   * Style attributes for react-json-view container.\n   * Explicit style attributes will override attributes provided by a theme.\n   *\n   * Default: \"rjv-default\"\n   */\n  style?: React.CSSProperties;\n  /**\n   * Style of expand/collapse icons. Accepted values are \"circle\", triangle\" or \"square\".\n   *\n   * Default: {}\n   */\n  iconStyle?: 'circle' | 'triangle' | 'square';\n  /**\n   * Set the indent-width for nested objects.\n   *\n   * Default: 4\n   */\n  indentWidth?: number;\n  /**\n   * When set to true, all nodes will be collapsed by default.\n   * Use an integer value to collapse at a particular depth.\n   *\n   * Default: false\n   */\n  collapsed?: boolean | number;\n  /**\n   * When an integer value is assigned, strings will be cut off at that length.\n   * Collapsed strings are followed by an ellipsis.\n   * String content can be expanded and collapsed by clicking on the string value.\n   *\n   * Default: false\n   */\n  collapseStringsAfterLength?: number | false;\n  /**\n   * Callback function to provide control over what objects and arrays should be collapsed by default.\n   * An object is passed to the callback containing name, src, type (\"array\" or \"object\") and namespace.\n   *\n   * Default: false\n   */\n  shouldCollapse?: false | ((field: CollapsedFieldProps) => boolean);\n  /**\n   * When an integer value is assigned, arrays will be displayed in groups by count of the value.\n   * Groups are displayed with brakcet notation and can be expanded and collapsed by clickong on the brackets.\n   *\n   * Default: 100\n   */\n  groupArraysAfterLength?: number;\n  /**\n   * When prop is not false, the user can copy objects and arrays to clipboard by clicking on the clipboard icon.\n   * Copy callbacks are supported.\n   *\n   * Default: true\n   */\n  enableClipboard?: boolean | ((copy: OnCopyProps) => void);\n  /**\n   * When set to true, objects and arrays are labeled with size.\n   *\n   * Default: true\n   */\n  displayObjectSize?: boolean;\n  /**\n   * When set to true, data type labels prefix values.\n   *\n   * Default: true\n   */\n  displayDataTypes?: boolean;\n  /**\n   * When a callback function is passed in, edit functionality is enabled.\n   * The callback is invoked before edits are completed. Returning false\n   * from onEdit will prevent the change from being made. see: onEdit docs.\n   *\n   * Default: false\n   */\n  onEdit?: ((edit: InteractionProps) => false | any) | false;\n  /**\n   * When a callback function is passed in, add functionality is enabled.\n   * The callback is invoked before additions are completed.\n   * Returning false from onAdd will prevent the change from being made. see: onAdd docs\n   *\n   * Default: false\n   */\n  onAdd?: ((add: InteractionProps) => false | any) | false;\n  /**\n   * When a callback function is passed in, delete functionality is enabled.\n   * The callback is invoked before deletions are completed.\n   * Returning false from onDelete will prevent the change from being made. see: onDelete docs\n   *\n   * Default: false\n   */\n  onDelete?: ((del: InteractionProps) => false | any) | false;\n  /**\n   * When a function is passed in, clicking a value triggers the onSelect method to be called.\n   *\n   * Default: false\n   */\n  onSelect?: ((select: OnSelectProps) => void) | false;\n  /**\n   * Custom message for validation failures to onEdit, onAdd, or onDelete callbacks.\n   *\n   * Default: \"Validation Error\"\n   */\n  validationMessage?: string;\n}\n\nexport interface OnCopyProps {\n  /**\n   * The JSON tree source object\n   */\n  src: object;\n  /**\n   * List of keys.\n   */\n  namespace: Array<string | null>;\n  /**\n   * The last key in the namespace array.\n   */\n  name: string | null;\n}\n\nexport interface CollapsedFieldProps {\n  /**\n   * The name of the entry.\n   */\n  name: string | null;\n  /**\n   * The corresponding JSON subtree.\n   */\n  src: object;\n  /**\n   * The type of src. Can only be \"array\" or \"object\".\n   */\n  type: 'array' | 'object';\n  /**\n   * The scopes above the current entry.\n   */\n  namespace: Array<string | null>;\n}\n\nexport interface InteractionProps {\n  /**\n   * The updated subtree of the JSON tree.\n   */\n  updated_src: object;\n  /**\n   * The existing subtree of the JSON tree.\n   */\n  existing_src: object;\n  /**\n   * The key of the entry that is interacted with.\n   */\n  name: string | null;\n  /**\n   * List of keys.\n   */\n  namespace: Array<string | null>;\n  /**\n   * The original value of the entry that is interacted with.\n   */\n  existing_value: object | string | number | boolean | null;\n  /**\n   * The updated value of the entry that is interacted with.\n   */\n  new_value?: object | string | number | boolean | null;\n}\n\nexport interface OnSelectProps {\n  /**\n   * The name of the currently selected entry.\n   */\n  name: string | null;\n  /**\n   * The value of the currently selected entry.\n   */\n  value: object | string | number | boolean | null;\n  /**\n   * The type of the value. For \"number\" type, it will be replaced with the more\n   * accurate types: \"float\", \"integer\", or \"nan\".\n   */\n  type: string;\n  /**\n   * List of keys representing the scopes above the selected entry.\n   */\n  namespace: Array<string | null>;\n\n}\n\nexport interface ThemeObject {\n  base00: string;\n  base01: string;\n  base02: string;\n  base03: string;\n  base04: string;\n  base05: string;\n  base06: string;\n  base07: string;\n  base08: string;\n  base09: string;\n  base0A: string;\n  base0B: string;\n  base0C: string;\n  base0D: string;\n  base0E: string;\n  base0F: string;\n}\n\nexport type ThemeKeys =\n  | 'apathy'\n  | 'apathy:inverted'\n  | 'ashes'\n  | 'bespin'\n  | 'brewer'\n  | 'bright:inverted'\n  | 'bright'\n  | 'chalk'\n  | 'codeschool'\n  | 'colors'\n  | 'eighties'\n  | 'embers'\n  | 'flat'\n  | 'google'\n  | 'grayscale'\n  | 'grayscale:inverted'\n  | 'greenscreen'\n  | 'harmonic'\n  | 'hopscotch'\n  | 'isotope'\n  | 'marrakesh'\n  | 'mocha'\n  | 'monokai'\n  | 'ocean'\n  | 'paraiso'\n  | 'pop'\n  | 'railscasts'\n  | 'rjv-default'\n  | 'shapeshifter'\n  | 'shapeshifter:inverted'\n  | 'solarized'\n  | 'summerfruit'\n  | 'summerfruit:inverted'\n  | 'threezerotwofour'\n  | 'tomorrow'\n  | 'tube'\n  | 'twilight';\n\ndeclare const ReactJson: React.ComponentType<ReactJsonViewProps>;\nexport default ReactJson;\n"
  },
  {
    "path": "console2/src/App.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Dispatch, useEffect, useReducer, useState } from 'react';\nimport { HashRouter, Navigate, Route, Routes } from 'react-router';\n\nimport { ProtectedRoute } from './components/organisms';\nimport {\n    AboutPage,\n    AddRepositoryPage,\n    CustomResourcePage,\n    JsonStorePage,\n    LoginPage,\n    LogoutPage,\n    NewProjectPage,\n    NewSecretPage,\n    NewTeamPage,\n    NotFoundPage,\n    OrganizationListPage,\n    OrganizationPage,\n    ProcessFormPage,\n    ProcessListPage,\n    ProcessCardFormPage,\n    ProcessPage,\n    ProcessWizardPage,\n    ProfilePage,\n    ProjectPage,\n    RepositoryPage,\n    SecretPage,\n    TeamPage,\n    UnauthorizedPage,\n    UserActivityPage,\n} from './components/pages';\nimport { Layout } from './components/templates';\nimport NewStorageQueryPage from './components/pages/JsonStorePage/NewStorageQueryPage';\nimport EditStoreQueryPage from './components/pages/JsonStorePage/EditStoreQueryPage';\nimport { initialState, LoadingAction, reducer } from './reducers/loading';\nimport NewStorePage from './components/pages/JsonStorePage/NewStorePage';\nimport NodeRosterPage from './components/pages/NodeRoster/NodeRosterPage';\nimport HostPage from './components/pages/NodeRoster/HostPage';\nimport { UserSessionContext, checkSession, UserInfo } from './session';\n\nexport const LoadingDispatch = React.createContext<Dispatch<LoadingAction>>(\n    {} as Dispatch<LoadingAction>\n);\n\nexport const LoadingState = React.createContext(false);\n\nconst App = () => {\n    const [state, dispatch] = useReducer(reducer, initialState);\n\n    const [userInfo, setUserInfo] = useState<UserInfo | undefined>();\n    const [loggingIn, setLoggingIn] = useState(true);\n\n    useEffect(() => {\n        checkSession({ userInfo, setUserInfo, loggingIn: false, setLoggingIn });\n    }, [userInfo]);\n\n    return (\n        <LoadingState.Provider value={state.loading}>\n            <LoadingDispatch.Provider value={dispatch}>\n                <HashRouter>\n                    <UserSessionContext.Provider\n                        value={{ userInfo, setUserInfo, loggingIn, setLoggingIn }}\n                    >\n                        <Routes>\n                            <Route path=\"/\" element={<Navigate to=\"/activity\" replace={true} />} />\n\n                            {/* pages with no decorations */}\n                            <Route path=\"/login\" element={<LoginPage />} />\n                            <Route path=\"/logout/done\" element={<LogoutPage />} />\n                            <Route path=\"/unauthorized\" element={<UnauthorizedPage />} />\n                            <Route\n                                path=\"/processCard/:cardId/form\"\n                                element={\n                                    <ProtectedRoute>\n                                        <ProcessCardFormPage />\n                                    </ProtectedRoute>\n                                }\n                            />\n\n                            {/* pages with standard decorations (provided by Layout) */}\n                            <Route element={<ProtectedRoute />}>\n                                <Route element={<Layout />}>\n                                    <Route path=\"/activity\" element={<UserActivityPage />} />\n                                    <Route path=\"/org\" element={<OrganizationListPage />} />\n                                    <Route\n                                        path=\"/org/:orgName/project/:projectName/repository/_new\"\n                                        element={<AddRepositoryPage />}\n                                    />\n                                    <Route\n                                        path=\"/org/:orgName/project/:projectName/repository/:repoName/*\"\n                                        element={<RepositoryPage />}\n                                    />\n                                    <Route\n                                        path=\"/org/:orgName/project/_new\"\n                                        element={<NewProjectPage />}\n                                    />\n                                    <Route\n                                        path=\"/org/:orgName/project/:projectName/*\"\n                                        element={<ProjectPage />}\n                                    />\n                                    <Route\n                                        path=\"/org/:orgName/secret/_new\"\n                                        element={<NewSecretPage />}\n                                    />\n                                    <Route\n                                        path=\"/org/:orgName/secret/:secretName/*\"\n                                        element={<SecretPage />}\n                                    />\n                                    <Route\n                                        path=\"/org/:orgName/team/_new\"\n                                        element={<NewTeamPage />}\n                                    />\n                                    <Route\n                                        path=\"/org/:orgName/team/:teamName/*\"\n                                        element={<TeamPage />}\n                                    />\n                                    <Route\n                                        path=\"/org/:orgName/jsonstore/_new\"\n                                        element={<NewStorePage />}\n                                    />\n                                    <Route\n                                        path=\"/org/:orgName/jsonstore/:storeName/query/_new\"\n                                        element={<NewStorageQueryPage />}\n                                    />\n                                    <Route\n                                        path=\"/org/:orgName/jsonstore/:storeName/query/:queryName/edit\"\n                                        element={<EditStoreQueryPage />}\n                                    />\n                                    <Route\n                                        path=\"/org/:orgName/jsonstore/:storeName/*\"\n                                        element={<JsonStorePage />}\n                                    />\n                                    <Route path=\"/org/:orgName/*\" element={<OrganizationPage />} />\n\n                                    <Route path=\"/process\" element={<ProcessListPage />} />\n                                    <Route\n                                        path=\"/process/:processInstanceId/form/:formName/:mode\"\n                                        element={<ProcessFormPage />}\n                                    />\n                                    <Route\n                                        path=\"/process/:instanceId/wizard\"\n                                        element={<ProcessWizardPage />}\n                                    />\n                                    <Route\n                                        path=\"/process/:instanceId/*\"\n                                        element={<ProcessPage />}\n                                    />\n\n                                    <Route path=\"/noderoster/host/:id/*\" element={<HostPage />} />\n                                    <Route path=\"/noderoster/*\" element={<NodeRosterPage />} />\n\n                                    <Route path=\"/about\" element={<AboutPage />} />\n                                    <Route path=\"/profile/*\" element={<ProfilePage />} />\n                                    <Route\n                                        path=\"/custom/:resourceName\"\n                                        element={<CustomResourcePage />}\n                                    />\n                                    <Route path=\"*\" element={<NotFoundPage />} />\n                                </Route>\n                            </Route>\n                            <Route path=\"*\" element={<NotFoundPage />} />\n                        </Routes>\n                    </UserSessionContext.Provider>\n                </HashRouter>\n            </LoadingDispatch.Provider>\n        </LoadingState.Provider>\n    );\n    // }\n};\n\nexport default App;\n"
  },
  {
    "path": "console2/src/api/__tests__/common.deepMerge.test.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { deepMerge } from '../common';\n\ntest('deepMerge works', () => {\n    const actual = deepMerge({ x: { y: 123 } }, { x: { z: 234 } });\n    const expected = { x: { y: 123, z: 234 } };\n    expect(actual).toEqual(expected);\n});\n"
  },
  {
    "path": "console2/src/api/__tests__/common.parseNestedQueryParams.test.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { parseNestedQueryParams } from '../common';\n\ntest('parseNestedQueryParams works with a single value', () => {\n    const actual = parseNestedQueryParams({ 'x.y.z': '123' }, ['x']);\n    const expected = { x: { y: { z: '123' } } };\n    expect(actual).toEqual(expected);\n});\n\ntest('parseNestedQueryParams works with multiple nested values', () => {\n    const actual = parseNestedQueryParams({ normalOne: 'true', 'x.y.a': '123', 'x.y.b': '234' }, [\n        'x'\n    ]);\n    const expected = { normalOne: 'true', x: { y: { b: '234', a: '123' } } };\n    expect(actual).toEqual(expected);\n});\n"
  },
  {
    "path": "console2/src/api/__tests__/common.parseQueryParams.test.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { parseQueryParams } from '../common';\n\ntest('parseQueryParams handles one query parameter', () => {\n    const actual = parseQueryParams('http://localhost:3000/#/org/?param1=123');\n    const expected = { param1: '123' };\n    expect(actual).toEqual(expected);\n});\n\ntest('parseQueryParams handles more than one query parameter', () => {\n    const actual = parseQueryParams('http://localhost:3000/#/org/?param1=123&param2=abc');\n    const expected = { param1: '123', param2: 'abc' };\n    expect(actual).toEqual(expected);\n});\n\ntest('parseQueryParams handles periods in a query parameter', () => {\n    const result = parseQueryParams('http://localhost:3000/#/org/?param.1=123&param.2=abc');\n    const expected = { 'param.1': '123', 'param.2': 'abc' };\n    expect(result).toEqual(expected);\n});\n\ntest('parseQueryParams handles url with no question mark \"?\"', () => {\n    const actual = parseQueryParams('http://localhost:3000/#/org/param.1=123');\n    const expected = {};\n    expect(actual).toEqual(expected);\n});\n\ntest('parseQueryParams handles url with no params', () => {\n    const result = parseQueryParams('http://localhost:3000/#/org/param.1=123');\n    const expected = {};\n    expect(result).toEqual(expected);\n});\n\ntest('parseQueryParams handles multi-value params', () => {\n    const result = parseQueryParams('http://localhost:3000/#/org/?param.1=123&param.1=234');\n    const expected = { 'param.1': ['123', '234'] };\n    expect(result).toEqual(expected);\n});\n"
  },
  {
    "path": "console2/src/api/__tests__/common.queryParams.test.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { queryParams } from '../common';\nimport { AnsibleStatus, SearchFilter, SortField, SortOrder } from '../process/ansible';\n\ntest('queryParams accepts an object of key value pairs.  e.g. { key: value, ... }', () => {\n    const actual = queryParams({ param1: '123', param2: 'abc' });\n    const expected = 'param1=123&param2=abc';\n    expect(actual).toEqual(expected);\n});\n\ntest('queryParams handles parameters with period characters in key name', () => {\n    const actual = queryParams({ 'param.1': '123', 'param.2': 'abc' });\n    const expected = 'param.1=123&param.2=abc';\n    expect(actual).toEqual(expected);\n});\n\ntest('queryParams accepts numbers as values', () => {\n    const actual = queryParams({ param: 1, param2: 999 });\n    const expected = 'param=1&param2=999';\n    expect(actual).toEqual(expected);\n});\n\ntest('queryParams handles multiple boolean values', () => {\n    const actual = queryParams({ param: true, param2: false });\n    const expected = 'param=true&param2=false';\n    expect(actual).toEqual(expected);\n});\n\ntest('queryParams handles undefined values', () => {\n    const actual = queryParams({ param: undefined, param2: 'works' });\n    const expected = 'param2=works';\n    expect(actual).toEqual(expected);\n});\n\ntest('queryParams handles SearchFilter type', () => {\n    const filters: SearchFilter = {\n        host: 'host',\n        hostGroup: 'host-group',\n        limit: 1,\n        offset: 10,\n        status: AnsibleStatus.CHANGED,\n        sortField: SortField.DURATION,\n        sortBy: SortOrder.DESC\n    };\n\n    const actual = queryParams({ ...filters });\n    const expected = 'host=host&hostGroup=host-group&limit=1&offset=10&status=CHANGED&sortField=DURATION&sortBy=DESC';\n    expect(actual).toEqual(expected);\n});\n"
  },
  {
    "path": "console2/src/api/audit/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { ConcordId, ConcordKey, EntityOwner, fetchJson, queryParams } from '../common';\n\n// must match the keys of com.walmartlabs.concord.server.audit.AuditObject enum\nexport enum AuditObject {\n    EXTERNAL_EVENT = 'EXTERNAL_EVENT',\n    JSON_STORE = 'JSON_STORE',\n    JSON_STORE_DATA = 'JSON_STORE_DATA',\n    JSON_STORE_QUERY = 'JSON_STORE_QUERY',\n    ORGANIZATION = 'ORGANIZATION',\n    PROJECT = 'PROJECT',\n    PROCESS = 'PROCESS',\n    SECRET = 'SECRET',\n    TEAM = 'TEAM'\n}\n\n// must match the keys of com.walmartlabs.concord.server.audit.AuditAction enum\nexport enum AuditAction {\n    CREATE = 'CREATE',\n    UPDATE = 'UPDATE',\n    DELETE = 'DELETE',\n    ACCESS = 'ACCESS'\n}\n\n// must match the allowed keys in AuditLogResource#ALLOWED_DETAILS_KEYS\nexport interface AuditLogFilter {\n    object?: AuditObject;\n    action?: AuditAction;\n    userId?: ConcordId;\n    username?: string;\n    after?: string;\n    before?: string;\n    details?: {\n        eventId?: string;\n        fullRepoName?: string;\n        githubEvent?: string;\n        source?: string;\n        orgName?: ConcordKey;\n        projectName?: ConcordKey;\n        secretName?: ConcordKey;\n        jsonStoreName?: ConcordKey;\n        teamName?: ConcordKey;\n    };\n    offset?: number; // TODO rename to \"page\"?\n    limit?: number;\n}\n\nexport interface AuditLogEntry {\n    entryDate: string;\n    action: string;\n    object: string;\n    details: {};\n    user?: EntityOwner;\n}\n\nexport interface PaginatedAuditLogEntries {\n    items: AuditLogEntry[];\n    next: boolean;\n}\n\nexport const list = async (filter: AuditLogFilter): Promise<PaginatedAuditLogEntries> => {\n    const { offset = 0, limit = 50 } = filter;\n\n    const offsetParam = offset > 0 && limit > 0 ? offset * limit : offset;\n    const limitParam = limit > 0 ? limit + 1 : limit;\n\n    // convert the `details` object into a set of `details.key=value` query parameters\n    const details = filter.details || {};\n\n    const detailsParam = {};\n    Object.keys(details).forEach((k) => (detailsParam[`details.${k}`] = details[k]));\n\n    const data: AuditLogEntry[] = await fetchJson(\n        `/api/v1/audit?${queryParams({\n            ...detailsParam,\n            object: filter.object,\n            action: filter.action,\n            userId: filter.userId,\n            username: filter.username,\n            after: filter.after,\n            before: filter.before,\n            offset: offsetParam,\n            limit: limitParam\n        })}`\n    );\n\n    const hasMoreElements: boolean = !!limit && data.length > limit;\n\n    if (limit > 0 && hasMoreElements) {\n        // TODO we should trim the list to the size instead of removing a single element\n        data.pop();\n    }\n\n    return {\n        items: data,\n        next: hasMoreElements\n    };\n};\n"
  },
  {
    "path": "console2/src/api/common.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nexport type ConcordId = string;\nexport type ConcordKey = string;\n\nexport interface Owner {\n    username: string;\n    userDomain?: string;\n}\n\nexport interface RequestErrorData {\n    instanceId?: ConcordId;\n    message?: string;\n    details?: string;\n    status: number;\n    level?: string;\n}\n\nexport type RequestError = RequestErrorData | null;\n\nexport const parseSiestaError = async (resp: Response) => {\n    const json = await resp.json();\n\n    let message;\n    if (resp.status < 400 || resp.status >= 500) {\n        message = `ERROR: ${resp.statusText} (${resp.status})`;\n    }\n\n    return {\n        message,\n        instanceId: json.instanceId,\n        details: json[0].message,\n        status: resp.status\n    };\n};\n\nexport const parseJsonError = async (resp: Response) => {\n    const json = await resp.json();\n\n    let message;\n    if (resp.status < 400 || resp.status >= 500) {\n        message = json.message;\n    }\n\n    return {\n        message,\n        instanceId: json.instanceId,\n        details: json.details,\n        level: json.level ? json.level : 'ERROR',\n        status: resp.status\n    };\n};\n\nexport const parseTextError = async (resp: Response) => {\n    const text = await resp.text();\n\n    let message;\n    if (resp.status < 400 && resp.status >= 500) {\n        message = `ERROR: ${resp.statusText} (${resp.status})`;\n    }\n\n    return {\n        message,\n        details: text,\n        status: resp.status\n    };\n};\n\nexport const makeError = async (resp: Response): Promise<RequestError> => {\n    const contentLength = resp.headers.get('Content-Length');\n    if (contentLength !== '0') {\n        const contentType = resp.headers.get('Content-Type') || '';\n        try {\n            if (contentType.indexOf('vnd.concord-validation-errors-v1+json') >= 0) {\n                return parseSiestaError(resp);\n            } else if (contentType.indexOf('json') >= 0) {\n                return parseJsonError(resp);\n            } else if (contentType.indexOf('text/plain') >= 0) {\n                return parseTextError(resp);\n            }\n        } catch (e) {\n            console.warn('makeError -> error while parsing the response: %o', e);\n            // fall back to the default error handling\n        }\n    }\n\n    return {\n        message: `ERROR: ${resp.statusText} (${resp.status})`,\n        status: resp.status\n    };\n};\n\nexport const managedFetch = async (input: RequestInfo, init?: RequestInit): Promise<Response> => {\n    if (!init) {\n        init = {};\n    }\n\n    init.credentials = 'same-origin';\n\n    if (!init.headers) {\n        init.headers = new Headers();\n    }\n\n    // send a special header with each request to indicate that this is, in fact, a UI request\n    if (init.headers instanceof Headers) {\n        init.headers.set('X-Concord-UI-Request', 'true');\n    } else {\n        init.headers = { ...init.headers, 'X-Concord-UI-Request': 'true' };\n    }\n\n    let response;\n    try {\n        response = await fetch(input, init);\n    } catch (err) {\n        console.warn(\n            \"managedFetch ['%o', '%o'] -> error while performing a request: %o\",\n            input,\n            init,\n            response,\n            err\n        );\n        return Promise.reject({ message: 'Error while performing a request', cause: err });\n    }\n\n    if (!response.ok) {\n        throw await makeError(response);\n    }\n\n    return response;\n};\n\n/**\n * Generates a query parameter string from an object of key/value pairs\n * @param params the key/value object accepted\n *\n * @return a query parameter string e.g. \"foo=123&bar=abc\"\n */\nexport const queryParams = (params: any, allowEmpty?: boolean): string => {\n    const esc = encodeURIComponent;\n    const result: string[] = [];\n\n    Object.keys(params)\n        .filter((k) => {\n            const v = params[k];\n\n            if (v === undefined || v === null || (!allowEmpty && v === '')) {\n                return false;\n            }\n\n            if (Array.isArray(v) && v.length === 0) {\n                return false;\n            }\n\n            return true;\n        })\n        .forEach((k) => {\n            const v = params[k];\n            if (Array.isArray(v)) {\n                v.forEach((vv) => {\n                    result.push(esc(k) + '=' + esc(vv));\n                });\n            } else {\n                result.push(esc(k) + '=' + esc(v));\n            }\n        });\n\n    return result.join('&');\n};\n\nexport type QueryParams = { [key: string]: string };\n\n/**\n * Parse url parameters from a url string\n * @param url a http url string\n *\n * @return an object e.g. { \"param\": \"value\", ... }\n */\nexport const parseQueryParams = (url: string): QueryParams => {\n    // Remove all non-valid characters\n    const validString = url.replace(/[^a-z0-9\\s-]/, '');\n\n    // Split url and take the right hand side\n    // ! assumption there is only one question mark\n    let queryParams: string;\n    if (validString.includes('?')) {\n        queryParams = validString.split('?')[1];\n    } else {\n        // No Query Params exist in the string\n        return {};\n    }\n\n    // Split find params by splitting on & characters\n    let kvs: string[] = [];\n    if (queryParams.includes('&')) {\n        kvs = queryParams.split('&');\n    } else {\n        // There is only one param to return, so return it\n        const [k, v] = queryParams.split('=');\n        return { [k]: v };\n    }\n\n    // initialize an object for iteration\n    let result = {};\n\n    // inject params as key value pairs\n    for (const kv of kvs) {\n        const [k, v] = kv.split('=');\n\n        // handle multi-value params\n        const prev = result[k];\n        if (prev) {\n            if (prev instanceof Array) {\n                prev.push(v);\n            } else {\n                result[k] = [prev, v];\n            }\n        } else {\n            result[k] = v;\n        }\n    }\n\n    return result;\n};\n\nexport const deepMerge = (a: any, b: any): any => {\n    const result = { ...a };\n\n    Object.keys(b).forEach((k) => {\n        const av = a[k];\n        const bv = b[k];\n\n        let o = bv;\n        if (typeof av === 'object' && typeof bv === 'object') {\n            o = deepMerge(av, bv);\n        }\n\n        result[k] = o;\n    });\n\n    return result;\n};\n\nexport type QueryMultiParams = { [key: string]: any };\n\nexport const parseNestedQueryParams = (params: QueryParams, keys: string[]): QueryMultiParams => {\n    let result: QueryMultiParams = { ...params };\n\n    Object.keys(params)\n        .filter((p) => keys.some((k) => p.startsWith(k) && p.includes('.')))\n        .forEach((p) => {\n            let as = p.split('.').reverse();\n\n            let obj: QueryMultiParams = { [as[0]]: params[p] };\n            for (let i = 1; i < as.length; i++) {\n                obj = { [as[i]]: obj };\n            }\n\n            delete result[p];\n            result = deepMerge(result, obj);\n        });\n\n    return result;\n};\n\nexport const fetchJson = async <T>(uri: string, init?: RequestInit): Promise<T> => {\n    const response = await managedFetch(uri, init);\n    return response.json();\n};\n\nconst wait = (delayMs: number) =>\n    new Promise<void>((resolve) => {\n        setTimeout(resolve, delayMs);\n    });\n\nexport const retryRequest = async <T>(\n    request: () => Promise<T>,\n    shouldRetry: (error: RequestError) => boolean,\n    attempts = 5,\n    delayMs = 250\n): Promise<T> => {\n    let lastError: RequestError;\n\n    for (let attempt = 1; attempt <= attempts; attempt++) {\n        try {\n            return await request();\n        } catch (e) {\n            lastError = e as RequestError;\n\n            if (attempt === attempts || !shouldRetry(lastError)) {\n                throw e;\n            }\n\n            await wait(delayMs);\n        }\n    }\n\n    throw lastError;\n};\n\nexport interface EntityOwner {\n    id: ConcordId;\n    username: string;\n    userDomain?: string;\n    displayName?: string;\n}\n\nexport enum OperationResult {\n    CREATED = 'CREATED',\n    UPDATED = 'UPDATED',\n    DELETED = 'DELETED',\n    ALREADY_EXISTS = 'ALREADY_EXISTS',\n    NOT_FOUND = 'NOT_FOUND'\n}\n\nexport interface GenericOperationResult {\n    ok: boolean;\n    result: OperationResult;\n}\n\nexport enum EntityType {\n    PROJECT = 'PROJECT',\n    SECRET = 'SECRET'\n}\n"
  },
  {
    "path": "console2/src/api/noderoster/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { fetchJson, ConcordId, queryParams, ConcordKey } from '../common';\nimport {\n    ProcessCheckpointEntry,\n    ProcessHistoryEntry,\n    ProcessKind,\n    ProcessMeta,\n    ProcessStatus,\n    TriggeredByEntry\n} from '../process';\n\nexport interface HostEntry {\n    id: ConcordId;\n    name: string;\n    createdAt: string;\n    artifactUrl?: string;\n}\n\nexport interface PaginatedHostEntry {\n    items: HostEntry[];\n    next: boolean;\n}\n\nexport interface HostFilter {\n    host?: string;\n    processInstanceId?: ConcordId;\n    artifact?: string;\n}\n\nexport interface HostArtifact {\n    url: string;\n    processInstanceId: ConcordId;\n}\n\nexport interface PaginatedHostArtifacts {\n    items: HostArtifact[];\n    next: boolean;\n}\n\nexport interface HostProcessEntry {\n    instanceId: ConcordId;\n    parentInstanceId?: ConcordId;\n    status: ProcessStatus;\n    kind: ProcessKind;\n    orgName?: ConcordKey;\n    projectName?: ConcordKey;\n    repoName?: ConcordKey;\n    repoUrl?: string;\n    repoPath?: string;\n    commitId?: string;\n    initiator: string;\n    createdAt: string;\n    startAt?: string;\n    lastUpdatedAt: string;\n    handlers?: string[];\n    meta?: ProcessMeta;\n    tags?: string[];\n    checkpoints?: ProcessCheckpointEntry[];\n    statusHistory?: ProcessHistoryEntry[];\n    disabled: boolean;\n    triggeredBy?: TriggeredByEntry;\n    timeout?: number;\n}\n\nexport interface PaginatedHostProcessEntry {\n    items: HostProcessEntry[];\n    next: boolean;\n}\n\nexport const getHost = async (id: ConcordId): Promise<HostEntry> => {\n    return fetchJson<HostEntry>(`/api/v1/noderoster/hosts/${id}`);\n};\n\nexport type HostsInclude = 'artifacts';\n\nexport const listHosts = async (\n    page: number,\n    limit: number,\n    includes: HostsInclude[],\n    filter?: HostFilter\n): Promise<PaginatedHostEntry> => {\n    const offsetParam = page > 0 && limit > 0 ? page * limit : page;\n    const limitParam = limit > 0 ? limit + 1 : limit;\n\n    const data: HostEntry[] = await fetchJson(\n        `/api/v1/noderoster/hosts?${queryParams({\n            offset: offsetParam,\n            limit: limitParam,\n            host: filter?.host,\n            processInstanceId: filter?.processInstanceId,\n            artifact: filter?.artifact,\n            include: includes\n        })}`\n    );\n\n    const hasMoreElements: boolean = limit > 0 && data.length > limit;\n\n    if (limit > 0 && hasMoreElements) {\n        data.pop();\n    }\n\n    return {\n        items: data,\n        next: hasMoreElements\n    };\n};\n\nexport const getLatestHostFacts = async (id: ConcordId): Promise<Object> => {\n    return fetchJson<Object>(`/api/v1/noderoster/facts/last?hostId=${id}`);\n};\n\nexport const listHostArtifacts = async (\n    hostId: ConcordId,\n    page: number,\n    limit: number,\n    filter?: string\n): Promise<PaginatedHostArtifacts> => {\n    const offsetParam = page > 0 && limit > 0 ? page * limit : page;\n    const limitParam = limit > 0 ? limit + 1 : limit;\n\n    const data: HostArtifact[] = await fetchJson(\n        `/api/v1/noderoster/artifacts?${queryParams({\n            hostId,\n            offset: offsetParam,\n            limit: limitParam,\n            filter\n        })}`\n    );\n\n    const hasMoreElements: boolean = limit > 0 && data.length > limit;\n\n    if (limit > 0 && hasMoreElements) {\n        data.pop();\n    }\n\n    return {\n        items: data,\n        next: hasMoreElements\n    };\n};\n\nexport const listHostProcesses = async (\n    hostId: ConcordId,\n    page: number,\n    limit: number\n): Promise<PaginatedHostProcessEntry> => {\n    const offsetParam = page > 0 && limit > 0 ? page * limit : page;\n    const limitParam = limit > 0 ? limit + 1 : limit;\n\n    const data: HostProcessEntry[] = await fetchJson(\n        `/api/v1/noderoster/processes?${queryParams({\n            hostId,\n            offset: offsetParam,\n            limit: limitParam\n        })}`\n    );\n\n    const hasMoreElements: boolean = limit > 0 && data.length > limit;\n\n    if (limit > 0 && hasMoreElements) {\n        data.pop();\n    }\n\n    return {\n        items: data,\n        next: hasMoreElements\n    };\n};\n"
  },
  {
    "path": "console2/src/api/org/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport {\n    fetchJson,\n    ConcordKey,\n    ConcordId,\n    OperationResult,\n    EntityOwner,\n    EntityType,\n    queryParams\n} from '../common';\n\nexport enum OrganizationVisibility {\n    PUBLIC = 'PUBLIC',\n    PRIVATE = 'PRIVATE'\n}\n\nexport type SearchType = 'substring' | 'equals';\n\nexport type SearchValueType = 'string' | 'boolean';\n\nexport interface SearchOption {\n    value: string;\n    text: string;\n}\n\nexport enum RenderType {\n    /**\n     * Render a link to the process using the value as the link's caption.\n     */\n    PROCESS_LINK = 'process-link',\n\n    /**\n     * Render a link to the process' project using the value as the link's caption.\n     */\n    PROJECT_LINK = 'project-link',\n\n    /**\n     * Render a link to the process' repository using the value as the link's caption.\n     */\n    REPO_LINK = 'repo-link',\n\n    /**\n     * Render the current process' status.\n     */\n    PROCESS_STATUS = 'process-status',\n\n    /**\n     * Render as a timestamp.\n     */\n    TIMESTAMP = 'timestamp',\n\n    /**\n     * Render as an array of strings.\n     */\n    STRING_ARRAY = 'string-array',\n\n    /**\n     * Render as a duration (current timestamp - value)\n     */\n    DURATION = 'duration',\n\n    /**\n     * Render as link\n     */\n    LINK = 'link'\n}\n\nexport interface ColumnDefinition {\n    builtin?: string;\n    caption: string;\n    source: string;\n    textAlign?: 'center' | 'left' | 'right';\n    collapsing?: boolean;\n    singleLine?: boolean;\n    render?: RenderType;\n    searchValueType?: SearchValueType;\n    searchType?: SearchType;\n    searchOptions?: SearchOption[];\n}\n\nexport interface OrganizationEntryMetaUI {\n    processList?: ColumnDefinition[];\n}\n\nexport interface CheckResult {\n    result: boolean;\n}\n\nexport interface OrganizationEntryMeta {\n    ui?: OrganizationEntryMetaUI;\n}\n\nexport interface OrganizationEntry {\n    id: string;\n    name: string;\n    owner?: EntityOwner;\n    visibility: OrganizationVisibility;\n    meta?: OrganizationEntryMeta;\n}\n\nexport enum ResourceAccessLevel {\n    OWNER = 'OWNER',\n    WRITER = 'WRITER',\n    READER = 'READER'\n}\n\nexport interface ResourceAccessEntry {\n    teamId: ConcordId;\n    teamName: ConcordKey;\n    level: ResourceAccessLevel;\n}\n\nexport interface OrganizationOperationResult {\n    ok: boolean;\n    id: ConcordId;\n    result: OperationResult;\n}\n\nexport interface PaginatedOrganizationEntries {\n    items: OrganizationEntry[];\n    next: boolean;\n}\n\nexport const list = async (\n    onlyCurrent: boolean,\n    page: number,\n    limit: number,\n    filter?: string\n): Promise<PaginatedOrganizationEntries> => {\n    const offsetParam = page > 0 && limit > 0 ? page * limit : page;\n    const limitParam = limit > 0 ? limit + 1 : limit;\n\n    const data: OrganizationEntry[] = await fetchJson(\n        `/api/v1/org?${queryParams({\n            onlyCurrent: onlyCurrent,\n            offset: offsetParam,\n            limit: limitParam,\n            filter\n        })}`\n    );\n\n    const hasMoreElements: boolean = limit > 0 && data.length > limit;\n\n    if (limit > 0 && hasMoreElements) {\n        data.pop();\n    }\n\n    return {\n        items: data,\n        next: hasMoreElements\n    };\n};\n\nexport const get = (orgName: ConcordKey): Promise<OrganizationEntry> =>\n    fetchJson<OrganizationEntry>(`/api/v1/org/${orgName}`);\n\nexport const checkResult = (entity: EntityType, orgName: ConcordKey): Promise<CheckResult> =>\n    fetchJson<CheckResult>(\n        `api/v1/${entity}/canCreate?${queryParams({\n            orgName\n        })}`\n    );\n\nexport const changeOwner = (\n    orgId: ConcordId,\n    ownerId: ConcordId\n): Promise<OrganizationOperationResult> => {\n    const opts = {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify({\n            id: orgId,\n            owner: { id: ownerId }\n        })\n    };\n\n    return fetchJson(`/api/v1/org`, opts);\n};\n"
  },
  {
    "path": "console2/src/api/org/jsonstore/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport {\n    ConcordId,\n    ConcordKey,\n    fetchJson,\n    GenericOperationResult,\n    EntityOwner,\n    queryParams,\n    OperationResult\n} from '../../common';\nimport { ResourceAccessEntry } from '../index';\n\nexport enum StorageVisibility {\n    PUBLIC = 'PUBLIC',\n    PRIVATE = 'PRIVATE'\n}\n\nexport interface StorageEntry {\n    id: ConcordId;\n    name: ConcordKey;\n\n    orgId: ConcordId;\n    orgName: ConcordKey;\n\n    projectId: ConcordId;\n    projectName: ConcordKey;\n\n    visibility: StorageVisibility;\n\n    owner?: EntityOwner;\n}\n\nexport interface StorageCapacity {\n    size?: number;\n    maxSize?: number;\n}\n\nexport interface StorageOperationResult {\n    ok: boolean;\n    id: ConcordId;\n    result: OperationResult;\n}\n\nexport interface PaginatedStorageEntries {\n    items: StorageEntry[];\n    next: boolean;\n}\n\nexport interface Pagination {\n    limit: number;\n    offset: number;\n}\n\nexport type StorageDataEntry = string;\n\nexport interface PaginatedStorageDataEntries {\n    items: StorageDataEntry[];\n    next: boolean;\n}\n\nexport interface StorageQueryEntry {\n    name: ConcordKey;\n    text: string;\n}\n\nexport interface PaginatedStorageQueryEntries {\n    items: StorageQueryEntry[];\n    next: boolean;\n}\n\nexport const get = (orgName: ConcordKey, storeName: ConcordKey): Promise<StorageEntry> => {\n    return fetchJson(`/api/v1/org/${orgName}/jsonstore/${storeName}`);\n};\n\nexport const getCapacity = (\n    orgName: ConcordKey,\n    storeName: ConcordKey\n): Promise<StorageCapacity> => {\n    return fetchJson(`/api/v1/org/${orgName}/jsonstore/${storeName}/capacity`);\n};\n\nexport const list = async (\n    orgName: ConcordKey,\n    offset: number,\n    limit: number,\n    filter?: string\n): Promise<PaginatedStorageEntries> => {\n    const offsetParam = offset > 0 && limit > 0 ? offset * limit : offset;\n    const limitParam = limit > 0 ? limit + 1 : limit;\n\n    const data: StorageEntry[] = await fetchJson(\n        `/api/v1/org/${orgName}/jsonstore?${queryParams({\n            offset: offsetParam,\n            limit: limitParam,\n            filter\n        })}`\n    );\n\n    const hasMoreElements: boolean = !!limit && data.length > limit;\n\n    if (limit > 0 && hasMoreElements) {\n        data.pop();\n    }\n\n    return {\n        items: data,\n        next: hasMoreElements\n    };\n};\n\nexport const createOrUpdate = (\n    orgName: ConcordKey,\n    storeName: ConcordKey,\n    visibility?: StorageVisibility,\n    newOrgName?: ConcordKey\n): Promise<StorageOperationResult> => {\n    const opts = {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify({\n            name: storeName,\n            orgName: newOrgName,\n            visibility\n        })\n    };\n    return fetchJson(`/api/v1/org/${orgName}/jsonstore`, opts);\n};\n\nexport const deleteStorage = (\n    orgName: ConcordKey,\n    storeName: ConcordKey\n): Promise<GenericOperationResult> => {\n    const opts = {\n        method: 'DELETE'\n    };\n\n    return fetchJson(`/api/v1/org/${orgName}/jsonstore/${storeName}`, opts);\n};\n\nexport const updateVisibility = (\n    orgName: ConcordKey,\n    storeName: ConcordKey,\n    visibility: StorageVisibility\n): Promise<GenericOperationResult> => {\n    const opts = {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify({\n            name: storeName,\n            visibility\n        })\n    };\n\n    return fetchJson(`/api/v1/org/${orgName}/jsonstore`, opts);\n};\n\nexport const changeOwner = (\n    orgName: ConcordKey,\n    storeName: ConcordKey,\n    ownerId: ConcordId\n): Promise<GenericOperationResult> => {\n    const opts = {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify({\n            name: storeName,\n            owner: { id: ownerId }\n        })\n    };\n\n    return fetchJson(`/api/v1/org/${orgName}/jsonstore`, opts);\n};\n\nexport const getAccess = (\n    orgName: ConcordKey,\n    storeName: ConcordKey\n): Promise<ResourceAccessEntry[]> => {\n    return fetchJson(`/api/v1/org/${orgName}/jsonstore/${storeName}/access`);\n};\n\nexport const updateAccess = (\n    orgName: ConcordKey,\n    storeName: ConcordKey,\n    entries: ResourceAccessEntry[]\n): Promise<GenericOperationResult> => {\n    const opts = {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify(entries)\n    };\n\n    return fetchJson(`/api/v1/org/${orgName}/jsonstore/${storeName}/access/bulk`, opts);\n};\n\nexport const listStorageData = async (\n    orgName: ConcordKey,\n    storeName: ConcordKey,\n    offset: number,\n    limit: number,\n    filter?: string\n): Promise<PaginatedStorageDataEntries> => {\n    const offsetParam = offset > 0 && limit > 0 ? offset * limit : offset;\n    const limitParam = limit > 0 ? limit + 1 : limit;\n\n    const data: StorageDataEntry[] = await fetchJson(\n        `/api/v1/org/${orgName}/jsonstore/${storeName}/item?${queryParams({\n            offset: offsetParam,\n            limit: limitParam,\n            filter\n        })}`\n    );\n\n    const hasMoreElements: boolean = !!limit && data.length > limit;\n\n    if (limit > 0 && hasMoreElements) {\n        data.pop();\n    }\n\n    return {\n        items: data,\n        next: hasMoreElements\n    };\n};\n\nexport const deleteStorageData = (\n    orgName: ConcordKey,\n    storeName: ConcordKey,\n    storagePath: string\n): Promise<GenericOperationResult> => {\n    const opts = {\n        method: 'DELETE'\n    };\n    const escapedPath = escapeStoragePath(storagePath);\n    return fetchJson(`/api/v1/org/${orgName}/jsonstore/${storeName}/item/${escapedPath}`, opts);\n};\n\nexport const escapeStoragePath = (s: string): string => s.replace(/\\//g, '%2F');\n\nexport const listStorageQuery = async (\n    orgName: ConcordKey,\n    storeName: ConcordKey,\n    offset: number,\n    limit: number,\n    filter?: string\n): Promise<PaginatedStorageQueryEntries> => {\n    const offsetParam = offset > 0 && limit > 0 ? offset * limit : offset;\n    const limitParam = limit > 0 ? limit + 1 : limit;\n\n    const data: StorageQueryEntry[] = await fetchJson(\n        `/api/v1/org/${orgName}/jsonstore/${storeName}/query?${queryParams({\n            offset: offsetParam,\n            limit: limitParam,\n            filter\n        })}`\n    );\n\n    const hasMoreElements: boolean = !!limit && data.length > limit;\n\n    if (limit > 0 && hasMoreElements) {\n        data.pop();\n    }\n\n    return {\n        items: data,\n        next: hasMoreElements\n    };\n};\n\nexport const deleteStorageQuery = (\n    orgName: ConcordKey,\n    storeName: ConcordKey,\n    storageQueryName: string\n): Promise<GenericOperationResult> => {\n    const opts = {\n        method: 'DELETE'\n    };\n    return fetchJson(\n        `/api/v1/org/${orgName}/jsonstore/${storeName}/query/${storageQueryName}`,\n        opts\n    );\n};\n\nexport const createOrUpdateStorageQuery = (\n    orgName: ConcordKey,\n    storeName: ConcordKey,\n    queryName: ConcordKey,\n    query: string\n): Promise<GenericOperationResult> => {\n    const opts = {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify({\n            name: queryName,\n            text: query\n        })\n    };\n    return fetchJson(`/api/v1/org/${orgName}/jsonstore/${storeName}/query`, opts);\n};\n\nexport const getStorageQuery = (\n    orgName: ConcordKey,\n    storeName: ConcordKey,\n    queryName: ConcordKey\n): Promise<StorageQueryEntry> => {\n    return fetchJson(`/api/v1/org/${orgName}/jsonstore/${storeName}/query/${queryName}`);\n};\n\nexport const executeQuery = (\n    orgName: ConcordKey,\n    storeName: ConcordKey,\n    query: string\n): Promise<Object> => {\n    const opts = {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'text/plain'\n        },\n        body: query\n    };\n    return fetchJson(`/api/v1/org/${orgName}/jsonstore/${storeName}/execQuery?maxLimit=50`, opts);\n};\n"
  },
  {
    "path": "console2/src/api/org/project/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { ColumnDefinition, ResourceAccessEntry } from '../';\nimport {\n    ConcordId,\n    ConcordKey,\n    EntityOwner,\n    fetchJson,\n    GenericOperationResult,\n    OperationResult,\n    queryParams,\n    RequestError,\n    retryRequest\n} from '../../common';\n\nexport enum ProjectVisibility {\n    PUBLIC = 'PUBLIC',\n    PRIVATE = 'PRIVATE'\n}\n\nexport interface ProjectEntryMetaUI {\n    processList?: ColumnDefinition[];\n    childrenProcessList?: ColumnDefinition[];\n}\n\nexport interface ProjectEntryMeta {\n    ui?: ProjectEntryMetaUI;\n}\n\nexport enum RawPayloadMode {\n    DISABLED = 'DISABLED',\n    OWNERS = 'OWNERS',\n    TEAM_MEMBERS = 'TEAM_MEMBERS',\n    ORG_MEMBERS = 'ORG_MEMBERS',\n    EVERYONE = 'EVERYONE'\n}\n\nexport enum OutVariablesMode {\n    DISABLED = 'DISABLED',\n    OWNERS = 'OWNERS',\n    TEAM_MEMBERS = 'TEAM_MEMBERS',\n    ORG_MEMBERS = 'ORG_MEMBERS',\n    EVERYONE = 'EVERYONE'\n}\n\nexport enum ProcessExecMode {\n    DISABLED = 'DISABLED',\n    READERS = 'READERS',\n    WRITERS = 'WRITERS'\n}\n\nconst shouldRetryProjectRequest = (error: RequestError) => {\n    if (!error || (error.status !== 400 && error.status !== 404)) {\n        return false;\n    }\n\n    return /(?:Organization|Project) not found:/i.test(error.details || '');\n};\n\nexport interface ProjectEntry {\n    id: ConcordId;\n    name: ConcordKey;\n\n    owner: EntityOwner;\n\n    orgId: ConcordId;\n    orgName: ConcordKey;\n\n    description?: string;\n    visibility: ProjectVisibility;\n\n    rawPayloadMode: RawPayloadMode;\n\n    meta?: ProjectEntryMeta;\n\n    outVariablesMode: OutVariablesMode;\n\n    processExecMode: ProcessExecMode;\n}\n\nexport interface NewProjectEntry {\n    name: ConcordKey;\n    description?: string;\n    visibility: ProjectVisibility;\n}\n\nexport interface UpdateProjectEntry {\n    id?: ConcordId;\n    name?: ConcordKey;\n    orgId?: ConcordId;\n    orgName?: ConcordKey;\n    description?: string;\n    visibility?: ProjectVisibility;\n    rawPayloadMode?: RawPayloadMode;\n    outVariablesMode?: OutVariablesMode;\n    processExecMode?: ProcessExecMode;\n}\n\nexport interface PaginatedProjectEntries {\n    items: ProjectEntry[];\n    next: boolean;\n}\n\nexport interface KVCapacity {\n    size: number;\n    maxSize?: number;\n}\n\nexport const get = (orgName: ConcordKey, projectName: ConcordKey): Promise<ProjectEntry> => {\n    return fetchJson<ProjectEntry>(`/api/v2/org/${orgName}/project/${projectName}`);\n};\n\nexport const getCapacity = (\n    orgName: ConcordKey,\n    projectName: ConcordKey\n): Promise<KVCapacity> => {\n    return fetchJson(`/api/v1/org/${orgName}/project/${projectName}/kv/capacity`);\n};\n\nexport const list = async (\n    orgName: ConcordKey,\n    offset: number,\n    limit: number,\n    filter?: string\n): Promise<PaginatedProjectEntries> => {\n    const offsetParam = offset > 0 && limit > 0 ? offset * limit : offset;\n    const limitParam = limit > 0 ? limit + 1 : limit;\n\n    const data: ProjectEntry[] = await fetchJson(\n        `/api/v1/org/${orgName}/project?${queryParams({\n            offset: offsetParam,\n            limit: limitParam,\n            filter\n        })}`\n    );\n\n    const hasMoreElements: boolean = !!limit && data.length > limit;\n\n    if (limit > 0 && hasMoreElements) {\n        data.pop();\n    }\n\n    return {\n        items: data,\n        next: hasMoreElements\n    };\n};\n\nexport interface ProjectOperationResult {\n    ok: boolean;\n    id: ConcordId;\n    result: OperationResult;\n}\n\n// TODO response type\nexport const createOrUpdate = (\n    orgName: ConcordKey,\n    entry: NewProjectEntry | UpdateProjectEntry\n): Promise<ProjectOperationResult> => {\n    const opts = {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify(entry)\n    };\n    return fetchJson(`/api/v1/org/${orgName}/project`, opts);\n};\n\n// TODO should we just use createOrUpdate instead?\nexport const rename = (\n    orgName: ConcordKey,\n    projectId: ConcordId,\n    projectName: ConcordKey\n): Promise<ProjectOperationResult> => {\n    const opts = {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify({\n            id: projectId,\n            name: projectName\n        })\n    };\n\n    return fetchJson(`/api/v1/org/${orgName}/project`, opts);\n};\n\n// TODO should we just use createOrUpdate instead?\nexport const changeOwner = (\n    orgName: ConcordKey,\n    projectName: ConcordKey,\n    ownerId: ConcordId\n): Promise<ProjectOperationResult> => {\n    const opts = {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify({\n            name: projectName,\n            owner: { id: ownerId }\n        })\n    };\n\n    return fetchJson(`/api/v1/org/${orgName}/project`, opts);\n};\n\nexport const deleteProject = (\n    orgName: ConcordKey,\n    projectName: ConcordKey\n): Promise<GenericOperationResult> =>\n    fetchJson(`/api/v1/org/${orgName}/project/${projectName}`, { method: 'DELETE' });\n\nexport const encrypt = (\n    orgName: ConcordKey,\n    projectName: ConcordKey,\n    value: string\n): Promise<GenericOperationResult> => {\n    const opts = {\n        method: 'POST',\n        body: value\n    };\n\n    return fetchJson(`/api/v1/org/${orgName}/project/${projectName}/encrypt`, opts);\n};\n\nexport const getProjectAccess = (\n    orgName: ConcordKey,\n    projectName: ConcordKey\n): Promise<Array<ResourceAccessEntry>> => {\n    return retryRequest(\n        () => fetchJson(`/api/v1/org/${orgName}/project/${projectName}/access`),\n        shouldRetryProjectRequest\n    );\n};\n\nexport const updateProjectAccess = (\n    orgName: ConcordKey,\n    projectName: ConcordKey,\n    entries: ResourceAccessEntry[]\n): Promise<GenericOperationResult> => {\n    const opts = {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify(entries)\n    };\n\n    return retryRequest(\n        () => fetchJson(`/api/v1/org/${orgName}/project/${projectName}/access/bulk`, opts),\n        shouldRetryProjectRequest\n    );\n};\n\nexport const getProjectConfiguration = (\n    orgName: ConcordKey,\n    projectName: ConcordKey\n): Promise<Object> => {\n    return fetchJson(`/api/v1/org/${orgName}/project/${projectName}/cfg`);\n};\n\nexport const updateProjectConfiguration = (\n    orgName: ConcordKey,\n    projectName: ConcordKey,\n    config: Object\n): Promise<GenericOperationResult> => {\n    const opts = {\n        method: 'PUT',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify(config)\n    };\n\n    return fetchJson(`/api/v1/org/${orgName}/project/${projectName}/cfg`, opts);\n};\n"
  },
  {
    "path": "console2/src/api/org/project/repository/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport {\n    ConcordId,\n    ConcordKey,\n    fetchJson,\n    queryParams,\n    GenericOperationResult,\n    OperationResult\n} from '../../../common';\n\nexport interface RepositoryMeta {\n    profiles?: string[];\n    entryPoints?: string[];\n}\n\nexport interface RepositoryEntry {\n    id: ConcordId;\n    name: ConcordKey;\n    url: string;\n    branch?: string;\n    commitId?: string;\n    path?: string;\n    secretStoreType?: string;\n    secretId: string;\n    secretName: string;\n    meta?: RepositoryMeta;\n    disabled: boolean;\n    triggersDisabled: boolean;\n}\n\nexport interface PaginatedRepositoryEntries {\n    items: RepositoryEntry[];\n    next: boolean;\n}\n\nexport interface EditRepositoryEntry {\n    id?: ConcordId;\n    name: ConcordKey;\n    url: string;\n    branch?: string;\n    commitId?: string;\n    path?: string;\n    secretId: string;\n    disabled: boolean;\n    triggers?: TriggerEntry;\n    triggersDisabled: boolean;\n}\n\nexport interface TriggerCfg {\n    entryPoint: string;\n    name?: string;\n}\n\nexport interface TriggerConditions {\n    spec?: string;\n    version?: string;\n}\n\nexport interface TriggerEntry {\n    id: ConcordId;\n    repositoryId: ConcordId;\n    eventSource: ConcordKey;\n    arguments?: object;\n    conditions?: TriggerConditions;\n    activeProfiles?: string[];\n    cfg: TriggerCfg;\n}\n\nexport interface RepositoryValidationResponse {\n    ok: boolean;\n    result: OperationResult;\n    errors?: string[];\n    warnings?: string[];\n}\n\nexport const get = async (\n    orgName: ConcordKey,\n    projectName: ConcordKey,\n    repoName: ConcordKey\n): Promise<RepositoryEntry> => {\n    return fetchJson<RepositoryEntry>(\n        `/api/v1/org/${orgName}/project/${projectName}/repository/${repoName}`\n    );\n};\n\nexport const list = async (\n    orgName: ConcordKey,\n    projectName: ConcordKey,\n    offset: number,\n    limit: number,\n    filter?: string\n): Promise<PaginatedRepositoryEntries> => {\n    const offsetParam = offset > 0 && limit > 0 ? offset * limit : offset;\n    const limitParam = limit > 0 ? limit + 1 : limit;\n\n    const data: RepositoryEntry[] = await fetchJson(\n        `/api/v1/org/${orgName}/project/${projectName}/repository?${queryParams({\n            offset: offsetParam,\n            limit: limitParam,\n            filter,\n        })}`\n    );\n\n    const hasMoreElements: boolean = !!limit && data.length > limit;\n\n    if (limit > 0 && hasMoreElements) {\n        data.pop();\n    }\n\n    return {\n        items: data,\n        next: hasMoreElements,\n    };\n};\n\nexport const createOrUpdate = (\n    orgName: ConcordKey,\n    projectName: ConcordKey,\n    entry: EditRepositoryEntry\n): Promise<GenericOperationResult> => {\n    const opts = {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify(entry)\n    };\n\n    return fetchJson(`/api/v1/org/${orgName}/project/${projectName}/repository`, opts);\n};\n\nexport const deleteRepository = (\n    orgName: ConcordKey,\n    projectName: ConcordKey,\n    repoName: ConcordKey\n): Promise<GenericOperationResult> => {\n    const opts = {\n        method: 'DELETE'\n    };\n\n    return fetchJson(`/api/v1/org/${orgName}/project/${projectName}/repository/${repoName}`, opts);\n};\n\nexport const refreshRepository = (\n    orgName: ConcordKey,\n    projectName: ConcordKey,\n    repoName: ConcordKey,\n    sync: boolean\n): Promise<GenericOperationResult> => {\n    const opts = {\n        method: 'POST'\n    };\n\n    return fetchJson(\n        `/api/v1/org/${orgName}/project/${projectName}/repository/${repoName}/refresh?sync=${sync}`,\n        opts\n    );\n};\n\nexport const validateRepository = (\n    orgName: ConcordKey,\n    projectName: ConcordKey,\n    repoName: ConcordKey\n): Promise<RepositoryValidationResponse> => {\n    const opts = {\n        method: 'POST'\n    };\n\n    return fetchJson(\n        `/api/v1/org/${orgName}/project/${projectName}/repository/${repoName}/validate`,\n        opts\n    );\n};\n\nexport const listTriggers = (\n    orgName: ConcordKey,\n    projectName: ConcordKey,\n    repoName: ConcordKey\n): Promise<TriggerEntry[]> =>\n    fetchJson(`/api/v1/org/${orgName}/project/${projectName}/repo/${repoName}/trigger`);\n\nexport interface TriggerFilter {\n    type?: ConcordKey;\n    orgName?: ConcordKey;\n    projectName?: ConcordKey;\n    repoName?: ConcordKey;\n}\n\nexport const listTriggersV2 = (filter: TriggerFilter): Promise<TriggerEntry[]> =>\n    fetchJson(`/api/v2/trigger?${queryParams({ ...filter })}`);\n"
  },
  {
    "path": "console2/src/api/org/secret/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport {\n    ConcordId,\n    ConcordKey,\n    fetchJson,\n    GenericOperationResult,\n    EntityOwner,\n    queryParams\n} from '../../common';\nimport { ResourceAccessEntry } from '../';\nimport { ProjectEntry } from '../project';\n\nexport enum SecretVisibility {\n    PUBLIC = 'PUBLIC',\n    PRIVATE = 'PRIVATE'\n}\n\nexport enum SecretType {\n    KEY_PAIR = 'KEY_PAIR',\n    USERNAME_PASSWORD = 'USERNAME_PASSWORD',\n    DATA = 'DATA'\n}\n\nexport enum SecretTypeExt {\n    NEW_KEY_PAIR,\n    EXISTING_KEY_PAIR,\n    USERNAME_PASSWORD,\n    VALUE_STRING,\n    VALUE_FILE\n}\n\nexport enum SecretEncryptedByType {\n    SERVER_KEY = 'SERVER_KEY',\n    PASSWORD = 'PASSWORD'\n}\n\nexport enum SecretStoreType {\n    CONCORD = 'CONCORD',\n    KEYWHIZ = 'KEYWHIZ'\n}\n\nexport interface SecretEntry {\n    id: ConcordId;\n    name: ConcordKey;\n\n    createdAt: string;\n    lastUpdatedAt?: string;\n\n    orgId: ConcordId;\n    orgName: ConcordKey;\n\n    projects: ProjectEntry[];\n\n    visibility: SecretVisibility;\n    type: SecretType;\n    encryptedBy: SecretEncryptedByType;\n\n    storeType: SecretStoreType;\n\n    owner?: EntityOwner;\n}\n\nexport interface CreateSecretResponse extends GenericOperationResult {\n    id: ConcordId;\n    password?: string;\n    publicKey?: string;\n}\n\nexport interface NewSecretEntry {\n    name: string;\n    visibility: SecretVisibility;\n    projects?: ProjectEntry[];\n    type: SecretTypeExt;\n    publicFile?: File;\n    privateFile?: File;\n    username?: string;\n    password?: string;\n    valueString?: string;\n    valueFile?: File;\n    generatePassword?: boolean;\n    storePassword?: string;\n    storeType?: SecretStoreType;\n}\n\nexport interface PaginatedSecretEntries {\n    items: SecretEntry[];\n    next?: boolean;\n}\n\nexport interface Pagination {\n    limit: number;\n    offset: number;\n}\n\nexport interface PublicKeyResponse {\n    publicKey: string;\n}\n\nexport const get = (orgName: ConcordKey, secretName: ConcordKey): Promise<SecretEntry> => {\n    return fetchJson(`/api/v2/org/${orgName}/secret/${secretName}`);\n};\n\nexport const list = async (\n    orgName: ConcordKey,\n    offset: number,\n    limit: number,\n    filter?: string\n): Promise<PaginatedSecretEntries> => {\n    const offsetParam = offset > 0 && limit > 0 ? offset * limit : offset;\n    const limitParam = limit > 0 ? limit + 1 : limit;\n\n    const data: SecretEntry[] = await fetchJson(\n        `/api/v2/org/${orgName}/secret?${queryParams({\n            offset: offsetParam,\n            limit: limitParam,\n            filter\n        })}`\n    );\n\n    const hasMoreElements: boolean = !!limit && data.length > limit;\n\n    if (limit > 0 && hasMoreElements) {\n        data.pop();\n    }\n\n    return {\n        items: data,\n        next: hasMoreElements\n    };\n};\n\nexport const deleteSecret = (\n    orgName: ConcordKey,\n    secretName: ConcordKey\n): Promise<GenericOperationResult> => {\n    const opts = {\n        method: 'DELETE'\n    };\n\n    return fetchJson(`/api/v1/org/${orgName}/secret/${secretName}`, opts);\n};\n\nexport const renameSecret = (\n    orgName: ConcordKey,\n    secretName: ConcordKey,\n    newSecretName: ConcordKey\n): Promise<GenericOperationResult> => {\n    const data = new FormData();\n    data.append('name', newSecretName);\n    const opts = {\n        method: 'POST',\n        body: data\n    };\n\n    return fetchJson(`/api/v2/org/${orgName}/secret/${secretName}`, opts);\n};\n\nexport const updateSecretVisibility = (\n    orgName: ConcordKey,\n    secretName: ConcordKey,\n    visibility: SecretVisibility\n): Promise<GenericOperationResult> => {\n    const data = new FormData();\n    data.append('visibility', visibility);\n    const opts = {\n        method: 'POST',\n        body: data\n    };\n\n    return fetchJson(`/api/v2/org/${orgName}/secret/${secretName}`, opts);\n};\n\nexport const updateSecretProject = (\n    orgName: ConcordKey,\n    secretName: ConcordKey,\n    projectIds: String[]\n): Promise<GenericOperationResult> => {\n    const data = new FormData();\n    if (projectIds.length > 0) {\n        data.append('projectIds', projectIds.join(','));\n    } else {\n        data.append('removeProjectLink', 'true');\n    }\n    const opts = {\n        method: 'POST',\n        body: data\n    };\n\n    return fetchJson(`/api/v2/org/${orgName}/secret/${secretName}`, opts);\n};\n\n// TODO response type\nexport const create = (\n    orgName: ConcordKey,\n    entry: NewSecretEntry\n): Promise<CreateSecretResponse> => {\n    const data = new FormData();\n\n    data.append('name', entry.name);\n    data.append('visibility', entry.visibility);\n\n    switch (entry.type) {\n        case SecretTypeExt.NEW_KEY_PAIR: {\n            data.append('type', SecretType.KEY_PAIR);\n            break;\n        }\n        case SecretTypeExt.EXISTING_KEY_PAIR: {\n            data.append('type', SecretType.KEY_PAIR);\n            data.append('public', entry.publicFile!);\n            data.append('private', entry.privateFile!);\n            break;\n        }\n        case SecretTypeExt.USERNAME_PASSWORD: {\n            data.append('type', SecretType.USERNAME_PASSWORD);\n            data.append('username', entry.username!);\n            data.append('password', entry.password!);\n            break;\n        }\n        case SecretTypeExt.VALUE_STRING: {\n            data.append('type', SecretType.DATA);\n            data.append('data', entry.valueString!);\n            break;\n        }\n        case SecretTypeExt.VALUE_FILE: {\n            data.append('type', SecretType.DATA);\n            data.append('data', entry.valueFile!);\n            break;\n        }\n        default: {\n            return Promise.reject(`Unsupported secret type: ${entry.type}`);\n        }\n    }\n\n    if (entry.generatePassword) {\n        data.append('generatePassword', 'true');\n    } else if (entry.storePassword) {\n        data.append('storePassword', entry.storePassword);\n    }\n\n    if (entry.storeType) {\n        data.append('storeType', entry.storeType);\n    }\n\n    if (entry.projects) {\n        data.append('projectIds', entry.projects.map((project) => project.id).join(','));\n    }\n\n    const opts = {\n        method: 'POST',\n        body: data\n    };\n    return fetchJson(`/api/v1/org/${orgName}/secret`, opts);\n};\n\nexport const changeOrganization = (\n    orgName: ConcordKey,\n    secretName: ConcordKey,\n    newOrgName: ConcordKey\n): Promise<GenericOperationResult> => {\n    const opts = {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify({\n            orgName: newOrgName\n        })\n    };\n\n    return fetchJson(`/api/v2/org/${orgName}/secret/${secretName}`, opts);\n};\n\nexport const getPublicKey = (orgName: string, secretName: string): Promise<PublicKeyResponse> =>\n    fetchJson(`/api/v1/org/${orgName}/secret/${secretName}/public`);\n\nexport const getSecretAccess = (\n    orgName: ConcordKey,\n    secretName: ConcordKey\n): Promise<ResourceAccessEntry[]> => fetchJson(`/api/v1/org/${orgName}/secret/${secretName}/access`);\n\nexport const updateSecretAccess = (\n    orgName: ConcordKey,\n    secretName: ConcordKey,\n    entries: ResourceAccessEntry[]\n): Promise<GenericOperationResult> => {\n    const opts = {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify(entries)\n    };\n\n    return fetchJson(`/api/v1/org/${orgName}/secret/${secretName}/access/bulk`, opts);\n};\n\nexport const changeOwner = (\n    orgName: ConcordKey,\n    secretName: ConcordKey,\n    ownerId: ConcordId\n): Promise<GenericOperationResult> => {\n    const opts = {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify({\n            owner: { id: ownerId }\n        })\n    };\n\n    return fetchJson(`/api/v1/org/${orgName}/secret/${secretName}`, opts);\n};\n\nexport const typeToText = (t: SecretType) => {\n    switch (t) {\n        case SecretType.KEY_PAIR: {\n            return 'Key pair';\n        }\n        case SecretType.USERNAME_PASSWORD: {\n            return 'Username/password';\n        }\n        case SecretType.DATA: {\n            return 'Single value';\n        }\n        default: {\n            throw new Error(`Unexpected value: ${t}`);\n        }\n    }\n};\n"
  },
  {
    "path": "console2/src/api/org/team/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport {\n    ConcordId,\n    ConcordKey,\n    fetchJson,\n    GenericOperationResult,\n    OperationResult,\n    queryParams,\n    RequestError,\n    retryRequest\n} from '../../common';\nimport { UserType } from '../../user';\n\nexport interface TeamEntry {\n    id: ConcordId;\n    orgId: ConcordId;\n    orgName: ConcordKey;\n    name: ConcordKey;\n    description?: string;\n}\n\nexport interface NewTeamEntry {\n    name: string;\n    description?: string;\n}\n\nexport interface CreateTeamResponse {\n    ok: boolean;\n    result: OperationResult;\n    id: ConcordId;\n}\n\nconst shouldRetryTeamRequest = (error: RequestError) => {\n    if (!error || (error.status !== 400 && error.status !== 404)) {\n        return false;\n    }\n\n    return /(?:Organization|Team) not found:/i.test(error.details || '');\n};\n\nexport const get = (orgName: ConcordKey, teamName: ConcordKey): Promise<TeamEntry> =>\n    retryRequest(\n        () => fetchJson(`/api/v1/org/${orgName}/team/${teamName}`),\n        shouldRetryTeamRequest\n    );\n\nexport const list = (orgName: ConcordKey): Promise<TeamEntry[]> =>\n    retryRequest(() => fetchJson(`/api/v1/org/${orgName}/team`), shouldRetryTeamRequest);\n\nexport const createOrUpdate = (\n    orgName: ConcordKey,\n    entry: NewTeamEntry\n): Promise<CreateTeamResponse> => {\n    const opts = {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify(entry)\n    };\n\n    return retryRequest(() => fetchJson(`/api/v1/org/${orgName}/team`, opts), shouldRetryTeamRequest);\n};\n\n// TODO should we just use createOrUpdate instead?\nexport const rename = (\n    orgName: ConcordKey,\n    teamId: ConcordId,\n    teamName: ConcordKey\n): Promise<CreateTeamResponse> => {\n    const opts = {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify({\n            id: teamId,\n            name: teamName\n        })\n    };\n\n    return retryRequest(() => fetchJson(`/api/v1/org/${orgName}/team`, opts), shouldRetryTeamRequest);\n};\n\nexport const deleteTeam = (\n    orgName: ConcordKey,\n    teamName: ConcordKey\n): Promise<GenericOperationResult> =>\n    retryRequest(\n        () => fetchJson(`/api/v1/org/${orgName}/team/${teamName}`, { method: 'DELETE' }),\n        shouldRetryTeamRequest\n    );\n\nexport enum TeamRole {\n    OWNER = 'OWNER',\n    MAINTAINER = 'MAINTAINER',\n    MEMBER = 'MEMBER'\n}\n\nexport interface TeamUserEntry {\n    userId: ConcordId;\n    username: string;\n    userDomain?: string;\n    displayName?: string;\n    userType?: UserType;\n    role: TeamRole;\n    memberType: MemberType;\n    ldapGroupSource?: string;\n}\n\nexport interface TeamLdapGroupEntry {\n    group: string;\n    role: TeamRole;\n}\n\nexport enum MemberType {\n    SINGLE = 'SINGLE',\n    LDAP_GROUP = 'LDAP_GROUP'\n}\n\nexport interface NewTeamUserEntry {\n    userId?: ConcordId;\n    username?: string;\n    userDomain?: string;\n    displayName?: string;\n    userType?: UserType;\n    role: TeamRole;\n}\n\nexport interface NewTeamLdapGroupEntry {\n    group: string;\n    role: TeamRole;\n}\n\nexport const listUsers = (orgName: ConcordKey, teamName: ConcordKey): Promise<TeamUserEntry[]> =>\n    retryRequest(\n        () => fetchJson(`/api/v1/org/${orgName}/team/${teamName}/users`),\n        shouldRetryTeamRequest\n    );\n\nexport const listLdapGroups = (\n    orgName: ConcordKey,\n    teamName: ConcordKey\n): Promise<TeamLdapGroupEntry[]> =>\n    retryRequest(\n        () => fetchJson(`/api/v1/org/${orgName}/team/${teamName}/ldapGroups`),\n        shouldRetryTeamRequest\n    );\n\nexport const addUsers = (\n    orgName: ConcordKey,\n    teamName: ConcordKey,\n    replace: boolean,\n    users: NewTeamUserEntry[]\n): Promise<{}> => {\n    const opts = {\n        method: 'PUT',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify(users)\n    };\n\n    return retryRequest(\n        () =>\n            fetchJson(\n                `/api/v1/org/${orgName}/team/${teamName}/users?${queryParams({ replace })}`,\n                opts\n            ),\n        shouldRetryTeamRequest\n    );\n};\n\nexport const addLdapGroups = (\n    orgName: ConcordKey,\n    teamName: ConcordKey,\n    replace: boolean,\n    groups: NewTeamLdapGroupEntry[]\n): Promise<{}> => {\n    const opts = {\n        method: 'PUT',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify(groups)\n    };\n\n    return retryRequest(\n        () =>\n            fetchJson(\n                `/api/v1/org/${orgName}/team/${teamName}/ldapGroups?${queryParams({ replace })}`,\n                opts\n            ),\n        shouldRetryTeamRequest\n    );\n};\n"
  },
  {
    "path": "console2/src/api/process/ansible/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { ConcordId, fetchJson, queryParams } from '../../common';\nimport { ProcessEventEntry } from '../event';\n\nexport interface SearchFilter {\n    limit?: number;\n    offset?: number;\n    host?: string;\n    hostGroup?: string;\n    status?: AnsibleStatus;\n    statuses?: AnsibleStatus[];\n    playbookId?: ConcordId;\n    sortField?: SortField;\n    sortBy?: SortOrder;\n}\n\nexport enum AnsibleStatus {\n    CHANGED = 'CHANGED',\n    FAILED = 'FAILED',\n    OK = 'OK',\n    RUNNING = 'RUNNING',\n    SKIPPED = 'SKIPPED',\n    UNREACHABLE = 'UNREACHABLE'\n}\n\nexport enum SortOrder {\n    ASC = 'ASC',\n    DESC = 'DESC'\n}\n\nexport enum SortField {\n    HOST = 'HOST',\n    DURATION = 'DURATION',\n    STATUS = 'STATUS',\n    HOST_GROUP = 'HOST_GROUP'\n}\n\nexport const getStatusColor = (status: AnsibleStatus) => {\n    switch (status) {\n        case AnsibleStatus.OK:\n            return '#5DB571';\n        case AnsibleStatus.CHANGED:\n            return '#00A4D3';\n        case AnsibleStatus.FAILED:\n            return '#EC6357';\n        case AnsibleStatus.UNREACHABLE:\n            return '#BDB9B9';\n        case AnsibleStatus.SKIPPED:\n            return '#F6BC32';\n        case AnsibleStatus.RUNNING:\n            return '#BDBABD';\n        default:\n            return '#3F3F3D';\n    }\n};\n\nexport interface AnsibleHost {\n    host: string;\n    hostGroup: string;\n    status: AnsibleStatus;\n    duration: number;\n}\n\nexport interface AnsibleHostListResponse {\n    hostGroups: string[];\n    items: AnsibleHost[];\n}\n\nexport interface PaginatedAnsibleHostEntries {\n    items: AnsibleHost[];\n    hostGroups: string[];\n    next?: number;\n    prev?: number;\n}\n\nexport interface AnsibleEvent {\n    host: string;\n    hostGroup: string;\n    playbook: string;\n    status?: AnsibleStatus;\n    task: string;\n    action?: string;\n    result?: object;\n    ignore_errors?: boolean;\n    duration?: number;\n    phase: 'pre' | 'post';\n    correlationId: string;\n}\n\nexport interface PlaybookInfo {\n    id: ConcordId;\n    name: string;\n    startedAt: string;\n    hostsCount: number;\n    failedHostsCount: number;\n    playsCount: number;\n    failedTasksCount: number;\n    progress: number;\n    status: string;\n    retryNum?: number;\n}\n\nexport interface PlayInfo {\n    playId: ConcordId;\n    playName: string;\n    playOrder: number;\n    hostCount: number;\n    taskCount: number;\n    taskStats: TaskStats;\n    finishedTaskCount: number;\n    flowEventCorrelationId: ConcordId;\n}\n\nexport interface TaskInfo {\n    taskName: string;\n    type: string;\n    taskOrder: number;\n    okCount: number;\n    failedCount: number;\n    unreachableCount: number;\n    skippedCount: number;\n    runningCount: number;\n}\n\nexport interface TaskStats {\n    ok: number;\n    failed: number;\n    unreachable: number;\n    skipped: number;\n    running: number;\n}\n\nexport const listAnsibleHosts = (\n    instanceId: ConcordId,\n    filters?: SearchFilter\n): Promise<PaginatedAnsibleHostEntries> => {\n    const limit = filters && filters.limit ? filters.limit : 50;\n    if (filters && filters.limit) {\n        filters.limit = parseInt(filters.limit.toString(), 10) + 1;\n    }\n\n    const qp = filters ? '?' + queryParams(filters) : '';\n\n    const data: Promise<AnsibleHostListResponse> = fetchJson(\n        `/api/v1/process/${instanceId}/ansible/hosts${qp}`\n    );\n    return data.then((resp: AnsibleHostListResponse) => {\n        const hosts = resp.items;\n\n        const hasMoreElements = limit && hosts.length > limit;\n        const offset: number = filters && filters.offset ? filters.offset : 0;\n\n        if (hasMoreElements) {\n            hosts.pop();\n        }\n\n        const nextOffset = offset + parseInt(limit.toString(), 10);\n        const prevOffset = offset - limit;\n        const onFirstPage = offset === 0;\n\n        const nextPage = !!hasMoreElements ? nextOffset : undefined;\n        const prevPage = !onFirstPage ? prevOffset : undefined;\n\n        return {\n            items: hosts,\n            hostGroups: resp.hostGroups,\n            next: nextPage,\n            prev: prevPage\n        };\n    });\n};\n\nexport const listAnsibleEvents = (\n    instanceId: ConcordId,\n    host?: string,\n    hostGroup?: string,\n    status?: string,\n    playbookId?: ConcordId\n): Promise<ProcessEventEntry<AnsibleEvent>[]> =>\n    fetchJson(\n        `/api/v1/process/${instanceId}/ansible/events?${queryParams({\n            host,\n            hostGroup,\n            status,\n            playbookId\n        })}`\n    );\n\nexport const listAnsiblePlaybooks = (instanceId: ConcordId): Promise<PlaybookInfo[]> =>\n    fetchJson(`/api/v1/process/${instanceId}/ansible/playbooks`);\n\nexport const listAnsiblePlays = (\n    instanceId: ConcordId,\n    playbookId: ConcordId\n): Promise<PlayInfo[]> => fetchJson(`/api/v1/process/${instanceId}/ansible/${playbookId}/plays`);\n\nexport const listAnsibleTaskStats = (\n    instanceId: ConcordId,\n    playId: ConcordId\n): Promise<TaskInfo[]> =>\n    fetchJson(\n        `/api/v1/process/${instanceId}/ansible/tasks?${queryParams({\n            playId\n        })}`\n    );\n\nexport const listAnsibleTasks = (\n    instanceId: ConcordId,\n    playbookId: ConcordId,\n    host?: string,\n    hostGroup?: string,\n    status?: string\n): Promise<ProcessEventEntry<AnsibleEvent>[]> =>\n    fetchJson(\n        `/api/v1/process/${instanceId}/ansible/events?${queryParams({\n            host,\n            hostGroup,\n            status,\n            playbookId\n        })}`\n    );\n"
  },
  {
    "path": "console2/src/api/process/attachment/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { ConcordId, fetchJson } from '../../common';\n\nexport const list = (instanceId: ConcordId): Promise<string[]> =>\n    fetchJson(`/api/v1/process/${instanceId}/attachment`);\n"
  },
  {
    "path": "console2/src/api/process/checkpoint/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { ConcordId, fetchJson } from '../../common';\nimport { ProcessEntry } from '../index';\n\nexport const restoreProcess = (\n    instanceId: ConcordId,\n    checkpointId: ConcordId\n): Promise<ProcessEntry> =>\n    fetchJson(`/api/v1/process/${instanceId}/checkpoint/restore`, {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify({ id: checkpointId })\n    });\n"
  },
  {
    "path": "console2/src/api/process/event/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { ConcordId, fetchJson, queryParams } from '../../common';\nimport { AnsibleEvent } from '../ansible';\n\nexport enum ProcessEventType {\n    ELEMENT = 'ELEMENT',\n    ANSIBLE = 'ANSIBLE'\n}\n\nexport interface VariableMapping {\n    source?: string;\n    sourceExpression?: string;\n    sourceValue: any;\n    target: string;\n    resolved: any;\n}\n\n// TODO find which properties are always defined\nexport interface ProcessElementEvent {\n    processDefinitionId: string;\n    threadId?: number;\n    fileName?: string;\n    elementId: string;\n    line: number;\n    column: number;\n    description?: string;\n    phase?: 'pre' | 'post';\n    in?: VariableMapping[] | {};\n    out?: VariableMapping[] | {};\n    correlationId?: string;\n    duration?: number;\n    error?: string;\n}\n\nexport type ProcessEventData = ProcessElementEvent | AnsibleEvent | {};\n\nexport interface ProcessEventFilter {\n    instanceId: ConcordId;\n    type?: string;\n    fromId?: number;\n    eventCorrelationId?: string;\n    eventPhase?: 'PRE' | 'POST';\n    includeAll?: boolean;\n    limit?: number;\n}\n\nexport interface ProcessEventEntry<T extends ProcessEventData> {\n    id: ConcordId;\n    seqId: number;\n    eventType: ProcessEventType;\n    eventDate: string;\n    data: T;\n}\n\nexport const listEvents = <T extends ProcessEventData>(\n    filter: ProcessEventFilter\n): Promise<ProcessEventEntry<T>[]> =>\n    fetchJson(`/api/v1/process/${filter.instanceId}/event?${queryParams({ ...filter })}`);\n"
  },
  {
    "path": "console2/src/api/process/form/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { ConcordId, fetchJson } from '../../common';\n\nexport interface FormRunAs {\n    username?: string;\n    ldap?: { group: string[] | string } | [{ group: string }];\n    keep?: boolean;\n}\n\nexport interface FormListEntry {\n    name: string;\n    custom: boolean;\n    yield: boolean;\n    runAs?: FormRunAs;\n}\n\nexport enum Cardinality {\n    ONE_OR_NONE = 'ONE_OR_NONE',\n    ONE_AND_ONLY_ONE = 'ONE_AND_ONLY_ONE',\n    AT_LEAST_ONE = 'AT_LEAST_ONE',\n    ANY = 'ANY'\n}\n\nexport enum FormFieldType {\n    STRING = 'string',\n    INT = 'int',\n    DECIMAL = 'decimal',\n    BOOLEAN = 'boolean',\n    FILE = 'file',\n    DATE = 'date',\n    DATE_TIME = 'dateTime'\n}\n\nexport interface FormField {\n    name: string;\n    label: string;\n    type: FormFieldType;\n    cardinality?: Cardinality;\n    value?: any;\n    allowedValue?: any;\n    options?: {\n        inputType?: string;\n        popupPosition?:\n            | 'top left'\n            | 'top right'\n            | 'bottom left'\n            | 'bottom right'\n            | 'right center'\n            | 'left center'\n            | 'top center'\n            | 'bottom center';\n    };\n}\n\nexport interface FormInstanceEntry {\n    processInstanceId: ConcordId;\n    name: string;\n    fields: FormField[];\n    custom: boolean;\n    yield: boolean;\n}\n\nexport interface FormSubmitErrors {\n    [name: string]: string;\n}\n\nexport interface FormDataType {\n    [name: string]: any;\n}\n\nexport interface FormSubmitResponse {\n    ok: boolean;\n    processInstanceId: ConcordId;\n    errors?: FormSubmitErrors;\n}\n\nexport const list = (processInstanceId: ConcordId): Promise<FormListEntry[]> =>\n    fetchJson(`/api/v1/process/${processInstanceId}/form`);\n\nexport const get = (processInstanceId: ConcordId, formName: string): Promise<FormInstanceEntry> =>\n    fetchJson(`/api/v1/process/${processInstanceId}/form/${formName}`);\n\nexport const submit = (\n    processInstanceId: ConcordId,\n    formName: string,\n    values: {}\n): Promise<FormSubmitResponse> => {\n    const body = new FormData();\n\n    Object.keys(values).forEach((name) => {\n        let k = name;\n        let v = values[k];\n\n        // special case: a JSON object encoded as a string in a multipart/form-data field\n        if (v instanceof Array) {\n            v = JSON.stringify(v);\n            k = k + '/jsonField';\n        }\n\n        if (v !== undefined) {\n            body.append(k, v);\n        }\n    });\n\n    const opts = {\n        method: 'POST',\n        body\n    };\n\n    return fetchJson(`/api/v1/process/${processInstanceId}/form/${formName}/multipart`, opts);\n};\n"
  },
  {
    "path": "console2/src/api/process/history/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { ConcordId, fetchJson } from '../../common';\nimport { ProcessHistoryEntry } from '../';\n\nexport const get = (instanceId: ConcordId): Promise<ProcessHistoryEntry[]> =>\n    fetchJson(`/api/v1/process/${instanceId}/history`);\n"
  },
  {
    "path": "console2/src/api/process/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { SemanticCOLORS, SemanticICONS } from 'semantic-ui-react';\nimport { ConcordId, ConcordKey, fetchJson, managedFetch, queryParams } from '../common';\nimport { ColumnDefinition } from '../org';\nimport 'url-search-params-polyfill';\n\nexport enum ProcessStatus {\n    NEW = 'NEW',\n    PREPARING = 'PREPARING',\n    ENQUEUED = 'ENQUEUED',\n    WAITING = 'WAITING',\n    STARTING = 'STARTING',\n    RUNNING = 'RUNNING',\n    SUSPENDED = 'SUSPENDED',\n    RESUMING = 'RESUMING',\n    FINISHED = 'FINISHED',\n    FAILED = 'FAILED',\n    CANCELLED = 'CANCELLED',\n    TIMED_OUT = 'TIMED_OUT'\n}\n\nexport const getStatusSemanticColor = (status: ProcessStatus): SemanticCOLORS => {\n    switch (status) {\n        case ProcessStatus.NEW:\n        case ProcessStatus.PREPARING:\n        case ProcessStatus.RUNNING:\n        case ProcessStatus.STARTING:\n        case ProcessStatus.SUSPENDED:\n            return 'blue';\n        case ProcessStatus.FINISHED:\n            return 'green';\n        case ProcessStatus.CANCELLED:\n        case ProcessStatus.FAILED:\n        case ProcessStatus.TIMED_OUT:\n            return 'red';\n        case ProcessStatus.ENQUEUED:\n        case ProcessStatus.RESUMING:\n        case ProcessStatus.WAITING:\n        default:\n            return 'grey';\n    }\n};\n\nexport const getStatusSemanticIcon = (status: ProcessStatus): SemanticICONS => {\n    switch (status) {\n        case ProcessStatus.NEW:\n        case ProcessStatus.PREPARING:\n        case ProcessStatus.RUNNING:\n        case ProcessStatus.STARTING:\n            return 'spinner';\n        case ProcessStatus.SUSPENDED:\n            return 'hourglass half';\n        case ProcessStatus.FINISHED:\n            return 'circle';\n        case ProcessStatus.CANCELLED:\n        case ProcessStatus.FAILED:\n        case ProcessStatus.TIMED_OUT:\n            return 'exclamation circle';\n        case ProcessStatus.ENQUEUED:\n        case ProcessStatus.RESUMING:\n        case ProcessStatus.WAITING:\n        default:\n            return 'circle notch';\n    }\n};\n\nexport const isFinal = (s?: ProcessStatus) =>\n    s === ProcessStatus.FINISHED ||\n    s === ProcessStatus.FAILED ||\n    s === ProcessStatus.CANCELLED ||\n    s === ProcessStatus.TIMED_OUT;\n\nexport const hasState = (s: ProcessStatus) => s !== ProcessStatus.PREPARING;\n\nexport const canBeCancelled = (s: ProcessStatus) =>\n    s === ProcessStatus.ENQUEUED ||\n    s === ProcessStatus.RUNNING ||\n    s === ProcessStatus.WAITING ||\n    s === ProcessStatus.SUSPENDED;\n\nexport const canBeRestarted = (s: ProcessStatus) =>\n    s === ProcessStatus.CANCELLED ||\n    s === ProcessStatus.FAILED ||\n    s === ProcessStatus.FINISHED ||\n    s === ProcessStatus.TIMED_OUT;\n\nexport interface ProcessCheckpointEntry {\n    id: string;\n    name: string;\n    createdAt: string;\n}\n\nexport interface CheckpointRestoreHistoryEntry {\n    id: number;\n    checkpointId: string;\n    processStatus: ProcessStatus;\n    changeDate: string;\n}\n\nexport interface ProcessHistoryEntry {\n    id: ConcordId;\n    status: ProcessStatus;\n    changeDate: string;\n}\n\nexport enum WaitType {\n    NONE = 'NONE',\n    PROCESS_COMPLETION = 'PROCESS_COMPLETION',\n    PROCESS_LOCK = 'PROCESS_LOCK',\n    PROCESS_SLEEP = 'PROCESS_SLEEP'\n}\n\nexport interface AbstractWaitCondition {\n    type: WaitType;\n    reason: string;\n}\n\nexport interface ProcessWaitCondition extends AbstractWaitCondition {\n    processes?: ConcordId[];\n}\n\nexport interface ProcessLockCondition extends AbstractWaitCondition {\n    instanceId: ConcordId;\n    name: string;\n    scope: string;\n}\n\nexport interface ProcessSleepCondition extends AbstractWaitCondition {\n    until: string;\n}\n\nexport type WaitCondition = ProcessWaitCondition | ProcessLockCondition | ProcessSleepCondition;\n\nexport interface ProcessWaitEntry {\n    isWaiting: boolean;\n    waits?: WaitCondition[];\n}\n\nexport interface ProcessMeta {\n    out?: {\n        lastError?: {};\n        [x: string]: any;\n    };\n    [x: string]: any;\n}\n\nexport enum ProcessKind {\n    DEFAULT = 'DEFAULT',\n    FAILURE_HANDLER = 'FAILURE_HANDLER',\n    CANCEL_HANDLER = 'CANCEL_HANDLER',\n    TIMEOUT_HANDLER = 'TIMEOUT_HANDLER'\n}\n\nexport type ProcessRuntime = 'concord-v1' | 'concord-v2' | string;\n\nexport interface TriggeredByEntry {\n    externalEventId?: string;\n    trigger: TriggerEntry;\n}\n\nexport interface TriggerEntry {\n    eventSource: string;\n}\n\nexport interface ProcessEntry {\n    instanceId: ConcordId;\n    parentInstanceId?: ConcordId;\n    status: ProcessStatus;\n    kind: ProcessKind;\n    orgName?: ConcordKey;\n    projectName?: ConcordKey;\n    repoName?: ConcordKey;\n    repoUrl?: string;\n    repoPath?: string;\n    commitId?: string;\n    initiator: string;\n    createdAt: string;\n    startAt?: string;\n    lastUpdatedAt: string;\n    lastRunAt?: string;\n    totalRuntimeMs?: number;\n    handlers?: string[];\n    meta?: ProcessMeta;\n    tags?: string[];\n    checkpoints?: ProcessCheckpointEntry[];\n    checkpointRestoreHistory?: CheckpointRestoreHistoryEntry[];\n    statusHistory?: ProcessHistoryEntry[];\n    disabled: boolean;\n    triggeredBy?: TriggeredByEntry;\n    timeout?: number;\n    suspendTimeout?: number;\n    runtime?: ProcessRuntime;\n    requirements?: {};\n}\n\nexport interface StartProcessResponse {\n    ok: boolean;\n    instanceId: string;\n}\n\nexport interface RestoreProcessResponse {\n    ok: boolean;\n}\n\nexport const start = (\n    orgName: ConcordKey,\n    projectName: ConcordKey,\n    repoName: ConcordKey,\n    entryPoint?: string,\n    profiles?: string[],\n    args?: object\n): Promise<StartProcessResponse> => {\n    const data = new FormData();\n\n    data.append('org', orgName);\n    data.append('project', projectName);\n    data.append('repo', repoName);\n    if (entryPoint) {\n        data.append('entryPoint', entryPoint);\n    }\n    if (profiles) {\n        data.append('activeProfiles', profiles.join(','));\n    }\n\n    if (args) {\n        const argsBlob = [JSON.stringify({ arguments: args })];\n        data.append('request', new Blob(argsBlob, { type: 'application/octet-stream' }));\n    }\n\n    const opts = {\n        method: 'POST',\n        body: data\n    };\n\n    return fetchJson('/api/v1/process', opts);\n};\n\nexport type ProcessDataInclude = 'checkpoints' | 'history' | 'childrenIds' | 'checkpointsHistory';\n\nexport const get = (\n    instanceId: ConcordId,\n    includes: ProcessDataInclude[]\n): Promise<ProcessEntry> => {\n    const params = new URLSearchParams();\n    includes.forEach((i) => params.append('include', i));\n    return fetchJson(`/api/v2/process/${instanceId}?${params.toString()}`);\n};\n\nexport const getRoot = (\n    instanceId: ConcordId\n): Promise<ProcessEntry> => {\n    return fetchJson(`/api/v1/process/${instanceId}/root`);\n};\n\nexport const disable = (instanceId: ConcordId, disabled: boolean): Promise<{}> =>\n    managedFetch(`/api/v1/process/${instanceId}/disable/${disabled}`, { method: 'POST' });\n\nexport const kill = (instanceId: ConcordId): Promise<{}> =>\n    managedFetch(`/api/v1/process/${instanceId}`, { method: 'DELETE' });\n\nexport const restart = (instanceId: ConcordId): Promise<{}> =>\n    managedFetch(`/api/v1/process/${instanceId}/restart`, { method: 'POST' });\n\nexport const killBulk = (instanceIds: ConcordId[]): Promise<{}> => {\n    const opts = {\n        method: 'DELETE',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify(instanceIds)\n    };\n    return managedFetch('/api/v1/process/bulk', opts);\n};\n\nexport interface ColumnFilter {\n    column: ColumnDefinition;\n    filter: string;\n}\n\n// TODO remove, use ProcessListQuery everywhere\nexport interface ProcessFilters {\n    [source: string]: string;\n}\n\nexport interface PaginatedProcessEntries {\n    items: ProcessEntry[];\n    next?: number;\n    prev?: number;\n}\n\nexport interface DateParam {\n    value: string | null;\n    compareType: 'ge' | 'len';\n}\n\nexport interface ProcessListQuery {\n    [meta: string]: any;\n\n    orgId?: ConcordId;\n    orgName?: ConcordKey;\n    projectId?: ConcordId;\n    projectName?: ConcordKey;\n    afterCreatedAt?: string;\n    beforeCreatedAt?: string;\n    startAt?: DateParam;\n    tags?: string[];\n    status?: ProcessStatus;\n    initiator?: string;\n    parentInstanceId?: ConcordId;\n    include?: ProcessDataInclude[];\n    limit?: number;\n    offset?: number;\n}\n\nconst filterToQueryParams = (params: object): string => {\n    let keyValues = {};\n\n    Object.keys(params).forEach((k) => {\n        const v = params[k];\n\n        const dp = v as DateParam;\n        if (dp !== undefined && v.compareType !== undefined) {\n            let value = '';\n            if (v.value !== null && v.value !== undefined) {\n                value = v.value;\n            }\n\n            keyValues[k + '.' + v.compareType] = value;\n        } else {\n            keyValues[k] = v;\n        }\n    });\n\n    return queryParams(keyValues, true);\n};\n\nexport const list = async (q: ProcessListQuery): Promise<PaginatedProcessEntries> => {\n    let { limit = 50 } = q;\n\n    // TODO fix the CheckpointView data instead\n    limit = parseInt(limit.toString());\n\n    const requestLimit = limit + 1;\n\n    const filters = { ...q, limit: requestLimit };\n    const qp = filters ? filterToQueryParams(filters) : '';\n\n    const data: ProcessEntry[] = await fetchJson(`/api/v2/process?${qp}`);\n\n    const hasMoreElements: boolean = !!limit && data.length > limit;\n    const offset: number = q.offset ? q.offset : 0;\n\n    if (hasMoreElements) {\n        data.pop();\n    }\n\n    const nextOffset = offset + limit;\n    const prevOffset = offset - limit;\n    const onFirstPage = offset === 0;\n\n    const nextPage = hasMoreElements ? nextOffset : undefined;\n    const prevPage = !onFirstPage ? prevOffset : undefined;\n\n    return {\n        items: data,\n        next: nextPage,\n        prev: prevPage\n    };\n};\n"
  },
  {
    "path": "console2/src/api/process/log/fetchLogAsBlobURL.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { managedFetch } from '../../common';\n\nexport default (processId: string) =>\n    managedFetch(`/api/v1/process/${processId}/log`)\n        .then((response) => response.body)\n        .then((rs) => {\n            if (rs) {\n                const reader = rs.getReader();\n\n                // ! Typescript currently does not support types for\n                // ! readable streams, thus @ts-ignore\n                // @ts-ignore\n                return new ReadableStream({\n                    async start(controller: any) {\n                        while (true) {\n                            const { done, value } = await reader.read();\n\n                            // When no more data needs to be consumed, break the reading\n                            if (done) {\n                                break;\n                            }\n\n                            // Enqueue the next data chunk into our target stream\n                            controller.enqueue(value);\n                        }\n                        // Close the stream\n                        controller.close();\n                        reader.releaseLock();\n                    }\n                });\n            } else {\n                throw new Error(`Process: ${processId} body missing`);\n            }\n        })\n        // Create a new response out of the stream\n        .then((rs) => new Response(rs))\n        // Create an object URL for the response\n        .then((response) => response.blob())\n        .then((blob) => URL.createObjectURL(blob));\n"
  },
  {
    "path": "console2/src/api/process/log/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { ConcordId, fetchJson, managedFetch, queryParams } from '../../common';\n\nexport interface LogRange {\n    unit?: string;\n    length?: number;\n    low?: number;\n    high?: number;\n}\n\nexport interface LogChunk {\n    data: string;\n    range: LogRange;\n}\n\nconst str = (s?: {}): string => (s === undefined ? '' : String(s));\n\nconst formatRangeHeader = (range: LogRange) => ({\n    Range: `bytes=${str(range.low)}-${str(range.high)}`\n});\n\nconst parseRange = (s: string): LogRange => {\n    const regex = /^bytes (\\d*)-(\\d*)\\/(\\d*)$/;\n    const m = regex.exec(s);\n    if (!m) {\n        throw Object({ error: true, message: `Invalid Content-Range header: ${s}` });\n    }\n\n    return {\n        unit: 'bytes',\n        length: parseInt(m[3], 10),\n        low: parseInt(m[1], 10),\n        high: parseInt(m[2], 10)\n    };\n};\n\nconst toChunk = (data: string, range: LogRange): LogChunk => {\n    // we assume that the data is aligned by \\n\n    // this will work only with our current implementation of the API\n    return {\n        data,\n        range\n    };\n};\n\nexport const getLog = async (instanceId: ConcordId, range: LogRange): Promise<LogChunk> => {\n    const opts = {\n        headers: formatRangeHeader(range)\n    };\n\n    const resp = await managedFetch(`/api/v1/process/${instanceId}/log`, opts);\n\n    const headers = resp.headers.get('Content-Range');\n    if (!headers) {\n        return Promise.reject({ error: true, message: `Range header is missing: ${instanceId}` });\n    }\n\n    const data = await resp.text();\n    return toChunk(data, parseRange(headers));\n};\n\nexport enum SegmentStatus {\n    OK = 'OK',\n    FAILED = 'FAILED',\n    RUNNING = 'RUNNING',\n    SUSPENDED = 'SUSPENDED'\n}\n\nexport interface LogSegmentEntry {\n    id: number;\n    correlationId?: string;\n    name: string;\n    createdAt: string;\n    status?: SegmentStatus;\n    statusUpdatedAt?: string;\n    warnings?: number;\n    errors?: number;\n}\n\nexport interface PaginatedLogSegmentEntry {\n    items: LogSegmentEntry[];\n    next: boolean;\n}\n\nexport const listLogSegments = async (\n    instanceId: ConcordId,\n    offset: number,\n    limit: number\n): Promise<PaginatedLogSegmentEntry> => {\n    const limitParam = limit > 0 ? limit + 1 : limit;\n\n    const data: LogSegmentEntry[] = await fetchJson(\n        `/api/v2/process/${instanceId}/log/segment?${queryParams({\n            offset,\n            limit: limitParam\n        })}`\n    );\n\n    const hasMoreElements: boolean = limit > 0 && data.length > limit;\n\n    if (limit > 0 && hasMoreElements) {\n        data.pop();\n    }\n\n    return {\n        items: data,\n        next: hasMoreElements\n    };\n};\n\nexport const getSegmentLog = async (\n    instanceId: ConcordId,\n    segmentId: number,\n    range: LogRange\n): Promise<LogChunk> => {\n    const opts = {\n        headers: formatRangeHeader(range)\n    };\n\n    const resp = await managedFetch(\n        `/api/v2/process/${instanceId}/log/segment/${segmentId}/data`,\n        opts\n    );\n\n    const headers = resp.headers.get('Content-Range');\n    if (!headers) {\n        return Promise.reject({ error: true, message: `Range header is missing: ${instanceId}` });\n    }\n\n    const data = await resp.text();\n    return toChunk(data, parseRange(headers));\n};\n"
  },
  {
    "path": "console2/src/api/process/wait/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { ProcessWaitEntry } from '../';\nimport { ConcordId, managedFetch } from '../../common';\n\nexport const get = async (instanceId: ConcordId): Promise<ProcessWaitEntry | undefined> => {\n    const response = await managedFetch(`/api/v1/process/${instanceId}/waits`);\n    if (response.status === 204) {\n        return undefined;\n    }\n    return response.json();\n};\n"
  },
  {
    "path": "console2/src/api/profile/api_token/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { fetchJson, GenericOperationResult, ConcordId, ConcordKey } from '../../common';\n\nexport interface TokenEntry {\n    id: ConcordId;\n    name: ConcordKey;\n    expiredAt: string;\n}\n\nexport interface NewTokenEntry {\n    name: ConcordKey;\n}\n\nexport interface CreateApiKeyResult {\n    ok: boolean;\n    id: string;\n    key: string;\n    expiredAt: string;\n}\n\nexport const list = (): Promise<TokenEntry[]> => fetchJson(`/api/v1/apikey`);\n\nexport const create = (entry: NewTokenEntry): Promise<CreateApiKeyResult> => {\n    const obj: RequestInit = {\n        method: 'POST',\n        credentials: 'same-origin',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify(entry)\n    };\n\n    return fetchJson('/api/v1/apikey', obj);\n};\n\nexport const deleteToken = (id: ConcordId): Promise<GenericOperationResult> =>\n    fetchJson(`/api/v1/apikey/${id}`, { method: 'DELETE' });\n"
  },
  {
    "path": "console2/src/api/profile/user/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport {ConcordId, fetchJson} from '../../common';\nimport {TeamRole} from \"../../org/team\";\n\nexport interface UserInfoTeam {\n    orgName: string;\n    teamName: string;\n    role: TeamRole;\n}\n\nexport interface UserInfoEntry {\n    id: ConcordId;\n    displayName: string;\n    teams?: UserInfoTeam[];\n    roles?: string[];\n    ldapGroups?: string[];\n}\n\nexport const get = (): Promise<UserInfoEntry> => fetchJson(`/api/service/console/userInfo`);\n"
  },
  {
    "path": "console2/src/api/secret/store/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { fetchJson } from '../../common';\nimport { SecretStoreType } from '../../org/secret';\n\nexport interface SecretStoreEntry {\n    storeType: SecretStoreType;\n    description: string;\n}\n\nexport const listActiveStores = (): Promise<SecretStoreEntry[]> =>\n    fetchJson('/api/v1/secret/store');\n"
  },
  {
    "path": "console2/src/api/server/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { fetchJson } from '../common';\n\nexport interface VersionResponse {\n    version: string;\n}\n\nexport const version = (): Promise<VersionResponse> => fetchJson('/api/v1/server/version');\n"
  },
  {
    "path": "console2/src/api/service/console/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { throttle } from 'lodash';\nimport { ConcordKey, fetchJson, managedFetch, queryParams } from '../../common';\nimport { Organizations } from '../../../state/data/orgs/types';\n\nexport interface UserResponse {\n    username: string;\n    displayName: string;\n    orgs: Organizations;\n}\n\nexport const whoami = async (\n    username?: string,\n    password?: string,\n    rememberMe?: boolean,\n    apiKey?: string\n): Promise<UserResponse> => {\n    const h = new Headers();\n    if (apiKey) {\n        h.set('Authorization', apiKey);\n        h.set('X-Concord-EnableSession', 'true');\n    } else if (username && password) {\n        h.set(\n            'Authorization',\n            `Basic ${btoa(unescape(encodeURIComponent(username + ':' + password)))}`\n        );\n    }\n\n    if (rememberMe) {\n        h.set('X-Concord-RememberMe', 'true');\n    }\n\n    const json = await fetchJson('/api/service/console/whoami', { headers: h });\n    return json as UserResponse;\n};\n\nexport const logout = async () => {\n    await managedFetch('/api/service/console/logout', { method: 'POST' });\n    return true;\n};\n\n// TODO throttle in sagas?\nexport const isProjectExists = throttle(async (orgName: ConcordKey, name: string): Promise<\n    boolean\n> => {\n    try {\n        const json = await fetchJson(`/api/service/console/org/${orgName}/project/${name}/exists`);\n        return json as boolean;\n    } catch (e) {\n        return false;\n    }\n}, 1000);\n\n// TODO throttle in sagas?\nexport const isSecretExists = throttle(async (orgName: ConcordKey, name: string): Promise<\n    boolean\n> => {\n    try {\n        const json = await fetchJson(`/api/service/console/org/${orgName}/secret/${name}/exists`);\n        return json as boolean;\n    } catch (e) {\n        return false;\n    }\n}, 1000);\n\nexport const isStorageExists = throttle(async (orgName: ConcordKey, name: string): Promise<\n    boolean\n> => {\n    try {\n        const json = await fetchJson(\n            `/api/service/console/org/${orgName}/jsonstore/${name}/exists`\n        );\n        return json as boolean;\n    } catch (exception) {\n        return false;\n    }\n}, 1000);\n\nexport const isStorageQueryExists = throttle(\n    async (orgName: ConcordKey, storageName: string, queryName: string): Promise<boolean> => {\n        try {\n            const json = await fetchJson(\n                `/api/service/console/org/${orgName}/jsonstore/${storageName}/query/${queryName}/exists`\n            );\n            return json as boolean;\n        } catch (exception) {\n            return false;\n        }\n    },\n    1000\n);\n\nexport const isRepositoryExists = throttle(\n    async (orgName: ConcordKey, projectName: ConcordKey, name: string): Promise<boolean> => {\n        try {\n            const json = await fetchJson(\n                `/api/service/console/org/${orgName}/project/${projectName}/repo/${name}/exists`\n            );\n            return json as boolean;\n        } catch (e) {\n            return false;\n        }\n    },\n    1000\n);\n\n// TODO throttle in sagas?\nexport const isTeamExists = throttle(async (orgName: ConcordKey, name: string): Promise<\n    boolean\n> => {\n    try {\n        const json = await fetchJson(`/api/service/console/org/${orgName}/team/${name}/exists`);\n        return json as boolean;\n    } catch (e) {\n        return false;\n    }\n}, 1000);\n\n// TODO throttle in sagas?\nexport const isApiTokenExists = throttle(async (name: string): Promise<boolean> => {\n    try {\n        const json = await fetchJson(`/api/service/console/apikey/${name}/exists`);\n        return json as boolean;\n    } catch (e) {\n        return false;\n    }\n}, 1000);\n\nexport interface RepositoryTestRequest {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    url: string;\n    branch?: string;\n    commitId?: string;\n    path?: string;\n    withSecret?: boolean;\n    secretId?: string;\n    secretName?: string;\n}\n\nexport const testRepository = async (req: RepositoryTestRequest): Promise<void> => {\n    const opts: RequestInit = {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json'\n        },\n        body: JSON.stringify(req)\n    };\n\n    const success = await fetchJson('/api/service/console/repository/test', opts);\n    if (!success) {\n        throw new Error('Unknown error');\n    }\n};\n\nexport interface LdapGroupSearchResult {\n    groupName: string;\n    displayName: string;\n}\n\nexport const findLdapGroups = (filter: string): Promise<LdapGroupSearchResult[]> =>\n    fetchJson(`/api/service/console/search/ldapGroups?${queryParams({ filter })}`);\n\nexport const validatePassword = throttle(\n    async (pwd: string): Promise<boolean> => {\n        const opts: RequestInit = {\n            method: 'POST',\n            headers: {\n                'Content-Type': 'text/plain'\n            },\n            body: pwd\n        };\n        const json = await fetchJson('/api/service/console/validate-password', opts);\n        return json as boolean;\n    }\n);\n"
  },
  {
    "path": "console2/src/api/service/console/user/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2023 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport {ConcordId, ConcordKey, fetchJson, queryParams} from '../../../common';\nimport { ProcessEntry } from '../../../process';\n\nexport interface UserActivity {\n    processes: ProcessEntry[];\n}\n\nexport interface ProcessCardEntry {\n    id: ConcordId;\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    repoName: ConcordKey;\n    entryPoint: string;\n    name: string;\n    description?: string;\n    icon?: string;\n    isCustomForm: boolean;\n}\n\nexport const getActivity = (\n    maxOwnProcesses: number\n): Promise<UserActivity> =>\n    fetchJson(\n        `/api/v2/service/console/user/activity?${queryParams({ maxOwnProcesses })}`\n    );\n\nexport const listProcessCards = (\n): Promise<ProcessCardEntry[]> =>\n    fetchJson(\n        `/api/v1/processcard`\n    );\n\nexport const getProcessCard = (\n    cardId: ConcordId\n): Promise<ProcessCardEntry> =>\n    fetchJson(\n        `/api/v1/processcard/${cardId}`\n    );\n"
  },
  {
    "path": "console2/src/api/service/custom_form/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { ConcordId, fetchJson } from '../../common';\n\nexport interface FormSessionResponse {\n    uri: string;\n}\n\nexport const startSession = (\n    processInstanceId: ConcordId,\n    formName: string\n): Promise<FormSessionResponse> =>\n    fetchJson(`/api/service/custom_form/${processInstanceId}/${formName}/start`, {\n        method: 'POST'\n    });\n"
  },
  {
    "path": "console2/src/api/usePolling.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { useEffect, useRef, useState } from 'react';\nimport { RequestError } from './common';\n\nexport const usePolling = (\n    request: () => Promise<boolean>,\n    interval: number,\n    loadingHandler: (inc: number) => void,\n    refresh: boolean\n): RequestError | undefined => {\n    const poll = useRef<number | undefined>(undefined);\n    const [error, setError] = useState<RequestError>();\n\n    useEffect(() => {\n        let cancelled = false;\n\n        const fetchData = async () => {\n            loadingHandler(1);\n\n            let result = false;\n            try {\n                result = await request();\n\n                setError(undefined);\n            } catch (e) {\n                setError(e);\n            } finally {\n                if (result) {\n                    if (!cancelled) {\n                        poll.current = window.setTimeout(() => fetchData(), interval);\n                    }\n                } else {\n                    stopPolling();\n                }\n\n                loadingHandler(-1);\n            }\n        };\n\n        fetchData();\n\n        return () => {\n            cancelled = true;\n            stopPolling();\n        };\n    }, [request, interval, refresh, loadingHandler]);\n\n    const stopPolling = () => {\n        if (poll.current) {\n            clearTimeout(poll.current);\n            poll.current = undefined;\n        }\n    };\n\n    return error;\n};\n"
  },
  {
    "path": "console2/src/api/user/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { ConcordId, fetchJson, queryParams } from '../common';\n\nexport enum UserType {\n    LDAP = 'LDAP',\n    LOCAL = 'LOCAL'\n}\n\nexport interface UserEntry {\n    id: ConcordId;\n    name: string;\n    domain?: string;\n    type: UserType;\n    displayName?: string;\n    email?: string;\n}\n\nexport interface PaginatedUserEntries {\n    items: UserEntry[];\n    next: boolean;\n}\n\nexport const get = async (id: ConcordId): Promise<UserEntry> => fetchJson(`/api/v2/user/${id}`);\n\nexport const list = async (\n    offset: number,\n    limit: number,\n    filter?: string\n): Promise<PaginatedUserEntries> => {\n    const offsetParam = offset > 0 && limit > 0 ? offset * limit : offset;\n    const limitParam = limit > 0 ? limit + 1 : limit;\n\n    const data: UserEntry[] = await fetchJson(\n        `/api/v2/user?${queryParams({\n            offset: offsetParam,\n            limit: limitParam,\n            filter\n        })}`\n    );\n\n    const hasMoreElements: boolean = !!limit && data.length > limit;\n\n    if (limit > 0 && hasMoreElements) {\n        data.pop();\n    }\n\n    return {\n        items: data,\n        next: hasMoreElements\n    };\n};\n"
  },
  {
    "path": "console2/src/components/atoms/ClassIcon.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\n\nexport const ClassIcon: React.SFC<{ classes: string; style?: object }> = ({ classes, style }) => (\n    <i style={{ padding: 0, ...style }} className={classes} />\n);\n\nexport default ClassIcon;\n"
  },
  {
    "path": "console2/src/components/atoms/ColumnSort/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport { Button } from \"semantic-ui-react\";\nimport * as React from \"react\";\n\ninterface Props {\n    ascSort: () => void;\n    descSort: () => void;\n}\n\nexport const ColumnSort: React.SFC<Props> = ({ ascSort, descSort }) => (\n    <Button.Group style={{ float:'right' }}>\n        <Button\n            basic={true}\n            icon=\"angle up\"\n            disabled={false}\n            onClick={ascSort}\n        />\n        <Button\n            basic={true}\n            icon=\"angle down\"\n            disabled={false}\n            onClick={descSort}\n        />\n    </Button.Group>\n);\n\nexport default ColumnSort;\n"
  },
  {
    "path": "console2/src/components/atoms/FormikCheckbox/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { Field, getIn } from 'formik';\nimport { FieldProps } from 'formik/dist/Field';\nimport * as React from 'react';\nimport { Checkbox, CheckboxProps, Form, FormCheckboxProps, Label } from 'semantic-ui-react';\n\ninterface Props {\n    name: string;\n}\n\nexport default class extends React.PureComponent<FormCheckboxProps & Props> {\n    render() {\n        const { name: fieldName, label, required, inline, ...rest } = this.props;\n\n        return (\n            <Field name={fieldName}>\n                {({ field, form }: FieldProps) => {\n                    const touched = getIn(form.touched, fieldName);\n                    const error = getIn(form.errors, fieldName);\n                    const invalid = !!(touched && error);\n\n                    const handleChanges = (ev: {}, { checked }: CheckboxProps) =>\n                        form.setFieldValue(fieldName, checked);\n\n                    return (\n                        <Form.Field error={invalid} required={required} inline={inline}>\n                            <label>{label}</label>\n                            <Checkbox {...rest} onChange={handleChanges} checked={field.value} />\n                            {invalid && error && (\n                                <Label basic={true} pointing={true} color=\"red\">\n                                    {error}\n                                </Label>\n                            )}\n                        </Form.Field>\n                    );\n                }}\n            </Field>\n        );\n    }\n}\n"
  },
  {
    "path": "console2/src/components/atoms/FormikDropdown/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { Field, getIn } from 'formik';\nimport { FieldProps } from 'formik/dist/Field';\nimport * as React from 'react';\nimport { Dropdown, DropdownProps, Form, FormDropdownProps, Label } from 'semantic-ui-react';\n\nexport default class extends React.PureComponent<FormDropdownProps> {\n    render() {\n        const { name: fieldName, label, required, ...rest } = this.props;\n\n        return (\n            <Field name={fieldName}>\n                {({ field, form }: FieldProps) => {\n                    const touched = getIn(form.touched, fieldName);\n                    const error = getIn(form.errors, fieldName);\n                    const invalid = !!(touched && error);\n\n                    const handleChange = (ev: {}, { value }: DropdownProps) => {\n                        form.setFieldValue(fieldName, value);\n                    };\n\n                    const handleBlur = () => {\n                        form.setFieldTouched(fieldName, true);\n                    };\n\n                    return (\n                        <Form.Field error={invalid} required={required}>\n                            <label>{label}</label>\n                            <Dropdown\n                                {...rest}\n                                selectOnBlur={true}\n                                onChange={handleChange}\n                                onBlur={handleBlur}\n                                value={field.value}\n                                error={invalid}\n                            />\n                            {invalid && error && (\n                                <Label basic={true} pointing={true} color=\"red\">\n                                    {error}\n                                </Label>\n                            )}\n                        </Form.Field>\n                    );\n                }}\n            </Field>\n        );\n    }\n}\n"
  },
  {
    "path": "console2/src/components/atoms/FormikFileInput/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { Field, getIn } from 'formik';\nimport { FieldProps } from 'formik/dist/Field';\nimport * as React from 'react';\nimport { Form, FormInputProps, Input, Label } from 'semantic-ui-react';\n\nexport default class extends React.Component<FormInputProps> {\n    render() {\n        const { name: fieldName, label, required, ...rest } = this.props;\n\n        return (\n            <Field name={fieldName}>\n                {({ field, form }: FieldProps) => {\n                    const touched = getIn(form.touched, fieldName);\n                    const error = getIn(form.errors, fieldName);\n                    const invalid = !!(touched && error);\n\n                    const handleChange = (ev: any) =>\n                        form.setFieldValue(fieldName, ev.target.files[0]);\n\n                    return (\n                        <Form.Field error={invalid} required={required}>\n                            <label>{label}</label>\n                            <Input {...rest} type=\"file\" onChange={handleChange} />\n                            {invalid && error && (\n                                <Label basic={true} pointing={true} color=\"red\">\n                                    {error}\n                                </Label>\n                            )}\n                        </Form.Field>\n                    );\n                }}\n            </Field>\n        );\n    }\n}\n"
  },
  {
    "path": "console2/src/components/atoms/FormikInput/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { Field, getIn } from 'formik';\nimport { FieldProps } from 'formik/dist/Field';\nimport * as React from 'react';\nimport { Form, FormInputProps, Input, Label } from 'semantic-ui-react';\n\ninterface ExternalProps {\n    validate?: (value: {}) => string | Promise<void> | undefined;\n    'data-testid'?: string;\n}\n\ntype Props = FormInputProps & ExternalProps;\n\nexport default class extends React.Component<Props> {\n    render() {\n        const { name: fieldName, label, required, validate, 'data-testid': dataTestId, ...rest } = this.props;\n\n        return (\n            <Field name={fieldName} validate={validate}>\n                {({ field, form }: FieldProps) => {\n                    const touched = getIn(form.touched, fieldName);\n                    const error = getIn(form.errors, fieldName);\n                    const invalid = !!(touched && error);\n\n                    if (!field.value) {\n                        field.value = '';\n                    }\n\n                    return (\n                        <Form.Field error={invalid} required={required} data-testid={dataTestId}>\n                            {label && <label>{label}</label>}\n                            <Input {...rest} {...field} />\n                            {invalid && error && (\n                                <Label basic={true} pointing={true} color=\"red\">\n                                    {error}\n                                </Label>\n                            )}\n                        </Form.Field>\n                    );\n                }}\n            </Field>\n        );\n    }\n}\n"
  },
  {
    "path": "console2/src/components/atoms/LogFileFromBlob/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport styled from 'styled-components';\nimport { Highlighter } from '../../../components/molecules';\nimport { escapeHtml } from '../../../utils';\n\nconst TextPre = styled.pre`\n    white-space: pre-wrap;\n    word-break: break-word;\n`;\n\ninterface Props {\n    blobUrl: string;\n    // activeHighlight should highlight the given text\n    // with a yellow background\n    activeHighlight?: string;\n}\n\ninterface State {\n    data: string[];\n    originalData: string;\n}\n\nclass FileFromBlob extends React.Component<Props, State> {\n    state = {\n        data: [''],\n        originalData: ''\n    };\n\n    // TODO: Add polling method to support quicker render for larger files.\n    // TODO: Add fetch to log Container\n    // * Saga code already supports this.\n    async componentDidMount() {\n        const blob = await fetch(this.props.blobUrl, { credentials: 'same-origin' }).then((r) =>\n            r.blob()\n        );\n        const reader = new FileReader();\n\n        // This fires after the blob has been read/loaded.\n        reader.addEventListener('loadend', (e: any) => {\n            const text = e.srcElement.result;\n            // Capture text in state\n            this.setState({\n                originalData: escapeHtml(text),\n                data: this.markCheckpointIds(text.split('\\n'))\n            });\n        });\n\n        // Triggers loadend event\n        reader.readAsText(blob);\n    }\n\n    componentDidUpdate(nextProps: Props, nextState: State) {\n        if (nextProps.activeHighlight !== this.props.activeHighlight) {\n            this.setState({ data: this.markCheckpointIds(this.state.originalData.split('\\n')) });\n        }\n    }\n\n    markCheckpointIds = (strings: string[]) => {\n        strings.forEach((line, index, array) => {\n            if (line.includes('Main - checkpoint')) {\n                array[index] = this.wrapCheckpoint(line);\n            }\n            if (this.props.activeHighlight) {\n                array[index] = this.wrapActiveHighlight(line);\n            }\n        });\n\n        return strings;\n    };\n\n    wrapCheckpoint = (line: string) => {\n        const concordKeyRE = /([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/g;\n\n        const checkpointId = line.match(concordKeyRE);\n\n        if (checkpointId) {\n            return line.replace(\n                concordKeyRE,\n                `<a style=\" font-weight: bold\" id=\"${checkpointId[0]}\">${checkpointId[0]}</a>`\n            );\n        } else {\n            console.error(`Could not find regex group for ${checkpointId}`);\n            return 'ERROR';\n        }\n    };\n\n    wrapActiveHighlight = (line: string) => {\n        const concordKeyRE = this.props.activeHighlight;\n        if (concordKeyRE) {\n            const checkpointId = line.match(concordKeyRE);\n\n            if (checkpointId) {\n                return line.replace(\n                    concordKeyRE,\n                    `<a style=\" background: yellow; color: black; font-weight: bold\" id=\"${checkpointId[0]}\">${checkpointId[0]}</a>`\n                );\n            } else {\n                return line;\n            }\n        } else {\n            console.error(`Could not find regex group for ${concordKeyRE}`);\n            return 'ERROR';\n        }\n    };\n\n    render() {\n        const { blobUrl } = this.props;\n        const { data } = this.state;\n\n        if (blobUrl) {\n            if (data) {\n                return (\n                    <TextPre>\n                        {' '}\n                        <Highlighter\n                            value={data.join('\\n')}\n                            config={[\n                                { string: 'INFO', style: 'color: #00B5F0' },\n                                { string: 'WARN ', style: 'color:  #ffae42' },\n                                { string: 'WARNING', style: 'color:  #ffae42' },\n                                { string: 'ERROR', style: 'color: #ff0000' },\n                                {\n                                    string: 'Process status: FINISHED',\n                                    style: 'color: green',\n                                    divide: true\n                                },\n                                {\n                                    string: 'Process status: FAILED',\n                                    style: 'color: #ff0000',\n                                    divide: true\n                                },\n                                {\n                                    string: 'Process status: CANCELLED',\n                                    style: 'color: #808080',\n                                    divide: true\n                                },\n                                { string: 'ANSIBLE:', style: 'color: #808080' },\n                                { string: 'DOCKER:', style: 'color: #808080' }\n                            ]}\n                        />\n                    </TextPre>\n                );\n            } else {\n                return null;\n            }\n        } else {\n            return null;\n        }\n    }\n}\n\nexport default FileFromBlob;\n"
  },
  {
    "path": "console2/src/components/atoms/ReactJson/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport type { ComponentType } from 'react';\nimport ReactJsonModule, { type ReactJsonViewProps } from 'react-json-view';\n\ntype ReactJsonModuleShape = ComponentType<ReactJsonViewProps> & {\n    default?: ComponentType<ReactJsonViewProps>;\n};\n\n// Vite 8 can surface this UMD dependency as either the component or a module object.\nconst ReactJson =\n    (ReactJsonModule as ReactJsonModuleShape).default ??\n    (ReactJsonModule as ReactJsonModuleShape);\n\nexport default ReactJson;\n"
  },
  {
    "path": "console2/src/components/atoms/RefreshButton/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { Button } from 'semantic-ui-react';\n\ninterface Props {\n    loading: boolean;\n    clickAction: () => void;\n}\n\nexport const RefreshButton: React.SFC<Props> = ({ loading, clickAction }) => (\n    <Button basic={true} icon=\"refresh\" loading={loading} onClick={clickAction} />\n);\n\nexport default RefreshButton;\n"
  },
  {
    "path": "console2/src/components/atoms/Scrollable.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport styled from 'styled-components';\n\nexport const ScrollableX = styled.div`\n    overflow-x: auto;\n`;\n"
  },
  {
    "path": "console2/src/components/atoms/TableSearchFilter/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Button, Checkbox, Dropdown, Grid, Header, Input, Popup } from 'semantic-ui-react';\nimport { ColumnDefinition } from '../../../api/org';\n\nimport './styles.css';\n\ninterface State {\n    value: string;\n    inputValue: string;\n    isOpen: boolean;\n    filtered: boolean;\n}\n\ninterface ExternalProps {\n    currentValue?: string;\n    column: ColumnDefinition;\n    onFilterChange: (column: ColumnDefinition, filterValue: string) => void;\n}\n\ntype Props = ExternalProps;\n\nexport default class extends React.Component<Props, State> {\n    constructor(props: Props) {\n        super(props);\n        const value = this.props.currentValue || '';\n\n        this.state = { value, inputValue: value, isOpen: false, filtered: value !== '' };\n    }\n\n    componentDidUpdate(prepProps: Props) {\n        const currentValue = this.props.currentValue || '';\n        const prevValue = prepProps.currentValue || '';\n        if (currentValue !== prevValue) {\n            this.setState({\n                value: currentValue,\n                inputValue: currentValue,\n                filtered: currentValue !== ''\n            });\n        }\n    }\n\n    renderInput(c: ColumnDefinition) {\n        return (\n            <Input\n                fluid={true}\n                name={c.source}\n                value={this.state.inputValue}\n                onChange={(event, data) => this.setState({ inputValue: data.value })}\n                autoFocus={true}\n            />\n        );\n    }\n\n    renderCheckbox(c: ColumnDefinition) {\n        return (\n            <Checkbox\n                type=\"checkbox\"\n                name={c.source}\n                checked={this.state.inputValue === 'true'}\n                onChange={(event, { checked }) =>\n                    this.setState({ inputValue: checked ? 'true' : 'false' })\n                }\n            />\n        );\n    }\n\n    renderDropDown(c: ColumnDefinition) {\n        return (\n            <Dropdown\n                fluid={true}\n                placeholder={'Choose ' + c.caption}\n                clearable={true}\n                selection={true}\n                name={c.source}\n                value={this.state.inputValue}\n                options={c.searchOptions}\n                onChange={(event, data) => this.setState({ inputValue: data.value as string })}\n            />\n        );\n    }\n\n    renderStringField(c: ColumnDefinition) {\n        if (c.searchOptions !== undefined) {\n            return this.renderDropDown(c);\n        } else {\n            return this.renderInput(c);\n        }\n    }\n\n    renderSearchField(c: ColumnDefinition) {\n        switch (c.searchValueType!) {\n            case 'string': {\n                return this.renderStringField(c);\n            }\n            case 'boolean': {\n                return this.renderCheckbox(c);\n            }\n            default:\n                return (\n                    <p key={c.searchValueType}>Unknown search field type: {c.searchValueType}</p>\n                );\n        }\n    }\n\n    handleOpen() {\n        const { value } = this.state;\n        this.setState({ isOpen: true, inputValue: value });\n    }\n\n    handleClose() {\n        this.setState({ isOpen: false });\n    }\n\n    handleClearFilter() {\n        const { value } = this.state;\n        if (value !== '') {\n            this.props.onFilterChange(this.props.column, '');\n        }\n        this.setState({ value: '', inputValue: '', filtered: false });\n\n        this.handleClose();\n    }\n\n    handleApplyFilter() {\n        const { value, inputValue } = this.state;\n        const changed = inputValue !== value;\n        if (changed) {\n            this.props.onFilterChange(this.props.column, this.state.inputValue);\n        }\n        this.setState({ value: inputValue, filtered: changed && inputValue !== '' });\n\n        this.handleClose();\n    }\n\n    render() {\n        const { column } = this.props;\n        const { isOpen, filtered, value, inputValue } = this.state;\n\n        return (\n            <Popup\n                open={isOpen}\n                onOpen={() => this.handleOpen()}\n                onClose={() => this.handleClose()}\n                trigger={\n                    <Button\n                        style={{ opacity: filtered ? 1 : 0.4 }}\n                        className=\"tableSearchFilter\"\n                        compact={true}\n                        icon={'filter'}\n                    />\n                }\n                on=\"click\">\n                <Popup.Content>\n                    <Grid textAlign=\"center\" verticalAlign=\"middle\">\n                        <Grid.Row>\n                            <Grid.Column>\n                                <Header as=\"h4\" textAlign=\"center\">\n                                    Show items with {column.caption}:\n                                </Header>\n                            </Grid.Column>\n                        </Grid.Row>\n\n                        <Grid.Row>\n                            <Grid.Column>{this.renderSearchField(column)}</Grid.Column>\n                        </Grid.Row>\n\n                        <Grid.Row>\n                            <Grid.Column>\n                                <Button.Group fluid={true}>\n                                    <Button\n                                        onClick={() => this.handleClearFilter()}\n                                        disabled={inputValue === ''}>\n                                        Clear\n                                    </Button>\n                                    <Button.Or />\n                                    <Button\n                                        primary={true}\n                                        disabled={value === inputValue}\n                                        onClick={() => this.handleApplyFilter()}>\n                                        Filter\n                                    </Button>\n                                </Button.Group>\n                            </Grid.Column>\n                        </Grid.Row>\n                    </Grid>\n                </Popup.Content>\n            </Popup>\n        );\n    }\n}\n"
  },
  {
    "path": "console2/src/components/atoms/TableSearchFilter/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n.tableSearchFilter {\n    margin: 0 !important;\n    background: none !important;\n    padding: 0 !important;\n}\n"
  },
  {
    "path": "console2/src/components/atoms/Truncate.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\n\ninterface Props {\n    text: string;\n    allowedCharCount?: number;\n    startSubstringCount?: number;\n    endSubstringCount?: number;\n}\n\n// TODO: Handle non existant values\n// * Returns a smaller string than what was originally supplied\nexport const Truncate: React.SFC<Props> = ({\n    text,\n    allowedCharCount = 15,\n    startSubstringCount = 6,\n    endSubstringCount = 6\n}) => {\n    const separator: string = '...';\n    const start = text.substring(0, startSubstringCount);\n    const end = text.substring(text.length - endSubstringCount, text.length);\n\n    if (text.length > allowedCharCount) {\n        return <span>{`${start + separator + end}`}</span>;\n    } else {\n        return <span>{text}</span>;\n    }\n};\n\nexport default Truncate;\n"
  },
  {
    "path": "console2/src/components/atoms/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nexport { default as FormikCheckbox } from './FormikCheckbox';\nexport { default as FormikDropdown } from './FormikDropdown';\nexport { default as FormikFileInput } from './FormikFileInput';\nexport { default as FormikInput } from './FormikInput';\nexport { default as RefreshButton } from './RefreshButton';\nexport { default as ReactJson } from './ReactJson';\nexport { default as Truncate } from './Truncate';\nexport { default as TableSearchFilter } from './TableSearchFilter';\n"
  },
  {
    "path": "console2/src/components/molecules/BreadcrumbSegment/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Breadcrumb, Segment } from 'semantic-ui-react';\n\nimport './styles.css';\n\nclass BreadcrumbSegment extends React.PureComponent {\n    render() {\n        return (\n            <Segment basic={true} className=\"breadcrumbSegment\">\n                <Breadcrumb size=\"big\">{this.props.children}</Breadcrumb>\n            </Segment>\n        );\n    }\n}\n\nexport default BreadcrumbSegment;\n"
  },
  {
    "path": "console2/src/components/molecules/BreadcrumbSegment/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n.breadcrumbSegment {\n    padding-bottom: 0 !important;\n    padding-left: 0 !important;\n}\n"
  },
  {
    "path": "console2/src/components/molecules/BulkProcessActionDropdown/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Dropdown, Icon } from 'semantic-ui-react';\n\nimport { ConcordId } from '../../../api/common';\nimport { BulkCancelProcessPopup } from '../../organisms';\n\ninterface ExternalProps {\n    data: ConcordId[];\n    refresh: () => void;\n}\n\nclass BulkProcessActionDropdown extends React.PureComponent<ExternalProps> {\n    render() {\n        const { data, refresh } = this.props;\n        return (\n            <Dropdown text=\"Actions\" disabled={data.length === 0} button={true}>\n                <Dropdown.Menu>\n                    <BulkCancelProcessPopup\n                        data={data}\n                        refresh={refresh}\n                        trigger={(onClick: any) => (\n                            <Dropdown.Item onClick={onClick}>\n                                <Icon name=\"delete\" color=\"red\" />\n                                <span className=\"text\">Cancel</span>\n                            </Dropdown.Item>\n                        )}\n                    />\n                </Dropdown.Menu>\n            </Dropdown>\n        );\n    }\n}\n\nexport default BulkProcessActionDropdown;\n"
  },
  {
    "path": "console2/src/components/molecules/ButtonWithConfirmation/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Button, Confirm, ButtonProps } from 'semantic-ui-react';\n\ninterface State {\n    showConfirm: boolean;\n}\n\ninterface Props extends ButtonProps {\n    renderOverride?: React.ReactNode;\n    confirmationHeader: string;\n    confirmationContent: string;\n    onConfirm: () => void;\n}\n\nclass ButtonWithConfirmation extends React.Component<Props, State> {\n    constructor(props: Props) {\n        super(props);\n        this.state = { showConfirm: false };\n        this.handleShowConfirm = this.handleShowConfirm.bind(this);\n        this.handleCancel = this.handleCancel.bind(this);\n        this.handleConfirm = this.handleConfirm.bind(this);\n    }\n\n    handleShowConfirm(ev: React.SyntheticEvent<{}>) {\n        ev.preventDefault();\n        ev.stopPropagation();\n        this.setState({ showConfirm: true });\n    }\n\n    handleCancel(ev: React.SyntheticEvent<{}>) {\n        ev.stopPropagation();\n        this.setState({ showConfirm: false });\n    }\n\n    handleConfirm(ev: React.SyntheticEvent<{}>) {\n        ev.stopPropagation();\n        this.setState({ showConfirm: false });\n        this.props.onConfirm();\n    }\n\n    render() {\n        const {\n            confirmationHeader,\n            confirmationContent,\n            onConfirm,\n            renderOverride,\n            ...rest\n        } = this.props;\n\n        return (\n            <>\n                {renderOverride ? (\n                    <div onClick={(ev) => this.handleShowConfirm(ev)}>{renderOverride}</div>\n                ) : (\n                    <Button {...rest} onClick={(ev) => this.handleShowConfirm(ev)} />\n                )}\n\n                <Confirm\n                    open={this.state.showConfirm}\n                    header={confirmationHeader}\n                    content={confirmationContent}\n                    onConfirm={this.handleConfirm}\n                    onCancel={this.handleCancel}\n                />\n            </>\n        );\n    }\n}\n\nexport default ButtonWithConfirmation;\n"
  },
  {
    "path": "console2/src/components/molecules/CreateNewEntityButton/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport RedirectButton from '../../organisms/RedirectButton';\nimport { ConcordKey } from '../../../api/common';\nimport { Popup } from 'semantic-ui-react';\n\ninterface Props {\n    entity: string;\n    title?: string;\n    orgName: ConcordKey;\n    userInOrg: boolean;\n    enabledInPolicy: boolean;\n}\n\nexport default ({ entity, title, orgName, userInOrg, enabledInPolicy }: Props) => {\n    const button = (disabled?: boolean) => (\n        <RedirectButton\n            disabled={disabled}\n            icon=\"plus\"\n            positive={true}\n            labelPosition=\"left\"\n            content={title || `New ${entity}`}\n            location={`/org/${orgName}/${entity}/_new`}\n        />\n    );\n\n    const disabled = !userInOrg || !enabledInPolicy;\n    if (!disabled) {\n        return button();\n    }\n\n    let explanation = '';\n    if (!userInOrg) {\n        explanation = `Only the organization members can create new ${entity}s.`;\n    } else if (!enabledInPolicy) {\n        explanation = `The organization's policy forbids the creation of new ${entity}s.`;\n    }\n\n    // by default Semantic-UI doesn't trigger popup for the disabled elements\n    // wrap in <span/> as a workaround\n    return <Popup trigger={<span>{button(true)}</span>} content={explanation} />;\n};\n"
  },
  {
    "path": "console2/src/components/molecules/DropdownWithAddition/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { Dropdown, DropdownItemProps, DropdownProps } from 'semantic-ui-react';\n\ninterface State {\n    stateOptions: any[];\n}\n\ninterface Props {\n    name: string;\n    required: boolean;\n    value?: any;\n    options: DropdownItemProps[];\n    multiple: boolean;\n    allowAdditions: boolean;\n    submitting?: boolean;\n    completed?: boolean;\n    onChange: (event: React.SyntheticEvent<HTMLElement>, data: DropdownProps) => void;\n}\n\nconst toState = (value: any[], options: DropdownItemProps[]): State => {\n    return { stateOptions: options };\n};\n\nclass DropdownWithAddition extends React.Component<Props, State> {\n    constructor(props: Props) {\n        super(props);\n        this.state = toState(props.value, this.props.options);\n\n        this.handleAddition = this.handleAddition.bind(this);\n    }\n\n    handleAddition = (e: any, { value }: DropdownProps) => {\n        this.setState({\n            stateOptions: [{ text: value, value }, ...this.state.stateOptions]\n        });\n    };\n\n    render() {\n        const {\n            name,\n            multiple,\n            allowAdditions,\n            value,\n            required,\n            submitting,\n            completed,\n            onChange\n        } = this.props;\n\n        return (\n            <>\n                <Dropdown\n                    clearable={!required}\n                    selection={true}\n                    multiple={multiple}\n                    name={name}\n                    disabled={submitting || completed}\n                    value={value}\n                    options={this.state.stateOptions}\n                    search={true}\n                    allowAdditions={allowAdditions}\n                    onChange={onChange}\n                    onAddItem={this.handleAddition}\n                />\n            </>\n        );\n    }\n}\n\nexport default DropdownWithAddition;\n"
  },
  {
    "path": "console2/src/components/molecules/EditProjectForm/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { InjectedFormikProps, withFormik } from 'formik';\nimport * as React from 'react';\nimport { Divider, Form } from 'semantic-ui-react';\nimport { ProjectVisibility, UpdateProjectEntry } from '../../../api/org/project';\nimport { FormikDropdown, FormikInput } from '../../atoms';\n\nexport interface FormValues {\n    data: UpdateProjectEntry;\n}\n\nexport type EditProjectFormValues = FormValues;\n\ninterface Props {\n    submitting: boolean;\n    data: UpdateProjectEntry;\n    onSubmit: (values: FormValues) => void;\n}\n\nconst visibilityOptions = [\n    {\n        text: 'Public',\n        icon: 'unlock',\n        value: ProjectVisibility.PUBLIC\n    },\n    {\n        text: 'Private',\n        icon: 'lock',\n        value: ProjectVisibility.PRIVATE\n    }\n];\n\nclass EditProjectForm extends React.Component<InjectedFormikProps<Props, FormValues>> {\n    updateProject(ev: React.SyntheticEvent<{}>) {\n        ev.preventDefault();\n        this.props.submitForm();\n    }\n\n    render() {\n        const { dirty, handleSubmit, submitting } = this.props;\n\n        return (\n            <Form onSubmit={handleSubmit} loading={submitting}>\n                <FormikDropdown\n                    selection={true}\n                    name=\"data.visibility\"\n                    label=\"Visibility\"\n                    options={visibilityOptions}\n                />\n\n                <FormikInput fluid={true} label=\"Description\" name=\"data.description\" />\n                <Divider />\n\n                <Form.Button\n                    primary={true}\n                    content=\"Save\"\n                    disabled={!dirty}\n                    onClick={(ev) => this.updateProject(ev)}\n                />\n            </Form>\n        );\n    }\n}\n\nexport default withFormik<Props, FormValues>({\n    handleSubmit: ({ data }, bag) => {\n        // update only the specific fields\n        bag.props.onSubmit({\n            data: {\n                id: data.id,\n                name: bag.props.data.name,\n                visibility: data.visibility,\n                description: data.description\n            }\n        });\n    }\n})(EditProjectForm);\n"
  },
  {
    "path": "console2/src/components/molecules/EntityId/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { ConcordId } from '../../../api/common';\nimport { WithCopyToClipboard } from '../index';\n\ninterface EntityIdProps {\n    id?: ConcordId;\n}\n\nexport default ({ id }: EntityIdProps) => {\n    if (!id) {\n        return <WithCopyToClipboard value={'...'}>ID: ...</WithCopyToClipboard>;\n    }\n\n    return <WithCopyToClipboard value={id}>ID: {id}</WithCopyToClipboard>;\n};\n"
  },
  {
    "path": "console2/src/components/molecules/EntityOwnerChangeForm/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport { Confirm, Form } from 'semantic-ui-react';\nimport { FindUserField2 } from '../../organisms';\nimport { UserEntry } from '../../../api/user';\nimport { ConcordId } from '../../../api/common';\n\ninterface Props {\n    originalOwnerId?: ConcordId;\n    confirmationHeader: string;\n    confirmationContent: string;\n    onSubmit: (value: ConcordId) => void;\n    submitting: boolean;\n    disabled?: boolean;\n}\n\ninterface State {\n    dirty: boolean;\n    showConfirm: boolean;\n    value?: ConcordId;\n}\n\nclass EntityOwnerChangeForm extends React.PureComponent<Props, State> {\n    constructor(props: Props) {\n        super(props);\n\n        this.state = { dirty: false, showConfirm: false, value: props.originalOwnerId };\n    }\n\n    onSelect(i: UserEntry) {\n        const dirty = this.props.originalOwnerId !== i.id;\n        this.setState({ dirty, value: i.id });\n    }\n\n    handleShowConfirm(ev: React.SyntheticEvent<{}>) {\n        ev.preventDefault();\n        this.setState({ showConfirm: true });\n    }\n\n    handleCancel() {\n        this.setState({ showConfirm: false });\n    }\n\n    handleConfirm() {\n        this.setState({ showConfirm: false });\n        this.handleSubmit();\n    }\n\n    handleSubmit() {\n        const { value } = this.state;\n        if (!value) {\n            return;\n        }\n\n        this.props.onSubmit(value);\n    }\n\n    render() {\n        const { dirty } = this.state;\n        const {\n            submitting,\n            originalOwnerId,\n            confirmationHeader,\n            confirmationContent,\n            disabled\n        } = this.props;\n\n        return (\n            <>\n                <Form loading={submitting}>\n                    <Form.Group widths={3}>\n                        <Form.Field disabled={disabled}>\n                            <FindUserField2\n                                placeholder=\"Search for a user...\"\n                                defaultUserId={originalOwnerId}\n                                onSelect={(u: UserEntry) => this.onSelect(u)}\n                            />\n                        </Form.Field>\n\n                        <Form.Button\n                            primary={true}\n                            negative={true}\n                            content=\"Change\"\n                            disabled={!dirty || disabled}\n                            onClick={(ev) => this.handleShowConfirm(ev)}\n                        />\n                    </Form.Group>\n\n                    <Confirm\n                        open={this.state.showConfirm}\n                        header={confirmationHeader}\n                        content={confirmationContent}\n                        onConfirm={() => this.handleConfirm()}\n                        onCancel={() => this.handleCancel()}\n                    />\n                </Form>\n            </>\n        );\n    }\n}\n\nexport default EntityOwnerChangeForm;\n"
  },
  {
    "path": "console2/src/components/molecules/EntityOwnerPopup/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { EntityOwner } from '../../../api/common';\nimport { Popup, Table } from 'semantic-ui-react';\n\ninterface Props {\n    data: EntityOwner;\n}\n\nexport default ({ data }: Props) => (\n    <Popup trigger={<div>{data.username}</div>}>\n        <Table size=\"small\" collapsing={true} compact={true} definition={true} singleLine={true}>\n            <Table.Body>\n                <Table.Row>\n                    <Table.Cell>ID</Table.Cell>\n                    <Table.Cell>{data.id}</Table.Cell>\n                </Table.Row>\n                <Table.Row>\n                    <Table.Cell>Display Name</Table.Cell>\n                    <Table.Cell>{data.displayName ? data.displayName : '-'}</Table.Cell>\n                </Table.Row>\n                <Table.Row>\n                    <Table.Cell>Domain</Table.Cell>\n                    <Table.Cell>{data.userDomain ? data.userDomain : '-'}</Table.Cell>\n                </Table.Row>\n            </Table.Body>\n        </Table>\n    </Popup>\n);\n"
  },
  {
    "path": "console2/src/components/molecules/EntityRenameForm/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { InjectedFormikProps, withFormik } from 'formik';\nimport * as React from 'react';\nimport { Confirm, Form } from 'semantic-ui-react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { notEmpty } from '../../../utils';\nimport { project as validation } from '../../../validation';\nimport { FormikInput } from '../../atoms';\n\ninterface State {\n    showConfirm: boolean;\n}\n\nexport interface FormValues {\n    name: ConcordKey;\n}\n\ninterface Props {\n    originalName: ConcordKey;\n    submitting: boolean;\n    inputPlaceholder?: string;\n    confirmationHeader: string;\n    confirmationContent: string;\n    onSubmit: (values: FormValues) => void;\n    isExists: (name: string) => Promise<boolean> | undefined;\n    alreadyExistsTemplate: (name: string) => string;\n    disabled?: boolean;\n    inputTestId?: string;\n    buttonTestId?: string;\n}\n\nclass EntityRenameForm extends React.Component<InjectedFormikProps<Props, FormValues>, State> {\n    constructor(props: InjectedFormikProps<Props, FormValues>) {\n        super(props);\n        this.state = { showConfirm: false };\n    }\n\n    handleShowConfirm(ev: React.SyntheticEvent<{}>) {\n        ev.preventDefault();\n        this.setState({ showConfirm: true });\n    }\n\n    handleCancel() {\n        this.setState({ showConfirm: false });\n    }\n\n    handleConfirm() {\n        this.setState({ showConfirm: false });\n        this.props.submitForm();\n    }\n\n    render() {\n        const {\n            inputPlaceholder,\n            dirty,\n            handleSubmit,\n            submitting,\n            confirmationHeader,\n            confirmationContent,\n            disabled,\n            inputTestId,\n            buttonTestId\n        } = this.props;\n        const hasErrors = notEmpty(this.props.errors);\n\n        return (\n            <Form onSubmit={handleSubmit} loading={submitting}>\n                <Form.Group widths={3}>\n                    <FormikInput\n                        fluid={true}\n                        name=\"name\"\n                        placeholder={inputPlaceholder}\n                        disabled={disabled}\n                        data-testid={inputTestId}\n                    />\n\n                    <Form.Button\n                        primary={true}\n                        negative={true}\n                        content=\"Rename\"\n                        disabled={hasErrors || !dirty || disabled}\n                        onClick={(ev) => this.handleShowConfirm(ev)}\n                        data-testid={buttonTestId}\n                    />\n                </Form.Group>\n\n                <Confirm\n                    open={this.state.showConfirm}\n                    header={confirmationHeader}\n                    content={confirmationContent}\n                    onConfirm={() => this.handleConfirm()}\n                    onCancel={() => this.handleCancel()}\n                />\n            </Form>\n        );\n    }\n}\n\nconst validator = async (\n    values: FormValues,\n    { originalName, isExists, alreadyExistsTemplate }: Props\n) => {\n    let e;\n\n    e = validation.name(values.name);\n    if (e) {\n        return Promise.resolve({ name: e });\n    }\n\n    if (values.name !== originalName) {\n        const exists = await isExists(values.name);\n        if (exists) {\n            return Promise.resolve({ name: alreadyExistsTemplate(values.name) });\n        }\n    }\n\n    return {};\n};\n\nexport default withFormik<Props, FormValues>({\n    handleSubmit: (values, bag) => {\n        bag.props.onSubmit(values);\n    },\n    mapPropsToValues: (props) => ({\n        name: props.originalName\n    }),\n    validate: validator\n})(EntityRenameForm);\n"
  },
  {
    "path": "console2/src/components/molecules/FormWizardAction/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Button } from 'semantic-ui-react';\n\ninterface Props {\n    onOpenWizard: () => void;\n}\n\nclass FormWizardAction extends React.PureComponent<Props> {\n    render() {\n        const { onOpenWizard } = this.props;\n\n        return (\n            <Button\n                id=\"formWizardButton\"\n                onClick={() => onOpenWizard()}\n                content=\"Form Wizard\"\n                color=\"blue\"\n            />\n        );\n    }\n}\n\nexport default FormWizardAction;\n"
  },
  {
    "path": "console2/src/components/molecules/GitHubLink/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { Icon } from 'semantic-ui-react';\nimport { REPOSITORY_SSH_URL_PATTERN } from '../../../validation';\n\ninterface Props {\n    url: string;\n    link?: string;\n    path?: string;\n    commitId?: string;\n    text?: string;\n    branch?: string;\n}\n\nexport const gitUrlParse = (s: string): string | undefined => {\n    const url = s.endsWith('.git') ? s : s + '.git';\n\n    const match = REPOSITORY_SSH_URL_PATTERN.exec(url);\n\n    if (match && match.length === 6) {\n        const path = match[4] !== undefined ? `/${match[4]}` : '';\n        return `https://${match[3]}${path}`;\n    } else if (url.startsWith('http')) {\n        // https://github.example.com/devtools/concord.git\n        const regex = /http[s]?:\\/\\/(.*)/;\n        const match = regex.exec(url);\n        if (!match || match.length !== 2) {\n            return;\n        }\n        return `https://${match[1]}`;\n    }\n\n    return;\n};\n\nconst normalizePath = (s: string): string => {\n    if (s.startsWith('/')) {\n        return s.substring(1);\n    }\n    return s;\n};\n\nclass GitHubLink extends React.PureComponent<Props> {\n    render() {\n        const { url, link, commitId, path, text, branch } = this.props;\n\n        let s = !link ? gitUrlParse(url) : link;\n        if (!s) {\n            return url;\n        }\n\n        if (s.endsWith('.git')) {\n            s = s.substr(0, s.length - 4);\n        }\n\n        if (commitId && !path) {\n            s += `/commit/${commitId}`;\n        } else if (commitId && path) {\n            s += `/tree/${commitId}/${normalizePath(path)}`;\n        } else if (!commitId && branch && !path) {\n            s += `/tree/${branch}`;\n        } else if (!commitId && path && branch) {\n            s += `/tree/${branch}/${normalizePath(path)}`;\n        }\n\n        return (\n            <a href={s} target=\"_blank\" rel=\"noopener noreferrer\">\n                {text ? text : s} <Icon name=\"external\" />\n            </a>\n        );\n    }\n}\n\nexport default GitHubLink;\n"
  },
  {
    "path": "console2/src/components/molecules/GlobalNavMenu/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Link } from 'react-router';\nimport { Dropdown, Image, Menu } from 'semantic-ui-react';\nimport { CustomResources, LinkMeta } from '../../../../cfg';\n\nexport type GlobalNavTab = 'activity' | 'process' | 'org' | 'noderoster' | null;\n\ninterface Props {\n    activeTab: GlobalNavTab;\n    userDisplayName?: string;\n    openUrl: (url: string) => void;\n    extraSystemLinks: LinkMeta[];\n    openAbout: () => void;\n    openProfile: () => void;\n    customResources: CustomResources;\n    openCustomResource: (name: string) => void;\n    logOut: () => void;\n}\n\nclass GlobalNavMenu extends React.PureComponent<Props> {\n    render() {\n        const {\n            activeTab,\n            userDisplayName,\n            extraSystemLinks,\n            openUrl,\n            openAbout,\n            openProfile,\n            customResources,\n            openCustomResource,\n            logOut,\n        } = this.props;\n\n        return (\n            <Menu fluid={true} inverted={true} size=\"small\" secondary={true}>\n                <Menu.Item>\n                    <Image id=\"concordLogo\" src=\"/images/logo.svg\" size=\"small\" />\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'activity'}>\n                    <Link to=\"/activity\">Activity</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'process'}>\n                    <Link to=\"/process\">Processes</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'org'}>\n                    <Link to=\"/org\">Organizations</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'noderoster'}>\n                    <Link to=\"/noderoster\">Node Roster</Link>\n                </Menu.Item>\n                <Menu.Item position=\"right\" fitted=\"vertically\">\n                    <Menu inverted={true} size=\"small\" secondary={true}>\n                        <Menu.Item as={Dropdown} text=\"System\" data-testid=\"topbar-system-menu\">\n                            <Dropdown.Menu>\n                                <Dropdown.Item\n                                    icon=\"info\"\n                                    text=\"About\"\n                                    data-testid=\"topbar-about\"\n                                    onClick={() => openAbout()}\n                                />\n\n                                {extraSystemLinks.map((x, idx) => (\n                                    <Dropdown.Item\n                                        key={`extra_${idx}`}\n                                        icon={x.icon}\n                                        text={x.text}\n                                        onClick={() => openUrl(x.url)}\n                                    />\n                                ))}\n\n                                {Object.keys(customResources).map((k, idx) => {\n                                    const r = customResources[k];\n                                    return (\n                                        <Dropdown.Item\n                                            key={`custom_${idx}`}\n                                            icon={r.icon}\n                                            text={r.title}\n                                            onClick={() => openCustomResource(k)}\n                                        />\n                                    );\n                                })}\n                            </Dropdown.Menu>\n                        </Menu.Item>\n                        <Menu.Item\n                            as={Dropdown}\n                            text={userDisplayName}\n                            data-testid=\"topbar-user-menu\"\n                        >\n                            {/* TODO can't add an icon here */}\n                            <Dropdown.Menu>\n                                <Dropdown.Item\n                                    icon=\"setting\"\n                                    text=\"Profile\"\n                                    data-testid=\"topbar-profile\"\n                                    onClick={() => openProfile()}\n                                />\n                                <Dropdown.Item\n                                    icon=\"log out\"\n                                    text=\"Log out\"\n                                    onClick={() => logOut()}\n                                />\n                            </Dropdown.Menu>\n                        </Menu.Item>\n                    </Menu>\n                </Menu.Item>\n            </Menu>\n        );\n    }\n}\n\nexport default GlobalNavMenu;\n"
  },
  {
    "path": "console2/src/components/molecules/Highlighter/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { highlight } from '../../../utils';\n\ninterface HighlighterProps {\n    config: Config[];\n    value: string;\n    caseInsensitive?: boolean;\n    global?: boolean;\n}\n\ninterface Config {\n    string: string;\n    style: string;\n    divide?: boolean;\n}\n\nclass Highlighter extends React.PureComponent<HighlighterProps> {\n    render() {\n        const { value } = this.props;\n\n        const txt = highlight(value, this.props);\n\n        return <div dangerouslySetInnerHTML={{ __html: txt }} />;\n    }\n}\nexport default Highlighter;\n"
  },
  {
    "path": "console2/src/components/molecules/HumanizedDuration/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { Popup } from 'semantic-ui-react';\n\nimport { formatDuration } from '../../../utils';\n\ninterface Props {\n    value?: number;\n    hint?: string;\n    children?: React.ReactNode;\n}\n\nfunction HintFormat({ value, hint }: Props) {\n    if (hint) {\n        return <>\n            {value}ms\n            <div>({hint})</div>\n        </>\n    }\n\n    return <>{value}ms</>;\n}\n\nexport default class extends React.PureComponent<Props> {\n    render() {\n        const {value, hint, children} = this.props;\n\n        const s = formatDuration(value);\n        if (!s) {\n            return '-';\n        }\n\n        return (\n            <Popup trigger={<span>{s}</span>} hoverable={true}>\n                <Popup.Content>\n                    {children ? children : <HintFormat value={value} hint={hint}/>}\n                </Popup.Content>\n            </Popup>\n        );\n    }\n}"
  },
  {
    "path": "console2/src/components/molecules/LoadingEditor/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\n\nimport { Loader } from 'semantic-ui-react';\nimport Editor from '@monaco-editor/react';\nimport { useCallback } from 'react';\n\ninterface LoadingEditorProps {\n    language?: string;\n    handleEditorDidMount: (getEditorValue: () => string) => void;\n    initValue?: string;\n    disabled: boolean;\n}\n\nconst LoadingEditor = ({\n    language,\n    handleEditorDidMount,\n    initValue,\n    disabled\n}: LoadingEditorProps) => {\n    const onEditorDidMount = useCallback(\n        (editor) => {\n            handleEditorDidMount(() => editor.getValue());\n        },\n        [handleEditorDidMount]\n    );\n\n    if (!initValue) {\n        return <Loader active={true} />;\n    }\n\n    return (\n        <Editor\n            language={language}\n            onMount={onEditorDidMount}\n            value={initValue}\n            options={{ lineNumbers: 'on', minimap: { enabled: false }, readOnly: disabled }}\n        />\n    );\n};\n\nexport default LoadingEditor;\n"
  },
  {
    "path": "console2/src/components/molecules/LocalTimestamp/__test__/LocalTimestamp.test.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { render } from '@testing-library/react';\nimport React from 'react';\nimport LocalTimeStamp from '..';\n\ntest('Accepts date input of 2018-02-05 19:03:19', () => {\n    const { container } = render(<LocalTimeStamp value={'2018-02-05 19:03:19'} />);\n    expect(container.innerHTML).toContain('2018-02-05 19:03:19');\n});\n\ntest('Renders a message if date format is invalid', () => {\n    const { container } = render(<LocalTimeStamp value={' '} />);\n    expect(container.innerHTML).toContain('Invalid Date');\n});\n\ntest('Renders a message if bad date value is provided', () => {\n    const { container } = render(<LocalTimeStamp value={''} />);\n    expect(container.innerHTML).toContain('Bad date value');\n});\n"
  },
  {
    "path": "console2/src/components/molecules/LocalTimestamp/__tests__/LocalTimestamp.test.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { render } from '@testing-library/react';\nimport React from 'react';\nimport LocalTimeStamp from '..';\n\ntest('Accepts date input of 2018-02-05 19:03:19', () => {\n    const { container } = render(<LocalTimeStamp value={'2018-02-05 19:03:19'} />);\n    expect(container.innerHTML).toContain('2018-02-05 19:03:19');\n});\n\ntest('Renders a message if date format is invalid', () => {\n    const { container } = render(<LocalTimeStamp value={' '} />);\n    expect(container.innerHTML).toContain('Invalid Date');\n});\n\ntest('Renders a message if bad date value is provided', () => {\n    const { container } = render(<LocalTimeStamp value={''} />);\n    expect(container.innerHTML).toContain('Bad date value');\n});\n"
  },
  {
    "path": "console2/src/components/molecules/LocalTimestamp/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Popup } from 'semantic-ui-react';\n\nimport { formatTimestamp } from '../../../utils';\n\ninterface Props {\n    value?: string;\n}\n\nexport default class extends React.PureComponent<Props> {\n    render() {\n        const { value } = this.props;\n\n        if (!value) {\n            return <div>Bad date value provided</div>;\n        }\n\n        return (\n            <Popup trigger={<span>{formatTimestamp(value)}</span>} hoverable={true}>\n                Browser time. Original value: {value}\n            </Popup>\n        );\n    }\n}\n"
  },
  {
    "path": "console2/src/components/molecules/LogSegment/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { Button, Icon, SemanticCOLORS, SemanticICONS } from 'semantic-ui-react';\nimport {\n    formatDistance,\n    formatDuration,\n    intervalToDuration,\n    parseISO as parseDate,\n} from 'date-fns';\nimport { Link, useLocation } from 'react-router';\n\nimport { SegmentStatus } from '../../../api/process/log';\nimport { ConcordId } from '../../../api/common';\nimport {\n    getStatusSemanticColor,\n    getStatusSemanticIcon,\n    isFinal,\n    ProcessStatus,\n} from '../../../api/process';\n\nimport './styles.css';\n\ninterface Props {\n    instanceId: ConcordId;\n    segmentId: number;\n    name: string;\n    createdAt: string;\n    processStatus?: ProcessStatus;\n    status?: SegmentStatus;\n    statusUpdatedAt?: string;\n    lowRange?: number;\n    warnings?: number;\n    errors?: number;\n    data: string[];\n    onStartLoading: (isLoadWholeLog: boolean) => void;\n    onStopLoading: () => void;\n    onSegmentInfo?: () => void;\n    loading: boolean;\n    forceOpen: boolean;\n}\n\nconst LogSegment = ({\n    instanceId,\n    segmentId,\n    name,\n    createdAt,\n    processStatus,\n    status,\n    statusUpdatedAt,\n    lowRange,\n    warnings,\n    errors,\n    data,\n    onStartLoading,\n    onStopLoading,\n    onSegmentInfo,\n    loading,\n    forceOpen,\n}: Props) => {\n    const scrollAnchorRef = useRef<HTMLDivElement>(null);\n    const location = useLocation();\n    const [isOpen, setOpen] = useState<boolean>(forceOpen);\n    const [isLoadAll, setLoadAll] = useState<boolean>(false);\n    const [isAutoScroll, setAutoScroll] = useState<boolean>(false);\n\n    const baseUrl = `/process/${instanceId}/log`;\n\n    const myRef = useRef<null | HTMLDivElement>(null);\n\n    useEffect(() => {\n        if (myRef.current && location.hash.includes(`#segmentId=${segmentId}`)) {\n            myRef.current.scrollIntoView({\n                behavior: 'smooth',\n                block: 'end',\n                inline: 'nearest',\n            });\n            setOpen(true);\n        }\n    }, [myRef, segmentId, location]);\n\n    const loadAllClickHandler = useCallback((ev: React.MouseEvent<any>) => {\n        ev.preventDefault();\n        ev.stopPropagation();\n        setLoadAll((prevState) => !prevState);\n    }, []);\n\n    const segmentInfoClickHandler = useCallback(\n        (event: React.MouseEvent<any>) => {\n            event.stopPropagation();\n            if (onSegmentInfo !== undefined) {\n                onSegmentInfo();\n            }\n        },\n        [onSegmentInfo]\n    );\n\n    const autoscrollClickHandler = useCallback((ev: React.MouseEvent<any>) => {\n        ev.preventDefault();\n        ev.stopPropagation();\n        setAutoScroll((prevState) => !prevState);\n    }, []);\n\n    useEffect(() => {\n        setOpen(forceOpen);\n    }, [forceOpen]);\n\n    useEffect(() => {\n        if (isOpen) {\n            onStartLoading(isLoadAll);\n        } else {\n            onStopLoading();\n        }\n    }, [isOpen, isLoadAll, name, onStartLoading, onStopLoading]);\n\n    useEffect(() => {\n        if (isAutoScroll && scrollAnchorRef.current !== null) {\n            scrollAnchorRef.current.scrollIntoView();\n        }\n    }, [isAutoScroll, data]);\n\n    const hasWarnings = !!(warnings && warnings > 0);\n    const hasErrors = !!(errors && errors > 0);\n\n    const createdAtDate = parseDate(createdAt);\n\n    let beenRunningFor;\n    if (status === SegmentStatus.RUNNING && !isFinal(processStatus)) {\n        beenRunningFor = formatDistance(new Date(), createdAtDate);\n    }\n\n    let wasRunningFor;\n    if (status !== SegmentStatus.RUNNING && statusUpdatedAt) {\n        const statusUpdatedAtDate = parseDate(statusUpdatedAt);\n        wasRunningFor = formatDuration(\n            intervalToDuration({\n                start: createdAtDate,\n                end: statusUpdatedAtDate,\n            })\n        );\n    }\n\n    return (\n        <div className=\"LogSegment\" id={`segmentId=${segmentId}`} ref={myRef}>\n            <Button\n                fluid={true}\n                size=\"medium\"\n                className=\"Segment\"\n                onClick={() => setOpen((prevState) => !prevState)}\n            >\n                <Icon name={isOpen ? 'caret down' : 'caret right'} className=\"State\" />\n\n                <StatusIcon\n                    status={status}\n                    processStatus={processStatus}\n                    warnings={warnings}\n                    errors={errors}\n                />\n\n                <span className=\"Caption\">{name}</span>\n\n                {(hasWarnings || hasErrors) && (\n                    <>\n                        <span className=\"Counter\">warn: {warnings ? warnings : 0}</span>\n                        <span className=\"Counter\">error: {errors ? errors : 0}</span>\n                    </>\n                )}\n\n                {beenRunningFor && <span className=\"RunningFor\">running for {beenRunningFor}</span>}\n                {wasRunningFor && <span className=\"RunningFor\">{wasRunningFor}</span>}\n\n                <Link\n                    to={`${baseUrl}#segmentId=${segmentId}`}\n                    className=\"AdditionalAction Anchor\"\n                    data-tooltip=\"Hyperlink\"\n                    data-inverted=\"\"\n                >\n                    <Icon name=\"linkify\" />\n                </Link>\n\n                <a\n                    href={`/api/v2/process/${instanceId}/log/segment/${segmentId}/data`}\n                    onClick={(event) => event.stopPropagation()}\n                    rel=\"noopener noreferrer\"\n                    target=\"_blank\"\n                    className=\"AdditionalAction Last\"\n                    data-tooltip=\"Download: InstanceId_SegmentId.log\"\n                    data-inverted=\"\"\n                >\n                    <Icon name=\"download\" />\n                </a>\n\n                {onSegmentInfo !== undefined && (\n                    <div className={'AdditionalAction'} data-tooltip=\"Show Info\" data-inverted=\"\">\n                        <Icon\n                            name={'info circle'}\n                            title={'Show info'}\n                            onClick={segmentInfoClickHandler}\n                        />\n                    </div>\n                )}\n\n                {isOpen && (\n                    <>\n                        <div className=\"AdditionalAction\">\n                            <div\n                                className={isAutoScroll ? 'on' : 'off'}\n                                data-tooltip=\"Auto Scroll\"\n                                data-inverted=\"\"\n                            >\n                                <Icon name={'angle double down'} onClick={autoscrollClickHandler} />\n                            </div>\n                        </div>\n                        <div className=\"AdditionalAction\">\n                            <div\n                                className={isLoadAll ? 'on' : 'off'}\n                                data-tooltip=\"Show Full Log\"\n                                data-inverted=\"\"\n                            >\n                                <Icon\n                                    name={'arrows alternate vertical'}\n                                    onClick={loadAllClickHandler}\n                                />\n                            </div>\n                        </div>\n                    </>\n                )}\n                {loading && <div className=\"Loader\" />}\n            </Button>\n\n            {isOpen && (\n                <div className=\"ContentContainer\">\n                    <div className=\"InnerContentContainer\">\n                        <div className=\"Content\">\n                            {lowRange !== undefined && lowRange !== 0 && (\n                                <>\n                                    <span>...showing only the last few lines. </span>\n                                    {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}\n                                    <a href=\"#\" onClick={loadAllClickHandler}>\n                                        Full log\n                                    </a>{' '}\n                                </>\n                            )}\n\n                            {data.map((value, index) => (\n                                <pre key={index} dangerouslySetInnerHTML={{ __html: value }} />\n                            ))}\n                        </div>\n                        <div ref={scrollAnchorRef} />\n                    </div>\n                </div>\n            )}\n        </div>\n    );\n};\n\ninterface StatusIconProps {\n    status?: SegmentStatus;\n    processStatus?: ProcessStatus;\n    loading?: boolean;\n    warnings?: number;\n    errors?: number;\n}\n\nconst StatusIcon = ({ status, processStatus, warnings = 0, errors = 0 }: StatusIconProps) => {\n    if (!status) {\n        return (\n            <Icon\n                loading={\n                    processStatus !== ProcessStatus.SUSPENDED &&\n                    processStatus !== ProcessStatus.FAILED &&\n                    processStatus !== ProcessStatus.FINISHED &&\n                    processStatus !== ProcessStatus.CANCELLED &&\n                    processStatus !== ProcessStatus.TIMED_OUT\n                }\n                name={processStatus ? getStatusSemanticIcon(processStatus) : 'circle'}\n                color={processStatus ? getStatusSemanticColor(processStatus) : 'grey'}\n                className=\"Status\"\n            />\n        );\n    }\n\n    let color: SemanticCOLORS = 'green';\n    let icon: SemanticICONS = 'circle';\n    let spinning = false;\n\n    if (status === SegmentStatus.RUNNING && isFinal(processStatus)) {\n        color = 'yellow';\n        icon = 'question circle';\n    } else if (status === SegmentStatus.RUNNING) {\n        color = 'teal';\n        icon = 'spinner';\n        spinning = true;\n    } else if (status === SegmentStatus.SUSPENDED) {\n        color = 'blue';\n        icon = 'hourglass half';\n    } else if (status === SegmentStatus.FAILED) {\n        color = 'red';\n        icon = 'close';\n    } else if (warnings > 0) {\n        color = 'orange';\n        icon = 'exclamation circle';\n    } else if (errors > 0) {\n        color = 'red';\n        icon = 'exclamation circle';\n    }\n    return <Icon loading={spinning} name={icon} color={color} className=\"Status\" />;\n};\n\nexport default LogSegment;\n"
  },
  {
    "path": "console2/src/components/molecules/LogSegment/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n.LogSegment {\n    color: rgb(22, 22, 22);\n}\n\n.LogSegment .Segment {\n    text-align: left;\n    background-color: white;\n    position: sticky;\n    top: 57px;\n}\n\n.LogSegment .Segment:focus {\n    background: white none;\n}\n\n.LogSegment .Segment:hover {\n    background-color: rgb(230, 230, 230);\n    border-radius: 3px;\n}\n\n.LogSegment .Loader {\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 0;\n    height: 100%;\n    background: #2a2929;\n    animation: progress-active 3s ease infinite;\n    animation-delay: 100ms;\n}\n\n.LogSegment .Segment .Caption {\n    padding-left: 10px;\n    font-family: monospace;\n    font-weight: normal;\n    color: rgba(43, 43, 43, 0.9);\n}\n\n.LogSegment .Segment .State {\n    margin: 0 !important;\n    padding-right: 20px;\n}\n\n.LogSegment .Segment .Status {\n    margin: 0 !important;\n}\n\n.LogSegment .Segment .EmptyStatus {\n    margin: 0;\n    width: 1.18em;\n    display: inline-block;\n}\n\n.LogSegment .Segment .AdditionalAction {\n    margin-top: 0 !important;\n    float: right;\n    padding-right: 10px;\n}\n\n.LogSegment .Segment .AdditionalAction.Last {\n    padding-right: 0;\n    color: #4183c4;\n}\n\n.LogSegment .Segment .AdditionalAction.Anchor {\n    padding-right: 0;\n    padding-left: 10px;\n    color: #4183c4;\n}\n\n.LogSegment .Segment .AdditionalAction .on {\n    color: #4183c4;\n}\n\n.LogSegment .Segment .AdditionalAction .off {\n    color: #767676;\n}\n\n.LogSegment .Segment .AdditionalAction .off:hover {\n    color: #000000;\n}\n\n.LogSegment .Segment .AdditionalAction i:hover{\n    color: rgb(43, 43, 43) !important;\n}\n\n.LogSegment .ContentContainer {\n    padding: 0 0 0 40px;\n}\n\n.LogSegment .InnerContentContainer {\n    overflow: auto;\n    min-height: 12px;\n}\n\n.LogSegment .Loading {\n    margin: 0;\n    padding: 10px 0 10px 10px;\n    color: rgb(241, 241, 241);\n    background: rgb(43, 43, 43);\n}\n\n.LogSegment .Content {\n    color: rgb(43, 43, 43);\n    font-family: monospace;\n    line-height: 18px;\n    background: rgb(255, 255, 255);\n    margin: 0;\n    display: inline-block;\n    min-width: 100%;\n}\n\n.LogSegment .Content pre {\n    margin: 0;\n    white-space: pre-wrap;\n}\n\n.LogSegment .Content a {\n    color: #00B5F0;\n    text-decoration: underline;\n}\n\n.LogSegment .Counter {\n    font-weight: normal;\n    color: #888888;\n    margin-left: 10px;\n}\n\n.LogSegment .RunningFor {\n    font-weight: normal;\n    color: #888888;\n    margin-left: 10px;\n}\n"
  },
  {
    "path": "console2/src/components/molecules/MainToolbar/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { Icon, Menu, MenuItem, Sticky } from 'semantic-ui-react';\nimport { memo, useCallback, useState } from 'react';\n\nimport './styles.css';\n\ninterface ExternalProps {\n    stickyRef: any;\n    loading?: boolean;\n    refresh?: () => void;\n    breadcrumbs: React.ReactNode;\n}\n\nconst MainToolbar = memo((props: ExternalProps) => {\n    const { stickyRef, loading, refresh, breadcrumbs } = props;\n\n    const [isFixed, setFixed] = useState(false);\n\n    const onStick = useCallback(() => {\n        setFixed(false);\n    }, []);\n\n    const onUnstick = useCallback(() => {\n        setFixed(true);\n    }, []);\n\n    return (\n        <Sticky context={stickyRef} onStick={onStick} onUnstick={onUnstick}>\n            <Menu\n                tabular={false}\n                secondary={true}\n                borderless={true}\n                className={isFixed ? 'mainToolbar' : 'mainToolbar unfixed'}>\n                {loading !== undefined && refresh !== undefined && (\n                    <MenuItem>\n                        <Icon name=\"refresh\" loading={loading} size={'large'} onClick={refresh} />\n                    </MenuItem>\n                )}\n\n                <MenuItem>{breadcrumbs}</MenuItem>\n            </Menu>\n        </Sticky>\n    );\n});\n\nexport default MainToolbar;\n"
  },
  {
    "path": "console2/src/components/molecules/MainToolbar/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n.mainToolbar {\n    padding-top: 1rem !important;\n    padding-bottom: 1rem !important;\n}\n\n.mainToolbar.unfixed {\n    background: white !important;\n    border: 0 !important;\n    border-bottom: 1px solid rgba(34,36,38,.15) !important;\n    box-shadow: none !important;\n}\n\n.mainToolbar .item {\n    padding: 0 !important;\n}\n\n.mainToolbar .button {\n    font-size: .92857143rem !important;\n    padding: .58928571em 1.125em .58928571em !important;\n}\n"
  },
  {
    "path": "console2/src/components/molecules/NewAPITokenForm/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { InjectedFormikProps, withFormik } from 'formik';\nimport * as React from 'react';\nimport { Button, Form } from 'semantic-ui-react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { NewTokenEntry } from '../../../api/profile/api_token';\nimport { isApiTokenExists } from '../../../api/service/console';\nimport { notEmpty } from '../../../utils';\nimport { apiTokenAlreadyExistsError, secret as validation } from '../../../validation';\nimport { FormikInput } from '../../atoms';\n\ninterface FormValues {\n    name: ConcordKey;\n}\n\nexport type NewAPITokenFormValues = FormValues;\n\ninterface Props {\n    initial: FormValues;\n    onSubmit: (values: NewTokenEntry) => void;\n    submitting: boolean;\n}\n\nclass NewAPITokenForm extends React.Component<InjectedFormikProps<Props, FormValues>> {\n    render() {\n        const { submitting, handleSubmit, errors, dirty } = this.props;\n\n        const hasErrors = notEmpty(errors);\n\n        return (\n            <Form onSubmit={handleSubmit} loading={submitting}>\n                <FormikInput\n                    name=\"name\"\n                    label=\"Name\"\n                    placeholder=\"Token name\"\n                    required={true}\n                    data-testid=\"api-token-form-name\"\n                />\n\n                <Button\n                    primary={true}\n                    type=\"submit\"\n                    disabled={!dirty || hasErrors}\n                    data-testid=\"api-token-form-submit\"\n                >\n                    Generate\n                </Button>\n            </Form>\n        );\n    }\n}\n\nconst validator = async (values: FormValues) => {\n    let e;\n\n    e = validation.name(values.name);\n    if (e) {\n        return Promise.resolve({ name: e });\n    }\n\n    const exists = await isApiTokenExists(values.name);\n    if (exists) {\n        return Promise.resolve({ name: apiTokenAlreadyExistsError(values.name) });\n    }\n\n    return {};\n};\n\nexport default withFormik<Props, FormValues>({\n    handleSubmit: (values, bag) => {\n        bag.props.onSubmit(values);\n    },\n    mapPropsToValues: (props) => props.initial,\n    validate: validator,\n})(NewAPITokenForm);\n"
  },
  {
    "path": "console2/src/components/molecules/NewProjectForm/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { InjectedFormikProps, withFormik } from 'formik';\nimport * as React from 'react';\nimport { Divider, Form } from 'semantic-ui-react';\nimport { ConcordKey } from '../../../api/common';\nimport { ProjectVisibility } from '../../../api/org/project';\nimport { isProjectExists } from '../../../api/service/console';\nimport { notEmpty } from '../../../utils';\nimport { project as validation, projectAlreadyExistsError } from '../../../validation';\nimport { FormikDropdown, FormikInput } from '../../atoms';\n\ninterface FormValues {\n    name: string;\n    visibility: ProjectVisibility;\n    description?: string;\n}\n\nexport type NewProjectFormValues = FormValues;\n\ninterface Props {\n    orgName: ConcordKey;\n    initial: FormValues;\n    submitting: boolean;\n    onSubmit: (values: FormValues) => void;\n}\n\nconst visibilityOptions = [\n    {\n        text: 'Public',\n        value: ProjectVisibility.PUBLIC,\n        description: 'Any user can start a process using a public project.',\n        icon: 'unlock'\n    },\n    {\n        text: 'Private',\n        value: ProjectVisibility.PRIVATE,\n        description: \"Private projects can be used only by their organization's teams.\",\n        icon: 'lock'\n    }\n];\n\nclass NewProjectForm extends React.PureComponent<InjectedFormikProps<Props, FormValues>> {\n    render() {\n        const { handleSubmit, submitting } = this.props;\n\n        const hasErrors = notEmpty(this.props.errors);\n\n        return (\n            <Form onSubmit={handleSubmit} loading={submitting}>\n                <FormikInput name=\"name\" label=\"Name\" placeholder=\"Project name\" required={true} />\n\n                <FormikDropdown\n                    name=\"visibility\"\n                    label=\"Visibility\"\n                    selection={true}\n                    options={visibilityOptions}\n                />\n\n                <FormikInput\n                    name=\"description\"\n                    label=\"Description\"\n                    placeholder=\"Project description\"\n                />\n\n                <Divider />\n\n                <Form.Button primary={true} type=\"submit\" disabled={hasErrors}>\n                    Create\n                </Form.Button>\n            </Form>\n        );\n    }\n}\n\nconst validator = async (values: FormValues, props: Props) => {\n    let e;\n\n    e = validation.name(values.name);\n    if (e) {\n        return Promise.resolve({ name: e });\n    }\n\n    const exists = await isProjectExists(props.orgName, values.name);\n    if (exists) {\n        return Promise.resolve({ name: projectAlreadyExistsError(values.name) });\n    }\n\n    e = validation.description(values.description);\n    if (e) {\n        return Promise.resolve({ description: e });\n    }\n\n    return {};\n};\n\nexport default withFormik<Props, FormValues>({\n    handleSubmit: (values, bag) => {\n        bag.props.onSubmit(values);\n    },\n    mapPropsToValues: (props) => props.initial,\n    validate: validator\n})(NewProjectForm);\n"
  },
  {
    "path": "console2/src/components/molecules/NewSecretForm/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { InjectedFormikProps, withFormik } from 'formik';\nimport * as React from 'react';\nimport { Button, Divider, Form, Segment } from 'semantic-ui-react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { ProjectEntry } from '../../../api/org/project';\nimport {\n    NewSecretEntry,\n    SecretStoreType,\n    SecretTypeExt,\n    SecretVisibility\n} from '../../../api/org/secret';\nimport { isSecretExists } from '../../../api/service/console';\nimport { notEmpty } from '../../../utils';\nimport { secret as validation, secretAlreadyExistsError } from '../../../validation';\nimport { FormikDropdown, FormikFileInput, FormikInput } from '../../atoms';\nimport { ProjectSearchFormField, SecretStoreDropdown } from '../../organisms';\n\nenum StorePasswordType {\n    DONT_USE,\n    SPECIFY,\n    GENERATE\n}\n\ninterface FormValues {\n    name: string;\n    visibility: SecretVisibility;\n    type: SecretTypeExt;\n    publicFile?: File;\n    privateFile?: File;\n    username?: string;\n    password?: string;\n    valueString?: string;\n    valueFile?: File;\n    storePasswordType?: StorePasswordType;\n    storePassword?: string;\n    storeType?: SecretStoreType;\n    projects?: ProjectEntry[];\n}\n\nexport type NewSecretFormValues = FormValues;\n\ninterface Props {\n    orgName: ConcordKey;\n    initial: FormValues;\n    onSubmit: (values: NewSecretEntry) => void;\n    submitting: boolean;\n}\n\nconst visibilityOptions = [\n    {\n        text: 'Public',\n        value: SecretVisibility.PUBLIC,\n        description: 'Public secrets can be used by any user.',\n        icon: 'unlock'\n    },\n    {\n        text: 'Private',\n        value: SecretVisibility.PRIVATE,\n        description: 'Private secrets can be used only by the specified teams.',\n        icon: 'lock'\n    }\n];\n\nconst typeOptions = [\n    { text: 'Generate a new key pair', value: SecretTypeExt.NEW_KEY_PAIR },\n    { text: 'Existing key pair', value: SecretTypeExt.EXISTING_KEY_PAIR },\n    { text: 'Username/password', value: SecretTypeExt.USERNAME_PASSWORD },\n    { text: 'Single value (string)', value: SecretTypeExt.VALUE_STRING },\n    { text: 'Single value (file)', value: SecretTypeExt.VALUE_FILE }\n];\n\nconst pwdTypeOptions = [\n    {\n        text: \"N/A (encrypt using the server's key)\",\n        value: StorePasswordType.DONT_USE\n    },\n    { text: 'Specify a password', value: StorePasswordType.SPECIFY },\n    { text: 'Generate a new password', value: StorePasswordType.GENERATE }\n];\n\nclass NewSecretForm extends React.Component<InjectedFormikProps<Props, FormValues>> {\n    render() {\n        const { submitting, handleSubmit, errors, dirty, values, orgName } = this.props;\n\n        const hasErrors = notEmpty(errors);\n\n        return (\n            <Form onSubmit={handleSubmit} loading={submitting}>\n                <FormikInput name=\"name\" label=\"Name\" placeholder=\"Secret name\" required={true} />\n\n                <FormikDropdown\n                    name=\"visibility\"\n                    label=\"Visibility\"\n                    required={true}\n                    selection={true}\n                    options={visibilityOptions}\n                />\n\n                <FormikDropdown\n                    name=\"type\"\n                    label=\"Type\"\n                    required={true}\n                    selection={true}\n                    options={typeOptions}\n                />\n\n                {values.type === SecretTypeExt.EXISTING_KEY_PAIR && (\n                    <Form.Group widths=\"equal\">\n                        <FormikFileInput name=\"publicFile\" label=\"Public key\" required={true} />\n\n                        <FormikFileInput name=\"privateFile\" label=\"Private key\" required={true} />\n                    </Form.Group>\n                )}\n\n                {values.type === SecretTypeExt.USERNAME_PASSWORD && (\n                    <Form.Group widths=\"equal\">\n                        <FormikInput name=\"username\" label=\"Username\" required={true} />\n\n                        <FormikInput\n                            name=\"password\"\n                            label=\"Password\"\n                            required={true}\n                            type=\"password\"\n                            autoComplete=\"off\"\n                        />\n                    </Form.Group>\n                )}\n\n                {values.type === SecretTypeExt.VALUE_STRING && (\n                    <FormikInput\n                        name=\"valueString\"\n                        label=\"Value\"\n                        required={true}\n                        autoComplete=\"off\"\n                    />\n                )}\n\n                {values.type === SecretTypeExt.VALUE_FILE && (\n                    <FormikFileInput name=\"valueFile\" label=\"Value\" required={true} />\n                )}\n\n                <FormikDropdown\n                    name=\"storePasswordType\"\n                    label=\"Store password\"\n                    selection={true}\n                    options={pwdTypeOptions}\n                />\n\n                {values.storePasswordType === StorePasswordType.SPECIFY && (\n                    <FormikInput\n                        name=\"storePassword\"\n                        label=\"Store password\"\n                        type=\"password\"\n                        required={true}\n                        autoComplete=\"off\"\n                    />\n                )}\n\n                <SecretStoreDropdown name=\"storeType\" label=\"Store type\" required={true} />\n\n                <Divider />\n\n                <ProjectSearchFormField\n                    orgName={orgName}\n                    fieldName={'projects'}\n                    label=\"Projects\"\n                    placeholder=\"any\"\n                />\n\n                <Segment secondary={true} basic={true}>\n                    <p>\n                        Project-scoped secrets can only be used in the processes of specified\n                        projects. They cannot be reused for multiple projects.\n                    </p>\n\n                    <p>\n                        Secrets not linked to any project can be used anywhere. Standard permission\n                        checks are applied in both cases.\n                    </p>\n                </Segment>\n\n                <Divider />\n\n                <Button primary={true} type=\"submit\" disabled={!dirty || hasErrors}>\n                    Create\n                </Button>\n            </Form>\n        );\n    }\n}\n\nconst validator = async (values: FormValues, props: Props): Promise<{}> => {\n    let e;\n\n    e = validation.name(values.name);\n    if (e) {\n        return Promise.resolve({ name: e });\n    }\n\n    const exists = await isSecretExists(props.orgName, values.name);\n    if (exists) {\n        return Promise.resolve({ name: secretAlreadyExistsError(values.name) });\n    }\n\n    switch (values.type) {\n        case SecretTypeExt.EXISTING_KEY_PAIR: {\n            e = validation.publicFile(values.publicFile);\n            if (e) {\n                return Promise.resolve({ publicFile: e });\n            }\n\n            e = validation.privateFile(values.privateFile);\n            if (e) {\n                return Promise.resolve({ privateFile: e });\n            }\n\n            break;\n        }\n        case SecretTypeExt.USERNAME_PASSWORD: {\n            e = validation.username(values.username);\n            if (e) {\n                return Promise.resolve({ username: e });\n            }\n\n            e = validation.password(values.password);\n            if (e) {\n                return Promise.resolve({ password: e });\n            }\n\n            break;\n        }\n        case SecretTypeExt.VALUE_STRING: {\n            e = validation.valueString(values.valueString);\n            if (e) {\n                return Promise.resolve({ valueString: e });\n            }\n\n            break;\n        }\n        case SecretTypeExt.VALUE_FILE: {\n            e = validation.valueFile(values.valueFile);\n            if (e) {\n                return Promise.resolve({ valueFile: e });\n            }\n\n            break;\n        }\n        default:\n            break;\n    }\n\n    if (values.storePasswordType === StorePasswordType.SPECIFY) {\n        e = validation.storePassword(values.storePassword);\n        if (e) {\n            return Promise.resolve({ storePassword: e });\n        }\n    }\n\n    return {};\n};\n\nexport default withFormik<Props, FormValues>({\n    handleSubmit: (values, bag) => {\n        const entry: NewSecretEntry = values;\n\n        switch (values.storePasswordType) {\n            case StorePasswordType.DONT_USE: {\n                entry.generatePassword = false;\n                entry.storePassword = undefined;\n                break;\n            }\n            case StorePasswordType.SPECIFY: {\n                entry.generatePassword = false;\n                break;\n            }\n            case StorePasswordType.GENERATE: {\n                entry.generatePassword = true;\n                entry.storePassword = undefined;\n                break;\n            }\n            default: {\n                break;\n            }\n        }\n\n        bag.props.onSubmit(entry);\n    },\n    mapPropsToValues: (props) => ({\n        storePasswordType: StorePasswordType.DONT_USE,\n        ...props.initial\n    }),\n    validate: validator\n})(NewSecretForm);\n"
  },
  {
    "path": "console2/src/components/molecules/NewStorageForm/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useEffect } from 'react';\nimport { Dropdown, Form, Input, Label } from 'semantic-ui-react';\nimport { useForm, type ValidateResult } from 'react-hook-form';\n\nimport { ConcordKey } from '../../../api/common';\nimport { StorageVisibility } from '../../../api/org/jsonstore';\nimport { storage as validation, jsonStoreAlreadyExistsError } from '../../../validation';\nimport { isStorageExists } from '../../../api/service/console';\n\ninterface FormValues {\n    name: string;\n    visibility: StorageVisibility;\n}\n\nexport type NewStorageFormValues = FormValues;\n\nexport interface Props {\n    orgName: ConcordKey;\n    initial: FormValues;\n    submitting: boolean;\n    onSubmit: (values: FormValues) => void;\n}\n\nconst visibilityOptions = [\n    {\n        text: 'Public',\n        value: StorageVisibility.PUBLIC,\n        icon: 'unlock'\n    },\n    {\n        text: 'Private',\n        value: StorageVisibility.PRIVATE,\n        icon: 'lock'\n    }\n];\n\nconst NewStoreForm = ({ orgName, onSubmit, submitting, initial }: Props) => {\n    const {\n        register,\n        handleSubmit,\n        formState: { errors },\n        setValue\n    } = useForm<FormValues>({\n        defaultValues: initial\n    });\n\n    useEffect(() => {\n        register('name', { required: true, validate: (data) => validateName(orgName, data) });\n        register('visibility', { required: true });\n    }, [orgName, register]);\n\n    return (\n        <Form onSubmit={handleSubmit((data) => onSubmit(data))} loading={submitting}>\n            <Form.Field name=\"name\" required={true}>\n                <label>Store name</label>\n                <Input\n                    name=\"name\"\n                    onChange={(event) => setValue('name', event.target.value)}\n                    error={!!errors.name}\n                />\n                {errors.name && (\n                    <Label basic={true} pointing={true} color=\"red\">\n                        {errors.name.message ? errors.name.message : 'Name is required'}\n                    </Label>\n                )}\n            </Form.Field>\n\n            <Form.Field name=\"visibility\" required={true}>\n                <label>Visibility</label>\n                <Dropdown\n                    selectOnBlur={true}\n                    onChange={(event, data) =>\n                        setValue('visibility', data.value as StorageVisibility)\n                    }\n                    options={visibilityOptions}\n                    selection={true}\n                    defaultValue={StorageVisibility.PRIVATE}\n                    error={!!errors.visibility}\n                />\n                {errors.visibility && (\n                    <Label basic={true} pointing={true} color=\"red\">\n                        {errors.visibility.message\n                            ? errors.visibility.message\n                            : 'Visibility is required'}\n                    </Label>\n                )}\n            </Form.Field>\n\n            <Form.Button primary={true} type=\"submit\" disabled={submitting}>\n                Create\n            </Form.Button>\n        </Form>\n    );\n};\n\nconst validateName = async (orgName: ConcordKey, name: string): Promise<ValidateResult> => {\n    const invalidName = validation.name(name);\n    if (invalidName) {\n        return Promise.resolve(invalidName);\n    }\n\n    const exists = await isStorageExists(orgName, name);\n    if (exists) {\n        return Promise.resolve(jsonStoreAlreadyExistsError(name));\n    }\n    return Promise.resolve(undefined);\n};\n\nexport default NewStoreForm;\n"
  },
  {
    "path": "console2/src/components/molecules/NewTeamForm/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { InjectedFormikProps, withFormik } from 'formik';\nimport * as React from 'react';\nimport { Button, Divider, Form } from 'semantic-ui-react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { NewTeamEntry } from '../../../api/org/team';\nimport { isTeamExists } from '../../../api/service/console';\nimport { notEmpty } from '../../../utils';\nimport { team as validation, teamAlreadyExistsError } from '../../../validation';\nimport { FormikInput } from '../../atoms';\n\ninterface Props {\n    orgName: ConcordKey;\n    onSubmit: (values: NewTeamEntry) => void;\n    submitting: boolean;\n}\n\nclass NewTeamForm extends React.Component<InjectedFormikProps<Props, NewTeamEntry>> {\n    render() {\n        const { submitting, handleSubmit, errors, dirty } = this.props;\n\n        const hasErrors = notEmpty(errors);\n\n        return (\n            <Form onSubmit={handleSubmit} loading={submitting}>\n                <FormikInput\n                    name=\"name\"\n                    label=\"Name\"\n                    placeholder=\"Team name\"\n                    required={true}\n                    data-testid=\"team-form-name\"\n                />\n\n                <FormikInput\n                    name=\"description\"\n                    label=\"Description\"\n                    placeholder=\"Short description\"\n                    data-testid=\"team-form-description\"\n                />\n\n                <Divider />\n\n                <Button primary={true} type=\"submit\" disabled={!dirty || hasErrors} data-testid=\"team-form-submit\">\n                    Create\n                </Button>\n            </Form>\n        );\n    }\n}\n\nconst validator = async (values: NewTeamEntry, { orgName }: Props): Promise<{}> => {\n    let e;\n\n    e = validation.name(values.name);\n    if (e) {\n        return Promise.resolve({ name: e });\n    }\n\n    const exists = await isTeamExists(orgName, values.name);\n    if (exists) {\n        return Promise.resolve({ name: teamAlreadyExistsError(values.name) });\n    }\n\n    e = validation.description(values.description);\n    if (e) {\n        return Promise.resolve({ description: e });\n    }\n\n    return {};\n};\n\nexport default withFormik<Props, NewTeamEntry>({\n    handleSubmit: (values, bag) => {\n        bag.props.onSubmit(values);\n    },\n    validate: validator\n})(NewTeamForm);\n"
  },
  {
    "path": "console2/src/components/molecules/PaginationToolBar/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Button, Dropdown } from 'semantic-ui-react';\n\n// TODO make customizable\nconst defaultDropDownValues = [50, 100, 500];\n\ninterface Props {\n    limit?: number;\n    handleLimitChange?: (limit: any) => void;\n    handleNext: () => void;\n    handlePrev: () => void;\n    handleFirst: () => void;\n    disablePrevious?: boolean;\n    disableNext?: boolean;\n    disableFirst?: boolean;\n    dropDownValues?: number[]; // Numbers\n    maxValue?: number; // Maximum Items that could render\n    disabled?: boolean;\n}\n\nclass PaginationToolBar extends React.PureComponent<Props> {\n    render() {\n        const {\n            limit,\n            handleLimitChange,\n            dropDownValues = defaultDropDownValues,\n            maxValue,\n            disabled\n        } = this.props;\n\n        return (\n            <>\n                {handleLimitChange !== undefined && (\n                    <Dropdown\n                        compact={true}\n                        options={dropDownValues.map((value) => ({\n                            text: value,\n                            value,\n                            disabled: maxValue ? value >= maxValue : false\n                        }))}\n                        value={limit || dropDownValues[0]}\n                        selection={true}\n                        basic={true}\n                        fluid={false}\n                        onChange={(v, data) => handleLimitChange(data.value)}\n                        disabled={disabled}\n                    />\n                )}\n                <Button.Group>\n                    <Button\n                        basic={true}\n                        icon=\"angle double left\"\n                        disabled={this.props.disableFirst}\n                        onClick={() => this.props.handleFirst()}\n                    />\n                    <Button\n                        basic={true}\n                        icon=\"angle left\"\n                        disabled={this.props.disablePrevious}\n                        onClick={() => this.props.handlePrev()}\n                    />\n                    <Button\n                        basic={true}\n                        icon=\"angle right\"\n                        disabled={this.props.disableNext}\n                        onClick={() => this.props.handleNext()}\n                    />\n                </Button.Group>\n            </>\n        );\n    }\n}\n\nexport default PaginationToolBar;\n"
  },
  {
    "path": "console2/src/components/molecules/PaginationToolBar/usePagination.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { useCallback, useState } from 'react';\n\nexport interface Pagination {\n    limit?: number;\n    offset?: number;\n}\n\nexport interface UsePaginationType {\n    paginationFilter: Pagination;\n    handleLimitChange: (limit: number) => void;\n    handleNext: () => void;\n    handlePrev: () => void;\n    handleFirst: () => void;\n    resetOffset: (offset: number) => void;\n}\n\n// TODO customizable defaults (e.g. a way to specify the default \"limit\")\nexport const usePagination = (): UsePaginationType => {\n    const [paginationFilter, setPaginationFilter] = useState<Pagination>({ offset: 0, limit: 50 });\n\n    const handleLimitChange = useCallback((limit: number) => {\n        setPaginationFilter({ offset: 0, limit });\n    }, []);\n\n    const handleNext = useCallback(() => {\n        setPaginationFilter((prev) => ({ offset: prev.offset + 1, limit: prev.limit }));\n    }, []);\n\n    const handlePrev = useCallback(() => {\n        setPaginationFilter((prev) => ({ offset: prev.offset - 1, limit: prev.limit }));\n    }, []);\n\n    const handleFirst = useCallback(() => {\n        setPaginationFilter((prev) => ({ offset: 0, limit: prev.limit }));\n    }, []);\n\n    const resetOffset = useCallback((offset: number) => {\n        setPaginationFilter((prev) => ({ offset, limit: prev.limit }));\n    }, []);\n\n    return {\n        paginationFilter,\n        handleLimitChange,\n        handleNext,\n        handlePrev,\n        handleFirst,\n        resetOffset\n    };\n};\n"
  },
  {
    "path": "console2/src/components/molecules/ProcessActionDropdown/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Dropdown, Icon } from 'semantic-ui-react';\n\nimport { ConcordId } from '../../../api/common';\nimport { canBeCancelled, ProcessStatus } from '../../../api/process';\nimport { CancelProcessPopup } from '../../organisms';\n\ninterface ExternalProps {\n    instanceId: ConcordId;\n    status: ProcessStatus;\n    refresh: () => void;\n}\n\nclass ProcessActionDropdown extends React.PureComponent<ExternalProps> {\n    render() {\n        const { instanceId, status, refresh } = this.props;\n\n        return (\n            <Dropdown icon=\"ellipsis vertical\">\n                <Dropdown.Menu>\n                    <CancelProcessPopup\n                        instanceId={instanceId}\n                        refresh={refresh}\n                        trigger={(onClick: any) => (\n                            <Dropdown.Item onClick={onClick} disabled={!canBeCancelled(status)}>\n                                <Icon name=\"delete\" color=\"red\" />\n                                <span className=\"text\">Cancel</span>\n                            </Dropdown.Item>\n                        )}\n                    />\n                </Dropdown.Menu>\n            </Dropdown>\n        );\n    }\n}\n\nexport default ProcessActionDropdown;\n"
  },
  {
    "path": "console2/src/components/molecules/ProcessActionList/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Link } from 'react-router';\nimport { Button, Table } from 'semantic-ui-react';\nimport { ConcordId } from '../../../api/common';\n\nimport { FormListEntry, FormRunAs } from '../../../api/process/form';\n\ninterface Props {\n    instanceId: ConcordId;\n    forms: FormListEntry[];\n    onOpenWizard: () => void;\n}\n\n// this ugly mess handles all our different ways of specifying LDAP groups in `runAs` form parameter\n// see also server/impl/src/main/java/com/walmartlabs/concord/server/process/form/FormUtils.java getRunAsLdapGroups method\n\nconst groupToString = (item: { group: string } | string): string => {\n    if (typeof item === 'string') {\n        return item;\n    }\n    return item.group;\n};\n\nconst renderStringOrArrayOfStrings = (item: string | string[]): string => {\n    if (typeof item === 'string') {\n        return item;\n    }\n    return item.join(',');\n};\n\nconst renderExpectedGroups = (items: string | string[]) => (\n    <p>\n        <b>Expect groups:</b> [{renderStringOrArrayOfStrings(items)}]\n    </p>\n);\n\nconst renderRunAs = (v?: FormRunAs) => {\n    if (!v) {\n        return;\n    }\n\n    if (v.username) {\n        return (\n            <p>\n                <b>Expects user:</b> {v.username}\n            </p>\n        );\n    }\n\n    if (v.ldap) {\n        if (Array.isArray(v.ldap)) {\n            return renderExpectedGroups(v.ldap.map(groupToString));\n        } else if (v.ldap.group) {\n            // for backward compatibility - previously suspended forms still have `group` as string\n            return renderExpectedGroups(v.ldap.group);\n        }\n    }\n\n    return;\n};\n\nclass ProcessActionList extends React.PureComponent<Props> {\n    render() {\n        const { instanceId, forms, onOpenWizard } = this.props;\n\n        return (\n            <>\n                <Button\n                    id=\"formWizardButton\"\n                    onClick={() => onOpenWizard()}\n                    content=\"Form Wizard\"\n                    color=\"blue\"\n                    compact={true}\n                />\n                <Table>\n                    <Table.Header>\n                        <Table.Row>\n                            <Table.HeaderCell collapsing={true}>Form Action</Table.HeaderCell>\n                            <Table.HeaderCell>Description</Table.HeaderCell>\n                        </Table.Row>\n                    </Table.Header>\n                    <Table.Body>\n                        {forms.map(({ name, runAs }) => (\n                            <Table.Row key={name}>\n                                <Table.Cell singleLine={true}>\n                                    <Link to={`/process/${instanceId}/form/${name}/step`}>\n                                        <Button\n                                            content={`${name}`}\n                                            compact={true}\n                                            basic={true}\n                                            color=\"red\"\n                                            size=\"small\"\n                                        />\n                                    </Link>\n                                </Table.Cell>\n                                <Table.Cell>\n                                    Form\n                                    {renderRunAs(runAs)}\n                                </Table.Cell>\n                            </Table.Row>\n                        ))}\n                    </Table.Body>\n                </Table>\n            </>\n        );\n    }\n}\n\nexport default ProcessActionList;\n"
  },
  {
    "path": "console2/src/components/molecules/ProcessAttachmentsList/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { ConcordId } from '../../../api/common';\nimport { Table } from 'semantic-ui-react';\n\ninterface ExternalProps {\n    instanceId: ConcordId;\n    data?: string[];\n}\n\nconst ProcessAttachmentsList = ({ instanceId, data }: ExternalProps) => {\n    return (\n        <Table celled={true} className={data ? '' : 'loading'}>\n            <Table.Header>{renderTableHeader()}</Table.Header>\n            <Table.Body>{renderElements(instanceId, data)}</Table.Body>\n        </Table>\n    );\n};\n\nconst renderTableHeader = () => {\n    return (\n        <Table.Row>\n            <Table.HeaderCell collapsing={true}>Attached File(s)</Table.HeaderCell>\n        </Table.Row>\n    );\n};\n\nconst renderTableRow = (instanceId: ConcordId, attachment: string) => {\n    const attachmentName = attachment.substring(attachment.lastIndexOf('/'));\n    return (\n        <Table.Row key={attachment}>\n            <Table.Cell>\n                <a\n                    href={'/api/v1/process/' + instanceId + '/attachment/' + attachment}\n                    download={attachmentName}>\n                    {attachment}\n                </a>\n            </Table.Cell>\n        </Table.Row>\n    );\n};\n\nconst renderElements = (instanceId: ConcordId, data?: string[]) => {\n    if (!data) {\n        return (\n            <tr style={{ fontWeight: 'bold' }}>\n                <Table.Cell colSpan={3}>-</Table.Cell>\n            </tr>\n        );\n    }\n\n    if (data.length === 0) {\n        return (\n            <tr style={{ fontWeight: 'bold' }}>\n                <Table.Cell colSpan={3}>No data available</Table.Cell>\n            </tr>\n        );\n    }\n\n    return data.map((p) => renderTableRow(instanceId, p));\n};\n\nexport default ProcessAttachmentsList;\n"
  },
  {
    "path": "console2/src/components/molecules/ProcessElementList/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Icon, Popup, Table } from 'semantic-ui-react';\nimport { ConcordId } from '../../../api/common';\nimport { ProcessStatus } from '../../../api/process';\nimport { ProcessElementEvent, ProcessEventEntry } from '../../../api/process/event';\nimport { formatTimestamp } from '../../../utils';\nimport { HumanizedDuration } from '../../molecules';\nimport { ProcessRestoreActivity } from '../../organisms';\n\ninterface Props {\n    instanceId: ConcordId;\n    processStatus?: ProcessStatus;\n    events?: ProcessEventEntry<ProcessElementEvent>[];\n    definitionLinkBase?: string;\n}\n\nconst renderDefinitionId = (\n    { data: { processDefinitionId, fileName } }: ProcessEventEntry<ProcessElementEvent>,\n    idx: number,\n    arr: Array<ProcessEventEntry<ProcessElementEvent>>\n) => {\n    if (idx !== 0 && arr[idx - 1].data.processDefinitionId === processDefinitionId) {\n        return;\n    }\n\n    if (fileName === undefined) {\n        return processDefinitionId;\n    }\n\n    return <Popup content={fileName} trigger={<span>{processDefinitionId}</span>} hoverable={true}/>;\n};\n\nconst renderTimestamp = (\n    { eventDate }: ProcessEventEntry<{}>,\n    idx: number,\n    arr: Array<ProcessEventEntry<{}>>\n) => {\n    const s = formatTimestamp(eventDate);\n\n    if (idx !== 0 && formatTimestamp(arr[idx - 1].eventDate) === s) {\n        return '(same)';\n    }\n\n    return s;\n};\n\nconst getParam = (p: {} | undefined, name: string, index: number) => {\n    if (p === undefined) {\n        return undefined;\n    }\n    if (p instanceof Array && p[index]) {\n        const { target, resolved } = p[index];\n        if (target === name) {\n            return resolved;\n        }\n    }\n    return undefined;\n};\n\nconst renderRestoreCheckpoint = (\n    instanceId: ConcordId,\n    processStatus: ProcessStatus,\n    outParams: {} | undefined\n) => {\n    const id = getParam(outParams, 'checkpointId', 0);\n    if (id === undefined) {\n        return;\n    }\n    const checkpoint = getParam(outParams, 'checkpointName', 1);\n    if (checkpoint === undefined) {\n        return;\n    }\n\n    return (\n        <ProcessRestoreActivity\n            instanceId={instanceId}\n            checkpointId={id}\n            checkpoint={checkpoint}\n            processStatus={processStatus}\n        />\n    );\n};\n\nconst definitionFileName = (data: ProcessElementEvent): string => {\n    const { processDefinitionId, fileName } = data;\n    if (fileName === undefined) {\n        return processDefinitionId;\n    }\n    return fileName;\n};\n\nconst definitionLink = (data: ProcessElementEvent, definitionLinkBase?: string) => {\n    if (!definitionLinkBase) {\n        return data.line;\n    }\n\n    const { processDefinitionId, fileName } = data;\n    if (!processDefinitionId && !fileName) {\n        return data.line;\n    }\n\n    return (\n        <a\n            href={definitionLinkBase + '/' + definitionFileName(data) + '#L' + data.line}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\">\n            {data.line}\n        </a>\n    );\n};\n\nconst renderElementRow = (\n    instanceId: ConcordId,\n    processStatus: ProcessStatus,\n    ev: ProcessEventEntry<ProcessElementEvent>,\n    idx: number,\n    arr: Array<ProcessEventEntry<ProcessElementEvent>>,\n    definitionLinkBase?: string\n) => {\n    return (\n        <Table.Row key={idx}>\n            <Table.Cell textAlign=\"right\">{renderDefinitionId(ev, idx, arr)}</Table.Cell>\n            <Table.Cell\n                verticalAlign={'middle'}\n                style={{ wordBreak: 'break-all' }}\n                className={ev.data.error !== undefined ? 'error' : ''}>\n                {ev.data.description}\n                {renderRestoreCheckpoint(instanceId, processStatus, ev.data.out)}\n            </Table.Cell>\n            <Table.Cell singleLine={true}>{renderTimestamp(ev, idx, arr)}</Table.Cell>\n            <Table.Cell singleLine={true}>\n                <HumanizedDuration value={ev.data.duration} />\n            </Table.Cell>\n            <Table.Cell>{definitionLink(ev.data, definitionLinkBase)}</Table.Cell>\n            <Table.Cell>{ev.data.column}</Table.Cell>\n        </Table.Row>\n    );\n};\n\nconst renderElements = (\n    instanceId: ConcordId,\n    processStatus?: ProcessStatus,\n    events?: ProcessEventEntry<ProcessElementEvent>[],\n    definitionLinkBase?: string\n) => {\n    if (!events || !processStatus) {\n        return (\n            <tr style={{ fontWeight: 'bold' }}>\n                <Table.Cell> </Table.Cell>\n                <Table.Cell colSpan={5}>-</Table.Cell>\n            </tr>\n        );\n    }\n\n    if (events.length === 0) {\n        return (\n            <tr style={{ fontWeight: 'bold' }}>\n                <Table.Cell> </Table.Cell>\n                <Table.Cell colSpan={5}>No data available</Table.Cell>\n            </tr>\n        );\n    }\n\n    return events.map((e, idx, arr) =>\n        renderElementRow(instanceId, processStatus, e, idx, arr, definitionLinkBase)\n    );\n};\n\nclass ProcessElementList extends React.PureComponent<Props> {\n    render() {\n        const { instanceId, processStatus, events, definitionLinkBase } = this.props;\n        return (\n            <Table celled={true} definition={true} className={events ? '' : 'loading'}>\n                <Table.Header>\n                    <Table.Row>\n                        <Table.HeaderCell width={1} />\n                        <Table.HeaderCell>Step</Table.HeaderCell>\n                        <Table.HeaderCell collapsing={true} singleLine={true}>\n                            <Icon name=\"time\" />\n                            Timestamp\n                        </Table.HeaderCell>\n                        <Table.HeaderCell collapsing={true} singleLine={true}>\n                            Duration\n                        </Table.HeaderCell>\n                        <Table.HeaderCell collapsing={true}>Line</Table.HeaderCell>\n                        <Table.HeaderCell collapsing={true}>Col</Table.HeaderCell>\n                    </Table.Row>\n                </Table.Header>\n\n                <Table.Body>\n                    {renderElements(instanceId, processStatus, events, definitionLinkBase)}\n                </Table.Body>\n            </Table>\n        );\n    }\n}\n\nexport default ProcessElementList;\n"
  },
  {
    "path": "console2/src/components/molecules/ProcessForm/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { format as formatDate, parseISO as parseDate } from 'date-fns';\nimport * as React from 'react';\n\nimport { DateInput, DateTimeInput } from 'semantic-ui-calendar-react';\nimport {\n    Button,\n    Checkbox,\n    CheckboxProps, DropdownItemProps,\n    DropdownProps,\n    Form,\n    Header,\n    Input,\n    Label\n} from 'semantic-ui-react';\n\nimport { RequestError } from '../../../api/common';\nimport {\n    Cardinality,\n    FormField,\n    FormFieldType,\n    FormInstanceEntry\n} from '../../../api/process/form';\nimport { DropdownWithAddition } from '../../molecules';\nimport { RequestErrorMessage } from '../index';\n\n// date-fns format patterns\nconst DATE_TIME_FORMAT = \"yyyy-MM-dd'T'HH:mm:ss.SSSXXX\";\n\n// moment.js format patterns\nconst MOMENT_DATE_FORMAT = 'YYYY-MM-DD';\nconst MOMENT_DATE_TIME_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSZ';\n\ninterface State {\n    [name: string]: any;\n}\n\ninterface Props {\n    form: FormInstanceEntry;\n    errors?: {\n        [name: string]: string;\n    };\n    submitting?: boolean;\n    submitError?: RequestError;\n    completed?: boolean;\n    wizard?: boolean;\n    onSubmit: (values: State) => void;\n    onReturn: () => void;\n}\n\ntype DropdownAllowedValue = Array<boolean | number | string> | undefined;\ntype DropdownValue = boolean | number | string | DropdownAllowedValue;\n\nconst convertAllowedValue = (allowedValue: any) => {\n    if (allowedValue === undefined) {\n        return [];\n    }\n\n    if (allowedValue instanceof Array) {\n        return allowedValue;\n    }\n\n    return [allowedValue];\n};\n\nclass ProcessForm extends React.Component<Props, State> {\n    constructor(props: Props) {\n        super(props);\n        this.state = {};\n    }\n\n    handleReturn(ev: React.FormEvent<HTMLButtonElement>) {\n        ev.preventDefault();\n        this.props.onReturn();\n    }\n\n    handleSubmit(ev: React.FormEvent<HTMLFormElement>) {\n        ev.preventDefault();\n\n        let values = { ...this.state };\n        if (!values) {\n            values = {};\n        }\n\n        const { form, onSubmit } = this.props;\n\n        for (const f of form.fields) {\n            const k = f.name;\n            const v = values[k];\n            const t = f.type;\n\n            if (v === null || v === undefined) {\n                values[k] = f.value;\n            } else if (v === '') {\n                values[k] = null;\n            } else if ((t === FormFieldType.INT || t === FormFieldType.DECIMAL) && isNaN(v)) {\n                values[k] = null;\n            }\n\n            if (\n                (t === FormFieldType.DATE || t === FormFieldType.DATE_TIME) &&\n                values[k] !== undefined\n            ) {\n                // Append the client zone information for date format consistency\n                const d = parseDate(values[k]);\n                values[k] = formatDate(d, DATE_TIME_FORMAT);\n            }\n        }\n\n        // remove undefined values\n        const result = {};\n        Object.keys(values).forEach((k) => {\n            const v = values[k];\n            if (v !== undefined && v !== null) {\n                result[k] = v;\n            }\n        });\n\n        onSubmit(result);\n    }\n\n    handleInput(\n        name: string,\n        type: FormFieldType\n    ): (event: React.SyntheticEvent<HTMLInputElement>) => void {\n        return ({ target }) => {\n            const t = target as HTMLInputElement;\n\n            let v: string | number | boolean | File = t.value;\n            if (type === FormFieldType.INT || type === FormFieldType.DECIMAL) {\n                v = t.valueAsNumber;\n            } else if (type === FormFieldType.FILE) {\n                v = t.files![0];\n            }\n\n            this.setState({ [name]: v });\n        };\n    }\n\n    handleDateInput(name: string): (e: React.SyntheticEvent<HTMLElement>, data: any) => void {\n        return (ev, { value }) => {\n            this.setState({ [name]: value });\n        };\n    }\n\n    handleDropdown(\n        name: string\n    ): (event: React.SyntheticEvent<HTMLElement>, data: DropdownProps) => void {\n        return (ev, { value }) => {\n            this.setState({ [name]: value });\n        };\n    }\n\n    handleCheckboxInput(\n        name: string\n    ): (event: React.FormEvent<HTMLInputElement>, data: CheckboxProps) => void {\n        return (ev, { checked }) => {\n            this.setState({ [name]: checked });\n        };\n    }\n\n    renderInput(name: string, type: FormFieldType, value: any, inputType?: string, opts?: {}) {\n        const { submitting, completed } = this.props;\n\n        return (\n            <Input\n                name={name}\n                disabled={submitting || completed}\n                defaultValue={value}\n                type={inputType}\n                onChange={this.handleInput(name, type)}\n                {...opts}\n            />\n        );\n    }\n\n    renderDropdown(\n        name: string,\n        cardinality: Cardinality,\n        value: DropdownValue | DropdownValue[],\n        allowedValue: DropdownAllowedValue,\n        multiple: boolean,\n        opts?: {}\n    ) {\n        const { submitting, completed } = this.props;\n\n        if (value === null) {\n            value = undefined;\n        }\n\n        if (multiple && value === undefined) {\n            value = [];\n        }\n\n        const allowedOptions: DropdownItemProps[] = this.toOpts(allowedValue);\n        const options: DropdownItemProps[] = allowedOptions.length > 0 ? allowedOptions : this.toOpts(value);\n        const required =\n            cardinality === Cardinality.AT_LEAST_ONE ||\n            cardinality === Cardinality.ONE_AND_ONLY_ONE;\n        const allowAdditions = allowedOptions.length === 0;\n\n        return (\n            <DropdownWithAddition\n                name={name}\n                options={options}\n                value={value}\n                required={required}\n                multiple={multiple}\n                completed={completed}\n                submitting={submitting}\n                allowAdditions={allowAdditions}\n                onChange={this.handleDropdown(name)}\n                {...opts}\n            />\n        );\n    }\n\n    toOpts(val: DropdownValue | DropdownValue[]) : DropdownItemProps[]  {\n        if (val === undefined) {\n            return [];\n        }\n\n        if (Array.isArray(val)) {\n            return val.map((v: DropdownValue) => ({text: v, value: v} as DropdownItemProps));\n        }\n\n        return [ { text: val, value: val } ]\n    }\n\n    renderStringField(\n        { name, label, type, cardinality, allowedValue, options }: FormField,\n        value: any\n    ) {\n        const { errors } = this.props;\n        const error = errors ? errors[name] : undefined;\n        const inputType = options ? options.inputType : undefined;\n\n        if (!cardinality) {\n            cardinality = Cardinality.ONE_OR_NONE;\n        }\n\n        const required =\n            cardinality === Cardinality.AT_LEAST_ONE ||\n            cardinality === Cardinality.ONE_AND_ONLY_ONE;\n\n        const allowedValues = convertAllowedValue(allowedValue);\n\n        const fixedInput = required && allowedValues.length === 1;\n        if (fixedInput) {\n            value = allowedValues[0];\n        }\n\n        const singleValue =\n            cardinality === Cardinality.ONE_AND_ONLY_ONE || cardinality === Cardinality.ONE_OR_NONE;\n\n        const input = fixedInput || (singleValue && allowedValues.length === 0);\n        const dropdown = !input;\n\n        const multiSelect =\n            (cardinality === Cardinality.AT_LEAST_ONE || cardinality === Cardinality.ANY) &&\n            allowedValues.length !== 1;\n\n        return (\n            <Form.Field key={name} error={!!error} required={required}>\n                <label>{label}</label>\n\n                {dropdown\n                    ? this.renderDropdown(\n                          name,\n                          cardinality,\n                          value,\n                          allowedValues,\n                          multiSelect,\n                          options\n                      )\n                    : this.renderInput(name, type, value, inputType, {\n                          readOnly: fixedInput,\n                          ...options\n                      })}\n\n                {error && (\n                    <Label basic={true} color=\"red\" pointing={true}>\n                        {error}\n                    </Label>\n                )}\n            </Form.Field>\n        );\n    }\n\n    renderNumberField({ name, label, type, options }: FormField, value?: number) {\n        const { errors } = this.props;\n        const error = errors ? errors[name] : undefined;\n\n        if (value !== undefined && isNaN(value)) {\n            value = undefined;\n        }\n\n        return (\n            <Form.Field key={name} error={!!error}>\n                <label>{label}</label>\n\n                {this.renderInput(name, type, value, 'number', {\n                    step: type === 'decimal' ? 'any' : '1',\n                    ...options\n                })}\n\n                {error && (\n                    <Label basic={true} color=\"red\" pointing={true}>\n                        {error}\n                    </Label>\n                )}\n            </Form.Field>\n        );\n    }\n\n    renderBooleanField({ name, label, type, options }: FormField, value: boolean) {\n        const { errors, submitting, completed } = this.props;\n        const error = errors ? errors[name] : undefined;\n\n        return (\n            <Form.Field key={name} error={!!error}>\n                <label>{label}</label>\n\n                <Checkbox\n                    name={name}\n                    disabled={submitting || completed}\n                    defaultChecked={value}\n                    onChange={this.handleCheckboxInput(name)}\n                    {...options}\n                />\n\n                {error && (\n                    <Label basic={true} color=\"red\" pointing={true}>\n                        {error}\n                    </Label>\n                )}\n            </Form.Field>\n        );\n    }\n\n    renderFileField({ name, label, type, cardinality }: FormField) {\n        const { errors, submitting, completed } = this.props;\n        const error = errors ? errors[name] : undefined;\n\n        const required = cardinality === Cardinality.ONE_AND_ONLY_ONE;\n\n        return (\n            <Form.Field key={name} error={!!error} required={required}>\n                <label>{label}</label>\n\n                <Input\n                    name={name}\n                    type=\"file\"\n                    disabled={submitting || completed}\n                    onChange={this.handleInput(name, type)}\n                />\n\n                {error && (\n                    <Label basic={true} color=\"red\" pointing={true}>\n                        {error}\n                    </Label>\n                )}\n            </Form.Field>\n        );\n    }\n\n    renderDateField({ name, label, cardinality, type, options }: FormField, value: any) {\n        const { errors } = this.props;\n        const error = errors ? errors[name] : undefined;\n        const popupPosition = options ? options.popupPosition : undefined;\n\n        if (value === undefined) {\n            value = '';\n        }\n\n        if (!cardinality) {\n            cardinality = Cardinality.ONE_OR_NONE;\n        }\n\n        const required =\n            cardinality === Cardinality.AT_LEAST_ONE ||\n            cardinality === Cardinality.ONE_AND_ONLY_ONE;\n\n        return (\n            <Form.Field key={name} error={!!error} required={required}>\n                <label>{label}</label>\n\n                <DateInput\n                    name={name}\n                    placeholder={`Date (${MOMENT_DATE_FORMAT})`}\n                    value={value}\n                    iconPosition=\"left\"\n                    closable={true}\n                    popupPosition={popupPosition}\n                    dateFormat={MOMENT_DATE_FORMAT}\n                    autoComplete={'off'}\n                    clearable={!required}\n                    onChange={this.handleDateInput(name)}\n                />\n\n                {error && (\n                    <Label basic={true} color=\"red\" pointing={true}>\n                        {error}\n                    </Label>\n                )}\n            </Form.Field>\n        );\n    }\n\n    renderDateTimeField({ name, label, cardinality, type, options }: FormField, value: any) {\n        const { errors } = this.props;\n        const error = errors ? errors[name] : undefined;\n        const popupPosition = options ? options.popupPosition : undefined;\n\n        if (value === undefined) {\n            value = '';\n        }\n\n        if (!cardinality) {\n            cardinality = Cardinality.ONE_OR_NONE;\n        }\n\n        const required =\n            cardinality === Cardinality.AT_LEAST_ONE ||\n            cardinality === Cardinality.ONE_AND_ONLY_ONE;\n\n        return (\n            <Form.Field key={name} error={!!error} required={required}>\n                <label>{label}</label>\n\n                <DateTimeInput\n                    name={name}\n                    placeholder={`Date/Time (${MOMENT_DATE_TIME_FORMAT})`}\n                    value={value}\n                    iconPosition=\"left\"\n                    closable={true}\n                    popupPosition={popupPosition}\n                    dateFormat={MOMENT_DATE_FORMAT}\n                    dateTimeFormat={MOMENT_DATE_TIME_FORMAT}\n                    autoComplete={'off'}\n                    clearable={!required}\n                    onChange={this.handleDateInput(name)}\n                />\n\n                {error && (\n                    <Label basic={true} color=\"red\" pointing={true}>\n                        {error}\n                    </Label>\n                )}\n            </Form.Field>\n        );\n    }\n\n    renderField(f: FormField) {\n        let value = this.state[f.name];\n        if (value === undefined) {\n            value = f.value;\n        }\n\n        switch (f.type) {\n            case FormFieldType.STRING:\n                return this.renderStringField(f, value);\n            case FormFieldType.INT:\n            case FormFieldType.DECIMAL:\n                return this.renderNumberField(f, value);\n            case FormFieldType.BOOLEAN:\n                return this.renderBooleanField(f, value);\n            case FormFieldType.FILE:\n                return this.renderFileField(f);\n            case FormFieldType.DATE:\n                return this.renderDateField(f, value);\n            case FormFieldType.DATE_TIME:\n                return this.renderDateTimeField(f, value);\n            default:\n                return <p key={f.name}>Unknown field type: {f.type}</p>;\n        }\n    }\n\n    render() {\n        const { form, submitting, submitError, completed } = this.props;\n\n        return (\n            <>\n                <Header as=\"h2\">{form.name}</Header>\n                {submitError && <RequestErrorMessage error={submitError} />}\n                <Form loading={submitting} onSubmit={(ev) => this.handleSubmit(ev)}>\n                    {form.fields.map((f) => this.renderField(f))}\n                    {completed ? (\n                        <Button\n                            icon=\"check\"\n                            primary={true}\n                            content=\"Return to the process page\"\n                            onClick={(ev) => this.handleReturn(ev)}\n                        />\n                    ) : (\n                        <Button\n                            id=\"formSubmitButton\"\n                            type=\"submit\"\n                            primary={true}\n                            disabled={submitting}\n                            content=\"Submit\"\n                        />\n                    )}\n                </Form>\n            </>\n        );\n    }\n}\n\nexport default ProcessForm;\n"
  },
  {
    "path": "console2/src/components/molecules/ProcessHistoryList/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Table } from 'semantic-ui-react';\n\nimport { isFinal, ProcessHistoryEntry } from '../../../api/process';\nimport { formatDuration } from '../../../utils';\nimport { LocalTimestamp } from '../index';\n\ninterface ExternalProps {\n    data?: ProcessHistoryEntry[];\n}\n\nconst renderTableHeader = () => {\n    return (\n        <Table.Row>\n            <Table.HeaderCell collapsing={true}>Status</Table.HeaderCell>\n            <Table.HeaderCell collapsing={true}>Change Time </Table.HeaderCell>\n            <Table.HeaderCell collapsing={true}>Elapsed Time </Table.HeaderCell>\n        </Table.Row>\n    );\n};\n\nconst renderTableRow = (data: ProcessHistoryEntry[], row: ProcessHistoryEntry, idx: number) => {\n    let elapsedTime: string | undefined;\n\n    if (idx === 0 && !isFinal(row.status)) {\n        const startTime: Date = new Date(data[idx].changeDate);\n        const currentTime: Date = new Date();\n        const duration = currentTime.getTime() - startTime.getTime();\n        elapsedTime = formatDuration(duration) + ' (so far)';\n    } else if (idx > 0) {\n        const endTime: Date = new Date(row.changeDate);\n        const startTime: Date = new Date(data[idx - 1].changeDate);\n        const duration = startTime.getTime() - endTime.getTime();\n        elapsedTime = formatDuration(duration);\n    }\n\n    return (\n        <Table.Row key={idx}>\n            <Table.Cell>{row.status}</Table.Cell>\n            <Table.Cell>\n                <LocalTimestamp value={row.changeDate} />\n            </Table.Cell>\n            <Table.Cell>{elapsedTime}</Table.Cell>\n        </Table.Row>\n    );\n};\n\nconst renderElements = (data?: ProcessHistoryEntry[]) => {\n    if (!data) {\n        return (\n            <tr style={{ fontWeight: 'bold' }}>\n                <Table.Cell colSpan={3}>&nbsp;</Table.Cell>\n            </tr>\n        );\n    }\n\n    if (data.length === 0) {\n        return (\n            <tr style={{ fontWeight: 'bold' }}>\n                <Table.Cell colSpan={3}>No data available</Table.Cell>\n            </tr>\n        );\n    }\n\n    return data.map((p, idx) => renderTableRow(data, p, idx));\n};\n\nconst ProcessHistoryList = ({ data }: ExternalProps) => {\n    return (\n        <Table celled={true} className={data ? '' : 'loading'}>\n            <Table.Header>{renderTableHeader()}</Table.Header>\n            <Table.Body>{renderElements(data)}</Table.Body>\n        </Table>\n    );\n};\n\nexport default ProcessHistoryList;\n"
  },
  {
    "path": "console2/src/components/molecules/ProcessLastErrorModal/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Icon, Modal } from 'semantic-ui-react';\n\nimport { ProcessMeta } from '../../../api/process';\nimport { ReactJson } from '../../atoms';\n\ninterface Props {\n    processMeta?: ProcessMeta;\n    title?: string;\n}\n\nexport default ({ processMeta, title = 'Last error' }: Props) => {\n    if (!processMeta || !processMeta.out || !processMeta.out.lastError) {\n        return <></>;\n    }\n\n    return (\n        <Modal\n            size=\"fullscreen\"\n            dimmer=\"inverted\"\n            trigger={\n                <Icon\n                    className=\"failureDetailsButton\"\n                    name=\"question circle outline\"\n                    color=\"red\"\n                    title=\"Failure details\"\n                    size={'large'}\n                />\n            }>\n            <Modal.Header>{title}</Modal.Header>\n            <Modal.Content scrolling={true}>\n                <ReactJson\n                    src={processMeta.out.lastError}\n                    collapsed={false}\n                    name={null}\n                    enableClipboard={false}\n                />\n            </Modal.Content>\n        </Modal>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/molecules/ProcessList/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Link } from 'react-router';\nimport { Checkbox, Table } from 'semantic-ui-react';\n\nimport { ConcordId } from '../../../api/common';\nimport { ColumnDefinition, RenderType } from '../../../api/org';\nimport { canBeCancelled, ProcessEntry, ProcessFilters } from '../../../api/process';\nimport { HumanizedDuration, LocalTimestamp, ProcessStatusIcon } from '../../molecules';\nimport { TableSearchFilter } from '../../atoms';\n\nimport './styles.css';\nimport { parseISO } from 'date-fns';\n\nexport enum Status {\n    NEW = 'NEW',\n    PREPARING = 'PREPARING',\n    ENQUEUED = 'ENQUEUED',\n    WAITING = 'WAITING',\n    SCHEDULED = 'ENQUEUED (future)',\n    STARTING = 'STARTING',\n    RUNNING = 'RUNNING',\n    SUSPENDED = 'SUSPENDED',\n    RESUMING = 'RESUMING',\n    FINISHED = 'FINISHED',\n    FAILED = 'FAILED',\n    CANCELLED = 'CANCELLED',\n    TIMED_OUT = 'TIMED_OUT'\n}\n\nexport const STATUS_COLUMN: ColumnDefinition = {\n    caption: 'Status',\n    source: 'status',\n    render: RenderType.PROCESS_STATUS,\n    textAlign: 'center',\n    collapsing: true,\n    searchValueType: 'string',\n    searchType: 'equals',\n    searchOptions: Object.keys(Status).map((k) => ({\n        value: k,\n        text: Status[k]\n    }))\n};\n\nexport const INSTANCE_ID_COLUMN: ColumnDefinition = {\n    caption: 'Instance ID',\n    source: 'instanceId',\n    render: RenderType.PROCESS_LINK\n};\n\nexport const PROJECT_COLUMN: ColumnDefinition = {\n    caption: 'Project',\n    source: 'projectName',\n    render: RenderType.PROJECT_LINK\n};\n\nexport const REPO_COLUMN: ColumnDefinition = {\n    caption: 'Repository',\n    source: 'repoName',\n    render: RenderType.REPO_LINK,\n    searchValueType: 'string',\n    searchType: 'substring'\n};\n\nexport const INITIATOR_COLUMN: ColumnDefinition = {\n    caption: 'Initiator',\n    source: 'initiator',\n    searchValueType: 'string',\n    searchType: 'substring'\n};\n\nexport const CREATED_AT_COLUMN: ColumnDefinition = {\n    caption: 'Created',\n    source: 'createdAt',\n    render: RenderType.TIMESTAMP\n};\n\nexport const UPDATED_AT_COLUMN: ColumnDefinition = {\n    caption: 'Updated',\n    source: 'lastUpdatedAt',\n    render: RenderType.TIMESTAMP\n};\n\nexport const DURATION_COLUMN: ColumnDefinition = {\n    caption: 'Duration',\n    source: 'lastRunAt',\n    render: RenderType.DURATION\n};\n\nexport const TAGS_COLUMN: ColumnDefinition = {\n    caption: 'Tags',\n    source: 'tags',\n    render: RenderType.STRING_ARRAY\n};\n\nexport const ENTRY_POINT_COLUMN: ColumnDefinition = {\n    caption: 'Entry Point',\n    source: 'meta.entryPoint',\n    searchValueType: 'string',\n    searchType: 'substring'\n};\n\ninterface Entry extends ProcessEntry {\n    checked: boolean;\n}\n\ninterface Props {\n    data?: ProcessEntry[];\n    orgName?: string;\n    columns: ColumnDefinition[];\n    onSelectProcess?: (selectedIds: ConcordId[]) => void;\n\n    filterProps?: ProcessFilters;\n    onFilterChange?: (column: ColumnDefinition, filterValue?: string) => void;\n}\n\ninterface State {\n    data: Entry[] | undefined;\n    active: boolean;\n}\n\nconst toState = (data?: ProcessEntry[]): Entry[] | undefined => {\n    return data ? data.map((e) => ({ ...e, checked: false })) : undefined;\n};\n\nconst getValueByPath = (path: string, e: ProcessEntry) => {\n    const paths = path.split('.');\n    let current = e;\n    for (let i = 0; i < paths.length; ++i) {\n        if (current[paths[i]] === undefined) {\n            return undefined;\n        } else {\n            current = current[paths[i]];\n        }\n    }\n    return current;\n};\n\nconst getValue = (source: string, e: ProcessEntry) => {\n    // TODO: remove and use getValueByPath...\n    if (source.startsWith('meta.')) {\n        if (e.meta === undefined) {\n            return 'n/a';\n        }\n\n        const src = source.substring('meta.'.length);\n        return e.meta[src];\n    }\n\n    const result = getValueByPath(source, e);\n    if (result !== null && result !== undefined && typeof result === 'object') {\n        return JSON.stringify(result);\n    }\n    return result;\n};\n\nconst renderDuration = (v: string, e: ProcessEntry) => {\n    // the correct way is to use the totalRuntimeMs field\n    if (e.totalRuntimeMs !== undefined) {\n        return (\n            <HumanizedDuration\n                value={e.totalRuntimeMs}\n                hint=\"Total RUNNING time\"\n            />\n        );\n    }\n\n    // fallback to the old behavior\n    if (!v || !e.lastUpdatedAt) {\n        return '-';\n    }\n\n    try {\n        const start = parseISO(v);\n        const end = parseISO(e.lastUpdatedAt);\n        return (\n            <HumanizedDuration\n                value={end.getTime() - start.getTime()}\n                hint=\"since last RUNNING status\"\n            />\n        );\n    } catch (e) {\n        return `Invalid value: ${v}`;\n    }\n}\n\nconst renderColumnContent = (e: Entry, c: ColumnDefinition) => {\n    const v = getValue(c.source, e);\n\n    switch (c.render) {\n        case RenderType.PROCESS_LINK: {\n            const caption = v || e.instanceId;\n            return <Link to={`/process/${e.instanceId}`}>{caption}</Link>;\n        }\n        case RenderType.TIMESTAMP: {\n            return v === undefined ? '' : <LocalTimestamp value={v} />;\n        }\n        case RenderType.PROJECT_LINK: {\n            return <Link to={`/org/${e.orgName}/project/${e.projectName}`}>{v}</Link>;\n        }\n        case RenderType.REPO_LINK: {\n            return (\n                <Link to={`/org/${e.orgName}/project/${e.projectName}/repository/${e.repoName}`}>\n                    {v}\n                </Link>\n            );\n        }\n        case RenderType.PROCESS_STATUS: {\n            return <ProcessStatusIcon process={e} />;\n        }\n        case RenderType.STRING_ARRAY: {\n            return v === undefined ? '' : v.join(', ');\n        }\n        case RenderType.DURATION: {\n            return renderDuration(v, e);\n        }\n        case RenderType.LINK: {\n            if (!v || !v.link || !v.title) {\n                return '';\n            }\n\n            return (\n                <a href={v.link} target=\"_blank\" rel=\"noopener noreferrer\">\n                    {v.title}\n                </a>\n            );\n        }\n        default: {\n            if (c.searchValueType === 'boolean') {\n                return <Checkbox type=\"checkbox\" checked={v} readOnly={true} disabled={true} />;\n            } else {\n                return v;\n            }\n        }\n    }\n};\n\nconst renderColumn = (idx: number, e: Entry, c: ColumnDefinition) => {\n    return (\n        <Table.Cell key={idx} textAlign={c.textAlign}>\n            {renderColumnContent(e, c)}\n        </Table.Cell>\n    );\n};\n\nconst renderSearchFilter = (\n    c: ColumnDefinition,\n    filterProps?: ProcessFilters,\n    onFilterChange?: (column: ColumnDefinition, filterValue: string) => void\n) => {\n    if (\n        c.searchValueType === undefined ||\n        onFilterChange === undefined ||\n        filterProps === undefined\n    ) {\n        return;\n    }\n\n    const filterCurrentValue = filterProps[c.source];\n\n    return (\n        <TableSearchFilter\n            column={c}\n            currentValue={filterCurrentValue}\n            onFilterChange={(column, filterValue) => onFilterChange(column, filterValue)}\n        />\n    );\n};\n\nconst renderColumnCaption = (\n    c: ColumnDefinition,\n    filterProps?: ProcessFilters,\n    onFilterChange?: (column: ColumnDefinition, filterValue: string) => void\n) => {\n    const searchFilter = renderSearchFilter(c, filterProps, onFilterChange);\n    return (\n        <Table.HeaderCell\n            style={{ whiteSpace: 'nowrap' }}\n            key={c.caption}\n            collapsing={c.collapsing}\n            singleLine={c.singleLine}>\n            {c.caption}\n            {searchFilter}\n        </Table.HeaderCell>\n    );\n};\n\nclass ProcessList extends React.Component<Props, State> {\n    constructor(props: Props) {\n        super(props);\n\n        this.state = { data: toState(props.data), active: false };\n    }\n\n    componentDidUpdate(prevProps: Props) {\n        if (prevProps.data !== this.props.data) {\n            this.setState({\n                data: toState(this.props.data)\n            });\n        }\n    }\n\n    onToggleRow(r: Entry) {\n        if (!canBeCancelled(r.status)) {\n            return;\n        }\n\n        const processes = [...(this.state.data || [])];\n\n        const idx: number = processes.findIndex((p) => p.instanceId === r.instanceId);\n        if (idx === -1) {\n            return;\n        }\n\n        const isChecked = processes[idx].checked;\n\n        this.onRowSelect(r, !isChecked);\n    }\n\n    onRowSelect(r: Entry, isChecked: any) {\n        const processes = [...(this.state.data || [])];\n\n        const idx: number = processes.findIndex((p) => p.instanceId === r.instanceId);\n        if (idx === -1) {\n            return;\n        }\n\n        processes[idx].checked = isChecked;\n\n        this.setSelectedProcessIds(processes);\n        this.setState({ data: processes });\n    }\n\n    onAllRowsSelect(isChecked: any) {\n        const processes = [...(this.state.data || [])];\n\n        processes.forEach((p) => {\n            if (canBeCancelled(p.status)) {\n                const idx: number = processes.findIndex((p2) => p2.instanceId === p.instanceId);\n                processes[idx].checked = isChecked;\n            }\n        });\n\n        this.setSelectedProcessIds(processes);\n        this.setState({ data: processes });\n    }\n\n    setSelectedProcessIds(processes: Entry[]) {\n        const { onSelectProcess } = this.props;\n        const selectedProcessIds: ConcordId[] = [];\n        processes.forEach((p) => {\n            if (p.checked) {\n                selectedProcessIds.push(p.instanceId);\n            }\n        });\n        if (onSelectProcess != null) {\n            onSelectProcess(selectedProcessIds);\n        }\n    }\n\n    renderTableHeader(rows: Entry[] | undefined, columns: ColumnDefinition[]) {\n        const { onSelectProcess, onFilterChange, filterProps } = this.props;\n        const selectedProcessIds: ConcordId[] = [];\n        rows?.forEach((p) => {\n            if (p.checked) {\n                selectedProcessIds.push(p.instanceId);\n            }\n        });\n\n        const cancellableProcessIds: ConcordId[] = [];\n        rows?.forEach((p) => {\n            if (canBeCancelled(p.status)) {\n                cancellableProcessIds.push(p.instanceId);\n            }\n        });\n\n        const isTopCheckboxDisabled = cancellableProcessIds.length === 0;\n        const isTopCheckboxSelected =\n            cancellableProcessIds.length !== 0 &&\n            cancellableProcessIds.length === selectedProcessIds.length;\n\n        return (\n            <Table.Row>\n                {onSelectProcess !== undefined && (\n                    <Table.HeaderCell collapsing={true}>\n                        <Checkbox\n                            onClick={(e, data) => this.onAllRowsSelect(data.checked)}\n                            checked={isTopCheckboxSelected}\n                            disabled={isTopCheckboxDisabled}\n                        />\n                    </Table.HeaderCell>\n                )}\n                {columns.map((c) => renderColumnCaption(c, filterProps, onFilterChange))}\n            </Table.Row>\n        );\n    }\n\n    renderTableRow(row: Entry, columns: ColumnDefinition[]) {\n        const { onSelectProcess } = this.props;\n        return (\n            <Table.Row key={row.instanceId} onClick={() => this.onToggleRow(row)}>\n                {onSelectProcess !== undefined && (\n                    <Table.Cell collapsing={true}>\n                        <Checkbox\n                            key={row.instanceId}\n                            checked={row.checked}\n                            disabled={!canBeCancelled(row.status)}\n                        />\n                    </Table.Cell>\n                )}\n                {columns.map((c, id) => renderColumn(id, row, c))}\n            </Table.Row>\n        );\n    }\n\n    render() {\n        const { columns, onFilterChange } = this.props;\n        const { data } = this.state;\n\n        const canBeFiltered = onFilterChange !== undefined;\n\n        if (!data || data.length === 0) {\n            return (\n                <>\n                    {canBeFiltered && (\n                        <div className={'container'}>\n                            <Table\n                                celled={true}\n                                attached=\"bottom\"\n                                selectable={true}\n                                style={{ borderBottom: 'none' }}>\n                                <Table.Header>{this.renderTableHeader(data, columns)}</Table.Header>\n                            </Table>\n                        </div>\n                    )}\n                    {data && <h3>No processes found.</h3>}\n                </>\n            );\n        }\n\n        return (\n            <div style={{ overflowX: 'auto' }} className={'container'}>\n                <Table celled={true} attached=\"bottom\" selectable={true}>\n                    <Table.Header>{this.renderTableHeader(data, columns)}</Table.Header>\n\n                    <Table.Body>{data.map((p) => this.renderTableRow(p, columns))}</Table.Body>\n                </Table>\n            </div>\n        );\n    }\n}\n\nexport default ProcessList;\n"
  },
  {
    "path": "console2/src/components/molecules/ProcessList/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n.container{\n    padding-left: 1px;\n    padding-right: 1px;\n}\n"
  },
  {
    "path": "console2/src/components/molecules/ProcessListWithSearch/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Button, Icon, Label, Popup, Table } from 'semantic-ui-react';\n\nimport { ConcordId } from '../../../api/common';\nimport { ProcessEntry } from '../../../api/process';\nimport { Pagination } from '../PaginationToolBar/usePagination';\nimport { ProcessList, BulkProcessActionDropdown, PaginationToolBar } from '../../molecules';\nimport { ColumnDefinition } from '../../../api/org';\n\nimport {\n    STATUS_COLUMN,\n    CREATED_AT_COLUMN,\n    INITIATOR_COLUMN,\n    INSTANCE_ID_COLUMN,\n    PROJECT_COLUMN\n} from '../ProcessList';\nimport { ProcessFilters } from '../../../api/process';\n\nimport '../ProcessList/styles.css';\n\n// list of columns for the default process list configuration\nconst defaultColumns = [\n    STATUS_COLUMN,\n    INSTANCE_ID_COLUMN,\n    PROJECT_COLUMN,\n    INITIATOR_COLUMN,\n    CREATED_AT_COLUMN\n];\n\n// columns used on the list of a project's processes\nconst withoutProjectColumns = [\n    STATUS_COLUMN,\n    INSTANCE_ID_COLUMN,\n    INITIATOR_COLUMN,\n    CREATED_AT_COLUMN\n];\n\ninterface Props {\n    processes?: ProcessEntry[];\n\n    orgName?: string;\n    projectName?: string;\n\n    processFilters?: ProcessFilters;\n    paginationFilter?: Pagination;\n\n    showInitiatorFilter?: boolean;\n\n    columns: ColumnDefinition[];\n\n    loading: boolean;\n\n    next?: number;\n    prev?: number;\n\n    usePagination?: boolean;\n\n    refresh: (processFilters?: ProcessFilters, paginationFilters?: Pagination) => void;\n}\n\ninterface State {\n    processFilters: ProcessFilters;\n    selectedProcessIds: ConcordId[];\n}\n\nconst toState = (selectedProcessIds: ConcordId[], processFilters?: ProcessFilters): State => {\n    return {\n        processFilters: processFilters || {},\n        selectedProcessIds\n    };\n};\n\nconst hasFilter = (processFilters: ProcessFilters, columns: ColumnDefinition[]) => {\n    return (\n        Object.keys(processFilters)\n            .filter((k) => processFilters[k] !== '')\n            .filter((k) => columns.find((c) => c.source === k) !== undefined).length > 0\n    );\n};\n\nconst renderFilter = (\n    filterValue: string,\n    c: ColumnDefinition,\n    clearFilter: (source: string) => void\n) => {\n    return (\n        <Label key={c.source} as=\"a\" onClick={() => clearFilter(c.source)}>\n            {c.caption}:<Label.Detail>{filterValue}</Label.Detail>\n            <Icon name=\"delete\" />\n        </Label>\n    );\n};\n\nconst getDefinition = (source: string, cols: ColumnDefinition[]) => {\n    for (const c of cols) {\n        if (c.source === source) {\n            return c;\n        }\n    }\n    return { source, caption: 'n/a' };\n};\n\nconst getFilterText = (column: ColumnDefinition, value: string) => {\n    if (column.searchOptions) {\n        const option = column.searchOptions.find((o) => o.value === value);\n        if (option !== undefined) {\n            return option.text;\n        }\n    }\n    return value;\n};\n\nconst renderFiltersToolbar = (\n    cols: ColumnDefinition[],\n    processFilters: ProcessFilters,\n    clearFilter: (source: string) => void\n) => {\n    return Object.keys(processFilters)\n        .filter((k) => cols.find((c) => c.source === k) !== undefined)\n        .map((k) => {\n            const c = getDefinition(k, cols);\n            return renderFilter(getFilterText(c, processFilters[k]), c, clearFilter);\n        });\n};\n\nclass ProcessListWithSearch extends React.Component<Props, State> {\n    constructor(props: Props) {\n        super(props);\n        this.state = toState([], this.props.processFilters);\n        this.onSelectProcess = this.onSelectProcess.bind(this);\n        this.onRefresh = this.onRefresh.bind(this);\n        this.handlePrev = this.handlePrev.bind(this);\n        this.handleNext = this.handleNext.bind(this);\n        this.handleFirst = this.handleFirst.bind(this);\n        this.onFilterChange = this.onFilterChange.bind(this);\n        this.onFiltersClear = this.onFiltersClear.bind(this);\n        this.onFilterClear = this.onFilterClear.bind(this);\n    }\n\n    onFilterChange(column: ColumnDefinition, filterValue?: string) {\n        const { processFilters } = this.state;\n        const { refresh, paginationFilter } = this.props;\n\n        const newProcessFilters = {};\n        Object.keys(processFilters)\n            .filter((k) => k !== column.source)\n            .forEach((k) => (newProcessFilters[k] = processFilters[k]));\n\n        if (filterValue !== undefined && filterValue !== '') {\n            newProcessFilters[column.source] = filterValue;\n        }\n\n        this.setState({ processFilters: newProcessFilters });\n        refresh(newProcessFilters, paginationFilter);\n    }\n\n    onFiltersClear() {\n        const { refresh, paginationFilter } = this.props;\n        const processFilters = {};\n        this.setState({ processFilters });\n        refresh(processFilters, paginationFilter);\n    }\n\n    onFilterClear(source: string) {\n        const { processFilters } = this.state;\n        const { refresh, paginationFilter } = this.props;\n        const newProcessFilters = {};\n        Object.keys(processFilters)\n            .filter((k) => k !== source)\n            .forEach((k) => (newProcessFilters[k] = processFilters[k]));\n        this.setState({ processFilters: newProcessFilters });\n        refresh(newProcessFilters, paginationFilter);\n    }\n\n    handleLimitChange(limit: any) {\n        const { processFilters } = this.state;\n        const { paginationFilter, refresh } = this.props;\n        if (!paginationFilter || paginationFilter.limit !== limit) {\n            refresh(processFilters, { limit });\n        }\n    }\n\n    handleNext() {\n        this.handleNavigation(this.props.next);\n    }\n\n    handlePrev() {\n        this.handleNavigation(this.props.prev);\n    }\n\n    handleFirst() {\n        this.handleNavigation(0);\n    }\n\n    handleNavigation(offset?: number) {\n        const { processFilters } = this.state;\n        const { refresh, paginationFilter } = this.props;\n\n        refresh(processFilters, { offset, limit: paginationFilter && paginationFilter.limit });\n    }\n\n    onSelectProcess(processIds: ConcordId[]) {\n        this.setState({ selectedProcessIds: processIds });\n    }\n\n    onRefresh() {\n        const { refresh, paginationFilter } = this.props;\n        const { processFilters } = this.state;\n        refresh(processFilters, paginationFilter);\n    }\n\n    renderFilterLabels(cols: ColumnDefinition[], processFilters: ProcessFilters, loading: boolean) {\n        return (\n            <>\n                <span style={{ marginRight: '10px' }}>Active filters:</span>\n                {renderFiltersToolbar(cols, processFilters, this.onFilterClear)}\n                <Popup\n                    trigger={\n                        <Button\n                            basic={true}\n                            icon=\"ban\"\n                            loading={loading}\n                            style={{ marginLeft: '10px' }}\n                            onClick={this.onFiltersClear}\n                        />\n                    }\n                    content=\"Clear filters\"\n                />\n            </>\n        );\n    }\n\n    render() {\n        const {\n            processes,\n            paginationFilter,\n            usePagination = false,\n            columns,\n            projectName,\n            loading,\n            prev,\n            next\n        } = this.props;\n\n        const { processFilters } = this.state;\n\n        const showProjectColumn = !projectName;\n        const displayColumns =\n            columns || (showProjectColumn ? defaultColumns : withoutProjectColumns);\n\n        return (\n            <>\n                <div className={'container'}>\n                    <Table attached=\"top\" basic={true} style={{ borderBottom: 'none' }}>\n                        <Table.Header>\n                            <Table.Row>\n                                <Table.HeaderCell\n                                    collapsing={true}\n                                    style={{ borderBottom: 'none' }}>\n                                    <BulkProcessActionDropdown\n                                        data={this.state.selectedProcessIds}\n                                        refresh={this.onRefresh}\n                                    />\n                                </Table.HeaderCell>\n                                <Table.HeaderCell style={{ borderBottom: 'none' }}>\n                                    {hasFilter(processFilters, displayColumns) &&\n                                        this.renderFilterLabels(\n                                            displayColumns,\n                                            processFilters,\n                                            loading\n                                        )}\n                                </Table.HeaderCell>\n                                <Table.HeaderCell\n                                    collapsing={true}\n                                    style={{ fontWeight: 'normal', borderBottom: 'none' }}>\n                                    {usePagination && (\n                                        <PaginationToolBar\n                                            limit={paginationFilter?.limit}\n                                            handleLimitChange={(limit) =>\n                                                this.handleLimitChange(limit)\n                                            }\n                                            handleNext={this.handleNext}\n                                            handlePrev={this.handlePrev}\n                                            handleFirst={this.handleFirst}\n                                            disablePrevious={prev === undefined}\n                                            disableNext={next === undefined}\n                                            disableFirst={prev === undefined}\n                                        />\n                                    )}\n                                </Table.HeaderCell>\n                            </Table.Row>\n                        </Table.Header>\n                    </Table>\n                </div>\n\n                <ProcessList\n                    data={processes}\n                    columns={displayColumns}\n                    onSelectProcess={this.onSelectProcess}\n                    filterProps={processFilters}\n                    onFilterChange={this.onFilterChange}\n                />\n            </>\n        );\n    }\n}\n\nexport default ProcessListWithSearch;\n"
  },
  {
    "path": "console2/src/components/molecules/ProcessLogContainer/LogContainer.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { useState } from 'react';\nimport fetchLogData from '../../../api/process/log/fetchLogAsBlobURL';\nimport constate from \"constate\";\n\ntype logDetails = {\n    // Blob URL containing log data\n    url?: string;\n    // Array of checkpoint ids\n    anchors?: { [s: string]: string };\n};\n\n// Custom hook to provide log functionalities\nexport const useLog = () => {\n    // The process we load the log for\n    const [activeProcess, setActiveProcess] = useState('');\n    // The Anchor tag we will try to find\n    const [activeAnchor, setActiveAnchor] = useState('');\n    // Object for storing multiple logs at one time\n    const [logsById, setLogsById] = useState<{ [s: string]: logDetails }>({});\n\n    // Add a checkpoint Id to a process's anchors\n    const addCheckpointId = (processId: string, checkpointId: string) => {\n        // ? Does this keep old state?\n        setLogsById({ [processId]: { anchors: { [checkpointId]: checkpointId } } });\n    };\n\n    const queueScrollTo = (Id: string) => async () => {\n        const pauseTime = 500; // # of ms before trying again\n        const tryLimit = 30; // # of retries\n\n        // State Vars\n        let tryCount = 0;\n        let queuedToScroll = true;\n\n        // Loop till we find the dom element\n        while (queuedToScroll) {\n            const element = document.getElementById(Id);\n\n            // If element is found, scroll it into view\n            if (element) {\n                element.scrollIntoView({ behavior: 'auto', block: 'start', inline: 'nearest' });\n                queuedToScroll = false;\n            } else {\n                // Await on an Async timeout to resolve before trying again\n                await new Promise((resolve) => setTimeout(resolve, pauseTime));\n\n                // Prevent infinite loops and such.\n                tryCount++;\n                if (tryCount >= tryLimit) {\n                    queuedToScroll = false;\n                    console.warn(\n                        `Log either is taking a long time to load or the ID of ${Id} does not exist.`\n                    );\n                }\n            }\n        }\n    };\n    const fetchLog = (processId: string) => async () => {\n        const resp = await fetchLogData(processId);\n\n        setLogsById({\n            [processId]: {\n                url: resp\n            }\n        });\n    };\n\n    return {\n        activeProcess,\n        setActiveProcess,\n        activeAnchor,\n        setActiveAnchor,\n        logsById,\n        setLogsById,\n        addCheckpointId,\n        queueScrollTo,\n        fetchLog\n    };\n};\n\nexport const [LogProvider, useLogContext] = constate(useLog);\n\nexport default LogProvider;"
  },
  {
    "path": "console2/src/components/molecules/ProcessLogContainer/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport LogContainer from './LogContainer';\n\nexport default LogContainer;\n"
  },
  {
    "path": "console2/src/components/molecules/ProcessLogViewer/datetime.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { format as formatDate, parseISO as parseDate } from 'date-fns';\n\nexport const formatDateTime = (useLocalTime: boolean, showDate: boolean, s: string): string => {\n    if (!useLocalTime) {\n        return s;\n    }\n\n    // we expect the runtime to use \"yyyy-MM-dd'T'HH:mm:ss.SSSZ\" format for timestamps\n    // see also /common/src/main/java/com/walmartlabs/concord/common/LogUtils.java\n    const re = /^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}.\\d{4})\\s.*/gm;\n\n    const timestamps = [];\n    while (true) {\n        const m = re.exec(s);\n        if (!m) {\n            break;\n        }\n        timestamps.push(m[1]);\n    }\n\n    if (timestamps.length === 0) {\n        return s;\n    }\n\n    timestamps.forEach((src) => {\n        const d = parseDate(src);\n        const dst = formatDate(d, `${showDate ? 'yyyy-MM-dd ' : ''}HH:mm:ss`);\n        s = s.replace(src, dst);\n    });\n\n    return s;\n};\n"
  },
  {
    "path": "console2/src/components/molecules/ProcessLogViewer/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Button, Divider, Icon, Popup, Radio, Transition } from 'semantic-ui-react';\n\nimport { ConcordId } from '../../../api/common';\nimport { ProcessStatus } from '../../../api/process';\nimport { LogProcessorOptions } from '../../../state/data/processes/logs/processors';\nimport { LogSegment, LogSegmentType, TagData } from '../../../state/data/processes/logs/types';\nimport { ProcessToolbar } from '../../molecules';\nimport { TaskCallDetails } from '../../organisms';\n\nimport './styles.css';\n\ninterface State {\n    scrollAnchorRef: boolean;\n    anchorTagRefScrolled: boolean;\n    opts: LogProcessorOptions;\n    expandedItems: ConcordId[];\n}\n\ninterface Props {\n    instanceId: ConcordId;\n\n    processStatus?: ProcessStatus;\n    data: LogSegment[];\n    completed: boolean;\n    opts: LogProcessorOptions;\n    selectedCorrelationId?: string;\n\n    optsHandler: (opts: LogProcessorOptions) => void;\n\n    loadWholeLog: (opts: LogProcessorOptions) => void;\n}\n\ninterface LogContainerProps {\n    instanceId: ConcordId;\n    data: LogSegment[];\n    onClick: (correlationId: ConcordId) => void;\n    expandedItems: ConcordId[];\n    tagRefs: any[];\n}\n\nconst renderTagHeader = (\n    taskName: string,\n    tagRefs: any[],\n    idx: number,\n    expanded?: boolean,\n    onClick?: () => void,\n    correlationId?: ConcordId\n) => (\n    <>\n        <div\n            ref={\n                correlationId\n                    ? (element) => {\n                          tagRefs[correlationId] = element;\n                      }\n                    : undefined\n            }\n        />\n\n        <Divider\n            horizontal={true}\n            key={idx}\n            className={onClick ? 'clickableTagHeader' : undefined}\n            onClick={onClick}>\n            {taskName}\n            {onClick && <Icon name={expanded ? 'chevron up' : 'chevron down'} />}\n        </Divider>\n    </>\n);\n\nconst renderTag = (\n    instanceId: ConcordId,\n    tag: TagData,\n    tagRefs: any[],\n    onClick: () => void,\n    expanded: boolean,\n    idx: number\n) => {\n    if (tag.phase === 'post') {\n        return <Divider key={idx} />;\n    }\n\n    if (!tag.correlationId) {\n        return renderTagHeader(tag.taskName, tagRefs, idx);\n    }\n\n    return (\n        <div key={idx} className=\"logTagDetails\">\n            {renderTagHeader(tag.taskName, tagRefs, idx, expanded, onClick, tag.correlationId)}\n            {expanded && (\n                <TaskCallDetails instanceId={instanceId} correlationId={tag.correlationId} />\n            )}\n        </div>\n    );\n};\n\nconst LogContainer = ({ instanceId, data, tagRefs, onClick, expandedItems }: LogContainerProps) => (\n    <>\n        {data.map(({ data, type }, idx) => {\n            switch (type) {\n                case LogSegmentType.DATA: {\n                    return (\n                        <pre className=\"logEntry\" key={idx}>\n                            <div dangerouslySetInnerHTML={{ __html: data as string }} />\n                        </pre>\n                    );\n                }\n                case LogSegmentType.TAG: {\n                    const tag = data as TagData;\n                    const expanded = !!expandedItems.find((i) => i === tag.correlationId);\n                    return renderTag(\n                        instanceId,\n                        tag,\n                        tagRefs,\n                        () => onClick(tag.correlationId),\n                        expanded,\n                        idx\n                    );\n                }\n                default: {\n                    return `Unknown log segment type: ${type}`;\n                }\n            }\n        })}\n    </>\n);\n\nclass ProcessLogViewer extends React.Component<Props, State> {\n    private scrollAnchorRef: any;\n    private tagRefs: any[];\n\n    constructor(props: Props) {\n        super(props);\n\n        this.tagRefs = [];\n\n        this.state = {\n            scrollAnchorRef: false,\n            anchorTagRefScrolled: false,\n            opts: props.opts,\n            expandedItems: []\n        };\n\n        this.handleScroll = this.handleScroll.bind(this);\n        this.scrollToBottom = this.scrollToBottom.bind(this);\n        this.handleTagClick = this.handleTagClick.bind(this);\n        this.scrollToTag = this.scrollToTag.bind(this);\n    }\n\n    componentDidUpdate(prevProps: Props) {\n        const { data, selectedCorrelationId } = this.props;\n        const { scrollAnchorRef } = this.state;\n\n        if (prevProps.data !== data) {\n            if (scrollAnchorRef) {\n                this.scrollToBottom();\n            }\n\n            if (prevProps.selectedCorrelationId !== selectedCorrelationId) {\n                this.setState({ anchorTagRefScrolled: selectedCorrelationId === '' });\n            }\n\n            this.scrollToTag();\n        }\n    }\n\n    handleScroll(ev: any, { checked }: any) {\n        this.setState({\n            scrollAnchorRef: checked!\n        });\n\n        if (checked === true) {\n            this.scrollToBottom();\n        }\n    }\n\n    handleOptionsChange(k: keyof LogProcessorOptions, v: boolean) {\n        const { optsHandler } = this.props;\n        const { opts } = this.state;\n\n        const newOpts = { ...opts, [k]: v };\n\n        optsHandler(newOpts);\n\n        this.setState({ opts: newOpts });\n    }\n\n    handleTagClick(correlationId: ConcordId) {\n        let { expandedItems } = this.state;\n\n        const i = expandedItems.findIndex((i) => i === correlationId);\n        if (i < 0) {\n            expandedItems.push(correlationId);\n        } else {\n            expandedItems.splice(i, 1);\n        }\n\n        this.setState({ expandedItems });\n    }\n\n    scrollToBottom() {\n        this.scrollAnchorRef.scrollIntoView({ behavior: 'instant' });\n    }\n\n    scrollToTag() {\n        const { selectedCorrelationId } = this.props;\n        const { anchorTagRefScrolled } = this.state;\n\n        if (!anchorTagRefScrolled && selectedCorrelationId && this.tagRefs[selectedCorrelationId]) {\n            this.tagRefs[selectedCorrelationId].scrollIntoView({ behavior: 'instant' });\n            this.setState({ anchorTagRefScrolled: true });\n        }\n    }\n\n    renderSettingsMenu(opts: LogProcessorOptions) {\n        return (\n            <Popup\n                size=\"huge\"\n                position=\"bottom left\"\n                trigger={<Button basic={true} icon=\"setting\" style={{ marginRight: 20 }} />}\n                on=\"click\">\n                <div>\n                    <Radio\n                        label=\"Separate tasks\"\n                        toggle={true}\n                        checked={opts.separateTasks}\n                        onChange={(ev, data) =>\n                            this.handleOptionsChange('separateTasks', data.checked as boolean)\n                        }\n                    />\n                </div>\n\n                <Divider horizontal={true}>Timestamps</Divider>\n\n                <div>\n                    <Radio\n                        label=\"Use local time\"\n                        toggle={true}\n                        checked={opts.useLocalTime}\n                        onChange={(ev, data) =>\n                            this.handleOptionsChange('useLocalTime', data.checked as boolean)\n                        }\n                    />\n                </div>\n\n                <div>\n                    <Radio\n                        label=\"Show date\"\n                        toggle={true}\n                        checked={opts.showDate}\n                        onChange={(ev, data) =>\n                            this.handleOptionsChange('showDate', data.checked as boolean)\n                        }\n                    />\n                </div>\n            </Popup>\n        );\n    }\n\n    createLogToolbarActions() {\n        const { completed, loadWholeLog, instanceId, processStatus } = this.props;\n        const { opts, scrollAnchorRef } = this.state;\n\n        return (\n            <>\n                <Radio\n                    label=\"Auto-Scroll\"\n                    toggle={true}\n                    checked={scrollAnchorRef}\n                    disabled={processStatus === undefined}\n                    onChange={this.handleScroll}\n                    style={{ paddingRight: 20 }}\n                />\n\n                {this.renderSettingsMenu(opts)}\n\n                <Button.Group>\n                    {processStatus && !completed && (\n                        <Button onClick={() => loadWholeLog(opts)}>Show the whole log</Button>\n                    )}\n                    <Button\n                        disabled={!instanceId}\n                        onClick={() => window.open(`/api/v1/process/${instanceId}/log`, '_blank')}>\n                        Raw\n                    </Button>\n                </Button.Group>\n            </>\n        );\n    }\n\n    render() {\n        const { instanceId, data } = this.props;\n\n        const { expandedItems } = this.state;\n\n        return (\n            <>\n                <ProcessToolbar>{this.createLogToolbarActions()}</ProcessToolbar>\n\n                <LogContainer\n                    instanceId={instanceId}\n                    data={data}\n                    expandedItems={expandedItems}\n                    tagRefs={this.tagRefs}\n                    onClick={this.handleTagClick}\n                />\n\n                <div\n                    ref={(scroll) => {\n                        this.scrollAnchorRef = scroll;\n                    }}\n                />\n                <Transition animation=\"fade up\" duration={550}>\n                    <div\n                        className=\"scrollToTopButton\"\n                        onClick={() => {\n                            window.scrollTo({ top: 0 });\n                            this.setState({\n                                scrollAnchorRef: false\n                            });\n                        }}>\n                        <Icon name=\"chevron circle up\" size=\"huge\" />\n                    </div>\n                </Transition>\n            </>\n        );\n    }\n}\n\nexport default ProcessLogViewer;\n"
  },
  {
    "path": "console2/src/components/molecules/ProcessLogViewer/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n.logEntry {\n    font-family: monospace;\n    white-space: pre-wrap;\n    margin: 0;\n}\n\n.scrollToTopButton {\n    position: fixed;\n    bottom: 20px;\n    right: 20px;\n    cursor: pointer;\n    opacity: 1;\n    color: #2185D0;\n}\n\n.clickableTagHeader:hover {\n    color: gray;\n    cursor: zoom-in;\n}\n\n.logTagDetails {\n    margin-bottom: 20px;\n}\n"
  },
  {
    "path": "console2/src/components/molecules/ProcessStatusIcon/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Icon, Popup, SemanticICONS, SemanticCOLORS } from 'semantic-ui-react';\n\nimport { ProcessEntry, ProcessStatus } from '../../../api/process';\nimport { formatDistanceToNow, isAfter, parseISO as parseDate } from 'date-fns';\n\nenum AdditionalProcessStatus {\n    /**\n     * ENQUEUED + (startAt is not null)\n     */\n    SCHEDULED = 'SCHEDULED'\n}\n\nexport const statusToIcon: {\n    [status: string]: { name: SemanticICONS; color?: SemanticCOLORS; loading?: boolean };\n} = {\n    NEW: { name: 'inbox', color: 'grey' },\n    PREPARING: { name: 'info', color: 'blue' },\n    ENQUEUED: { name: 'block layout', color: 'grey' },\n    WAITING: { name: 'block layout', color: 'grey' },\n    SCHEDULED: { name: 'hourglass start', color: 'grey' },\n    RESUMING: { name: 'circle notched', color: 'grey', loading: true },\n    SUSPENDED: { name: 'wait', color: 'blue' },\n    STARTING: { name: 'circle notched', color: 'grey', loading: true },\n    RUNNING: { name: 'circle notched', color: 'blue', loading: true },\n    FINISHED: { name: 'check', color: 'green' },\n    FAILED: { name: 'remove', color: 'red' },\n    CANCELLED: { name: 'remove', color: 'grey' },\n    TIMED_OUT: { name: 'wait', color: 'red' }\n};\n\ntype Status = ProcessStatus | AdditionalProcessStatus;\n\ninterface ProcessStatusIconProps {\n    process: ProcessEntry;\n}\n\nconst getStatus = (process: ProcessEntry): Status => {\n    if (process.status === ProcessStatus.ENQUEUED && process.startAt !== undefined) {\n        return AdditionalProcessStatus.SCHEDULED;\n    }\n    return process.status;\n};\n\nconst getLabel = (process: ProcessEntry): string => {\n    if (process.startAt && process.status === ProcessStatus.ENQUEUED) {\n        const startAt = parseDate(process.startAt);\n        if (isAfter(startAt, Date.now())) {\n            return 'starts in ' + formatDistanceToNow(startAt);\n        }\n    }\n\n    return process.status;\n};\n\nexport default ({ process }: ProcessStatusIconProps) => {\n    const status = getStatus(process);\n\n    let i = statusToIcon[status];\n    if (!i) {\n        i = { name: 'question' };\n    }\n\n    return (\n        <Popup\n            trigger={\n                <Icon\n                    name={i.name}\n                    color={i.color}\n                    size={'large'}\n                    loading={i.loading}\n                    style={{ margin: 0 }}\n                />\n            }\n            content={getLabel(process)}\n            inverted={true}\n            position=\"top center\"\n        />\n    );\n};\n"
  },
  {
    "path": "console2/src/components/molecules/ProcessStatusTable/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport {Link} from 'react-router';\nimport {Grid, Label, Popup, Table} from 'semantic-ui-react';\n\nimport {getStatusSemanticColor, ProcessEntry, ProcessKind, ProcessStatus} from '../../../api/process';\nimport {formatDuration} from '../../../utils';\nimport {GitHubLink, LocalTimestamp, ProcessLastErrorModal} from '../../molecules';\nimport {TriggeredByPopup} from '../../organisms';\n\ninterface Props {\n    process?: ProcessEntry;\n}\n\nconst kindToDescription = (k: ProcessKind): string => {\n    switch (k) {\n        case ProcessKind.DEFAULT:\n            return 'Default';\n        case ProcessKind.FAILURE_HANDLER:\n            return 'onFailure handler';\n        case ProcessKind.CANCEL_HANDLER:\n            return 'onCancel handler';\n        case ProcessKind.TIMEOUT_HANDLER:\n            return 'onTimeout handler';\n        default:\n            return 'Unknown';\n    }\n};\n\nclass ProcessStatusTable extends React.PureComponent<Props> {\n    static renderCommitId(process?: ProcessEntry) {\n        if (!process || !process.commitId || !process.repoUrl) {\n            return ' - ';\n        }\n\n        return (\n            <GitHubLink\n                url={process.repoUrl}\n                commitId={process.commitId}\n                text={process.commitId}\n            />\n        );\n    }\n\n    static renderProcessKind(process?: ProcessEntry) {\n        if (!process) {\n            return '-';\n        }\n\n        return (\n            <>\n                {kindToDescription(process.kind)}\n                {process.kind === ProcessKind.FAILURE_HANDLER &&\n                    process.status !== ProcessStatus.FAILED && (\n                        <ProcessLastErrorModal\n                            processMeta={process.meta}\n                            title=\"Parent process' error\"\n                        />\n                    )}\n            </>\n        );\n    }\n\n    static renderTags(process?: ProcessEntry) {\n        if (!process) {\n            return '-';\n        }\n\n        const tags = process.tags;\n        if (!tags || tags.length === 0) {\n            return ' - ';\n        }\n\n        const items = tags.map((t) => <Link to={`/process?tags=${t}`}>{t}</Link>);\n\n        const result = [];\n        for (let i = 0; i < items.length; i++) {\n            result.push(items[i]);\n            if (i + 1 !== items.length) {\n                result.push(', ');\n            }\n        }\n\n        return result;\n    }\n\n    static renderTriggeredBy(process?: ProcessEntry) {\n        if (!process) {\n            return ' - ';\n        }\n\n        return <TriggeredByPopup entry={process} />;\n    }\n\n    static renderParentInstanceId(process?: ProcessEntry) {\n        if (!process || !process.parentInstanceId) {\n            return '-';\n        }\n\n        const parentId = process.parentInstanceId;\n        return <Link to={`/process/${parentId}`}>{parentId}</Link>;\n    }\n\n    static renderInitiator(process?: ProcessEntry) {\n        if (!process) {\n            return '-';\n        }\n\n        return process.initiator;\n    }\n\n    static renderCreatedAt(process?: ProcessEntry) {\n        if (!process) {\n            return '-';\n        }\n\n        return <LocalTimestamp value={process.createdAt} />;\n    }\n\n    static renderStartAt(process?: ProcessEntry) {\n        if (!process || !process.startAt) {\n            return '-';\n        }\n\n        return <LocalTimestamp value={process.startAt} />;\n    }\n\n    static renderLastUpdatedAt(process?: ProcessEntry) {\n        if (!process) {\n            return '-';\n        }\n\n        return <LocalTimestamp value={process.lastUpdatedAt} />;\n    }\n\n    static renderTimeout(process?: ProcessEntry) {\n        if (!process || (!process.timeout && !process.suspendTimeout)) {\n            return '-';\n        }\n\n        return (\n            <>\n                {process.timeout && (\n                    <Label key={'running'}>\n                        <Popup\n                            trigger={<span>running: {formatDuration(process.timeout * 1000)}</span>}\n                            content={`${process.timeout}s`}\n                        />\n                    </Label>\n                )}\n                {process.suspendTimeout && (\n                    <Label key={'suspended'}>\n                        <Popup\n                            trigger={\n                                <span>\n                                    suspended: {formatDuration(process.suspendTimeout * 1000)}\n                                </span>\n                            }\n                            content={`${process.suspendTimeout}s`}\n                        />\n                    </Label>\n                )}\n            </>\n        );\n    }\n\n    static renderProject(process?: ProcessEntry) {\n        if (!process || !process.projectName) {\n            return '-';\n        }\n\n        return (\n            <Link to={`/org/${process.orgName}/project/${process.projectName}`}>\n                {process.projectName}\n            </Link>\n        );\n    }\n\n    static renderRepo(process?: ProcessEntry) {\n        if (!process || !process.repoName) {\n            return '-';\n        }\n\n        return (\n            <Link\n                to={`/org/${process.orgName}/project/${process.projectName}/repository/${process.repoName}`}>\n                {process.repoName}\n            </Link>\n        );\n    }\n\n    static renderRepoUrl(process?: ProcessEntry) {\n        if (!process || !process.repoUrl) {\n            return '-';\n        }\n\n        return <GitHubLink url={process.repoUrl} text={process.repoUrl} />;\n    }\n\n    static renderRepoPath(process?: ProcessEntry) {\n        if (!process || !process.commitId || !process.repoUrl) {\n            return '-';\n        }\n\n        return (\n            <GitHubLink\n                url={process.repoUrl!}\n                commitId={process.commitId}\n                path={process.repoPath || '/'}\n                text={process.repoPath || '/'}\n            />\n        );\n    }\n\n    render() {\n        const { process } = this.props;\n\n        return (\n            <Grid columns={2} className={process ? '' : 'loading'}>\n                <Grid.Column>\n                    <Table\n                        definition={true}\n                        color={process ? getStatusSemanticColor(process.status) : 'grey'}\n                        style={{ height: '100%' }}>\n                        <Table.Body>\n                            <Table.Row>\n                                <Table.Cell collapsing={true} singleLine={true}>\n                                    Parent ID\n                                </Table.Cell>\n                                <Table.Cell>\n                                    {ProcessStatusTable.renderParentInstanceId(process)}\n                                </Table.Cell>\n                            </Table.Row>\n                            <Table.Row>\n                                <Table.Cell collapsing={true} singleLine={true}>\n                                    Initiator\n                                </Table.Cell>\n                                <Table.Cell>\n                                    {ProcessStatusTable.renderInitiator(process)}\n                                </Table.Cell>\n                            </Table.Row>\n                            <Table.Row>\n                                <Table.Cell>Type</Table.Cell>\n                                <Table.Cell>\n                                    {ProcessStatusTable.renderProcessKind(process)}\n                                </Table.Cell>\n                            </Table.Row>\n                            <Table.Row>\n                                <Table.Cell collapsing={true} singleLine={true}>\n                                    Created At\n                                </Table.Cell>\n                                <Table.Cell>\n                                    {ProcessStatusTable.renderCreatedAt(process)}\n                                </Table.Cell>\n                            </Table.Row>\n                            <Table.Row>\n                                <Table.Cell collapsing={true} singleLine={true}>\n                                    Start At\n                                </Table.Cell>\n                                <Table.Cell>{ProcessStatusTable.renderStartAt(process)}</Table.Cell>\n                            </Table.Row>\n                            <Table.Row>\n                                <Table.Cell collapsing={true} singleLine={true}>\n                                    Last Update\n                                </Table.Cell>\n                                <Table.Cell>\n                                    {ProcessStatusTable.renderLastUpdatedAt(process)}\n                                </Table.Cell>\n                            </Table.Row>\n                            <Table.Row style={{ height: '100%' }}>\n                                <Table.Cell collapsing={true} singleLine={true}>\n                                    Timeout\n                                </Table.Cell>\n                                <Table.Cell>{ProcessStatusTable.renderTimeout(process)}</Table.Cell>\n                            </Table.Row>\n                        </Table.Body>\n                    </Table>\n                </Grid.Column>\n                <Grid.Column>\n                    <Table\n                        definition={true}\n                        color={process ? getStatusSemanticColor(process.status) : 'grey'}\n                        style={{ height: '100%' }}>\n                        <Table.Body style={{ wordBreak: 'break-all' }}>\n                            <Table.Row>\n                                <Table.Cell collapsing={true} singleLine={true}>\n                                    Project\n                                </Table.Cell>\n                                <Table.Cell>{ProcessStatusTable.renderProject(process)}</Table.Cell>\n                            </Table.Row>\n                            <Table.Row>\n                                <Table.Cell collapsing={true} singleLine={true}>\n                                    Concord Repository\n                                </Table.Cell>\n                                <Table.Cell>{ProcessStatusTable.renderRepo(process)}</Table.Cell>\n                            </Table.Row>\n                            <Table.Row>\n                                <Table.Cell collapsing={true} singleLine={true}>\n                                    Repository URL\n                                </Table.Cell>\n                                <Table.Cell>{ProcessStatusTable.renderRepoUrl(process)}</Table.Cell>\n                            </Table.Row>\n                            <Table.Row>\n                                <Table.Cell collapsing={true} singleLine={true}>\n                                    Repository Path\n                                </Table.Cell>\n                                <Table.Cell>\n                                    {ProcessStatusTable.renderRepoPath(process)}\n                                </Table.Cell>\n                            </Table.Row>\n                            <Table.Row>\n                                <Table.Cell collapsing={true} singleLine={true}>\n                                    Commit ID\n                                </Table.Cell>\n                                <Table.Cell>\n                                    {ProcessStatusTable.renderCommitId(process)}\n                                </Table.Cell>\n                            </Table.Row>\n                            <Table.Row>\n                                <Table.Cell collapsing={true} singleLine={true}>\n                                    Process Tags\n                                </Table.Cell>\n                                <Table.Cell>{ProcessStatusTable.renderTags(process)}</Table.Cell>\n                            </Table.Row>\n                            <Table.Row>\n                                <Table.Cell collapsing={true} singleLine={true}>\n                                    Triggered By\n                                </Table.Cell>\n                                <Table.Cell>\n                                    {ProcessStatusTable.renderTriggeredBy(process)}\n                                </Table.Cell>\n                            </Table.Row>\n                        </Table.Body>\n                    </Table>\n                </Grid.Column>\n            </Grid>\n        );\n    }\n}\n\nexport default ProcessStatusTable;\n"
  },
  {
    "path": "console2/src/components/molecules/ProcessToolbar/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Menu } from 'semantic-ui-react';\n\nimport './styles.css';\n\ninterface ExternalProps {\n    children: React.ReactNode;\n}\n\nconst ProcessToolbar = ({ children }: ExternalProps) => {\n    return (\n        <Menu borderless={true} secondary={true} className={'processToolbar'}>\n            <Menu.Item style={{width: '100%'}}>{children}</Menu.Item>\n        </Menu>\n    );\n};\n\nexport default ProcessToolbar;\n"
  },
  {
    "path": "console2/src/components/molecules/ProcessToolbar/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n.processToolbar{\n    min-height: 0 !important;\n}\n\n.processToolbar .item{\n    padding-top: 0 !important;\n    padding-bottom: 0 !important;\n}"
  },
  {
    "path": "console2/src/components/molecules/ProcessWaitList/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\n\nimport {\n    ProcessLockCondition,\n    ProcessSleepCondition,\n    ProcessWaitCondition,\n    WaitCondition,\n    WaitType\n} from '../../../api/process';\nimport { Accordion, Icon, Table } from 'semantic-ui-react';\nimport { ConcordId } from '../../../api/common';\nimport { Link } from 'react-router';\nimport { LocalTimestamp } from '../index';\n\ninterface ExternalProps {\n    data?: WaitCondition[];\n}\n\nconst ProcessWaitList = ({ data }: ExternalProps) => {\n    return (\n        <Table celled={true} className={data ? '' : 'loading'}>\n            <Table.Header>{renderTableHeader()}</Table.Header>\n            <Table.Body>{renderElements(data)}</Table.Body>\n        </Table>\n    );\n};\n\nconst renderTableHeader = () => {\n    return (\n        <Table.Row>\n            <Table.HeaderCell collapsing={true}>Condition</Table.HeaderCell>\n            <Table.HeaderCell>Dependencies</Table.HeaderCell>\n        </Table.Row>\n    );\n};\n\nconst renderElements = (data?: WaitCondition[]) => {\n    if (!data) {\n        return (\n            <tr style={{ fontWeight: 'bold' }}>\n                <Table.Cell colSpan={3}>&nbsp;</Table.Cell>\n            </tr>\n        );\n    }\n\n    if (data.length === 0) {\n        return (\n            <tr style={{ fontWeight: 'bold' }}>\n                <Table.Cell colSpan={3}>No data available</Table.Cell>\n            </tr>\n        );\n    }\n\n    return data.map((p, index) => renderTableRow(index, p));\n};\n\nconst renderProcessLink = (id: ConcordId) => {\n    return (\n        <p>\n            <Link to={`/process/${id}`} key={id}>\n                {id}\n            </Link>\n        </p>\n    );\n};\n\nconst renderCondition = (condition: WaitCondition) => {\n    const type = condition.type;\n    const reason = condition.reason;\n\n    switch (type) {\n        case WaitType.NONE: {\n            return (\n                <>\n                    <Icon name=\"check\" /> No wait conditions\n                </>\n            );\n        }\n        case WaitType.PROCESS_COMPLETION: {\n            return (\n                <>\n                    <Icon name=\"hourglass half\" />\n                    Waiting for the process to complete\n                    {reason && ` (${reason})`}\n                </>\n            );\n        }\n        case WaitType.PROCESS_LOCK: {\n            const lockPayload = condition as ProcessLockCondition;\n            return (\n                <>\n                    <Icon name=\"hourglass half\" />\n                    Waiting for the lock ({lockPayload.name})\n                </>\n            );\n        }\n        case WaitType.PROCESS_SLEEP: {\n            const sleepPayload = condition as ProcessSleepCondition;\n            return (\n                <>\n                    <Icon name=\"hourglass half\" />\n                    Waiting until ({<LocalTimestamp value={sleepPayload.until} />})\n                </>\n            );\n        }\n        default:\n            return type;\n    }\n};\n\nconst renderProcessWaitDetails = (condition: ProcessWaitCondition) => {\n    if (condition.processes === undefined || condition.processes.length === 0) {\n        return <></>;\n    } else if (condition.processes.length === 1) {\n        return renderProcessLink(condition.processes[0]);\n    }\n\n    const panels = [\n        {\n            key: 'k1',\n            title: {\n                content: (\n                    <Link to={`/process/${condition.processes[0]}`} key={condition.processes[0]}>\n                        {condition.processes[0]}\n                    </Link>\n                ),\n                style: { padding: 0 }\n            },\n            content: [condition.processes.slice(1).map((id) => renderProcessLink(id))]\n        }\n    ];\n    return <Accordion panels={panels} />;\n};\n\nconst renderProcessLockDetails = (payload: ProcessLockCondition) => {\n    return renderProcessLink(payload.instanceId);\n};\n\nconst renderDependencies = (condition: WaitCondition) => {\n    const type = condition.type;\n\n    switch (type) {\n        case WaitType.PROCESS_COMPLETION: {\n            return renderProcessWaitDetails(condition as ProcessWaitCondition);\n        }\n        case WaitType.PROCESS_LOCK: {\n            return renderProcessLockDetails(condition as ProcessLockCondition);\n        }\n        default:\n            return '';\n    }\n};\n\nconst renderTableRow = (idx: number, row: WaitCondition) => {\n    return (\n        <Table.Row key={idx} verticalAlign=\"top\">\n            <Table.Cell collapsing={true}>{renderCondition(row)}</Table.Cell>\n            <Table.Cell>{renderDependencies(row)}</Table.Cell>\n        </Table.Row>\n    );\n};\n\nexport default ProcessWaitList;\n"
  },
  {
    "path": "console2/src/components/molecules/ProjectConfiguration/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport React, { useRef, useState } from 'react';\nimport { Button } from 'semantic-ui-react';\nimport _ from 'lodash';\nimport './styles.css';\nimport LoadingEditor from '../LoadingEditor';\n\ninterface Props {\n    config?: Object;\n    submitting: boolean;\n    submit: (config: Object) => void;\n}\n\nconst ProjectConfiguration: React.FunctionComponent<Props> = ({ config, submitting, submit }) => {\n    const [isEditorReady, setIsEditorReady] = useState(false);\n    const [jsonError, setJsonError] = useState('');\n    const valueGetter = useRef();\n\n    const handleEditorDidMount = (_valueGetter: any) => {\n        setIsEditorReady(true);\n        valueGetter.current = _valueGetter;\n    };\n\n    const handleSubmit = () => {\n        if (valueGetter && valueGetter.current) {\n            try {\n                // @ts-ignore: Cannot invoke an object which is possibly 'undefined'.\n                const jsonObj = JSON.parse(valueGetter.current());\n                setJsonError('');\n                if (!_.isEqual(jsonObj, config)) {\n                    submit(jsonObj);\n                } else {\n                    setJsonError('No changes detected');\n                }\n            } catch (error) {\n                setJsonError(error.message);\n            }\n        }\n    };\n\n    const loading = submitting || !isEditorReady;\n    return (\n        <div className={'projectEditorContainer'}>\n            <LoadingEditor\n                language=\"json\"\n                initValue={JSON.stringify(config, null, 4)}\n                handleEditorDidMount={handleEditorDidMount}\n                disabled={loading}\n            />\n            <div style={{ width: '130px', marginLeft: '20px' }}>\n                <Button\n                    primary={true}\n                    content=\"Save\"\n                    disabled={!isEditorReady}\n                    loading={submitting}\n                    onClick={handleSubmit}\n                    style={{ width: '130px' }}\n                />\n                {jsonError && <p style={{ color: 'red', marginTop: '15px' }}>{jsonError}</p>}\n            </div>\n        </div>\n    );\n};\n\nexport default ProjectConfiguration;\n"
  },
  {
    "path": "console2/src/components/molecules/ProjectConfiguration/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n.projectEditorContainer {\n    display: flex;\n    height: 75vh;\n    min-height: 500px;\n}\n\n.projectEditorContainer .loader {\n    display: flex;\n    position: relative;\n    text-align: initial;\n    width: 100%;\n    height: 100%;\n}\n"
  },
  {
    "path": "console2/src/components/molecules/ProjectRenameForm/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { InjectedFormikProps, withFormik } from 'formik';\nimport * as React from 'react';\nimport { Confirm, Form } from 'semantic-ui-react';\nimport { ConcordKey } from '../../../api/common';\nimport { isProjectExists } from '../../../api/service/console';\nimport { notEmpty } from '../../../utils';\nimport { project as validation, projectAlreadyExistsError } from '../../../validation';\nimport { FormikInput } from '../../atoms';\n\ninterface State {\n    showConfirm: boolean;\n}\n\ninterface FormValues {\n    name: ConcordKey;\n}\n\ninterface Props {\n    orgName: ConcordKey;\n    initial: FormValues;\n    submitting: boolean;\n    onSubmit: (values: FormValues) => void;\n}\n\nclass ProjectRenameForm extends React.Component<InjectedFormikProps<Props, FormValues>, State> {\n    constructor(props: InjectedFormikProps<Props, FormValues>) {\n        super(props);\n        this.state = { showConfirm: false };\n    }\n\n    handleShowConfirm(ev: React.SyntheticEvent<{}>) {\n        ev.preventDefault();\n        this.setState({ showConfirm: true });\n    }\n\n    handleCancel() {\n        this.setState({ showConfirm: false });\n    }\n\n    handleConfirm() {\n        this.props.submitForm();\n    }\n\n    render() {\n        const { dirty, handleSubmit, submitting } = this.props;\n        const hasErrors = notEmpty(this.props.errors);\n\n        return (\n            <Form onSubmit={handleSubmit} loading={submitting}>\n                <FormikInput name=\"name\" placeholder=\"Project name\" />\n\n                <Form.Button\n                    primary={true}\n                    negative={true}\n                    content=\"Rename\"\n                    disabled={hasErrors || !dirty}\n                    onClick={(ev) => this.handleShowConfirm(ev)}\n                />\n\n                <Confirm\n                    open={this.state.showConfirm}\n                    header=\"Rename the project?\"\n                    content=\"Are you sure you want to rename the project?\"\n                    onConfirm={() => this.handleConfirm()}\n                    onCancel={() => this.handleCancel()}\n                />\n            </Form>\n        );\n    }\n}\n\nconst validator = async (values: FormValues, props: Props) => {\n    let e;\n\n    e = validation.name(values.name);\n    if (e) {\n        return Promise.resolve({ name: e });\n    }\n\n    if (values.name !== props.initial.name) {\n        const exists = await isProjectExists(props.orgName, values.name);\n        if (exists) {\n            return Promise.resolve({ name: projectAlreadyExistsError(values.name) });\n        }\n    }\n\n    return {};\n};\n\nexport default withFormik<Props, FormValues>({\n    handleSubmit: (values, bag) => {\n        bag.props.onSubmit(values);\n    },\n    mapPropsToValues: (props) => props.initial,\n    validate: validator\n})(ProjectRenameForm);\n"
  },
  {
    "path": "console2/src/components/molecules/RepositoryForm/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { Field, getIn, InjectedFormikProps, withFormik } from 'formik';\nimport * as React from 'react';\nimport { Button, Divider, Form, Label, Popup, Segment } from 'semantic-ui-react';\n\nimport { ConcordId, ConcordKey } from '../../../api/common';\nimport { isRepositoryExists } from '../../../api/service/console';\nimport { notEmpty } from '../../../utils';\nimport { repository as validation, repositoryAlreadyExistsError } from '../../../validation';\nimport { FormikCheckbox, FormikDropdown, FormikInput } from '../../atoms';\nimport { SecretSearch } from '../../organisms';\nimport { FieldProps } from 'formik/dist/Field';\n\nexport enum RepositorySourceType {\n    BRANCH_OR_TAG = 'branchOrTag',\n    COMMIT_ID = 'commitId'\n}\n\ninterface FormValues {\n    id?: ConcordId;\n    name: string;\n    url: string;\n    sourceType: RepositorySourceType;\n    branch?: string;\n    commitId?: string;\n    path?: string;\n    withSecret?: boolean;\n    secretId?: string;\n    secretName?: string;\n    enabled: boolean;\n    triggersEnabled: boolean;\n}\n\nexport type RepositoryFormValues = FormValues;\n\ninterface Props {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    initial: FormValues;\n    submitting: boolean;\n    editMode?: boolean;\n    onSubmit: (values: FormValues, setSubmitting: (isSubmitting: boolean) => void) => void;\n    testRepository: (values: FormValues) => Promise<void>;\n}\n\ninterface State {\n    testRunning: boolean;\n    testSuccess: boolean;\n    testError?: string;\n    testWarning?: string;\n}\n\nconst sourceOptions = [\n    {\n        text: 'Branch/tag/version',\n        value: RepositorySourceType.BRANCH_OR_TAG\n    },\n    {\n        text: 'Commit ID',\n        value: RepositorySourceType.COMMIT_ID\n    }\n];\n\nconst sanitize = (data: FormValues): FormValues => {\n    const v = { ...data };\n\n    if (v.path === '') {\n        v.path = undefined;\n    }\n\n    if (v.branch === '') {\n        v.branch = undefined;\n    }\n\n    if (v.commitId === '') {\n        v.commitId = undefined;\n    }\n\n    if (v.withSecret === false) {\n        v.secretId = undefined;\n    }\n\n    return v;\n};\n\nclass RepositoryForm extends React.Component<InjectedFormikProps<Props, FormValues>, State> {\n    constructor(props: InjectedFormikProps<Props, FormValues>) {\n        super(props);\n        this.state = { testRunning: false, testSuccess: false };\n    }\n\n    handleTestConnection() {\n        const { values, testRepository } = this.props;\n        this.setState({ testRunning: true, testSuccess: false, testError: '', testWarning: '' });\n\n        testRepository(sanitize(values))\n            .then(() => {\n                this.setState({\n                    testSuccess: true,\n                    testRunning: false\n                });\n            })\n            .catch((e) => {\n                this.setState({\n                    testSuccess: false,\n                    testRunning: false,\n                    testError: e.details && e.level !== 'WARN' ? e.details : e.message,\n                    testWarning: e.level === 'WARN' ? e.details : ''\n                });\n            });\n    }\n\n    render() {\n        const {\n            orgName,\n            handleSubmit,\n            values,\n            errors,\n            dirty,\n            editMode = false,\n            isValid\n        } = this.props;\n\n        const hasErrors = notEmpty(errors);\n        const testConnectionDisabled = dirty && (!isValid || hasErrors);\n\n        return (\n            <>\n                <Form onSubmit={handleSubmit}>\n                    <FormikCheckbox name=\"enabled\" label=\"Enabled\" toggle={true} inline={true} />\n\n                    <FormikInput\n                        name=\"name\"\n                        label=\"Name\"\n                        placeholder=\"Repository name\"\n                        required={true}\n                    />\n\n                    <Segment>\n                        <Popup\n                            trigger={\n                                <FormikCheckbox\n                                    name=\"withSecret\"\n                                    label=\"Custom authentication\"\n                                    toggle={true}\n                                    inline={true}\n                                />\n                            }>\n                            <Popup.Content>\n                                Personal repositories require additional authentication - a SSH key,\n                                a username/password pair or an OAuth (personal) token\n                            </Popup.Content>\n                        </Popup>\n\n                        {values.withSecret && (\n                            <>\n                                <Field name={'secretId'}>\n                                    {({ field, form }: FieldProps) => {\n                                        const fieldName = 'secretId';\n                                        const touched = getIn(form.touched, fieldName);\n                                        const error = getIn(form.errors, fieldName);\n                                        const invalid = !!(touched && error);\n\n                                        return (\n                                            <Form.Field error={invalid} required={true}>\n                                                <label>Credentials</label>\n                                                <SecretSearch\n                                                    orgName={orgName}\n                                                    placeholder={'Search for a secret...'}\n                                                    fluid={true}\n                                                    defaultSecretName={form.values.secretName}\n                                                    invalid={invalid}\n                                                    onBlur={(value) => {\n                                                        form.setFieldTouched(fieldName, true);\n                                                        form.setFieldValue(fieldName, value?.id);\n                                                    }}\n                                                    onSelect={(value) => {\n                                                        form.setFieldValue(fieldName, value.id);\n                                                    }}\n                                                />\n\n                                                {invalid && error && (\n                                                    <Label basic={true} pointing={true} color=\"red\">\n                                                        {error}\n                                                    </Label>\n                                                )}\n                                            </Form.Field>\n                                        );\n                                    }}\n                                </Field>\n                            </>\n                        )}\n\n                        <FormikInput name=\"url\" label=\"URL\" placeholder=\"Git URL\" required={true} />\n                    </Segment>\n\n                    <Form.Group widths=\"equal\">\n                        <FormikDropdown\n                            name=\"sourceType\"\n                            label=\"Source\"\n                            selection={true}\n                            options={sourceOptions}\n                        />\n\n                        {values.sourceType === RepositorySourceType.BRANCH_OR_TAG && (\n                            <FormikInput\n                                name=\"branch\"\n                                label=\"Branch/Tag/Version\"\n                                fluid={true}\n                                required={true}\n                            />\n                        )}\n\n                        {values.sourceType === RepositorySourceType.COMMIT_ID && (\n                            <FormikInput\n                                name=\"commitId\"\n                                label=\"Commit ID\"\n                                fluid={true}\n                                required={true}\n                            />\n                        )}\n                    </Form.Group>\n\n                    <Popup\n                        trigger={\n                            <FormikInput name=\"path\" label=\"Path\" placeholder=\"Repository path\" />\n                        }>\n                        <Popup.Content>\n                            (Optional) Path in the repository that will be used as the root\n                            directory.\n                        </Popup.Content>\n                    </Popup>\n\n                    <FormikCheckbox\n                        name=\"triggersEnabled\"\n                        label=\"Enable Triggers\"\n                        toggle={true}\n                        inline={true}\n                    />\n\n                    <Divider />\n\n                    <Button\n                        primary={true}\n                        type=\"submit\"\n                        disabled={!dirty || hasErrors || this.state.testRunning || this.props.isSubmitting}\n                        loading={this.props.isSubmitting}>\n                        {editMode ? 'Save' : 'Add'}\n                    </Button>\n\n                    <Popup\n                        trigger={\n                            <Button\n                                basic={true}\n                                positive={this.state.testSuccess}\n                                negative={!!this.state.testError}\n                                floated=\"right\"\n                                loading={this.state.testRunning}\n                                disabled={testConnectionDisabled}\n                                onClick={(ev) => {\n                                    ev.preventDefault();\n                                    this.handleTestConnection();\n                                }}>\n                                Test connection\n                            </Button>\n                        }\n                        open={\n                            (!!this.state.testError || !!this.state.testWarning) &&\n                            !this.props.isSubmitting\n                        }\n                        wide={true}>\n                        {!!this.state.testWarning && (\n                            <Popup.Content>\n                                <p style={{ color: 'orange' }}>Warning: {this.state.testWarning}</p>\n                            </Popup.Content>\n                        )}\n                        {!!this.state.testError && (\n                            <Popup.Content>\n                                <p style={{ color: 'red' }}>Error: {this.state.testError}</p>\n                            </Popup.Content>\n                        )}\n                    </Popup>\n                </Form>\n            </>\n        );\n    }\n}\n\nconst validator = async (values: FormValues, props: Props) => {\n    let e;\n\n    e = validation.name(values.name);\n    if (e) {\n        return Promise.resolve({ name: e });\n    }\n\n    if (values.name !== props.initial.name) {\n        const exists = await isRepositoryExists(props.orgName, props.projectName, values.name);\n        if (exists) {\n            return Promise.resolve({ name: repositoryAlreadyExistsError(values.name) });\n        }\n    }\n\n    e = validation.url(values.url);\n    if (e) {\n        return Promise.resolve({ url: e });\n    }\n\n    switch (values.sourceType) {\n        case RepositorySourceType.BRANCH_OR_TAG:\n            e = validation.branch(values.branch);\n            if (e) {\n                return Promise.resolve({ branch: e });\n            }\n            break;\n        case RepositorySourceType.COMMIT_ID:\n            e = validation.commitId(values.commitId);\n            if (e) {\n                return Promise.resolve({ commitId: e });\n            }\n            break;\n        default:\n            throw new Error(`Unknown repository source type: ${values.sourceType}`);\n    }\n\n    e = validation.path(values.path);\n    if (e) {\n        return Promise.resolve({ path: e });\n    }\n\n    if (!values.withSecret) {\n        if (!values.url.startsWith('https://') && !values.url.startsWith('mvn://')) {\n            return Promise.resolve({\n                url:\n                    \"Invalid repository URL: must begin with 'https://' or 'mvn://'. SSH repository URLs require additional credentials to be specified.\"\n            });\n        }\n    } else {\n        e = validation.secretId(values.secretId);\n        if (e) {\n            return Promise.resolve({ secretId: e });\n        }\n    }\n\n    return {};\n};\n\nexport default withFormik<Props, FormValues>({\n    handleSubmit: (values, bag) => {\n        bag.props.onSubmit(sanitize(values), bag.setSubmitting);\n    },\n    mapPropsToValues: (props) => ({\n        ...props.initial,\n        withSecret: !!props.initial.secretId\n    }),\n    validate: validator,\n    enableReinitialize: true\n})(RepositoryForm);\n"
  },
  {
    "path": "console2/src/components/molecules/RepositoryList/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Link } from 'react-router';\nimport { Icon, Table, Popup } from 'semantic-ui-react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { RepositoryEntry, TriggerEntry } from '../../../api/org/project/repository';\nimport { GitHubLink } from '../../molecules';\nimport { RepositoryActionDropdown } from '../../organisms';\nimport { gitUrlParse } from \"../GitHubLink\";\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    data?: RepositoryEntry[];\n    triggerMap : {[id: string] : TriggerEntry[]}\n    loading: boolean;\n    refresh: () => void;\n}\n\nconst RepositoryList = ({ orgName, projectName, data, loading, refresh, triggerMap }: ExternalProps) => {\n    return (\n        <div style={{ overflowX: 'auto', width: '100%', height: '100%' }}>\n            <Table striped>\n                <Table.Header>\n                    <Table.Row>\n                        <Table.HeaderCell collapsing={true} />\n                        <Table.HeaderCell collapsing={true}>Name</Table.HeaderCell>\n                        <Table.HeaderCell>Repository URL</Table.HeaderCell>\n                        <Table.HeaderCell collapsing={true}>Branch/Commit ID/Version</Table.HeaderCell>\n                        <Table.HeaderCell singleLine={true}>Path</Table.HeaderCell>\n                        <Table.HeaderCell collapsing={true} style={{ width: '8%' }}>\n                            Secret\n                        </Table.HeaderCell>\n                        <Table.HeaderCell\n                            collapsing={true}\n                            colSpan={2}\n                            style={{ width: '1%', textAlign: 'center' }}>\n                            Execute\n                        </Table.HeaderCell>\n                    </Table.Row>\n                </Table.Header>\n                <Table.Body>\n                    {!loading && data?.length === 0 && (\n                        <tr style={{ fontWeight: 'bold' }}>\n                            <Table.Cell> </Table.Cell>\n                            <Table.Cell colSpan={6}>No repositories found</Table.Cell>\n                        </tr>\n                    )}\n                    {data?.map((r) => renderTableRow(orgName, projectName, r, triggerMap[r.id], refresh))}\n                </Table.Body>\n            </Table>\n        </div>\n    );\n};\n\nconst renderRepoPath = (r: RepositoryEntry) => {\n    const urlLink = gitUrlParse(r.url);\n    if (!urlLink) {\n        return r.path;\n    }\n    if (r.commitId) {\n        return (\n            <GitHubLink\n                link={urlLink}\n                url={r.url!}\n                commitId={r.commitId}\n                path={r.path || '/'}\n                text={r.path || '/'}\n            />\n        );\n    }\n    return <GitHubLink url={r.url!} link={urlLink} branch={r.branch} path={r.path || '/'} text={r.path || '/'} />;\n};\n\nconst renderRepoCommitIdOrBranch = (r: RepositoryEntry) => {\n\n    const urlLink = gitUrlParse(r.url);\n    if (!urlLink) {\n        return r.branch;\n    }\n\n    if (r.commitId) {\n        return <GitHubLink url={r.url!} link={urlLink} commitId={r.commitId} text={r.commitId}/>;\n    }\n    return <GitHubLink url={r.url!} link={urlLink} branch={r.branch} text={r.branch}/>;\n};\n\nconst renderTableRow = (\n    orgName: ConcordKey,\n    projectName: ConcordKey,\n    row: RepositoryEntry,\n    triggerData: TriggerEntry[],\n    refresh: () => void\n) => {\n    return (\n        <Table.Row key={row.id}>\n            <Table.Cell>\n                <Popup\n                    trigger={\n                        <Icon\n                            name={row.disabled ? 'power off' : 'power'}\n                            color={row.disabled ? 'grey' : 'green'}\n                        />\n                    }>\n                    {row.disabled ? 'Disabled' : 'Enabled'}\n                </Popup>\n            </Table.Cell>\n            <Table.Cell singleLine={true}>\n                <Link to={`/org/${orgName}/project/${projectName}/repository/${row.name}`}>\n                    {row.name}\n                </Link>\n            </Table.Cell>\n            <Table.Cell>\n                <GitHubLink url={row.url} text={row.url} />\n            </Table.Cell>\n            <Table.Cell>{renderRepoCommitIdOrBranch(row)}</Table.Cell>\n            <Table.Cell>{renderRepoPath(row)}</Table.Cell>\n            <Table.Cell>{row.secretName}</Table.Cell>\n            <RepositoryActionDropdown\n                orgName={orgName}\n                projectName={projectName}\n                repo={row}\n                triggerData={triggerData}\n                refresh={refresh}\n            />\n        </Table.Row>\n    );\n};\n\nexport default RepositoryList;\n"
  },
  {
    "path": "console2/src/components/molecules/RequestErrorMessage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Link } from 'react-router';\nimport { Message } from 'semantic-ui-react';\n\nimport { RequestError } from '../../../api/common';\n\ninterface Props {\n    error: RequestError;\n}\n\nexport default class extends React.PureComponent<Props> {\n    render() {\n        const { error } = this.props;\n\n        if (!error) {\n            return <p>No error</p>;\n        }\n\n        const details = error.details && error.details.length > 0 ? error.details : undefined;\n\n        return (\n            <Message negative={true} error={true}>\n                {error.message && <Message.Header>{error.message}</Message.Header>}\n                {details && details.split('\\n').map((item, i) => <p key={i}>{item}</p>)}\n                {error.instanceId && (\n                    <p>\n                        <Link to={`/process/${error.instanceId}/log`}>Open the process log</Link>\n                    </p>\n                )}\n            </Message>\n        );\n    }\n}\n"
  },
  {
    "path": "console2/src/components/molecules/SingleOperationPopup/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Button, Header, Modal, Icon, SemanticICONS, SemanticCOLORS } from 'semantic-ui-react';\n\nimport { RequestError, RequestErrorData } from '../../../api/common';\nimport { RequestErrorMessage } from '../../molecules';\n\ninterface State {\n    open: boolean;\n}\n\nexport type OnClickFn = () => void;\n\ninterface Props {\n    trigger: (onClick: OnClickFn) => React.ReactNode;\n\n    title: string;\n    introMsg: React.ReactNode;\n    icon?: SemanticICONS;\n    iconColor?: SemanticCOLORS;\n    customStyle?: object; // CSS Object\n\n    customNo?: string;\n    customYes?: string;\n\n    running: boolean;\n    runningMsg?: React.ReactNode;\n\n    success: boolean;\n    successMsg?: React.ReactNode;\n\n    error?: RequestError;\n    errorRenderer?: (error: RequestErrorData) => React.ReactNode;\n\n    reset?: () => void;\n    onConfirm: () => void;\n    onDone?: () => void;\n    onOpen?: () => void;\n\n    onDoneElements?: () => React.ReactNode;\n\n    disableYes?: boolean;\n}\n\nclass SingleOperationPopup extends React.Component<Props, State> {\n    constructor(props: Props) {\n        super(props);\n        this.state = { open: false };\n\n        this.handleOpen = this.handleOpen.bind(this);\n        this.handleClose = this.handleClose.bind(this);\n        this.handleConfirm = this.handleConfirm.bind(this);\n        this.renderContent = this.renderContent.bind(this);\n        this.renderActions = this.renderActions.bind(this);\n        this.stopPropagation = this.stopPropagation.bind(this);\n    }\n\n    handleOpen(event?: React.SyntheticEvent) {\n        this.stopPropagation(event);\n        this.props.reset && this.props.reset();\n        this.setState({ open: true });\n    }\n\n    handleClose() {\n        this.setState({ open: false });\n    }\n\n    handleConfirm() {\n        this.props.onConfirm();\n    }\n\n    renderContent() {\n        const {\n            success,\n            successMsg,\n            error,\n            errorRenderer,\n            running,\n            runningMsg,\n            introMsg\n        } = this.props;\n\n        if (success) {\n            return successMsg ? successMsg : <p>The operation was completed successfully.</p>;\n        }\n\n        if (error) {\n            if (errorRenderer) {\n                return errorRenderer(error);\n            }\n            return <RequestErrorMessage error={error} />;\n        }\n\n        if (running) {\n            return runningMsg ? runningMsg : <p>Processing the request...</p>;\n        }\n\n        return introMsg;\n    }\n\n    renderActions() {\n        const {\n            success,\n            error,\n            running,\n            onDone,\n            onDoneElements,\n            customNo,\n            customYes,\n            disableYes = false\n        } = this.props;\n\n        if (success) {\n            return (\n                <>\n                    {onDoneElements && onDoneElements()}\n                    <Button\n                        color=\"green\"\n                        onClick={() => {\n                            this.handleClose();\n                            if (onDone) {\n                                onDone();\n                            }\n                        }}>\n                        Done\n                    </Button>\n                </>\n            );\n        }\n\n        if (error) {\n            return (\n                <>\n                    <Button onClick={() => this.handleClose()}>Close</Button>\n                    <Button onClick={() => this.handleConfirm()}>Retry</Button>\n                </>\n            );\n        }\n\n        return (\n            <>\n                <Button basic={true} disabled={running} onClick={() => this.handleClose()}>\n                    {customNo || 'No'}\n                </Button>\n                <Button\n                    color=\"blue\"\n                    loading={running}\n                    onClick={() => this.handleConfirm()}\n                    disabled={disableYes}>\n                    {customYes || 'Yes'}\n                </Button>\n            </>\n        );\n    }\n\n    stopPropagation(event?: React.SyntheticEvent) {\n        if (event) {\n            event.stopPropagation();\n        }\n    }\n\n    render() {\n        const { onOpen, trigger, title, icon, iconColor, customStyle = {} } = this.props;\n\n        return (\n            <Modal\n                onClick={this.stopPropagation}\n                onClose={this.stopPropagation}\n                onOpen={onOpen}\n                style={customStyle}\n                open={this.state.open}\n                dimmer=\"inverted\"\n                trigger={trigger(() => this.handleOpen())}>\n                {/* TODO: Header padding CSS */}\n                <Header>\n                    <Icon name={icon} color={iconColor || 'black'} />\n                    <Header.Content>{title}</Header.Content>\n                </Header>\n\n                <Modal.Content>{this.renderContent()}</Modal.Content>\n                <Modal.Actions>{this.renderActions()}</Modal.Actions>\n            </Modal>\n        );\n    }\n}\n\nexport default SingleOperationPopup;\n"
  },
  {
    "path": "console2/src/components/molecules/TeamAccessDropdown/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Dropdown, DropdownItemProps, DropdownProps } from 'semantic-ui-react';\n\nimport { ResourceAccessLevel } from '../../../api/org';\n\ninterface Props extends DropdownProps {\n    onRoleChange: (value: ResourceAccessLevel) => void;\n}\n\nconst options: DropdownItemProps[] = [\n    { text: 'Writer', value: ResourceAccessLevel.WRITER },\n    { text: 'Reader', value: ResourceAccessLevel.READER },\n    { text: 'Owner', value: ResourceAccessLevel.OWNER }\n];\n\nclass TeamAccessDropdown extends React.PureComponent<Props> {\n    render() {\n        const { onRoleChange, ...rest } = this.props;\n        return (\n            <Dropdown\n                selection={true}\n                options={options}\n                onChange={(ev, data) => onRoleChange(ResourceAccessLevel[data.value as string])}\n                {...rest}\n            />\n        );\n    }\n}\n\nexport default TeamAccessDropdown;\n"
  },
  {
    "path": "console2/src/components/molecules/TeamAccessList/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Button, Container, Menu, Form, Table } from 'semantic-ui-react';\nimport { ResourceAccessLevel, ResourceAccessEntry } from '../../../api/org';\nimport { TeamAccessDropdown } from '../../molecules';\nimport { FindTeamDropdown } from '../../organisms';\nimport { ConcordKey } from '../../../api/common';\nimport { TeamEntry } from '../../../api/org/team';\n\ninterface Entry extends ResourceAccessEntry {\n    added: boolean;\n    deleted: boolean;\n}\n\ninterface State {\n    data: Entry[];\n    dirty: boolean;\n    editMode: boolean;\n}\n\ninterface Props {\n    data: ResourceAccessEntry[];\n    submitting: boolean;\n    orgName: ConcordKey;\n    submit: (entries: ResourceAccessEntry[]) => void;\n}\n\nconst toState = (data: ResourceAccessEntry[]): Entry[] =>\n    data.map((e) => ({ ...e, added: false, deleted: false }));\n\nclass TeamAccessList extends React.Component<Props, State> {\n    constructor(props: Props) {\n        super(props);\n        this.state = { data: toState(props.data), dirty: false, editMode: false };\n    }\n\n    handleEditMode() {\n        this.setState({ editMode: true });\n    }\n\n    handleCancelEdit() {\n        this.setState({ data: toState(this.props.data), dirty: false, editMode: false });\n    }\n\n    handleRoleChange(idx: number, level: ResourceAccessLevel) {\n        const { data } = this.state;\n        data[idx].level = level;\n        this.setState({ data, dirty: true });\n    }\n\n    handleDelete(idx: number) {\n        const { data } = this.state;\n        data[idx].deleted = !data[idx].deleted;\n        this.setState({ data, dirty: true });\n    }\n\n    handleAddTeam(u: TeamEntry) {\n        if (!u.name) {\n            return;\n        }\n        const { data } = this.state;\n        const e = {\n            teamId: u.id,\n            teamName: u.name,\n            level: ResourceAccessLevel.READER,\n            added: true,\n            deleted: false\n        };\n        this.setState({ data: [e, ...data], dirty: true });\n    }\n\n    handleSave(ev: React.SyntheticEvent<{}>) {\n        ev.preventDefault();\n        this.setState({ editMode: false });\n        const { data } = this.state;\n        const { submit } = this.props;\n        submit(data.filter((e) => !e.deleted));\n    }\n    componentDidUpdate(prevProps: Props) {\n        if (prevProps !== this.props) {\n            this.setState({ data: toState(this.props.data) });\n        }\n    }\n\n    render() {\n        const { data, editMode, dirty } = this.state;\n        const { submitting, orgName } = this.props;\n        return (\n            <>\n                <Menu secondary={true} widths={3}>\n                    {editMode && (\n                        <Menu.Item disabled={submitting}>\n                            <Container fluid={true} textAlign=\"left\">\n                                <Form>\n                                    <Form.Field>\n                                        <FindTeamDropdown\n                                            onSelect={(u: TeamEntry) => this.handleAddTeam(u)}\n                                            orgName={orgName}\n                                            name=\"teams\"\n                                            data-testid=\"team-access-add-dropdown\"\n                                        />\n                                    </Form.Field>\n                                </Form>\n                            </Container>\n                        </Menu.Item>\n                    )}\n                    <Menu.Item position=\"right\">\n                        <Container textAlign=\"right\">\n                            {editMode && (\n                                <>\n                                    <Button\n                                        primary={true}\n                                        content=\"Save changes\"\n                                        disabled={!dirty}\n                                        loading={submitting}\n                                        onClick={(ev) => this.handleSave(ev)}\n                                        data-testid=\"team-access-save-btn\"\n                                    />\n                                    <Button\n                                        basic={true}\n                                        negative={true}\n                                        icon=\"cancel\"\n                                        content=\"Cancel\"\n                                        disabled={submitting}\n                                        onClick={() => this.handleCancelEdit()}\n                                        data-testid=\"team-access-cancel-btn\"\n                                    />\n                                </>\n                            )}\n\n                            {!editMode && (\n                                <Button\n                                    icon=\"edit\"\n                                    content=\"Edit\"\n                                    onClick={() => this.handleEditMode()}\n                                    data-testid=\"team-access-edit-btn\"\n                                />\n                            )}\n                        </Container>\n                    </Menu.Item>\n                </Menu>\n\n                {data.length === 0 && <h3 data-testid=\"team-access-empty-message\">No access rules defined.</h3>}\n                {data.length > 0 && (\n                    <Table data-testid=\"team-access-table\">\n                        <Table.Header>\n                            <Table.Row>\n                                <Table.HeaderCell>Team Name</Table.HeaderCell>\n                                <Table.HeaderCell collapsing={true}>Access Level</Table.HeaderCell>\n                                {editMode && <Table.HeaderCell collapsing={true} />}\n                            </Table.Row>\n                        </Table.Header>\n                        <Table.Body>\n                            {data.map((e, idx) => (\n                                <Table.Row\n                                    key={idx}\n                                    negative={e.deleted}\n                                    positive={e.added}\n                                    data-testid={`team-access-row-${e.teamName}`}\n                                >\n                                    <Table.Cell data-testid={`team-name-${e.teamName}`}>{e.teamName}</Table.Cell>\n                                    <Table.Cell data-testid={`team-access-level-${e.teamName}`}>\n                                        {editMode ? (\n                                            <TeamAccessDropdown\n                                                value={e.level}\n                                                disabled={submitting}\n                                                onRoleChange={(value) =>\n                                                    this.handleRoleChange(idx, value)\n                                                }\n                                                data-testid={`team-access-dropdown-${e.teamName}`}\n                                            />\n                                        ) : (\n                                            e.level\n                                        )}\n                                    </Table.Cell>\n                                    {editMode && (\n                                        <Table.Cell>\n                                            <Button\n                                                basic={true}\n                                                negative={!e.deleted}\n                                                icon={e.deleted ? 'undo' : 'delete'}\n                                                disabled={submitting}\n                                                onClick={() => this.handleDelete(idx)}\n                                                data-testid={`team-access-delete-btn-${e.teamName}`}\n                                            />\n                                        </Table.Cell>\n                                    )}\n                                </Table.Row>\n                            ))}\n                        </Table.Body>\n                    </Table>\n                )}\n            </>\n        );\n    }\n}\n\nexport default TeamAccessList;\n"
  },
  {
    "path": "console2/src/components/molecules/TeamRoleDropdown/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Dropdown, DropdownItemProps, DropdownProps } from 'semantic-ui-react';\n\nimport { TeamRole } from '../../../api/org/team';\n\ninterface Props extends DropdownProps {\n    onRoleChange: (value: TeamRole) => void;\n}\n\nconst options: DropdownItemProps[] = [\n    { text: 'Member', value: TeamRole.MEMBER },\n    { text: 'Maintainer', value: TeamRole.MAINTAINER },\n    { text: 'Owner', value: TeamRole.OWNER }\n];\n\nclass TeamRoleDropdown extends React.PureComponent<Props> {\n    render() {\n        const { onRoleChange, ...rest } = this.props;\n        return (\n            <Dropdown\n                selection={true}\n                options={options}\n                onChange={(ev, data) => onRoleChange(TeamRole[data.value as string])}\n                {...rest}\n            />\n        );\n    }\n}\n\nexport default TeamRoleDropdown;\n"
  },
  {
    "path": "console2/src/components/molecules/WithCopyToClipboard/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport copyToClipboard from 'copy-to-clipboard';\nimport * as React from 'react';\nimport { Icon } from 'semantic-ui-react';\n\nimport './styles.css';\n\nexport interface Props {\n    children: React.ReactNode;\n    value: string;\n}\n\nexport default ({ children, value }: Props) => {\n    return (\n        <>\n            {children}\n            &nbsp;\n            <Icon\n                className=\"copyToClipboardButton\"\n                name=\"copy\"\n                onClick={() => (copyToClipboard as any)(value)}\n            />\n        </>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/molecules/WithCopyToClipboard/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n.copyToClipboardButton:hover {\n    color: gray;\n    cursor: pointer;\n}\n"
  },
  {
    "path": "console2/src/components/molecules/ansible/AnsibleHostList/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Dropdown, DropdownItemProps, Grid, Input, Modal, Table } from 'semantic-ui-react';\n\nimport { AnsibleHost, AnsibleStatus, SearchFilter, SortField, SortOrder } from '../../../../api/process/ansible';\nimport { HumanizedDuration, PaginationToolBar } from '../../../molecules';\nimport { ConcordId } from '../../../../api/common';\nimport { AnsibleTaskListActivity } from '../../../organisms';\nimport ColumnSort from \"../../../atoms/ColumnSort\";\n\ninterface State {\n    hostFilter?: string;\n    prevHostFilter?: string;\n    hostGroupFilter?: string;\n    hostStatusFilter?: AnsibleStatus;\n    hostSortFieldFilter?: SortField;\n    hostSortByFilter?: SortOrder;\n}\n\ninterface Props {\n    instanceId: ConcordId;\n    playbookId?: ConcordId;\n    hosts?: AnsibleHost[];\n    hostGroups: string[];\n    showStatusFilter?: boolean;\n\n    next?: number;\n    prev?: number;\n    refresh: (filter: SearchFilter) => void;\n}\n\nconst hostStatusesOptions = [\n    { text: AnsibleStatus.RUNNING, value: AnsibleStatus.RUNNING },\n    { text: AnsibleStatus.CHANGED, value: AnsibleStatus.CHANGED },\n    { text: AnsibleStatus.FAILED, value: AnsibleStatus.FAILED },\n    { text: AnsibleStatus.OK, value: AnsibleStatus.OK },\n    { text: AnsibleStatus.SKIPPED, value: AnsibleStatus.SKIPPED },\n    { text: AnsibleStatus.UNREACHABLE, value: AnsibleStatus.UNREACHABLE }\n];\n\nconst makeHostGroupOptions = (data: string[]): DropdownItemProps[] => {\n    if (!data) {\n        return [];\n    }\n\n    return data.map((value) => ({ value, text: value }));\n};\n\nclass AnsibleHostList extends React.Component<Props, State> {\n    static renderHostItem(\n        instanceId: ConcordId,\n        host: string,\n        hostGroup: string,\n        hostStatus: AnsibleStatus,\n        duration: number,\n        idx: number,\n        playbookId?: ConcordId\n    ) {\n        return (\n            <Modal\n                key={idx}\n                basic={true}\n                size=\"fullscreen\"\n                dimmer=\"inverted\"\n                trigger={\n                    <Table.Row error={hostStatus === AnsibleStatus.FAILED}>\n                        <Table.Cell>{host}</Table.Cell>\n                        <Table.Cell>{hostGroup}</Table.Cell>\n                        <Table.Cell>{hostStatus}</Table.Cell>\n                        <Table.Cell>\n                            <HumanizedDuration value={duration !== 0 ? duration : undefined} />\n                        </Table.Cell>\n                    </Table.Row>\n                }>\n                <Modal.Content scrolling={true}>\n                    <AnsibleTaskListActivity\n                        instanceId={instanceId}\n                        playbookId={playbookId}\n                        host={host}\n                        hostGroup={hostGroup}\n                    />\n                </Modal.Content>\n            </Modal>\n        );\n    }\n\n    static renderHosts(instanceId: ConcordId, playbookId?: ConcordId, hosts?: AnsibleHost[]) {\n        if (!hosts) {\n            return (\n                <tr style={{ fontWeight: 'bold' }}>\n                    <Table.Cell colSpan={4}>-</Table.Cell>\n                </tr>\n            );\n        }\n\n        if (hosts.length === 0) {\n            return (\n                <Table.Row style={{ fontWeight: 'bold' }}>\n                    <Table.Cell colSpan={4}>No data available</Table.Cell>\n                </Table.Row>\n            );\n        }\n\n        return hosts.map((host, idx) =>\n            AnsibleHostList.renderHostItem(\n                instanceId,\n                host.host,\n                host.hostGroup,\n                host.status,\n                host.duration,\n                idx,\n                playbookId\n            )\n        );\n    }\n\n    constructor(props: Props) {\n        super(props);\n        this.state = {};\n\n        this.handleNext = this.handleNext.bind(this);\n        this.handlePrev = this.handlePrev.bind(this);\n        this.handleFirst = this.handleFirst.bind(this);\n        this.handleHostOnBlur = this.handleHostOnBlur.bind(this);\n        this.handleHostChange = this.handleHostChange.bind(this);\n        this.handleHostGroupChange = this.handleHostGroupChange.bind(this);\n        this.handleHostStatusChange = this.handleHostStatusChange.bind(this);\n        this.handleOrderByChange = this.handleOrderByChange.bind(this);\n    }\n\n    handleNext() {\n        this.handleNavigation(this.props.next);\n    }\n\n    handlePrev() {\n        this.handleNavigation(this.props.prev);\n    }\n\n    handleFirst() {\n        this.handleNavigation(0);\n    }\n\n    handleNavigation(offset?: number) {\n        const { hostFilter, hostGroupFilter, hostStatusFilter, hostSortFieldFilter, hostSortByFilter } = this.state;\n        const { refresh } = this.props;\n\n        refresh({\n            offset,\n            host: hostFilter,\n            hostGroup: hostGroupFilter,\n            status: hostStatusFilter,\n            sortField: hostSortFieldFilter,\n            sortBy: hostSortByFilter\n        });\n    }\n\n    handleHostOnBlur() {\n        const { hostFilter, prevHostFilter, hostGroupFilter, hostStatusFilter, hostSortFieldFilter, hostSortByFilter } = this.state;\n        if (hostFilter !== prevHostFilter) {\n            this.setState({ prevHostFilter: hostFilter });\n            this.props.refresh({\n                host: hostFilter,\n                hostGroup: hostGroupFilter,\n                status: hostStatusFilter,\n                sortField: hostSortFieldFilter,\n                sortBy: hostSortByFilter\n            });\n        }\n    }\n\n    handleHostChange(s?: string) {\n        const { hostFilter } = this.state;\n        const host = s && s.length > 0 ? s : undefined;\n\n        if (hostFilter !== host) {\n            this.setState({ hostFilter: host });\n        }\n    }\n\n    handleHostGroupChange(s?: string) {\n        const { hostFilter, hostGroupFilter, hostStatusFilter, hostSortFieldFilter, hostSortByFilter } = this.state;\n        const hostGroup = s && s.length > 0 ? s : undefined;\n\n        if (hostGroupFilter !== hostGroup) {\n            this.setState({ hostGroupFilter: hostGroup });\n            this.props.refresh({ host: hostFilter, hostGroup, status: hostStatusFilter, sortField: hostSortFieldFilter, sortBy: hostSortByFilter });\n        }\n    }\n\n    handleHostStatusChange(s?: AnsibleStatus) {\n        const { hostFilter, hostGroupFilter, hostStatusFilter, hostSortFieldFilter, hostSortByFilter } = this.state;\n        const status = s && s.length > 0 ? s : undefined;\n\n        if (status !== hostStatusFilter) {\n            this.setState({ hostStatusFilter: status });\n            this.props.refresh({ host: hostFilter, hostGroup: hostGroupFilter, status, sortField: hostSortFieldFilter, sortBy: hostSortByFilter });\n        }\n    }\n    \n    handleOrderByChange(sf: SortField, sb: SortOrder) {\n        const { hostFilter, hostGroupFilter, hostStatusFilter, hostSortFieldFilter, hostSortByFilter } = this.state;\n        const sortField = sf && sf.length > 0 ? sf : undefined;\n        const sortBy = sb && sb.length > 0 ? sb : undefined;\n        \n        if (hostSortFieldFilter !== sortField || hostSortByFilter !== sortBy) {\n            this.setState({ hostSortFieldFilter: sortField });\n            this.setState({hostSortByFilter: sortBy});\n            this.props.refresh({ host: hostFilter, hostGroup: hostGroupFilter, status: hostStatusFilter, sortField, sortBy });\n        }\n    }\n    \n    render() {\n        const {\n            instanceId,\n            playbookId,\n            hosts,\n            hostGroups,\n            prev,\n            next,\n            showStatusFilter\n        } = this.props;\n\n        return (\n            <>\n                <Grid columns={showStatusFilter ? 4 : 3} style={{ marginBottom: '5px' }}>\n                    <Grid.Column>\n                        <Input\n                            disabled={hosts === undefined}\n                            fluid={true}\n                            type=\"text\"\n                            icon=\"filter\"\n                            placeholder=\"Host\"\n                            onBlur={this.handleHostOnBlur}\n                            onChange={(ev, data) => this.handleHostChange(data.value)}\n                        />\n                    </Grid.Column>\n                    <Grid.Column>\n                        <Dropdown\n                            disabled={hosts === undefined}\n                            clearable={true}\n                            fluid={true}\n                            placeholder=\"Host group\"\n                            search={true}\n                            selection={true}\n                            options={makeHostGroupOptions(hostGroups)}\n                            onChange={(ev, data) =>\n                                this.handleHostGroupChange(data.value as string)\n                            }\n                        />\n                    </Grid.Column>\n                    {showStatusFilter && (\n                        <Grid.Column>\n                            <Dropdown\n                                disabled={hosts === undefined}\n                                clearable={true}\n                                fluid={true}\n                                placeholder=\"Host status\"\n                                search={true}\n                                selection={true}\n                                options={hostStatusesOptions}\n                                onChange={(ev, data) =>\n                                    this.handleHostStatusChange(data.value as AnsibleStatus)\n                                }\n                            />\n                        </Grid.Column>\n                    )}\n                    <Grid.Column textAlign={'right'}>\n                        <PaginationToolBar\n                            handleNext={this.handleNext}\n                            handlePrev={this.handlePrev}\n                            handleFirst={this.handleFirst}\n                            disablePrevious={prev === undefined}\n                            disableNext={next === undefined}\n                            disableFirst={prev === undefined}\n                        />\n                    </Grid.Column>\n                </Grid>\n\n                <Table\n                    celled={true}\n                    attached=\"bottom\"\n                    selectable={true}\n                    basic={true}\n                    compact={true}\n                    style={{ cursor: 'pointer' }}\n                    className={hosts ? '' : 'loading'}>\n                    <Table.Header>\n                        <Table.Row>\n                            <Table.HeaderCell singleLine={true}>\n                                <div style={{ height:'0px', lineHeight:'36px' }}>Host</div>\n                                <ColumnSort \n                                    ascSort={() => this.handleOrderByChange(SortField.HOST, SortOrder.ASC)} \n                                    descSort={() => this.handleOrderByChange(SortField.HOST, SortOrder.DESC)} \n                                />\n                            </Table.HeaderCell>\n                            <Table.HeaderCell singleLine={true}>\n                                <div style={{ height:'0px', lineHeight:'36px' }}>Host Group</div>\n                                <ColumnSort\n                                    ascSort={() => this.handleOrderByChange(SortField.HOST_GROUP, SortOrder.ASC)}\n                                    descSort={() => this.handleOrderByChange(SortField.HOST_GROUP, SortOrder.DESC)}\n                                />\n                            </Table.HeaderCell>\n                            <Table.HeaderCell singleLine={true}>\n                                <div style={{ height:'0px', lineHeight:'36px' }}>Host Status</div>\n                                <ColumnSort\n                                    ascSort={() => this.handleOrderByChange(SortField.STATUS, SortOrder.ASC)}\n                                    descSort={() => this.handleOrderByChange(SortField.STATUS, SortOrder.DESC)}\n                                />\n                            </Table.HeaderCell>\n                            <Table.HeaderCell singleLine={true}>\n                                <div style={{ height:'0px', lineHeight:'36px' }}>Duration</div>\n                                <ColumnSort\n                                    ascSort={() => this.handleOrderByChange(SortField.DURATION, SortOrder.ASC)}\n                                    descSort={() => this.handleOrderByChange(SortField.DURATION, SortOrder.DESC)}\n                                />\n                            </Table.HeaderCell>\n                        </Table.Row>\n                    </Table.Header>\n\n                    <Table.Body>\n                        {AnsibleHostList.renderHosts(instanceId, playbookId, hosts)}\n                    </Table.Body>\n                </Table>\n            </>\n        );\n    }\n}\n\nexport default AnsibleHostList;\n"
  },
  {
    "path": "console2/src/components/molecules/ansible/AnsibleTaskList/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Header, Table } from 'semantic-ui-react';\n\nimport { AnsibleEvent, AnsibleStatus } from '../../../../api/process/ansible';\nimport { ProcessEventEntry } from '../../../../api/process/event';\nimport { ReactJson } from '../../../atoms';\nimport { HumanizedDuration, LocalTimestamp } from '../../../molecules';\n\ninterface Props {\n    title?: string;\n    showHosts?: boolean;\n    hideStatus?: boolean;\n    hidePlaybook?: boolean;\n    tasks?: Array<ProcessEventEntry<AnsibleEvent>>;\n}\n\nclass AnsibleTaskList extends React.Component<Props> {\n    renderTaskList = () => {\n        const { tasks, showHosts, hideStatus, hidePlaybook } = this.props;\n\n        return (\n            <Table\n                celled={true}\n                attached=\"bottom\"\n                basic={true}\n                compact={true}\n                style={{ width: '100%', margin: 0 }}>\n                <Table.Header>\n                    <Table.Row>\n                        {showHosts && <Table.HeaderCell collapsing={true}>Host</Table.HeaderCell>}\n                        <Table.HeaderCell collapsing={true}>Ansible Task</Table.HeaderCell>\n                        <Table.HeaderCell collapsing={true}>Action</Table.HeaderCell>\n                        {!hideStatus && (\n                            <Table.HeaderCell collapsing={true}>Status</Table.HeaderCell>\n                        )}\n                        <Table.HeaderCell collapsing={true}>Event Time</Table.HeaderCell>\n                        <Table.HeaderCell collapsing={true}>Duration</Table.HeaderCell>\n                        <Table.HeaderCell>Results</Table.HeaderCell>\n                        {!hidePlaybook && (\n                            <Table.HeaderCell collapsing={true}>Playbook</Table.HeaderCell>\n                        )}\n                    </Table.Row>\n                </Table.Header>\n\n                <Table.Body>\n                    {tasks &&\n                        tasks.map((value, index) => {\n                            const { status, ignore_errors } = value.data;\n\n                            const error = status === AnsibleStatus.FAILED && !ignore_errors;\n                            const positive = status === AnsibleStatus.OK;\n                            const warning = value.data.status === AnsibleStatus.UNREACHABLE;\n\n                            const statusString =\n                                status + (ignore_errors ? ' (errors ignored)' : '');\n\n                            return (\n                                <Table.Row\n                                    key={index}\n                                    error={error}\n                                    positive={positive}\n                                    warning={warning}>\n                                    {showHosts && (\n                                        <Table.Cell singleLine={true}>{value.data.host}</Table.Cell>\n                                    )}\n                                    <Table.Cell singleLine={true}>{value.data.task}</Table.Cell>\n                                    <Table.Cell singleLine={true}>\n                                        {value.data.action ? value.data.action : '-'}\n                                    </Table.Cell>\n                                    {!hideStatus && <Table.Cell>{statusString}</Table.Cell>}\n                                    <Table.Cell singleLine={true}>\n                                        <LocalTimestamp value={value.eventDate} />\n                                    </Table.Cell>\n                                    <Table.Cell singleLine={true}>\n                                        <HumanizedDuration value={value.data.duration} />\n                                    </Table.Cell>\n                                    <Table.Cell>\n                                        <ReactJson\n                                            src={value.data.result as object}\n                                            collapsed={true}\n                                            name={null}\n                                            enableClipboard={false}\n                                            displayObjectSize={false}\n                                            displayDataTypes={false}\n                                        />\n                                    </Table.Cell>\n                                    {!hidePlaybook && (\n                                        <Table.Cell singleLine={true}>\n                                            {value.data.playbook}\n                                        </Table.Cell>\n                                    )}\n                                </Table.Row>\n                            );\n                        })}\n                    {!tasks && (\n                        <tr style={{ fontWeight: 'bold' }}>\n                            <Table.Cell colSpan={8}>-</Table.Cell>\n                        </tr>\n                    )}\n                    {tasks && tasks.length === 0 && (\n                        <tr style={{ fontWeight: 'bold' }}>\n                            <Table.Cell colSpan={8}>No data available</Table.Cell>\n                        </tr>\n                    )}\n                </Table.Body>\n            </Table>\n        );\n    };\n\n    render() {\n        const { title } = this.props;\n\n        if (!title) {\n            return this.renderTaskList();\n        }\n\n        return (\n            <>\n                {/*TODO: replace this table in table with two line header */}\n                <Table\n                    style={{ border: 'none', padding: '0' }}\n                    className={this.props.tasks ? '' : 'loading'}>\n                    <Table.Body>\n                        <Table.Row>\n                            <Table.Cell style={{ padding: '0', border: 'none' }}>\n                                <Header as=\"h3\" attached=\"top\">\n                                    {title}\n                                </Header>\n                            </Table.Cell>\n                        </Table.Row>\n                        <Table.Row>\n                            <Table.Cell style={{ padding: '0', border: 'none' }}>\n                                {this.renderTaskList()}\n                            </Table.Cell>\n                        </Table.Row>\n                    </Table.Body>\n                </Table>\n            </>\n        );\n    }\n}\n\nexport default AnsibleTaskList;\n"
  },
  {
    "path": "console2/src/components/molecules/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nexport { default as AnsibleHostList } from './ansible/AnsibleHostList';\nexport { default as AnsibleTaskList } from './ansible/AnsibleTaskList';\nexport { default as BreadcrumbSegment } from './BreadcrumbSegment';\nexport { default as BulkProcessActionDropdown } from './BulkProcessActionDropdown';\nexport { default as ButtonWithConfirmation } from './ButtonWithConfirmation';\nexport { default as CreateNewEntityButton } from './CreateNewEntityButton';\nexport { default as DropdownWithAddition } from './DropdownWithAddition';\nexport { default as EditProjectForm } from './EditProjectForm';\nexport { default as EntityOwnerChangeForm } from './EntityOwnerChangeForm';\nexport { default as EntityOwnerPopup } from './EntityOwnerPopup';\nexport { default as EntityRenameForm } from './EntityRenameForm';\nexport { default as FormWizardAction } from './FormWizardAction';\nexport { default as GitHubLink } from './GitHubLink';\nexport { default as GlobalNavMenu } from './GlobalNavMenu';\nexport { default as Highlighter } from './Highlighter';\nexport { default as HumanizedDuration } from './HumanizedDuration';\nexport { default as LocalTimestamp } from './LocalTimestamp';\nexport { default as LogSegment } from './LogSegment';\nexport { default as MainToolbar } from './MainToolbar';\nexport { default as NewAPITokenForm } from './NewAPITokenForm';\nexport { default as NewProjectForm } from './NewProjectForm';\nexport { default as NewSecretForm } from './NewSecretForm';\nexport { default as NewStorageForm } from './NewStorageForm';\nexport { default as NewTeamForm } from './NewTeamForm';\nexport { default as PaginationToolBar } from './PaginationToolBar';\nexport { default as ProcessActionList } from './ProcessActionList';\nexport { default as ProcessAttachmentsList } from './ProcessAttachmentsList';\nexport { default as ProcessElementList } from './ProcessElementList';\nexport { default as ProcessForm } from './ProcessForm';\nexport { default as ProcessHistoryList } from './ProcessHistoryList';\nexport { default as ProcessLastErrorModal } from './ProcessLastErrorModal';\nexport { default as ProcessList } from './ProcessList';\nexport { default as ProcessLogContainer } from './ProcessLogContainer';\nexport { default as ProcessLogViewer } from './ProcessLogViewer';\nexport { default as ProcessStatusIcon } from './ProcessStatusIcon';\nexport { default as ProcessStatusTable } from './ProcessStatusTable';\nexport { default as ProcessToolbar } from './ProcessToolbar';\nexport { default as ProcessWaitList } from './ProcessWaitList';\nexport { default as RepositoryForm } from './RepositoryForm';\nexport { default as RepositoryList } from './RepositoryList';\nexport { default as RequestErrorMessage } from './RequestErrorMessage';\nexport { default as SingleOperationPopup } from './SingleOperationPopup';\nexport { default as TeamAccessDropdown } from './TeamAccessDropdown';\nexport { default as TeamAccessList } from './TeamAccessList';\nexport { default as TeamRoleDropdown } from './TeamRoleDropdown';\nexport { default as WithCopyToClipboard } from './WithCopyToClipboard';\n\n// https://github.com/facebook/create-react-app/issues/6054\nexport * from './EditProjectForm';\nexport * from './GlobalNavMenu';\nexport * from './NewAPITokenForm';\nexport * from './NewProjectForm';\nexport * from './NewSecretForm';\nexport * from './RepositoryForm';\n"
  },
  {
    "path": "console2/src/components/organisms/APITokenDeleteActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useState } from 'react';\nimport { Button } from 'semantic-ui-react';\n\nimport {ConcordId, ConcordKey, GenericOperationResult, RequestError} from '../../../api/common';\nimport { RequestErrorMessage, SingleOperationPopup } from '../../molecules';\nimport { deleteToken as apiDelete } from '../../../api/profile/api_token';\n\ninterface Props {\n    id: ConcordId;\n    name: ConcordKey;\n    onDone: () => void;\n}\n\nexport default ({ id, name, onDone }: Props) => {\n    const [running, setRunning] = useState(false);\n    const [error, setError] = useState<RequestError>();\n    const [response, setResponse] = useState<GenericOperationResult>();\n\n    const postData = async () => {\n        try {\n            setError(undefined);\n            setRunning(true);\n            setResponse(await apiDelete(id));\n        } catch (e) {\n            setError(e);\n        } finally {\n            setRunning(false);\n        }\n    };\n\n    return (\n        <>\n            {error && <RequestErrorMessage error={error} />}\n\n            <SingleOperationPopup\n                trigger={(onClick) => (\n                    <Button negative={true} icon=\"delete\" content=\"Delete\" onClick={onClick} />\n                )}\n                title=\"Delete API Token?\"\n                introMsg={\n                    <p>\n                        Are you sure you want to delete the <b>{name}</b> API token?\n                    </p>\n                }\n                running={running}\n                runningMsg={<p>Removing the API Token...</p>}\n                success={response ? response.ok : false}\n                successMsg={<p>The API Token was removed successfully.</p>}\n                error={error}\n                onConfirm={postData}\n                onDone={onDone}\n            />\n        </>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/APITokenList/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Wal-Mart Store, Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { Icon, List, Loader } from 'semantic-ui-react';\n\nimport { APITokenDeleteActivity } from '../../organisms';\nimport { list as apiList, TokenEntry } from '../../../api/profile/api_token';\nimport { LocalTimestamp, RequestErrorMessage } from '../../molecules';\nimport { useApi } from '../../../hooks/useApi';\n\nexport default () => {\n    const { data, error, isLoading, fetch } = useApi<TokenEntry[]>(apiList, {\n        fetchOnMount: true\n    });\n\n    if (error) {\n        return <RequestErrorMessage error={error} />;\n    }\n\n    if (isLoading) {\n        return <Loader active={true} />;\n    }\n\n    if (!data || data.length === 0) {\n        return <p>There are no API tokens associated with your account.</p>;\n    }\n\n    return (\n        <List divided={true} relaxed={true} size=\"large\">\n            {data.map((token, index) => (\n                <List.Item key={index}>\n                    <List.Content floated={'right'}>\n                        <APITokenDeleteActivity\n                            id={token.id}\n                            name={token.name}\n                            onDone={() => fetch()}\n                        />\n                    </List.Content>\n                    <Icon name=\"key\" size=\"large\" />\n                    <List.Content>\n                        <List.Header>{token.name}</List.Header>\n                        {token.expiredAt && (\n                            <List.Description>\n                                expired at: <LocalTimestamp value={token.expiredAt} />\n                            </List.Description>\n                        )}\n                    </List.Content>\n                </List.Item>\n            ))}\n        </List>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/AuditLogActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { memo, useCallback, useState } from 'react';\nimport { Button, DropdownItemProps, Form, Menu, Popup, Table } from 'semantic-ui-react';\nimport { DateTimeInput } from 'semantic-ui-calendar-react';\nimport { addHours, format as formatDate, parse as parseDate } from 'date-fns';\n\nimport {\n    AuditAction,\n    AuditLogFilter,\n    AuditObject,\n    list as apiList,\n    PaginatedAuditLogEntries\n} from '../../../api/audit';\nimport { EntityOwnerPopup, LocalTimestamp, PaginationToolBar } from '../../molecules';\nimport { usePagination } from '../../molecules/PaginationToolBar/usePagination';\nimport { LoadingDispatch } from '../../../App';\nimport { useApi } from '../../../hooks/useApi';\nimport { FindUserField2, RequestErrorActivity } from '../../organisms';\nimport { ReactJson, RefreshButton } from '../../atoms';\n\n// date-fns format used to parse date-time strings set by the UI component\nexport const SRC_DATE_TIME_FORMAT = 'yyyy-MM-dd HH:mm';\n// date-fns format used to convert UI date-time strings into the API's date-time format\nexport const DST_DATE_TIME_FORMAT = \"yyyy-MM-dd'T'HH:mm:ss.SSSXXX\";\n// date-time format used by the UI component (pickers)\nexport const UI_DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm';\n\nexport const formatForApi = (s: string) =>\n    formatDate(parseDate(s, SRC_DATE_TIME_FORMAT, new Date()), DST_DATE_TIME_FORMAT);\n\n// converts timestamps from the UI format to the format accepted by the API\nconst prepareCall = (f: AuditLogFilter) => {\n    const result = { ...f };\n\n    if (result.before) {\n        result.before = formatForApi(result.before);\n    }\n\n    if (result.after) {\n        result.after = formatForApi(result.after);\n    }\n\n    return result;\n};\n\ninterface Props {\n    filter: AuditLogFilter;\n    forceRefresh?: boolean;\n    showRefreshButton?: boolean;\n}\n\nconst areEqual = (prev: Props, next: Props): boolean => {\n    return (\n        prev.forceRefresh === next.forceRefresh &&\n        prev.filter.details?.orgName === next.filter.details?.orgName &&\n        prev.filter.details?.jsonStoreName === next.filter.details?.jsonStoreName\n    );\n};\n\nconst keysToOptions = (o: any): DropdownItemProps[] =>\n    Object.keys(o).map((k) => ({ key: k, text: k, value: k }));\n\nconst objectOptions = keysToOptions(AuditObject);\nconst actionOptions = keysToOptions(AuditAction);\n\n// a toggle to force search even if the filter is unchanged\ninterface ForceSearch {\n    force: boolean;\n}\n\nconst DateTimeField = (props: {\n    value: string;\n    label: string;\n    onChange: (data: string) => void;\n}) => (\n    <Form.Field>\n        <DateTimeInput\n            value={props.value}\n            label={props.label}\n            dateTimeFormat={UI_DATE_TIME_FORMAT}\n            closable={true}\n            animation={'none' as any} // workaround for jittery animations in Chrome\n            duration={0}\n            onChange={(ev: {}, data: any) => props.onChange(data.value as string)}\n        />\n    </Form.Field>\n);\n\n// wrapped in \"memo\" with a custom prop equality function to avoid the infinite re-rendering loop\n// when it tries to compare old props and new. Because React typically does only a shallow compare,\n// this wrapper is necessary when the parent component triggers a re-render and a child component\n// re-creates the props object\nexport default memo(({ filter: initialFilter, forceRefresh, showRefreshButton = true }: Props) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const defaultAfter = formatDate(addHours(new Date(), -8), SRC_DATE_TIME_FORMAT);\n    const defaultBefore = formatDate(addHours(new Date(), 1), SRC_DATE_TIME_FORMAT);\n\n    // contains parameters shown in the filtering controls\n    const [filter, setFilter] = useState<AuditLogFilter>({\n        after: defaultAfter,\n        before: defaultBefore,\n        ...initialFilter\n    });\n\n    // contains parameters used for search\n    // whenever the Search button is clicked the filters are copied here in order\n    // to trigger the API call\n    const [effectiveFilter, setEffectiveFilter] = useState<AuditLogFilter & ForceSearch>({\n        ...filter,\n        force: false\n    });\n\n    const {\n        paginationFilter,\n        handleLimitChange,\n        handleNext,\n        handlePrev,\n        handleFirst\n    } = usePagination();\n\n    const fetchData = useCallback(() => {\n        return apiList({ ...prepareCall(effectiveFilter), ...paginationFilter });\n    }, [effectiveFilter, paginationFilter]);\n\n    const { data, error, isLoading, fetch } = useApi<PaginatedAuditLogEntries>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    return (\n        <>\n            <Menu secondary={true}>\n                <Menu.Item style={{ padding: 0 }}>\n                    <Form>\n                        <Form.Group inline={true} style={{ margin: 0 }}>\n                            {showRefreshButton && (\n                                <RefreshButton loading={isLoading} clickAction={() => fetch()} />\n                            )}\n\n                            <Form.Field>\n                                <FindUserField2\n                                    placeholder=\"User\"\n                                    onSelect={(value) => {\n                                        setFilter((prev) => ({ ...prev, userId: value.id }));\n                                    }}\n                                />\n                            </Form.Field>\n\n                            <Form.Dropdown\n                                placeholder=\"Action\"\n                                clearable={true}\n                                selection={true}\n                                options={actionOptions}\n                                onChange={(ev, data) =>\n                                    setFilter({\n                                        ...filter,\n                                        action: data.value as AuditAction\n                                    })\n                                }\n                            />\n\n                            <Form.Dropdown\n                                placeholder=\"Object\"\n                                clearable={true}\n                                selection={true}\n                                options={objectOptions}\n                                onChange={(ev, data) =>\n                                    setFilter({\n                                        ...filter,\n                                        object: data.value as AuditObject\n                                    })\n                                }\n                            />\n\n                            <Form.Field>\n                                <Popup\n                                    trigger={\n                                        <Button\n                                            icon=\"calendar alternate outline\"\n                                            size=\"large\"\n                                            basic={true}\n                                        />\n                                    }\n                                    openOnTriggerClick={true}\n                                    openOnTriggerMouseEnter={false}\n                                    closeOnTriggerMouseLeave={false}\n                                    closeOnDocumentClick={false}>\n                                    <Form>\n                                        <DateTimeField\n                                            label=\"From\"\n                                            value={filter.after || defaultAfter}\n                                            onChange={(data) =>\n                                                setFilter({ ...filter, after: data })\n                                            }\n                                        />\n\n                                        <DateTimeField\n                                            label=\"To\"\n                                            value={filter.before || defaultBefore}\n                                            onChange={(data) =>\n                                                setFilter({ ...filter, before: data })\n                                            }\n                                        />\n                                    </Form>\n                                </Popup>\n                            </Form.Field>\n\n                            <Form.Button\n                                content=\"Search\"\n                                icon=\"search\"\n                                primary={true}\n                                onClick={() =>\n                                    setEffectiveFilter((prev) => ({\n                                        ...filter,\n                                        force: !prev.force\n                                    }))\n                                }\n                            />\n                        </Form.Group>\n                    </Form>\n                </Menu.Item>\n\n                <Menu.Item style={{ padding: 0 }} position=\"right\">\n                    <PaginationToolBar\n                        limit={paginationFilter.limit}\n                        handleLimitChange={(limit) => handleLimitChange(limit)}\n                        handleNext={handleNext}\n                        handlePrev={handlePrev}\n                        handleFirst={handleFirst}\n                        disablePrevious={paginationFilter.offset === 0}\n                        disableNext={data === undefined ? true : !data.next}\n                        disableFirst={paginationFilter.offset === 0}\n                        disabled={data === undefined}\n                    />\n                </Menu.Item>\n            </Menu>\n\n            {error && <RequestErrorActivity error={error} />}\n            {data && data.items.length === 0 && <h3>No audit log entries found.</h3>}\n\n            {data && data.items.length > 0 && (\n                <div style={{ overflowX: 'auto' }}>\n                    <Table>\n                        <Table.Header>\n                            <Table.Row>\n                                <Table.HeaderCell collapsing={true}>Date</Table.HeaderCell>\n                                <Table.HeaderCell collapsing={true}>Action</Table.HeaderCell>\n                                <Table.HeaderCell collapsing={true}>Object</Table.HeaderCell>\n                                <Table.HeaderCell collapsing={true}>User</Table.HeaderCell>\n                                <Table.HeaderCell>Details</Table.HeaderCell>\n                            </Table.Row>\n                        </Table.Header>\n\n                        <Table.Body>\n                            {data &&\n                                data.items &&\n                                data.items.map((e, idx) => (\n                                    <Table.Row key={idx} verticalAlign=\"top\">\n                                        <Table.Cell collapsing={true}>\n                                            <LocalTimestamp value={e.entryDate} />\n                                        </Table.Cell>\n                                        <Table.Cell collapsing={true}>{e.action}</Table.Cell>\n                                        <Table.Cell collapsing={true}>{e.object}</Table.Cell>\n                                        <Table.Cell collapsing={true}>\n                                            {e.user ? <EntityOwnerPopup data={e.user} /> : '-'}\n                                        </Table.Cell>\n                                        <Table.Cell>\n                                            <ReactJson\n                                                src={e.details}\n                                                name={null}\n                                                collapsed={true}\n                                            />\n                                        </Table.Cell>\n                                    </Table.Row>\n                                ))}\n                        </Table.Body>\n                    </Table>\n                </div>\n            )}\n        </>\n    );\n}, areEqual);\n"
  },
  {
    "path": "console2/src/components/organisms/BreadcrumbsToolbar/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { Breadcrumb, Button, Icon, Menu, MenuItem } from 'semantic-ui-react';\nimport './styles.css';\n\ninterface ExternalProps {\n    refreshHandler: () => void;\n    loading: boolean;\n    children: React.ReactNode;\n}\n\nexport default ({ loading, refreshHandler, children }: ExternalProps) => {\n    return (\n        <Menu tabular={false} secondary={true} borderless={true} className=\"BreadcrumbsToolbar\">\n            <MenuItem>\n                <Icon name=\"refresh\" loading={loading} size={'large'} onClick={refreshHandler} />\n            </MenuItem>\n\n            <MenuItem>\n                <Breadcrumb size=\"big\">{children}</Breadcrumb>\n            </MenuItem>\n\n            {/* add a hidden button to match (the toolbar's + action buttons) vertical size */}\n            <MenuItem position={'right'} style={{ opacity: 0 }}>\n                <Button.Group>\n                    <Button\n                        attached={false}\n                        icon=\"refresh\"\n                        disabled={true}\n                        size={'small'}\n                        basic={true}\n                    />\n                </Button.Group>\n            </MenuItem>\n        </Menu>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/BreadcrumbsToolbar/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n.BreadcrumbsToolbar {\n    padding-top: 1rem !important;\n    padding-bottom: 1rem !important;\n    margin-bottom: 0 !important;\n}\n\n.BreadcrumbsToolbar .item {\n    padding: 0 !important;\n}\n\n.BreadcrumbsToolbar .button {\n    font-size: .92857143rem !important;\n    padding: .58928571em 1.125em .58928571em !important;\n}\n"
  },
  {
    "path": "console2/src/components/organisms/BulkCancelProcessPopup/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { useCallback, useState } from 'react';\n\nimport { ConcordId, RequestError } from '../../../api/common';\nimport { killBulk as apiKillBulk } from '../../../api/process';\nimport { SingleOperationPopup } from '../../molecules';\n\ninterface Props {\n    data: ConcordId[];\n    refresh: () => void;\n    trigger: (onClick: () => void) => React.ReactNode;\n}\n\nconst BulkCancelProcessPopup = ({ data, refresh, trigger }: Props) => {\n    const [cancelling, setCancelling] = useState(false);\n    const [success, setSuccess] = useState(false);\n    const [error, setError] = useState<RequestError>();\n\n    const reset = useCallback(() => {\n        setCancelling(false);\n        setSuccess(false);\n        setError(undefined);\n    }, []);\n\n    const onConfirm = useCallback(async () => {\n        setCancelling(true);\n        setSuccess(false);\n        setError(undefined);\n\n        try {\n            await apiKillBulk(data);\n            setSuccess(true);\n        } catch (e) {\n            setError(e);\n        } finally {\n            setCancelling(false);\n        }\n    }, [data]);\n\n    return (\n        <SingleOperationPopup\n            trigger={trigger}\n            title=\"Cancel the process(es)?\"\n            introMsg={<p>Are you sure you want to cancel the selected process(es)?</p>}\n            running={cancelling}\n            runningMsg={<p>Cancelling...</p>}\n            success={success}\n            successMsg={<p>The cancel command was sent successfully.</p>}\n            error={error}\n            reset={reset}\n            onConfirm={onConfirm}\n            onDone={refresh}\n        />\n    );\n};\n\nexport default BulkCancelProcessPopup;\n"
  },
  {
    "path": "console2/src/components/organisms/CancelProcessPopup/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport { ConcordId, RequestError } from '../../../api/common';\nimport { SingleOperationPopup } from '../../molecules';\nimport { memo, useCallback } from 'react';\nimport { kill as apiKill } from '../../../api/process';\nimport { useState } from 'react';\n\ninterface ExternalProps {\n    instanceId: ConcordId;\n    refresh: () => void;\n    trigger: (onClick: () => void) => React.ReactNode;\n}\n\nconst CancelProcessPopup = memo((props: ExternalProps) => {\n    const [cancelling, setCancelling] = useState(false);\n    const [error, setError] = useState<RequestError>();\n    const [success, setSuccess] = useState(false);\n\n    const instanceId = props.instanceId;\n\n    const cancelProcess = useCallback(async () => {\n        setCancelling(true);\n\n        try {\n            await apiKill(instanceId);\n            setSuccess(true);\n        } catch (e) {\n            setError(e);\n        } finally {\n            setCancelling(false);\n        }\n    }, [instanceId]);\n\n    const reset = useCallback(() => {\n        setCancelling(false);\n        setSuccess(false);\n        setError(undefined);\n    }, []);\n\n    const { trigger, refresh } = props;\n\n    return (\n        <SingleOperationPopup\n            trigger={trigger}\n            title=\"Cancel the process?\"\n            introMsg={<p>Are you sure you want to cancel the selected process?</p>}\n            running={cancelling}\n            runningMsg={<p>Cancelling...</p>}\n            success={success}\n            successMsg={<p>The cancel command was sent successfully.</p>}\n            error={error}\n            reset={reset}\n            onDone={refresh}\n            onConfirm={cancelProcess}\n        />\n    );\n});\n\nexport default CancelProcessPopup;\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/ActionBar/ActiveFilters.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport React, { FunctionComponent } from 'react';\nimport { Label, Icon } from 'semantic-ui-react';\nimport { CancelButton } from './CancelButton';\nimport { useCheckpointContext } from '../Container';\nimport { Item } from './styles';\n\nexport interface FilterLabelProps {\n    caption: string;\n    source: string;\n    value: string;\n}\n\n/**\n * A label for things\n *\n * @param {string} source the key of the label\n * @param {string} value the filter input\n */\nexport const FilterLabel: FunctionComponent<FilterLabelProps> = ({ caption, source, value }) => {\n    const { removeFilter } = useCheckpointContext();\n\n    return (\n        <Label as=\"a\" onClick={() => removeFilter(source)}>\n            {caption ? caption : source}:<Label.Detail> {value}</Label.Detail>\n            <Icon name=\"delete\" />\n        </Label>\n    );\n};\n\n/**\n * Component Lists all active filters\n */\nexport const ActiveFilters: FunctionComponent<{}> = () => {\n    const { activeFilters, removeAllFilters, getConfigBySourceName } = useCheckpointContext();\n\n    if (Object.keys(activeFilters).length > 0) {\n        return (\n            <Item>\n                <span style={{ marginRight: '10px', fontWeight: 'bold', fontSize: '1em' }}>\n                    Active filters:\n                </span>\n                {Object.keys(activeFilters).map((key, index, array) => {\n                    const cfg = getConfigBySourceName(key);\n                    const caption = cfg ? cfg.caption : key;\n                    return (\n                        <FilterLabel\n                            key={index}\n                            caption={caption}\n                            source={key}\n                            value={activeFilters[key]}\n                        />\n                    );\n                })}\n\n                <CancelButton loading={false} clearFiltersFn={() => removeAllFilters()} />\n            </Item>\n        );\n    } else {\n        return null;\n    }\n};\n\nexport default ActiveFilters;\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/ActionBar/CancelButton.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport React, { SFC } from 'react';\nimport { Popup, Button } from 'semantic-ui-react';\n\ninterface CancelButtonProps {\n    loading: boolean;\n    clearFiltersFn: () => void;\n}\n\nexport const CancelButton: SFC<CancelButtonProps> = ({ loading, clearFiltersFn }) => {\n    return (\n        <Popup\n            trigger={\n                <Button\n                    basic={true}\n                    icon=\"ban\"\n                    loading={loading}\n                    style={{ marginLeft: '10px' }}\n                    onClick={clearFiltersFn}\n                />\n            }\n            content=\"Clear filters\"\n        />\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/ActionBar/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport React, { FunctionComponent } from 'react';\n\nimport { FullBar, Item } from './styles';\nimport RefreshButton from '../../../atoms/RefreshButton';\nimport { PaginationToolBar } from '../../../../components/molecules';\nimport { MetaFilterPopup } from '../MetaFilterForm';\nimport ActiveFilters from './ActiveFilters';\nimport {useCheckpointContext} from \"../Container\";\n\nconst ActionBar: FunctionComponent = () => {\n    const {\n        currentPage,\n        limitPerPage,\n        setCurrentPage,\n        loadData,\n        reloadData,\n        loadingData,\n        getPaginationAsString,\n        isFirstPage,\n        orgId,\n        projectId,\n        setPageLimit,\n        processes\n    } = useCheckpointContext();\n\n    return (\n        <FullBar>\n            {/* Left side of toolbar */}\n            <Item>\n                <RefreshButton\n                    loading={loadingData}\n                    clickAction={() => {\n                        reloadData();\n                    }}\n                />\n            </Item>\n            <Item>{getPaginationAsString()}</Item>\n\n            <ActiveFilters />\n\n            {/* Right side of Toolbar */}\n            <Item pushRight>\n                <MetaFilterPopup />\n            </Item>\n            <Item>\n                <PaginationToolBar\n                    limit={limitPerPage}\n                    handleLimitChange={(limit) => {\n                        setPageLimit(limit);\n                        loadData({\n                            orgId,\n                            projectId,\n                            limit,\n                            offset: 0\n                        });\n                    }}\n                    handleNext={() => {\n                        setCurrentPage(currentPage + 1);\n                        loadData({\n                            orgId,\n                            projectId,\n                            limit: limitPerPage,\n                            offset: currentPage * limitPerPage // offset calc e.g. 4 -> 5 (4 * 10 = 40)\n                        });\n                    }}\n                    handlePrev={() => {\n                        setCurrentPage(currentPage - 1);\n                        loadData({\n                            orgId,\n                            projectId,\n                            limit: limitPerPage,\n                            offset: (currentPage - 1 - 1) * limitPerPage // offset calc e.g. 4 -> 3 (4 - 2 = 2 : 2 * 10 = 20)\n                        });\n                    }}\n                    handleFirst={() => {\n                        setCurrentPage(1);\n                        loadData({\n                            orgId,\n                            projectId,\n                            limit: limitPerPage,\n                            offset: 0\n                        });\n                    }}\n                    disablePrevious={isFirstPage()}\n                    disableNext={processes ? processes.length < limitPerPage : false}\n                    disableFirst={isFirstPage()}\n                    dropDownValues={[10, 25, 50]}\n                />\n            </Item>\n        </FullBar>\n    );\n};\n\nexport default ActionBar;\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/ActionBar/styles.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport styled from 'styled-components';\n\nexport const FullBar = styled.div`\n    width: 100%;\n    min-height: 2.85714286em;\n\n    display: flex;\n    vertical-align: middle;\n\n    margin-top: 1rem;\n    margin-bottom: 1rem;\n\n    border: 1px solid #d4d4d5;\n    border-radius: 5px;\n\n    @media (min-width: 769px) {\n        align-items: center;\n    }\n\n    @media (max-width: 768px) {\n        align-items: left;\n        flex-direction: column;\n    }\n`;\n\nexport const Item = styled.div`\n    padding: 0.92857143em 1.14285714em;\n    position: relative;\n    vertical-align: middle;\n    line-height: 1;\n    text-decoration: none;\n\n    ${({ pushRight }: { pushRight?: boolean }) => (pushRight ? `margin-left: auto` : ``)}\n    @media (max-width: 768px) {\n        margin-left: initial;\n        align-items: left;\n        flex-direction: column;\n    }\n`;\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/CheckpointErrorBoundry/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\n\nclass CheckpointErrorBoundary extends React.Component<{}, { hasError: boolean }> {\n    constructor(props: {}) {\n        super(props);\n        this.state = { hasError: false };\n    }\n\n    static getDerivedStateFromError(error: any) {\n        // Update state so the next render will show the fallback UI.\n        return { hasError: true };\n    }\n\n    componentDidCatch(error: any, info: any) {\n        // You can also log the error to an error reporting service\n        console.debug(error, info);\n    }\n\n    render() {\n        if (this.state.hasError) {\n            // You can render any custom fallback UI\n            return <h1>Something went wrong.</h1>;\n        }\n\n        return this.props.children;\n    }\n}\n\nexport default CheckpointErrorBoundary;\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/CheckpointGroup/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\n\nimport { CheckpointGroupName } from '../shared/Labels';\nimport { ProcessEntry } from '../../../../api/process';\nimport { CheckpointGroup } from '../shared/types';\n\nimport {\n    FlexWrapper,\n    GroupWrapper,\n    GroupItems,\n    CheckpointNode,\n    CheckpointBox,\n    getStatusColor,\n    EmptyBox\n} from './styles';\nimport CheckpointPopup from '../CheckpointPopup';\nimport NoCheckpointsMessage from '../NoCheckpointsMessage';\n\ninterface Props {\n    process: ProcessEntry;\n    checkpointGroups?: CheckpointGroup[];\n}\n\n// TODO a better layout\nexport default ({ process, checkpointGroups }: Props) => {\n    if (checkpointGroups && checkpointGroups.length > 0) {\n        return (\n            <FlexWrapper>\n                {checkpointGroups.map(({ name, checkpoints }, indexA) => (\n                    <GroupWrapper key={indexA}>\n                        <CheckpointGroupName>Run {name}</CheckpointGroupName>\n                        <GroupItems>\n                            {checkpoints.length === 0 && (\n                                <div>\n                                    <EmptyBox>No checkpoints</EmptyBox>\n                                </div>\n                            )}\n                            {checkpoints.length > 0 &&\n                                checkpoints.map((checkpoint, indexB) => (\n                                    <CheckpointNode key={indexB}>\n                                        <CheckpointPopup\n                                            checkpoint={checkpoint}\n                                            process={process}\n                                            render={\n                                                <CheckpointBox\n                                                    statusColor={getStatusColor(checkpoint.status)}>\n                                                    {checkpoint.name}\n                                                </CheckpointBox>\n                                            }\n                                        />\n                                    </CheckpointNode>\n                                ))}\n                        </GroupItems>\n                    </GroupWrapper>\n                ))}\n            </FlexWrapper>\n        );\n    } else {\n        return <NoCheckpointsMessage />;\n    }\n};\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/CheckpointGroup/styles.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport styled from 'styled-components';\nimport { ProcessStatus } from '../../../../api/process';\nimport { SemanticCOLORS } from 'semantic-ui-react';\n\nexport const FlexWrapper = styled.div`\n    height: 100%;\n    display: flex;\n    flex-direction: row;\n    justify-content: left;\n    padding: 2rem 1.5rem;\n    white-space: nowrap;\n`;\n\nexport const GroupWrapper = styled.div`\n    margin: auto 1rem;\n`;\n\nexport const CheckpointNode = styled.div`\n    cursor: pointer;\n    padding: 4px;\n`;\n\nexport const getStatusColor = (status: string): string => {\n    switch (status) {\n        case ProcessStatus.NEW:\n        case ProcessStatus.PREPARING:\n        case ProcessStatus.RUNNING:\n        case ProcessStatus.STARTING:\n        case ProcessStatus.SUSPENDED:\n            return '#4182C3';\n        case ProcessStatus.FINISHED:\n            return '#0ca934';\n        case ProcessStatus.CANCELLED:\n        case ProcessStatus.FAILED:\n        case ProcessStatus.TIMED_OUT:\n            return '#DB2928';\n        case ProcessStatus.ENQUEUED:\n        case ProcessStatus.RESUMING:\n        case ProcessStatus.WAITING:\n        default:\n            return 'grey';\n    }\n};\n\nexport const getStatusButtonColor = (status: string): SemanticCOLORS => {\n    switch (status) {\n        case ProcessStatus.NEW:\n        case ProcessStatus.PREPARING:\n        case ProcessStatus.RUNNING:\n        case ProcessStatus.STARTING:\n        case ProcessStatus.SUSPENDED:\n            return 'blue';\n        case ProcessStatus.FINISHED:\n            return 'green';\n        case ProcessStatus.CANCELLED:\n        case ProcessStatus.FAILED:\n        case ProcessStatus.TIMED_OUT:\n            return 'red';\n        case ProcessStatus.ENQUEUED:\n        case ProcessStatus.RESUMING:\n        case ProcessStatus.WAITING:\n        default:\n            return 'grey';\n    }\n};\n\ninterface CheckpointBoxProps {\n    statusColor?: any;\n}\n\nexport const CheckpointBox = styled('div')<CheckpointBoxProps>`\n    background: ${(prop: CheckpointBoxProps) => (prop.statusColor ? prop.statusColor : 'gray')};\n    padding: 0.7rem 0.7rem;\n    text-align: center;\n    border-radius: 5px;\n    color: #ffffff;\n    font-size: 1rem;\n    font-weight: 900;\n`;\n\nexport const EmptyBox = styled('div')`\n    background: lightgray;\n    padding: 0.7rem 0.7rem;\n    text-align: center;\n    border-radius: 5px;\n    color: #ffffff;\n    font-size: 1rem;\n    font-weight: 900;\n`;\n\nexport const GroupItems = styled.div`\n    display: flex;\n    flex-direction: row;\n    border-left: 1px solid gray;\n    padding: 0 0.5rem;\n    margin: 1rem;\n`;\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/CheckpointPopup/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport React, { useCallback, useState } from 'react';\nimport { Button, Popup, Grid } from 'semantic-ui-react';\n\nimport ClassIcon from '../../../atoms/ClassIcon';\nimport Truncate from '../../../atoms/Truncate';\n\nimport { CheckpointName } from '../shared/Labels';\nimport { CustomCheckpoint } from '../shared/types';\nimport { ProcessEntry, isFinal } from '../../../../api/process';\nimport { format as formatDate } from 'date-fns';\nimport {useCheckpointContext} from \"../Container\";\nimport {useLogContext} from \"../../../molecules/ProcessLogContainer/LogContainer\";\nimport { restoreProcess as apiRestoreProcess } from '../../../../api/process/checkpoint';\nimport { RequestError } from '../../../../api/common';\nimport { RequestErrorMessage } from '../../../molecules';\n\nconst CheckpointPopup: React.SFC<{\n    checkpoint: CustomCheckpoint;\n    process: ProcessEntry;\n    render: React.ReactNode;\n}> = ({ checkpoint, process, render }) => {\n    const { currentPage, limitPerPage, loadData, orgId, projectId } = useCheckpointContext();\n    useLogContext();\n    const [restoring, setRestoring] = useState(false);\n    const [error, setError] = useState<RequestError>();\n\n    const restoreProcess = useCallback(async () => {\n        setRestoring(true);\n        setError(undefined);\n\n        try {\n            await apiRestoreProcess(process.instanceId, checkpoint.id);\n            setTimeout(() => {\n                loadData({\n                    orgId,\n                    projectId,\n                    limit: limitPerPage,\n                    offset: (currentPage - 1) * limitPerPage\n                });\n            }, 200);\n        } catch (e) {\n            setError(e);\n        } finally {\n            setRestoring(false);\n        }\n    }, [checkpoint.id, currentPage, limitPerPage, loadData, orgId, process.instanceId, projectId]);\n\n    return (\n        <Popup\n            inverted={false}\n            trigger={<div>{render}</div>}\n            on={['click', 'hover']}\n            closeOnTriggerClick={true}\n            hoverable={true}\n            hideOnScroll={true}\n            position=\"top center\"\n            flowing={true}\n            style={{ maxWidth: '300px' }}\n            content={\n                <>\n                    <Grid divided={true} columns={2}>\n                        <Grid.Row style={{ minWidth: '20em' }}>\n                            <Grid.Column>\n                                <CheckpointName>\n                                    <Truncate text={checkpoint.name} />\n                                </CheckpointName>\n                                <br />\n                            </Grid.Column>\n                            <Grid.Column\n                                style={{\n                                    whiteSpace: 'nowrap',\n                                    paddingRight: '1em'\n                                }}>\n                                <CheckpointName>\n                                    {checkpoint.startTime.toDateString()}\n                                    <br />\n                                    <span\n                                        style={{\n                                            fontWeight: 300,\n                                            paddingRight: '1em'\n                                        }}>\n                                        {formatDate(checkpoint.startTime, 'HH:mm:ss')}\n                                    </span>\n                                </CheckpointName>\n                            </Grid.Column>\n                        </Grid.Row>\n                    </Grid>\n                    <hr />\n                    {error && <RequestErrorMessage error={error} />}\n                    <Button\n                        primary={true}\n                        disabled={!isFinal(process.status) || process.disabled}\n                        loading={restoring}\n                        onClick={restoreProcess}>\n                        <ClassIcon\n                            classes=\"icon redo white\"\n                            style={{\n                                fontSize: '0.7rem',\n                                fontWeight: 'bold'\n                            }}\n                        />\n                        Restore this Checkpoint\n                    </Button>\n                </>\n            }\n        />\n    );\n};\n\nexport default CheckpointPopup;\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/Container/__mocks__/checkpointUtils.mocks.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport {\n    CheckpointRestoreHistoryEntry,\n    ProcessCheckpointEntry,\n    ProcessHistoryEntry,\n    ProcessStatus\n} from '../../../../../api/process';\nimport { ColumnDefinition, RenderType } from '../../../../../api/org';\n\nexport const validProcessCheckpoints: ProcessCheckpointEntry[] = [\n    {\n        id: '1',\n        name: 'checkpoint 1',\n        createdAt: '2019-02-18T17:23:32.520Z'\n    },\n    {\n        id: '2',\n        name: 'checkpoint 2',\n        createdAt: '2019-02-18T17:23:32.790Z'\n    },\n    {\n        id: '3',\n        name: 'checkpoint 3',\n        createdAt: '2019-02-18T17:23:32.950Z'\n    }\n];\n\nexport const validProcessHistory: CheckpointRestoreHistoryEntry[] = [\n    {\n        changeDate: '2019-02-18T17:23:29.678Z',\n        id: 1,\n        processStatus: ProcessStatus.PREPARING,\n        checkpointId: 'e90e2280-33a1-11e9-855e-fa163e7ef419'\n    },\n    {\n        id: 2,\n        changeDate: '2019-02-18T17:23:30.426Z',\n        checkpointId: 'e90e2280-33a1-11e9-855e-fa163e7ef419',\n        processStatus: ProcessStatus.ENQUEUED\n    },\n    {\n        id: 2,\n        changeDate: '2019-02-18T17:23:30.907Z',\n        checkpointId: 'e9590f7a-33a1-11e9-aa54-fa163e7ef419',\n        processStatus: ProcessStatus.STARTING\n    },\n    {\n        id: 3,\n        changeDate: '2019-02-18T17:23:31.878Z',\n        checkpointId: 'e9ec1a36-33a1-11e9-bbef-fa163e7ef419',\n        processStatus: ProcessStatus.RUNNING\n    },\n    {\n        id: 4,\n        changeDate: '2019-02-18T17:23:33.445Z',\n        checkpointId: 'eadac2da-33a1-11e9-bbef-fa163e7ef419',\n        processStatus: ProcessStatus.FINISHED\n    }\n];\n\nexport const emptyProcessHistory: CheckpointRestoreHistoryEntry[] = [];\nexport const emptyProcessCheckpoints: ProcessCheckpointEntry[] = [];\n\nexport const hasMetaColumnDefinition: ColumnDefinition = {\n    source: 'meta.repoMetadata',\n    caption: 'Target Repo',\n    searchType: 'substring',\n    searchValueType: 'string'\n};\n\nexport const missingMetaColumnDefinition: ColumnDefinition = {\n    render: RenderType.TIMESTAMP,\n    source: 'lastUpdatedAt',\n    caption: 'Updated'\n};\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/Container/__tests__/__snapshots__/checkpointUtils.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`generateCheckpointGroups handles empty checkpoints 1`] = `\nArray [\n  Object {\n    \"checkpoints\": Array [],\n    \"end\": 2019-02-18T17:23:33.445Z,\n    \"name\": \"#1\",\n    \"start\": 2019-02-18T17:23:29.678Z,\n  },\n]\n`;\n\nexports[`generateCheckpointGroups handles empty history 1`] = `Array []`;\n\nexports[`generateCheckpointGroups handles valid data 1`] = `\nArray [\n  Object {\n    \"checkpoints\": Array [\n      Object {\n        \"createdAt\": \"2019-02-18T17:23:32.520Z\",\n        \"endTime\": 2019-02-18T17:23:32.790Z,\n        \"id\": \"1\",\n        \"name\": \"checkpoint 1\",\n        \"startTime\": 2019-02-18T17:23:32.520Z,\n        \"status\": \"FINISHED\",\n      },\n      Object {\n        \"createdAt\": \"2019-02-18T17:23:32.790Z\",\n        \"endTime\": 2019-02-18T17:23:32.950Z,\n        \"id\": \"2\",\n        \"name\": \"checkpoint 2\",\n        \"startTime\": 2019-02-18T17:23:32.790Z,\n        \"status\": \"FINISHED\",\n      },\n      Object {\n        \"createdAt\": \"2019-02-18T17:23:32.950Z\",\n        \"endTime\": 2019-02-18T17:23:33.445Z,\n        \"id\": \"3\",\n        \"name\": \"checkpoint 3\",\n        \"startTime\": 2019-02-18T17:23:32.950Z,\n        \"status\": \"FINISHED\",\n      },\n    ],\n    \"end\": 2019-02-18T17:23:33.445Z,\n    \"name\": \"#1\",\n    \"start\": 2019-02-18T17:23:29.678Z,\n  },\n]\n`;\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/Container/__tests__/checkpointUtils.test.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { generateCheckpointGroups } from '../checkpointUtils';\nimport {\n    emptyProcessCheckpoints,\n    emptyProcessHistory,\n    validProcessCheckpoints,\n    validProcessHistory\n} from '../__mocks__/checkpointUtils.mocks';\nimport { ProcessStatus } from '../../../../../api/process';\n\ntest('generateCheckpointGroups handles valid data', () => {\n    const result = generateCheckpointGroups(\n        ProcessStatus.FINISHED,\n        validProcessCheckpoints,\n        validProcessHistory\n    );\n    expect(result).toMatchSnapshot();\n});\n\ntest('generateCheckpointGroups handles no data', () => {\n    const result = generateCheckpointGroups(\n        ProcessStatus.FINISHED,\n        emptyProcessCheckpoints,\n        emptyProcessHistory\n    );\n    expect(result).toEqual([]);\n});\n\ntest('generateCheckpointGroups handles empty checkpoints', () => {\n    const result = generateCheckpointGroups(\n        ProcessStatus.FINISHED,\n        emptyProcessCheckpoints,\n        validProcessHistory\n    );\n    expect(result).toMatchSnapshot();\n});\n\ntest('generateCheckpointGroups handles empty history', () => {\n    const result = generateCheckpointGroups(\n        ProcessStatus.FINISHED,\n        validProcessCheckpoints,\n        emptyProcessHistory\n    );\n    expect(result).toMatchSnapshot();\n});\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/Container/__tests__/useQueryParams.test.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport React from 'react';\n\nconst dummyComponent = () => {};\n\ntest.skip('Test the thing ', () => {});\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/Container/checkpointUtils.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { isAfter, isBefore, isEqual, parseISO as parseDate } from 'date-fns';\n\nimport {\n    CheckpointRestoreHistoryEntry,\n    ProcessCheckpointEntry,\n    ProcessStatus\n} from '../../../../api/process';\nimport { comparators } from '../../../../utils';\nimport { CheckpointGroup, CustomCheckpoint } from '../shared/types';\n\n/**\n * Generate CustomCheckpoint array between time\n *\n * @param start Start Date Object\n * @param end End Date Object\n * @param checkpoints Concord Process Checkpoint array\n * @param status Process Status type for this run\n * */\n\nexport const geCheckpointsBetweenTime = (\n    checkpoints: ProcessCheckpointEntry[],\n    status: ProcessStatus,\n    start: Date,\n    end?: Date\n): CustomCheckpoint[] => {\n    // * Custom checkpoints extend checkpoints and add start/end times and status\n    const resultCheckpoints: CustomCheckpoint[] = [];\n\n    checkpoints\n        // * Filter for checkpoints between a start and end date\n        .filter((checkpoint) => {\n            const checkTime = parseDate(checkpoint.createdAt);\n            return (\n                (isEqual(checkTime, start) || isAfter(checkTime, start)) &&\n                (end === undefined || isBefore(checkTime, end))\n            );\n        })\n        .forEach((checkpoint, index, array) => {\n            if (index !== array.length - 1) {\n                resultCheckpoints.push({\n                    ...checkpoint,\n                    status: ProcessStatus.FINISHED,\n                    startTime: parseDate(checkpoint.createdAt)\n                });\n            } else {\n                resultCheckpoints.push({\n                    ...checkpoint,\n                    status,\n                    startTime: parseDate(checkpoint.createdAt)\n                });\n            }\n        });\n\n    return resultCheckpoints;\n};\n\n/**\n * Generates a custom object array of type CheckpointGroup\n * Correlates checkpoint data with history data to generate said object.\n *\n * @param processStatus status of the process\n * @param checkpoints Original Process Checkpoint Array\n * @param checkpointRestoreHistory\n */\nexport const generateCheckpointGroups = (\n    processStatus: ProcessStatus,\n    checkpoints: ProcessCheckpointEntry[],\n    checkpointRestoreHistory?: CheckpointRestoreHistoryEntry[]\n): CheckpointGroup[] => {\n    const points = checkpoints.sort(comparators.byProperty((i) => parseDate(i.createdAt)));\n    if (points.length === 0) {\n        return [];\n    }\n\n    const history = (checkpointRestoreHistory || []).sort(\n        comparators.byProperty((i) => parseDate(i.changeDate))\n    );\n\n    const groups: CheckpointGroup[] = [];\n    let currentGroup: CheckpointGroup = {\n        name: `#1`,\n        status: ProcessStatus.FINISHED,\n        checkpoints: []\n    };\n    let currentGroupStart = parseDate(points[0].createdAt);\n    history.forEach((h) => {\n        let currentGroupEnd = parseDate(h.changeDate);\n\n        currentGroup.status = h.processStatus;\n        currentGroup.checkpoints = geCheckpointsBetweenTime(\n            points,\n            h.processStatus,\n            currentGroupStart,\n            currentGroupEnd\n        );\n        groups.push(currentGroup);\n\n        currentGroup = {\n            name: `#${groups.length + 1}`,\n            status: processStatus,\n            checkpoints: []\n        };\n        currentGroupStart = currentGroupEnd;\n    });\n\n    currentGroup.checkpoints = geCheckpointsBetweenTime(\n        checkpoints,\n        processStatus,\n        currentGroupStart,\n        undefined\n    );\n    groups.push(currentGroup);\n\n    return groups;\n};\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/Container/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { useEffect, useState } from 'react';\nimport constate from \"constate\";\nimport {\n    ProcessEntry,\n    PaginatedProcessEntries,\n    list as apiList,\n    ProcessListQuery\n} from '../../../../api/process';\nimport { CheckpointGroup } from '../shared/types';\nimport { generateCheckpointGroups } from './checkpointUtils';\nimport { ProjectEntry } from '../../../../api/org/project';\nimport useQueryParams from './useQueryParams';\nimport { ColumnDefinition } from '../../../../api/org';\n\n/**\n * Interface for the inital props passed to this container.\n */\nexport interface InitialProps {\n    project: ProjectEntry;\n    refreshInterval?: number;\n}\n\n/**\n * Custom React hook to isolate state and features specific to the Checkpoint page.\n * We are exporting this as a [constate](https://github.com/diegohaz/constate) container\n * Use with the useContext react hook\n *\n * TODO: This hook is probably too big at this point, think about refactoring into more composable hooks\n * TODO: Add Error handling and error states\n *\n * @param initial: InitialProps values you can pass in via a Context or Provider\n */\nexport const useCheckpoint = (initial: InitialProps) => {\n    // currentPage for pagination\n    const [currentPage, setPage] = useState(1);\n\n    // limitPerPage is the limit of items we will show on a page at one time\n    const [limitPerPage, setLimitPerPage] = useState(10);\n\n    // loadingData boolean to tell us if data is loading or not\n    const [loadingData, setLoadingData] = useState(false);\n\n    // orgId comes from initial, represents a concord org Id\n    const [orgId] = useState(initial.project!.orgId);\n\n    // projectId comes from iniital, represents a concord project Id\n    const [projectId] = useState(initial.project!.id);\n\n    // project comes from iniital, represents a concord project\n    const [project] = useState(initial.project!);\n\n    // processes an array of ProcessEntries which we can map over to render components\n    const [processes, setProcesses] = useState<ProcessEntry[]>([]);\n\n    // checkpointGroups are a custom object correlating a processes checkpoints to timestamps\n    const [checkpointGroups, setCheckpointGroups] = useState<{\n        [key: string]: CheckpointGroup[];\n    }>({});\n\n    // the current query parameters existing\n    const { queryParams, replaceQueryParams, getCurrentParams } = useQueryParams();\n\n    /**\n     * an active filter is represented by the source name of a ui config object\n     * @see getConfigBySourceName to pull specific filter data\n     * */\n    const [activeFilters, setActiveFilters] = useState<{ [source: string]: string }>({\n        ...queryParams\n    });\n\n    /**\n     * Selector for the project meta configs\n     * @return An array of configs or return empty object if no data is found\n     */\n    const getProjectUIConfigs = (): ColumnDefinition[] => {\n        const project = initial.project;\n        if (project.meta && project.meta.ui && project.meta.ui.processList) {\n            return project.meta.ui.processList;\n        }\n        // No data to return\n        return [];\n    };\n\n    /**\n     * Selector for metadata configs\n     * @return An array of metadata configs or return empty object if no data is found\n     */\n    const getMetaDataConfigs = () => {\n        // Filter for meta properties and return the results\n        return getProjectUIConfigs().filter((value) => {\n            if (value.source && value.source.includes('meta')) {\n                // Found some meta\n                return true;\n            }\n            // This object is not meta\n            return false;\n        });\n    };\n\n    /**\n     * Select a specific UI config for details by it's source name\n     * @return a single metadata config\n     */\n    const getConfigBySourceName = (sourceName: string) => {\n        return getProjectUIConfigs().find((value) => {\n            if (value.source === sourceName) {\n                // Found an exact match\n                return true;\n            }\n            // Nothing found\n            return false;\n        });\n    };\n\n    /**\n     * Add Active filter\n     * @param sourceName is the unique key these configs are known by\n     * @param value the filter input for the source field\n     */\n    const addActiveFilter = (sourceName: string, value: string) => {\n        // Try to get the new filter\n        const newFilter = getConfigBySourceName(sourceName);\n\n        // Did we find the config item?\n        if (newFilter) {\n            // Add the source name to the active filters\n            setActiveFilters({ ...activeFilters, [newFilter.source]: value });\n        }\n    };\n\n    /**\n     * Remove a specific filter from the list\n     * Updates the url query params to new values\n     * @param sourceName the unique key of the meta filter to remove\n     */\n    const removeFilter = (sourceName: string) => {\n        // find out if the property exist on the object\n        const exists = Object.keys(activeFilters).includes(sourceName);\n\n        if (exists === false) {\n            return; // does not exist, do_nothing()\n        }\n\n        // It exists, so lets delete it\n        let newFilters = activeFilters;\n        delete newFilters[sourceName];\n\n        setActiveFilters(newFilters);\n        replaceQueryParams(newFilters);\n    };\n\n    /**\n     * Remove all active filters\n     * This just wipes the array clean\n     */\n    const removeAllFilters = () => {\n        setActiveFilters({});\n        replaceQueryParams({});\n    };\n\n    /**\n     * Set current page to the provided page\n     * @param newPage to be set\n     */\n    const setCurrentPage = (newPage: number) => setPage(newPage);\n\n    /**\n     * Set page limit to the new limit\n     * @param newLimit to be set\n     */\n    const setPageLimit = (newLimit: number) => setLimitPerPage(newLimit);\n\n    /**\n     * Refresh the process data and generate checkpoint data\n     * Function is async for the nice await api.\n     * This is not exposed through the container api and should be called internally.\n     *\n     * @param args Arguments for the fetch @see ProcessListQuery type for args\n     */\n    const refreshProcessData = async (args: ProcessListQuery) => {\n        if (loadingData === true) {\n            return;\n        }\n\n        setLoadingData(true);\n\n        const { items: processes }: PaginatedProcessEntries = await apiList({\n            ...args,\n            include: ['checkpoints', 'checkpointsHistory']\n        });\n\n        const checkpointGroups = {};\n        processes.forEach((p) => {\n            if (p.checkpoints) {\n                checkpointGroups[p.instanceId] = generateCheckpointGroups(\n                    p.status,\n                    p.checkpoints,\n                    p.checkpointRestoreHistory\n                );\n            }\n        });\n\n        setCheckpointGroups(checkpointGroups);\n        setProcesses(processes);\n        setLoadingData(false);\n    };\n\n    /**\n     * Get the total processes length of a process\n     *\n     * @return number of process length\n     */\n    const getProcessCount = (): number => (processes ? processes.length : 0);\n\n    /**\n     * Get a humanized display of what page they are on.\n     *\n     * @return humanized string if the data exists otherwise return an empty string\n     */\n    const getPaginationAsString = (): string => {\n        if (processes) {\n            const upperLimit = currentPage * limitPerPage;\n            const lowerLimit = (currentPage - 1) * limitPerPage + 1;\n\n            return `Showing ${lowerLimit} - ${upperLimit}`; // e.g. \"Showing 1 - 10\"\n        } else {\n            return '';\n        }\n    };\n\n    /**\n     * Reload function calls refreshProcessData function with parameters to refresh\n     * the current page.\n     *\n     * @param filters - additional filter values, currently used to grab initial query params on page load\n     *\n     * @returns nothing\n     */\n    const reloadData = (filters?: { [source: string]: string }): void => {\n        refreshProcessData({\n            orgId,\n            projectId,\n            limit: limitPerPage,\n            offset: (currentPage - 1) * limitPerPage,\n            ...activeFilters,\n            ...filters\n        });\n    };\n\n    /**\n     * Similar to reload data, calls refreshProcessData, but allows you to customize\n     * the args passed to the reload function.\n     *\n     * TODO This function has the potential to throw warning if unmounted whilst fetch is in progress\n     * ! Warning: Can't perform a React state update on an unmounted component.\n     *\n     * @param args Arguments for the fetch @see ProcessListQuery type for args\n     */\n    const loadData = (args: ProcessListQuery): void => {\n        refreshProcessData({ ...args, ...activeFilters });\n    };\n\n    // TODO react-hooks/exhaustive-deps warning\n    useEffect(() => {\n        if (initial.refreshInterval !== undefined) {\n            // Load initial dataset\n            reloadData(getCurrentParams());\n\n            // Continue to request data updates on\n            const onPollInterval = setInterval(() => {\n                reloadData();\n            }, initial.refreshInterval);\n\n            return () => {\n                clearInterval(onPollInterval);\n            };\n        }\n    }, [activeFilters, currentPage]); // eslint-disable-line react-hooks/exhaustive-deps\n\n    // If activefilters change\n    useEffect(() => {\n        // Reset to page 1\n        setPage(1);\n    }, [activeFilters]);\n\n    /**\n     * Update active filter when query params change\n     */\n    useEffect(() => {\n        setActiveFilters({ ...queryParams });\n    }, [queryParams]);\n\n    /**\n     * Selector to see if we are currently on the first page.\n     *\n     * @return true if first page, otherwise false\n     */\n    const isFirstPage = (): boolean => currentPage === 1;\n\n    /**\n     * Selector to determine if filtering is possible based on current state\n     * @return true if filtering is allowed, false if it is not\n     */\n    const canFilter = (): boolean => {\n        if (getMetaDataConfigs().length > 0) {\n            return true;\n        }\n\n        // Nothing true, so false\n        return false;\n    };\n\n    return {\n        checkpointGroups,\n        currentPage,\n        limitPerPage,\n        setCurrentPage,\n        getProcessCount,\n        getPaginationAsString,\n        isFirstPage,\n        loadData,\n        reloadData,\n        loadingData,\n        orgId,\n        projectId,\n        project,\n        setPageLimit,\n        processes,\n        queryParams,\n        replaceQueryParams,\n        getProjectUIConfigs,\n        getMetaDataConfigs,\n        getConfigBySourceName,\n        addActiveFilter,\n        removeFilter,\n        removeAllFilters,\n        canFilter,\n        activeFilters,\n        setActiveFilters\n    };\n};\nexport const [CheckpointProvider, useCheckpointContext] = constate(useCheckpoint);\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/Container/useForm.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { useState } from 'react';\n\nexport interface FormValues {\n    // name of field and it's value\n    [name: string]: string;\n}\n\n/**\n * React hook to handle general form values\n * @param initialFormValues\n *\n * @return {FormValues} form - The current form values\n * @return setField - Function to set a new form value\n * @return clear - Function to clear all form values\n * @return reset - Function to reset values to the initial form values\n */\nexport const useForm = (initialFormValues: FormValues) => {\n    const [initialForm] = useState<FormValues>(initialFormValues);\n    const [form, setForm] = useState<FormValues>(initialForm);\n\n    /**\n     * Deletes a field from form state\n     *\n     * @param name - name of the field to delete\n     */\n    const deleteField = (name: string) => {\n        const newForm = form;\n        delete newForm[name];\n        setForm(newForm);\n    };\n\n    /**\n     * Sets a form field value in state\n     *\n     * @param name - name of the field to modify in state\n     * @param value - value to set on the field property\n     */\n    const setField = (name: string, value: string) => {\n        // If name given but value is empty, delete the field\n        if (name && value === '') {\n            deleteField(name);\n        }\n\n        setForm({\n            ...form,\n            [name]: value\n        });\n    };\n\n    /**\n     * Form will be empty\n     */\n    const clear = () => {\n        setForm({});\n    };\n\n    /**\n     * Form will be set to it's initial values\n     */\n    const reset = () => {\n        setForm(initialForm);\n    };\n\n    return { form, setField, clear, reset };\n};\n\nexport default useForm;\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/Container/usePopup.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { useState } from 'react';\n\n/**\n * Custom React hook to manage state of a popup externally\n */\nexport const usePopup = () => {\n    const [visible, setVisible] = useState(false);\n\n    const close = () => {\n        setVisible(false);\n    };\n\n    const open = () => {\n        setVisible(true);\n    };\n\n    return { visible, setVisible, open, close };\n};\n\nexport default usePopup;\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/Container/useQueryParams.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { useState, useEffect, useLayoutEffect } from 'react';\n\nimport { parseQueryParams, QueryParams } from '../../../../api/common';\nimport 'url-search-params-polyfill';\n\n/**\n * Custom React Hook to provide the current query parameters\n * When this hook is used it creates a HachChangeEvent listener\n *\n * When the event fires query params in state are updated.\n */\nexport function useQueryParams() {\n    const [queryParams, setQueryParams] = useState<QueryParams>();\n\n    const [currentUrl, setCurrentUrl] = useState<string>('');\n    const [oldUrl, setOldUrl] = useState<string>('');\n\n    /**\n     * Get url parameters directly\n     * Useful if you need to load parameters on initial render\n     *\n     * @returns QueryParameterObject\n     */\n    const getCurrentParams = (): QueryParams => {\n        // Parse, decode, return\n        return decodeAllUriValues(parseQueryParams(window.location.href));\n    };\n\n    /**\n     * Remove empty values from the queryParamObject\n     * @param params a query object to check\n     */\n    const removeEmptyValues = (params: QueryParams): QueryParams => {\n        const newValues = Object.entries(params).reduce((previous, current) => {\n            // is the value empty?\n            if (current[1] === '') {\n                // value is empty don't add to the result\n                return { ...previous };\n            } else {\n                // there is a value so add it to the result\n                return { ...previous, [current[0]]: current[1] };\n            }\n        }, {});\n\n        return newValues;\n    };\n\n    /**\n     * decode all uri parameters\n     * @param params key/value pairs to iterate through\n     */\n    const decodeAllUriValues = (params: QueryParams): QueryParams => {\n        let dec = decodeURIComponent;\n\n        let decodedResult = {};\n        Object.keys(params).forEach((key) => {\n            decodedResult[key] = dec(params[key]);\n        });\n\n        return decodedResult;\n    };\n\n    /**\n     * Replaces all query params in the url with the object provided\n     * Store that value in queryParams state\n     *\n     * @param params a QueryParams e.g. { key: value, ... }\n     */\n    const replaceQueryParams = (params: QueryParams = {}) => {\n        // Construct URLSearchParams instance\n        let UrlParams = new URLSearchParams();\n        Object.keys(removeEmptyValues(params)).forEach((i) => UrlParams.append(i, params[i]));\n\n        // The full url that shows in the browser currently\n        let baseUrl = window.location.href;\n\n        // Store oldUrl in state\n        setOldUrl(baseUrl);\n\n        // Edgecase if the url happens to have a ? in it\n        if (baseUrl.includes('?')) {\n            baseUrl = baseUrl.split('?')[0];\n        }\n\n        // Generate the new complete href\n        let newUrl = baseUrl;\n        // Only add query params if there are query params\n        if (UrlParams.toString().length > 0) newUrl += `?${UrlParams.toString()}`;\n\n        // Set the query params in the URL\n        window.location.assign(newUrl);\n\n        // Save new url to current\n        setCurrentUrl(newUrl);\n\n        // Store params in state\n        setQueryParams(parseQueryParams(UrlParams.toString()));\n    };\n\n    /**\n     * Sets the queryParams state values if the URL hash changes\n     * Saves the old and new url to state\n     * @param event Window event object containing new and old url addresses\n     */\n    const onHashChange = (event: HashChangeEvent) => {\n        if (event.newURL === event.oldURL) {\n            // No change to URL.  do_nothing();\n            return;\n        }\n\n        // Store so we can expose these\n        setOldUrl(event.oldURL);\n        setCurrentUrl(event.newURL);\n\n        // Parse, decode, set\n        setQueryParams(decodeAllUriValues(parseQueryParams(event.newURL)));\n    };\n\n    /**\n     * Sets up a hashchange event listener for any changes to the url\n     * This Effect only runs on initial mount\n     *\n     * On unmount the hashchange event listener is removed via a useEffect\n     * cleanup function\n     */\n    // TODO react-hooks/exhaustive-deps warning\n    useEffect(() => {\n        const eventName = 'hashchange';\n\n        window.addEventListener(eventName, onHashChange, false);\n\n        return () => {\n            window.removeEventListener(eventName, onHashChange, false);\n        };\n    }, []); // eslint-disable-line react-hooks/exhaustive-deps\n\n    /**\n     * Set queryParams on first render\n     */\n    // TODO react-hooks/exhaustive-deps warning\n    useLayoutEffect(() => {\n        setQueryParams(getCurrentParams());\n    }, []); // eslint-disable-line react-hooks/exhaustive-deps\n\n    return {\n        queryParams,\n        replaceQueryParams,\n        decodeAllUriValues,\n        getCurrentParams,\n        currentUrl,\n        oldUrl\n    };\n}\n\nexport default useQueryParams;\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/MetaFilterForm/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport React, { FunctionComponent } from 'react';\nimport { Grid, Header, Button, Divider, Popup } from 'semantic-ui-react';\n\nimport { useCheckpointContext } from '../Container';\n\nimport useQueryParams from '../Container/useQueryParams';\nimport useForm from '../Container/useForm';\nimport { usePopup } from '../Container/usePopup';\n\n/**\n * This form uses the metadata config data to allow for filtering of processes\n *\n * On submit, the URL parameters will be updated with the results\n * On load it should pull from state to populate it's data\n */\nexport const MetaFilterForm: FunctionComponent<{ onClear: () => void }> = ({ onClear }) => {\n    const { getMetaDataConfigs, activeFilters, removeAllFilters } = useCheckpointContext();\n\n    const { replaceQueryParams } = useQueryParams();\n\n    // Loop over all possible meta configs and create an object with active filters set\n    const initialFormState = getMetaDataConfigs().reduce((previous, current) => {\n        // Is the current config currently active?\n        if (Object.keys(activeFilters).includes(current.source)) {\n            // Found an active filter for a config, set value to the active filter value\n            return { ...previous, [current.source]: activeFilters[current.source] };\n        } else {\n            // No active filter found, set value to empty string\n            return { ...previous, [current.source]: '' };\n        }\n    }, {});\n\n    const { form, setField, clear } = useForm(initialFormState);\n\n    return (\n        <div style={{ padding: '8px' }}>\n            <Header as=\"h2\">\n                <Header.Content>Meta Filters</Header.Content>\n                <Header.Subheader>Set filters based on your project metadata</Header.Subheader>\n            </Header>\n            <Divider style={{ marginBottom: '32px' }} />\n            <form\n                onSubmit={(e) => {\n                    e.preventDefault();\n                    replaceQueryParams(form);\n                }}>\n                {getMetaDataConfigs().map(({ source, caption }) => {\n                    return (\n                        <Grid key={source} divided=\"vertically\">\n                            <Grid.Row\n                                columns={2}\n                                centered\n                                verticalAlign=\"middle\"\n                                style={{ padding: '0px' }}>\n                                <Grid.Column textAlign=\"right\">\n                                    <Header size=\"small\" textAlign=\"right\" color=\"grey\">\n                                        {/* Display caption if it exists, otherwise display the source-name } */}\n                                        {caption ? caption : source}\n                                    </Header>\n                                </Grid.Column>\n                                <Grid.Column style={{ paddingLeft: '0' }}>\n                                    <div className=\"ui input mini\">\n                                        <input\n                                            type=\"search\"\n                                            value={form[source]}\n                                            onChange={(e) => setField(source, e.target.value)}\n                                        />\n                                    </div>\n                                </Grid.Column>\n                            </Grid.Row>\n                        </Grid>\n                    );\n                })}\n\n                <Grid divided=\"vertically\">\n                    <Grid.Row columns=\"1\" centered verticalAlign=\"bottom\">\n                        <Button.Group style={{ width: '80%' }}>\n                            <Button\n                                onClick={() => {\n                                    clear();\n                                    removeAllFilters();\n                                    onClear();\n                                }}\n                                type=\"button\">\n                                Clear\n                            </Button>\n                            <Button.Or />\n                            <Button primary type=\"submit\">\n                                Filter\n                            </Button>\n                        </Button.Group>\n                    </Grid.Row>\n                </Grid>\n            </form>\n        </div>\n    );\n};\n\nexport const MetaFilterPopup: FunctionComponent = () => {\n    const { canFilter } = useCheckpointContext();\n    const { visible, open, close } = usePopup();\n\n    return (\n        <Popup\n            on={['click']}\n            trigger={<Button basic icon=\"filter\" disabled={!canFilter()} />}\n            content={<MetaFilterForm onClear={close} />}\n            open={visible}\n            onClose={close}\n            onOpen={open}\n            position=\"bottom right\"\n            flowing\n            wide\n            closeOnEscape\n            keepInViewPort\n            openOnTriggerClick\n        />\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/NoCheckpointsMessage/NoCheckpointsMessge.test.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport React from 'react';\nimport { render } from '@testing-library/react';\nimport Message from './';\n\ntest('Renders a message describing there are no checkpoints', () => {\n    const { container } = render(<Message />);\n    expect(container.innerHTML).toContain('No checkpoints');\n});\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/NoCheckpointsMessage/__tests__/NoCheckpointsMessge.test.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport React from 'react';\nimport { render } from '@testing-library/react';\nimport Message from '../';\n\ntest('Renders a message describing there are no checkpoints', () => {\n    const { container } = render(<Message />);\n    expect(container.innerHTML).toContain('No checkpoints');\n});\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/NoCheckpointsMessage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport { LoadError } from '../shared/Labels';\n\nexport default () => <LoadError>No checkpoints have been created for this process.</LoadError>;\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/ProcessCheckpoint/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\n\nimport { ContentBlock } from '../ProcessList/styles';\nimport CheckpointGroup from '../CheckpointGroup';\nimport { generateCheckpointGroups } from '../Container/checkpointUtils';\nimport { ProcessEntry } from '../../../../api/process';\nimport NoCheckpointsMessage from '../NoCheckpointsMessage';\nimport CheckpointErrorBoundary from '../CheckpointErrorBoundry';\n\ninterface Props {\n    process: ProcessEntry;\n}\n\nexport const ProcessCheckpoint: React.SFC<Props> = ({ process }) => {\n    if (process.checkpoints) {\n        return (\n            <ContentBlock style={{ margin: '16px 0' }}>\n                <CheckpointErrorBoundary>\n                    <CheckpointGroup\n                        process={process}\n                        checkpointGroups={generateCheckpointGroups(\n                            process.status,\n                            process.checkpoints,\n                            process.checkpointRestoreHistory\n                        )}\n                    />\n                </CheckpointErrorBoundary>\n            </ContentBlock>\n        );\n    } else {\n        return (\n            <ContentBlock style={{ margin: '16px 0' }}>\n                <NoCheckpointsMessage />\n            </ContentBlock>\n        );\n    }\n};\n\nexport default ProcessCheckpoint;\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/ProcessCheckpointView.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { ProcessEntry } from '../../../api/process';\n\ninterface ExternalProps {\n    process: ProcessEntry;\n}\n\nconst ProcessCheckpointView = (props: ExternalProps) => {};\n\nexport default ProcessCheckpointView;\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/ProcessList/LeftContent.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { formatDistanceToNow, parseISO as parseDate } from 'date-fns';\nimport React from 'react';\nimport { Link } from 'react-router';\nimport { Divider, Icon } from 'semantic-ui-react';\nimport { ProjectEntry, ProjectEntryMeta } from '../../../../api/org/project';\n\nimport { ProcessEntry } from '../../../../api/process';\nimport { Truncate } from '../../../atoms';\nimport { Label, Status } from '../shared/Labels';\nimport { LeftWrap, ListItem } from './styles';\n\ninterface Props {\n    project: ProjectEntry;\n    process: ProcessEntry;\n}\n\nconst renderMeta = (projectMeta?: ProjectEntryMeta, processMeta?: {}) => {\n    if (!projectMeta || !projectMeta.ui || !projectMeta.ui.processList) {\n        return;\n    }\n\n    // here we're going to render only `meta.` variables\n    return projectMeta.ui.processList\n        .filter((i) => i.source && i.source.startsWith('meta.'))\n        .map((i, idx) => {\n            const k = i.source.substr(5); // cut off the `meta.` prefix\n            const v = processMeta ? (processMeta[k] ? processMeta[k] : 'n/a') : 'n/a';\n\n            return (\n                <div key={idx}>\n                    <Label>{i.caption ? i.caption : i.source}:</Label> {v}\n                </div>\n            );\n        });\n};\n\nexport default ({ project, process }: Props) => {\n    return (\n        <LeftWrap maxWidth={300}>\n            <ListItem>\n                <div>\n                    <Label>\n                        Process:{' '}\n                        <Link to={`/process/${process.instanceId}`}>\n                            <Truncate text={process.instanceId} />\n                        </Link>\n                    </Label>\n                </div>\n\n                {/* Show repository name if it exists */}\n                {process.repoName && (\n                    <div>\n                        <Label>Repo: </Label>\n                        <Link\n                            to={`/org/${process.orgName}/project/${process.projectName}/repository/${process.repoName}`}>\n                            {process.repoName}\n                        </Link>\n                    </div>\n                )}\n\n                {renderMeta(project.meta, process.meta)}\n\n                <Divider />\n                <div>\n                    <Label>Current Status: </Label>\n                    <Status>{process.status}</Status>\n                </div>\n\n                <div>\n                    <Label>Enable: </Label>\n                    <Icon name=\"power\" color={process.disabled ? 'grey' : 'green'} />\n                </div>\n\n                {/* Last update time, tooltip on mouse hover */}\n                <div title={process.lastUpdatedAt}>\n                    <Label>Last update: </Label>\n                    <Status>{formatDistanceToNow(parseDate(process.lastUpdatedAt))}</Status>\n                </div>\n            </ListItem>\n        </LeftWrap>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/ProcessList/RightContent.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n// @ts-nocheck\n\nimport React from 'react';\nimport { RightWrap } from './styles';\nimport CheckpointGroup from '../CheckpointGroup';\nimport NoCheckpointsMessage from '../NoCheckpointsMessage';\nimport { ProcessEntry } from '../../../../api/process';\nimport {useCheckpointContext} from \"../Container\";\n\ninterface Props {\n    process: ProcessEntry;\n}\n\nexport default ({ process }: Props) => {\n    const { checkpointGroups } = useCheckpointContext();\n\n    return (\n        <RightWrap>\n            {process.checkpoints && (\n                <>\n                    {process.checkpoints.length && (\n                        <CheckpointGroup\n                            process={process}\n                            checkpointGroups={checkpointGroups[process.instanceId]}\n                        />\n                    )}\n                </>\n            )}\n            {!process.checkpoints && <NoCheckpointsMessage />}\n        </RightWrap>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/ProcessList/styles.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport styled from 'styled-components';\nimport { Column } from '../shared/Layout';\n\nexport const LeftWrap = styled(Column)`\n    border: 1px solid #dedfde;\n    border-top-left-radius: 5px;\n    border-bottom-left-radius: 5px;\n    float: left;\n`;\n\nexport const ContentBlock = styled(Column)`\n    border: 1px solid #dedfde;\n    border-radius: 5px;\n    overflow-y: hidden;\n    overflow-x: auto;\n\n    --shadow-height: 100%;\n    --shadow-color: rgba(0, 0, 0, 0.1);\n    --shadow-weight: 9px;\n\n    /* Left start and right start 'inside' container colors (they overlap the shadows) */\n    background: linear-gradient(90deg, white 0%, rgba(255, 255, 255, 0)),\n        linear-gradient(-90deg, white 0%, rgba(255, 255, 255, 0)) 100% 0,\n        /* Left and right scroll shadows */\n            linear-gradient(90deg, var(--shadow-color), rgba(0, 0, 0, 0)),\n        linear-gradient(-90deg, var(--shadow-color), rgba(0, 0, 0, 0)) 100% 0;\n    background-repeat: no-repeat;\n    background-color: #fff;\n    background-size: 100px 100%, 100px 100%, var(--shadow-weight) var(--shadow-height),\n        var(--shadow-weight) var(--shadow-height);\n    background-attachment: local, local, scroll, scroll;\n\n    /* Scrollbar has a bit of a custom look */\n    &::-webkit-scrollbar {\n        height: 6px;\n    }\n\n    &::-webkit-scrollbar-track {\n        box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.3);\n        border-radius: 10px;\n    }\n\n    &::-webkit-scrollbar-thumb {\n        border-radius: 10px;\n        box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.5);\n    }\n`;\n\nexport const RightWrap = styled(ContentBlock)`\n    border-top-left-radius: 0px;\n    border-bottom-left-radius: 0px;\n`;\n\nexport const ListItem = styled('li')`\n    font-family: lato;\n    text-align: left;\n    list-style-type: none;\n    color: #706f70;\n\n    padding: 16px;\n\n    i {\n        padding: 0px 8px;\n        display: inline;\n        position: relative;\n        bottom: 2px;\n    }\n`;\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport React, { FunctionComponent } from 'react';\nimport CheckpointErrorBoundary from './CheckpointErrorBoundry';\nimport ActionBar from './ActionBar';\nimport LeftContent from './ProcessList/LeftContent';\nimport { Row } from './shared/Layout';\nimport RightContent from './ProcessList/RightContent';\nimport { ProjectEntry } from '../../../api/org/project';\nimport {useCheckpointContext, CheckpointProvider} from \"./Container\";\n\n/**\n * This View renders the two bigger components that make up the this checkpoint view\n *\n * @Component ActionBar contains refresh, filter, and pagination elements\n * @Component Map over process details to create list of process items and details\n */\nexport const View = () => {\n    const { project, processes } = useCheckpointContext();\n\n    return (\n        <CheckpointErrorBoundary>\n            <ActionBar />\n\n            {processes &&\n                processes.map((process) => {\n                    return (\n                        <Row key={process.instanceId}>\n                            <LeftContent project={project} process={process} />\n                            <RightContent process={process} />\n                        </Row>\n                    );\n                })}\n        </CheckpointErrorBoundary>\n    );\n};\n\n/**\n * Renders Context Providers for the Checkpoint View to consume\n * @param project the Concord Project\n */\nexport const CheckpointView: FunctionComponent<{ project: ProjectEntry }> = ({ project }) => (\n    <CheckpointProvider project={project} refreshInterval={5000}>\n        <View />\n    </CheckpointProvider>\n);\n\nexport default CheckpointView;\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/shared/Labels.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { ClassIcon } from '../../../atoms/ClassIcon';\nimport styled from 'styled-components';\n\nconst TextBase = styled.span`\n    font-family: lato;\n    color: #706f70;\n`;\n\nexport const Label = styled(TextBase)`\n    font-weight: bold;\n    font-size: 1rem;\n`;\n\nexport const StatusText = styled(TextBase)`\n    font-size: 1rem;\n    display: inline;\n`;\n\nexport const CheckpointName = styled(TextBase)`\n    font-size: 1rem;\n    font-weight: bold;\n    margin-bottom: 4px;\n`;\n\nexport const CheckpointGroupName = styled(CheckpointName)`\n    font-size: 1.2rem;\n    font-weight: bold;\n`;\n\nexport const LoadError = styled.div`\n    color: grey;\n    font-weight: bold;\n    font-size: 1.2rem;\n    margin: auto auto;\n    padding: 1em;\n`;\n\nexport const Status: React.SFC<{ as?: 'span' | 'div' | 'td' }> = ({ as = 'span', children }) => {\n    switch (children) {\n        case 'FAILED':\n            return React.createElement(\n                as,\n                { style: { color: '#DB2928' } },\n                <>\n                    {children} <ClassIcon classes=\"red cancel icon\" />\n                </>\n            );\n        case 'FINISHED':\n            return React.createElement(\n                as,\n                { style: { color: 'green' } },\n                <>\n                    {children} <ClassIcon classes=\"green check icon\" />\n                </>\n            );\n        default:\n            return React.createElement(as, null, children);\n    }\n};\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/shared/Layout.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport styled from 'styled-components';\n\nexport const Row = styled.div`\n    /* display: grid;\n    grid-template-columns: fit-content(300px) 1fr; */\n\n    /* // TODO: Determine a better way to handle long repoMetadata */\n\n    display: flex;\n    flex-direction: row;\n    flex-wrap: nowrap;\n\n    width: 100%;\n    margin: 24px 0px;\n    border-radius: 5px;\n    box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15);\n`;\n\ninterface ColumnProps {\n    flex?: number;\n    background?: string;\n    maxWidth?: number;\n}\n\nexport const Column = styled('div')<ColumnProps>`\n    display: flex;\n    flex-direction: column;\n    flex-basis: 100%;\n    flex: ${(props: ColumnProps) => (props.flex ? props.flex : 1)};\n    background-color: ${(props: ColumnProps) => props.background};\n    ${(props: ColumnProps) => (props.maxWidth ? `max-width: ${props.maxWidth}px` : null)};\n`;\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/shared/types.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { ProcessCheckpointEntry, ProcessStatus } from '../../../../api/process';\n\nexport interface CheckpointGroup {\n    name: string;\n    SubText?: string;\n    checkpoints: CustomCheckpoint[];\n    status?: ProcessStatus;\n}\n\nexport interface CustomCheckpoint extends ProcessCheckpointEntry {\n    startTime: Date;\n    status: ProcessStatus;\n}\n"
  },
  {
    "path": "console2/src/components/organisms/CheckpointView/shared/utils.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nexport { getStatusSemanticColor } from '../../../../api/process';\n"
  },
  {
    "path": "console2/src/components/organisms/DeleteRepositoryPopup/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useState } from 'react';\nimport { Input } from 'semantic-ui-react';\n\nimport { ConcordKey, GenericOperationResult } from '../../../api/common';\nimport { SingleOperationPopup } from '../../molecules';\nimport { deleteRepository as apiRepoDelete } from '../../../api/org/project/repository';\nimport { useApi } from '../../../hooks/useApi';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    repoName: ConcordKey;\n    trigger: (onClick: () => void) => React.ReactNode;\n    onDone: () => void;\n}\n\nconst DeleteRepositoryPopup = (props: ExternalProps) => {\n    const { orgName, projectName, repoName, trigger, onDone } = props;\n\n    const [confirmation, setConfirmation] = useState('');\n\n    const deleteDataRequest = useCallback(() => {\n        return apiRepoDelete(orgName, projectName, repoName);\n    }, [orgName, projectName, repoName]);\n\n    const { data, isLoading, error, clearState, fetch } = useApi<GenericOperationResult>(\n        deleteDataRequest,\n        { fetchOnMount: false, requestByFetch: true }\n    );\n\n    const resetHandler = useCallback(() => {\n        clearState();\n    }, [clearState]);\n\n    return (\n        <SingleOperationPopup\n            trigger={trigger}\n            title=\"Delete repository?\"\n            introMsg={\n                <>\n                    <p>\n                        Are you sure you want to delete the repository? Any process or repository\n                        that uses this repository may stop working correctly.\n                    </p>\n                    <p>\n                        Please type <strong>{repoName}</strong> to confirm.\n                    </p>\n                    <div className={`ui input ${confirmation !== repoName ? 'error' : ''}`}>\n                        <Input\n                            type=\"text\"\n                            name=\"name\"\n                            placeholder=\"Repository name\"\n                            value={confirmation}\n                            onChange={(e, data) => setConfirmation(data.value)}\n                        />\n                    </div>\n                </>\n            }\n            running={isLoading}\n            runningMsg={<p>Removing the repository...</p>}\n            success={data !== undefined}\n            successMsg={<p>The repository was removed successfully.</p>}\n            error={error}\n            reset={resetHandler}\n            onConfirm={fetch}\n            onDone={onDone}\n            disableYes={confirmation !== repoName}\n        />\n    );\n};\n\nexport default DeleteRepositoryPopup;\n"
  },
  {
    "path": "console2/src/components/organisms/DisableProcessPopup/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport { ConcordId, RequestError } from '../../../api/common';\nimport { SingleOperationPopup } from '../../molecules';\nimport { memo } from 'react';\nimport { useState } from 'react';\nimport { useCallback } from 'react';\nimport { disable as apiDisable } from '../../../api/process';\n\ninterface ExternalProps {\n    instanceId: ConcordId;\n    disabled: boolean;\n    refresh: () => void;\n    trigger: (onClick: () => void) => React.ReactNode;\n}\n\nconst DisableProcessPopup = memo((props: ExternalProps) => {\n    const [disabling, setDisabling] = useState(false);\n    const [error, setError] = useState<RequestError>();\n    const [success, setSuccess] = useState(false);\n\n    const { instanceId, disabled } = props;\n\n    const disableProcess = useCallback(async () => {\n        setDisabling(true);\n\n        try {\n            await apiDisable(instanceId, disabled);\n            setSuccess(true);\n        } catch (e) {\n            setError(e);\n        } finally {\n            setDisabling(false);\n        }\n    }, [instanceId, disabled]);\n\n    const reset = useCallback(() => {\n        setDisabling(false);\n        setSuccess(false);\n        setError(undefined);\n    }, []);\n\n    const { trigger, refresh } = props;\n    const operation = disabled ? 'Disable' : 'Enable';\n\n    return (\n        <SingleOperationPopup\n            trigger={trigger}\n            title={operation + ' the process?'}\n            introMsg={\n                <p>Are you sure you want to {operation.toLowerCase()} the selected process?</p>\n            }\n            running={disabling}\n            runningMsg={disabled ? 'Disabling...' : 'Enabling...'}\n            success={success}\n            error={error}\n            reset={reset}\n            onDone={refresh}\n            onConfirm={disableProcess}\n        />\n    );\n});\n\nexport default DisableProcessPopup;\n"
  },
  {
    "path": "console2/src/components/organisms/EditProjectActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport {ConcordKey, GenericOperationResult} from '../../../api/common';\nimport {EditProjectForm, FormValues} from '../../molecules';\nimport { UpdateProjectEntry, ProjectEntry } from '../../../api/org/project';\nimport { RequestErrorActivity } from '../index';\nimport {useCallback, useState} from \"react\";\nimport {createOrUpdate as apiUpdate} from \"../../../api/org/project\";\nimport {useApi} from \"../../../hooks/useApi\";\nimport {LoadingDispatch} from \"../../../App\";\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    initial?: ProjectEntry;\n}\n\nconst toUpdateProjectEntry = (p?: ProjectEntry): UpdateProjectEntry => {\n    return {\n        id: p?.id,\n        name: p?.name,\n        visibility: p?.visibility,\n        description: p?.description\n    };\n};\n\nconst EditProjectActivity = (props: ExternalProps) => {\n    const {orgName, initial} = props;\n\n    const dispatch = React.useContext(LoadingDispatch);\n    const [updateEntry, setUpdateEntry] = useState(toUpdateProjectEntry(initial));\n\n    const postData = useCallback(() => {\n        return apiUpdate(orgName, updateEntry);\n    }, [orgName, updateEntry]);\n\n    const { error, isLoading, fetch } = useApi<GenericOperationResult>(postData, {\n        fetchOnMount: false,\n        requestByFetch: true,\n        dispatch\n    });\n\n    const handleSubmit = useCallback(\n        (values: FormValues) => {\n            setUpdateEntry(values.data);\n            fetch();\n        },\n        [fetch]\n    );\n\n    if (!initial) {\n        return <></>;\n    }\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n\n            <EditProjectForm\n                submitting={isLoading}\n                data={toUpdateProjectEntry(initial)}\n                onSubmit={handleSubmit}\n            />\n        </>\n    );\n};\n\nexport default EditProjectActivity;"
  },
  {
    "path": "console2/src/components/organisms/EditRepositoryActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport { testRepository } from '../../../api/service/console';\nimport { RepositoryForm, RepositoryFormValues, RepositorySourceType } from '../../molecules';\nimport { RequestErrorActivity } from '../index';\nimport {\n    createOrUpdate as apiCreateOrUpdate,\n    EditRepositoryEntry,\n    get as apiGetRepo,\n    RepositoryEntry,\n} from '../../../api/org/project/repository';\nimport { useApi } from '../../../hooks/useApi';\nimport { ConcordKey, RequestError } from '../../../api/common';\nimport { Navigate } from 'react-router';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n\n    /** defined for edit, undefined for new repos */\n    repoName?: ConcordKey;\n\n    forceRefresh: any;\n}\n\nconst INITIAL_VALUES: RepositoryFormValues = {\n    name: '',\n    url: '',\n    enabled: true,\n    sourceType: RepositorySourceType.BRANCH_OR_TAG,\n    triggersEnabled: true,\n};\n\nconst EditRepositoryActivity = (props: ExternalProps) => {\n    const { orgName, projectName, repoName, forceRefresh } = props;\n\n    const [success, setSuccess] = useState<boolean>(false);\n    const [error, setError] = useState<RequestError>();\n    const [isLoading, setLoading] = useState<boolean>(false);\n\n    const loadRepo = useCallback(() => {\n        return apiGetRepo(orgName, projectName, repoName!);\n    }, [orgName, projectName, repoName]);\n\n    const {\n        fetch: loadRepoFetch,\n        clearState: loadRepoClearState,\n        data: loadRepoData,\n        error: loadError,\n    } = useApi<RepositoryEntry>(loadRepo, { fetchOnMount: false });\n\n    useEffect(() => {\n        if (repoName === undefined) {\n            return;\n        }\n\n        loadRepoClearState();\n        loadRepoFetch();\n    }, [loadRepoFetch, loadRepoClearState, forceRefresh, repoName]);\n\n    const handleSubmit = useCallback(\n        async (values: RepositoryFormValues, setSubmitting: (isSubmitting: boolean) => void) => {\n            setLoading(true);\n\n            try {\n                const result = await apiCreateOrUpdate(\n                    orgName,\n                    projectName,\n                    toEditRepositoryEntry(values)\n                );\n                setSuccess(result.ok);\n            } catch (e) {\n                setError(e);\n            } finally {\n                setLoading(false);\n                setSubmitting(false);\n            }\n        },\n        [orgName, projectName]\n    );\n\n    if (success) {\n        return <Navigate to={`/org/${orgName}/project/${projectName}/repository`} />;\n    }\n\n    if (loadError) {\n        return <RequestErrorActivity error={loadError} />;\n    }\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n\n            <RepositoryForm\n                orgName={orgName}\n                projectName={projectName}\n                onSubmit={handleSubmit}\n                submitting={isLoading}\n                editMode={true}\n                initial={toFormValues(loadRepoData) || INITIAL_VALUES}\n                testRepository={({ name, sourceType, id, ...rest }) =>\n                    testRepository({ orgName, projectName, ...rest })\n                }\n            />\n        </>\n    );\n};\n\nconst toFormValues = (r?: RepositoryEntry): RepositoryFormValues | undefined => {\n    if (!r) {\n        return;\n    }\n\n    const sourceType = r.commitId\n        ? RepositorySourceType.COMMIT_ID\n        : RepositorySourceType.BRANCH_OR_TAG;\n\n    return {\n        id: r.id,\n        name: r.name,\n        url: r.url,\n        sourceType,\n        branch: r.branch,\n        commitId: r.commitId,\n        path: r.path,\n        secretId: r.secretId,\n        secretName: r.secretName,\n        enabled: !r.disabled,\n        triggersEnabled: !r.triggersDisabled,\n    };\n};\n\nconst notEmpty = (s: string | undefined): string | undefined => {\n    if (!s) {\n        return;\n    }\n\n    if (s === '') {\n        return;\n    }\n\n    return s;\n};\n\nconst toEditRepositoryEntry = (repo: RepositoryFormValues): EditRepositoryEntry => {\n    let branch = notEmpty(repo.branch);\n    if (repo.sourceType !== RepositorySourceType.BRANCH_OR_TAG) {\n        branch = undefined;\n    }\n\n    let commitId = notEmpty(repo.commitId);\n    if (repo.sourceType !== RepositorySourceType.COMMIT_ID) {\n        commitId = undefined;\n    }\n\n    return {\n        id: repo.id,\n        name: repo.name,\n        url: repo.url,\n        branch,\n        commitId,\n        path: repo.path,\n        secretId: repo.secretId!,\n        disabled: !repo.enabled,\n        triggersDisabled: !repo.triggersEnabled,\n    };\n};\n\nexport default EditRepositoryActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/EncryptValueActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport copyToClipboard from 'copy-to-clipboard';\nimport * as React from 'react';\nimport { Form, Icon, Input, Message } from 'semantic-ui-react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { encrypt } from '../../../api/org/project';\n\nimport './styles.css';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n}\n\ninterface State {\n    encrypting: boolean;\n    result: any;\n    success: boolean | undefined;\n    data: string;\n    dirty: boolean;\n}\n\nclass EncryptValueActivity extends React.PureComponent<ExternalProps, State> {\n    constructor(props: ExternalProps) {\n        super(props);\n\n        this.state = {\n            encrypting: false,\n            result: undefined,\n            success: undefined,\n            data: '',\n            dirty: false\n        };\n    }\n\n    handleEncryptValue(value: string) {\n        this.setState({ data: value });\n\n        if (value !== '') {\n            this.setState({ dirty: true });\n        } else {\n            this.setState({ dirty: false });\n        }\n    }\n\n    reset() {\n        this.setState({\n            data: '',\n            result: undefined,\n            success: undefined\n        });\n    }\n\n    encryptValue(data: string) {\n        this.setState({\n            encrypting: true,\n            result: false\n        });\n\n        encrypt(this.props.orgName, this.props.projectName, data)\n            .then((responseData) => {\n                this.setState({\n                    encrypting: false,\n                    success: true,\n                    result: responseData\n                });\n            })\n            .catch((error) => {\n                this.setState({\n                    encrypting: false,\n                    success: false,\n                    result: error.details\n                });\n            });\n    }\n\n    render() {\n        const { result, success, encrypting, data, dirty } = this.state;\n\n        return (\n            <>\n                <Form>\n                    <Form.Group widths={3}>\n                        <Form.Input\n                            name=\"encrypt\"\n                            value={data}\n                            autoComplete=\"off\"\n                            onChange={(e, { value }) => this.handleEncryptValue(value)}\n                        />\n\n                        <Form.Button\n                            primary={true}\n                            loading={encrypting}\n                            negative={false}\n                            content=\"Encrypt\"\n                            disabled={!dirty}\n                            onClick={(ev) => {\n                                ev.preventDefault();\n                                this.encryptValue(data);\n                            }}\n                        />\n                    </Form.Group>\n                </Form>\n\n                <p>\n                    The encrypted value can be later decrypted in flows using{' '}\n                    <span className=\"codeSnippet\">{`\\${crypto.decryptString(\"value\")}`}</span>{' '}\n                    expression.\n                </p>\n                <p>The value is valid for the current project only.</p>\n\n                {result && (\n                    <Message success={success} error={!success} onDismiss={() => this.reset()}>\n                        <Message.Header>{success ? 'Success' : 'Failure'}</Message.Header>\n                        <br />\n                        <Message.Content>\n                            {success ? (\n                                <div>\n                                    <Input\n                                        icon={\n                                            <Icon\n                                                name=\"copy\"\n                                                link={true}\n                                                onClick={() =>\n                                                    (copyToClipboard as any)(result.data)\n                                                }\n                                            />\n                                        }\n                                        fluid={true}\n                                        value={result.data}\n                                        className=\"encryptedValue\"\n                                    />\n                                </div>\n                            ) : (\n                                <div>{result}</div>\n                            )}\n                        </Message.Content>\n                    </Message>\n                )}\n            </>\n        );\n    }\n}\n\nexport default EncryptValueActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/EncryptValueActivity/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n.codeSnippet {\n    color: #B03060;\n    background-color: #F0F0F0;\n    font-family: monospace;\n}\n\n.encryptedValue input {\n    font-family: monospace !important;\n}\n"
  },
  {
    "path": "console2/src/components/organisms/FindLdapGroupField/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport { Search, SearchResultData, SearchResultProps } from 'semantic-ui-react';\n\nimport { RequestError } from '../../../api/common';\nimport {\n    findLdapGroups as apiFindLdapGroups,\n    LdapGroupSearchResult\n} from '../../../api/service/console';\nimport { useThrottle } from '../../../hooks/useThrottle';\n\ninterface Props {\n    onSelect: (value: LdapGroupSearchResult) => void;\n    onChange?: (value?: string) => void;\n    placeholder?: string;\n}\n\n// TODO remove when the Search component will support custom result types\nconst toResults = (items: LdapGroupSearchResult[]) =>\n    items.map((i) => ({\n        title: i.displayName,\n        description: i.groupName\n    }));\n\n// TODO remove when the Search component will support custom result types\nconst resultToItem = (result: SearchResultProps, items: LdapGroupSearchResult[]) =>\n    items.find((i) => i.groupName === result.description)!;\n\nconst FindLdapGroupField = ({ onSelect, onChange, placeholder }: Props) => {\n    const [filter, setFilter] = useState<string>('');\n    const [loading, setLoading] = useState(false);\n    const [error, setError] = useState<RequestError>();\n    const [items, setItems] = useState<LdapGroupSearchResult[]>([]);\n\n    const performSearch = useCallback(async (searchFilter: string) => {\n        if (searchFilter.length < 5) {\n            setItems([]);\n            return;\n        }\n\n        try {\n            setLoading(true);\n            setError(undefined);\n            const result = await apiFindLdapGroups(searchFilter);\n            setItems(result || []);\n        } catch (e) {\n            setError(e);\n            setItems([]);\n        } finally {\n            setLoading(false);\n        }\n    }, []);\n\n    const throttledSearch = useThrottle(performSearch, 2000);\n\n    useEffect(() => {\n        throttledSearch(filter);\n    }, [filter, throttledSearch]);\n\n    const handleSelect = useCallback(\n        ({ result }: SearchResultData) => {\n            if (!result) {\n                return;\n            }\n\n            const item = resultToItem(result, items);\n            if (!item) {\n                return;\n            }\n\n            onSelect(item);\n        },\n        [items, onSelect]\n    );\n\n    return (\n        <Search\n            input={{\n                fluid: true,\n                placeholder: placeholder ? placeholder : 'Search for a user...',\n                error: !!error\n            }}\n            fluid={true}\n            loading={loading}\n            showNoResults={!loading}\n            onSearchChange={(ev, data) => {\n                const newFilter = data.value || '';\n                setFilter(newFilter);\n                if (onChange) {\n                    onChange(newFilter);\n                }\n            }}\n            onResultSelect={(ev, data) => handleSelect(data)}\n            results={toResults(items)}\n        />\n    );\n};\n\nexport default FindLdapGroupField;\n"
  },
  {
    "path": "console2/src/components/organisms/FindOrganizationsField/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport { Search } from 'semantic-ui-react';\n\nimport { list as apiFindOrganizations, get as apiGet, OrganizationEntry } from '../../../api/org';\nimport { SearchProps } from 'semantic-ui-react/dist/commonjs/modules/Search/Search';\n\ninterface Props {\n    defaultOrgName?: string;\n    placeholder?: string;\n    required?: boolean;\n\n    onReset?: (value?: OrganizationEntry) => void;\n    onClear?: () => void;\n    onSelect?: (value: OrganizationEntry) => void;\n}\n\ninterface Result {\n    title: string;\n    description: string;\n}\n\nconst renderTitle = (e: OrganizationEntry) => `${e.name}`;\n\nconst renderDescription = (e: OrganizationEntry): string => '';\n\nexport default ({ defaultOrgName, placeholder, required, onClear, onReset, onSelect }: Props) => {\n    const [defaultItem, setDefaultItem] = useState<OrganizationEntry | undefined>();\n    const [value, setValue] = useState<string | undefined>();\n    const [loading, setLoading] = useState(false);\n    const [error, setError] = useState<boolean>();\n    const [items, setItems] = useState<OrganizationEntry[]>([]);\n    const [results, setResults] = useState<Result[]>([]);\n\n    // perform search whenever the filter changes\n    useEffect(() => {\n        if (!value || value.trim().length < 3) {\n            setResults([]);\n            return;\n        }\n\n        const fetchData = async () => {\n            setLoading(true);\n            try {\n                const result = await apiFindOrganizations(true, 0, 10, value);\n                setItems(result.items);\n            } catch (e) {\n                setError(e);\n            } finally {\n                setLoading(false);\n            }\n        };\n\n        fetchData();\n    }, [value]);\n\n    // convert OrganizationEntries into whatever <Search> accepts\n    useEffect(() => {\n        const r = items.map((i) => ({\n            key: i.id,\n            title: renderTitle(i),\n            description: renderDescription(i)\n        }));\n\n        setResults(r);\n    }, [items]);\n\n    // load the default organization's data\n    useEffect(() => {\n        if (!defaultOrgName) {\n            setDefaultItem(undefined);\n            return;\n        }\n\n        const fetchData = async () => {\n            setLoading(true);\n            try {\n                const result = await apiGet(defaultOrgName);\n                setValue(result ? renderTitle(result) : '');\n                setDefaultItem(result);\n            } catch (e) {\n                setError(e);\n            } finally {\n                setLoading(false);\n            }\n        };\n\n        fetchData();\n    }, [defaultOrgName]);\n\n    const onChangeCallBack = useCallback(\n        (event: React.MouseEvent<HTMLElement>, data: SearchProps) => {\n            setValue(data.value);\n        },\n        []\n    );\n\n    const handleItemSelected = useCallback(\n        (item?: OrganizationEntry) => {\n            setValue(item ? renderTitle(item) : '');\n\n            const isDefault = item?.id === defaultItem?.id;\n            if (isDefault) {\n                onReset?.(item);\n            } else if (item) {\n                onSelect?.(item);\n            } else {\n                if (required) {\n                    setValue(defaultItem ? renderTitle(defaultItem) : '');\n                    onReset?.(defaultItem);\n                } else {\n                    onClear?.();\n                }\n            }\n        },\n        [required, onReset, onSelect, onClear, defaultItem]\n    );\n\n    return (\n        <Search\n            fluid={true}\n            input={{\n                fluid: true,\n                placeholder,\n                error\n            }}\n            value={value}\n            loading={loading}\n            results={results}\n            onBlur={(event, data) => {\n                if (data.value !== '') {\n                    const item = items.find((i) => i.name === data.value);\n                    handleItemSelected(item || defaultItem);\n                } else {\n                    handleItemSelected(undefined);\n                }\n            }}\n            showNoResults={!loading}\n            onSearchChange={onChangeCallBack}\n            onResultSelect={(ev, data) => {\n                const item = items.find((i) => i.id === data.result.key);\n                handleItemSelected(item || defaultItem);\n            }}\n        />\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/FindTeamDropdown/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport { Dropdown, DropdownItemProps } from 'semantic-ui-react';\n\nimport { ConcordKey, RequestError } from '../../../api/common';\nimport { comparators } from '../../../utils';\nimport { list as apiList, TeamEntry } from '../../../api/org/team';\n\ninterface Props {\n    onSelect: (item: TeamEntry) => void;\n    orgName: ConcordKey;\n    name: string;\n}\n\nconst makeOptions = (data: TeamEntry[]): DropdownItemProps[] => {\n    if (!data || data.length === 0) {\n        return [];\n    }\n\n    return data.sort(comparators.byName).map(({ name, id }) => ({\n        value: id,\n        text: name\n    }));\n};\n\nexport default ({ orgName, name, onSelect, ...rest }: Props) => {\n    const [items, setItems] = useState<TeamEntry[]>([]);\n    const [loading, setLoading] = useState(false);\n    const [error, setError] = useState<RequestError | undefined>();\n\n    const handleChange = useCallback(\n        (id: ConcordKey) => {\n            const item = items.find((i) => i.id === id);\n            if (!item) {\n                return;\n            }\n\n            onSelect(item);\n        },\n        [items, onSelect]\n    );\n\n    useEffect(() => {\n        const load = async () => {\n            setLoading(true);\n            setError(undefined);\n            try {\n                setItems(await apiList(orgName));\n            } catch (e) {\n                setError(e);\n            } finally {\n                setLoading(false);\n            }\n        };\n\n        load();\n    }, [orgName]);\n\n    return (\n        <Dropdown\n            placeholder=\"Select team\"\n            loading={loading}\n            error={!!error}\n            selection={true}\n            search={true}\n            options={makeOptions(items)}\n            {...rest}\n            onChange={(ev, { value }) => handleChange(value as ConcordKey)}\n        />\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/FindUserField2/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { useEffect, useState } from 'react';\nimport { Search } from 'semantic-ui-react';\n\nimport { get as apiGet, list as apiList, UserEntry } from '../../../api/user';\nimport { ConcordId, RequestError } from '../../../api/common';\n\ninterface Props {\n    defaultUserId?: ConcordId;\n    placeholder?: string;\n    onSelect?: (value: UserEntry) => void;\n}\n\ninterface Result {\n    title: string;\n    description: string;\n}\n\nconst renderDescription = (e: UserEntry): string => (e.email ? `${e.name} - ${e.email}` : e.name);\n\nconst renderTitle = (e: UserEntry) => (e.displayName ? e.displayName : e.name);\n\nexport default ({ defaultUserId, placeholder, onSelect }: Props) => {\n    const [value, setValue] = useState<string | undefined>();\n    const [loading, setLoading] = useState(false);\n    const [items, setItems] = useState<UserEntry[]>([]);\n    const [results, setResults] = useState<Result[]>([]);\n    const [error, setError] = useState<RequestError>();\n\n    // perform search whenever the filter changes\n    useEffect(() => {\n        if (!value || value.trim().length < 3) {\n            setResults([]);\n            return;\n        }\n\n        const fetchData = async () => {\n            setLoading(true);\n            try {\n                const result = await apiList(0, 10, value);\n                setItems(result.items);\n            } catch (e) {\n                setError(e);\n            } finally {\n                setLoading(false);\n            }\n        };\n\n        fetchData();\n    }, [value]);\n\n    // convert UserEntries into whatever <Search> accepts\n    useEffect(() => {\n        const r = items.map((i) => ({\n            key: i.id,\n            title: renderTitle(i),\n            description: renderDescription(i)\n        }));\n\n        setResults(r);\n    }, [items]);\n\n    // load the default user's data\n    useEffect(() => {\n        if (!defaultUserId) {\n            return;\n        }\n\n        const fetchData = async () => {\n            setLoading(true);\n            try {\n                const result = await apiGet(defaultUserId);\n                setValue(result ? renderTitle(result) : undefined);\n            } catch (e) {\n                setError(e);\n            } finally {\n                setLoading(false);\n            }\n        };\n\n        fetchData();\n    }, [defaultUserId]);\n\n    return (\n        <Search\n            fluid={true}\n            input={{\n                placeholder,\n                error: !!error\n            }}\n            value={value}\n            loading={loading}\n            results={results}\n            onSearchChange={(ev, data) => setValue(data.value)}\n            onResultSelect={(ev, data) => {\n                setValue(data.result.title);\n\n                if (onSelect) {\n                    const item = items.find((i) => i.id === data.result.key);\n                    if (item) {\n                        onSelect(item);\n                    }\n                }\n            }}\n        />\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/Login2/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { RouteComponentProps, withRouter } from '@/router';\nimport { useCallback, useContext, useState } from 'react';\nimport {\n    Card,\n    CardContent,\n    Dimmer,\n    Divider,\n    Form,\n    Image,\n    Loader,\n    Message,\n} from 'semantic-ui-react';\n\nimport { whoami as apiWhoami } from '../../../api/service/console';\nimport { UserSessionContext } from '../../../session';\n\nimport './styles.css';\nimport { Link } from 'react-router';\nimport { parse as parseQueryString } from 'query-string';\n\nconst nonEmpty = (s?: string) => {\n    if (!s) {\n        return;\n    }\n\n    const v = s.trim();\n    if (v.length === 0) {\n        return;\n    }\n\n    return v;\n};\n\nconst getLastLoginType = (): string | null => {\n    return localStorage.getItem('lastLoginType');\n};\n\nconst saveLastLoginType = (type: string) => {\n    localStorage.setItem('lastLoginType', type);\n};\n\nconst clearLastLoginType = () => {\n    localStorage.removeItem('lastLoginType');\n};\n\nconst DEFAULT_FROM_VALUE = '/';\n\nconst getFrom = (props: RouteComponentProps<{}>): string => {\n    const location = props.location as any;\n\n    if (location && location.state && location.state.from && location.state.from.pathname) {\n        return location.state.from.pathname;\n    }\n\n    const fromUrl = parseQueryString(props.location.search);\n\n    if (fromUrl && typeof fromUrl.from === 'string') {\n        return fromUrl.from;\n    }\n\n    return DEFAULT_FROM_VALUE;\n};\n\nconst getRedirectTo = (props: RouteComponentProps<{}>): string | undefined => {\n    const qs = parseQueryString(props.location.search);\n    const redirectTo = qs ? qs.redirectTo : undefined;\n    if (typeof redirectTo === 'string') {\n        return redirectTo;\n    }\n};\n\nconst Login = (props: RouteComponentProps<{}>) => {\n    const [apiError, setApiError] = useState<string | undefined>();\n    const [validationError] = useState<string | undefined>();\n\n    const [username, setUsername] = useState<string>('');\n    const [password, setPassword] = useState<string>('');\n    const [apiKey, setApiKey] = useState<string>('');\n    const [rememberMe, setRememberMe] = useState<boolean | undefined>();\n    const { loggingIn, setLoggingIn, setUserInfo } = useContext(UserSessionContext);\n\n    const handleSubmit = useCallback(async () => {\n        setLoggingIn(true);\n        try {\n            const response = await apiWhoami(\n                nonEmpty(username),\n                nonEmpty(password),\n                rememberMe,\n                nonEmpty(apiKey)\n            );\n            setUserInfo({ ...response });\n\n            saveLastLoginType(nonEmpty(apiKey) ? 'apiKey' : 'username');\n\n            // with 'redirectTo' the user will be redirected to the specified href\n            // the values can be arbitrary endpoints\n            const redirectTo = getRedirectTo(props);\n\n            // the 'from' query parameter value will be pushed to history\n            // the values must be valid routes\n            // e.g. from=/org will be turned into http.../#/org\n            const from = getFrom(props);\n\n            if (redirectTo) {\n                setTimeout(() => {\n                    window.location.href = redirectTo;\n                }, 100);\n            } else {\n                props.history.push(from);\n            }\n        } catch (e) {\n            let msg = e.message || 'Log in error';\n            if (e.status === 401) {\n                msg = 'Invalid username and/or password';\n            }\n\n            setApiError(msg);\n        } finally {\n            setLoggingIn(false);\n        }\n    }, [username, password, rememberMe, apiKey, setLoggingIn, setUserInfo, props]);\n\n    const onChangeLoginType = useCallback(() => {\n        clearLastLoginType();\n        setApiKey('');\n        setUsername('');\n        setPassword('');\n        setRememberMe(false);\n    }, []);\n\n    const lastLoginType = getLastLoginType();\n    const useApiKey =\n        props.location.search.search('useApiKey=true') >= 0 || lastLoginType === 'apiKey';\n    const usernameHint = (window.concord?.login || {}).usernameHint || 'Username';\n\n    return (\n        <Card centered={true}>\n            <CardContent>\n                <Image id=\"concord-logo\" src=\"/images/concord.svg\" size=\"medium\" />\n\n                <Dimmer active={loggingIn} inverted={true}>\n                    <Loader />\n                </Dimmer>\n\n                <Form\n                    error={!!apiError || validationError !== undefined}\n                    onSubmit={() => handleSubmit()}\n                >\n                    {!useApiKey && (\n                        <>\n                            <Form.Input\n                                name=\"username\"\n                                label=\"Username\"\n                                icon=\"user\"\n                                required={true}\n                                value={username}\n                                placeholder={usernameHint}\n                                onChange={(e, { value }) => setUsername(value)}\n                            />\n                            <Form.Input\n                                name=\"password\"\n                                label=\"Password\"\n                                type=\"password\"\n                                icon=\"lock\"\n                                required={true}\n                                value={password}\n                                autoComplete=\"current-password\"\n                                onChange={(e, { value }) => setPassword(value)}\n                            />\n                        </>\n                    )}\n\n                    {useApiKey && (\n                        <Form.Input\n                            name=\"apiKey\"\n                            label=\"API Key\"\n                            type=\"password\"\n                            icon=\"lock\"\n                            required={true}\n                            value={apiKey}\n                            autoComplete=\"current-password\"\n                            onChange={(e, { value }) => setApiKey(value)}\n                        />\n                    )}\n\n                    <Form.Checkbox\n                        name=\"rememberMe\"\n                        label=\"Remember me\"\n                        checked={rememberMe}\n                        onChange={(e, { checked }) => setRememberMe(checked)}\n                    />\n\n                    <Divider />\n\n                    <Message error={true} content={apiError} />\n                    <Message error={true} content={validationError} />\n                    <Form.Button id=\"loginButton\" primary={true} fluid={true}>\n                        Login\n                    </Form.Button>\n                </Form>\n            </CardContent>\n            <CardContent extra={true} textAlign={'center'}>\n                {useApiKey && (\n                    <Link onClick={onChangeLoginType} to=\"/login\">\n                        Login with Username and Password\n                    </Link>\n                )}\n                {!useApiKey && (\n                    <Link onClick={onChangeLoginType} to=\"/login?useApiKey=true\">\n                        Login with API Key\n                    </Link>\n                )}\n            </CardContent>\n        </Card>\n    );\n};\n\nexport default withRouter(Login);\n"
  },
  {
    "path": "console2/src/components/organisms/Login2/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n#useApiKeyLink {\n    font-size: smaller;\n    text-align: right;\n    width: 100%;\n}\n"
  },
  {
    "path": "console2/src/components/organisms/NewAPITokenActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useState } from 'react';\nimport { useHistory } from '@/router';\nimport { Button, Icon, Message } from 'semantic-ui-react';\n\nimport { RequestError } from '../../../api/common';\nimport { NewAPITokenForm, RequestErrorMessage, WithCopyToClipboard } from '../../molecules';\nimport {\n    create as apiCreate,\n    CreateApiKeyResult,\n    NewTokenEntry,\n} from '../../../api/profile/api_token';\n\nconst renderResponse = (response: CreateApiKeyResult, done: () => void, error?: RequestError) => {\n    if (error) {\n        return <RequestErrorMessage error={error} />;\n    }\n\n    const { key } = response;\n\n    return (\n        <>\n            <Message success={true}>\n                <Message.Header>API Token created</Message.Header>\n                <Message.Content>\n                    <div>\n                        <b>Token: </b>\n                        <WithCopyToClipboard value={key}>\n                            <span style={{ fontFamily: 'monospace' }}>{key}</span>\n                        </WithCopyToClipboard>\n                        <p>\n                            <Icon color=\"black\" name=\"info circle\" />\n                            <strong>Store this token for future use.</strong>\n                        </p>\n                    </div>\n                </Message.Content>\n            </Message>\n\n            <Button\n                primary={true}\n                content={'Done'}\n                data-testid=\"api-token-created-done\"\n                onClick={() => done()}\n            />\n        </>\n    );\n};\n\nexport default () => {\n    const history = useHistory();\n    const [submitting, setSubmitting] = useState(false);\n    const [error, setError] = useState<RequestError>();\n    const [response, setResponse] = useState<CreateApiKeyResult>();\n\n    const postData = async (t: NewTokenEntry) => {\n        try {\n            setError(undefined);\n            setSubmitting(true);\n            setResponse(await apiCreate(t));\n        } catch (e) {\n            setError(e);\n        } finally {\n            setSubmitting(false);\n        }\n    };\n\n    if (!error && response) {\n        return renderResponse(response, () => history.push('/profile/api-token'), error);\n    }\n\n    return (\n        <>\n            {error && <RequestErrorMessage error={error} />}\n\n            <NewAPITokenForm\n                submitting={submitting}\n                onSubmit={(t) => postData(t)}\n                initial={{ name: '' }}\n            />\n        </>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/NewProjectActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport { ConcordKey, GenericOperationResult } from '../../../api/common';\nimport { ProjectVisibility } from '../../../api/org/project';\nimport { NewProjectForm, NewProjectFormValues } from '../../molecules';\nimport { RequestErrorActivity } from '../index';\nimport { useCallback, useState } from 'react';\nimport { createOrUpdate as apiCreate } from '../../../api/org/project';\nimport { useApi } from '../../../hooks/useApi';\nimport { LoadingDispatch } from '../../../App';\nimport { Navigate } from 'react-router';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n}\n\nconst INIT_VALUES: NewProjectFormValues = {\n    name: '',\n    visibility: ProjectVisibility.PRIVATE,\n    description: '',\n};\n\nconst NewProjectActivity = (props: ExternalProps) => {\n    const { orgName } = props;\n\n    const dispatch = React.useContext(LoadingDispatch);\n    const [values, setValues] = useState(INIT_VALUES);\n\n    const postQuery = useCallback(() => {\n        return apiCreate(orgName, values);\n    }, [orgName, values]);\n\n    const { error, isLoading, data, fetch } = useApi<GenericOperationResult>(postQuery, {\n        fetchOnMount: false,\n        requestByFetch: true,\n        dispatch: dispatch,\n    });\n\n    const handleSubmit = useCallback(\n        (values: NewProjectFormValues) => {\n            setValues(values);\n            fetch();\n        },\n        [fetch]\n    );\n\n    if (data) {\n        return <Navigate to={`/org/${orgName}/project/${values.name}`} />;\n    }\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n            <NewProjectForm\n                orgName={orgName}\n                submitting={isLoading}\n                onSubmit={handleSubmit}\n                initial={INIT_VALUES}\n            />\n        </>\n    );\n};\n\nexport default NewProjectActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/NewSecretActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport copyToClipboard from 'copy-to-clipboard';\nimport * as React from 'react';\nimport { Button, Message, TextArea } from 'semantic-ui-react';\n\nimport { ConcordKey } from '../../../api/common';\nimport {\n    CreateSecretResponse,\n    SecretStoreType,\n    SecretTypeExt,\n    SecretVisibility,\n    create as apiCreate,\n} from '../../../api/org/secret';\nimport { useApi } from '../../../hooks/useApi';\nimport NewSecretForm, { NewSecretFormValues } from '../../molecules/NewSecretForm';\n\nimport './styles.css';\nimport { LoadingDispatch } from '../../../App';\nimport { useCallback, useState } from 'react';\nimport { RequestErrorActivity } from '../index';\nimport { useHistory } from '@/router';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n}\n\nconst INIT_VALUES: NewSecretFormValues = {\n    name: '',\n    visibility: SecretVisibility.PRIVATE,\n    type: SecretTypeExt.NEW_KEY_PAIR,\n    storeType: SecretStoreType.CONCORD,\n};\n\nconst NewSecretActivity = ({ orgName }: ExternalProps) => {\n    const history = useHistory();\n    const dispatch = React.useContext(LoadingDispatch);\n    const [values, setValues] = useState(INIT_VALUES);\n\n    const postQuery = useCallback(() => apiCreate(orgName, values), [orgName, values]);\n\n    const { error, isLoading, data, fetch } = useApi<CreateSecretResponse>(postQuery, {\n        fetchOnMount: false,\n        requestByFetch: true,\n        dispatch,\n    });\n\n    const handleSubmit = useCallback(\n        (submittedValues: NewSecretFormValues) => {\n            setValues(submittedValues);\n            fetch();\n        },\n        [fetch]\n    );\n\n    if (data) {\n        const { publicKey, password } = data;\n\n        return (\n            <>\n                <Message success={true}>\n                    <Message.Header>Secret created</Message.Header>\n\n                    {publicKey && (\n                        <div>\n                            <b>Public key: </b>\n                            <Button\n                                icon=\"copy\"\n                                size=\"mini\"\n                                basic={true}\n                                onClick={() => (copyToClipboard as any)(publicKey)}\n                            />\n                            <TextArea className=\"secretData\" value={publicKey} rows={5} />\n                        </div>\n                    )}\n\n                    {password && (\n                        <div>\n                            <b>Export password: </b>\n                            <Button\n                                icon=\"copy\"\n                                size=\"mini\"\n                                basic={true}\n                                onClick={() => (copyToClipboard as any)(password)}\n                            />\n                            <TextArea className=\"secretData\" value={password} rows={2} />\n                        </div>\n                    )}\n                </Message>\n\n                <Button\n                    primary={true}\n                    content=\"Done\"\n                    onClick={() => history.push(`/org/${orgName}/secret/${values.name}`)}\n                />\n            </>\n        );\n    }\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n\n            <NewSecretForm\n                orgName={orgName}\n                submitting={isLoading}\n                onSubmit={handleSubmit}\n                initial={INIT_VALUES}\n            />\n        </>\n    );\n};\n\nexport default NewSecretActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/NewSecretActivity/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n.secretData {\n    width: 100%;\n    font-family: monospace;\n    margin-top: 5px;\n}\n"
  },
  {
    "path": "console2/src/components/organisms/NewStorageActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport { ConcordKey, GenericOperationResult } from '../../../api/common';\nimport { NewStorageForm } from '../../molecules';\nimport RequestErrorActivity from '../RequestErrorActivity';\nimport { useCallback, useState } from 'react';\nimport { StorageVisibility, createOrUpdate as apiCreate } from '../../../api/org/jsonstore';\nimport { useApi } from '../../../hooks/useApi';\nimport { NewStorageFormValues } from '../../molecules/NewStorageForm';\nimport { Navigate } from 'react-router';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n}\n\nconst INIT_VALUES = {\n    name: '',\n    visibility: StorageVisibility.PRIVATE,\n};\n\nconst NewStoreActivity = ({ orgName }: ExternalProps) => {\n    const [values, setValues] = useState<NewStorageFormValues>(INIT_VALUES);\n\n    const postData = useCallback(() => {\n        return apiCreate(orgName, values.name, values.visibility);\n    }, [orgName, values]);\n\n    const { error, isLoading, data } = useApi<GenericOperationResult>(postData, {\n        fetchOnMount: false,\n    });\n\n    const handleSubmit = useCallback((values: NewStorageFormValues) => {\n        setValues({ ...values });\n    }, []);\n\n    if (data) {\n        return <Navigate to={`/org/${orgName}/jsonstore/${values.name}`} />;\n    }\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n            <NewStorageForm\n                orgName={orgName}\n                submitting={isLoading}\n                onSubmit={handleSubmit}\n                initial={INIT_VALUES}\n            />\n        </>\n    );\n};\n\nexport default NewStoreActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/NewTeamActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useHistory } from '@/router';\n\nimport { ConcordKey, RequestError } from '../../../api/common';\nimport { NewTeamEntry, createOrUpdate as apiCreateOrUpdate } from '../../../api/org/team';\nimport { NewTeamForm, RequestErrorMessage } from '../../molecules';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n}\n\nconst NewTeamActivity = ({ orgName }: ExternalProps) => {\n    const history = useHistory();\n    const [error, setError] = React.useState<RequestError>();\n    const [submitting, setSubmitting] = React.useState(false);\n\n    const submit = React.useCallback(\n        async (entry: NewTeamEntry) => {\n            setSubmitting(true);\n            setError(undefined);\n\n            try {\n                await apiCreateOrUpdate(orgName, entry);\n                history.push(`/org/${orgName}/team/${entry.name}`);\n            } catch (e) {\n                setError(e);\n            } finally {\n                setSubmitting(false);\n            }\n        },\n        [history, orgName]\n    );\n\n    return (\n        <>\n            {error && <RequestErrorMessage error={error} />}\n            <NewTeamForm\n                orgName={orgName}\n                submitting={submitting}\n                onSubmit={(values) => submit(values)}\n            />\n        </>\n    );\n};\n\nexport default NewTeamActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/OrganizationActivity/OrganizationProcesses.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { ProcessListActivity, RequestErrorActivity } from '../index';\nimport { LoadingDispatch } from '../../../App';\nimport { useApi } from '../../../hooks/useApi';\nimport { get as apiGet, OrganizationEntry } from '../../../api/org';\nimport { useCallback } from 'react';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    forceRefresh: any;\n}\n\nconst OrganizationProcesses = ({ orgName, forceRefresh }: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const fetchData = useCallback(() => {\n        return apiGet(orgName);\n    }, [orgName]);\n\n    const { data, error } = useApi<OrganizationEntry>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n\n    if (!data) {\n        return <></>;\n    }\n\n    if (\n        data.meta !== undefined &&\n        data.meta.ui !== undefined &&\n        data.meta.ui.processList !== undefined\n    ) {\n        return (\n            <ProcessListActivity\n                orgName={orgName}\n                columns={data.meta.ui.processList}\n                usePagination={true}\n            />\n        );\n    } else {\n        return <ProcessListActivity orgName={orgName} usePagination={true} />;\n    }\n};\n\nexport default OrganizationProcesses;\n"
  },
  {
    "path": "console2/src/components/organisms/OrganizationActivity/OrganizationSettings.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { Divider, Header, Segment } from 'semantic-ui-react';\nimport { OrganizationOwnerChangeActivity, RequestErrorActivity } from '../index';\nimport { LoadingDispatch } from '../../../App';\nimport { useApi } from '../../../hooks/useApi';\nimport { get as apiGet, OrganizationEntry } from '../../../api/org';\nimport { useCallback } from 'react';\nimport EntityId from '../../molecules/EntityId';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    forceRefresh: any;\n}\n\nconst OrganizationSettings = ({ orgName, forceRefresh }: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const fetchData = useCallback(() => {\n        return apiGet(orgName);\n    }, [orgName]);\n\n    const { data, error } = useApi<OrganizationEntry>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n\n    const disabled = !data;\n\n    return (\n        <>\n            <Header as=\"h5\" disabled={true}>\n                <EntityId id={data?.id} />\n            </Header>\n\n            <Divider horizontal={true} content=\"Danger Zone\" disabled={disabled} />\n\n            <Segment color=\"red\" disabled={disabled}>\n                <Header as=\"h4\">Organization owner</Header>\n                <OrganizationOwnerChangeActivity\n                    orgId={data?.id}\n                    initialOwnerId={data?.owner?.id}\n                    disabled={disabled}\n                />\n            </Segment>\n        </>\n    );\n};\n\nexport default OrganizationSettings;\n"
  },
  {
    "path": "console2/src/components/organisms/OrganizationActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Navigate, Route, Routes } from 'react-router';\nimport { Link } from 'react-router';\nimport { Icon, Menu } from 'semantic-ui-react';\nimport { ConcordKey } from '../../../api/common';\nimport {\n    AuditLogActivity,\n    ProjectListActivity,\n    SecretListActivity,\n    TeamListActivity,\n} from '../../organisms';\n\nimport { NotFoundPage } from '../../pages';\nimport StorageListActivity from '../../pages/JsonStorePage/StoreListActivity';\nimport OrganizationSettings from './OrganizationSettings';\nimport OrganizationProcesses from './OrganizationProcesses';\n\nexport type TabLink =\n    | 'process'\n    | 'project'\n    | 'secret'\n    | 'team'\n    | 'jsonstore'\n    | 'settings'\n    | 'audit'\n    | null;\n\ninterface ExternalProps {\n    activeTab: TabLink;\n    orgName: ConcordKey;\n    forceRefresh: any;\n}\n\nconst OrganizationActivity = ({ activeTab, orgName, forceRefresh }: ExternalProps) => {\n    const baseUrl = `/org/${orgName}`;\n\n    return (\n        <>\n            <Menu tabular={true} style={{ marginTop: 0 }}>\n                <Menu.Item active={activeTab === 'project'}>\n                    <Icon name=\"sitemap\" />\n                    <Link to={`${baseUrl}/project`}>Projects</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'process'}>\n                    <Icon name=\"tasks\" />\n                    <Link to={`${baseUrl}/process`}>Processes</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'secret'}>\n                    <Icon name=\"lock\" />\n                    <Link to={`${baseUrl}/secret`}>Secrets</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'team'}>\n                    <Icon name=\"users\" />\n                    <Link to={`${baseUrl}/team`}>Teams</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'jsonstore'}>\n                    <Icon name=\"database\" />\n                    <Link to={`${baseUrl}/jsonstore`}>JSON Stores</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'settings'}>\n                    <Icon name=\"setting\" />\n                    <Link to={`${baseUrl}/settings`}>Settings</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'audit'}>\n                    <Icon name=\"history\" />\n                    <Link to={`${baseUrl}/audit`}>Audit Log</Link>\n                </Menu.Item>\n            </Menu>\n\n            <Routes>\n                <Route index={true} element={<Navigate to=\"project\" replace={true} />} />\n                <Route\n                    path=\"project\"\n                    element={<ProjectListActivity orgName={orgName} forceRefresh={forceRefresh} />}\n                />\n                <Route\n                    path=\"process\"\n                    element={\n                        <OrganizationProcesses orgName={orgName} forceRefresh={forceRefresh} />\n                    }\n                />\n                <Route\n                    path=\"secret\"\n                    element={<SecretListActivity orgName={orgName} forceRefresh={forceRefresh} />}\n                />\n                <Route\n                    path=\"team\"\n                    element={<TeamListActivity orgName={orgName} forceRefresh={forceRefresh} />}\n                />\n                <Route\n                    path=\"jsonstore\"\n                    element={<StorageListActivity orgName={orgName} forceRefresh={forceRefresh} />}\n                />\n                <Route\n                    path=\"settings\"\n                    element={<OrganizationSettings orgName={orgName} forceRefresh={forceRefresh} />}\n                />\n                <Route\n                    path=\"audit\"\n                    element={\n                        <AuditLogActivity\n                            showRefreshButton={false}\n                            filter={{ details: { orgName: orgName } }}\n                            forceRefresh={forceRefresh}\n                        />\n                    }\n                />\n                <Route path=\"*\" element={<NotFoundPage />} />\n            </Routes>\n        </>\n    );\n};\n\nexport default OrganizationActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/OrganizationList/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useRef, useState } from 'react';\nimport { Link } from 'react-router';\nimport { Input, List, Menu, Radio } from 'semantic-ui-react';\n\nimport { OrganizationVisibility, PaginatedOrganizationEntries } from '../../../api/org';\nimport { list as getPaginatedOrgList } from '../../../api/org/index';\nimport { PaginationToolBar } from '../../molecules';\nimport { usePagination } from '../../molecules/PaginationToolBar/usePagination';\nimport { RequestErrorActivity } from '../index';\nimport { useApi } from '../../../hooks/useApi';\nimport { LoadingDispatch } from '../../../App';\n\ninterface ExternalProps {\n    forceRefresh: any;\n}\n\nconst OrganizationList = ({ forceRefresh }: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const {\n        paginationFilter,\n        handleLimitChange,\n        handleNext,\n        handlePrev,\n        handleFirst,\n        resetOffset\n    } = usePagination();\n    const oldFilter = useRef<string>();\n    const oldOnlyCurrent = useRef<boolean>(true);\n\n    const [filter, setFilter] = useState<string>();\n    const [onlyCurrent, setOnlyCurrent] = useState(true);\n\n    const fetchData = useCallback(() => {\n        if (filter && oldFilter.current !== filter) {\n            oldFilter.current = filter;\n            resetOffset(0);\n        }\n\n        if (oldOnlyCurrent.current !== onlyCurrent) {\n            oldOnlyCurrent.current = onlyCurrent;\n            resetOffset(0);\n        }\n\n        return getPaginatedOrgList(\n            onlyCurrent,\n            paginationFilter.offset,\n            paginationFilter.limit,\n            filter\n        );\n    }, [onlyCurrent, filter, paginationFilter.offset, paginationFilter.limit, resetOffset]);\n\n    const { data, error, isLoading } = useApi<PaginatedOrganizationEntries>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    return (\n        <>\n            <Menu secondary={true} style={{ marginTop: 0 }}>\n                <Menu.Item>\n                    <Input\n                        icon=\"search\"\n                        placeholder=\"Filter...\"\n                        onChange={(ev, data) => setFilter(data.value)}\n                    />\n                </Menu.Item>\n\n                <Menu.Item position=\"right\">\n                    <Radio\n                        label=\"Show only user's organizations\"\n                        toggle={true}\n                        checked={onlyCurrent}\n                        onChange={(ev, { checked }) => setOnlyCurrent(checked!)}\n                    />\n                </Menu.Item>\n\n                <Menu.Item style={{ padding: 0 }}>\n                    <PaginationToolBar\n                        limit={paginationFilter.limit}\n                        handleLimitChange={(limit) => handleLimitChange(limit)}\n                        handleNext={handleNext}\n                        handlePrev={handlePrev}\n                        handleFirst={handleFirst}\n                        disablePrevious={paginationFilter.offset === 0}\n                        disableNext={!data?.next}\n                        disableFirst={paginationFilter.offset === 0}\n                    />\n                </Menu.Item>\n            </Menu>\n\n            {error && <RequestErrorActivity error={error} />}\n            {!isLoading && data?.items.length === 0 && <h3>No organizations found</h3>}\n\n            <List divided={true} relaxed={true} size=\"large\">\n                {data?.items.map((org, idx) => (\n                    <List.Item key={idx}>\n                        <List.Icon\n                            name={\n                                org.visibility === OrganizationVisibility.PRIVATE\n                                    ? 'lock'\n                                    : 'unlock'\n                            }\n                            color=\"grey\"\n                        />\n                        <List.Content>\n                            <List.Header as={Link} to={`/org/${org.name}`}>\n                                {org.name}\n                            </List.Header>\n                            <List.Description>\n                                {org.owner ? `Owner: ${org.owner.username}` : ''}\n                            </List.Description>\n                        </List.Content>\n                    </List.Item>\n                ))}\n            </List>\n        </>\n    );\n};\n\nexport default OrganizationList;\n"
  },
  {
    "path": "console2/src/components/organisms/OrganizationOwnerChangeActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport { ConcordId, GenericOperationResult } from '../../../api/common';\nimport EntityOwnerChangeForm from '../../molecules/EntityOwnerChangeForm';\nimport { useCallback, useState } from 'react';\nimport { changeOwner as apiChangeOwner } from '../../../api/org';\nimport { useApi } from '../../../hooks/useApi';\nimport { RequestErrorActivity } from '../index';\n\ninterface ExternalProps {\n    orgId?: ConcordId;\n    initialOwnerId?: ConcordId;\n    disabled: boolean;\n}\n\nconst OrganizationOwnerChangeActivity = ({ orgId, initialOwnerId, disabled }: ExternalProps) => {\n    const [value, setValue] = useState(initialOwnerId);\n\n    const postData = useCallback(() => {\n        return apiChangeOwner(orgId!, value!);\n    }, [orgId, value]);\n\n    const { error, isLoading, fetch, clearState } = useApi<GenericOperationResult>(postData, {\n        fetchOnMount: false,\n        requestByFetch: true\n    });\n\n    const ownerChangeHandler = useCallback(\n        (value: ConcordId) => {\n            setValue(value);\n            clearState();\n            fetch();\n        },\n        [clearState, fetch]\n    );\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n            <EntityOwnerChangeForm\n                originalOwnerId={initialOwnerId}\n                confirmationHeader=\"Change organization owner?\"\n                confirmationContent=\"Are you sure you want to change the organization's owner?\"\n                onSubmit={ownerChangeHandler}\n                submitting={isLoading}\n                disabled={disabled}\n            />\n        </>\n    );\n};\n\nexport default OrganizationOwnerChangeActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessActivity/Toolbar.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { memo, useCallback, useState } from 'react';\nimport {\n    canBeCancelled,\n    canBeRestarted,\n    hasState,\n    isFinal,\n    ProcessEntry,\n    ProcessStatus\n} from '../../../api/process';\nimport {\n    Breadcrumb,\n    Button,\n    Dropdown,\n    Icon,\n    Label,\n    Menu,\n    MenuItem,\n    Sticky\n} from 'semantic-ui-react';\nimport { Link } from 'react-router';\nimport { formatDistanceToNow, isAfter, parseISO as parseDate } from 'date-fns';\nimport { SemanticCOLORS } from 'semantic-ui-react/dist/commonjs/generic';\n\nimport { ProcessLastErrorModal, WithCopyToClipboard } from '../../molecules';\nimport { CancelProcessPopup, DisableProcessPopup } from '../../organisms';\nimport { ConcordId } from '../../../api/common';\n\nimport './styles.css';\nimport { formatDuration } from '../../../utils';\nimport RestartProcessPopup from \"../RestartProcessPopup\";\n\ninterface ExternalProps {\n    stickyRef: any;\n    loading: boolean;\n    instanceId: ConcordId;\n    process?: ProcessEntry;\n    rootInstanceId?: ConcordId;\n    refresh: () => void;\n}\n\nconst ProcessToolbar = memo((props: ExternalProps) => {\n    const { stickyRef, loading, refresh, process, instanceId, rootInstanceId } = props;\n\n    const [isFixed, setFixed] = useState(false);\n\n    const onStick = useCallback(() => {\n        setFixed(false);\n    }, []);\n\n    const onUnstick = useCallback(() => {\n        setFixed(true);\n    }, []);\n\n    return (\n        <Sticky context={stickyRef} onStick={onStick} onUnstick={onUnstick}>\n            <Menu\n                tabular={false}\n                secondary={true}\n                borderless={true}\n                className={isFixed ? 'ProcessMainToolbar' : 'ProcessMainToolbar unfixed'}>\n                <MenuItem>\n                    <Icon name=\"refresh\" loading={loading} size={'large'} onClick={refresh} />\n                </MenuItem>\n\n                <MenuItem>{renderBreadcrumbs(instanceId, process)}</MenuItem>\n\n                <MenuItem>{renderProcessStatus(process)}</MenuItem>\n\n                <MenuItem>{renderStartAt(process)}</MenuItem>\n\n                <MenuItem position={'right'}>{renderProcessMainActions(refresh, process, rootInstanceId)}</MenuItem>\n\n                <MenuItem>{renderProcessSecondaryActions(refresh, process)}</MenuItem>\n            </Menu>\n        </Sticky>\n    );\n});\n\nconst renderBreadcrumbs = (instanceId: ConcordId, process?: ProcessEntry) => {\n    if (!process) {\n        return (\n            <Breadcrumb size=\"big\">\n                <Breadcrumb.Section active={true}>{instanceId}</Breadcrumb.Section>\n            </Breadcrumb>\n        );\n    }\n\n    if (!process.orgName) {\n        return (\n            <Breadcrumb size=\"big\">\n                <Breadcrumb.Section>\n                    <Link to={`/process`}>Processes</Link>\n                </Breadcrumb.Section>\n                <Breadcrumb.Divider />\n                <Breadcrumb.Section active={true}>\n                    <WithCopyToClipboard value={process.instanceId}>\n                        {process.instanceId}\n                    </WithCopyToClipboard>\n                </Breadcrumb.Section>\n            </Breadcrumb>\n        );\n    }\n\n    return (\n        <Breadcrumb size=\"big\">\n            <Breadcrumb.Section>\n                <Link to={`/org/${process.orgName}`}>{process.orgName}</Link>\n            </Breadcrumb.Section>\n            <Breadcrumb.Divider />\n            <Breadcrumb.Section>\n                <Link to={`/org/${process.orgName}/project/${process.projectName}`}>\n                    {process.projectName}\n                </Link>\n            </Breadcrumb.Section>\n            <Breadcrumb.Divider />\n            <Breadcrumb.Section active={true}>\n                <WithCopyToClipboard value={process.instanceId}>\n                    {process.instanceId}\n                </WithCopyToClipboard>\n            </Breadcrumb.Section>\n        </Breadcrumb>\n    );\n};\n\nconst renderProcessStatus = (process?: ProcessEntry) => {\n    if (!process) {\n        return;\n    }\n\n    let duration;\n    if (process.status === ProcessStatus.RUNNING) {\n        duration = formatDuration(new Date().getTime() - parseDate(process.lastRunAt || process.createdAt).getTime());\n    } else if (isFinal(process.status)) {\n        duration = formatDuration(process.totalRuntimeMs);\n    }\n    return (\n        <>\n            <Label color={getStatusColor(process.status)}>\n                {process.status}\n                {duration && <Label.Detail>{duration}</Label.Detail>}\n            </Label>\n            {process.status === ProcessStatus.FAILED && (\n                <>\n                    &nbsp;\n                    <ProcessLastErrorModal processMeta={process.meta} />\n                </>\n            )}\n        </>\n    );\n};\n\nconst renderStartAt = (process?: ProcessEntry) => {\n    if (!process || !process.startAt || process.status !== ProcessStatus.ENQUEUED) {\n        return;\n    }\n\n    let startAt = parseDate(process.startAt);\n    if (isAfter(startAt, Date.now())) {\n        return <span className=\"startAt\">starts in {formatDistanceToNow(startAt)}</span>;\n    }\n\n    return;\n};\n\nconst renderProcessMainActions = (refresh: () => void, process?: ProcessEntry, rootInstanceId?: ConcordId) => {\n    if (!process) {\n        return (\n            <Button.Group>\n                <Button\n                    attached={false}\n                    negative={false}\n                    icon=\"delete\"\n                    content=\"Cancel\"\n                    disabled={true}\n                    size={'small'}\n                />\n            </Button.Group>\n        );\n    }\n\n    const renderRestartProcessTrigger = (onClick: () => void) => {\n        return (\n            <Button\n                attached={false}\n                negative={false}\n                icon=\"sync\"\n                content=\"Restart\"\n                disabled={!canBeRestarted(process.status)}\n                size={'small'}\n                onClick={onClick}\n            />\n        );\n    };\n\n    const renderCancelProcessTrigger = (onClick: () => void) => {\n        return (\n            <Button\n                attached={false}\n                negative={true}\n                icon=\"delete\"\n                content=\"Cancel\"\n                disabled={!canBeCancelled(process.status)}\n                size={'small'}\n                onClick={onClick}\n            />\n        );\n    };\n\n    return (\n        <Button.Group>\n            {!canBeRestarted(process.status) &&\n                <CancelProcessPopup\n                    instanceId={process.instanceId}\n                    refresh={refresh}\n                    trigger={renderCancelProcessTrigger}\n                />\n            }\n            {canBeRestarted(process.status) && process.runtime === 'concord-v2' &&\n                <RestartProcessPopup\n                    instanceId={process.instanceId}\n                    rootInstanceId={rootInstanceId}\n                    refresh={refresh}\n                    trigger={renderRestartProcessTrigger}\n                />\n            }\n        </Button.Group>\n    );\n};\n\nconst renderProcessSecondaryActions = (refresh: () => void, process?: ProcessEntry) => {\n    if (!process) {\n        return (\n            <Dropdown\n                icon=\"ellipsis vertical\"\n                pointing={'top right'}\n                error={false}\n                disabled={true}\n            />\n        );\n    }\n\n    const { instanceId, status, disabled } = process;\n\n    const renderDisableProcessTrigger = (onClick: () => void) => {\n        return (\n            <Dropdown.Item onClick={onClick}>\n                {disableIcon(disabled)}\n                <span className=\"text\">{disabled ? 'Enable' : 'Disable'}</span>\n            </Dropdown.Item>\n        );\n    };\n\n    const extraProcessMenuLinks = window.concord?.extraProcessMenuLinks;\n\n    const getIcon = ({ props }: { props: any }) => <Icon color={props.color} name={props.icon} />;\n\n    return (\n        <Dropdown icon=\"ellipsis vertical\" pointing={'top right'} error={false}>\n            <Dropdown.Menu>\n                {isFinal(status) && (\n                    <DisableProcessPopup\n                        instanceId={instanceId}\n                        disabled={!disabled}\n                        refresh={refresh}\n                        trigger={renderDisableProcessTrigger}\n                    />\n                )}\n\n                {hasState(status) && (\n                    <Dropdown.Item\n                        href={`/api/v1/process/${instanceId}/state/snapshot`}\n                        download={`Concord_${status}_${instanceId}.zip`}>\n                        <Icon name=\"download\" color={'blue'} />\n                        <span className=\"text\">State</span>\n                    </Dropdown.Item>\n                )}\n\n                <Dropdown.Item\n                    href={`/api/v1/process/${process.instanceId}/state/snapshot/.concord/effective.concord.yml`}\n                    download={`${instanceId}.concord.yml`}>\n                    <Icon name=\"download\" color={'green'} />\n                    <span className=\"text\">Effective YAML</span>\n                </Dropdown.Item>\n\n                {extraProcessMenuLinks &&\n                    extraProcessMenuLinks.map((x, idx) => (\n                        <Dropdown.Item\n                            key={idx}\n                            text={x.label}\n                            onClick={() =>\n                                window.open(`${x.url}?arguments.instanceId=${instanceId}`, '_blank')\n                            }>\n                            {getIcon({ props: x })}\n                            <span className=\"text\">{x.label}</span>\n                        </Dropdown.Item>\n                    ))}\n            </Dropdown.Menu>\n        </Dropdown>\n    );\n};\n\nconst disableIcon = (disable: boolean) => {\n    return <Icon name=\"power\" color={disable ? 'green' : 'grey'} />;\n};\n\nconst getStatusColor = (status: string): SemanticCOLORS => {\n    switch (status) {\n        case ProcessStatus.FINISHED:\n            return 'green';\n        case ProcessStatus.FAILED:\n            return 'red';\n        default:\n            return 'grey';\n    }\n};\n\nexport default ProcessToolbar;\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessActivity/favicon.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { useEffect } from 'react';\nimport { ProcessEntry, ProcessStatus } from '../../../api/process';\n\nconst setFavicon = (src: string) => {\n    const head = document.head || document.getElementsByTagName('head')[0];\n\n    const oldLink = document.getElementById('favicon');\n\n    const newLink = document.createElement('link');\n    newLink.id = 'favicon';\n    newLink.rel = 'shortcut icon';\n    newLink.href = src;\n\n    if (oldLink) {\n        head.removeChild(oldLink);\n    }\n\n    document.head.appendChild(newLink);\n};\n\nexport const useStatusFavicon = (process: ProcessEntry | undefined) =>\n    useEffect(() => {\n        if (!process) {\n            return;\n        }\n\n        switch (process.status) {\n            case ProcessStatus.NEW:\n                setFavicon('/favicon-status-new.png');\n                break;\n            case ProcessStatus.PREPARING:\n                setFavicon('/favicon-status-preparing.png');\n                break;\n            case ProcessStatus.ENQUEUED:\n                setFavicon('/favicon-status-enqueued.png');\n                break;\n            case ProcessStatus.WAITING:\n                setFavicon('/favicon-status-waiting.png');\n                break;\n            case ProcessStatus.STARTING:\n                setFavicon('/favicon-status-starting.png');\n                break;\n            case ProcessStatus.RESUMING:\n                setFavicon('/favicon-status-resuming.png');\n                break;\n            case ProcessStatus.SUSPENDED:\n                setFavicon('/favicon-status-suspended.png');\n                break;\n            case ProcessStatus.RUNNING:\n                setFavicon('/favicon-status-running.png');\n                break;\n            case ProcessStatus.FINISHED:\n                setFavicon('/favicon-status-finished.png');\n                break;\n            case ProcessStatus.FAILED:\n                setFavicon('/favicon-status-failed.png');\n                break;\n            case ProcessStatus.CANCELLED:\n                setFavicon('/favicon-status-cancelled.png');\n                break;\n            case ProcessStatus.TIMED_OUT:\n                setFavicon('/favicon-status-timedout.png');\n                break;\n        }\n\n        return () => setFavicon('/favicon.png');\n    }, [process]);\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useRef, useState, useEffect } from 'react';\n\nimport { Link, Navigate, Route, Routes } from 'react-router';\nimport { Icon, Menu } from 'semantic-ui-react';\n\nimport { ConcordId } from '../../../api/common';\nimport { get as apiGet, getRoot as apiGetRoot, isFinal, ProcessEntry } from '../../../api/process';\nimport { NotFoundPage } from '../../pages';\nimport {\n    ProcessAnsibleActivity,\n    ProcessAttachmentsActivity,\n    ProcessChildrenActivity,\n    ProcessEventsActivity,\n    ProcessHistoryActivity,\n    ProcessLogActivity,\n    ProcessLogActivityV2,\n    ProcessStatusActivity,\n    ProcessWaitActivity,\n} from '../index';\nimport ProcessToolbar from './Toolbar';\nimport { usePolling } from '../../../api/usePolling';\nimport RequestErrorActivity from '../RequestErrorActivity';\nimport { useStatusFavicon } from './favicon';\nimport { gitUrlParse } from '../../molecules/GitHubLink';\nimport { useIdleTimer } from 'react-idle-timer';\n\nexport type TabLink =\n    | 'status'\n    | 'ansible'\n    | 'log'\n    | 'events'\n    | 'history'\n    | 'wait'\n    | 'children'\n    | 'attachments'\n    | null;\n\ninterface ExternalProps {\n    instanceId: ConcordId;\n    activeTab: TabLink;\n}\n\nconst DATA_FETCH_INTERVAL_ACTIVE = 5_000;\nconst DATA_FETCH_INTERVAL_IDLE = 60_000;\nconst IDLE_TIMEOUT = 1_000 * 60 * 10;\n\nconst normalizePath = (p: string) => {\n    let result = p;\n    if (result.endsWith('/')) {\n        result = result.substring(0, result.length - 1);\n    }\n    if (!result.startsWith('/')) {\n        result = '/' + result;\n    }\n    return result;\n};\n\nconst buildDefinitionLinkBase = (process?: ProcessEntry) => {\n    if (!process) {\n        return undefined;\n    }\n\n    if (process.runtime !== 'concord-v2') {\n        return undefined;\n    }\n\n    if (process.repoUrl !== undefined) {\n        let link = gitUrlParse(process.repoUrl);\n        if (!link) {\n            return undefined;\n        }\n\n        if (link.endsWith('.git')) {\n            link = link.substr(0, link.length - 4);\n        }\n\n        link += '/blob/' + process.commitId;\n        if (process.repoPath) {\n            link += normalizePath(process.repoPath);\n        }\n\n        return link;\n    } else {\n        return '/api/v1/process/' + process.instanceId + '/state/snapshot';\n    }\n};\n\nconst ProcessActivity = (props: ExternalProps) => {\n    const stickyRef = useRef(null);\n\n    const [loading, setLoading] = useState<boolean>(false);\n    const loadingCounter = useRef<number>(0);\n    const [refresh, toggleRefresh] = useState<boolean>(false);\n    const [dataFetchInterval, setDataFetchInterval] = useState(\n        document.visibilityState === 'visible'\n            ? DATA_FETCH_INTERVAL_ACTIVE\n            : DATA_FETCH_INTERVAL_IDLE\n    );\n\n    const loadingHandler = useCallback((inc: number) => {\n        loadingCounter.current += inc;\n        setLoading(loadingCounter.current > 0);\n    }, []);\n\n    const [process, setProcess] = useState<ProcessEntry>();\n    const rootProcessRef = useRef<ProcessEntry | null>(null);\n\n    const fetchData = useCallback(async () => {\n        const process = await apiGet(props.instanceId, []);\n        setProcess(process);\n\n        if (process.parentInstanceId && !rootProcessRef.current) {\n            try {\n                rootProcessRef.current = await apiGetRoot(process.parentInstanceId);\n            } catch {\n                rootProcessRef.current = process;\n            }\n        }\n\n        return !isFinal(process.status);\n    }, [props.instanceId]);\n\n    useEffect(() => {\n        rootProcessRef.current = null;\n    }, [props.instanceId]);\n\n    useStatusFavicon(process);\n\n    const refreshHandler = useCallback(() => {\n        toggleRefresh((prevState) => !prevState);\n    }, []);\n\n    useIdleTimer({\n        timeout: IDLE_TIMEOUT,\n        immediateEvents: ['visibilitychange'],\n        onActive: (event?: Event) => {\n            // this essentially prevents a data fetch on processes we already know are \"final\"\n            if (process !== undefined && !isFinal(process.status)) {\n                setDataFetchInterval(DATA_FETCH_INTERVAL_ACTIVE);\n            }\n        },\n        onIdle: () => {\n            setDataFetchInterval(DATA_FETCH_INTERVAL_IDLE);\n        },\n    });\n\n    const error = usePolling(fetchData, dataFetchInterval, loadingHandler, refresh);\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n\n    const { instanceId, activeTab } = props;\n\n    const baseUrl = `/process/${instanceId}`;\n\n    return (\n        <div ref={stickyRef}>\n            <ProcessToolbar\n                loading={loading}\n                instanceId={instanceId}\n                process={process}\n                rootInstanceId={rootProcessRef.current?.instanceId}\n                refresh={refreshHandler}\n                stickyRef={stickyRef}\n            />\n\n            <Menu tabular={true} style={{ marginTop: 0 }}>\n                <Menu.Item active={activeTab === 'status'}>\n                    <Icon name=\"hourglass half\" />\n                    <Link to={`${baseUrl}/status`}>Status</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'events'}>\n                    <Icon name=\"content\" />\n                    <Link to={`${baseUrl}/events`}>Events</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'ansible'}>\n                    <Icon name=\"chart area\" />\n                    <Link to={`${baseUrl}/ansible`}>Ansible</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'log'}>\n                    <Icon name=\"book\" />\n                    <Link to={`${baseUrl}/log`}>Logs</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'history'}>\n                    <Icon name=\"history\" />\n                    <Link to={`${baseUrl}/history`}>History</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'wait'}>\n                    <Icon name=\"wait\" />\n                    <Link to={`${baseUrl}/wait`}>Wait Conditions</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'children'}>\n                    <Icon name=\"chain\" />\n                    <Link to={`${baseUrl}/children`}>Child Processes</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'attachments'}>\n                    <Icon name=\"paperclip\" />\n                    <Link to={`${baseUrl}/attachments`}>Attachments</Link>\n                </Menu.Item>\n            </Menu>\n\n            <Routes>\n                <Route index={true} element={<Navigate to=\"status\" replace={true} />} />\n                <Route\n                    path=\"status\"\n                    element={\n                        <ProcessStatusActivity\n                            instanceId={instanceId}\n                            loadingHandler={loadingHandler}\n                            forceRefresh={refresh}\n                            refreshHandler={refreshHandler}\n                            dataFetchInterval={dataFetchInterval}\n                        />\n                    }\n                />\n                <Route\n                    path=\"events\"\n                    element={\n                        <ProcessEventsActivity\n                            instanceId={instanceId}\n                            processStatus={process ? process.status : undefined}\n                            loadingHandler={loadingHandler}\n                            forceRefresh={refresh}\n                            definitionLinkBase={buildDefinitionLinkBase(process)}\n                            dataFetchInterval={dataFetchInterval}\n                        />\n                    }\n                />\n                <Route\n                    path=\"ansible\"\n                    element={\n                        <ProcessAnsibleActivity\n                            instanceId={instanceId}\n                            loadingHandler={loadingHandler}\n                            forceRefresh={refresh}\n                            dataFetchInterval={dataFetchInterval}\n                        />\n                    }\n                />\n                <Route\n                    path=\"log\"\n                    element={\n                        <>\n                            {process && process.runtime === 'concord-v1' && (\n                                <ProcessLogActivity\n                                    instanceId={instanceId}\n                                    processStatus={process ? process.status : undefined}\n                                    loadingHandler={loadingHandler}\n                                    forceRefresh={refresh}\n                                    dataFetchInterval={dataFetchInterval}\n                                />\n                            )}\n                            {process &&\n                                (process.runtime === 'concord-v2' ||\n                                    process.runtime === undefined) && (\n                                    <ProcessLogActivityV2\n                                        instanceId={instanceId}\n                                        processStatus={process ? process.status : undefined}\n                                        loadingHandler={loadingHandler}\n                                        forceRefresh={refresh}\n                                        dataFetchInterval={dataFetchInterval}\n                                    />\n                                )}\n                        </>\n                    }\n                />\n                <Route\n                    path=\"history\"\n                    element={\n                        <ProcessHistoryActivity\n                            instanceId={instanceId}\n                            loadingHandler={loadingHandler}\n                            forceRefresh={refresh}\n                            dataFetchInterval={dataFetchInterval}\n                        />\n                    }\n                />\n                <Route\n                    path=\"wait\"\n                    element={\n                        <ProcessWaitActivity\n                            instanceId={instanceId}\n                            processStatus={process ? process.status : undefined}\n                            loadingHandler={loadingHandler}\n                            forceRefresh={refresh}\n                            dataFetchInterval={dataFetchInterval}\n                        />\n                    }\n                />\n                <Route\n                    path=\"children\"\n                    element={\n                        <ProcessChildrenActivity\n                            instanceId={instanceId}\n                            processStatus={process ? process.status : undefined}\n                            processOrgName={process ? process.orgName : undefined}\n                            processProjectName={process ? process.projectName : undefined}\n                            loadingHandler={loadingHandler}\n                            forceRefresh={refresh}\n                            dataFetchInterval={dataFetchInterval}\n                        />\n                    }\n                />\n                <Route\n                    path=\"attachments\"\n                    element={\n                        <ProcessAttachmentsActivity\n                            instanceId={instanceId}\n                            processStatus={process ? process.status : undefined}\n                            loadingHandler={loadingHandler}\n                            forceRefresh={refresh}\n                            dataFetchInterval={dataFetchInterval}\n                        />\n                    }\n                />\n                <Route path=\"*\" element={<NotFoundPage />} />\n            </Routes>\n        </div>\n    );\n};\n\nexport default ProcessActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessActivity/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n.ProcessMainToolbar {\n    padding-top: 1rem !important;\n    padding-bottom: 1rem !important;\n}\n\n.ProcessMainToolbar.unfixed {\n    background: white !important;\n    border: 0 !important;\n    border-bottom: 1px solid rgba(34,36,38,.15) !important;\n    box-shadow: none !important;\n}\n\n.ProcessMainToolbar .item {\n    padding: 0 !important;\n}\n\n.ProcessMainToolbar .button {\n    font-size: .92857143rem !important;\n    padding: .58928571em 1.125em .58928571em !important;\n}\n\n.ProcessMainToolbar .startAt {\n    color: #888888;\n}"
  },
  {
    "path": "console2/src/components/organisms/ProcessAttachmentsActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport { list as apiList } from '../../../api/process/attachment';\nimport { useState } from 'react';\nimport { isFinal, ProcessStatus } from '../../../api/process';\nimport { ProcessAttachmentsList } from '../../molecules';\nimport RequestErrorActivity from '../RequestErrorActivity';\nimport { useCallback } from 'react';\nimport { usePolling } from '../../../api/usePolling';\nimport { ConcordId } from '../../../api/common';\n\ninterface ExternalProps {\n    instanceId: ConcordId;\n    loadingHandler: (inc: number) => void;\n    processStatus?: ProcessStatus;\n    forceRefresh: boolean;\n    dataFetchInterval: number;\n}\n\nconst ProcessAttachmentsActivity = ({\n    instanceId,\n    processStatus,\n    loadingHandler,\n    forceRefresh,\n    dataFetchInterval\n}: ExternalProps) => {\n    const [data, setData] = useState<string[]>();\n\n    const fetchData = useCallback(async () => {\n        const data = await apiList(instanceId);\n        setData(makeAttachmentsList(data));\n\n        return !isFinal(processStatus);\n    }, [instanceId, processStatus]);\n\n    const error = usePolling(fetchData, dataFetchInterval, loadingHandler, forceRefresh);\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n\n    return <ProcessAttachmentsList instanceId={instanceId} data={data} />;\n};\n\nconst makeAttachmentsList = (data: string[]): string[] => {\n    return data\n        .filter((attachment) => attachment.indexOf('_state') < 0)\n        .filter((attachment) => attachment.indexOf('_session_files') < 0);\n};\n\nexport default ProcessAttachmentsActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessCheckpointActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { useCallback, useState } from 'react';\nimport {\n    CheckpointRestoreHistoryEntry,\n    ProcessCheckpointEntry,\n    ProcessStatus\n} from '../../../api/process';\nimport { ContentBlock } from '../CheckpointView/ProcessList/styles';\nimport { generateCheckpointGroups } from '../CheckpointView/Container/checkpointUtils';\nimport {\n    CheckpointNode,\n    EmptyBox,\n    FlexWrapper,\n    getStatusButtonColor,\n    GroupItems,\n    GroupWrapper\n} from '../CheckpointView/CheckpointGroup/styles';\nimport { CheckpointGroupName } from '../CheckpointView/shared/Labels';\nimport { format as formatDate } from 'date-fns';\nimport { SingleOperationPopup } from '../../molecules';\nimport { isFinalStatus } from '../ProcessRestoreActivity';\nimport { Button, Popup } from 'semantic-ui-react';\nimport { restoreProcess as apiRestore } from '../../../api/process/checkpoint';\nimport { ConcordId, RequestError } from '../../../api/common';\n\ninterface ExternalProps {\n    instanceId: ConcordId;\n    processStatus: ProcessStatus;\n    processDisabled: boolean;\n    checkpoints: ProcessCheckpointEntry[];\n    checkpointRestoreHistory?: CheckpointRestoreHistoryEntry[];\n    onRestoreComplete: () => void;\n}\n\ninterface Props {\n    instanceId: ConcordId;\n    processStatus: ProcessStatus;\n    processDisabled: boolean;\n    checkpointId: string;\n    checkpointStatus: ProcessStatus;\n    checkpointName: string;\n    checkpointStartTime: Date;\n    onRestoreComplete: () => void;\n}\n\nconst Checkpoint = (props: Props) => {\n    const [restoring, setRestoring] = useState(false);\n    const [success, setSuccess] = useState(false);\n    const [restoringError, setRestoringError] = useState<RequestError>();\n\n    const { onRestoreComplete, instanceId, checkpointId } = props;\n\n    const restoreProcess = useCallback(async () => {\n        setRestoring(true);\n\n        try {\n            await apiRestore(instanceId, checkpointId);\n            setRestoringError(undefined);\n            setSuccess(true);\n\n            onRestoreComplete();\n        } catch (e) {\n            setRestoringError(e);\n        } finally {\n            setRestoring(false);\n        }\n    }, [onRestoreComplete, instanceId, checkpointId]);\n\n    const reset = useCallback(() => {\n        setRestoring(false);\n        setSuccess(false);\n        setRestoringError(undefined);\n    }, []);\n\n    const {\n        processStatus,\n        checkpointName,\n        checkpointStartTime,\n        checkpointStatus,\n        processDisabled\n    } = props;\n\n    const popupContent =\n        checkpointStartTime.toDateString() + ' ' + formatDate(checkpointStartTime, 'HH:mm:ss');\n    const additionalPopupContent =\n        processStatus === ProcessStatus.SUSPENDED\n            ? 'Cancel the process before restoring a checkpoint'\n            : undefined;\n\n    return (\n        <SingleOperationPopup\n            trigger={(onClick: any) => (\n                <Popup\n                    hoverable={true}\n                    position={'bottom left'}\n                    wide={true}\n                    header={additionalPopupContent === undefined ? undefined : popupContent}\n                    content={\n                        additionalPopupContent === undefined ? popupContent : additionalPopupContent\n                    }\n                    trigger={\n                        <div>\n                            <Button\n                                onClick={onClick}\n                                disabled={!isFinalStatus(processStatus) || processDisabled}\n                                color={getStatusButtonColor(checkpointStatus)}\n                                style={{\n                                    margin: '0',\n                                    padding: '0.7rem 0.7rem',\n                                    fontSize: '1rem',\n                                    fontWeight: '900'\n                                }}>\n                                {checkpointName}\n                            </Button>\n                        </div>\n                    }\n                />\n            )}\n            title=\"Restore the process?\"\n            introMsg={\n                <p>\n                    Are you sure you want to restore process at '<b>{checkpointName}</b>'\n                    checkpoint?\n                </p>\n            }\n            running={restoring}\n            success={success}\n            successMsg={<p>Process restored successfully.</p>}\n            error={restoringError}\n            reset={reset}\n            onConfirm={restoreProcess}\n        />\n    );\n};\n\nconst ProcessCheckpointActivity = (props: ExternalProps) => {\n    const checkpointGroups = generateCheckpointGroups(\n        props.processStatus,\n        props.checkpoints,\n        props.checkpointRestoreHistory\n    );\n\n    return (\n        <ContentBlock style={{ margin: '16px 0' }}>\n            <FlexWrapper>\n                {checkpointGroups.map(({ name, checkpoints }, indexA) => (\n                    <GroupWrapper key={indexA}>\n                        <CheckpointGroupName>Run {name}</CheckpointGroupName>\n                        <GroupItems>\n                            {checkpoints.length === 0 && (\n                                <div>\n                                    <EmptyBox>No checkpoints</EmptyBox>\n                                </div>\n                            )}\n                            {checkpoints.length > 0 &&\n                                checkpoints.map((checkpoint, indexB) => (\n                                    <CheckpointNode key={indexB}>\n                                        <Checkpoint\n                                            instanceId={props.instanceId}\n                                            processStatus={props.processStatus}\n                                            processDisabled={props.processDisabled}\n                                            checkpointId={checkpoint.id}\n                                            checkpointName={checkpoint.name}\n                                            checkpointStatus={checkpoint.status}\n                                            checkpointStartTime={checkpoint.startTime}\n                                            onRestoreComplete={props.onRestoreComplete}\n                                        />\n                                    </CheckpointNode>\n                                ))}\n                        </GroupItems>\n                    </GroupWrapper>\n                ))}\n            </FlexWrapper>\n        </ContentBlock>\n    );\n};\n\nexport default ProcessCheckpointActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessChildrenActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { ConcordId, ConcordKey, queryParams } from '../../../api/common';\nimport {\n    list as apiProcessList,\n    isFinal,\n    ProcessListQuery,\n    PaginatedProcessEntries,\n    ProcessStatus,\n} from '../../../api/process';\nimport ProcessListWithSearch from '../../molecules/ProcessListWithSearch';\nimport {\n    CREATED_AT_COLUMN,\n    INITIATOR_COLUMN,\n    INSTANCE_ID_COLUMN,\n    REPO_COLUMN,\n    STATUS_COLUMN,\n} from '../../molecules/ProcessList';\nimport { useLocation } from 'react-router';\nimport { useHistory } from '@/router';\nimport {\n    addBuiltInColumns,\n    filtersToQuery,\n    parseSearchFilter,\n    ProcessSearchFilter,\n} from '../ProcessListActivity';\nimport { ProcessFilters } from '../../../api/process';\nimport RequestErrorActivity from '../RequestErrorActivity';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { usePolling } from '../../../api/usePolling';\nimport { get as apiGetProject, ProjectEntry } from '../../../api/org/project';\nimport { useApi } from '../../../hooks/useApi';\nimport { LoadingDispatch } from '../../../App';\nimport { Pagination } from '../../molecules/PaginationToolBar/usePagination';\n\nconst COLUMNS = [\n    STATUS_COLUMN,\n    INSTANCE_ID_COLUMN,\n    REPO_COLUMN,\n    INITIATOR_COLUMN,\n    CREATED_AT_COLUMN,\n];\n\ninterface ExternalProps {\n    instanceId: ConcordId;\n    loadingHandler: (inc: number) => void;\n    processStatus?: ProcessStatus;\n    processOrgName?: ConcordKey;\n    processProjectName?: ConcordKey;\n    forceRefresh: boolean;\n    dataFetchInterval: number;\n}\n\nconst ProcessChildrenActivity = ({\n    instanceId,\n    loadingHandler,\n    processStatus,\n    forceRefresh,\n    dataFetchInterval,\n    processOrgName,\n    processProjectName,\n}: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const isInitialMount = useRef(true);\n    const location = useLocation();\n    const history = useHistory();\n    const [data, setData] = useState<PaginatedProcessEntries>();\n    const [searchFilter, setSearchFilter] = useState<ProcessSearchFilter>(\n        parseSearchFilter(location.search)\n    );\n\n    const fetchProjectData = useCallback(() => {\n        if (!processOrgName || !processProjectName) {\n            return Promise.resolve(undefined);\n        }\n\n        return apiGetProject(processOrgName, processProjectName);\n    }, [processOrgName, processProjectName]);\n\n    const { data: project, error: projectError } = useApi<ProjectEntry | undefined>(\n        fetchProjectData,\n        {\n            fetchOnMount: true,\n            forceRequest: forceRefresh,\n            dispatch: dispatch,\n        }\n    );\n\n    useEffect(() => {\n        if (isInitialMount.current) {\n            isInitialMount.current = false;\n        } else {\n            setSearchFilter(parseSearchFilter(location.search));\n        }\n    }, [location]);\n\n    const fetchData = useCallback(async () => {\n        const query = {\n            parentInstanceId: instanceId,\n            ...searchFilter.pagination,\n        } as ProcessListQuery;\n\n        const processEntries = await apiProcessList(filtersToQuery(query, searchFilter.filters));\n        setData(processEntries);\n\n        return !isFinal(processStatus);\n    }, [instanceId, searchFilter, processStatus]);\n\n    const error = usePolling(fetchData, dataFetchInterval, loadingHandler, forceRefresh);\n\n    const onRefresh = useCallback(\n        (processFilters?: ProcessFilters, paginationFilters?: Pagination) => {\n            if (processFilters || paginationFilters) {\n                const f = {};\n                if (processFilters !== undefined) {\n                    Object.keys(processFilters)\n                        .filter((k) => k !== undefined)\n                        .forEach((key) => (f[key] = processFilters[key]));\n                }\n                if (paginationFilters !== undefined) {\n                    Object.keys(paginationFilters)\n                        .filter((k) => k !== undefined)\n                        .forEach((key) => (f[key] = paginationFilters[key]));\n                }\n                // will update location\n                history.push({ search: queryParams(f) });\n            }\n        },\n        [history]\n    );\n\n    const columns = addBuiltInColumns(project?.meta?.ui?.childrenProcessList) ?? COLUMNS;\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n            {projectError && <RequestErrorActivity error={projectError} />}\n\n            <ProcessListWithSearch\n                processFilters={searchFilter.filters}\n                paginationFilter={searchFilter.pagination}\n                processes={data?.items}\n                columns={columns}\n                loading={false}\n                refresh={onRefresh}\n                next={data?.next}\n                prev={data?.prev}\n                usePagination={true}\n            />\n        </>\n    );\n};\n\nexport default ProcessChildrenActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessEventsActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport { isFinal, ProcessStatus } from '../../../api/process';\nimport {\n    listEvents as apiListEvents,\n    ProcessElementEvent,\n    ProcessEventEntry\n} from '../../../api/process/event';\nimport { ProcessElementList } from '../../molecules';\nimport { useCallback, useRef } from 'react';\nimport { useState } from 'react';\nimport { usePolling } from '../../../api/usePolling';\nimport { ConcordId } from '../../../api/common';\nimport RequestErrorActivity from '../RequestErrorActivity';\n\ninterface ExternalProps {\n    instanceId: ConcordId;\n    loadingHandler: (inc: number) => void;\n    forceRefresh: boolean;\n    processStatus?: ProcessStatus;\n    definitionLinkBase?: string;\n    dataFetchInterval: number;\n}\n\ntype PrePostEventData = {\n    phase?: 'pre' | 'post';\n    correlationId?: string;\n};\n\nconst ProcessEventsActivity = (props: ExternalProps) => {\n    const { instanceId, processStatus, loadingHandler, forceRefresh, definitionLinkBase, dataFetchInterval } = props;\n\n    const lastEventId = useRef<number>();\n\n    const [events, setEvents] = useState<ProcessEventEntry<ProcessElementEvent>[]>();\n\n    const fetchData = useCallback(async () => {\n        const events = await apiListEvents<ProcessElementEvent>({\n            instanceId: instanceId,\n            type: 'ELEMENT',\n            fromId: lastEventId.current,\n            limit: 100\n        });\n\n        if (events.length > 0) {\n            lastEventId.current = Math.max.apply(\n                Math,\n                events.map((e) => e.seqId)\n            );\n        }\n\n        setEvents((prevEvents) => reduceEvents(prevEvents ? prevEvents : [], events));\n\n        return !(events.length === 0 && isFinal(processStatus));\n    }, [instanceId, processStatus]);\n\n    const error = usePolling(fetchData, dataFetchInterval, loadingHandler, forceRefresh);\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n\n    return (\n        <ProcessElementList\n            instanceId={instanceId}\n            events={events}\n            processStatus={processStatus}\n            definitionLinkBase={definitionLinkBase}\n        />\n    );\n};\n\nconst reduceEvents = (\n    prevEvents: ProcessEventEntry<ProcessElementEvent>[],\n    events: ProcessEventEntry<ProcessElementEvent>[]\n): ProcessEventEntry<ProcessElementEvent>[] => {\n    function hasEvent(id: ConcordId, events: ProcessEventEntry<ProcessElementEvent>[]): boolean {\n        return events.find((value) => value.id === id) !== undefined;\n    }\n\n    const newEvents = events.filter((value) => !hasEvent(value.id, prevEvents));\n    if (newEvents.length === 0) {\n        return prevEvents;\n    }\n\n    return combinePrePostEvents(prevEvents.concat(sortEvents(newEvents)));\n};\n\nconst sortEvents = (\n    events: ProcessEventEntry<ProcessElementEvent>[]\n): Array<ProcessEventEntry<ProcessElementEvent>> => {\n    return events.sort((a, b) =>\n        a.eventDate > b.eventDate ? 1 : a.eventDate < b.eventDate ? -1 : 0\n    );\n};\n\nexport const combinePrePostEvents = <T extends PrePostEventData>(\n    events: Array<ProcessEventEntry<T>>\n): Array<ProcessEventEntry<T>> => {\n    function findEvent(\n        phase: string,\n        correlationId: string\n    ): ProcessEventEntry<T> | undefined {\n        return events.find(\n            (value) => value.data.phase === phase && value.data.correlationId === correlationId\n        );\n    }\n\n    const processed: Record<string, boolean> = {};\n    const result = new Array<ProcessEventEntry<T>>();\n    events.forEach((event) => {\n        const data = event.data;\n        if (!data.correlationId) {\n            result.push(event);\n            return;\n        }\n\n        if (processed[data.correlationId]) {\n            return;\n        }\n\n        processed[data.correlationId] = true;\n\n        if (data.phase === 'pre' && data.correlationId) {\n            const postEvent = findEvent('post', data.correlationId);\n            if (postEvent) {\n                const clone = { ...event };\n                clone.data = postEvent.data;\n                result.push(clone);\n            } else {\n                result.push(event);\n            }\n        } else if (data.phase === 'post' && data.correlationId) {\n            const preEvent = findEvent('pre', data.correlationId);\n            if (preEvent) {\n                const clone = { ...preEvent };\n                clone.data = event.data;\n                result.push(clone);\n            } else {\n                result.push(event);\n            }\n        } else {\n            result.push(event);\n        }\n    });\n\n    return result;\n};\n\nexport default ProcessEventsActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessFormActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useHistory } from '@/router';\nimport { Loader, Segment } from 'semantic-ui-react';\n\nimport { ConcordId, RequestError } from '../../../api/common';\nimport {\n    FormDataType,\n    FormInstanceEntry,\n    FormSubmitErrors,\n    get as apiGetForm,\n    submit as apiSubmitForm,\n} from '../../../api/process/form';\nimport { ProcessForm, RequestErrorMessage } from '../../molecules';\nimport { getProcessLocation, getWizardLocation, openProcessForm } from './processFormNavigation';\n\ninterface Props {\n    processInstanceId: ConcordId;\n    formName: string;\n    wizard: boolean;\n}\n\nconst ProcessFormActivity = ({ processInstanceId, formName, wizard }: Props) => {\n    const history = useHistory();\n    const redirectTimeoutRef = useRef<number | undefined>(undefined);\n    const submitRequestIdRef = useRef(0);\n\n    const [form, setForm] = useState<FormInstanceEntry>();\n    const [loading, setLoading] = useState(false);\n    const [loadError, setLoadError] = useState<RequestError>();\n    const [submitting, setSubmitting] = useState(false);\n    const [submitError, setSubmitError] = useState<RequestError>();\n    const [validationErrors, setValidationErrors] = useState<FormSubmitErrors>();\n    const [completed, setCompleted] = useState(false);\n\n    const clearSubmitState = useCallback(() => {\n        setSubmitting(false);\n        setSubmitError(undefined);\n        setValidationErrors(undefined);\n        setCompleted(false);\n    }, []);\n\n    const clearRedirectTimeout = useCallback(() => {\n        if (redirectTimeoutRef.current !== undefined) {\n            window.clearTimeout(redirectTimeoutRef.current);\n            redirectTimeoutRef.current = undefined;\n        }\n    }, []);\n\n    useEffect(() => {\n        let cancelled = false;\n\n        submitRequestIdRef.current += 1;\n        clearRedirectTimeout();\n        clearSubmitState();\n\n        const loadForm = async () => {\n            setLoading(true);\n            setLoadError(undefined);\n            setForm(undefined);\n\n            try {\n                const response = await apiGetForm(processInstanceId, formName);\n                if (!cancelled) {\n                    setForm(response);\n                }\n            } catch (e) {\n                if (!cancelled) {\n                    setLoadError(e);\n                }\n            } finally {\n                if (!cancelled) {\n                    setLoading(false);\n                }\n            }\n        };\n\n        loadForm();\n\n        return () => {\n            cancelled = true;\n            submitRequestIdRef.current += 1;\n            clearRedirectTimeout();\n        };\n    }, [clearRedirectTimeout, clearSubmitState, formName, processInstanceId]);\n\n    useEffect(() => () => clearRedirectTimeout(), [clearRedirectTimeout]);\n\n    const onSubmit = useCallback(\n        async (data: FormDataType) => {\n            const requestId = submitRequestIdRef.current + 1;\n            submitRequestIdRef.current = requestId;\n\n            clearRedirectTimeout();\n            setSubmitting(true);\n            setSubmitError(undefined);\n            setValidationErrors(undefined);\n            setCompleted(false);\n\n            try {\n                const response = await apiSubmitForm(processInstanceId, formName, data);\n\n                if (submitRequestIdRef.current !== requestId) {\n                    return;\n                }\n\n                setValidationErrors(response.errors);\n                setCompleted(response.ok);\n\n                if (!response.ok || !wizard || !form) {\n                    return;\n                }\n\n                if (form.yield) {\n                    redirectTimeoutRef.current = window.setTimeout(() => {\n                        if (submitRequestIdRef.current === requestId) {\n                            history.push(getProcessLocation(processInstanceId));\n                        }\n                    }, 1000);\n                    return;\n                }\n\n                history.push(getWizardLocation(processInstanceId));\n            } catch (e) {\n                if (submitRequestIdRef.current === requestId) {\n                    setSubmitError(e);\n                }\n            } finally {\n                if (submitRequestIdRef.current === requestId) {\n                    setSubmitting(false);\n                }\n            }\n        },\n        [clearRedirectTimeout, form, formName, history, processInstanceId, wizard]\n    );\n\n    const onStartCustomForm = useCallback(\n        async (event: React.MouseEvent<HTMLAnchorElement>) => {\n            event.preventDefault();\n\n            if (!form) {\n                return;\n            }\n\n            const requestId = submitRequestIdRef.current + 1;\n            submitRequestIdRef.current = requestId;\n\n            clearRedirectTimeout();\n            setSubmitError(undefined);\n\n            try {\n                await openProcessForm({\n                    history,\n                    processInstanceId,\n                    formName,\n                    custom: form.custom,\n                    yieldFlow: form.yield,\n                });\n            } catch (e) {\n                if (submitRequestIdRef.current === requestId) {\n                    setSubmitError(e);\n                }\n            }\n        },\n        [clearRedirectTimeout, form, formName, history, processInstanceId]\n    );\n\n    const onReturn = useCallback(() => {\n        history.push(getProcessLocation(processInstanceId));\n    }, [history, processInstanceId]);\n\n    if (loading) {\n        return <Loader active={true} />;\n    }\n\n    if (loadError) {\n        return <RequestErrorMessage error={loadError} />;\n    }\n\n    if (!form || !form.fields) {\n        return <h3>Form not found.</h3>;\n    }\n\n    return (\n        <>\n            {form.custom && (\n                <Segment>\n                    This form has a {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}\n                    <a href=\"#\" onClick={onStartCustomForm}>\n                        custom view\n                    </a>\n                    .\n                </Segment>\n            )}\n\n            <ProcessForm\n                form={form}\n                submitting={submitting}\n                submitError={submitError}\n                completed={completed}\n                errors={validationErrors}\n                onSubmit={onSubmit}\n                onReturn={onReturn}\n            />\n        </>\n    );\n};\n\nexport default ProcessFormActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessFormActivity/processFormNavigation.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { ConcordId } from '../../../api/common';\nimport { startSession as apiStartSession } from '../../../api/service/custom_form';\n\nexport interface RouterLocation {\n    pathname: string;\n    search?: string;\n}\n\ninterface NavigationHistory {\n    push(location: RouterLocation): void;\n    replace(location: RouterLocation): void;\n}\n\ninterface OpenProcessFormArgs {\n    history: NavigationHistory;\n    processInstanceId: ConcordId;\n    formName: string;\n    custom: boolean;\n    yieldFlow: boolean;\n}\n\nconst CUSTOM_FORM_SESSION_RETRIES = 5;\nconst CUSTOM_FORM_SESSION_RETRY_DELAY_MS = 300;\n\nconst updateForDev = (uri: string) => {\n    if (process.env.NODE_ENV !== 'production') {\n        return `http://localhost:8001${uri}`;\n    }\n\n    return uri;\n};\n\nconst delay = (ms: number) =>\n    new Promise<void>((resolve) => {\n        window.setTimeout(resolve, ms);\n    });\n\nconst shouldRetryCustomFormSession = (error: unknown) =>\n    typeof error === 'object' &&\n    error !== null &&\n    'status' in error &&\n    (error as { status?: number }).status === 404;\n\nconst startCustomFormSession = async (processInstanceId: ConcordId, formName: string) => {\n    for (let attempt = 0; attempt < CUSTOM_FORM_SESSION_RETRIES; attempt++) {\n        try {\n            return await apiStartSession(processInstanceId, formName);\n        } catch (error) {\n            if (\n                !shouldRetryCustomFormSession(error) ||\n                attempt === CUSTOM_FORM_SESSION_RETRIES - 1\n            ) {\n                throw error;\n            }\n\n            await delay(CUSTOM_FORM_SESSION_RETRY_DELAY_MS);\n        }\n    }\n\n    throw new Error('Unreachable');\n};\n\nexport const getProcessLocation = (processInstanceId: ConcordId): RouterLocation => ({\n    pathname: `/process/${processInstanceId}`,\n});\n\nexport const getWizardLocation = (processInstanceId: ConcordId): RouterLocation => ({\n    pathname: `/process/${processInstanceId}/wizard`,\n    search: 'fullScreen=true',\n});\n\nexport const getWizardFormLocation = (\n    processInstanceId: ConcordId,\n    formName: string,\n    yieldFlow: boolean\n): RouterLocation => ({\n    pathname: `/process/${processInstanceId}/form/${formName}/wizard`,\n    search: `fullScreen=true&yieldFlow=${yieldFlow}`,\n});\n\nexport const openProcessForm = async ({\n    history,\n    processInstanceId,\n    formName,\n    custom,\n    yieldFlow,\n}: OpenProcessFormArgs) => {\n    if (custom) {\n        const { uri } = await startCustomFormSession(processInstanceId, formName);\n        window.location.replace(updateForDev(uri));\n        return;\n    }\n\n    history.replace(getWizardFormLocation(processInstanceId, formName, yieldFlow));\n};\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessHistoryActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useState } from 'react';\nimport { ProcessHistoryEntry, ProcessEntry, get as apiGet, isFinal } from '../../../api/process';\nimport { ProcessHistoryList } from '../../molecules';\nimport { useCallback } from 'react';\nimport { usePolling } from '../../../api/usePolling';\nimport RequestErrorActivity from '../RequestErrorActivity';\nimport { ConcordId } from '../../../api/common';\n\ninterface ExternalProps {\n    instanceId: ConcordId;\n    loadingHandler: (inc: number) => void;\n    forceRefresh: boolean;\n    dataFetchInterval: number;\n}\n\nconst ProcessHistoryActivity = ({ instanceId, loadingHandler, forceRefresh, dataFetchInterval }: ExternalProps) => {\n    const [data, setData] = useState<ProcessHistoryEntry[]>();\n\n    const fetchData = useCallback(async () => {\n        const process = await apiGet(instanceId, ['history']);\n\n        setData(makeProcessHistoryList(process));\n\n        return !isFinal(process.status);\n    }, [instanceId]);\n\n    const error = usePolling(fetchData, dataFetchInterval, loadingHandler, forceRefresh);\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n\n    return <ProcessHistoryList data={data} />;\n};\n\nconst makeProcessHistoryList = (process: ProcessEntry): ProcessHistoryEntry[] => {\n    if (process.statusHistory === undefined) {\n        return [];\n    }\n\n    return process.statusHistory.sort((a, b) =>\n        a.changeDate < b.changeDate ? 1 : a.changeDate > b.changeDate ? -1 : 0\n    );\n};\n\nexport default ProcessHistoryActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessListActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { parse as parseQueryString } from 'query-string';\nimport * as React from 'react';\nimport { useLocation } from 'react-router';\nimport { useHistory } from '@/router';\n\nimport { queryParams } from '../../../api/common';\nimport {\n    list as apiProcessList,\n    PaginatedProcessEntries,\n    ProcessFilters,\n    ProcessListQuery,\n} from '../../../api/process';\nimport { Pagination } from '../../molecules/PaginationToolBar/usePagination';\nimport {\n    CREATED_AT_COLUMN,\n    DURATION_COLUMN,\n    ENTRY_POINT_COLUMN,\n    INITIATOR_COLUMN,\n    INSTANCE_ID_COLUMN,\n    PROJECT_COLUMN,\n    REPO_COLUMN,\n    Status,\n    STATUS_COLUMN,\n    UPDATED_AT_COLUMN,\n} from '../../molecules/ProcessList';\nimport { ColumnDefinition } from '../../../api/org';\nimport ProcessListWithSearch from '../../molecules/ProcessListWithSearch';\nimport RequestErrorActivity from '../RequestErrorActivity';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useApi } from '../../../hooks/useApi';\nimport { LoadingDispatch } from '../../../App';\nimport _ from 'lodash';\n\n// list of \"built-in\" columns, i.e. columns that can be referenced using \"builtin\" parameter\n// of the custom column configuration\nconst builtInColumns = [\n    STATUS_COLUMN,\n    INSTANCE_ID_COLUMN,\n    DURATION_COLUMN,\n    PROJECT_COLUMN,\n    INITIATOR_COLUMN,\n    CREATED_AT_COLUMN,\n    UPDATED_AT_COLUMN,\n    ENTRY_POINT_COLUMN,\n];\n\n// list of columns visible by default\nconst defaultColumns = [\n    STATUS_COLUMN,\n    INSTANCE_ID_COLUMN,\n    DURATION_COLUMN,\n    PROJECT_COLUMN,\n    INITIATOR_COLUMN,\n    CREATED_AT_COLUMN,\n];\n\n// list of columns visible by default for views without the project column\nconst withoutProjectColumns = [\n    STATUS_COLUMN,\n    INSTANCE_ID_COLUMN,\n    REPO_COLUMN,\n    INITIATOR_COLUMN,\n    CREATED_AT_COLUMN,\n];\n\nexport interface ProcessSearchFilter {\n    filters?: ProcessFilters;\n    pagination?: Pagination;\n}\n\ninterface ExternalProps {\n    orgName?: string;\n    projectName?: string;\n    showInitiatorFilter?: boolean;\n    usePagination?: boolean;\n    columns?: ColumnDefinition[];\n    forceRefresh?: any;\n}\n\nexport const parseSearchFilter = (s: string): ProcessSearchFilter => {\n    const v: any = parseQueryString(s);\n\n    const filters: ProcessFilters = {};\n    Object.keys(v)\n        .filter((k) => k !== 'limit')\n        .filter((k) => k !== 'offset')\n        .filter((k) => v[k] !== undefined)\n        .filter((k) => typeof v[k] === 'string')\n        .forEach((key) => (filters[key] = v[key]));\n\n    return {\n        pagination: { limit: Number(v.limit) || undefined, offset: Number(v.offset) || undefined },\n        filters,\n    };\n};\n\nexport const addBuiltInColumns = (columns?: ColumnDefinition[]): ColumnDefinition[] | undefined => {\n    if (!columns) {\n        return;\n    }\n\n    return columns.map((c) => {\n        if (c.builtin) {\n            const b = builtInColumns.find(\n                (x) => x.source === c.builtin || x.source === 'meta.' + c.builtin\n            );\n            if (!b) {\n                return {\n                    caption: `Built-in column not found: ${c.builtin}`,\n                    source: 'n/a',\n                };\n            }\n            return b;\n        }\n        return c;\n    });\n};\n\nconst ProcessListActivity = ({\n    showInitiatorFilter = false,\n    usePagination = false,\n    columns,\n    orgName,\n    projectName,\n    forceRefresh,\n}: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const isInitialMount = useRef(true);\n    const location = useLocation();\n    const history = useHistory();\n    const [searchFilter, setSearchFilter] = useState<ProcessSearchFilter>(\n        parseSearchFilter(location.search)\n    );\n\n    useEffect(() => {\n        if (isInitialMount.current) {\n            isInitialMount.current = false;\n        } else {\n            const filter = parseSearchFilter(location.search);\n            setSearchFilter((prev) => (_.isEqual(filter, prev) ? prev : filter));\n        }\n    }, [location]);\n\n    const fetchData = useCallback(() => {\n        const query = {\n            orgName,\n            projectName,\n            ...searchFilter.pagination,\n        } as ProcessListQuery;\n\n        return apiProcessList(filtersToQuery(query, searchFilter.filters));\n    }, [orgName, projectName, searchFilter]);\n\n    const { error, isLoading, data } = useApi<PaginatedProcessEntries>(fetchData, {\n        fetchOnMount: true,\n        dispatch: dispatch,\n        forceRequest: forceRefresh,\n    });\n\n    const onRefresh = useCallback(\n        (processFilters?: ProcessFilters, paginationFilters?: Pagination) => {\n            const f = {};\n            if (processFilters || paginationFilters) {\n                if (processFilters !== undefined) {\n                    Object.keys(processFilters)\n                        .filter((k) => k !== undefined)\n                        .forEach((key) => (f[key] = processFilters[key]));\n                }\n                if (paginationFilters !== undefined) {\n                    Object.keys(paginationFilters)\n                        .filter((k) => k !== undefined)\n                        .forEach((key) => (f[key] = paginationFilters[key]));\n                }\n            }\n            setSearchFilter(parseSearchFilter(queryParams(f)));\n            // will update location\n            history.push({ search: queryParams(f) });\n        },\n        [history]\n    );\n\n    const showProjectColumn = !projectName;\n    const cols =\n        addBuiltInColumns(columns) || (showProjectColumn ? defaultColumns : withoutProjectColumns);\n    const f = parseSearchFilter(location.search);\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n\n            <ProcessListWithSearch\n                paginationFilter={f.pagination}\n                processFilters={f.filters}\n                processes={data?.items}\n                next={data?.next}\n                prev={data?.prev}\n                columns={cols}\n                loading={isLoading}\n                refresh={onRefresh}\n                showInitiatorFilter={showInitiatorFilter}\n                usePagination={usePagination}\n            />\n        </>\n    );\n};\n\nexport const filtersToQuery = (\n    query: ProcessListQuery,\n    filters?: ProcessFilters\n): ProcessListQuery => {\n    if (!filters) {\n        return query;\n    }\n\n    Object.keys(filters).forEach((key) => {\n        if (key === STATUS_COLUMN.source && filters[key] === 'SCHEDULED') {\n            query[key] = Status.ENQUEUED;\n            query.startAt = { compareType: 'ge', value: null };\n        } else if (key === STATUS_COLUMN.source && filters[key] === 'ENQUEUED') {\n            query[key] = Status.ENQUEUED;\n            query.startAt = { compareType: 'len', value: null };\n        } else {\n            query[key] = filters[key];\n        }\n    });\n\n    return query;\n};\n\nexport default ProcessListActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessLogActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { ConcordId, RequestError } from '../../../api/common';\nimport { isFinal, ProcessStatus } from '../../../api/process';\nimport { LogProcessorOptions, process } from '../../../state/data/processes/logs/processors';\nimport { LogSegment, LogSegmentType } from '../../../state/data/processes/logs/types';\nimport { ProcessLogViewer } from '../../molecules';\n\nimport './styles.css';\nimport {\n    default as React,\n    Dispatch,\n    MutableRefObject,\n    SetStateAction,\n    useCallback,\n    useEffect,\n    useRef,\n    useState,\n} from 'react';\nimport RequestErrorActivity from '../RequestErrorActivity';\nimport { getLog as apiGetLog, LogRange } from '../../../api/process/log';\nimport { useLocation } from 'react-router';\n\ninterface ExternalProps {\n    instanceId: ConcordId;\n    processStatus?: ProcessStatus;\n    loadingHandler: (inc: number) => void;\n    forceRefresh: boolean;\n    dataFetchInterval: number;\n}\n\ninterface FetchResponse {\n    data: LogSegment[];\n    range: LogRange;\n}\n\nconst DEFAULT_RANGE: LogRange = { low: undefined, high: 2048 };\n\nconst ProcessLogActivity = ({\n    instanceId,\n    processStatus,\n    loadingHandler,\n    forceRefresh,\n    dataFetchInterval,\n}: ExternalProps) => {\n    const location = useLocation();\n    const didMountRef = useRef(false);\n    const range = useRef<LogRange>(DEFAULT_RANGE);\n    const [data, setData] = useState<LogSegment[]>([]);\n    const [opts, setOpts] = useState<LogProcessorOptions>(getStoredOpts());\n    const [refresh, setRefresh] = useState<boolean>(false);\n    const [stopPolling, setStopPolling] = useState<boolean>(isFinal(processStatus));\n    const [wholeLogLoading, setWholeLogLoading] = useState<boolean>(location.hash !== '');\n    const [selectedCorrelationId, setSelectedCorrelationId] = useState<string>(\n        location.hash.substring(1)\n    );\n\n    useEffect(() => {\n        if (didMountRef.current) {\n            const hasHash = location.hash !== '';\n            setSelectedCorrelationId(location.hash.substring(1));\n\n            setData([]);\n            setWholeLogLoading(hasHash);\n            if (hasHash) {\n                range.current = { low: 0 };\n            } else {\n                range.current = DEFAULT_RANGE;\n            }\n            setRefresh((prevState) => !prevState);\n        }\n    }, [location]);\n\n    const optsHandler = useCallback((o: LogProcessorOptions) => {\n        setData([]);\n        setOpts(o);\n        storeOpts(o);\n        setRefresh((prevState) => !prevState);\n    }, []);\n\n    const loadWholeLog = useCallback(() => {\n        setData([]);\n        setWholeLogLoading(true);\n        setRefresh((prevState) => !prevState);\n    }, []);\n\n    useEffect(() => {\n        if (wholeLogLoading) {\n            range.current = { low: 0 };\n        } else {\n            range.current = DEFAULT_RANGE;\n        }\n    }, [wholeLogLoading, opts]);\n\n    useEffect(() => {\n        setStopPolling(isFinal(processStatus));\n    }, [processStatus]);\n\n    useEffect(() => {\n        if (!didMountRef.current) {\n            didMountRef.current = true;\n            return;\n        }\n\n        setRefresh((prevState) => !prevState);\n    }, [forceRefresh]);\n\n    const fetchData = useCallback(\n        async (range: LogRange) => {\n            const chunk = await apiGetLog(instanceId, range);\n\n            const data = chunk && chunk.data.length > 0 ? chunk.data : undefined;\n            const segment: LogSegment | undefined = data\n                ? { data, type: LogSegmentType.DATA }\n                : undefined;\n            const processedData = segment ? process(segment, opts ? opts : {}) : [];\n\n            return {\n                data: processedData,\n                range: chunk.range,\n            };\n        },\n        [instanceId, opts]\n    );\n\n    const error = usePolling(\n        fetchData,\n        range,\n        setData,\n        loadingHandler,\n        refresh,\n        stopPolling,\n        dataFetchInterval\n    );\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n\n    return (\n        <ProcessLogViewer\n            instanceId={instanceId}\n            processStatus={processStatus}\n            data={data}\n            opts={opts}\n            completed={wholeLogLoading}\n            optsHandler={optsHandler}\n            loadWholeLog={loadWholeLog}\n            selectedCorrelationId={selectedCorrelationId}\n        />\n    );\n};\n\nconst DEFAULT_OPTS: LogProcessorOptions = {\n    useLocalTime: true,\n    showDate: false,\n    separateTasks: true,\n};\n\nconst getStoredOpts = (): LogProcessorOptions => {\n    const data = localStorage.getItem('logViewerOpts');\n    if (!data) {\n        return DEFAULT_OPTS;\n    }\n\n    return JSON.parse(data);\n};\n\nconst storeOpts = (opts: LogProcessorOptions) => {\n    const data = JSON.stringify(opts);\n    localStorage.setItem('logViewerOpts', data);\n};\n\nconst usePolling = (\n    request: (range: LogRange) => Promise<FetchResponse>,\n    rangeRef: MutableRefObject<LogRange>,\n    setData: Dispatch<SetStateAction<LogSegment[]>>,\n    loadingHandler: (inc: number) => void,\n    refresh: boolean,\n    stopPollingIndicator: boolean,\n    dataFetchInterval: number\n): RequestError | undefined => {\n    const poll = useRef<number | undefined>(undefined);\n    const [error, setError] = useState<RequestError>();\n\n    useEffect(() => {\n        let cancelled = false;\n\n        const fetchData = async (range: LogRange) => {\n            loadingHandler(1);\n\n            let r = range;\n            try {\n                const resp = await request(r);\n\n                r = { low: resp.range.high, high: undefined };\n                rangeRef.current = r;\n\n                setError(undefined);\n\n                if (!cancelled) {\n                    setData((prev) => [...prev, ...resp.data]);\n                }\n            } catch (e) {\n                setError(e);\n            } finally {\n                if (!stopPollingIndicator && !cancelled) {\n                    poll.current = window.setTimeout(() => fetchData(r), dataFetchInterval);\n                } else {\n                    stopPolling();\n                }\n\n                loadingHandler(-1);\n            }\n        };\n\n        fetchData(rangeRef.current);\n\n        return () => {\n            cancelled = true;\n            stopPolling();\n        };\n    }, [\n        request,\n        setData,\n        rangeRef,\n        refresh,\n        loadingHandler,\n        stopPollingIndicator,\n        dataFetchInterval,\n    ]);\n\n    const stopPolling = () => {\n        if (poll.current) {\n            clearTimeout(poll.current);\n            poll.current = undefined;\n        }\n    };\n\n    return error;\n};\n\nexport default ProcessLogActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessLogActivity/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n.logViewer {\n    font-family: monospace;\n    white-space: pre-wrap;\n    margin-top: 10px;\n}\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessLogActivityV2/LogSegmentActivity.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport {\n    Dispatch,\n    MutableRefObject,\n    SetStateAction,\n    useCallback,\n    useEffect,\n    useRef,\n    useState\n} from 'react';\nimport { Header, Modal } from 'semantic-ui-react';\n\nimport { ConcordId, RequestError } from '../../../api/common';\nimport { LogSegment } from '../../molecules';\nimport { getSegmentLog as apiGetLog, LogRange, SegmentStatus } from '../../../api/process/log';\nimport RequestErrorActivity from '../RequestErrorActivity';\nimport { LogProcessorOptions, processText } from '../../../state/data/processes/logs/processors';\nimport { isFinal, ProcessStatus } from '../../../api/process';\nimport { TaskCallDetails } from '../index';\n\nimport './styles.css';\n\nconst DATA_FETCH_INTERVAL = 5000;\nconst DEFAULT_RANGE: LogRange = { low: undefined, high: 2048 };\n\ninterface ExternalProps {\n    instanceId: ConcordId;\n    segmentId: number;\n    correlationId?: string;\n    name: string;\n    createdAt: string;\n    processStatus?: ProcessStatus;\n    status?: SegmentStatus;\n    statusUpdatedAt?: string;\n    warnings?: number;\n    errors?: number;\n    opts: LogProcessorOptions;\n    forceRefresh: boolean;\n    forceOpen: boolean;\n}\n\ninterface FetchResponse {\n    data: string;\n    range: LogRange;\n}\n\nconst LogSegmentActivity = ({\n    instanceId,\n    processStatus,\n    segmentId,\n    correlationId,\n    name,\n    createdAt,\n    status,\n    statusUpdatedAt,\n    warnings,\n    errors,\n    opts,\n    forceRefresh,\n    forceOpen\n}: ExternalProps) => {\n    const range = useRef<LogRange>(DEFAULT_RANGE);\n    const rangeInit = useRef<LogRange>(DEFAULT_RANGE);\n\n    const [refresh, setRefresh] = useState<boolean>(false);\n    const [stopPolling, setStopPolling] = useState<boolean>(true);\n    const [data, setData] = useState<string[]>([]);\n    const [loadedRange, setLoadedRange] = useState<LogRange>({});\n    const [visibleData, setVisibleData] = useState<string[]>([]);\n    const [segmentInfoOpen, setSegmentInfoOpen] = useState<boolean>(false);\n    const [loading, setLoading] = useState<boolean>(false);\n    const [continueFetch, setContinueFetch] = useState<boolean>(true);\n\n    const fetchData = useCallback(\n        async (range: LogRange) => {\n            const chunk = await apiGetLog(instanceId, segmentId, range);\n\n            const data = chunk && chunk.data.length > 0 ? chunk.data : undefined;\n            const processedData = data ? processText(data, opts) : '';\n\n            return {\n                data: processedData,\n                range: chunk.range\n            };\n        },\n        [instanceId, segmentId, opts]\n    );\n\n    const startPollingHandler = useCallback((isLoadWholeLog: boolean) => {\n        if (isLoadWholeLog) {\n            range.current = { low: 0 };\n            rangeInit.current = { low: 0 };\n        } else {\n            range.current = DEFAULT_RANGE;\n            rangeInit.current = DEFAULT_RANGE;\n        }\n\n        setData([]);\n        setStopPolling(false);\n        setRefresh((prevState) => !prevState);\n    }, []);\n\n    const stopPollingHandler = useCallback(() => {\n        setData([]);\n        setStopPolling(true);\n    }, []);\n\n    const segmentInfoHandler = useCallback(() => {\n        setSegmentInfoOpen(true);\n    }, []);\n\n    useEffect(() => {\n        setRefresh((prevState) => !prevState);\n    }, [forceRefresh]);\n\n    useEffect(() => {\n        range.current = rangeInit.current;\n        setData([]);\n    }, [opts]);\n\n    useEffect(() => {\n        if (data.length > 0) {\n            setVisibleData(data);\n        }\n    }, [data]);\n\n    useEffect(() => {\n        const isFinalOrSuspended =\n            isFinal(processStatus) || processStatus === ProcessStatus.SUSPENDED;\n        const isFinalSegmentStatus = status !== undefined && status !== SegmentStatus.RUNNING;\n        setContinueFetch(!isFinalOrSuspended && !isFinalSegmentStatus);\n    }, [processStatus, status]);\n\n    const error = usePolling(\n        fetchData,\n        range,\n        setData,\n        setLoadedRange,\n        setLoading,\n        refresh,\n        stopPolling,\n        continueFetch\n    );\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n\n    return (\n        <>\n            <LogSegment\n                instanceId={instanceId}\n                segmentId={segmentId}\n                processStatus={processStatus}\n                name={name}\n                createdAt={createdAt}\n                status={status}\n                statusUpdatedAt={statusUpdatedAt}\n                warnings={warnings}\n                errors={errors}\n                onStartLoading={startPollingHandler}\n                onStopLoading={stopPollingHandler}\n                onSegmentInfo={correlationId ? segmentInfoHandler : undefined}\n                data={visibleData}\n                lowRange={loadedRange.low}\n                loading={loading}\n                forceOpen={forceOpen}\n            />\n\n            {correlationId && (\n                <Modal\n                    open={segmentInfoOpen}\n                    onClose={() => setSegmentInfoOpen(false)}\n                    size=\"small\">\n                    <Header icon=\"browser\" content={name} />\n                    <Modal.Content scrolling={true}>\n                        <TaskCallDetails instanceId={instanceId} correlationId={correlationId} />\n                    </Modal.Content>\n                </Modal>\n            )}\n        </>\n    );\n};\n\nconst usePolling = (\n    request: (range: LogRange) => Promise<FetchResponse>,\n    rangeRef: MutableRefObject<LogRange>,\n    setData: Dispatch<SetStateAction<string[]>>,\n    setRange: Dispatch<SetStateAction<LogRange>>,\n    setLoading: Dispatch<SetStateAction<boolean>>,\n    refresh: boolean,\n    stopPollingIndicator: boolean,\n    continueFetch?: boolean\n): RequestError | undefined => {\n    const poll = useRef<number | undefined>(undefined);\n    const [error, setError] = useState<RequestError>();\n\n    useEffect(() => {\n        let cancelled = false;\n\n        const fetchData = async (range: LogRange) => {\n            let r = range;\n            try {\n                setLoading(true);\n                const resp = await request(r);\n\n                r = { low: resp.range.high, high: undefined };\n\n                setError(undefined);\n\n                if (!cancelled && resp.data.length > 0) {\n                    rangeRef.current = r;\n                    setData((prev) => [...prev, resp.data]);\n                    setRange((prev) => {\n                        // was a full load or a first tail load\n                        if (range.low === 0 || range.high !== undefined) {\n                            return {\n                                low: resp.range.low,\n                                high: resp.range.high,\n                                length: resp.range.length\n                            };\n                        }\n\n                        return prev;\n                    });\n                }\n            } catch (e) {\n                setError(e);\n            } finally {\n                setLoading(false);\n\n                if (!stopPollingIndicator && !cancelled && continueFetch) {\n                    poll.current = window.setTimeout(() => fetchData(r), DATA_FETCH_INTERVAL);\n                } else {\n                    stopPolling();\n                }\n            }\n        };\n\n        if (stopPollingIndicator) {\n            return;\n        }\n\n        fetchData(rangeRef.current);\n\n        return () => {\n            cancelled = true;\n            stopPolling();\n        };\n    }, [\n        request,\n        setData,\n        setRange,\n        rangeRef,\n        setLoading,\n        refresh,\n        stopPollingIndicator,\n        continueFetch\n    ]);\n\n    const stopPolling = () => {\n        if (poll.current) {\n            clearTimeout(poll.current);\n            poll.current = undefined;\n        }\n    };\n\n    return error;\n};\n\nexport default LogSegmentActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessLogActivityV2/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useEffect, useState } from 'react';\n\nimport { ConcordId } from '../../../api/common';\nimport { isFinal, ProcessStatus } from '../../../api/process';\n\nimport './styles.css';\nimport {\n    listLogSegments as apiListLogSegments,\n    LogSegmentEntry,\n    SegmentStatus,\n} from '../../../api/process/log';\nimport { usePolling } from '../../../api/usePolling';\nimport { RequestErrorActivity } from '../../organisms';\nimport LogSegmentActivity from './LogSegmentActivity';\nimport { FormWizardAction, ProcessToolbar } from '../../molecules';\nimport { Button, Divider, Dropdown, DropdownProps, Popup, Radio } from 'semantic-ui-react';\nimport { LogProcessorOptions } from '../../../state/data/processes/logs/processors';\nimport { useNavigate } from 'react-router';\nimport { FormListEntry, list as apiListForms } from '../../../api/process/form';\n\nconst DEFAULT_SEGMENT_OPTS: LogProcessorOptions = {\n    useLocalTime: true,\n    showDate: false,\n    separateTasks: true,\n};\n\nconst DEFAULT_OPTS: LogOptions = {\n    expandAllSegments: false,\n    showSystemSegment: true,\n    segmentOptions: DEFAULT_SEGMENT_OPTS,\n};\n\nconst LOG_SEGMENT_STATUS_OPTS = [\n    {\n        key: 'ALL',\n        text: 'All logs',\n        value: 'ALL',\n    },\n    {\n        key: SegmentStatus.OK,\n        text: SegmentStatus.OK,\n        value: SegmentStatus.OK,\n        icon: { name: 'circle', color: 'green' },\n    },\n    {\n        key: SegmentStatus.FAILED,\n        text: SegmentStatus.FAILED,\n        value: SegmentStatus.FAILED,\n        icon: { name: 'close', color: 'red' },\n    },\n    {\n        key: SegmentStatus.RUNNING,\n        text: SegmentStatus.RUNNING,\n        value: SegmentStatus.RUNNING,\n        icon: { name: 'spinner', color: 'teal' },\n    },\n    {\n        key: SegmentStatus.SUSPENDED,\n        text: SegmentStatus.SUSPENDED,\n        value: SegmentStatus.SUSPENDED,\n        icon: { name: 'hourglass half', color: 'blue' },\n    },\n];\n\ninterface LogOptions {\n    expandAllSegments: boolean;\n    showSystemSegment: boolean;\n    segmentOptions: LogProcessorOptions;\n}\n\ninterface ExternalProps {\n    instanceId: ConcordId;\n    processStatus?: ProcessStatus;\n    loadingHandler: (inc: number) => void;\n    forceRefresh: boolean;\n    dataFetchInterval: number;\n}\n\nconst ProcessLogActivityV2 = ({\n    instanceId,\n    processStatus,\n    loadingHandler,\n    forceRefresh,\n    dataFetchInterval,\n}: ExternalProps) => {\n    const navigate = useNavigate();\n    const [segments, setSegments] = useState<LogSegmentEntry[]>([]);\n    const [logOpts, setLogOptions] = useState<LogOptions>(getStoredOpts());\n    const [forms, setForms] = useState<FormListEntry[]>([]);\n\n    const [logSegmentStatusFilter, setLogSegmentStatusFilter] = useState<string>(\n        LOG_SEGMENT_STATUS_OPTS[0].value\n    );\n\n    const handleLogSegmentStatusFilterChange = useCallback(\n        (e: React.SyntheticEvent<HTMLElement>, data: DropdownProps) => {\n            setLogSegmentStatusFilter(data.value as string);\n        },\n        []\n    );\n\n    const segmentOptsHandler = useCallback((o: LogProcessorOptions) => {\n        setLogOptions((prev) => {\n            return { ...prev, segmentOptions: o };\n        });\n    }, []);\n\n    const logOptsHandler = useCallback((o: LogOptions) => {\n        setLogOptions(o);\n    }, []);\n\n    useEffect(() => {\n        storeOpts(logOpts);\n    }, [logOpts]);\n\n    const fetchSegments = useCallback(async () => {\n        // TODO: real limit/offset\n        const limit = -1;\n        const offset = 0;\n        const segments = await apiListLogSegments(instanceId, offset, limit);\n\n        setSegments(segments.items);\n\n        return !isFinal(processStatus) && processStatus !== ProcessStatus.SUSPENDED;\n    }, [instanceId, processStatus]);\n\n    const fetchForm = useCallback(async () => {\n        const forms = await apiListForms(instanceId);\n        setForms(forms);\n\n        return !isFinal(processStatus);\n    }, [instanceId, processStatus]);\n\n    const formError = usePolling(fetchForm, dataFetchInterval, loadingHandler, forceRefresh);\n\n    const error = usePolling(fetchSegments, dataFetchInterval, loadingHandler, forceRefresh);\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n\n    if (formError) {\n        return <RequestErrorActivity error={formError} />;\n    }\n\n    return (\n        <>\n            <ProcessToolbar>\n                <Dropdown\n                    icon=\"filter\"\n                    labeled={true}\n                    button={true}\n                    basic={true}\n                    className=\"icon\"\n                    style={{ marginRight: 20, width: 160 }}\n                    options={LOG_SEGMENT_STATUS_OPTS}\n                    onChange={handleLogSegmentStatusFilterChange}\n                    value={logSegmentStatusFilter}\n                />\n\n                <div style={{ flexGrow: 1 }} />\n\n                {forms.length > 0 && processStatus === ProcessStatus.SUSPENDED && (\n                    <div style={{ marginRight: 20 }}>\n                        <FormWizardAction\n                            onOpenWizard={() =>\n                                navigate(`/process/${instanceId}/wizard?fullScreen=true`)\n                            }\n                        />\n                    </div>\n                )}\n\n                <Popup\n                    size=\"huge\"\n                    position=\"bottom left\"\n                    trigger={<Button basic={true} icon=\"setting\" style={{ marginRight: 20 }} />}\n                    on=\"click\"\n                >\n                    <div>\n                        <Radio\n                            label=\"Expand all segments\"\n                            toggle={true}\n                            checked={logOpts.expandAllSegments}\n                            onChange={(ev, data) =>\n                                logOptsHandler({\n                                    ...logOpts,\n                                    expandAllSegments: data.checked as boolean,\n                                })\n                            }\n                        />\n                    </div>\n\n                    <div>\n                        <Radio\n                            label=\"Show system logs\"\n                            toggle={true}\n                            checked={logOpts.showSystemSegment}\n                            onChange={(ev, data) =>\n                                logOptsHandler({\n                                    ...logOpts,\n                                    showSystemSegment: data.checked as boolean,\n                                })\n                            }\n                        />\n                    </div>\n\n                    <Divider horizontal={true}>Timestamps</Divider>\n\n                    <div>\n                        <Radio\n                            label=\"Use local time\"\n                            toggle={true}\n                            checked={logOpts.segmentOptions.useLocalTime}\n                            onChange={(ev, data) =>\n                                segmentOptsHandler({\n                                    ...logOpts.segmentOptions,\n                                    useLocalTime: data.checked as boolean,\n                                })\n                            }\n                        />\n                    </div>\n\n                    <div>\n                        <Radio\n                            label=\"Show date\"\n                            toggle={true}\n                            checked={logOpts.segmentOptions.showDate}\n                            onChange={(ev, data) =>\n                                segmentOptsHandler({\n                                    ...logOpts.segmentOptions,\n                                    showDate: data.checked as boolean,\n                                })\n                            }\n                        />\n                    </div>\n                </Popup>\n\n                <Button.Group>\n                    <Button\n                        disabled={!instanceId}\n                        onClick={() => window.open(`/api/v1/process/${instanceId}/log`, '_blank')}\n                    >\n                        Raw\n                    </Button>\n                </Button.Group>\n            </ProcessToolbar>\n\n            {segments\n                .filter(\n                    (value) =>\n                        logOpts.showSystemSegment || (!logOpts.showSystemSegment && value.id !== 0)\n                )\n                .filter(\n                    (value) =>\n                        logSegmentStatusFilter === 'ALL' ||\n                        value.status === undefined ||\n                        logSegmentStatusFilter === value.status\n                )\n                .map((s) => {\n                    return (\n                        <LogSegmentActivity\n                            instanceId={instanceId}\n                            segmentId={s.id}\n                            correlationId={s.correlationId}\n                            name={s.name}\n                            createdAt={s.createdAt}\n                            status={s.status}\n                            statusUpdatedAt={s.statusUpdatedAt}\n                            warnings={s.warnings}\n                            errors={s.errors}\n                            processStatus={processStatus}\n                            opts={logOpts.segmentOptions}\n                            forceRefresh={forceRefresh}\n                            key={s.id}\n                            forceOpen={logOpts.expandAllSegments}\n                        />\n                    );\n                })}\n        </>\n    );\n};\n\nconst getStoredOpts = (): LogOptions => {\n    const data = localStorage.getItem('logOptsV2');\n    if (!data) {\n        return DEFAULT_OPTS;\n    }\n\n    return JSON.parse(data);\n};\n\nconst storeOpts = (opts: LogOptions) => {\n    const data = JSON.stringify(opts);\n    localStorage.setItem('logOptsV2', data);\n};\n\nexport default ProcessLogActivityV2;\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessLogActivityV2/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n.logViewer {\n    font-family: monospace;\n    white-space: pre-wrap;\n    margin-top: 10px;\n}\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessRestoreActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { useCallback, useState } from 'react';\n\nimport { ConcordKey, RequestError } from '../../../api/common';\nimport { isFinal, ProcessStatus } from '../../../api/process';\nimport { restoreProcess as apiRestoreProcess } from '../../../api/process/checkpoint';\nimport { ButtonWithConfirmation, RequestErrorMessage } from '../../molecules';\n\ninterface Props {\n    instanceId: ConcordKey;\n    processStatus: ProcessStatus;\n    checkpointId: ConcordKey;\n    checkpoint: string;\n    renderOverride?: React.ReactNode;\n}\n\nexport const isFinalStatus = (status: ProcessStatus): boolean => isFinal(status);\n\nconst ProcessRestoreActivity = ({\n    instanceId,\n    processStatus,\n    checkpointId,\n    checkpoint,\n    renderOverride\n}: Props) => {\n    const [restoring, setRestoring] = useState(false);\n    const [error, setError] = useState<RequestError>();\n\n    const restoreProcess = useCallback(async () => {\n        setRestoring(true);\n        setError(undefined);\n\n        try {\n            await apiRestoreProcess(instanceId, checkpointId);\n        } catch (e) {\n            setError(e);\n        } finally {\n            setRestoring(false);\n        }\n    }, [checkpointId, instanceId]);\n\n    return (\n        <>\n            {error && <RequestErrorMessage error={error} />}\n\n            <ButtonWithConfirmation\n                renderOverride={renderOverride}\n                size={'mini'}\n                floated={'right'}\n                disabled={!isFinalStatus(processStatus)}\n                content=\"restore\"\n                loading={restoring}\n                confirmationHeader=\"Restore the process?\"\n                confirmationContent={`Are you sure you want to restore process at ${checkpoint} checkpoint?`}\n                onConfirm={restoreProcess}\n            />\n        </>\n    );\n};\n\nexport default ProcessRestoreActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessStatusActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useState } from 'react';\nimport { useNavigate } from 'react-router';\nimport { Divider } from 'semantic-ui-react';\n\nimport { get as apiGet, isFinal, ProcessEntry, ProcessStatus } from '../../../api/process';\nimport { FormListEntry, list as apiListForms } from '../../../api/process/form';\n\nimport { usePolling } from '../../../api/usePolling';\nimport { ProcessActionList, ProcessStatusTable } from '../../molecules';\nimport { ProcessCheckpointActivity } from '../../organisms';\nimport RequestErrorActivity from '../RequestErrorActivity';\n\nimport './styles.css';\nimport { ConcordId } from '../../../api/common';\n\ninterface ExternalProps {\n    instanceId: ConcordId;\n    forceRefresh: boolean;\n    loadingHandler: (inc: number) => void;\n    refreshHandler: () => void;\n    dataFetchInterval: number;\n}\n\nconst ProcessStatusActivity = ({\n    instanceId,\n    loadingHandler,\n    forceRefresh,\n    refreshHandler,\n    dataFetchInterval,\n}: ExternalProps) => {\n    const navigate = useNavigate();\n    const [process, setProcess] = useState<ProcessEntry>();\n    const [forms, setForms] = useState<FormListEntry[]>([]);\n\n    const fetchData = useCallback(async () => {\n        const process = await apiGet(instanceId, ['checkpoints', 'checkpointsHistory']);\n        setProcess(process);\n\n        const forms = await apiListForms(instanceId);\n        setForms(forms);\n\n        return !isFinal(process.status);\n    }, [instanceId]);\n\n    const error = usePolling(fetchData, dataFetchInterval, loadingHandler, forceRefresh);\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n\n    const hasCheckpoints = process && process.checkpoints && process.checkpoints.length > 0;\n\n    return (\n        <>\n            <ProcessStatusTable process={process} />\n\n            {process && forms.length > 0 && process.status === ProcessStatus.SUSPENDED && (\n                <>\n                    <Divider content=\"Required Actions\" horizontal={true} />\n                    <ProcessActionList\n                        instanceId={instanceId}\n                        forms={forms}\n                        onOpenWizard={() =>\n                            navigate(`/process/${instanceId}/wizard?fullScreen=true`)\n                        }\n                    />\n                </>\n            )}\n\n            {process && hasCheckpoints && (\n                <>\n                    <Divider content=\"Checkpoints\" horizontal={true} />\n                    <ProcessCheckpointActivity\n                        instanceId={process.instanceId}\n                        processStatus={process.status}\n                        processDisabled={process.disabled}\n                        checkpoints={process.checkpoints!}\n                        checkpointRestoreHistory={process.checkpointRestoreHistory}\n                        onRestoreComplete={refreshHandler}\n                    />\n                </>\n            )}\n        </>\n    );\n};\n\nexport default ProcessStatusActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessStatusActivity/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n.failureDetailsButton {\n    padding-left: 10px;\n}\n\n.failureDetailsButton:hover {\n    cursor: pointer;\n}\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessWaitActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { isFinal, ProcessStatus, WaitCondition } from '../../../api/process';\nimport { get as apiGetWaits } from '../../../api/process/wait';\nimport { ProcessWaitList } from '../../molecules';\nimport { useState } from 'react';\nimport RequestErrorActivity from '../RequestErrorActivity';\nimport { useCallback } from 'react';\nimport { usePolling } from '../../../api/usePolling';\nimport { ConcordId } from '../../../api/common';\n\ninterface ExternalProps {\n    instanceId: ConcordId;\n    loadingHandler: (inc: number) => void;\n    processStatus?: ProcessStatus;\n    forceRefresh: boolean;\n    dataFetchInterval: number;\n}\n\nconst ProcessWaitActivity = ({\n    instanceId,\n    processStatus,\n    loadingHandler,\n    forceRefresh,\n    dataFetchInterval\n}: ExternalProps) => {\n    const [waitConditions, setWaitConditions] = useState<WaitCondition[] | undefined>(undefined);\n\n    const fetchData = useCallback(async () => {\n        const result = await apiGetWaits(instanceId);\n        setWaitConditions(result?.waits || []);\n\n        return !isFinal(processStatus);\n    }, [instanceId, processStatus]);\n\n    const error = usePolling(fetchData, dataFetchInterval, loadingHandler, forceRefresh);\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n\n    return (\n        <>\n            <ProcessWaitList data={waitConditions} />\n        </>\n    );\n};\n\nexport default ProcessWaitActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessWizard/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { Loader } from 'semantic-ui-react';\n\nimport { ConcordId } from '../../../api/common';\nimport { RequestErrorMessage } from '../../molecules';\nimport { useProcessWizard } from './useProcessWizard';\n\ninterface Props {\n    processInstanceId: ConcordId;\n}\n\nconst ProcessWizard = ({ processInstanceId }: Props) => {\n    const error = useProcessWizard(processInstanceId);\n\n    if (error) {\n        return <RequestErrorMessage error={error} />;\n    }\n\n    return <Loader active={true} />;\n};\n\nexport default ProcessWizard;\n"
  },
  {
    "path": "console2/src/components/organisms/ProcessWizard/useProcessWizard.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { useEffect, useState } from 'react';\nimport { useHistory } from '@/router';\n\nimport { ConcordId, RequestError } from '../../../api/common';\nimport { get as apiGetProcess, isFinal, ProcessStatus } from '../../../api/process';\nimport { list as apiListForms } from '../../../api/process/form';\nimport { getProcessLocation, openProcessForm } from '../ProcessFormActivity/processFormNavigation';\n\nconst POLL_INTERVAL = 1000;\n\nexport const useProcessWizard = (processInstanceId: ConcordId) => {\n    const history = useHistory();\n    const [error, setError] = useState<RequestError>();\n\n    useEffect(() => {\n        let cancelled = false;\n        let timeoutId: number | undefined;\n\n        const stopPolling = () => {\n            if (timeoutId !== undefined) {\n                window.clearTimeout(timeoutId);\n                timeoutId = undefined;\n            }\n        };\n\n        const poll = async () => {\n            try {\n                const process = await apiGetProcess(processInstanceId, []);\n                const forms = await apiListForms(processInstanceId);\n\n                if (cancelled) {\n                    return;\n                }\n\n                if (forms.length > 0 && process.status === ProcessStatus.SUSPENDED) {\n                    const [listedForm] = forms;\n\n                    await openProcessForm({\n                        history,\n                        processInstanceId,\n                        formName: listedForm.name,\n                        custom: listedForm.custom,\n                        yieldFlow: listedForm.yield,\n                    });\n                    return;\n                }\n\n                if (isFinal(process.status)) {\n                    history.push(getProcessLocation(processInstanceId));\n                    return;\n                }\n\n                timeoutId = window.setTimeout(poll, POLL_INTERVAL);\n            } catch (e) {\n                if (!cancelled) {\n                    setError(e);\n                }\n            }\n        };\n\n        setError(undefined);\n        poll();\n\n        return () => {\n            cancelled = true;\n            stopPolling();\n        };\n    }, [history, processInstanceId]);\n\n    return error;\n};\n"
  },
  {
    "path": "console2/src/components/organisms/ProjectActivity/ProjectCheckpoints.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { RequestErrorActivity } from '../index';\nimport { LoadingDispatch } from '../../../App';\nimport { useApi } from '../../../hooks/useApi';\nimport { useCallback } from 'react';\nimport { get as apiGet, ProjectEntry } from '../../../api/org/project';\nimport CheckpointView from '../CheckpointView';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    forceRefresh: any;\n}\n\nconst ProjectCheckpoints = ({ orgName, projectName, forceRefresh }: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const fetchData = useCallback(() => {\n        return apiGet(orgName, projectName);\n    }, [orgName, projectName]);\n\n    const { data, error } = useApi<ProjectEntry>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n\n    if (!data) {\n        return <></>;\n    }\n\n    return <CheckpointView project={data} />;\n};\n\nexport default ProjectCheckpoints;\n"
  },
  {
    "path": "console2/src/components/organisms/ProjectActivity/ProjectProcesses.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { ProcessListActivity, RequestErrorActivity } from '../index';\nimport { LoadingDispatch } from '../../../App';\nimport { useApi } from '../../../hooks/useApi';\nimport { useCallback } from 'react';\nimport { get as apiGet, ProjectEntry } from '../../../api/org/project';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    forceRefresh: any;\n}\n\nconst ProjectProcesses = ({ orgName, projectName, forceRefresh }: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const fetchData = useCallback(() => {\n        return apiGet(orgName, projectName);\n    }, [orgName, projectName]);\n\n    const { data, error } = useApi<ProjectEntry>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n\n    if (!data) {\n        return <></>;\n    }\n\n    let columns;\n    if (data.meta && data.meta.ui && data.meta.ui.processList) {\n        columns = data.meta.ui.processList;\n    }\n    return (\n        <ProcessListActivity\n            orgName={orgName}\n            projectName={projectName}\n            columns={columns}\n            usePagination={true}\n            forceRefresh={forceRefresh}\n        />\n    );\n};\n\nexport default ProjectProcesses;\n"
  },
  {
    "path": "console2/src/components/organisms/ProjectActivity/ProjectRepositories.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { RedirectButton, RequestErrorActivity } from '../index';\nimport { LoadingDispatch } from '../../../App';\nimport { useApi } from '../../../hooks/useApi';\nimport { useCallback, useMemo, useRef, useState } from 'react';\nimport {\n    list as apiRepositoryList,\n    listTriggersV2 as apiListTriggers,\n    PaginatedRepositoryEntries,\n    TriggerEntry\n} from '../../../api/org/project/repository';\nimport { Input, Menu } from 'semantic-ui-react';\nimport { PaginationToolBar, RepositoryList, RequestErrorMessage } from '../../molecules';\nimport { usePagination } from '../../molecules/PaginationToolBar/usePagination';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    forceRefresh: any;\n}\n\nconst ProjectRepositories = ({ orgName, projectName, forceRefresh }: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n    const [refresh, toggleRefresh] = useState<boolean>(false);\n\n    const {\n        paginationFilter,\n        handleLimitChange,\n        handleNext,\n        handlePrev,\n        handleFirst,\n        resetOffset,\n    } = usePagination();\n    const oldFilter = useRef<string>();\n\n    const [filter, setFilter] = useState<string>();\n\n    const refreshHandler = useCallback(() => {\n        toggleRefresh((prevState) => !prevState);\n    }, []);\n\n    const fetchData = useCallback(() => {\n        if (filter && oldFilter.current !== filter) {\n            oldFilter.current = filter;\n            resetOffset(0);\n        }\n\n        return apiRepositoryList(\n            orgName,\n            projectName,\n            paginationFilter.offset,\n            paginationFilter.limit,\n            filter\n        );\n    }, [\n        orgName,\n        projectName,\n        filter,\n        paginationFilter.offset,\n        paginationFilter.limit,\n        resetOffset,\n    ]);\n\n    const fetchTriggers = useCallback(() => {\n        return apiListTriggers({\n            type: 'manual',\n            orgName: orgName,\n            projectName: projectName\n        });\n\n    }, [\n        orgName,\n        projectName\n    ]);\n\n    const { data, isLoading, error } = useApi<PaginatedRepositoryEntries>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: (forceRefresh ? 1 : 0) + (refresh ? 10 : 0),\n        debounceTime: 1000,\n        dispatch,\n    });\n\n    const triggerInfo = useApi<TriggerEntry[]>(fetchTriggers, {\n        fetchOnMount: true,\n        forceRequest: (forceRefresh ? 1 : 0 ) + (refresh ? 10 : 0),\n        dispatch\n    });\n\n    const repoTriggerMap = useMemo(()=>{\n        const mapData : {[id: string] : TriggerEntry[]} = {};\n        if (Array.isArray(triggerInfo.data)) {\n            for (let triggerData of triggerInfo.data) {\n                const triggerKey = triggerData.repositoryId;\n                if (Array.isArray(mapData[triggerKey])) {\n                    mapData[triggerKey].push(triggerData);\n                } else {\n                    mapData[triggerKey] = [triggerData];\n                }\n            }\n        }\n        return mapData;\n    }, [triggerInfo.data]);\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n\n    if (triggerInfo.error) {\n        return <RequestErrorMessage error={triggerInfo.error} />;\n    }\n\n    return (\n        <>\n            <Menu secondary={true}>\n                <Menu.Item>\n                    <Input\n                        icon=\"search\"\n                        placeholder=\"Filter...\"\n                        onChange={(ev, data) => setFilter(data.value)}\n                    />\n                </Menu.Item>\n\n                <Menu.Menu position={'right'}>\n                    <Menu.Item>\n                        <RedirectButton\n                            icon=\"plus\"\n                            positive={true}\n                            labelPosition=\"left\"\n                            content=\"Add repository\"\n                            location={`/org/${orgName}/project/${projectName}/repository/_new`}\n                        />\n                    </Menu.Item>\n\n                    <Menu.Item style={{ padding: 0 }}>\n                        <PaginationToolBar\n                            limit={paginationFilter.limit}\n                            handleLimitChange={(limit) => handleLimitChange(limit)}\n                            handleNext={handleNext}\n                            handlePrev={handlePrev}\n                            handleFirst={handleFirst}\n                            disablePrevious={paginationFilter.offset <= 0}\n                            disableNext={!data?.next}\n                            disableFirst={paginationFilter.offset <= 0}\n                        />\n                    </Menu.Item>\n                </Menu.Menu>\n            </Menu>\n\n            <RepositoryList\n                orgName={orgName}\n                projectName={projectName}\n                data={data?.items}\n                triggerMap={repoTriggerMap}\n                loading={isLoading}\n                refresh={refreshHandler}\n            />\n        </>\n    );\n};\n\nexport default ProjectRepositories;\n"
  },
  {
    "path": "console2/src/components/organisms/ProjectActivity/ProjectSettings.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\n\nimport { ConcordKey } from '../../../api/common';\nimport {\n    EditProjectActivity,\n    EncryptValueActivity,\n    ProjectDeleteActivity,\n    ProjectOrganizationChangeActivity,\n    ProjectOwnerChangeActivity,\n    ProjectRawPayloadModeActivity,\n    ProjectOutVariablesModeActivity,\n    ProjectRenameActivity,\n    RequestErrorActivity,\n    ProjectProcessExecModeActivity\n} from '../index';\nimport { LoadingDispatch } from '../../../App';\nimport { useApi } from '../../../hooks/useApi';\nimport { useCallback } from 'react';\nimport {get as apiGet, getCapacity as apiGetCapacity, KVCapacity, ProjectEntry} from '../../../api/org/project';\nimport {Divider, Header, Icon, Progress, Segment} from 'semantic-ui-react';\nimport EntityId from '../../molecules/EntityId';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    forceRefresh: any;\n}\n\nconst ProjectSettings = ({ orgName, projectName, forceRefresh }: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const fetchData = useCallback(async () => {\n        const project = await apiGet(orgName, projectName);\n        const capacity = await apiGetCapacity(orgName, projectName);\n        return { ...project, ...capacity };\n    }, [orgName, projectName]);\n\n    const { data, error, isLoading } = useApi<ProjectEntry & KVCapacity>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n\n    return (\n        <>\n            <Header as=\"h5\" disabled={true}>\n                <EntityId id={data?.id} />\n            </Header>\n\n            <Segment disabled={isLoading}>\n                <Header as=\"h4\">KV capacity</Header>\n                <Capacity org={data?.orgName} project={data?.name} current={data?.size} max={data?.maxSize} />\n            </Segment>\n\n            <Segment disabled={isLoading}>\n                <Header as=\"h4\">Allow process execution</Header>\n                {data && (\n                    <ProjectProcessExecModeActivity\n                        orgName={orgName}\n                        projectId={data.id}\n                        initialValue={data.processExecMode}\n                    />\n                )}\n            </Segment>\n\n            <Segment disabled={isLoading}>\n                <Header as=\"h4\">Allow payload archives</Header>\n                {data && (\n                    <ProjectRawPayloadModeActivity\n                        orgName={orgName}\n                        projectId={data.id}\n                        initialValue={data.rawPayloadMode}\n                    />\n                )}\n            </Segment>\n\n            <Segment disabled={isLoading}>\n                <Header as=\"h4\">Allow out variable names in request</Header>\n                {data && (\n                    <ProjectOutVariablesModeActivity\n                        orgName={orgName}\n                        projectId={data.id}\n                        initialValue={data.outVariablesMode}\n                    />\n                )}\n            </Segment>\n\n            <Segment disabled={isLoading}>\n                <Header as=\"h4\">Encrypt a value</Header>\n                <EncryptValueActivity orgName={orgName} projectName={projectName} />\n            </Segment>\n\n            <Segment disabled={isLoading}>\n                <EditProjectActivity orgName={orgName} projectName={projectName} initial={data} />\n            </Segment>\n\n            <Divider horizontal={true} content=\"Danger Zone\" disabled={isLoading} />\n\n            <Segment color=\"red\" disabled={isLoading}>\n                <Header as=\"h4\">Project name</Header>\n                <ProjectRenameActivity\n                    orgName={orgName}\n                    projectId={data?.id}\n                    projectName={projectName}\n                    disabled={isLoading}\n                />\n\n                <Header as=\"h4\">Project owner</Header>\n                <ProjectOwnerChangeActivity\n                    orgName={orgName}\n                    projectName={projectName}\n                    initialOwnerId={data?.owner?.id}\n                    disabled={isLoading}\n                />\n\n                <Header as=\"h4\">Organization</Header>\n                <ProjectOrganizationChangeActivity\n                    orgName={orgName}\n                    projectName={projectName}\n                    disabled={isLoading}\n                />\n\n                <Header as=\"h4\">Delete project</Header>\n                <ProjectDeleteActivity\n                    orgName={orgName}\n                    projectName={projectName}\n                    disabled={isLoading}\n                />\n            </Segment>\n        </>\n    );\n};\n\ninterface CapacityProps {\n    org?: ConcordKey;\n    project?: ConcordKey;\n    current?: number;\n    max?: number;\n}\n\nconst Capacity = ({ org, project, current, max }: CapacityProps) => {\n    if (org === undefined || project === undefined) {\n        return (\n            <div>\n                <em>Loading</em>\n            </div>\n        );\n    }\n\n    return (\n        <>\n            {(current !== undefined && max !== undefined) &&\n            <div>\n                <Progress\n                    percent={(current / max) * 100}\n                    size={'tiny'}\n                    color={'red'}\n                    style={{ width: '30%' }}\n                />\n            </div>\n            }\n            <Header size=\"tiny\" style={{ marginTop: '0px', color: 'rgba(0, 0, 0, 0.5)' }}>\n                <a\n                    href={`/api/v1/org/${org}/project/${project}/kv`}\n                    rel=\"noopener noreferrer\"\n                    target=\"_blank\">\n                    <Icon name={'database'} color={'blue'} link={true}/>\n                    Used {current} of {max || 'Unlimited'}\n                </a>\n            </Header>\n        </>\n    );\n};\n\nexport default ProjectSettings;\n"
  },
  {
    "path": "console2/src/components/organisms/ProjectActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Navigate, Route, Routes } from 'react-router';\nimport { Link } from 'react-router';\nimport { Icon, Menu } from 'semantic-ui-react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { AuditLogActivity, ProjectTeamAccessActivity } from '../../organisms';\nimport { NotFoundPage } from '../../pages';\nimport ProjectConfigurationActivity from '../ProjectConfigurationActivity';\nimport ProjectProcesses from './ProjectProcesses';\nimport ProjectRepositories from './ProjectRepositories';\nimport ProjectSettings from './ProjectSettings';\nimport ProjectCheckpoints from './ProjectCheckpoints';\n\nexport type TabLink =\n    | 'process'\n    | 'checkpoint'\n    | 'repository'\n    | 'settings'\n    | 'access'\n    | 'configuration'\n    | 'audit'\n    | null;\n\ninterface ExternalProps {\n    activeTab: TabLink;\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    forceRefresh: any;\n}\n\nconst ProjectActivity = ({ activeTab, orgName, projectName, forceRefresh }: ExternalProps) => {\n    const baseUrl = `/org/${orgName}/project/${projectName}`;\n\n    return (\n        <>\n            <Menu tabular={true} style={{ marginTop: 0 }}>\n                <Menu.Item active={activeTab === 'process'}>\n                    <Icon name=\"tasks\" />\n                    <Link to={`${baseUrl}/process`}>Processes</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'checkpoint'}>\n                    <Icon name=\"checkmark\" />\n                    <Link to={`${baseUrl}/checkpoint`}>Checkpoints</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'repository'}>\n                    <Icon name=\"code\" />\n                    <Link to={`${baseUrl}/repository`}>Repositories</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'access'}>\n                    <Icon name=\"key\" />\n                    <Link to={`${baseUrl}/access`}>Access</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'configuration'}>\n                    <Icon name=\"save\" />\n                    <Link to={`${baseUrl}/configuration`}>Configuration</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'settings'}>\n                    <Icon name=\"setting\" />\n                    <Link to={`${baseUrl}/settings`}>Settings</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'audit'}>\n                    <Icon name=\"history\" />\n                    <Link to={`${baseUrl}/audit`}>Audit Log</Link>\n                </Menu.Item>\n            </Menu>\n\n            <Routes>\n                <Route index={true} element={<Navigate to=\"process\" replace={true} />} />\n                <Route\n                    path=\"process\"\n                    element={\n                        <ProjectProcesses\n                            orgName={orgName}\n                            projectName={projectName}\n                            forceRefresh={forceRefresh}\n                        />\n                    }\n                />\n                <Route\n                    path=\"checkpoint\"\n                    element={\n                        <ProjectCheckpoints\n                            orgName={orgName}\n                            projectName={projectName}\n                            forceRefresh={forceRefresh}\n                        />\n                    }\n                />\n                <Route\n                    path=\"repository\"\n                    element={\n                        <ProjectRepositories\n                            orgName={orgName}\n                            projectName={projectName}\n                            forceRefresh={forceRefresh}\n                        />\n                    }\n                />\n                <Route\n                    path=\"access\"\n                    element={\n                        <ProjectTeamAccessActivity orgName={orgName} projectName={projectName} />\n                    }\n                />\n                <Route\n                    path=\"configuration\"\n                    element={\n                        <ProjectConfigurationActivity\n                            orgName={orgName}\n                            projectName={projectName}\n                            forceRefresh={forceRefresh}\n                        />\n                    }\n                />\n                <Route\n                    path=\"settings\"\n                    element={\n                        <ProjectSettings\n                            orgName={orgName}\n                            projectName={projectName}\n                            forceRefresh={forceRefresh}\n                        />\n                    }\n                />\n                <Route\n                    path=\"audit\"\n                    element={\n                        <AuditLogActivity\n                            showRefreshButton={false}\n                            filter={{ details: { orgName: orgName, projectName: projectName } }}\n                            forceRefresh={forceRefresh}\n                        />\n                    }\n                />\n                <Route path=\"*\" element={<NotFoundPage />} />\n            </Routes>\n        </>\n    );\n};\n\nexport default ProjectActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ProjectConfigurationActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useState } from 'react';\n\nimport { ConcordKey, RequestError } from '../../../api/common';\nimport {\n    getProjectConfiguration as apiGetProjectConfiguration,\n    updateProjectConfiguration as apiUpdateProjectConfiguration\n} from '../../../api/org/project';\nimport ProjectConfiguration from '../../molecules/ProjectConfiguration';\nimport { RequestErrorActivity } from '../index';\nimport { LoadingDispatch } from '../../../App';\nimport { useApi } from '../../../hooks/useApi';\n\ninterface Props {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    forceRefresh: any;\n}\n\nexport default ({ orgName, projectName, forceRefresh }: Props) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const [updating, setUpdating] = useState(false);\n    const [updateError, setUpdateError] = useState<RequestError>();\n\n    const fetchData = useCallback(() => {\n        return apiGetProjectConfiguration(orgName, projectName);\n    }, [orgName, projectName]);\n\n    const { data, error } = useApi<Object>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    const update = useCallback((orgName: ConcordKey, projectName: ConcordKey, config: Object) => {\n        const update = async () => {\n            try {\n                setUpdating(true);\n                await apiUpdateProjectConfiguration(orgName, projectName, config);\n                setUpdateError(undefined);\n            } catch (e) {\n                setUpdateError(e);\n            } finally {\n                setUpdating(false);\n            }\n        };\n\n        update();\n    }, []);\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n    if (updateError) {\n        return <RequestErrorActivity error={updateError} />;\n    }\n\n    return (\n        <div>\n            <ProjectConfiguration\n                config={data}\n                submitting={updating}\n                submit={(config) => update(orgName, projectName, config)}\n            />\n        </div>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/ProjectDeleteActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { ConcordKey, GenericOperationResult } from '../../../api/common';\nimport { ButtonWithConfirmation } from '../../molecules';\nimport { RequestErrorActivity } from '../index';\nimport { useCallback } from 'react';\nimport { deleteProject as apiDelete } from '../../../api/org/project';\nimport { useApi } from '../../../hooks/useApi';\nimport { Navigate } from 'react-router';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    disabled?: boolean;\n}\n\nconst ProjectDeleteActivity = (props: ExternalProps) => {\n    const { orgName, projectName, disabled } = props;\n\n    const deleteData = useCallback(async () => {\n        return await apiDelete(orgName, projectName);\n    }, [orgName, projectName]);\n\n    const { data, error, isLoading, fetch, clearState } = useApi<GenericOperationResult>(\n        deleteData,\n        {\n            fetchOnMount: false,\n            requestByFetch: true,\n        }\n    );\n\n    const confirmHandler = useCallback(() => {\n        clearState();\n        fetch();\n    }, [clearState, fetch]);\n\n    if (data) {\n        return <Navigate to={`/org/${orgName}/project`} />;\n    }\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n            <ButtonWithConfirmation\n                primary={true}\n                negative={true}\n                content=\"Delete\"\n                loading={isLoading}\n                disabled={disabled}\n                confirmationHeader=\"Delete the project?\"\n                confirmationContent=\"Are you sure you want to delete the project?\"\n                onConfirm={confirmHandler}\n            />\n        </>\n    );\n};\n\nexport default ProjectDeleteActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ProjectListActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { Link } from 'react-router';\nimport { Icon, Input, List, Menu } from 'semantic-ui-react';\n\nimport { ConcordKey, EntityType } from '../../../api/common';\nimport { checkResult as apiCheckResult } from '../../../../src/api/org';\nimport {\n    list as getPaginatedProjectList,\n    PaginatedProjectEntries,\n    ProjectEntry,\n    ProjectVisibility\n} from '../../../api/org/project';\nimport { CreateNewEntityButton, PaginationToolBar } from '../../molecules';\nimport { usePagination } from '../../molecules/PaginationToolBar/usePagination';\nimport { RequestErrorActivity } from '../index';\nimport { useApi } from '../../../hooks/useApi';\nimport { LoadingDispatch } from '../../../App';\nimport './styles.css';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    forceRefresh: any;\n}\n\nexport default ({ orgName, forceRefresh }: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const [canCreate, setCanCreate] = useState<boolean>(false);\n    const {\n        paginationFilter,\n        handleLimitChange,\n        handleNext,\n        handlePrev,\n        handleFirst,\n        resetOffset\n    } = usePagination();\n    const oldFilter = useRef<string>();\n\n    const [filter, setFilter] = useState<string>();\n\n    const fetchData = useCallback(() => {\n        if (filter && oldFilter.current !== filter) {\n            oldFilter.current = filter;\n            resetOffset(0);\n        }\n\n        return getPaginatedProjectList(\n            orgName,\n            paginationFilter.offset,\n            paginationFilter.limit,\n            filter\n        );\n    }, [orgName, filter, paginationFilter.offset, paginationFilter.limit, resetOffset]);\n\n    const { data, error } = useApi<PaginatedProjectEntries>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    const fetchCanCreateStatus = useCallback(async () => {\n        try {\n            const response = await apiCheckResult(EntityType.PROJECT, orgName);\n            setCanCreate(!!response);\n        } catch (e) {\n            // ignore\n        }\n    }, [orgName]);\n\n    useEffect(() => {\n        fetchCanCreateStatus();\n    }, [fetchCanCreateStatus]);\n\n    const ProjectVisibilityIcon = ({ project }: { project: ProjectEntry }) => {\n        if (project.visibility === ProjectVisibility.PUBLIC) {\n            return <Icon name=\"unlock\" size=\"large\" />;\n        } else {\n            return <Icon name=\"lock\" color=\"red\" size=\"large\" />;\n        }\n    };\n\n    return (\n        <>\n            <Menu secondary={true}>\n                <Menu.Item>\n                    <Input\n                        icon=\"search\"\n                        placeholder=\"Filter...\"\n                        onChange={(ev, data) => setFilter(data.value)}\n                    />\n                </Menu.Item>\n\n                <Menu.Menu position={'right'}>\n                    <Menu.Item>\n                        <CreateNewEntityButton\n                            entity=\"project\"\n                            orgName={orgName}\n                            userInOrg={true}\n                            enabledInPolicy={canCreate}\n                        />\n                    </Menu.Item>\n                    <Menu.Item style={{ padding: 0 }}>\n                        <PaginationToolBar\n                            limit={paginationFilter.limit}\n                            handleLimitChange={(limit) => handleLimitChange(limit)}\n                            handleNext={handleNext}\n                            handlePrev={handlePrev}\n                            handleFirst={handleFirst}\n                            disablePrevious={paginationFilter.offset <= 0}\n                            disableNext={!data?.next}\n                            disableFirst={paginationFilter.offset <= 0}\n                        />\n                    </Menu.Item>\n                </Menu.Menu>\n            </Menu>\n\n            {error && <RequestErrorActivity error={error} />}\n            {data?.items.length === 0 && <h3>No projects found</h3>}\n\n            <List divided={true} relaxed={true} size=\"large\">\n                {data?.items.map((project, index) => (\n                    <List.Item key={index}>\n                        <ProjectVisibilityIcon project={project} />\n                        <List.Content>\n                            <List.Header>\n                                <Link to={`/org/${orgName}/project/${project.name}`}>\n                                    {project.name}\n                                </Link>\n                            </List.Header>\n                            <List.Description>\n                                {project.description ? project.description : 'No description'}\n                            </List.Description>\n                        </List.Content>\n                    </List.Item>\n                ))}\n            </List>\n        </>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/ProjectListActivity/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n.ui.menu .item span {\n    position: relative;\n    top: 0;\n    margin: -.5em 0;\n    font-size: 1em;\n}\n\n.ui.menu .item span button.disabled {\n    margin-right: 0;\n}\n"
  },
  {
    "path": "console2/src/components/organisms/ProjectOrganizationChangeActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useState } from 'react';\n\nimport {\n    createOrUpdate as apiChangeOrganization,\n    UpdateProjectEntry,\n} from '../../../api/org/project';\n\nimport { ConcordKey, RequestError } from '../../../api/common';\nimport { SingleOperationPopup } from '../../molecules';\nimport { Form, Input } from 'semantic-ui-react';\nimport { FindOrganizationsField, RequestErrorActivity } from '../index';\nimport { Navigate } from 'react-router';\n\ninterface Props {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    disabled: boolean;\n}\n\nexport default ({ orgName, projectName, disabled }: Props) => {\n    const [dirty, setDirty] = useState<boolean>(false);\n    const [state, setState] = useState<ConcordKey>(orgName);\n    const [confirmation, setConfirmation] = useState('');\n    const [error, setError] = useState<RequestError>();\n    const [changing, setChanging] = useState<boolean>(false);\n    const [success, setSuccess] = useState<boolean>(false);\n    const [redirect, setRedirect] = useState<boolean>(false);\n\n    const toUpdateProjectEntry = (orgName: string, projectName: string): UpdateProjectEntry => {\n        return {\n            name: projectName,\n            orgName,\n        };\n    };\n\n    const confirmHandler = useCallback(async () => {\n        setChanging(true);\n\n        try {\n            const result = await apiChangeOrganization(\n                orgName,\n                toUpdateProjectEntry(state, projectName)\n            );\n            setSuccess(result.ok);\n        } catch (e) {\n            setError(e);\n        } finally {\n            setChanging(false);\n        }\n    }, [state, orgName, projectName]);\n\n    const redirectHandler = useCallback(() => {\n        setRedirect((prevState) => !prevState);\n    }, []);\n\n    const resetHandler = useCallback(() => {\n        setConfirmation('');\n    }, []);\n\n    if (redirect) {\n        return <Navigate to={`/org/${state}/project/${projectName}`} />;\n    }\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n            <Form loading={changing}>\n                <Form.Group widths={3}>\n                    <Form.Field disabled={disabled}>\n                        <FindOrganizationsField\n                            placeholder=\"Search for an organization...\"\n                            defaultOrgName={orgName}\n                            required={true}\n                            onReset={() => {\n                                setDirty(false);\n                                setState(orgName);\n                            }}\n                            onSelect={(value) => {\n                                setDirty(true);\n                                setState(value.name);\n                            }}\n                        />\n                    </Form.Field>\n                    <SingleOperationPopup\n                        trigger={(onClick) => (\n                            <Form.Button\n                                primary={true}\n                                negative={true}\n                                content=\"Move\"\n                                disabled={!dirty || disabled}\n                                onClick={onClick}\n                            />\n                        )}\n                        title=\"Move project?\"\n                        introMsg={\n                            <>\n                                <p>\n                                    Are you sure you want to move the project to{' '}\n                                    <strong>{state}</strong> organization?\n                                </p>\n                                <ul>\n                                    <li>\n                                        Any secret used by repositories in this project will not be\n                                        available.\n                                    </li>\n                                    <li>\n                                        Any secrets scoped to this project, the mapping will be\n                                        removed.{' '}\n                                    </li>\n                                </ul>\n                                <p>\n                                    <strong>NOTE:</strong> Move the secrets to the same organization\n                                    as this project, and map them to repositories or projects again\n                                    to use those secrets.\n                                </p>\n                                <p>\n                                    Please type <strong>{projectName}</strong> in the text box below\n                                    to confirm.\n                                </p>\n                                <div\n                                    className={`ui input ${\n                                        confirmation !== projectName ? 'error' : ''\n                                    }`}\n                                >\n                                    <Input\n                                        type=\"text\"\n                                        name=\"name\"\n                                        placeholder=\"Project name\"\n                                        value={confirmation}\n                                        onChange={(e, data) => setConfirmation(data.value)}\n                                    />\n                                </div>\n                            </>\n                        }\n                        running={changing}\n                        runningMsg={\n                            <p>\n                                Moving the project <strong>{projectName}</strong> to{' '}\n                                <strong>{state}</strong> organization...\n                            </p>\n                        }\n                        success={success}\n                        successMsg={\n                            <p>\n                                The project <strong>{projectName}</strong> was moved successfully to{' '}\n                                <strong>{state}</strong> organization.\n                            </p>\n                        }\n                        error={error}\n                        reset={resetHandler}\n                        onConfirm={confirmHandler}\n                        onDone={redirectHandler}\n                        disableYes={confirmation !== projectName}\n                    />\n                </Form.Group>\n            </Form>\n        </>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/ProjectOutVariablesModeActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { useEffect, useRef, useState } from 'react';\nimport { Form } from 'semantic-ui-react';\nimport { ConcordId, ConcordKey, RequestError } from '../../../api/common';\nimport { createOrUpdate as apiCreateOrUpdate, OutVariablesMode } from '../../../api/org/project';\nimport { RequestErrorActivity } from '../index';\n\nexport interface Props {\n    orgName: ConcordKey;\n    projectId: ConcordId;\n    initialValue?: OutVariablesMode;\n}\n\nconst getDescription = (m: OutVariablesMode): string => {\n    switch (m) {\n        case OutVariablesMode.DISABLED:\n            return 'Sending custom out variable names is disabled. Only out variables defined in concord.yml can be used.';\n        case OutVariablesMode.OWNERS:\n            return \"Only the project's owner and team members with OWNER privileges can specify custom out variable names.\";\n        case OutVariablesMode.TEAM_MEMBERS:\n            return \"Only the members of the teams assigned to the project's can specify custom out variable names.\";\n        case OutVariablesMode.ORG_MEMBERS:\n            return \"Only the project's organization members can specify custom out variable names.\";\n        case OutVariablesMode.EVERYONE:\n            return 'Everyone can can specify custom out variable names. This is the least secure option.';\n        default:\n            return `Unknown raw payload mode: ${m}`;\n    }\n};\n\nexport default ({ orgName, projectId, initialValue = OutVariablesMode.DISABLED }: Props) => {\n    const [value, setValue] = useState(initialValue);\n    const [updating, setUpdating] = useState(false);\n    const [error, setError] = useState<RequestError>();\n\n    const didMountRef = useRef(false);\n    // TODO react-hooks/exhaustive-deps warning\n    useEffect(() => {\n        const update = async () => {\n            if (!didMountRef.current) {\n                didMountRef.current = true;\n                return;\n            }\n\n            try {\n                setUpdating(true);\n                await apiCreateOrUpdate(orgName, {\n                    id: projectId,\n                    outVariablesMode: value\n                });\n            } catch (e) {\n                setError(e);\n            } finally {\n                setUpdating(false);\n            }\n        };\n\n        update();\n    }, [orgName, projectId, value]); // eslint-disable-line react-hooks/exhaustive-deps\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n            <Form loading={updating}>\n                <Form.Group inline={true}>\n                    <Form.Dropdown\n                        selection={true}\n                        value={value}\n                        onChange={(ev, data) => setValue(data.value as OutVariablesMode)}\n                        options={[\n                            { value: OutVariablesMode.DISABLED, text: 'Disabled', icon: 'lock' },\n                            { value: OutVariablesMode.OWNERS, text: 'Only owners' },\n                            { value: OutVariablesMode.TEAM_MEMBERS, text: 'Only team members' },\n                            {\n                                value: OutVariablesMode.ORG_MEMBERS,\n                                text: 'Only organization members'\n                            },\n                            {\n                                value: OutVariablesMode.EVERYONE,\n                                text: 'Everyone',\n                                icon: 'lock open'\n                            }\n                        ]}\n                    />\n\n                    <p>{getDescription(value)}</p>\n                </Form.Group>\n            </Form>\n        </>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/ProjectOwnerChangeActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport { ConcordId, ConcordKey, GenericOperationResult } from '../../../api/common';\nimport { EntityOwnerChangeForm } from '../../molecules';\nimport { useCallback, useState } from 'react';\nimport { changeOwner as apiChangeOwner } from '../../../api/org/project';\nimport { useApi } from '../../../hooks/useApi';\nimport { RequestErrorActivity } from '../index';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    initialOwnerId?: ConcordId;\n    disabled: boolean;\n}\n\nconst ProjectOwnerChangeActivity = ({\n    orgName,\n    projectName,\n    initialOwnerId,\n    disabled\n}: ExternalProps) => {\n    const [value, setValue] = useState(initialOwnerId);\n\n    const postData = useCallback(() => {\n        return apiChangeOwner(orgName, projectName, value!);\n    }, [orgName, projectName, value]);\n\n    const { error, isLoading, fetch, clearState } = useApi<GenericOperationResult>(postData, {\n        fetchOnMount: false,\n        requestByFetch: true\n    });\n\n    const ownerChangeHandler = useCallback(\n        (value: ConcordId) => {\n            setValue(value);\n            clearState();\n            fetch();\n        },\n        [clearState, fetch]\n    );\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n            <EntityOwnerChangeForm\n                originalOwnerId={initialOwnerId}\n                confirmationHeader=\"Change project owner?\"\n                confirmationContent=\"Are you sure you want to change the project's owner?\"\n                onSubmit={ownerChangeHandler}\n                submitting={isLoading}\n                disabled={disabled}\n            />\n        </>\n    );\n};\n\nexport default ProjectOwnerChangeActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ProjectProcessExecModeActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { useEffect, useRef, useState } from 'react';\nimport { Form } from 'semantic-ui-react';\nimport { ConcordId, ConcordKey, RequestError } from '../../../api/common';\nimport { createOrUpdate as apiCreateOrUpdate, ProcessExecMode } from '../../../api/org/project';\nimport { RequestErrorActivity } from '../index';\n\nexport interface Props {\n    orgName: ConcordKey;\n    projectId: ConcordId;\n    initialValue?: ProcessExecMode;\n}\n\nconst getDescription = (m: ProcessExecMode): string => {\n    switch (m) {\n        case ProcessExecMode.DISABLED:\n            return 'No new processes are allowed to run within the context of the project.';\n        case ProcessExecMode.READERS:\n            return \"READER (or above) privileges are necessary to execute a process. All users have READER access to public projects. Private projects must assign permissions explicitly. This is the default mode.\";\n        case ProcessExecMode.WRITERS:\n            return \"WRITER privileges are necessary to execute a process.\";\n        default:\n            return `Unknown process exec mode: ${m}`;\n    }\n};\n\nexport default ({ orgName, projectId, initialValue = ProcessExecMode.DISABLED }: Props) => {\n    const [value, setValue] = useState(initialValue);\n    const [updating, setUpdating] = useState(false);\n    const [error, setError] = useState<RequestError>();\n\n    const didMountRef = useRef(false);\n    // TODO react-hooks/exhaustive-deps warning\n    useEffect(() => {\n        const update = async () => {\n            if (!didMountRef.current) {\n                didMountRef.current = true;\n                return;\n            }\n\n            try {\n                setUpdating(true);\n                await apiCreateOrUpdate(orgName, {\n                    id: projectId,\n                    processExecMode: value\n                });\n            } catch (e) {\n                setError(e);\n            } finally {\n                setUpdating(false);\n            }\n        };\n\n        update();\n    }, [orgName, projectId, value]); // eslint-disable-line react-hooks/exhaustive-deps\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n            <Form loading={updating}>\n                <Form.Group inline={true}>\n                    <Form.Dropdown\n                        selection={true}\n                        value={value}\n                        onChange={(ev, data) => setValue(data.value as ProcessExecMode)}\n                        options={[\n                            { value: ProcessExecMode.DISABLED, text: 'Disabled', icon: 'lock' },\n                            { value: ProcessExecMode.READERS, text: 'READERs or WRITERs' },\n                            { value: ProcessExecMode.WRITERS, text: 'WRITERs only' },\n                        ]}\n                    />\n\n                    <p>{getDescription(value)}</p>\n                </Form.Group>\n            </Form>\n        </>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/ProjectRawPayloadModeActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { useEffect, useRef, useState } from 'react';\nimport { Form } from 'semantic-ui-react';\nimport { ConcordId, ConcordKey, RequestError } from '../../../api/common';\nimport { createOrUpdate as apiCreateOrUpdate, RawPayloadMode } from '../../../api/org/project';\nimport { RequestErrorActivity } from '../index';\n\nexport interface Props {\n    orgName: ConcordKey;\n    projectId: ConcordId;\n    initialValue?: RawPayloadMode;\n}\n\nconst getDescription = (m: RawPayloadMode): string => {\n    switch (m) {\n        case RawPayloadMode.DISABLED:\n            return 'Sending payload archives (ZIP files or individual workflow files) is disabled. Only the configured repositories can be used to start a new process.';\n        case RawPayloadMode.OWNERS:\n            return \"Only the project's owner and team members with OWNER privileges can send payload archives.\";\n        case RawPayloadMode.TEAM_MEMBERS:\n            return \"Only the members of the teams assigned to the project's can send payload archives.\";\n        case RawPayloadMode.ORG_MEMBERS:\n            return \"Only the project's organization members can send payload archives.\";\n        case RawPayloadMode.EVERYONE:\n            return 'Everyone can send payload archives. This is the least secure option.';\n        default:\n            return `Unknown raw payload mode: ${m}`;\n    }\n};\n\nexport default ({ orgName, projectId, initialValue = RawPayloadMode.DISABLED }: Props) => {\n    const [value, setValue] = useState(initialValue);\n    const [updating, setUpdating] = useState(false);\n    const [error, setError] = useState<RequestError>();\n\n    const didMountRef = useRef(false);\n    // TODO react-hooks/exhaustive-deps warning\n    useEffect(() => {\n        const update = async () => {\n            if (!didMountRef.current) {\n                didMountRef.current = true;\n                return;\n            }\n\n            try {\n                setUpdating(true);\n                await apiCreateOrUpdate(orgName, {\n                    id: projectId,\n                    rawPayloadMode: value\n                });\n            } catch (e) {\n                setError(e);\n            } finally {\n                setUpdating(false);\n            }\n        };\n\n        update();\n    }, [orgName, projectId, value]); // eslint-disable-line react-hooks/exhaustive-deps\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n            <Form loading={updating}>\n                <Form.Group inline={true}>\n                    <Form.Dropdown\n                        selection={true}\n                        value={value}\n                        onChange={(ev, data) => setValue(data.value as RawPayloadMode)}\n                        options={[\n                            { value: RawPayloadMode.DISABLED, text: 'Disabled', icon: 'lock' },\n                            { value: RawPayloadMode.OWNERS, text: 'Only owners' },\n                            { value: RawPayloadMode.TEAM_MEMBERS, text: 'Only team members' },\n                            {\n                                value: RawPayloadMode.ORG_MEMBERS,\n                                text: 'Only organization members'\n                            },\n                            { value: RawPayloadMode.EVERYONE, text: 'Everyone', icon: 'lock open' }\n                        ]}\n                    />\n\n                    <p>{getDescription(value)}</p>\n                </Form.Group>\n            </Form>\n        </>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/ProjectRenameActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport { ConcordId, ConcordKey } from '../../../api/common';\nimport { isProjectExists } from '../../../api/service/console';\nimport { projectAlreadyExistsError } from '../../../validation';\nimport { EntityRenameForm } from '../../molecules';\nimport { RequestErrorActivity } from '../index';\nimport { memo, useCallback, useState } from 'react';\nimport { rename as apiRename } from '../../../api/org/project';\nimport { useApi } from '../../../hooks/useApi';\nimport { FormValues } from '../../molecules/EntityRenameForm';\nimport { Navigate } from 'react-router';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    projectId?: ConcordId;\n    projectName: ConcordKey;\n    disabled: boolean;\n}\n\nconst areEqual = (prev: ExternalProps, next: ExternalProps): boolean => {\n    return (\n        prev.orgName === next.orgName &&\n        prev.projectId === next.projectId &&\n        prev.projectName === next.projectName &&\n        prev.disabled === next.disabled\n    );\n};\n\nconst ProjectRenameActivity = memo(\n    ({ orgName, projectId, projectName, disabled }: ExternalProps) => {\n        const [value, setValue] = useState(projectName);\n\n        const postData = useCallback(async () => {\n            await apiRename(orgName, projectId!, value!);\n            return value;\n        }, [orgName, projectId, value]);\n\n        const { data, error, isLoading, fetch, clearState } = useApi<string>(postData, {\n            fetchOnMount: false,\n            requestByFetch: true,\n        });\n\n        const renameHandler = useCallback(\n            (values: FormValues) => {\n                setValue(values.name);\n                clearState();\n                fetch();\n            },\n            [clearState, fetch]\n        );\n\n        if (data && data !== projectName) {\n            return <Navigate to={`/org/${orgName}/project/${value}/settings`} />;\n        }\n\n        return (\n            <>\n                {error && <RequestErrorActivity error={error} />}\n                <EntityRenameForm\n                    originalName={projectName}\n                    submitting={isLoading}\n                    onSubmit={renameHandler}\n                    inputPlaceholder=\"Project name\"\n                    confirmationHeader=\"Rename the project?\"\n                    confirmationContent=\"Are you sure you want to rename the project?\"\n                    isExists={(name) => isProjectExists(orgName, name)}\n                    alreadyExistsTemplate={projectAlreadyExistsError}\n                    disabled={disabled}\n                />\n            </>\n        );\n    },\n    areEqual\n);\n\nexport default ProjectRenameActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ProjectSearch/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Search } from 'semantic-ui-react';\n\nimport { ConcordKey, RequestError } from '../../../api/common';\nimport { useCallback, useEffect, useState } from 'react';\nimport { SearchProps } from 'semantic-ui-react/dist/commonjs/modules/Search/Search';\nimport { ProjectEntry, list as apiList, get as apiGet } from '../../../api/org/project';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    defaultProjectName?: ConcordKey;\n    placeholder?: string;\n\n    fluid?: boolean;\n    invalid?: boolean;\n    projectsToNotShow?: ProjectEntry[];\n\n    clearOnSelect?: boolean;\n\n    onReset?: (value?: ProjectEntry) => void;\n    onClear?: () => void;\n    onSelect?: (value: ProjectEntry) => void;\n}\n\ninterface Result {\n    title: string;\n    description: string;\n}\n\nconst renderTitle = (e: ProjectEntry) => `${e.name}`;\n\nconst renderDescription = (e: ProjectEntry): string => `${e.description ? e.description : ''}`;\n\nconst isEquals = (a?: ProjectEntry, b?: ProjectEntry): boolean => {\n    if (a === undefined && b === undefined) {\n        return true;\n    }\n    if (a === undefined || b === undefined) {\n        return false;\n    }\n    return a.id === b.id;\n};\n\nexport default ({\n    orgName,\n    defaultProjectName,\n    projectsToNotShow,\n    clearOnSelect,\n    placeholder,\n    fluid,\n    invalid,\n    onClear,\n    onReset,\n    onSelect\n}: ExternalProps) => {\n    const [defaultItem, setDefaultItem] = useState<ProjectEntry | undefined>();\n    const [value, setValue] = useState<string | undefined>();\n    const [loading, setLoading] = useState(false);\n    const [error, setError] = useState<RequestError>();\n    const [items, setItems] = useState<ProjectEntry[]>([]);\n    const [results, setResults] = useState<Result[]>([]);\n\n    // perform search whenever the filter changes\n    useEffect(() => {\n        if (!value || value.trim().length < 3) {\n            setResults([]);\n            return;\n        }\n\n        const fetchData = async () => {\n            setLoading(true);\n            try {\n                const result = await apiList(orgName, 0, 10, value);\n                let projects = result.items;\n                if (projectsToNotShow != null) {\n                    projects = projects.filter(\n                        (project) =>\n                            !projectsToNotShow.map((project) => project.id).includes(project.id)\n                    );\n                }\n                setItems(projects);\n            } catch (e) {\n                setError(e);\n            } finally {\n                setLoading(false);\n            }\n        };\n\n        fetchData();\n    }, [orgName, value, projectsToNotShow]);\n\n    // convert ProjectEntries into whatever <Search> accepts\n    useEffect(() => {\n        const r = items.map((i) => ({\n            key: i.id,\n            title: renderTitle(i),\n            description: renderDescription(i)\n        }));\n\n        setResults(r);\n    }, [items]);\n\n    // load the default project's data\n    useEffect(() => {\n        if (!defaultProjectName) {\n            setDefaultItem(undefined);\n            return;\n        }\n\n        const fetchData = async () => {\n            setLoading(true);\n            try {\n                const result = await apiGet(orgName, defaultProjectName);\n                setValue(result ? renderTitle(result) : '');\n                setDefaultItem(result);\n            } catch (e) {\n                setError(e);\n            } finally {\n                setLoading(false);\n            }\n        };\n\n        fetchData();\n    }, [orgName, defaultProjectName, projectsToNotShow]);\n\n    const onChangeCallBack = useCallback(\n        (event: React.MouseEvent<HTMLElement>, data: SearchProps) => {\n            setValue(data.value);\n        },\n        []\n    );\n\n    const handleItemSelected = useCallback(\n        (item?: ProjectEntry) => {\n            setValue(item ? renderTitle(item) : '');\n\n            const isDefault = isEquals(item, defaultItem);\n            if (isDefault) {\n                onReset?.(item);\n            } else if (item) {\n                onSelect?.(item);\n            } else {\n                onClear?.();\n            }\n        },\n        [onReset, onSelect, onClear, defaultItem]\n    );\n\n    return (\n        <Search\n            fluid={fluid}\n            input={{\n                placeholder,\n                error: !!error || invalid\n            }}\n            value={value}\n            loading={loading}\n            results={results}\n            onBlur={(event, data) => {\n                if (data.value !== '') {\n                    const item = items.find((i) => i.name === data.value);\n                    handleItemSelected(item || defaultItem);\n                } else {\n                    handleItemSelected(undefined);\n                }\n            }}\n            onSearchChange={onChangeCallBack}\n            onResultSelect={(ev, data) => {\n                const item = items.find((i) => i.id === data.result.key);\n                handleItemSelected(item || defaultItem);\n                if (clearOnSelect === true) {\n                    setValue('');\n                }\n            }}\n        />\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/ProjectSearchFormField/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Form, Icon, Label, LabelGroup } from 'semantic-ui-react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { FieldProps } from 'formik/dist/Field';\nimport { Field, getIn } from 'formik';\nimport { ProjectSearch } from '../index';\nimport { ProjectEntry } from '../../../api/org/project';\n\ninterface ExternalProps {\n    fieldName: string;\n    orgName: ConcordKey;\n    defaultProjectName?: string;\n    label?: string;\n    placeholder?: string;\n    onChange?: (projectName?: ConcordKey) => void;\n}\n\nexport default ({\n    fieldName,\n    orgName,\n    defaultProjectName,\n    label,\n    placeholder,\n    onChange\n}: ExternalProps) => {\n    return (\n        <Field name={fieldName}>\n            {({ form }: FieldProps) => {\n                const touched = getIn(form.touched, fieldName);\n                const error = getIn(form.errors, fieldName);\n                const invalid = !!(touched && error);\n                const [projects, setProjects] = React.useState<ProjectEntry[]>([]);\n\n                return (\n                    <Form.Field error={invalid}>\n                        {label && <label>{label}</label>}\n                        <ProjectSearch\n                            orgName={orgName}\n                            placeholder={placeholder}\n                            fluid={true}\n                            defaultProjectName={defaultProjectName}\n                            invalid={invalid}\n                            projectsToNotShow={projects}\n                            clearOnSelect={true}\n                            onSelect={(value) => {\n                                if (!projects.map((project) => project.id).includes(value.id)) {\n                                    let _projects = projects.concat(value);\n                                    form.setFieldValue(fieldName, _projects);\n                                    setProjects(_projects);\n                                    onChange?.(value.name);\n                                }\n                            }}\n                        />\n                        <br />\n                        <LabelGroup>\n                            {projects &&\n                                projects.map((project, index) => (\n                                    <Label key={index}>\n                                        {project.name}\n                                        <Icon\n                                            name=\"delete\"\n                                            onClick={() => {\n                                                const index = projects\n                                                    .map((p) => p.id)\n                                                    .indexOf(project.id);\n                                                projects.splice(index, 1);\n                                                setProjects(projects.slice(0));\n                                                form.setFieldValue(fieldName, projects);\n                                            }}\n                                        ></Icon>\n                                    </Label>\n                                ))}\n                        </LabelGroup>\n                        {invalid && error && (\n                            <Label basic={true} pointing={true} color=\"red\">\n                                {error}\n                            </Label>\n                        )}\n                    </Form.Field>\n                );\n            }}\n        </Field>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/ProjectTeamAccessActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport {useCallback, useState} from 'react';\nimport {Loader} from 'semantic-ui-react';\nimport {ConcordKey, GenericOperationResult} from '../../../api/common';\nimport {ResourceAccessEntry} from '../../../api/org';\nimport {getProjectAccess, updateProjectAccess} from '../../../api/org/project';\nimport {useApi} from '../../../hooks/useApi';\nimport {LoadingDispatch} from '../../../App';\nimport {RequestErrorMessage, TeamAccessList} from '../../molecules';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n}\n\nconst ProjectTeamAccessActivity = ({orgName, projectName}: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n    const [refresh, toggleRefresh] = useState<boolean>(false);\n\n    const fetchData = useCallback(() => {\n        return getProjectAccess(orgName, projectName);\n    }, [orgName, projectName]);\n\n    const {data, error} = useApi<ResourceAccessEntry[]>(fetchData, {\n        fetchOnMount: true,\n        dispatch: dispatch,\n        forceRequest: refresh\n    });\n\n    const [updateValue, setUpdateValue] = useState({\n        access: [] as ResourceAccessEntry[]\n    });\n\n    const postData = useCallback(async () => {\n        const result = await updateProjectAccess(orgName, projectName, updateValue.access);\n        toggleRefresh((prevState) => !prevState);\n        return result;\n    }, [orgName, projectName, updateValue]);\n\n    const update = useApi<GenericOperationResult>(postData, {\n        fetchOnMount: false,\n        dispatch: dispatch,\n        requestByFetch: true\n    });\n\n    const handleUpdate = useCallback(\n        (entries: ResourceAccessEntry[]) => {\n            setUpdateValue({access: entries});\n            update.fetch();\n        },\n        [update]\n    );\n\n    if (error) {\n        return <RequestErrorMessage error={error}/>;\n    }\n\n    if (update.error) {\n        return <RequestErrorMessage error={update.error}/>;\n    }\n\n    if (!data || update.isLoading) {\n        return <Loader active={true}/>;\n    }\n\n    const entries = data || [];\n\n    return (\n        <div>\n            <TeamAccessList\n                data={entries}\n                submitting={update.isLoading}\n                orgName={orgName}\n                submit={handleUpdate}\n            />\n        </div>\n    );\n};\n\nexport default ProjectTeamAccessActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ProtectedRoute/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { ReactNode, useContext } from 'react';\nimport { Navigate, Outlet, useLocation } from 'react-router';\nimport { Dimmer, Loader } from 'semantic-ui-react';\n\nimport { UserSessionContext } from '../../../session';\nimport { setQueryParam } from '../../../utils';\n\ninterface ProtectedRouteProps {\n    children?: ReactNode;\n}\n\nconst ProtectedRoute = ({ children }: ProtectedRouteProps) => {\n    const { loggingIn, userInfo } = useContext(UserSessionContext);\n    const location = useLocation();\n\n    if (loggingIn) {\n        return (\n            <Dimmer active={true} inverted={true} page={true}>\n                <Loader active={true} size=\"massive\" content={'Logging in'} />\n            </Dimmer>\n        );\n    }\n\n    const loggedIn = !!userInfo?.username;\n\n    if (!loggedIn) {\n        const loginUrl = window.concord?.loginUrl;\n        if (loginUrl) {\n            const requested = new URL(window.location.href).hash;\n            // delay the redirect to avoid layout issues\n            setTimeout(() => {\n                window.location.href = setQueryParam(loginUrl, 'from', '/' + requested);\n            }, 1000);\n\n            return (\n                <Dimmer active={true} inverted={true} page={true}>\n                    <Loader active={true} size=\"massive\" content={'Logging in'} />\n                </Dimmer>\n            );\n        } else {\n            return (\n                <Navigate\n                    to=\"/login\"\n                    replace={true}\n                    state={{\n                        from: location,\n                    }}\n                />\n            );\n        }\n    }\n\n    return <>{children || <Outlet />}</>;\n};\n\nexport default ProtectedRoute;\n"
  },
  {
    "path": "console2/src/components/organisms/PublicKeyPopup/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport copyToClipboard from 'copy-to-clipboard';\nimport * as React from 'react';\nimport { Button, Loader, Popup, TextArea } from 'semantic-ui-react';\n\nimport { ConcordKey, RequestError } from '../../../api/common';\nimport { getPublicKey as apiGetPublicKey } from '../../../api/org/secret';\nimport { RequestErrorMessage } from '../../molecules';\n\nimport './styles.css';\n\ninterface Props {\n    orgName: ConcordKey;\n    secretName: ConcordKey;\n}\n\ninterface State {\n    open: boolean;\n    loading: boolean;\n    publicKey?: string;\n    error: RequestError;\n}\n\nclass PublicKeyPopup extends React.Component<Props, State> {\n    constructor(props: Props) {\n        super(props);\n        this.state = { open: false, loading: false, error: null };\n    }\n\n    handleToggle() {\n        if (this.state.open) {\n            this.setState({ open: false, loading: false });\n            return;\n        }\n\n        const { orgName, secretName } = this.props;\n\n        this.setState({ open: true, loading: true });\n\n        // TODO replace with redux\n        apiGetPublicKey(orgName, secretName)\n            .then((r) => {\n                this.setState({\n                    loading: false,\n                    error: null,\n                    publicKey: r.publicKey\n                });\n            })\n            .catch((e) => {\n                this.setState({\n                    loading: false,\n                    error: e,\n                    publicKey: undefined\n                });\n            });\n    }\n\n    renderContent() {\n        const { error, publicKey } = this.state;\n\n        if (error) {\n            return <RequestErrorMessage error={error} />;\n        }\n\n        if (!publicKey) {\n            return;\n        }\n\n        return (\n            <>\n                <Button\n                    size=\"mini\"\n                    icon=\"copy\"\n                    content=\"Copy\"\n                    onClick={() => (copyToClipboard as any)(publicKey)}\n                />\n                <TextArea className=\"publicKeyData\" value={publicKey} />\n            </>\n        );\n    }\n\n    render() {\n        return (\n            <Popup\n                className=\"publicKeyPopup\"\n                hideOnScroll={true}\n                open={this.state.open}\n                trigger={\n                    <Button\n                        icon=\"unlock\"\n                        content=\"Public Key\"\n                        onClick={() => this.handleToggle()}\n                    />\n                }>\n                <Popup.Header>Public Key</Popup.Header>\n                <Popup.Content>\n                    <Loader active={this.state.loading} />\n                    {this.renderContent()}\n                </Popup.Content>\n            </Popup>\n        );\n    }\n}\n\nexport default PublicKeyPopup;\n"
  },
  {
    "path": "console2/src/components/organisms/PublicKeyPopup/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n.publicKeyData {\n  width: 100%;\n  min-width: 500px !important;\n  font-family: monospace;\n  margin-top: 5px;\n}\n"
  },
  {
    "path": "console2/src/components/organisms/RedirectButton/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useHistory } from '@/router';\nimport { Button } from 'semantic-ui-react';\nimport { ButtonProps } from 'semantic-ui-react/dist/commonjs/elements/Button/Button';\n\ninterface ExternalProps extends ButtonProps {\n    location: string;\n}\n\nconst RedirectButton = ({ location, ...rest }: ExternalProps) => {\n    const history = useHistory();\n\n    return <Button {...rest} onClick={() => history.push(location)} />;\n};\n\nexport default RedirectButton;\n"
  },
  {
    "path": "console2/src/components/organisms/RefreshRepositoryPopup/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport {ConcordKey, GenericOperationResult} from '../../../api/common';\nimport { SingleOperationPopup } from '../../molecules';\nimport {useCallback, useState} from \"react\";\nimport {refreshRepository as apiRefreshRepo} from \"../../../api/org/project/repository\";\nimport {useApi} from \"../../../hooks/useApi\";\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    repoName: ConcordKey;\n    trigger: (onClick: () => void) => React.ReactNode;\n    onDone?: () => void;\n}\n\nconst RefreshRepositoryPopup = (props: ExternalProps) => {\n    const {orgName, projectName, repoName, trigger, onDone} = props;\n    const [forceRequest, toggleForceRequest] = useState<boolean>(false);\n\n    const refreshRepo = useCallback(async () => {\n        return await apiRefreshRepo(orgName, projectName, repoName, true);\n    }, [orgName, projectName, repoName]);\n\n    const { data, error, clearState, isLoading } = useApi<GenericOperationResult>(refreshRepo, {\n        fetchOnMount: false,\n        forceRequest\n    });\n\n    const confirmHandler = useCallback(() => {\n        toggleForceRequest((prevState) => !prevState);\n    }, []);\n\n    const resetHandler = useCallback(() => {\n        clearState();\n    }, [clearState]);\n\n    return (\n        <SingleOperationPopup\n            trigger={trigger}\n            title=\"Refresh repository?\"\n            introMsg={\n                <p>\n                    Refreshing the repository will update the Concord's cache and reload the\n                    project's trigger definitions.\n                </p>\n            }\n            running={isLoading}\n            success={data !== undefined}\n            successMsg={<p>The repository was refreshed successfully.</p>}\n            error={error}\n            onConfirm={confirmHandler}\n            onDone={onDone}\n            reset={resetHandler}\n        />\n    );\n};\n\nexport default RefreshRepositoryPopup;"
  },
  {
    "path": "console2/src/components/organisms/RepositoryActionDropdown/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useEffect, useState } from 'react';\nimport { Dropdown, Icon, Table } from 'semantic-ui-react';\n\nimport { ConcordKey, RequestError } from '../../../api/common';\nimport {\n    listTriggersV2 as apiListTriggers,\n    RepositoryEntry,\n    TriggerEntry\n} from '../../../api/org/project/repository';\nimport {\n    DeleteRepositoryPopup,\n    RefreshRepositoryPopup,\n    StartRepositoryPopup,\n    ValidateRepositoryPopup\n} from '../../organisms';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    repo: RepositoryEntry;\n    triggerData: TriggerEntry[];\n    refresh: () => void;\n}\n\nconst renderManualTrigger = ({\n    trigger,\n    orgName,\n    projectName,\n    repoName,\n    repoURL,\n    repoBranchOrCommitId,\n    repoPathOrDefault,\n    repoDisabled\n}: {\n    trigger: TriggerEntry;\n    orgName: string;\n    projectName: string;\n    repoName: string;\n    repoURL: string;\n    repoBranchOrCommitId: string;\n    repoPathOrDefault: string;\n    repoDisabled: boolean;\n}) => {\n    return (\n        <StartRepositoryPopup\n            orgName={orgName}\n            projectName={projectName}\n            repoName={repoName}\n            repoURL={repoURL}\n            repoBranchOrCommitId={repoBranchOrCommitId!}\n            repoPath={repoPathOrDefault}\n            allowEntryPoint={false}\n            entryPoint={trigger.cfg.entryPoint}\n            allowProfile={false}\n            profiles={trigger.activeProfiles}\n            showArgs={trigger.arguments !== null}\n            args={trigger.arguments}\n            title={`Start '${trigger.cfg.name}' from repository '${repoName}'`}\n            trigger={(onClick: any) => (\n                <Dropdown.Item onClick={onClick} disabled={repoDisabled} key={trigger.id}>\n                    <Icon name=\"play\" color=\"green\" />\n                    <span className=\"text\">{trigger.cfg.name}</span>\n                </Dropdown.Item>\n            )}\n        />\n    );\n};\n\nconst RepositoryActionDropdown = (props: ExternalProps) => {\n    const [loading, setLoading] = useState(false);\n    const [error, setError] = useState<RequestError>();\n\n    const { orgName, projectName, repo, refresh } = props;\n\n    const {\n        name: repoName,\n        url: repoURL,\n        commitId: repoCommitId,\n        branch: repoBranch,\n        path: repoPath,\n        disabled: repoDisabled\n    } = repo;\n\n    // show the commit ID if defined, otherwise show the branch name\n    const repoBranchOrCommitId = repoCommitId ? repoCommitId : repoBranch;\n    const repoPathOrDefault = repoPath ? repoPath : '/';\n\n    return (\n        <>\n            <Table.Cell style={{ paddingRight: '0px' }}>\n                {props.triggerData?.length > 0 ? (\n                    <Dropdown\n                        icon=\"play green\"\n                        pointing={'top right'}\n                        style={{ paddingLeft: '25%' }}\n                        loading={loading}\n                        error={error != null}>\n                        <Dropdown.Menu>\n                            <StartRepositoryPopup\n                                orgName={orgName}\n                                projectName={projectName}\n                                repoName={repoName}\n                                repoURL={repoURL}\n                                repoBranchOrCommitId={repoBranchOrCommitId!}\n                                repoPath={repoPathOrDefault}\n                                allowEntryPoint={true}\n                                allowProfile={true}\n                                trigger={(onClick: any) => (\n                                    <Dropdown.Item\n                                        onClick={onClick}\n                                        disabled={repoDisabled}\n                                        key={'run'}\n                                        data-testid={`repository-run-button-${repoName}`}>\n                                        <Icon name=\"play\" color=\"blue\" />\n                                        <span className=\"text\">Run</span>\n                                    </Dropdown.Item>\n                                )}\n                            />\n                            <Dropdown.Divider />\n                            {props.triggerData.map((t) =>\n                                renderManualTrigger({\n                                    trigger: t,\n                                    orgName,\n                                    projectName,\n                                    repoName,\n                                    repoURL,\n                                    repoBranchOrCommitId: repoBranchOrCommitId!,\n                                    repoPathOrDefault,\n                                    repoDisabled: repo.disabled\n                                })\n                            )}\n                        </Dropdown.Menu>\n                    </Dropdown>\n                ) : (\n                    <StartRepositoryPopup\n                        orgName={orgName}\n                        projectName={projectName}\n                        repoName={repoName}\n                        repoURL={repoURL}\n                        repoBranchOrCommitId={repoBranchOrCommitId!}\n                        repoPath={repoPathOrDefault}\n                        allowEntryPoint={true}\n                        allowProfile={true}\n                        trigger={(onClick: any) => (\n                            <button\n                                type=\"button\"\n                                onClick={onClick}\n                                className=\"ui medium compact button\"\n                                style={{\n                                    backgroundColor: 'rgba(255, 255, 255, 0)',\n                                    paddingLeft: '25%'\n                                }}\n                                disabled={repoDisabled}\n                                data-testid={`repository-run-button-${repoName}`}\n                            >\n                                <div data-toggle=\"tooltip\" data-placement=\"bottom\" title=\"Run\">\n                                    <Icon name=\"play\" color=\"blue\" />\n                                </div>\n                            </button>\n                        )}\n                    />\n                )}\n            </Table.Cell>\n            <Table.Cell style={{ paddingLeft: '0px' }}>\n                <Dropdown\n                    icon=\"bars\"\n                    pointing={'top right'}\n                    style={{ paddingTop: '25%', paddingBottom: '25%' }}\n                    loading={loading}\n                    error={error != null}>\n                    <Dropdown.Menu>\n                        <ValidateRepositoryPopup\n                            orgName={orgName}\n                            projectName={projectName}\n                            repoName={repoName}\n                            trigger={(onClick: any) => (\n                                <Dropdown.Item onClick={onClick}>\n                                    <Icon name=\"check\" />\n                                    <span className=\"text\">Validate</span>\n                                </Dropdown.Item>\n                            )}\n                        />\n\n                        <RefreshRepositoryPopup\n                            orgName={orgName}\n                            projectName={projectName}\n                            repoName={repoName}\n                            trigger={(onClick: any) => (\n                                <Dropdown.Item onClick={onClick}>\n                                    <Icon name=\"refresh\" />\n                                    <span className=\"text\">Refresh</span>\n                                </Dropdown.Item>\n                            )}\n                        />\n\n                        <DeleteRepositoryPopup\n                            orgName={orgName}\n                            projectName={projectName}\n                            repoName={repoName}\n                            trigger={(onClick: any) => (\n                                <Dropdown.Item onClick={onClick}>\n                                    <Icon name=\"delete\" color=\"red\" />\n                                    <span className=\"text\">Delete</span>\n                                </Dropdown.Item>\n                            )}\n                            onDone={refresh}\n                        />\n                    </Dropdown.Menu>\n                </Dropdown>\n            </Table.Cell>\n        </>\n    );\n};\n\nexport default RepositoryActionDropdown;\n"
  },
  {
    "path": "console2/src/components/organisms/RequestErrorActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Navigate } from 'react-router';\n\nimport { RequestError } from '../../../api/common';\nimport { RequestErrorMessage } from '../../molecules';\nimport { useLocation } from 'react-router';\nimport { Dimmer, Loader } from 'semantic-ui-react';\nimport { setQueryParam } from '../../../utils';\n\ninterface Props {\n    error: RequestError;\n}\n\nexport default ({ error }: Props) => {\n    const location = useLocation();\n\n    if (error && error.status === 401) {\n        const loginUrl = window.concord?.loginUrl;\n        if (loginUrl) {\n            const requested = new URL(window.location.href).hash;\n            // delay the redirect to avoid layout issues\n            setTimeout(() => {\n                window.location.href = setQueryParam(loginUrl, 'from', '/' + requested);\n            }, 1000);\n\n            return (\n                <Dimmer active={true} inverted={true} page={true}>\n                    <Loader active={true} size=\"massive\" content={'Logging in'} />\n                </Dimmer>\n            );\n        } else {\n            return (\n                <Navigate\n                    to=\"/login\"\n                    replace={true}\n                    state={{\n                        from: location,\n                    }}\n                />\n            );\n        }\n    }\n\n    return <RequestErrorMessage error={error} />;\n};\n"
  },
  {
    "path": "console2/src/components/organisms/RestartProcessPopup/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport { ConcordId, RequestError } from '../../../api/common';\nimport { SingleOperationPopup } from '../../molecules';\nimport { memo, useCallback } from 'react';\nimport { restart as apiRestart } from '../../../api/process';\nimport { useState } from 'react';\nimport { Message } from \"semantic-ui-react\";\n\ninterface ExternalProps {\n    instanceId: ConcordId;\n    rootInstanceId?: ConcordId;\n    refresh: () => void;\n    trigger: (onClick: () => void) => React.ReactNode;\n}\n\nconst RestartProcessPopup = memo((props: ExternalProps) => {\n    const [restarting, setRestarting] = useState(false);\n    const [error, setError] = useState<RequestError>();\n    const [success, setSuccess] = useState(false);\n\n    const instanceId = props.instanceId;\n    const rootInstanceId = props.rootInstanceId;\n\n    const restartProcess = useCallback(async () => {\n        setRestarting(true);\n\n        try {\n            await apiRestart(instanceId);\n            setSuccess(true);\n        } catch (e) {\n            setError(e);\n        } finally {\n            setRestarting(false);\n        }\n    }, [instanceId]);\n\n    const reset = useCallback(() => {\n        setRestarting(false);\n        setSuccess(false);\n        setError(undefined);\n    }, []);\n\n    const { trigger, refresh } = props;\n\n    return (\n        <SingleOperationPopup\n            trigger={trigger}\n            title=\"Restart the process?\"\n            introMsg={\n                <>\n                    {!rootInstanceId && <p>Are you sure you want to restart the selected process?</p>}\n                    {rootInstanceId &&\n                        <>\n                        <Message warning>\n                            <Message.Header>You are about to restart a child process. Please note:</Message.Header>\n                            <Message.List style={{marginTop: \"10px\"}}>\n                                <Message.Item>\n                                    Only the <a href={`#/process/${rootInstanceId}/log`} target=\"_blank\"\n                                                   rel=\"noopener noreferrer\">parent (root) process</a> will be restarted.\n                                </Message.Item>\n                                <Message.Item>\n                                    Restarting the parent process may also re-run this and other child processes.\n                                </Message.Item>\n                                <Message.Item>\n                                    Ensure that this is the desired action before proceeding.\n                                </Message.Item>\n                            </Message.List>\n                        </Message>\n\n                        <p>Do you want to continue with restarting the parent process?</p>\n                        </>\n                    }\n                </>\n            }\n            running={restarting}\n            runningMsg={<p>Restarting...</p>}\n            success={success}\n            successMsg={<p>The restart command was sent successfully.</p>}\n            error={error}\n            reset={reset}\n            onDone={refresh}\n            onConfirm={restartProcess}\n        />\n    );\n});\n\nexport default RestartProcessPopup;\n"
  },
  {
    "path": "console2/src/components/organisms/SecretActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Link, Navigate, Route, Routes } from 'react-router';\nimport { Divider, Grid, Header, Icon, Loader, Menu, Segment, Table } from 'semantic-ui-react';\nimport { parseISO as parseDate } from 'date-fns';\n\nimport { ConcordKey, Owner, RequestError } from '../../../api/common';\nimport {\n    SecretEncryptedByType,\n    SecretEntry,\n    SecretType,\n    SecretVisibility,\n    typeToText,\n} from '../../../api/org/secret';\nimport { get as apiGetSecret } from '../../../api/org/secret';\nimport { useApi } from '../../../hooks/useApi';\nimport {\n    HumanizedDuration,\n    LocalTimestamp,\n    RequestErrorMessage,\n    WithCopyToClipboard,\n} from '../../molecules';\nimport {\n    AuditLogActivity,\n    PublicKeyPopup,\n    SecretDeleteActivity,\n    SecretOrganizationChangeActivity,\n    SecretOwnerChangeActivity,\n    SecretProjectActivity,\n    SecretRenameActivity,\n    SecretTeamAccessActivity,\n    SecretVisibilityActivity,\n} from '../../organisms';\nimport { NotFoundPage } from '../../pages';\n\nexport type TabLink = 'info' | 'settings' | 'access' | 'audit' | null;\n\ninterface ExternalProps {\n    activeTab: TabLink;\n    orgName: ConcordKey;\n    secretName: ConcordKey;\n}\n\nconst visibilityToText = (v: SecretVisibility) =>\n    v === SecretVisibility.PUBLIC ? 'Public' : 'Private';\n\nconst encryptedByToText = (t: SecretEncryptedByType) =>\n    t === SecretEncryptedByType.SERVER_KEY ? 'Server key' : 'Password';\n\nconst renderUser = (e: Owner) => {\n    if (!e.userDomain) {\n        return e.username;\n    }\n\n    return `${e.username}@${e.userDomain}`;\n};\n\nconst SecretActivity = ({ activeTab, orgName, secretName }: ExternalProps) => {\n    const [refreshSecret, toggleRefresh] = React.useState(false);\n    const fetchData = React.useCallback(\n        () => apiGetSecret(orgName, secretName),\n        [orgName, secretName]\n    );\n    const { data, error, isLoading } = useApi<SecretEntry>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: refreshSecret,\n    });\n\n    const reloadSecret = React.useCallback(() => {\n        toggleRefresh((prevState) => !prevState);\n    }, [toggleRefresh]);\n\n    const renderPublicKey = (entry: SecretEntry) => (\n        <PublicKeyPopup orgName={entry.orgName} secretName={entry.name} />\n    );\n\n    const renderInfo = (entry: SecretEntry) => (\n        <Grid columns={2}>\n            <Grid.Column>\n                <Table definition={true}>\n                    <Table.Body>\n                        <Table.Row>\n                            <Table.Cell>Name</Table.Cell>\n                            <Table.Cell>{entry.name}</Table.Cell>\n                        </Table.Row>\n                        <Table.Row>\n                            <Table.Cell>Type</Table.Cell>\n                            <Table.Cell>{typeToText(entry.type)}</Table.Cell>\n                        </Table.Row>\n                        <Table.Row>\n                            <Table.Cell>Visibility</Table.Cell>\n                            <Table.Cell>{visibilityToText(entry.visibility)}</Table.Cell>\n                        </Table.Row>\n                        <Table.Row>\n                            <Table.Cell>Owner</Table.Cell>\n                            <Table.Cell>{entry.owner ? renderUser(entry.owner) : '-'}</Table.Cell>\n                        </Table.Row>\n                        <Table.Row>\n                            <Table.Cell>Actions</Table.Cell>\n                            <Table.Cell>\n                                {entry.type === SecretType.KEY_PAIR &&\n                                    entry.encryptedBy === SecretEncryptedByType.SERVER_KEY &&\n                                    renderPublicKey(entry)}\n                            </Table.Cell>\n                        </Table.Row>\n                    </Table.Body>\n                </Table>\n            </Grid.Column>\n            <Grid.Column>\n                <Table definition={true}>\n                    <Table.Body>\n                        <Table.Row>\n                            <Table.Cell>Protected by</Table.Cell>\n                            <Table.Cell>{encryptedByToText(entry.encryptedBy)}</Table.Cell>\n                        </Table.Row>\n                        <Table.Row>\n                            <Table.Cell>Restricted to a project</Table.Cell>\n                            <Table.Cell>\n                                {entry.projects && entry.projects.length > 0\n                                    ? entry.projects.map((project, index) => (\n                                          <span key={index}>\n                                              <Link\n                                                  to={`/org/${entry.orgName}/project/${project.name}`}\n                                              >\n                                                  {project.name}\n                                              </Link>\n                                              <span>\n                                                  {index !== entry.projects.length - 1 ? ', ' : ''}\n                                              </span>\n                                          </span>\n                                      ))\n                                    : ' - '}\n                            </Table.Cell>\n                        </Table.Row>\n                        <Table.Row>\n                            <Table.Cell>Age</Table.Cell>\n                            <Table.Cell>\n                                <HumanizedDuration\n                                    value={Date.now() - parseDate(entry.createdAt).getTime()}\n                                >\n                                    <div>\n                                        created at:<div>{entry.createdAt}</div>\n                                    </div>\n                                </HumanizedDuration>\n                            </Table.Cell>\n                        </Table.Row>\n                        <Table.Row>\n                            <Table.Cell>Last updated at</Table.Cell>\n                            <Table.Cell>\n                                {entry.lastUpdatedAt ? (\n                                    <LocalTimestamp value={entry.lastUpdatedAt} />\n                                ) : (\n                                    '-'\n                                )}\n                            </Table.Cell>\n                        </Table.Row>\n                    </Table.Body>\n                </Table>\n            </Grid.Column>\n        </Grid>\n    );\n\n    const renderSettings = (entry: SecretEntry) => {\n        const disabled = !entry;\n\n        return (\n            <>\n                <Header as=\"h5\" disabled={true} data-testid=\"secret-settings-id\">\n                    <WithCopyToClipboard value={entry.id}>ID: {entry.id}</WithCopyToClipboard>\n                </Header>\n\n                <Segment>\n                    <Header as=\"h4\">Visibility</Header>\n                    <SecretVisibilityActivity\n                        orgName={entry.orgName}\n                        secretId={entry.id}\n                        secretName={entry.name}\n                        visibility={entry.visibility}\n                        onUpdated={reloadSecret}\n                    />\n                </Segment>\n\n                <Divider horizontal={true} content=\"Danger Zone\" />\n\n                <Segment color=\"red\">\n                    <Header as=\"h4\">Projects</Header>\n                    <SecretProjectActivity\n                        orgName={entry.orgName}\n                        secretName={entry.name}\n                        projects={entry.projects}\n                        onUpdated={reloadSecret}\n                    />\n\n                    <Header as=\"h4\">Secret name</Header>\n                    <SecretRenameActivity orgName={entry.orgName} secretName={entry.name} />\n\n                    <Header as=\"h4\">Secret owner</Header>\n                    <SecretOwnerChangeActivity\n                        orgName={entry.orgName}\n                        secretName={entry.name}\n                        initialOwnerId={entry?.owner?.id}\n                        disabled={disabled}\n                    />\n\n                    <Header as=\"h4\">Organization</Header>\n                    <SecretOrganizationChangeActivity\n                        orgName={entry.orgName}\n                        secretName={entry.name}\n                    />\n\n                    <Header as=\"h4\">Delete Secret</Header>\n                    <SecretDeleteActivity orgName={entry.orgName} secretName={entry.name} />\n                </Segment>\n            </>\n        );\n    };\n\n    const renderTeamAccess = (entry: SecretEntry) => (\n        <SecretTeamAccessActivity\n            orgName={entry.orgName}\n            secretName={entry.name}\n            onUpdated={reloadSecret}\n        />\n    );\n\n    const renderAuditLog = (entry: SecretEntry) => (\n        <AuditLogActivity\n            showRefreshButton={false}\n            filter={{ details: { orgName: entry.orgName, secretName: entry.name } }}\n        />\n    );\n\n    if (error) {\n        return <RequestErrorMessage error={error as RequestError} />;\n    }\n\n    if (isLoading || !data) {\n        return <Loader active={true} />;\n    }\n\n    const baseUrl = `/org/${orgName}/secret/${secretName}`;\n\n    return (\n        <>\n            <Menu tabular={true} style={{ marginTop: 0 }}>\n                <Menu.Item active={activeTab === 'info'} data-testid=\"secret-tab-info\">\n                    <Icon name=\"file\" />\n                    <Link to={`${baseUrl}/info`}>Info</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'access'} data-testid=\"secret-tab-access\">\n                    <Icon name=\"key\" />\n                    <Link to={`${baseUrl}/access`}>Access</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'settings'} data-testid=\"secret-tab-settings\">\n                    <Icon name=\"setting\" />\n                    <Link to={`${baseUrl}/settings`}>Settings</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'audit'} data-testid=\"secret-tab-audit\">\n                    <Icon name=\"history\" />\n                    <Link to={`${baseUrl}/audit`}>Audit Log</Link>\n                </Menu.Item>\n            </Menu>\n\n            <Routes>\n                <Route index={true} element={<Navigate to=\"info\" replace={true} />} />\n                <Route path=\"info\" element={renderInfo(data)} />\n                <Route path=\"settings\" element={renderSettings(data)} />\n                <Route path=\"access\" element={renderTeamAccess(data)} />\n                <Route path=\"audit\" element={renderAuditLog(data)} />\n                <Route path=\"*\" element={<NotFoundPage />} />\n            </Routes>\n        </>\n    );\n};\n\nexport default SecretActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/SecretDeleteActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useHistory } from '@/router';\n\nimport { ConcordKey, RequestError } from '../../../api/common';\nimport { deleteSecret as apiDeleteSecret } from '../../../api/org/secret';\nimport { ButtonWithConfirmation, RequestErrorMessage } from '../../molecules';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    secretName: ConcordKey;\n}\n\nconst SecretDeleteActivity = ({ orgName, secretName }: ExternalProps) => {\n    const history = useHistory();\n    const [error, setError] = React.useState<RequestError>();\n    const [deleting, setDeleting] = React.useState(false);\n\n    const deleteSecret = React.useCallback(async () => {\n        setDeleting(true);\n        setError(undefined);\n\n        try {\n            await apiDeleteSecret(orgName, secretName);\n            history.push(`/org/${orgName}/secret`);\n        } catch (e) {\n            setError(e);\n        } finally {\n            setDeleting(false);\n        }\n    }, [history, orgName, secretName]);\n\n    return (\n        <>\n            {error && <RequestErrorMessage error={error} />}\n            <ButtonWithConfirmation\n                primary={true}\n                negative={true}\n                content=\"Delete\"\n                loading={deleting}\n                confirmationHeader=\"Delete the secret?\"\n                confirmationContent=\"Are you sure you want to delete the secret?\"\n                onConfirm={deleteSecret}\n            />\n        </>\n    );\n};\n\nexport default SecretDeleteActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/SecretListActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { Icon, Input, Menu, Table } from 'semantic-ui-react';\n\nimport { ConcordKey, EntityType } from '../../../api/common';\nimport { checkResult as apiCheckResult } from '../../../api/org';\nimport { CreateNewEntityButton, PaginationToolBar } from '../../molecules';\nimport { Link } from 'react-router';\nimport {\n    PaginatedSecretEntries,\n    SecretEntry,\n    SecretVisibility,\n    typeToText\n} from '../../../api/org/secret';\nimport { usePagination } from '../../molecules/PaginationToolBar/usePagination';\nimport { LoadingDispatch } from '../../../App';\nimport { useApi } from '../../../hooks/useApi';\nimport { list as getPaginatedSecretList } from '../../../api/org/secret';\nimport { RequestErrorActivity } from '../index';\n\ninterface Props {\n    orgName: ConcordKey;\n    forceRefresh: any;\n}\n\nconst SecretListActivity = ({ orgName, forceRefresh }: Props) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const [canCreate, setCanCreate] = useState<boolean>(false);\n    const {\n        paginationFilter,\n        handleLimitChange,\n        handleNext,\n        handlePrev,\n        handleFirst,\n        resetOffset\n    } = usePagination();\n    const oldFilter = useRef<string>();\n    const [filter, setFilter] = useState<string>();\n\n    const fetchData = useCallback(() => {\n        if (filter && oldFilter.current !== filter) {\n            oldFilter.current = filter;\n            resetOffset(0);\n        }\n\n        return getPaginatedSecretList(\n            orgName,\n            paginationFilter.offset,\n            paginationFilter.limit,\n            filter\n        );\n    }, [orgName, filter, paginationFilter.offset, paginationFilter.limit, resetOffset]);\n\n    const { data, error } = useApi<PaginatedSecretEntries>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    const fetchCanCreateStatus = useCallback(async () => {\n        try {\n            const response = await apiCheckResult(EntityType.SECRET, orgName);\n            setCanCreate(!!response);\n        } catch (e) {\n            // ignore\n        }\n    }, [orgName]);\n\n    useEffect(() => {\n        fetchCanCreateStatus();\n    }, [fetchCanCreateStatus]);\n\n    return (\n        <>\n            <Menu secondary={true}>\n                <Menu.Item>\n                    <Input\n                        icon=\"search\"\n                        placeholder=\"Filter...\"\n                        onChange={(ev, data) => setFilter(data.value)}\n                    />\n                </Menu.Item>\n\n                <Menu.Item position={'right'}>\n                    <CreateNewEntityButton\n                        entity=\"secret\"\n                        orgName={orgName}\n                        userInOrg={true}\n                        enabledInPolicy={canCreate}\n                    />\n                </Menu.Item>\n\n                <Menu.Item style={{ padding: 0 }}>\n                    <PaginationToolBar\n                        limit={paginationFilter.limit}\n                        handleLimitChange={(limit) => handleLimitChange(limit)}\n                        handleNext={handleNext}\n                        handlePrev={handlePrev}\n                        handleFirst={handleFirst}\n                        disablePrevious={paginationFilter.offset <= 0}\n                        disableNext={!data?.next}\n                        disableFirst={paginationFilter.offset <= 0}\n                    />\n                </Menu.Item>\n            </Menu>\n\n            {error && <RequestErrorActivity error={error} />}\n\n            <Table celled={true} compact={true}>\n                <Table.Header>\n                    <Table.Row>\n                        <Table.HeaderCell collapsing={true} />\n                        <Table.HeaderCell>Name</Table.HeaderCell>\n                        <Table.HeaderCell collapsing={true}>Type</Table.HeaderCell>\n                        <Table.HeaderCell>Projects</Table.HeaderCell>\n                    </Table.Row>\n                </Table.Header>\n                <Table.Body>\n                    {data?.items.length === 0 && (\n                        <tr style={{ fontWeight: 'bold' }}>\n                            <Table.Cell> </Table.Cell>\n                            <Table.Cell colSpan={4}>No data available</Table.Cell>\n                        </tr>\n                    )}\n                    {data?.items.map((secret, index) => (\n                        <Table.Row key={index}>\n                            <Table.Cell singleLine={true}>\n                                <SecretVisibilityIcon secret={secret} />\n                            </Table.Cell>\n                            <Table.Cell singleLine={true}>\n                                <Link to={`/org/${orgName}/secret/${secret.name}`}>\n                                    {secret.name}\n                                </Link>\n                            </Table.Cell>\n                            <Table.Cell singleLine={true}>{typeToText(secret.type)}</Table.Cell>\n                            <Table.Cell singleLine={true}>\n                                {secret.projects && secret.projects.length > 0\n                                    ? secret.projects.map((project, index) => (\n                                          <span key={index}>\n                                              <Link\n                                                  to={`/org/${secret.orgName}/project/${project.name}`}\n                                              >\n                                                  {project.name}\n                                              </Link>\n                                              <span>\n                                                  {index !== secret.projects.length - 1 ? ', ' : ''}\n                                              </span>\n                                          </span>\n                                      ))\n                                    : ' - '}\n                            </Table.Cell>\n                        </Table.Row>\n                    ))}\n                </Table.Body>\n            </Table>\n        </>\n    );\n};\n\nconst SecretVisibilityIcon = ({ secret }: { secret: SecretEntry }) => {\n    if (secret.visibility === SecretVisibility.PUBLIC) {\n        return <Icon name=\"unlock\" />;\n    } else {\n        return <Icon name=\"lock\" color=\"red\" />;\n    }\n};\n\nexport default SecretListActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/SecretOrganizationChangeActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useState } from 'react';\n\nimport { changeOrganization as apiChangeOrganization } from '../../../api/org/secret';\n\nimport { RequestError } from '../../../api/common';\nimport { SingleOperationPopup } from '../../molecules';\nimport { Form, Input } from 'semantic-ui-react';\nimport { FindOrganizationsField, RequestErrorActivity } from '../index';\nimport { Navigate } from 'react-router';\n\ninterface Props {\n    orgName: string;\n    secretName: string;\n}\n\nexport default ({ orgName, secretName }: Props) => {\n    const [dirty, setDirty] = useState<boolean>(false);\n    const [orgNameValue, setOrgNameValue] = useState<string>(orgName);\n    const [confirmation, setConfirmation] = useState('');\n    const [error, setError] = useState<RequestError>();\n    const [changing, setChanging] = useState<boolean>(false);\n    const [success, setSuccess] = useState<boolean>(false);\n    const [redirect, setRedirect] = useState<boolean>(false);\n\n    const confirmHandler = useCallback(async () => {\n        if (!orgNameValue) {\n            return;\n        }\n\n        setChanging(true);\n        try {\n            const result = await apiChangeOrganization(orgName, secretName, orgNameValue);\n            setSuccess(result.ok);\n        } catch (e) {\n            setError(e);\n        } finally {\n            setChanging(false);\n        }\n    }, [orgName, secretName, orgNameValue]);\n\n    const redirectHandler = useCallback(() => {\n        setRedirect((prevState) => !prevState);\n    }, []);\n\n    const resetHandler = useCallback(() => {\n        setConfirmation('');\n    }, []);\n\n    if (redirect) {\n        return <Navigate to={`/org/${orgNameValue}/secret/${secretName}`} />;\n    }\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n\n            <Form loading={changing}>\n                <Form.Group widths={3}>\n                    <Form.Field>\n                        <FindOrganizationsField\n                            placeholder=\"Search for an organization...\"\n                            defaultOrgName={orgName}\n                            required={true}\n                            onReset={() => {\n                                setDirty(false);\n                                setOrgNameValue(orgName);\n                            }}\n                            onSelect={(value) => {\n                                setDirty(true);\n                                setOrgNameValue(value.name);\n                            }}\n                        />\n                    </Form.Field>\n\n                    <SingleOperationPopup\n                        trigger={(onClick) => (\n                            <Form.Button\n                                primary={true}\n                                negative={true}\n                                content=\"Move\"\n                                disabled={!dirty}\n                                onClick={onClick}\n                            />\n                        )}\n                        title=\"Move secret?\"\n                        introMsg={\n                            <>\n                                <p>\n                                    Are you sure you want to move the secret to{' '}\n                                    <strong>{orgNameValue}</strong> organization?\n                                </p>\n                                <ul>\n                                    <li>\n                                        Any repositories using this secret in this organization, the\n                                        mapping will be removed{' '}\n                                    </li>\n                                    <li>\n                                        If this secret is scoped to a project, the mapping will be\n                                        removed.{' '}\n                                    </li>\n                                </ul>\n                                <p>\n                                    Please type <strong>{secretName}</strong> to confirm.\n                                </p>\n                                <div\n                                    className={`ui input ${\n                                        confirmation !== secretName ? 'error' : ''\n                                    }`}\n                                >\n                                    <Input\n                                        type=\"text\"\n                                        name=\"name\"\n                                        placeholder=\"Secret name\"\n                                        value={confirmation}\n                                        onChange={(e, data) => setConfirmation(data.value)}\n                                    />\n                                </div>\n                            </>\n                        }\n                        running={changing}\n                        runningMsg={\n                            <p>\n                                Moving the secret <strong>{secretName}</strong> to{' '}\n                                <strong>{orgNameValue}</strong> organization...\n                            </p>\n                        }\n                        success={success}\n                        successMsg={\n                            <p>\n                                The secret <strong>{secretName}</strong> was moved successfully to{' '}\n                                <strong>{orgNameValue}</strong> organization.\n                            </p>\n                        }\n                        error={error}\n                        reset={resetHandler}\n                        onConfirm={confirmHandler}\n                        onDone={redirectHandler}\n                        disableYes={confirmation !== secretName}\n                    />\n                </Form.Group>\n            </Form>\n        </>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/SecretOwnerChangeActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport { ConcordId, ConcordKey, GenericOperationResult } from '../../../api/common';\nimport EntityOwnerChangeForm from '../../molecules/EntityOwnerChangeForm';\nimport { useCallback, useState } from 'react';\nimport { changeOwner as apiChangeOwner } from '../../../api/org/secret';\nimport { useApi } from '../../../hooks/useApi';\nimport { RequestErrorActivity } from '../index';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    secretName: ConcordId;\n    initialOwnerId?: ConcordId;\n    disabled: boolean;\n}\n\nconst SecretOwnerChangeActivity = ({\n    orgName,\n    secretName,\n    initialOwnerId,\n    disabled\n}: ExternalProps) => {\n    const [value, setValue] = useState(initialOwnerId);\n\n    const postData = useCallback(() => {\n        return apiChangeOwner(orgName, secretName, value!);\n    }, [orgName, secretName, value]);\n\n    const { error, isLoading, fetch, clearState } = useApi<GenericOperationResult>(postData, {\n        fetchOnMount: false,\n        requestByFetch: true\n    });\n\n    const ownerChangeHandler = useCallback(\n        (value: ConcordId) => {\n            setValue(value);\n            clearState();\n            fetch();\n        },\n        [clearState, fetch]\n    );\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n            <EntityOwnerChangeForm\n                originalOwnerId={initialOwnerId}\n                confirmationHeader=\"Change secret owner?\"\n                confirmationContent=\"Are you sure you want to change the secret's owner?\"\n                onSubmit={ownerChangeHandler}\n                submitting={isLoading}\n                disabled={disabled}\n            />\n        </>\n    );\n};\n\nexport default SecretOwnerChangeActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/SecretProjectActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Confirm, Form, Icon, Label } from 'semantic-ui-react';\nimport { useCallback, useState } from 'react';\n\nimport { ConcordKey, RequestError } from '../../../api/common';\nimport { ProjectEntry } from '../../../api/org/project';\nimport { updateSecretProject as apiUpdateSecretProject } from '../../../api/org/secret';\nimport { RequestErrorActivity, ProjectSearch } from '../index';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    projects: ProjectEntry[];\n    secretName: ConcordKey;\n    onUpdated?: () => void;\n}\n\nconst SecretProjectActivity = ({ orgName, projects, secretName, onUpdated }: ExternalProps) => {\n    const [dirty, setDirty] = useState<boolean>(false);\n    const [showConfirm, setShowConfirm] = useState<boolean>(false);\n    const [updating, setUpdating] = useState(false);\n    const [error, setError] = useState<RequestError>();\n    const [existingProjects, setExistingProjects] = useState<Array<ProjectEntry>>(projects.slice(0));\n    const [newProjects, setNewProjects] = useState<Array<ProjectEntry>>(projects.slice(0));\n    const [editMode, setEditMode] = useState<boolean>(false);\n\n    React.useEffect(() => {\n        if (!editMode) {\n            setExistingProjects(projects.slice(0));\n            setNewProjects(projects.slice(0));\n        }\n    }, [editMode, projects]);\n\n    const update = useCallback(async () => {\n        setUpdating(true);\n        setError(undefined);\n\n        try {\n            await apiUpdateSecretProject(\n                orgName,\n                secretName,\n                newProjects.map((project) => project.id)\n            );\n        } catch (e) {\n            setError(e);\n            throw e;\n        } finally {\n            setUpdating(false);\n        }\n    }, [orgName, secretName, newProjects]);\n\n    const onConfirmHandler = useCallback(async () => {\n        try {\n            await update();\n            onUpdated && onUpdated();\n            setShowConfirm(false);\n            setDirty(false);\n            setEditMode(false);\n            setExistingProjects(newProjects);\n        } catch (e) {\n            // keep the dialog open so the user can retry or adjust the selection\n        }\n    }, [newProjects, onUpdated, update]);\n\n    const onCancelHandler = useCallback(() => {\n        setShowConfirm(false);\n        setNewProjects(existingProjects);\n        setDirty(false);\n        setError(undefined);\n    }, [existingProjects]);\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n\n            <Form loading={updating}>\n                {editMode ? (\n                    <Form.Group widths={3}>\n                        <Form.Field>\n                            <ProjectSearch\n                                orgName={orgName}\n                                placeholder=\"Search for projects\"\n                                fluid={true}\n                                onSelect={(project) => {\n                                    if (!newProjects.map((item) => item.id).includes(project.id)) {\n                                        setNewProjects(newProjects.concat(project));\n                                        setDirty(true);\n                                    }\n                                }}\n                                projectsToNotShow={newProjects}\n                                clearOnSelect={true}\n                            />\n                        </Form.Field>\n                    </Form.Group>\n                ) : null}\n                <Form.Group>\n                    <Form.Field>\n                        {newProjects && newProjects.length > 0\n                            ? newProjects.map((project, index) => (\n                                  <Label key={index}>\n                                      {project.name}\n                                      {editMode ? (\n                                          <Icon\n                                              name=\"delete\"\n                                              onClick={() => {\n                                                  if (\n                                                      newProjects\n                                                          .map((item) => item.id)\n                                                          .includes(project.id)\n                                                  ) {\n                                                      const nextProjects = newProjects.slice(0);\n                                                      nextProjects.splice(\n                                                          nextProjects\n                                                              .map((item) => item.id)\n                                                              .indexOf(project.id),\n                                                          1\n                                                      );\n                                                      setNewProjects(nextProjects);\n                                                      setDirty(true);\n                                                  }\n                                              }}\n                                          />\n                                      ) : null}\n                                  </Label>\n                              ))\n                            : !editMode\n                              ? 'No restriction on projects.'\n                              : null}\n                    </Form.Field>\n                </Form.Group>\n                <Form.Group>\n                    <Form.Button\n                        primary={!editMode}\n                        negative={editMode}\n                        content={editMode ? 'Update' : 'Edit'}\n                        disabled={!editMode && dirty}\n                        onClick={() => {\n                            if (editMode) {\n                                setShowConfirm(true);\n                                return;\n                            }\n\n                            setError(undefined);\n                            setEditMode(true);\n                        }}\n                    />\n                    {editMode ? (\n                        <Form.Button\n                            primary={true}\n                            content=\"Cancel\"\n                            onClick={() => {\n                                setEditMode(false);\n                                setNewProjects(existingProjects);\n                                setDirty(false);\n                                setError(undefined);\n                            }}\n                        />\n                    ) : null}\n                </Form.Group>\n\n                <Confirm\n                    open={showConfirm}\n                    header=\"Update the project?\"\n                    content=\"Are you sure you want to update the project?\"\n                    onConfirm={onConfirmHandler}\n                    onCancel={onCancelHandler}\n                />\n            </Form>\n        </>\n    );\n};\n\nexport default SecretProjectActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/SecretRenameActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useHistory } from '@/router';\n\nimport { ConcordKey, RequestError } from '../../../api/common';\nimport { isSecretExists } from '../../../api/service/console';\nimport { renameSecret as apiRenameSecret } from '../../../api/org/secret';\nimport { secretAlreadyExistsError } from '../../../validation';\nimport { EntityRenameForm, RequestErrorMessage } from '../../molecules';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    secretName: ConcordKey;\n}\n\nconst SecretRenameActivity = ({ orgName, secretName }: ExternalProps) => {\n    const history = useHistory();\n    const [error, setError] = React.useState<RequestError>();\n    const [renaming, setRenaming] = React.useState(false);\n\n    const rename = React.useCallback(\n        async (newSecretName: ConcordKey) => {\n            setRenaming(true);\n            setError(undefined);\n\n            try {\n                await apiRenameSecret(orgName, secretName, newSecretName);\n                history.push(`/org/${orgName}/secret`);\n            } catch (e) {\n                setError(e);\n            } finally {\n                setRenaming(false);\n            }\n        },\n        [history, orgName, secretName]\n    );\n\n    return (\n        <>\n            {error && <RequestErrorMessage error={error} />}\n            <EntityRenameForm\n                originalName={secretName}\n                submitting={renaming}\n                onSubmit={(values) => rename(values.name)}\n                inputPlaceholder=\"Secret name\"\n                confirmationHeader=\"Rename the secret?\"\n                confirmationContent=\"Are you sure you want to rename the secret?\"\n                isExists={(name) => isSecretExists(orgName, name)}\n                alreadyExistsTemplate={secretAlreadyExistsError}\n            />\n        </>\n    );\n};\n\nexport default SecretRenameActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/SecretSearch/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport { Search } from 'semantic-ui-react';\n\nimport { ConcordKey, RequestError } from '../../../api/common';\nimport {\n    get as apiGet,\n    list as apiList,\n    SecretEntry,\n    SecretType,\n    SecretVisibility\n} from '../../../api/org/secret';\nimport { SearchProps } from 'semantic-ui-react/dist/commonjs/modules/Search/Search';\n\ninterface ExternalProps {\n    fluid?: boolean;\n\n    invalid?: boolean;\n    orgName: ConcordKey;\n    defaultSecretName?: ConcordKey;\n    placeholder?: string;\n    onBlur?: (value?: SecretEntry) => void;\n    onChange?: (value?: string) => void;\n    onSelect?: (value: SecretEntry) => void;\n}\n\ninterface Result {\n    title: string;\n    description: string;\n}\n\nconst renderTitle = (e: SecretEntry) => `${e.name}`;\n\nconst renderType = (e: SecretEntry): string => {\n    switch (e.type) {\n        case SecretType.DATA:\n            return 'Single value';\n        case SecretType.KEY_PAIR:\n            return 'SSH key pair';\n        case SecretType.USERNAME_PASSWORD:\n            return 'Username/password';\n        default:\n            return `${e.type}`;\n    }\n};\n\nconst renderVisibility = (e: SecretEntry): string => {\n    switch (e.visibility) {\n        case SecretVisibility.PRIVATE:\n            return 'private';\n        case SecretVisibility.PUBLIC:\n            return 'public';\n        default:\n            return `${e.visibility}`;\n    }\n};\n\nconst renderDescription = (e: SecretEntry): string => `${renderType(e)} (${renderVisibility(e)})`;\n\nexport default ({\n    orgName,\n    defaultSecretName,\n    placeholder,\n    fluid,\n    invalid,\n    onBlur,\n    onChange,\n    onSelect\n}: ExternalProps) => {\n    const [value, setValue] = useState<string | undefined>();\n    const [loading, setLoading] = useState(false);\n    const [error, setError] = useState<RequestError>();\n    const [items, setItems] = useState<SecretEntry[]>([]);\n    const [results, setResults] = useState<Result[]>([]);\n\n    // perform search whenever the filter changes\n    useEffect(() => {\n        if (!value || value.trim().length < 3) {\n            setResults([]);\n            return;\n        }\n\n        const fetchData = async () => {\n            setLoading(true);\n            try {\n                const result = await apiList(orgName, 0, 10, value);\n                setItems(result.items);\n            } catch (e) {\n                setError(e);\n            } finally {\n                setLoading(false);\n            }\n        };\n\n        fetchData();\n    }, [orgName, value]);\n\n    // convert SecretEntries into whatever <Search> accepts\n    useEffect(() => {\n        const r = items.map((i) => ({\n            key: i.id,\n            title: renderTitle(i),\n            description: renderDescription(i)\n        }));\n\n        setResults(r);\n    }, [items]);\n\n    // load the default secret's data\n    useEffect(() => {\n        if (!defaultSecretName) {\n            return;\n        }\n\n        const fetchData = async () => {\n            setLoading(true);\n            try {\n                const result = await apiGet(orgName, defaultSecretName);\n                setValue(result ? renderTitle(result) : undefined);\n            } catch (e) {\n                setError(e);\n            } finally {\n                setLoading(false);\n            }\n        };\n\n        fetchData();\n    }, [orgName, defaultSecretName]);\n\n    const onChangeCallBack = useCallback(\n        (event: React.MouseEvent<HTMLElement>, data: SearchProps) => {\n            setValue(data.value);\n            if (onChange) {\n                onChange(data.value);\n            }\n        },\n        [onChange]\n    );\n\n    return (\n        <Search\n            fluid={fluid}\n            input={{\n                placeholder,\n                error: !!error || invalid\n            }}\n            value={value}\n            loading={loading}\n            results={results}\n            onBlur={(event, data) => {\n                if (onBlur) {\n                    const item = items.find((i) => i.name === data.value);\n                    onBlur(item);\n                }\n            }}\n            onSearchChange={onChangeCallBack}\n            onResultSelect={(ev, data) => {\n                setValue(data.result.title);\n\n                if (onSelect) {\n                    const item = items.find((i) => i.id === data.result.key);\n                    if (item) {\n                        onSelect(item);\n                    }\n                }\n            }}\n        />\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/SecretStoreDropdown/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { DropdownItemProps } from 'semantic-ui-react';\nimport { RequestError } from '../../../api/common';\n\nimport { listActiveStores as apiList, SecretStoreEntry } from '../../../api/secret/store';\nimport { FormikDropdown } from '../../atoms';\nimport { RequestErrorMessage } from '../../molecules';\n\ninterface State {\n    loading: boolean;\n    options: DropdownItemProps[];\n    error: RequestError;\n}\n\ninterface Props {\n    name: string;\n    label?: string;\n    required?: boolean;\n    fluid?: boolean;\n}\n\nconst makeOptions = (data: SecretStoreEntry[]): DropdownItemProps[] => {\n    if (!data) {\n        return [];\n    }\n\n    return Object.keys(data)\n        .map((k) => data[k])\n        .sort((a, b) => (a.storeType > b.storeType ? 1 : a.storeType < b.storeType ? -1 : 0))\n        .map(({ storeType, description }) => ({\n            value: storeType,\n            text: description\n        }));\n};\n\nclass SecretStoreDropdown extends React.Component<Props, State> {\n    constructor(props: Props) {\n        super(props);\n        this.state = { loading: false, options: [], error: null };\n    }\n\n    componentDidMount() {\n        this.setState({ loading: true, error: null });\n\n        apiList()\n            .then((items) => this.setState({ loading: false, options: makeOptions(items) }))\n            .catch((e) => {\n                this.setState({\n                    loading: false,\n                    error: e,\n                    options: []\n                });\n            });\n    }\n\n    render() {\n        const { loading, error, options } = this.state;\n        return (\n            <>\n                {error && <RequestErrorMessage error={error} />}\n                <FormikDropdown\n                    disabled={options.length <= 1}\n                    selection={true}\n                    {...this.props}\n                    options={options}\n                    loading={loading}\n                    error={!!error}\n                />\n            </>\n        );\n    }\n}\n\nexport default SecretStoreDropdown;\n"
  },
  {
    "path": "console2/src/components/organisms/SecretTeamAccessActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Loader } from 'semantic-ui-react';\n\nimport { ConcordKey, GenericOperationResult, RequestError } from '../../../api/common';\nimport { ResourceAccessEntry } from '../../../api/org';\nimport {\n    getSecretAccess as apiGetSecretAccess,\n    updateSecretAccess as apiUpdateSecretAccess\n} from '../../../api/org/secret';\nimport { useApi } from '../../../hooks/useApi';\nimport { RequestErrorMessage, TeamAccessList } from '../../molecules';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    secretName: ConcordKey;\n    onUpdated?: () => void;\n}\n\nconst SecretTeamAccessActivity = ({ orgName, secretName, onUpdated }: ExternalProps) => {\n    const [refresh, toggleRefresh] = React.useState(false);\n    const [updateValue, setUpdateValue] = React.useState({\n        access: [] as ResourceAccessEntry[]\n    });\n\n    const fetchData = React.useCallback(() => apiGetSecretAccess(orgName, secretName), [\n        orgName,\n        secretName\n    ]);\n\n    const { data, error, isLoading } = useApi<ResourceAccessEntry[]>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: refresh\n    });\n\n    const postData = React.useCallback(async () => {\n        const result = await apiUpdateSecretAccess(orgName, secretName, updateValue.access);\n        toggleRefresh((prevState) => !prevState);\n        onUpdated && onUpdated();\n        return result;\n    }, [onUpdated, orgName, secretName, updateValue]);\n\n    const update = useApi<GenericOperationResult>(postData, {\n        fetchOnMount: false,\n        requestByFetch: true\n    });\n\n    const handleUpdate = React.useCallback(\n        (entries: ResourceAccessEntry[]) => {\n            setUpdateValue({ access: entries });\n            update.fetch();\n        },\n        [update]\n    );\n\n    if (error) {\n        return <RequestErrorMessage error={error as RequestError} />;\n    }\n\n    if (!data || isLoading || update.isLoading) {\n        return <Loader active={true} />;\n    }\n\n    return (\n        <div>\n            <TeamAccessList\n                data={data}\n                submitting={update.isLoading}\n                orgName={orgName}\n                submit={handleUpdate}\n            />\n        </div>\n    );\n};\n\nexport default SecretTeamAccessActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/SecretVisibilityActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Form } from 'semantic-ui-react';\n\nimport { ConcordId, ConcordKey, RequestError } from '../../../api/common';\nimport { SecretVisibility, updateSecretVisibility as apiUpdateSecretVisibility } from '../../../api/org/secret';\nimport { ButtonWithConfirmation, RequestErrorMessage } from '../../molecules';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    secretId: ConcordId;\n    secretName: ConcordKey;\n    visibility: SecretVisibility;\n    renderOverride?: React.ReactNode;\n    onUpdated?: () => void;\n}\n\nconst SecretVisibilityActivity = ({\n    orgName,\n    secretId,\n    secretName,\n    visibility,\n    renderOverride,\n    onUpdated\n}: ExternalProps) => {\n    const [newVisibility, setNewVisibility] = React.useState(visibility);\n    const [updating, setUpdating] = React.useState(false);\n    const [error, setError] = React.useState<RequestError>();\n\n    React.useEffect(() => {\n        setNewVisibility(visibility);\n    }, [visibility]);\n\n    const changeVisibility = React.useCallback(async () => {\n        setUpdating(true);\n        setError(undefined);\n\n        try {\n            await apiUpdateSecretVisibility(orgName, secretName, newVisibility);\n            onUpdated && onUpdated();\n        } catch (e) {\n            setError(e);\n        } finally {\n            setUpdating(false);\n        }\n    }, [newVisibility, onUpdated, orgName, secretName]);\n\n    return (\n        <>\n            {error && <RequestErrorMessage error={error} />}\n            <Form loading={updating}>\n                <Form.Group>\n                    <Form.Field>\n                        <label>Visibility</label>\n                        <select\n                            className=\"ui dropdown\"\n                            data-testid=\"secret-visibility-select\"\n                            value={newVisibility}\n                            onChange={(ev) => setNewVisibility(ev.target.value as SecretVisibility)}\n                        >\n                            <option value={SecretVisibility.PUBLIC}>Public</option>\n                            <option value={SecretVisibility.PRIVATE}>Private</option>\n                        </select>\n                    </Form.Field>\n                    <ButtonWithConfirmation\n                        renderOverride={renderOverride}\n                        floated={'right'}\n                        disabled={visibility === newVisibility}\n                        content=\"Change\"\n                        loading={updating}\n                        confirmationHeader=\"Change the visibility?\"\n                        confirmationContent={`Are you sure you want to change the visibility to ${newVisibility}`}\n                        onConfirm={changeVisibility}\n                        data-testid=\"secret-visibility-change-button\"\n                    />\n                </Form.Group>\n            </Form>\n        </>\n    );\n};\n\nexport default SecretVisibilityActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ServerVersion/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useEffect, useState } from 'react';\nimport { Icon } from 'semantic-ui-react';\nimport { version as apiVersion } from '../../../api/server';\n\nexport default () => {\n    const [loading, setLoading] = useState(false);\n    const [version, setVersion] = useState('n/a');\n\n    useEffect(() => {\n        const fetchData = async () => {\n            setLoading(true);\n            setVersion('n/a');\n\n            try {\n                const result = await apiVersion();\n                setVersion(result.version);\n            } catch (e) {\n                setVersion(`error: ${e}`);\n            } finally {\n                setLoading(false);\n            }\n        };\n\n        fetchData();\n    }, []);\n\n    if (loading) {\n        return <Icon name=\"circle notched\" loading={true} />;\n    }\n\n    return <>{version}</>;\n};\n"
  },
  {
    "path": "console2/src/components/organisms/StartRepositoryPopup/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { ReactNode, useCallback, useMemo, useState } from 'react';\nimport { useHistory } from '@/router';\nimport { Dropdown, DropdownItemProps, Message, Table } from 'semantic-ui-react';\n\nimport { ConcordId, ConcordKey, RequestError } from '../../../api/common';\nimport { start as apiStart, StartProcessResponse } from '../../../api/process';\nimport { get as getRepo, RepositoryMeta } from '../../../api/org/project/repository';\nimport { ReactJson } from '../../atoms';\nimport { GitHubLink, RequestErrorMessage, SingleOperationPopup } from '../../molecules';\nimport { RefreshRepositoryPopup } from '../../organisms';\n\nimport './styles.css';\n\ninterface Props {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    repoName: ConcordKey;\n    repoURL: string;\n    repoBranchOrCommitId: string;\n    repoPath: string;\n    title?: string;\n    allowEntryPoint?: boolean;\n    entryPoint?: string;\n    allowProfile?: boolean;\n    profiles?: string[];\n    showArgs?: boolean;\n    args?: object;\n    trigger: (onClick: () => void) => ReactNode;\n}\n\ninterface DropdownProps {\n    data: string[];\n    defaultValue?: string;\n    disabled: boolean;\n    onAdd: (value: string) => void;\n    onChange: (value: string) => void;\n    loading: boolean;\n    testId?: string;\n}\n\ninterface MultiSelectDropdownProps {\n    data: string[];\n    defaultValue?: string;\n    disabled: boolean;\n    onAdd: (value: string) => void;\n    onChange: (value: string[]) => void;\n    loading: boolean;\n}\n\nconst makeOptions = (data?: string[]): DropdownItemProps[] => {\n    if (!data) {\n        return [];\n    }\n\n    return data.map((name) => ({ text: name, value: name }));\n};\n\nconst getProfiles = (meta?: RepositoryMeta): string[] => {\n    if (!meta) {\n        return [];\n    }\n\n    return meta.profiles || [];\n};\n\nconst getEntryPoints = (meta?: RepositoryMeta): string[] => {\n    if (!meta) {\n        return [];\n    }\n\n    return meta.entryPoints?.sort() || [];\n};\n\nconst SimpleDropdown = ({\n    data,\n    defaultValue,\n    disabled,\n    onAdd,\n    onChange,\n    loading,\n    testId,\n}: DropdownProps) => (\n    <Dropdown\n        clearable={true}\n        selection={true}\n        allowAdditions={false}\n        search={true}\n        defaultValue={defaultValue}\n        disabled={disabled}\n        options={makeOptions(data)}\n        onAddItem={(event, item) => onAdd(item.value as string)}\n        onChange={(event, item) => onChange(item.value as string)}\n        loading={loading}\n        data-testid={testId}\n    />\n);\n\nconst MultiSelectDropdown = ({\n    data,\n    defaultValue,\n    disabled,\n    onAdd,\n    onChange,\n    loading,\n}: MultiSelectDropdownProps) => (\n    <Dropdown\n        clearable={true}\n        selection={true}\n        multiple={true}\n        allowAdditions={false}\n        search={true}\n        defaultValue={defaultValue}\n        disabled={disabled}\n        options={makeOptions(data)}\n        onAddItem={(event, item) => onAdd(item.value as string)}\n        onChange={(event, item) => onChange(item.value as string[])}\n        loading={loading}\n    />\n);\n\nconst StartRepositoryPopup = ({\n    orgName,\n    projectName,\n    repoName,\n    repoURL,\n    repoBranchOrCommitId,\n    repoPath,\n    trigger,\n    title,\n    allowEntryPoint,\n    entryPoint,\n    allowProfile,\n    profiles,\n    showArgs,\n    args,\n}: Props) => {\n    const history = useHistory();\n\n    const [entryPoints, setEntryPoints] = useState<string[]>([]);\n    const [selectedEntryPoint, setSelectedEntryPoint] = useState<string | undefined>(entryPoint);\n    const [availableProfiles, setAvailableProfiles] = useState<string[]>([]);\n    const [selectedProfiles, setSelectedProfiles] = useState<string[] | undefined>(profiles);\n    const [loading, setLoading] = useState(false);\n    const [disableStart, setDisableStart] = useState(false);\n    const [apiError, setApiError] = useState<RequestError>();\n\n    const [starting, setStarting] = useState(false);\n    const [success, setSuccess] = useState(false);\n    const [response, setResponse] = useState<StartProcessResponse | null>(null);\n    const [error, setError] = useState<RequestError>();\n\n    const reset = useCallback(() => {\n        setStarting(false);\n        setSuccess(false);\n        setResponse(null);\n        setError(undefined);\n    }, []);\n\n    const loadRepo = useCallback(async () => {\n        if (!allowEntryPoint) {\n            return;\n        }\n\n        try {\n            setLoading(true);\n            setApiError(undefined);\n            setDisableStart(false);\n\n            const repo = await getRepo(orgName, projectName, repoName);\n            setEntryPoints(getEntryPoints(repo.meta));\n            setAvailableProfiles(getProfiles(repo.meta));\n        } catch (e) {\n            setDisableStart(true);\n            setApiError(e);\n        } finally {\n            setLoading(false);\n        }\n    }, [allowEntryPoint, orgName, projectName, repoName]);\n\n    const onConfirm = useCallback(async () => {\n        setStarting(true);\n        setSuccess(false);\n        setError(undefined);\n\n        try {\n            const result = await apiStart(\n                orgName,\n                projectName,\n                repoName,\n                selectedEntryPoint,\n                selectedProfiles,\n                args\n            );\n\n            setResponse(result);\n            setSuccess(result.ok);\n        } catch (e) {\n            setError(e);\n        } finally {\n            setStarting(false);\n        }\n    }, [args, orgName, projectName, repoName, selectedEntryPoint, selectedProfiles]);\n\n    const openProcessPage = useCallback(\n        (instanceId: ConcordId) => {\n            history.push(`/process/${instanceId}`);\n        },\n        [history]\n    );\n\n    const renderRefreshWarning = useCallback(() => {\n        return (\n            <Message warning={true} size={'small'}>\n                If your flows or profiles are not visible, please{' '}\n                <RefreshRepositoryPopup\n                    orgName={orgName}\n                    projectName={projectName}\n                    repoName={repoName}\n                    trigger={(onClick: any) => (\n                        <span className=\"asLink\" onClick={onClick}>\n                            refresh\n                        </span>\n                    )}\n                    onDone={loadRepo}\n                />{' '}\n                the repository before starting this process.\n            </Message>\n        );\n    }, [loadRepo, orgName, projectName, repoName]);\n\n    const successMsg = useMemo(() => {\n        if (!response) {\n            return null;\n        }\n\n        return (\n            <ReactJson\n                name={null}\n                displayDataTypes={false}\n                displayObjectSize={false}\n                src={{\n                    ok: response.ok,\n                    instanceId: response.instanceId,\n                }}\n            />\n        );\n    }, [response]);\n\n    if (apiError) {\n        return <RequestErrorMessage error={apiError} />;\n    }\n\n    return (\n        <SingleOperationPopup\n            customStyle={{ maxWidth: '800px' }}\n            trigger={trigger}\n            onOpen={loadRepo}\n            title={title || `Start repository: ${repoName}`}\n            icon=\"triangle right\"\n            iconColor=\"blue\"\n            introMsg={\n                <div>\n                    <Table definition={true}>\n                        <Table.Body>\n                            <Table.Row>\n                                <Table.Cell textAlign={'right'}>Repository URL</Table.Cell>\n                                <Table.Cell>\n                                    <GitHubLink url={repoURL} text={repoURL} />\n                                </Table.Cell>\n                            </Table.Row>\n                            <Table.Row>\n                                <Table.Cell textAlign={'right'}>Branch</Table.Cell>\n                                <Table.Cell>{repoBranchOrCommitId}</Table.Cell>\n                            </Table.Row>\n                            <Table.Row>\n                                <Table.Cell textAlign={'right'}>Path</Table.Cell>\n                                <Table.Cell>{repoPath}</Table.Cell>\n                            </Table.Row>\n                            <Table.Row>\n                                <Table.Cell textAlign={'right'}>Flow*</Table.Cell>\n                                <Table.Cell>\n                                    {allowEntryPoint ? (\n                                        <SimpleDropdown\n                                            data={entryPoints}\n                                            defaultValue={entryPoint}\n                                            disabled={entryPoint !== undefined}\n                                            onAdd={(value) => {\n                                                setSelectedEntryPoint(value);\n                                                setEntryPoints((prev) => [value, ...prev]);\n                                            }}\n                                            onChange={setSelectedEntryPoint}\n                                            loading={loading}\n                                            testId=\"repository-run-flow-dropdown\"\n                                        />\n                                    ) : (\n                                        entryPoint\n                                    )}\n                                </Table.Cell>\n                            </Table.Row>\n                            <Table.Row>\n                                <Table.Cell textAlign={'right'}>Profile(s)*</Table.Cell>\n                                <Table.Cell>\n                                    {allowProfile ? (\n                                        <MultiSelectDropdown\n                                            data={availableProfiles}\n                                            defaultValue={profiles ? profiles[0] : undefined}\n                                            disabled={profiles !== undefined}\n                                            onAdd={(value) => {\n                                                setSelectedProfiles([value]);\n                                                setAvailableProfiles((prev) => [value, ...prev]);\n                                            }}\n                                            onChange={setSelectedProfiles}\n                                            loading={loading}\n                                        />\n                                    ) : profiles !== undefined ? (\n                                        profiles.join(',')\n                                    ) : (\n                                        ''\n                                    )}\n                                </Table.Cell>\n                            </Table.Row>\n                            {showArgs ? (\n                                <Table.Row>\n                                    <Table.Cell textAlign={'right'}>Arguments</Table.Cell>\n                                    <Table.Cell>\n                                        <ReactJson\n                                            src={args != null ? args : {}}\n                                            collapsed={false}\n                                            name={null}\n                                        />\n                                    </Table.Cell>\n                                </Table.Row>\n                            ) : (\n                                ''\n                            )}\n                        </Table.Body>\n                    </Table>\n                    {renderRefreshWarning()}\n                </div>\n            }\n            running={starting}\n            runningMsg={<p>Starting the process...</p>}\n            success={success}\n            successMsg={successMsg}\n            error={error}\n            reset={reset}\n            onConfirm={onConfirm}\n            onDoneElements={() =>\n                response?.instanceId ? (\n                    <button\n                        type=\"button\"\n                        className=\"ui basic button\"\n                        data-testid=\"repository-open-process-page-button\"\n                        onClick={() => openProcessPage(response.instanceId)}\n                    >\n                        Open the process page\n                    </button>\n                ) : null\n            }\n            customYes=\"Start\"\n            customNo=\"Cancel\"\n            disableYes={loading || disableStart}\n        />\n    );\n};\n\nexport default StartRepositoryPopup;\n"
  },
  {
    "path": "console2/src/components/organisms/StartRepositoryPopup/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n.asLink {\n    text-decoration: underline;\n}\n\n.asLink:hover {\n    font-weight: bold;\n    cursor: pointer;\n}\n"
  },
  {
    "path": "console2/src/components/organisms/TaskCallDetails/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { isObject } from 'formik';\nimport * as React from 'react';\nimport { useEffect, useState } from 'react';\nimport { Grid, Loader, Table } from 'semantic-ui-react';\n\nimport { ConcordId, RequestError } from '../../../api/common';\nimport {\n    listEvents as apiListEvents,\n    ProcessElementEvent,\n    ProcessEventEntry,\n    VariableMapping\n} from '../../../api/process/event';\nimport { comparators } from '../../../utils';\nimport { ReactJson } from '../../atoms';\nimport { RequestErrorMessage } from '../../molecules';\nimport { ScrollableX } from '../../atoms/Scrollable';\n\nexport interface Props {\n    instanceId: ConcordId;\n    correlationId: ConcordId;\n}\n\nconst renderSource = (v: VariableMapping) => {\n    if (v.source) {\n        return `Flow variable: ${v.source}`;\n    }\n\n    if (v.sourceValue) {\n        return 'Literal value';\n    }\n\n    if (v.sourceExpression) {\n        return <pre>{v.sourceExpression}</pre>;\n    }\n};\n\nconst renderTarget = (v: VariableMapping) => {\n    return <pre>{v.target}</pre>;\n};\n\nconst renderValue = (v: VariableMapping) => {\n    const data = v.resolved || v.sourceValue;\n\n    if (isObject(data)) {\n        return <ReactJson src={data} name={null} enableClipboard={false} collapsed={true} />;\n    }\n\n    return <pre>{JSON.stringify(data)}</pre>;\n};\n\nconst renderInputVariable = (v: VariableMapping, idx: number) => (\n    <Table.Row key={idx}>\n        <Table.Cell collapsing={true}>{renderTarget(v)}</Table.Cell>\n        <Table.Cell collapsing={true}>{renderSource(v)}</Table.Cell>\n        <Table.Cell>{renderValue(v)}</Table.Cell>\n    </Table.Row>\n);\n\nconst renderInput = (data: VariableMapping[]) => (\n    <>\n        <h4>Input</h4>\n        <div style={{ overflowX: 'auto' }}>\n            <Table celled={true} compact={true}>\n                <Table.Header>\n                    <Table.Row>\n                        <Table.HeaderCell collapsing={true}>Parameter</Table.HeaderCell>\n                        <Table.HeaderCell collapsing={true}>Source</Table.HeaderCell>\n                        <Table.HeaderCell>Value</Table.HeaderCell>\n                    </Table.Row>\n                </Table.Header>\n                <Table.Body>\n                    {data.sort(comparators.byProperty((i) => i.target)).map(renderInputVariable)}\n                </Table.Body>\n            </Table>\n        </div>\n    </>\n);\n\nconst renderValueV2 = (v: any) => {\n    if (isObject(v)) {\n        return <ReactJson src={v} name={null} enableClipboard={false} collapsed={true} />;\n    }\n\n    return <pre>{JSON.stringify(v)}</pre>;\n};\n\nconst renderVariablesV2 = (key: string, value: any, index: number) => {\n    return (\n        <Table.Row key={index}>\n            <Table.Cell collapsing={true}>\n                <pre>{key}</pre>\n            </Table.Cell>\n            <Table.Cell>{renderValueV2(value)}</Table.Cell>\n        </Table.Row>\n    );\n};\n\nconst renderDetailsV2 = (label: string, data: {}) => (\n    <>\n        <h4>{label}</h4>\n        <div style={{ overflowX: 'auto' }}>\n            <Table celled={true} compact={true}>\n                <Table.Header>\n                    <Table.Row>\n                        <Table.HeaderCell collapsing={true}>Parameter</Table.HeaderCell>\n                        <Table.HeaderCell>Value</Table.HeaderCell>\n                    </Table.Row>\n                </Table.Header>\n                <Table.Body>\n                    {Object.keys(data).sort().map((key, index) =>\n                        renderVariablesV2(key, data[key], index)\n                    )}\n                </Table.Body>\n            </Table>\n        </div>\n    </>\n);\n\n/**\n * Return a POST event if it's present in the array or PRE if it's not.\n */\nconst pickEvent = (\n    data?: ProcessEventEntry<ProcessElementEvent>[]\n): ProcessEventEntry<ProcessElementEvent> | undefined => {\n    if (!data) {\n        return;\n    }\n\n    if (data.length === 1) {\n        return data[0];\n    }\n\n    if (data.length !== 2) {\n        return;\n    }\n\n    if (data[0].data.phase === 'pre') {\n        // runtime-v1 events have 'in' data only in the 'pre' phase\n        // Copy the 'post' event to a new object and merge 'pre' phase 'in' data\n        // if it exists\n        const entry: ProcessEventEntry<ProcessElementEvent> = {\n            ...data[1]\n        };\n\n        if (!entry.data.in && data[0].data.in) {\n            entry.data.in = data[0].data.in;\n        }\n\n        return entry;\n    } else {\n        return data[0];\n    }\n};\n\nconst TaskCallDetails = (props: Props) => {\n    const [loading, setLoading] = useState(false);\n    const [error, setError] = useState<RequestError>();\n    const [data, setData] = useState<ProcessEventEntry<ProcessElementEvent>[]>();\n\n    useEffect(() => {\n        const fetchData = async () => {\n            setLoading(true);\n            setError(undefined);\n\n            try {\n                const result = await apiListEvents({\n                    instanceId: props.instanceId,\n                    type: 'ELEMENT',\n                    eventCorrelationId: props.correlationId,\n                    includeAll: true,\n                    limit: 2\n                });\n\n                setData(result as ProcessEventEntry<ProcessElementEvent>[]);\n            } catch (e) {\n                setError(e);\n            } finally {\n                setLoading(false);\n            }\n        };\n\n        fetchData();\n    }, [props.instanceId, props.correlationId]);\n\n    if (loading) {\n        return <Loader active={loading} />;\n    }\n\n    if (error) {\n        return <RequestErrorMessage error={error} />;\n    }\n\n    const details = pickEvent(data)?.data;\n\n    if (!details) {\n        return <p>No data found.</p>;\n    }\n\n    return (\n        <>\n            <Grid columns={2}>\n                <Grid.Row>\n                    <Grid.Column>\n                        <ScrollableX style={{ height: '100%' }}>\n                            <Table definition={true} style={{ height: '100%' }}>\n                                <Table.Body>\n                                    <Table.Row>\n                                        <Table.Cell collapsing={true}>Location</Table.Cell>\n                                        <Table.Cell>\n                                            {details.fileName ? details.fileName + ' @ ' : ''} line:{' '}\n                                            {details.line}, column: {details.column}\n                                        </Table.Cell>\n                                    </Table.Row>\n                                </Table.Body>\n                            </Table>\n                        </ScrollableX>\n                    </Grid.Column>\n                    <Grid.Column>\n                        <ScrollableX style={{ height: '100%' }}>\n                            <Table definition={true} style={{ height: '100%' }}>\n                                <Table.Body>\n                                    <Table.Row>\n                                        <Table.Cell collapsing={true}>Flow</Table.Cell>\n                                        <Table.Cell>{details.processDefinitionId}@{details.threadId || 0}</Table.Cell>\n                                    </Table.Row>\n                                </Table.Body>\n                            </Table>\n                        </ScrollableX>\n                    </Grid.Column>\n                </Grid.Row>\n            </Grid>\n\n            {details.in &&\n                details.in instanceof Array &&\n                details.in.length > 0 &&\n                renderInput(details.in)}\n\n            {details.in &&\n                !(details.in instanceof Array) &&\n                isObject(details.in) &&\n                renderDetailsV2('Input', details.in)}\n\n            {details.out &&\n                !(details.out instanceof Array) &&\n                isObject(details.out) &&\n                renderDetailsV2('Output', details.out)}\n        </>\n    );\n};\n\nexport default TaskCallDetails;\n"
  },
  {
    "path": "console2/src/components/organisms/TeamActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Link, Navigate, Route, Routes } from 'react-router';\nimport { Divider, Header, Icon, Loader, Menu, Segment } from 'semantic-ui-react';\n\nimport { ConcordKey, RequestError } from '../../../api/common';\nimport { TeamEntry, get as apiGetTeam } from '../../../api/org/team';\nimport { useApi } from '../../../hooks/useApi';\nimport { RequestErrorMessage, WithCopyToClipboard } from '../../molecules';\nimport { NotFoundPage } from '../../pages';\nimport {\n    AuditLogActivity,\n    TeamDeleteActivity,\n    TeamMemberList2,\n    TeamLdapGroupList2,\n    TeamRenameActivity,\n} from '../../organisms';\n\nexport type TabLink = 'members' | 'ldapGroups' | 'settings' | 'audit' | null;\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    teamName: ConcordKey;\n    activeTab: TabLink;\n}\n\nconst TeamActivity = ({ orgName, teamName, activeTab }: ExternalProps) => {\n    const fetchData = React.useCallback(() => apiGetTeam(orgName, teamName), [orgName, teamName]);\n    const { data, error, isLoading } = useApi<TeamEntry>(fetchData, {\n        fetchOnMount: true,\n    });\n\n    const renderSetting = (team: TeamEntry) => (\n        <>\n            <Header as=\"h5\" disabled={true} data-testid=\"team-settings-id\">\n                <WithCopyToClipboard value={team.id}>ID: {team.id}</WithCopyToClipboard>\n            </Header>\n\n            <Divider horizontal={true} content=\"Danger Zone\" />\n\n            <Segment color=\"red\">\n                <Header as=\"h4\">Team name</Header>\n                <TeamRenameActivity orgName={team.orgName} teamId={team.id} teamName={team.name} />\n\n                <Header as=\"h4\">Delete team</Header>\n                <TeamDeleteActivity orgName={team.orgName} teamName={team.name} />\n            </Segment>\n        </>\n    );\n\n    if (error) {\n        return <RequestErrorMessage error={error as RequestError} />;\n    }\n\n    if (isLoading || !data) {\n        return <Loader active={true} />;\n    }\n\n    const baseUrl = `/org/${orgName}/team/${teamName}`;\n\n    return (\n        <>\n            <Menu tabular={true}>\n                <Menu.Item active={activeTab === 'members'} data-testid=\"team-tab-members\">\n                    <Icon name=\"users\" />\n                    <Link to={`/org/${orgName}/team/${teamName}/members`}>Members</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'ldapGroups'} data-testid=\"team-tab-ldapGroups\">\n                    <Icon name=\"users\" />\n                    <Link to={`/org/${orgName}/team/${teamName}/ldapGroups`}>LDAP Groups</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'settings'} data-testid=\"team-tab-settings\">\n                    <Icon name=\"setting\" />\n                    <Link to={`/org/${orgName}/team/${teamName}/settings`}>Settings</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'audit'} data-testid=\"team-tab-audit\">\n                    <Icon name=\"history\" />\n                    <Link to={`/org/${orgName}/team/${teamName}/audit`}>Audit Log</Link>\n                </Menu.Item>\n            </Menu>\n\n            <Routes>\n                <Route index={true} element={<Navigate to=\"members\" replace={true} />} />\n                <Route\n                    path=\"members\"\n                    element={<TeamMemberList2 orgName={orgName} teamName={teamName} />}\n                />\n                <Route\n                    path=\"ldapGroups\"\n                    element={<TeamLdapGroupList2 orgName={orgName} teamName={teamName} />}\n                />\n                <Route path=\"settings\" element={renderSetting(data)} />\n                <Route\n                    path=\"audit\"\n                    element={<AuditLogActivity filter={{ details: { orgName, teamName } }} />}\n                />\n                <Route path=\"*\" element={<NotFoundPage />} />\n            </Routes>\n        </>\n    );\n};\n\nexport default TeamActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/TeamDeleteActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useHistory } from '@/router';\n\nimport { ConcordKey, RequestError } from '../../../api/common';\nimport { deleteTeam as apiDeleteTeam } from '../../../api/org/team';\nimport { ButtonWithConfirmation, RequestErrorMessage } from '../../molecules';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    teamName: ConcordKey;\n}\n\nconst TeamDeleteActivity = ({ orgName, teamName }: ExternalProps) => {\n    const history = useHistory();\n    const [error, setError] = React.useState<RequestError>();\n    const [deleting, setDeleting] = React.useState(false);\n\n    const deleteTeam = React.useCallback(async () => {\n        setDeleting(true);\n        setError(undefined);\n\n        try {\n            await apiDeleteTeam(orgName, teamName);\n            history.push(`/org/${orgName}/team/`);\n        } catch (e) {\n            setError(e);\n        } finally {\n            setDeleting(false);\n        }\n    }, [history, orgName, teamName]);\n\n    return (\n        <>\n            {error && <RequestErrorMessage error={error} />}\n            <ButtonWithConfirmation\n                primary={true}\n                negative={true}\n                content=\"Delete\"\n                loading={deleting}\n                confirmationHeader=\"Delete the team?\"\n                confirmationContent=\"Are you sure you want to delete the team?\"\n                onConfirm={deleteTeam}\n                data-testid=\"team-delete-button\"\n            />\n        </>\n    );\n};\n\nexport default TeamDeleteActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/TeamLdapGroupList2/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { useCallback, useEffect, useState } from 'react';\n\nimport {\n    addLdapGroups as apiAddGroups,\n    listLdapGroups as apiListGroups,\n    NewTeamLdapGroupEntry,\n    TeamRole\n} from '../../../api/org/team';\nimport { ConcordKey, RequestError } from '../../../api/common';\nimport { Button, Container, Form, Loader, Menu, Table } from 'semantic-ui-react';\nimport { FindLdapGroupField, RequestErrorActivity } from '../../organisms';\nimport { LdapGroupSearchResult } from '../../../api/service/console';\nimport { TeamRoleDropdown } from '../../molecules';\n\ninterface Entry extends NewTeamLdapGroupEntry {\n    added?: boolean;\n    deleted?: boolean;\n    updated?: boolean;\n}\n\ninterface Props {\n    orgName: ConcordKey;\n    teamName: ConcordKey;\n}\n\nexport default ({ orgName, teamName }: Props) => {\n    const [loading, setLoading] = useState(false);\n    const [editMode, setEditMode] = useState(false);\n    const [submitting, setSubmitting] = useState(false);\n    const [data, setData] = useState<Entry[]>([]);\n    const [error, setError] = useState<RequestError>();\n    const [dirty, setDirty] = useState(false);\n\n    const load = useCallback(async () => {\n        try {\n            setLoading(true);\n            setError(undefined);\n\n            let result = (await apiListGroups(orgName, teamName)).map((r) => ({ ...r }));\n            setData(result.map((r) => ({ ...r })));\n        } catch (e) {\n            setError(e);\n        } finally {\n            setLoading(false);\n            setDirty(false);\n        }\n    }, [orgName, teamName]);\n\n    // initial load\n    useEffect(() => {\n        load();\n    }, [load]);\n\n    // set dirty to true on any changes\n    useEffect(() => {\n        setDirty(!!data.find((e) => e.added || e.deleted || e.updated));\n    }, [data]);\n\n    const cancel = () => {\n        setEditMode(false);\n        load();\n    };\n\n    const addGroup = (r: LdapGroupSearchResult) => {\n        setData((prev) => {\n            const e: Entry = {\n                added: true,\n                role: TeamRole.MEMBER,\n                group: r.groupName\n            };\n            return [e, ...prev];\n        });\n    };\n\n    const deleteGroup = (idx: number) => {\n        setData((prev) => {\n            let e = prev[idx];\n\n            if (e.added) {\n                let result = [...prev];\n                result.splice(idx, 1);\n                return result;\n            }\n\n            e.deleted = true;\n            e.updated = false;\n            return [...prev];\n        });\n    };\n\n    const undoGroup = (idx: number) => {\n        setData((prev) => {\n            prev[idx].deleted = false;\n            return [...prev];\n        });\n    };\n\n    const updateRole = (idx: number, role: TeamRole) => {\n        setData((prev) => {\n            let e = prev[idx];\n            e.role = role;\n            if (!e.deleted && !e.added) {\n                e.updated = true;\n            }\n            return [...prev];\n        });\n    };\n\n    const save = useCallback(async () => {\n        const sanitizedData = data\n            .filter((e) => !e.deleted)\n            .map((e) => ({ group: e.group, role: e.role }));\n\n        try {\n            setSubmitting(true);\n            await apiAddGroups(orgName, teamName, true, sanitizedData);\n        } catch (e) {\n            setError(e);\n            return;\n        } finally {\n            setSubmitting(false);\n        }\n\n        load();\n    }, [orgName, teamName, data, load]);\n\n    if (loading) {\n        return <Loader active={true} />;\n    }\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n            <Menu secondary={true} widths={3}>\n                {editMode && (\n                    <Menu.Item disabled={submitting}>\n                        <Container fluid={true} textAlign=\"left\">\n                            <Form>\n                                <Form.Field>\n                                    <FindLdapGroupField\n                                        placeholder=\"Add a team LDAP group\"\n                                        onSelect={(r: LdapGroupSearchResult) => addGroup(r)}\n                                    />\n                                </Form.Field>\n                            </Form>\n                        </Container>\n                    </Menu.Item>\n                )}\n                <Menu.Item position=\"right\">\n                    <Container textAlign=\"right\">\n                        {editMode && (\n                            <>\n                                <Button\n                                    primary={true}\n                                    content=\"Save changes\"\n                                    disabled={!dirty}\n                                    loading={submitting}\n                                    onClick={(ev) => save()}\n                                />\n                                <Button\n                                    basic={true}\n                                    negative={true}\n                                    icon=\"cancel\"\n                                    content=\"Cancel\"\n                                    disabled={submitting}\n                                    onClick={() => cancel()}\n                                />\n                            </>\n                        )}\n\n                        {!editMode && (\n                            <Button icon=\"edit\" content=\"Edit\" onClick={() => setEditMode(true)} />\n                        )}\n                    </Container>\n                </Menu.Item>\n            </Menu>\n\n            {data.length === 0 && <h3>No team LDAP groups.</h3>}\n\n            {data.length > 0 && (\n                <Table>\n                    <Table.Header>\n                        <Table.Row>\n                            <Table.HeaderCell>LDAP Group</Table.HeaderCell>\n                            <Table.HeaderCell collapsing={true}>Role</Table.HeaderCell>\n                            {editMode && <Table.HeaderCell collapsing={true} />}\n                        </Table.Row>\n                    </Table.Header>\n                    <Table.Body>\n                        {data.map((e, idx) => (\n                            <Table.Row\n                                key={idx}\n                                negative={e.deleted}\n                                positive={e.added}\n                                warning={e.updated}>\n                                <Table.Cell>{e.group}</Table.Cell>\n                                <Table.Cell>\n                                    {editMode ? (\n                                        <TeamRoleDropdown\n                                            value={e.role}\n                                            disabled={submitting}\n                                            onRoleChange={(value) => updateRole(idx, value)}\n                                        />\n                                    ) : (\n                                        e.role\n                                    )}\n                                </Table.Cell>\n                                {editMode && (\n                                    <Table.Cell>\n                                        <Button\n                                            basic={true}\n                                            negative={!e.deleted}\n                                            icon={e.deleted ? 'undo' : 'delete'}\n                                            disabled={submitting}\n                                            onClick={() =>\n                                                e.deleted ? undoGroup(idx) : deleteGroup(idx)\n                                            }\n                                        />\n                                    </Table.Cell>\n                                )}\n                            </Table.Row>\n                        ))}\n                    </Table.Body>\n                </Table>\n            )}\n        </>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/TeamList/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback } from 'react';\nimport { Link } from 'react-router';\nimport { List } from 'semantic-ui-react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { list as apiList, TeamEntry } from '../../../api/org/team';\nimport { comparators } from '../../../utils';\nimport { useApi } from '../../../hooks/useApi';\nimport { LoadingDispatch } from '../../../App';\nimport { RequestErrorActivity } from '../index';\n\ninterface Props {\n    orgName: ConcordKey;\n    forceRefresh: any;\n    filter?: string;\n}\n\nconst makeTeamList = (data: TeamEntry[], filter?: string): TeamEntry[] =>\n    data\n        .filter((e) => (filter ? e.name.toLowerCase().indexOf(filter.toLowerCase()) >= 0 : true))\n        .sort(comparators.byName);\n\nexport default ({ orgName, forceRefresh, filter }: Props) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const fetchData = useCallback(() => {\n        return apiList(orgName);\n    }, [orgName]);\n\n    const { data, error } = useApi<TeamEntry[]>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n\n    return (\n        <>\n            <List divided={true} relaxed={true} size=\"large\">\n                {data?.length === 0 && <h3>No teams found</h3>}\n                {data &&\n                    makeTeamList(data, filter).map((team: TeamEntry, index: number) => (\n                        <List.Item key={index}>\n                            <List.Content>\n                                <List.Header>\n                                    <Link to={`/org/${team.orgName}/team/${team.name}`}>\n                                        {team.name}\n                                    </Link>\n                                </List.Header>\n                                {team.description && (\n                                    <List.Description>{team.description}</List.Description>\n                                )}\n                            </List.Content>\n                        </List.Item>\n                    ))}\n            </List>\n        </>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/TeamListActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Input, Menu } from 'semantic-ui-react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { RedirectButton, TeamList } from '../../organisms';\nimport { useState } from 'react';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    forceRefresh: any;\n}\n\nexport default ({ orgName, forceRefresh }: ExternalProps) => {\n    const [filter, setFilter] = useState<string>();\n\n    return (\n        <>\n            <Menu secondary={true}>\n                <Menu.Item>\n                    <Input\n                        icon=\"search\"\n                        placeholder=\"Filter...\"\n                        onChange={(ev, data) => setFilter(data.value)}\n                    />\n                </Menu.Item>\n\n                <Menu.Item position={'right'}>\n                    <RedirectButton\n                        icon=\"plus\"\n                        positive={true}\n                        labelPosition=\"left\"\n                        content=\"New team\"\n                        location={`/org/${orgName}/team/_new`}\n                    />\n                </Menu.Item>\n            </Menu>\n\n            <TeamList orgName={orgName} filter={filter} forceRefresh={forceRefresh} />\n        </>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/TeamMemberList2/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport { ConcordId, ConcordKey, RequestError } from '../../../api/common';\nimport { Button, Container, Divider, Form, Loader, Menu, Table } from 'semantic-ui-react';\n\nimport { FindUserField2, RequestErrorActivity } from '../../organisms';\nimport { UserEntry } from '../../../api/user';\nimport { TeamRoleDropdown } from '../../molecules';\nimport {\n    addUsers as apiAddUsers,\n    listUsers as apiListUsers,\n    MemberType,\n    NewTeamUserEntry,\n    TeamRole,\n    TeamUserEntry\n} from '../../../api/org/team';\n\ninterface Entry extends NewTeamUserEntry {\n    added?: boolean;\n    deleted?: boolean;\n    updated?: boolean;\n}\n\ninterface LdapGroupRole {\n    ldapGroup: string;\n    role: TeamRole;\n}\n\ninterface LdapEntry extends TeamUserEntry {\n    rolesFromLdapGroup: LdapGroupRole[];\n}\n\ninterface Props {\n    orgName: ConcordKey;\n    teamName: ConcordKey;\n}\n\nconst renderUser = (e: NewTeamUserEntry) => {\n    if (!e.userDomain) {\n        return e.displayName ? `${e.displayName} (${e.username})` : e.username;\n    }\n\n    return e.displayName\n        ? `${e.displayName} (${e.username}@${e.userDomain})`\n        : `${e.username}@${e.userDomain}`;\n};\n\nconst renderUserLdapGroups = (e: LdapEntry, rowId: number) => {\n    return e.rolesFromLdapGroup.map((r, rIdx) => (\n        <Table.Row key={rIdx}>\n            {(e.rolesFromLdapGroup.length <= 1 || rIdx === 0) && (\n                <Table.Cell rowSpan={e.rolesFromLdapGroup.length}>{e.username}</Table.Cell>\n            )}\n            <Table.Cell>{r.ldapGroup}</Table.Cell>\n            <Table.Cell>{r.role}</Table.Cell>\n        </Table.Row>\n    ));\n};\n\nexport default ({ orgName, teamName }: Props) => {\n    const [loading, setLoading] = useState(false);\n    const [editMode, setEditMode] = useState(false);\n    const [submitting, setSubmitting] = useState(false);\n    const [singleUsers, setSingleUsers] = useState<Entry[]>([]);\n    const [ldapUsers, setLdapUsers] = useState<LdapEntry[]>([]);\n    const [error, setError] = useState<RequestError>();\n    const [dirty, setDirty] = useState(false);\n\n    const load = useCallback(async () => {\n        try {\n            setLoading(true);\n            setError(undefined);\n\n            let result = (await apiListUsers(orgName, teamName)).map((r) => ({ ...r }));\n            setSingleUsers(\n                result.filter((r) => r.memberType === MemberType.SINGLE).map((r) => ({ ...r }))\n            );\n            let aggregatedUsers = new Map<ConcordId, LdapEntry>();\n            result\n                .filter((r) => r.memberType === MemberType.LDAP_GROUP)\n                .forEach((r) => {\n                    if (aggregatedUsers.has(r.userId)) {\n                        let u = aggregatedUsers.get(r.userId);\n                        if (u !== undefined) {\n                            u.rolesFromLdapGroup.push({\n                                ldapGroup: r.ldapGroupSource || 'not found',\n                                role: r.role\n                            });\n                        }\n                    } else {\n                        aggregatedUsers.set(r.userId, {\n                            ...r,\n                            rolesFromLdapGroup: [\n                                { ldapGroup: r.ldapGroupSource || 'not found', role: r.role }\n                            ]\n                        });\n                    }\n                });\n            setLdapUsers(Array.from(aggregatedUsers, ([k, v]) => v));\n        } catch (e) {\n            setError(e);\n        } finally {\n            setLoading(false);\n            setDirty(false);\n        }\n    }, [orgName, teamName]);\n\n    // initial load\n    useEffect(() => {\n        load();\n    }, [load]);\n\n    // set dirty to true on any changes\n    useEffect(() => {\n        setDirty(!!singleUsers.find((e) => e.added || e.deleted || e.updated));\n    }, [singleUsers]);\n\n    const cancel = () => {\n        setEditMode(false);\n        load();\n    };\n\n    const addMember = (u: UserEntry) => {\n        setSingleUsers((prev) => {\n            const e: Entry = {\n                added: true,\n                userId: u.id,\n                role: TeamRole.MEMBER,\n                username: u.name,\n                userDomain: u.domain,\n                displayName: u.displayName\n            };\n            return [e, ...prev];\n        });\n    };\n\n    const deleteMember = (idx: number) => {\n        setSingleUsers((prev) => {\n            let e = prev[idx];\n\n            if (e.added) {\n                let result = [...prev];\n                result.splice(idx, 1);\n                return result;\n            }\n\n            e.deleted = true;\n            e.updated = false;\n            return [...prev];\n        });\n    };\n\n    const undoMember = (idx: number) => {\n        setSingleUsers((prev) => {\n            prev[idx].deleted = false;\n            return [...prev];\n        });\n    };\n\n    const updateRole = (idx: number, role: TeamRole) => {\n        setSingleUsers((prev) => {\n            let e = prev[idx];\n            e.role = role;\n            if (!e.deleted && !e.added) {\n                e.updated = true;\n            }\n            return [...prev];\n        });\n    };\n\n    const save = useCallback(async () => {\n        const sanitizedData = singleUsers\n            .filter((e) => !e.deleted)\n            .map((e) => ({ userId: e.userId, role: e.role, memberType: MemberType.SINGLE }));\n\n        try {\n            setSubmitting(true);\n            await apiAddUsers(orgName, teamName, true, sanitizedData);\n        } catch (e) {\n            setError(e);\n            return;\n        } finally {\n            setSubmitting(false);\n        }\n\n        load();\n    }, [orgName, teamName, singleUsers, load]);\n\n    if (loading) {\n        return <Loader active={true} />;\n    }\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n            <Menu secondary={true} widths={3}>\n                {editMode && (\n                    <Menu.Item disabled={submitting}>\n                        <Container fluid={true} textAlign=\"left\">\n                            <Form>\n                                <Form.Field>\n                                    <FindUserField2\n                                        placeholder=\"Add a team member\"\n                                        onSelect={(u) => addMember(u)}\n                                    />\n                                </Form.Field>\n                            </Form>\n                        </Container>\n                    </Menu.Item>\n                )}\n                <Menu.Item position=\"right\">\n                    <Container textAlign=\"right\">\n                        {editMode && (\n                            <>\n                                <Button\n                                    primary={true}\n                                    content=\"Save changes\"\n                                    disabled={!dirty}\n                                    loading={loading || submitting}\n                                    onClick={(ev) => save()}\n                                />\n                                <Button\n                                    basic={true}\n                                    negative={true}\n                                    icon=\"cancel\"\n                                    content=\"Cancel\"\n                                    disabled={loading || submitting}\n                                    onClick={() => cancel()}\n                                />\n                            </>\n                        )}\n\n                        {!editMode && (\n                            <Button icon=\"edit\" content=\"Edit\" onClick={() => setEditMode(true)} />\n                        )}\n                    </Container>\n                </Menu.Item>\n            </Menu>\n\n            {singleUsers.length === 0 && <h3>No team members.</h3>}\n\n            {singleUsers.length > 0 && (\n                <Table>\n                    <Table.Header>\n                        <Table.Row>\n                            <Table.HeaderCell>Username</Table.HeaderCell>\n                            <Table.HeaderCell collapsing={true}>Type</Table.HeaderCell>\n                            <Table.HeaderCell collapsing={true}>Role</Table.HeaderCell>\n                            {editMode && <Table.HeaderCell collapsing={true} />}\n                        </Table.Row>\n                    </Table.Header>\n                    <Table.Body>\n                        {singleUsers.map((e, idx) => (\n                            <Table.Row\n                                key={idx}\n                                negative={e.deleted}\n                                positive={e.added}\n                                warning={e.updated}>\n                                <Table.Cell>{renderUser(e)}</Table.Cell>\n                                <Table.Cell>{e.userType}</Table.Cell>\n                                <Table.Cell>\n                                    {editMode ? (\n                                        <TeamRoleDropdown\n                                            value={e.role}\n                                            disabled={submitting}\n                                            onRoleChange={(value) => updateRole(idx, value)}\n                                        />\n                                    ) : (\n                                        e.role\n                                    )}\n                                </Table.Cell>\n                                {editMode && (\n                                    <Table.Cell>\n                                        <Button\n                                            basic={true}\n                                            negative={!e.deleted}\n                                            icon={e.deleted ? 'undo' : 'delete'}\n                                            disabled={submitting}\n                                            onClick={() =>\n                                                e.deleted ? undoMember(idx) : deleteMember(idx)\n                                            }\n                                        />\n                                    </Table.Cell>\n                                )}\n                            </Table.Row>\n                        ))}\n                    </Table.Body>\n                </Table>\n            )}\n\n            {ldapUsers.length > 0 && (\n                <>\n                    <Divider horizontal>LDAP Group Members</Divider>\n\n                    <Table selectable>\n                        <Table.Header>\n                            <Table.Row>\n                                <Table.HeaderCell collapsing={true}>Username</Table.HeaderCell>\n                                <Table.HeaderCell collapsing={true}>Source</Table.HeaderCell>\n                                <Table.HeaderCell collapsing={true}>Role</Table.HeaderCell>\n                            </Table.Row>\n                        </Table.Header>\n                        <Table.Body>\n                            {ldapUsers.map((e, idx) => renderUserLdapGroups(e, idx))}\n                        </Table.Body>\n                    </Table>\n                </>\n            )}\n        </>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/TeamRenameActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useHistory } from '@/router';\n\nimport { ConcordId, ConcordKey, RequestError } from '../../../api/common';\nimport { isTeamExists } from '../../../api/service/console';\nimport { rename as apiRename } from '../../../api/org/team';\nimport { teamAlreadyExistsError } from '../../../validation';\nimport { EntityRenameForm, RequestErrorMessage } from '../../molecules';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    teamId: ConcordId;\n    teamName: ConcordKey;\n}\n\nconst TeamRenameActivity = ({ orgName, teamId, teamName }: ExternalProps) => {\n    const history = useHistory();\n    const [error, setError] = React.useState<RequestError>();\n    const [renaming, setRenaming] = React.useState(false);\n\n    const rename = React.useCallback(\n        async (newTeamName: ConcordKey) => {\n            setRenaming(true);\n            setError(undefined);\n\n            try {\n                await apiRename(orgName, teamId, newTeamName);\n                history.push(`/org/${orgName}/team/${newTeamName}`);\n            } catch (e) {\n                setError(e);\n            } finally {\n                setRenaming(false);\n            }\n        },\n        [history, orgName, teamId]\n    );\n\n    return (\n        <>\n            {error && <RequestErrorMessage error={error} />}\n            <EntityRenameForm\n                originalName={teamName}\n                submitting={renaming}\n                onSubmit={(values) => rename(values.name)}\n                inputPlaceholder=\"Team name\"\n                confirmationHeader=\"Rename the team?\"\n                confirmationContent=\"Are you sure you want to rename the team?\"\n                isExists={(name) => isTeamExists(orgName, name)}\n                alreadyExistsTemplate={teamAlreadyExistsError}\n                inputTestId=\"team-rename-input\"\n                buttonTestId=\"team-rename-button\"\n            />\n        </>\n    );\n};\n\nexport default TeamRenameActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/TopBar/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useContext } from 'react';\nimport { useLocation, useNavigate } from 'react-router';\n\nimport { CustomResources, LinkMeta } from '../../../../cfg';\nimport { GlobalNavMenu, GlobalNavTab } from '../../molecules';\nimport { logout, UserSessionContext } from '../../../session';\n\nconst pathToTab = (s: string): GlobalNavTab => {\n    if (s.startsWith('/activity')) {\n        return 'activity';\n    } else if (s.startsWith('/process')) {\n        return 'process';\n    } else if (s.startsWith('/org')) {\n        return 'org';\n    } else if (s.startsWith('/noderoster')) {\n        return 'noderoster';\n    }\n\n    return null;\n};\n\nconst getExtraSystemLinks = (): LinkMeta[] => {\n    const topBar = window.concord?.topBar;\n\n    if (topBar && topBar.systemLinks) {\n        return topBar.systemLinks;\n    }\n\n    return [];\n};\n\nconst getCustomResources = (): CustomResources => {\n    const customResources = window.concord?.customResources;\n    return customResources || {};\n};\n\ninterface NavigationProps {\n    openUrl: (url: string) => void;\n    openAbout: () => void;\n    openProfile: () => void;\n    openCustomResource: (name: string) => void;\n}\n\nconst TopBar = () => {\n    const userSession = useContext(UserSessionContext);\n    const location = useLocation();\n    const navigate = useNavigate();\n\n    const navigationProps: NavigationProps = {\n        openUrl: (url: string) => window.open(url, '_blank'),\n        openAbout: () => navigate('/about'),\n        openProfile: () => navigate('/profile'),\n        openCustomResource: (name: string) => navigate(`/custom/${name}`),\n    };\n\n    const logOut = async () => {\n        const logoutUrl = window.concord?.logoutUrl;\n        if (logoutUrl) {\n            window.location.href = logoutUrl;\n            return;\n        }\n\n        if (await logout(userSession)) {\n            navigate('/logout/done');\n        }\n    };\n\n    const activeTab = pathToTab(location.pathname);\n    return (\n        <GlobalNavMenu\n            activeTab={activeTab}\n            extraSystemLinks={getExtraSystemLinks()}\n            customResources={getCustomResources()}\n            userDisplayName={userSession.userInfo?.displayName}\n            {...navigationProps}\n            logOut={logOut}\n        />\n    );\n};\n\nexport default TopBar;\n"
  },
  {
    "path": "console2/src/components/organisms/TriggeredByPopup/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { useCallback, useState } from 'react';\nimport { Button, Divider, Header, Modal, Popup, Table } from 'semantic-ui-react';\nimport { add, format, parseISO, sub } from 'date-fns';\n\nimport { ProcessEntry } from '../../../api/process';\nimport { AuditLogEntry, AuditObject, list as apiList } from '../../../api/audit';\nimport { RequestError } from '../../../api/common';\nimport { ReactJson } from '../../atoms';\nimport { WithCopyToClipboard } from '../../molecules';\nimport { RequestErrorActivity } from '../index';\n\n// TODO move into common constants?\nconst DATE_TIME_FORMAT = \"yyyy-MM-dd'T'HH:mm:ss.SSSXXX\";\n\ninterface Props {\n    entry: ProcessEntry;\n}\n\nconst getIcon = (type?: string) => {\n    switch (type) {\n        case 'github':\n            return 'github';\n        case 'cron':\n            return 'clock outline';\n        default:\n            return 'bell';\n    }\n};\n\nconst getTitle = (type?: string) => {\n    if (!type) {\n        return 'Unknown';\n    }\n\n    switch (type) {\n        case 'github':\n            return 'GitHub';\n        default:\n            return type;\n    }\n};\n\nconst getEventIdTitle = (type?: string) => {\n    switch (type) {\n        case 'github':\n            return 'GitHub Delivery ID';\n        default:\n            return 'ID';\n    }\n};\n\nconst getGitHubEvent = (e?: AuditLogEntry) => {\n    if (!e) {\n        return;\n    }\n\n    return (e.details as any)?.githubEvent;\n};\n\nconst getEventPayload = (e?: AuditLogEntry) => {\n    if (!e) {\n        return;\n    }\n\n    return (e.details as any)?.payload;\n};\n\nexport default ({ entry }: Props) => {\n    const triggeredBy = entry.triggeredBy;\n    if (!triggeredBy || !triggeredBy.trigger) {\n        return <> - </>;\n    }\n\n    const type = triggeredBy.trigger.eventSource;\n\n    const icon = getIcon(type);\n    const title = getTitle(type);\n\n    const [loading, setLoading] = useState(false);\n    const [error, setError] = useState<RequestError>();\n    const [auditEntry, setAuditEntry] = useState<AuditLogEntry | undefined>();\n\n    const loadGitHubEvent = useCallback(\n        async (externalEventId?: string) => {\n            if (!externalEventId) {\n                return;\n            }\n\n            // ±1 hour range\n            const createdAt = parseISO(entry.createdAt);\n            const after = format(\n                sub(createdAt, {\n                    hours: 1\n                }),\n                DATE_TIME_FORMAT\n            );\n            const before = format(\n                add(createdAt, {\n                    hours: 1\n                }),\n                DATE_TIME_FORMAT\n            );\n\n            setLoading(true);\n            setError(undefined);\n            setAuditEntry(undefined);\n            try {\n                const result = await apiList({\n                    object: AuditObject.EXTERNAL_EVENT,\n                    after,\n                    before,\n                    details: { eventId: externalEventId },\n                    offset: 0,\n                    limit: 1\n                });\n\n                if (result.items && result.items.length > 0) {\n                    setAuditEntry(result.items[0]);\n                }\n            } catch (e) {\n                setError(e);\n            } finally {\n                setLoading(false);\n            }\n        },\n        [entry]\n    );\n\n    return (\n        <Modal dimmer=\"inverted\" trigger={<Button icon={icon} basic={true} content={title} />}>\n            <Header icon={icon} content={`${title} Trigger`} />\n            <Modal.Content scrolling={true}>\n                {error && <RequestErrorActivity error={error} />}\n\n                <Table definition={true}>\n                    <Table.Body>\n                        <Table.Row>\n                            <Table.Cell collapsing={true}>{getEventIdTitle(type)}</Table.Cell>\n                            <Table.Cell>\n                                {triggeredBy.externalEventId ? (\n                                    <WithCopyToClipboard value={triggeredBy.externalEventId}>\n                                        {triggeredBy.externalEventId}\n                                    </WithCopyToClipboard>\n                                ) : (\n                                    ''\n                                )}\n                            </Table.Cell>\n                        </Table.Row>\n                        <Table.Row>\n                            <Table.Cell collapsing={true}>Event</Table.Cell>\n                            <Table.Cell>\n                                <Popup\n                                    trigger={\n                                        <Button\n                                            loading={loading}\n                                            onClick={() =>\n                                                loadGitHubEvent(triggeredBy.externalEventId)\n                                            }>\n                                            Load Data\n                                        </Button>\n                                    }\n                                    content=\"Click to load the event's data.\"\n                                />\n                            </Table.Cell>\n                        </Table.Row>\n                    </Table.Body>\n                </Table>\n\n                {getEventPayload(auditEntry) && (\n                    <>\n                        <Divider horizontal={true}>{title} Notification</Divider>\n                        {type === 'github' && (\n                            <Table definition={true}>\n                                <Table.Row>\n                                    <Table.Cell collapsing={true}>Event Type</Table.Cell>\n                                    <Table.Cell>{getGitHubEvent(auditEntry)}</Table.Cell>\n                                </Table.Row>\n                            </Table>\n                        )}\n                        <ReactJson\n                            src={getEventPayload(auditEntry)}\n                            name={null}\n                            enableClipboard={true}\n                            displayDataTypes={false}\n                        />\n                    </>\n                )}\n\n                <Divider horizontal={true}>Process Details</Divider>\n\n                <ReactJson\n                    src={triggeredBy}\n                    collapsed={false}\n                    name={null}\n                    enableClipboard={true}\n                    displayDataTypes={false}\n                />\n            </Modal.Content>\n        </Modal>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/UserInfo/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Wal-Mart Store, Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport {\n    List,\n    ListContent,\n    ListDescription,\n    ListItem,\n    Loader\n} from 'semantic-ui-react';\n\nimport { get as apiGet, UserInfoEntry } from '../../../api/profile/user';\nimport {RequestErrorMessage, WithCopyToClipboard} from '../../molecules';\nimport { useApi } from '../../../hooks/useApi';\nimport {Link} from \"react-router\";\n\nexport default () => {\n    const { data, error, isLoading } = useApi<UserInfoEntry>(apiGet, {\n        fetchOnMount: true\n    });\n\n    if (error) {\n        return <RequestErrorMessage error={error} />;\n    }\n\n    if (isLoading) {\n        return <Loader active={true} />;\n    }\n\n    if (!data) {\n        return <p>There are no User info available</p>;\n    }\n\n    return (\n        <div style={{padding: '.85714286em 1.14285714em'}}>\n            <h5>Name</h5>\n            <List divided={true} relaxed={true}>\n                <ListItem>\n                    <ListContent>\n                        <ListDescription>{data.displayName}</ListDescription>\n                    </ListContent>\n                </ListItem>\n            </List>\n\n            <h5>Internal ID</h5>\n            <List divided={true} relaxed={true}>\n                <ListItem>\n                    <ListContent>\n                        <ListDescription><WithCopyToClipboard value={data.id}>{data.id}</WithCopyToClipboard></ListDescription>\n                    </ListContent>\n                </ListItem>\n            </List>\n\n            <h5>Roles</h5>\n            <List divided={true} relaxed={true}>\n                {data.roles && data.roles\n                    .sort((a, b) => a.localeCompare(b))\n                    .map((role, index) => (\n                        <ListItem key={index}>\n                            <ListContent>\n                                <ListDescription>{role}</ListDescription>\n                            </ListContent>\n                        </ListItem>\n                    ))}\n            </List>\n\n            <h5>Teams</h5>\n            <List divided={true} relaxed={true}>\n                {data.teams && data.teams\n                    .sort((a, b) => {\n                        const aConcat = `${a.orgName}/${a.teamName}`;\n                        const bConcat = `${b.orgName}/${b.teamName}`;\n                        return aConcat.localeCompare(bConcat);\n                    })\n                    .map((team, index) => (\n                        <ListItem key={index}>\n                            <ListContent>\n                                <ListDescription>\n                                    <Link\n                                        to={`/org/${team.orgName}/team/${team.teamName}`}>{team.orgName} / {team.teamName} ({team.role})</Link>\n                                </ListDescription>\n                            </ListContent>\n                        </ListItem>\n                    ))}\n            </List>\n\n            {data.ldapGroups && data.ldapGroups.length > 0 && (\n                <>\n                    <h5>LDAP Groups</h5>\n                    <List divided={true} relaxed={true}>\n                        {data.ldapGroups.map((group, index) => (\n                            <ListItem key={index}>\n                                <ListContent>\n                                    <ListDescription>{group}</ListDescription>\n                                </ListContent>\n                            </ListItem>\n                        ))}\n                    </List>\n                </>\n            )}\n        </div>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/organisms/UserProcessActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport {\n    getActivity as apiGetActivity,\n    listProcessCards as apiListProcessCards, ProcessCardEntry\n} from '../../../api/service/console/user';\nimport {Button, Card, CardGroup, Header, Icon, Image, Modal, Popup, PopupContent} from 'semantic-ui-react';\nimport { ProcessList } from '../../molecules/index';\nimport { ProcessEntry } from '../../../api/process';\nimport {\n    CREATED_AT_COLUMN,\n    INITIATOR_COLUMN,\n    REPO_COLUMN,\n    INSTANCE_ID_COLUMN,\n    PROJECT_COLUMN,\n    STATUS_COLUMN\n} from '../../molecules/ProcessList';\nimport { useCallback, useEffect, useState } from 'react';\nimport { useApi } from '../../../hooks/useApi';\nimport { LoadingDispatch } from '../../../App';\nimport RequestErrorActivity from '../RequestErrorActivity';\nimport {Link} from \"react-router\";\nimport './styles.css';\n\nexport interface ExternalProps {\n    forceRefresh: any;\n}\n\nconst MAX_OWN_PROCESSES = 10;\n\nconst renderCard = (card: ProcessCardEntry) => {\n    return (\n        <Card key={card.id}>\n            <Card.Content>\n                {card.icon && <Image floated='right' size='mini' src={`data:image/png;base64, ${card.icon}`}/>}\n                <Card.Header>{card.name}&nbsp;\n                    <Popup trigger={<a href={`/#/processCard/${card.id}/form`} target=\"_blank\" rel=\"noreferrer\"><Icon name=\"share\" color=\"grey\"/></a>}>\n                        <PopupContent>\n                            Shareable link to this process card.\n                        </PopupContent>\n                    </Popup>\n                </Card.Header>\n\n                <Card.Meta>\n                    Project: <Link to={`/org/${card.orgName}/project/${card.projectName}`}>{card.projectName}</Link>\n                </Card.Meta>\n\n                <Card.Description>{card.description}</Card.Description>\n            </Card.Content>\n\n            <Card.Content extra>\n                <div className='ui two buttons'>\n                    {card.isCustomForm &&\n                        <Modal trigger={<Button basic color='green'>Start process</Button>}>\n                            <Modal.Content>\n                                <div className={\"ui active embed\"}>\n                                    <iframe title={card.id}\n                                            src={`/api/v1/processcard/${card.id}/form`}\n                                            height={\"100%\"}\n                                            width={\"100%\"}\n                                            frameBorder={0}\n                                            allowFullScreen={true}/>\n                                </div>\n                            </Modal.Content>\n                        </Modal>\n                    }\n\n                    {!card.isCustomForm &&\n                        <Button basic color='green' href={`/api/v1/org/${card.orgName}/project/${card.projectName}/repo/${card.repoName}/start/${card.entryPoint}`} target=\"_blank\" rel=\"noopener noreferrer\">Start process</Button>\n                    }\n                </div>\n            </Card.Content>\n        </Card>\n    );\n};\n\nconst renderCards = (cards: ProcessCardEntry[]) => {\n    if (cards.length === 0) {\n        return;\n    }\n\n    return (\n        <CardGroup>\n            {cards.map((card) => renderCard(card))}\n        </CardGroup>\n    );\n};\n\ninterface ActivityEntry {\n    processes?: ProcessEntry[];\n    cards?: ProcessCardEntry[];\n}\n\nconst UserProcesses = ({ forceRefresh }: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const [processes, setProcesses] = useState<ProcessEntry[]>();\n    const [processCards, setProcessCards] = useState<ProcessCardEntry[]>();\n    const [processCardsShow, toggleProcessCardsShow] = useState<boolean>(true);\n\n    const processCardsShowHandler = useCallback(() => {\n        toggleProcessCardsShow((prevState) => !prevState);\n    }, []);\n\n    const fetchData = useCallback(async () => {\n        const activity = await apiGetActivity(MAX_OWN_PROCESSES);\n        const cards = await apiListProcessCards();\n        return { processes: activity.processes, cards}\n    }, []);\n\n    const { data, error } = useApi<ActivityEntry>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    useEffect(() => {\n        if (!data) {\n            return;\n        }\n\n        setProcesses(data.processes);\n        setProcessCards(data.cards)\n    }, [data]);\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n\n            {processCards && processCards.length > 0 &&\n                <Header dividing={true} as=\"h3\" className={\"no-margin-top\"}>\n                    Actions\n                    <Icon link={true} name={processCardsShow ? \"angle down\" : \"angle left\"} onClick={processCardsShowHandler} style={{fontSize: '1em', verticalAlign: 'top'}}></Icon>\n                </Header>\n            }\n\n            {processCardsShow && processCards && renderCards(processCards)}\n\n            <Header dividing={true} as=\"h3\">\n                Your last {MAX_OWN_PROCESSES} processes\n            </Header>\n            <ProcessList\n                data={processes}\n                columns={[\n                    STATUS_COLUMN,\n                    INSTANCE_ID_COLUMN,\n                    PROJECT_COLUMN,\n                    REPO_COLUMN,\n                    INITIATOR_COLUMN,\n                    CREATED_AT_COLUMN\n                ]}\n            />\n        </>\n    );\n};\n\nexport default UserProcesses;\n"
  },
  {
    "path": "console2/src/components/organisms/UserProcessActivity/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n.ui.cards>.card {\n    border: 1px solid #D4D4ED;\n    box-shadow: none;\n    border-radius: 0;\n}\n\n.no-margin-top {\n    margin-top: 0 !important;\n}\n"
  },
  {
    "path": "console2/src/components/organisms/ValidateRepositoryPopup/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\n\nimport {ConcordKey} from '../../../api/common';\nimport { SingleOperationPopup } from '../../molecules';\n\nimport './styles.css';\nimport { SemanticCOLORS, SemanticICONS } from 'semantic-ui-react';\nimport {useCallback} from \"react\";\nimport {RepositoryValidationResponse, validateRepository as apiValidateRepo} from \"../../../api/org/project/repository\";\nimport {useApi} from \"../../../hooks/useApi\";\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    repoName: ConcordKey;\n    trigger: (onClick: () => void) => React.ReactNode;\n}\n\nconst ValidateRepositoryPopup = (props: ExternalProps) => {\n    const {orgName, projectName, repoName, trigger} = props;\n\n    const validateRepoRequest = useCallback(() => {\n        return apiValidateRepo(orgName, projectName, repoName);\n    }, [orgName, projectName, repoName]);\n\n    const { data, isLoading, error, clearState, fetch } = useApi<RepositoryValidationResponse>(\n        validateRepoRequest,\n        { fetchOnMount: false, requestByFetch: true }\n    );\n\n    const resetHandler = useCallback(() => {\n        clearState();\n    }, [clearState]);\n\n\n    let title = 'Validate repository?';\n\n    let icon: SemanticICONS | undefined;\n    let iconColor: SemanticCOLORS | undefined;\n    let msg;\n\n    if (error) {\n        icon = 'exclamation circle';\n        iconColor = 'red';\n        title = 'Validation error';\n    }\n\n    if (data?.ok) {\n        title = 'Validation complete';\n        icon = 'check circle';\n        iconColor = 'green';\n        msg = <p>Repository validated successfully.</p>;\n    }\n\n    const warnings = data?.warnings;\n    let warningDetails;\n    if (warnings !== undefined && warnings.length > 0) {\n        icon = 'warning circle';\n        iconColor = 'yellow';\n        warningDetails = (\n            <>\n                <p>Warnings:</p>\n                <ul>\n                    {warnings.map((e) => (\n                        <li>{e}</li>\n                    ))}\n                </ul>\n            </>\n        );\n    }\n\n    const errors = data?.errors;\n    let errorDetails;\n    if (errors !== undefined && errors.length > 0) {\n        icon = 'exclamation circle';\n        iconColor = 'red';\n        errorDetails = (\n            <>\n                <p>Errors:</p>\n                <ul>\n                    {errors.map((e) => (\n                        <li>{e}</li>\n                    ))}\n                </ul>\n            </>\n        );\n    }\n\n    return (\n        <SingleOperationPopup\n            trigger={trigger}\n            title={title}\n            icon={icon}\n            iconColor={iconColor}\n            introMsg={\n                <p>\n                    Run syntax validation for <b>{repoName}</b> repository?\n                </p>\n            }\n            running={isLoading}\n            success={data?.ok || false}\n            successMsg={\n                <>\n                    {msg}\n                    {warningDetails}\n                    {errorDetails}\n                </>\n            }\n            error={error}\n            reset={resetHandler}\n            onConfirm={fetch}\n        />\n    );\n};\n\nexport default ValidateRepositoryPopup;"
  },
  {
    "path": "console2/src/components/organisms/ValidateRepositoryPopup/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n.resultBox {\n    width: 100%;\n    font-family: monospace;\n    white-space: pre-wrap;\n}\n"
  },
  {
    "path": "console2/src/components/organisms/ansible/AnsibleTaskActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { useCallback, useEffect, useState } from 'react';\nimport { Loader } from 'semantic-ui-react';\n\nimport {\n    AnsibleEvent,\n    AnsibleStatus,\n    listAnsibleEvents as apiListAnsibleEvents\n} from '../../../../api/process/ansible';\nimport { ProcessEventEntry } from '../../../../api/process/event';\nimport { ConcordId, RequestError } from '../../../../api/common';\nimport { AnsibleTaskList, RequestErrorMessage } from '../../../molecules';\nimport { combinePrePostEvents } from '../../ProcessEventsActivity';\n\ninterface Props {\n    instanceId: ConcordId;\n    playbookId?: ConcordId;\n    host?: string;\n    hostGroup?: string;\n    status?: AnsibleStatus;\n    showHosts?: boolean;\n}\n\nconst sortAnsibleEvents = (events: Array<ProcessEventEntry<AnsibleEvent>>) =>\n    events\n        .filter((value) => value.data.status !== undefined)\n        .sort((a, b) => (a.eventDate > b.eventDate ? 1 : a.eventDate < b.eventDate ? -1 : 0));\n\nconst AnsibleTaskActivity = ({\n    instanceId,\n    playbookId,\n    host,\n    hostGroup,\n    status,\n    showHosts\n}: Props) => {\n    const [loading, setLoading] = useState(false);\n    const [events, setEvents] = useState<Array<ProcessEventEntry<AnsibleEvent>>>([]);\n    const [error, setError] = useState<RequestError>();\n\n    const loadEvents = useCallback(async () => {\n        setLoading(true);\n        setError(undefined);\n\n        try {\n            const response = await apiListAnsibleEvents(\n                instanceId,\n                host,\n                hostGroup,\n                status,\n                playbookId\n            );\n\n            setEvents(combinePrePostEvents(sortAnsibleEvents(response)));\n        } catch (e) {\n            setError(e);\n        } finally {\n            setLoading(false);\n        }\n    }, [host, hostGroup, instanceId, playbookId, status]);\n\n    useEffect(() => {\n        loadEvents();\n    }, [loadEvents]);\n\n    if (error) {\n        return <RequestErrorMessage error={error} />;\n    }\n\n    if (loading) {\n        return <Loader active={true} />;\n    }\n\n    if (events.length === 0) {\n        return <h4>No failures detected.</h4>;\n    }\n\n    return <AnsibleTaskList title={host} showHosts={showHosts} tasks={events} />;\n};\n\nexport default AnsibleTaskActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ansible/ProcessAnsibleActivity/PlayInfoList.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { Label, Popup, Progress, SemanticCOLORS, Table } from 'semantic-ui-react';\n\nimport {\n    FAILED_COLOR,\n    OK_COLOR,\n    PlayInfoEntry,\n    RUNNING_COLOR,\n    SKIPPED_COLOR,\n    UNREACHABLE_COLOR\n} from './types';\nimport { ConcordId } from '../../../../api/common';\n\nexport interface PlayStatsProps {\n    stats?: PlayInfoEntry[];\n    playbookStatus: string;\n    selectedPlayId?: ConcordId;\n    playClickAction: (id: ConcordId) => void;\n}\n\nconst renderTaskCount = (value: number, label: string, color?: SemanticCOLORS) => {\n    return (\n        <Popup\n            content={label}\n            mouseEnterDelay={500}\n            trigger={\n                <Label\n                    color={color}\n                    horizontal\n                    className={value > 0 ? 'taskCounterActive' : 'taskCounterNonactive'}>\n                    {value}\n                </Label>\n            }\n        />\n    );\n};\n\nconst renderPlayRow = (\n    s: PlayInfoEntry,\n    isPlaybookRunning: boolean,\n    index: number,\n    playClickAction: (id: ConcordId) => void,\n    selectedPlayId?: ConcordId\n) => {\n    return (\n        <Table.Row\n            key={index}\n            onClick={() => playClickAction(s.id)}\n            style={{ cursor: 'pointer' }}\n            className={selectedPlayId === s.id ? 'selectedPlay' : ''}>\n            <Table.Cell singleLine={true}>{s.play}</Table.Cell>\n            <Table.Cell singleLine={true} textAlign={'center'} collapsing={true}>\n                {s.hostsCount}\n            </Table.Cell>\n            <Table.Cell width={1} textAlign={'center'} collapsing={true} singleLine={true}>\n                {s.tasksCount}\n            </Table.Cell>\n\n            <Table.Cell singleLine={true} collapsing={true}>\n                {renderTaskCount(s.taskStats.ok, 'OK', OK_COLOR)}\n                {renderTaskCount(s.taskStats.failed, 'FAILED', FAILED_COLOR)}\n                {renderTaskCount(s.taskStats.unreachable, 'UNREACHABLE', UNREACHABLE_COLOR)}\n                {renderTaskCount(s.taskStats.skipped, 'SKIPPED', SKIPPED_COLOR)}\n                {renderTaskCount(s.taskStats.running, 'RUNNING', RUNNING_COLOR)}\n            </Table.Cell>\n\n            <Table.Cell singleLine={true} width={2}>\n                <Progress\n                    percent={s.progress}\n                    active={isPlaybookRunning && s.progress > 0 && s.progress < 100}\n                    progress={'percent'}\n                    className={'playProgress ' + (s.progress > 0 ? '' : 'nonactive')}\n                />\n            </Table.Cell>\n        </Table.Row>\n    );\n};\n\nconst renderElements = ({\n    stats,\n    playbookStatus,\n    playClickAction,\n    selectedPlayId\n}: PlayStatsProps) => {\n    if (!stats) {\n        return (\n            <tr style={{ fontWeight: 'bold' }}>\n                <Table.Cell colSpan={4}>-</Table.Cell>\n            </tr>\n        );\n    }\n\n    if (stats.length === 0) {\n        return (\n            <tr style={{ fontWeight: 'bold' }}>\n                <Table.Cell colSpan={4}>No data available</Table.Cell>\n            </tr>\n        );\n    }\n\n    return stats.map((value, index) =>\n        renderPlayRow(value, playbookStatus === 'RUNNING', index, playClickAction, selectedPlayId)\n    );\n};\n\nconst PlayInfoList = (props: PlayStatsProps) => {\n    return (\n        <Table\n            compact={true}\n            basic={true}\n            selectable={true}\n            className={props.stats ? '' : 'loading'}>\n            <Table.Header>\n                <Table.Row>\n                    <Table.HeaderCell>Play</Table.HeaderCell>\n                    <Table.HeaderCell>Hosts</Table.HeaderCell>\n                    <Table.HeaderCell singleLine={true} collapsing={true}>\n                        Unique Tasks\n                    </Table.HeaderCell>\n                    <Table.HeaderCell colSpan={2} collapsing={true}>\n                        Progress (tasks * hosts)\n                    </Table.HeaderCell>\n                </Table.Row>\n            </Table.Header>\n            <Table.Body>{renderElements(props)}</Table.Body>\n        </Table>\n    );\n};\n\nexport default PlayInfoList;\n"
  },
  {
    "path": "console2/src/components/organisms/ansible/ProcessAnsibleActivity/PlaybookChooser.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { Dropdown, Table } from 'semantic-ui-react';\n\nimport { ConcordId } from '../../../../api/common';\nimport { memo } from 'react';\n\nexport interface PlaybookEntry {\n    value: ConcordId;\n    text: string;\n}\n\nexport interface ExternalProps {\n    currentValue?: ConcordId;\n    options?: PlaybookEntry[];\n    onPlaybookChange: (flowCorrelationId: ConcordId) => void;\n}\n\nconst PlaybookChooser = memo(({ currentValue, options, onPlaybookChange }: ExternalProps) => {\n    return (\n        <Table basic={'very'} compact={true} singleLine={true}>\n            <Table.Body>\n                <Table.Row>\n                    <Table.Cell\n                        style={{ fontWeight: 'bold' }}\n                        width={1}\n                        className={currentValue ? '' : 'loading'}>\n                        Playbook:\n                    </Table.Cell>\n                    <Table.Cell width={15}>\n                        <Dropdown\n                            selection={options !== undefined ? true : undefined}\n                            value={currentValue}\n                            options={options}\n                            onChange={(ev, data) => onPlaybookChange(data.value as ConcordId)}\n                            disabled={currentValue === undefined}\n                            style={{ width: '100%' }}\n                        />\n                    </Table.Cell>\n                </Table.Row>\n            </Table.Body>\n        </Table>\n    );\n});\n\nexport default PlaybookChooser;\n"
  },
  {
    "path": "console2/src/components/organisms/ansible/ProcessAnsibleActivity/PlaybookStats.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { Card, SemanticCOLORS, Statistic } from 'semantic-ui-react';\n\nimport { memo } from 'react';\n\nexport enum Blocks {\n    'hosts',\n    'failedHosts',\n    'plays',\n    'failedTasks'\n}\n\nexport interface PlaybookStatsProps {\n    hostsCount?: number;\n    failedHostsCount?: number;\n    playsCount?: number;\n    failedTasksCount?: number;\n    selectedBlock?: Blocks;\n    onBlockChange: (block: Blocks) => void;\n}\n\nconst activeBlockColor: SemanticCOLORS = 'green';\n\nconst PlaybookStats = memo((props: PlaybookStatsProps) => {\n    const {\n        hostsCount,\n        failedHostsCount,\n        playsCount,\n        failedTasksCount,\n        selectedBlock,\n        onBlockChange\n    } = props;\n\n    return (\n        <>\n            <Card.Group itemsPerRow={4} className={hostsCount ? '' : 'loading'}>\n                <Card\n                    onClick={\n                        hostsCount && hostsCount > 0 ? () => onBlockChange(Blocks.hosts) : undefined\n                    }\n                    color={selectedBlock === Blocks.hosts ? activeBlockColor : undefined}\n                    className={selectedBlock === Blocks.hosts ? 'playbookStatsBlockToggled' : ''}>\n                    <Card.Content textAlign={'center'}>\n                        <Statistic color=\"black\" size={'tiny'}>\n                            <Statistic.Value>{hostsCount}</Statistic.Value>\n                            <Statistic.Label className={'playbookStatsLabel'}>\n                                UNIQUE HOSTS\n                            </Statistic.Label>\n                        </Statistic>\n                    </Card.Content>\n                </Card>\n                <Card\n                    onClick={\n                        failedHostsCount && failedHostsCount > 0\n                            ? () => onBlockChange(Blocks.failedHosts)\n                            : undefined\n                    }\n                    color={selectedBlock === Blocks.failedHosts ? activeBlockColor : undefined}\n                    className={\n                        selectedBlock === Blocks.failedHosts ? 'playbookStatsBlockToggled' : ''\n                    }>\n                    <Card.Content textAlign={'center'}>\n                        <Statistic\n                            color={failedHostsCount && failedHostsCount > 0 ? 'red' : 'black'}\n                            size={'tiny'}>\n                            <Statistic.Value>{failedHostsCount}</Statistic.Value>\n                            <Statistic.Label className={'playbookStatsLabel'}>\n                                FAILED HOSTS\n                            </Statistic.Label>\n                        </Statistic>\n                    </Card.Content>\n                </Card>\n                <Card\n                    onClick={\n                        playsCount && playsCount > 0 ? () => onBlockChange(Blocks.plays) : undefined\n                    }\n                    color={selectedBlock === Blocks.plays ? activeBlockColor : undefined}\n                    className={selectedBlock === Blocks.plays ? 'playbookStatsBlockToggled' : ''}>\n                    <Card.Content textAlign={'center'}>\n                        <Statistic color=\"black\" size={'tiny'}>\n                            <Statistic.Value>{playsCount}</Statistic.Value>\n                            <Statistic.Label className={'playbookStatsLabel'}>\n                                PLAYS\n                            </Statistic.Label>\n                        </Statistic>\n                    </Card.Content>\n                </Card>\n                <Card\n                    onClick={\n                        failedTasksCount && failedTasksCount > 0\n                            ? () => onBlockChange(Blocks.failedTasks)\n                            : undefined\n                    }\n                    color={selectedBlock === Blocks.failedTasks ? activeBlockColor : undefined}\n                    className={\n                        selectedBlock === Blocks.failedTasks ? 'playbookStatsBlockToggled' : ''\n                    }>\n                    <Card.Content textAlign={'center'}>\n                        <Statistic\n                            color={failedTasksCount && failedTasksCount > 0 ? 'red' : 'black'}\n                            size={'tiny'}>\n                            <Statistic.Value>{failedTasksCount}</Statistic.Value>\n                            <Statistic.Label className={'playbookStatsLabel'}>\n                                FAILED TASKS\n                            </Statistic.Label>\n                        </Statistic>\n                    </Card.Content>\n                </Card>\n            </Card.Group>\n        </>\n    );\n});\n\nexport default PlaybookStats;\n"
  },
  {
    "path": "console2/src/components/organisms/ansible/ProcessAnsibleActivity/TaskProgress.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport React from 'react';\n\nimport './styles.css';\nimport {\n    FAILED_COLOR_HEX,\n    OK_COLOR_HEX,\n    RUNNING_COLOR_HEX,\n    SKIPPED_COLOR_HEX,\n    UNREACHABLE_COLOR_HEX\n} from './types';\n\nexport interface ProgressStats {\n    failed: number;\n    ok: number;\n    unreachable: number;\n    skipped: number;\n    running: number;\n}\n\nexport interface ProgressProps {\n    total: number;\n    stats: ProgressStats;\n}\n\nclass TaskProgress extends React.PureComponent<ProgressProps> {\n    render() {\n        const { failed, ok, unreachable, skipped, running } = this.props.stats;\n        const { total } = this.props;\n\n        if (total <= 0) {\n            return <div />;\n        }\n\n        const t = 100 / total;\n        const current = failed + ok + unreachable + skipped + running;\n        const unknown = total > current ? total - current : 0;\n        return (\n            <div\n                className=\"progress\"\n                style={{\n                    width: '400px',\n                    height: '20px',\n                    position: 'relative',\n                    overflow: 'hidden'\n                }}>\n                <div\n                    className=\"ok\"\n                    style={{\n                        backgroundColor: OK_COLOR_HEX,\n                        position: 'absolute',\n                        left: 0,\n                        width: '100%',\n                        paddingLeft: '5px',\n                        color: '#FFFFFF'\n                    }}>\n                    {ok}\n                </div>\n                <div\n                    className=\"failed\"\n                    style={{\n                        backgroundColor: FAILED_COLOR_HEX,\n                        position: 'absolute',\n                        left: t * ok + '%',\n                        width: '100%',\n                        paddingLeft: '5px',\n                        color: '#FFFFFF'\n                    }}>\n                    {failed}\n                </div>\n                <div\n                    className=\"unreachable\"\n                    style={{\n                        backgroundColor: UNREACHABLE_COLOR_HEX,\n                        position: 'absolute',\n                        left: t * ok + t * failed + '%',\n                        width: '100%',\n                        paddingLeft: '5px',\n                        color: '#FFFFFF'\n                    }}>\n                    {unreachable}\n                </div>\n                <div\n                    className=\"skipped\"\n                    style={{\n                        backgroundColor: SKIPPED_COLOR_HEX,\n                        position: 'absolute',\n                        left: t * ok + t * failed + t * unreachable + '%',\n                        width: '100%',\n                        paddingLeft: '5px',\n                        color: '#FFFFFF'\n                    }}>\n                    {skipped}\n                </div>\n                <div\n                    className=\"running\"\n                    style={{\n                        backgroundColor: RUNNING_COLOR_HEX,\n                        position: 'absolute',\n                        left: t * ok + t * failed + t * unreachable + t * skipped + '%',\n                        width: '100%',\n                        paddingLeft: '5px',\n                        color: '#00000099',\n                        textShadow: '1px 1px #FFFFFF'\n                    }}>\n                    {running}\n                </div>\n                {unknown > 0 && (\n                    <div\n                        className=\"unknown\"\n                        style={{\n                            backgroundColor: 'white',\n                            position: 'absolute',\n                            left:\n                                t * ok +\n                                t * failed +\n                                t * unreachable +\n                                t * skipped +\n                                t * running +\n                                '%',\n                            width: '100%',\n                            paddingLeft: '5px',\n                            color: '#656565'\n                        }}>\n                        &nbsp;\n                    </div>\n                )}\n            </div>\n        );\n    }\n}\n\nexport default TaskProgress;\n"
  },
  {
    "path": "console2/src/components/organisms/ansible/ProcessAnsibleActivity/TaskProgressLegend.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport React from 'react';\nimport {\n    FAILED_COLOR_HEX,\n    OK_COLOR_HEX,\n    RUNNING_COLOR_HEX,\n    SKIPPED_COLOR_HEX,\n    UNREACHABLE_COLOR_HEX\n} from './types';\nimport { Grid } from 'semantic-ui-react';\n\nexport interface ExternalProps {\n    loading: boolean;\n}\n\nconst LEGEND_ITEMS = [\n    { name: 'OK', color: OK_COLOR_HEX },\n    { name: 'FAILED', color: FAILED_COLOR_HEX },\n    { name: 'UNREACHABLE', color: UNREACHABLE_COLOR_HEX },\n    { name: 'SKIPPED', color: SKIPPED_COLOR_HEX },\n    { name: 'RUNNING', color: RUNNING_COLOR_HEX }\n];\n\nconst renderLegendItem = (item: { color: string; name: string }) => {\n    return (\n        <Grid.Column key={item.name}>\n            <h5 className=\"ui image header\">\n                <svg width={16} height={16} className=\"taskLegendColor\">\n                    <g key={item.name}>\n                        <rect x={0} y={0} width={16} height={16} fill={item.color} />\n                    </g>\n                </svg>\n                <div className=\"content taskLegendContent\">{item.name}</div>\n            </h5>\n        </Grid.Column>\n    );\n};\n\nconst TaskProgressLegend = ({ loading }: ExternalProps) => {\n    return (\n        <Grid columns={7} centered={true} className={loading ? 'loading' : ''}>\n            {LEGEND_ITEMS.map((item) => renderLegendItem(item))}\n        </Grid>\n    );\n};\n\nexport default TaskProgressLegend;\n"
  },
  {
    "path": "console2/src/components/organisms/ansible/ProcessAnsibleActivity/TaskStats.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { Table } from 'semantic-ui-react';\nimport * as React from 'react';\nimport TaskProgress, { ProgressStats } from './TaskProgress';\nimport { TaskInfoEntry } from './types';\nimport { memo } from 'react';\n\nexport interface TaskStatsProps {\n    totalTaskWork: number;\n    tasks?: TaskInfoEntry[];\n}\n\nconst renderTaskRow = (\n    name: string,\n    type: string,\n    stats: ProgressStats,\n    total: number,\n    index: number\n) => {\n    return (\n        <Table.Row key={index} style={type !== 'TASK' ? { opacity: 0.5 } : {}}>\n            <Table.Cell\n                collapsing={true}\n                singleLine={true}\n                textAlign={'right'}\n                className=\"taskProgressCell\">\n                {name}\n            </Table.Cell>\n            <Table.Cell collapsing={true} singleLine={true} className=\"taskProgressCell\">\n                <TaskProgress total={total} stats={stats} />\n            </Table.Cell>\n        </Table.Row>\n    );\n};\n\nconst renderTableBody = (totalTaskWork: number, tasks?: TaskInfoEntry[]) => {\n    if (!tasks) {\n        return (\n            <Table.Row style={{ fontWeight: 'bold' }}>\n                <Table.Cell\n                    collapsing={true}\n                    singleLine={true}\n                    textAlign={'right'}\n                    className=\"taskProgressCell\">\n                    -\n                </Table.Cell>\n                <Table.Cell>&nbsp;</Table.Cell>\n            </Table.Row>\n        );\n    }\n\n    return tasks.map((value, index) =>\n        renderTaskRow(value.name, value.type, value.stats, totalTaskWork, index)\n    );\n};\n\nconst TaskStats = memo(({ totalTaskWork, tasks }: TaskStatsProps) => {\n    return (\n        <>\n            <Table compact={true} basic={'very'} columns={2} className={tasks ? '' : 'loading'}>\n                <Table.Header>\n                    <Table.Row>\n                        <Table.HeaderCell collapsing={true} singleLine={true} textAlign={'right'}>\n                            Task\n                        </Table.HeaderCell>\n                        <Table.HeaderCell collapsing={true} singleLine={true}>\n                            Host execution count\n                        </Table.HeaderCell>\n                    </Table.Row>\n                </Table.Header>\n                <Table.Body>{renderTableBody(totalTaskWork, tasks)}</Table.Body>\n            </Table>\n        </>\n    );\n});\n\nexport default TaskStats;\n"
  },
  {
    "path": "console2/src/components/organisms/ansible/ProcessAnsibleActivity/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport { Divider, Progress, Segment } from 'semantic-ui-react';\n\nimport {\n    AnsibleEvent,\n    AnsibleHost,\n    AnsibleStatus,\n    listAnsibleHosts as apiListAnsibleHosts,\n    listAnsiblePlaybooks as apiListAnsiblePlaybooks,\n    listAnsiblePlays as apiListAnsiblePlays,\n    listAnsibleTasks as apiListAnsibleTasks,\n    listAnsibleTaskStats as apiListAnsibleTaskStats,\n    PaginatedAnsibleHostEntries,\n    PlaybookInfo,\n    PlayInfo,\n    SearchFilter,\n    TaskInfo\n} from '../../../../api/process/ansible';\n\nimport { AnsibleHostList, AnsibleTaskList } from '../../../molecules';\nimport RequestErrorActivity from '../../RequestErrorActivity';\nimport { PlayInfoEntry, TaskInfoEntry } from './types';\nimport TaskProgressLegend from './TaskProgressLegend';\nimport PlayInfoList from './PlayInfoList';\nimport { get as apiGet, isFinal, ProcessEntry } from '../../../../api/process';\nimport { usePolling } from '../../../../api/usePolling';\nimport { ConcordId } from '../../../../api/common';\n\nimport './styles.css';\nimport TaskStats from './TaskStats';\nimport PlaybookChooser, { PlaybookEntry } from './PlaybookChooser';\nimport PlaybookStats, { Blocks } from './PlaybookStats';\nimport { ProcessEventEntry } from '../../../../api/process/event';\nimport { addMinutes, isBefore, parseISO as parseDate } from 'date-fns';\nimport { formatTimestamp } from '../../../../utils';\n\ninterface ExternalProps {\n    instanceId: ConcordId;\n    loadingHandler: (inc: number) => void;\n    forceRefresh: boolean;\n    dataFetchInterval: number;\n}\n\nconst ANSIBLE_HOST_LIMIT = 10;\n\nconst ProcessAnsibleActivity = (props: ExternalProps) => {\n    const { instanceId, loadingHandler, forceRefresh, dataFetchInterval } = props;\n\n    const [process, setProcess] = useState<ProcessEntry>();\n    const [playbooks, setPlaybooks] = useState<PlaybookInfo[]>();\n    const [playbookOptions, setPlaybookOptions] = useState<PlaybookEntry[]>();\n    const [selectedPlaybookId, setSelectedPlaybookId] = useState<ConcordId>();\n    const [selectedBlock, setSelectedBlock] = useState<Blocks>();\n    const [ansibleHosts, setAnsibleHosts] = useState<AnsibleHost[]>();\n    const [ansibleHostGroups, setAnsibleHostGroups] = useState<string[]>([]);\n    const [ansibleHostsNext, setAnsibleHostsNext] = useState<number>();\n    const [ansibleHostsPrev, setAnsibleHostsPrev] = useState<number>();\n    const [ansibleHostsFilter, setAnsibleHostsFilter] = useState({});\n    const [failedAnsibleHosts, setFailedAnsibleHosts] = useState<AnsibleHost[]>();\n    const [failedAnsibleHostGroups, setFailedAnsibleHostGroups] = useState<string[]>([]);\n    const [failedAnsibleHostsNext, setFailedAnsibleHostsNext] = useState<number>();\n    const [failedAnsibleHostsPrev, setFailedAnsibleHostsPrev] = useState<number>();\n    const [failedAnsibleHostsFilter, setFailedAnsibleHostsFilter] = useState({});\n    const [failedTasks, setFailedTasks] = useState<ProcessEventEntry<AnsibleEvent>[]>();\n    const [playStats, setPlayStats] = useState<PlayInfoEntry[]>();\n    const [taskStats, setTaskStats] = useState<TaskInfoEntry[]>();\n\n    const [selectedPlayId, setSelectedPlayId] = useState<ConcordId>();\n\n    const fetchAnsibleHosts = useCallback((instanceId: ConcordId, filter: SearchFilter): Promise<\n        PaginatedAnsibleHostEntries\n    > => {\n        const limit = filter.limit || ANSIBLE_HOST_LIMIT;\n        return apiListAnsibleHosts(instanceId, {\n            ...filter,\n            limit\n        });\n    }, []);\n\n    const fetchData = useCallback(async () => {\n        const process = await apiGet(instanceId, []);\n        setProcess(process);\n\n        let playbooks = await apiListAnsiblePlaybooks(instanceId);\n        playbooks = playbooks.sort((a, b) =>\n            a.startedAt < b.startedAt ? -1 : a.startedAt > b.startedAt ? 1 : 0\n        );\n\n        setPlaybooks(playbooks);\n        setPlaybookOptions((prevState) => buildPlaybookOptions(playbooks, prevState));\n\n        if (playbooks.length > 0 && selectedBlock !== undefined) {\n            const playbookId = selectedPlaybookId || playbooks[0].id;\n            const playbook = playbooks.find((p) => p.id === selectedPlaybookId) || playbooks[0];\n            switch (selectedBlock) {\n                case Blocks.hosts: {\n                    const newFilter = { ...ansibleHostsFilter, playbookId };\n                    const hosts = await fetchAnsibleHosts(instanceId, newFilter);\n                    setAnsibleHosts(hosts.items);\n                    setAnsibleHostGroups(hosts.hostGroups);\n                    setAnsibleHostsNext(hosts.next);\n                    setAnsibleHostsPrev(hosts.prev);\n                    break;\n                }\n                case Blocks.failedHosts: {\n                    const newFilter = {\n                        ...failedAnsibleHostsFilter,\n                        statuses: [AnsibleStatus.FAILED, AnsibleStatus.UNREACHABLE],\n                        playbookId\n                    };\n                    const hosts = await fetchAnsibleHosts(instanceId, newFilter);\n                    setFailedAnsibleHosts(hosts.items);\n                    setFailedAnsibleHostGroups(hosts.hostGroups);\n                    setFailedAnsibleHostsNext(hosts.next);\n                    setFailedAnsibleHostsPrev(hosts.prev);\n                    break;\n                }\n                case Blocks.plays: {\n                    const plays = await apiListAnsiblePlays(instanceId, playbookId);\n                    setPlayStats(makePlayStats(plays, playbook.status));\n\n                    if (selectedPlayId !== undefined) {\n                        const tasks = await apiListAnsibleTaskStats(instanceId, selectedPlayId);\n                        setTaskStats(makeTaskStats(tasks));\n                    }\n                    break;\n                }\n                case Blocks.failedTasks: {\n                    const tasks = await apiListAnsibleTasks(\n                        instanceId,\n                        playbookId,\n                        undefined,\n                        undefined,\n                        AnsibleStatus.FAILED\n                    );\n                    setFailedTasks(\n                        tasks\n                            .filter((value) => value.data.status !== undefined)\n                            .sort((a, b) =>\n                                a.eventDate > b.eventDate ? 1 : a.eventDate < b.eventDate ? -1 : 0\n                            )\n                    );\n                    break;\n                }\n            }\n        }\n\n        // because Ansible stats are calculated by an async process on the backend, we poll for\n        // additional 10 minutes after the process finishes to make sure we got everything\n        const changedRecently = isBefore(\n            Date.now(),\n            addMinutes(parseDate(process.lastUpdatedAt), 10)\n        );\n\n        return !isFinal(process.status) || changedRecently;\n    }, [\n        instanceId,\n        fetchAnsibleHosts,\n        selectedPlaybookId,\n        selectedBlock,\n        ansibleHostsFilter,\n        failedAnsibleHostsFilter,\n        selectedPlayId\n    ]);\n\n    const onPlaybookChangeHandler = useCallback((playbookId: ConcordId) => {\n        setSelectedPlaybookId(playbookId);\n        setSelectedBlock(undefined);\n        setSelectedPlayId(undefined);\n        setTaskStats(undefined);\n        setAnsibleHosts(undefined);\n        setAnsibleHostGroups([]);\n        setAnsibleHostsFilter({});\n        setFailedAnsibleHosts(undefined);\n        setFailedAnsibleHostGroups([]);\n        setFailedAnsibleHostsFilter({});\n        setFailedTasks(undefined);\n    }, []);\n\n    const onBlockChangeHandler = useCallback((block: Blocks) => {\n        setSelectedBlock(block);\n        setSelectedPlayId(undefined);\n        setTaskStats(undefined);\n        setAnsibleHosts(undefined);\n        setAnsibleHostGroups([]);\n        setAnsibleHostsFilter({});\n        setFailedAnsibleHosts(undefined);\n        setFailedAnsibleHostGroups([]);\n        setFailedAnsibleHostsFilter({});\n        setFailedTasks(undefined);\n    }, []);\n\n    const playClickHandler = useCallback((playId: ConcordId) => {\n        setSelectedPlayId(playId);\n    }, []);\n\n    useEffect(() => {\n        setTaskStats(undefined);\n    }, [selectedPlayId]);\n\n    const error = usePolling(fetchData, dataFetchInterval, loadingHandler, forceRefresh);\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n\n    if (playbooks && playbooks.length === 0) {\n        return (\n            <Segment basic={true} style={{ padding: 0 }}>\n                <div style={{ fontWeight: 'bold' }}>No data available</div>\n            </Segment>\n        );\n    }\n\n    const selectedPlaybook = findPlaybookOrFirst(selectedPlaybookId, playbooks);\n    const playbookProgress = selectedPlaybook ? selectedPlaybook.progress : -1;\n\n    let totalTaskWork = 0;\n    if (selectedPlayId !== undefined && playStats !== undefined) {\n        const p = playStats.find((p) => p.id === selectedPlayId);\n        if (p !== undefined) {\n            totalTaskWork = p.hostsCount;\n        }\n    }\n\n    const statusColor = getStatusColor(selectedPlaybook && selectedPlaybook.status);\n\n    return (\n        <>\n            <Segment basic={true} style={{ padding: 0 }}>\n                <PlaybookChooser\n                    currentValue={selectedPlaybook?.id}\n                    options={playbookOptions}\n                    onPlaybookChange={onPlaybookChangeHandler}\n                />\n            </Segment>\n\n            <PlaybookStats\n                hostsCount={selectedPlaybook?.hostsCount}\n                failedHostsCount={selectedPlaybook?.failedHostsCount}\n                playsCount={selectedPlaybook?.playsCount}\n                failedTasksCount={selectedPlaybook?.failedTasksCount}\n                selectedBlock={selectedBlock}\n                onBlockChange={onBlockChangeHandler}\n            />\n\n            <Segment basic={true} style={{ paddingLeft: 0, paddingRight: 0, paddingBottom: 0 }}>\n                <Progress\n                    size={'small'}\n                    percent={playbookProgress}\n                    progress={'percent'}\n                    active={!isFinal(process?.status) && playbookProgress < 100}\n                    color={statusColor}\n                />\n            </Segment>\n\n            {selectedBlock === Blocks.hosts && (\n                <>\n                    <Divider content=\"Host Stats\" horizontal={true} />\n\n                    <AnsibleHostList\n                        instanceId={instanceId}\n                        playbookId={selectedPlaybook?.id}\n                        showStatusFilter={true}\n                        hosts={ansibleHosts}\n                        hostGroups={ansibleHostGroups}\n                        prev={ansibleHostsPrev}\n                        next={ansibleHostsNext}\n                        refresh={setAnsibleHostsFilter}\n                    />\n                </>\n            )}\n\n            {selectedBlock === Blocks.failedHosts && (\n                <>\n                    <Divider content=\"Failed Host Stats\" horizontal={true} />\n\n                    <AnsibleHostList\n                        instanceId={instanceId}\n                        playbookId={selectedPlaybook?.id}\n                        hosts={failedAnsibleHosts}\n                        hostGroups={failedAnsibleHostGroups}\n                        prev={failedAnsibleHostsPrev}\n                        next={failedAnsibleHostsNext}\n                        refresh={setFailedAnsibleHostsFilter}\n                    />\n                </>\n            )}\n\n            {selectedBlock === Blocks.plays && selectedPlaybook && (\n                <>\n                    <Divider content=\"Play Stats\" horizontal={true} />\n\n                    <PlayInfoList\n                        stats={playStats}\n                        playClickAction={playClickHandler}\n                        playbookStatus={selectedPlaybook.status}\n                        selectedPlayId={selectedPlayId}\n                    />\n                </>\n            )}\n\n            {selectedBlock === Blocks.plays && selectedPlayId && (\n                <>\n                    <Divider content=\"Task Stats\" horizontal={true} />\n\n                    <TaskProgressLegend loading={taskStats === undefined} />\n\n                    <TaskStats totalTaskWork={totalTaskWork} tasks={taskStats} />\n                </>\n            )}\n\n            {selectedBlock === Blocks.failedTasks && (\n                <>\n                    <Divider content=\"Failed Tasks\" horizontal={true} />\n                    <div style={{ overflowX: 'auto' }}>\n                        <AnsibleTaskList showHosts={true} tasks={failedTasks} hidePlaybook={true} />\n                    </div>\n                </>\n            )}\n        </>\n    );\n};\n\nconst makePlayStats = (plays: PlayInfo[], playbookStatus: string): PlayInfoEntry[] => {\n    if (plays === undefined) {\n        return [];\n    }\n\n    const sorted = plays.sort((a, b) =>\n        a.playOrder > b.playOrder ? 1 : a.playOrder < b.playOrder ? -1 : 0\n    );\n\n    return sorted.map((s, i) => {\n        const nextPlayStarted = i + 1 < sorted.length && sorted[i + 1].finishedTaskCount > 0;\n        let progress;\n        if ((playbookStatus === 'OK' && s.finishedTaskCount > 0) || nextPlayStarted) {\n            progress = 100;\n        } else {\n            const totalWork = s.hostCount * s.taskCount;\n            progress =\n                s.finishedTaskCount > 0 ? Math.round((s.finishedTaskCount * 100) / totalWork) : 0;\n        }\n\n        return {\n            id: s.playId,\n            play: s.playName,\n            hostsCount: s.hostCount,\n            tasksCount: s.taskCount,\n            taskStats: s.taskStats,\n            progress\n        };\n    });\n};\n\nconst makeTaskStats = (tasks: TaskInfo[]): TaskInfoEntry[] => {\n    if (tasks === undefined) {\n        return [];\n    }\n\n    const sorted = tasks.sort((a, b) =>\n        a.taskOrder > b.taskOrder ? 1 : a.taskOrder < b.taskOrder ? -1 : 0\n    );\n\n    return sorted.map((s) => {\n        return {\n            name: s.taskName,\n            type: s.type,\n            stats: {\n                failed: s.failedCount,\n                ok: s.okCount,\n                unreachable: s.unreachableCount,\n                skipped: s.skippedCount,\n                running: s.runningCount\n            }\n        };\n    });\n};\n\nconst getStatusColor = (status?: string) => {\n    if (status === 'OK') {\n        return 'green';\n    } else if (status === 'FAILED') {\n        return 'red';\n    }\n\n    return undefined;\n};\n\nconst findPlaybookOrFirst = (id?: ConcordId, playbooks?: PlaybookInfo[]) => {\n    if (playbooks === undefined || playbooks.length === 0) {\n        return undefined;\n    }\n\n    return id === undefined ? playbooks[0] : playbooks.find((p) => p.id === id) || playbooks[0];\n};\n\nconst buildPlaybookOptions = (playbooks: PlaybookInfo[], oldValues?: PlaybookEntry[]) => {\n    const result = playbooks.map((s) => {\n        let retryInfo = '';\n        if (s.retryNum && s.retryNum > 0) {\n            retryInfo = ' (retry: ' + s.retryNum + ')';\n        }\n\n        return { value: s.id, text: s.name + ' @ ' + formatTimestamp(s.startedAt) + retryInfo };\n    });\n\n    if (!oldValues) {\n        return result;\n    }\n\n    if (oldValues.length !== playbooks.length) {\n        return result;\n    }\n\n    return oldValues;\n};\n\nexport default ProcessAnsibleActivity;\n"
  },
  {
    "path": "console2/src/components/organisms/ansible/ProcessAnsibleActivity/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n.playbookStatsLabel {\n    color: rgba(0, 0, 0, 0.5) !important;\n}\n\n.playbookStatsBlockToggled {\n    transform: translateY(-4px) !important;\n}\n\n.playProgress {\n    margin: 0 !important;\n}\n\n.playProgress.nonactive .bar {\n    opacity: 0;\n}\n\n.taskProgressCell {\n    border-top: 0 !important;\n}\n\n.taskLegendColor {\n    display: inline-block !important;\n    vertical-align: middle;\n}\n\n.taskLegendContent {\n    padding-left: .75rem;\n    font-weight: normal !important;\n}\n\n.taskProgress {\n    position: relative;\n    display: block;\n    max-width: 100%;\n    border: none;\n    background: rgba(0,0,0,.1);\n    padding: 0;\n}\n\n.taskProgress .bar {\n    display: block;\n    line-height: 1;\n    position: relative;\n    min-width: 2em;\n    height: 1.75em;\n    background: #888;\n}\n\n.taskProgress .bar .progress {\n    white-space: nowrap;\n    position: absolute;\n    width: auto;\n    font-size: .92857143em;\n    top: 50%;\n    right: .5em;\n    left: auto;\n    bottom: auto;\n    color: rgba(255,255,255,.7);\n    text-shadow: none;\n    margin-top: -.5em;\n    font-weight: 700;\n    text-align: left;\n}\n\n.taskCounterActive {\n    opacity: 1;\n}\n\n.taskCounterNonactive {\n    opacity: 0.3;\n}\n\n.selectedPlay {\n    background-color: #F2F2F2;\n}"
  },
  {
    "path": "console2/src/components/organisms/ansible/ProcessAnsibleActivity/types.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { ConcordId } from '../../../../api/common';\n\nexport interface PlayInfoEntry {\n    id: ConcordId;\n    play: string;\n    hostsCount: number;\n    tasksCount: number;\n    taskStats: TaskStats;\n    progress: number;\n}\n\nexport interface TaskInfoEntry {\n    name: string;\n    type: string;\n    stats: TaskStats;\n}\n\nexport interface TaskStats {\n    failed: number;\n    ok: number;\n    unreachable: number;\n    skipped: number;\n    running: number;\n}\n\nexport const OK_COLOR = 'green';\nexport const FAILED_COLOR = 'red';\nexport const UNREACHABLE_COLOR = 'orange';\nexport const SKIPPED_COLOR = 'grey';\nexport const RUNNING_COLOR = undefined;\n\nexport const OK_COLOR_HEX = '#21ba45';\nexport const FAILED_COLOR_HEX = '#db2828';\nexport const UNREACHABLE_COLOR_HEX = '#f2711c';\nexport const SKIPPED_COLOR_HEX = '#767676';\nexport const RUNNING_COLOR_HEX = '#e8e8e8';\n"
  },
  {
    "path": "console2/src/components/organisms/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nexport { default as AnsibleTaskListActivity } from './ansible/AnsibleTaskActivity';\nexport { default as APITokenDeleteActivity } from './APITokenDeleteActivity';\nexport { default as APITokenList } from './APITokenList';\nexport { default as AuditLogActivity } from './AuditLogActivity';\nexport { default as BreadcrumbsToolbar } from './BreadcrumbsToolbar';\nexport { default as BulkCancelProcessPopup } from './BulkCancelProcessPopup';\nexport { default as CancelProcessPopup } from './CancelProcessPopup';\nexport { default as DeleteRepositoryPopup } from './DeleteRepositoryPopup';\nexport { default as DisableProcessPopup } from './DisableProcessPopup';\nexport { default as EditProjectActivity } from './EditProjectActivity';\nexport { default as EditRepositoryActivity } from './EditRepositoryActivity';\nexport { default as EncryptValueActivity } from './EncryptValueActivity';\nexport { default as FindLdapGroupField } from './FindLdapGroupField';\nexport { default as FindOrganizationsField } from './FindOrganizationsField';\nexport { default as FindTeamDropdown } from './FindTeamDropdown';\nexport { default as FindUserField2 } from './FindUserField2';\nexport { default as Login2 } from './Login2';\nexport { default as NewAPITokenActivity } from './NewAPITokenActivity';\nexport { default as NewProjectActivity } from './NewProjectActivity';\nexport { default as NewSecretActivity } from './NewSecretActivity';\nexport { default as NewStorageActivity } from './NewStorageActivity';\nexport { default as NewTeamActivity } from './NewTeamActivity';\nexport { default as OrganizationActivity } from './OrganizationActivity';\nexport { default as OrganizationList } from './OrganizationList';\nexport { default as OrganizationOwnerChangeActivity } from './OrganizationOwnerChangeActivity';\nexport { default as ProcessActivity } from './ProcessActivity';\nexport { default as ProcessAnsibleActivity } from './ansible/ProcessAnsibleActivity';\nexport { default as ProcessAttachmentsActivity } from './ProcessAttachmentsActivity';\nexport { default as ProcessCheckpointActivity } from './ProcessCheckpointActivity';\nexport { default as ProcessChildrenActivity } from './ProcessChildrenActivity';\nexport { default as ProcessEventsActivity } from './ProcessEventsActivity';\nexport { default as ProcessFormActivity } from './ProcessFormActivity';\nexport { default as ProcessHistoryActivity } from './ProcessHistoryActivity';\nexport { default as ProcessListActivity } from './ProcessListActivity';\nexport { default as ProcessLogActivity } from './ProcessLogActivity';\nexport { default as ProcessLogActivityV2 } from './ProcessLogActivityV2';\nexport { default as ProcessRestoreActivity } from './ProcessRestoreActivity';\nexport { default as ProcessStatusActivity } from './ProcessStatusActivity';\nexport { default as ProcessWaitActivity } from './ProcessWaitActivity';\nexport { default as ProcessWizard } from './ProcessWizard';\nexport { default as ProjectActivity } from './ProjectActivity';\nexport { default as ProjectDeleteActivity } from './ProjectDeleteActivity';\nexport { default as ProjectSearch } from './ProjectSearch';\nexport { default as ProjectSearchFormField } from './ProjectSearchFormField';\nexport { default as ProjectListActivity } from './ProjectListActivity';\nexport { default as ProjectOutVariablesModeActivity } from './ProjectOutVariablesModeActivity';\nexport { default as ProjectOrganizationChangeActivity } from './ProjectOrganizationChangeActivity';\nexport { default as ProjectOwnerChangeActivity } from './ProjectOwnerChangeActivity';\nexport { default as ProjectProcessExecModeActivity } from './ProjectProcessExecModeActivity';\nexport { default as ProjectRawPayloadModeActivity } from './ProjectRawPayloadModeActivity';\nexport { default as ProjectRenameActivity } from './ProjectRenameActivity';\nexport { default as ProjectTeamAccessActivity } from './ProjectTeamAccessActivity';\nexport { default as ProtectedRoute } from './ProtectedRoute';\nexport { default as PublicKeyPopup } from './PublicKeyPopup';\nexport { default as RedirectButton } from './RedirectButton';\nexport { default as RefreshRepositoryPopup } from './RefreshRepositoryPopup';\nexport { default as RepositoryActionDropdown } from './RepositoryActionDropdown';\nexport { default as RequestErrorActivity } from './RequestErrorActivity';\nexport { default as SecretActivity } from './SecretActivity';\nexport { default as SecretDeleteActivity } from './SecretDeleteActivity';\nexport { default as SecretListActivity } from './SecretListActivity';\nexport { default as SecretOrganizationChangeActivity } from './SecretOrganizationChangeActivity';\nexport { default as SecretOwnerChangeActivity } from './SecretOwnerChangeActivity';\nexport { default as SecretProjectActivity } from './SecretProjectActivity';\nexport { default as SecretRenameActivity } from './SecretRenameActivity';\nexport { default as SecretSearch } from './SecretSearch';\nexport { default as SecretStoreDropdown } from './SecretStoreDropdown';\nexport { default as SecretTeamAccessActivity } from './SecretTeamAccessActivity';\nexport { default as SecretVisibilityActivity } from './SecretVisibilityActivity';\nexport { default as ServerVersion } from './ServerVersion';\nexport { default as StartRepositoryPopup } from './StartRepositoryPopup';\nexport { default as TaskCallDetails } from './TaskCallDetails';\nexport { default as TeamActivity } from './TeamActivity';\nexport { default as TeamDeleteActivity } from './TeamDeleteActivity';\nexport { default as TeamLdapGroupList2 } from './TeamLdapGroupList2';\nexport { default as TeamList } from './TeamList';\nexport { default as TeamListActivity } from './TeamListActivity';\nexport { default as TeamMemberList2 } from './TeamMemberList2';\nexport { default as TeamRenameActivity } from './TeamRenameActivity';\nexport { default as TopBar } from './TopBar';\nexport { default as TriggeredByPopup } from './TriggeredByPopup';\nexport { default as UserInfo } from './UserInfo';\nexport { default as UserProcessActivity } from './UserProcessActivity';\nexport { default as ValidateRepositoryPopup } from './ValidateRepositoryPopup';\n"
  },
  {
    "path": "console2/src/components/pages/APITokensListPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Wal-Mart Store, Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { Menu, Message } from 'semantic-ui-react';\n\nimport { APITokenList, RedirectButton } from '../../organisms/index';\n\nclass APITokensListPage extends React.Component {\n    render() {\n        return (\n            <>\n                <Menu secondary={true} pointing={true}>\n                    <Menu.Item position={'left'}>\n                        <h4>API Tokens</h4>\n                    </Menu.Item>\n\n                    <Menu.Item position={'right'}>\n                        <RedirectButton\n                            icon=\"plus\"\n                            positive={true}\n                            labelPosition=\"left\"\n                            content=\"New token\"\n                            data-testid=\"api-token-new-button\"\n                            location={`/profile/api-token/_new`}\n                        />\n                    </Menu.Item>\n                </Menu>\n\n                <Message warning={true}>\n                    Tokens are not stored and cannot be restored - only recreated.\n                </Message>\n\n                <APITokenList />\n            </>\n        );\n    }\n}\n\nexport default APITokensListPage;\n"
  },
  {
    "path": "console2/src/components/pages/AboutPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Breadcrumb } from 'semantic-ui-react';\n\nimport { BreadcrumbSegment } from '../../molecules';\nimport { ServerVersion } from '../../organisms';\n\nclass AboutPage extends React.PureComponent {\n    render() {\n        return (\n            <>\n                <BreadcrumbSegment>\n                    <BreadcrumbSegment>\n                        <Breadcrumb.Section active={true}>About</Breadcrumb.Section>\n                    </BreadcrumbSegment>\n                </BreadcrumbSegment>\n\n                <p>\n                    Server version: <ServerVersion />\n                </p>\n                <p>Console version: {import.meta.env.VITE_CONCORD_VERSION || 'n/a'}</p>\n                <p>Last updated: {window.concord.lastUpdated || 'n/a'}</p>\n            </>\n        );\n    }\n}\n\nexport default AboutPage;\n"
  },
  {
    "path": "console2/src/components/pages/AddRepositoryPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { RouteComponentProps, withRouter } from '@/router';\nimport { Link } from 'react-router';\nimport { Breadcrumb, Container, Header, Segment } from 'semantic-ui-react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { BreadcrumbSegment } from '../../molecules';\nimport { EditRepositoryActivity } from '../../organisms';\n\ninterface RouteProps {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n}\n\nclass AddRepositoryPage extends React.PureComponent<RouteComponentProps<RouteProps>> {\n    render() {\n        const { orgName, projectName } = this.props.match.params;\n\n        return (\n            <>\n                <BreadcrumbSegment>\n                    <Breadcrumb.Section>\n                        <Link to={`/org/${orgName}/project/${projectName}/repository`}>\n                            {projectName}\n                        </Link>\n                    </Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section active={true}>Add Repository</Breadcrumb.Section>\n                </BreadcrumbSegment>\n\n                <Segment basic={true}>\n                    <Container text={true}>\n                        <Header>\n                            <Header.Content>Add a Repository</Header.Content>\n                            <Header.Subheader>Register an existing GIT repository</Header.Subheader>\n                        </Header>\n                        <EditRepositoryActivity\n                            orgName={orgName}\n                            projectName={projectName}\n                            forceRefresh={undefined}\n                        />\n                    </Container>\n                </Segment>\n            </>\n        );\n    }\n}\n\nexport default withRouter(AddRepositoryPage);\n"
  },
  {
    "path": "console2/src/components/pages/CustomResourcePage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { RouteComponentProps, withRouter } from '@/router';\nimport { Breadcrumb } from 'semantic-ui-react';\n\nimport { BreadcrumbSegment } from '../../molecules';\n\ninterface RouteProps {\n    resourceName: string;\n}\n\ntype Props = RouteComponentProps<RouteProps>;\n\nconst CustomResourcePage = (props: Props) => {\n    const key = props.match.params.resourceName;\n    const resource = window.concord.customResources && window.concord.customResources[key];\n    if (!resource) {\n        return <p>'Not available.'</p>;\n    }\n\n    return (\n        <>\n            <BreadcrumbSegment>\n                <Breadcrumb.Section>{resource?.title || key}</Breadcrumb.Section>\n            </BreadcrumbSegment>\n\n            {resource?.description && (\n                <p>Number of free workers for different \"flavors\" of Agents.</p>\n            )}\n\n            <iframe\n                title=\"Extenal Resource\"\n                src={resource.url}\n                width={resource.width || '100%'}\n                height={resource.height || '500'}\n                frameBorder=\"0\"\n            />\n        </>\n    );\n};\n\nexport default withRouter(CustomResourcePage);\n"
  },
  {
    "path": "console2/src/components/pages/JsonStorePage/EditStoreQueryActivity.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useEffect, useState } from 'react';\n\nimport { ConcordKey, GenericOperationResult } from '../../../api/common';\nimport {\n    createOrUpdateStorageQuery as apiCreate,\n    executeQuery as apiExecuteQuery,\n    getStorageQuery as apiGetQuery,\n    StorageQueryEntry,\n} from '../../../api/org/jsonstore';\nimport { useApi } from '../../../hooks/useApi';\nimport { Navigate } from 'react-router';\nimport { RequestErrorActivity } from '../../organisms';\nimport EditStoreQueryForm from './EditStoreQueryForm';\nimport ExecuteQueryResult from './ExecuteQueryResult';\nimport { LoadingDispatch } from '../../../App';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    storeName: ConcordKey;\n    queryName: ConcordKey;\n    forceRefresh: any;\n}\n\nconst EditStoreQueryActivity = (props: ExternalProps) => {\n    const { orgName, storeName, queryName, forceRefresh } = props;\n\n    const dispatch = React.useContext(LoadingDispatch);\n    const [query, setQuery] = useState('');\n    const [queryForExecute, setQueryForExecute] = useState('');\n\n    const loadQuery = useCallback(() => {\n        return apiGetQuery(orgName, storeName, queryName);\n    }, [orgName, storeName, queryName]);\n\n    const postData = useCallback(() => {\n        return apiCreate(orgName, storeName, queryName, query);\n    }, [orgName, storeName, queryName, query]);\n\n    const execQuery = useCallback(() => {\n        return apiExecuteQuery(orgName, storeName, queryForExecute);\n    }, [orgName, storeName, queryForExecute]);\n\n    const {\n        fetch: loadQueryFetch,\n        clearState: loadQueryClear,\n        data: loadQueryData,\n    } = useApi<StorageQueryEntry>(loadQuery, { fetchOnMount: true });\n    useEffect(() => {\n        loadQueryClear();\n        loadQueryFetch();\n    }, [loadQueryFetch, loadQueryClear, forceRefresh]);\n\n    const { error, isLoading, data, fetch } = useApi<GenericOperationResult>(postData, {\n        fetchOnMount: false,\n        requestByFetch: true,\n        dispatch,\n    });\n\n    const {\n        error: execError,\n        isLoading: isExecLoading,\n        data: execData,\n        fetch: execFetch,\n        clearState: execClearState,\n    } = useApi<Object>(execQuery, {\n        fetchOnMount: false,\n        requestByFetch: true,\n        dispatch,\n    });\n\n    const handleSubmit = useCallback(\n        (query: string) => {\n            setQuery(query);\n            fetch();\n        },\n        [fetch]\n    );\n\n    const handleExecute = useCallback(\n        (query: string) => {\n            setQueryForExecute(query);\n            execFetch();\n        },\n        [execFetch]\n    );\n\n    if (data) {\n        return <Navigate to={`/org/${orgName}/jsonstore/${storeName}/query`} />;\n    }\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n            {execError && <RequestErrorActivity error={execError} />}\n            {execData && <ExecuteQueryResult data={execData} onClose={execClearState} />}\n            <EditStoreQueryForm\n                submitting={isLoading}\n                executing={isExecLoading}\n                onSubmit={handleSubmit}\n                onExecute={handleExecute}\n                initialQuery={loadQueryData?.text}\n            />\n        </>\n    );\n};\n\nexport default EditStoreQueryActivity;\n"
  },
  {
    "path": "console2/src/components/pages/JsonStorePage/EditStoreQueryForm.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Button, Form, Label } from 'semantic-ui-react';\nimport type { ValidateResult } from 'react-hook-form';\n\nimport { storageQuery } from '../../../validation';\nimport { useCallback, useRef, useState } from 'react';\nimport './styles.css';\nimport LoadingEditor from '../../molecules/LoadingEditor';\n\nexport interface Props {\n    initialQuery?: string;\n    submitting: boolean;\n    executing: boolean;\n    onSubmit: (query: string) => void;\n    onExecute: (query: string) => void;\n}\n\nconst EditStoreQueryForm = (props: Props) => {\n    const { onSubmit, onExecute, submitting, executing, initialQuery } = props;\n\n    const [queryError, setQueryError] = useState<ValidateResult>();\n    const [isValidating, setIsValidating] = useState(false);\n    const [isEditorReady, setIsEditorReady] = useState(false);\n    const valueGetter = useRef<() => string>(() => '');\n\n    const handleEditorDidMount = useCallback((getEditorValue: () => string) => {\n        setIsEditorReady(true);\n        valueGetter.current = getEditorValue;\n    }, []);\n\n    const handleValidate = useCallback(async () => {\n        let success = false;\n        setIsValidating(true);\n        try {\n            const queryValidateResult = await validateQuery(valueGetter.current());\n            if (queryValidateResult !== undefined) {\n                setQueryError(queryValidateResult);\n            } else {\n                setQueryError(undefined);\n                success = true;\n            }\n            return success;\n        } finally {\n            setIsValidating(false);\n        }\n    }, []);\n\n    const handleSubmit = useCallback(async () => {\n        const success = await handleValidate();\n        if (success) {\n            await onSubmit(valueGetter.current());\n        }\n    }, [handleValidate, onSubmit]);\n\n    const handleExecute = useCallback(async () => {\n        const success = await handleValidate();\n\n        if (success) {\n            await onExecute(valueGetter.current());\n        }\n    }, [handleValidate, onExecute]);\n\n    const loading = submitting || executing || isValidating || !isEditorReady;\n    return (\n        <>\n            <Form>\n                <Form.Field name=\"query\" required={true} disabled={loading}>\n                    <label>Query</label>\n                </Form.Field>\n            </Form>\n\n            <div className={loading ? 'editorContainer loading' : 'editorContainer'}>\n                <div className={'editor'}>\n                    <LoadingEditor\n                        language=\"sql\"\n                        handleEditorDidMount={handleEditorDidMount}\n                        initValue={initialQuery}\n                        disabled={loading}\n                    />\n                </div>\n                {queryError && (\n                    <Label basic={true} pointing={true} color=\"red\">\n                        {queryError}\n                    </Label>\n                )}\n            </div>\n\n            <Button\n                primary={true}\n                onClick={handleSubmit}\n                disabled={loading || initialQuery === undefined}>\n                Save\n            </Button>\n            <Button\n                primary={false}\n                onClick={handleExecute}\n                disabled={loading || initialQuery === undefined}>\n                Execute\n            </Button>\n        </>\n    );\n};\n\nconst validateQuery = (query?: string): Promise<ValidateResult> => {\n    const error = storageQuery.query(query);\n    if (error) {\n        return Promise.resolve(error);\n    }\n    return Promise.resolve(undefined);\n};\n\nexport default EditStoreQueryForm;\n"
  },
  {
    "path": "console2/src/components/pages/JsonStorePage/EditStoreQueryPage.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { RouteComponentProps, withRouter } from '@/router';\nimport { Link } from 'react-router';\nimport { Breadcrumb, Container, Header, Segment } from 'semantic-ui-react';\n\nimport { MainToolbar } from '../../molecules';\nimport { useRef } from 'react';\nimport { useState } from 'react';\nimport { useCallback } from 'react';\nimport EditStoreQueryActivity from './EditStoreQueryActivity';\nimport { useContext } from 'react';\nimport { LoadingState } from '../../../App';\n\ninterface RouteProps {\n    orgName: string;\n    storeName: string;\n    queryName: string;\n}\n\nconst EditStoreQueryPage = (props: RouteComponentProps<RouteProps>) => {\n    const loading = useContext(LoadingState);\n    const stickyRef = useRef(null);\n    const [refresh, toggleRefresh] = useState<boolean>(false);\n\n    const { orgName, storeName, queryName } = props.match.params;\n\n    const refreshHandler = useCallback(() => {\n        toggleRefresh((prevState) => !prevState);\n    }, []);\n\n    return (\n        <div ref={stickyRef}>\n            <MainToolbar\n                loading={loading}\n                refresh={refreshHandler}\n                stickyRef={stickyRef}\n                breadcrumbs={renderBreadcrumbs(orgName, storeName, queryName)}\n            />\n\n            <Segment basic={true}>\n                <Container text={true}>\n                    <Header>Edit query: {queryName}</Header>\n                    <EditStoreQueryActivity\n                        orgName={orgName}\n                        storeName={storeName}\n                        queryName={queryName}\n                        forceRefresh={refresh}\n                    />\n                </Container>\n            </Segment>\n        </div>\n    );\n};\n\nconst renderBreadcrumbs = (orgName: string, storeName: string, queryName: string) => {\n    return (\n        <Breadcrumb size=\"big\">\n            <Breadcrumb.Section>\n                <Link to={`/org/${orgName}/jsonstore`}>{orgName}</Link>\n            </Breadcrumb.Section>\n            <Breadcrumb.Divider />\n            <Breadcrumb.Section>\n                <Link to={`/org/${orgName}/jsonstore/${storeName}/query`}>{storeName}</Link>\n            </Breadcrumb.Section>\n            <Breadcrumb.Divider />\n            <Breadcrumb.Section active={true}>{queryName}</Breadcrumb.Section>\n        </Breadcrumb>\n    );\n};\n\nexport default withRouter(EditStoreQueryPage);\n"
  },
  {
    "path": "console2/src/components/pages/JsonStorePage/ExecuteQueryResult.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { Button, Form, Modal, TextArea } from 'semantic-ui-react';\nimport * as React from 'react';\nimport { useCallback, useState } from 'react';\n\nimport { ReactJson } from '../../atoms';\n\nimport './styles.css';\n\nexport type OnCloseFn = () => void;\n\nexport interface Props {\n    data: object;\n    onClose: OnCloseFn;\n}\n\nconst renderJsonView = (data: object, onShowRaw: () => void) => (\n    <>\n        <div style={{ textAlign: 'right' }}>\n            <Button className=\"viewModeToggle\" basic={true} size=\"small\" onClick={onShowRaw}>\n                Disable highlighting\n            </Button>\n        </div>\n        <ReactJson\n            src={data}\n            collapsed={false}\n            name={null}\n            enableClipboard={true}\n            displayDataTypes={false}\n            displayObjectSize={false}\n        />\n    </>\n);\n\nconst renderRawView = (data: object, onShowJson: () => void) => (\n    <>\n        <div style={{ textAlign: 'right' }}>\n            <Button className=\"viewModeToggle\" basic={true} size=\"small\" onClick={onShowJson}>\n                Enable highlighting\n            </Button>\n        </div>\n        <Form>\n            <TextArea style={{ minHeight: 300 }} readOnly={true}>\n                {JSON.stringify(data, null, 2)}\n            </TextArea>\n        </Form>\n    </>\n);\n\nexport default ({ data, onClose }: Props) => {\n    const [open, setOpen] = useState(true);\n    const [showRaw, setShowRaw] = useState(false);\n\n    const handleClose = useCallback(() => {\n        setOpen(false);\n        onClose();\n    }, [onClose]);\n\n    return (\n        <>\n            <Modal dimmer=\"inverted\" open={open} onClose={handleClose}>\n                <Modal.Header>Query results (max rows: 50)</Modal.Header>\n                <Modal.Content scrolling={true}>\n                    {showRaw\n                        ? renderRawView(data, () => setShowRaw(false))\n                        : renderJsonView(data, () => setShowRaw(true))}\n                </Modal.Content>\n            </Modal>\n        </>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/pages/JsonStorePage/NewStorageQueryActivity.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useState } from 'react';\n\nimport { ConcordKey, GenericOperationResult } from '../../../api/common';\nimport {\n    createOrUpdateStorageQuery as apiCreate,\n    executeQuery as apiExecuteQuery,\n} from '../../../api/org/jsonstore';\nimport { useApi } from '../../../hooks/useApi';\nimport { Navigate } from 'react-router';\nimport { RequestErrorActivity } from '../../organisms';\nimport NewStorageQueryForm, { NewStorageQueryFormValues } from './NewStorageQueryForm';\nimport ExecuteQueryResult from './ExecuteQueryResult';\nimport { LoadingDispatch } from '../../../App';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    storeName: ConcordKey;\n}\n\nconst INIT_VALUES = {\n    name: '',\n    query: 'select cast(item_data as varchar) \\nfrom json_store_data',\n};\n\nconst NewStorageQueryActivity = (props: ExternalProps) => {\n    const { orgName, storeName } = props;\n\n    const dispatch = React.useContext(LoadingDispatch);\n    const [queryForExecute, setQueryForExecute] = useState('');\n    const [values, setValues] = useState(INIT_VALUES);\n\n    const postQuery = useCallback(() => {\n        return apiCreate(orgName, storeName, values.name, values.query);\n    }, [orgName, storeName, values]);\n\n    const { error, isLoading, data, fetch } = useApi<GenericOperationResult>(postQuery, {\n        fetchOnMount: false,\n        requestByFetch: true,\n        dispatch: dispatch,\n    });\n\n    const execQuery = useCallback(() => {\n        return apiExecuteQuery(orgName, storeName, queryForExecute);\n    }, [orgName, storeName, queryForExecute]);\n\n    const {\n        error: execError,\n        isLoading: isExecLoading,\n        data: execData,\n        fetch: execFetch,\n        clearState: execClearState,\n    } = useApi<Object>(execQuery, {\n        fetchOnMount: false,\n        requestByFetch: true,\n        dispatch: dispatch,\n    });\n\n    const handleSubmit = useCallback(\n        (values: NewStorageQueryFormValues) => {\n            setValues({ ...values });\n            fetch();\n        },\n        [fetch]\n    );\n\n    const handleExecute = useCallback(\n        (query: string) => {\n            setQueryForExecute(query);\n            execFetch();\n        },\n        [execFetch]\n    );\n\n    if (data) {\n        return <Navigate to={`/org/${orgName}/jsonstore/${storeName}/query`} />;\n    }\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n            {execError && <RequestErrorActivity error={execError} />}\n            {execData && <ExecuteQueryResult data={execData} onClose={execClearState} />}\n            <NewStorageQueryForm\n                orgName={orgName}\n                storeName={storeName}\n                submitting={isLoading}\n                executing={isExecLoading}\n                onSubmit={handleSubmit}\n                onExecute={handleExecute}\n                initial={INIT_VALUES}\n            />\n        </>\n    );\n};\n\nexport default NewStorageQueryActivity;\n"
  },
  {
    "path": "console2/src/components/pages/JsonStorePage/NewStorageQueryForm.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Button, Form, Input, Label } from 'semantic-ui-react';\nimport type { ValidateResult } from 'react-hook-form';\n\nimport { ConcordKey } from '../../../api/common';\nimport { isStorageQueryExists } from '../../../api/service/console';\nimport { storageQuery, jsonStoreQueryAlreadyExistsError } from '../../../validation';\nimport { useCallback, useRef, useState } from 'react';\n\nimport './styles.css';\nimport LoadingEditor from '../../molecules/LoadingEditor';\n\ninterface FormValues {\n    name: ConcordKey;\n    query: string;\n}\n\nexport type NewStorageQueryFormValues = FormValues;\n\nexport interface Props {\n    orgName: ConcordKey;\n    storeName: ConcordKey;\n    initial: FormValues;\n    submitting: boolean;\n    executing: boolean;\n    onSubmit: (values: FormValues) => void;\n    onExecute: (query: string) => void;\n}\n\nconst NewStorageQueryForm = ({\n    orgName,\n    storeName,\n    onSubmit,\n    onExecute,\n    submitting,\n    executing,\n    initial\n}: Props) => {\n    const [queryName, setQueryName] = useState(initial.name);\n    const [queryNameError, setQueryNameError] = useState<string>();\n    const [queryError, setQueryError] = useState<ValidateResult>();\n    const [isValidating, setIsValidating] = useState(false);\n    const [isEditorReady, setIsEditorReady] = useState(false);\n    const valueGetter = useRef<() => string>(() => initial.query);\n\n    const handleEditorDidMount = useCallback((getEditorValue: () => string) => {\n        setIsEditorReady(true);\n        valueGetter.current = getEditorValue;\n    }, []);\n\n    const handleQueryNameChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {\n        setQueryName(event.target.value);\n    }, []);\n\n    const handleValidate = useCallback(async () => {\n        let hasError = false;\n        setIsValidating(true);\n        try {\n            const nameValidateResult = await validateName(orgName, storeName, queryName);\n            if (nameValidateResult !== undefined) {\n                setQueryNameError(nameValidateResult);\n                hasError = true;\n            } else {\n                setQueryNameError(undefined);\n            }\n\n            const queryValidateResult = await validateQuery(valueGetter.current());\n            if (queryValidateResult !== undefined) {\n                setQueryError(queryValidateResult);\n                hasError = true;\n            } else {\n                setQueryError(undefined);\n            }\n        } finally {\n            setIsValidating(false);\n        }\n\n        return hasError;\n    }, [orgName, storeName, queryName]);\n\n    const handleSubmit = useCallback(async () => {\n        const hasError = await handleValidate();\n\n        if (!hasError) {\n            await onSubmit({ name: queryName, query: valueGetter.current() });\n        }\n    }, [handleValidate, queryName, onSubmit]);\n\n    const handleExecute = useCallback(async () => {\n        const hasError = await handleValidate();\n\n        if (!hasError) {\n            await onExecute(valueGetter.current());\n        }\n    }, [handleValidate, onExecute]);\n\n    const loading = submitting || executing || isValidating || !isEditorReady;\n\n    return (\n        <>\n            <Form>\n                <Form.Field name=\"name\" required={true} disabled={loading}>\n                    <label>Name</label>\n                    <Input placeholder=\"Query name\" name=\"name\" onChange={handleQueryNameChange} />\n                    {queryNameError && (\n                        <Label basic={true} pointing={true} color=\"red\">\n                            {queryNameError}\n                        </Label>\n                    )}\n                </Form.Field>\n\n                <Form.Field name=\"query\" required={true} disabled={loading}>\n                    <label>Query</label>\n                </Form.Field>\n            </Form>\n\n            <div className={loading ? 'editorContainer loading' : 'editorContainer'}>\n                <div className={'editor'}>\n                    <LoadingEditor\n                        language=\"sql\"\n                        handleEditorDidMount={handleEditorDidMount}\n                        initValue={initial.query}\n                        disabled={loading}\n                    />\n                </div>\n                {queryError && (\n                    <Label basic={true} pointing={true} color=\"red\">\n                        {queryError}\n                    </Label>\n                )}\n            </div>\n\n            <Button primary={true} onClick={handleSubmit} disabled={loading}>\n                Create\n            </Button>\n            <Button primary={false} onClick={handleExecute} disabled={loading}>\n                Execute\n            </Button>\n        </>\n    );\n};\n\nconst validateName = async (\n    orgName: ConcordKey,\n    storeName: ConcordKey,\n    name: string\n): Promise<string | undefined> => {\n    const error = storageQuery.name(name);\n    if (error) {\n        return Promise.resolve(error);\n    }\n    const exists = await isStorageQueryExists(orgName, storeName, name);\n    if (exists) {\n        return Promise.resolve(jsonStoreQueryAlreadyExistsError(name));\n    }\n    return Promise.resolve(undefined);\n};\n\nconst validateQuery = (query?: string): Promise<ValidateResult> => {\n    const error = storageQuery.query(query);\n    if (error) {\n        return Promise.resolve(error);\n    }\n    return Promise.resolve(undefined);\n};\n\nexport default NewStorageQueryForm;\n"
  },
  {
    "path": "console2/src/components/pages/JsonStorePage/NewStorageQueryPage.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { RouteComponentProps, withRouter } from '@/router';\nimport { Link } from 'react-router';\nimport { Breadcrumb, Container, Header, Segment } from 'semantic-ui-react';\n\nimport { MainToolbar } from '../../molecules';\nimport { useRef } from 'react';\nimport NewStorageQueryActivity from './NewStorageQueryActivity';\nimport { useContext } from 'react';\nimport { LoadingState } from '../../../App';\n\ninterface RouteProps {\n    orgName: string;\n    storeName: string;\n}\n\nconst NewStorageQueryPage = (props: RouteComponentProps<RouteProps>) => {\n    const loading = useContext(LoadingState);\n    const stickyRef = useRef(null);\n\n    const { orgName, storeName } = props.match.params;\n\n    return (\n        <div ref={stickyRef}>\n            <MainToolbar\n                loading={loading}\n                stickyRef={stickyRef}\n                breadcrumbs={renderBreadcrumbs(orgName, storeName)}\n            />\n\n            <Segment basic={true}>\n                <Container text={true}>\n                    <Header>Create a New Query</Header>\n                    <NewStorageQueryActivity orgName={orgName} storeName={storeName} />\n                </Container>\n            </Segment>\n        </div>\n    );\n};\n\nconst renderBreadcrumbs = (orgName: string, storeName: string) => {\n    return (\n        <Breadcrumb size=\"big\">\n            <Breadcrumb.Section>\n                <Link to={`/org/${orgName}/jsonstore`}>{orgName}</Link>\n            </Breadcrumb.Section>\n            <Breadcrumb.Divider />\n            <Breadcrumb.Section>\n                <Link to={`/org/${orgName}/jsonstore/${storeName}/query`}>{storeName}</Link>\n            </Breadcrumb.Section>\n            <Breadcrumb.Divider />\n            <Breadcrumb.Section active={true}>New Query</Breadcrumb.Section>\n        </Breadcrumb>\n    );\n};\n\nexport default withRouter(NewStorageQueryPage);\n"
  },
  {
    "path": "console2/src/components/pages/JsonStorePage/NewStorePage.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { RouteComponentProps, withRouter } from '@/router';\nimport { Link } from 'react-router';\nimport { Breadcrumb, Container, Header, Segment } from 'semantic-ui-react';\n\nimport { MainToolbar } from '../../molecules';\nimport { NewStorageActivity } from '../../organisms';\nimport { useContext, useRef } from 'react';\nimport { LoadingState } from '../../../App';\n\ninterface RouteProps {\n    orgName: string;\n}\n\nconst NewStorePage = (props: RouteComponentProps<RouteProps>) => {\n    const stickyRef = useRef(null);\n\n    const loading = useContext(LoadingState);\n\n    const { orgName } = props.match.params;\n\n    return (\n        <div ref={stickyRef}>\n            <MainToolbar\n                loading={loading}\n                stickyRef={stickyRef}\n                breadcrumbs={renderBreadcrumbs(orgName)}\n            />\n\n            <Segment basic={true}>\n                <Container text={true}>\n                    <Header>Create a New JSON Store</Header>\n                    <NewStorageActivity orgName={orgName} />\n                </Container>\n            </Segment>\n        </div>\n    );\n};\n\nconst renderBreadcrumbs = (orgName: string) => {\n    return (\n        <Breadcrumb size=\"big\">\n            <Breadcrumb.Section>\n                <Link to=\"/org\">Organizations</Link>\n            </Breadcrumb.Section>\n            <Breadcrumb.Divider />\n            <Breadcrumb.Section>\n                <Link to={`/org/${orgName}/jsonstore`}>{orgName}</Link>\n            </Breadcrumb.Section>\n            <Breadcrumb.Divider />\n            <Breadcrumb.Section active={true}>New JSON Store</Breadcrumb.Section>\n        </Breadcrumb>\n    );\n};\n\nexport default withRouter(NewStorePage);\n"
  },
  {
    "path": "console2/src/components/pages/JsonStorePage/StoreDataDeleteActivity.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport { ConcordKey, GenericOperationResult } from '../../../api/common';\nimport { SingleOperationPopup } from '../../molecules';\nimport { useCallback } from 'react';\nimport { deleteStorageData as apiDelete } from '../../../api/org/jsonstore';\nimport { useApi } from '../../../hooks/useApi';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    storeName: ConcordKey;\n    storageDataPath: ConcordKey;\n    trigger: (onClick: (event: React.SyntheticEvent) => void) => React.ReactNode;\n    onDone: () => void;\n}\n\nconst StoreDataDeleteActivity = (props: ExternalProps) => {\n    const { orgName, storeName, storageDataPath, trigger, onDone } = props;\n\n    const deleteDataRequest = useCallback(() => {\n        return apiDelete(orgName, storeName, storageDataPath);\n    }, [orgName, storeName, storageDataPath]);\n\n    const { data, isLoading, error, clearState, fetch } = useApi<GenericOperationResult>(\n        deleteDataRequest,\n        { fetchOnMount: false, requestByFetch: true }\n    );\n\n    const resetHandler = useCallback(() => {\n        clearState();\n    }, [clearState]);\n\n    return (\n        <SingleOperationPopup\n            trigger={trigger}\n            title=\"Delete storage data?\"\n            introMsg={\n                <p>\n                    Are you sure you want to delete the storage data at '<b>{storageDataPath}</b>'\n                    path?\n                </p>\n            }\n            running={isLoading}\n            success={data !== undefined}\n            successMsg={<p>Storage data was deleted successfully.</p>}\n            error={error}\n            reset={resetHandler}\n            onConfirm={fetch}\n            onDone={onDone}\n        />\n    );\n};\n\nexport default StoreDataDeleteActivity;\n"
  },
  {
    "path": "console2/src/components/pages/JsonStorePage/StoreDataList.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useState } from 'react';\nimport { Button, Input, List, Menu } from 'semantic-ui-react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { PaginationToolBar } from '../../molecules';\nimport { usePagination } from '../../molecules/PaginationToolBar/usePagination';\nimport { listStorageData as apiGet, PaginatedStorageDataEntries } from '../../../api/org/jsonstore';\nimport { useApi } from '../../../hooks/useApi';\nimport { InputOnChangeData } from 'semantic-ui-react/dist/commonjs/elements/Input/Input';\nimport RequestErrorActivity from '../../organisms/RequestErrorActivity';\nimport StoreDataDeleteActivity from './StoreDataDeleteActivity';\nimport { LoadingDispatch } from '../../../App';\n\nimport './styles.css';\n\nexport interface ExternalProps {\n    orgName: ConcordKey;\n    storeName: ConcordKey;\n    forceRefresh: any;\n}\n\nconst StoreDataList = ({ orgName, storeName, forceRefresh }: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const [filter, setFilter] = useState<string>();\n    const {\n        paginationFilter,\n        handleLimitChange,\n        handleNext,\n        handlePrev,\n        handleFirst\n    } = usePagination();\n\n    const fetchData = useCallback(() => {\n        return apiGet(orgName, storeName, paginationFilter.offset, paginationFilter.limit, filter);\n    }, [orgName, storeName, paginationFilter, filter]);\n\n    const { data, error, clearState, fetch } = useApi<PaginatedStorageDataEntries>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    const filterChangeHandler = useCallback(\n        (event: React.ChangeEvent<HTMLInputElement>, data: InputOnChangeData) => {\n            setFilter(data.value);\n        },\n        []\n    );\n\n    const storageDataDeleteTrigger = useCallback((onClick: any) => {\n        return (\n            <Button\n                size={'mini'}\n                primary={true}\n                negative={true}\n                onClick={(event) => {\n                    event.stopPropagation();\n                    event.preventDefault();\n                    onClick(event);\n                }}>\n                Delete\n            </Button>\n        );\n    }, []);\n\n    const deleteHandler = useCallback(() => {\n        //TODO: reset pagination?\n        clearState();\n        fetch();\n    }, [clearState, fetch]);\n\n    return (\n        <>\n            <Menu secondary={true}>\n                <Menu.Item>\n                    <Input\n                        icon=\"search\"\n                        placeholder=\"Filter...\"\n                        onChange={filterChangeHandler}\n                        disabled={data === undefined}\n                    />\n                </Menu.Item>\n\n                <Menu.Item style={{ padding: 0 }} position={'right'}>\n                    <PaginationToolBar\n                        limit={paginationFilter.limit}\n                        handleLimitChange={(limit) => handleLimitChange(limit)}\n                        handleNext={handleNext}\n                        handlePrev={handlePrev}\n                        handleFirst={handleFirst}\n                        disablePrevious={paginationFilter.offset === 0}\n                        disableNext={data === undefined ? true : !data.next}\n                        disableFirst={paginationFilter.offset === 0}\n                        disabled={data === undefined}\n                    />\n                </Menu.Item>\n            </Menu>\n\n            {error && <RequestErrorActivity error={error} />}\n            {data && data.items.length === 0 && <h3>No store items found</h3>}\n\n            <List\n                divided={true}\n                relaxed={true}\n                size=\"large\"\n                verticalAlign={'middle'}\n                selection={true}>\n                {data &&\n                    data.items.map((d, idx) => (\n                        <List.Item\n                            key={idx}\n                            as=\"a\"\n                            href={`/api/v1/org/${orgName}/jsonstore/${storeName}/item/${encodeURIComponent(\n                                d\n                            )}`}\n                            target=\"_blank\"\n                            style={{ backgroundColor: idx % 2 === 0 ? '#F5F5F5' : '#FFFFFF' }}>\n                            <List.Content floated=\"right\">\n                                <StoreDataDeleteActivity\n                                    orgName={orgName}\n                                    storeName={storeName}\n                                    storageDataPath={d}\n                                    trigger={storageDataDeleteTrigger}\n                                    onDone={deleteHandler}\n                                />\n                            </List.Content>\n\n                            <List.Content verticalAlign={'middle'} className=\"itemLink\">\n                                {d}\n                            </List.Content>\n                        </List.Item>\n                    ))}\n            </List>\n        </>\n    );\n};\n\nexport default StoreDataList;\n"
  },
  {
    "path": "console2/src/components/pages/JsonStorePage/StoreDeleteActivity.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { ConcordKey, GenericOperationResult } from '../../../api/common';\nimport { ButtonWithConfirmation } from '../../molecules';\nimport { useCallback, useState } from 'react';\nimport { deleteStorage as apiDelete } from '../../../api/org/jsonstore';\nimport { useApi } from '../../../hooks/useApi';\nimport RequestErrorActivity from '../../organisms/RequestErrorActivity';\nimport { Navigate } from 'react-router';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    storeName: ConcordKey;\n    disabled: boolean;\n}\n\nconst StoreDeleteActivity = ({ orgName, storeName, disabled }: ExternalProps) => {\n    const [forceRequest, toggleForceRequest] = useState<boolean>(false);\n\n    const deleteData = useCallback(async () => {\n        return await apiDelete(orgName, storeName);\n    }, [orgName, storeName]);\n\n    const { data, error, isLoading } = useApi<GenericOperationResult>(deleteData, {\n        fetchOnMount: false,\n        forceRequest,\n    });\n\n    const confirmHandler = useCallback(() => {\n        toggleForceRequest((prevState) => !prevState);\n    }, []);\n\n    if (data) {\n        return <Navigate to={`/org/${orgName}/jsonstore`} />;\n    }\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n            <ButtonWithConfirmation\n                primary={true}\n                negative={true}\n                content=\"Delete\"\n                loading={isLoading}\n                confirmationHeader=\"Delete the storage?\"\n                confirmationContent=\"Are you sure you want to delete the storage?\"\n                onConfirm={confirmHandler}\n                disabled={disabled}\n            />\n        </>\n    );\n};\n\nexport default StoreDeleteActivity;\n"
  },
  {
    "path": "console2/src/components/pages/JsonStorePage/StoreListActivity.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useContext, useState } from 'react';\nimport { Input, List, Menu } from 'semantic-ui-react';\nimport { Link } from 'react-router';\n\nimport { ConcordKey } from '../../../api/common';\nimport { CreateNewEntityButton, PaginationToolBar } from '../../molecules';\nimport { Organizations } from '../../../state/data/orgs/types';\nimport {\n    list as apiList,\n    PaginatedStorageEntries,\n    StorageVisibility\n} from '../../../api/org/jsonstore';\nimport { usePagination } from '../../molecules/PaginationToolBar/usePagination';\nimport { useApi } from '../../../hooks/useApi';\nimport RequestErrorActivity from '../../organisms/RequestErrorActivity';\nimport { UserSessionContext } from '../../../session';\nimport { LoadingDispatch } from '../../../App';\n\ninterface Props {\n    orgName: ConcordKey;\n    forceRefresh: any;\n}\n\nexport default ({ orgName, forceRefresh }: Props) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const [filter, setFilter] = useState<string>();\n    const {\n        paginationFilter,\n        handleLimitChange,\n        handleNext,\n        handlePrev,\n        handleFirst\n    } = usePagination();\n\n    const fetchData = useCallback(() => {\n        return apiList(orgName, paginationFilter.offset, paginationFilter.limit, filter);\n    }, [orgName, paginationFilter.offset, paginationFilter.limit, filter]);\n\n    const { data, error } = useApi<PaginatedStorageEntries>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    const { userInfo } = useContext(UserSessionContext);\n\n    return (\n        <>\n            <Menu secondary={true}>\n                <Menu.Item>\n                    <Input\n                        icon=\"search\"\n                        placeholder=\"Filter...\"\n                        onChange={(ev, data) => setFilter(data.value)}\n                    />\n                </Menu.Item>\n\n                <Menu.Menu position={'right'}>\n                    <Menu.Item>\n                        <CreateNewEntityButton\n                            entity=\"jsonstore\"\n                            title=\"New store\"\n                            orgName={orgName}\n                            userInOrg={isUserOrgMember(orgName, userInfo!.orgs)}\n                            enabledInPolicy={true}\n                        />\n                    </Menu.Item>\n                    <Menu.Item style={{ padding: 0 }}>\n                        <PaginationToolBar\n                            limit={paginationFilter.limit}\n                            handleLimitChange={handleLimitChange}\n                            handleNext={handleNext}\n                            handlePrev={handlePrev}\n                            handleFirst={handleFirst}\n                            disablePrevious={paginationFilter.offset <= 0}\n                            disableNext={!data?.next}\n                            disableFirst={paginationFilter.offset <= 0}\n                        />\n                    </Menu.Item>\n                </Menu.Menu>\n            </Menu>\n\n            {error && <RequestErrorActivity error={error} />}\n            {data?.items.length === 0 && <h3>No JSON stores defined</h3>}\n\n            <List divided={true} relaxed={true} size=\"large\">\n                {data?.items.map((s, idx) => (\n                    <List.Item key={idx}>\n                        <List.Icon\n                            name={s.visibility === StorageVisibility.PRIVATE ? 'lock' : 'unlock'}\n                            color=\"grey\"\n                        />\n                        <List.Content>\n                            <List.Header as={Link} to={`/org/${orgName}/jsonstore/${s.name}`}>\n                                {s.name}\n                            </List.Header>\n                            <List.Description>\n                                {s.owner ? `Owner: ${s.owner.username}` : ''}\n                            </List.Description>\n                        </List.Content>\n                    </List.Item>\n                ))}\n            </List>\n        </>\n    );\n};\n\nconst isUserOrgMember = (orgName: string, userOrgs: Organizations) => {\n    return (\n        Object.keys(userOrgs)\n            .map((k) => userOrgs[k])\n            .filter((org) => org.name === orgName).length > 0\n    );\n};\n"
  },
  {
    "path": "console2/src/components/pages/JsonStorePage/StoreOrganizationChangeActivity.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useState } from 'react';\n\nimport { createOrUpdate as apiChangeOrganization } from '../../../api/org/jsonstore';\n\nimport { ConcordKey, RequestError } from '../../../api/common';\nimport { SingleOperationPopup } from '../../molecules';\nimport { Form, Input } from 'semantic-ui-react';\nimport { Navigate } from 'react-router';\nimport { FindOrganizationsField, RequestErrorActivity } from '../../organisms';\n\ninterface Props {\n    orgName: ConcordKey;\n    storeName: ConcordKey;\n    disabled: boolean;\n}\n\nconst StoreOrganizationChangeActivity = ({ orgName, storeName, disabled }: Props) => {\n    const [dirty, setDirty] = useState<boolean>(false);\n    const [state, setState] = useState<ConcordKey>(orgName);\n    const [confirmation, setConfirmation] = useState('');\n    const [error, setError] = useState<RequestError>();\n    const [changing, setChanging] = useState<boolean>(false);\n    const [success, setSuccess] = useState<boolean>(false);\n    const [redirect, setRedirect] = useState<boolean>(false);\n\n    const confirmHandler = useCallback(async () => {\n        setChanging(true);\n\n        try {\n            const result = await apiChangeOrganization(orgName, storeName, undefined, state);\n            setSuccess(result.ok);\n        } catch (e) {\n            setError(e);\n        } finally {\n            setChanging(false);\n        }\n    }, [state, orgName, storeName]);\n\n    const redirectHandler = useCallback(() => {\n        setRedirect((prevState) => !prevState);\n    }, []);\n\n    const resetHandler = useCallback(() => {\n        setConfirmation('');\n    }, []);\n\n    if (redirect) {\n        return <Navigate to={`/org/${state}/jsonstore/${storeName}`} />;\n    }\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n            <Form loading={changing}>\n                <Form.Group widths={3}>\n                    <Form.Field disabled={disabled}>\n                        <FindOrganizationsField\n                            placeholder=\"Search for an organization...\"\n                            defaultOrgName={orgName}\n                            required={true}\n                            onReset={() => {\n                                setDirty(false);\n                                setState(orgName);\n                            }}\n                            onSelect={(value) => {\n                                setDirty(true);\n                                setState(value.name);\n                            }}\n                        />\n                    </Form.Field>\n                    <SingleOperationPopup\n                        trigger={(onClick) => (\n                            <Form.Button\n                                primary={true}\n                                negative={true}\n                                content=\"Move\"\n                                disabled={!dirty || disabled}\n                                onClick={onClick}\n                            />\n                        )}\n                        title=\"Move JSON Store?\"\n                        introMsg={\n                            <>\n                                <p>\n                                    Are you sure you want to move the JSON Store to{' '}\n                                    <strong>{state}</strong> organization?\n                                </p>\n                                <p>\n                                    Please type <strong>{storeName}</strong> in the text box below\n                                    to confirm.\n                                </p>\n                                <div\n                                    className={`ui input ${\n                                        confirmation !== storeName ? 'error' : ''\n                                    }`}\n                                >\n                                    <Input\n                                        type=\"text\"\n                                        name=\"name\"\n                                        placeholder=\"JSON Store name\"\n                                        value={confirmation}\n                                        onChange={(e, data) => setConfirmation(data.value)}\n                                    />\n                                </div>\n                            </>\n                        }\n                        running={changing}\n                        runningMsg={\n                            <p>\n                                Moving the JSON store <strong>{storeName}</strong> to{' '}\n                                <strong>{state}</strong> organization...\n                            </p>\n                        }\n                        success={success}\n                        successMsg={\n                            <p>\n                                The JSON store <strong>{storeName}</strong> was moved successfully\n                                to <strong>{state}</strong> organization.\n                            </p>\n                        }\n                        error={error}\n                        reset={resetHandler}\n                        onConfirm={confirmHandler}\n                        onDone={redirectHandler}\n                        disableYes={confirmation !== storeName}\n                    />\n                </Form.Group>\n            </Form>\n        </>\n    );\n};\n\nexport default StoreOrganizationChangeActivity;\n"
  },
  {
    "path": "console2/src/components/pages/JsonStorePage/StoreOwnerChangeActivity.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useState } from 'react';\n\nimport { changeOwner as apiChangeOwner } from '../../../api/org/jsonstore';\nimport { ConcordId, ConcordKey, GenericOperationResult } from '../../../api/common';\nimport EntityOwnerChangeForm from '../../molecules/EntityOwnerChangeForm';\nimport RequestErrorActivity from '../../organisms/RequestErrorActivity';\nimport { useApi } from '../../../hooks/useApi';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    storeName: ConcordKey;\n    initialOwnerId?: ConcordId;\n    disabled: boolean;\n}\n\nconst StoreOwnerChangeActivity = ({\n    orgName,\n    storeName,\n    disabled,\n    initialOwnerId\n}: ExternalProps) => {\n    const [value, setValue] = useState(initialOwnerId);\n\n    const postData = useCallback(() => {\n        return apiChangeOwner(orgName, storeName, value!);\n    }, [orgName, storeName, value]);\n\n    const { error, isLoading, fetch, clearState } = useApi<GenericOperationResult>(postData, {\n        fetchOnMount: false,\n        requestByFetch: true\n    });\n\n    const ownerChangeHandler = useCallback(\n        (value: ConcordId) => {\n            setValue(value);\n            clearState();\n            fetch();\n        },\n        [clearState, fetch]\n    );\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n            <EntityOwnerChangeForm\n                originalOwnerId={initialOwnerId}\n                confirmationHeader=\"Change storage owner?\"\n                confirmationContent=\"Are you sure you want to change the storage's owner?\"\n                onSubmit={ownerChangeHandler}\n                submitting={isLoading}\n                disabled={disabled}\n            />\n        </>\n    );\n};\n\nexport default StoreOwnerChangeActivity;\n"
  },
  {
    "path": "console2/src/components/pages/JsonStorePage/StoreQueryDeleteActivity.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback } from 'react';\n\nimport { ConcordKey, GenericOperationResult } from '../../../api/common';\nimport { SingleOperationPopup } from '../../molecules';\nimport { deleteStorageQuery as apiDelete } from '../../../api/org/jsonstore';\nimport { useApi } from '../../../hooks/useApi';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    storeName: ConcordKey;\n    storageQueryName: ConcordKey;\n    trigger: (onClick: (event: React.SyntheticEvent) => void) => React.ReactNode;\n    onDone: () => void;\n}\n\nconst StoreQueryDeleteActivity = (props: ExternalProps) => {\n    const { orgName, storeName, storageQueryName, trigger, onDone } = props;\n\n    const deleteDataRequest = useCallback(() => {\n        return apiDelete(orgName, storeName, storageQueryName);\n    }, [orgName, storeName, storageQueryName]);\n\n    const { data, isLoading, error, clearState, fetch } = useApi<GenericOperationResult>(\n        deleteDataRequest,\n        { fetchOnMount: false, requestByFetch: true }\n    );\n\n    const resetHandler = useCallback(() => {\n        clearState();\n    }, [clearState]);\n\n    return (\n        <SingleOperationPopup\n            trigger={trigger}\n            title=\"Delete storage query?\"\n            introMsg={\n                <p>\n                    Are you sure you want to delete the storage query '<b>{storageQueryName}</b>'?\n                </p>\n            }\n            running={isLoading}\n            success={data !== undefined}\n            successMsg={<p>Storage query was deleted successfully.</p>}\n            error={error}\n            reset={resetHandler}\n            onConfirm={fetch}\n            onDone={onDone}\n        />\n    );\n};\n\nexport default StoreQueryDeleteActivity;\n"
  },
  {
    "path": "console2/src/components/pages/JsonStorePage/StoreQueryList.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useState } from 'react';\nimport { Button, Input, List, Menu, InputOnChangeData } from 'semantic-ui-react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { PaginationToolBar } from '../../molecules';\nimport { usePagination } from '../../molecules/PaginationToolBar/usePagination';\nimport {\n    listStorageQuery as apiGet,\n    PaginatedStorageQueryEntries\n} from '../../../api/org/jsonstore';\nimport { useApi } from '../../../hooks/useApi';\nimport RequestErrorActivity from '../../organisms/RequestErrorActivity';\nimport StoreQueryDeleteActivity from './StoreQueryDeleteActivity';\nimport RedirectButton from '../../organisms/RedirectButton';\nimport { LoadingDispatch } from '../../../App';\n\nimport './styles.css';\n\nexport interface ExternalProps {\n    orgName: ConcordKey;\n    storeName: ConcordKey;\n    forceRefresh: any;\n}\n\nconst StoreQueryList = ({ orgName, storeName, forceRefresh }: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const [filter, setFilter] = useState<string>();\n    const {\n        paginationFilter,\n        handleLimitChange,\n        handleNext,\n        handlePrev,\n        handleFirst\n    } = usePagination();\n\n    const fetchData = useCallback(() => {\n        return apiGet(orgName, storeName, paginationFilter.offset, paginationFilter.limit, filter);\n    }, [orgName, storeName, paginationFilter, filter]);\n\n    const { data, error, clearState, fetch } = useApi<PaginatedStorageQueryEntries>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    const filterChangeHandler = useCallback(\n        (event: React.ChangeEvent<HTMLInputElement>, data: InputOnChangeData) => {\n            setFilter(data.value);\n        },\n        []\n    );\n\n    const storageQueryDeleteTrigger = useCallback((onClick: any) => {\n        return (\n            <Button\n                size={'mini'}\n                primary={true}\n                negative={true}\n                onClick={(event) => {\n                    event.stopPropagation();\n                    event.preventDefault();\n                    onClick(event);\n                }}>\n                Delete\n            </Button>\n        );\n    }, []);\n\n    const deleteHandler = useCallback(() => {\n        //TODO: reset pagination?\n        clearState();\n        fetch();\n    }, [clearState, fetch]);\n\n    return (\n        <>\n            <Menu secondary={true}>\n                <Menu.Item>\n                    <Input\n                        icon=\"search\"\n                        placeholder=\"Filter...\"\n                        onChange={filterChangeHandler}\n                        disabled={data === undefined}\n                    />\n                </Menu.Item>\n\n                <Menu.Menu position={'right'}>\n                    <Menu.Item>\n                        <RedirectButton\n                            disabled={false}\n                            icon=\"plus\"\n                            positive={true}\n                            labelPosition=\"left\"\n                            content={`New query`}\n                            location={`/org/${orgName}/jsonstore/${storeName}/query/_new`}\n                        />\n                    </Menu.Item>\n\n                    <Menu.Item style={{ padding: 0 }}>\n                        <PaginationToolBar\n                            limit={paginationFilter.limit}\n                            handleLimitChange={(limit) => handleLimitChange(limit)}\n                            handleNext={handleNext}\n                            handlePrev={handlePrev}\n                            handleFirst={handleFirst}\n                            disablePrevious={paginationFilter.offset === 0}\n                            disableNext={data === undefined ? true : !data.next}\n                            disableFirst={paginationFilter.offset === 0}\n                            disabled={data === undefined}\n                        />\n                    </Menu.Item>\n                </Menu.Menu>\n            </Menu>\n\n            {error && <RequestErrorActivity error={error} />}\n            {data && data.items.length === 0 && <h3>No storage query items found</h3>}\n\n            <List\n                divided={true}\n                relaxed={true}\n                size=\"large\"\n                verticalAlign={'middle'}\n                selection={true}>\n                {data &&\n                    data.items.map((d, idx) => (\n                        <List.Item\n                            key={idx}\n                            as=\"a\"\n                            href={`#/org/${orgName}/jsonstore/${storeName}/query/${d.name}/edit`}\n                            style={{ backgroundColor: idx % 2 === 0 ? '#F5F5F5' : '#FFFFFF' }}>\n                            <List.Content floated=\"right\">\n                                <StoreQueryDeleteActivity\n                                    orgName={orgName}\n                                    storeName={storeName}\n                                    storageQueryName={d.name}\n                                    trigger={storageQueryDeleteTrigger}\n                                    onDone={deleteHandler}\n                                />\n                            </List.Content>\n\n                            <List.Content verticalAlign={'middle'} className=\"itemLink\">\n                                {d.name}\n                            </List.Content>\n                        </List.Item>\n                    ))}\n            </List>\n        </>\n    );\n};\n\nexport default StoreQueryList;\n"
  },
  {
    "path": "console2/src/components/pages/JsonStorePage/StoreSettings.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { useCallback } from 'react';\nimport { Divider, Header, Icon, Menu, Progress, Segment } from 'semantic-ui-react';\n\nimport { ConcordKey } from '../../../api/common';\nimport {\n    get as apiGet,\n    getCapacity as apiGetCapacity,\n    StorageCapacity,\n    StorageEntry\n} from '../../../api/org/jsonstore';\nimport { useApi } from '../../../hooks/useApi';\nimport RequestErrorActivity from '../../organisms/RequestErrorActivity';\nimport StoreOwnerChangeActivity from './StoreOwnerChangeActivity';\nimport StoreDeleteActivity from './StoreDeleteActivity';\nimport StoreVisibilityActivity from './StoreVisibilityActivity';\nimport { formatFileSize } from '../../../utils';\nimport { LoadingDispatch } from '../../../App';\nimport EntityId from '../../molecules/EntityId';\nimport StoreOrganizationChangeActivity from \"./StoreOrganizationChangeActivity\";\n\nexport interface ExternalProps {\n    orgName: ConcordKey;\n    storeName: ConcordKey;\n    forceRefresh: any;\n}\n\nconst StoreSettings = ({ orgName, storeName, forceRefresh }: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const fetchData = useCallback(async () => {\n        const storage = await apiGet(orgName, storeName);\n        const capacity = await apiGetCapacity(orgName, storeName);\n\n        return { ...storage, ...capacity };\n    }, [orgName, storeName]);\n\n    const { data, error } = useApi<StorageEntry & StorageCapacity>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n\n    const disabled = !data;\n\n    return (\n        <>\n            <Menu tabular={false} secondary={true} borderless={true}>\n                <Menu.Item>\n                    <Header as=\"h5\" disabled={true}>\n                        <EntityId id={data?.id} />\n                    </Header>\n                </Menu.Item>\n            </Menu>\n\n            <Segment>\n                <Header as=\"h4\" disabled={disabled}>\n                    Capacity\n                </Header>\n                <Capacity current={data?.size} max={data?.maxSize} />\n            </Segment>\n\n            <Segment>\n                <Header as=\"h4\" disabled={disabled}>\n                    Visibility\n                </Header>\n                <StoreVisibilityActivity\n                    orgName={orgName}\n                    storeName={storeName}\n                    initialVisibility={data?.visibility}\n                    disabled={disabled}\n                />\n            </Segment>\n\n            <Divider horizontal={true} content=\"Danger Zone\" disabled={disabled} />\n\n            <Segment color=\"red\" disabled={disabled}>\n                <Header as=\"h4\" disabled={disabled}>\n                    JSON store owner\n                </Header>\n                <StoreOwnerChangeActivity\n                    orgName={orgName}\n                    storeName={storeName}\n                    initialOwnerId={data?.owner?.id}\n                    disabled={disabled}\n                />\n\n                <Header as=\"h4\">Organization</Header>\n                <StoreOrganizationChangeActivity\n                    orgName={orgName}\n                    storeName={storeName}\n                    disabled={disabled}\n                />\n\n                <Header as=\"h4\" disabled={disabled}>\n                    Delete storage\n                </Header>\n                <StoreDeleteActivity orgName={orgName} storeName={storeName} disabled={disabled} />\n            </Segment>\n        </>\n    );\n};\n\ninterface CapacityProps {\n    current?: number;\n    max?: number;\n}\n\nconst Capacity = ({ current, max }: CapacityProps) => {\n    if (current === undefined || max === undefined) {\n        return (\n            <div>\n                <em>No capacity limits configured</em>\n            </div>\n        );\n    }\n\n    return (\n        <>\n            <div>\n                <Progress\n                    percent={(current / max) * 100}\n                    size={'tiny'}\n                    color={'red'}\n                    style={{ width: '30%' }}\n                />\n            </div>\n            <Header size=\"tiny\" style={{ marginTop: '0px', color: 'rgba(0, 0, 0, 0.5)' }}>\n                <Icon name={'database'} color={'blue'} />\n                Used {formatFileSize(current)} of {formatFileSize(max)}\n            </Header>\n        </>\n    );\n};\n\nexport default StoreSettings;\n"
  },
  {
    "path": "console2/src/components/pages/JsonStorePage/StoreTeamAccessActivity.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useEffect, useState } from 'react';\n\nimport { ConcordKey, GenericOperationResult } from '../../../api/common';\nimport { ResourceAccessEntry } from '../../../api/org';\nimport { TeamAccessList } from '../../molecules';\nimport {\n    getAccess as apiGetAccess,\n    updateAccess as apiUpdateAccess\n} from '../../../api/org/jsonstore';\nimport { useApi } from '../../../hooks/useApi';\nimport { RequestErrorActivity } from '../../organisms';\nimport { LoadingDispatch } from '../../../App';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    storeName: ConcordKey;\n    forceRefresh: boolean;\n}\n\nconst StoreTeamAccessActivity = ({ orgName, storeName, forceRefresh }: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n    const [refresh, toggleRefresh] = useState<boolean>(forceRefresh);\n\n    useEffect(() => {\n        toggleRefresh((prevState) => !prevState);\n    }, [forceRefresh]);\n\n    const fetchData = useCallback(() => {\n        return apiGetAccess(orgName, storeName);\n    }, [orgName, storeName]);\n\n    const { data, error } = useApi<ResourceAccessEntry[]>(fetchData, {\n        fetchOnMount: false,\n        dispatch: dispatch,\n        forceRequest: refresh\n    });\n\n    // using object for making same request after submit error\n    const [value, setValue] = useState({ access: [] as ResourceAccessEntry[] });\n\n    const postData = useCallback(async () => {\n        const result = await apiUpdateAccess(orgName, storeName, value.access);\n        toggleRefresh((prevState) => !prevState);\n        return result;\n    }, [orgName, storeName, value]);\n\n    const post = useApi<GenericOperationResult>(postData, {\n        fetchOnMount: false,\n        dispatch: dispatch\n    });\n\n    const accessChangeHandler = useCallback((entries: ResourceAccessEntry[]) => {\n        setValue({ access: entries });\n    }, []);\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n    if (post.error) {\n        return <RequestErrorActivity error={post.error} />;\n    }\n\n    if (!data) {\n        // TODO: add some loader/loading indicator\n        return <></>;\n    }\n\n    return (\n        <div>\n            <TeamAccessList\n                data={data}\n                submitting={post.isLoading}\n                orgName={orgName}\n                submit={accessChangeHandler}\n            />\n        </div>\n    );\n};\n\nexport default StoreTeamAccessActivity;\n"
  },
  {
    "path": "console2/src/components/pages/JsonStorePage/StoreVisibilityActivity.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport { DropdownProps, Form, Icon } from 'semantic-ui-react';\n\nimport { ConcordKey, GenericOperationResult } from '../../../api/common';\nimport {\n    StorageVisibility,\n    updateVisibility as apiUpdateVisibility\n} from '../../../api/org/jsonstore';\nimport { useApi } from '../../../hooks/useApi';\nimport { RequestErrorActivity } from '../../organisms';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    storeName: ConcordKey;\n    initialVisibility?: StorageVisibility;\n    disabled: boolean;\n}\n\nconst StoreVisibilityActivity = ({\n    orgName,\n    storeName,\n    initialVisibility,\n    disabled\n}: ExternalProps) => {\n    const [value, setValue] = useState<StorageVisibility | undefined>(initialVisibility);\n\n    const postData = useCallback(() => {\n        return apiUpdateVisibility(orgName, storeName, value!);\n    }, [orgName, storeName, value]);\n\n    const { error, isLoading, data, fetch, clearState } = useApi<GenericOperationResult>(postData, {\n        fetchOnMount: false,\n        requestByFetch: true\n    });\n\n    const visibilityChangeHandler = useCallback(\n        (v: StorageVisibility) => {\n            setValue(v);\n            clearState();\n            fetch();\n        },\n        [clearState, fetch]\n    );\n\n    return (\n        <>\n            {error && <RequestErrorActivity error={error} />}\n            <Form loading={isLoading}>\n                <Form.Group>\n                    <RestorableDropdown\n                        options={[\n                            {\n                                text: 'Public',\n                                icon: 'unlock',\n                                value: StorageVisibility.PUBLIC\n                            },\n                            {\n                                text: 'Private',\n                                icon: 'lock',\n                                value: StorageVisibility.PRIVATE\n                            }\n                        ]}\n                        initialValue={initialVisibility}\n                        onChangeValue={visibilityChangeHandler}\n                        error={error !== undefined}\n                        disabled={disabled}\n                    />\n\n                    {data && !isLoading && (\n                        <Form.Field>\n                            <Icon\n                                name={'check'}\n                                color={'green'}\n                                style={{ verticalAlign: 'middle' }}\n                            />\n                        </Form.Field>\n                    )}\n                </Form.Group>\n            </Form>\n        </>\n    );\n};\n\ninterface RestorableDropdownProps {\n    initialValue?: boolean | number | string | (boolean | number | string)[];\n    onChangeValue: (data?: any) => void;\n    error: boolean;\n}\n\n// Dropdown that restore previous value on error change\nconst RestorableDropdown = ({\n    initialValue,\n    onChangeValue,\n    error,\n    ...rest\n}: RestorableDropdownProps & DropdownProps) => {\n    const [value, setValue] = useState({ prev: initialValue, current: initialValue });\n\n    const onChangeHandler = useCallback(\n        (event: React.SyntheticEvent<HTMLElement>, data: DropdownProps) => {\n            onChangeValue(data.value);\n            setValue((prevState) => ({ prev: prevState.current, current: data.value }));\n        },\n        [onChangeValue]\n    );\n\n    // restore previous dropdown value on error\n    useEffect(() => {\n        if (error) {\n            setValue((prevState) => ({ prev: prevState.prev, current: prevState.prev }));\n        }\n    }, [error]);\n\n    useEffect(() => {\n        setValue({ prev: initialValue, current: initialValue });\n    }, [initialValue]);\n\n    return (\n        <Form.Dropdown\n            selection={true}\n            value={value.current}\n            onChange={onChangeHandler}\n            {...rest}\n        />\n    );\n};\n\nexport default StoreVisibilityActivity;\n"
  },
  {
    "path": "console2/src/components/pages/JsonStorePage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useState } from 'react';\nimport { Navigate, Route, Routes } from 'react-router';\nimport { Link } from 'react-router';\nimport { Breadcrumb, Icon, Menu } from 'semantic-ui-react';\n\nimport { RouteComponentProps, withRouter } from '@/router';\nimport { ConcordId } from '../../../api/common';\nimport { NotFoundPage } from '../index';\nimport StoreSettings from './StoreSettings';\nimport StoreTeamAccessActivity from './StoreTeamAccessActivity';\nimport StoreDataList from './StoreDataList';\nimport StoreQueryList from './StoreQueryList';\nimport { LoadingState } from '../../../App';\nimport { AuditLogActivity, BreadcrumbsToolbar } from '../../organisms';\n\ninterface RouteProps {\n    orgName: ConcordId;\n    storeName: ConcordId;\n}\n\ntype TabLink = 'data' | 'query' | 'access' | 'settings' | 'audit' | null;\n\nconst pathToTab = (s: string): TabLink => {\n    if (s.endsWith('/data')) {\n        return 'data';\n    } else if (s.endsWith('/query')) {\n        return 'query';\n    } else if (s.endsWith('/access')) {\n        return 'access';\n    } else if (s.endsWith('/settings')) {\n        return 'settings';\n    } else if (s.endsWith('/audit')) {\n        return 'audit';\n    }\n\n    return null;\n};\n\nconst StoragePage = (props: RouteComponentProps<RouteProps>) => {\n    const loading = React.useContext(LoadingState);\n    const [refresh, toggleRefresh] = useState<boolean>(false);\n\n    const { orgName, storeName } = props.match.params;\n    const activeTab = pathToTab(props.location.pathname);\n\n    const refreshHandler = useCallback(() => {\n        toggleRefresh((prevState) => !prevState);\n    }, []);\n\n    const baseUrl = `/org/${orgName}/jsonstore/${storeName}`;\n\n    return (\n        <>\n            <BreadcrumbsToolbar loading={loading} refreshHandler={refreshHandler}>\n                <Breadcrumb.Section>\n                    <Link to={`/org/${orgName}/jsonstore`}>{orgName}</Link>\n                </Breadcrumb.Section>\n                <Breadcrumb.Divider />\n                <Breadcrumb.Section active={true}>{storeName}</Breadcrumb.Section>\n            </BreadcrumbsToolbar>\n\n            <Menu tabular={true} style={{ marginTop: 0 }}>\n                <Menu.Item active={activeTab === 'data'}>\n                    <Icon name=\"file alternate\" />\n                    <Link to={`${baseUrl}/data`}>Data</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'query'}>\n                    <Icon name=\"search\" />\n                    <Link to={`${baseUrl}/query`}>Queries</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'access'}>\n                    <Icon name=\"key\" />\n                    <Link to={`${baseUrl}/access`}>Access</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'settings'}>\n                    <Icon name=\"setting\" />\n                    <Link to={`${baseUrl}/settings`}>Settings</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'audit'}>\n                    <Icon name=\"history\" />\n                    <Link to={`${baseUrl}/audit`}>Audit Log</Link>\n                </Menu.Item>\n            </Menu>\n\n            <Routes>\n                <Route index={true} element={<Navigate to=\"data\" replace={true} />} />\n                <Route\n                    path=\"data\"\n                    element={\n                        <StoreDataList\n                            orgName={orgName}\n                            storeName={storeName}\n                            forceRefresh={refresh}\n                        />\n                    }\n                />\n                <Route\n                    path=\"query\"\n                    element={\n                        <StoreQueryList\n                            orgName={orgName}\n                            storeName={storeName}\n                            forceRefresh={refresh}\n                        />\n                    }\n                />\n                <Route\n                    path=\"access\"\n                    element={\n                        <StoreTeamAccessActivity\n                            orgName={orgName}\n                            storeName={storeName}\n                            forceRefresh={refresh}\n                        />\n                    }\n                />\n                <Route\n                    path=\"settings\"\n                    element={\n                        <StoreSettings\n                            orgName={orgName}\n                            storeName={storeName}\n                            forceRefresh={refresh}\n                        />\n                    }\n                />\n                <Route\n                    path=\"audit\"\n                    element={\n                        <AuditLogActivity\n                            forceRefresh={refresh}\n                            showRefreshButton={false}\n                            filter={{ details: { orgName: orgName, jsonStoreName: storeName } }}\n                        />\n                    }\n                />\n                <Route path=\"*\" element={<NotFoundPage />} />\n            </Routes>\n        </>\n    );\n};\n\nexport default withRouter(StoragePage);\n"
  },
  {
    "path": "console2/src/components/pages/JsonStorePage/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n.editorContainer {\n    margin: 0 0 1em;\n}\n\n.editorContainer.loading {\n    opacity: 0.45;\n}\n\n.editorContainer .editor {\n    border: 1px solid rgba(34,36,38,.15);\n    border-radius: .28571429rem;\n    height: 40vh;\n}\n\n.itemLink {\n    color: #4183c4;\n}\n\n.itemLink:hover {\n    color: #1e70bf;\n}\n\n.viewModeToggle {\n    margin: 5px !important;\n}"
  },
  {
    "path": "console2/src/components/pages/LoginPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport { Login2 } from '../../organisms';\n\nimport './styles.css';\n\nexport default class extends React.PureComponent {\n    render() {\n        return (\n            <div className=\"flexbox-container\">\n                <Login2 />\n            </div>\n        );\n    }\n}\n"
  },
  {
    "path": "console2/src/components/pages/LoginPage/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n.flexbox-container {\n  display: -ms-flexbox;\n  display: -webkit-flex;\n  display: flex;\n\n  -ms-flex-align: center;\n  -webkit-align-items: center;\n  -webkit-box-align: center;\n  align-items: center;\n\n  height: 100%;\n}\n\n#concord-logo {\n  padding: 1em;\n  margin-bottom: 1em;\n}\n"
  },
  {
    "path": "console2/src/components/pages/LogoutPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport './styles.css';\nimport { Card, CardContent, CardHeader, Divider, Image } from 'semantic-ui-react';\nimport { RedirectButton } from '../../organisms';\n\nexport default class extends React.PureComponent {\n    render() {\n        return (\n            <div className=\"flexbox-container\">\n                <Card centered={true}>\n                    <CardContent textAlign={'center'}>\n                        <Image id=\"concord-logo\" src=\"/images/concord.svg\" size=\"medium\" />\n\n                        <CardHeader>You are now logged out.</CardHeader>\n\n                        <Divider />\n\n                        <RedirectButton primary={true} fluid={true} location={'/'}>\n                            Login again\n                        </RedirectButton>\n                    </CardContent>\n                </Card>\n            </div>\n        );\n    }\n}\n"
  },
  {
    "path": "console2/src/components/pages/LogoutPage/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n.flexbox-container {\n  display: -ms-flexbox;\n  display: -webkit-flex;\n  display: flex;\n\n  -ms-flex-align: center;\n  -webkit-align-items: center;\n  -webkit-box-align: center;\n  align-items: center;\n\n  height: 100%;\n}\n\n#concord-logo {\n  padding: 1em;\n  margin-bottom: 1em;\n}\n"
  },
  {
    "path": "console2/src/components/pages/NewAPITokenPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Menu } from 'semantic-ui-react';\nimport { NewAPITokenActivity } from '../../organisms';\n\nclass NewAPITokenPage extends React.PureComponent {\n    render() {\n        return (\n            <>\n                <Menu secondary={true} pointing={true}>\n                    <Menu.Item position={'left'}>\n                        <h4>New API Token</h4>\n                    </Menu.Item>\n                </Menu>\n\n                <NewAPITokenActivity />\n            </>\n        );\n    }\n}\n\nexport default NewAPITokenPage;\n"
  },
  {
    "path": "console2/src/components/pages/NewProjectPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { RouteComponentProps, withRouter } from '@/router';\nimport { Link } from 'react-router';\nimport { Breadcrumb, Container, Header, Segment } from 'semantic-ui-react';\n\nimport { BreadcrumbSegment } from '../../molecules';\nimport { NewProjectActivity } from '../../organisms';\n\ninterface RouteProps {\n    orgName: string;\n}\n\nclass NewProjectPage extends React.PureComponent<RouteComponentProps<RouteProps>> {\n    render() {\n        const { orgName } = this.props.match.params;\n\n        return (\n            <>\n                <BreadcrumbSegment>\n                    <Breadcrumb.Section>\n                        <Link to=\"/org\">Organizations</Link>\n                    </Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section>\n                        <Link to={`/org/${orgName}`}>{orgName}</Link>\n                    </Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section active={true}>New project</Breadcrumb.Section>\n                </BreadcrumbSegment>\n\n                <Segment basic={true}>\n                    <Container text={true}>\n                        <Header>Create a New Project</Header>\n                        <NewProjectActivity orgName={orgName} />\n                    </Container>\n                </Segment>\n            </>\n        );\n    }\n}\n\n// TODO use OrgActivityPage\nexport default withRouter(NewProjectPage);\n"
  },
  {
    "path": "console2/src/components/pages/NewSecretPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { RouteComponentProps, withRouter } from '@/router';\nimport { Link } from 'react-router';\nimport { Breadcrumb, Container, Header, Segment } from 'semantic-ui-react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { BreadcrumbSegment } from '../../molecules';\nimport { NewSecretActivity } from '../../organisms';\n\ninterface RouteProps {\n    orgName: ConcordKey;\n}\n\nclass NewSecretPage extends React.PureComponent<RouteComponentProps<RouteProps>> {\n    render() {\n        const { orgName } = this.props.match.params;\n\n        return (\n            <>\n                <BreadcrumbSegment>\n                    <Breadcrumb.Section>\n                        <Link to={`/org/${orgName}/secret`}>{orgName}</Link>\n                    </Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section active={true}>New Secret</Breadcrumb.Section>\n                </BreadcrumbSegment>\n\n                <Segment basic={true}>\n                    <Container text={true}>\n                        <Header>Create a New Secret</Header>\n                        <NewSecretActivity orgName={orgName} />\n                    </Container>\n                </Segment>\n            </>\n        );\n    }\n}\n\n// TODO use OrgActivityPage\nexport default withRouter(NewSecretPage);\n"
  },
  {
    "path": "console2/src/components/pages/NewTeamPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Link } from 'react-router';\nimport { Breadcrumb } from 'semantic-ui-react';\nimport { NewTeamActivity } from '../../organisms';\nimport { OrgActivityPage } from '../../templates';\n\nexport default () => (\n    <OrgActivityPage\n        title=\"Create a New Team\"\n        breadcrumbs={(orgName) => (\n            <>\n                <Breadcrumb.Section>\n                    <Link to={`/org/${orgName}/team`}>{orgName}</Link>\n                </Breadcrumb.Section>\n                <Breadcrumb.Divider />\n                <Breadcrumb.Section active={true}>New Team</Breadcrumb.Section>\n            </>\n        )}\n        activity={(orgName) => <NewTeamActivity orgName={orgName} />}\n    />\n);\n"
  },
  {
    "path": "console2/src/components/pages/NodeRoster/HostArtifacts.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useState } from 'react';\nimport { Input, List, Menu, Popup, Table } from 'semantic-ui-react';\n\nimport { ConcordId } from '../../../api/common';\nimport { PaginationToolBar } from '../../molecules';\nimport { usePagination } from '../../molecules/PaginationToolBar/usePagination';\nimport {\n    HostArtifact,\n    listHostArtifacts as apiList,\n    PaginatedHostArtifacts\n} from '../../../api/noderoster';\nimport { useApi } from '../../../hooks/useApi';\nimport { InputOnChangeData } from 'semantic-ui-react/dist/commonjs/elements/Input/Input';\nimport RequestErrorActivity from '../../organisms/RequestErrorActivity';\nimport { LoadingDispatch } from '../../../App';\nimport { Link } from 'react-router';\n\nexport interface ExternalProps {\n    hostId: ConcordId;\n    forceRefresh: any;\n}\n\nconst HostArtifacts = ({ hostId, forceRefresh }: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const [filter, setFilter] = useState<string>();\n    const {\n        paginationFilter,\n        handleLimitChange,\n        handleNext,\n        handlePrev,\n        handleFirst\n    } = usePagination();\n\n    const fetchData = useCallback(() => {\n        return apiList(hostId, paginationFilter.offset, paginationFilter.limit, filter);\n    }, [hostId, paginationFilter, filter]);\n\n    const { data, error } = useApi<PaginatedHostArtifacts>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    const filterChangeHandler = useCallback(\n        (event: React.ChangeEvent<HTMLInputElement>, data: InputOnChangeData) => {\n            setFilter(data.value);\n        },\n        []\n    );\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n\n    return (\n        <>\n            <Menu secondary={true}>\n                <Menu.Item style={{ paddingTop: 0, paddingBottom: 0 }}>\n                    <Input\n                        icon=\"search\"\n                        placeholder=\"Filter...\"\n                        onChange={filterChangeHandler}\n                        disabled={data === undefined}\n                    />\n                </Menu.Item>\n\n                <Menu.Item style={{ padding: 0 }} position={'right'}>\n                    <PaginationToolBar\n                        limit={paginationFilter.limit}\n                        handleLimitChange={(limit) => handleLimitChange(limit)}\n                        handleNext={handleNext}\n                        handlePrev={handlePrev}\n                        handleFirst={handleFirst}\n                        disablePrevious={paginationFilter.offset === 0}\n                        disableNext={data === undefined ? true : !data.next}\n                        disableFirst={paginationFilter.offset === 0}\n                        disabled={data === undefined}\n                    />\n                </Menu.Item>\n            </Menu>\n\n            <HostArtifactList data={data?.items} />\n        </>\n    );\n};\n\ninterface HostArtifactListProps {\n    data?: HostArtifact[];\n}\n\nconst HostArtifactList = ({ data }: HostArtifactListProps) => {\n    return (\n        <Table\n            celled={true}\n            attached=\"bottom\"\n            selectable={true}\n            style={data === undefined ? { opacity: 0.4 } : {}}>\n            <Table.Header>\n                <Table.Row>\n                    <Table.HeaderCell collapsing={true}>Filename</Table.HeaderCell>\n                    <Table.HeaderCell collapsing={true}>Process ID</Table.HeaderCell>\n                </Table.Row>\n            </Table.Header>\n\n            {data && <Table.Body>{renderTableBody(data)}</Table.Body>}\n        </Table>\n    );\n};\n\nconst renderTableRow = (h: HostArtifact, idx: number) => {\n    return (\n        <Table.Row key={idx}>\n            <Table.Cell collapsing={true}>\n                <Popup\n                    key={idx}\n                    wide=\"very\"\n                    mouseEnterDelay={500}\n                    content={h.url}\n                    trigger={\n                        <List.Item key={idx}>\n                            <List.Content verticalAlign={'middle'}>\n                                {getArtifactName(h.url)}\n                            </List.Content>\n                        </List.Item>\n                    }\n                />\n            </Table.Cell>\n            <Table.Cell collapsing={true}>\n                <Link to={`/process/${h.processInstanceId}`}>{h.processInstanceId}</Link>\n            </Table.Cell>\n        </Table.Row>\n    );\n};\n\nconst renderTableBody = (data: HostArtifact[]) => {\n    if (data.length === 0) {\n        return (\n            <Table.Row style={{ fontWeight: 'bold' }}>\n                <Table.Cell colSpan={2}>No data available</Table.Cell>\n            </Table.Row>\n        );\n    }\n\n    return data.map((h, idx) => renderTableRow(h, idx));\n};\n\nconst getArtifactName = (url: string) => {\n    return url.split(/[/]+/).pop();\n};\n\nexport default HostArtifacts;\n"
  },
  {
    "path": "console2/src/components/pages/NodeRoster/HostFacts.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback } from 'react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { getLatestHostFacts as apiGet } from '../../../api/noderoster';\nimport { useApi } from '../../../hooks/useApi';\nimport RequestErrorActivity from '../../organisms/RequestErrorActivity';\nimport { ReactJson } from '../../atoms';\nimport { LoadingDispatch } from '../../../App';\n\nexport interface ExternalProps {\n    hostId: ConcordKey;\n    forceRefresh: any;\n}\n\nconst HostFacts = ({ hostId, forceRefresh }: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const fetchData = useCallback(() => {\n        return apiGet(hostId);\n    }, [hostId]);\n\n    const { data, error } = useApi<Object>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n\n    return (\n        <ReactJson\n            src={data === undefined ? {} : data}\n            collapsed={false}\n            name={null}\n            enableClipboard={true}\n            displayObjectSize={false}\n            displayDataTypes={false}\n            style={data === undefined ? { opacity: 0.4 } : { overflow: 'auto' }}\n        />\n    );\n};\n\nexport default HostFacts;\n"
  },
  {
    "path": "console2/src/components/pages/NodeRoster/HostPage.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Navigate, Route, Routes } from 'react-router';\nimport { Link } from 'react-router';\nimport { Breadcrumb, Icon, Menu } from 'semantic-ui-react';\n\nimport { RouteComponentProps, withRouter } from '@/router';\nimport { ConcordId } from '../../../api/common';\nimport { MainToolbar } from '../../molecules';\nimport { useRef } from 'react';\nimport { useCallback } from 'react';\nimport { useState } from 'react';\nimport { NotFoundPage } from '../index';\nimport { LoadingDispatch, LoadingState } from '../../../App';\nimport { getHost as apiGetHost, HostEntry } from '../../../api/noderoster';\nimport { useApi } from '../../../hooks/useApi';\nimport RequestErrorActivity from '../../organisms/RequestErrorActivity';\nimport HostFacts from './HostFacts';\nimport HostArtifacts from './HostArtifacts';\nimport HostProcesses from './HostProcesses';\n\ninterface RouteProps {\n    id: ConcordId;\n}\n\ntype TabLink = 'facts' | 'artifacts' | 'processes' | null;\n\nconst pathToTab = (s: string): TabLink => {\n    if (s.endsWith('/facts')) {\n        return 'facts';\n    } else if (s.endsWith('/artifacts')) {\n        return 'artifacts';\n    } else if (s.endsWith('/processes')) {\n        return 'processes';\n    }\n\n    return null;\n};\n\nconst HostPage = (props: RouteComponentProps<RouteProps>) => {\n    const stickyRef = useRef(null);\n\n    const loading = React.useContext(LoadingState);\n    const dispatch = React.useContext(LoadingDispatch);\n    const [refresh, toggleRefresh] = useState<boolean>(false);\n\n    const { id } = props.match.params;\n    const activeTab = pathToTab(props.location.pathname);\n\n    const fetchData = useCallback(() => {\n        return apiGetHost(id);\n    }, [id]);\n\n    const { data, error } = useApi<HostEntry>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: refresh,\n        dispatch: dispatch,\n    });\n\n    const refreshHandler = useCallback(() => {\n        toggleRefresh((prevState) => !prevState);\n    }, []);\n\n    const baseUrl = `/noderoster/host/${id}`;\n\n    return (\n        <div ref={stickyRef}>\n            <MainToolbar\n                loading={loading}\n                refresh={refreshHandler}\n                stickyRef={stickyRef}\n                breadcrumbs={renderBreadcrumbs(data?.name)}\n            />\n\n            {error && <RequestErrorActivity error={error} />}\n\n            {!error && (\n                <Menu tabular={true} style={{ marginTop: 0 }}>\n                    <Menu.Item active={activeTab === 'facts'}>\n                        <Icon name=\"th\" />\n                        <Link to={`${baseUrl}/facts`}>Facts</Link>\n                    </Menu.Item>\n                    <Menu.Item active={activeTab === 'artifacts'}>\n                        <Icon name=\"cubes\" />\n                        <Link to={`${baseUrl}/artifacts`}>Artifacts</Link>\n                    </Menu.Item>\n                    <Menu.Item active={activeTab === 'processes'}>\n                        <Icon name=\"tasks\" />\n                        <Link to={`${baseUrl}/processes`}>Processes</Link>\n                    </Menu.Item>\n                </Menu>\n            )}\n\n            {!error && (\n                <Routes>\n                    <Route index={true} element={<Navigate to=\"facts\" replace={true} />} />\n                    <Route\n                        path=\"facts\"\n                        element={<HostFacts hostId={id} forceRefresh={refresh} />}\n                    />\n                    <Route\n                        path=\"artifacts\"\n                        element={<HostArtifacts hostId={id} forceRefresh={refresh} />}\n                    />\n                    <Route\n                        path=\"processes\"\n                        element={<HostProcesses hostId={id} forceRefresh={refresh} />}\n                    />\n                    <Route path=\"*\" element={<NotFoundPage />} />\n                </Routes>\n            )}\n        </div>\n    );\n};\n\nconst renderBreadcrumbs = (hostName?: string) => {\n    return (\n        <Breadcrumb size=\"big\">\n            <Breadcrumb.Section>\n                <Link to={`/noderoster`}>Node Roster</Link>\n            </Breadcrumb.Section>\n            <Breadcrumb.Divider />\n            <Breadcrumb.Section active={true}>\n                {hostName === undefined ? '...' : hostName}\n            </Breadcrumb.Section>\n        </Breadcrumb>\n    );\n};\n\nexport default withRouter(HostPage);\n"
  },
  {
    "path": "console2/src/components/pages/NodeRoster/HostProcesses.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback } from 'react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { listHostProcesses as apiList, PaginatedHostProcessEntry } from '../../../api/noderoster';\nimport { useApi } from '../../../hooks/useApi';\nimport RequestErrorActivity from '../../organisms/RequestErrorActivity';\nimport { LoadingDispatch } from '../../../App';\nimport { PaginationToolBar, ProcessList } from '../../molecules';\nimport {\n    CREATED_AT_COLUMN,\n    INITIATOR_COLUMN,\n    INSTANCE_ID_COLUMN,\n    PROJECT_COLUMN\n} from '../../molecules/ProcessList';\nimport { usePagination } from '../../molecules/PaginationToolBar/usePagination';\nimport { Menu } from 'semantic-ui-react';\n\nexport interface ExternalProps {\n    hostId: ConcordKey;\n    forceRefresh: any;\n}\n\nconst COLUMNS = [INSTANCE_ID_COLUMN, PROJECT_COLUMN, INITIATOR_COLUMN, CREATED_AT_COLUMN];\n\nconst HostProcesses = ({ hostId, forceRefresh }: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const {\n        paginationFilter,\n        handleLimitChange,\n        handleNext,\n        handlePrev,\n        handleFirst\n    } = usePagination();\n\n    const fetchData = useCallback(() => {\n        return apiList(hostId, paginationFilter.offset, paginationFilter.limit);\n    }, [hostId, paginationFilter]);\n\n    const { data, error } = useApi<PaginatedHostProcessEntry>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    if (error) {\n        return <RequestErrorActivity error={error} />;\n    }\n\n    return (\n        <>\n            <Menu secondary={true}>\n                <Menu.Item style={{ padding: 0 }} position={'right'}>\n                    <PaginationToolBar\n                        limit={paginationFilter.limit}\n                        handleLimitChange={(limit) => handleLimitChange(limit)}\n                        handleNext={handleNext}\n                        handlePrev={handlePrev}\n                        handleFirst={handleFirst}\n                        disablePrevious={paginationFilter.offset === 0}\n                        disableNext={data === undefined ? true : !data.next}\n                        disableFirst={paginationFilter.offset === 0}\n                        disabled={data === undefined}\n                    />\n                </Menu.Item>\n            </Menu>\n\n            <ProcessList data={data?.items} columns={COLUMNS} />\n        </>\n    );\n};\n\nexport default HostProcesses;\n"
  },
  {
    "path": "console2/src/components/pages/NodeRoster/NodeRosterArtifactsList.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useState } from 'react';\nimport { Link } from 'react-router';\nimport { Form, Icon, Menu, Popup, Table } from 'semantic-ui-react';\n\nimport {\n    HostEntry,\n    HostFilter,\n    listHosts as apiListHosts,\n    PaginatedHostEntry\n} from '../../../api/noderoster/index';\nimport { LocalTimestamp, PaginationToolBar } from '../../molecules';\nimport { usePagination } from '../../molecules/PaginationToolBar/usePagination';\nimport { LoadingDispatch } from '../../../App';\nimport RequestErrorActivity from '../../organisms/RequestErrorActivity';\nimport { useApi } from '../../../hooks/useApi';\n\nexport interface ExternalProps {\n    forceRefresh: any;\n}\n\nconst NodeRosterArtifactsList = ({ forceRefresh }: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n    const {\n        paginationFilter,\n        handleLimitChange,\n        handleNext,\n        handlePrev,\n        handleFirst,\n        resetOffset\n    } = usePagination();\n    const [hostFilter, setHostFilter] = useState<string>();\n    const [artifactFilter, setArtifactFilter] = useState<string>();\n    const [filter, setFilter] = useState<HostFilter>();\n\n    const fetchData = useCallback(() => {\n        return apiListHosts(paginationFilter.offset, paginationFilter.limit, ['artifacts'], filter);\n    }, [paginationFilter, filter]);\n\n    const { data, error } = useApi<PaginatedHostEntry>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    const disabled = data === undefined;\n\n    return (\n        <>\n            <Menu secondary={true} style={{ marginTop: 0 }}>\n                <Menu.Item style={{ padding: 0 }}>\n                    <Form>\n                        <Form.Group widths=\"equal\" inline={true} style={{ margin: 0 }}>\n                            <Form.Input\n                                placeholder=\"Hostname\"\n                                disabled={disabled}\n                                onChange={(ev, data) => setHostFilter(data.value)}\n                            />\n\n                            <Form.Input\n                                placeholder=\"Artifact\"\n                                disabled={disabled}\n                                onChange={(ev, data) => setArtifactFilter(data.value)}\n                            />\n\n                            <Form.Button\n                                content=\"Search\"\n                                icon=\"search\"\n                                primary={true}\n                                onClick={() => {\n                                    setFilter({\n                                        host: hostFilter,\n                                        artifact: artifactFilter\n                                    });\n                                    resetOffset(0);\n                                }}\n                                disabled={disabled}\n                            />\n                        </Form.Group>\n                    </Form>\n                </Menu.Item>\n\n                <Menu.Item style={{ padding: 0 }} position=\"right\">\n                    <PaginationToolBar\n                        limit={paginationFilter.limit}\n                        handleLimitChange={(limit) => handleLimitChange(limit)}\n                        handleNext={handleNext}\n                        handlePrev={handlePrev}\n                        handleFirst={handleFirst}\n                        disablePrevious={paginationFilter.offset === 0}\n                        disableNext={!data?.next}\n                        disableFirst={paginationFilter.offset === 0}\n                        disabled={disabled}\n                    />\n                </Menu.Item>\n            </Menu>\n\n            {error && <RequestErrorActivity error={error} />}\n\n            <Table celled={true} selectable={!disabled} style={disabled ? { opacity: 0.4 } : {}}>\n                <Table.Header>\n                    <Table.Row>\n                        <Table.HeaderCell width={8}>Hostname</Table.HeaderCell>\n                        <Table.HeaderCell width={2}>\n                            Registered At{' '}\n                            <Popup\n                                trigger={<Icon name=\"question circle outline\" />}\n                                content=\"Date and time when the host was registered by Node Roster\"\n                            />\n                        </Table.HeaderCell>\n                        <Table.HeaderCell width={6}>Filename</Table.HeaderCell>\n                    </Table.Row>\n                </Table.Header>\n\n                <Table.Body>{renderItems(data)}</Table.Body>\n            </Table>\n        </>\n    );\n};\n\nconst renderItems = (data?: PaginatedHostEntry) => {\n    if (!data) {\n        return (\n            <tr>\n                <Table.Cell colSpan={3}>&nbsp;</Table.Cell>\n            </tr>\n        );\n    }\n\n    if (data.items.length === 0) {\n        return (\n            <tr style={{ fontWeight: 'bold' }}>\n                <Table.Cell colSpan={3}>No data available</Table.Cell>\n            </tr>\n        );\n    }\n\n    return data.items.map((h, idx) => renderTableRow(idx, h));\n};\n\nconst renderTableRow = (idx: number, h: HostEntry) => {\n    return (\n        <Table.Row key={idx}>\n            <Table.Cell collapsing={true}>\n                <Link to={`/noderoster/host/${h.id}`}>{h.name}</Link>\n            </Table.Cell>\n            <Table.Cell collapsing={true}>\n                <LocalTimestamp value={h.createdAt} />\n            </Table.Cell>\n            <Table.Cell collapsing={true}>{getArtifactName(h.artifactUrl)}</Table.Cell>\n        </Table.Row>\n    );\n};\n\nconst getArtifactName = (url?: string) => {\n    if (url === undefined) {\n        return undefined;\n    }\n    return url.split(/[/]+/).pop();\n};\n\nexport default NodeRosterArtifactsList;\n"
  },
  {
    "path": "console2/src/components/pages/NodeRoster/NodeRosterHostsList.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport { Link } from 'react-router';\nimport { Form, Icon, Menu, Popup, Table } from 'semantic-ui-react';\n\nimport {\n    HostEntry,\n    HostFilter,\n    listHosts as apiListHosts,\n    PaginatedHostEntry\n} from '../../../api/noderoster/index';\nimport { LocalTimestamp, PaginationToolBar } from '../../molecules';\nimport { usePagination } from '../../molecules/PaginationToolBar/usePagination';\nimport { LoadingDispatch } from '../../../App';\nimport RequestErrorActivity from '../../organisms/RequestErrorActivity';\nimport { useApi } from '../../../hooks/useApi';\n\nexport interface ExternalProps {\n    forceRefresh: any;\n}\n\nconst NodeRosterHostsList = ({ forceRefresh }: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n    const {\n        paginationFilter,\n        handleLimitChange,\n        handleNext,\n        handlePrev,\n        handleFirst,\n        resetOffset\n    } = usePagination();\n    const [hostFilter, setHostFilter] = useState<string>();\n    const [processInstanceIdFilter, setProcessInstanceIdFilter] = useState<string>();\n    const [processInstanceIdFilterError, setProcessInstanceIdFilterError] = useState<boolean>();\n    const [filter, setFilter] = useState<HostFilter>();\n    const [searchEnabled, setSearchEnabled] = useState<boolean>(false);\n\n    const fetchData = useCallback(() => {\n        return apiListHosts(paginationFilter.offset, paginationFilter.limit, [], filter);\n    }, [paginationFilter, filter]);\n\n    const { data, error, isLoading } = useApi<PaginatedHostEntry>(fetchData, {\n        fetchOnMount: false,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    useEffect(() => {\n        let valid = true;\n\n        if (notEmpty(processInstanceIdFilter)) {\n            valid = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(\n                processInstanceIdFilter!\n            );\n            setProcessInstanceIdFilterError(!valid);\n        } else {\n            setProcessInstanceIdFilterError(false);\n        }\n\n        setSearchEnabled(valid);\n    }, [hostFilter, processInstanceIdFilter]);\n\n    return (\n        <>\n            <Menu secondary={true} style={{ marginTop: 0 }}>\n                <Menu.Item style={{ padding: 0 }}>\n                    <Form>\n                        <Form.Group widths=\"equal\" inline={true} style={{ margin: 0 }}>\n                            <Form.Input\n                                placeholder=\"Hostname\"\n                                disabled={isLoading}\n                                onChange={(ev, data) => setHostFilter(data.value)}\n                            />\n\n                            <Form.Input\n                                placeholder=\"Process ID\"\n                                disabled={isLoading}\n                                maxLength={36}\n                                style={{ width: 305 }}\n                                error={processInstanceIdFilterError}\n                                onChange={(ev, data) => setProcessInstanceIdFilter(data.value)}\n                            />\n\n                            <Form.Button\n                                content=\"Search\"\n                                icon=\"search\"\n                                primary={true}\n                                onClick={() => {\n                                    setFilter({\n                                        host: hostFilter,\n                                        processInstanceId: processInstanceIdFilter\n                                    });\n                                    resetOffset(0);\n                                }}\n                                disabled={isLoading || !searchEnabled}\n                            />\n                        </Form.Group>\n                    </Form>\n                </Menu.Item>\n\n                <Menu.Item style={{ padding: 0 }} position=\"right\">\n                    <PaginationToolBar\n                        limit={paginationFilter.limit}\n                        handleLimitChange={(limit) => handleLimitChange(limit)}\n                        handleNext={handleNext}\n                        handlePrev={handlePrev}\n                        handleFirst={handleFirst}\n                        disablePrevious={paginationFilter.offset === 0}\n                        disableNext={!data?.next}\n                        disableFirst={paginationFilter.offset === 0}\n                        disabled={isLoading}\n                    />\n                </Menu.Item>\n            </Menu>\n\n            {error && <RequestErrorActivity error={error} />}\n\n            <Table celled={true} selectable={!isLoading} style={isLoading ? { opacity: 0.4 } : {}}>\n                <Table.Header>\n                    <Table.Row>\n                        <Table.HeaderCell collapsing={true}>Hostname</Table.HeaderCell>\n                        <Table.HeaderCell collapsing={true}>\n                            Registered At{' '}\n                            <Popup\n                                trigger={<Icon name=\"question circle outline\" />}\n                                content=\"Date and time when the host was registered by Node Roster\"\n                            />\n                        </Table.HeaderCell>\n                    </Table.Row>\n                </Table.Header>\n\n                <Table.Body>{renderItems(data)}</Table.Body>\n            </Table>\n        </>\n    );\n};\n\nconst renderItems = (data?: PaginatedHostEntry) => {\n    if (!data) {\n        return (\n            <tr>\n                <Table.Cell colSpan={2}>&nbsp;</Table.Cell>\n            </tr>\n        );\n    }\n\n    if (data.items.length === 0) {\n        return (\n            <tr style={{ fontWeight: 'bold' }}>\n                <Table.Cell colSpan={2}>No data available</Table.Cell>\n            </tr>\n        );\n    }\n\n    return data.items.map((h, idx) => renderTableRow(idx, h));\n};\n\nconst renderTableRow = (idx: number, h: HostEntry) => {\n    return (\n        <Table.Row key={idx}>\n            <Table.Cell collapsing={true}>\n                <Link to={`/noderoster/host/${h.id}`}>{h.name}</Link>\n            </Table.Cell>\n            <Table.Cell collapsing={true}>\n                <LocalTimestamp value={h.createdAt} />\n            </Table.Cell>\n        </Table.Row>\n    );\n};\n\nconst notEmpty = (x?: string) => {\n    return x !== undefined && x !== '';\n};\n\nexport default NodeRosterHostsList;\n"
  },
  {
    "path": "console2/src/components/pages/NodeRoster/NodeRosterPage.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useRef, useState } from 'react';\nimport { Breadcrumb, Icon, Menu } from 'semantic-ui-react';\n\nimport { LoadingState } from '../../../App';\nimport { Link } from 'react-router';\nimport { Navigate, Route, Routes } from 'react-router';\nimport { RouteComponentProps, withRouter } from '@/router';\nimport { NotFoundPage } from '../index';\nimport NodeRosterHostsList from './NodeRosterHostsList';\nimport NodeRosterArtifactsList from './NodeRosterArtifactsList';\nimport { BreadcrumbsToolbar } from '../../organisms';\n\ntype TabLink = 'hosts' | 'artifacts' | null;\n\nconst pathToTab = (s: string): TabLink => {\n    if (s.endsWith('/hosts')) {\n        return 'hosts';\n    } else if (s.endsWith('/artifacts')) {\n        return 'artifacts';\n    }\n\n    return null;\n};\n\nconst NodeRosterPage = (props: RouteComponentProps) => {\n    const activeTab = pathToTab(props.location.pathname);\n\n    const stickyRef = useRef(null);\n\n    const loading = React.useContext(LoadingState);\n    const [refresh, toggleRefresh] = useState<boolean>(false);\n\n    const refreshHandler = useCallback(() => {\n        toggleRefresh((prevState) => !prevState);\n    }, []);\n\n    const baseUrl = `/noderoster`;\n\n    return (\n        <div ref={stickyRef}>\n            <BreadcrumbsToolbar loading={loading} refreshHandler={refreshHandler}>\n                <Breadcrumb.Section active={true}>Node Roster</Breadcrumb.Section>\n            </BreadcrumbsToolbar>\n\n            <Menu tabular={true} style={{ marginTop: 0 }}>\n                <Menu.Item active={activeTab === 'hosts'}>\n                    <Icon name=\"server\" />\n                    <Link to={`${baseUrl}/hosts`}>Hosts</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'artifacts'}>\n                    <Icon name=\"cubes\" />\n                    <Link to={`${baseUrl}/artifacts`}>Artifacts</Link>\n                </Menu.Item>\n            </Menu>\n\n            <Routes>\n                <Route index={true} element={<Navigate to=\"hosts\" replace={true} />} />\n                <Route path=\"hosts\" element={<NodeRosterHostsList forceRefresh={refresh} />} />\n                <Route\n                    path=\"artifacts\"\n                    element={<NodeRosterArtifactsList forceRefresh={refresh} />}\n                />\n                <Route path=\"*\" element={<NotFoundPage />} />\n            </Routes>\n        </div>\n    );\n};\n\nexport default withRouter(NodeRosterPage);\n"
  },
  {
    "path": "console2/src/components/pages/NotFoundPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nexport default class extends React.PureComponent {\n    render() {\n        return <h1>404 Not Found</h1>;\n    }\n}\n"
  },
  {
    "path": "console2/src/components/pages/OrganizationListPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport { OrganizationList } from '../../organisms';\nimport { LoadingState } from '../../../App';\nimport { useCallback, useState } from 'react';\nimport { BreadcrumbsToolbar } from '../../organisms';\nimport { Breadcrumb } from 'semantic-ui-react';\n\nexport default () => {\n    const loading = React.useContext(LoadingState);\n\n    const [refresh, toggleRefresh] = useState<boolean>(false);\n\n    const refreshHandler = useCallback(() => {\n        toggleRefresh((prevState) => !prevState);\n    }, []);\n\n    return (\n        <>\n            <BreadcrumbsToolbar loading={loading} refreshHandler={refreshHandler}>\n                <Breadcrumb.Section active={true}>Organizations</Breadcrumb.Section>\n            </BreadcrumbsToolbar>\n\n            <OrganizationList forceRefresh={refresh} />\n        </>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/pages/OrganizationPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { RouteComponentProps, withRouter } from '@/router';\n\nimport { TabLink } from '../../organisms/OrganizationActivity';\nimport { OrganizationActivity } from '../../organisms';\nimport { LoadingState } from '../../../App';\nimport { useCallback, useState } from 'react';\nimport { BreadcrumbsToolbar } from '../../organisms';\nimport { Breadcrumb } from 'semantic-ui-react';\nimport { Link } from 'react-router';\n\ninterface RouteProps {\n    orgName: string;\n}\n\nconst pathToTab = (s: string): TabLink => {\n    if (s.endsWith('/process')) {\n        return 'process';\n    } else if (s.endsWith('/project')) {\n        return 'project';\n    } else if (s.endsWith('/secret')) {\n        return 'secret';\n    } else if (s.endsWith('/team')) {\n        return 'team';\n    } else if (s.endsWith('/jsonstore')) {\n        return 'jsonstore';\n    } else if (s.endsWith('/settings')) {\n        return 'settings';\n    } else if (s.endsWith('/audit')) {\n        return 'audit';\n    }\n\n    return null;\n};\n\nconst OrganizationPage = (props: RouteComponentProps<RouteProps>) => {\n    const activeTab = pathToTab(props.location.pathname);\n\n    const { orgName } = props.match.params;\n\n    const loading = React.useContext(LoadingState);\n\n    const [refresh, toggleRefresh] = useState<boolean>(false);\n\n    const refreshHandler = useCallback(() => {\n        toggleRefresh((prevState) => !prevState);\n    }, []);\n\n    return (\n        <>\n            <BreadcrumbsToolbar loading={loading} refreshHandler={refreshHandler}>\n                <Breadcrumb.Section>\n                    <Link to=\"/org\">Organizations</Link>\n                </Breadcrumb.Section>\n                <Breadcrumb.Divider />\n                <Breadcrumb.Section active={true}>{orgName}</Breadcrumb.Section>\n            </BreadcrumbsToolbar>\n\n            <OrganizationActivity activeTab={activeTab} orgName={orgName} forceRefresh={refresh} />\n        </>\n    );\n};\n\nexport default withRouter(OrganizationPage);\n"
  },
  {
    "path": "console2/src/components/pages/ProcessCardFormPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport { RouteComponentProps, withRouter } from '@/router';\nimport { Button, Divider, Header, Image, Loader } from 'semantic-ui-react';\nimport {\n    getProcessCard as apiGetProcessCard,\n    ProcessCardEntry,\n} from '../../../api/service/console/user';\nimport { useApi } from '../../../hooks/useApi';\nimport { RequestErrorMessage } from '../../molecules';\nimport { useCallback, useContext } from 'react';\nimport { UserSessionContext } from '../../../session';\n\ninterface RouteProps {\n    cardId: string;\n}\n\nfunction ProcessCardFormPage(props: RouteComponentProps<RouteProps>) {\n    const { cardId } = props.match.params;\n\n    const { userInfo } = useContext(UserSessionContext);\n\n    const getProcessCard = useCallback(() => apiGetProcessCard(cardId), [cardId]);\n\n    const {\n        data: card,\n        error,\n        isLoading,\n    } = useApi<ProcessCardEntry>(getProcessCard, {\n        fetchOnMount: true,\n    });\n\n    if (error) {\n        return <RequestErrorMessage error={error} />;\n    }\n\n    if (!card || isLoading) {\n        return <Loader active={true} />;\n    }\n\n    return (\n        <>\n            <div style={{ margin: '24px' }}>\n                <div style={{ display: 'flex', flexDirection: 'row' }}>\n                    <div style={{ flexGrow: 1 }}>\n                        <Header as=\"h2\">\n                            {card.icon && (\n                                <Image size=\"mini\" src={`data:image/png;base64, ${card.icon}`} />\n                            )}\n                            {card.name}\n                        </Header>\n                        <Header as=\"h5\">{card.description}</Header>\n                    </div>\n                    <div>\n                        Logged in as:{' '}\n                        <b>\n                            {userInfo?.displayName\n                                ? `${userInfo.displayName} (${userInfo.username})`\n                                : userInfo?.username}\n                        </b>\n                    </div>\n                </div>\n                <Divider />\n                {card.isCustomForm && (\n                    <div className={'ui active embed'}>\n                        <iframe\n                            title={card.id}\n                            src={`/api/v1/processcard/${card.id}/form`}\n                            height={'100%'}\n                            width={'100%'}\n                            allowFullScreen={true}\n                        />\n                    </div>\n                )}\n\n                {!card.isCustomForm && (\n                    <Button\n                        basic\n                        color=\"green\"\n                        href={`/api/v1/org/${card.orgName}/project/${card.projectName}/repo/${card.repoName}/start/${card.entryPoint}`}\n                    >\n                        Start\n                    </Button>\n                )}\n            </div>\n        </>\n    );\n}\n\nexport default withRouter(ProcessCardFormPage);\n"
  },
  {
    "path": "console2/src/components/pages/ProcessFormPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { RouteComponentProps, withRouter } from '@/router';\nimport { Link } from 'react-router';\nimport { Breadcrumb } from 'semantic-ui-react';\n\nimport { ConcordId } from '../../../api/common';\nimport { BreadcrumbSegment } from '../../molecules';\nimport { ProcessFormActivity } from '../../organisms';\n\ninterface Props {\n    processInstanceId: ConcordId;\n    formName: string;\n    mode: string;\n}\n\nclass ProcessFormPage extends React.PureComponent<RouteComponentProps<Props>> {\n    render() {\n        const { processInstanceId, formName, mode } = this.props.match.params;\n\n        return (\n            <>\n                <BreadcrumbSegment>\n                    <Breadcrumb.Section>\n                        <Link to={`/process`}>Processes</Link>\n                    </Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section>\n                        <Link to={`/process/${processInstanceId}`}>{processInstanceId}</Link>\n                    </Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section active={true}>Form</Breadcrumb.Section>\n                </BreadcrumbSegment>\n\n                <ProcessFormActivity\n                    processInstanceId={processInstanceId}\n                    formName={formName}\n                    wizard={mode === 'wizard'}\n                />\n            </>\n        );\n    }\n}\n\nexport default withRouter(ProcessFormPage);\n"
  },
  {
    "path": "console2/src/components/pages/ProcessListPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport { ProcessListActivity } from '../../organisms';\nimport { BreadcrumbsToolbar } from '../../organisms';\nimport { Breadcrumb } from 'semantic-ui-react';\nimport { LoadingState } from '../../../App';\nimport { useCallback, useState } from 'react';\n\nexport default () => {\n    const loading = React.useContext(LoadingState);\n\n    const [refresh, toggleRefresh] = useState<boolean>(false);\n\n    const refreshHandler = useCallback(() => {\n        toggleRefresh((prevState) => !prevState);\n    }, []);\n\n    return (\n        <>\n            <BreadcrumbsToolbar loading={loading} refreshHandler={refreshHandler}>\n                <Breadcrumb.Section active={true}>Processes</Breadcrumb.Section>\n            </BreadcrumbsToolbar>\n\n            <ProcessListActivity\n                showInitiatorFilter={true}\n                usePagination={true}\n                forceRefresh={refresh}\n                columns={window.concord.processListColumns}\n            />\n        </>\n    );\n};\n"
  },
  {
    "path": "console2/src/components/pages/ProcessPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { RouteComponentProps, withRouter } from '@/router';\n\nimport { ConcordId } from '../../../api/common';\nimport { ProcessActivity } from '../../organisms';\nimport { TabLink } from '../../organisms/ProcessActivity';\n\ninterface Props {\n    instanceId: ConcordId;\n}\n\nconst pathToTab = (s: string): TabLink => {\n    if (s.endsWith('/status')) {\n        return 'status';\n    } else if (s.endsWith('/ansible')) {\n        return 'ansible';\n    } else if (s.endsWith('/log')) {\n        return 'log';\n    } else if (s.endsWith('/history')) {\n        return 'history';\n    } else if (s.endsWith('/children')) {\n        return 'children';\n    } else if (s.endsWith('/attachments')) {\n        return 'attachments';\n    } else if (s.endsWith('/events')) {\n        return 'events';\n    } else if (s.endsWith('/wait')) {\n        return 'wait';\n    }\n\n    return null;\n};\n\nclass ProcessPage extends React.PureComponent<RouteComponentProps<Props>> {\n    render() {\n        const { instanceId } = this.props.match.params;\n\n        const activeTab = pathToTab(this.props.location.pathname);\n\n        return (\n            <>\n                <ProcessActivity instanceId={instanceId} activeTab={activeTab} />\n            </>\n        );\n    }\n}\n\nexport default withRouter(ProcessPage);\n"
  },
  {
    "path": "console2/src/components/pages/ProcessWizardPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { RouteComponentProps, withRouter } from '@/router';\n\nimport { ConcordId } from '../../../api/common';\nimport { ProcessWizard } from '../../organisms';\n\nimport './styles.css';\n\ninterface Props {\n    instanceId: ConcordId;\n}\n\nclass ProcessWizardPage extends React.PureComponent<RouteComponentProps<Props>> {\n    render() {\n        const { instanceId } = this.props.match.params;\n        return (\n            <div className=\"flexbox-container\">\n                <ProcessWizard processInstanceId={instanceId} />\n            </div>\n        );\n    }\n}\n\nexport default withRouter(ProcessWizardPage);\n"
  },
  {
    "path": "console2/src/components/pages/ProcessWizardPage/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n.flexbox-container {\n  display: -ms-flexbox;\n  display: -webkit-flex;\n  display: flex;\n\n  -ms-flex-align: center;\n  -webkit-align-items: center;\n  -webkit-box-align: center;\n  align-items: center;\n\n  height: 100%;\n}\n"
  },
  {
    "path": "console2/src/components/pages/ProfilePage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Link, Navigate, Route, Routes, useLocation } from 'react-router';\nimport { Breadcrumb, Grid, Menu } from 'semantic-ui-react';\n\nimport { BreadcrumbSegment } from '../../molecules';\nimport { NotFoundPage } from '../index';\nimport { APITokensListPage, NewAPITokenPage, UserInfoPage } from '../../../components/pages';\n\ntype TabLink = 'user-info' | 'token' | null;\n\nconst pathToTab = (s: string): TabLink => {\n    if (s.includes('/api-token')) {\n        return 'token';\n    } else if (s.includes('/user-info')) {\n        return 'user-info';\n    }\n\n    return null;\n};\n\nconst ProfilePage = () => {\n    const location = useLocation();\n    const activeTab = pathToTab(location.pathname);\n\n    return (\n        <>\n            <BreadcrumbSegment>\n                <Breadcrumb.Section active={true}>Profile Options</Breadcrumb.Section>\n            </BreadcrumbSegment>\n\n            <Grid centered={true}>\n                <Grid.Column width={3}>\n                    <Menu tabular={true} vertical={true} fluid={true}>\n                        <Menu.Item active={activeTab === 'user-info'}>\n                            <Link to=\"/profile/user-info\">User</Link>\n                        </Menu.Item>\n                        <Menu.Item active={activeTab === 'token'}>\n                            <Link to=\"/profile/api-token\">API Tokens</Link>\n                        </Menu.Item>\n                    </Menu>\n                </Grid.Column>\n\n                <Grid.Column width={13}>\n                    <Routes>\n                        <Route index={true} element={<Navigate to=\"api-token\" replace={true} />} />\n                        <Route path=\"api-token/_new\" element={<NewAPITokenPage />} />\n                        <Route path=\"api-token\" element={<APITokensListPage />} />\n                        <Route path=\"user-info\" element={<UserInfoPage />} />\n                        <Route path=\"*\" element={<NotFoundPage />} />\n                    </Routes>\n                </Grid.Column>\n            </Grid>\n        </>\n    );\n};\n\nexport default ProfilePage;\n"
  },
  {
    "path": "console2/src/components/pages/ProjectPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { RouteComponentProps, withRouter } from '@/router';\nimport { Link } from 'react-router';\nimport { Breadcrumb } from 'semantic-ui-react';\n\nimport { ConcordId } from '../../../api/common';\nimport { ProjectActivity } from '../../organisms';\nimport { TabLink } from '../../organisms/ProjectActivity';\nimport { LoadingState } from '../../../App';\nimport { useCallback, useState } from 'react';\nimport { BreadcrumbsToolbar } from '../../organisms';\n\ninterface RouteProps {\n    orgName: ConcordId;\n    projectName: ConcordId;\n}\n\nconst pathToTab = (s: string): TabLink => {\n    if (s.endsWith('/process')) {\n        return 'process';\n    } else if (s.endsWith('/checkpoint')) {\n        return 'checkpoint';\n    } else if (s.endsWith('/repository')) {\n        return 'repository';\n    } else if (s.endsWith('/settings')) {\n        return 'settings';\n    } else if (s.endsWith('/access')) {\n        return 'access';\n    } else if (s.endsWith('/configuration')) {\n        return 'configuration';\n    } else if (s.endsWith('/audit')) {\n        return 'audit';\n    }\n\n    return null;\n};\n\nconst ProjectPage = (props: RouteComponentProps<RouteProps>) => {\n    const loading = React.useContext(LoadingState);\n\n    const { orgName, projectName } = props.match.params;\n\n    const activeTab = pathToTab(props.location.pathname);\n\n    const [refresh, toggleRefresh] = useState<boolean>(false);\n\n    const refreshHandler = useCallback(() => {\n        toggleRefresh((prevState) => !prevState);\n    }, []);\n\n    return (\n        <>\n            <BreadcrumbsToolbar loading={loading} refreshHandler={refreshHandler}>\n                <Breadcrumb.Section>\n                    <Link to={`/org/${orgName}`}>{orgName}</Link>\n                </Breadcrumb.Section>\n                <Breadcrumb.Divider />\n                <Breadcrumb.Section active={true}>{projectName}</Breadcrumb.Section>\n            </BreadcrumbsToolbar>\n\n            <ProjectActivity\n                activeTab={activeTab}\n                orgName={orgName}\n                projectName={projectName}\n                forceRefresh={refresh}\n            />\n        </>\n    );\n};\n\nexport default withRouter(ProjectPage);\n"
  },
  {
    "path": "console2/src/components/pages/RepositoryPage/RepositoryEventsActivity.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport { Button, Form, Menu, Popup, Table } from 'semantic-ui-react';\n\nimport {\n    AuditAction,\n    AuditLogEntry,\n    AuditObject,\n    list as apiList,\n    PaginatedAuditLogEntries\n} from '../../../api/audit/index';\nimport { get as getRepo } from '../../../api/org/project/repository/index';\nimport { LocalTimestamp, PaginationToolBar } from '../../molecules';\nimport { usePagination } from '../../molecules/PaginationToolBar/usePagination';\nimport { LoadingDispatch } from '../../../App';\nimport RequestErrorActivity from '../../organisms/RequestErrorActivity';\nimport { useApi } from '../../../hooks/useApi';\nimport { ConcordKey } from '../../../api/common';\nimport { ReactJson } from '../../atoms';\nimport { DateTimeInput } from 'semantic-ui-calendar-react';\nimport { addHours, format as formatDate } from 'date-fns';\nimport {\n    formatForApi,\n    SRC_DATE_TIME_FORMAT,\n    UI_DATE_TIME_FORMAT\n} from '../../organisms/AuditLogActivity';\nimport { useRef } from 'react';\n\nexport interface ExternalProps {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    repoName: ConcordKey;\n    forceRefresh: any;\n}\n\ninterface EventDetails {\n    eventId: string;\n    githubEvent: string;\n    payload: {};\n}\n\ninterface Filter {\n    eventId?: string;\n    githubEvent?: string;\n    after: string;\n    before: string;\n}\n\nconst eventTypeOptions = [\n    { value: 'commit_comment', text: 'commit_comment' },\n    { value: 'create', text: 'create' },\n    { value: 'delete', text: 'delete' },\n    { value: 'fork', text: 'fork' },\n    { value: 'issue_comment', text: 'issue_comment' },\n    { value: 'issues', text: 'issues' },\n    { value: 'label', text: 'label' },\n    { value: 'member', text: 'member' },\n    { value: 'pull_request', text: 'pull_request' },\n    { value: 'pull_request_review', text: 'pull_request_review' },\n    { value: 'push', text: 'push' },\n    { value: 'release', text: 'release' },\n    { value: 'team_add', text: 'team_add' },\n    { value: 'repository', text: 'repository' }\n];\n\nexport default ({ orgName, projectName, repoName, forceRefresh }: ExternalProps) => {\n    const dispatch = React.useContext(LoadingDispatch);\n\n    const defaultAfter = formatDate(addHours(new Date(), -8), SRC_DATE_TIME_FORMAT);\n    const defaultBefore = formatDate(addHours(new Date(), 1), SRC_DATE_TIME_FORMAT);\n\n    const {\n        paginationFilter,\n        handleLimitChange,\n        handleNext,\n        handlePrev,\n        handleFirst,\n        resetOffset\n    } = usePagination();\n\n    const repoFullNameRef = useRef<string | undefined>('');\n    const [isCalendarOpen, setCalendarOpen] = useState(false);\n    const [eventTypeFilter, setEventTypeFilter] = useState<string>();\n    const [eventIdFilter, setEventIdFilter] = useState<string>();\n    const [eventIdFilterError, setEventIdFilterError] = useState<boolean>();\n    const [before, setBefore] = useState<string>(defaultBefore);\n    const [after, setAfter] = useState<string>(defaultAfter);\n    const [filter, setFilter] = useState<Filter>({ after: defaultAfter, before: defaultBefore });\n    const [searchEnabled, setSearchEnabled] = useState<boolean>(false);\n\n    const fetchData = useCallback(async () => {\n        if (repoFullNameRef.current === '') {\n            const repo = await getRepo(orgName, projectName, repoName);\n            repoFullNameRef.current = getFullRepoName(repo.url);\n        } else if (repoFullNameRef.current === undefined) {\n            return { next: false, items: [] };\n        }\n\n        return apiList({\n            object: AuditObject.EXTERNAL_EVENT,\n            action: AuditAction.ACCESS,\n            details: {\n                source: 'github',\n                eventId: filter.eventId,\n                githubEvent: filter.githubEvent,\n                fullRepoName: repoFullNameRef.current\n            },\n            offset: paginationFilter.offset,\n            limit: paginationFilter.limit,\n            before: formatForApi(filter.before),\n            after: formatForApi(filter.after)\n        });\n    }, [orgName, projectName, repoName, paginationFilter, filter]);\n\n    const { data, error } = useApi<PaginatedAuditLogEntries>(fetchData, {\n        fetchOnMount: true,\n        forceRequest: forceRefresh,\n        dispatch: dispatch\n    });\n\n    useEffect(() => {\n        let valid = true;\n\n        if (notEmpty(eventIdFilter)) {\n            valid = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(\n                eventIdFilter!\n            );\n            setEventIdFilterError(!valid);\n        } else {\n            setEventIdFilterError(false);\n        }\n\n        setSearchEnabled(valid);\n    }, [eventIdFilter]);\n\n    const disabled = data === undefined;\n\n    return (\n        <>\n            <Menu secondary={true} style={{ marginTop: 0 }}>\n                <Menu.Item style={{ padding: 0 }}>\n                    <Form>\n                        <Form.Group inline={true} style={{ margin: 0 }}>\n                            <Form.Input\n                                placeholder=\"Event ID\"\n                                disabled={disabled}\n                                maxLength={36}\n                                style={{ width: 305 }}\n                                error={eventIdFilterError}\n                                onChange={(ev, data) => setEventIdFilter(data.value)}\n                            />\n\n                            <Form.Select\n                                placeholder=\"Event Type\"\n                                disabled={disabled}\n                                clearable={true}\n                                search={true}\n                                options={eventTypeOptions}\n                                onChange={(ev, data) => setEventTypeFilter(data.value as string)}\n                            />\n\n                            <Form.Field disabled={disabled}>\n                                <Popup\n                                    trigger={\n                                        <Button\n                                            icon=\"calendar alternate outline\"\n                                            size=\"large\"\n                                            basic={true}\n                                        />\n                                    }\n                                    open={isCalendarOpen}\n                                    onClose={() => setCalendarOpen(false)}\n                                    onOpen={() => setCalendarOpen(true)}\n                                    openOnTriggerMouseEnter={false}\n                                    closeOnTriggerMouseLeave={false}\n                                    closeOnDocumentClick={false}>\n                                    <Form>\n                                        <DateTimeField\n                                            disabled={disabled}\n                                            label=\"From\"\n                                            value={after}\n                                            onChange={(data) => setAfter(data)}\n                                        />\n\n                                        <DateTimeField\n                                            disabled={disabled}\n                                            label=\"To\"\n                                            value={before}\n                                            onChange={(data) => setBefore(data)}\n                                        />\n                                    </Form>\n                                </Popup>\n                            </Form.Field>\n\n                            <Form.Button\n                                content=\"Search\"\n                                icon=\"search\"\n                                primary={true}\n                                onClick={() => {\n                                    setFilter({\n                                        after: after,\n                                        before: before,\n                                        githubEvent: eventTypeFilter,\n                                        eventId: eventIdFilter\n                                    });\n                                    resetOffset(0);\n                                    setCalendarOpen(false);\n                                }}\n                                disabled={disabled || !searchEnabled}\n                            />\n                        </Form.Group>\n                    </Form>\n                </Menu.Item>\n\n                <Menu.Item style={{ padding: 0 }} position=\"right\">\n                    <PaginationToolBar\n                        limit={paginationFilter.limit}\n                        handleLimitChange={(limit) => handleLimitChange(limit)}\n                        handleNext={handleNext}\n                        handlePrev={handlePrev}\n                        handleFirst={handleFirst}\n                        disablePrevious={paginationFilter.offset === 0}\n                        disableNext={!data?.next}\n                        disableFirst={paginationFilter.offset === 0}\n                        disabled={disabled}\n                    />\n                </Menu.Item>\n            </Menu>\n\n            {error && <RequestErrorActivity error={error} />}\n\n            <div style={{ overflowX: 'auto' }}>\n                <Table\n                    celled={true}\n                    selectable={!disabled && data !== undefined && data.items.length > 0}\n                    style={disabled ? { opacity: 0.4 } : {}}>\n                    <Table.Header>\n                        <Table.Row>\n                            <Table.HeaderCell collapsing={true} width={2}>\n                                ID\n                            </Table.HeaderCell>\n                            <Table.HeaderCell collapsing={true} width={1}>\n                                Date\n                            </Table.HeaderCell>\n                            <Table.HeaderCell collapsing={true} width={2}>\n                                Type\n                            </Table.HeaderCell>\n                            <Table.HeaderCell width={11}>Payload</Table.HeaderCell>\n                        </Table.Row>\n                    </Table.Header>\n\n                    <Table.Body>{renderItems(data)}</Table.Body>\n                </Table>\n            </div>\n        </>\n    );\n};\n\nconst renderItems = (data?: PaginatedAuditLogEntries) => {\n    if (!data) {\n        return (\n            <Table.Row>\n                <Table.Cell colSpan={4}>&nbsp;</Table.Cell>\n            </Table.Row>\n        );\n    }\n\n    if (data.items.length === 0) {\n        return (\n            <Table.Row style={{ fontWeight: 'bold' }}>\n                <Table.Cell colSpan={4}>No data available</Table.Cell>\n            </Table.Row>\n        );\n    }\n\n    return data.items.map((h, idx) => renderTableRow(idx, h));\n};\n\nconst renderTableRow = (idx: number, h: AuditLogEntry) => {\n    const d = h.details as EventDetails;\n    return (\n        <Table.Row key={idx} verticalAlign=\"top\">\n            <Table.Cell collapsing={true}>{d.eventId}</Table.Cell>\n            <Table.Cell collapsing={true}>\n                <LocalTimestamp value={h.entryDate} />\n            </Table.Cell>\n            <Table.Cell collapsing={true}>{d.githubEvent}</Table.Cell>\n            <Table.Cell collapsing={true}>\n                <ReactJson\n                    src={d.payload}\n                    name={null}\n                    collapsed={true}\n                    displayObjectSize={false}\n                    displayDataTypes={false}\n                    enableClipboard={false}\n                />\n            </Table.Cell>\n        </Table.Row>\n    );\n};\n\nconst notEmpty = (x?: string) => {\n    return x !== undefined && x !== '';\n};\n\nconst DateTimeField = (props: {\n    disabled: boolean;\n    value: string;\n    label: string;\n    onChange: (data: string) => void;\n}) => (\n    <Form.Field>\n        <DateTimeInput\n            value={props.value}\n            label={props.label}\n            dateTimeFormat={UI_DATE_TIME_FORMAT}\n            closable={true}\n            animation={'none' as any}\n            duration={0}\n            disabled={props.disabled}\n            onChange={(ev: {}, data: any) => {\n                props.onChange(data.value as string);\n            }}\n        />\n    </Form.Field>\n);\n\nconst getFullRepoName = (repoUrl: string) => {\n    let repoPath = removeSchema(repoUrl);\n    repoPath = removeHost(repoPath);\n\n    const u = repoPath.split('/');\n    if (u.length < 2) {\n        return undefined;\n    }\n\n    return owner(u[0]) + '/' + name(u[1]);\n};\n\nconst removeSchema = (repoUrl: string) => {\n    let index = repoUrl.indexOf('://');\n    if (index > 0) {\n        return repoUrl.substring(index + '://'.length);\n    }\n    index = repoUrl.indexOf('@');\n    if (index > 0) {\n        return repoUrl.substring(index + '@'.length);\n    }\n    return repoUrl;\n};\n\nconst removeHost = (repoUrl: string) => {\n    let index = repoUrl.indexOf(':');\n    if (index > 0) {\n        const portEndIndex = repoUrl.indexOf('/', index);\n        if (portEndIndex > 0) {\n            const port = repoUrl.substring(index + 1, portEndIndex);\n            if (isPort(port)) {\n                return repoUrl.substring(portEndIndex + '/'.length);\n            }\n        }\n        return repoUrl.substring(index + ':'.length);\n    }\n    index = repoUrl.indexOf('/');\n    if (index > 0) {\n        return repoUrl.substring(index + '/'.length);\n    }\n    return repoUrl;\n};\n\nconst isPort = (str: string) => {\n    const port = Number(str);\n    if (isNaN(port)) {\n        return false;\n    }\n    return port > 0 && port <= 65535;\n};\n\nconst name = (str: string) => {\n    return str.replace(/^\\W+|\\.git$/, '');\n};\n\nconst owner = (str: string) => {\n    const idx = str.indexOf(':');\n    if (idx > 0) {\n        return str.substring(idx + 1);\n    }\n    return str;\n};\n"
  },
  {
    "path": "console2/src/components/pages/RepositoryPage/RepositoryTriggersActivity.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { useCallback, useContext } from 'react';\nimport { Loader, Table } from 'semantic-ui-react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { TriggerEntry, listTriggers as apiListTriggers } from '../../../api/org/project/repository';\nimport { useApi } from '../../../hooks/useApi';\nimport { LoadingDispatch } from '../../../App';\nimport { comparators } from '../../../utils';\nimport { ReactJson } from '../../atoms';\nimport { LocalTimestamp, RequestErrorMessage } from '../../molecules';\n\nimport * as cronjsMatcher from '@datasert/cronjs-matcher';\n\ninterface ExternalProps {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    repoName: ConcordKey;\n}\n\nconst prepareData = (data: TriggerEntry[] | undefined) => {\n    if (!data) {\n        return undefined;\n    }\n\n    return data\n        .sort(comparators.byProperty((i) => i.cfg.entryPoint))\n        .sort(comparators.byProperty((i) => i.eventSource));\n};\n\nconst RepositoryTriggersActivity = ({ orgName, projectName, repoName }: ExternalProps) => {\n    const dispatch = useContext(LoadingDispatch);\n\n    const fetchData = useCallback(() => {\n        return apiListTriggers(orgName, projectName, repoName);\n    }, [orgName, projectName, repoName]);\n\n    const { data, error, isLoading } = useApi<TriggerEntry[]>(fetchData, {\n        fetchOnMount: true,\n        dispatch\n    });\n\n    if (error) {\n        return <RequestErrorMessage error={error} />;\n    }\n\n    if (isLoading || !data) {\n        return <Loader active={true} />;\n    }\n\n    const triggers = prepareData(data);\n    const cronTriggers = triggers?.filter((t) => t.eventSource === 'cron');\n    const otherTriggers = triggers?.filter((t) => t.eventSource !== 'cron');\n\n    return (\n        <>\n            {cronTriggers && <>\n                <h3>Cron Triggers</h3>\n                <Table celled={true} striped={true}>\n                    <Table.Header>\n                        <Table.Row>\n                            <Table.HeaderCell width={3}>Conditions</Table.HeaderCell>\n                            <Table.HeaderCell collapsing={true}>Entry Point</Table.HeaderCell>\n                            <Table.HeaderCell width={5}>Configuration</Table.HeaderCell>\n                            <Table.HeaderCell width={5}>Arguments</Table.HeaderCell>\n                        </Table.Row>\n                    </Table.Header>\n\n                    <Table.Body>\n                        {cronTriggers.map((t, idx) => (\n                            <Table.Row key={idx}>\n                                <Table.Cell>\n                                    {t.conditions?.spec !== undefined && (\n                                        <pre>\n                                            Expression: <b>{t.conditions?.spec}</b>\n                                            <br/>\n                                            Next run: <LocalTimestamp value={cronjsMatcher.getFutureMatches(t.conditions?.spec, {matchCount: 1})[0]}/>\n                                        </pre>)}\n                                </Table.Cell>\n                                <Table.Cell>\n                                    <pre>{t.cfg.entryPoint}</pre>\n                                </Table.Cell>\n                                <Table.Cell>\n                                    <ReactJson\n                                        src={t.cfg}\n                                        collapsed={true}\n                                        name={null}\n                                        enableClipboard={false}\n                                    />\n                                </Table.Cell>\n                                <Table.Cell>\n                                    {t.arguments && (\n                                        <ReactJson\n                                            src={t.arguments}\n                                            collapsed={true}\n                                            name={null}\n                                            enableClipboard={false}\n                                        />\n                                    )}\n                                </Table.Cell>\n                            </Table.Row>\n                        ))}\n                        {cronTriggers.length === 0 && (\n                            <Table.Row>\n                                <Table.Cell colSpan={5}>No cron triggers</Table.Cell>\n                            </Table.Row>\n                        )}\n                    </Table.Body>\n                </Table>\n            </>}\n            {otherTriggers && <>\n                <h3>Other Triggers</h3>\n                <Table celled={true} striped={true}>\n                    <Table.Header>\n                        <Table.Row>\n                            <Table.HeaderCell collapsing={true}>Source</Table.HeaderCell>\n                            <Table.HeaderCell>Conditions</Table.HeaderCell>\n                            <Table.HeaderCell collapsing={true}>Entry Point</Table.HeaderCell>\n                            <Table.HeaderCell>Configuration</Table.HeaderCell>\n                            <Table.HeaderCell>Arguments</Table.HeaderCell>\n                        </Table.Row>\n                    </Table.Header>\n\n                    <Table.Body>\n                        {otherTriggers.map((t, idx) => (\n                            <Table.Row key={idx}>\n                                <Table.Cell>{t.eventSource}</Table.Cell>\n                                <Table.Cell>\n                                    {t.conditions && (\n                                        <ReactJson\n                                            src={t.conditions}\n                                            collapsed={true}\n                                            name={null}\n                                            enableClipboard={false}\n                                        />\n                                    )}\n                                </Table.Cell>\n                                <Table.Cell>{t.cfg.entryPoint}</Table.Cell>\n                                <Table.Cell>\n                                    <ReactJson\n                                        src={t.cfg}\n                                        collapsed={true}\n                                        name={null}\n                                        enableClipboard={false}\n                                    />\n                                </Table.Cell>\n                                <Table.Cell>\n                                    {t.arguments && (\n                                        <ReactJson\n                                            src={t.arguments}\n                                            collapsed={true}\n                                            name={null}\n                                            enableClipboard={false}\n                                        />\n                                    )}\n                                </Table.Cell>\n                            </Table.Row>\n                        ))}\n                        {otherTriggers.length === 0 && (\n                            <Table.Row>\n                                <Table.Cell colSpan={5}>No other triggers</Table.Cell>\n                            </Table.Row>\n                        )}\n                    </Table.Body>\n                </Table>\n            </>}\n        </>\n    );\n};\n\nexport default RepositoryTriggersActivity;\n"
  },
  {
    "path": "console2/src/components/pages/RepositoryPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { useCallback, useRef, useState } from 'react';\nimport { Navigate, Route, Routes } from 'react-router';\nimport { Link } from 'react-router';\nimport { Breadcrumb, Icon, Menu } from 'semantic-ui-react';\n\nimport { RouteComponentProps, withRouter } from '@/router';\nimport { MainToolbar } from '../../molecules';\nimport { ConcordKey } from '../../../api/common';\nimport { LoadingState } from '../../../App';\nimport { NotFoundPage } from '../index';\nimport RepositoryEventsActivity from './RepositoryEventsActivity';\nimport { EditRepositoryActivity } from '../../organisms';\nimport RepositoryTriggersActivity from './RepositoryTriggersActivity';\n\ninterface RouteProps {\n    orgName: ConcordKey;\n    projectName: ConcordKey;\n    repoName: ConcordKey;\n}\n\ntype TabLink = 'settings' | 'events' | 'triggers' | null;\n\nconst pathToTab = (s: string): TabLink => {\n    if (s.endsWith('/settings')) {\n        return 'settings';\n    } else if (s.endsWith('/events')) {\n        return 'events';\n    } else if (s.endsWith('/triggers')) {\n        return 'triggers';\n    }\n\n    return null;\n};\n\nconst RepositoryPage = (props: RouteComponentProps<RouteProps>) => {\n    const { orgName, projectName, repoName } = props.match.params;\n    const activeTab = pathToTab(props.location.pathname);\n\n    const stickyRef = useRef(null);\n    const loading = React.useContext(LoadingState);\n    const [refresh, toggleRefresh] = useState<boolean>(false);\n\n    const refreshHandler = useCallback(() => {\n        toggleRefresh((prevState) => !prevState);\n    }, []);\n\n    const baseUrl = `/org/${orgName}/project/${projectName}/repository/${repoName}`;\n\n    return (\n        <div ref={stickyRef}>\n            <MainToolbar\n                loading={loading}\n                refresh={refreshHandler}\n                stickyRef={stickyRef}\n                breadcrumbs={renderBreadcrumbs(orgName, projectName, repoName)}\n            />\n\n            <Menu tabular={true} style={{ marginTop: 0 }}>\n                <Menu.Item active={activeTab === 'settings'}>\n                    <Icon name=\"setting\" />\n                    <Link to={`${baseUrl}/settings`}>Settings</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'triggers'}>\n                    <Icon name=\"lightning\" />\n                    <Link to={`${baseUrl}/triggers`}>Triggers</Link>\n                </Menu.Item>\n                <Menu.Item active={activeTab === 'events'}>\n                    <Icon name=\"search\" />\n                    <Link to={`${baseUrl}/events`}>Events</Link>\n                </Menu.Item>\n            </Menu>\n\n            <Routes>\n                <Route index={true} element={<Navigate to=\"settings\" replace={true} />} />\n                <Route\n                    path=\"settings\"\n                    element={\n                        <EditRepositoryActivity\n                            orgName={orgName}\n                            projectName={projectName}\n                            repoName={repoName}\n                            forceRefresh={refresh}\n                        />\n                    }\n                />\n                <Route\n                    path=\"triggers\"\n                    element={\n                        <RepositoryTriggersActivity\n                            orgName={orgName}\n                            projectName={projectName}\n                            repoName={repoName}\n                        />\n                    }\n                />\n                <Route\n                    path=\"events\"\n                    element={\n                        <RepositoryEventsActivity\n                            orgName={orgName}\n                            projectName={projectName}\n                            repoName={repoName}\n                            forceRefresh={refresh}\n                        />\n                    }\n                />\n                <Route path=\"*\" element={<NotFoundPage />} />\n            </Routes>\n        </div>\n    );\n};\n\nconst renderBreadcrumbs = (orgName: ConcordKey, projectName: ConcordKey, repoName: ConcordKey) => {\n    return (\n        <Breadcrumb size=\"big\">\n            <Breadcrumb.Section>\n                <Link to={`/org/${orgName}/project/${projectName}/repository`}>{projectName}</Link>\n            </Breadcrumb.Section>\n            <Breadcrumb.Divider />\n            <Breadcrumb.Section active={true}>{repoName}</Breadcrumb.Section>\n        </Breadcrumb>\n    );\n};\n\nexport default withRouter(RepositoryPage);\n"
  },
  {
    "path": "console2/src/components/pages/SecretPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { RouteComponentProps, withRouter } from '@/router';\nimport { Link } from 'react-router';\nimport { Breadcrumb } from 'semantic-ui-react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { LoadingState } from '../../../App';\nimport { BreadcrumbsToolbar } from '../../organisms';\nimport { SecretActivity } from '../../organisms';\nimport { TabLink } from '../../organisms/SecretActivity';\n\ninterface RouteProps {\n    orgName: ConcordKey;\n    secretName: ConcordKey;\n}\n\nconst pathToTab = (s: string): TabLink => {\n    if (s.endsWith('/info')) {\n        return 'info';\n    } else if (s.endsWith('/settings')) {\n        return 'settings';\n    } else if (s.endsWith('/access')) {\n        return 'access';\n    } else if (s.endsWith('/audit')) {\n        return 'audit';\n    }\n\n    return null;\n};\n\nconst SecretPage = (props: RouteComponentProps<RouteProps>) => {\n    const loading = React.useContext(LoadingState);\n    const { orgName, secretName } = props.match.params;\n    const activeTab = pathToTab(props.location.pathname);\n    const [refreshKey, setRefreshKey] = React.useState(0);\n\n    const refreshHandler = React.useCallback(() => {\n        setRefreshKey((prevState) => prevState + 1);\n    }, []);\n\n    return (\n        <>\n            <BreadcrumbsToolbar loading={loading} refreshHandler={refreshHandler}>\n                <Breadcrumb.Section>\n                    <Link to={`/org/${orgName}/secret`}>{orgName}</Link>\n                </Breadcrumb.Section>\n                <Breadcrumb.Divider />\n                <Breadcrumb.Section active={true}>{secretName}</Breadcrumb.Section>\n            </BreadcrumbsToolbar>\n\n            <SecretActivity\n                key={refreshKey}\n                orgName={orgName}\n                secretName={secretName}\n                activeTab={activeTab}\n            />\n        </>\n    );\n};\n\nexport default withRouter(SecretPage);\n"
  },
  {
    "path": "console2/src/components/pages/TeamPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { RouteComponentProps, withRouter } from '@/router';\nimport { Link } from 'react-router';\nimport { Breadcrumb } from 'semantic-ui-react';\nimport { ConcordId } from '../../../api/common';\n\nimport { BreadcrumbSegment } from '../../molecules';\nimport { TeamActivity } from '../../organisms';\nimport { TabLink } from '../../organisms/TeamActivity';\n\ninterface RouteProps {\n    orgName: ConcordId;\n    teamName: ConcordId;\n}\n\nconst pathToTab = (s: string): TabLink => {\n    if (s.endsWith('/members')) {\n        return 'members';\n    } else if (s.endsWith('/ldapGroups')) {\n        return 'ldapGroups';\n    } else if (s.endsWith('/settings')) {\n        return 'settings';\n    }\n\n    return null;\n};\n\nclass TeamPage extends React.PureComponent<RouteComponentProps<RouteProps>> {\n    render() {\n        const { orgName, teamName } = this.props.match.params;\n\n        const activeTab = pathToTab(this.props.location.pathname);\n\n        return (\n            <>\n                <BreadcrumbSegment>\n                    <Breadcrumb.Section>\n                        <Link to={`/org/${orgName}/team`}>{orgName}</Link>\n                    </Breadcrumb.Section>\n                    <Breadcrumb.Divider />\n                    <Breadcrumb.Section active={true}>{teamName}</Breadcrumb.Section>\n                </BreadcrumbSegment>\n\n                <TeamActivity orgName={orgName} teamName={teamName} activeTab={activeTab} />\n            </>\n        );\n    }\n}\n\nexport default withRouter(TeamPage);\n"
  },
  {
    "path": "console2/src/components/pages/UnauthorizedPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport { RedirectButton } from '../../organisms';\n\nimport './styles.css';\nimport { Card, CardContent, CardDescription, CardHeader, Divider, Image } from 'semantic-ui-react';\nimport { withRouter } from '@/router';\n\nexport default withRouter((props) => {\n    const error = new URLSearchParams(props.location.search).get('error');\n    return (\n        <div className=\"flexbox-container\">\n            <Card centered={true}>\n                <CardContent textAlign={'center'}>\n                    <Image id=\"concord-logo\" src=\"/images/concord.svg\" size=\"medium\" />\n\n                    <CardHeader>You are not authorized.</CardHeader>\n\n                    {error && <CardDescription>Error: {error}</CardDescription>}\n\n                    <Divider />\n\n                    <RedirectButton primary={true} fluid={true} location={'/'}>\n                        Login\n                    </RedirectButton>\n                </CardContent>\n            </Card>\n        </div>\n    );\n});\n"
  },
  {
    "path": "console2/src/components/pages/UnauthorizedPage/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n.flexbox-container {\n  display: -ms-flexbox;\n  display: -webkit-flex;\n  display: flex;\n\n  -ms-flex-align: center;\n  -webkit-align-items: center;\n  -webkit-box-align: center;\n  align-items: center;\n\n  height: 100%;\n}\n\n#concord-logo {\n  padding: 1em;\n  margin-bottom: 1em;\n}\n"
  },
  {
    "path": "console2/src/components/pages/UserActivityPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\n\nimport { UserProcessActivity } from '../../organisms';\nimport { BreadcrumbsToolbar } from '../../organisms';\nimport { Breadcrumb } from 'semantic-ui-react';\nimport { useCallback, useState } from 'react';\nimport { LoadingState } from '../../../App';\n\nconst UserActivityPage = () => {\n    const loading = React.useContext(LoadingState);\n\n    const [refresh, toggleRefresh] = useState<boolean>(false);\n\n    const refreshHandler = useCallback(() => {\n        toggleRefresh((prevState) => !prevState);\n    }, []);\n\n    return (\n        <>\n            <BreadcrumbsToolbar loading={loading} refreshHandler={refreshHandler}>\n                <Breadcrumb.Section active={true}>Activity</Breadcrumb.Section>\n            </BreadcrumbsToolbar>\n\n            <UserProcessActivity forceRefresh={refresh} />\n        </>\n    );\n};\n\nexport default UserActivityPage;\n"
  },
  {
    "path": "console2/src/components/pages/UserInfoPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Wal-Mart Store, Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\nimport {\n    Menu\n} from 'semantic-ui-react';\n\nimport {UserInfo} from '../../organisms/index';\n\nclass UserInfoPage extends React.Component {\n    render() {\n        return (\n            <>\n                <Menu secondary={true} pointing={true} style={{height: '48px'}}>\n                    <Menu.Item position={'left'}>\n                        <h4>User Info</h4>\n                    </Menu.Item>\n                </Menu>\n\n                <UserInfo/>\n            </>\n        );\n    }\n}\n\nexport default UserInfoPage;\n"
  },
  {
    "path": "console2/src/components/pages/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nexport { default as AboutPage } from './AboutPage';\nexport { default as AddRepositoryPage } from './AddRepositoryPage';\nexport { default as APITokensListPage } from './APITokensListPage';\nexport { default as CustomResourcePage } from './CustomResourcePage';\nexport { default as LoginPage } from './LoginPage';\nexport { default as LogoutPage } from './LogoutPage';\nexport { default as NewAPITokenPage } from './NewAPITokenPage';\nexport { default as NewProjectPage } from './NewProjectPage';\nexport { default as NewSecretPage } from './NewSecretPage';\nexport { default as NewTeamPage } from './NewTeamPage';\nexport { default as NotFoundPage } from './NotFoundPage';\nexport { default as OrganizationListPage } from './OrganizationListPage';\nexport { default as OrganizationPage } from './OrganizationPage';\nexport { default as ProcessCardFormPage } from './ProcessCardFormPage';\nexport { default as ProcessFormPage } from './ProcessFormPage';\nexport { default as ProcessListPage } from './ProcessListPage';\nexport { default as ProcessPage } from './ProcessPage';\nexport { default as ProcessWizardPage } from './ProcessWizardPage';\nexport { default as ProfilePage } from './ProfilePage';\nexport { default as ProjectPage } from './ProjectPage';\nexport { default as RepositoryPage } from './RepositoryPage';\nexport { default as SecretPage } from './SecretPage';\nexport { default as JsonStorePage } from './JsonStorePage';\nexport { default as TeamPage } from './TeamPage';\nexport { default as UserActivityPage } from './UserActivityPage';\nexport { default as UserInfoPage } from './UserInfoPage';\nexport { default as UnauthorizedPage } from './UnauthorizedPage';\n"
  },
  {
    "path": "console2/src/components/templates/Layout/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { Outlet, useLocation } from 'react-router';\nimport { Grid } from 'semantic-ui-react';\n\nimport { TopBar } from '../../organisms';\n\nimport './styles.css';\n\ninterface Props {\n    children?: React.ReactNode;\n}\n\nconst Layout = ({ children }: Props) => {\n    const location = useLocation();\n    // TODO is there a better way?\n    const fullScreen = location.search.search('fullScreen=true') >= 0;\n\n    return (\n        <Grid centered={true}>\n            {!fullScreen && (\n                <Grid.Column width={16} className=\"topBar\">\n                    <Grid centered={true}>\n                        <Grid.Column width={14} className=\"topBarColumn\">\n                            <TopBar />\n                        </Grid.Column>\n                    </Grid>\n                </Grid.Column>\n            )}\n            <Grid.Column width={14} className=\"contentColumn\">\n                {children || <Outlet />}\n            </Grid.Column>\n        </Grid>\n    );\n};\n\nexport default Layout;\n"
  },
  {
    "path": "console2/src/components/templates/Layout/styles.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n.topBar {\n    background-color: #1B1C1D;\n}\n\n.topBarColumn {\n    padding: 0px !important;\n    padding-top: 15px !important;;\n    padding-bottom: 5px !important;;\n}\n\n.contentColumn {\n    padding-top: 3px !important;\n}\n"
  },
  {
    "path": "console2/src/components/templates/OrgActivityPage/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { RouteComponentProps, withRouter } from '@/router';\nimport { Container, Header, Segment } from 'semantic-ui-react';\n\nimport { ConcordKey } from '../../../api/common';\nimport { BreadcrumbSegment } from '../../molecules';\n\ninterface ExternalProps {\n    title: string;\n    breadcrumbs: (orgName: ConcordKey) => React.ReactNode;\n    activity: (orgName: ConcordKey) => React.ReactNode;\n}\n\ninterface RouteProps {\n    orgName: ConcordKey;\n}\n\ntype Props = ExternalProps & RouteComponentProps<RouteProps>;\n\n// TODO: Make a generic layout rather than page specific?\nclass OrgActivityPage extends React.PureComponent<Props> {\n    render() {\n        const { title, breadcrumbs, activity } = this.props;\n        const { orgName } = this.props.match.params;\n\n        return (\n            <>\n                <BreadcrumbSegment>{breadcrumbs(orgName)}</BreadcrumbSegment>\n\n                <Segment basic={true}>\n                    <Container text={true}>\n                        <Header>{title}</Header>\n                        {activity(orgName)}\n                    </Container>\n                </Segment>\n            </>\n        );\n    }\n}\n\nexport default withRouter(OrgActivityPage);\n"
  },
  {
    "path": "console2/src/components/templates/index.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nexport { default as Layout } from './Layout';\nexport { default as OrgActivityPage } from './OrgActivityPage';\n"
  },
  {
    "path": "console2/src/hooks/useApi.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { RequestError } from '../api/common';\nimport { LoadingAction } from '../reducers/loading';\n\nexport interface Props<S> {\n    fetchOnMount?: boolean;\n    initialData?: S;\n    forceRequest?: any;\n    debounceTime?: number | null;\n    dispatch?: (value: LoadingAction) => void;\n    requestByFetch?: boolean;\n}\n\nexport function useApi<S>(dataFetcher: () => Promise<S>, props: Props<S>) {\n    const { fetchOnMount, initialData, forceRequest, dispatch, requestByFetch, debounceTime } = props;\n\n    const didMountRef = useRef(false);\n    const didMountRef2 = useRef(false);\n    const fetchNowRef = useRef(false);\n    const debounceRef = useRef(null);\n    const [fetchNow, toggleFetchNow] = useState<boolean>(false);\n    const [data, setData] = useState(initialData);\n    const [isLoading, setIsLoading] = useState(false);\n    const [error, setError] = useState<RequestError>();\n\n    const clearState = useCallback((initialData?: S) => {\n        setData(initialData);\n        setIsLoading(false);\n        setError(undefined);\n    }, []);\n\n    const fetch = useCallback(() => {\n        toggleFetchNow((prevState) => !prevState);\n    }, []);\n\n    useEffect(() => {\n        if (!didMountRef2.current) {\n            didMountRef2.current = true;\n            return;\n        }\n\n        fetchNowRef.current = true;\n    }, [fetchNow]);\n\n    useEffect(() => {\n        let cancelled = false;\n        let loading = false;\n\n        const fetchData = async () => {\n            setIsLoading(true);\n            setError(undefined);\n\n            if (dispatch) {\n                loading = true;\n                dispatch(LoadingAction.START);\n            }\n            try {\n                const result = await dataFetcher();\n                if (!cancelled) {\n                    setData(result);\n                }\n            } catch (e) {\n                if (!cancelled) {\n                    setError(e);\n                }\n            } finally {\n                loading = false;\n                if (!cancelled) {\n                    setIsLoading(false);\n                    if (dispatch) {\n                        dispatch(LoadingAction.STOP);\n                    }\n                }\n            }\n        };\n\n        const startBouncing = () => {\n            if (typeof debounceTime === \"number\" && debounceTime > 0) {\n                if (debounceRef.current !== null) {\n                    clearTimeout(debounceRef.current);\n                }\n                debounceRef.current = setTimeout(async ()=>{\n                    debounceRef.current = null;\n                    await fetchData();\n                }, debounceTime);\n            } else {\n                fetchData();\n            }\n        }\n\n        if (!didMountRef.current && !fetchOnMount) {\n            didMountRef.current = true;\n            return;\n        }\n\n        if (!requestByFetch) {\n            fetchNowRef.current = false;\n            startBouncing();\n        } else if (requestByFetch && fetchNowRef.current) {\n            fetchNowRef.current = false;\n            startBouncing();\n        }\n\n        return () => {\n            cancelled = true;\n            if (debounceRef.current !== null) {\n                clearTimeout(debounceRef.current);\n                debounceRef.current = null;\n            }\n            if (dispatch && loading) {\n                dispatch(LoadingAction.STOP);\n            }\n        };\n    }, [dataFetcher, fetchOnMount, forceRequest, requestByFetch, dispatch, fetchNow, debounceTime]);\n\n    return { data, isLoading, error, fetch, clearState };\n}\n"
  },
  {
    "path": "console2/src/hooks/useThrottle.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { useCallback, useRef, useEffect } from 'react';\nimport { throttle } from 'lodash';\n\n/**\n * Creates a throttled version of the provided callback function.\n *\n * @param callback - The function to throttle\n * @param delay - The number of milliseconds to throttle invocations to\n * @returns A throttled version of the callback that will only execute at most once per delay period\n */\nexport function useThrottle<T extends (...args: any[]) => any>(\n    callback: T,\n    delay: number\n): T {\n    const throttledFnRef = useRef<ReturnType<typeof throttle>>();\n\n    useEffect(() => {\n        throttledFnRef.current = throttle(callback, delay, {\n            leading: true,\n            trailing: true\n        });\n\n        return () => {\n            throttledFnRef.current?.cancel();\n        };\n    }, [callback, delay]);\n\n    return useCallback(\n        ((...args) => throttledFnRef.current?.(...args)) as T,\n        []\n    );\n}\n"
  },
  {
    "path": "console2/src/index.css",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nhtml {\n  height: 100% !important;\n  min-height: 100% !important;\n}\n\nbody {\n  min-height: 100% !important;\n  margin-right: calc(-1 * (100vw - 100%));\n}\n\n#root {\n  height: 100% !important;\n  min-height: 100% !important;\n  margin-top: 5px;\n}\n\n.maxHeight {\n  height: 100% !important;\n  min-height: 100% !important;\n}\n\n.ellipsis {\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n.ui.fullscreen.modal {\n  left: auto !important;\n}\n\n.loading {\n  opacity: 0.5;\n}"
  },
  {
    "path": "console2/src/index.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport { createRoot } from 'react-dom/client';\n\nimport 'typeface-lato';\nimport 'semantic-ui-css/semantic.min.css';\n\nimport App from './App';\nimport './index.css';\n\nconst rootEl = document.getElementById('root') as HTMLElement;\nconst root = createRoot(rootEl);\n\nroot.render(<App />);\n\n// remove any old service worker\nif (navigator.serviceWorker) {\n    navigator.serviceWorker\n        .getRegistrations()\n        .then((regs) => regs.forEach((reg) => reg.unregister()));\n}\n"
  },
  {
    "path": "console2/src/react-app-env.d.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n/// <reference types=\"react-scripts\" />\n"
  },
  {
    "path": "console2/src/reducers/loading.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nexport enum LoadingAction {\n    START,\n    STOP\n}\n\nexport interface LoadingState {\n    loading: boolean;\n    loadingCounter: number;\n}\n\nexport const initialState = { loading: false, loadingCounter: 0 };\n\nexport const reducer = (state: LoadingState, action: LoadingAction): LoadingState => {\n    switch (action) {\n        case LoadingAction.START:\n            return { ...state, loadingCounter: state.loadingCounter + 1, loading: true };\n        case LoadingAction.STOP: {\n            const count = state.loadingCounter - 1;\n            return { ...state, loadingCounter: count, loading: count > 0 };\n        }\n    }\n};\n"
  },
  {
    "path": "console2/src/router.tsx",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport * as React from 'react';\nimport {\n    To,\n    useLocation,\n    useNavigate,\n    useParams,\n    type Location,\n    type NavigateFunction,\n} from 'react-router';\n\ninterface HistoryCompat {\n    location: Location;\n    push: NavigateFunction;\n    replace: (to: To) => void;\n}\n\nexport interface RouteComponentProps<P = {}> {\n    history: HistoryCompat;\n    location: Location;\n    match: {\n        isExact: boolean;\n        params: P;\n        path: string;\n        url: string;\n    };\n}\n\nexport const useHistory = (): HistoryCompat => {\n    const location = useLocation();\n    const navigate = useNavigate();\n\n    return React.useMemo(\n        () => ({\n            location,\n            push: navigate,\n            replace: (to: To) => navigate(to, { replace: true }),\n        }),\n        [location, navigate]\n    );\n};\n\nexport const withRouter = <P extends RouteComponentProps<any>>(\n    Component: React.ComponentType<P>\n) => {\n    const WithRouter = (props: Omit<P, keyof RouteComponentProps<any>>) => {\n        const location = useLocation();\n        const params = useParams();\n        const history = useHistory();\n\n        return (\n            <Component\n                {...(props as P)}\n                history={history}\n                location={location}\n                match={{\n                    isExact: false,\n                    params: params as any,\n                    path: '',\n                    url: location.pathname,\n                }}\n            />\n        );\n    };\n\n    WithRouter.displayName = `withRouter(${Component.displayName || Component.name || 'Component'})`;\n\n    return WithRouter;\n};\n"
  },
  {
    "path": "console2/src/session.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport * as React from 'react';\n\nimport { Organizations } from './state/data/orgs/types';\nimport { logout as apiLogout, whoami as apiWhoami } from './api/service/console';\n\nexport interface UserInfo {\n    username: string;\n    displayName: string;\n    orgs: Organizations;\n}\n\nexport interface UserSession {\n    userInfo?: UserInfo;\n    setUserInfo: (userInfo?: UserInfo) => void;\n\n    loggingIn: boolean;\n    setLoggingIn: (loggingIn: boolean) => void;\n}\n\nexport const UserSessionContext = React.createContext<UserSession>({\n    loggingIn: true,\n    setUserInfo: () => {},\n    setLoggingIn: () => {},\n});\n\nexport const checkSession = async (session: UserSession) => {\n    const { userInfo } = session;\n\n    const loggedIn = !!userInfo?.username;\n    if (loggedIn) {\n        return;\n    }\n\n    await refreshSession(session);\n};\n\nconst refreshSession = async ({ setUserInfo, setLoggingIn }: UserSession) => {\n    setLoggingIn(true);\n    try {\n        const response = await apiWhoami();\n        setUserInfo({ ...response });\n    } catch (e) {\n        console.warn(e);\n    } finally {\n        setLoggingIn(false);\n    }\n};\n\nexport const logout = async ({ setUserInfo }: UserSession): Promise<boolean> => {\n    try {\n        await apiLogout();\n        setUserInfo(undefined);\n        return true;\n    } catch (e) {\n        console.error('logout:', e);\n        return false;\n    }\n};\n"
  },
  {
    "path": "console2/src/state/data/orgs/types.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { OrganizationEntry } from '../../../api/org';\n\nexport interface Organizations {\n    [id: string]: OrganizationEntry;\n}\n"
  },
  {
    "path": "console2/src/state/data/processes/logs/processors.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\nimport { format as formatDate, parseISO as parseDate } from 'date-fns';\nimport { escapeHtml, highlight } from '../../../../utils';\nimport { LogSegment, LogSegmentType, TagData } from './types';\n\nconst TAG = '__logTag:';\n\nexport interface LogProcessorOptions {\n    separateTasks?: boolean;\n    useLocalTime?: boolean;\n    showDate?: boolean;\n}\n\nexport const process = (s: LogSegment, opts: LogProcessorOptions): LogSegment[] => {\n    return split(s, opts).map((s) => {\n        if (s.type === LogSegmentType.DATA) {\n            return { ...s, data: processText(s.data as string, opts) };\n        }\n        return s;\n    });\n};\n\nconst split = (s: LogSegment, opts: LogProcessorOptions): LogSegment[] => {\n    if (s.type !== LogSegmentType.DATA) {\n        return [s];\n    }\n\n    const result: LogSegment[] = [];\n\n    let data = s.data as string;\n    while (true) {\n        const tagStart = data.indexOf(TAG);\n        if (tagStart < 0) {\n            result.push({ type: LogSegmentType.DATA, data });\n            break;\n        }\n\n        const tagEnd = data.indexOf('\\n', tagStart);\n        if (tagEnd < 0) {\n            break;\n        }\n\n        // grab the log segment before the tag\n        const prev = data.substring(0, tagStart);\n        result.push({ type: LogSegmentType.DATA, data: prev });\n\n        if (opts.separateTasks) {\n            // grab the tag's data\n            const tag = data.substring(tagStart + TAG.length, tagEnd);\n            try {\n                result.push({ type: LogSegmentType.TAG, data: JSON.parse(tag) as TagData });\n            } catch (e) {\n                console.warn('Error while parsing a log tag: ', tag, e);\n            }\n        }\n\n        // next segment\n        data = data.substring(tagEnd + 1);\n    }\n\n    return result;\n};\n\nexport const processText = (s: string, { useLocalTime, showDate }: LogProcessorOptions): string => {\n    s = processDate(s, useLocalTime, showDate);\n    s = escapeHtml(s);\n    s = processLinks(s);\n    s = processTags(s);\n    s = colorize(s);\n\n    return s;\n};\n\nconst URL_PATTERN = /(\\b(https?):\\/\\/([-A-Z0-9+@#/%?=~_|!:,.;]|&amp;)*)/;\n\nconst processLinks = (value: string): string => {\n    return value.replace(\n        RegExp(URL_PATTERN, 'ig'),\n        (url) => `<a href=\"${url}\" target=\"_blank\">${url}</a>`\n    );\n};\n\n// html tags (<, >, ..) already escaped\nconst INSTANCE_ID_TAG = /&lt;concord:instanceId&gt;([^&]*)&lt;\\/concord:instanceId&gt;/;\n\nconst processTags = (value: string): string => {\n    return value.replace(\n        RegExp(INSTANCE_ID_TAG, 'ig'),\n        (s, instanceId) => `<a href=\"#/process/${instanceId}/log\" target=\"_blank\">${instanceId}</a>`\n    );\n};\n\nconst colorizeProps = {\n    config: [\n        { string: 'INFO ', style: 'color: #00B5F0' },\n        { string: 'WARN ', style: 'color: #ffae42' },\n        { string: 'ERROR', style: 'color: #ff0000' },\n        { string: 'ANSIBLE:', style: 'color: #808080' },\n        { string: 'DOCKER:', style: 'color: #808080' }\n    ]\n};\n\nconst colorize = (value: string): string => {\n    return highlight(value, colorizeProps);\n};\n\n// we expect the runtime to use \"yyyy-MM-dd'T'HH:mm:ss.SSSZ\" format for timestamps\n// see also /common/src/main/java/com/walmartlabs/concord/common/LogUtils.java\nconst DATE_PATTERN = /^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}.\\d{4})/;\nconst DATE_LENGTH = '2019-04-11T19:41:24.839+0000'.length;\n\nconst processDate = (value: string, useLocalTime?: boolean, showDate?: boolean): string => {\n    if (!useLocalTime) {\n        return value;\n    }\n\n    const lines = value.split('\\n');\n    for (let i = 0; i < lines.length; i++) {\n        const l = lines[i];\n        const dt = l.substring(0, DATE_LENGTH);\n        if (DATE_PATTERN.test(dt)) {\n            const d = parseDate(dt);\n            const dst = formatDate(d, `${showDate ? 'yyyy-MM-dd ' : ''}HH:mm:ss`);\n            lines[i] = dst + l.substring(DATE_LENGTH);\n        }\n    }\n    return lines.join('\\n');\n};\n"
  },
  {
    "path": "console2/src/state/data/processes/logs/types.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { ConcordId } from '../../../../api/common';\n\nexport enum LogSegmentType {\n    DATA,\n    TAG\n}\n\nexport interface TagData {\n    phase: 'pre' | 'post';\n    taskName: string;\n    correlationId: ConcordId;\n}\n\nexport interface LogSegment {\n    data: string | TagData;\n    type: LogSegmentType;\n}\n"
  },
  {
    "path": "console2/src/utils.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport { format as formatDate, parseISO as parseDate } from 'date-fns';\nimport {AnsiUp} from \"ansi_up/ansi_up\";\n\ninterface HasName {\n    name: string;\n}\n\n/**\n * General rules to follow for comparator definitions\n *\n * used with Array.prototype.sort()\n *\n * If compareFunction(a, b) is less than 0, sort a to an index lower than b (i.e. a comes first).\n *\n * If compareFunction(a, b) returns 0, leave a and b unchanged with respect to each other,\n * but sorted with respect to all different elements.\n * Note: the ECMAscript standard does not guarantee this behaviour, and thus not all browsers\n * (e.g. Mozilla versions dating back to at least 2003) respect this.\n *\n * If compareFunction(a, b) is greater than 0, sort b to an index lower than a (i.e. b comes first).\n *\n * compareFunction(a, b) must always return the same value when given a\n * specific pair of elements a and b as its two arguments.\n * If inconsistent results are returned then the sort order is undefined.\n */\nexport const comparators = {\n    byProperty: <T, P>(getter: (i: T) => P) => (a: T, b: T): number => {\n        const x = getter(a);\n        const y = getter(b);\n        return x > y ? 1 : x < y ? -1 : 0;\n    },\n    byName: (a: HasName, b: HasName) => (a.name > b.name ? 1 : a.name < b.name ? -1 : 0)\n};\n\nexport const notEmpty = (x: {}) => {\n    if (!x) {\n        return false;\n    }\n\n    const ks = Object.keys(x);\n    if (ks.length === 0) {\n        return false;\n    }\n\n    for (const k of ks) {\n        const v = x[k];\n        if (v !== undefined) {\n            return true;\n        }\n    }\n\n    return false;\n};\n\nexport const formatTimestamp = (t?: string): string | undefined => {\n    if (!t) {\n        return;\n    }\n\n    return formatDate(parseDate(t), 'yyyy-MM-dd HH:mm:ss');\n};\n\nconst second2Ms = 1000;\nconst minute2Ms = 60 * second2Ms;\nconst hour2Ms = 60 * minute2Ms;\nconst day2Ms = 24 * hour2Ms;\n\nexport const formatDuration = (ms?: number): string | undefined => {\n    if (ms === 0) {\n        return '0ms';\n    }\n\n    if (!ms) {\n        return;\n    }\n\n    let t = ms;\n\n    if (t < second2Ms) {\n        return `${t}ms`;\n    }\n\n    if (t < minute2Ms) {\n        return `~ ${Math.ceil(t / second2Ms)}s`;\n    }\n\n    if (t < hour2Ms) {\n        return `~ ${Math.ceil(t / minute2Ms)}m`;\n    }\n\n    let s = '~';\n\n    const days = Math.floor(t / day2Ms);\n    if (days > 0) {\n        s += ` ${days}d`;\n        t -= days * day2Ms;\n    }\n\n    const hours = Math.floor(t / hour2Ms);\n    if (hours > 0) {\n        s += ` ${hours}h`;\n        t -= hours * hour2Ms;\n    }\n\n    const mins = Math.floor(t / minute2Ms);\n    if (mins > 0) {\n        s += ` ${mins}m`;\n    }\n\n    return s;\n};\n\nconst thresh = 1024;\nconst units = ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];\nexport const formatFileSize = (bytes: number) => {\n    if (Math.abs(bytes) < thresh) {\n        return bytes + ' B';\n    }\n\n    let u = -1;\n    do {\n        bytes /= thresh;\n        ++u;\n    } while (Math.abs(bytes) >= thresh && u < units.length - 1);\n    return bytes.toFixed(1) + ' ' + units[u];\n};\n\nexport const escapeHtml = (s: string): string =>\n    s\n        .replace(/&/g, '&amp;')\n        .replace(/</g, '&lt;')\n        .replace(/>/g, '&gt;')\n        .replace(/\"/g, '&quot;')\n        .replace(/'/g, '&#039;');\n\nexport interface Config {\n    string: string;\n    style: string;\n    divide?: boolean;\n}\n\nexport interface HighlighterProps {\n    config: Config[];\n    caseInsensitive?: boolean;\n    global?: boolean;\n}\n\nconst ansiUp = new AnsiUp();\nansiUp.escape_html = false;\n\nexport const highlight = (value: string, props: HighlighterProps): string => {\n    const { config, caseInsensitive = false, global = true } = props;\n    const regExpCfg = `${caseInsensitive ? 'i' : ''}\n            ${global ? 'g' : ''}`.trim();\n    let txt = value;\n\n    for (const cfg of config) {\n        if (typeof txt === 'string') {\n            txt = txt.replace(\n                RegExp(cfg.string, regExpCfg),\n                () =>\n                    `<span style=\"${cfg.style}\"><b>${cfg.string}</b></span>${\n                        cfg.divide ? '<hr/>' : ''\n                    }`\n            );\n        }\n    }\n\n    txt = ansiUp.ansi_to_html(txt);\n\n    return txt;\n};\n\nexport const setQueryParam = (url: string, key: string, value: string): string => {\n    const isAbsoluteUrl = url.startsWith('http') || url.startsWith('https');\n    const fakeBase = !isAbsoluteUrl ? 'http://fake-base.com' : undefined;\n    var modifiedUrl = new URL(url, fakeBase);\n\n    modifiedUrl.searchParams.set(key, value);\n\n    if (isAbsoluteUrl) {\n        return modifiedUrl.toString();\n    } else {\n        return modifiedUrl.toString().replace(fakeBase!, '');\n    }\n}\n"
  },
  {
    "path": "console2/src/validation.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n/**\n * @see com.walmartlabs.concord.common.validation.ConcordKey\n * @type {RegExp}\n */\nconst CONCORD_KEY_PATTERN = /^[0-9a-zA-Z][0-9a-zA-Z_@.\\-~]{2,128}$/;\nconst COMMIT_ID_PATTERN = /^[0-9a-f]{1,40}$/;\n\nexport const REPOSITORY_SSH_URL_PATTERN = /^(ssh:\\/\\/)?([a-zA-Z0-9\\-_.]+)@([^:]+):?(.*)(.git)$/;\n\nconst requiredError = () => 'Required';\nconst tooLongError = (n: number) => `Must be not more than ${n} characters.`;\nconst invalidRepositoryUrlError = () =>\n    \"Invalid repository URL: must begin with 'https://', 'mvn://', 'ssh://' or use 'user@host:path' scheme.\";\nconst invalidCommitIdError = () => 'Invalid commit ID: must be a valid revision.';\nconst concordKeyPatternError = () =>\n    \"Must start with a digit or a letter, may contain only digits, letters, underscores, hyphens, tildes, '.' or '@' or. Must be between 3 and 128 characters in length.\";\n\nexport const projectAlreadyExistsError = (n: string) => `Project already exists: ${n}`;\n\nexport const secretAlreadyExistsError = (n: string) => `Secret already exists: ${n}`;\n\nexport const jsonStoreAlreadyExistsError = (n: string) => `JSON store already exists: ${n}`;\n\nexport const jsonStoreQueryAlreadyExistsError = (n: string) =>\n    `JSON store query already exists: ${n}`;\n\nexport const repositoryAlreadyExistsError = (n: string) => `Repository already exists: ${n}`;\n\nexport const teamAlreadyExistsError = (n: string) => `Team already exists: ${n}`;\n\nexport const apiTokenAlreadyExistsError = (n: string) => `API Token already exists: ${n}`;\n\nexport const passwordTooWeakError = () =>\n    `Password is too weak. It must contain at least 7 characters, a digit and an uppercase character.`;\n\nconst concordKeyValidator = (v?: string) => {\n    if (!v) {\n        return requiredError();\n    }\n\n    if (!v.match(CONCORD_KEY_PATTERN)) {\n        return concordKeyPatternError();\n    }\n\n    return;\n};\n\nconst repositoryUrlValidator = (v?: string) => {\n    if (!v) {\n        return requiredError();\n    }\n\n    if (!v.startsWith('https://') && !v.match(REPOSITORY_SSH_URL_PATTERN) && !v.startsWith('mvn://')) {\n        return invalidRepositoryUrlError();\n    }\n\n    return;\n};\n\nconst requiredValidator = (v?: {}) => {\n    if (!v) {\n        return requiredError();\n    }\n    return;\n};\n\nexport const project = {\n    name: concordKeyValidator,\n    description: (v?: string) => {\n        if (v && v.length > 1024) {\n            return tooLongError(1024);\n        }\n        return;\n    }\n};\n\nexport const storage = {\n    name: concordKeyValidator\n};\n\nexport const storageQuery = {\n    name: concordKeyValidator,\n    query: (v?: string) => {\n        if (!v) {\n            return requiredError();\n        }\n        if (v && v.length > 4000) {\n            return tooLongError(4000);\n        }\n        return;\n    }\n};\n\nexport const repository = {\n    name: concordKeyValidator,\n    url: repositoryUrlValidator,\n    branch: (v?: string) => {\n        if (!v) {\n            return requiredError();\n        }\n        if (v && v.length > 255) {\n            return tooLongError(255);\n        }\n        return;\n    },\n    commitId: (v?: string) => {\n        if (!v) {\n            return requiredError();\n        }\n        if (v && !v.match(COMMIT_ID_PATTERN)) {\n            return invalidCommitIdError();\n        }\n        return;\n    },\n    path: (v?: string) => {\n        if (v && v.length > 2048) {\n            return tooLongError(2048);\n        }\n        return;\n    },\n    secret: concordKeyValidator,\n    secretId: requiredValidator\n};\n\nexport const secret = {\n    name: concordKeyValidator,\n    publicFile: requiredValidator,\n    privateFile: requiredValidator,\n    username: requiredValidator,\n    password: requiredValidator,\n    valueString: requiredValidator,\n    valueFile: requiredValidator,\n    storePassword: requiredValidator\n};\n\nexport const team = {\n    name: concordKeyValidator,\n    description: (v?: string) => {\n        if (v && v.length > 2048) {\n            return tooLongError(2048);\n        }\n        return;\n    }\n};\n"
  },
  {
    "path": "console2/src/vite-env.d.ts",
    "content": "/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n/// <reference types=\"vite/client\" />\n\ninterface ImportMetaEnv {\n  readonly VITE_CONCORD_VERSION?: string;\n  // Add other env variables as needed\n}\n\ninterface ImportMeta {\n  readonly env: ImportMetaEnv;\n}\n"
  },
  {
    "path": "console2/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": false,\n    \"noFallthroughCasesInSwitch\": true,\n\n    /* Existing settings to preserve */\n    \"allowJs\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"downlevelIteration\": true,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"useUnknownInCatchVariables\": false,\n\n    /* Path aliases */\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"target\", \"scripts\"],\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "console2/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "console2/types.d.ts",
    "content": "declare module 'react-scroll-up-button';\n"
  },
  {
    "path": "console2/vite.config.ts",
    "content": "import { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport path from 'path';\n\nconst manualChunkGroups = [\n  ['react-vendor', ['react', 'react-dom', 'react-router']],\n  ['ui-vendor', ['semantic-ui-react']]\n] as const;\n\nfunction manualChunks(id: string) {\n  const normalizedId = id.replaceAll('\\\\', '/');\n\n  if (!normalizedId.includes('/node_modules/')) {\n    return undefined;\n  }\n\n  for (const [chunkName, packages] of manualChunkGroups) {\n    if (packages.some((pkg) => normalizedId.includes(`/node_modules/${pkg}/`))) {\n      return chunkName;\n    }\n  }\n\n  return undefined;\n}\n\nexport default defineConfig({\n  plugins: [react()],\n  base: '/',\n  server: {\n    port: 3000,\n    proxy: {\n      '/api': {\n        target: 'http://localhost:8001',\n        changeOrigin: true,\n        secure: false\n      }\n    }\n  },\n  build: {\n    outDir: 'target/classes/META-INF/console2',\n    emptyOutDir: true,\n    // Semantic UI CSS contains selectors Lightning CSS rejects in Vite 8.\n    cssMinify: 'esbuild',\n    sourcemap: true,\n    rollupOptions: {\n      output: {\n        manualChunks\n      }\n    }\n  },\n  resolve: {\n    alias: {\n      '@': path.resolve(__dirname, './src')\n    }\n  },\n  optimizeDeps: {\n    include: [\n      'react',\n      'react-dom',\n      'react-router',\n      'semantic-ui-react'\n    ]\n  }\n});\n"
  },
  {
    "path": "console2/wallaby.js",
    "content": "module.exports = function(wallaby) {\n    return {\n        files: ['src/**/*.ts*', '!src/**/*.test.ts*'],\n        tests: ['src/**/*test.ts*'],\n        env: {\n            type: 'node',\n            runner: 'node'\n        },\n        compilers: {\n            '**/*.ts?(x)': wallaby.compilers.typeScript({\n                module: 'commonjs',\n                jsx: 'React'\n            })\n        },\n        testFramework: 'jest',\n        debug: true,\n        setup: function(wallaby) {\n            \n            // you can access 'window' object in a browser environment,\n            // 'global' object or require(...) something in node environment\n        }\n    };\n};\n"
  },
  {
    "path": "dependency-manager/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-dependency-manager</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n        </dependency>\n\n        <!-- Aether & Co -->\n        <dependency>\n            <groupId>org.apache.maven.resolver</groupId>\n            <artifactId>maven-resolver-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.maven.resolver</groupId>\n            <artifactId>maven-resolver-spi</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.maven.resolver</groupId>\n            <artifactId>maven-resolver-util</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.maven.resolver</groupId>\n            <artifactId>maven-resolver-impl</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.maven.resolver</groupId>\n            <artifactId>maven-resolver-connector-basic</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.maven.resolver</groupId>\n            <artifactId>maven-resolver-transport-file</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>ca.ibodrov.concord.maven</groupId>\n            <artifactId>concord-maven-resolver-transport-http</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.maven</groupId>\n            <artifactId>maven-resolver-provider</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n        </dependency>\n\n        <!-- Immutables -->\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.errorprone</groupId>\n            <artifactId>error_prone_annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <!-- JDK9+ compatibility -->\n        <dependency>\n            <groupId>javax.xml.bind</groupId>\n            <artifactId>jaxb-api</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>ch.qos.logback</groupId>\n            <artifactId>logback-classic</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <resources>\n            <resource>\n                <filtering>false</filtering>\n                <directory>${project.basedir}/src/main/resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n            <resource>\n                <filtering>true</filtering>\n                <directory>${project.basedir}/src/main/filtered-resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n        </resources>\n    </build>\n</project>\n"
  },
  {
    "path": "dependency-manager/src/main/filtered-resources/com/walmartlabs/concord/dependencymanager/version.properties",
    "content": "version=${project.version}\n"
  },
  {
    "path": "dependency-manager/src/main/java/com/walmartlabs/concord/dependencymanager/DependencyEntity.java",
    "content": "package com.walmartlabs.concord.dependencymanager;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.net.URI;\nimport java.nio.file.Path;\nimport java.util.Objects;\n\npublic class DependencyEntity {\n\n    private final Path path;\n    private final Artifact artifact;\n    private final URI directLink;\n\n    public DependencyEntity(Path path, String groupId, String artifactId, String version) {\n        this.path = path;\n        this.artifact = new Artifact(groupId, artifactId, version);\n        this.directLink = null;\n    }\n\n    public DependencyEntity(Path path, URI directLink) {\n        this.path = path;\n        this.artifact = null;\n        this.directLink = directLink;\n    }\n\n    public Path getPath() {\n        return path;\n    }\n\n    public Artifact getArtifact() {\n        return artifact;\n    }\n\n    public URI getDirectLink() {\n        return directLink;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        DependencyEntity that = (DependencyEntity) o;\n        return Objects.equals(path, that.path);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(path);\n    }\n\n    @Override\n    public String toString() {\n        if (artifact != null) {\n            return artifact.toString();\n        } else {\n            return directLink.toString();\n        }\n    }\n\n    public static class Artifact {\n\n        private final String groupId;\n        private final String artifactId;\n        private final String version;\n\n        public Artifact(String groupId, String artifactId, String version) {\n            this.groupId = groupId;\n            this.artifactId = artifactId;\n            this.version = version;\n        }\n\n        public String getGroupId() {\n            return groupId;\n        }\n\n        public String getArtifactId() {\n            return artifactId;\n        }\n\n        public String getVersion() {\n            return version;\n        }\n\n        @Override\n        public String toString() {\n            return \"Artifact{\" +\n                    \"groupId='\" + groupId + '\\'' +\n                    \", artifactId='\" + artifactId + '\\'' +\n                    \", version='\" + version + '\\'' +\n                    '}';\n        }\n    }\n}\n"
  },
  {
    "path": "dependency-manager/src/main/java/com/walmartlabs/concord/dependencymanager/DependencyManager.java",
    "content": "package com.walmartlabs.concord.dependencymanager;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.annotations.VisibleForTesting;\nimport com.walmartlabs.concord.common.ExceptionUtils;\nimport org.apache.maven.repository.internal.MavenRepositorySystemUtils;\nimport org.eclipse.aether.DefaultRepositorySystemSession;\nimport org.eclipse.aether.RepositorySystem;\nimport org.eclipse.aether.RepositorySystemSession;\nimport org.eclipse.aether.artifact.Artifact;\nimport org.eclipse.aether.artifact.DefaultArtifact;\nimport org.eclipse.aether.collection.CollectRequest;\nimport org.eclipse.aether.collection.DependencyCollectionContext;\nimport org.eclipse.aether.collection.DependencySelector;\nimport org.eclipse.aether.graph.Dependency;\nimport org.eclipse.aether.repository.LocalRepository;\nimport org.eclipse.aether.repository.Proxy;\nimport org.eclipse.aether.repository.RemoteRepository;\nimport org.eclipse.aether.repository.RepositoryPolicy;\nimport org.eclipse.aether.resolution.*;\nimport org.eclipse.aether.transfer.AbstractTransferListener;\nimport org.eclipse.aether.transfer.ArtifactNotFoundException;\nimport org.eclipse.aether.transfer.RepositoryOfflineException;\nimport org.eclipse.aether.transfer.TransferEvent;\nimport org.eclipse.aether.util.artifact.JavaScopes;\nimport org.eclipse.aether.util.filter.ExclusionsDependencyFilter;\nimport org.eclipse.aether.util.graph.selector.AndDependencySelector;\nimport org.eclipse.aether.util.graph.selector.ExclusionDependencySelector;\nimport org.eclipse.aether.util.graph.selector.OptionalDependencySelector;\nimport org.eclipse.aether.util.repository.AuthenticationBuilder;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Singleton;\nimport javax.xml.bind.DatatypeConverter;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.UnsupportedEncodingException;\nimport java.net.URI;\nimport java.net.URLDecoder;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.*;\nimport java.security.MessageDigest;\nimport java.util.*;\nimport java.util.concurrent.Callable;\nimport java.util.stream.Collectors;\n\n@Singleton\npublic class DependencyManager {\n\n    private static final Logger log = LoggerFactory.getLogger(DependencyManager.class);\n\n    private static final int RETRY_COUNT = 3;\n    private static final long RETRY_INTERVAL = 5000;\n\n    private static final String FILES_CACHE_DIR = \"files\";\n    public static final String MAVEN_SCHEME = \"mvn\";\n    private static final String LATEST = \"LATEST\";\n\n    private final Path cacheDir;\n    private final Path localCacheDir;\n    private final List<RemoteRepository> repositories;\n    private final Object mutex = new Object();\n    private final RepositorySystem maven;\n    private final boolean strictRepositories;\n\n    private final List<String> defaultExclusions;\n\n    private final boolean explicitlyResolveV1Client;\n    private final boolean offlineMode;\n\n    @Inject\n    public DependencyManager(DependencyManagerConfiguration cfg) throws IOException {\n        this.cacheDir = cfg.cacheDir();\n        if (!Files.exists(cacheDir)) {\n            Files.createDirectories(cacheDir);\n        }\n        this.localCacheDir = resolveLocalCacheDir();\n\n        log.info(\"init -> using repositories: {}\", cfg.repositories());\n        this.repositories = toRemote(cfg.repositories());\n        this.maven = RepositorySystemFactory.create();\n        this.strictRepositories = cfg.strictRepositories();\n        this.defaultExclusions = cfg.exclusions();\n        this.explicitlyResolveV1Client = cfg.explicitlyResolveV1Client();\n        this.offlineMode = cfg.offlineMode();\n    }\n\n    @VisibleForTesting\n    static Path resolveLocalCacheDir() {\n        String localRepo = System.getProperty(\"maven.repo.local\");\n        if (localRepo != null && !localRepo.isBlank()) {\n            return Paths.get(localRepo).toAbsolutePath().normalize();\n        }\n\n        return Paths.get(System.getProperty(\"user.home\"))\n                .resolve(\".m2/repository\")\n                .toAbsolutePath()\n                .normalize();\n    }\n\n    public Collection<DependencyEntity> resolve(Collection<URI> items) throws IOException {\n        return resolve(items, null);\n    }\n\n    public Collection<DependencyEntity> resolve(Collection<URI> items, ProgressListener listener) throws IOException {\n        ResolveExceptionConverter exceptionConverter = new ResolveExceptionConverter(items);\n        ProgressNotifier progressNotifier = new ProgressNotifier(listener, exceptionConverter);\n        return withRetry(() -> tryResolve(items, progressNotifier), exceptionConverter, progressNotifier);\n    }\n\n    public DependencyEntity resolveSingle(URI item) throws IOException {\n        return resolveSingle(item, null);\n    }\n\n    public DependencyEntity resolveSingle(URI item, ProgressListener listener) throws IOException {\n        ResolveExceptionConverter exceptionConverter = new ResolveExceptionConverter(item);\n        ProgressNotifier progressNotifier = new ProgressNotifier(listener, exceptionConverter);\n        return withRetry(() -> tryResolveSingle(item, progressNotifier), exceptionConverter, progressNotifier);\n    }\n\n    private Collection<DependencyEntity> tryResolve(Collection<URI> items, ProgressNotifier progressNotifier) throws IOException {\n        if (items == null || items.isEmpty()) {\n            return Collections.emptySet();\n        }\n\n        // ensure stable order\n        List<URI> uris = new ArrayList<>(new HashSet<>(items));\n        Collections.sort(uris);\n\n        DependencyList deps = categorize(uris);\n\n        Collection<DependencyEntity> result = new HashSet<>();\n\n        result.addAll(resolveDirectLinks(deps.directLinks));\n\n        result.addAll(resolveMavenTransitiveDependencies(deps.mavenTransitiveDependencies, deps.mavenExclusions, progressNotifier).stream()\n                .map(DependencyManager::toDependency)\n                .toList());\n\n        result.addAll(resolveMavenSingleDependencies(deps.mavenSingleDependencies, progressNotifier).stream()\n                .map(DependencyManager::toDependency)\n                .toList());\n\n        return result;\n    }\n\n    private DependencyEntity tryResolveSingle(URI item, ProgressNotifier progressNotifier) throws IOException {\n        String scheme = item.getScheme();\n        if (MAVEN_SCHEME.equalsIgnoreCase(scheme)) {\n            String id = item.getAuthority();\n            Artifact artifact = resolveMavenSingle(new MavenDependency(new DefaultArtifact(id), JavaScopes.COMPILE), progressNotifier);\n            return toDependency(artifact);\n        } else {\n            return new DependencyEntity(resolveFile(item), item);\n        }\n    }\n\n    private DependencyList categorize(List<URI> items) throws IOException {\n        List<MavenDependency> mavenTransitiveDependencies = new ArrayList<>();\n        List<MavenDependency> mavenSingleDependencies = new ArrayList<>();\n        List<String> mavenExclusions = new ArrayList<>();\n        List<URI> directLinks = new ArrayList<>();\n\n        for (URI item : items) {\n            String scheme = item.getScheme();\n            if (MAVEN_SCHEME.equalsIgnoreCase(scheme)) {\n                String id = item.getAuthority();\n\n                Artifact artifact = new DefaultArtifact(id);\n\n                Map<String, List<String>> cfg = splitQuery(item);\n                String scope = getSingleValue(cfg, \"scope\", JavaScopes.COMPILE);\n                boolean transitive = Boolean.parseBoolean(getSingleValue(cfg, \"transitive\", \"true\"));\n\n                if (transitive) {\n                    mavenTransitiveDependencies.add(new MavenDependency(artifact, scope));\n                } else {\n                    mavenSingleDependencies.add(new MavenDependency(artifact, scope));\n                }\n\n                mavenExclusions.addAll(cfg.getOrDefault(\"exclude\", Collections.emptyList()));\n            } else {\n                directLinks.add(item);\n            }\n        }\n\n        return new DependencyList(mavenTransitiveDependencies, mavenSingleDependencies, mavenExclusions, directLinks);\n    }\n\n    private Collection<DependencyEntity> resolveDirectLinks(Collection<URI> items) throws IOException {\n        Collection<DependencyEntity> paths = new HashSet<>();\n        for (URI item : items) {\n            paths.add(new DependencyEntity(resolveFile(item), item));\n        }\n        return paths;\n    }\n\n    private Path resolveFile(URI uri) throws IOException {\n        boolean skipCache = shouldSkipCache(uri);\n        String name = getLastPart(uri);\n\n        Path baseDir = cacheDir.resolve(FILES_CACHE_DIR).resolve(hash(uri.toString()));\n        if (!Files.exists(baseDir)) {\n            Files.createDirectories(baseDir);\n        }\n\n        Path dst = baseDir.resolve(name);\n\n        synchronized (mutex) {\n            if (!skipCache && Files.exists(dst)) {\n                log.info(\"resolveFile -> using a cached copy of {}...\", uri);\n                return dst;\n            }\n\n            log.info(\"resolveFile -> downloading {}\", uri);\n\n            Path tmp = baseDir.resolve(name + \".tmp\");\n            try {\n                download(uri, tmp);\n                Files.move(tmp, dst, StandardCopyOption.REPLACE_EXISTING);\n            } finally {\n                if (Files.exists(tmp)) {\n                    Files.delete(tmp);\n                }\n            }\n\n\n            return dst;\n        }\n    }\n\n    private static String hash(String s) {\n        try {\n            MessageDigest md = MessageDigest.getInstance(\"MD5\");\n            md.update(s.getBytes());\n            return DatatypeConverter.printHexBinary(md.digest()).toUpperCase();\n        } catch (Exception e) {\n            throw new RuntimeException(\"Hash error\", e);\n        }\n    }\n\n    private Artifact resolveMavenSingle(MavenDependency dep, ProgressNotifier progressNotifier) throws IOException {\n        RepositorySystemSession session = getRepositorySession(Collections.singletonList(dep), maven, progressNotifier);\n\n        ArtifactRequest req = new ArtifactRequest();\n        req.setArtifact(dep.artifact);\n        req.setRepositories(repositories);\n\n        synchronized (mutex) {\n            try {\n                ArtifactResult r = maven.resolveArtifact(session, req);\n                return r.getArtifact();\n            } catch (ArtifactResolutionException e) {\n                throw new IOException(e);\n            }\n        }\n    }\n\n    private RepositorySystemSession getRepositorySession(Collection<MavenDependency> mavenDependencies, RepositorySystem system, ProgressNotifier progressNotifier) {\n        boolean isLatest = mavenDependencies.stream()\n                .map(d -> d.artifact.getVersion())\n                .anyMatch(a -> a.equals(LATEST));\n\n        if (isLatest) {\n            return newRepositorySystemSession(system, progressNotifier, true);\n        }\n\n        return newRepositorySystemSession(system, progressNotifier);\n    }\n\n    private Collection<Artifact> resolveMavenSingleDependencies(Collection<MavenDependency> deps, ProgressNotifier progressNotifier) throws IOException {\n        Collection<Artifact> paths = new HashSet<>();\n        for (MavenDependency dep : deps) {\n            paths.add(resolveMavenSingle(dep, progressNotifier));\n        }\n        return paths;\n    }\n\n    private Collection<Artifact> resolveMavenTransitiveDependencies(Collection<MavenDependency> deps, List<String> exclusions, ProgressNotifier progressNotifier) throws IOException {\n        // TODO: why we need new RepositorySystem?\n        RepositorySystem system = RepositorySystemFactory.create();\n        RepositorySystemSession session = getRepositorySession(deps, system, progressNotifier);\n\n        CollectRequest req = new CollectRequest();\n        req.setDependencies(deps.stream()\n                .map(d -> new Dependency(d.artifact, d.scope))\n                .collect(Collectors.toList()));\n        req.setRepositories(repositories);\n\n        List<String> excludes = new ArrayList<>(exclusions);\n        excludes.addAll(defaultExclusions);\n\n        DependencyRequest dependencyRequest = new DependencyRequest(req, new ExclusionsDependencyFilter(excludes));\n        if (explicitlyResolveV1Client) {\n            dependencyRequest.getCollectRequest().addManagedDependency(new Dependency(ClientDepSelector.CLIENT1_ARTIFACT, \"\"));\n        }\n\n        synchronized (mutex) {\n            try {\n                return system.resolveDependencies(session, dependencyRequest)\n                        .getArtifactResults().stream()\n                        .map(ArtifactResult::getArtifact)\n                        .collect(Collectors.toSet());\n            } catch (DependencyResolutionException e) {\n                throw new IOException(e);\n            }\n        }\n    }\n\n    private DefaultRepositorySystemSession newRepositorySystemSession(RepositorySystem system, ProgressNotifier progressNotifier) {\n        return newRepositorySystemSession(system, progressNotifier, false);\n    }\n\n    private DefaultRepositorySystemSession newRepositorySystemSession(RepositorySystem system, ProgressNotifier progressNotifier, boolean updatePolicy) {\n        DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();\n        session.setChecksumPolicy(RepositoryPolicy.CHECKSUM_POLICY_IGNORE);\n        session.setIgnoreArtifactDescriptorRepositories(strictRepositories);\n        if (explicitlyResolveV1Client) {\n            DependencySelector selector = new AndDependencySelector(\n                    new ClientDepSelector(),\n                    new OptionalDependencySelector(),\n                    new ExclusionDependencySelector());\n\n            session.setDependencySelector(selector);\n        }\n\n        LocalRepository localRepo = new LocalRepository(localCacheDir.toFile());\n        session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo));\n        session.setTransferListener(new AbstractTransferListener() {\n            @Override\n            public void transferFailed(TransferEvent event) {\n                progressNotifier.transferFailed(event);\n            }\n        });\n\n        session.setOffline(offlineMode);\n\n        if (updatePolicy) {\n            session.setUpdatePolicy(RepositoryPolicy.UPDATE_POLICY_ALWAYS);\n        }\n\n        return session;\n    }\n\n    private static DependencyEntity toDependency(Artifact artifact) {\n        return new DependencyEntity(artifact.getFile().toPath(),\n                artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion());\n    }\n\n    private static void download(URI uri, Path dst) throws IOException {\n        try (InputStream in = uri.toURL().openStream();\n             OutputStream out = Files.newOutputStream(dst, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {\n\n            byte[] ab = new byte[4096];\n            int read;\n            while ((read = in.read(ab)) > 0) {\n                out.write(ab, 0, read);\n            }\n        }\n    }\n\n    private static String getLastPart(URI uri) {\n        String p = uri.getPath();\n        int idx = p.lastIndexOf('/');\n        if (idx >= 0 && idx + 1 < p.length()) {\n            return p.substring(idx + 1);\n        }\n        throw new IllegalArgumentException(\"Invalid dependency URL. Can't get a file name, uri=\" + uri);\n    }\n\n    private static boolean shouldSkipCache(URI u) {\n        return \"file\".equalsIgnoreCase(u.getScheme()) || u.getPath().contains(\"SNAPSHOT\");\n    }\n\n    private static List<RemoteRepository> toRemote(List<MavenRepository> l) {\n        return l.stream()\n                .map(DependencyManager::toRemote)\n                .collect(Collectors.toList());\n    }\n\n    private static RemoteRepository toRemote(MavenRepository r) {\n        RemoteRepository.Builder b = new RemoteRepository.Builder(r.id(), r.contentType(), r.url());\n\n        MavenRepositoryPolicy releasePolicy = r.releasePolicy();\n        if (releasePolicy != null) {\n            b.setReleasePolicy(new RepositoryPolicy(releasePolicy.enabled(), releasePolicy.updatePolicy(), releasePolicy.checksumPolicy()));\n        }\n\n        MavenRepositoryPolicy snapshotPolicy = r.snapshotPolicy();\n        if (snapshotPolicy != null) {\n            b.setSnapshotPolicy(new RepositoryPolicy(snapshotPolicy.enabled(), snapshotPolicy.updatePolicy(), snapshotPolicy.checksumPolicy()));\n        }\n\n        Map<String, String> auth = r.auth();\n        if (auth != null) {\n            AuthenticationBuilder ab = new AuthenticationBuilder();\n            auth.forEach(ab::addString);\n            b.setAuthentication(ab.build());\n        }\n\n        MavenProxy proxy = r.proxy();\n        if (proxy != null) {\n            b.setProxy(new Proxy(proxy.type(), proxy.host(), proxy.port()));\n        }\n\n        return b.build();\n    }\n\n    private static String getSingleValue(Map<String, List<String>> m, String k, String defaultValue) {\n        List<String> vv = m.get(k);\n        if (vv == null || vv.isEmpty()) {\n            return defaultValue;\n        }\n        return vv.get(0);\n    }\n\n    private static Map<String, List<String>> splitQuery(URI uri) throws UnsupportedEncodingException {\n        String query = uri.getQuery();\n        if (query == null || query.isEmpty()) {\n            return Collections.emptyMap();\n        }\n\n        Map<String, List<String>> m = new LinkedHashMap<>();\n        String[] pairs = query.split(\"&\");\n        for (String pair : pairs) {\n            int idx = pair.indexOf(\"=\");\n            String k = URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8);\n            String v = URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8);\n\n            List<String> vv = m.computeIfAbsent(k, s -> new ArrayList<>());\n            vv.add(v);\n            m.put(k, vv);\n        }\n        return m;\n    }\n\n    private static class ClientDepSelector implements DependencySelector {\n\n        private static final String CONCORD_CLIENT_GROUP_ID = \"com.walmartlabs.concord\";\n        private static final String CONCORD_CLIENT_ARTIFACT_ID = \"concord-client\";\n\n        public static final Artifact CLIENT1_ARTIFACT = new DefaultArtifact(CONCORD_CLIENT_GROUP_ID, CONCORD_CLIENT_ARTIFACT_ID, \"jar\", Version.get());\n\n        private final boolean transitive;\n        private final Collection<String> excluded;\n\n        public ClientDepSelector() {\n            this(false, Arrays.asList(\"test\", \"provided\"));\n        }\n\n        public ClientDepSelector(boolean transitive, Collection<String> excluded) {\n            this.transitive = transitive;\n            this.excluded = excluded;\n        }\n\n        @Override\n        public boolean selectDependency(Dependency dependency) {\n            if (CONCORD_CLIENT_GROUP_ID.equals(dependency.getArtifact().getGroupId()) &&\n                CONCORD_CLIENT_ARTIFACT_ID.equals(dependency.getArtifact().getArtifactId())) {\n                return true;\n            }\n\n            if (!transitive) {\n                return true;\n            }\n\n            String scope = dependency.getScope();\n            return !excluded.contains(scope);\n        }\n\n        @Override\n        public DependencySelector deriveChildSelector(DependencyCollectionContext context) {\n            if (this.transitive || context.getDependency() == null) {\n                return this;\n            }\n\n            return new ClientDepSelector(true, excluded);\n        }\n    }\n\n    private static final class DependencyList {\n\n        private final List<MavenDependency> mavenTransitiveDependencies;\n        private final List<MavenDependency> mavenSingleDependencies;\n\n        private final List<String> mavenExclusions;\n\n        private final List<URI> directLinks;\n\n        private DependencyList(List<MavenDependency> mavenTransitiveDependencies,\n                               List<MavenDependency> mavenSingleDependencies,\n                               List<String> mavenExclusions,\n                               List<URI> directLinks) {\n\n            this.mavenTransitiveDependencies = mavenTransitiveDependencies;\n            this.mavenSingleDependencies = mavenSingleDependencies;\n            this.mavenExclusions = mavenExclusions;\n            this.directLinks = directLinks;\n        }\n    }\n\n    private static final class MavenDependency {\n\n        private final Artifact artifact;\n        private final String scope;\n\n        private MavenDependency(Artifact artifact, String scope) {\n            this.artifact = artifact;\n            this.scope = scope;\n        }\n    }\n\n    private static class ProgressNotifier implements RetryUtils.RetryListener {\n\n        private final ProgressListener listener;\n        private final ResolveExceptionConverter exceptionConverter;\n\n        private ProgressNotifier(ProgressListener listener, ResolveExceptionConverter exceptionConverter) {\n            this.listener = listener;\n            this.exceptionConverter = exceptionConverter;\n        }\n\n        @Override\n        public void onRetry(int tryCount, int retryCount, long retryInterval, Exception e) {\n            if (listener == null) {\n                return;\n            }\n\n            DependencyManagerException ex = exceptionConverter.convert(e);\n            listener.onRetry(tryCount, retryCount, retryInterval, ex.getMessage());\n        }\n\n        public void transferFailed(TransferEvent event) {\n            if (listener == null || event == null) {\n                return;\n            }\n\n            String error = Optional.ofNullable(event.getException()).map(Throwable::toString).orElse(\"n/a\");\n            listener.onTransferFailed(event + \", error: \" + error);\n        }\n    }\n\n    private static <T> T withRetry(Callable<T> c,\n                                   ResolveExceptionConverter exceptionConverter,\n                                   ProgressNotifier notifier) throws IOException {\n        try {\n            return RetryUtils.withRetry(DependencyManager.RETRY_COUNT, DependencyManager.RETRY_INTERVAL,\n                    c, ResolveRetryStrategy.INSTANCE, notifier);\n        } catch (Exception e) {\n            throw exceptionConverter.convert(e);\n        }\n    }\n\n    private static class ResolveRetryStrategy implements RetryUtils.RetryStrategy {\n\n        public static final ResolveRetryStrategy INSTANCE = new ResolveRetryStrategy();\n\n        @Override\n        public boolean canRetry(Exception e) {\n            List<Throwable> exceptions = ExceptionUtils.getExceptionList(e);\n            Throwable last = exceptions.get(exceptions.size() - 1);\n            return !((last instanceof RepositoryOfflineException) || (last instanceof ArtifactNotFoundException));\n        }\n    }\n}\n"
  },
  {
    "path": "dependency-manager/src/main/java/com/walmartlabs/concord/dependencymanager/DependencyManagerConfiguration.java",
    "content": "package com.walmartlabs.concord.dependencymanager;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.List;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface DependencyManagerConfiguration {\n\n    static DependencyManagerConfiguration of(Path cacheDir) {\n        return builder()\n                .cacheDir(cacheDir)\n                .build();\n    }\n\n    static DependencyManagerConfiguration of(Path cacheDir, List<MavenRepository> repositories) {\n        return builder()\n                .cacheDir(cacheDir)\n                .repositories(repositories)\n                .build();\n    }\n\n    Path cacheDir();\n\n    @Value.Default\n    default boolean strictRepositories() {\n        return false;\n    }\n\n    @Value.Default\n    default List<MavenRepository> repositories() {\n        return DependencyManagerRepositories.get();\n    }\n\n    @Value.Default\n    default List<String> exclusions() {\n        return Collections.emptyList();\n    }\n\n    @Value.Default\n    default boolean explicitlyResolveV1Client() {\n        return false;\n    }\n\n    @Value.Default\n    default boolean offlineMode() {\n        return false;\n    }\n\n    static ImmutableDependencyManagerConfiguration.Builder builder() {\n        return ImmutableDependencyManagerConfiguration.builder();\n    }\n}\n"
  },
  {
    "path": "dependency-manager/src/main/java/com/walmartlabs/concord/dependencymanager/DependencyManagerException.java",
    "content": "package com.walmartlabs.concord.dependencymanager;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\n\npublic class DependencyManagerException extends IOException {\n\n    private static final long serialVersionUID = 1L;\n\n    public DependencyManagerException(String message) {\n        super(message);\n    }\n\n    public DependencyManagerException(Throwable cause) {\n        super(cause);\n    }\n}\n"
  },
  {
    "path": "dependency-manager/src/main/java/com/walmartlabs/concord/dependencymanager/DependencyManagerRepositories.java",
    "content": "package com.walmartlabs.concord.dependencymanager;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Collections;\nimport java.util.List;\n\npublic class DependencyManagerRepositories {\n\n    private static final Logger log = LoggerFactory.getLogger(DependencyManager.class);\n\n    private static final String CFG_FILE_KEY = \"CONCORD_MAVEN_CFG\";\n\n    private static final MavenRepository MAVEN_CENTRAL = MavenRepository.builder()\n            .id(\"central\")\n            .contentType(\"default\")\n            .url(\"https://repo.maven.apache.org/maven2/\")\n            .snapshotPolicy(MavenRepositoryPolicy.builder()\n                    .enabled(false)\n                    .build())\n            .build();\n\n    private static final List<MavenRepository> DEFAULT_REPOS = Collections.singletonList(MAVEN_CENTRAL);\n\n    public static List<MavenRepository> get() {\n        Path src = getConfigFileLocation();\n        if (src == null) {\n            return DEFAULT_REPOS;\n        }\n        return readCfg(src);\n    }\n\n    public static List<MavenRepository> get(Path cfgFile) {\n        return readCfg(cfgFile);\n    }\n\n    private static Path getConfigFileLocation() {\n        String s = System.getenv(CFG_FILE_KEY);\n        if (s == null || s.trim().isEmpty()) {\n            return null;\n        }\n        return Paths.get(s);\n    }\n\n    private static List<MavenRepository> readCfg(Path src) {\n        src = src.toAbsolutePath().normalize();\n\n        if (!Files.exists(src)) {\n            log.warn(\"readCfg -> file not found: {}, using the default repos\", src);\n            return DEFAULT_REPOS;\n        }\n\n        ObjectMapper om = new ObjectMapper();\n        try (InputStream in = Files.newInputStream(src)) {\n            MavenRepositoryConfiguration cfg = om.readValue(in, MavenRepositoryConfiguration.class);\n            return cfg.repositories();\n        } catch (IOException e) {\n            throw new RuntimeException(\"Error while reading the Maven configuration file: \" + src, e);\n        }\n    }\n}\n"
  },
  {
    "path": "dependency-manager/src/main/java/com/walmartlabs/concord/dependencymanager/MavenProxy.java",
    "content": "package com.walmartlabs.concord.dependencymanager;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.eclipse.aether.repository.Proxy;\nimport org.immutables.value.Value;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonSerialize(as = ImmutableMavenProxy.class)\n@JsonDeserialize(as = ImmutableMavenProxy.class)\n@JsonIgnoreProperties(ignoreUnknown = true)\npublic interface MavenProxy {\n\n    @Value.Default\n    default String type() {\n        return Proxy.TYPE_HTTP;\n    }\n\n    String host();\n\n    int port();\n\n    static ImmutableMavenProxy.Builder builder() {\n        return ImmutableMavenProxy.builder();\n    }\n}\n"
  },
  {
    "path": "dependency-manager/src/main/java/com/walmartlabs/concord/dependencymanager/MavenRepository.java",
    "content": "package com.walmartlabs.concord.dependencymanager;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonAlias;\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.util.Map;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonSerialize(as = ImmutableMavenRepository.class)\n@JsonDeserialize(as = ImmutableMavenRepository.class)\n@JsonIgnoreProperties(ignoreUnknown = true)\npublic interface MavenRepository {\n\n    String id();\n\n    @Value.Default\n    @JsonAlias(\"layout\")\n    default String contentType() {\n        return \"default\";\n    }\n\n    String url();\n\n    @Nullable\n    @Value.Redacted\n    Map<String, String> auth();\n\n    @Value.Default\n    default MavenRepositoryPolicy snapshotPolicy() {\n        return MavenRepositoryPolicy.builder().build();\n    }\n\n    @Value.Default\n    default MavenRepositoryPolicy releasePolicy() {\n        return MavenRepositoryPolicy.builder().build();\n    }\n\n    @Nullable\n    MavenProxy proxy();\n\n    static ImmutableMavenRepository.Builder builder() {\n        return ImmutableMavenRepository.builder();\n    }\n}\n"
  },
  {
    "path": "dependency-manager/src/main/java/com/walmartlabs/concord/dependencymanager/MavenRepositoryConfiguration.java",
    "content": "package com.walmartlabs.concord.dependencymanager;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport java.util.Collections;\nimport java.util.List;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonSerialize(as = ImmutableMavenRepositoryConfiguration.class)\n@JsonDeserialize(as = ImmutableMavenRepositoryConfiguration.class)\npublic interface MavenRepositoryConfiguration {\n\n    @Value.Default\n    default List<MavenRepository> repositories() {\n        return Collections.emptyList();\n    }\n}\n"
  },
  {
    "path": "dependency-manager/src/main/java/com/walmartlabs/concord/dependencymanager/MavenRepositoryPolicy.java",
    "content": "package com.walmartlabs.concord.dependencymanager;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.eclipse.aether.repository.RepositoryPolicy;\nimport org.immutables.value.Value;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonSerialize(as = ImmutableMavenRepositoryPolicy.class)\n@JsonDeserialize(as = ImmutableMavenRepositoryPolicy.class)\npublic interface MavenRepositoryPolicy {\n\n    @Value.Default\n    default boolean enabled() {\n        return true;\n    }\n\n    @Value.Default\n    default String updatePolicy() {\n        return RepositoryPolicy.UPDATE_POLICY_NEVER;\n    }\n\n    @Value.Default\n    default String checksumPolicy() {\n        return RepositoryPolicy.CHECKSUM_POLICY_IGNORE;\n    }\n\n    static ImmutableMavenRepositoryPolicy.Builder builder() {\n        return ImmutableMavenRepositoryPolicy.builder();\n    }\n}\n"
  },
  {
    "path": "dependency-manager/src/main/java/com/walmartlabs/concord/dependencymanager/ProgressListener.java",
    "content": "package com.walmartlabs.concord.dependencymanager;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface ProgressListener {\n\n    void onRetry(int retryCount, int maxRetry, long interval, String cause);\n\n    default void onTransferFailed(String error) {\n        // do nothing\n    }\n}\n"
  },
  {
    "path": "dependency-manager/src/main/java/com/walmartlabs/concord/dependencymanager/RepositorySystemFactory.java",
    "content": "package com.walmartlabs.concord.dependencymanager;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.maven.http.ConcordHttpTransporterFactory;\nimport org.apache.maven.repository.internal.MavenRepositorySystemUtils;\nimport org.eclipse.aether.RepositorySystem;\nimport org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;\nimport org.eclipse.aether.impl.DefaultServiceLocator;\nimport org.eclipse.aether.spi.connector.RepositoryConnectorFactory;\nimport org.eclipse.aether.spi.connector.transport.TransporterFactory;\nimport org.eclipse.aether.transport.file.FileTransporterFactory;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class RepositorySystemFactory {\n\n    private static final Logger log = LoggerFactory.getLogger(RepositorySystemFactory.class);\n\n    public static RepositorySystem create() {\n        DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();\n        locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);\n        locator.addService(TransporterFactory.class, FileTransporterFactory.class);\n        locator.addService(TransporterFactory.class, ConcordHttpTransporterFactory.class);\n\n        locator.setErrorHandler(new DefaultServiceLocator.ErrorHandler() {\n            @Override\n            public void serviceCreationFailed(Class<?> type, Class<?> impl, Throwable exception) {\n                log.error(\"newMavenRepositorySystem -> service creation error: type={}, impl={}\", type, impl, exception);\n            }\n        });\n\n        return locator.getService(RepositorySystem.class);\n    }\n}\n"
  },
  {
    "path": "dependency-manager/src/main/java/com/walmartlabs/concord/dependencymanager/ResolveExceptionConverter.java",
    "content": "package com.walmartlabs.concord.dependencymanager;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ExceptionUtils;\nimport org.eclipse.aether.artifact.Artifact;\nimport org.eclipse.aether.resolution.ArtifactResolutionException;\nimport org.eclipse.aether.transfer.ArtifactNotFoundException;\nimport org.eclipse.aether.transfer.ArtifactTransferException;\n\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.UnknownHostException;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\n\npublic final class ResolveExceptionConverter {\n\n    private final Collection<URI> deps;\n\n    public ResolveExceptionConverter(URI dep) {\n        this(Collections.singletonList(dep));\n    }\n\n    public ResolveExceptionConverter(Collection<URI> deps) {\n        this.deps = deps;\n    }\n\n    public DependencyManagerException convert(Exception e) {\n        if (e instanceof IOException) {\n            return new DependencyManagerException(e);\n        }\n\n        List<Throwable> exceptions = ExceptionUtils.getExceptionList(e);\n        String cause = getCause(exceptions);\n        return new DependencyManagerException(cause);\n    }\n\n    private String getCause(List<Throwable> exceptions) {\n        ArtifactResolutionException are = exceptions.stream()\n                .filter(ex -> ex instanceof ArtifactResolutionException)\n                .map(ex -> (ArtifactResolutionException) ex)\n                .reduce((first, second) -> second)\n                .orElse(null);\n\n        if (are == null || are.getCause() == null) {\n            Throwable t = exceptions.get(0);\n            if (t instanceof FileNotFoundException) {\n                return \"not found \" + t.getMessage();\n            }\n            return t.getMessage();\n        }\n\n        if (are.getCause() instanceof ArtifactNotFoundException) {\n            return fromNotFoundException((ArtifactNotFoundException) are.getCause());\n        } else if (are.getCause() instanceof ArtifactTransferException) {\n            return fromTransferException((ArtifactTransferException) are.getCause());\n        }\n        return exceptions.get(0).getMessage();\n    }\n\n    private String fromNotFoundException(ArtifactNotFoundException ane) {\n        URI userDependency = deps.stream()\n                .filter(d -> isSameDependency(d, ane.getArtifact()))\n                .findAny().orElse(null);\n        if (userDependency == null) {\n            return ane.getMessage();\n        }\n\n        String msg = \"Could not find artifact '\" + userDependency + \"'\";\n        if (ane.getRepository() != null) {\n            msg += \" in \" + ane.getRepository().getUrl();\n        }\n        return msg;\n    }\n\n    private String fromTransferException(ArtifactTransferException ate) {\n        URI userDependency = deps.stream()\n                .filter(d -> isSameDependency(d, ate.getArtifact()))\n                .findAny().orElse(null);\n        if (userDependency == null) {\n            return ate.getMessage();\n        }\n\n        String msg = \"Could not transfer artifact '\" + userDependency + \"'\";\n        if (ate.getRepository() != null) {\n            msg += \" from \" + ate.getRepository().getUrl();\n        }\n\n        if (ate.getCause() == null) {\n            return msg;\n        }\n\n        String errorDescription;\n        if (ate.getCause() instanceof UnknownHostException) {\n            errorDescription = \"unknown host: \" + ate.getCause().getMessage();\n        } else {\n            errorDescription = ate.getCause().getMessage();\n        }\n\n        return msg + \": \" + errorDescription;\n    }\n\n    private boolean isSameDependency(URI d, Artifact a) {\n        String s = d.toString();\n        if (s.contains(a.toString())) {\n            return true;\n        }\n\n        if (!s.contains(a.getArtifactId())) {\n            return false;\n        }\n\n        if (!s.contains(a.getGroupId())) {\n            return false;\n        }\n\n        if (!s.contains(a.getVersion())) {\n            return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "dependency-manager/src/main/java/com/walmartlabs/concord/dependencymanager/RetryUtils.java",
    "content": "package com.walmartlabs.concord.dependencymanager;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.concurrent.Callable;\n\npublic final class RetryUtils {\n\n    public interface RetryStrategy {\n        boolean canRetry(Exception e);\n    }\n\n    public interface RetryListener {\n        void onRetry(int tryCount, int retryCount, long retryInterval, Exception e);\n    }\n\n    public static <T> T withRetry(int retryCount, long retryInterval, Callable<T> c,\n                                  RetryStrategy strategy, RetryListener listener) throws Exception {\n        Exception exception = null;\n        int tryCount = 1;\n        while (!Thread.currentThread().isInterrupted() && tryCount < retryCount + 1) {\n            try {\n                return c.call();\n            } catch (Exception e) {\n                if (!strategy.canRetry(e)) {\n                    throw e;\n                }\n                exception = e;\n            }\n            listener.onRetry(tryCount, retryCount, retryInterval, exception);\n            sleep(retryInterval);\n            tryCount++;\n        }\n        if (exception == null) {\n            return null; // TODO: throw exception?\n        }\n        throw exception;\n    }\n\n    private static void sleep(long t) {\n        try {\n            Thread.sleep(t);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    private RetryUtils() {\n    }\n}\n"
  },
  {
    "path": "dependency-manager/src/main/java/com/walmartlabs/concord/dependencymanager/Version.java",
    "content": "package com.walmartlabs.concord.dependencymanager;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.Properties;\n\npublic record Version(String version) {\n\n    private static final Version INSTANCE;\n\n    static {\n        Properties props = new Properties();\n\n        try (InputStream in = Version.class.getResourceAsStream(\"version.properties\")) {\n            props.load(in);\n        } catch (IOException e) {\n            throw new RuntimeException(\"version load error\", e);\n        }\n\n        String version = props.getProperty(\"version\");\n\n        INSTANCE = new Version(version);\n    }\n\n    public static String get() {\n        return INSTANCE.version();\n    }\n}\n"
  },
  {
    "path": "dependency-manager/src/test/java/com/walmartlabs/concord/dependencymanager/DependencyManagerTest.java",
    "content": "package com.walmartlabs.concord.dependencymanager;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.io.TempDir;\n\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Duration;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTimeout;\n\npublic class DependencyManagerTest {\n\n    private final String originalMavenRepoLocal = System.getProperty(\"maven.repo.local\");\n    private final String originalUserHome = System.getProperty(\"user.home\");\n\n    @AfterEach\n    public void restoreProperties() {\n        restoreProperty(\"maven.repo.local\", originalMavenRepoLocal);\n        restoreProperty(\"user.home\", originalUserHome);\n    }\n\n    @Disabled\n    @Test\n    public void testResolveDependencies() throws Exception {\n        assertTimeout(Duration.ofMillis(30000), () -> {\n            Path tmpDir = Files.createTempDirectory(\"test\");\n            URI uriA = new URI(\"mvn://com.walmartlabs.concord:concord-policy-engine:1.44.0?scope=runtime\");\n            URI uriB = new URI(\"mvn://com.walmartlabs.concord:concord-policy-engine:1.43.0?scope=runtime\");\n\n            DependencyManager m = new DependencyManager(DependencyManagerConfiguration.of(tmpDir));\n            Collection<DependencyEntity> paths = m.resolve(Arrays.asList(uriA, uriB));\n            assertEquals(46, paths.size());\n        });\n    }\n\n    @Disabled\n    @Test\n    public void testProxy() {\n        assertTimeout(Duration.ofMillis(30000), () -> {\n            Path tmpDir = Files.createTempDirectory(\"test\");\n\n            List<MavenRepository> repositories = Collections.singletonList(\n                    MavenRepository.builder()\n                            .id(\"test\")\n                            .url(\"https://repo.maven.apache.org/maven2/\")\n                            .proxy(MavenProxy.builder()\n                                    .host(\"localhost\")\n                                    .port(3128)\n                                    .build())\n                            .build()\n            );\n\n            DependencyManager m = new DependencyManager(DependencyManagerConfiguration.of(tmpDir, repositories));\n            m.resolveSingle(new URI(\"mvn://com.walmartlabs.concord:concord-sdk:1.54.0\"));\n        });\n    }\n\n    @Disabled\n    @Test\n    public void testOfflineMode() {\n        assertTimeout(Duration.ofMillis(30000), () -> {\n            Path tmpDir = Files.createTempDirectory(\"test\");\n\n            List<MavenRepository> repositories = Collections.singletonList(\n                    MavenRepository.builder()\n                            .id(\"test\")\n                            .url(\"https://repo.maven.apache.org/maven2/\")\n                            .proxy(MavenProxy.builder()\n                                    .host(\"localhost\")\n                                    .port(3128)\n                                    .build())\n                            .build()\n            );\n\n            DependencyManager m = new DependencyManager(DependencyManagerConfiguration.builder()\n                    .cacheDir(tmpDir)\n                    .repositories(repositories)\n                    .offlineMode(true)\n                    .build());\n            m.resolveSingle(new URI(\"mvn://com.walmartlabs.concord:concord-sdk:1.54.0\"));\n        });\n    }\n\n    @Test\n    @Disabled\n    public void testLatest() {\n        assertTimeout(Duration.ofMillis(30000), () -> {\n            Path tmpDir = Files.createTempDirectory(\"test\");\n            URI uriA = new URI(\"mvn://com.walmartlabs.concord:concord-sdk:2.8.0\");\n            URI uriB = new URI(\"mvn://com.walmartlabs.concord:concord-policy-engine:LATEST\");\n            URI uriC = new URI(\"mvn://com.walmartlabs.concord:concord-agent:LATEST\");\n            List<MavenRepository> repositories = Collections.singletonList(\n                    MavenRepository.builder()\n                            .id(\"test\")\n                            .url(\"https://repo.maven.apache.org/maven2/\")\n                            .build()\n            );\n\n            DependencyManager m = new DependencyManager(DependencyManagerConfiguration\n                    .builder()\n                    .repositories(repositories)\n                    .cacheDir(tmpDir)\n                    .build());\n            Collection<DependencyEntity> paths = m.resolve(Arrays.asList(uriA, uriB));\n\n            assertEquals(\"2.11.1\", paths.stream()\n                    .filter(a -> a.getArtifact().getGroupId().equals(\"com.walmartlabs.concord\")\n                            && a.getArtifact().getArtifactId().equals(\"concord-policy-engine\"))\n                    .findFirst().get().getArtifact().getVersion());\n\n            DependencyEntity deps = m.resolveSingle(uriC);\n            assertEquals(\"2.11.1\", deps.getArtifact().getVersion());\n        });\n    }\n\n    @Test\n    public void shouldPreferMavenRepoLocal(@TempDir Path tmpDir) {\n        Path explicitRepo = tmpDir.resolve(\"custom-repository\");\n        System.setProperty(\"maven.repo.local\", explicitRepo.toString());\n        System.setProperty(\"user.home\", tmpDir.resolve(\"home\").toString());\n\n        assertEquals(explicitRepo.toAbsolutePath().normalize(), DependencyManager.resolveLocalCacheDir());\n    }\n\n    @Test\n    public void shouldFallbackToUserHomeM2Repository(@TempDir Path tmpDir) {\n        System.clearProperty(\"maven.repo.local\");\n\n        Path userHome = tmpDir.resolve(\"home\");\n        System.setProperty(\"user.home\", userHome.toString());\n\n        assertEquals(userHome.resolve(\".m2/repository\").toAbsolutePath().normalize(),\n                DependencyManager.resolveLocalCacheDir());\n    }\n\n    private static void restoreProperty(String name, String value) {\n        if (value == null) {\n            System.clearProperty(name);\n        } else {\n            System.setProperty(name, value);\n        }\n    }\n}\n"
  },
  {
    "path": "dependency-manager/src/test/resources/__files/repository/concord-cli-1.82.0.pom",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>com.walmartlabs.concord</groupId>\n    <artifactId>concord-cli</artifactId>\n    <packaging>takari-jar</packaging>\n    <version>1.0.6</version>\n\n    <dependencies>\n        <dependency>\n            <groupId>net.minidev</groupId>\n            <artifactId>json-smart</artifactId>\n            <version>[1.3.2,2.4.2]</version>\n        </dependency>\n    </dependencies>\n\n    <repositories>\n        <repository>\n            <id>shibboleth-repo</id>\n            <name>Shibboleth repo for OpenSAML dependencies</name>\n            <url>http://192.168.1.116:8090/</url>\n        </repository>\n    </repositories>\n\n</project>\n"
  },
  {
    "path": "dependency-manager/src/test/resources/logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] [%-5level] %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "docker-images/agent/oss/debian/Dockerfile",
    "content": "ARG docker_namespace=walmartlabs\nARG concord_version=latest\nARG concord_ansible_image=${docker_namespace}/concord-ansible:${concord_version}\nARG DEBIAN_FRONTEND=noninteractive\n\nFROM ${concord_ansible_image}\nLABEL maintainer=\"amith.k.b@walmartlabs.com\"\n\nENV DOCKER_HOST=tcp://dind:2375\nENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt\n\nUSER root\n\nRUN DEBIAN_FRONTEND=${DEBIAN_FRONTEND} apt-get -y install \\\n    ca-certificates \\\n    gnupg \\\n    lsb-release\n\nRUN DEBIAN_FRONTEND=${DEBIAN_FRONTEND} mkdir -p /etc/apt/keyrings && \\\n    curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg\n    \nRUN echo \\\n      \"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \\\n      $(lsb_release -cs) stable\" | tee /etc/apt/sources.list.d/docker.list > /dev/null && \\\n      apt-get update\n      \nRUN DEBIAN_FRONTEND=${DEBIAN_FRONTEND} apt-get -y install \\\n    uuid \\\n    docker-ce-cli \\\n    && apt-get clean\n\nCOPY --chown=concord:concord target/deps/ /home/concord/.m2/repository\nADD --chown=concord:concord target/dist/agent.tar.gz /opt/concord/agent\n\nUSER concord\nCMD [\"bash\", \"/opt/concord/agent/start.sh\"]\n"
  },
  {
    "path": "docker-images/agent/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.docker</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-agent</artifactId>\n    <packaging>pom</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <properties>\n        <agent.image>${docker.namespace}/concord-agent</agent.image>\n        <docker.tagbase>${agent.image}</docker.tagbase>\n        <docker.file>oss/debian/Dockerfile</docker.file>\n        <docker.allowPull>false</docker.allowPull>\n    </properties>\n\n    <!-- default dependencies, will be copied into the agent's local .m2 -->\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-targetplatform</artifactId>\n            <version>${project.version}</version>\n            <type>pom</type>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-client2</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>concord-tasks</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>slack-tasks</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>http-tasks</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n\n        <!-- dependency tree fix for mvnd -->\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-agent</artifactId>\n            <version>${project.version}</version>\n            <type>pom</type>\n            <scope>test</scope>\n            <exclusions>\n                <exclusion>\n                    <groupId>*</groupId>\n                    <artifactId>*</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-dependency-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>copy-dist</id>\n                        <phase>prepare-package</phase>\n                        <goals>\n                            <goal>copy</goal>\n                        </goals>\n                        <configuration>\n                            <artifactItems>\n                                <artifactItem>\n                                    <groupId>com.walmartlabs.concord</groupId>\n                                    <artifactId>concord-agent</artifactId>\n                                    <version>${project.version}</version>\n                                    <type>tar.gz</type>\n                                    <classifier>dist</classifier>\n                                    <outputDirectory>${project.build.directory}/dist</outputDirectory>\n                                    <destFileName>agent.tar.gz</destFileName>\n                                </artifactItem>\n                            </artifactItems>\n                        </configuration>\n                    </execution>\n                    <execution>\n                        <id>copy-default-deps</id>\n                        <phase>prepare-package</phase>\n                        <goals>\n                            <goal>copy-dependencies</goal>\n                        </goals>\n                        <configuration>\n                            <useRepositoryLayout>true</useRepositoryLayout>\n                            <copyPom>true</copyPom>\n                            <addParentPoms>true</addParentPoms>\n                            <outputDirectory>${project.build.directory}/deps</outputDirectory>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>exec-maven-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>docker-build</id>\n                        <phase>package</phase>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "docker-images/agent-operator/oss/Dockerfile",
    "content": "ARG from_image=gcr.io/distroless/java17\nFROM $from_image\n\nCOPY target/operator.jar /operator.jar\nCMD [\"/operator.jar\", \"-Xmx128m\"]\n"
  },
  {
    "path": "docker-images/agent-operator/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.docker</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-agent-operator</artifactId>\n    <packaging>pom</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <properties>\n        <agent.image>${docker.namespace}/concord-agent-operator</agent.image>\n        <docker.tagbase>${agent.image}</docker.tagbase>\n        <docker.file>oss/Dockerfile</docker.file>\n        <docker.allowPull>false</docker.allowPull>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.k8s</groupId>\n            <artifactId>concord-agent-operator</artifactId>\n            <version>${project.version}</version>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-dependency-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>copy-dist</id>\n                        <phase>prepare-package</phase>\n                        <goals>\n                            <goal>copy</goal>\n                        </goals>\n                        <configuration>\n                            <artifactItems>\n                                <artifactItem>\n                                    <groupId>com.walmartlabs.concord.k8s</groupId>\n                                    <artifactId>concord-agent-operator</artifactId>\n                                    <version>${project.version}</version>\n                                    <type>jar</type>\n                                    <classifier>uber</classifier>\n                                    <outputDirectory>${project.build.directory}</outputDirectory>\n                                    <destFileName>operator.jar</destFileName>\n                                </artifactItem>\n                            </artifactItems>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>exec-maven-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>docker-build</id>\n                        <phase>package</phase>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "docker-images/ansible/galaxy_requirements.yml",
    "content": "---\ncollections:\n  - name: \"community.general\"\n    version: \"7.0.1\"\n  - name: \"ansible.windows\"\n    version: \"1.14.0\"\n  - name: \"ansible.posix\"\n    version: \"1.5.4\"\n  - name: \"community.windows\"\n    version: \"1.13.0\"\n  - name: \"kubernetes.core\"\n    version: \"2.4.0\"\n  - name: \"community.crypto\"\n    version: \"2.13.1\"\n  - name: \"community.mysql\"\n    version: \"3.7.1\"\n  - name: \"openstack.cloud\"\n    version: \"2.1.0\"\n  - name: \"community.postgresql\"\n    version: \"2.4.1\"\n  - name: \"community.docker\"\n    version: \"3.12.1\"\n  - name: \"ansible.netcommon\"\n    version: \"5.1.1\"\n  - name: \"google.cloud\"\n    version: \"1.1.3\"\n  - name: \"chocolatey.chocolatey\"\n    version: \"1.4.0\"\n"
  },
  {
    "path": "docker-images/ansible/oss/debian/Dockerfile",
    "content": "ARG docker_namespace=walmartlabs\nARG concord_version=latest\nARG concord_base_image=${docker_namespace}/concord-base:${concord_version}\nARG DEBIAN_FRONTEND=noninteractive\n\nFROM ${concord_base_image}\nLABEL maintainer=\"amith.k.b@walmartlabs.com\"\n\nENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt\n\nRUN mkdir -p /workspace\nWORKDIR /workspace\n\nCOPY galaxy_requirements.yml galaxy_requirements.yml\n\nRUN DEBIAN_FRONTEND=${DEBIAN_FRONTEND} apt-get -y install \\\n    gcc \\\n    g++ \\\n    libkrb5-dev \\\n    krb5-user \\\n    libpam-krb5 \\\n    libffi-dev \\\n    openssh-client \\\n    libssl-dev \\\n    rsync \\\n    util-linux \\\n    && apt-get clean \n\nENV PATH=/usr/local/bin/concord_venv/bin:${PATH}\nENV VIRTUAL_ENV=/usr/local/bin/concord_venv\n\nRUN umask 0022 && \\\n    pip3 install --no-cache-dir --upgrade --break-system-packages --ignore-installed \\\n        \"cryptography<=3.4.8\" \\\n        \"ansible-core>=2.14,<2.15\" \\\n        \"Appium-Python-Client<1.0\" \\\n        \"openshift==0.13.2\" \\\n        \"jinja2<=3.1.0\" \\\n        boto3 \\\n        botocore \\\n        bzt \\\n        docker \\\n        kerberos \\\n        kubernetes \\\n        pyyaml \\\n        pbr \\\n        pyvmomi \\\n        \"pywinrm>=0.4.3\" \\\n        requests_kerberos \\\n        urllib3 \\\n        ujson \\\n        virtualenv\n\nRUN umask 0022 && \\\n    ansible-galaxy collection install -p /usr/share/ansible/collections -r galaxy_requirements.yml\n\nUSER concord\n"
  },
  {
    "path": "docker-images/ansible/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.docker</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-ansible</artifactId>\n    <packaging>pom</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <properties>\n        <ansible.image>${docker.namespace}/concord-ansible</ansible.image>\n        <docker.tagbase>${ansible.image}</docker.tagbase>\n        <docker.file>oss/debian/Dockerfile</docker.file>\n        <docker.allowPull>false</docker.allowPull>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.docker</groupId>\n            <artifactId>concord-base</artifactId>\n            <version>${project.version}</version>\n            <type>pom</type>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>exec-maven-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>docker-build</id>\n                        <phase>package</phase>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "docker-images/base/get_arch.sh",
    "content": "#!/usr/bin/env bash\n\n## Mapping between different ways to describe architectures\n## (uname -m) vs Docker's way\n\nset -e\n\ncase \"$(uname -m)\" in\n  \"x86_64\")\n    echo \"amd64\"\n    ;;\n  \"aarch64\")\n    echo \"arm64\"\n    ;;\n  *)\n    >&2 echo \"Unsupported architecture $(uname -m): $(uname -a)\"\n    exit 1;\n    ;;\nesac\n"
  },
  {
    "path": "docker-images/base/get_jdk_url.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\nJDK_17_AMD64=\"https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.15%2B6/OpenJDK17U-jdk_x64_linux_hotspot_17.0.15_6.tar.gz\"\nJDK_17_ARM64=\"https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.15%2B6/OpenJDK17U-jdk_aarch64_linux_hotspot_17.0.15_6.tar.gz\"\nJDK_21_AMD64=\"https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.7%2B6/OpenJDK21U-jdk_x64_linux_hotspot_21.0.7_6.tar.gz\"\nJDK_21_ARM64=\"https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.7%2B6/OpenJDK21U-jdk_aarch64_linux_hotspot_21.0.7_6.tar.gz\"\n\ncase \"${JDK_VERSION}-${TARGETARCH}\" in\n  \"17-amd64\")\n    echo ${JDK_17_AMD64}\n    ;;\n  \"17-arm64\")\n    echo ${JDK_17_ARM64}\n    ;;\n  \"21-amd64\")\n    echo ${JDK_21_AMD64}\n    ;;\n  \"21-arm64\")\n    echo ${JDK_21_ARM64}\n    ;;\n  *)\n    >&2 echo \"Unsupported JDK ${JDK_VERSION}-${TARGETARCH}\"\n    exit 1;\n    ;;\nesac\n"
  },
  {
    "path": "docker-images/base/oss/debian/Dockerfile",
    "content": "ARG DEBIAN_FRONTEND=noninteractive\n\nFROM library/debian:12\nLABEL maintainer=\"amith.k.b@walmartlabs.com\"\n\nENTRYPOINT [\"/usr/local/bin/concord_venv/bin/dumb-init\", \"--\"]\n\nRUN apt-get update && \\\n    apt-get -y upgrade && \\\n    apt-get -y install openssh-client libltdl-dev wget unzip diffutils strace git gdebi-core \\\n               python3 python3-pip python-is-python3 coreutils locales locales-all curl bash && \\\n    apt-get clean && \\\n    pip3 install --no-cache-dir --break-system-packages dumb-init virtualenv\n    \n\nADD --chmod=755 ./get_jdk_url.sh /tmp/\nADD --chmod=755 ./get_arch.sh /tmp/\nARG jdk_version\nENV JDK_VERSION=${jdk_version}\nRUN export DEFAULT_TARGETARCH=$(/tmp/get_arch.sh); \\\n    export TARGETARCH=${TARGETARCH:-${DEFAULT_TARGETARCH}}; \\\n    curl --location --output /tmp/jdk.tar.gz $(/tmp/get_jdk_url.sh) && \\\n    mkdir /opt/jdk && \\\n    tar xpf /tmp/jdk.tar.gz --strip 1 -C /opt/jdk && \\\n    rm /tmp/jdk.tar.gz\n\nENV JAVA_HOME=/opt/jdk\nENV PATH=\"${JAVA_HOME}/bin:${PATH}\"\nENV LC_CTYPE=en_US.UTF-8\nENV LANG=en_US.UTF-8\n\nRUN virtualenv /usr/local/bin/concord_venv && \\\n    /usr/local/bin/concord_venv/bin/pip3 --no-cache-dir install dumb-init\n\nRUN groupadd -g 456 concord && \\\n    useradd --no-log-init -u 456 -g concord -m -s /sbin/nologin concord && \\\n    echo \"[safe]\\n\\tdirectory = *\\n\" > ~concord/.gitconfig && \\\n    chown concord:concord ~concord/.gitconfig\n\n# Point /bin/sh to bash from dash\nRUN echo \"dash dash/sh boolean false\" | debconf-set-selections && \\\n    DEBIAN_FRONTEND=${DEBIAN_FRONTEND} dpkg-reconfigure dash\n"
  },
  {
    "path": "docker-images/base/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.docker</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-base</artifactId>\n    <packaging>pom</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <properties>\n        <base.image>${docker.namespace}/concord-base</base.image>\n        <docker.tagbase>${base.image}</docker.tagbase>\n        <docker.file>oss/debian/Dockerfile</docker.file>\n        <docker.allowPull>false</docker.allowPull>\n    </properties>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>exec-maven-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>docker-build</id>\n                        <phase>package</phase>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "docker-images/compose/README.md",
    "content": "# Running Concord with Docker Compose\n\nRequires [docker-compose](https://docs.docker.com/compose/) >= 1.26.x.\n\n## Usage\n\n```\n$ docker-compose up\n```\n\nThe Server generates the default admin API token on the first start up - check\nthe logs. Use http://localhost:8001/#/login?useApiKey=true to access the\nConcord UI with an API key.\n"
  },
  {
    "path": "docker-images/compose/concord.conf",
    "content": "concord-server {\n    db {\n        url = \"jdbc:postgresql://concord-db:5432/postgres\"\n        appPassword = \"q1q1q1q1\"\n        inventoryPassword = \"q1q1q1q1\"\n\n        changeLogParameters {\n            defaultAgentToken = \"cTJxMnEycTI=\"  # base64 of 'q2q2q2q2'\n        }\n    }\n\n    secretStore {\n        serverPassword = \"cTE=\"\n        secretStoreSalt = \"cTE=\"\n        projectSecretSalt = \"cTE=\"\n    }\n}\n\nconcord-agent {\n    server {\n        apiBaseUrl = \"http://concord-server:8001\"\n        websocketUrl = \"ws://concord-server:8001/websocket\"\n        apiKey = \"cTJxMnEycTI=\"\n    }\n}\n"
  },
  {
    "path": "docker-images/compose/docker-compose.yml",
    "content": "version: \"3.7\"\nservices:\n  concord-db:\n    image: \"library/postgres:14.21-alpine\"\n    environment:\n      POSTGRES_PASSWORD: \"q1q1q1q1\"\n  concord-server:\n    image: \"walmartlabs/concord-server\"\n    depends_on:\n      - \"concord-db\"\n    ports:\n      - \"8001:8001\"\n    volumes:\n      - \"./concord.conf:/concord.conf:ro\"\n    environment:\n      CONCORD_CFG_FILE: \"/concord.conf\"\n  concord-dind:\n    image: \"docker:dind\"\n    privileged: true\n    volumes:\n      - \"agent-tmp:/tmp\"\n    command: \"dockerd -H tcp://0.0.0.0:6666 --bip=10.11.13.1/24\"\n  concord-agent:\n    image: \"walmartlabs/concord-agent\"\n    depends_on:\n      - \"concord-server\"\n      - \"concord-dind\"\n    environment:\n      CONCORD_CFG_FILE: \"/concord.conf\"\n      CONCORD_DOCKER_LOCAL_MODE: \"false\"\n      DOCKER_HOST: \"tcp://concord-dind:6666\"\n    volumes:\n      - \"./concord.conf:/concord.conf:ro\"\n      - \"agent-tmp:/tmp\"\nvolumes:\n  agent-tmp: {}\n"
  },
  {
    "path": "docker-images/docker-bake.hcl",
    "content": "variable \"DOCKER_NAMESPACE\" {\n  default = \"walmartlabs\"\n}\n\nvariable \"DOCKER_TAG\" {\n  default = \"latest\"\n}\n\nvariable \"JDK_VERSION\" {\n  default = \"17\"\n}\n\ngroup \"default\" {\n  targets = [\n    \"concord-base\",\n    \"concord-ansible\",\n    \"concord-agent\",\n    \"concord-server\",\n    \"concord-agent-operator\",\n  ]\n}\n\ntarget \"_common\" {\n  platforms = [\"linux/amd64\"]\n}\n\ntarget \"concord-base\" {\n  inherits = [\"_common\"]\n  context = \"./base\"\n  dockerfile = \"oss/debian/Dockerfile\"\n  tags = [\"${DOCKER_NAMESPACE}/concord-base:${DOCKER_TAG}\"]\n  args = {\n    jdk_version = JDK_VERSION\n  }\n}\n\ntarget \"concord-ansible\" {\n  inherits = [\"_common\"]\n  context = \"./ansible\"\n  dockerfile = \"oss/debian/Dockerfile\"\n  tags = [\"${DOCKER_NAMESPACE}/concord-ansible:${DOCKER_TAG}\"]\n  args = {\n    concord_base_image = \"concord-base\"\n    concord_version = DOCKER_TAG\n    docker_namespace = DOCKER_NAMESPACE\n  }\n  contexts = {\n    \"concord-base\" = \"target:concord-base\"\n  }\n}\n\ntarget \"concord-agent\" {\n  inherits = [\"_common\"]\n  context = \"./agent\"\n  dockerfile = \"oss/debian/Dockerfile\"\n  tags = [\"${DOCKER_NAMESPACE}/concord-agent:${DOCKER_TAG}\"]\n  args = {\n    concord_ansible_image = \"concord-ansible\"\n    concord_version = DOCKER_TAG\n    docker_namespace = DOCKER_NAMESPACE\n  }\n  contexts = {\n    \"concord-ansible\" = \"target:concord-ansible\"\n  }\n}\n\ntarget \"concord-server\" {\n  inherits = [\"_common\"]\n  context = \"./server\"\n  dockerfile = \"oss/Dockerfile\"\n  tags = [\"${DOCKER_NAMESPACE}/concord-server:${DOCKER_TAG}\"]\n  args = {\n    concord_base_image = \"concord-base\"\n    concord_version = DOCKER_TAG\n    docker_namespace = DOCKER_NAMESPACE\n  }\n  contexts = {\n    \"concord-base\" = \"target:concord-base\"\n  }\n}\n\ntarget \"concord-agent-operator\" {\n  inherits = [\"_common\"]\n  context = \"./agent-operator\"\n  dockerfile = \"oss/Dockerfile\"\n  tags = [\"${DOCKER_NAMESPACE}/concord-agent-operator:${DOCKER_TAG}\"]\n}\n"
  },
  {
    "path": "docker-images/mvn.json",
    "content": "{\n  \"repositories\": [\n    {\n      \"id\": \"host\",\n      \"url\": \"file:///host/.m2/repository\",\n      \"snapshotPolicy\": {\n        \"enabled\": true,\n        \"updatePolicy\": \"always\"\n      }\n    },\n    {\n      \"id\": \"central\",\n      \"url\": \"https://repo.maven.apache.org/maven2/\"\n    }\n  ]\n}\n"
  },
  {
    "path": "docker-images/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <groupId>com.walmartlabs.concord.docker</groupId>\n    <artifactId>parent</artifactId>\n    <packaging>pom</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <modules>\n        <module>base</module>\n        <module>ansible</module>\n        <module>agent</module>\n        <module>server</module>\n        <module>agent-operator</module>\n    </modules>\n\n    <properties>\n        <docker.skip>true</docker.skip>\n        <docker.maintainer>ibodrov@gmail.com</docker.maintainer>\n        <docker.namespace>walmartlabs</docker.namespace>\n        <docker.allowPull>true</docker.allowPull>\n        <concord.baseDir>/opt/concord</concord.baseDir>\n        <image.platforms>linux/amd64</image.platforms>\n    </properties>\n\n    <build>\n        <pluginManagement>\n            <plugins>\n                <plugin>\n                    <groupId>org.codehaus.mojo</groupId>\n                    <artifactId>exec-maven-plugin</artifactId>\n                    <executions>\n                        <execution>\n                            <id>docker-build</id>\n                            <goals>\n                                <goal>exec</goal>\n                            </goals>\n                            <configuration>\n                                <skip>${docker.skip}</skip>\n                                <executable>docker</executable>\n                                <workingDirectory>${project.basedir}</workingDirectory>\n                                <arguments>\n                                    <argument>buildx</argument>\n                                    <argument>build</argument>\n\n                                    <argument>-t</argument>\n                                    <argument>${docker.tagbase}:latest</argument>\n                                    <argument>-t</argument>\n                                    <argument>${docker.tagbase}:${project.version}</argument>\n\n                                    <argument>-f</argument>\n                                    <argument>${docker.file}</argument>\n\n                                    <argument>--build-arg=docker_namespace=${docker.namespace}</argument>\n                                    <argument>--build-arg=concord_version=${project.version}</argument>\n                                    <argument>--build-arg=concord_base_image=${docker.namespace}/concord-base:${project.version}</argument>\n                                    <argument>--build-arg=concord_ansible_image=${docker.namespace}/concord-ansible:${project.version}</argument>\n                                    <argument>--build-arg=jdk_version=${jdk.version}</argument>\n\n                                    <argument>--network</argument>\n                                    <argument>host</argument>\n\n                                    <argument>--platform=${image.platforms}</argument>\n\n                                    <argument>--load</argument>\n                                    <argument>--pull=${docker.allowPull}</argument>\n\n                                    <argument>.</argument>\n                                </arguments>\n                            </configuration>\n                        </execution>\n                    </executions>\n                </plugin>\n            </plugins>\n        </pluginManagement>\n    </build>\n\n    <profiles>\n        <profile>\n            <id>docker</id>\n            <properties>\n                <docker.skip>false</docker.skip>\n            </properties>\n        </profile>\n        <profile>\n            <id>jdk17-aarch64</id>\n            <properties>\n                <image.platforms>linux/arm64</image.platforms>\n            </properties>\n        </profile>\n        <profile>\n            <id>jdk21-aarch64</id>\n            <properties>\n                <image.platforms>linux/arm64</image.platforms>\n            </properties>\n        </profile>\n    </profiles>\n</project>\n"
  },
  {
    "path": "docker-images/push.sh",
    "content": "#!/bin/bash\n\nTAG=\"$1\"\n\ndocker push walmartlabs/concord-server:${TAG}\ndocker push walmartlabs/concord-ansible:${TAG}\ndocker push walmartlabs/concord-agent:${TAG}\ndocker push walmartlabs/concord-agent-operator:${TAG}\n"
  },
  {
    "path": "docker-images/run_dev.sh",
    "content": "#!/bin/bash\n\nBASE_DIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\n\nif [[ -z ${VERSION} ]]; then\n    VERSION=\"latest\"\nfi\necho \"VERSION: ${VERSION}\"\n\nif [[ -z ${DOCKER_PREFIX} ]]; then\n    DOCKER_PREFIX=\"walmartlabs\"\nfi\necho \"DOCKER_PREFIX: ${DOCKER_PREFIX}\"\n\nif [[ -z ${CONCORD_CFG_FILE} ]]; then\n    CONCORD_CFG_FILE=${BASE_DIR}/server.conf\nfi\necho \"CONCORD_CFG_FILE: ${CONCORD_CFG_FILE}\"\n\nif grep 'AD_' \"${CONCORD_CFG_FILE}\"; then\n    echo \"To start, enter your AD/LDAP credentials into ${CONCORD_CFG_FILE}\"\n    exit 1\nfi\n\necho \"Removing old containers...\"\ndocker rm -f db dind agent server > /dev/null\n\necho \"Cleaning tmp volume...\"\ndocker volume rm concordAgentTmp\n\ndocker run -d \\\n--name db \\\n-e 'POSTGRES_PASSWORD=q1' \\\n-e 'PGDATA=/var/lib/postgresql/data/pgdata' \\\n--mount source=concordDB,target=/var/lib/postgresql/data \\\n-p 5432:5432 \\\n\"library/postgres:14.21-alpine\"\n\ndocker run -d \\\n--link db \\\n--name server \\\n-p 8001:8001 \\\n-v \"/tmp:/tmp\" \\\n-v \"${HOME}/.m2/repository:/host/.m2/repository:ro\" \\\n-v \"${BASE_DIR}/mvn.json:/opt/concord/conf/mvn.json:ro\" \\\n-v \"${CONCORD_CFG_FILE}:${CONCORD_CFG_FILE}:ro\" \\\n-e \"CONCORD_CFG_FILE=${CONCORD_CFG_FILE}\" \\\n-e 'DB_URL=jdbc:postgresql://db:5432/postgres' \\\n-e 'NODEROSTER_DB_URL=jdbc:postgresql://db:5432/postgres' \\\n-e 'CONCORD_MAVEN_CFG=/opt/concord/conf/mvn.json' \\\n\"${DOCKER_PREFIX}/concord-server:${VERSION}\"\n\n# wait for the server to start\necho -n \"Waiting for the server to start\"\nuntil $(curl --output /dev/null --silent --head --fail \"http://localhost:8001/api/v1/server/ping\"); do\n    printf '.'\n    sleep 1\ndone\necho \"done!\"\n\ndocker run -d \\\n--name dind \\\n--privileged \\\n--mount source=concordAgentTmp,target=/tmp \\\ndocker:dind \\\ndockerd -H tcp://0.0.0.0:6666 --bip=10.11.13.1/24\n\ndocker run -d \\\n--name agent \\\n--link server \\\n--link dind \\\n--mount source=concordAgentTmp,target=/tmp \\\n-v \"${HOME}/.m2/repository:/host/.m2/repository:ro\" \\\n-v \"${BASE_DIR}/mvn.json:/opt/concord/conf/mvn.json:ro\" \\\n-v \"${CONCORD_CFG_FILE}:${CONCORD_CFG_FILE}:ro\" \\\n-e \"CONCORD_CFG_FILE=${CONCORD_CFG_FILE}\" \\\n-e 'CONCORD_MAVEN_CFG=/opt/concord/conf/mvn.json' \\\n-e 'CONCORD_DOCKER_LOCAL_MODE=false' \\\n-e 'SERVER_API_BASE_URL=http://server:8001' \\\n-e 'SERVER_WEBSOCKET_URL=ws://server:8001/websocket' \\\n-e 'DOCKER_HOST=tcp://dind:6666' \\\n\"${DOCKER_PREFIX}/concord-agent:${VERSION}\"\n"
  },
  {
    "path": "docker-images/server/oss/Dockerfile",
    "content": "ARG docker_namespace=walmartlabs\nARG concord_version=latest\nARG concord_base_image=$docker_namespace/concord-base:$concord_version\n\nFROM $concord_base_image\nLABEL maintainer=\"ibodrov@gmail.com\"\n\nEXPOSE 8001\n\nADD --chown=concord:concord target/dist/server.tar.gz /opt/concord/server/\n\nRUN mkdir -p /opt/concord/server/logs && \\\n    chown -R concord:concord /opt/concord/server/logs\n\nUSER concord\n\nCMD [\"bash\", \"/opt/concord/server/start.sh\"]\n"
  },
  {
    "path": "docker-images/server/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.docker</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-server</artifactId>\n    <packaging>pom</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <properties>\n        <server.image>${docker.namespace}/concord-server</server.image>\n        <docker.tagbase>${server.image}</docker.tagbase>\n        <docker.file>oss/Dockerfile</docker.file>\n        <docker.allowPull>false</docker.allowPull>\n    </properties>\n\n    <dependencies>\n        <!-- dependency tree fix for mvnd -->\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server</artifactId>\n            <version>${project.version}</version>\n            <type>pom</type>\n            <scope>test</scope>\n            <exclusions>\n                <exclusion>\n                    <groupId>*</groupId>\n                    <artifactId>*</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-dependency-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>copy-dist</id>\n                        <phase>prepare-package</phase>\n                        <goals>\n                            <goal>copy</goal>\n                        </goals>\n                        <configuration>\n                            <outputDirectory>${project.build.directory}/dist</outputDirectory>\n                            <artifactItems>\n                                <artifactItem>\n                                    <groupId>com.walmartlabs.concord.server</groupId>\n                                    <artifactId>concord-server</artifactId>\n                                    <version>${project.version}</version>\n                                    <type>tar.gz</type>\n                                    <classifier>dist</classifier>\n                                    <destFileName>server.tar.gz</destFileName>\n                                </artifactItem>\n                            </artifactItems>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>exec-maven-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>docker-build</id>\n                        <phase>package</phase>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "docker-images/server.conf",
    "content": "concord-server {\n    db {\n        appPassword = \"q1\"\n        inventoryPassword = \"q1\"\n\n        changeLogParameters {\n            defaultAgentToken = \"cTJxMnEycTI=\"  # base64 of 'q2q2q2q2'\n        }\n    }\n\n    secretStore {\n        # base64 of 'q1' \n        serverPassword = \"cTE=\"\n        secretStoreSalt = \"cTE=\"\n        projectSecretSalt = \"cTE=\"\n    }\n}\n\nconcord-agent {\n    server {\n        apiKey = \"cTJxMnEycTI=\"\n    }\n}\n"
  },
  {
    "path": "docker-images/stop.sh",
    "content": "#!/bin/bash\n\ndocker rm -f agent server dind\n"
  },
  {
    "path": "examples/README.md",
    "content": "# Examples\n\n## Running\n\nGenerally, examples can be executed using\n[Concord Command-Line Interface](./../cli). Examples that use Concord Forms\nrequire a running concord-server. Some examples may require external\ndependencies such as Ansible, JIRA and others to be installed:\n\n```\n$ cd ansible\n$ concord run\nStarting...\n16:20:00.000 [main] Using a playbook: playbook/hello.yml\n16:20:00.020 [main] ANSIBLE: Can't find ansible-playbook binary in $PATH. Install a local copy or use 'dockerImage' or 'virtualenv' options.\n```\n\n## Basic\n\n* [ansible](ansible) - running an Ansible playbook from a workflow process;\n* [datetime](datetime) - how to work with dates and date/time formats;\n* [forms](forms) - using basic forms;\n* [git](git) - how to clone a GIT repository;\n* [hello_initiator](hello_initiator) - a simple example that shows how to use automatically provided variables such as `${initiator}`;\n* [hello_world2](hello_world2) - how to send external parameters;\n* [hello_world](hello_world) - a simple example demonstrating how to pass a variable to a process;\n* [http](http) - a simple example demonstrating how to call restful endpoints\n* [in_variables](in_variables) - how to use IN-variables when calling a flow;\n* [jira](jira) - how to create an issue in JIRA;\n* [ldap](ldap) - how to query an AD/LDAP server;\n* [loglevel](loglevel) - how to use different logging levels;\n* [long_running](long_running) - a simple example of a long-running process using `sleep` task;\n* [loops](loops) - how to iterate a collection;\n* [multiple_flows](multiple_flows) - multiple flows in a single YAML file;\n* [out](out) - how to use process OUT variables;\n* [out_groovy](out_groovy) - using OUT variables coming from Groovy scripts;\n* [parsing_yaml_json](parsing_yaml_json) - how to work with YAML and JSON files;\n* [project_file](project_file) - basic usage of a `concord.yml` project file;\n* [slack](slack) - sending a message to a Slack channel;\n* [slackChannel](slackChannel) - how to manage Slack channels;\n* [smtp](smtp) - using SMTP task.\n\n## Intermediate\n\n* [ansible_docker](ansible_docker) - using a custom Ansible Docker image to run a playbook;\n* [ansible_dynamic_inventory](ansible_dynamic_inventory) - using dynamic inventory scripts in Ansible;\n* [ansible_form](ansible_form) - using forms and Ansible in a single flow;\n* [ansible_form_as_inventory](ansible_form_as_inventory) - using forms to specify an Ansible inventory;\n* [ansible_kerberos](ansible_kerberos) - how to use Kerberos with Ansible;\n* [ansible_limit](ansible_limit) - how to use Ansible's limit/retry files;\n* [ansible_project](ansible_project) - an example of creating an running an Ansible project;\n* [ansible_remote](ansible_remote) - running an Ansible playbook an a remote host;\n* [ansible_retry](ansible_retry) - how to automatically retry Ansible deployment for failed hosts;\n* [ansible_stats](ansible_stats) - shows how to get Ansible deployment stats back from a playbook run;\n* [ansible_template](ansible_template) - running an Ansible playbook using the Ansible template;\n* [ansible_vault](ansible_vault) - using Ansible Vault to store encrypted data;\n* [ansible_windows](ansible_windows) - how to use Ansible with Windows hosts;\n* [custom_form](custom_form) - using forms with custom HTML/CSS and a templating library;\n* [custom_form_basic](custom_form_basic) - a basic example of a custom form;\n* [docker](docker) - running a docker image with custom environment and arguments;\n* [docker_simple](docker_simple) - running a simple command inside of a docker container;\n* [error_handling](error_handling) - how to handle failures;\n* [external_script](external_script) - calling an external JavaScript file;\n* [form_and_long_process](form_and_long_process) - how to deal with long \"background\" processes;\n* [forms_override](forms_override) - how to override default values in forms;\n* [groovy](groovy) - running a Groovy script from a flow;\n* [groovy_grape](groovy_grape) - how to use Groovy's `@Grab` in scripts;\n* [groovy_rest](groovy_rest) - calling a REST endpoint from a flow using Groovy;\n* [imports](imports) - how to use external GIT/http/mvn resources as project files;\n* [juel_java_streams](juel_java_streams) - using expressions, Groovy and Java Streams;\n* [noderoster](noderoster) - how to use the Node Roster task;\n* [process_card_htmx](process_card_htmx) - how to use \"process cards\" and [HTMX](https://htmx.org/) to implement custom forms to start processes;\n* [process_card_jquery](process_card_jquery) - how to use \"process cards\" and [jQuery](https://jquery.com/) to implement custom forms to start processes;\n* [profiles](profiles) - how to use profiles;\n* [python_script](python_script) - running a Python script from a flow;\n* [ruby](ruby) - running a Ruby snippet from a flow;\n* [script_url](script_url) - running an external script file;\n* [smtp_html](smtp_html) - how to send a HTML email.\n\n\n## Advanced\n\n* [ansible_out_vars](ansible_out_vars) - saving Ansible variables as Concord flow variables;\n* [ansible_roles](ansible_roles) - how to use external Ansible roles;\n* [approval](approval) - using forms and `runAs` to implement an approval process;\n* [context_injection](context_injection) - how to use automatic variable injection with custom tasks written in Groovy;\n* [custom_task](custom_task) - how to create a custom Concord task (plugin);\n* [dynamic_form_values](dynamic_form_values) - using custom forms with values added/removed dynamically;\n* [dynamic_tasks](dynamic_tasks) - using custom tasks provided with the process;\n* [form_l10n](form_l10n) - how to use custom validation error messages in forms;\n* [forms_multi_group](forms_multi_group) - how to restrict a form to a set of LDAP groups;\n* [forms_wizard](forms_wizard) - multi-step forms;\n* [inventory](inventory) - using Concord Inventory to retrieve Ansible's inventory data;\n* [inventory_lookup](inventory_lookup) - using the inventory lookup plugin for Ansible;\n* [process_meta](process_meta) - exporting process variables as process metadata;\n* [secret_files](secret_files) - how to store and export secrets as files;\n* [secrets](secrets) - working with Concord's Secrets storage.\n* [secrets_lookup](secret_lookup) - using the secret lookup plugin for Ansible;\n\n## Expert\n\n* [dynamic_form_fields](dynamic_form_fields) - how to define form fields at runtime;\n* [dynamic_forms](dynamic_forms) - how to create a form dynamically (at runtime);\n* [fork](fork) - starting a subprocess;\n* [fork_join](fork_join) - starting multiple subprocesses and waiting for completion;\n* [generic_triggers](generic_triggers) - how to use custom trigger events;\n* [logback_config](logback_config) - overriding logging configuration;\n* [process_from_a_process2](process_from_a_process2) - using output variables, starting a new subprocess from a project;\n* [process_from_a_process3](process_from_a_process3) - starting a new subprocess using a directory as the payload.\n* [process_from_a_process](process_from_a_process) - starting a new subprocess from a flow using a payload archive;\n- [mocking](mocking) - how to use Groovy to replace \"real\" tasks with \"mock\" versions for testing;\n"
  },
  {
    "path": "examples/ansible/README.md",
    "content": "# Ansible\n\nExample of running an Ansible playbook without creating a project.\n\n## Running\n\n```\ncd examples/ansible\n./run.sh localhost:8001\n```\n\nor\n\n1. Prepare the payload:\n\n```\nrm -rf target && mkdir target\ncp -R playbook concord.yml target/\n```\n\n2. Archive the payload:\n\n```\ncd target && zip -r payload.zip ./*\n```\n\n3. Send the payload to the server:\n\n```\ncurl -v -u username -F archive=@payload.zip http://localhost:8001/api/v1/process\n```"
  },
  {
    "path": "examples/ansible/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.10.1\"\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        greetings: \"Hi there!\"\n"
  },
  {
    "path": "examples/ansible/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ greetings }}\"\n      verbosity: 0"
  },
  {
    "path": "examples/ansible/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml playbook target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/ansible_docker/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.10.1\"\n\nflows:\n  default:\n  - task: ansible\n    in:\n      # specify the docker image to use\n      # add a prefix to use an alternative registry\n      dockerImage: \"walmartlabs/concord-ansible:latest\"\n      # rest of the parameters are the usual\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n          - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        greetings: \"Hi there!\"\n"
  },
  {
    "path": "examples/ansible_docker/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ greetings }}\"\n      verbosity: 0\n"
  },
  {
    "path": "examples/ansible_docker/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml playbook target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/ansible_dynamic_inventory/README.md",
    "content": "# Ansible\n\nExample of running an Ansible playbook without creating a project.\n\n## Running\n\n```\ncd examples/ansible\n./run.sh localhost:8001\n```\n\nor\n\n1. Prepare the payload:\n\n```\nrm -rf target && mkdir target\ncp -R playbook concord.yml target/\n```\n\n2. Archive the payload:\n\n```\ncd target && zip -r payload.zip ./*\n```\n\n3. Send the payload to the server:\n\n```\ncurl -v -u username -F archive=@payload.zip http://localhost:8001/api/v1/process\n```"
  },
  {
    "path": "examples/ansible_dynamic_inventory/concord.yml",
    "content": "configuration:\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.10.1\"\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      dynamicInventoryFile: my_inventory.sh\n      extraVars:\n        greetings: \"Hi there!\"\n"
  },
  {
    "path": "examples/ansible_dynamic_inventory/my_inventory.sh",
    "content": "#!/bin/sh\ncat <<EOF\n{\n    \"local\": {\n        \"hosts\": [\"127.0.0.1\"],\n        \"vars\": {\n            \"ansible_connection\": \"local\"\n        }\n    }\n}\nEOF\n"
  },
  {
    "path": "examples/ansible_dynamic_inventory/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ greetings }}\"\n      verbosity: 0\n"
  },
  {
    "path": "examples/ansible_dynamic_inventory/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml playbook my_inventory.sh target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/ansible_form/concord.yml",
    "content": "configuration:\n  # to use `ansible` task\n  dependencies:\n  - \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.10.1\"\n\n  arguments:\n    # default values\n    myForm:\n      myMessage: \"Hello, Concord\"\n\nflows:\n  default:\n  # open the form\n  - form: myForm\n\n  # call the playbook\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n          - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        # pass the form field's value into the playbook\n        message: ${myForm.myMessage}\n\nforms:\n  myForm:\n    - myMessage: { type: \"string\" }\n"
  },
  {
    "path": "examples/ansible_form/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ message }}\"\n      verbosity: 0\n"
  },
  {
    "path": "examples/ansible_form/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml playbook target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/ansible_form_as_inventory/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.10.1\"\n\nflows:\n  default:\n  - form: myForm\n    yield: true\n\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        myHostGroup:\n          hosts: ${myForm.ips.split(\",\")}\n      extraVars:\n        greetings: \"Hi there!\"\n\nforms:\n  myForm:\n  - ips: { type: \"string\", label: \"IP addresses\" }\n"
  },
  {
    "path": "examples/ansible_form_as_inventory/playbook/hello.yml",
    "content": "---\n- hosts: myHostGroup\n  tasks:\n  - debug:\n      msg: \"{{ greetings }}\"\n      verbosity: 0\n"
  },
  {
    "path": "examples/ansible_form_as_inventory/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml playbook target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/ansible_kerberos/concord.yml",
    "content": "configuration:\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.10.1\"\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      auth:\n        krb5:\n          user: \"testid\"\n          password: \"PASSWORD\"\n      inventory:\n        myHosts:\n          hosts:\n            - \"testhost\"\n"
  },
  {
    "path": "examples/ansible_kerberos/playbook/hello.yml",
    "content": "---\n- hosts: all\n  tasks:\n    - name: ping\n      ping:\n\n    - name: test dzdo\n      command: whoami\n      become: yes\n"
  },
  {
    "path": "examples/ansible_kerberos/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml playbook target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/ansible_limit/README.md",
    "content": "# Ansible\n\nExample of running an Ansible playbook with --limit option.\n\n## Running\n\n```\ncd examples/ansible\n./run.sh localhost:8001\n```\n\nor\n\n1. Prepare the payload:\n\n```\nrm -rf target && mkdir target\ncp -R playbook concord.yml target/\n```\n\n2. Archive the payload:\n\n```\ncd target && zip -r payload.zip ./*\n```\n\n3. Send the payload to the server:\n\n```\ncurl -v -u username -F archive=@payload.zip http://localhost:8001/api/v1/process\n```"
  },
  {
    "path": "examples/ansible_limit/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.10.1\"\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n            - \"127.0.0.2\"\n            - \"127.0.0.3\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        greetings: \"Hi there!\"\n      limit: \"@playbook/hello.limit\""
  },
  {
    "path": "examples/ansible_limit/playbook/hello.limit",
    "content": "127.0.0.1"
  },
  {
    "path": "examples/ansible_limit/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ greetings }}\"\n      verbosity: 0\n"
  },
  {
    "path": "examples/ansible_limit/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml playbook target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/ansible_out_vars/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.10.1\"\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      outVars:\n      - \"myVar\" # created using the `register` statement in the playbook\n\n  # `myVar` contains the variable values for all hosts in the play\n  - log: ${myVar['127.0.0.1']['msg']}\n"
  },
  {
    "path": "examples/ansible_out_vars/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"Hi there!\"\n      verbosity: 0\n    register: myVar\n"
  },
  {
    "path": "examples/ansible_out_vars/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml playbook target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/ansible_project/README.md",
    "content": "# Example: running an Ansible project\n\nThis example shows how to run an Ansible playbook using Concord.\n\n## Playbook's repository\n\nCreate a git repository of the following structure:\n\n```\nplaybook/\n  hello.yml\n```\n\nThe `hello.yml` playbook consists of a simple debug task:\n\n```yaml\n\n---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ greeting }}\"\n      verbosity: 0\n```\n\nCommit and push the repository to a remote server (e.g. GitHub). If you are using SSH, a\nnew SSH key pair will created on the step 2 - you will need access to the repository\nsettings to add a new public key.\n\n## Concord project\n\nBefore we create our own user, all requests are perfomed using the default admin API key.\nThis example assumes that the `ansible` template is already uploaded to the server.\n\n### 1. Create a new repository key\n\nUse `mySecret` as a name of the key pair, it will be used on the next step.\n\n### 2. Create a new Concord project\n\nWe are going to create a new project using the `ansible` project template. The `ansible` template\nwill automatically add the necessary boilerplate - a workflow process definition to run our playbook\nand necessary runtime dependencies.\n\n```\ncurl -v \\\n-H \"Content-Type: application/json\" \\\n-H \"Authorization: API_TOKEN\" \\\n-d '{ \"name\": \"myProject\", \"cfg\": { \"template\": \"ansible\" }, \"repositories\": { \"myRepo\": {\"url\": \"git@github.com:my/repo.git\", \"secret\": \"mySecret\" } } }' \\\nhttp://localhost:8001/api/v1/org/Default/project\n```\n\nThe `secret` parameters is the name of the key created on the step 2.\n\n```json\n{\n  \"ok\": true\n}\n```\n\n\n### 4. Add a new user (optional)\n\n```\ncurl -v \\\n-H \"Content-Type: application/json\" \\\n-H \"Authorization: API_TOKEN\" \\\n-d '{ \"username\": \"myUser\" }' \\\nhttp://localhost:8001/api/v1/user\n```\n\n```json\n{\n    \"ok\": true,\n    \"id\": \"9458c42e-db11-11e6-8356-07c51e4e3ef5\"\n}\n```\n\n### 5. Create an API key (optional)\n\nUse the `username` value of the user created in the previous step.\n\n```\ncurl -v \\\n-H \"Content-Type: application/json\" \\\n-H \"Authorization: API_TOKEN\" \\\n-d '{ \"username\": \"myUser\" }' \\\nhttp://localhost:8001/api/v1/apikey\n```\n\n```json\n{\n    \"ok\": true,\n    \"key\": \"API_TOKEN\"\n}\n```\n\nThe `key` value can be used for further access to the API, e.g. to start a process.\n\n### 6. Start a process\n\nCreate the `inventory.ini` file:\n\n```text\n[local]\n127.0.0.1\n\n[local:vars]\nansible_connection=local\n```\n\nCreate the `request.json` file:\n\n```json\n{\n  \"playbook\": \"playbook/hello.yml\",\n  \"extraVars\": {\n    \"greeting\": \"Hello, world\"\n  }\n}\n```\n\nMake a call:\n\n```\ncurl -v \\\n-H \"Authorization: API_TOKEN\" \\\n-F org=Default \\\n-F project=myProject \\\n-F repo=myRepo \\\n-F request=@request.json \\\n-F inventory=@inventory.ini \\\nhttp://localhost:8001/api/v1/process\n```\n\n```json\n{\n    \"ok\": true,\n    \"instanceId\": \"33c8f91e-db14-11e6-8d94-a3efec7ccd7b\"\n}\n```\n\n### 7. Check the logs (optional)\n\nUse the `instanceId` value returned by the server in the previous step.\nOpen the UI to see the log:\n\n```\nhttp://localhost:8001/#/process/33c8f91e-db14-11e6-8d94-a3efec7ccd7b.log\n```\n\n### 8. Get Ansible's statistics (optional)\n\nYou can download Ansible play's statistics with this request:\n\n```\ncurl -v \\\n-H \"Authorization: API_TOKEN\" \\\nhttp://localhost:8001/api/v1/process/33c8f91e-db14-11e6-8d94-a3efec7ccd7b/attachment/ansible_stats.json\n```\n\nExample of response:\n\n```json\n{\n  \"failures\": [], \n  \"skipped\": [], \n  \"changed\": [], \n  \"ok\": [\n    \"127.0.0.1\"\n  ], \n  \"unreachable\": []\n}\n```"
  },
  {
    "path": "examples/ansible_project/inventory.ini",
    "content": "[local]\n127.0.0.1\n\n[local:vars]\nansible_connection=local\n\n"
  },
  {
    "path": "examples/ansible_project/inventory.py",
    "content": "#!/usr/bin/python\nimport argparse\n\ntry:\n    import json\nexcept ImportError:\n    import simplejson as json\n\nclass ExampleInventory(object):\n    def __init__(self):\n        print json.dumps(self.example_inventory());\n    def example_inventory(self):\n        return {\n          \"local\": {\n \t    \"hosts\": [\"127.0.0.1\"],\n            \"vars\": {\n              \"ansible_connection\": \"local\"\n            }\n          }\n        }\n\n# Get the inventory.\nExampleInventory()\n"
  },
  {
    "path": "examples/ansible_project/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ greeting }}\"\n      verbosity: 0\n"
  },
  {
    "path": "examples/ansible_project/request.json",
    "content": "{\n  \"playbook\": \"playbook/hello.yml\",\n  \"extraVars\": {\n    \"greeting\": \"Hello, world\"\n  }\n}\n\n"
  },
  {
    "path": "examples/ansible_remote/README.md",
    "content": "# Ansible\n\nExample of running an Ansible playbook on a remote host without creating a project.\n\n## Running\n\n1. Upload the remote ssh key to the server:\n```\ncurl -H \"Authorization: API_TOKEN\" \\\n-F private=@/path/to/id_rsa \\\n-F public=@/path/to/id_rsa.pub \\\n-F storePassword=mySecretPassword \\\n'http://localhost:8001/api/v1/org/Default/secret/keypair?name=mySecret'\n```\n\nThe `name` should be unique. Remember `storePassword`\n\n2. Start the process:\n\n```\ncd examples/ansible\n./run.sh localhost:8001\n```\n\n3. Open the Console and find the process in the queue.\n\n4. Open the process status page and click `Wizard` button.\n\n5. Enter the name and the password of the secret created in the step 1. Click `Submit`.\n\n6. Open the process' log and check the results.\n"
  },
  {
    "path": "examples/ansible_remote/concord.yml",
    "content": "configuration:\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.10.1\"\n\nflows:\n  default:\n  # ask the user to fill the form\n  - form: authForm\n    yield: true\n\n  - task: ansible\n    in:\n      # location of the playbook\n      playbook: playbook/hello.yml\n\n      # remote server auth\n      auth:\n        privateKey:\n          # remote user's name\n          user: \"myuser\"\n          # remote server's key\n          secret:\n            name: ${authForm.secretName}\n            password: ${authForm.password}\n\n      # inventory data, should match the playbook's host groups\n      inventory:\n        local:\n          hosts:\n          - \"somehost.example.com\"\n\n      # pass additional variables to the playbook\n      extraVars:\n        greetings: \"Hi there!\"\n\nforms:\n  authForm:\n  # the secret's password\n  - secretName: {type: \"string\", label: \"Secret name\"}\n  - password: {type: \"string\", inputType: \"password\", label: \"Password\"}\n\n"
  },
  {
    "path": "examples/ansible_remote/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ greetings }}\"\n      verbosity: 0\n"
  },
  {
    "path": "examples/ansible_remote/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml playbook target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/ansible_retry/README.md",
    "content": "# Ansible\n\nExample of running an Ansible playbook with retry (--limit <playbook>.retry).\n\n## Running\n\n```\ncd examples/ansible\n./run.sh localhost:8001\n# or\n./run.sh localhost:8001 retryAfterSuspend\n# or\n./run.sh localhost:8001 retryAfterForm\n```\n\nor\n\n1. Prepare the payload:\n\n```\nrm -rf target && mkdir target\ncp -R playbook concord.yml target/\n```\n\n2. Archive the payload:\n\n```\ncd target && zip -r payload.zip ./*\n```\n\n3. Send the payload to the server:\n\n```\ncurl -v -H \"Content-Type: application/octet-stream\" --data-binary @payload.zip http://localhost:8001/api/v1/process\n```"
  },
  {
    "path": "examples/ansible_retry/concord.yml",
    "content": "configuration:\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.10.1\"\n\n  arguments:\n    attempts: 0\n    maxAttempts: 3\n    retryFile: null\n    makeItFail: true # just to simulate a failure inside of the playbook\n\nflows:\n  default:\n    # simply retry immediately after failure\n    - task: ansible\n      in:\n        playbook: playbook/hello.yml\n        inventory:\n          local:\n            hosts:\n              - \"127.0.0.1\"\n              - \"127.0.0.2\"\n              - \"127.0.0.3\"\n            vars:\n              ansible_connection: \"local\"\n        extraVars:\n          makeItFail: \"${makeItFail}\"\n      retry:\n        # specify new task parameters on the retry\n        in:\n          retry: true # force Ansible to re-use the existing *.retry file\n\n          # this bit is just for example\n          extraVars: # override the task's `extraVars` on retry\n            makeItFail: false # this time the playbook should succeed\n\n        times: 1\n        delay: 3\n\n  retryAfterSuspend:\n    # Retry the playbook after suspending for some period of time\n    - try:\n        - task: ansible\n          in:\n            playbook: playbook/hello.yml\n            saveRetryFile: true    # saves hello.retry as an attachment on playbook error\n            limit: \"${retryFile}\"  # default is null, will be a .retry file on retries\n            inventory:\n              local:\n                hosts:\n                  - \"127.0.0.1\"\n                  - \"127.0.0.2\"\n                  - \"127.0.0.3\"\n                vars:\n                  ansible_connection: \"local\"\n            extraVars:\n              makeItFail: \"${makeItFail}\"\n      error:\n        - if: \"${(attempts + 1) >= maxAttempts}\"  # give up eventually\n          then:\n            - throw: \"Too many attempts for hosts: ${resource.asString('_attachments/hello.retry')}\"\n\n        # suspend until retry time\n        - task: sleep\n          in:\n            suspend: true\n            # 3 seconds to prove the point in this example\n            # more useful would be 2 hours: ${2 * 60 * 60}\n            duration: 3\n        # try again, with retry file\n        - call: retryAfterSuspend\n          in:\n            attempts: \"${attempts + 1}\"\n            retryFile: \"@_attachments/hello.retry\"\n            makeItFail: false\n\n  retryAfterForm:\n    # Prompt to retry playbook after failure\n    - try:\n        - task: ansible\n          in:\n            playbook: playbook/hello.yml\n            saveRetryFile: true    # saves hello.retry as an attachment on playbook error\n            limit: \"${retryFile}\"  # default is null, will be a .retry file on retries\n            inventory:\n              local:\n                hosts:\n                  - \"127.0.0.1\"\n                  - \"127.0.0.2\"\n                  - \"127.0.0.3\"\n                vars:\n                  ansible_connection: \"local\"\n            extraVars:\n              makeItFail: \"${makeItFail}\"\n      error:\n        - if: \"${(attempts + 1) >= maxAttempts}\"  # give up eventually\n          then:\n            - throw: \"Too many attempts for hosts: ${resource.asString('_attachments/hello.retry')}\"\n\n        - form: retryForm\n          fields:\n            - doRetry: { label: \"Retry deployment?\", type: \"boolean\" }\n            - makeItFail: { label: \"Make deployment fail?\", type: \"boolean\" }\n        - if: \"${retryForm.doRetry}\"\n          then:\n            # try again, with retry file\n            - call: retryAfterForm\n              in:\n                attempts: \"${attempts + 1}\"\n                retryFile: \"@_attachments/hello.retry\"\n                makeItFail: \"${retryForm.makeItFail}\"\n          else:\n            - log: \"Retry denied\"\n            - exit\n"
  },
  {
    "path": "examples/ansible_retry/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"makeItFail={{ makeItFail }}\"\n      verbosity: 0\n\n  - fail:\n      msg: \"Making it fail\"\n    when: (makeItFail is defined) and (makeItFail) and (inventory_hostname == \"127.0.0.3\") # simulate a failure on one of the hosts\n"
  },
  {
    "path": "examples/ansible_retry/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\nENTRY_POINT=\"${2:-default}\"\n\nrm -rf target && mkdir target\ncp -R concord.yml playbook target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u \"${CURL_USER}\" -F entryPoint=\"${ENTRY_POINT}\" -F archive=@target/payload.zip \"http://${SERVER_ADDR}/api/v1/process\"\n"
  },
  {
    "path": "examples/ansible_roles/concord.yml",
    "content": "configuration:\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.10.1\"\n\nflows:\n  default:\n  - task: ansible\n    in:\n      # location of the playbook\n      playbook: \"playbook/hello.yml\"\n\n      # remote server auth\n      auth:\n        privateKey:\n          # remote user's name\n          user: \"app\"\n          # remote server's key\n          secret:\n            name: \"testKey\"\n\n      roles:\n      - name: \"devtools/tekton-ansible\"\n      inventory:\n        myServers:\n          hosts:\n          - \"myRemoteHost\"\n"
  },
  {
    "path": "examples/ansible_roles/playbook/hello.yml",
    "content": "---\n- hosts: myServers\n  become: true\n  roles:\n  - walmartlabs.jdk\n"
  },
  {
    "path": "examples/ansible_roles/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml playbook target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/ansible_stats/README.md",
    "content": "# Ansible\n\nExample of running an Ansible playbook without creating a project.\n\n## Running\n\n```\ncd examples/ansible\n./run.sh localhost:8001\n```\n\nor\n\n1. Prepare the payload:\n\n```\nrm -rf target && mkdir target\ncp -R playbook concord.yml target/\n```\n\n2. Archive the payload:\n\n```\ncd target && zip -r payload.zip ./*\n```\n\n3. Send the payload to the server:\n\n```\ncurl -v -u username -F archive=@payload.zip http://localhost:8001/api/v1/process\n```"
  },
  {
    "path": "examples/ansible_stats/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.10.1\"\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        greetings: \"Hi there!\"\n      debug: true\n      outVars:\n        - \"_stats\"    # register variable for concord ansible stats\n\n  - log: \"${ _stats }\" # will print {failures=[], skipped=[], changed=[], ok=[127.0.0.1], unreachable=[]}\n\n\n\n"
  },
  {
    "path": "examples/ansible_stats/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ greetings }}\"\n      verbosity: 0\n"
  },
  {
    "path": "examples/ansible_stats/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml playbook target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/ansible_template/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ greetings }}\"\n      verbosity: 0\n"
  },
  {
    "path": "examples/ansible_template/request.json",
    "content": "{\n  \"template\": \"ansible\",\n  \"playbook\": \"playbook/hello.yml\",\n\n  \"inventory\": {\n    \"local\": {\n      \"hosts\": [\"127.0.0.1\"],\n      \"vars\": {\n        \"ansible_connection\": \"local\"\n      }\n    }\n  },\n\n  \"extraVars\": {\n    \"greetings\": \"Hello, Concord\"\n  }\n}"
  },
  {
    "path": "examples/ansible_template/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R playbook target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip -F request=@request.json http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/ansible_vault/concord.yml",
    "content": "configuration:\n  # to use `ansible` task\n  dependencies:\n  - \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.10.1\"\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n          - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      vaultPassword: myVaultPassword\n"
  },
  {
    "path": "examples/ansible_vault/playbook/group_vars/local.yml",
    "content": "$ANSIBLE_VAULT;1.1;AES256\n37643333653539316339653436376236666530316338636336373466356332666532303938343634\n3964623138366632313765613134636263353531343137660a313462633136323064663064653238\n38646336336263303532363563373433336563356339393334633935363264643937356433373763\n3235313063633665650a643766356535373733633666356263336164613563613734313966373966\n63643636313336666535386437396432366263663439666661326464663361366636\n"
  },
  {
    "path": "examples/ansible_vault/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"Let me tell you a secret: {{ mySecret }}\"\n      verbosity: 0\n"
  },
  {
    "path": "examples/ansible_vault/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml playbook target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/ansible_windows/README.md",
    "content": "# Using Ansible with Windows Hosts\n\nIn this example, `groupVars` param is used as a configuration to export\na secret as an Ansible [group_vars](https://concord.walmartlabs.com/docs/plugins/ansible.html#group-vars) file.\nGroup variables are a convenient way to apply variables to multiple hosts at once.\n\nAuthorization of Windows servers using Concord is covered [here](https://concord.walmartlabs.com/docs/plugins/ansible.html#windows).\n"
  },
  {
    "path": "examples/ansible_windows/concord.yml",
    "content": "configuration:\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.10.1\"\n\nflows:\n  default:\n    - task: ansible\n      in:\n        playbook: playbook/hello.yml\n        inventoryFile: inventory.ini\n        groupVars:\n          - my_hosts:\n              secretName: myWindowsKey\n"
  },
  {
    "path": "examples/ansible_windows/inventory.ini",
    "content": "[my_hosts]\nmyWindows1\nmyWindows2\n"
  },
  {
    "path": "examples/ansible_windows/playbook/hello.yml",
    "content": "---\n- hosts: my_hosts\n  tasks:\n    - name: Ping Server\n      win_ping:\n"
  },
  {
    "path": "examples/ansible_windows/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml playbook target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/approval/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Starting as ${initiator}\"\n  - form: approvalForm\n    runAs:\n      ldap:\n        group: \"CN=Strati-SDE-Concord-sdeconcord,.*\"\n  - if: ${approvalForm.approved}\n    then:\n    - log: \"Approved =)\"\n    else:\n    - log: \"Rejected =(\"\n\nforms:\n  approvalForm:\n  - approved: { type: boolean }\n"
  },
  {
    "path": "examples/approval/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n\n"
  },
  {
    "path": "examples/context_injection/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://org.codehaus.groovy:groovy-all:pom:2.5.23\"\n  - \"mvn://com.walmartlabs.concord.plugins.basic:dynamic-tasks:1.76.1\"\n  arguments:\n    greeting: \"Hello, %s\"\n\nflows:\n  default:\n  - loadTasks: \"tasks\"\n  - ${myTask.hey(\"Concord\")}\n\n"
  },
  {
    "path": "examples/context_injection/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml tasks target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/context_injection/tasks/test.groovy",
    "content": "import com.walmartlabs.concord.common.Task\nimport com.walmartlabs.concord.common.InjectVariable\n\nimport javax.inject.Named\n\n@Named(\"myTask\")\nclass MyTask implements Task {\n\n    void hey(@InjectVariable(\"greeting\") String greeting, String name) {\n        println String.format(greeting, name)\n    }\n}"
  },
  {
    "path": "examples/custom_form/README.md",
    "content": "# Forms\n\nExample of using custom forms.\n"
  },
  {
    "path": "examples/custom_form/concord.yml",
    "content": "configuration:\n  arguments:\n    myForm:\n      lastName: Smith\n\nflows:\n  default:\n  - form: myForm\n    # form calls can override form values or provide additional data\n    values:\n      lastName: \"Appleseed\"\n      sum: \"${1 + 2}\"\n      address:\n        city: Toronto\n        province: Ontario\n        country: Canada\n\n  - log: \"Hello, ${myForm.firstName} ${myForm.lastName}\"\n  - log: \"We got your file and stored it as ${myForm.aFile}\"\n  - log: \"You have following skills\"\n  - task: log\n    in:\n      msg: \"Skill -> ${item}\"\n    withItems: ${myForm.skills}\n  - if: ${myForm.tosAgree}\n    then:\n      - log: \"${myForm.firstName} says: I have agreed to the Terms and Conditions\"\n\nforms:\n  myForm:\n  - firstName: { label: \"First name\", type: \"string\", value: \"John\" }\n  - lastName: { label: \"Last name\", type: \"string\" }\n  - age: { label: \"Age\", type: \"int\", min: 21, max: 999 }\n  - color: { label: \"Favorite color\", type: \"string\", allow: [\"red\", \"green\", \"blue\"] }\n  - aFile: { label: \"A file\", type: \"file?\" }\n  - tosAgree: { label: \"Terms and conditions read\", type: \"boolean\", value: true }\n  - skills: { label: \"Skills\", type: \"string*\", allow: [\"css\", \"design\", \"angular\"]}\n\n"
  },
  {
    "path": "examples/custom_form/forms/myForm/data.js",
    "content": "data = {\n    \"submitUrl\" : \"/api/service/custom_form/6cf435f0-b85c-42ea-a454-1ac308aff8c3/9fe117b3-5dec-4de2-afe7-1765a0e4e305/continue\",\n    \"success\" : false,\n    \"definitions\" : {\n        \"firstName\" : {\n            \"type\" : \"string\",\n            \"cardinality\" : \"ONE_AND_ONLY_ONE\"\n        },\n        \"lastName\" : {\n            \"type\" : \"string\",\n            \"cardinality\" : \"ONE_AND_ONLY_ONE\"\n        },\n        \"color\" : {\n            \"type\" : \"string\",\n            \"cardinality\" : \"ONE_AND_ONLY_ONE\",\n            \"allow\" : [ \"red\", \"green\", \"blue\" ]\n        },\n        \"age\" : {\n            \"type\" : \"int\",\n            \"cardinality\" : \"ONE_AND_ONLY_ONE\"\n        }\n    },\n    \"values\" : {\n        \"lastName\" : \"Smith\",\n        \"age\": 21,\n        \"color\": \"green\"\n    }\n};"
  },
  {
    "path": "examples/custom_form/forms/myForm/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0\">\n\n    <title>My Form</title>\n\n    <script src=\"data.js\"></script>\n\n    <script src=\"https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js\"></script>\n    <script src=\"https://cdn.jsdelivr.net/npm/handlebars@4.1.2/dist/handlebars.min.js\"></script>\n    <script src=\"https://cdn.jsdelivr.net/semantic-ui/2.2.10/semantic.min.js\"></script>\n    <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/semantic-ui/2.2.10/semantic.min.css\"/>\n</head>\n<body>\n\n<h1 class=\"ui center aligned header\">My Form</h1>\n\n<div class=\"ui text center aligned container\">\n    Example of using custom HTML/CSS/JS/etc resources.\n</div>\n\n<div class=\"ui text container\">\n    <div class=\"ui segment\">\n        <div id=\"container\">\n            <script id=\"formTemplate\" type=\"text/x-handlebars-template\">\n                {{#if success}}\n                <h3 id=\"success\" class=\"ui header\">Success</h3>\n                {{else}}\n\n                <!-- multipart/form-data is required for uploading files -->\n                <form class=\"ui form {{#if errors}}error{{/if}}\" method=\"post\" enctype=\"multipart/form-data\"\n                      action=\"{{submitUrl}}\">\n\n                    {{#if errors}}\n                    <div class=\"ui error message\">\n                        <ul class='list'>\n                            {{#each errors}}\n                            <li>{{this}}</li>\n                            {{/each}}\n                        </ul>\n                    </div>\n                    {{/if}}\n\n                    <div class=\"field {{#if errors.firstName}}error{{/if}}\">\n                        <label>First name:</label>\n                        <input name=\"firstName\" value=\"{{values.firstName}}\"/>\n                    </div>\n                    <div class=\"field\">\n                        <label>Last name:</label>\n                        <input name=\"lastName\" value=\"{{values.lastName}}\"/>\n                    </div>\n                    <div class=\"field\">\n                        <label>Age:</label>\n                        <input name=\"age\" value=\"{{values.age}}\" type=\"number\"/>\n                    </div>\n                    <div class=\"field\">\n                        <label>Favorite color:</label>\n                        <div class=\"ui selection dropdown\">\n                            <input type=\"hidden\" name=\"color\" value=\"{{values.color}}\"/>\n                            <i class=\"dropdown icon\"></i>\n                            <div class=\"default text\">Favorite color</div>\n                            <div class=\"menu\">\n                                {{#each definitions.color.allow}}\n                                <div class=\"item\" data-value=\"{{this}}\">{{this}}</div>\n                                {{/each}}\n                            </div>\n                        </div>\n                    </div>\n                    <div class=\"field {{#if errors.aFile}}error{{/if}}\">\n                        <label>File:</label>\n                        <input name=\"aFile\" type=\"file\"/>\n                    </div>\n                    <div class=\"field\">\n                        <label>Skills:</label>\n                        <select name=\"skills\" multiple=\"multiple\" class=\"ui fluid dropdown\">\n                            <option value=\"\">Skills</option>\n                            <option value=\"angular\">Angular</option>\n                            <option value=\"css\">CSS</option>\n                            <option value=\"design\">Graphic Design</option>\n                        </select>\n                    </div>\n                    <div class=\"field\">\n                        <div class=\"ui checkbox\">\n                            <input id=\"tosAgreeField\" type=\"checkbox\" name=\"tosAgree\" {{#if values.tosAgree}}checked{{/if}} value=\"true\">\n                            <label for=\"tosAgreeField\">I agree to the Terms and Conditions</label>\n                        </div>\n                    </div>\n\n                    <button class=\"ui button\" type=\"reset\">Reset</button>\n                    <button class=\"ui primary button\" type=\"submit\" onclick=\"handleSubmit(event)\">Submit</button>\n                </form>\n                {{/if}}\n            </script>\n        </div>\n    </div>\n\n    <img class=\"ui centered image small\" src=\"../shared/logo.png\"/>\n</div>\n\n<script>\n    function handleSubmit(ev) {\n        var button = ev.target;\n        var form = button.parentElement;\n        form.classList.add(\"loading\");\n    }\n\n    var source = document.getElementById(\"formTemplate\").innerHTML;\n    var template = Handlebars.compile(source);\n    var html = template(data);\n\n    document.getElementById(\"container\").innerHTML = html;\n\n    $('.ui.dropdown').dropdown();\n</script>\n\n</body>\n</html>"
  },
  {
    "path": "examples/custom_form/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml forms target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/custom_form_basic/README.md",
    "content": "# Forms\n\nExample of using custom forms.\n"
  },
  {
    "path": "examples/custom_form_basic/concord.yml",
    "content": "flows:\n  default:\n  - form: myForm\n    fields:\n      - name: { type: \"string\", value: \"${initiator.displayName}\" }\n\n  - log: \"${myForm}\"\n"
  },
  {
    "path": "examples/custom_form_basic/forms/myForm/data.js",
    "content": "data = {\n    \"submitUrl\" : \"/api/service/custom_form/6cf435f0-b85c-42ea-a454-1ac308aff8c3/9fe117b3-5dec-4de2-afe7-1765a0e4e305/continue\",\n    \"success\" : false,\n    \"definitions\" : {\n        \"name\" : {\n            \"type\" : \"string\",\n            \"cardinality\" : \"ONE_AND_ONLY_ONE\"\n        }\n    },\n    \"values\" : {\n        \"name\" : \"John Smith\"\n    }\n};"
  },
  {
    "path": "examples/custom_form_basic/forms/myForm/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>My Form</title>\n    <script src=\"data.js\"></script>\n</head>\n<body>\n\n<h1>My Form</h1>\n\n<p>Example of a very basic custom form.</p>\n\n<h2 id=\"successBox\" style=\"display: none;\">\n    Success!\n</h2>\n\n<h2 id=\"errorBox\" style=\"display: none;\">\n    Validation error!\n</h2>\n\n<form id=\"myForm\" method=\"post\" enctype=\"multipart/form-data\" onsubmit=\"handleOnSubmit()\">\n    <label for=\"nameField\">Name</label>\n    <input id=\"nameField\" name=\"name\"/>\n    <button id=\"submitButton\" type=\"submit\">Submit</button>\n</form>\n\n<script>\n    function handleOnSubmit() {\n        // disable the button after submit\n        var b = document.getElementById(\"submitButton\");\n        b.disabled = true;\n        b.textContent = \"Submitting...\";\n    }\n\n    var form = document.getElementById(\"myForm\");\n    form.action = data.submitUrl;\n\n    document.getElementById(\"nameField\").value = data.values.name;\n\n    if (data.success) {\n        // make the \"success box\" visible if the form was successfully submitted\n        document.getElementById(\"successBox\").style.display = \"block\";\n        // and hide the form, we don't need it anymore\n        form.style.display = \"none\";\n    }\n\n    if (data.errors) {\n        // show validation errors if any\n        document.getElementById(\"errorBox\").style.display = \"block\";\n    }\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "examples/custom_form_basic/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml forms target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/custom_task/.gitignore",
    "content": "*.iml\n.idea\ntarget\n"
  },
  {
    "path": "examples/custom_task/README.md",
    "content": "# Custom Task Example\n\nAn example Maven project which shows how to create a custom Concord task and use payload archives to test local changes.\n\n## Usage\n\nRun `test.sh` to test local changes:\n```yaml\n$ ./test.sh concord.example.com\n```\n\nThe script builds the task's JAR, collects all runtime dependencies and creates a\n[payload archive](https://concord.walmartlabs.com/docs/api/process.html#zip-file) with the [test flow](test.yml).\n\n## Unit Tests\n\nGenerally speaking, Concord tasks can be unit tested without running them in a Concord environment.\nSee the [provided JUnit file](src/test/java/com/walmartlabs/concord/examples/customtask/CustomTaskTest.java).\n"
  },
  {
    "path": "examples/custom_task/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>com.walmartlabs.concord.examples</groupId>\n    <artifactId>custom-task</artifactId>\n    <version>1.0.0-SNAPSHOT</version>\n\n    <properties>\n        <maven.compiler.source>17</maven.compiler.source>\n        <maven.compiler.target>17</maven.compiler.target>\n\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n\n        <concord.version>2.24.0</concord.version>\n        <junit.version>4.13.2</junit.version>\n        <okhttp.version>4.9.2</okhttp.version>\n    </properties>\n\n    <dependencyManagement>\n        <dependencies>\n            <dependency>\n                <groupId>com.walmartlabs.concord</groupId>\n                <artifactId>concord-targetplatform</artifactId>\n                <version>${concord.version}</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n        </dependencies>\n    </dependencyManagement>\n\n    <dependencies>\n        <!-- system dependencies, should use the \"provided\" scope -->\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <!-- for runtime-v2 support -->\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <version>1</version>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <!-- the task's dependencies provided by the runtime -->\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <!-- the task's 3rd-party dependencies -->\n        <dependency>\n            <groupId>com.squareup.okhttp3</groupId>\n            <artifactId>okhttp</artifactId>\n            <version>${okhttp.version}</version>\n        </dependency>\n\n        <!-- test dependencies -->\n        <dependency>\n            <groupId>junit</groupId>\n            <artifactId>junit</artifactId>\n            <version>${junit.version}</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>ch.qos.logback</groupId>\n            <artifactId>logback-classic</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <!-- required for runtime-v2 -->\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n                <version>0.9.0.M4</version>\n                <executions>\n                    <execution>\n                        <id>index</id>\n                        <goals>\n                            <goal>main-index</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "examples/custom_task/src/main/java/com/walmartlabs/concord/examples/customtask/CustomTask.java",
    "content": "package com.walmartlabs.concord.examples.customtask;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.ContextUtils;\nimport com.walmartlabs.concord.sdk.Task;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.Response;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Named;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * A custom task example. All tasks must implement {@link Task} interface and\n * be marked with {@link Named} annotation.\n *\n * The task fetches a specified URL and parses the response as JSON, converting\n * the data into regular Java objects.\n */\n@Named(\"customTask\")\npublic class CustomTask implements Task {\n\n    private static final Logger log = LoggerFactory.getLogger(CustomTask.class);\n\n    @Override\n    public void execute(Context ctx) throws Exception {\n        // retrieve a variable using the helper class\n        String url = ContextUtils.assertString(ctx, \"url\");\n\n\n        OkHttpClient client = new OkHttpClient();\n        Request req = new Request.Builder()\n                .url(url)\n                .build();\n\n        try (Response resp = client.newCall(req).execute()) {\n            if (!resp.isSuccessful()) {\n                log.warn(\"Error while fetching {}: {}\", url, resp.code());\n                ctx.setVariable(\"result\", error(resp.code()));\n                return;\n            }\n\n            ObjectMapper om = new ObjectMapper();\n            Object data = om.readValue(resp.body().byteStream(), Object.class);\n\n            // do not use custom classes and data structures when setting variables in a task\n            // stick to the default JDK classes to avoid serialization issues\n            ctx.setVariable(\"result\", ok(data));\n        }\n    }\n\n    private static Object ok(Object data) {\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"ok\", true);\n        m.put(\"data\", data);\n        return m;\n    }\n\n    private static Object error(int code) {\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"ok\", false);\n        m.put(\"errorCode\", code);\n        return m;\n    }\n}\n"
  },
  {
    "path": "examples/custom_task/src/main/java/com/walmartlabs/concord/examples/customtask/CustomTaskV2.java",
    "content": "package com.walmartlabs.concord.examples.customtask;\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\n\nimport javax.inject.Named;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.Response;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n\n/**\n * A custom task example. All tasks must implement {@link Task} interface and\n * be marked with {@link Named} annotation.\n *\n * The task fetches a specified URL and parses the response as JSON, converting\n * the data into regular Java objects.\n */\n@Named(\"customTask\")\npublic class CustomTaskV2 implements Task {\n\n    private static final Logger log = LoggerFactory.getLogger(CustomTaskV2.class);\n\n    @Override\n    public TaskResult execute(Variables input) throws Exception {\n        String url = input.assertString(\"url\");\n\n        OkHttpClient client = new OkHttpClient();\n        Request req = new Request.Builder()\n                .url(url)\n                .build();\n\n        try (Response resp = client.newCall(req).execute()) {\n            if (!resp.isSuccessful()) {\n                log.warn(\"Error while fetching {}: {}\", url, resp.code());\n                return TaskResult.error(\"Error while fetching\")\n                        .value(\"errorCode\", resp.code());\n            }\n\n            ObjectMapper om = new ObjectMapper();\n            Object data = om.readValue(resp.body().byteStream(), Object.class);\n\n            return TaskResult.success()\n                    .value(\"data\", data);\n        }\n    }\n}\n"
  },
  {
    "path": "examples/custom_task/src/test/java/com/walmartlabs/concord/examples/customtask/CustomTaskTest.java",
    "content": "package com.walmartlabs.concord.examples.customtask;\n\nimport com.walmartlabs.concord.sdk.ContextUtils;\nimport com.walmartlabs.concord.sdk.MockContext;\nimport org.junit.Test;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.Assert.assertEquals;\n\npublic class CustomTaskTest {\n\n    @Test\n    public void test() throws Exception {\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"url\", \"https://jsonplaceholder.typicode.com/todos/1\");\n\n        MockContext ctx = new MockContext(args);\n\n        CustomTask task = new CustomTask();\n        task.execute(ctx);\n\n        Map<String, Object> result = ContextUtils.getMap(ctx, \"result\", Collections.emptyMap());\n        assertEquals(true, result.get(\"ok\"));\n\n        System.out.println(result.get(\"data\"));\n    }\n}\n"
  },
  {
    "path": "examples/custom_task/test-v2.yml",
    "content": "configuration:\n  runtime: concord-v2\n\nflows:\n  default:\n    - task: customTask\n      in:\n        url: \"https://jsonplaceholder.typicode.com/todos/1\"\n      out: result\n\n    - if: \"${!result.ok}\"\n      then:\n        - throw: \"The request returned ${result.errorCode}\"\n\n    - log: \"Data: ${result.data}\"\n"
  },
  {
    "path": "examples/custom_task/test.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\nSERVER_ADDR=\"$1\"\nCONCORD_YAML=\"$2\"\n\n# build the task\nmvn clean package -DskipTests\n\n# prepare the target dir\nrm -rf target/test && mkdir -p target/test\n\n# copy the test flow\ncp ${CONCORD_YAML:-test.yml} target/test/concord.yml\n\n# copy the task's JAR...\nmkdir -p target/test/lib/\ncp target/custom-task-*-SNAPSHOT.jar target/test/lib/\n\n# ...and dependencies\nmvn dependency:copy-dependencies -DoutputDirectory=/tmp/deps -Dmdep.useSubDirectoryPerScope\ncp /tmp/deps/compile/*.jar target/test/lib/\n\n# create the payload archive\npushd target/test && zip -r payload.zip ./* > /dev/null && popd\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/test/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/custom_task/test.yml",
    "content": "flows:\n  default:\n    - task: customTask\n      in:\n        url: \"https://jsonplaceholder.typicode.com/todos/1\"\n\n    - if: \"${!result.ok}\"\n      then:\n        - throw: \"The request returned ${result.errorCode}\"\n\n    - log: \"Data: ${result.data}\"\n"
  },
  {
    "path": "examples/datetime/concord.yml",
    "content": "flows:\n  default:\n  - log: \"${datetime.current()}\"\n  - log: \"${datetime.current('dd.MM.yyy')}\"\n  - log: \"${datetime.format(datetime.current(), 'dd.MM.yyyy')}\"\n  - log: \"${datetime.parse('01.01.2018', 'dd.MM.yyyy')}\"\n"
  },
  {
    "path": "examples/datetime/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./concord.yml > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n\n"
  },
  {
    "path": "examples/docker/README.md",
    "content": "# Docker\n\nExample of running an Ansible playbook from docker's container.\n\n## Running\n\n```\ncd docs/examples/ansible\n./run.sh localhost:8001\n```\n\nor\n\n1. Prepare the payload:\n\n```\nrm -rf target && mkdir target\ncp -R playbook ansible.cfg inventory.ini concord.yml target/\n```\n\n2. Archive the payload:\n\n```\ncd target && zip -r payload.zip ./*\n```\n\n3. Send the payload to the server:\n\n```\ncurl -v -u username -F archive=@payload.zip http://localhost:8001/api/v1/process\n```"
  },
  {
    "path": "examples/docker/ansible.cfg",
    "content": "[defaults]\nhost_key_checking = false\nremote_tmp = /tmp/ansible/$USER\ntransport = local\n[ssh_connection]\ncontrol_path = %(directory)s/%%h-%%p-%%r\n"
  },
  {
    "path": "examples/docker/concord.yml",
    "content": "flows:\n  default:\n  - docker: \"walmartlabs/concord-ansible\"\n    env:\n      ANSIBLE_CONFIG: ansible.cfg\n    cmd: ansible-playbook playbook/hello.yml -i inventory.ini -e greetings=Hi\n"
  },
  {
    "path": "examples/docker/inventory.ini",
    "content": "[local]\n127.0.0.1\n\n[local:vars]\nansible_connection=local"
  },
  {
    "path": "examples/docker/playbook/hello.yml",
    "content": "---\n- hosts: localhost\n  tasks:\n  - debug:\n      msg: \"{{ greetings }}\"\n      verbosity: 0\n"
  },
  {
    "path": "examples/docker/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R playbook concord.yml ansible.cfg inventory.ini target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/docker_simple/concord.yml",
    "content": "flows:\n  default:\n  - docker: library/alpine\n    cmd: echo \"Hello, ${name}\"\n\nconfiguration:\n  arguments:\n    name: \"world\"\n"
  },
  {
    "path": "examples/docker_simple/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/dynamic_form_fields/concord.yml",
    "content": "flows:\n  default:\n  # a regular form\n  - form: myForm1\n  - log: \"${myForm1}\"\n\n  # an one-off form\n  - form: myForm2\n    fields:\n    - firstName: {type: \"string\", label: \"First Name\"}\n  - log: \"${myForm2}\"\n\n  # a form with fields stored in a variable\n  - form: myForm3\n    fields: ${myForm3Fields}\n  - log: \"${myForm3}\"\n\n  # a form with fields created in a script\n  - script: groovy\n    body: |\n      def myFields = [\n        [\"firstName\": [\"type\": \"string\", \"label\": \"First Name\"]],\n        [\"lastName\": [\"type\": \"string\", \"label\": \"Last Name\"]]\n      ]\n\n      execution.setVariable('myForm4Fields', myFields)\n  - form: myForm4\n    fields: ${myForm4Fields}\n  - log: \"${myForm4}\"\n\nforms:\n  myForm1:\n  - firstName: {type: \"string\", label: \"First Name\"}\n\nconfiguration:\n  dependencies:\n    - \"mvn://org.codehaus.groovy:groovy-all:pom:2.5.23\"\n  arguments:\n    myForm3Fields:\n    - firstName: {type: \"string\", label: \"First Name\"}\n"
  },
  {
    "path": "examples/dynamic_form_fields/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/dynamic_form_values/concord.yml",
    "content": "flows:\n  default:\n    - form: myForm\n    - log: \"I've chosen those colors: ${myForm.colors}\"\n\nforms:\n  myForm:\n    - colors: {type: \"string+\"}\n\nconfiguration:\n  arguments:\n    myForm:\n      colors:\n        - red\n        - green\n        - blue"
  },
  {
    "path": "examples/dynamic_form_values/forms/myForm/data.js",
    "content": "data = {\n    \"submitUrl\" : \"/api/service/custom_form/36cea620-8b75-45d7-a53a-cb1c094e1b34/bdcd87b0-07ad-41dd-8c8d-b25942621e27/continue\",\n    \"definitions\" : {\n        \"colors\" : {\n            \"label\" : \"colors\",\n            \"type\" : \"string\",\n            \"cardinality\" : \"AT_LEAST_ONE\"\n        }\n    },\n    \"values\" : {\n        \"colors\" : [ \"red\", \"green\", \"blue\" ]\n    }\n};"
  },
  {
    "path": "examples/dynamic_form_values/forms/myForm/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0\">\n\n    <title>My Form</title>\n\n    <script src=\"data.js\"></script>\n\n    <script src=\"https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js\"></script>\n    <script src=\"https://cdn.jsdelivr.net/npm/handlebars@4.1.2/dist/handlebars.min.js\"></script>\n    <script src=\"https://cdn.jsdelivr.net/semantic-ui/2.2.10/semantic.min.js\"></script>\n    <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/semantic-ui/2.2.10/semantic.min.css\"/>\n</head>\n<body>\n\n<h1 class=\"ui center aligned header\">My Form</h1>\n\n<div class=\"ui text center aligned container\">\n    Dynamic form example.\n</div>\n\n<div class=\"ui text container\">\n    <div class=\"ui segment\">\n        <div id=\"container\">\n            <script id=\"formTemplate\" type=\"text/x-handlebars-template\">\n                {{#if success}}\n                <h3 id=\"success\" class=\"ui header\">Success</h3>\n                {{else}}\n                <form class=\"ui form {{#if errors}}error{{/if}}\" method=\"post\" action=\"{{submitUrl}}\">\n\n                    {{#if errors}}\n                    <div class=\"ui error message\">\n                        <ul class='list'>\n                            {{#each errors}}\n                            <li>{{this}}</li>\n                            {{/each}}\n                        </ul>\n                    </div>\n                    {{/if}}\n\n                    <div class=\"field\">\n                        <div id=\"list\" class=\"ui list\">\n                            {{#each values.colors}}\n                            <div class=\"item\"><input type=\"text\" name=\"colors\" value=\"{{this}}\"/></div>\n                            {{/each}}\n                        </div>\n\n                        <button class=\"ui labeled icon button\" onclick=\"handleAdd(event)\">\n                            <i class=\"add icon\"></i>\n                            Add\n                        </button>\n                    </div>\n\n                    <div class=\"ui divider\"></div>\n\n                    <button class=\"ui button\" type=\"reset\">Reset</button>\n                    <button class=\"ui primary button\" type=\"submit\" onclick=\"handleSubmit(event)\">Submit</button>\n                </form>\n                {{/if}}\n            </script>\n        </div>\n    </div>\n\n    <img class=\"ui centered image small\" src=\"logo.png\"/>\n</div>\n\n<script>\n    function handleSubmit(ev) {\n        var button = ev.target;\n        var form = button.parentElement;\n        form.classList.add(\"loading\");\n    }\n\n    function handleAdd(ev) {\n        ev.preventDefault();\n        $(\"#list\").append(\"<div class='item'><input type='text' name='colors' value='enter something'/>\");\n    }\n\n    var source = document.getElementById(\"formTemplate\").innerHTML;\n    var template = Handlebars.compile(source);\n    var html = template(data);\n\n    document.getElementById(\"container\").innerHTML = html;\n\n    $('.ui.dropdown').dropdown();\n</script>\n\n</body>\n</html>"
  },
  {
    "path": "examples/dynamic_form_values/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R forms concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/dynamic_forms/concord.yml",
    "content": "configuration:\n  dependencies:\n    - \"mvn://org.codehaus.groovy:groovy-all:pom:2.5.23\"\n\nflows:\n  default:\n\n    # creating a form using a Groovy script\n    - script: groovy\n      body: |\n        // define the form's fields and options\n        // the structure is the same as in the form call's syntax\n        def myForm = [\n          \"fields\": [\n            [\"firstName\": [\"type\": \"string\", \"label\": \"First Name\"]],\n            [\"lastName\": [\"type\": \"string\", \"label\": \"Last Name\"]]\n          ],\n          \"values\": [\n              \"firstName\": \"John\",\n              \"lastName\": \"Smith\"\n          ]\n        ]\n\n        // create the form, the process will be suspended after the script is done\n        execution.form('myForm', myForm);\n\n    - log: \"${myForm}\"\n\n    # creating a form using an expression\n    - set:\n        myForm:\n          fields:\n          - firstName: { label: \"First name\", type: \"string\" }\n          - lastName: { label: \"Last name\", type: \"string\" }\n          values:\n            firstName: \"John\"\n            lastName: \"Smith\"\n\n    - ${execution.form('myForm', myForm)}\n\n    - log: \"${myForm}\"\n"
  },
  {
    "path": "examples/dynamic_forms/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/dynamic_tasks/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://org.codehaus.groovy:groovy-all:pom:2.5.23\"\n  - \"mvn://com.walmartlabs.concord.plugins.basic:dynamic-tasks:1.76.1\"\n\nflows:\n  default:\n  - loadTasks: \"tasks\"\n  - ${myTask.hey(\"world\")}\n\n"
  },
  {
    "path": "examples/dynamic_tasks/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml tasks target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/dynamic_tasks/runtime-v2/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://org.codehaus.groovy:groovy-all:pom:2.5.23\"\n  - \"mvn://com.walmartlabs.concord.plugins.basic:dynamic-tasks:2.36.0\"\n\nflows:\n  default:\n  - task: loadTasks\n    in:\n      path: \"tasks\"\n  - expr: ${myTask.hey(\"world\")}\n"
  },
  {
    "path": "examples/dynamic_tasks/runtime-v2/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml tasks target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/dynamic_tasks/runtime-v2/tasks/test.groovy",
    "content": "import com.walmartlabs.concord.runtime.v2.sdk.Task\n\nimport javax.inject.Named\n\n@Named(\"myTask\")\nclass MyTask implements Task {\n\n    void hey(String name) {\n        println \"Hey, ${name}\"\n    }\n}\n"
  },
  {
    "path": "examples/dynamic_tasks/tasks/test.groovy",
    "content": "import com.walmartlabs.concord.common.Task\n\nimport javax.inject.Named\n\n@Named(\"myTask\")\nclass MyTask implements Task {\n\n    void hey(String name) {\n        println \"Hey, ${name}\"\n    }\n}"
  },
  {
    "path": "examples/error_handling/concord.yml",
    "content": "flows:\n  default:\n  - ${misc.throwBpmnError('kaboom!')}\n\n  onFailure:\n  - log: \"Phew! That was close\"\n"
  },
  {
    "path": "examples/error_handling/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/external_script/concord.yml",
    "content": "flows:\n  default:\n  - script: example.js\n  - log: Hello, ${x}\n\n"
  },
  {
    "path": "examples/external_script/example.js",
    "content": "execution.setVariable(\"x\", \"world\");"
  },
  {
    "path": "examples/external_script/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml example.js target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/fork/concord.yml",
    "content": "configuration:\n  arguments:\n    myName: \"Concord\"\n\nflows:\n  default:\n  # \"forks\" the current process as a child process\n  - task: concord\n    in:\n      action: fork\n\n      # if not specified, the parent's entry point will be used\n      entryPoint: sayHello\n\n      # wait for completion\n      sync: true\n\n      # additional arguments\n      arguments:\n        otherName: \"${initiator.username}\"\n\n  - log: \"Done! ${jobs} is completed\"\n\n  sayHello:\n  # forked processes can access the latest snapshot of the parent's\n  # state in addition to the arguments provided by the parent task\n  - log: \"FORK: Hello, ${otherName}. I'm ${myName}\"\n\n  # simulate a long-running process, sleep for 10s\n  - ${sleep.ms(10000)}\n\n"
  },
  {
    "path": "examples/fork/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/fork_join/concord.yml",
    "content": "flows:\n  default:\n  # \"forks\" the current process as multiple subprocesses\n  - task: concord\n    in:\n      action: fork\n      tags: forkJoinChild\n      # disable the `onCancel` handler, because it's going to handle\n      # the parent's cancellation only\n      disableOnCancel: true\n      forks:\n      # spawn multiple jobs with different parameters\n      - entryPoint: aJob\n        arguments:\n          color: \"red\"\n      - entryPoint: aJob\n        arguments:\n          color: \"green\"\n      - entryPoint: aJob\n        arguments:\n            color: \"blue\"\n\n\n    # out variable \"myJobs\" will contain a list of process IDs\n    out:\n      myJobs: ${jobs}\n\n  - log: \"Done! Status of the jobs: ${concord.waitForCompletion(myJobs)}\"\n\n  aJob:\n  - log: \"FORK (${color}) starting...\"\n  - ${sleep.ms(15000)}\n  - log: \"...done!\"\n\n  onCancel:\n  # find and cancel the tagged subprocesses\n  - task: concord\n    in:\n      action: kill\n      # because onCancel is a separate subprocess, we need to use\n      # 'parentInstanceId'\n      instanceId: \"${concord.listSubprocesses(parentInstanceId, 'forkJoinChild')}\"\n      sync: true\n  - log: \"Jobs are cancelled!\"\n\n"
  },
  {
    "path": "examples/fork_join/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/form_and_long_process/concord.yml",
    "content": "flows:\n  default:\n\n  # \"yield\" makes the process to continue in background after this\n  # form. It will stop a UI \"spinner\" in redirects a user back to the\n  # process' page\n  - form: myForm\n    yield: true\n\n  - log: \"Hello, ${myForm.name}! I'm starting a long-running task...\"\n\n  # imitates a long-running task\n  - ${sleep.ms(30000)}\n\n  - log: \"Done!\"\n\nforms:\n  myForm:\n  - name: { type: \"string\" }\n\n"
  },
  {
    "path": "examples/form_and_long_process/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/form_l10n/concord.yml",
    "content": "flows:\n  default:\n  # this form uses locale.properties in the root directory\n  - form: myForm\n\n  # this form has it's own locale.properties file in forms/myOtherForm directory\n  # per-form localization files are supported only for custom forms\n  - form: myOtherForm\n\nforms:\n  myForm:\n  - name: { type: \"string\", label: \"Name\" }\n  - age: { type: \"int\", label: \"Age\", min: 21 }\n\n  myOtherForm:\n  - name: { type: \"string\", label: \"Name\" }\n  - age: { type: \"int\", label: \"Age\", min: 21 }\n"
  },
  {
    "path": "examples/form_l10n/forms/myOtherForm/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0\">\n\n    <title>My Other Form</title>\n\n    <script src=\"data.js\"></script>\n\n    <script src=\"https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js\"></script>\n    <script src=\"https://cdn.jsdelivr.net/npm/handlebars@4.1.2/dist/handlebars.min.js\"></script>\n    <script src=\"https://cdn.jsdelivr.net/semantic-ui/2.2.10/semantic.min.js\"></script>\n    <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/semantic-ui/2.2.10/semantic.min.css\"/>\n</head>\n<body>\n\n<h1 class=\"ui center aligned header\">My Form</h1>\n\n<div class=\"ui text container\">\n    <div class=\"ui segment\">\n        <div id=\"container\">\n            <script id=\"formTemplate\" type=\"text/x-handlebars-template\">\n                {{#if success}}\n                <h3 id=\"success\" class=\"ui header\">Success</h3>\n                {{else}}\n\n                <form class=\"ui form {{#if errors}}error{{/if}}\" method=\"post\" enctype=\"multipart/form-data\"\n                      action=\"{{submitUrl}}\">\n\n                    {{#if errors}}\n                    <div class=\"ui error message\">\n                        <ul class='list'>\n                            {{#each errors}}\n                            <li>{{this}}</li>\n                            {{/each}}\n                        </ul>\n                    </div>\n                    {{/if}}\n\n                    <div class=\"field {{#if errors.name}}error{{/if}}\">\n                        <label>Name:</label>\n                        <input name=\"name\" value=\"{{values.name}}\"/>\n                    </div>\n                    <div class=\"field\">\n                        <label>Age:</label>\n                        <input name=\"age\" value=\"{{values.age}}\" type=\"number\"/>\n                    </div>\n\n                    <button class=\"ui button\" type=\"reset\">Reset</button>\n                    <button class=\"ui primary button\" type=\"submit\" onclick=\"handleSubmit(event)\">Submit</button>\n                </form>\n                {{/if}}\n            </script>\n        </div>\n    </div>\n</div>\n\n<script>\n    function handleSubmit(ev) {\n        var button = ev.target;\n        var form = button.parentElement;\n        form.classList.add(\"loading\");\n    }\n\n    var source = document.getElementById(\"formTemplate\").innerHTML;\n    var template = Handlebars.compile(source);\n    var html = template(data);\n\n    document.getElementById(\"container\").innerHTML = html;\n\n    $('.ui.dropdown').dropdown();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "examples/form_l10n/forms/myOtherForm/locale.properties",
    "content": "invalidCardinality=What is my {0}?\nintegerRangeError={0} should be greater than 21!\n"
  },
  {
    "path": "examples/form_l10n/locale.properties",
    "content": "# applies to all fields\ninvalidCardinality=We really need {0}!\n\n# applies only to a specific field\nage.integerRangeError=Hey, the age should be {3}!\n"
  },
  {
    "path": "examples/form_l10n/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml locale.properties forms target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/forms/README.md",
    "content": "# Forms\n\nExample of using forms in processes.\n\n## Running\n\n1. start the process\n\n   ```\n   cd docs/examples/forms\n   ./run.sh localhost:8001\n   ```\n   \n2. open [the console](http://localhost:8001);  \n3. open the process page;\n4. start the wizard;\n5. fill-out the form and click \"Submit\";\n6. check the process logs: there should be \"Hello, John Smith\" message.\n"
  },
  {
    "path": "examples/forms/concord.yml",
    "content": "flows:\n  default:\n  - form: myForm\n  - log: \"Hello, ${myForm.firstName} ${myForm.lastName}\"\n  - log: \"We got your password: ${myForm.password}\"\n  - log: \"We know that you are ${myForm.age} years old\"\n  - log: \"And your height is: ${myForm.height}\"\n  - log: \"'Remember me' checked: ${myForm.rememberMe}\"\n  - log: \"File ${myForm.file} content: ${resource.asString(myForm.file)}\"\n  - log: \"Email: ${myForm.email}\"\n  - log: \"Skills -> ${myForm.skills}\"\n  - task: log\n    in:\n     msg: \"Skill -> ${item}\"\n    withItems: ${myForm.skills}\n\nforms:\n  myForm:\n  - firstName: { label: \"First name\", type: \"string\", placeholder: \"Place first name here\" }\n  - lastName: { label: \"Last name\", type: \"string\" }\n  - password:  { label: \"Password\", type: \"string\", inputType: 'password' }\n  - age: { label: \"Age\", type: \"int?\" }\n  - height: { label: \"Height\", type: \"decimal?\" }\n  - rememberMe: { label: \"Remember me\", type: \"boolean\", value: true }\n  - file: { label: \"File\", type: \"file\"}\n  - skills: { label: \"Skills\", type: \"string*\", allow: [\"css\", \"design\", \"angular\"], search: true }\n  - email: { label: \"Email\", type: \"string\", inputType: \"email\" }\n  - readOnlyField: { label: \"Read only field\", type: \"string\", value: \"constant\", readonly: true}\n"
  },
  {
    "path": "examples/forms/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/forms_multi_group/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Starting as ${initiator}\"\n  - form: myForm\n    runAs:\n      ldap:\n       - group: \"CN=Strati-SDE-Concord-sdeconcord,.*\"\n       - group: \"CN=Open Source Developers-opensource_devs,.*\"\n\nforms:\n  myForm:\n  - firstName: { label: \"First name\", type: \"string\", value: \"John\" }\n"
  },
  {
    "path": "examples/forms_multi_group/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/forms_override/concord.yml",
    "content": "configuration:\n## DEFAULT VARIABLES\n  arguments:\n    myForm:\n      myField: \"AAA\"\n\nflows:\n  default:\n  # call a form using the current (default) value of ${myForm}\n  - form: myForm\n  - log: \"Value: ${myForm.myField}\"\n\n  # call a form and override a value\n  - form: myForm\n    values:\n      myField: \"BBB\"\n  - log: \"Value: ${myForm.myField}\"\n\n  # call a process and override a value in ${myForm}\n  - call: myFlow\n    in:\n      myForm:\n        myField: \"CCC\"\n  - log: \"Value: ${myForm.myField}\"\n\n  myFlow:\n  # call a form using the current value of ${myForm}\n  - form: myForm\n\n## FORMS\n\nforms:\n  myForm:\n  - myField: { type: \"string\" }\n\n"
  },
  {
    "path": "examples/forms_override/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/forms_wizard/concord.yml",
    "content": "flows:\n  default:\n  - call: askUserForDetails\n\n  askUserForDetails:\n  - form: userData\n  - if: ${userData.amount > 50}\n    then:\n    - call: warnUser\n    else:\n    - call: finishIt\n\n  warnUser:\n  - form: userWarning\n    values:\n      amount: ${userData.amount}\n\n  - if: ${userWarning.continue == \"yes\"}\n    then:\n    - call: finishIt\n    else:\n    # recursively call the initial form\n    - call: askUserForDetails\n\n  finishIt:\n  - log: \"All done!\"\n\nforms:\n  userData:\n  - amount: { label: \"Amount\", type: \"int\", max: 100 }\n\n  userWarning:\n  - continue: { type: \"string?\", allow: [\"yes\"] }\n"
  },
  {
    "path": "examples/forms_wizard/forms/shared/common.js",
    "content": "function handleSubmit(ev) {\n    var button = ev.target;\n    var form = button.parentElement;\n    form.classList.add(\"loading\");\n}\n\nfunction init() {\n    var source = document.getElementById(\"formTemplate\").innerHTML;\n    var template = Handlebars.compile(source);\n    var html = template(data);\n\n    document.getElementById(\"container\").innerHTML = html;\n\n    $('.ui.dropdown').dropdown();\n}\n"
  },
  {
    "path": "examples/forms_wizard/forms/userData/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0\">\n\n    <title>Wizard</title>\n\n    <script src=\"https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js\"></script>\n    <script src=\"https://cdn.jsdelivr.net/npm/handlebars@4.1.2/dist/handlebars.min.js\"></script>\n    <script src=\"https://cdn.jsdelivr.net/semantic-ui/2.2.10/semantic.min.js\"></script>\n    <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/semantic-ui/2.2.10/semantic.min.css\"/>\n\n    <script src=\"data.js\"></script>\n    <script src=\"../shared/common.js\"></script>\n</head>\n<body>\n\n<h1 class=\"ui center aligned header\">Wizard</h1>\n\n<div class=\"ui text center aligned container\">\n    Form wizard example.\n</div>\n\n<div class=\"ui text container\">\n    <div class=\"ui segment\">\n        <div id=\"container\">\n            <script id=\"formTemplate\" type=\"text/x-handlebars-template\">\n                {{#if success}}\n                <h3 id=\"success\" class=\"ui header\">Success</h3>\n                {{else}}\n                <form class=\"ui form {{#if errors}}error{{/if}}\" method=\"post\" action=\"{{submitUrl}}\">\n\n                    {{#if errors}}\n                    <div class=\"ui error message\">\n                        <ul class='list'>\n                            {{#each errors}}\n                            <li>{{this}}</li>\n                            {{/each}}\n                        </ul>\n                    </div>\n                    {{/if}}\n\n                    <p>Please enter the amount of items to reserve:</p>\n\n                    <div class=\"field {{#if errors.firstName}}error{{/if}}\">\n                        <label>Amount:</label>\n                        <input name=\"amount\" value=\"{{values.amount}}\" type=\"number\"/>\n                    </div>\n\n                    <button class=\"ui button\" type=\"reset\">Reset</button>\n                    <button class=\"ui primary button\" type=\"submit\" onclick=\"handleSubmit(event)\">Submit</button>\n                </form>\n                {{/if}}\n            </script>\n        </div>\n    </div>\n\n    <img class=\"ui centered image small\" src=\"../shared/logo.png\"/>\n</div>\n\n<script>\n    init();\n</script>\n\n</body>\n</html>"
  },
  {
    "path": "examples/forms_wizard/forms/userWarning/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0\">\n\n    <title>Wizard</title>\n\n    <script src=\"https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js\"></script>\n    <script src=\"https://cdn.jsdelivr.net/npm/handlebars@4.1.2/dist/handlebars.min.js\"></script>\n    <script src=\"https://cdn.jsdelivr.net/semantic-ui/2.2.10/semantic.min.js\"></script>\n    <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/semantic-ui/2.2.10/semantic.min.css\"/>\n\n    <script src=\"data.js\"></script>\n    <script src=\"../shared/common.js\"></script>\n</head>\n<body>\n\n<h1 class=\"ui center aligned header\">Wizard</h1>\n\n<div class=\"ui text center aligned container\">\n    Form wizard example.\n</div>\n\n<div class=\"ui text container\">\n    <div class=\"ui segment\">\n        <div id=\"container\">\n            <script id=\"formTemplate\" type=\"text/x-handlebars-template\">\n                {{#if success}}\n                <h3 id=\"success\" class=\"ui header\">Thank you for reservation of {{values.amount}} items!</h3>\n                {{else}}\n                <form class=\"ui form {{#if errors}}error{{/if}}\" method=\"post\" action=\"{{submitUrl}}\">\n\n                    {{#if errors}}\n                    <div class=\"ui error message\">\n                        <ul class='list'>\n                            {{#each errors}}\n                            <li>{{this}}</li>\n                            {{/each}}\n                        </ul>\n                    </div>\n                    {{/if}}\n\n                    <p>Confirm the reservation of more that 50 items:</p>\n\n                    <div class=\"field {{#if errors.firstName}}error{{/if}}\">\n                        <div class=\"ui checkbox\">\n                            <input name=\"continue\" type=\"checkbox\" value=\"yes\"/>\n                            <label>Confirm</label>\n                        </div>\n                    </div>\n\n                    <button class=\"ui primary button\" type=\"submit\" onclick=\"handleSubmit(event)\">Submit</button>\n                </form>\n                {{/if}}\n            </script>\n        </div>\n    </div>\n\n    <img class=\"ui centered image small\" src=\"../shared/logo.png\"/>\n</div>\n\n<script>\n    init();\n</script>\n\n</body>\n</html>"
  },
  {
    "path": "examples/forms_wizard/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml forms target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/generic_triggers/README.md",
    "content": "# Example: Using Generic Event Trigger\n\nThe example shows how to subscribe to generic events.\n\n## Usage\n\nCreate a new project with a repository pointing to this example and\nsend a JSON request with the event's body:\n```\ncurl -v -u username -H 'Content-Type: application/json' \\\n-d '{\"myVar\": \"abc\", \"otherStuff\": [1, 2, 3]}' \\\nhttp://localhost:8001/api/v1/events/mySystem\n``` \n\nThe process log should contain a record similar to this:\n```\n[INFO ] c.w.concord.plugins.log.LoggingTask - Received {myVar=abc, otherStuff=[1, 2, 3]}\n```\n\nSend another request to test the second trigger:\n```\ncurl -v -u username -H 'Content-Type: application/json' \\\n-d '{\"myVar\": \"testing stuff\"}' \\\nhttp://localhost:8001/api/v1/events/mySystem\n```"
  },
  {
    "path": "examples/generic_triggers/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Running the default flow...\"\n\n  onEvent:\n  - log: \"Received ${event}\"\n\n  onEvent2:\n  - log: \"${msg}\"\n\ntriggers:\n  - mySystem:\n      # listen for \"mySystem\" events that have myVar=abc\n      myVar: \"abc\"\n      entryPoint: onEvent\n\n  - mySystem:\n      # using regular expressions to match the data\n      myVar: \"test.*\"\n      entryPoint: onEvent2\n      # passing additional variables\n      arguments:\n        msg: \"We got ${event}\"\n\n"
  },
  {
    "path": "examples/git/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://com.walmartlabs.concord.plugins:git:1.32.3\"\n\nflows:\n  default:\n  # cloning a repository\n  - task: git\n    in:\n      action: clone\n      url: git@github.com:myorg/myrepo.git\n      privateKey:\n        org: myOrg # optional\n        secretName: mySecret\n        password: myPwd # optional\n      workingDir: myRepo\n  # the repo will be cloned into `myRepo` directory\n\n  # creating a new branch and pushing it to remote origin\n  - task: git\n    in:\n      action: createBranch\n      url: git@github.com:myorg/myrepo.git\n      privateKey:\n         org: myOrg # optional\n         secretName: mySecret\n         password: myPwd # optional\n      workingDir: myRepo\n      baseBranch: feature-a # optional name of the branch to use as the starting point for the new branch\n      newBranch: myNewBranch\n      pushBranch: true # set this parameter to 'false' if you do not want to push the new branch to the origin\n\n  # merging branches\n  - task: git\n    in:\n      action: merge\n      url: git@github.com:myorg/myrepo.git\n      privateKey:\n         org: myOrg # optional\n         secretName: mySecret\n         password: myPwd # optional\n      workingDir: myRepo\n      sourceBranch: feature-a\n      destinationBranch: myNewBranch\n\n  # creating a pull request\n  - task: github\n    in:\n      action: createPR\n      accessToken: myGitToken\n      org: myOrg\n      repo: myRepo\n      prTitle: \"my PullRequest Title\"\n      prBody: \"my PullRequest Body\"\n      prSourceBranch: mySource   # the name of the branch where your changes are implemented.\n      prDestinationBranch: master # the name of the branch you want the changes pulled into\n  # the ID of the created PR will be stored as `${prId}`\n\n  # merging a pull request\n  - task: github\n    in:\n      action: mergePR\n      accessToken: myGitToken\n      org: myOrg\n      repo: myRepo\n      prId: ${prId}\n\n  # create a tag based on a specific commit SHA\n  - task: github\n    in:\n      action: createTag\n      accessToken: myGitToken\n      org: myOrg\n      repo: myRepo\n      tagVersion: v0.0.1\n      tagMessage: \"Release 1.0.0\"\n      tagAuthorName: \"myUsedId\"\n      tagAuthorEmail: \"myEmail\"\n      commitSHA: ${gitHubBranchSHA}\n"
  },
  {
    "path": "examples/git/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/groovy/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://org.codehaus.groovy:groovy-all:pom:2.5.23\"\n\nflows:\n  default:\n  - script: groovy\n    body: |\n      execution.setVariable(\"x\", 123)\n\n  - script: groovy\n    body: |\n      // variables can be accessed via the context\n      println execution.getVariable(\"x\")\n\n      // ...or used directly\n      println x\n\n"
  },
  {
    "path": "examples/groovy/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/groovy_grape/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://org.codehaus.groovy:groovy-all:pom:2.5.23\"\n  - \"mvn://org.apache.ivy:ivy:2.4.0\"\n\nflows:\n  default:\n  - script: test.groovy\n\n"
  },
  {
    "path": "examples/groovy_grape/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml test.groovy target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/groovy_grape/test.groovy",
    "content": "// to use an internal repository\n// @GrabResolver(name = 'internal', root = 'https://repository.example.com/nexus/content/groups/public')\n@Grab(group='org.apache.commons', module='commons-lang3', version='3.7')\nimport org.apache.commons.lang3.RandomStringUtils\n\nprintln RandomStringUtils.random(10)"
  },
  {
    "path": "examples/groovy_rest/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://org.codehaus.groovy:groovy-all:pom:2.5.23\"\n  - \"mvn://io.github.http-builder-ng:http-builder-ng-core:0.16.1\"\n  - \"mvn://xml-resolver:xml-resolver:1.2\"\n\nflows:\n  default:\n  - script: groovy\n    body: |\n      import static groovyx.net.http.HttpBuilder.configure\n\n      def http = configure {\n        request.uri = \"http://localhost:8001\"\n      }\n\n      Map result = http.get(Map) {\n        request.uri.path = \"/api/v1/server/version\"\n      }\n\n      execution.setVariable(\"serverVersion\", result[\"version\"])\n\n  - log: \"Server's version: ${serverVersion}\"\n\n\n"
  },
  {
    "path": "examples/groovy_rest/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/hello_initiator/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Hello, ${initiator.displayName}\"\n"
  },
  {
    "path": "examples/hello_initiator/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/hello_world/concord.yml",
    "content": "flows:\n  default:\n    - log: \"Hello, ${name}!\"\n\nconfiguration:\n  arguments:\n    name: \"Concord\"\n"
  },
  {
    "path": "examples/hello_world/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/hello_world2/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Hello, ${myName}\"\n"
  },
  {
    "path": "examples/hello_world2/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip -F arguments.myName=Concord http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/http/README.md",
    "content": "# HTTP Task\n\nA simple example of interacting with a REST endpoint with the HTTP task.\n\n"
  },
  {
    "path": "examples/http/concord.yml",
    "content": "flows:\n  default:\n  - log: \"${http.asString('http://localhost:8001/api/v1/server/ping')}\"\n\n  - task: http\n    in:\n      method: GET\n      url: http://localhost:8001/api/v1/server/ping\n      response: json\n  - log: \"Response received: ${response}\"\n"
  },
  {
    "path": "examples/http/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/imports/concord.yml",
    "content": "imports:\n  # import a \"Hello, world!\" flow from a remote GIT repository\n  # and store it in the `${workDir}/concord/` directory\n  - git:\n      url: \"https://github.com/walmartlabs/concord.git\"\n      path: \"examples/hello_world\"\n\nconfiguration:\n  arguments:\n    name: \"you\"\n\n# running this example produces a `Hello, you!` log message.\n"
  },
  {
    "path": "examples/imports/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/in_variables/concord.yml",
    "content": "configuration:\n  arguments:\n    name: \"Concord\"\n\nflows:\n  default:\n  # override \"name\" variable\n  - call: sayHello\n    in:\n      name: \"${initiator.username}\"\n\n  # the set value will be kept after the call is ended\n\n  # will log \"Bye, admin\"\n  - sayBye\n\n  sayHello:\n  - log: \"Hello, ${name}\"\n\n  sayBye:\n  - log: \"Bye, ${name}\"\n\n"
  },
  {
    "path": "examples/in_variables/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/inventory/README.md",
    "content": "# Concord Inventory Example\n\n**Deprecated** use [the JSON store API](https://concord.walmartlabs.com/docs/getting-started/json-store.html).\n\n## Usage\n\n1. Create an inventory:\n```\n./create_inventory.sh localhost:8001\n```\n\n2. Start the process:\n```\n./run.sh localhost:8001\n```\n\n3. Check the logs in the Console"
  },
  {
    "path": "examples/inventory/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.10.1\"\n\nflows:\n  default:\n  - task: ansible\n    in:\n      # arguments: [org name], inventory name, ansible host group name, query name, query params, [additional inventory variables]\n      inventory: \"${inventory.ansible('Default', 'myInventory', 'myHostsGroup', 'endpointsByZypperVersion', {'facter_zypper_version': '1.6.333'})}\"\n      # will produce a JSON structure like this:\n      # {\n      #   \"myHostsGroup\": {\n      #     \"hosts\":[\"xx.xxx.xx.xxx\"]\n      #   },\n      #   \"_meta\": {\n      #     \"hostvars\":{\n      #       \"xx.xxx.xx.xxx\":{\n      #         \"ansible_connection\":\"local\"\n      #       }\n      #     }\n      #   }\n      # }\n\n      playbook: playbook/hello.yml\n      extraVars:\n        greetings: \"Hi there!\"\n"
  },
  {
    "path": "examples/inventory/create_inventory.sh",
    "content": "#!/bin/bash\n\nread -p \"Username: \" CURL_USER\nread -p \"Password:\" -s CURL_PASSOWRD\n\nINVENTORY_NAME=\"myInventory\"\nQUERY_NAME=\"endpointsByZypperVersion\"\nSERVER_ADDR=\"$1\"\n\n# create inventory\necho -e \"\\nCreating an inventory ...\"\ncurl -f -u ${CURL_USER}:${CURL_PASSOWRD} -H \"Content-Type: application/json\" -d \"{\\\"name\\\": \\\"$INVENTORY_NAME\\\"}\" \"http://${SERVER_ADDR}/api/v1/org/Default/inventory\"\n\n# create inventory data\necho -e \"\\nUploading inventory data isp.s01160.ca_ansiblefacts ...\"\ncurl -f -u ${CURL_USER}:${CURL_PASSOWRD} -H \"Content-Type: application/json\" -d @isp.s01160.ca_ansiblefacts.json \"http://${SERVER_ADDR}/api/v1/org/Default/inventory/$INVENTORY_NAME/data/s01160/ansible_facts\"\necho -e \"\\nUploading inventory data s05505.us_ansiblefacts ...\"\ncurl -f -u ${CURL_USER}:${CURL_PASSOWRD} -H \"Content-Type: application/json\" -d @isp.s05505.us_ansiblefacts.json \"http://${SERVER_ADDR}/api/v1/org/Default/inventory/$INVENTORY_NAME/data/s05505/ansible_facts\"\necho -e \"\\nUploading inventory data s00524.us_ansiblefacts ...\"\ncurl -f -u ${CURL_USER}:${CURL_PASSOWRD} -H \"Content-Type: application/json\" -d @rxp.s00524.us_ansiblefacts.json \"http://${SERVER_ADDR}/api/v1/org/Default/inventory/$INVENTORY_NAME/data/s00524/ansible_facts\"\n\n# create query\necho -e \"\\nCreating a named query ...\"\ncurl -f -u ${CURL_USER}:${CURL_PASSOWRD} -H \"Content-Type: text/plain\" --data-binary @query.sql \"http://${SERVER_ADDR}/api/v1/org/Default/inventory/$INVENTORY_NAME/query/${QUERY_NAME}\"\n"
  },
  {
    "path": "examples/inventory/isp.s01160.ca_ansiblefacts.json",
    "content": "{\n  \"host\": \"test1.mysite.com\",\n  \"site\": \"testsite\",\n  \"type\": [\n    \"worker\"\n  ],\n  \"zone\": \"testzone\",\n  \"_meta\": {\n    \"version\": 1\n  },\n  \"region\": \"\",\n  \"country\": \"us\",\n  \"profile\": \"prod\",\n  \"hostname\": \"testHost\",\n  \"provider\": \"testProvider\",\n  \"cluster_id\": \"testCluster\",\n  \"cluster_seq\": \"c1\",\n  \"ansible_host\": \"127.0.0.1\",\n  \"clusterInventoryRef\": \"testClusterInventory\"\n}\n"
  },
  {
    "path": "examples/inventory/isp.s05505.us_ansiblefacts.json",
    "content": "{\n  \"host\": \"test2.mysite.com\",\n  \"site\": \"testsite\",\n  \"type\": [\n    \"worker\"\n  ],\n  \"zone\": \"testzone\",\n  \"_meta\": {\n    \"version\": 1\n  },\n  \"region\": \"\",\n  \"country\": \"us\",\n  \"profile\": \"prod\",\n  \"hostname\": \"testHost\",\n  \"provider\": \"testProvider\",\n  \"cluster_id\": \"testCluster\",\n  \"cluster_seq\": \"c1\",\n  \"ansible_host\": \"127.0.0.1\",\n  \"clusterInventoryRef\": \"testClusterInventory\"\n}\n"
  },
  {
    "path": "examples/inventory/playbook/hello.yml",
    "content": "---\n- hosts: myHostsGroup\n  tasks:\n  - debug:\n      msg: \"{{ greetings }}\"\n      verbosity: 0\n"
  },
  {
    "path": "examples/inventory/query.sql",
    "content": "select cast(\n    json_build_object(\n        'host', a.item_data->'ansible_default_ipv4'->'address',\n        'ansible_connection', 'local'\n    ) as varchar)\nfrom inventory_data a\nwhere\n  item_path like '%/ansible_facts' and\n  item_data @> ?::jsonb"
  },
  {
    "path": "examples/inventory/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R playbook concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\necho \"Sending the payload...\"\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/inventory/rxp.s00524.us_ansiblefacts.json",
    "content": "{\n  \"host\": \"test3.mysite.com\",\n  \"site\": \"testsite\",\n  \"type\": [\n    \"worker\"\n  ],\n  \"zone\": \"testzone\",\n  \"_meta\": {\n    \"version\": 1\n  },\n  \"region\": \"\",\n  \"country\": \"us\",\n  \"profile\": \"prod\",\n  \"hostname\": \"testHost\",\n  \"provider\": \"testProvider\",\n  \"cluster_id\": \"testCluster\",\n  \"cluster_seq\": \"c1\",\n  \"ansible_host\": \"127.0.0.1\",\n  \"clusterInventoryRef\": \"testClusterInventory\"\n}\n"
  },
  {
    "path": "examples/inventory_lookup/README.md",
    "content": "# Concord Inventory Example\n\n**Deprecated** use [the JSON store API](https://concord.walmartlabs.com/docs/getting-started/json-store.html).\n\n## Usage\n\n1. Create an inventory:\n```\n./create_inventory.sh concord.example.com:8001\n```\n\n2. Start the process:\n```\n./run.sh concord.example.com:8001\n```\n\n3. Check the logs in the Console"
  },
  {
    "path": "examples/inventory_lookup/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.10.1\"\n\nflows:\n  default:\n  - task: ansible\n    in:\n      dockerImage: \"walmartlabs/concord-ansible:latest\"\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      playbook: playbook/hello.yml\n      extraVars:\n        greetings: \"Hi there!\"\n"
  },
  {
    "path": "examples/inventory_lookup/create_inventory.sh",
    "content": "#!/bin/bash\n\nread -p \"Username: \" CURL_USER\nread -p \"Password:\" -s CURL_PASSOWRD\n\nINVENTORY_NAME=\"myInventory2\"\nQUERY_NAME=\"lookupQuery\"\nSERVER_ADDR=\"$1\"\n\n# create inventory\necho -e \"\\nCreating an inventory ...\"\ncurl -f -u ${CURL_USER}:${CURL_PASSOWRD} -H \"Content-Type: application/json\" -d \"{\\\"name\\\": \\\"$INVENTORY_NAME\\\"}\" \"http://${SERVER_ADDR}/api/v1/org/Default/inventory\"\n\n# create query\necho -e \"\\nCreating a query ...\"\ncurl -f -u ${CURL_USER}:${CURL_PASSOWRD} -H \"Content-Type: text/plain\" --data-binary @query.sql \"http://${SERVER_ADDR}/api/v1/org/Default/inventory/$INVENTORY_NAME/query/${QUERY_NAME}\"\n"
  },
  {
    "path": "examples/inventory_lookup/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"greetings {{ lookup('concord_inventory', 'Default', 'myInventory2', 'lookupQuery result=0', {'qparam1':'qparam1-value'}) }}\"\n      # if the process was started using a project, then the org parameter can be omitted:\n      # msg: \"greetings {{ lookup('concord_inventory', 'myInventory2', 'lookupQuery result=0', {'qparam1':'qparam1-value'}) }}\"\n      verbosity: 0\n"
  },
  {
    "path": "examples/inventory_lookup/query.sql",
    "content": "select cast(?::jsonb as varchar)"
  },
  {
    "path": "examples/inventory_lookup/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R playbook concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\necho \"Sending the payload...\"\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/jira/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://com.walmartlabs.concord.plugins:jira-task:2.11.0\"\n\nflows:\n  default:\n\n  # create new issue\n  - task: jira\n    in:\n      action: \"createIssue\"\n      userId: \"${initiator.username}\"\n      password: \"myJiraPassword\" # should be encrypted and exported using `crypto` task\n      projectKey: \"MYPROJECTKEY\"\n      summary: \"mySummary\"\n      description: \"myDescription\"\n      requestorUid: \"${initiator.username}\"\n      issueType: \"Bug\"\n      priority: \"P4\"\n\n      # complex fields, the actual field names and values depend on the configuration of the JIRA instance\n      customFieldsTypeFieldAttr:\n        customfield_10212: # environment\n          value: \"Development\"\n\n        customfield_10216: # severity\n          value: \"4 - Cosmetic\"\n\n        customfield_20400: # application/service (array of values)\n          - \"2125921\"      # \"SDE - Concord\"\n\n  # the issue ID is stored as `issueId`\n  - log: \"Issue ID: ${issueId}\"\n\n  # add a comment\n  - task: jira\n    in:\n      action: \"addComment\"\n      userId: \"${initiator.username}\"\n      password: \"myJiraPassword\"\n      issueKey: \"${issueId}\"\n      comment: \"This is my comment from Concord\"\n\n  # transition to another status\n  - task: jira\n    in:\n      action: \"transition\"\n      userId: \"${initiator.username}\"\n      password: \"myJiraPassword\"\n      issueKey: \"${issueId}\"\n      transitionId: 321\n      transitionComment: \"Marking as Done\"\n      customFieldsTypeFieldAttr:\n        customfield_10229: # resolution\n          value: \"Done\"\n        customfield_20106: # release handling option\n          id: \"24226\"      # \"This is not going into production (ever)\"\n\n  # delete an existing issue\n  - task: jira\n    in:\n      action: \"deleteIssue\"\n      userId: \"${initiator.username}\"\n      password: \"myJiraPassword\"\n      issueKey: \"${issueId}\"\n\n  - log: \"Done!\"\n"
  },
  {
    "path": "examples/jira/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/juel_java_streams/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://org.codehaus.groovy:groovy-all:pom:2.5.23\"\n  arguments:\n    items1:\n    - \"a\"\n    - \"b\"\n    items2:\n    - \"a\"\n    - \"b\"\n    - \"c\"\n    items3: [1, 2, 3] # using a different syntax to make list\n\nflows:\n  default:\n  # using expressions\n  - log: \"${items3.stream().filter(i -> i % 2 == 0).toList()}\"\n\n  # using Java Streams API in Groovy\n  - script: groovy\n    # calculates the difference between items1 and items2\n    body: |\n      execution.setVariable(\"delta\",\n        items2.stream()\n              .filter { a -> !items1.contains(a) }\n              .collect())\n\n  - log: \"We got ${delta}\"\n\n"
  },
  {
    "path": "examples/juel_java_streams/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/ldap/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://com.walmartlabs.concord.plugins:ldap-task:2.11.0\"\n\nflows:\n  default:\n  - task: ldap\n    in:\n      action: getUser\n      ldapAdServer: \"ldap://ldaphost:port\"\n      bindUserDn: \"myBindUser\"\n      bindPassword: \"myBindPwd\"\n      searchBase: \"DC=MyOrg,DC=com\"\n      user: \"myUser\"\n\n  - log: \"${ldapResult}\"\n"
  },
  {
    "path": "examples/ldap/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/logback_config/_agent.json",
    "content": "{\n  \"jvmArgs\": [\"-Dlogback.configurationFile=my_logback.xml\"]\n}"
  },
  {
    "path": "examples/logback_config/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://org.codehaus.groovy:groovy-all:pom:2.5.23\"\n\nflows:\n  default:\n    - script: groovy\n      body: |\n        import org.slf4j.*\n\n        def logger = org.slf4j.LoggerFactory.getLogger(\"test\")\n        logger.debug(\"Hi there\")\n\n"
  },
  {
    "path": "examples/logback_config/my_logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>[%-5level] %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <logger name=\"test\" level=\"DEBUG\"/>\n\n    <root level=\"ERROR\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "examples/logback_config/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml my_logback.xml _agent.json target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/loglevel/concord.yml",
    "content": "configuration:\n  arguments:\n    name: \"Concord\"\n  runner:\n    logLevel: \"DEBUG\"\n\nflows:\n  default:\n    - ${log.debug(\"This is a debug log\")}\n    - ${log.info(\"This is a info log\")}\n    - ${log.warn(\"This is a warn log\")}\n    - ${log.error(\"This is a error log\")}\n\n    - logDebug: \"Hello, ${name}. This is a debug log\"\n    - log: \"Hello, ${name}. This is a normal log\"\n    - logWarn: \"Hello, ${name}. This is a warn log\"\n    - logError: \"Hello, ${name}. This is a error log\""
  },
  {
    "path": "examples/loglevel/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/long_running/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Taking a nap...\"\n  - ${sleep.ms(60000)}\n  - log: \"Done napping!\"\n\n"
  },
  {
    "path": "examples/long_running/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/loops/concord.yml",
    "content": "flows:\n  default:\n  # calling a task for each element\n  - task: log\n    in:\n      msg: ${item}\n    withItems:\n    - \"Hello!\"\n    - \"Bye!\"\n\n  # calling a flow for each element\n  - call: myFlow\n    withItems:\n    - \"first element\"\n    - \"second element\"\n\n  # using a variable\n  - call: myFlow\n    withItems: ${myItems}\n\n  myFlow:\n  - log: \"We got ${item}\"\n\nconfiguration:\n  arguments:\n    # withItems supports any item type\n    myItems:\n    - 100500\n    - false\n    - \"a string value\"\n"
  },
  {
    "path": "examples/loops/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/mocking/README.md",
    "content": "Example of how to replace a runtime dependency with a \"mock\" version.\nCan be useful for testing flows.\n\nNote the ` -F activeProfiles=test` in `run.sh` -- activates the test\nprofile which loads the task replacement."
  },
  {
    "path": "examples/mocking/concord.yml",
    "content": "configuration:\n  dependencies:\n    - \"mvn://org.codehaus.groovy:groovy-all:pom:2.5.23\"\n    - \"mvn://com.walmartlabs.concord.plugins.basic:dynamic-tasks:1.76.1\"\n    - \"mvn://com.walmartlabs.concord.plugins:git:1.32.3\"\n\nflows:\n  default:\n    - call: myFlow\n\n  # the flow we want to test\n  myFlow:\n    # normally, it would call the \"real\" task\n    - task: github\n      in:\n        action: mergePR\n        accessToken: \"...\"\n        org: myOrg\n        repo: myRepo\n        prId: 123\n\n  test:\n    - loadTasks: \"mocks\"\n    - try:\n      - call: myFlow\n      error:\n        - log: \"Failed as expected with ${lastError.cause}\"\n\nprofiles:\n  test:\n    configuration:\n      # for testing purposes we omit the github plugin, which is replaced with a \"mock\" version loaded dynamically\n      dependencies:\n        - \"mvn://org.codehaus.groovy:groovy-all:pom:2.5.23\"\n        - \"mvn://com.walmartlabs.concord.plugins.basic:dynamic-tasks:1.76.1\"\n      entryPoint: test\n"
  },
  {
    "path": "examples/mocking/mocks/github.groovy",
    "content": "import com.walmartlabs.concord.sdk.*\n\nimport javax.inject.Named\n\n@Named(\"github\")\nclass MyTask implements Task {\n\n    void execute(Context ctx) throws Exception {\n        throw new RuntimeException(\"Kablamo!\")\n    }\n}"
  },
  {
    "path": "examples/mocking/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml mocks target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F activeProfiles=test -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/multiple_flows/concord.yml",
    "content": "flows:\n  default:\n  - script: javascript\n    body: |\n      execution.setVariable(\"name\", \"Concord\");\n\n  # call another flow\n  - sayHello\n\n  # call another flow and add/override its variables\n  - call: sayHello\n    in:\n      name: \"world\"\n\n  sayHello:\n  - log: \"Hello, ${name}\"\n"
  },
  {
    "path": "examples/multiple_flows/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/noderoster/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://com.walmartlabs.concord.plugins.basic:noderoster-tasks:1.76.1\"\n\nflows:\n  default:\n    - call: findHostsWithArtifacts\n    - call: findFacts\n    - call: findDeployedOnHosts\n\n  findHostsWithArtifacts:\n  - task: noderoster\n    in:\n      action: \"hostsWithArtifacts\"\n      artifactPattern: \"storesystems\"\n  - log: \"${result}\"\n\n  findFacts:\n  - task: noderoster\n    in:\n      action: \"facts\"\n      hostName: \"host.example.com\"\n  - log: \"${result}\"\n\n  findDeployedOnHosts:\n  - task: noderoster\n    in:\n      action: \"deployedOnHost\"\n      hostName: \"host.example.com\"\n  - log: \"${result}\"\n"
  },
  {
    "path": "examples/noderoster/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/out/concord.yml",
    "content": "flows:\n  default:\n  - set:\n      x: 123\n      y:\n        some:\n          nested: [\"data\", \"in\", \"arrays\"]\n          boolean: true\n          number: 234\n"
  },
  {
    "path": "examples/out/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -v \\\n-u ${CURL_USER} \\\n-F archive=@target/payload.zip \\\n-F sync=true \\\n-F out=x,y,z \\\nhttp://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/out_groovy/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://org.codehaus.groovy:groovy-all:pom:2.5.23\"\n\nflows:\n  default:\n  - script: groovy\n    body: |\n      execution.setVariable(\"myVar\", \"myValue\");\n\n"
  },
  {
    "path": "examples/out_groovy/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -v \\\n-u ${CURL_USER} \\\n-F archive=@target/payload.zip \\\n-F sync=true \\\n-F out=myVar \\\nhttp://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/parsing_yaml_json/concord.yml",
    "content": "configuration:\n  arguments:\n    nested:\n      value: \"Hello!\"\n\nflows:\n  default:\n  # read a JSON file\n  # prints out '{value:${nested.value}}'\n  - log: \"${resource.asJson('my.json')}\"\n\n  # read a JSON file and evaluate all expressions\n  # prints out '{value:Hello!}'\n  - log: \"${resource.asJson('my.json', true)}\"\n\n  # read a YAML file\n  # prints out '{value:${nested.value}}'\n  - log: \"${resource.asYaml('my.yml')}\"\n\n  # read a YAML file and evaluate all expressions\n  # prints out '{value:Hello!}'\n  - log: \"${resource.asYaml('my.yml', true)}\"\n"
  },
  {
    "path": "examples/parsing_yaml_json/my.json",
    "content": "{\n  \"value\": \"${nested.value}\"\n}"
  },
  {
    "path": "examples/parsing_yaml_json/my.yml",
    "content": "value: \"${nested.value}\"\n"
  },
  {
    "path": "examples/parsing_yaml_json/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml my.json my.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/process_card_htmx/README.md",
    "content": "# Process Cards\n\n\"Process Cards\" are a little tiles on the Activity page of the Concord UI.\nThey can be used to start a predefined process with no parameters or to show a \"form\".\nThe form is a static HTML page that will be displayed when user clicks on the card.\nThe page can contain any HTML, CSS, and JavaScript code.\n\nThis particular example demonstrates how to use [HTMX](https://htmx.org/) to\nimplement a custom form to start a process.\n\nSteps:\n- create a new Concord project and register the Git repository with the flow\n  you want to run;\n- upload the form by calling the API (while in this directory):\n  ```\n  curl -i \\\n  -H 'Authorization: yourApiKey' \\\n  -F org=myOrg \\\n  -F project=myProject \\\n  -F repo=myRepo \\\n  -F entryPoint=myFlow \\\n  -F name=foobar \\\n  -F description=Test \\\n  -F form=@index.html \\\n  http://localhost:8001/api/v1/processcard\n  ```\n- log into the Concord UI and check the Activity page:\n  ![Process Card](images/card.png)\n- click on \"Start process\" and you should see the form:\n  ![Form](images/form.png)\n\nThe uploaded [index.html](index.html) file contains a simple form with a single input field.\nWhen the form is submitted, the `htmx:post` attribute triggers a POST request to the\n`/api/v1/process` endpoint with the form data.\n"
  },
  {
    "path": "examples/process_card_htmx/data.js",
    "content": "data = {\n    \"org\": \"Default\",\n    \"project\": \"test\",\n    \"repo\": \"test\",\n    \"entryPoint\": \"test\",\n\n    \"values\" : {\n        \"release\" : \"1.0.0\"\n    }\n};"
  },
  {
    "path": "examples/process_card_htmx/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>Process start form example</title>\n    <script src=\"data.js\"></script>\n    <script src=\"https://unpkg.com/htmx.org@1.9.11\"></script>\n    <script src=\"https://unpkg.com/htmx.org@1.9.11/dist/ext/client-side-templates.js\"></script>\n    <script src=\"https://unpkg.com/mustache@latest\"></script>\n    <style>\n        body {\n            background-color: white;\n            font-family: sans-serif;\n        }\n    </style>\n</head>\n<body hx-ext=\"client-side-templates\">\n\n<form id='form'\n      hx-encoding='multipart/form-data'\n      hx-post='/api/v1/process'\n      hx-swap=\"innerHTML\"\n      mustache-template=\"response-template\">\n    <div style=\"display: flex; flex-direction: column\">\n        <div style=\"padding: 10px\">\n            <label for=\"name\">Name</label>\n            <input id=\"name\" name=\"arguments.name\" type=\"text\" required/>\n        </div>\n        <input type=\"hidden\" name=\"org\"/>\n        <input type=\"hidden\" name=\"project\"/>\n        <input type=\"hidden\" name=\"repo\"/>\n        <input type=\"hidden\" name=\"entryPoint\"/>\n        <button style=\"padding: 10px\" type=\"submit\">Run process</button>\n    </div>\n</form>\n\n<template id=\"response-template\">\n    <div>\n        The process has been started, see <a href=\"/#/process/{{instanceId}}/log\" target=\"_parent\">the log</a> for\n        details.\n    </div>\n</template>\n\n<script>\n    document.getElementsByName('org')[0].value = data.org;\n    document.getElementsByName('project')[0].value = data.project;\n    document.getElementsByName('repo')[0].value = data.repo;\n    document.getElementsByName('entryPoint')[0].value = data.entryPoint;\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "examples/process_card_jquery/README.md",
    "content": "# Process Cards\n\nSee the description in the [process_card_htmx](../process_card_htmx) example.\n"
  },
  {
    "path": "examples/process_card_jquery/data.js",
    "content": "data = {\n    \"org\": \"Default\",\n    \"project\": \"test\",\n    \"repo\": \"test\",\n    \"entryPoint\": \"test\",\n\n    \"values\" : {\n        \"release\" : \"1.0.0\"\n    }\n};"
  },
  {
    "path": "examples/process_card_jquery/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>Process start form example</title>\n    <script src=\"data.js\"></script>\n\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.css\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\" />\n    <script src=\"https://code.jquery.com/jquery-3.7.1.min.js\" crossorigin=\"anonymous\"></script>\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.js\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\"></script>\n\n</head>\n<body>\n    <h1>Deployment info</h1>\n\n    <form id=\"myForm\" class=\"ui form\" enctype=\"multipart/form-data\">\n        <div class=\"ui error message\"></div>\n\n        <div class=\"field required\">\n            <label for=\"release\">Release version</label>\n            <input id=\"release\" type=\"text\" name=\"arguments.release\" placeholder=\"Release version\">\n            <div class=\"ui pointing red basic label release-error\" style=\"display: none;\">Please enter value</div>\n        </div>\n\n        <div class=\"field required\">\n            <label for=\"field1\">field1</label>\n            <input id=\"field1\" type=\"text\" name=\"arguments.field1\" placeholder=\"field1\">\n            <div class=\"ui pointing red basic label field1-error\" style=\"display: none;\">Please enter value</div>\n        </div>\n\n        <div class=\"field required\">\n            <label for=\"field2\">field1</label>\n            <input id=\"field2\" type=\"text\" name=\"arguments.field1\" placeholder=\"field2\">\n            <div class=\"ui pointing red basic label field2-error\" style=\"display: none;\">Please enter value</div>\n        </div>\n\n        <div class=\"field required\">\n            <label for=\"field3\">field1</label>\n            <input id=\"field3\" type=\"text\" name=\"arguments.field3\" placeholder=\"field3\">\n            <div class=\"ui pointing red basic label field3-error\" style=\"display: none;\">Please enter value</div>\n        </div>\n\n\n        <input id=\"org\" name=\"org\" type=\"hidden\"/>\n        <input id=\"project\" name=\"project\" type=\"hidden\"/>\n        <input id=\"repo\" name=\"repo\" type=\"hidden\"/>\n        <input id=\"entryPoint\" name=\"entryPoint\" type=\"hidden\"/>\n\n        <button id=\"submitButton\" type=\"button\" class=\"ui primary submit button\">Submit</button>\n    </form>\n\n<script>\n    const myForm = $('#myForm');\n    myForm\n        .form('set values', data.values)\n        .form('set values', data);\n\n    $('#submitButton').on('click', function() {\n        clearError();\n\n        let isValid = true;\n\n        const releaseField = $('#release');\n        if (releaseField.val() === '') {\n            releaseField.parent().addClass('error');\n            $('.release-error').show();\n            isValid = false;\n        } else {\n            releaseField.removeClass('error');\n            $('.release-error').hide();\n        }\n\n        const field1Field = $('#field1');\n        if (field1Field.val() === '') {\n            field1Field.parent().addClass('error');\n            $('.field1-error').show();\n            isValid = false;\n        } else {\n            field1Field.removeClass('error');\n            $('.field1-error').hide();\n        }\n\n        if (!isValid) {\n            return;\n        }\n\n        formLoading(true);\n\n        $.ajax({\n            type: 'POST',\n            url: `/api/v1/process`,\n            data: new FormData(myForm[0]),\n            processData: false,\n            contentType: false,\n            success: function(response) {\n                console.log(response);\n\n                if (response.ok) {\n                    window.parent.window.location.href = `${window.location.origin}/#/process/${response.instanceId}/log`;\n                    // window.parent.window.location.href = `http://localhost:3000/#/process/${response.instanceId}/log`;\n                } else {\n                    formLoading(false);\n\n                    let ul = $('<ul>');\n                    $.each(response.errors, function(index, err) {\n                        ul.append($('<li>').text(err));\n                    });\n\n                    $('.ui.error.message')\n                        .empty()\n                        .append(ul)\n                        .show();\n                }\n            },\n            error: function(error) {\n                console.log(error)\n\n                showError('Error starting process', '[' + error.status + ']: ' + error.responseText);\n\n                formLoading(false);\n            }\n        });\n    });\n\n    $('#release').on('input', function() {\n        $(this).parent().removeClass('error');\n        $('.release-error').hide();\n    });\n\n    $('#field1').on('input', function() {\n        $(this).parent().removeClass('error');\n        $('.field1-error').hide();\n    });\n\n    const formLoading = (loading) => {\n        const myForm = $('#myForm');\n        if (loading) {\n            myForm.addClass('loading')\n        } else {\n            myForm.removeClass('loading')\n        }\n    };\n\n    const clearError = () => {\n        $('.ui.error.message')\n            .hide()\n            .empty();\n    };\n\n    const showError = (header, err) => {\n        $('.ui.error.message')\n            .show()\n            .text(header + ': ' + err);\n    };\n</script>\n\n</body>\n</html>"
  },
  {
    "path": "examples/process_from_a_process/concord.yml",
    "content": "flows:\n  default:\n  # executes the provided payload archive as a child process\n  - task: concord\n    in:\n      action: start\n      archive: example.zip\n      # wait for completion\n      sync: true\n  - log: \"Done! ${jobs[0]} is completed\"\n"
  },
  {
    "path": "examples/process_from_a_process/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml example.zip target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/process_from_a_process2/concord.yml",
    "content": "flows:\n  default:\n  # starts a process and retrieves it's variable value\n  - task: concord\n    in:\n      action: start\n      archive: out.zip\n      sync: true\n      # list of output variables\n      outVars:\n      - result\n  - log: \"${jobOut.result}\"\n\n  # starts a process from an existing project (it must be created beforehand)\n  - task: concord\n    in:\n      action: start\n      project: test\n      repository: default\n      sync: true\n  - log: \"Done! ${jobs[0]} is completed\"\n"
  },
  {
    "path": "examples/process_from_a_process2/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml out.zip target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/process_from_a_process3/concord.yml",
    "content": "flows:\n  default:\n  # uses the specified directory as the process payload\n  - task: concord\n    in:\n      action: start\n      payload: example\n      arguments:\n        name: \"Concord\"\n      sync: true\n  - log: \"Done! ${jobs[0]} is completed\"\n"
  },
  {
    "path": "examples/process_from_a_process3/example/concord.yml",
    "content": "flows:\n  default:\n  # use a local file and a process argument to create the message\n  - log: \"${resource.asString('file.txt')}, ${name}!\"\n"
  },
  {
    "path": "examples/process_from_a_process3/example/file.txt",
    "content": "Hello"
  },
  {
    "path": "examples/process_from_a_process3/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml example target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/process_meta/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Hello, ${name}!\"\n  - form: myForm\n  - log: \"Action: ${myForm.action}!\"\n\nforms:\n  myForm:\n  - action: {label: \"Action\", type: \"string?\", allow: [\"Approve\", \"Reject\"]}\n\nconfiguration:\n  arguments:\n    name: \"Concord\"\n  meta:\n    test: \"INIT\"\n    myForm.action: \"xxxx\"\n"
  },
  {
    "path": "examples/process_meta/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F org=Default -F project=test -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/profiles/concord/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Hello, ${name}!\"\n\nconfiguration:\n  arguments:\n    name: \"Concord\"\n"
  },
  {
    "path": "examples/profiles/concord.yml",
    "content": "configuration:\n  arguments:\n    name: \"Concord\"\n\nprofiles:\n  stranger:\n    configuration:\n      arguments:\n        name: \"stranger\"\n\nflows:\n  default:\n  - log: \"Hello, ${name}\""
  },
  {
    "path": "examples/profiles/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip -F activeProfiles=stranger http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/project_file/_main.json",
    "content": "{\n  \"activeProfiles\": [\"example\"]\n}\n"
  },
  {
    "path": "examples/project_file/concord.yml",
    "content": "configuration:\n  arguments:\n    name: stranger\n\nprofiles:\n  example:\n    configuration:\n      arguments:\n        name: world\n\nflows:\n  default:\n  - log: Hello, ${name}"
  },
  {
    "path": "examples/project_file/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml _main.json _agent.json target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/python_script/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://org.python:jython-standalone:2.7.4\"\n  arguments:\n    x: 2\n\nflows:\n  default:\n  - script: example.py\n  - log: \"The result: ${y}\"\n"
  },
  {
    "path": "examples/python_script/example.py",
    "content": "import sys\nsys.path.append(\".\") # add local directory as a module path\n\nimport my_module\n\n# 'execution' is the Concord process' context, 'x' is a Concord process' variable\nmy_module.test(execution, x)\n"
  },
  {
    "path": "examples/python_script/my_module.py",
    "content": "def test(ctx, x):\n    print 'Hello from another module!'\n    ctx.setVariable('y', x + 3)\n"
  },
  {
    "path": "examples/python_script/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml *.py target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/ruby/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://org.jruby:jruby:9.1.17.0\"\n\nflows:\n  default:\n  - script: ruby\n    body: |\n      puts \"Hello!\"\n      $execution.setVariable(\"test\", \"foo\");\n  - log: \"done: ${test}\""
  },
  {
    "path": "examples/ruby/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/runtime-v2/a_basic_example/README.md",
    "content": "# About Runtime V2\n\nStarting from version 1.57.0, Concord introduces a new runtime for process execution. This runtime v2 allows robust\nperformance and error handling coupled with improved user experience.\n\nThe new runtime’s features requires changes in flows and plugins. That’s why initially it will be an opt-in feature - \nboth v1 and v2 versions will coexist for foreseeable future.\n\nCurrently, the v2 runtime is considered a “preview” feature and is subject to change.\n\nTo enable the v2 runtime, add the following to your concord.yml file:\n```yaml\nconfiguration:\n  runtime: \"concord-v2\"\n```\n\nAlternatively, it is possible to specify the runtime parameter’s value in the API request:\n```\n$ curl ... -F runtime=concord-v2 http://concord.example.com/api/v1/process\n```\n\nLearn more using the [runtime v2 overview](http://concord.walmart.com/docs/processes-v2/index.html) and the \n[migration guide](http://concord.walmart.com/docs/processes-v2/migration.html) docs."
  },
  {
    "path": "examples/runtime-v2/a_basic_example/concord/example.concord.yml",
    "content": "flows:\n  default:\n    - set:\n        url: https://concord.prod.walmart.com\n\n# in v2, variables are scoped to a flow, unlike global variables in v1\n\n    - task: http\n      in:\n        url: \"${url}\"   # required to be passed. in v1, this was implicit\n        method: \"GET\"\n      out: response\n\n    - log: \"Response Code: ${response.statusCode}\"\n\n# Task inputs are explicit in v2 – all required parameters must be specified in the `in` block\n    - name: Log Me!    # log segments can be named\n      task: log\n      in:\n        msg: \"Hello! I'm being logged in a separate (and named!) segment!\"\n        level: \"WARN\"\n\n# separate and customizable log segments allow cleaner readability and log management\n\n    - call: flow-v2\n\n  flow-v2:\n    - try:\n        - log: \"${invalid expression}\"\n      error:\n        - log: \"${lastError}\" # will print error log\n\n# In v2 all ${lastError} values are the original exceptions thrown by tasks or expressions"
  },
  {
    "path": "examples/runtime-v2/a_basic_example/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\npublicFlows:\n  - default\n\n# In v2, flows listed in the `publicFlows` section are the only flows allowed as entry point values.\n# This also limits the flows listed in the repository run dialog.\n# When the `publicFlows` section is omitted, Concord considers all flows as public."
  },
  {
    "path": "examples/runtime-v2/a_basic_example/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/runtime-v2/ansible_out_vars/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  dependencies:\n  - \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.36.0\"\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: \"playbook/hello.yml\"\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      outVars:\n      - \"myVar\" # created using the `register` statement in the playbook\n    out: ansibleResult\n\n  # `myVar` contains the variable values for all hosts in the play\n  - log: \"${ansibleResult.myVar['127.0.0.1']['msg']}\"\n"
  },
  {
    "path": "examples/runtime-v2/ansible_out_vars/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"Hi there!\"\n      verbosity: 0\n    register: myVar\n"
  },
  {
    "path": "examples/runtime-v2/ansible_out_vars/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml playbook target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/runtime-v2/demo-flow/README.md",
    "content": "## Runtime v2\n\nThis demo flow intends to highlight the salient features introduced by Concord's runtime-v2 and can act as a helpful\nguide to migrate from v1 DSL to v2 syntax for Concord users/stakeholders.\n\n> The `errorFlow` flow has been commented as it will cause an error. Uncomment the flow lines\n> to see error handling improvements introduced in runtime-v2.\n\n\n"
  },
  {
    "path": "examples/runtime-v2/demo-flow/concord/forms.concord.yml",
    "content": "flows:\n\n  # easier to access form links in v2\n  # execute forms in parallel\n  formFlow:\n    - log: \"Going to do some form stuff...\"\n    - parallel:\n        - block:\n            - form: getFullName\n            - log: \"${getFullName.firstName}\"\n        - block:\n            - form: fetchMetadata\n            - log: \"${fetchMetadata.age}\"\n        - block:\n            - form: approvalCRQ\n            - log: \"${approvalCRQ.password}\"\n  # form wizard will be rendered in alphabetical order when used with parallel step\n\nforms:\n  getFullName:\n    - firstName: { label: \"First Name\", type: \"string\" }\n    - lastName: { label: \"Last Name\", type: \"string\" }\n  fetchMetadata:\n    - age: { label: \"Age\", type: \"int\", min: 21, max: 100 }\n    - favouriteColour: { label: \"Favourite colour\", type: \"string\", allow: [\"gray\", \"grey\"], search: true }\n  approvalCRQ:\n    - password: { label: \"Password\", type: \"string\", inputType: \"password\" }\n    - rememberMe: { label: \"Remember me\", type: \"boolean\" }"
  },
  {
    "path": "examples/runtime-v2/demo-flow/concord/test.concord.yml",
    "content": "flows:\n  externalFlow:\n\n    # named log segments\n    - name: ${item}\n      task: log\n      in:\n        msg: \"Hello! I'm being logged in a separate segment named ${item}!\"\n      withItems:\n        - \"Run Validation\"\n        - \"Trigger Deployment\"\n        - \"Send Notification\"\n        \n    # Other UI/UX features:\n       # show effective concord yaml\n       # Show task debug params by recording in/out vars\n       # View yaml line number in the Events tab\n\n    # parallel execution\n    # The v2 runtime was designed with parallel execution in mind. It adds a new step - parallel.\n    - parallel:\n        - ${sleep.ms(5000)}\n        - ${sleep.ms(5000)}\n    # executes each expression in its own Java thread\n\n    - log: \"Total sleeping duration should be 5 seconds!\"\n\n    # parallel execution in loop\n    - task: http\n      in:\n        url: https://concord.${item}.walmart.com/\n        method: \"GET\"\n        debug: true\n      out: results # withItems turns \"results\" into a list of results for each item\n      parallelWithItems:\n        - \"prod\"\n        - \"test\"\n        - \"ci\"\n    - log: ${results.stream().map(o -> o.statusCode).toList()}\n"
  },
  {
    "path": "examples/runtime-v2/demo-flow/concord.yml",
    "content": "# how to enable v2\nconfiguration:\n  runtime: \"concord-v2\"\n  entryPoint: \"aFlow\"\n  events:\n    recordTaskInVars: true\n    recordTaskOutVars: true\n  dependencies:\n    - \"mvn://org.codehaus.groovy:groovy-all:pom:2.5.23\"\n\n# new directory structure\n# new SDK\n\n  arguments:\n    url: \"https://rubygems.org\"\n    myVar: \"world\"\n    newVar: \" Hello earth\"\n\n# restricted entrypoints. if not specified, all flows are public and can be used as an entrypoint\npublicFlows:\n  - default\n  - aFlow\n\nflows:\n  default:\n\n    # The \"allVariables()\" function returns a Java Map with all the provided process variables\n    # The \"hasVariable()\" function accepts a string parameter and returns true if the variable exists\n\n    - log: \"All variables: ${allVariables()}\"\n\n    - if: ${hasVariable('projectInfo.orgName')}\n      then:\n        - log: \"Yep, we have 'orgName' variable with value ${projectInfo.orgName}\"\n      else:\n        - log: \"Nope, we do not have 'orgName' variable\"\n\n    - call: externalFlow\n    - call: scriptFlow\n    - call: formFlow\n    - call: errorFlow\n\n  # variable scoping changes\n  aFlow:\n    - set:\n        x: 123\n\n    - log: \"${x}\" # prints out \"123\"\n\n    - call: anotherFlow\n\n    - log: \"${x}\" # prints out \"123\"\n\n    - call: anotherFlow\n      out: x  # required if output needs to be used in the parent flow\n\n    - log: \"${x}\" # prints out \"789\"\n\n    - call: taskFlow\n\n  anotherFlow:\n    - log: \"${x}\" # prints out \"123\"\n    - set:\n        x: 789\n\n  # explicit task inputs\n  taskFlow:\n    - set:\n        url: https://github.com\n\n    - name: \"Concord Endpoint Call\"\n      task: http\n      in:\n        url: \"${url}\" # will not use the global variable as tasks now use local variables\n        method: \"GET\" # need to explicitly specify each input parameter\n        debug: true\n      out: response # need to explicitly specify output var\n    - if: ${response.ok} # in v2, http response object contains the `ok` attribute instead of the `success` attribute in v1\n      then:\n        - log: \"Concord Endpoint Status: ${response.statusCode}\"\n      else:\n        - log: \"Something went wrong!\"\n\n    - call: default\n    \n  # get and set script variables\n  scriptFlow:\n    - script: scripts/test-script.groovy\n      out: newVar\n      meta:\n        segmentName: \"Processing ${myVar} Script Variable...\"\n      error:\n        - log: \"${lastError}\" # prints original exception, nothing added by concord\n      \n    - log: \"The new value is: ${newVar}\" # prints new value set inside the groovy script\n\n  # improved error syntax\n  errorFlow:\n    - log: \"uncomment the lines to see improved error messages!\"\n#     - if: \"${true}\"\n#       then:\n#         log: \"It's true!\"   # the \"then\" block should’ve been a list\n# # this will throw an error with the problematic line and path\n"
  },
  {
    "path": "examples/runtime-v2/demo-flow/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord scripts concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/runtime-v2/demo-flow/scripts/test-script.groovy",
    "content": "// get a variable\ndef v = myVar\nprintln('Hello ' + v)\n\n// set a variable\nresult.set('newVar', 'Hello, world!')\n"
  },
  {
    "path": "examples/runtime-v2/mocks/main.concord.yaml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  # flow that we will test\n  checkService:\n    - task: \"http\"\n      in:\n        method: GET\n        url: \"http://localhost:8001/api/v1/server/ping\"\n        response: json\n      out: response\n\n    - if: \"${!response.ok}\"\n      then:\n        - name: \"Service unavailable\"\n          throw: \"Service unavailable\"\n\n    - if: \"${!response.content.ok}\"\n      then:\n        - throw: \"Service not ok\"\n\n\n"
  },
  {
    "path": "examples/runtime-v2/mocks/run-tests.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R main.concord.yaml tests target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F entryPoint=allTests -F concord.yaml=@tests-runner.concord.yaml -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/runtime-v2/mocks/tests/main.tests.concord.yaml",
    "content": "profiles:\n  testCheckServiceUnavailable:\n    configuration:\n      arguments:\n        mocks:\n          - name: \"http\"\n            in:\n              url: \".*\"\n              method: \"GET\"\n            out:\n              ok: false\n              statusCode: 500\n\n  testCheckServiceResponseTypeArg:\n    configuration:\n      arguments:\n        mocks:\n          - name: \"http\"\n            in:\n              url: \".*\"\n              method: \"GET\"\n            out:\n              ok: true\n              content:\n                ok: true\n            inputStoreId: \"httpTaskInput\"   # should be unique for flow run\n\n  testCheckServiceHttpException:\n    configuration:\n      arguments:\n        mocks:\n          - name: \"http\"\n            in:\n              url: \".*\"\n              method: \"GET\"\n            throwError: \"BOOM\"\n\nflows:\n  testCheckServiceUnavailable:\n    - try:\n        - call: checkService\n\n        - throw: \"Exception expected\"   # TODO: implement `asserts.fail('')` so that this exception is not caught in the error block...\n      error:\n        - name: \"Assert error message\"\n          expr: \"${asserts.assertEquals('Service unavailable', lastError.message)}\"\n\n  # how to capture and assert task input args\n  testCheckServiceResponseTypeArg:\n    - call: checkService\n\n    - set:\n        capturedInput: \"${mockInputs.get('httpTaskInput')}\" # inputStoreId\n\n    - name: \"Assert http task response type\"\n      expr: \"${asserts.assertEquals('json', capturedInput.response)}\"\n\n  testCheckServiceHttpException:\n    - try:\n        - call: checkService\n\n        - throw: \"Exception expected\"\n      error:\n        - name: \"Assert error message\"\n          expr: \"${asserts.assertEquals('BOOM', lastError.message)}\"\n"
  },
  {
    "path": "examples/runtime-v2/mocks/tests-runner.concord.yaml",
    "content": "configuration:\n  debug: true\n  runtime: concord-v2\n  events:\n    recordTaskInVars: true\n    recordTaskOutVars: true\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.plugins.basic:mock-tasks:2.13.0\"\n    - \"mvn://com.walmartlabs.concord.plugins.basic:asserts-tasks:2.12.0\"\n\nresources:\n  concord:\n    - \"glob:{*.,}concord.yaml\"\n    - \"glob:tests/{**/,}{*.,}concord.yaml\"\n\nflows:\n  # tests runner\n  allTests:\n    - name: \"Loading all test flows\"\n      script: \"js\"\n      body: |\n        var testFlows = Java.from(context.execution().processDefinition().flows().entrySet().stream().toList())\n          .filter(e => !e.getValue().isEmpty())\n          .filter(e => e.getValue().get(0).getLocation().fileName().startsWith('tests/'))\n          .filter(e => e.getKey() != 'allTests')\n          .filter(e => e.getKey().startsWith('test'))\n          .map(e => e.getKey())\n\n        result.set(\"flows\", testFlows);\n        result.set(\"processDefinition\", context.execution().processDefinition());\n      out: tests\n\n    # tests as forks\n    - name: \"Starting test ${item} as fork\"\n      task: concord\n      in:\n        action: fork\n        entryPoint: \"${item}\"\n#        activeProfiles: \"${item}\"    # TODO: allow active profiles for forks?\n        arguments: \"${tests.processDefinition.profiles().get(item).configuration.arguments}\"\n      loop:\n        items: \"${tests.flows}\"\n        mode: parallel\n\n    # tests in a loop\n    - name: \"Starting test ${item}\"\n      call: \"${item}\"\n      in: \"${tests.processDefinition.profiles().get(item).configuration.arguments}\"\n      loop:\n        items: \"${tests.flows}\"\n        mode: parallel\n"
  },
  {
    "path": "examples/runtime-v2/out_groovy/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  dependencies:\n  - \"mvn://org.codehaus.groovy:groovy-all:pom:2.5.23\"\n\nflows:\n  default:\n  - script: groovy\n    body: |\n      result.set(\"myVar\", \"myValue\");\n    out: scriptResult\n\n  - log: \"result: ${scriptResult}\"    # result: {myVar=myValue}\n\n  - script: groovy\n    body: |\n      result.set(\"myVar\", \"myValue\");\n    out:\n      myVar: ${result.myVar}\n\n  - log: \"myVar: ${myVar}\"    # myVar: myValue\n\n"
  },
  {
    "path": "examples/runtime-v2/out_groovy/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -v \\\n-u ${CURL_USER} \\\n-F archive=@target/payload.zip \\\n-F out=myVar \\\nhttp://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/runtime-v2/out_js/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n  - script: js\n    body: |\n      result.set(\"myVar\", \"myValue\");\n    out: scriptResult\n\n  - log: \"result: ${scriptResult}\"    # result: {myVar=myValue}\n\n  - script: js\n    body: |\n      result.set(\"myVar\", \"myValue\");\n    out:\n      myVar: ${result.myVar}\n\n  - log: \"myVar: ${myVar}\"    # myVar: myValue\n"
  },
  {
    "path": "examples/runtime-v2/out_js/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/runtime-v2/out_python/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  dependencies:\n  - \"mvn://org.python:jython-standalone:2.7.4\"\n\nflows:\n  default:\n  - script: python\n    body:\n      result.set(\"myVar\", \"myValue\");\n    out: scriptResult\n\n  - log: \"result: ${scriptResult}\"    # result: {myVar=myValue}\n\n  - script: python\n    body:\n      result.set(\"myVar\", \"myValue\");\n    out:\n      myVar: ${result.myVar}\n\n  - log: \"myVar: ${myVar}\"    # myVar: myValue\n"
  },
  {
    "path": "examples/runtime-v2/out_python/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/runtime-v2/out_ruby/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  dependencies:\n  - \"mvn://org.jruby:jruby:9.1.17.0\"\n\nflows:\n  default:\n  - script: ruby\n    body: |\n      $result.set(\"myVar\", \"myValue\");\n    out: scriptResult\n\n  - log: \"result: ${scriptResult}\"    # result: {myVar=myValue}\n\n  - script: ruby\n    body: |\n      $result.set(\"myVar\", \"myValue\");\n    out:\n      myVar: ${result.myVar}\n\n  - log: \"myVar: ${myVar}\"    # myVar: myValue\n"
  },
  {
    "path": "examples/runtime-v2/out_ruby/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/runtime-v2/parallel_execution/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n\n    # The v1 runtime provides no satisfactory way to run flow steps in parallel in one single process.\n    # The v2 runtime was designed with parallel execution in mind. It adds a new step - parallel.\n    - parallel:\n        - ${sleep.ms(5000)}\n        - ${sleep.ms(5000)}\n\n    - log: \"Total sleeping duration should be 5 seconds!\"\n\n    # sequence of tasks can be run inside the block statement of the parallel step\n    - parallel:\n        - block:\n            - name: \"HTTP Google Segment\"\n              task: http\n              in:\n                url: https://google.com/\n                method: \"GET\"\n              out: googleResponse\n            - log: \"Google: ${googleResponse.statusCode}\"\n\n        - block:\n            - name: \"HTTP Bing Segment\"\n              task: http\n              in:\n                url: https://bing.com/\n                method: \"GET\"\n              out: bingResponse\n            - log: \"Bing: ${bingResponse.statusCode}\"\n\n      out:\n        - googleResponse\n        - bingResponse\n\n    - log: |\n        Google: ${googleResponse}\n        Bing: ${bingResponse}"
  },
  {
    "path": "examples/runtime-v2/parallel_execution/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/runtime-v2/python_script/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n  dependencies:\n  - \"mvn://org.python:jython-standalone:2.7.4\"\n  arguments:\n    x: 2\n\nflows:\n  default:\n  - script: example.py\n  - log: \"The result: ${y}\"\n  - log: \"The result2: ${y2}\""
  },
  {
    "path": "examples/runtime-v2/python_script/example.py",
    "content": "import sys\nsys.path.append(\".\") # add local directory as a module path\n\nimport my_module\n\n# 'execution' is the Concord process' context, 'x' is a Concord process' variable\nmy_module.test(execution, x)\n\nexecution.variables().set('y2', my_module.test2(x))"
  },
  {
    "path": "examples/runtime-v2/python_script/my_module.py",
    "content": "def test(ctx, x):\n    print 'Hello from another module!'\n    ctx.variables().set('y', x + 3)\n    return\n\ndef test2(x):\n    print 'Hello from another module!'\n    return x + 3\n"
  },
  {
    "path": "examples/runtime-v2/python_script/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/script_url/concord.yml",
    "content": "flows:\n  default:\n  # the script file has to be served by some external web server\n  - script: \"http://localhost:8000/example.groovy\"\n\nconfiguration:\n  dependencies:\n  - \"mvn://org.codehaus.groovy:groovy-all:pom:2.5.23\"\n  arguments:\n    name: \"${initiator.displayName}\""
  },
  {
    "path": "examples/script_url/example.groovy",
    "content": "println \"Hello, ${name}!\"\n"
  },
  {
    "path": "examples/script_url/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/secret_files/README.md",
    "content": "# Secrets\n\nExample of exporting \"secrets\" as files.\n\n## Running\n\n1. upload a couple of files as secrets:\n```\n$ curl -u AD_USERNAME -F name=myFileA -F type=DATA -F storePassword=12345678 -F data=@myFileA.txt 'http://localhost:8001/api/v1/org/Default/secret'\n$ curl -u AD_USERNAME -F name=myFileB -F type=DATA -F storePassword=12345678 -F data=@myFileB.txt 'http://localhost:8001/api/v1/org/Default/secret'\n\n{\n  \"id\" : \"8febf8ae-0511-11e8-8c13-fa163ec7b48b\",\n  \"result\" : \"CREATED\",\n  \"password\" : \"12345678\",\n  \"ok\" : true\n}\n```\n\nThe `storePassword` value is the password you must use to decrypt/export the secret later.\n\n2. start the process:\n```\n$ ./run.sh localhost:8001\n{\n  \"instanceId\" : \"8ea63d60-10f5-43dd-ba8b-87150fb20182\",\n  \"ok\" : true\n}\n```\n\n3. open [the UI](http://localhost:8001), find the process entry and\nopen its log. You should see messages like these:\n```\n13:51:54 [INFO ] My file A: .tmp/file698533367913439655.bin\n13:51:54 [INFO ] My file B: .tmp/file4802153809032830974.bin\n```\n"
  },
  {
    "path": "examples/secret_files/concord.yml",
    "content": "configuration:\n  arguments:\n    # alternatively, a form, an encrypted value or an external\n    # service can be used to retrieve the password\n    pwd: \"12345678\"\n\nflows:\n  default:\n\n  # exporting secrets as files\n  - set:\n      myFileA: ${crypto.exportAsFile('myFileA', pwd)}\n      myFileB: ${crypto.exportAsFile('myFileB', pwd)}\n\n  # the resulting variables will contain the path of the exported files\n  - log: \"My file A: ${myFileA}\"\n  - log: \"My file B: ${myFileB}\"\n"
  },
  {
    "path": "examples/secret_files/myFileA.txt",
    "content": "FILE A"
  },
  {
    "path": "examples/secret_files/myFileB.txt",
    "content": "FILE B"
  },
  {
    "path": "examples/secret_files/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/secret_lookup/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.10.1\"\n\nflows:\n  default:\n  - task: ansible\n    in:\n      dockerImage: \"walmartlabs/concord-ansible:latest\"\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        greetings: \"Hi there!\"\n"
  },
  {
    "path": "examples/secret_lookup/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      # the org name can be optional if the process was started from a project\n      msg: \"We got {{ lookup('concord_data_secret', 'Default', 'mySecret', 'myPassword') }}\"\n      verbosity: 0\n  - debug:\n      # password can be set to None if secret was created without password.\n      msg: \"We got {{ lookup('concord_data_secret', 'mySecret', None) }}\"\n      verbosity: 0\n  - debug:\n      # the org name can be optional if the process was started from a project\n      msg: \"We got {{ lookup('concord_public_key_secret', 'Default', 'mySecret') }}\"\n      verbosity: 0\n"
  },
  {
    "path": "examples/secret_lookup/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml playbook target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/secrets/README.md",
    "content": "# Secrets\n\nExample of using \"secrets\" in processes.\n\n## Running\n\n1. create a new SSH key pair:\n```\n$ curl -H \"Authorization: API_TOKEN\" -F storePassword=12345678 -F name=myKey -F type=KEY_PAIR 'http://localhost:8001/api/v1/org/Default/secret'\n{\n  \"name\" : \"myKey\",\n  \"publicKey\" : \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCmV6+q6Uh8j8GYl0nzcwTGpjBwY1Dvv3QIAfmdwC8N6HredMl5hV3RCtpplYR7aItTorWVUYF1MMmXYKr6tjgU9hha2N2NogRjgPWzSVuR8GVa7CF155NB4nlUxt5cGidLj5Uwmy/uQm4Mni5pg/kZMGyIf+gMmcuQXDG3TOHwmJ48HOrpqxkUKaft3SYYOy7F8TjWFnmyXNlMCskSEJd5XdLDhyuDhDEXGpDSsT1brsq0WRXtFyBDjjNeYfI4J9jyOCpAXzbDCt7eYoYK+kod/b6RV8nbaxWALx2fwJS0bDhV3a9chwEyat24Ml66Z5LfCabCE7SGpFhTas56xYkH concord-server\",\n  \"exportPassword\" : \"12345678\",\n  \"ok\" : true\n}\n```\n\nThe `storePassword` value is the password you must use to decrypt/export the secret later.\n\n2. create a new username/password pair:\n```\n$ curl -H \"Authorization: API_TOKEN\" -F username=myUser -F password=myPassword -F storePassword=12345678 -F name=myCreds -F type=USERNAME_PASSWORD 'http://localhost:8001/api/v1/org/Default/secret'\n{\n  \"exportPassword\" : \"12345678\",\n  \"ok\" : true\n}\n```\n\nFor the sake of the example, the same `storePassword` value is used.\n\n3. create a plain value secret:\n```\n$ curl -H \"Authorization: API_TOKEN\" -F secret='my horrible secret' -F storePassword=12345678 -F name=myValue -F type=DATA 'http://localhost:8001/api/v1/org/Default/secret'\n{\n  \"exportPassword\" : \"12345678\",\n  \"ok\" : true\n}\n```\n\n4. start the process:\n```\n$ ./run.sh localhost:8001\n{\n  \"instanceId\" : \"8ea63d60-10f5-43dd-ba8b-87150fb20182\",\n  \"ok\" : true\n}\n```\n\n5. open [the UI](http://localhost:8001), find the process entry and\nopen its log. You should see messages like this:\n```\n12:00:55.817 [INFO ] c.w.c.runner.engine.LoggingTask - Public key file: .tmp/public1856673009465277934.key\n12:00:55.821 [INFO ] c.w.c.runner.engine.LoggingTask - Private key file: .tmp/private5358774395817724546.key\n12:00:55.832 [INFO ] c.w.c.runner.engine.LoggingTask - Credentials: {password=myPassword, username=myUser}\n12:00:55.846 [INFO ] c.w.c.runner.engine.LoggingTask - Plain secret: my horrible secret\n```\n"
  },
  {
    "path": "examples/secrets/concord.yml",
    "content": "configuration:\n  arguments:\n    # alternatively, a form, an encrypted value or an external\n    # service can be used to retrieve the password\n    pwd: \"12345678\"\n\nflows:\n  default:\n  # working with SSH key pairs\n  - set:\n      myKey: ${crypto.exportKeyAsFile('myKey', pwd)}\n\n  - log: \"Public key file: ${myKey.public}\"\n  - log: \"Private key file: ${myKey.private}\"\n\n  # working with username/password credentials\n  - log: \"Credentials: ${crypto.exportCredentials('myCreds', pwd)}\"\n\n  # working with encrypted strings\n  - log: \"Plain secret: ${crypto.exportAsString('myValue', pwd)}\"\n\n"
  },
  {
    "path": "examples/secrets/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F org=Default -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/slack/concord.yml",
    "content": "flows:\n  default:\n  - task: slack\n    in:\n      channelId: \"C5W9ELY7Q\"\n      text: \"Process ${txId} is completed\"\n      username: \"my bot\"\n      iconEmoji: \":chart_with_upwards_trend:\"\n      attachments:\n        - fallback: \"Book your flights at https://flights.example.com/book/r123456\"\n          actions:\n            - type: \"button\"\n              text: \"Book flights\"\n              url: \"https://flights.example.com/book/r123456\"\n\n  - log: notified\n"
  },
  {
    "path": "examples/slack/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process"
  },
  {
    "path": "examples/slackChannel/concord.yml",
    "content": "flows:\n  default:\n\n  # create a new slack channel\n  - task: slackChannel\n    in:\n      action: create\n      channelName: myChannelName\n      apiToken: mySlackApiToken\n\n  # the channel ID is stored as `slackChannelId`\n  - log: \"Channel ID: ${slackChannelId}\"\n\n  # archive a slack channel\n  - task: slackChannel\n    in:\n      action: archive\n      channelId: ${slackChannelId}\n      apiToken: mySlackApiToken\n\n  # create a new slack group\n  - task: slackChannel\n    in:\n      action: createGroup\n      channelName: myGroupName\n      apiToken: mySlackApiToken\n\n  # the channel ID is stored as `slackChannelId`\n  - log: \"Channel ID: ${slackChannelId}\"\n\n  # archive a slack group\n  - task: slackChannel\n    in:\n      action: archiveGroup\n      channelId: ${slackChannelId}\n      apiToken: mySlackApiToken    \n"
  },
  {
    "path": "examples/slackChannel/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/smtp/README.md",
    "content": "# SMTP task example\n\n1. change the address of the SMTP server in `concord.yml`\n2. start the process:\n  ```\n./run.sh localhost:8001\n  ```\n3. Use your AD/LDAP credentials when prompted."
  },
  {
    "path": "examples/smtp/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://com.walmartlabs.concord.plugins.basic:smtp-tasks:1.76.1\"\n\nflows:\n  default:\n  - task: smtp\n    in:\n      # a custom SMTP server can be specified here.\n      # Otherwise, the task will use the global SMTP configuration.\n\n      #smtpParams:\n      #  host: \"localhost\"\n      #  port: 25\n\n      mail:\n        from: ${initiator.attributes.mail}\n        to: ${initiator.attributes.mail}\n        subject: \"Howdy!\"\n        template: \"mail.mustache\"\n        attachments:\n          - \"first.txt\"\n          - path: \"second.txt\"\n            disposition: \"attachment\"             # optional, valid values: \"attachment\" or \"inline\"\n            description: \"attachment description\" # optional\n            name: \"attachment name\"               # optional\n  - log: mail sent to ${initiator.attributes.mail}\n"
  },
  {
    "path": "examples/smtp/first.txt",
    "content": "First attachment's content."
  },
  {
    "path": "examples/smtp/mail.mustache",
    "content": "Hello, {{initiator.displayName}}\n\nLove,\n{{initiator.attributes.mail}}."
  },
  {
    "path": "examples/smtp/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml *.mustache *.txt target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "examples/smtp/second.txt",
    "content": "Second attachment's content."
  },
  {
    "path": "examples/smtp_html/README.md",
    "content": "# SMTP task example\n\n1. change the address of the SMTP server in `concord.yml`\n2. start the process:\n  ```\n./run.sh localhost:8001\n  ```\n3. Use your AD/LDAP credentials when prompted."
  },
  {
    "path": "examples/smtp_html/concord.yml",
    "content": "configuration:\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.plugins.basic:smtp-tasks:1.76.1\"\n\nflows:\n  default:\n  - task: smtp\n    in:\n      # a custom SMTP server can be specified here.\n      # Otherwise, the task will use the global SMTP configuration.\n\n      #smtpParams:\n      #  host: \"localhost\"\n      #  port: 25\n\n      mail:\n        from: ${initiator.attributes.mail}\n        to: ${initiator.attributes.mail}\n        subject: \"Howdy!\"\n        template: \"mail.mustache.html\"\n  - log: mail sent to ${initiator.attributes.mail}\n\n\n"
  },
  {
    "path": "examples/smtp_html/mail.mustache.html",
    "content": "<html>\n<body>\n    <p>Hello, {{initiator.displayName}}</p>\n    <p>Love,\n        {{initiator.attributes.mail}}.</p>\n</body>\n</html>\n"
  },
  {
    "path": "examples/smtp_html/run.sh",
    "content": "#!/bin/bash\n\nSERVER_ADDR=\"$1\"\n\nrm -rf target && mkdir target\ncp -R concord.yml *.mustache.html target/\n\ncd target && zip -r payload.zip ./* > /dev/null && cd ..\n\nread -p \"Username: \" CURL_USER\ncurl -u ${CURL_USER} -F archive=@target/payload.zip http://${SERVER_ADDR}/api/v1/process\n"
  },
  {
    "path": "forms/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-forms</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n        </dependency>\n\n        <!-- Immutables -->\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>serial</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.errorprone</groupId>\n            <artifactId>error_prone_annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>commons-validator</groupId>\n            <artifactId>commons-validator</artifactId>\n        </dependency>\n    </dependencies>\n</project>\n"
  },
  {
    "path": "forms/src/main/java/com/walmartlabs/concord/forms/Constants.java",
    "content": "package com.walmartlabs.concord.forms;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class Constants {\n\n    public static final String RUN_AS_LDAP_KEY = \"ldap\";\n\n    public static final String RUN_AS_GROUP_KEY = \"group\";\n\n    public static final String RUN_AS_KEY = \"runAs\";\n\n    public static final String RUN_AS_USERNAME_KEY = \"username\";\n\n    public static final String FORM_FILES = \"_form_files\";\n}\n"
  },
  {
    "path": "forms/src/main/java/com/walmartlabs/concord/forms/DefaultFormValidator.java",
    "content": "package com.walmartlabs.concord.forms;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.apache.commons.validator.routines.EmailValidator;\n\nimport java.util.*;\n\nimport static com.walmartlabs.concord.forms.FormField.Cardinality;\nimport static com.walmartlabs.concord.forms.FormFields.*;\n\npublic class DefaultFormValidator implements FormValidator {\n\n    private final FormValidatorLocale locale;\n    private final Collection<FieldValidator> validators;\n\n    public DefaultFormValidator() {\n        this(new DefaultFormValidatorLocale());\n    }\n\n    public DefaultFormValidator(FormValidatorLocale locale) {\n        this.locale = locale;\n\n        List<FieldValidator> vs = new ArrayList<>();\n        vs.add(new StringFieldValidator(locale));\n        vs.add(new IntegerFieldValidator(locale));\n        vs.add(new DecimalFieldValidator(locale));\n        vs.add(new BooleanFieldValidator(locale));\n        vs.add(new FileFieldValidator());\n        vs.add(new DateFieldValidator());\n\n        this.validators = vs;\n    }\n\n    @Override\n    public List<ValidationError> validate(Form form, Map<String, Object> data) {\n        List<ValidationError> errors = new ArrayList<>();\n\n        String formId = form.name();\n\n        List<FormField> fields = form.fields();\n        if (fields == null || fields.isEmpty()) {\n            errors.add(ValidationError.of(ValidationError.GLOBAL_ERROR, locale.noFieldsDefined(formId)));\n            return errors;\n        }\n\n        Map<String, Object> values = data;\n        if (values == null) {\n            values = Collections.emptyMap();\n        }\n\n        for (FormField f : fields) {\n            Object v = values.get(f.name());\n            Object allowed = f.allowedValue();\n\n            ValidationError e = validate(formId, f, v, allowed);\n            if (e != null) {\n                errors.add(e);\n            }\n        }\n\n        return errors;\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public ValidationError validate(String formId, FormField f, Object v, Object allowed) {\n        String fieldName = f.name();\n\n        Cardinality expectedCardinality = f.cardinality();\n\n        if (!checkCardinality(v, expectedCardinality)) {\n            return ValidationError.of(fieldName, locale.invalidCardinality(formId, f, v));\n        }\n\n        if (v == null) {\n            return null;\n        }\n\n        v = box(v);\n        allowed = box(allowed);\n\n        if (v instanceof Collection) {\n            Collection<Object> vs = (Collection<Object>) v;\n            int idx = 0;\n            for (Object vv : vs) {\n                ValidationError e = validateSingleValue(formId, f, idx++, vv, allowed);\n                if (e != null) {\n                    return e;\n                }\n            }\n        } else if (v instanceof Object[]) {\n            Object[] vs = (Object[]) v;\n            for (int i = 0; i < vs.length; i++) {\n                ValidationError e = validateSingleValue(formId, f, i, vs[i], allowed);\n                if (e != null) {\n                    return e;\n                }\n            }\n        } else {\n            return validateSingleValue(formId, f, null, v, allowed);\n        }\n\n        return null;\n    }\n\n    private ValidationError validateSingleValue(String formId, FormField f, Integer idx, Object v, Object allowed) {\n        String type = f.type();\n        String fieldName = f.name();\n\n        if (allowed != null) {\n            if (!checkAllowedValue(v, allowed)) {\n                return ValidationError.of(fieldName, locale.valueNotAllowed(formId, f, idx, allowed, v));\n            }\n        }\n\n        boolean validated = false;\n        for (FieldValidator validator : validators) {\n            for (String t : validator.allowedTypes()) {\n                if (type.equals(t)) {\n                    ValidationError e = validator.validate(formId, f, idx, v);\n                    if (e != null) {\n                        return e;\n                    }\n\n                    validated = true;\n                }\n            }\n        }\n\n        if (!validated) {\n            throw new RuntimeException(\"Unsupported form field type: \" + type);\n        }\n\n        return null;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static boolean checkAllowedValue(Object v, Object allowed) {\n        if (v.equals(allowed)) {\n            return true;\n        }\n\n        if (allowed instanceof Collection) {\n            Collection<Object> aa = (Collection<Object>) allowed;\n            return aa.contains(v);\n        } else if (allowed instanceof Object[]) {\n            Object[] as = (Object[]) allowed;\n            for (Object aa : as) {\n                if (v.equals(aa)) {\n                    return true;\n                }\n            }\n        }\n\n        return false;\n    }\n\n    private static boolean checkCardinality(Object v, FormField.Cardinality required) {\n        Set<FormField.Cardinality> actual = guessCardinality(v);\n        return actual.contains(required);\n    }\n\n    private static Set<FormField.Cardinality> guessCardinality(Object v) {\n        int len = length(v);\n        if (len < 0) {\n            throw new IllegalArgumentException(\"Can't determine object's length: \" + v);\n        }\n\n        if (len == 0) {\n            return set(Cardinality.ANY, Cardinality.ONE_OR_NONE);\n        } else if (len == 1) {\n            return set(Cardinality.ANY, Cardinality.ONE_OR_NONE, Cardinality.ONE_AND_ONLY_ONE, Cardinality.AT_LEAST_ONE);\n        } else {\n            return set(Cardinality.ANY, Cardinality.AT_LEAST_ONE);\n        }\n    }\n\n    private static Set<Cardinality> set(Cardinality... values) {\n        Set<Cardinality> s = new HashSet<>(values.length);\n        Collections.addAll(s, values);\n        return s;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static int length(Object v) {\n        if (v == null) {\n            return 0;\n        }\n\n        if (v instanceof Collection) {\n            return ((Collection<Object>) v).size();\n        }\n\n        if (v instanceof Object[]) {\n            Object[] arr = (Object[]) v;\n            return arr.length;\n        }\n\n        return 1;\n    }\n\n    private static boolean withinBounds(Object v, Long min, Long max) {\n        if (min == null && max == null) {\n            return true;\n        }\n\n        boolean low = true, high = true;\n\n        if (v instanceof Long) {\n            Long l = (Long) v;\n            if (min != null) {\n                low = l >= min;\n            }\n            if (max != null) {\n                high = l <= max;\n            }\n        } else if (v instanceof Integer) {\n            int i = (Integer) v;\n            if (min != null) {\n                if (!validInt(min)) {\n                    throw new IllegalArgumentException(\"Invalid integer min bound: \" + min + \", use long values if needed\");\n                }\n                low = i >= min.intValue();\n            }\n            if (max != null) {\n                if (!validInt(max)) {\n                    throw new IllegalArgumentException(\"Invalid integer max bound: \" + max + \", use long values if needed\");\n                }\n                high = i <= max.intValue();\n            }\n        } else {\n            throw new IllegalArgumentException(\"Unsupported integer type: \" + v.getClass());\n        }\n\n        return low && high;\n    }\n\n    private static boolean withinBounds(Object v, Double min, Double max) {\n        if (min == null && max == null) {\n            return true;\n        }\n\n        boolean low = true, high = true;\n\n        if (v instanceof Double) {\n            Double l = (Double) v;\n            if (min != null) {\n                low = l >= min;\n            }\n            if (max != null) {\n                high = l <= max;\n            }\n        } else if (v instanceof Float) {\n            float i = (Float) v;\n            if (min != null) {\n                if (!validFloat(min)) {\n                    throw new IllegalArgumentException(\"Invalid float min bound: \" + min + \", use double values if needed\");\n                }\n                low = i >= min.floatValue();\n            }\n            if (max != null) {\n                if (!validFloat(max)) {\n                    throw new IllegalArgumentException(\"Invalid float max bound: \" + max + \", use double values if needed\");\n                }\n                high = i <= max.floatValue();\n            }\n        } else {\n            throw new IllegalArgumentException(\"Unsupported decimal type: \" + v.getClass());\n        }\n\n        return low && high;\n    }\n\n    private static boolean validInt(Long l) {\n        return l <= Integer.MAX_VALUE && l >= Integer.MIN_VALUE;\n    }\n\n    private static boolean validFloat(Double d) {\n        return d <= Float.MAX_VALUE && d >= Float.MIN_VALUE;\n    }\n\n    private static Object box(Object v) {\n        if (v == null) {\n            return null;\n        }\n\n        if (v instanceof int[]) {\n            int[] is = (int[]) v;\n\n            Object[] os = new Object[is.length];\n            for (int i = 0; i < is.length; i++) {\n                os[i] = is[i];\n            }\n\n            return os;\n        }\n\n        if (v instanceof long[]) {\n            long[] is = (long[]) v;\n\n            Object[] os = new Object[is.length];\n            for (int i = 0; i < is.length; i++) {\n                os[i] = is[i];\n            }\n\n            return os;\n        }\n\n        if (v instanceof float[]) {\n            float[] is = (float[]) v;\n\n            Object[] os = new Object[is.length];\n            for (int i = 0; i < is.length; i++) {\n                os[i] = is[i];\n            }\n\n            return os;\n        }\n\n        if (v instanceof double[]) {\n            double[] is = (double[]) v;\n\n            Object[] os = new Object[is.length];\n            for (int i = 0; i < is.length; i++) {\n                os[i] = is[i];\n            }\n\n            return os;\n        }\n\n        if (v instanceof boolean[]) {\n            boolean[] is = (boolean[]) v;\n\n            Object[] os = new Object[is.length];\n            for (int i = 0; i < is.length; i++) {\n                os[i] = is[i];\n            }\n\n            return os;\n        }\n\n        if (v instanceof char[]) {\n            char[] is = (char[]) v;\n\n            Object[] os = new Object[is.length];\n            for (int i = 0; i < is.length; i++) {\n                os[i] = is[i];\n            }\n\n            return os;\n        }\n\n        return v;\n    }\n\n    public interface FieldValidator {\n\n        /**\n         * @return field types that are supported by this validator.\n         */\n        String[] allowedTypes();\n\n        /**\n         * Validate a single value of a field.\n         *\n         * @param formId ID of a form.\n         * @param f      the validated field\n         * @param idx    index of the value if the field is a collection.\n         * @param v      the value of the field.\n         * @return validation error or {@code null} if valid.\n         */\n        ValidationError validate(String formId, FormField f, Integer idx, Object v);\n    }\n\n    public static final class IntegerFieldValidator implements FieldValidator {\n\n        private static final String[] TYPES = {IntegerField.TYPE};\n\n        private final FormValidatorLocale locale;\n\n        public IntegerFieldValidator(FormValidatorLocale locale) {\n            this.locale = locale;\n        }\n\n        @Override\n        public String[] allowedTypes() {\n            return TYPES;\n        }\n\n        @Override\n        public ValidationError validate(String formId, FormField f, Integer idx, Object v) {\n            String fieldName = f.name();\n\n            if (v instanceof Integer || v instanceof Long) {\n                Long min = f.getOption(IntegerField.MIN);\n                Long max = f.getOption(IntegerField.MAX);\n\n                if (!withinBounds(v, min, max)) {\n                    return ValidationError.of(fieldName, locale.integerRangeError(formId, f, idx, min, max, v));\n                }\n            } else {\n                return ValidationError.of(fieldName, locale.expectedInteger(formId, f, idx, v));\n            }\n\n            return null;\n        }\n    }\n\n    public static final class DecimalFieldValidator implements FieldValidator {\n\n        private static final String[] TYPES = {DecimalField.TYPE};\n\n        private final FormValidatorLocale locale;\n\n        public DecimalFieldValidator(FormValidatorLocale locale) {\n            this.locale = locale;\n        }\n\n        @Override\n        public String[] allowedTypes() {\n            return TYPES;\n        }\n\n        @Override\n        public ValidationError validate(String formId, FormField f, Integer idx, Object v) {\n            String fieldName = f.name();\n\n            if (v instanceof Double || v instanceof Float) {\n                Double min = f.getOption(DecimalField.MIN);\n                Double max = f.getOption(DecimalField.MAX);\n\n                if (!withinBounds(v, min, max)) {\n                    return ValidationError.of(fieldName, locale.decimalRangeError(formId, f, idx, min, max, v));\n                }\n            } else {\n                return ValidationError.of(fieldName, locale.expectedDecimal(formId, f, idx, v));\n            }\n\n            return null;\n        }\n    }\n\n    public static final class BooleanFieldValidator implements FieldValidator {\n\n        private static final String[] TYPES = {BooleanField.TYPE};\n\n        private final FormValidatorLocale locale;\n\n        public BooleanFieldValidator(FormValidatorLocale locale) {\n            this.locale = locale;\n        }\n\n        @Override\n        public String[] allowedTypes() {\n            return TYPES;\n        }\n\n        @Override\n        public ValidationError validate(String formId, FormField f, Integer idx, Object v) {\n            String fieldName = f.name();\n\n            if (!(v instanceof Boolean)) {\n                return ValidationError.of(fieldName, locale.expectedBoolean(formId, f, idx, v));\n            }\n\n            return null;\n        }\n    }\n\n    public static final class DateFieldValidator implements DefaultFormValidator.FieldValidator {\n\n        private static final String[] TYPES = {DateField.TYPE, DateTimeField.TYPE};\n\n        @Override\n        public String[] allowedTypes() {\n            return TYPES;\n        }\n\n        @Override\n        public ValidationError validate(String formId, FormField f, Integer idx, Object v) {\n            if (!(v instanceof String)) {\n                throw new IllegalArgumentException(\"Expected a date value: \" + f.name());\n            }\n\n            return null;\n        }\n    }\n\n    public static final class FileFieldValidator implements DefaultFormValidator.FieldValidator {\n\n        private static final String[] TYPES = {FileField.TYPE};\n\n        @Override\n        public String[] allowedTypes() {\n            return TYPES;\n        }\n\n        @Override\n        public ValidationError validate(String formId, FormField f, Integer idx, Object v) {\n            String fieldName = f.name();\n\n            if (!(v instanceof String)) {\n                throw new IllegalArgumentException(\"Expected a file value: \" + fieldName);\n            }\n\n            return null;\n        }\n    }\n\n    public static final class StringFieldValidator implements FieldValidator {\n\n        private static final String[] TYPES = {StringField.TYPE};\n\n        private final FormValidatorLocale locale;\n\n        public StringFieldValidator(FormValidatorLocale locale) {\n            this.locale = locale;\n        }\n\n        @Override\n        public String[] allowedTypes() {\n            return TYPES;\n        }\n\n        @Override\n        public ValidationError validate(String formId, FormField f, Integer idx, Object v) {\n            String fieldName = f.name();\n\n            if (v instanceof String) {\n                String pattern = f.getOption(StringField.PATTERN);\n                if (pattern == null) {\n                    return null;\n                }\n\n                String sv = (String) v;\n                if (!sv.matches(pattern)) {\n                    return ValidationError.of(fieldName, locale.doesntMatchPattern(formId, f, idx, pattern, v));\n                }\n            } else {\n                return ValidationError.of(fieldName, locale.expectedString(formId, f, idx, v));\n            }\n\n            String inputType = f.getOption(StringField.INPUT_TYPE);\n            if (\"email\".equalsIgnoreCase(inputType)) {\n                boolean valid = EmailValidator.getInstance().isValid((String)v);\n                if (!valid) {\n                    return ValidationError.of(f.name(), \"Invalid email address\");\n                }\n            }\n\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "forms/src/main/java/com/walmartlabs/concord/forms/DefaultFormValidatorLocale.java",
    "content": "package com.walmartlabs.concord.forms;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class DefaultFormValidatorLocale implements FormValidatorLocale {\n\n    @Override\n    public String noFieldsDefined(String formId) {\n        return String.format(\"Form (%s) has no fields\", formId);\n    }\n\n    @Override\n    public String invalidCardinality(String formId, FormField field, Object value) {\n        return String.format(\"%s: Invalid cardinality, expected %s\", fieldName(field, null), spell(field.cardinality()));\n    }\n\n    @Override\n    public String expectedString(String formId, FormField field, Integer idx, Object value) {\n        return String.format(\"%s: expected a string value, got %s\", fieldName(field, idx), value);\n    }\n\n    @Override\n    public String expectedInteger(String formId, FormField field, Integer idx, Object value) {\n        return String.format(\"%s: expected an integer value, got %s\", fieldName(field, idx), value);\n    }\n\n    @Override\n    public String expectedDecimal(String formId, FormField field, Integer idx, Object value) {\n        return String.format(\"%s: expected a decimal value, got %s\", fieldName(field, idx), value);\n    }\n\n    @Override\n    public String expectedBoolean(String formId, FormField field, Integer idx, Object value) {\n        return String.format(\"%s: expected a boolean value, got %s\", fieldName(field, idx), value);\n    }\n\n    @Override\n    public String doesntMatchPattern(String formId, FormField field, Integer idx, String pattern, Object value) {\n        return String.format(\"%s: value '%s' doesn't match pattern '%s'\", fieldName(field, idx), value, pattern);\n    }\n\n    @Override\n    public String integerRangeError(String formId, FormField field, Integer idx, Long min, Long max, Object value) {\n        return String.format(\"%s: value '%s' must be %s\", fieldName(field, idx), value, bounds(min, max));\n    }\n\n    @Override\n    public String decimalRangeError(String formId, FormField field, Integer idx, Double min, Double max, Object value) {\n        return String.format(\"%s: value '%s' must be %s\", fieldName(field, idx), value, bounds(min, max));\n    }\n\n    @Override\n    public String valueNotAllowed(String formId, FormField field, Integer idx, Object allowed, Object value) {\n        return String.format(\"%s: value '%s' is not allowed, valid values: %s\", fieldName(field, idx), value, allowed);\n    }\n\n    @Override\n    public String expectedDate(String formId, FormField field, Integer idx, Object value) {\n        return String.format(\"%s: expected a date value, got %s\", fieldName(field, idx), value);\n    }\n\n    public static String fieldName(FormField field, Integer idx) {\n        String s = field.label();\n        if (s == null) {\n            s = field.name();\n        }\n\n        if (idx != null) {\n            s = s + \" [\" + idx + \"]\";\n        }\n\n        return s;\n    }\n\n    public static String spell(FormField.Cardinality c) {\n        if (c == null) {\n            throw new IllegalArgumentException(\"Cardinality can't be null\");\n        }\n\n        switch (c) {\n            case ANY:\n                return \"any number of values\";\n            case ONE_AND_ONLY_ONE:\n                return \"a single value\";\n            case ONE_OR_NONE:\n                return \"a single optional value\";\n            case AT_LEAST_ONE:\n                return \"at least a single value\";\n            default:\n                throw new IllegalArgumentException(\"Unsupported cardinality type: \" + c);\n        }\n    }\n\n    public static String bounds(Object min, Object max) {\n        if (min != null && max != null) {\n            return String.format(\"within %s and %s (inclusive)\", min, max);\n        } else if (min == null) {\n            return String.format(\"less or equal than %s\", max);\n        } else {\n            return String.format(\"equal or greater than %s\", min);\n        }\n    }\n}\n"
  },
  {
    "path": "forms/src/main/java/com/walmartlabs/concord/forms/Form.java",
    "content": "package com.walmartlabs.concord.forms;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.serial.Serial;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@Serial.Version(1)\npublic interface Form extends Serializable {\n\n    String name();\n\n    String eventName();\n\n    @Value.Default\n    default FormOptions options() {\n        return FormOptions.builder().build();\n    }\n\n    @Value.Default\n    default List<FormField> fields() {\n        return Collections.emptyList();\n    }\n\n    static ImmutableForm.Builder builder() {\n        return ImmutableForm.builder();\n    }\n}\n"
  },
  {
    "path": "forms/src/main/java/com/walmartlabs/concord/forms/FormField.java",
    "content": "package com.walmartlabs.concord.forms;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AllowNulls;\nimport org.immutables.serial.Serial;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.Map;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@Serial.Version(-9087815156857537835L) // for backward compatibility (java8 concord 1.92.0 version)\npublic interface FormField extends Serializable {\n\n    String name();\n\n    @Nullable\n    String label();\n\n    String type();\n\n    Cardinality cardinality();\n\n    @Nullable\n    Serializable defaultValue();\n\n    @Nullable\n    Serializable allowedValue();\n\n    @Value.Default\n    @AllowNulls\n    default Map<String, Serializable> options() {\n        return Collections.emptyMap();\n    }\n\n    static ImmutableFormField.Builder builder() {\n        return ImmutableFormField.builder();\n    }\n\n    default <T> T getOption(Option<T> option) {\n        Object v = options().get(option.name());\n        if (v == null) {\n            return null;\n        }\n\n        return option.cast(v);\n    }\n\n    enum Cardinality {\n\n        ONE_OR_NONE,\n        ONE_AND_ONLY_ONE,\n        AT_LEAST_ONE,\n        ANY\n    }\n\n    @Value.Immutable\n    interface Option<T> {\n\n        @Value.Parameter\n        String name();\n\n        @Value.Parameter\n        Class<T> type();\n\n        static <T> Option<T> of(String name, Class<T> type) {\n            return ImmutableOption.of(name, type);\n        }\n\n        default T cast(Object v) {\n            if (v == null) {\n                return null;\n            }\n\n            Class<?> other = v.getClass();\n            if (!type().isAssignableFrom(other)) {\n                throw new IllegalArgumentException(\"Invalid value type: expected \" + type() + \", got \" + other);\n            }\n\n            return type().cast(v);\n        }\n    }\n}\n"
  },
  {
    "path": "forms/src/main/java/com/walmartlabs/concord/forms/FormFields.java",
    "content": "package com.walmartlabs.concord.forms;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport static com.walmartlabs.concord.forms.FormField.Option;\n\n/**\n * Standard form field types and options.\n */\npublic final class FormFields {\n\n    public static final class StringField {\n\n        public static final String TYPE = \"string\";\n        public static final Option<String> PATTERN = Option.of(\"pattern\", String.class);\n        public static final Option<String> INPUT_TYPE = Option.of(\"inputType\", String.class);\n        public static final Option<String> PLACEHOLDER = Option.of(\"placeholder\", String.class);\n        public static final Option<Boolean> SEARCH = Option.of(\"search\", Boolean.class);\n    }\n\n    public static final class IntegerField {\n\n        public static final String TYPE = \"int\";\n        public static final Option<Long> MIN = Option.of(\"min\", Long.class);\n        public static final Option<Long> MAX = Option.of(\"max\", Long.class);\n        public static final Option<String> PLACEHOLDER = Option.of(\"placeholder\", String.class);\n    }\n\n    public static final class DecimalField {\n\n        public static final String TYPE = \"decimal\";\n        public static final Option<Double> MIN = Option.of(\"min\", Double.class);\n        public static final Option<Double> MAX = Option.of(\"max\", Double.class);\n        public static final Option<String> PLACEHOLDER = Option.of(\"placeholder\", String.class);\n    }\n\n    public static final class BooleanField {\n\n        public static final String TYPE = \"boolean\";\n    }\n\n    public static final class FileField {\n\n        public static final String TYPE = \"file\";\n\n        private FileField() {\n        }\n    }\n\n    public static final class DateField {\n\n        public static final String TYPE = \"date\";\n\n        private DateField() {\n        }\n    }\n\n    public static final class DateTimeField {\n\n        public static final String TYPE = \"dateTime\";\n\n        private DateTimeField() {\n        }\n    }\n\n    public static final class DateFieldOptions {\n\n        public static final Option<String> POPUP_POSITION = Option.of(\"popupPosition\", String.class);\n\n        private DateFieldOptions() {\n        }\n    }\n\n    public static final class CommonFieldOptions {\n\n        public static final Option<Boolean> READ_ONLY = Option.of(\"readOnly\", Boolean.class);\n\n        private CommonFieldOptions() {\n        }\n    }\n\n    private FormFields() {\n    }\n}\n"
  },
  {
    "path": "forms/src/main/java/com/walmartlabs/concord/forms/FormOptions.java",
    "content": "package com.walmartlabs.concord.forms;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AllowNulls;\nimport org.immutables.serial.Serial;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.Map;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@Serial.Version(1)\npublic interface FormOptions extends Serializable {\n\n    @Value.Default\n    default boolean isYield() {\n        return false;\n    }\n\n    @Value.Default\n    default boolean saveSubmittedBy() {\n        return false;\n    }\n\n    @Value.Default\n    @AllowNulls\n    default Map<String, Serializable> runAs() {\n        return Collections.emptyMap();\n    }\n\n    @Value.Default\n    default Map<String, Serializable> extraValues() {\n        return Collections.emptyMap();\n    }\n\n    static ImmutableFormOptions.Builder builder() {\n        return ImmutableFormOptions.builder();\n    }\n}\n"
  },
  {
    "path": "forms/src/main/java/com/walmartlabs/concord/forms/FormUtils.java",
    "content": "package com.walmartlabs.concord.forms;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.Serializable;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.time.format.DateTimeFormatter;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.forms.FormFields.CommonFieldOptions.READ_ONLY;\nimport static java.nio.file.StandardCopyOption.REPLACE_EXISTING;\n\npublic final class FormUtils {\n\n    private static final String RUN_AS_USERNAME_PATH = Constants.RUN_AS_KEY + \".\" +\n            Constants.RUN_AS_USERNAME_KEY;\n\n    private static final String RUN_AS_LDAP_GROUP_PATH = Constants.RUN_AS_KEY + \".\" +\n            Constants.RUN_AS_LDAP_KEY + \".\" +\n            Constants.RUN_AS_GROUP_KEY;\n\n    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(\"yyyy-MM-dd'T'HH:mm:ss.SSSZ\", Locale.US);\n\n    @SuppressWarnings(\"unchecked\")\n    public static Set<String> getRunAsUsers(String formName, Map<String, Serializable> runAsParams) {\n        if (runAsParams == null) {\n            return Collections.emptySet();\n        }\n\n        Object v = runAsParams.get(Constants.RUN_AS_USERNAME_KEY);\n        if (v == null) {\n            return Collections.emptySet();\n        }\n\n        if (v instanceof String) {\n            return Collections.singleton((String) v);\n        } else if (v instanceof Collection) {\n            Collection<Object> items = (Collection<Object>) v;\n\n            Set<String> result = new HashSet<>();\n            for (Object item : items) {\n                if (item instanceof String) {\n                    result.add((String) item);\n                } else {\n                    throw new RuntimeException(\"Expected a string or a list of strings value, got: \" + item + \". \" +\n                            \"Check the '\" + RUN_AS_USERNAME_PATH + \"' parameter of '\" + formName + \"' form definition.\");\n                }\n            }\n            return result;\n        }\n\n        throw new RuntimeException(\"Invalid form definition: \" + formName + \". \" +\n                \"Expected a username definition, got: \" + v);\n    }\n\n    /**\n     * Returns a collection of LDAP groups that can submit the form or an empty collection if no restrictions are\n     * specified.\n     * <p>\n     * This method takes care of all our different ways to specify the form's LDAP groups.\n     * Because form options are evaluated at the runtime, we can't transform all syntax variants into a single one\n     * on the YAML parsing phase, we need to coerce the data at the runtime.\n     * <p>\n     * <h2>Supported syntax variants</h2>\n     * <p>\n     * The original, single value:\n     * <pre>\n     * runAs:\n     *   ldap:\n     *     group: \"aGroupName\"\n     * </pre>\n     * <p>\n     * The updated, multi value:\n     * <pre>\n     * runAs:\n     *   ldap:\n     *     - group: \"aGroupName\"\n     *     - group: \"bGroupName\"\n     * </pre>\n     * <p>\n     * The recommended multi value:\n     * <pre>\n     * runAs:\n     *   ldap:\n     *     group:\n     *     - \"aGroupName\"\n     *     - \"bGroupName\"\n     * </pre>\n     */\n    @SuppressWarnings(\"unchecked\")\n    public static Set<String> getRunAsLdapGroups(String formName, Map<String, Serializable> runAsParams) {\n        if (runAsParams == null) {\n            return Collections.emptySet();\n        }\n\n        Object ldap = runAsParams.get(Constants.RUN_AS_LDAP_KEY);\n        if (ldap == null) {\n            return Collections.emptySet();\n        }\n\n        // the recommended syntax\n        if (ldap instanceof Map) {\n            // ldap:\n            //   group: VALUE\n            Map<Object, Object> m = (Map<Object, Object>) ldap;\n            final Object value = m.get(Constants.RUN_AS_GROUP_KEY);\n\n            // a single string value\n            if (value instanceof String) {\n                return Collections.singleton((String) value);\n            }\n\n            // a collection of strings (or something else, but that would be an error)\n            if (value instanceof Collection) {\n                Collection<Object> items = (Collection<Object>) value;\n\n                Set<String> result = new HashSet<>();\n                for (Object i : items) {\n                    if (i instanceof String) {\n                        result.add((String) i);\n                    } else {\n                        throw invalidLdapGroupElement(formName, RUN_AS_LDAP_GROUP_PATH, i);\n                    }\n                }\n                return result;\n            }\n        }\n\n        if (ldap instanceof Collection) {\n            // ldap:\n            // - VALUE\n            // - VALUE\n            Collection<Object> items = (Collection<Object>) ldap;\n            return items.stream()\n                    .map(o -> parseOldGroupDefinition(formName, o))\n                    .collect(Collectors.toSet());\n        }\n\n        throw new RuntimeException(\"Invalid form definition: \" + formName + \". \" +\n                \"Expected a LDAP group definition, got: \" + ldap);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static String parseOldGroupDefinition(String formName, Object item) {\n        if (item instanceof Map) {\n            Map<Object, Object> m = (Map<Object, Object>) item;\n            Object o = m.get(Constants.RUN_AS_GROUP_KEY);\n            if (o instanceof String) {\n                return (String) o;\n            } else {\n                throw invalidLdapGroupElement(formName, RUN_AS_LDAP_GROUP_PATH, o);\n            }\n        }\n\n        throw invalidLdapGroupElement(formName, RUN_AS_LDAP_GROUP_PATH, item);\n    }\n\n    private static RuntimeException invalidLdapGroupElement(String formName, String k, Object v) {\n        return new RuntimeException(\"Expected a string value or a group definition, \" +\n                \"got: \" + v + \". Check the '\" + k + \"' parameter of '\" + formName + \"' form definition\");\n    }\n\n    public static Map<String, Object> convert(FormValidatorLocale locale, Form form, Map<String, Object> m) throws ValidationException {\n        return convert(locale, form, m, new HashMap<>());\n    }\n\n    public static Map<String, Object> convert(FormValidatorLocale locale, Form form, Map<String, Object> m, Map<String, String> tmpFiles) throws ValidationException {\n        if (m == null) {\n            return Collections.emptyMap();\n        }\n\n        Map<String, Object> m2 = new HashMap<>();\n        m2.put(Constants.FORM_FILES, tmpFiles);\n\n        for (FormField f : form.fields()) {\n            String k = f.name();\n\n            Object v = Boolean.TRUE.equals(f.getOption(READ_ONLY)) ? f.defaultValue() : m.getOrDefault(k, f.defaultValue());\n\n            boolean isOptional = f.cardinality() == FormField.Cardinality.ONE_OR_NONE || f.cardinality() == FormField.Cardinality.ANY;\n            if (v == null && !isOptional) {\n                v = allowedValueAsFieldValue(f.allowedValue());\n            }\n\n            /*\n             * Use cardinality as an indicator to convert single value (coming as a string) into an array\n             * for the scenario when only one value was provided by the user\n             */\n            if (v instanceof String && (f.cardinality() == FormField.Cardinality.ANY || f.cardinality() == FormField.Cardinality.AT_LEAST_ONE)) {\n                v = Collections.singletonList(v);\n            }\n\n            v = convert(locale, form.name(), f, null, v);\n\n            if (v == null && !isOptional) {\n                continue;\n            }\n\n            if (v != null && f.type().equals(FormFields.FileField.TYPE)) {\n                String wsFileName = \"_form_files/\" + form.name() + \"/\" + f.name();\n                tmpFiles.put(wsFileName, (String) v);\n                m2.put(k, wsFileName);\n            } else {\n                m2.put(k, v);\n            }\n        }\n        return m2;\n    }\n\n    public static Object convert(FormValidatorLocale locale, String formName, FormField f, Integer idx, Object v) throws ValidationException {\n        if (v instanceof String) {\n            String s = (String) v;\n\n            switch (f.type()) {\n                case FormFields.StringField.TYPE: {\n                    if (s.isEmpty()) {\n                        return null;\n                    }\n                    break;\n                }\n                case FormFields.IntegerField.TYPE: {\n                    if (s.isEmpty()) {\n                        return null;\n                    }\n\n                    try {\n                        return Long.parseLong(s);\n                    } catch (NumberFormatException e) {\n                        throw new ValidationException(f, s, locale.expectedInteger(formName, f, idx, s));\n                    }\n                }\n                case FormFields.DecimalField.TYPE: {\n                    if (s.isEmpty()) {\n                        return null;\n                    }\n\n                    try {\n                        return Double.parseDouble(s);\n                    } catch (NumberFormatException e) {\n                        throw new ValidationException(f, s, locale.expectedDecimal(formName, f, idx, s));\n                    }\n                }\n                case FormFields.BooleanField.TYPE: {\n                    if (s.isEmpty()) {\n                        // default HTML checkbox will be submitted as an empty value if checked\n                        return true;\n                    }\n                    return Boolean.parseBoolean(s);\n                }\n                case FormFields.FileField.TYPE: {\n                    try {\n                        Path tmp = PathUtils.createTempFile(f.name(), \".tmp\");\n                        Files.write(tmp, s.getBytes(), StandardOpenOption.TRUNCATE_EXISTING);\n                        return tmp.toString();\n                    } catch (IOException e) {\n                        throw new RuntimeException(\"Error reading file for form field '\" + f.name() + \"'\", e);\n                    }\n                }\n                case FormFields.DateField.TYPE:\n                case FormFields.DateTimeField.TYPE: {\n                    if (s.isEmpty()) {\n                        return null;\n                    }\n\n                    // adjust the value to the default system timezone\n                    // on the process level those values are represented as java.util.Date (i.e. no TZ info retained)\n                    // so we assume all Date values are in the default system TZ (which is typically UTC)\n                    return ZonedDateTime.parse(s)\n                            .withZoneSameInstant(ZoneId.systemDefault())\n                            .format(DATE_TIME_FORMATTER);\n                }\n            }\n        } else if (v instanceof List) {\n            List<?> l = (List<?>) v;\n            if (l.isEmpty()) {\n                return null;\n            }\n\n            List<Object> ll = new ArrayList<>(l.size());\n            int i = 0;\n            for (Object o : l) {\n                ll.add(convert(locale, formName, f, i, o));\n                i++;\n            }\n            return ll;\n        } else if (v instanceof InputStream) {\n            if (f.type().equals(FormFields.FileField.TYPE)) {\n                try (InputStream is = (InputStream) v) {\n                    Path tmp = PathUtils.createTempFile(f.name(), \".tmp\");\n                    Files.copy(is, tmp, REPLACE_EXISTING);\n                    return tmp.toString();\n                } catch (IOException e) {\n                    throw new RuntimeException(\"Error reading file for form field '\" + f.name() + \"'\", e);\n                }\n            }\n        } else if (v == null) {\n            if (f.type().equals(FormFields.BooleanField.TYPE)) {\n                return false;\n            }\n        }\n\n        return v;\n    }\n\n    private static Object allowedValueAsFieldValue(Serializable allowedValue) {\n        if (allowedValue instanceof Collection) {\n            if (((Collection<?>) allowedValue).size() == 1) {\n                return ((Collection<?>) allowedValue).iterator().next();\n            }\n\n            return null;\n        }\n\n        return allowedValue;\n    }\n\n    public static class ValidationException extends Exception {\n\n        private static final long serialVersionUID = 1L;\n\n        private final FormField field;\n        private final String input;\n        private final String message;\n\n        private ValidationException(FormField field, String input, String message) {\n            this.field = field;\n            this.input = input;\n            this.message = message;\n        }\n\n        public FormField getField() {\n            return field;\n        }\n\n        public String getInput() {\n            return input;\n        }\n\n        @Override\n        public String getMessage() {\n            return message;\n        }\n    }\n\n    private FormUtils() {\n    }\n}\n"
  },
  {
    "path": "forms/src/main/java/com/walmartlabs/concord/forms/FormValidator.java",
    "content": "package com.walmartlabs.concord.forms;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.List;\nimport java.util.Map;\n\npublic interface FormValidator {\n\n    List<ValidationError> validate(Form form, Map<String, Object> data);\n\n    ValidationError validate(String formId, FormField f, Object v, Object allowed);\n}\n"
  },
  {
    "path": "forms/src/main/java/com/walmartlabs/concord/forms/FormValidatorLocale.java",
    "content": "package com.walmartlabs.concord.forms;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface FormValidatorLocale {\n\n    /**\n     * The form has no fields defined.\n     */\n    String noFieldsDefined(String formId);\n\n    /**\n     * Invalid cardinality of a value.\n     */\n    String invalidCardinality(String formId, FormField field, Object value);\n\n    /**\n     * Expected a string value.\n     */\n    String expectedString(String formId, FormField field, Integer idx, Object value);\n\n    /**\n     * Expected an integer value.\n     */\n    String expectedInteger(String formId, FormField field, Integer idx, Object value);\n\n    /**\n     * Expected a decimal value.\n     */\n    String expectedDecimal(String formId, FormField field, Integer idx, Object value);\n\n    /**\n     * Expected a boolean value.\n     */\n    String expectedBoolean(String formId, FormField field, Integer idx, Object value);\n\n    /**\n     * A string value doesn't match the specified pattern.\n     */\n    String doesntMatchPattern(String formId, FormField field, Integer idx, String pattern, Object value);\n\n    /**\n     * Value must be within the specified range.\n     */\n    String integerRangeError(String formId, FormField field, Integer idx, Long min, Long max, Object value);\n\n    /**\n     * Value must be within the specified range.\n     */\n    String decimalRangeError(String formId, FormField field, Integer idx, Double min, Double max, Object value);\n\n    /**\n     * Value is not allowed.\n     */\n    String valueNotAllowed(String formId, FormField field, Integer idx, Object allowed, Object value);\n\n    /**\n     * Expected a date value.\n     *\n     */\n    String expectedDate(String formId, FormField field, Integer idx, Object value);\n}\n"
  },
  {
    "path": "forms/src/main/java/com/walmartlabs/concord/forms/ValidationError.java",
    "content": "package com.walmartlabs.concord.forms;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface ValidationError extends Serializable {\n\n    String GLOBAL_ERROR = \"_global\";\n\n    String fieldName();\n\n    String error();\n\n    static ValidationError of(String field, String error) {\n        return builder()\n                .fieldName(field)\n                .error(error)\n                .build();\n    }\n\n    static ImmutableValidationError.Builder builder() {\n        return ImmutableValidationError.builder();\n    }\n}\n"
  },
  {
    "path": "github-app-installation/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-github-app-installation</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.takari.bpm</groupId>\n            <artifactId>bpm-engine-impl</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>javax.validation</groupId>\n            <artifactId>validation-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.typesafe</groupId>\n            <artifactId>config</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.nimbusds</groupId>\n            <artifactId>nimbus-jose-jwt</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.bouncycastle</groupId>\n            <artifactId>bcpkix-jdk18on</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.bouncycastle</groupId>\n            <artifactId>bcprov-jdk18on</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>builder</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jdk8</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-guava</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jsr310</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <!-- JDK9+ compatibility -->\n        <dependency>\n            <groupId>javax.xml.bind</groupId>\n            <artifactId>jaxb-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.sun.xml.bind</groupId>\n            <artifactId>jaxb-impl</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-params</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-core</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-junit-jupiter</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>ch.qos.logback</groupId>\n            <artifactId>logback-classic</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <configuration>\n                    <environmentVariables>\n                        <CONCORD_TMP_DIR>${java.io.tmpdir}</CONCORD_TMP_DIR>\n                    </environmentVariables>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "github-app-installation/src/main/java/com/walmartlabs/concord/github/appinstallation/AccessTokenProvider.java",
    "content": "package com.walmartlabs.concord.github.appinstallation;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.nimbusds.jose.JOSEException;\nimport com.nimbusds.jose.JWSAlgorithm;\nimport com.nimbusds.jose.JWSHeader;\nimport com.nimbusds.jose.crypto.RSASSASigner;\nimport com.nimbusds.jose.jwk.JWK;\nimport com.nimbusds.jwt.JWTClaimsSet;\nimport com.nimbusds.jwt.SignedJWT;\nimport com.walmartlabs.concord.common.ExternalAuthToken;\nimport com.walmartlabs.concord.github.appinstallation.cfg.GitHubAppInstallationConfig;\nimport com.walmartlabs.concord.github.appinstallation.exception.GitHubAppException;\nimport org.immutables.value.Value;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.net.http.HttpResponse.BodyHandlers;\nimport java.time.Duration;\nimport java.util.Date;\nimport java.util.function.BiFunction;\n\npublic class AccessTokenProvider {\n\n    private static final Logger log = LoggerFactory.getLogger(AccessTokenProvider.class);\n\n    private final HttpClient httpClient;\n    private final Duration httpTimeout;\n    private final ObjectMapper objectMapper;\n\n    public AccessTokenProvider(GitHubAppInstallationConfig cfg, ObjectMapper objectMapper) {\n        this.httpTimeout = cfg.getHttpClientTimeout();\n        this.objectMapper = objectMapper;\n        this.httpClient = HttpClient.newBuilder()\n                .connectTimeout(httpTimeout)\n                .build();\n    }\n\n    public AccessTokenProvider(GitHubAppInstallationConfig cfg,\n                               ObjectMapper objectMapper,\n                               HttpClient httpClient) {\n        this.httpTimeout = cfg.getHttpClientTimeout();\n        this.objectMapper = objectMapper;\n        this.httpClient = httpClient;\n    }\n\n    ExternalAuthToken getRepoInstallationToken(GitHubAppAuthConfig app, String orgRepo) throws GitHubAppException {\n        try {\n            var jwt = generateJWT(app);\n            var accessTokenUrl = getAccessTokenUrl(app.apiUrl(), orgRepo, jwt);\n            return ExternalAuthToken.StaticToken.builder()\n                    .from(createAccessToken(accessTokenUrl, jwt))\n                    .authId(app.id())\n                    .username(app.username())\n                    .build();\n        } catch (JOSEException e) {\n            throw new GitHubAppException(\"Error generating JWT for app: \" + app.clientId());\n        }\n    }\n\n    private String getAccessTokenUrl(String apiBaseUrl, String installationRepo, String jwt) throws GitHubAppException {\n        var req = HttpRequest.newBuilder().GET()\n                .uri(URI.create(apiBaseUrl + \"/repos/\" + installationRepo + \"/installation\"))\n                .header(\"Authorization\", \"Bearer \" + jwt)\n                .header(\"Accept\", \"application/vnd.github+json\")\n                .header(\"X-GitHub-Api-Version\", \"2022-11-28\")\n                .timeout(httpTimeout)\n                .build();\n\n        var appInstallation = sendRequest(req, 200, AccessTokenProvider.GitHubAppInstallationResp.class, (code, body) -> {\n            if (code == 404) {\n                // not possible to discern between repo not found and app not installed for existing (private) repo\n                log.warn(\"getAccessTokenUrl ['{}'] -> not found\", installationRepo);\n                return new GitHubAppException.NotFoundException(\"Repo not found or App installation not found for repo\");\n            }\n\n            log.warn(\"getAccessTokenUrl ['{}'] -> error: {} : {}\", installationRepo, code, body);\n            return new GitHubAppException(\"Unexpected error locating repo installation: \" + code);\n        });\n\n        return appInstallation.accessTokensUrl();\n    }\n\n    private ExternalAuthToken createAccessToken(String accessTokenUrl, String jwt) {\n        var req = HttpRequest.newBuilder()\n                .POST(HttpRequest.BodyPublishers.noBody())\n                .uri(URI.create(accessTokenUrl))\n                .header(\"Authorization\", \"Bearer \" + jwt)\n                .header(\"Accept\", \"application/vnd.github+json\")\n                .header(\"X-GitHub-Api-Version\", \"2022-11-28\")\n                .timeout(httpTimeout)\n                .build();\n\n        return sendRequest(req, 201, ExternalAuthToken.class, (code, body) -> {\n            log.warn(\"createAccessToken ['{}'] -> error: {} : {}\", accessTokenUrl, code, body);\n\n            if (code == 404) {\n                // this would be pretty odd to hit, this means the url returned from the installation lookup is invalid\n                return new GitHubAppException.NotFoundException(\"App access token url not found\");\n            }\n\n            return new GitHubAppException(\"Unexpected error creating app access token: \" + code);\n        });\n    }\n\n    private static String generateJWT(GitHubAppAuthConfig auth) throws JOSEException {\n        var pk = auth.privateKey();\n        var rsaJWK = JWK.parseFromPEMEncodedObjects(pk).toRSAKey();\n\n        // Create RSA-signer with the private key\n        var signer = new RSASSASigner(rsaJWK);\n\n        // Prepare JWT with claims set\n        var claimsSet = new JWTClaimsSet.Builder()\n                .issueTime(new Date())\n                .issuer(auth.clientId())\n                // JWT expiration. GH requires less than 10 minutes\n                .expirationTime(new Date(new Date().getTime() + Duration.ofMinutes(10).toMillis()))\n                .build();\n\n        var signedJWT = new SignedJWT(\n                new JWSHeader.Builder(JWSAlgorithm.RS256)\n                        .keyID(rsaJWK.getKeyID())\n                        .build(),\n                claimsSet);\n\n        // Compute the RSA signature\n        signedJWT.sign(signer);\n\n        // Serialize to compact form\n        return signedJWT.serialize();\n    }\n\n    private <T> T sendRequest(HttpRequest httpRequest, int expectedCode, Class<T> clazz, BiFunction<Integer, String, GitHubAppException> exFun) throws GitHubAppException {\n        try {\n            var resp = httpClient.send(httpRequest, BodyHandlers.ofInputStream());\n            if (resp.statusCode() != expectedCode) {\n                throw exFun.apply(resp.statusCode(), readBody(resp));\n            }\n            return objectMapper.readValue(resp.body(), clazz);\n        } catch (IOException e) {\n            throw new GitHubAppException(\"Error sending request\", e);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n\n        throw new IllegalStateException(\"Unexpected error sending HTTP request\");\n    }\n\n    private static String readBody(HttpResponse<InputStream> resp) throws IOException {\n        try (var is = resp.body()) {\n            return new String(is.readAllBytes());\n        }\n    }\n\n    @JsonIgnoreProperties(ignoreUnknown = true)\n    public record GitHubAppInstallationResp(\n            /*\n                This is the only attribute we **need**, even though there's other\n                attributes. Some may differ between GitHub \"cloud\" and GitHub\n                Enterprise/private. Be care if/when adding more.\n             */\n            @JsonProperty(\"access_tokens_url\")\n            String accessTokensUrl\n    ) {\n    }\n}\n"
  },
  {
    "path": "github-app-installation/src/main/java/com/walmartlabs/concord/github/appinstallation/GitHubAppAuthCacheKey.java",
    "content": "package com.walmartlabs.concord.github.appinstallation;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.net.URI;\nimport java.util.Arrays;\n\npublic record GitHubAppAuthCacheKey(URI repoUri, byte[] secretData, int weight) {\n\n    static GitHubAppAuthCacheKey from(URI repoUri) {\n        return from(repoUri, null);\n    }\n\n    static GitHubAppAuthCacheKey from(URI repoUri, byte[] secretData) {\n        var weight = 1;\n\n        if (secretData != null) {\n            weight += 1;\n            weight += secretData.length / 1024;\n        }\n\n        return new GitHubAppAuthCacheKey(repoUri, secretData, weight);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (o == null || getClass() != o.getClass()) return false;\n\n        var key = (GitHubAppAuthCacheKey) o;\n        return weight() == key.weight() && repoUri().equals(key.repoUri()) && Arrays.equals(secretData(), key.secretData());\n    }\n\n    @Override\n    public int hashCode() {\n        int result = repoUri().hashCode();\n        result = 31 * result + Arrays.hashCode(secretData());\n        result = 31 * result + weight();\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"GitHubAppAuthCacheKey{\" +\n                \"repoUri=\" + repoUri +\n                \", weight=\" + weight +\n                '}';\n    }\n}\n"
  },
  {
    "path": "github-app-installation/src/main/java/com/walmartlabs/concord/github/appinstallation/GitHubAppAuthConfig.java",
    "content": "package com.walmartlabs.concord.github.appinstallation;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.cfg.MappingAuthConfig;\n\nimport java.util.regex.Pattern;\n\npublic record GitHubAppAuthConfig(String id,\n                                  String apiUrl,\n                                  String clientId,\n                                  String privateKey,\n                                  String username,\n                                  Pattern urlPattern) implements MappingAuthConfig {\n\n    public GitHubAppAuthConfig(String id, String apiUrl, String clientId, String privateKey, String username, Pattern urlPattern) {\n        if (clientId == null || clientId.isBlank()) {\n            throw new IllegalArgumentException(\"clientId must be provided\");\n        }\n\n        if (privateKey == null || privateKey.isBlank()) {\n            throw new IllegalArgumentException(\"privateKey must be provided\");\n        }\n\n        // sanity check url pattern before this object gets too far out there\n        if (!urlPattern.toString().contains(\"?<baseUrl>\")) {\n            throw new IllegalArgumentException(\"The url pattern must contain the ?<baseUrl> named group\");\n        }\n\n        this.id = id;\n        this.apiUrl = (apiUrl == null) ? \"https://api.github.com\" : apiUrl;\n        this.clientId = clientId;\n        this.privateKey = privateKey;\n        this.username = username;\n        this.urlPattern = urlPattern;\n    }\n}\n"
  },
  {
    "path": "github-app-installation/src/main/java/com/walmartlabs/concord/github/appinstallation/GitHubAppInstallation.java",
    "content": "package com.walmartlabs.concord.github.appinstallation;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.google.common.cache.CacheBuilder;\nimport com.google.common.cache.CacheLoader;\nimport com.google.common.cache.LoadingCache;\nimport com.google.common.cache.Weigher;\nimport com.google.common.util.concurrent.UncheckedExecutionException;\nimport com.walmartlabs.concord.common.AuthTokenProvider;\nimport com.walmartlabs.concord.common.ExternalAuthToken;\nimport com.walmartlabs.concord.common.cfg.MappingAuthConfig;\nimport com.walmartlabs.concord.common.secret.BinaryDataSecret;\nimport com.walmartlabs.concord.github.appinstallation.cfg.GitHubAppInstallationConfig;\nimport com.walmartlabs.concord.github.appinstallation.exception.GitHubAppException;\nimport com.walmartlabs.concord.github.appinstallation.exception.RepoExtractionException;\nimport com.walmartlabs.concord.sdk.Secret;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.annotation.Nonnull;\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport java.net.URI;\nimport java.util.Optional;\nimport java.util.concurrent.ExecutionException;\n\npublic class GitHubAppInstallation implements AuthTokenProvider {\n\n    private static final Logger log = LoggerFactory.getLogger(GitHubAppInstallation.class);\n\n    private final GitHubAppInstallationConfig cfg;\n    private final AccessTokenProvider tokenProvider;\n    private final ObjectMapper objectMapper;\n\n    private final LoadingCache<GitHubAppAuthCacheKey, Optional<ExternalAuthToken>> cache;\n\n    @Inject\n    public GitHubAppInstallation(GitHubAppInstallationConfig cfg, ObjectMapper objectMapper) {\n        this.cfg = cfg;\n        this.objectMapper = objectMapper;\n        this.tokenProvider = new AccessTokenProvider(cfg, objectMapper);\n\n        this.cache = CacheBuilder.newBuilder()\n                .expireAfterWrite(cfg.getSystemAuthCacheDuration())\n                .maximumWeight(cfg.getSystemAuthCacheMaxWeight())\n                .weigher((Weigher<GitHubAppAuthCacheKey, Optional<ExternalAuthToken>>) (key, value) -> key.weight())\n                .build(new CacheLoader<>() {\n                    @Override\n                    public @Nonnull Optional<ExternalAuthToken> load(@Nonnull GitHubAppAuthCacheKey key) {\n                        return fetchToken(key.repoUri(), key.secretData());\n                    }\n                });\n    }\n\n    @Override\n    public boolean supports(URI repo, @Nullable Secret secret) {\n        return Utils.validateSecret(secret, objectMapper) || systemSupports(repo);\n    }\n\n    private GitHubAppAuthCacheKey createKey(URI repoUri, @Nullable Secret secret) {\n        if (secret == null) {\n            return GitHubAppAuthCacheKey.from(repoUri);\n        }\n\n        if (secret instanceof BinaryDataSecret bds) {\n            return GitHubAppAuthCacheKey.from(repoUri, bds.getData());\n        }\n\n        return null;\n    }\n\n    @Override\n    public Optional<ExternalAuthToken> getToken(URI repo, @Nullable Secret secret) {\n        var cacheKey = createKey(repo, secret);\n\n        if (cacheKey == null) {\n            return Optional.empty();\n        }\n\n        try {\n            var activeToken = cache.get(cacheKey);\n\n            return activeToken.map(t -> refreshBeforeExpire(t, cacheKey));\n        } catch (ExecutionException e) {\n            throw new GitHubAppException(\"Error retrieving access token for repo: \" + repo, e);\n        } catch (UncheckedExecutionException e) {\n            // unwrap from guava\n            if (e.getCause() instanceof GitHubAppException repoEx) {\n                throw repoEx;\n            }\n\n            log.warn(\"getAccessToken ['{}'] -> error: {}\", repo,  e.getMessage());\n\n            throw new GitHubAppException(\"Unexpected error retrieving access token for repo: \" + repo);\n        }\n    }\n\n    public long cacheSize() {\n        return cache.size();\n    }\n\n    private boolean systemSupports(URI repoUri) {\n        return cfg.getAuthConfigs().stream().anyMatch(auth -> auth.canHandle(repoUri));\n    }\n\n    private Optional<ExternalAuthToken> fetchToken(URI repo, @Nullable byte[] secret) {\n        if (secret != null) {\n            return Optional.ofNullable(fromBinaryData(repo, secret));\n        }\n\n        // no secret, see if system config has something for this repo\n        return cfg.getAuthConfigs().stream()\n                .filter(auth -> auth.canHandle(repo))\n                .findFirst()\n                .map(auth -> {\n                    if (auth instanceof MappingAuthConfig.OauthAuthConfig tokenAuth) {\n                        return ExternalAuthToken.StaticToken.builder()\n                                .authId(tokenAuth.id())\n                                .token(tokenAuth.token())\n                                .username(tokenAuth.username())\n                                .build();\n                    }\n\n                    if (auth instanceof GitHubAppAuthConfig app) {\n                        return getTokenFromAppInstall(app, repo);\n                    }\n\n                    throw new IllegalArgumentException(\"Unsupported GitAuth type for repo: \" + repo);\n                });\n    }\n\n    /**\n     * Cache may return a token that's close to expiring. If it's too close,\n     * invalidate and get a new one. If it's just a little close, refresh the\n     * cache in the background and return the still-active token.\n     */\n    private ExternalAuthToken refreshBeforeExpire(@Nonnull ExternalAuthToken token, GitHubAppAuthCacheKey cacheKey) {\n        if (token.secondsUntilExpiration() < 10) {\n            // not enough time to be useful. get a new token right now\n            cache.invalidate(cacheKey);\n            try {\n                return cache.get(cacheKey).orElse(null);\n            } catch (ExecutionException e) {\n                throw new GitHubAppException(\"Error retrieving access token for repo: \" + cacheKey.repoUri(), e);\n            }\n        }\n\n        // refresh cache if the token is expiring soon, doesn't affect current token\n        if (token.secondsUntilExpiration() < 300) {\n            cache.refresh(cacheKey);\n        }\n\n        return token;\n    }\n\n    private ExternalAuthToken fromBinaryData(URI repo, byte[] data) {\n        var appInfo = Utils.parseAppInstallation(data, objectMapper);\n        if (appInfo.isPresent()) {\n            // great, it's apparently a valid app installation config\n            return getTokenFromAppInstall(appInfo.get(), repo);\n        }\n\n        // hopefully it's just a token a plaintext token\n        return ExternalAuthToken.StaticToken.builder()\n                .token(new String(data).trim())\n                .build();\n    }\n\n    private ExternalAuthToken getTokenFromAppInstall(GitHubAppAuthConfig app, URI repo) {\n        try {\n            var ownerAndRepo = Utils.extractOwnerAndRepo(app, repo);\n            return accessTokenProvider().getRepoInstallationToken(app, ownerAndRepo);\n        } catch (RepoExtractionException | GitHubAppException e) {\n            var msg = e.getMessage();\n            log.warn(\"getTokenFromAppInstall ['{}', '{}'] Error retrieving GitHub access token: {}\", app.apiUrl(), repo, msg);\n        }\n\n        return null;\n    }\n\n    AccessTokenProvider accessTokenProvider() {\n        return tokenProvider;\n    }\n\n}\n"
  },
  {
    "path": "github-app-installation/src/main/java/com/walmartlabs/concord/github/appinstallation/GitHubInstallationToken.java",
    "content": "package com.walmartlabs.concord.github.appinstallation;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ExternalAuthToken;\nimport org.immutables.value.Value;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface GitHubInstallationToken extends ExternalAuthToken {\n\n    static ImmutableGitHubInstallationToken.Builder builder() {\n        return ImmutableGitHubInstallationToken.builder();\n    }\n}\n"
  },
  {
    "path": "github-app-installation/src/main/java/com/walmartlabs/concord/github/appinstallation/Utils.java",
    "content": "package com.walmartlabs.concord.github.appinstallation;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.typesafe.config.Config;\nimport com.walmartlabs.concord.common.secret.BinaryDataSecret;\nimport com.walmartlabs.concord.github.appinstallation.exception.GitHubAppException;\nimport com.walmartlabs.concord.github.appinstallation.exception.RepoExtractionException;\nimport com.walmartlabs.concord.sdk.Secret;\n\nimport java.net.URI;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.function.Supplier;\n\npublic class Utils {\n\n    private static final String KEY_GITHUB_APP_INSTALLATION = \"githubAppInstallation\";\n\n    /**\n     * Validates given secret is usable enough to attempt a remote lookup. Not\n     * guaranteed to actually work, just a sanity check useful to avoid attempting\n     * API calls with something that will definitely not work.\n     */\n    static boolean validateSecret(Secret secret, ObjectMapper mapper) {\n        // secret must be one of:\n        // * JSON-formatted GitHub app installation details: clientId, privateKey, urlPattern\n        // * single-line, plaintext access token\n\n        if (secret == null) {\n            return false;\n        }\n\n        if (!(secret instanceof BinaryDataSecret bds)) {\n            // this class is not the place for handling key pairs or username/password\n            return false;\n        }\n\n        var base = parseRawAppInstallation(bds.getData(), mapper);\n        if (base == null) {\n            // It's not JSON, may be an oauth token\n            return isPrintableAscii(bds.getData());\n        } else if (base.isEmpty()) {\n            // Doesn't match something we can parse\n            return false;\n        }\n\n        // App installation config format is either valid or not\n        return parseAppInstallation(bds.getData(), mapper).isPresent();\n    }\n\n    private static boolean isPrintableAscii(byte[] bytes) {\n        if (bytes == null || bytes.length == 0) {\n            return false;\n        }\n\n        for (byte b : bytes) {\n            // Cast byte to int to avoid issues with negative byte values\n            int asciiValue = b & 0xFF; // Use bitwise AND to get unsigned value\n\n            if (asciiValue < 32 || asciiValue > 126) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    static Map<String, Object> parseRawAppInstallation(byte[] bds, ObjectMapper mapper) {\n        var t = mapper.getTypeFactory().constructMapType(HashMap.class, String.class, Object.class);\n\n        try { // find out if it's at least valid JSON.\n            var base = mapper.<Map<String, Object>>readValue(bds, t);\n            if (base.containsKey(KEY_GITHUB_APP_INSTALLATION)) {\n                return base;\n            } else {\n                // it's JSON, but not in our format\n                return Map.of();\n            }\n        } catch (Exception e) {\n            // invalid JSON, may be a plaintext token\n            return null;\n        }\n    }\n\n    static Optional<GitHubAppAuthConfig> parseAppInstallation(byte[] bds, ObjectMapper mapper) {\n        var base = parseRawAppInstallation(bds, mapper);\n\n        if (base == null || !base.containsKey(KEY_GITHUB_APP_INSTALLATION)) {\n            // it's either not JSON or not in our format\n            return Optional.empty();\n        }\n\n        try { // great, now convert it to the expected structure\n            return Optional.of(mapper.convertValue(base.get(KEY_GITHUB_APP_INSTALLATION), GitHubAppAuthConfig.class));\n        } catch (IllegalArgumentException e) {\n            // doesn't match the expected structure\n            throw new GitHubAppException(\"Invalid app installation definition.\", e);\n        }\n    }\n\n    static String extractOwnerAndRepo(GitHubAppAuthConfig auth, URI repo) throws RepoExtractionException {\n        var baseUrl = getBaseUrl(auth, repo);\n        var relevantPath = repo.toString().replaceAll(\"^.*\" + baseUrl + \"/?\", \"\")\n                .replaceAll(repo.getQuery() != null ? \"\\\\?\" + repo.getQuery() : \"\", \"\")\n                .replaceFirst(\"\\\\.git$\", \"\");\n\n        // parse out the owner/repo from the path\n        var pathParts = Arrays.stream(relevantPath.split(\"/\"))\n                .filter(e -> !e.isBlank())\n                .limit(2)\n                .toList();\n\n        if (pathParts.size() != 2) {\n            throw new RepoExtractionException(\"Failed to parse owner and repository from path: \" + repo.getPath());\n        }\n\n        return pathParts.get(0) + \"/\" + pathParts.get(1);\n    }\n\n    private static String getBaseUrl(GitHubAppAuthConfig auth, URI repo) {\n        var port = (repo.getPort() == -1 ? \"\" : (\":\" + repo.getPort()));\n        var path = (repo.getPath() == null ? \"\" : repo.getPath());\n        var repoHostPortAndPath = repo.getHost() + port + path;\n\n        var match = auth.urlPattern().matcher(repoHostPortAndPath);\n\n        if (!match.matches()) {\n            // at this point, this should only fail if the urlPattern is not\n            // constructed correctly. We wouldn't get there if the pattern didn't\n            // match the repo in the first place.\n            throw new RepoExtractionException(\"Failed to parse owner and repository from path: \" + repo.getPath());\n        }\n\n        return match.group(\"baseUrl\");\n    }\n\n    public static String getStringOrDefault(Config cfg, String key, Supplier<String> defaultValueSupplier) {\n        if (cfg.hasPath(key)) {\n            return cfg.getString(key);\n        }\n        return defaultValueSupplier.get();\n    }\n\n    private Utils() {}\n\n}\n"
  },
  {
    "path": "github-app-installation/src/main/java/com/walmartlabs/concord/github/appinstallation/cfg/GitHubAppInstallationConfig.java",
    "content": "package com.walmartlabs.concord.github.appinstallation.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.typesafe.config.Config;\nimport com.walmartlabs.concord.common.cfg.MappingAuthConfig;\nimport com.walmartlabs.concord.github.appinstallation.GitHubAppAuthConfig;\nimport com.walmartlabs.concord.github.appinstallation.exception.GitHubAppException;\nimport org.immutables.value.Value;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.Optional;\n\nimport static com.walmartlabs.concord.common.cfg.MappingAuthConfig.assertBaseUrlPattern;\nimport static com.walmartlabs.concord.github.appinstallation.Utils.getStringOrDefault;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface GitHubAppInstallationConfig {\n\n    List<MappingAuthConfig> getAuthConfigs();\n\n    @Value.Default\n    default Duration getSystemAuthCacheDuration() {\n        return Duration.ofMinutes(50);\n    }\n\n    @Value.Default\n    default Duration getHttpClientTimeout() {\n        return Duration.ofSeconds(30);\n    }\n\n    /**\n     * Weight is roughly calculated in kilobytes. Any cached item will have a\n     * minimum weight of 1. While further weight calculations are based on size\n     * of the given secret, if any.\n     * <p/>\n     * The default of 10,240 (~10MB) can hold approximately:\n     * <ul>\n     *     <li>10,000 tokens with no secret</li>\n     *     <li>5,000 tokens with string or credentials secret</li>\n     *     <li>3,500 tokens with app private key secret</li>\n     * </ul>\n     */\n    @Value.Default\n    default long getSystemAuthCacheMaxWeight() {\n        return 1024 * 10L;\n    }\n\n    static ImmutableGitHubAppInstallationConfig.Builder builder() {\n        return ImmutableGitHubAppInstallationConfig.builder();\n    }\n\n    static GitHubAppInstallationConfig fromConfig(Config config) {\n        var auths = config.getConfigList(\"auth\").stream()\n                .map(GitHubAppInstallationConfig::toGitAuth)\n                .toList();\n\n        var builder = builder();\n\n        if (config.hasPath(\"httpClientTimeout\")) {\n            builder.httpClientTimeout(config.getDuration(\"httpClientTimeout\"));\n        }\n\n        if (config.hasPath(\"systemAuthCacheDuration\")) {\n            builder.systemAuthCacheDuration(config.getDuration(\"systemAuthCacheDuration\"));\n        }\n\n        if (config.hasPath(\"systemAuthCacheMaxWeight\")) {\n            builder.systemAuthCacheMaxWeight(config.getInt(\"systemAuthCacheMaxWeight\"));\n        }\n\n        return builder\n                .authConfigs(auths)\n                .build();\n    }\n\n    enum AuthSource {\n        OAUTH_TOKEN,\n        GITHUB_APP_INSTALLATION,\n    }\n\n    interface AuthConfig {\n        MappingAuthConfig toGitAuth();\n    }\n\n    static MappingAuthConfig toGitAuth(com.typesafe.config.Config auth) {\n        var a = switch (AuthSource.valueOf(auth.getString(\"type\").toUpperCase())) {\n                    case OAUTH_TOKEN -> OauthConfig.from(auth);\n                    case GITHUB_APP_INSTALLATION -> AppInstallationConfig.from(auth);\n                };\n        return a.toGitAuth();\n    }\n\n    record OauthConfig(String id, String urlPattern, String username, String token) implements AuthConfig {\n\n        static OauthConfig from(com.typesafe.config.Config cfg) {\n            return new OauthConfig(\n                    getStringOrDefault(cfg, \"id\", () -> \"github-oauth-token\"),\n                    cfg.getString(\"urlPattern\"),\n                    getStringOrDefault(cfg, \"username\", () -> null),\n                    cfg.getString(\"token\")\n            );\n        }\n\n        @Override\n        public MappingAuthConfig toGitAuth() {\n            return MappingAuthConfig.OauthAuthConfig.builder()\n                    .id(this.id())\n                    .token(this.token())\n                    .username(this.username())\n                    .urlPattern(assertBaseUrlPattern(this.urlPattern()))\n                    .build();\n        }\n    }\n\n    record AppInstallationConfig(String id,\n                                 String urlPattern,\n                                 String username,\n                                 String apiUrl,\n                                 String clientId,\n                                 String privateKey) implements AuthConfig {\n\n        static AppInstallationConfig from(com.typesafe.config.Config cfg) {\n\n            var username = Optional.ofNullable(getStringOrDefault(cfg, \"username\", () -> null))\n                    .filter(s -> !s.isBlank())\n                    .orElse(\"x-access-token\");\n\n            var apiUrl = Optional.ofNullable(getStringOrDefault(cfg, \"apiUrl\", () -> null))\n                    .filter(s -> !s.isBlank())\n                    .orElse(\"https://api.github.com\");\n\n            return new AppInstallationConfig(\n                    getStringOrDefault(cfg, \"id\", () -> \"github-app-installation\"),\n                    cfg.getString(\"urlPattern\"),\n                    username,\n                    apiUrl,\n                    cfg.getString(\"clientId\"),\n                    cfg.getString(\"privateKey\")\n            );\n        }\n\n        @Override\n        public MappingAuthConfig toGitAuth() {\n            try {\n                var pkData = Files.readString(Paths.get(this.privateKey()));\n\n                return new GitHubAppAuthConfig(\n                        this.id(),\n                        this.apiUrl(),\n                        this.clientId(),\n                        pkData,\n                        this.username(),\n                        assertBaseUrlPattern(this.urlPattern())\n                );\n            } catch (IOException e) {\n                throw new GitHubAppException(\"Error initializing Git App Installation auth\", e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "github-app-installation/src/main/java/com/walmartlabs/concord/github/appinstallation/exception/GitHubAppException.java",
    "content": "package com.walmartlabs.concord.github.appinstallation.exception;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class GitHubAppException extends RuntimeException {\n    public GitHubAppException(String message) {\n        super(message);\n    }\n\n    public GitHubAppException(String message, Throwable cause) {\n        super(message, cause);\n    }\n\n    public static class NotFoundException extends GitHubAppException {\n        public NotFoundException(String message) {\n            super(message);\n        }\n\n        public NotFoundException(String message, Throwable cause) {\n            super(message, cause);\n        }\n    }\n}\n"
  },
  {
    "path": "github-app-installation/src/main/java/com/walmartlabs/concord/github/appinstallation/exception/RepoExtractionException.java",
    "content": "package com.walmartlabs.concord.github.appinstallation.exception;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class RepoExtractionException extends IllegalArgumentException {\n\n    public RepoExtractionException(String s) {\n        super(s);\n    }\n}\n"
  },
  {
    "path": "github-app-installation/src/test/java/com/walmartlabs/concord/github/appinstallation/AccessTokenProviderTest.java",
    "content": "package com.walmartlabs.concord.github.appinstallation;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.cfg.MappingAuthConfig;\nimport com.walmartlabs.concord.github.appinstallation.cfg.GitHubAppInstallationConfig;\nimport com.walmartlabs.concord.github.appinstallation.exception.GitHubAppException;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.InputStream;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpResponse;\nimport java.util.List;\n\nimport static com.walmartlabs.concord.github.appinstallation.TestConstants.PRIVATE_KEY_TEXT;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.when;\n\n@ExtendWith(MockitoExtension.class)\nclass AccessTokenProviderTest {\n\n    @Mock\n    HttpClient httpClient;\n\n    @Mock\n    HttpResponse<InputStream> tokenUrlResponse;\n\n    @Mock\n    HttpResponse<InputStream> accessTokenResponse;\n\n    private static final GitHubAppAuthConfig auth = new GitHubAppAuthConfig(\"test-auth\", null, \"123\", PRIVATE_KEY_TEXT, null, MappingAuthConfig.assertBaseUrlPattern(\"(?<baseUrl>github.local)/\"));\n\n    private static final GitHubAppInstallationConfig CFG = GitHubAppInstallationConfig.builder()\n            .authConfigs(List.of(auth))\n            .build();\n\n    @Test\n    void test() throws Exception {\n        when(httpClient.send(any(), any(HttpResponse.BodyHandler.class)))\n                .thenReturn(tokenUrlResponse, accessTokenResponse);\n\n        when(tokenUrlResponse.statusCode()).thenReturn(200);\n        when(tokenUrlResponse.body()).thenReturn(asInputStream(ACCESS_TOKEN_INSTALLATION_RESPONSE));\n\n        when(accessTokenResponse.statusCode()).thenReturn(201);\n        when(accessTokenResponse.body()).thenReturn(asInputStream(ACCESS_TOKEN_RESPONSE));\n\n        var provider = new AccessTokenProvider(CFG, TestConstants.MAPPPER, httpClient);\n\n        // --\n\n        var result = provider.getRepoInstallationToken(auth, \"owner/repo\");\n\n        // --\n\n        assertNotNull(result);\n        assertEquals(\"mock-token\", result.token());\n        assertTrue(result.secondsUntilExpiration() > 300);\n    }\n\n    @Test\n    void testAppNotInstalled() throws Exception {\n        when(httpClient.send(any(), any(HttpResponse.BodyHandler.class)))\n                .thenReturn(tokenUrlResponse);\n\n        when(tokenUrlResponse.statusCode()).thenReturn(404);\n        when(tokenUrlResponse.body()).thenReturn(asInputStream(\"App is not installed on repo\"));\n\n        var provider = new AccessTokenProvider(CFG, TestConstants.MAPPPER, httpClient);\n\n        // --\n\n        var ex = assertThrows(GitHubAppException.NotFoundException.class,\n                () -> provider.getRepoInstallationToken(auth, \"owenr/repo\"));\n\n        // --\n\n        assertTrue(ex.getMessage().contains(\"Repo not found or App installation not found for repo\"));\n    }\n\n    @Test\n    void testErrorCreatingToken() throws Exception {\n        when(httpClient.send(any(), any(HttpResponse.BodyHandler.class)))\n                .thenReturn(tokenUrlResponse, accessTokenResponse);\n\n        when(tokenUrlResponse.statusCode()).thenReturn(200);\n        when(tokenUrlResponse.body()).thenReturn(asInputStream(ACCESS_TOKEN_INSTALLATION_RESPONSE));\n\n        when(accessTokenResponse.statusCode()).thenReturn(500);\n        when(accessTokenResponse.body()).thenReturn(asInputStream(\"server error\"));\n\n        var provider = new AccessTokenProvider(CFG, TestConstants.MAPPPER, httpClient);\n\n        // --\n\n        var ex = assertThrows(GitHubAppException.class,\n                () -> provider.getRepoInstallationToken(auth, \"owenr/repo\"));\n\n        // --\n\n        assertTrue(ex.getMessage().contains(\"Unexpected error creating app access token: 500\"));\n    }\n\n    private static final String ACCESS_TOKEN_INSTALLATION_RESPONSE = \"\"\"\n            {\n                \"access_tokens_url\": \"https://github.local/access_tokens\",\n                \"extra_field\": \"should be ignored\"\n            }\"\"\";\n\n    private static final String ACCESS_TOKEN_RESPONSE = \"\"\"\n            {\n                \"token\": \"mock-token\",\n                \"expires_at\": \"2099-12-31T23:59:59Z\"\n            }\"\"\";\n\n    private static InputStream asInputStream(String s) {\n        return new ByteArrayInputStream(s.getBytes());\n    }\n\n}\n"
  },
  {
    "path": "github-app-installation/src/test/java/com/walmartlabs/concord/github/appinstallation/GitHubAppAuthConfigTest.java",
    "content": "package com.walmartlabs.concord.github.appinstallation;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.cfg.MappingAuthConfig;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nclass GitHubAppAuthConfigTest {\n\n    @Test\n    void testUrlPatternMissingNamedGroup() {\n        var ex = assertThrows(IllegalArgumentException.class, () -> new GitHubAppAuthConfig(\n                \"test-auth\",\n                \"https://api.github.com\",\n                \"mock-client-id\",\n                \"/not/used/in/test\",\n                null,\n                MappingAuthConfig.assertBaseUrlPattern(\".*\")\n        ));\n\n        assertTrue(ex.getMessage().contains(\"The url pattern must contain the ?<baseUrl> named group\"));\n    }\n}\n"
  },
  {
    "path": "github-app-installation/src/test/java/com/walmartlabs/concord/github/appinstallation/GitHubAppInstallationTest.java",
    "content": "package com.walmartlabs.concord.github.appinstallation;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.common.ExternalAuthToken;\nimport com.walmartlabs.concord.common.ObjectMapperProvider;\nimport com.walmartlabs.concord.common.cfg.MappingAuthConfig;\nimport com.walmartlabs.concord.common.secret.BinaryDataSecret;\nimport com.walmartlabs.concord.github.appinstallation.cfg.GitHubAppInstallationConfig;\nimport com.walmartlabs.concord.sdk.Secret;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\nimport java.net.URI;\nimport java.nio.charset.StandardCharsets;\nimport java.time.OffsetDateTime;\nimport java.util.List;\n\nimport static com.walmartlabs.concord.github.appinstallation.TestConstants.APP_INSTALL_CONTENT;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\n@ExtendWith(MockitoExtension.class)\nclass GitHubAppInstallationTest {\n\n    private static final ObjectMapper MAPPER = new ObjectMapperProvider().get();\n\n    @Mock\n    AccessTokenProvider accessTokenProvider;\n\n    private static final URI VALID_APP_AUTH_URI_01 = URI.create(\"https://github.local/owner/repo-1.git\");\n    private static final URI VALID_APP_AUTH_URI_02 = URI.create(\"https://github.local/owner/repo-2.git\");\n    private static final URI VALID_STATIC_AUTH_URI_01 = URI.create(\"https://staticgithub.local/owner/repo-1.git\");\n\n    private static final GitHubAppAuthConfig auth = new GitHubAppAuthConfig(\n            \"test-app-auth\",\n            null,\n            \"123\",\n            \"/does/not/exist\",\n            null,\n            MappingAuthConfig.assertBaseUrlPattern(\"(?<baseUrl>github.local)/\"));\n    private static final MappingAuthConfig.OauthAuthConfig staticKeyAuth = MappingAuthConfig.OauthAuthConfig.builder()\n            .id(\"test-static-auth\")\n            .urlPattern(MappingAuthConfig.assertBaseUrlPattern(\"(?<baseUrl>staticgithub.local)/\"))\n            .token(\"static-token\")\n            .build();\n    private final GitHubAppInstallationConfig CFG = GitHubAppInstallationConfig.builder()\n            .authConfigs(List.of(staticKeyAuth, auth))\n            .build();\n    private static final ExternalAuthToken TOKEN_1HR = ExternalAuthToken.SimpleToken.builder()\n            .token(\"mock-installation-token\")\n            .expiresAt(OffsetDateTime.now().plusHours(1))\n            .build();\n    private static final ExternalAuthToken TOKEN_200S = ExternalAuthToken.SimpleToken.builder()\n            .token(\"mock-200-seconds-expiration-token\")\n            .expiresAt(OffsetDateTime.now().plusSeconds(200))\n            .build();\n    private static final ExternalAuthToken TOKEN_9S = ExternalAuthToken.SimpleToken.builder()\n            .token(\"mock-9-seconds-expiration-token\")\n            .expiresAt(OffsetDateTime.now().plusSeconds(9))\n            .build();\n    private static final Secret MOCK_STATIC_SECRET = new BinaryDataSecret(staticKeyAuth.token().getBytes(StandardCharsets.UTF_8));\n\n    private static final Secret MOCK_APP_INSTALL_SECRET = new BinaryDataSecret(APP_INSTALL_CONTENT.getBytes(StandardCharsets.UTF_8));\n\n    @Test\n    void testCache() {\n        // -- prepare 0\n        var app = new TestApp(CFG, MAPPER, accessTokenProvider);\n\n        // -- validate 0\n        // empty when nothing was ever looked up\n        assertEquals(0, app.cacheSize());\n\n        // -- prepare 1\n        when(accessTokenProvider.getRepoInstallationToken(any(), any()))\n                .thenReturn(TOKEN_1HR);\n\n        // -- execute 1\n        var tokenResp = app.getToken(VALID_APP_AUTH_URI_01, null);\n\n        // -- verify 1\n        assertTrue(tokenResp.isPresent());\n        assertEquals(TOKEN_1HR, tokenResp.get());\n        assertEquals(1, app.cacheSize());\n        verify(accessTokenProvider, times(1))\n                .getRepoInstallationToken(any(), any());\n\n        // -- execute 2\n        // should hit cache\n        app.getToken(VALID_APP_AUTH_URI_01, null);\n        app.getToken(VALID_APP_AUTH_URI_01, null);\n\n        // -- verify 2\n        verify(accessTokenProvider, times(1))\n                .getRepoInstallationToken(any(), any());\n\n        // -- execute 3\n        // Different repo, will retrieve first and hit cache second\n        app.getToken(VALID_APP_AUTH_URI_02, null);\n        app.getToken(VALID_APP_AUTH_URI_02, null);\n\n        // -- verify 3\n        // cache now has 2 entries\n        verify(accessTokenProvider, times(2))\n                .getRepoInstallationToken(any(), any());\n    }\n\n    @Test\n    void testRefreshBeforeExpire() {\n        // -- prepare 0\n        when(accessTokenProvider.getRepoInstallationToken(any(), any()))\n                .thenReturn(TOKEN_200S, TOKEN_1HR);\n        var app = new TestApp(CFG, MAPPER, accessTokenProvider);\n\n        // -- execute 1\n        var tokenResp = app.getToken(VALID_APP_AUTH_URI_01, null);\n\n        // -- verify 1\n        // first is usable so we receive that token, new token is refreshed in background\n        assertTrue(tokenResp.isPresent());\n        assertEquals(TOKEN_200S, tokenResp.get());\n        assertEquals(1, app.cacheSize());\n        //\n        verify(accessTokenProvider, times(2))\n                .getRepoInstallationToken(any(), any());\n    }\n\n    @Test\n    void testForceRefreshBeforeExpire() {\n        // -- prepare 0\n        when(accessTokenProvider.getRepoInstallationToken(any(), any()))\n                .thenReturn(TOKEN_9S, TOKEN_1HR);\n        var app = new TestApp(CFG, MAPPER, accessTokenProvider);\n\n        // -- execute 1\n        var tokenResp = app.getToken(VALID_APP_AUTH_URI_01, null);\n\n        // -- verify 1\n        assertTrue(tokenResp.isPresent());\n        assertEquals(TOKEN_1HR, tokenResp.get());\n        assertEquals(1, app.cacheSize());\n        // first is usable refresh is forced we get the new token\n        verify(accessTokenProvider, times(2))\n                .getRepoInstallationToken(any(), any());\n    }\n\n    @Test\n    void testFromStaticTokenTokenConfig() {\n        // -- prepare 0\n        var app = new TestApp(CFG, MAPPER, accessTokenProvider);\n\n        // -- execute 1\n        var tokenResp = app.getToken(VALID_STATIC_AUTH_URI_01, null);\n\n        // -- verify 1\n        assertTrue(tokenResp.isPresent());\n        assertEquals(staticKeyAuth.token(), tokenResp.get().token());\n        assertEquals(1, app.cacheSize());\n        // not an app installation, no lookup expected\n        verify(accessTokenProvider, times(0))\n                .getRepoInstallationToken(any(), any());\n    }\n\n    @Test\n    void testFromStaticTokenSecret() {\n        // -- prepare 0\n        var app = new TestApp(CFG, MAPPER, accessTokenProvider);\n\n        // -- execute 1\n        var tokenResp = app.getToken(VALID_APP_AUTH_URI_01, MOCK_STATIC_SECRET);\n\n        // -- verify 1\n        assertTrue(tokenResp.isPresent());\n        assertEquals(staticKeyAuth.token(), tokenResp.get().token());\n        assertEquals(1, app.cacheSize());\n        // not an app installation, no lookup expected\n        verify(accessTokenProvider, times(0))\n                .getRepoInstallationToken(any(), any());\n    }\n\n    @Test\n    void testFromAppInstallSecret() {\n        // -- prepare 0\n        when(accessTokenProvider.getRepoInstallationToken(any(), any()))\n                .thenReturn(TOKEN_1HR);\n        var app = new TestApp(CFG, MAPPER, accessTokenProvider);\n\n        // -- execute 1\n        var tokenResp = app.getToken(VALID_APP_AUTH_URI_01, MOCK_APP_INSTALL_SECRET);\n\n        // -- verify 1\n        assertTrue(tokenResp.isPresent());\n        assertEquals(TOKEN_1HR, tokenResp.get());\n        assertEquals(1, app.cacheSize());\n        verify(accessTokenProvider, times(1))\n                .getRepoInstallationToken(any(), any());\n    }\n\n    private static class TestApp extends GitHubAppInstallation {\n        private final AccessTokenProvider tokenProvider;\n\n        public TestApp(GitHubAppInstallationConfig cfg, ObjectMapper objectMapper, AccessTokenProvider tokenProvider) {\n            super(cfg, objectMapper);\n            this.tokenProvider = tokenProvider;\n        }\n\n        @Override\n        AccessTokenProvider accessTokenProvider() {\n            return tokenProvider;\n        }\n    }\n}\n"
  },
  {
    "path": "github-app-installation/src/test/java/com/walmartlabs/concord/github/appinstallation/RepoExtractionTest.java",
    "content": "package com.walmartlabs.concord.github.appinstallation;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.cfg.MappingAuthConfig;\nimport org.junit.jupiter.params.ParameterizedTest;\nimport org.junit.jupiter.params.provider.MethodSource;\n\nimport java.net.URI;\nimport java.util.stream.Stream;\n\nimport static org.junit.jupiter.api.Assertions.assertDoesNotThrow;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nclass RepoExtractionTest {\n\n    static Stream<URI> validPublicUris() {\n        return Stream.of(\n                \"https://github.com/owner/repo.git\", // typicalRepo\n                \"https://github.com/owner/repo\",             // no trailing '.git', should still work\n                \"https://github.com/owner/repo/\",            // ...same with trailing slash\n                // with query params. not very typical, but does work in this pattern matching\n                \"https://github.com/owner/repo.git?hello=world\",\n                \"https://github.com/owner/repo?hello=world\",\n                \"https://github.com/owner/repo/?hello=world\"\n        ).map(URI::create);\n    }\n\n    static Stream<URI> invalidPublicUris() {\n        return Stream.of(\n                \"https://github.com/owner\", // no repo\n                \"https://github.com/owner/\"         // no repo with slash\n        ).map(URI::create);\n    }\n\n    static Stream<URI> validProxiedUris() {\n        return Stream.of(\n                \"https://git.company.local/proxypath/owner/repo.git\", // typicalRepo\n                \"https://git.company.local/proxypath/owner/repo\",             // no trailing '.git', should still work\n                \"https://git.company.local/proxypath/owner/repo/\",            // ...same with trailing slash\n                // with query params. not very typical, but does work in this pattern matching\n                \"https://git.company.local/proxypath/owner/repo.git?hello=world\",\n                \"https://git.company.local/proxypath/owner/repo?hello=world\",\n                \"https://git.company.local/proxypath/owner/repo/?hello=world\"\n        ).map(URI::create);\n    }\n\n    static Stream<URI> invalidProxiedUris() {\n        return Stream.of(\n                \"https://git.company.local/proxypath/owner\", // no repo\n                \"https://git.company.local/proxypath/owner/\"         // no repo with slash\n        ).map(URI::create);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"validPublicUris\")\n    void testValidPublicUris(URI repo) {\n        var ownerAndRepo = assertDoesNotThrow(() -> runExtract(\"(?<baseUrl>github.com).*\", repo));\n        assertEquals(\"owner/repo\", ownerAndRepo);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"invalidPublicUris\")\n    void testInvalidPublicUris(URI repo) {\n        var ex = assertThrows(IllegalArgumentException.class,\n                () -> runExtract(\"(?<baseUrl>github.com).*\", repo));\n        assertTrue(ex.getMessage().contains(\"Failed to parse owner and repository from path\"));\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"validProxiedUris\")\n    void testValidProxiedUris(URI repo) {\n        var ownerAndRepo = assertDoesNotThrow(() -> runExtract(\"(?<baseUrl>git.company.local/proxypath).*\", repo));\n        assertEquals(\"owner/repo\", ownerAndRepo);\n    }\n\n    @ParameterizedTest\n    @MethodSource(\"invalidProxiedUris\")\n    void testInvalidProxiedUris(URI repo) {\n        var ex = assertThrows(IllegalArgumentException.class,\n                () -> runExtract(\"(?<baseUrl>git.company.local/proxypath).*\", repo));\n        assertTrue(ex.getMessage().contains(\"Failed to parse owner and repository from path\"));\n    }\n\n    private static String runExtract(String pattern, URI repo) {\n        var auth = getAuth(pattern);\n        return Utils.extractOwnerAndRepo(auth, repo);\n    }\n\n    private static GitHubAppAuthConfig getAuth(String urlPattern) {\n        return new GitHubAppAuthConfig(\n                \"test-auth\",\n                \"https://api.github.com\",\n                \"1234\",\n                \"/not/used\",\n                null,\n                MappingAuthConfig.assertBaseUrlPattern(urlPattern)\n        );\n    }\n}\n"
  },
  {
    "path": "github-app-installation/src/test/java/com/walmartlabs/concord/github/appinstallation/TestConstants.java",
    "content": "package com.walmartlabs.concord.github.appinstallation;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.common.ObjectMapperProvider;\nimport com.walmartlabs.concord.common.secret.BinaryDataSecret;\nimport com.walmartlabs.concord.sdk.Secret;\n\nimport java.nio.charset.StandardCharsets;\nimport java.security.KeyPair;\nimport java.security.KeyPairGenerator;\nimport java.security.NoSuchAlgorithmException;\nimport java.util.Base64;\n\npublic class TestConstants {\n    static final ObjectMapper MAPPPER = new ObjectMapperProvider().get();\n    static final String APP_INSTALL_CONTENT = \"\"\"\n            {\n                \"githubAppInstallation\": {\n                    \"urlPattern\": \"(?<baseUrl>github.local)/.*\",\n                    \"clientId\": \"123\",\n                    \"privateKey\": \"mock-key-data\"\n                }\n            }\"\"\";\n    static final Secret MOCK_APP_INSTALL_SECRET = new BinaryDataSecret(APP_INSTALL_CONTENT.getBytes(StandardCharsets.UTF_8));\n    static final Secret MOCK_STATIC_TOKEN_SECRET = new BinaryDataSecret(\"mock-static-token\".getBytes(StandardCharsets.UTF_8));\n\n    public static final String PRIVATE_KEY_TEXT = generatePrivateKey();\n\n    private static String generatePrivateKey() {\n        KeyPairGenerator kpg;\n        try {\n            kpg = KeyPairGenerator.getInstance(\"RSA\");\n        } catch (NoSuchAlgorithmException e) {\n            throw new RuntimeException(\"Algorithm not found\", e);\n        }\n\n        kpg.initialize(2048);\n        KeyPair kp = kpg.generateKeyPair();\n\n        Base64.Encoder encoder = Base64.getEncoder();\n\n        return \"-----BEGIN PRIVATE KEY-----\\n\" +\n                encoder.encodeToString(kp.getPrivate().getEncoded()) +\n                \"\\n-----END PRIVATE KEY-----\\n\";\n    }\n}\n"
  },
  {
    "path": "github-app-installation/src/test/java/com/walmartlabs/concord/github/appinstallation/UtilsTest.java",
    "content": "package com.walmartlabs.concord.github.appinstallation;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.secret.BinaryDataSecret;\nimport com.walmartlabs.concord.common.secret.UsernamePassword;\nimport com.walmartlabs.concord.github.appinstallation.exception.GitHubAppException;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.Mockito;\n\nimport static com.walmartlabs.concord.github.appinstallation.TestConstants.APP_INSTALL_CONTENT;\nimport static com.walmartlabs.concord.github.appinstallation.TestConstants.MAPPPER;\nimport static com.walmartlabs.concord.github.appinstallation.TestConstants.MOCK_APP_INSTALL_SECRET;\nimport static com.walmartlabs.concord.github.appinstallation.TestConstants.MOCK_STATIC_TOKEN_SECRET;\nimport static com.walmartlabs.concord.github.appinstallation.Utils.parseAppInstallation;\nimport static com.walmartlabs.concord.github.appinstallation.Utils.parseRawAppInstallation;\nimport static com.walmartlabs.concord.github.appinstallation.Utils.validateSecret;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nclass UtilsTest {\n\n    @Test\n    void testValidateSecret() {\n        assertTrue(validateSecret(MOCK_APP_INSTALL_SECRET, MAPPPER));\n        assertTrue(validateSecret(MOCK_STATIC_TOKEN_SECRET, MAPPPER));\n        assertFalse(validateSecret(null, MAPPPER));\n        assertFalse(validateSecret(Mockito.mock(UsernamePassword.class), MAPPPER));\n        assertFalse(validateSecret(new BinaryDataSecret(null), MAPPPER));\n        assertFalse(validateSecret(new BinaryDataSecret(new byte[]{}), MAPPPER));\n        assertFalse(validateSecret(new BinaryDataSecret(\"\\nmytoken\".getBytes()), MAPPPER));\n        assertFalse(validateSecret(new BinaryDataSecret(\"{\\\"hello\\\":\\\"world\\\"}\".getBytes()), MAPPPER));\n    }\n\n    @Test\n    void parseAppInstallation_ValidJson() {\n        var o = parseAppInstallation(APP_INSTALL_CONTENT.getBytes(), MAPPPER);\n\n        assertTrue(o.isPresent());\n        var result = o.get();\n        assertEquals(\"123\", result.clientId());\n    }\n\n    @Test\n    void parseAppInstallation_MissingElement() {\n        var missingClientId = \"\"\"\n                {\n                    \"githubAppInstallation\": {\n                        \"urlPattern\": \"(?<baseUrl>github.local)/.*\",\n                        \"privateKey\": \"mock-key-data\"\n                    }\n                }\"\"\";\n        var data = missingClientId.getBytes();\n        var ex = assertThrows(GitHubAppException.class, () -> parseAppInstallation(data, MAPPPER));\n\n        assertTrue(ex.getMessage().contains(\"Invalid app installation definition\"));\n    }\n\n    @Test\n    void parseAppInstallation_OtherJson() {\n        var unexpectedJson = \"{ \\\"valid\\\": \\\"but not usable here\\\"}\";\n        var result = parseAppInstallation(unexpectedJson.getBytes(), MAPPPER);\n\n        assertFalse(result.isPresent());\n    }\n\n    @Test\n    void parseRawAppInstallation_NotJson() {\n        var unexpectedJson = \"justText\";\n        var result = parseRawAppInstallation(unexpectedJson.getBytes(), MAPPPER);\n\n        assertNull(result);\n    }\n\n    @Test\n    void parseRawAppInstallation_OtherJson() {\n        var unexpectedJson = \"{ \\\"valid\\\": \\\"but not usable here\\\"}\";\n        var result = parseRawAppInstallation(unexpectedJson.getBytes(), MAPPPER);\n\n        assertNotNull(result);\n        assertTrue(result.isEmpty());\n    }\n}\n"
  },
  {
    "path": "github-app-installation/src/test/java/com/walmartlabs/concord/github/appinstallation/cfg/ConfigTest.java",
    "content": "package com.walmartlabs.concord.github.appinstallation.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.typesafe.config.ConfigFactory;\nimport com.walmartlabs.concord.common.cfg.MappingAuthConfig;\nimport com.walmartlabs.concord.github.appinstallation.GitHubAppAuthConfig;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.io.TempDir;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Duration;\n\nimport static com.walmartlabs.concord.github.appinstallation.TestConstants.PRIVATE_KEY_TEXT;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\n\nclass ConfigTest {\n\n    @TempDir\n    private static Path workDir;\n\n    @Test\n    void simpleConfig() throws Exception {\n        var pk = Files.writeString(workDir.resolve(\"pk.pem\"), PRIVATE_KEY_TEXT);\n        var typesafeConfig = ConfigFactory.parseString(\"\"\"\n                {\n                    \"auth\" = [\n                        { id = \"app-auth\", type = \"GITHUB_APP_INSTALLATION\", urlPattern = \"(?<baseUrl>github.local)\", clientId = \"123\", privateKey = \"{{PK_PATH}}\" },\n                        { id = \"static-auth\", type = \"OAUTH_TOKEN\", urlPattern = \"(?<baseUrl>github.local)\", token = \"mock-token\" }\n                    ]\n                }\"\"\".replace(\"{{PK_PATH}}\", pk.toString()));\n        var cfg = GitHubAppInstallationConfig.fromConfig(typesafeConfig);\n        assertNotNull(cfg);\n        assertEquals(10240, cfg.getSystemAuthCacheMaxWeight());\n        assertEquals(Duration.ofSeconds(30), cfg.getHttpClientTimeout());\n        assertEquals(Duration.ofMinutes(50), cfg.getSystemAuthCacheDuration());\n        assertEquals(2, cfg.getAuthConfigs().size());\n\n        var appInstall = assertInstanceOf(GitHubAppAuthConfig.class, cfg.getAuthConfigs().get(0));\n        assertEquals(\"app-auth\", appInstall.id());\n        assertEquals(\"x-access-token\", appInstall.username());\n        assertEquals(\"https://api.github.com\", appInstall.apiUrl());\n\n        var oauth = assertInstanceOf(MappingAuthConfig.OauthAuthConfig.class, cfg.getAuthConfigs().get(1));\n        assertEquals(\"static-auth\", oauth.id());\n        assertNull(oauth.username());\n        assertEquals(\"mock-token\", oauth.token());\n    }\n\n    @Test\n    void overrideConfig() throws Exception {\n        var pk = Files.writeString(workDir.resolve(\"pk.pem\"), PRIVATE_KEY_TEXT);\n        var typesafeConfig = ConfigFactory.parseString(\"\"\"\n                {\n                    httpClientTimeout = \"1 minute\",\n                    systemAuthCacheDuration = \"1 minute\",\n                    systemAuthCacheMaxWeight = \"10\"\n                    \"auth\" = [\n                        { id = \"app-auth\", type = \"GITHUB_APP_INSTALLATION\", urlPattern = \"(?<baseUrl>github.local)\", username = \"custom\", apiUrl = \"https://api.github.local\", clientId = \"123\", privateKey = \"{{PK_PATH}}\" },\n                        { id = \"static-auth\", type = \"OAUTH_TOKEN\", urlPattern = \"(?<baseUrl>github.local)\", token = \"mock-token\", username = \"custom\" }\n                    ]\n                }\"\"\".replace(\"{{PK_PATH}}\", pk.toString()));\n        var cfg = GitHubAppInstallationConfig.fromConfig(typesafeConfig);\n        assertNotNull(cfg);\n        assertEquals(10, cfg.getSystemAuthCacheMaxWeight());\n        assertEquals(Duration.ofMinutes(1), cfg.getHttpClientTimeout());\n        assertEquals(Duration.ofMinutes(1), cfg.getSystemAuthCacheDuration());\n        assertEquals(2, cfg.getAuthConfigs().size());\n\n        var appInstall = assertInstanceOf(GitHubAppAuthConfig.class, cfg.getAuthConfigs().get(0));\n        assertEquals(\"app-auth\", appInstall.id());\n        assertEquals(\"custom\", appInstall.username());\n        assertEquals(\"https://api.github.local\", appInstall.apiUrl());\n\n        var oauth = assertInstanceOf(MappingAuthConfig.OauthAuthConfig.class, cfg.getAuthConfigs().get(1));\n        assertEquals(\"static-auth\", oauth.id());\n        assertEquals(\"custom\", oauth.username());\n        assertEquals(\"mock-token\", oauth.token());\n    }\n}\n"
  },
  {
    "path": "github-app-installation/src/test/resources/logback-test.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] [%-5level] %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <logger name=\"com.walmartlabs.concord.github\" level=\"INFO\"/>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "idea-code-style.xml",
    "content": "<code_scheme name=\"Project\" version=\"173\">\n    <JavaCodeStyleSettings>\n        <option name=\"CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND\" value=\"7\"/>\n    </JavaCodeStyleSettings>\n    <codeStyleSettings language=\"JSON\">\n        <indentOptions>\n            <option name=\"INDENT_SIZE\" value=\"4\"/>\n        </indentOptions>\n    </codeStyleSettings>\n</code_scheme>\n"
  },
  {
    "path": "imports/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-imports</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-dependency-manager</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-repository</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n\n        <!-- Immutables -->\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>serial</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.errorprone</groupId>\n            <artifactId>error_prone_annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n</project>\n"
  },
  {
    "path": "imports/src/main/java/com/walmartlabs/concord/imports/DefaultImportManager.java",
    "content": "package com.walmartlabs.concord.imports;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.repository.Snapshot;\n\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\npublic class DefaultImportManager implements ImportManager {\n\n    private final Map<String, ImportProcessor<Import>> processors;\n    private final Set<String> disabledProcessors;\n\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    public DefaultImportManager(List<ImportProcessor> processors, Set<String> disabledProcessors) {\n        this.processors = processors.stream().collect(Collectors.toMap(ImportProcessor::type, o -> o));\n        this.disabledProcessors = disabledProcessors;\n    }\n\n    @Override\n    public List<Snapshot> process(Imports imports, Path dest, ImportsListener listener) throws Exception {\n        if (listener == null) {\n            listener = ImportsListener.NOP_LISTENER;\n        }\n\n        List<Snapshot> result = new ArrayList<>();\n\n        List<Import> items = imports.items();\n        if (items == null || items.isEmpty()) {\n            return result;\n        }\n\n        listener.onStart(items);\n\n        for (Import i : items) {\n            listener.beforeImport(i);\n            Snapshot s;\n            try {\n                s = assertProcessor(i.type()).process(i, dest);\n            } catch (Exception e) {\n                throw new ImportProcessingException(i, e);\n            }\n            listener.afterImport(i);\n            result.add(s);\n        }\n\n        listener.onEnd(items);\n\n        return result;\n    }\n\n    private ImportProcessor<Import> assertProcessor(String type) {\n        if (disabledProcessors.contains(type)) {\n            throw new RuntimeException(\"Disabled import type: \" + type);\n        }\n\n        ImportProcessor<Import> p = processors.get(type);\n        if (p != null) {\n            return p;\n        }\n        throw new RuntimeException(\"Unknown import type: \" + type);\n    }\n}\n"
  },
  {
    "path": "imports/src/main/java/com/walmartlabs/concord/imports/DirectoryProcessor.java",
    "content": "package com.walmartlabs.concord.imports;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.imports.Import.DirectoryDefinition;\nimport com.walmartlabs.concord.repository.LastModifiedSnapshot;\nimport com.walmartlabs.concord.repository.Snapshot;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardCopyOption;\nimport java.util.Collections;\n\npublic class DirectoryProcessor implements ImportProcessor<DirectoryDefinition> {\n\n    @Override\n    public String type() {\n        return DirectoryDefinition.TYPE;\n    }\n\n    @Override\n    public Snapshot process(DirectoryDefinition importEntry, Path workDir) throws Exception {\n        String entrySrc = importEntry.src();\n\n        Path src;\n        if (entrySrc.startsWith(\"/\")) {\n            src = Paths.get(entrySrc);\n        } else {\n            src = workDir.resolve(entrySrc).normalize();\n        }\n\n        if (!Files.exists(src) || !Files.isDirectory(src)) {\n            throw new IllegalArgumentException(\"Can't import '\" + src + \"': the specified path doesn't exist or not a directory.\");\n        }\n\n        if (workDir.startsWith(src)) {\n            throw new IllegalArgumentException(\"Only external directories are allowed in imports. \" +\n                    \"To include resources from directories located in the process' working directory use the 'resources' configuration block.\");\n        }\n\n        Path dest = workDir;\n\n        String entryDest = importEntry.dest();\n        if (entryDest != null) {\n            dest = workDir.resolve(entryDest);\n        }\n\n        if (!Files.exists(dest)) {\n            Files.createDirectories(dest);\n        }\n\n        LastModifiedSnapshot snapshot = new LastModifiedSnapshot();\n        PathUtils.copy(src, dest, Collections.emptyList(), snapshot, StandardCopyOption.REPLACE_EXISTING);\n        return snapshot;\n    }\n}\n"
  },
  {
    "path": "imports/src/main/java/com/walmartlabs/concord/imports/Import.java",
    "content": "package com.walmartlabs.concord.imports;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonSubTypes;\nimport com.fasterxml.jackson.annotation.JsonSubTypes.Type;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.common.ToStringHelper;\nimport org.immutables.value.Value;\nimport org.immutables.serial.Serial;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.net.URL;\nimport java.util.Collections;\nimport java.util.List;\n\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonTypeInfo(\n        use = JsonTypeInfo.Id.NAME,\n        property = \"type\")\n@JsonSubTypes({\n        @Type(value = Import.GitDefinition.class, name = Import.GitDefinition.TYPE),\n        @Type(value = ImmutableGitDefinition.class, name = Import.GitDefinition.TYPE),\n        @Type(value = Import.MvnDefinition.class, name = Import.MvnDefinition.TYPE),\n        @Type(value = ImmutableMvnDefinition.class, name = Import.MvnDefinition.TYPE),\n        @Type(value = Import.DirectoryDefinition.class, name = Import.DirectoryDefinition.TYPE),\n        @Type(value = ImmutableDirectoryDefinition.class, name = Import.DirectoryDefinition.TYPE),\n})\n@Serial.Version(1)\npublic interface Import extends Serializable {\n\n    String type();\n\n    @Value.Immutable\n    @JsonInclude(JsonInclude.Include.NON_EMPTY)\n    @JsonSerialize(as = ImmutableGitDefinition.class)\n    @JsonDeserialize(as = ImmutableGitDefinition.class)\n    abstract class GitDefinition implements Import {\n\n        private static final long serialVersionUID = 1L;\n\n        public static final String TYPE = \"git\";\n\n        @Nullable\n        public abstract String name();\n\n        @Nullable\n        public abstract String url();\n\n        @Nullable\n        public abstract String version();\n\n        @Nullable\n        public abstract String path();\n\n        @Nullable\n        public abstract String dest();\n\n        @Nullable\n        public abstract SecretDefinition secret();\n\n        @Value.Default\n        public List<String> exclude() {\n            return Collections.emptyList();\n        }\n\n        @Override\n        public String type() {\n            return TYPE;\n        }\n\n        public static ImmutableGitDefinition.Builder builder() {\n            return ImmutableGitDefinition.builder();\n        }\n\n        public String toString() {\n            return ToStringHelper.prefix(TYPE + \": \")\n                    .add(\"name\", name())\n                    .add(\"url\", hideSensitiveData(url()))\n                    .add(\"version\", version())\n                    .add(\"path\", path())\n                    .add(\"dest\", dest())\n                    .add(\"secret\", secret())\n                    .add(\"exclude\", exclude())\n                    .toString();\n        }\n    }\n\n    @Value.Immutable\n    @JsonInclude(JsonInclude.Include.NON_EMPTY)\n    @JsonSerialize(as = ImmutableMvnDefinition.class)\n    @JsonDeserialize(as = ImmutableMvnDefinition.class)\n    abstract class MvnDefinition implements Import {\n\n        private static final long serialVersionUID = 1L;\n\n        public static final String TYPE = \"mvn\";\n\n        @JsonProperty(value = \"url\", required = true)\n        public abstract String url();\n\n        @Nullable\n        public abstract String dest();\n\n        @Override\n        public String type() {\n            return TYPE;\n        }\n\n        public static ImmutableMvnDefinition.Builder builder() {\n            return ImmutableMvnDefinition.builder();\n        }\n\n        public String toString() {\n            return ToStringHelper.prefix(TYPE + \": \")\n                    .add(\"url\", hideSensitiveData(url()))\n                    .add(\"dest\", dest())\n                    .toString();\n        }\n    }\n\n    @Value.Immutable\n    @JsonInclude(JsonInclude.Include.NON_EMPTY)\n    @JsonSerialize(as = ImmutableDirectoryDefinition.class)\n    @JsonDeserialize(as = ImmutableDirectoryDefinition.class)\n    abstract class DirectoryDefinition implements Import {\n\n        private static final long serialVersionUID = 1L;\n\n        public static final String TYPE = \"dir\";\n\n        @JsonProperty(value = \"src\", required = true)\n        public abstract String src();\n\n        @Nullable\n        public abstract String dest();\n\n        @Override\n        public String type() {\n            return TYPE;\n        }\n\n        public static ImmutableDirectoryDefinition.Builder builder() {\n            return ImmutableDirectoryDefinition.builder();\n        }\n\n        public String toString() {\n            return ToStringHelper.prefix(TYPE + \": \")\n                    .add(\"src\", src())\n                    .add(\"dest\", dest())\n                    .toString();\n        }\n    }\n\n    @Value.Immutable\n    @JsonInclude(JsonInclude.Include.NON_EMPTY)\n    @JsonSerialize(as = ImmutableSecretDefinition.class)\n    @JsonDeserialize(as = ImmutableSecretDefinition.class)\n    abstract class SecretDefinition implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        @Nullable\n        public abstract String org();\n\n        @JsonProperty(value = \"name\", required = true)\n        public abstract String name();\n\n        @Nullable\n        public abstract String password();\n\n        public static ImmutableSecretDefinition.Builder builder() {\n            return ImmutableSecretDefinition.builder();\n        }\n\n        public String toString() {\n            return ToStringHelper.prefix(\"\")\n                    .add(\"org\", org())\n                    .add(\"name\", name())\n                    .add(\"password\", password() != null ? \"***\" : null)\n                    .toString();\n        }\n    }\n\n    static String hideSensitiveData(String url) {\n        try {\n            URL u = new URL(url);\n            if (u.getUserInfo() == null) {\n                return url;\n            }\n\n            return url.replace(u.getUserInfo(), \"***\");\n        } catch (Exception e) {\n            return url;\n        }\n    }\n}\n"
  },
  {
    "path": "imports/src/main/java/com/walmartlabs/concord/imports/ImportManager.java",
    "content": "package com.walmartlabs.concord.imports;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.repository.Snapshot;\n\nimport java.nio.file.Path;\nimport java.util.List;\n\npublic interface ImportManager {\n\n    /**\n     * Process the specified imports and save the result into {@code dest}.\n     * Assumes all import definitions were normalized (i.e. contain valid URLs, secret/org names, etc).\n     */\n    List<Snapshot> process(Imports imports, Path dest, ImportsListener listener) throws Exception;\n}\n"
  },
  {
    "path": "imports/src/main/java/com/walmartlabs/concord/imports/ImportManagerFactory.java",
    "content": "package com.walmartlabs.concord.imports;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.dependencymanager.DependencyManager;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Set;\n\npublic class ImportManagerFactory {\n\n    private final DependencyManager dependencyManager;\n    private final RepositoryExporter repositoryExporter;\n    private final Set<String> disabledProcessors;\n\n    public ImportManagerFactory(DependencyManager dependencyManager, RepositoryExporter repositoryExporter, Set<String> disabledProcessors) {\n        this.dependencyManager = dependencyManager;\n        this.repositoryExporter = repositoryExporter;\n        this.disabledProcessors = disabledProcessors;\n    }\n\n    public ImportManager create() {\n        List<ImportProcessor> processors = new ArrayList<>();\n        processors.add(new RepositoryProcessor(repositoryExporter));\n        processors.add(new MvnProcessor(dependencyManager));\n        processors.add(new DirectoryProcessor());\n        return new DefaultImportManager(processors, disabledProcessors);\n    }\n}\n"
  },
  {
    "path": "imports/src/main/java/com/walmartlabs/concord/imports/ImportProcessingException.java",
    "content": "package com.walmartlabs.concord.imports;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class ImportProcessingException extends Exception {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Import value;\n\n    public ImportProcessingException(Import value, Exception e) {\n        super(e.getMessage(), e);\n        this.value = value;\n    }\n\n    public Import getImport() {\n        return value;\n    }\n}\n"
  },
  {
    "path": "imports/src/main/java/com/walmartlabs/concord/imports/ImportProcessor.java",
    "content": "package com.walmartlabs.concord.imports;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.repository.Snapshot;\n\nimport java.nio.file.Path;\n\npublic interface ImportProcessor<T extends Import> {\n\n    String type();\n\n    Snapshot process(T importEntry, Path workDir) throws Exception;\n}\n"
  },
  {
    "path": "imports/src/main/java/com/walmartlabs/concord/imports/Imports.java",
    "content": "package com.walmartlabs.concord.imports;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * A collection of {@link Import}s.\n * Necessary for correct serialization of {@code items} of different types.\n */\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableImports.class)\n@JsonDeserialize(as = ImmutableImports.class)\npublic interface Imports extends Serializable  {\n\n    long serialVersionUID = 1L;\n\n    @Value.Default\n    default List<Import> items() {\n        return Collections.emptyList();\n    }\n\n    @JsonIgnore\n    default boolean isEmpty() {\n        return items().isEmpty();\n    }\n\n    static Imports of(List<Import> items) {\n        return builder().items(items).build();\n    }\n\n    static ImmutableImports.Builder builder() {\n        return ImmutableImports.builder();\n    }\n\n    static Imports merge(Imports a, Imports b) {\n        List<Import> result = new ArrayList<>();\n        result.addAll(a.items());\n        result.addAll(b.items());\n        return Imports.of(result);\n    }\n}\n"
  },
  {
    "path": "imports/src/main/java/com/walmartlabs/concord/imports/ImportsListener.java",
    "content": "package com.walmartlabs.concord.imports;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.List;\n\npublic interface ImportsListener {\n\n    /**\n     * Listener that does nothing.\n     */\n    ImportsListener NOP_LISTENER = new ImportsListener() {\n    };\n\n    default void onStart(List<Import> items) {\n    }\n\n    default void onEnd(List<Import> items) {\n    }\n\n    default void beforeImport(Import i) {\n    }\n\n    default void afterImport(Import i) {\n    }\n}"
  },
  {
    "path": "imports/src/main/java/com/walmartlabs/concord/imports/MvnProcessor.java",
    "content": "package com.walmartlabs.concord.imports;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ZipUtils;\nimport com.walmartlabs.concord.dependencymanager.DependencyManager;\nimport com.walmartlabs.concord.imports.Import.MvnDefinition;\nimport com.walmartlabs.concord.repository.LastModifiedSnapshot;\nimport com.walmartlabs.concord.repository.Snapshot;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\n\npublic class MvnProcessor implements ImportProcessor<MvnDefinition> {\n\n    private final DependencyManager dependencyManager;\n\n    public MvnProcessor(DependencyManager dependencyManager) {\n        this.dependencyManager = dependencyManager;\n    }\n\n    @Override\n    public String type() {\n        return MvnDefinition.TYPE;\n    }\n\n    @Override\n    public Snapshot process(MvnDefinition entry, Path workDir) throws Exception {\n        URI uri = new URI(entry.url());\n        Path dependencyPath = dependencyManager.resolveSingle(uri).getPath();\n        return extract(entry, workDir, dependencyPath);\n    }\n\n    private Snapshot extract(MvnDefinition entry, Path workDir, Path archivePath) throws IOException {\n        Path dest = workDir;\n        if (entry.dest() != null) {\n            dest = dest.resolve(entry.dest());\n        }\n\n        LastModifiedSnapshot snapshot = new LastModifiedSnapshot();\n        ZipUtils.unzip(archivePath, dest, false, snapshot, StandardCopyOption.REPLACE_EXISTING);\n        return snapshot;\n    }\n}\n"
  },
  {
    "path": "imports/src/main/java/com/walmartlabs/concord/imports/NoopImportManager.java",
    "content": "package com.walmartlabs.concord.imports;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.repository.Snapshot;\n\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.List;\n\npublic class NoopImportManager implements ImportManager {\n\n    @Override\n    public List<Snapshot> process(Imports imports, Path dest, ImportsListener listener) {\n        return Collections.emptyList();\n    }\n}\n"
  },
  {
    "path": "imports/src/main/java/com/walmartlabs/concord/imports/RepositoryExporter.java",
    "content": "package com.walmartlabs.concord.imports;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Import.GitDefinition;\nimport com.walmartlabs.concord.repository.Snapshot;\n\nimport java.nio.file.Path;\n\npublic interface RepositoryExporter {\n\n    Snapshot export(GitDefinition entry, Path workDir) throws Exception;\n}\n"
  },
  {
    "path": "imports/src/main/java/com/walmartlabs/concord/imports/RepositoryProcessor.java",
    "content": "package com.walmartlabs.concord.imports;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Import.GitDefinition;\nimport com.walmartlabs.concord.repository.Snapshot;\n\nimport java.nio.file.Path;\n\npublic class RepositoryProcessor implements ImportProcessor<GitDefinition> {\n\n    private final RepositoryExporter repositoryExporter;\n\n    public RepositoryProcessor(RepositoryExporter repositoryExporter) {\n        this.repositoryExporter = repositoryExporter;\n    }\n\n    @Override\n    public String type() {\n        return GitDefinition.TYPE;\n    }\n\n    @Override\n    public Snapshot process(GitDefinition entry, Path workDir) throws Exception {\n        return repositoryExporter.export(entry, workDir);\n    }\n}\n"
  },
  {
    "path": "imports/src/main/java/com/walmartlabs/concord/imports/package-info.java",
    "content": "@Value.Style(jdkOnly = true)\npackage com.walmartlabs.concord.imports;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n"
  },
  {
    "path": "it/README.md",
    "content": "# Integration Tests\n\nThis directory contains Concord's integration test modules.\n\nCommands:\n\n- Full docker-profile build without tests:\n  ```shell\n  ./mvnw clean install -Pdocker -DskipTests\n  ```\n- Single console integration test with Docker-managed dependencies:\n  ```shell\n  ./mvnw -pl it/console verify -Pit -Pdocker -Dit.test=LoginIT\n  ```\n\nThe `-Pdocker` IT flow uses the locally built `walmartlabs/concord-server` and\n`walmartlabs/concord-agent` images, along with their upstream dependencies. If\nyou changed those areas, rebuild first with the full docker-profile command\nabove before running `it/console`.\n\nNotes:\n\n- The single-test command starts Postgres, Concord Server, Concord Agent,\n  and Selenium via Docker.\n- The first `LoginIT` run may need to pull browser images, so it can be\n  noticeably slower than subsequent runs.\n- Failing `it/console` tests write screenshots to\n  `it/console/target/screenshots/`.\n- New test modules should prefer `testcontainer-concord` instead of the\n  older setup pattern used in `it/server`.\n- For broader context on prerequisites and the general `-Pit` flow, see\n  [../README.md](../README.md).\n"
  },
  {
    "path": "it/common/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.it</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-common-it</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-client2</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jsr310</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-compress</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.sshd</groupId>\n            <artifactId>sshd-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.sshd</groupId>\n            <artifactId>sshd-git</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.sshd</groupId>\n            <artifactId>sshd-common</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jgit</groupId>\n            <artifactId>org.eclipse.jgit</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n\n        <!-- IDEA extensions -->\n        <dependency>\n            <groupId>org.jetbrains</groupId>\n            <artifactId>annotations</artifactId>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <resources>\n            <resource>\n                <filtering>true</filtering>\n                <directory>${project.basedir}/src/main/filtered-resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n        </resources>\n    </build>\n</project>\n"
  },
  {
    "path": "it/common/src/main/filtered-resources/com/walmartlabs/concord/it/common/version.properties",
    "content": "project.version = ${project.version}"
  },
  {
    "path": "it/common/src/main/java/com/walmartlabs/concord/it/common/ForbiddenException.java",
    "content": "package com.walmartlabs.concord.it.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic class ForbiddenException extends RuntimeException {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Object data;\n\n    public ForbiddenException(String message, Object data) {\n        super(message);\n        this.data = data;\n    }\n\n    public Object getData() {\n        return data;\n    }\n}\n"
  },
  {
    "path": "it/common/src/main/java/com/walmartlabs/concord/it/common/GitHubUtils.java",
    "content": "package com.walmartlabs.concord.it.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.crypto.Mac;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.math.BigInteger;\n\npublic final class GitHubUtils {\n\n    // from github.secret configuration parameter\n    private static final String GITHUB_WEBHOOK_SECRET = \"12345\";\n    private static final String HMAC_SHA1_ALGORITHM = \"HmacSHA1\";\n\n    public static String sign(String payload) throws Exception {\n        SecretKeySpec signingKey = new SecretKeySpec(GITHUB_WEBHOOK_SECRET.getBytes(), HMAC_SHA1_ALGORITHM);\n        Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);\n        mac.init(signingKey);\n        byte[] digest = mac.doFinal(payload.getBytes());\n        return hex(digest);\n    }\n\n    private static String hex(byte[] str){\n        return String.format(\"%040x\", new BigInteger(1, str));\n    }\n\n    private GitHubUtils() {\n    }\n}\n"
  },
  {
    "path": "it/common/src/main/java/com/walmartlabs/concord/it/common/GitUtils.java",
    "content": "package com.walmartlabs.concord.it.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport org.eclipse.jgit.api.Git;\nimport org.eclipse.jgit.revwalk.RevCommit;\nimport org.eclipse.jgit.transport.RefSpec;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.nio.file.attribute.PosixFilePermissions;\n\npublic final class GitUtils {\n\n    public static Path createBareRepository(Path data) throws Exception {\n        return createBareRepository(data, (Path)null);\n    }\n\n    public static Path createBareRepository(Path data, Path baseTmpDir) throws Exception {\n        return createBareRepository(data, \"initial message\", baseTmpDir, null);\n    }\n\n    public static Path createBareRepository(Path data, String commitMessage) throws Exception {\n        return createBareRepository(data, commitMessage, null, null);\n    }\n\n    public static Path createBareRepository(Path data, String commitMessage, Path baseTmpDir, String leaf) throws Exception {\n        if (leaf == null) {\n            leaf = \"test\";\n        }\n\n        // init bare repository\n        Path tmp = createTempDir(baseTmpDir);\n        Path repo = tmp.resolve(leaf);\n        Files.createDirectories(repo);\n\n        Git.init().setInitialBranch(\"master\").setBare(true).setDirectory(repo.toFile()).call();\n\n        // clone the repository into a new directory\n        Path workdir = createTempDir(baseTmpDir);\n        Git git = Git.cloneRepository()\n                .setDirectory(workdir.toFile())\n                .setURI(\"file://\" + repo.toString())\n                .call();\n\n        // copy our files into the repository\n        PathUtils.copy(data, workdir);\n\n        // add, commit, and push copied files\n        git.add().addFilepattern(\".\").call();\n        git.commit().setMessage(commitMessage).call();\n        git.push().call();\n\n        return repo;\n    }\n\n    public static String createNewBranch(Path bareRepo, String branch, Path src) throws Exception {\n        return createNewBranch(bareRepo, branch, src, null);\n    }\n\n    public static String createNewBranch(Path bareRepo, String branch, Path src, Path baseTmpDir) throws Exception {\n        Path dir = createTempDir(baseTmpDir);\n\n        Git git = Git.cloneRepository()\n                .setDirectory(dir.toFile())\n                .setURI(bareRepo.toAbsolutePath().toString())\n                .call();\n\n        git.checkout()\n                .setCreateBranch(true)\n                .setName(branch)\n                .call();\n\n        PathUtils.copy(src, dir, StandardCopyOption.REPLACE_EXISTING);\n\n        git.add()\n                .addFilepattern(\".\")\n                .call();\n\n        RevCommit commit = git.commit()\n                .setMessage(\"adding files from \" + src.getFileName())\n                .call();\n\n        git.push()\n                .setRefSpecs(new RefSpec(branch + \":\" + branch))\n                .call();\n\n        return commit.name();\n    }\n\n    protected static Path createTempDir(Path base) throws IOException {\n        Path tmpDir ;\n        if (base != null) {\n            tmpDir = Files.createTempDirectory(base, \"test\");\n        } else {\n            tmpDir = PathUtils.createTempDir(\"test\");\n        }\n        Files.setPosixFilePermissions(tmpDir, PosixFilePermissions.fromString(\"rwxr-xr-x\"));\n        return tmpDir;\n    }\n\n    private GitUtils() {\n    }\n}\n"
  },
  {
    "path": "it/common/src/main/java/com/walmartlabs/concord/it/common/ITUtils.java",
    "content": "package com.walmartlabs.concord.it.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.TemporaryPath;\nimport com.walmartlabs.concord.common.ZipUtils;\nimport org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;\nimport org.eclipse.jgit.api.Git;\nimport org.eclipse.jgit.api.errors.GitAPIException;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.net.URL;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardOpenOption;\nimport java.nio.file.attribute.PosixFilePermissions;\nimport java.util.Random;\nimport java.util.concurrent.ThreadLocalRandom;\nimport java.util.stream.Stream;\n\npublic final class ITUtils {\n\n    private static final char[] RANDOM_CHARS = \"abcdef0123456789\".toCharArray();\n\n    public static byte[] archive(URI uri) throws IOException {\n        try (TemporaryPath tmpDir = preprocessDir(uri)) {\n            ByteArrayOutputStream out = new ByteArrayOutputStream();\n            try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(out)) {\n                ZipUtils.zip(zip, tmpDir.path());\n            }\n            return out.toByteArray();\n        }\n    }\n\n    public static String resourceToString(Class<?> klass, String resource) throws Exception {\n        URL url = klass.getResource(resource);\n\n        ByteArrayOutputStream out = new ByteArrayOutputStream();\n        try (InputStream in = url.openStream()) {\n            in.transferTo(out);\n        }\n\n        return new String(out.toByteArray());\n    }\n\n    public static Path createTempDir() throws IOException {\n        Path dir = PathUtils.createTempDir(\"test\");\n        Files.setPosixFilePermissions(dir, PosixFilePermissions.fromString(\"rwxr-xr-x\"));\n        return dir;\n    }\n\n    public static String createGitRepo(Class<?> klass, String resource) throws IOException, GitAPIException, URISyntaxException {\n        Path src = Paths.get(klass.getResource(resource).toURI());\n        return createGitRepo(src);\n    }\n\n    public static String createGitRepo(Path src) throws IOException, GitAPIException {\n        Path tmpDir = createTempDir();\n        PathUtils.copy(src, tmpDir);\n\n        Git repo = Git.init().setInitialBranch(\"master\").setDirectory(tmpDir.toFile()).call();\n        repo.add().addFilepattern(\".\").call();\n        repo.commit().setMessage(\"import\").call();\n\n        return tmpDir.toAbsolutePath().toString();\n    }\n\n    public static String randomString() {\n        StringBuilder b = new StringBuilder();\n        b.append(System.currentTimeMillis()).append(\"_\");\n\n        Random rng = ThreadLocalRandom.current();\n        for (int i = 0; i < 6; i++) {\n            int n = rng.nextInt(RANDOM_CHARS.length);\n            b.append(RANDOM_CHARS[n]);\n        }\n\n        return b.toString();\n    }\n\n    public static String randomPwd() {\n        return \"pwd_\" + randomString() + \"A!\";\n    }\n\n    private static TemporaryPath preprocessDir(URI uri) throws IOException {\n        Path src = Paths.get(uri);\n\n        // copy files from the specified URI to a temporary directory\n        TemporaryPath tmpDir = PathUtils.tempDir(\"test\");\n        PathUtils.copy(src, tmpDir.path());\n\n        // find and replace all PROJECT_VERSION strings with the current ${project.version}\n        try (Stream<Path> yamlFiles = Files.walk(tmpDir.path())) {\n            for (Path yamlFile : yamlFiles.filter(f -> {\n                String fileName = f.getFileName().toString().toLowerCase();\n                return fileName.endsWith(\".yaml\") || fileName.endsWith(\".yml\") || fileName.endsWith(\".json\");\n            }).toList()) {\n                String content = Files.readString(yamlFile);\n                if (!content.contains(\"PROJECT_VERSION\")) {\n                    continue;\n                }\n                content = content.replaceAll(\"PROJECT_VERSION\", Version.PROJECT_VERSION);\n                Files.writeString(yamlFile, content, StandardOpenOption.TRUNCATE_EXISTING);\n            }\n        }\n\n        return tmpDir;\n    }\n\n    private ITUtils() {\n    }\n}\n"
  },
  {
    "path": "it/common/src/main/java/com/walmartlabs/concord/it/common/JGitUtils.java",
    "content": "package com.walmartlabs.concord.it.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.eclipse.jgit.util.SystemReader;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic final class JGitUtils {\n\n    private static final Logger log = LoggerFactory.getLogger(JGitUtils.class);\n\n    public static void applyWorkarounds() {\n        // avoid consuming any local git configs\n        try {\n            SystemReader.getInstance().getUserConfig().clear();\n        } catch (Exception e) {\n            log.warn(\"Failed while trying to clear JGit's user config: {}\", e.getMessage());\n        }\n    }\n\n    private JGitUtils() {\n    }\n}\n"
  },
  {
    "path": "it/common/src/main/java/com/walmartlabs/concord/it/common/MockGitSshServer.java",
    "content": "package com.walmartlabs.concord.it.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.apache.sshd.git.GitLocationResolver;\nimport org.apache.sshd.git.pack.GitPackCommand;\nimport org.apache.sshd.git.pack.GitPackCommandFactory;\nimport org.apache.sshd.server.SshServer;\nimport org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\n\npublic class MockGitSshServer {\n\n    private static final Logger log = LoggerFactory.getLogger(MockGitSshServer.class);\n\n    private final SshServer server;\n\n    public MockGitSshServer(int port, Path repository) {\n        log.info(\"Creating a mock git+ssh server on port {}, using {}...\", port, repository);\n        this.server = createServer(port, repository);\n    }\n\n    public void start() throws IOException {\n        this.server.start();\n    }\n\n    public void stop() throws IOException {\n        this.server.stop();\n    }\n\n    public int getPort() {\n        return server.getPort();\n    }\n\n    private static SshServer createServer(int port, Path repository) {\n        SshServer s = SshServer.setUpDefaultServer();\n        s.setPort(port);\n        s.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());\n        s.setPublickeyAuthenticator((username, key, session) -> true);\n        s.setCommandFactory(new GitPackCommandFactory(GitLocationResolver.constantPath(repository)) {\n            @Override\n            public GitPackCommand createGitCommand(String command) {\n                return new GitPackCommand(getGitLocationResolver(), command, resolveExecutorService(command)) {\n                    @Override\n                    protected Path resolveRootDirectory(String command, String[] args) {\n                        return repository;\n                    }\n                };\n            }\n        });\n        return s;\n    }\n}\n"
  },
  {
    "path": "it/common/src/main/java/com/walmartlabs/concord/it/common/OffsetDateTimeDeserializer.java",
    "content": "package com.walmartlabs.concord.it.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer;\n\nimport java.time.Instant;\nimport java.time.OffsetDateTime;\nimport java.time.format.DateTimeFormatter;\n\n/**\n * Supports the Server's timestamp format.\n */\npublic class OffsetDateTimeDeserializer extends InstantDeserializer<OffsetDateTime> {\n\n    private static final long serialVersionUID = 1L;\n\n    public OffsetDateTimeDeserializer() {\n        super(OffsetDateTime.class, DateTimeFormatter.ofPattern(\"yyyy-MM-dd'T'HH:mm:ss.SSSX\"),\n                OffsetDateTime::from,\n                a -> OffsetDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId),\n                a -> OffsetDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId),\n                (d, z) -> d.withOffsetSameInstant(z.getRules().getOffset(d.toLocalDateTime())),\n                true);\n    }\n}\n"
  },
  {
    "path": "it/common/src/main/java/com/walmartlabs/concord/it/common/ServerClient.java",
    "content": "package com.walmartlabs.concord.it.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.intellij.lang.annotations.Language;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.time.Duration;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.common.GrepUtils.grep;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ServerClient {\n\n    /**\n     * As defined in server.conf\n     */\n    public static final String DEFAULT_API_KEY = envApiKey();\n\n    private final ApiClient client;\n\n    public ServerClient(String baseUrl) {\n        this.client = createClient(baseUrl, DEFAULT_API_KEY, null);\n    }\n\n    public ApiClient getClient() {\n        return client;\n    }\n\n    public ApiClient getClientForApiKey(String apiKey) {\n        return createClient(client.getBaseUrl(), apiKey, null);\n    }\n\n    public void resetApiKey() {\n        setApiKey(DEFAULT_API_KEY);\n    }\n\n    public synchronized void setApiKey(String apiKey) {\n        this.client.setApiKey(apiKey);\n    }\n\n    public void setGithubKey(String githubKey) {\n        this.client.addDefaultHeader(\"X-Hub-Signature\", githubKey);\n    }\n\n    public StartProcessResponse start(Map<String, Object> input) throws ApiException {\n        return new ProcessApi(client).startProcess(input);\n    }\n\n    public SecretOperationResponse postSecret(String orgName, Map<String, Object> input) throws ApiException {\n        SecretsApi api = new SecretsApi(client);\n        return api.createSecret(orgName, input);\n    }\n\n    public SecretOperationResponse generateKeyPair(String orgName, String projectName, String name,\n                                                   boolean generatePassword,\n                                                   String storePassword) throws ApiException {\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"name\", name);\n        m.put(\"generatePassword\", generatePassword);\n        m.put(\"type\", SecretEntryV2.TypeEnum.KEY_PAIR.toString());\n        if (storePassword != null) {\n            m.put(\"storePassword\", storePassword);\n        }\n\n        if (projectName != null && !projectName.isEmpty()) {\n            m.put(\"project\", projectName);\n        }\n        return postSecret(orgName, m);\n    }\n\n    public SecretOperationResponse generateKeyPair(String orgName, Set<String> projectNames, Set<UUID> projectIds,\n                                                   String name, boolean generatePassword,\n                                                   String storePassword) throws ApiException {\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"name\", name);\n        m.put(\"generatePassword\", generatePassword);\n        m.put(\"type\", SecretEntryV2.TypeEnum.KEY_PAIR.toString());\n        if (storePassword != null) {\n            m.put(\"storePassword\", storePassword);\n        }\n\n        if (projectIds != null && !projectIds.isEmpty()) {\n            m.put(\"projectIds\", projectIds.stream().map(UUID::toString).collect(Collectors.joining(\",\")));\n        } else if (projectNames != null && !projectNames.isEmpty()) {\n            m.put(\"projects\", String.join(\",\", projectNames));\n        }\n        return postSecret(orgName, m);\n    }\n\n    public SecretOperationResponse addPlainSecret(String orgName, String name, String projectName,\n                                                  boolean generatePassword, String storePassword,\n                                                  byte[] secret) throws ApiException {\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"name\", name);\n        m.put(\"type\", SecretEntryV2.TypeEnum.DATA.toString());\n        m.put(\"generatePassword\", generatePassword);\n\n        if (projectName != null && !projectName.isEmpty()) {\n            m.put(\"project\", projectName);\n        }\n        m.put(\"data\", secret);\n        if (storePassword != null) {\n            m.put(\"storePassword\", storePassword);\n        }\n\n        return postSecret(orgName, m);\n    }\n\n    public SecretOperationResponse addPlainSecret(String orgName, String name, Set<String> projectNames,\n                                                  Set<UUID> projectIds, boolean generatePassword, String storePassword,\n                                                  byte[] secret) throws ApiException {\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"name\", name);\n        m.put(\"type\", SecretEntryV2.TypeEnum.DATA.toString());\n        m.put(\"generatePassword\", generatePassword);\n\n        if (projectIds != null && !projectIds.isEmpty()) {\n            m.put(\"projectIds\", projectIds.stream().map(UUID::toString).collect(Collectors.joining(\",\")));\n        } else if (projectNames != null && !projectNames.isEmpty()) {\n            m.put(\"projects\", String.join(\",\", projectNames));\n        }\n        m.put(\"data\", secret);\n        if (storePassword != null) {\n            m.put(\"storePassword\", storePassword);\n        }\n\n        return postSecret(orgName, m);\n    }\n\n    public SecretOperationResponse addUsernamePassword(String orgName, String projectName, String name,\n                                                       boolean generatePassword, String storePassword, String username,\n                                                       String password) throws ApiException {\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"name\", name);\n        m.put(\"type\", SecretEntryV2.TypeEnum.USERNAME_PASSWORD.toString());\n        m.put(\"generatePassword\", generatePassword);\n        m.put(\"username\", username);\n        m.put(\"password\", password);\n        if (projectName != null && !projectName.isEmpty()) {\n            m.put(\"project\", projectName);\n        }\n        if (storePassword != null) {\n            m.put(\"storePassword\", storePassword);\n        }\n\n        return postSecret(orgName, m);\n    }\n\n    public byte[] getLog(UUID instanceId) throws ApiException {\n        try (InputStream is = new ProcessApi(client).getProcessLog(instanceId, null)) {\n            return is.readAllBytes();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static ProcessEntry waitForStatus(ApiClient apiClient, UUID instanceId,\n                                             ProcessEntry.StatusEnum status, ProcessEntry.StatusEnum... more) throws InterruptedException {\n        int retries = 60;\n\n        ProcessV2Api apiV2 = new ProcessV2Api(apiClient);\n\n        ProcessEntry pir;\n        while (true) {\n            try {\n                pir = apiV2.getProcess(instanceId, Collections.singleton(\"childrenIds\"));\n                if (pir.getStatus() == ProcessEntry.StatusEnum.FINISHED || pir.getStatus() == ProcessEntry.StatusEnum.FAILED || pir.getStatus() == ProcessEntry.StatusEnum.CANCELLED) {\n                    return pir;\n                }\n\n                if (isSame(pir.getStatus(), status, more)) {\n                    return pir;\n                }\n            } catch (ApiException e) {\n                if (e.getCode() == 404) {\n                    System.out.printf(\"waitForCompletion ['%s'] -> not found, retrying... (%s)%n\", instanceId, retries);\n                    if (--retries < 0) {\n                        throw new RuntimeException(e);\n                    }\n                }\n            }\n\n            Thread.sleep(1000);\n        }\n    }\n\n    public static ProcessEntry waitForChild(ProcessApi api,\n                                            UUID parentInstanceId, ProcessEntry.KindEnum kind,\n                                            ProcessEntry.StatusEnum status, ProcessEntry.StatusEnum... more) throws InterruptedException, ApiException {\n\n        int retries = 20;\n        while (true) {\n            List<ProcessEntry> l = api.listSubprocesses(parentInstanceId, null);\n            ProcessEntry e = findByKindAndStatus(l, kind, status, more);\n            if (e != null) {\n                return e;\n            }\n\n            if (--retries < 0) {\n                throw new IllegalStateException(\"Child process not found: \" +\n                        \"kind=\" + kind + \", status=\" + status + \"+\" + Arrays.toString(more) + \", \" +\n                        \"got \" + l);\n            }\n\n            Thread.sleep(3000);\n        }\n    }\n\n    private static ProcessEntry findByKindAndStatus(Collection<ProcessEntry> c, ProcessEntry.KindEnum kind,\n                                                    ProcessEntry.StatusEnum status, ProcessEntry.StatusEnum... more) {\n\n        for (ProcessEntry e : c) {\n            if (e.getKind() != kind) {\n                continue;\n            }\n\n            if (e.getStatus() == status) {\n                return e;\n            }\n\n            for (ProcessEntry.StatusEnum s : more) {\n                if (e.getStatus() == s) {\n                    return e;\n                }\n            }\n        }\n\n        return null;\n    }\n\n    public static ProcessEntry waitForCompletion(ApiClient apiClient, UUID instanceId) throws InterruptedException {\n        return waitForStatus(apiClient, instanceId, ProcessEntry.StatusEnum.FAILED, ProcessEntry.StatusEnum.FINISHED);\n    }\n\n    public static void assertLog(@Language(\"RegExp\") String pattern, byte[] ab) throws IOException {\n        String msg = \"Expected: \" + pattern + \"\\n\"\n                + \"Got: \" + new String(ab);\n        assertEquals(1, grep(pattern, ab).size(), msg);\n    }\n\n    public static void assertNoLog(@Language(\"RegExp\") String pattern, byte[] ab) throws IOException {\n        String msg = \"Expected: \" + pattern + \"\\n\"\n                + \"Got: \" + new String(ab);\n        assertEquals(0, grep(pattern, ab).size(), msg);\n    }\n\n    public static void assertLog(@Language(\"RegExp\") String pattern, int times, byte[] ab) throws IOException {\n        assertEquals(times, grep(pattern, ab).size());\n    }\n\n    public static void assertLogAtLeast(@Language(\"RegExp\") String pattern, int times, byte[] ab) throws IOException {\n        int matches = grep(pattern, ab).size();\n        assertTrue(times <= matches, \"Expected to find \" + pattern + \" at least \" + times + \" time(s), found only \" + matches);\n    }\n\n    public void waitForLog(UUID instanceId, @Language(\"RegExp\") String pattern) throws ApiException, IOException, InterruptedException {\n        waitForLog(instanceId, 5, pattern);\n    }\n\n    public void waitForLog(UUID instanceId, int retries, @Language(\"RegExp\") String pattern) throws ApiException, IOException, InterruptedException {\n        while (true) {\n            byte[] ab = getLog(instanceId);\n            if (!grep(pattern, ab).isEmpty()) {\n                break;\n            }\n\n            if (--retries < 0) {\n                fail(\"waitForLog: \" + pattern);\n            }\n\n            Thread.sleep(500);\n        }\n    }\n\n    private static boolean isSame(ProcessEntry.StatusEnum status, ProcessEntry.StatusEnum first, ProcessEntry.StatusEnum... more) {\n        if (status == first) {\n            return true;\n        }\n\n        if (more != null) {\n            for (ProcessEntry.StatusEnum s : more) {\n                if (status == s) {\n                    return true;\n                }\n            }\n        }\n\n        return false;\n    }\n\n    private static ApiClient createClient(String baseUrl, String apiKey, String gitHubKey) {\n        ApiClient c = new DefaultApiClientFactory(baseUrl, Duration.ofMillis(10000))\n                .create(ApiClientConfiguration.builder()\n                        .apiKey(apiKey)\n                        .build());\n        c.setReadTimeout(Duration.ofMillis(60000));\n\n        c.addDefaultHeader(\"X-Concord-Trace-Enabled\", \"true\");\n\n        if (gitHubKey != null) {\n            c.addDefaultHeader(\"X-Hub-Signature\", gitHubKey);\n        }\n\n        return c;\n    }\n\n    private static String envApiKey() {\n        String s = System.getenv(\"IT_DEFAULT_API_KEY\");\n        if (s == null) {\n            throw new IllegalStateException(\"The default (admin) API key must be configured via IT_DEFAULT_API_KEY environment variable. \" +\n                    \"The value must match the db.changeLogParameters.defaultAdminToken value in the server's configuration file\");\n        }\n        return s;\n    }\n}\n"
  },
  {
    "path": "it/common/src/main/java/com/walmartlabs/concord/it/common/ServerCompatModule.java",
    "content": "package com.walmartlabs.concord.it.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.module.SimpleModule;\n\nimport java.time.OffsetDateTime;\n\n/**\n * Jackson module to support Concord Server API types.\n */\npublic class ServerCompatModule extends SimpleModule {\n\n    private static final long serialVersionUID = 1L;\n\n    public ServerCompatModule() {\n        addDeserializer(OffsetDateTime.class, new OffsetDateTimeDeserializer());\n    }\n}\n"
  },
  {
    "path": "it/common/src/main/java/com/walmartlabs/concord/it/common/Version.java",
    "content": "package com.walmartlabs.concord.it.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.Properties;\n\npublic final class Version {\n\n    public static final String PROJECT_VERSION;\n\n    static {\n        try (InputStream in = Version.class.getResourceAsStream(\"version.properties\")) {\n            Properties props = new Properties();\n            props.load(in);\n            PROJECT_VERSION = props.getProperty(\"project.version\");\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private Version() {\n    }\n}\n"
  },
  {
    "path": "it/compat/README.md",
    "content": "# Integration Tests to Ensure Backward Compatibility\n"
  },
  {
    "path": "it/compat/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.it</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-compat-it</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <properties>\n        <!-- version of Agent to use in tests -->\n        <!-- should be one or more release versions behind the current version -->\n        <!-- must be a multi-arch image to pass tests on both x86 and arm64 -->\n        <prev.concord.version>2.14.2</prev.concord.version>\n\n        <server.image>walmartlabs/concord-server</server.image>\n        <prev.agent.image>walmartlabs/concord-agent:${prev.concord.version}</prev.agent.image>\n\n        <ryuk.image>testcontainers/ryuk:0.6.0</ryuk.image>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>ca.ibodrov.concord</groupId>\n            <artifactId>testcontainers-concord-core</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>ca.ibodrov.concord</groupId>\n            <artifactId>testcontainers-concord-junit5</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.testcontainers</groupId>\n            <artifactId>testcontainers</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-client2</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <!-- LOCAL mode support -->\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server-impl</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-agent</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <resources>\n            <resource>\n                <filtering>false</filtering>\n                <directory>${project.basedir}/src/test/resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n            <resource>\n                <filtering>true</filtering>\n                <directory>${project.basedir}/src/test/filtered-resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n        </resources>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-failsafe-plugin</artifactId>\n                <configuration>\n                    <skipTests>${skip.it.tests}</skipTests>\n                    <systemProperties>\n                        <server.image>${server.image}</server.image>\n                        <agent.image>${prev.agent.image}</agent.image>\n                    </systemProperties>\n                </configuration>\n            </plugin>\n\n            <!-- LOCAL mode support -->\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-dependency-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>copy-runner-jar</id>\n                        <phase>process-test-resources</phase>\n                        <goals>\n                            <goal>copy</goal>\n                        </goals>\n                        <configuration>\n                            <overWriteIfNewer>true</overWriteIfNewer>\n                            <artifactItems>\n                                <artifactItem>\n                                    <groupId>com.walmartlabs.concord.runtime.v1</groupId>\n                                    <artifactId>concord-runtime-impl-v1</artifactId>\n                                    <classifier>jar-with-dependencies</classifier>\n                                    <destFileName>runner-v1.jar</destFileName>\n                                </artifactItem>\n                            </artifactItems>\n                            <outputDirectory>${project.build.directory}</outputDirectory>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "it/compat/src/test/filtered-resources/testcontainers.properties",
    "content": "ryuk.container.image = ${ryuk.image}"
  },
  {
    "path": "it/compat/src/test/java/com/walmartlabs/concord/it/compat/ITConstants.java",
    "content": "package com.walmartlabs.concord.it.compat;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic final class ITConstants {\n\n    public static final long DEFAULT_TEST_TIMEOUT = 120000;\n\n    private ITConstants() {\n    }\n}\n"
  },
  {
    "path": "it/compat/src/test/java/com/walmartlabs/concord/it/compat/LocalModeIT.java",
    "content": "package com.walmartlabs.concord.it.compat;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.Concord;\nimport ca.ibodrov.concord.testcontainers.ConcordProcess;\nimport ca.ibodrov.concord.testcontainers.Payload;\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport java.util.concurrent.TimeUnit;\n\nimport static com.walmartlabs.concord.it.compat.ITConstants.DEFAULT_TEST_TIMEOUT;\n\n/**\n * Runs the current versions of the Server and the Agent in testcontainer-concord's LOCAL mode.\n *\n * Currently, the test is ignored due to some port conflicts.\n */\n@Disabled\n@Timeout(value = DEFAULT_TEST_TIMEOUT, unit = TimeUnit.MILLISECONDS)\npublic class LocalModeIT {\n\n    @RegisterExtension\n    public ConcordRule concord = new ConcordRule()\n            .mode(Concord.Mode.LOCAL);\n\n    @Test\n    public void test() throws Exception {\n        String concordYml = \"flows:\\n\" +\n                \"  default:\\n\" +\n                \"    - log: \\\"Hello!\\\"\\n\";\n\n        ConcordProcess proc = concord.processes().start(new Payload()\n                .concordYml(concordYml));\n\n        proc.waitForStatus(ProcessEntry.StatusEnum.FINISHED);\n        proc.assertLog(\".*Hello!.*\");\n    }\n}\n"
  },
  {
    "path": "it/compat/src/test/java/com/walmartlabs/concord/it/compat/OldAgentIT.java",
    "content": "package com.walmartlabs.concord.it.compat;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.ConcordProcess;\nimport ca.ibodrov.concord.testcontainers.Payload;\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\nimport org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport org.testcontainers.images.PullPolicy;\n\nimport java.util.concurrent.TimeUnit;\n\nimport static com.walmartlabs.concord.it.compat.ITConstants.DEFAULT_TEST_TIMEOUT;\n\n/**\n * Runs an older version of the Agent with the current version of the Server.\n */\n@Timeout(value = DEFAULT_TEST_TIMEOUT, unit = TimeUnit.MILLISECONDS)\n@DisabledIfEnvironmentVariable(named = \"SKIP_OLD_AGENT_TESTS\", matches = \"true\", disabledReason = \"Requires a published agent image of the old version.\")\npublic class OldAgentIT {\n\n    @RegisterExtension\n    public final ConcordRule concord = new ConcordRule()\n            .serverImage(System.getProperty(\"server.image\", \"walmartlabs/concord-server\"))\n            .agentImage(System.getProperty(\"agent.image\", \"walmartlabs/concord-agent\"))\n            .pullPolicy(PullPolicy.defaultPolicy())\n            .streamServerLogs(true)\n            .streamAgentLogs(true);\n\n    @Test\n    public void test() throws Exception {\n        String concordYml = \"flows:\\n\" +\n                            \"  default:\\n\" +\n                            \"    - log: \\\"Hello!\\\"\\n\";\n\n        ConcordProcess proc = concord.processes().start(new Payload()\n                .concordYml(concordYml));\n\n        proc.waitForStatus(ProcessEntry.StatusEnum.FINISHED);\n        proc.assertLog(\".*Hello!.*\");\n    }\n}\n"
  },
  {
    "path": "it/compat/src/test/resources/logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss} [%-5level] %logger{12} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "it/console/README.md",
    "content": "# Integration Tests for Concord UI"
  },
  {
    "path": "it/console/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.it</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-console-it</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <properties>\n        <!-- pin down the version of Selenium image -->\n        <selenium.image>seleniarm/standalone-chromium:110.0</selenium.image>\n\n        <!-- use the latest locally built Concord images -->\n        <server.image>walmartlabs/concord-server</server.image>\n        <agent.image>walmartlabs/concord-agent</agent.image>\n        <console.image>walmartlabs/concord-console</console.image>\n\n        <docker.daemon.addr>tcp://127.0.0.1:2375</docker.daemon.addr>\n\n        <it.webdriver.type>local</it.webdriver.type>\n        <local.repository.src.mount>${settings.localRepository}</local.repository.src.mount>\n        <tmp.dir.src.mount>${java.io.tmpdir}</tmp.dir.src.mount>\n        <tmp.dir>${java.io.tmpdir}</tmp.dir>\n        <agent.work.dir.base>${tmp.dir}/concord-agent/workDirs</agent.work.dir.base>\n        <network>net-console-it</network>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>${project.groupId}</groupId>\n            <artifactId>concord-common-it</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-client2</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.seleniumhq.selenium</groupId>\n            <artifactId>selenium-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.seleniumhq.selenium</groupId>\n            <artifactId>selenium-java</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.seleniumhq.selenium</groupId>\n            <artifactId>selenium-remote-driver</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.seleniumhq.selenium</groupId>\n            <artifactId>selenium-chrome-driver</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.seleniumhq.selenium</groupId>\n            <artifactId>selenium-chromium-driver</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.seleniumhq.selenium</groupId>\n            <artifactId>selenium-support</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>ch.qos.logback</groupId>\n            <artifactId>logback-classic</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jgit</groupId>\n            <artifactId>org.eclipse.jgit</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-failsafe-plugin</artifactId>\n                <configuration>\n                    <skipTests>${skip.it.tests}</skipTests>\n                    <environmentVariables>\n                        <CONCORD_TMP_DIR>${tmp.dir}</CONCORD_TMP_DIR>\n                        <IT_CONSOLE_BASE_URL>http://server-node:8001</IT_CONSOLE_BASE_URL>\n                        <IT_DEFAULT_API_KEY>cTFxMXExcTE=</IT_DEFAULT_API_KEY>\n                        <IT_SCREENSHOTS_DIR>${project.build.directory}/screenshots</IT_SCREENSHOTS_DIR>\n                        <IT_SELENIUM_PORT>${it.selenium.port}</IT_SELENIUM_PORT>\n                        <IT_SERVER_BASE_URL>http://localhost:${it.server.port}</IT_SERVER_BASE_URL>\n                        <IT_WEBDRIVER_TYPE>${it.webdriver.type}</IT_WEBDRIVER_TYPE>\n                    </environmentVariables>\n                    <systemProperties>\n                        <java.io.tmpdir>${tmp.dir}</java.io.tmpdir>\n                    </systemProperties>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n    <profiles>\n        <profile>\n            <id>docker</id>\n            <activation>\n                <activeByDefault>false</activeByDefault>\n            </activation>\n            <properties>\n                <it.webdriver.type>remote</it.webdriver.type>\n            </properties>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>io.fabric8</groupId>\n                        <artifactId>docker-maven-plugin</artifactId>\n                        <extensions>true</extensions>\n                        <executions>\n                            <execution>\n                                <id>start</id>\n                                <phase>pre-integration-test</phase>\n                                <goals>\n                                    <goal>start</goal>\n                                </goals>\n                            </execution>\n                            <execution>\n                                <id>stop</id>\n                                <phase>post-integration-test</phase>\n                                <goals>\n                                    <goal>stop</goal>\n                                </goals>\n                            </execution>\n                        </executions>\n                        <configuration>\n                            <skipRun>${skip.it.tests}</skipRun>\n                            <showLogs>false</showLogs>\n                            <autoCreateCustomNetworks>true</autoCreateCustomNetworks>\n                            <images>\n                                <image>\n                                    <name>${db.image}</name>\n                                    <alias>db</alias>\n                                    <run>\n                                        <ports>\n                                            <port>it.db.port:8001</port>\n                                        </ports>\n                                        <network>\n                                            <mode>custom</mode>\n                                            <name>${network}</name>\n                                            <alias>db-node</alias>\n                                        </network>\n                                        <env>\n                                            <POSTGRES_PASSWORD>it</POSTGRES_PASSWORD>\n                                            <POSTGRES_INITDB_ARGS>--no-sync</POSTGRES_INITDB_ARGS>\n                                        </env>\n                                        <wait>\n                                            <log>(?s).*ready for start up.*ready to accept connections.*</log>\n                                            <time>60000</time>\n                                        </wait>\n                                    </run>\n                                </image>\n                                <image>\n                                    <name>${server.image}</name>\n                                    <alias>server</alias>\n                                    <run>\n                                        <ports>\n                                            <port>it.server.port:8001</port>\n                                        </ports>\n                                        <network>\n                                            <mode>custom</mode>\n                                            <name>${network}</name>\n                                            <alias>server-node</alias>\n                                        </network>\n                                        <volumes>\n                                            <bind>\n                                                <!-- to access test files -->\n                                                <volume>${tmp.dir.src.mount}:${tmp.dir}</volume>\n                                                <!-- allows the server to pick up local dependencies -->\n                                                <volume>${local.repository.src.mount}:/home/concord/.m2/repository:ro</volume>\n                                                <volume>${basedir}/src/test/resources/server.conf:/opt/concord/conf/server.conf:ro</volume>\n                                            </bind>\n                                        </volumes>\n                                        <env>\n                                            <CONCORD_CFG_FILE>/opt/concord/conf/server.conf</CONCORD_CFG_FILE>\n                                            <CONCORD_TMP_DIR>${tmp.dir}</CONCORD_TMP_DIR>\n                                            <DB_INVENTORY_PASSWORD>it</DB_INVENTORY_PASSWORD>\n                                            <DB_INVENTORY_USERNAME>postgres</DB_INVENTORY_USERNAME>\n                                            <DB_PASSWORD>it</DB_PASSWORD>\n                                            <DB_URL>jdbc:postgresql://db-node:5432/postgres</DB_URL>\n                                            <DB_USERNAME>postgres</DB_USERNAME>\n                                            <NODEROSTER_DB_PASSWORD>it</NODEROSTER_DB_PASSWORD>\n                                            <NODEROSTER_DB_URL>jdbc:postgresql://db-node:5432/postgres</NODEROSTER_DB_URL>\n                                            <NODEROSTER_DB_USERNAME>postgres</NODEROSTER_DB_USERNAME>\n                                        </env>\n                                        <wait>\n                                            <!--suppress MavenModelInspection -->\n                                            <http>\n                                                <url>http://localhost:${it.server.port}/api/v1/server/ping</url>\n                                            </http>\n                                            <time>60000</time>\n                                        </wait>\n                                    </run>\n                                </image>\n                                <image>\n                                    <name>${agent.image}</name>\n                                    <alias>agent</alias>\n                                    <run>\n                                        <network>\n                                            <mode>custom</mode>\n                                            <name>${network}</name>\n                                            <alias>agent-node</alias>\n                                        </network>\n                                        <volumes>\n                                            <bind>\n                                                <!-- to share files between process containers -->\n                                                <volume>${tmp.dir.src.mount}:${tmp.dir}</volume>\n                                                <!-- share host artifacts -->\n                                                <volume>${local.repository.src.mount}:/host/.m2/repository:ro</volume>\n                                                <volume>${basedir}/src/test/resources/console.conf:/opt/concord/console/nginx/app.conf:ro</volume>\n                                                <volume>${basedir}/src/test/resources/mvn.json:/opt/concord/conf/mvn.json:ro</volume>\n                                                <volume>${basedir}/src/test/resources/agent.conf:/opt/concord/conf/agent.conf:ro</volume>\n                                            </bind>\n                                        </volumes>\n                                        <env>\n                                            <CONCORD_CFG_FILE>/opt/concord/conf/agent.conf</CONCORD_CFG_FILE>\n                                            <CONCORD_DOCKER_LOCAL_MODE>false</CONCORD_DOCKER_LOCAL_MODE>\n                                            <CONCORD_MAVEN_CFG>/opt/concord/conf/mvn.json</CONCORD_MAVEN_CFG>\n                                            <CONCORD_TMP_DIR>${tmp.dir}</CONCORD_TMP_DIR>\n                                            <SERVER_API_BASE_URL>http://server-node:8001</SERVER_API_BASE_URL>\n                                            <SERVER_WEBSOCKET_URL>ws://server-node:8001/websocket</SERVER_WEBSOCKET_URL>\n                                            <WORK_DIR_BASE>${agent.work.dir.base}</WORK_DIR_BASE>\n                                        </env>\n                                    </run>\n                                </image>\n                                <image>\n                                    <name>${selenium.image}</name>\n                                    <alias>selenium-chromium</alias>\n                                    <run>\n                                        <ports>\n                                            <port>it.selenium.port:4444</port>\n                                        </ports>\n                                        <network>\n                                            <mode>custom</mode>\n                                            <name>${network}</name>\n                                            <alias>selenium-node</alias>\n                                        </network>\n                                        <volumes>\n                                            <bind>\n                                                <volume>/dev/shm:/dev/shm</volume>\n                                            </bind>\n                                        </volumes>\n                                        <wait>\n                                            <log>(?s).*Started Selenium Standalone.*</log>\n                                            <time>60000</time>\n                                        </wait>\n                                    </run>\n                                </image>\n                            </images>\n                        </configuration>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n    </profiles>\n</project>\n"
  },
  {
    "path": "it/console/src/test/java/com/walmartlabs/concord/it/console/Base.java",
    "content": "package com.walmartlabs.concord.it.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Timeout;\n\nimport java.util.concurrent.TimeUnit;\n\n@Timeout(value = 5000, unit = TimeUnit.MILLISECONDS)\npublic abstract class Base {\n}\n"
  },
  {
    "path": "it/console/src/test/java/com/walmartlabs/concord/it/console/Concord.java",
    "content": "package com.walmartlabs.concord.it.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic final class Concord {\n\n    public static final String ADMIN_API_KEY = getApiKey();\n\n    private Concord() {\n    }\n\n    private static String getApiKey() {\n        String s = System.getenv(\"IT_DEFAULT_API_KEY\");\n        if (s == null) {\n            throw new IllegalStateException(\"The default (admin) API key must be configured via IT_DEFAULT_API_KEY environment variable. \" +\n                    \"The value must match the db.changeLogParameters.defaultAdminToken value in the server's configuration file\");\n        }\n        return s;\n    }\n}\n"
  },
  {
    "path": "it/console/src/test/java/com/walmartlabs/concord/it/console/ConcordConsoleRule.java",
    "content": "package com.walmartlabs.concord.it.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.openqa.selenium.*;\nimport org.openqa.selenium.support.ui.ExpectedCondition;\nimport org.openqa.selenium.support.ui.WebDriverWait;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.time.Duration;\n\nimport static com.walmartlabs.concord.it.console.Utils.env;\n\npublic class ConcordConsoleRule extends WebDriverRule {\n\n    private static final Logger log = LoggerFactory.getLogger(ConcordConsoleRule.class);\n\n    private final String baseUrl;\n\n    public ConcordConsoleRule() {\n        this.baseUrl = env(\"IT_CONSOLE_BASE_URL\", \"http://localhost:3000\");\n        log.info(\"Using baseUrl: {}\", baseUrl);\n    }\n\n    public WebDriver navigateToRelative(String path) throws URISyntaxException {\n        WebDriver driver = getDriver();\n        URI uri = new URI(driver.getCurrentUrl()).resolve(path);\n        driver.get(uri.toString());\n        return driver;\n    }\n\n    public void login(String apiKey) throws Exception {\n        WebDriver driver = navigateToRelative(\"/#/login?useApiKey=true\");\n\n        WebElement apiKeyField = driver.findElement(By.name(\"apiKey\"));\n        apiKeyField.sendKeys(apiKey);\n\n        WebElement loginButton = driver.findElement(By.id(\"loginButton\"));\n        loginButton.click();\n\n        waitForLoad();\n    }\n\n    public Object executeJavaScript(String js) {\n        WebDriver driver = getDriver();\n        JavascriptExecutor exec = (JavascriptExecutor) driver;\n        return exec.executeScript(js);\n    }\n\n    public WebElement waitFor(By by) {\n        ExpectedCondition<Boolean> condition = d -> {\n            if (d == null) {\n                return false;\n            }\n\n            try {\n                d.findElement(by);\n            } catch (NoSuchElementException e) {\n                return false;\n            }\n\n            return true;\n        };\n\n        WebDriver driver = getDriver();\n\n        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(60));\n        wait.until(condition);\n\n        return driver.findElement(by);\n    }\n\n    public void waitForLoad() throws InterruptedException {\n        ExpectedCondition<Boolean> expectation = driver -> executeJavaScript(\"return document.readyState\")\n                .toString()\n                .equals(\"complete\");\n\n        Thread.sleep(500);\n\n        WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(30));\n        wait.until(expectation);\n    }\n\n    @Override\n    protected void setUp() throws Exception {\n        super.setUp();\n\n        WebDriver driver = getDriver();\n        driver.get(baseUrl);\n    }\n}\n"
  },
  {
    "path": "it/console/src/test/java/com/walmartlabs/concord/it/console/ConcordServerRule.java",
    "content": "package com.walmartlabs.concord.it.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.it.common.JGitUtils;\nimport org.junit.jupiter.api.extension.BeforeEachCallback;\nimport org.junit.jupiter.api.extension.ExtensionContext;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.it.console.Utils.env;\n\npublic class ConcordServerRule implements BeforeEachCallback {\n\n    private static final Logger log = LoggerFactory.getLogger(ConcordServerRule.class);\n\n    private final String baseUrl;\n\n    private ApiClient client;\n\n    public ConcordServerRule() {\n        this.baseUrl = env(\"IT_SERVER_BASE_URL\", \"http://localhost:8001\");\n        log.info(\"Using baseUrl: {}\", baseUrl);\n\n        JGitUtils.applyWorkarounds();\n    }\n\n    public ApiClient getClient() {\n        return client;\n    }\n\n    public StartProcessResponse start(Map<String, Object> input) throws ApiException {\n        return new ProcessApi(client).startProcess(input);\n    }\n\n    public byte[] getLog(UUID instanceId) throws ApiException {\n        try (InputStream is = new ProcessApi(client).getProcessLog(instanceId, null)) {\n            return is.readAllBytes();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public void beforeEach(ExtensionContext context) {\n        setUp();\n    }\n\n    private void setUp() {\n        this.client = new DefaultApiClientFactory(baseUrl)\n                .create(ApiClientConfiguration.builder().apiKey(Concord.ADMIN_API_KEY).build());\n    }\n}\n"
  },
  {
    "path": "it/console/src/test/java/com/walmartlabs/concord/it/console/CustomFormsIT.java",
    "content": "package com.walmartlabs.concord.it.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.it.common.ITUtils;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport org.openqa.selenium.By;\nimport org.openqa.selenium.WebElement;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.TimeUnit;\n\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static com.walmartlabs.concord.it.console.Utils.DEFAULT_TEST_TIMEOUT;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n/**\n * The Console must be running in Docker, i.e. API redirects must be correctly working.\n */\n@Timeout(value = DEFAULT_TEST_TIMEOUT, unit = TimeUnit.MILLISECONDS)\npublic class CustomFormsIT {\n\n    @RegisterExtension\n    public static ConcordServerRule serverRule = new ConcordServerRule();\n\n    @RegisterExtension\n    public static ConcordConsoleRule consoleRule = new ConcordConsoleRule();\n\n    @SuppressWarnings(\"unchecked\")\n    @Test\n    public void test() throws Exception {\n        ApiClient client = serverRule.getClient();\n\n        // ---\n\n        String gitUrl = ITUtils.createGitRepo(CustomFormsIT.class, \"customForm\");\n\n        // ---\n\n        String orgName = \"org_\" + ITUtils.randomString();\n\n        OrganizationsApi organizationsApi = new OrganizationsApi(client);\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + ITUtils.randomString();\n        String repoName = \"test\";\n\n        ProjectsApi projectsApi = new ProjectsApi(client);\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .url(gitUrl)\n                        .branch(\"master\"))));\n\n        // ---\n\n        consoleRule.login(Concord.ADMIN_API_KEY);\n\n        // ---\n\n        String testValue = \"test_\" + ITUtils.randomString();\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"repo\", repoName);\n        input.put(\"arguments.testValue\", testValue);\n\n        StartProcessResponse spr = serverRule.start(input);\n        assertNotNull(spr.getInstanceId());\n\n        ProcessEntry pir = waitForStatus(client, spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n\n        String url = \"/#/process/\" + spr.getInstanceId() + \"/wizard\";\n        consoleRule.navigateToRelative(url);\n\n        //  -- validate form rules and submit it\n\n        By selector = By.id(\"testValue\");\n        WebElement element = consoleRule.waitFor(selector);\n        assertEquals(testValue, element.getText());\n\n        Map<String, Object> formFields = (Map<String, Object>) consoleRule.executeJavaScript(\"return data.definitions\");\n        Map<String, Object> fieldX = (Map<String, Object>) formFields.get(\"x\");\n        List<Object> allowedValues = (List<Object>) fieldX.get(\"allow\");\n        assertEquals(2, allowedValues.size(), \"Expression object should have added two allowed values\");\n\n        WebElement submitButton = consoleRule.waitFor(By.id(\"submitButton\"));\n        submitButton.click();\n\n        //  -- validate log output\n\n        pir = waitForCompletion(client, pir.getInstanceId());\n\n        byte[] ab = serverRule.getLog(pir.getInstanceId());\n        assertLog(\".*uploaded contents: \\\\{hello=world\\\\}.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/console/src/test/java/com/walmartlabs/concord/it/console/FormsIT.java",
    "content": "package com.walmartlabs.concord.it.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport org.openqa.selenium.By;\nimport org.openqa.selenium.Keys;\nimport org.openqa.selenium.WebElement;\nimport org.openqa.selenium.interactions.Actions;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.TimeUnit;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static com.walmartlabs.concord.it.console.Utils.DEFAULT_TEST_TIMEOUT;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n@Timeout(value = DEFAULT_TEST_TIMEOUT, unit = TimeUnit.MILLISECONDS)\nclass FormsIT {\n\n    @RegisterExtension\n    public static ConcordServerRule serverRule = new ConcordServerRule();\n\n    @RegisterExtension\n    public static ConcordConsoleRule consoleRule = new ConcordConsoleRule();\n\n    @Test\n    void testDateTimeField() throws Exception {\n        ProcessEntry pir = startConsoleProcess(\"dateTimeField\");\n\n        // ---\n\n        consoleRule.login(Concord.ADMIN_API_KEY);\n\n        String url = \"/#/process/\" + pir.getInstanceId();\n        consoleRule.navigateToRelative(url);\n\n        WebElement wizardButton = consoleRule.waitFor(By.id(\"formWizardButton\"));\n        wizardButton.click();\n\n        WebElement dateField = consoleRule.waitFor(By.name(\"dateField\"));\n        dateField.sendKeys(\"2019-09-04\");\n\n        WebElement dateTimeField = consoleRule.waitFor(By.name(\"dateTimeField\"));\n        dateTimeField.sendKeys(\"2019-09-04T01:05:00.000-04:00\");\n\n        // close the popup\n        consoleRule.waitFor(By.id(\"root\")).click();\n\n        Thread.sleep(1000);\n\n        WebElement submitButton = consoleRule.waitFor(By.id(\"formSubmitButton\"));\n        submitButton.click();\n\n        // ---\n\n        pir = waitForCompletion(serverRule.getClient(), pir.getInstanceId());\n\n        byte[] ab = serverRule.getLog(pir.getInstanceId());\n        assertLog(\".*dateField=2019-09-04.*\", ab);\n        assertLog(\".*dateTimeField=2019-09-04T05:05:00.000Z.*\", ab);\n    }\n\n    @Test\n    void testStringValues() throws Exception {\n        ProcessEntry pir = startConsoleProcess(\"stringValues\");\n\n        // ---\n\n        consoleRule.login(Concord.ADMIN_API_KEY);\n\n        String url = \"/#/process/\" + pir.getInstanceId();\n        consoleRule.navigateToRelative(url);\n\n        WebElement wizardButton = consoleRule.waitFor(By.id(\"formWizardButton\"));\n        wizardButton.click();\n\n        consoleRule.waitFor(By.name(\"field0\")).click();\n        new Actions(consoleRule.getDriver()) // first option is selected on open\n                .sendKeys(Keys.DOWN)\n                .sendKeys(Keys.ENTER) // select \"second\" value\n                .sendKeys(Keys.ENTER) // select \"third\" value\n                .sendKeys(Keys.ESCAPE)\n                .perform();\n\n        consoleRule.waitFor(By.name(\"field1\")).click();\n        new Actions(consoleRule.getDriver())\n                .sendKeys(Keys.ENTER) // select \"third\" value\n                .sendKeys(Keys.ESCAPE)\n                .perform();\n\n        consoleRule.waitFor(By.name(\"field2\")).click();\n        new Actions(consoleRule.getDriver())\n                .sendKeys(Keys.BACK_SPACE) // remove \"second\" value\n                .sendKeys(\"third\")         // add \"third\" value\n                .sendKeys(Keys.ENTER)\n                .sendKeys(\"fourth\")        // add \"fourth\" value\n                .sendKeys(Keys.ENTER)\n                .sendKeys(Keys.ESCAPE)\n                .perform();\n\n        consoleRule.waitFor(By.name(\"field3\")).click();\n        new Actions(consoleRule.getDriver())\n                .sendKeys(\"hello\")    // add two strings\n                .sendKeys(Keys.ENTER)\n                .sendKeys(\"world\")\n                .sendKeys(Keys.ENTER)\n                .sendKeys(Keys.ESCAPE)\n                .perform();\n\n        consoleRule.waitFor(By.name(\"field4\")).click();\n        new Actions(consoleRule.getDriver())\n                .sendKeys(\"single string\")\n                .perform();\n\n        WebElement submitButton = consoleRule.waitFor(By.id(\"formSubmitButton\"));\n        submitButton.click();\n\n        // ---\n\n        pir = waitForCompletion(serverRule.getClient(), pir.getInstanceId());\n\n        byte[] ab = serverRule.getLog(pir.getInstanceId());\n        assertLog(\".*\\\"field0\\\" : \\\\[ \\\"second\\\", \\\"third\\\" ].*\", ab);\n        assertLog(\".*\\\"field1\\\" : \\\\[ \\\"first\\\", \\\"second\\\", \\\"third\\\" ].*\", ab);\n        assertLog(\".*\\\"field2\\\" : \\\\[ \\\"first\\\", \\\"third\\\", \\\"fourth\\\" ].*\", ab);\n        assertLog(\".*\\\"field3\\\" : \\\\[ \\\"hello\\\", \\\"world\\\" ].*\", ab);\n        assertLog(\".*\\\"field4\\\" : \\\"single string\\\".*\", ab);\n    }\n\n    private ProcessEntry startConsoleProcess(String res) throws Exception {\n        byte[] payload = archive(FormsIT.class.getResource(res).toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n\n        StartProcessResponse spr = serverRule.start(input);\n        assertNotNull(spr.getInstanceId());\n\n        return waitForStatus(serverRule.getClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n    }\n}\n"
  },
  {
    "path": "it/console/src/test/java/com/walmartlabs/concord/it/console/LoginIT.java",
    "content": "package com.walmartlabs.concord.it.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport org.openqa.selenium.By;\nimport org.openqa.selenium.WebDriver;\n\nimport java.util.concurrent.TimeUnit;\n\nimport static com.walmartlabs.concord.it.console.Utils.DEFAULT_TEST_TIMEOUT;\n\n@Timeout(value = 60000, unit = TimeUnit.MILLISECONDS)\npublic class LoginIT {\n\n    @RegisterExtension\n    public static ConcordConsoleRule consoleRule = new ConcordConsoleRule();\n\n    @Test\n    public void test() throws Exception {\n        consoleRule.login(Concord.ADMIN_API_KEY);\n        consoleRule.waitFor(By.id(\"concordLogo\"));\n    }\n\n    @Test\n    public void refresh() throws Exception {\n        consoleRule.login(Concord.ADMIN_API_KEY);\n        consoleRule.waitFor(By.id(\"concordLogo\"));\n\n        WebDriver driver = consoleRule.getDriver();\n        driver.navigate().refresh();\n\n        consoleRule.waitFor(By.id(\"concordLogo\"));\n    }\n}\n"
  },
  {
    "path": "it/console/src/test/java/com/walmartlabs/concord/it/console/ProcessAnsibleIT.java",
    "content": "package com.walmartlabs.concord.it.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport org.openqa.selenium.By;\nimport org.openqa.selenium.Keys;\nimport org.openqa.selenium.interactions.Actions;\n\nimport java.nio.charset.StandardCharsets;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.TimeUnit;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static com.walmartlabs.concord.it.console.Utils.DEFAULT_TEST_TIMEOUT;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.fail;\n\n@Timeout(value = DEFAULT_TEST_TIMEOUT, unit = TimeUnit.MILLISECONDS)\npublic class ProcessAnsibleIT {\n\n    @RegisterExtension\n    public static ConcordServerRule serverRule = new ConcordServerRule();\n\n    @RegisterExtension\n    public static ConcordConsoleRule consoleRule = new ConcordConsoleRule();\n\n    @Test\n    public void testAnsibleTabSmoke() throws Exception {\n        var process = startProcess(\"processAnsible\");\n        assertFinished(process);\n        var processUrl = \"/#/process/\" + process.getInstanceId() + \"/ansible\";\n\n        consoleRule.login(Concord.ADMIN_API_KEY);\n        consoleRule.navigateToRelative(processUrl);\n        consoleRule.waitForLoad();\n\n        var uniqueHostsCard = consoleRule.waitFor(By.xpath(\"//div[normalize-space()='UNIQUE HOSTS']\"));\n        assertNotNull(uniqueHostsCard);\n\n        uniqueHostsCard.click();\n        consoleRule.waitFor(By.xpath(\"//div[normalize-space()='Host Stats']\"));\n\n        var hostRow = consoleRule.waitFor(By.xpath(\"//table//tr[.//td[normalize-space()='127.0.0.1']]\"));\n        hostRow.click();\n\n        consoleRule.waitFor(By.xpath(\"//h3[normalize-space()='127.0.0.1']\"));\n        consoleRule.waitFor(By.xpath(\"//td[normalize-space()='Announce host']\"));\n        consoleRule.waitFor(By.xpath(\"//td[normalize-space()='Verify host']\"));\n\n        closeActiveModal();\n\n        consoleRule.navigateToRelative(processUrl);\n        consoleRule.waitForLoad();\n\n        var playsCard = consoleRule.waitFor(By.xpath(\"//div[normalize-space()='PLAYS']\"));\n        playsCard.click();\n\n        consoleRule.waitFor(By.xpath(\"//div[normalize-space()='Play Stats']\"));\n\n        var playRow = consoleRule.waitFor(By.xpath(\"//table//tr[.//td[normalize-space()='Smoke play']]\"));\n        playRow.click();\n\n        consoleRule.waitFor(By.xpath(\"//div[normalize-space()='Task Stats']\"));\n        consoleRule.waitFor(By.xpath(\"//td[normalize-space()='Announce host']\"));\n        consoleRule.waitFor(By.xpath(\"//td[normalize-space()='Verify host']\"));\n\n    }\n\n    private ProcessEntry startProcess(String resource) throws Exception {\n        byte[] payload = archive(ProcessAnsibleIT.class.getResource(resource).toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n\n        StartProcessResponse spr = serverRule.start(input);\n        assertNotNull(spr.getInstanceId());\n\n        return waitForCompletion(serverRule.getClient(), spr.getInstanceId());\n    }\n\n    private void assertFinished(ProcessEntry process) throws Exception {\n        if (process.getStatus() == ProcessEntry.StatusEnum.FINISHED) {\n            return;\n        }\n\n        var log = new String(serverRule.getLog(process.getInstanceId()), StandardCharsets.UTF_8);\n        fail(\"Expected process \" + process.getInstanceId() + \" to finish, got \" + process.getStatus() + \". Log:\\n\" + log);\n    }\n\n    private void closeActiveModal() throws Exception {\n        new Actions(consoleRule.getDriver())\n                .sendKeys(Keys.ESCAPE)\n                .perform();\n\n        waitForNoElements(By.cssSelector(\".ui.fullscreen.modal.active\"));\n    }\n\n    private void waitForNoElements(By by) throws Exception {\n        for (var attempt = 0; attempt < 20; attempt++) {\n            if (consoleRule.getDriver().findElements(by).isEmpty()) {\n                return;\n            }\n\n            Thread.sleep(250);\n        }\n\n        fail(\"Timed out waiting for element to disappear: \" + by);\n    }\n}\n"
  },
  {
    "path": "it/console/src/test/java/com/walmartlabs/concord/it/console/ProfileNavigationIT.java",
    "content": "package com.walmartlabs.concord.it.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.it.common.ITUtils;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport org.openqa.selenium.By;\nimport org.openqa.selenium.WebElement;\nimport org.openqa.selenium.support.ui.ExpectedConditions;\nimport org.openqa.selenium.support.ui.WebDriverWait;\n\nimport java.time.Duration;\nimport java.util.concurrent.TimeUnit;\n\nimport static com.walmartlabs.concord.it.console.Utils.DEFAULT_TEST_TIMEOUT;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\n@Timeout(value = DEFAULT_TEST_TIMEOUT, unit = TimeUnit.MILLISECONDS)\npublic class ProfileNavigationIT {\n\n    @RegisterExtension\n    public static ConcordConsoleRule consoleRule = new ConcordConsoleRule();\n\n    @Test\n    public void testTopBarDropdownNavigation() throws Exception {\n        consoleRule.login(Concord.ADMIN_API_KEY);\n\n        clickable(By.cssSelector(\"[data-testid='topbar-system-menu']\")).click();\n        clickable(By.cssSelector(\"[data-testid='topbar-about']\")).click();\n\n        consoleRule.waitFor(By.xpath(\"//*[contains(text(), 'Server version:')]\"));\n        assertTrue(consoleRule.getDriver().getCurrentUrl().contains(\"/#/about\"));\n\n        clickable(By.cssSelector(\"[data-testid='topbar-user-menu']\")).click();\n        clickable(By.cssSelector(\"[data-testid='topbar-profile']\")).click();\n\n        consoleRule.waitFor(By.xpath(\"//h4[contains(text(), 'API Tokens')]\"));\n        assertTrue(consoleRule.getDriver().getCurrentUrl().contains(\"/#/profile/api-token\"));\n    }\n\n    @Test\n    public void testApiTokenDoneNavigation() throws Exception {\n        consoleRule.login(Concord.ADMIN_API_KEY);\n        consoleRule.navigateToRelative(\"/#/profile/api-token\");\n\n        clickable(By.cssSelector(\"[data-testid='api-token-new-button']\")).click();\n        consoleRule.waitFor(By.cssSelector(\"[data-testid='api-token-form-name'] input\"))\n                .sendKeys(\"token_\" + ITUtils.randomString());\n\n        clickable(By.cssSelector(\"[data-testid='api-token-form-submit']\")).click();\n        clickable(By.cssSelector(\"[data-testid='api-token-created-done']\")).click();\n\n        consoleRule.waitFor(By.xpath(\"//h4[contains(text(), 'API Tokens')]\"));\n\n        String currentUrl = consoleRule.getDriver().getCurrentUrl();\n        assertTrue(currentUrl.contains(\"/#/profile/api-token\"));\n        assertFalse(currentUrl.contains(\"_new\"));\n    }\n\n    @Test\n    public void testDirectNavigationRedirects() throws Exception {\n        consoleRule.login(Concord.ADMIN_API_KEY);\n\n        consoleRule.navigateToRelative(\"/#/\");\n        waitForUrlContaining(\"/#/activity\");\n        consoleRule.waitFor(By.id(\"concordLogo\"));\n\n        consoleRule.navigateToRelative(\"/#/profile\");\n        waitForUrlContaining(\"/#/profile/api-token\");\n        consoleRule.waitFor(By.xpath(\"//h4[contains(text(), 'API Tokens')]\"));\n    }\n\n    @Test\n    public void testProtectedRouteRedirectsToLogin() throws Exception {\n        consoleRule.navigateToRelative(\"/#/profile/api-token\");\n\n        waitForUrlContaining(\"/#/login\");\n        consoleRule.waitFor(By.id(\"loginButton\"));\n    }\n\n    private static WebElement clickable(By by) {\n        return new WebDriverWait(consoleRule.getDriver(), Duration.ofSeconds(30))\n                .until(ExpectedConditions.elementToBeClickable(by));\n    }\n\n    private static void waitForUrlContaining(String value) {\n        new WebDriverWait(consoleRule.getDriver(), Duration.ofSeconds(30))\n                .until(ExpectedConditions.urlContains(value));\n    }\n}\n"
  },
  {
    "path": "it/console/src/test/java/com/walmartlabs/concord/it/console/ProjectTeamAccessIT.java",
    "content": "package com.walmartlabs.concord.it.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.it.common.ITUtils;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport org.openqa.selenium.By;\nimport org.openqa.selenium.Keys;\nimport org.openqa.selenium.WebElement;\nimport org.openqa.selenium.interactions.Actions;\n\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\n\nimport static com.walmartlabs.concord.it.console.Utils.DEFAULT_TEST_TIMEOUT;\nimport static org.junit.jupiter.api.Assertions.*;\n\n@Timeout(value = DEFAULT_TEST_TIMEOUT, unit = TimeUnit.MILLISECONDS)\npublic class ProjectTeamAccessIT {\n\n    @RegisterExtension\n    public static ConcordServerRule serverRule = new ConcordServerRule();\n\n    @RegisterExtension\n    public static ConcordConsoleRule consoleRule = new ConcordConsoleRule();\n\n    @Test\n    public void testAddTeamAccess() throws Exception {\n        ApiClient client = serverRule.getClient();\n\n        // ---\n\n        String orgName = \"org_\" + ITUtils.randomString();\n        String projectName = \"project_\" + ITUtils.randomString();\n        createOrgAndProject(client, orgName, projectName);\n\n        String team1Name = \"team_\" + ITUtils.randomString();\n        String team2Name = \"team_\" + ITUtils.randomString();\n        createTeamWithUser(client, orgName, team1Name);\n        createTeamWithUser(client, orgName, team2Name);\n\n        assignTeamAccess(client, orgName, projectName, team1Name, ResourceAccessEntry.LevelEnum.READER);\n\n        // ---\n\n        consoleRule.login(Concord.ADMIN_API_KEY);\n        consoleRule.navigateToRelative(\"/#/org/\" + orgName + \"/project/\" + projectName + \"/access\");\n        consoleRule.waitForLoad();\n\n        // ---\n\n        WebElement team1Row = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-row-\" + team1Name + \"']\"));\n        assertNotNull(team1Row);\n        WebElement team1AccessCell = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-level-\" + team1Name + \"']\"));\n        assertEquals(\"READER\", team1AccessCell.getText());\n\n        // ---\n\n        WebElement editButton = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-edit-btn']\"));\n        editButton.click();\n        Thread.sleep(500);\n\n        // ---\n\n        WebElement teamDropdown = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-add-dropdown'] input\"));\n        teamDropdown.click();\n        new Actions(consoleRule.getDriver())\n                .sendKeys(team2Name)\n                .pause(500)\n                .sendKeys(Keys.ENTER)\n                .perform();\n        Thread.sleep(500);\n\n        WebElement team2Row = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-row-\" + team2Name + \"']\"));\n        assertTrue(team2Row.getDomAttribute(\"class\").contains(\"positive\"));\n\n        // ---\n\n        WebElement saveButton = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-save-btn']:not([disabled])\"));\n        saveButton.click();\n        Thread.sleep(1500);\n\n        // ---\n\n        consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-row-\" + team1Name + \"']\"));\n        consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-row-\" + team2Name + \"']\"));\n\n        // ---\n\n        ProjectsApi projectsApi = new ProjectsApi(client);\n        List<ResourceAccessEntry> accessList = projectsApi.getProjectAccessLevel(orgName, projectName);\n        assertEquals(2, accessList.size());\n    }\n\n    @Test\n    public void testChangeAccessLevel() throws Exception {\n        ApiClient client = serverRule.getClient();\n\n        // ---\n\n        String orgName = \"org_\" + ITUtils.randomString();\n        String projectName = \"project_\" + ITUtils.randomString();\n        createOrgAndProject(client, orgName, projectName);\n\n        String teamName = \"team_\" + ITUtils.randomString();\n        createTeamWithUser(client, orgName, teamName);\n\n        assignTeamAccess(client, orgName, projectName, teamName, ResourceAccessEntry.LevelEnum.READER);\n\n        // ---\n\n        consoleRule.login(Concord.ADMIN_API_KEY);\n        consoleRule.navigateToRelative(\"/#/org/\" + orgName + \"/project/\" + projectName + \"/access\");\n        consoleRule.waitForLoad();\n\n        // ---\n\n        WebElement accessCell = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-level-\" + teamName + \"']\"));\n        assertEquals(\"READER\", accessCell.getText());\n\n        // ---\n\n        WebElement editButton = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-edit-btn']\"));\n        editButton.click();\n        Thread.sleep(500);\n\n        // ---\n\n        WebElement dropdown = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-dropdown-\" + teamName + \"']\"));\n        dropdown.click();\n        Thread.sleep(800);\n\n        WebElement ownerOption = consoleRule.waitFor(By.xpath(\"//div[@role='listbox']//div[@role='option' and .//span[text()='Owner']]\"));\n        ownerOption.click();\n        Thread.sleep(300);\n\n        // ---\n\n        WebElement saveButton = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-save-btn']\"));\n        saveButton.click();\n        Thread.sleep(1500);\n\n        // ---\n\n        accessCell = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-level-\" + teamName + \"']\"));\n        assertEquals(\"OWNER\", accessCell.getText());\n\n        // ---\n\n        ProjectsApi projectsApi = new ProjectsApi(client);\n        List<ResourceAccessEntry> accessList = projectsApi.getProjectAccessLevel(orgName, projectName);\n        assertEquals(1, accessList.size());\n        assertEquals(ResourceAccessEntry.LevelEnum.OWNER, accessList.get(0).getLevel());\n    }\n\n    @Test\n    public void testRemoveTeamAccess() throws Exception {\n        ApiClient client = serverRule.getClient();\n\n        // ---\n\n        String orgName = \"org_\" + ITUtils.randomString();\n        String projectName = \"project_\" + ITUtils.randomString();\n        createOrgAndProject(client, orgName, projectName);\n\n        String team1Name = \"team_\" + ITUtils.randomString();\n        String team2Name = \"team_\" + ITUtils.randomString();\n        createTeamWithUser(client, orgName, team1Name);\n        createTeamWithUser(client, orgName, team2Name);\n\n        assignTeamAccess(client, orgName, projectName, team1Name, ResourceAccessEntry.LevelEnum.READER);\n        assignTeamAccess(client, orgName, projectName, team2Name, ResourceAccessEntry.LevelEnum.WRITER);\n\n        // ---\n\n        consoleRule.login(Concord.ADMIN_API_KEY);\n        consoleRule.navigateToRelative(\"/#/org/\" + orgName + \"/project/\" + projectName + \"/access\");\n        consoleRule.waitForLoad();\n\n        // ---\n\n        consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-row-\" + team1Name + \"']\"));\n        consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-row-\" + team2Name + \"']\"));\n\n        // ---\n\n        WebElement editButton = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-edit-btn']\"));\n        editButton.click();\n        Thread.sleep(500);\n\n        // ---\n\n        WebElement team1Row = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-row-\" + team1Name + \"']\"));\n        WebElement deleteButton = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-delete-btn-\" + team1Name + \"']\"));\n        deleteButton.click();\n        Thread.sleep(300);\n        assertTrue(team1Row.getDomAttribute(\"class\").contains(\"negative\"));\n\n        deleteButton.click();\n        Thread.sleep(300);\n        assertFalse(team1Row.getDomAttribute(\"class\").contains(\"negative\"));\n\n        deleteButton.click();\n        Thread.sleep(300);\n        assertTrue(team1Row.getDomAttribute(\"class\").contains(\"negative\"));\n\n        // ---\n\n        WebElement saveButton = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-save-btn']\"));\n        saveButton.click();\n        Thread.sleep(1500);\n\n        // ---\n\n        List<WebElement> team1Rows = consoleRule.getDriver().findElements(By.cssSelector(\"[data-testid='team-access-row-\" + team1Name + \"']\"));\n        assertEquals(0, team1Rows.size());\n        consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-row-\" + team2Name + \"']\"));\n\n        // ---\n\n        ProjectsApi projectsApi = new ProjectsApi(client);\n        List<ResourceAccessEntry> accessList = projectsApi.getProjectAccessLevel(orgName, projectName);\n        assertEquals(1, accessList.size());\n        assertEquals(team2Name, accessList.get(0).getTeamName());\n    }\n\n    private void createOrgAndProject(ApiClient client, String orgName, String projectName) throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(client);\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        ProjectsApi projectsApi = new ProjectsApi(client);\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry().name(projectName));\n    }\n\n    private void createTeamWithUser(ApiClient client, String orgName, String teamName) throws Exception {\n        TeamsApi teamsApi = new TeamsApi(client);\n        teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamName));\n\n        String userName = \"user_\" + ITUtils.randomString();\n        UsersApi usersApi = new UsersApi(client);\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        teamsApi.addUsersToTeam(orgName, teamName, false,\n                Collections.singletonList(new TeamUserEntry()\n                        .username(userName)\n                        .role(TeamUserEntry.RoleEnum.MEMBER)));\n    }\n\n    private void assignTeamAccess(ApiClient client, String orgName, String projectName,\n                                  String teamName, ResourceAccessEntry.LevelEnum level) throws Exception {\n        ProjectsApi projectsApi = new ProjectsApi(client);\n        projectsApi.updateProjectAccessLevel(orgName, projectName,\n                new ResourceAccessEntry()\n                        .orgName(orgName)\n                        .teamName(teamName)\n                        .level(level));\n    }\n}\n"
  },
  {
    "path": "it/console/src/test/java/com/walmartlabs/concord/it/console/RepositoryRunIT.java",
    "content": "package com.walmartlabs.concord.it.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.it.common.ITUtils;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport org.openqa.selenium.By;\n\nimport java.util.Collections;\nimport java.util.concurrent.TimeUnit;\n\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static com.walmartlabs.concord.it.console.Utils.DEFAULT_TEST_TIMEOUT;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n@Timeout(value = DEFAULT_TEST_TIMEOUT, unit = TimeUnit.MILLISECONDS)\npublic class RepositoryRunIT {\n\n    @RegisterExtension\n    public static ConcordServerRule serverRule = new ConcordServerRule();\n\n    @RegisterExtension\n    public static ConcordConsoleRule consoleRule = new ConcordConsoleRule();\n\n    @Test\n    public void testRunRepositoryFromUi() throws Exception {\n        var client = serverRule.getClient();\n\n        var gitUrl = ITUtils.createGitRepo(RepositoryRunIT.class, \"repositoryRun\");\n        var orgName = \"org_\" + ITUtils.randomString();\n        var projectName = \"project_\" + ITUtils.randomString();\n        var repoName = \"repo_\" + ITUtils.randomString();\n\n        createOrgAndProject(client, orgName, projectName, repoName, gitUrl);\n\n        consoleRule.login(Concord.ADMIN_API_KEY);\n        var repositoriesUrl = \"/#/org/\" + orgName + \"/project/\" + projectName + \"/repository\";\n        var runButtonSelector = By.cssSelector(\"[data-testid='repository-run-button-\" + repoName + \"']\");\n        waitForRepositoryPage(consoleRule, repositoriesUrl, runButtonSelector);\n\n        var runButton = consoleRule.waitFor(runButtonSelector);\n        runButton.click();\n\n        Thread.sleep(500);\n        var startButton = consoleRule.waitFor(By.cssSelector(\".ui.modal.active .actions .blue.button\"));\n        startButton.click();\n\n        var openProcessPageButton = consoleRule.waitFor(By.xpath(\"//button[contains(normalize-space(.), 'Open the process page')]\"));\n        assertNotNull(openProcessPageButton);\n        openProcessPageButton.click();\n        Thread.sleep(1000);\n        consoleRule.waitForLoad();\n\n        var processV2Api = new ProcessV2Api(client);\n        var processes = processV2Api.listProcesses(ProcessListFilter.builder()\n                .orgName(orgName)\n                .projectName(projectName)\n                .repoName(repoName)\n                .build());\n\n        assertFalse(processes.isEmpty());\n\n        var process = waitForCompletion(client, processes.get(0).getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, process.getStatus());\n\n        var log = serverRule.getLog(process.getInstanceId());\n        assertLog(\".*Hello!.*\", log);\n    }\n\n    private void createOrgAndProject(\n            ApiClient client,\n            String orgName,\n            String projectName,\n            String repoName,\n            String gitUrl\n    ) throws Exception {\n        var organizationsApi = new OrganizationsApi(client);\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        var projectsApi = new ProjectsApi(client);\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .url(gitUrl)\n                        .branch(\"master\"))));\n    }\n\n    private void waitForRepositoryPage(ConcordConsoleRule consoleRule, String url, By runButtonSelector) throws Exception {\n        for (var attempt = 0; attempt < 10; attempt++) {\n            consoleRule.navigateToRelative(url);\n            consoleRule.waitForLoad();\n\n            if (!consoleRule.getDriver().findElements(runButtonSelector).isEmpty()) {\n                return;\n            }\n\n            Thread.sleep(1000);\n        }\n    }\n}\n"
  },
  {
    "path": "it/console/src/test/java/com/walmartlabs/concord/it/console/SecretIT.java",
    "content": "package com.walmartlabs.concord.it.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.it.common.ITUtils;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport org.openqa.selenium.By;\nimport org.openqa.selenium.Keys;\nimport org.openqa.selenium.WebElement;\nimport org.openqa.selenium.interactions.Actions;\nimport org.openqa.selenium.support.ui.Select;\n\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\n\nimport static com.walmartlabs.concord.it.console.Utils.DEFAULT_TEST_TIMEOUT;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n@Timeout(value = DEFAULT_TEST_TIMEOUT, unit = TimeUnit.MILLISECONDS)\npublic class SecretIT {\n\n    @RegisterExtension\n    public static ConcordServerRule serverRule = new ConcordServerRule();\n\n    @RegisterExtension\n    public static ConcordConsoleRule consoleRule = new ConcordConsoleRule();\n\n    @Test\n    public void testSecretSettingsVisibility() throws Exception {\n        var client = serverRule.getClient();\n\n        var orgName = \"org_\" + ITUtils.randomString();\n        var secretName = \"secret_\" + ITUtils.randomString();\n\n        createOrg(client, orgName);\n        createUsernamePasswordSecret(client, orgName, secretName, SecretEntryV2.VisibilityEnum.PUBLIC);\n\n        consoleRule.login(Concord.ADMIN_API_KEY);\n        consoleRule.navigateToRelative(\"/#/org/\" + orgName + \"/secret/\" + secretName + \"/settings\");\n        consoleRule.waitForLoad();\n\n        var visibilitySelect = consoleRule.waitFor(By.cssSelector(\"[data-testid='secret-visibility-select']\"));\n        new Select(visibilitySelect).selectByVisibleText(\"Private\");\n        Thread.sleep(300);\n\n        var changeButton = consoleRule.waitFor(By.xpath(\"//button[normalize-space()='Change' and not(@disabled)]\"));\n        changeButton.click();\n\n        Thread.sleep(500);\n        var confirmButton = consoleRule.waitFor(By.cssSelector(\".ui.modal.active .actions .ui.primary.button\"));\n        confirmButton.click();\n        Thread.sleep(1500);\n\n        var secret = new SecretsV2Api(client).getSecret(orgName, secretName);\n        assertEquals(SecretEntryV2.VisibilityEnum.PRIVATE, secret.getVisibility());\n    }\n\n    @Test\n    public void testSecretTeamAccess() throws Exception {\n        var client = serverRule.getClient();\n\n        var orgName = \"org_\" + ITUtils.randomString();\n        var secretName = \"secret_\" + ITUtils.randomString();\n        var teamName = \"team_\" + ITUtils.randomString();\n\n        createOrg(client, orgName);\n        createUsernamePasswordSecret(client, orgName, secretName, SecretEntryV2.VisibilityEnum.PUBLIC);\n        createTeamWithUser(client, orgName, teamName);\n\n        consoleRule.login(Concord.ADMIN_API_KEY);\n        consoleRule.navigateToRelative(\"/#/org/\" + orgName + \"/secret/\" + secretName + \"/access\");\n        consoleRule.waitForLoad();\n\n        var editButton = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-edit-btn']\"));\n        editButton.click();\n        Thread.sleep(500);\n\n        var teamDropdown = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-add-dropdown'] input\"));\n        teamDropdown.click();\n        new Actions(consoleRule.getDriver())\n                .sendKeys(teamName)\n                .pause(500)\n                .sendKeys(Keys.ENTER)\n                .perform();\n        Thread.sleep(500);\n\n        var teamRow = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-row-\" + teamName + \"']\"));\n        assertNotNull(teamRow);\n\n        var saveButton = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-access-save-btn']:not([disabled])\"));\n        saveButton.click();\n        Thread.sleep(1500);\n\n        var accessList = new SecretsApi(client).getSecretAccessLevel(orgName, secretName);\n        assertEquals(1, accessList.size());\n        assertEquals(teamName, accessList.get(0).getTeamName());\n        assertEquals(ResourceAccessEntry.LevelEnum.READER, accessList.get(0).getLevel());\n    }\n\n    private void createOrg(ApiClient client, String orgName) throws Exception {\n        var organizationsApi = new OrganizationsApi(client);\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n    }\n\n    private void createUsernamePasswordSecret(\n            ApiClient client,\n            String orgName,\n            String secretName,\n            SecretEntryV2.VisibilityEnum visibility\n    ) throws Exception {\n        var secretClient = new SecretClient(client);\n        secretClient.createSecret(CreateSecretRequest.builder()\n                .org(orgName)\n                .name(secretName)\n                .visibility(visibility)\n                .usernamePassword(CreateSecretRequest.UsernamePassword.of(\"user\", \"pass\"))\n                .build());\n    }\n\n    private void createTeamWithUser(ApiClient client, String orgName, String teamName) throws Exception {\n        var teamsApi = new TeamsApi(client);\n        teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamName));\n\n        var userName = \"user_\" + ITUtils.randomString();\n        var usersApi = new UsersApi(client);\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        teamsApi.addUsersToTeam(orgName, teamName, false,\n                Collections.singletonList(new TeamUserEntry()\n                        .username(userName)\n                        .role(TeamUserEntry.RoleEnum.MEMBER)));\n    }\n}\n"
  },
  {
    "path": "it/console/src/test/java/com/walmartlabs/concord/it/console/TeamIT.java",
    "content": "package com.walmartlabs.concord.it.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.it.common.ITUtils;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport org.openqa.selenium.By;\nimport org.openqa.selenium.Keys;\nimport org.openqa.selenium.WebElement;\n\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\n\nimport static com.walmartlabs.concord.it.console.Utils.DEFAULT_TEST_TIMEOUT;\nimport static org.junit.jupiter.api.Assertions.*;\n\n@Timeout(value = DEFAULT_TEST_TIMEOUT, unit = TimeUnit.MILLISECONDS)\npublic class TeamIT {\n\n    @RegisterExtension\n    public static ConcordServerRule serverRule = new ConcordServerRule();\n\n    @RegisterExtension\n    public static ConcordConsoleRule consoleRule = new ConcordConsoleRule();\n\n    @Test\n    public void testViewTeam() throws Exception {\n        ApiClient client = serverRule.getClient();\n\n        // --- setup: create org and team via API\n\n        String orgName = \"org_\" + ITUtils.randomString();\n        String teamName = \"team_\" + ITUtils.randomString();\n        String teamDescription = \"Test team description\";\n\n        OrganizationsApi orgApi = new OrganizationsApi(client);\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        TeamsApi teamsApi = new TeamsApi(client);\n        teamsApi.createOrUpdateTeam(orgName, new TeamEntry()\n                .name(teamName)\n                .description(teamDescription));\n\n        // --- login and navigate to team page\n\n        consoleRule.login(Concord.ADMIN_API_KEY);\n        consoleRule.navigateToRelative(\"/#/org/\" + orgName + \"/team/\" + teamName);\n        consoleRule.waitForLoad();\n\n        // --- verify team page loads with correct tabs\n\n        WebElement membersTab = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-tab-members']\"));\n        assertNotNull(membersTab);\n        assertTrue(membersTab.getText().contains(\"Members\"));\n\n        WebElement ldapGroupsTab = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-tab-ldapGroups']\"));\n        assertNotNull(ldapGroupsTab);\n\n        WebElement settingsTab = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-tab-settings']\"));\n        assertNotNull(settingsTab);\n\n        // --- navigate to settings and verify team ID is displayed\n\n        settingsTab.click();\n        consoleRule.waitForLoad();\n        Thread.sleep(500);\n\n        WebElement teamIdElement = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-settings-id']\"));\n        assertNotNull(teamIdElement);\n        assertTrue(teamIdElement.getText().contains(\"ID:\"));\n    }\n\n    @Test\n    public void testCreateTeam() throws Exception {\n        ApiClient client = serverRule.getClient();\n\n        // --- setup: create org via API\n\n        String orgName = \"org_\" + ITUtils.randomString();\n        String newTeamName = \"team_\" + ITUtils.randomString();\n        String newTeamDescription = \"New team description\";\n\n        OrganizationsApi orgApi = new OrganizationsApi(client);\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // --- login and navigate to new team page\n\n        consoleRule.login(Concord.ADMIN_API_KEY);\n        consoleRule.navigateToRelative(\"/#/org/\" + orgName + \"/team/_new\");\n        consoleRule.waitForLoad();\n\n        // --- fill in the form\n\n        WebElement nameInput = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-form-name'] input\"));\n        nameInput.sendKeys(newTeamName);\n\n        WebElement descriptionInput = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-form-description'] input\"));\n        descriptionInput.sendKeys(newTeamDescription);\n\n        Thread.sleep(500); // wait for validation\n\n        // --- submit the form\n\n        WebElement createButton = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-form-submit']\"));\n        assertTrue(createButton.isEnabled());\n        createButton.click();\n\n        // --- wait for navigation to the new team page\n\n        Thread.sleep(2000);\n        consoleRule.waitForLoad();\n\n        // --- verify we're on the new team's page\n\n        String currentUrl = consoleRule.getDriver().getCurrentUrl();\n        assertTrue(currentUrl.contains(\"/org/\" + orgName + \"/team/\" + newTeamName),\n                \"Expected URL to contain team path, but was: \" + currentUrl);\n\n        // --- verify team was created via API\n\n        TeamsApi teamsApi = new TeamsApi(client);\n        TeamEntry team = teamsApi.getTeam(orgName, newTeamName);\n        assertNotNull(team);\n        assertEquals(newTeamName, team.getName());\n        assertEquals(newTeamDescription, team.getDescription());\n    }\n\n    @Test\n    public void testRenameTeam() throws Exception {\n        ApiClient client = serverRule.getClient();\n\n        // --- setup: create org and team via API\n\n        String orgName = \"org_\" + ITUtils.randomString();\n        String originalTeamName = \"team_\" + ITUtils.randomString();\n        String newTeamName = \"renamed_\" + ITUtils.randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(client);\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        TeamsApi teamsApi = new TeamsApi(client);\n        teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(originalTeamName));\n\n        // --- login and navigate to team settings\n\n        consoleRule.login(Concord.ADMIN_API_KEY);\n        consoleRule.navigateToRelative(\"/#/org/\" + orgName + \"/team/\" + originalTeamName + \"/settings\");\n        consoleRule.waitForLoad();\n\n        // --- find the rename input, select all text, then enter new name\n\n        WebElement renameInput = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-rename-input'] input\"));\n        renameInput.sendKeys(Keys.chord(Keys.CONTROL, \"a\"));\n        renameInput.sendKeys(newTeamName);\n\n        Thread.sleep(500); // wait for validation\n\n        // --- click rename button\n\n        WebElement renameButton = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-rename-button']\"));\n        assertTrue(renameButton.isEnabled());\n        renameButton.click();\n\n        // --- wait for confirmation modal and confirm\n\n        Thread.sleep(500);\n        WebElement confirmButton = consoleRule.waitFor(By.cssSelector(\".ui.modal.active .actions .ui.primary.button\"));\n        confirmButton.click();\n\n        // --- wait for navigation\n\n        Thread.sleep(2000);\n        consoleRule.waitForLoad();\n\n        // --- verify we're on the renamed team's page\n\n        String currentUrl = consoleRule.getDriver().getCurrentUrl();\n        assertTrue(currentUrl.contains(\"/org/\" + orgName + \"/team/\" + newTeamName),\n                \"Expected URL to contain new team name, but was: \" + currentUrl);\n\n        // --- verify team was renamed via API\n\n        TeamEntry team = teamsApi.getTeam(orgName, newTeamName);\n        assertNotNull(team);\n        assertEquals(newTeamName, team.getName());\n\n        // --- verify old team name no longer exists\n\n        try {\n            teamsApi.getTeam(orgName, originalTeamName);\n            fail(\"Expected team with original name to not exist\");\n        } catch (ApiException e) {\n            // Accept any 4xx error - team should not be accessible\n            assertTrue(e.getCode() >= 400 && e.getCode() < 500,\n                    \"Expected 4xx error but got: \" + e.getCode());\n        }\n    }\n\n    @Test\n    public void testDeleteTeam() throws Exception {\n        ApiClient client = serverRule.getClient();\n\n        // --- setup: create org and team via API\n\n        String orgName = \"org_\" + ITUtils.randomString();\n        String teamName = \"team_\" + ITUtils.randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(client);\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        TeamsApi teamsApi = new TeamsApi(client);\n        teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamName));\n\n        // --- verify team exists\n\n        TeamEntry team = teamsApi.getTeam(orgName, teamName);\n        assertNotNull(team);\n\n        // --- login and navigate to team settings\n\n        consoleRule.login(Concord.ADMIN_API_KEY);\n        consoleRule.navigateToRelative(\"/#/org/\" + orgName + \"/team/\" + teamName + \"/settings\");\n        consoleRule.waitForLoad();\n\n        // --- click delete button\n\n        WebElement deleteButton = consoleRule.waitFor(By.cssSelector(\"[data-testid='team-delete-button']\"));\n        deleteButton.click();\n\n        // --- wait for confirmation modal and confirm\n\n        Thread.sleep(500);\n        WebElement confirmButton = consoleRule.waitFor(By.cssSelector(\".ui.modal.active .actions .ui.primary.button\"));\n        confirmButton.click();\n\n        // --- wait for navigation to team list\n\n        Thread.sleep(2000);\n        consoleRule.waitForLoad();\n\n        // --- verify we're redirected to team list\n\n        String currentUrl = consoleRule.getDriver().getCurrentUrl();\n        assertTrue(currentUrl.contains(\"/org/\" + orgName + \"/team\") && !currentUrl.contains(teamName),\n                \"Expected URL to be team list, but was: \" + currentUrl);\n\n        // --- verify team was deleted via API\n\n        try {\n            teamsApi.getTeam(orgName, teamName);\n            fail(\"Expected team to be deleted\");\n        } catch (ApiException e) {\n            // Accept any 4xx error - team should not be accessible\n            assertTrue(e.getCode() >= 400 && e.getCode() < 500,\n                    \"Expected 4xx error but got: \" + e.getCode());\n        }\n    }\n\n    @Test\n    public void testTeamListNavigation() throws Exception {\n        ApiClient client = serverRule.getClient();\n\n        // --- setup: create org and teams via API\n\n        String orgName = \"org_\" + ITUtils.randomString();\n        String team1Name = \"team_a_\" + ITUtils.randomString();\n        String team2Name = \"team_b_\" + ITUtils.randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(client);\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        TeamsApi teamsApi = new TeamsApi(client);\n        teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(team1Name));\n        teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(team2Name));\n\n        // --- login and navigate to team list\n\n        consoleRule.login(Concord.ADMIN_API_KEY);\n        consoleRule.navigateToRelative(\"/#/org/\" + orgName + \"/team\");\n        consoleRule.waitForLoad();\n\n        // --- verify both teams are listed\n\n        WebElement team1Link = consoleRule.waitFor(By.linkText(team1Name));\n        assertNotNull(team1Link);\n\n        WebElement team2Link = consoleRule.waitFor(By.linkText(team2Name));\n        assertNotNull(team2Link);\n\n        // --- click on team1 and verify navigation\n\n        team1Link.click();\n        consoleRule.waitForLoad();\n        Thread.sleep(500);\n\n        String currentUrl = consoleRule.getDriver().getCurrentUrl();\n        assertTrue(currentUrl.contains(\"/org/\" + orgName + \"/team/\" + team1Name),\n                \"Expected URL to contain team1 path, but was: \" + currentUrl);\n    }\n}\n"
  },
  {
    "path": "it/console/src/test/java/com/walmartlabs/concord/it/console/TryingIT.java",
    "content": "package com.walmartlabs.concord.it.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\nimport org.junit.jupiter.api.extension.*;\nimport org.junit.jupiter.api.parallel.Execution;\nimport org.junit.jupiter.api.parallel.ExecutionMode;\n\n@Execution(ExecutionMode.CONCURRENT)\npublic class TryingIT extends Base {\n\n    @RegisterExtension\n    public static TestRule rule = new TestRule();\n\n    @Test\n    public void testTimeout() throws Exception {\n        System.out.println(\"first\");\n        Thread.sleep(10_000);\n    }\n\n    @Test\n    public void testTimeout2() throws Exception {\n        System.out.println(\"second\");\n        Thread.sleep(10_000);\n    }\n\n    public static class TestRule implements BeforeEachCallback, TestExecutionExceptionHandler, AfterEachCallback {\n        @Override\n        public void afterEach(ExtensionContext context) {\n            System.out.println(\"afterEach\");\n        }\n\n        @Override\n        public void beforeEach(ExtensionContext context) {\n            System.out.println(\"beforeEach\");\n        }\n\n        @Override\n        public void handleTestExecutionException(ExtensionContext context, Throwable throwable) throws Throwable {\n            String fileName = context.getTestClass().get().getName() + context.getTestMethod().get().getName() + \".png\";\n            System.out.println(\"handleTestExecutionException:\" + fileName);\n        }\n    }\n}\n"
  },
  {
    "path": "it/console/src/test/java/com/walmartlabs/concord/it/console/Utils.java",
    "content": "package com.walmartlabs.concord.it.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic final class Utils {\n\n    public static final long DEFAULT_TEST_TIMEOUT = 120000;\n\n    public static String env(String k, String def) {\n        String v = System.getenv(k);\n        if (v == null) {\n            return def;\n        }\n        return v;\n    }\n\n    private Utils() {\n    }\n}\n"
  },
  {
    "path": "it/console/src/test/java/com/walmartlabs/concord/it/console/WebDriverRule.java",
    "content": "package com.walmartlabs.concord.it.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.extension.AfterEachCallback;\nimport org.junit.jupiter.api.extension.BeforeEachCallback;\nimport org.junit.jupiter.api.extension.ExtensionContext;\nimport org.junit.jupiter.api.extension.TestExecutionExceptionHandler;\nimport org.openqa.selenium.OutputType;\nimport org.openqa.selenium.TakesScreenshot;\nimport org.openqa.selenium.WebDriver;\nimport org.openqa.selenium.chrome.ChromeDriver;\nimport org.openqa.selenium.chrome.ChromeOptions;\nimport org.openqa.selenium.logging.LogType;\nimport org.openqa.selenium.logging.LoggingPreferences;\nimport org.openqa.selenium.remote.LocalFileDetector;\nimport org.openqa.selenium.remote.RemoteWebDriver;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.net.URL;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardCopyOption;\nimport java.util.logging.Level;\n\nimport static com.walmartlabs.concord.it.console.Utils.env;\n\npublic class WebDriverRule implements BeforeEachCallback, TestExecutionExceptionHandler, AfterEachCallback {\n\n    private static final Logger log = LoggerFactory.getLogger(WebDriverRule.class);\n\n    private final int seleniumPort;\n    private final String driverType;\n    private final String screenshotsDir;\n\n    private WebDriver driver;\n\n    public WebDriverRule() {\n        this.seleniumPort = Integer.parseInt(env(\"IT_SELENIUM_PORT\", \"4444\"));\n        this.driverType = env(\"IT_WEBDRIVER_TYPE\", \"local\");\n        this.screenshotsDir = env(\"IT_SCREENSHOTS_DIR\", \"target/screenshots\");\n    }\n\n    public WebDriver getDriver() {\n        return driver;\n    }\n\n    public boolean isRemote() {\n        return \"remote\".equals(driverType);\n    }\n\n    protected void setUp() throws Exception {\n        ChromeOptions opts = new ChromeOptions();\n\n        LoggingPreferences logPrefs = new LoggingPreferences();\n        logPrefs.enable(LogType.BROWSER, Level.ALL);\n        opts.setCapability(\"goog:loggingPrefs\", logPrefs);\n\n        opts.addArguments(\"--dns-prefetch-disable\");\n\n        if (isRemote()) {\n            URL url = new URL(\"http://localhost:\" + seleniumPort + \"/wd/hub\");\n            log.info(\"Using a remote driver: {}\", url);\n            RemoteWebDriver d = new RemoteWebDriver(url, opts);\n            d.setFileDetector(new LocalFileDetector());\n            driver = d;\n        } else {\n            log.info(\"Using a local driver...\");\n            driver = new ChromeDriver();\n        }\n    }\n\n    private void tearDown() {\n        driver.quit();\n    }\n\n    private void takeScreenshot(ExtensionContext context) throws IOException {\n        File src = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);\n\n        Path dstDir = Paths.get(screenshotsDir);\n        Files.createDirectories(dstDir);\n\n        String fileName = context.getTestClass().get().getName() + context.getTestMethod().get().getName() + \".png\";\n        Path dst = dstDir.resolve(fileName);\n\n        Files.copy(src.toPath(), dst, StandardCopyOption.REPLACE_EXISTING);\n\n        System.out.println(\"Screenshot saved to: \" + dst);\n    }\n\n    @Override\n    public void beforeEach(ExtensionContext context) throws Exception {\n        setUp();\n    }\n\n    @Override\n    public void afterEach(ExtensionContext context) {\n        tearDown();\n    }\n\n    @Override\n    public void handleTestExecutionException(ExtensionContext context, Throwable throwable) throws Throwable {\n        takeScreenshot(context);\n        throw throwable;\n    }\n}\n"
  },
  {
    "path": "it/console/src/test/resources/agent.conf",
    "content": "concord-agent {\n\n    dependencyResolveTimeout = \"30 seconds\"\n    logMaxDelay = \"250 milliseconds\"\n    pollInterval = \"250 milliseconds\"\n\n    server {\n        apiKey = \"cTJxMnEycTI=\"\n    }\n}\n"
  },
  {
    "path": "it/console/src/test/resources/com/walmartlabs/concord/it/console/customForm/concord.yml",
    "content": "flows:\n  default:\n  - set:\n      yamlObj:\n        values:\n        - \"value_1\"\n        - \"value_2\"\n\n  - form: testForm\n    values:\n      testValue: \"${testValue}\"\n  - log: \"uploaded contents: ${resource.asJson(testForm.f)}\"\n\nforms:\n  testForm:\n  - x: { type: \"string?\", allow: \"${yamlObj.values}\" }\n  - f: { type: \"file\" }\n"
  },
  {
    "path": "it/console/src/test/resources/com/walmartlabs/concord/it/console/customForm/forms/testForm/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>Test Form</title>\n    <script src=\"data.js\"></script>\n</head>\n<body>\n<div id=\"testValue\"></div>\n<form id=\"theForm\" method=\"post\" enctype=\"multipart/form-data\">\n    <input name=\"x\" id=\"x\" type=\"text\"/>\n    <input name=\"f\" id=\"f\" type=\"file\"/>\n    <button name=\"submit\" id=\"submitButton\" type=\"submit\">Submit</button>\n</form>\n\n<script type=\"text/javascript\">\n    document.getElementById('testValue').innerHTML = data.values.testValue;\n\n    const populate = () => {\n      document.getElementById('theForm').action = data.submitUrl;\n      document.getElementById('x').value = \"value_2\";\n\n      // set file value\n      let contents = [JSON.stringify({ \"hello\":\"world\" })];\n      let contentsBlob = new Blob(contents, { type: 'application/octet-stream' });\n      let file = new File([contentsBlob],\n        'upload.json',\n        {\n          type:\"application/octet-stream\",\n          lastModified: new Date().getTime()\n        },\n        'utf-8');\n      let container = new DataTransfer();\n      container.items.add(file);\n      document.getElementById('f').files = container.files;\n    };\n\n    populate();\n</script>\n</body>\n</html>\n"
  },
  {
    "path": "it/console/src/test/resources/com/walmartlabs/concord/it/console/dateTimeField/concord.yml",
    "content": "flows:\n  default:\n    - form: myForm\n      fields:\n        - dateField: { type: \"date\" }\n        - dateTimeField: { type: \"dateTime\" }\n    - log: \"myForm: ${myForm}\"\n"
  },
  {
    "path": "it/console/src/test/resources/com/walmartlabs/concord/it/console/processAnsible/concord.yml",
    "content": "configuration:\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\"\n\nflows:\n  default:\n    - task: ansible\n      in:\n        playbook: \"playbook.yml\"\n        inventory:\n          local:\n            hosts:\n              - \"127.0.0.1\"\n            vars:\n              ansible_connection: \"local\"\n"
  },
  {
    "path": "it/console/src/test/resources/com/walmartlabs/concord/it/console/processAnsible/playbook.yml",
    "content": "---\n- name: Smoke play\n  hosts: all\n  gather_facts: false\n  tasks:\n    - name: Announce host\n      debug:\n        msg: \"Hello from {{ inventory_hostname }}\"\n        verbosity: 0\n    - name: Verify host\n      debug:\n        msg: \"Verified {{ inventory_hostname }}\"\n        verbosity: 0\n"
  },
  {
    "path": "it/console/src/test/resources/com/walmartlabs/concord/it/console/repositoryRun/concord.yml",
    "content": "flows:\n  default:\n    - log: \"Hello!\"\n"
  },
  {
    "path": "it/console/src/test/resources/com/walmartlabs/concord/it/console/stringValues/concord.yml",
    "content": "flows:\n  default:\n    - set:\n        defaults: [ \"first\", \"second\" ]\n\n    - form: myForm\n      fields:\n        - field0: { label: \"field0\", type: \"string*\", allow: [\"first\", \"second\", \"third\"] }\n        - field1: { label: \"field1\", type: \"string*\", value: \"${defaults}\", allow: [\"first\", \"second\", \"third\"] }\n        - field2: { label: \"field2\", type: \"string+\", value: \"${defaults}\" }\n        - field3: { label: \"field3\", type: \"string*\" }\n        - field4: { label: \"field4\", type: \"string\" }\n    - log: \"${resource.prettyPrintJson(myForm)}\"\n"
  },
  {
    "path": "it/console/src/test/resources/console.conf",
    "content": "server {\n\tlisten                  8080;\n\tserver_name             _;\n\troot                    /opt/concord/console/dist;\n\n\taccess_log              /opt/concord/logs/access.log main;\n\n\tproxy_read_timeout      1800;\n    client_max_body_size    32M;\n\n\tlocation ~ ^/(api/|logs/|forms/|events/|swagger/|metrics|resources/) {\n        expires             off;\n\t    proxy_pass          http://server-node:8001;\n\t    proxy_redirect      off;\n\t    proxy_set_header    Host $http_host;\n\t    proxy_set_header    X-Real-IP $remote_addr;\n        proxy_set_header    X-Forwarded-For $remote_addr;\n\t}\n\n    location /websocket {\n        proxy_pass          http://server-node:8001/websocket;\n        proxy_http_version  1.1;\n        proxy_set_header    Upgrade $http_upgrade;\n        proxy_set_header    Connection \"Upgrade\";\n    }\n\n\tlocation ~ ^/landing/(.*)$ {\n\t    alias               /opt/concord/console/landing/$1;\n\t}\n\n\tlocation ~ ^/public/(.*)$ {\n\t    alias               /opt/concord/console/public/$1;\n\t}\n\n\tlocation ~* \\.(?:manifest|appcache|html?|xml|json)$ {\n        expires             -1;\n    }\n\n\tlocation ~* \\.(?:ico|css|js|gif|jpe?g|png)$ {\n        expires             30d;\n        add_header          Pragma public;\n        add_header          Cache-Control \"public\";\n    }\n}\n"
  },
  {
    "path": "it/console/src/test/resources/logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] [%-5level] %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "it/console/src/test/resources/mvn.json",
    "content": "{\n  \"repositories\": [\n    {\n      \"id\": \"host\",\n      \"url\": \"file:///host/.m2/repository\"\n    }\n  ]\n}\n"
  },
  {
    "path": "it/console/src/test/resources/server.conf",
    "content": "concord-server {\n    db {\n        changeLogParameters {\n            defaultAdminToken = \"cTFxMXExcTE=\"\n            defaultAgentToken = \"cTJxMnEycTI=\"\n        }\n    }\n\n    queue {\n        enqueuePollInterval = \"250 milliseconds\"\n        dispatcher {\n            pollDelay = \"250 milliseconds\"\n        }\n    }\n\n    secretStore {\n        serverPassword = \"aXRpdGl0\"\n        secretStoreSalt = \"aXRpdGl0\"\n        projectSecretSalt = \"aXRpdGl0\"\n    }\n}\n"
  },
  {
    "path": "it/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <groupId>com.walmartlabs.concord.it</groupId>\n    <artifactId>parent</artifactId>\n    <packaging>pom</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <modules>\n        <module>tasks/broken-deps</module>\n        <module>tasks/dependency-manager-test</module>\n        <module>tasks/schema-test</module>\n        <module>tasks/serialization-test</module>\n        <module>tasks/suspend-test</module>\n\n        <module>common</module>\n        <module>server</module>\n        <module>console</module>\n        <module>runtime-v1</module>\n        <module>runtime-v2</module>\n        <module>compat</module>\n\n        <module>testing-server</module>\n    </modules>\n\n    <properties>\n        <skip.it.tests>true</skip.it.tests>\n    </properties>\n\n    <profiles>\n        <profile>\n            <id>it</id>\n            <activation>\n                <activeByDefault>false</activeByDefault>\n            </activation>\n            <properties>\n                <skip.it.tests>false</skip.it.tests>\n            </properties>\n        </profile>\n    </profiles>\n</project>\n"
  },
  {
    "path": "it/runtime-v1/README.md",
    "content": "# Integration Tests for Concord Runtime v1"
  },
  {
    "path": "it/runtime-v1/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.it</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-runtime-v1-it</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <properties>\n        <it.local.mode>false</it.local.mode>\n        <maxParallelTestThreads>3</maxParallelTestThreads>\n        <server.image>walmartlabs/concord-server</server.image>\n        <agent.image>walmartlabs/concord-agent</agent.image>\n        <ryuk.image>testcontainers/ryuk:0.6.0</ryuk.image>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-client2</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.it</groupId>\n            <artifactId>concord-common-it</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>ch.qos.logback</groupId>\n            <artifactId>logback-classic</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-compress</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>ca.ibodrov.concord</groupId>\n            <artifactId>testcontainers-concord-core</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>ca.ibodrov.concord</groupId>\n            <artifactId>testcontainers-concord-junit5</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.testcontainers</groupId>\n            <artifactId>testcontainers</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <resources>\n            <resource>\n                <filtering>false</filtering>\n                <directory>${project.basedir}/src/test/resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n            <resource>\n                <filtering>true</filtering>\n                <directory>${project.basedir}/src/test/filtered-resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n        </resources>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-failsafe-plugin</artifactId>\n                <configuration>\n                    <skipTests>${skip.it.tests}</skipTests>\n                    <systemProperties>\n                        <it.local.mode>${it.local.mode}</it.local.mode>\n                        <db.image>${db.image}</db.image>\n                        <server.image>${server.image}</server.image>\n                        <agent.image>${agent.image}</agent.image>\n                        <ryuk.image>${ryuk.image}</ryuk.image>\n                    </systemProperties>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n    <profiles>\n        <profile>\n            <id>local</id>\n            <activation>\n                <activeByDefault>false</activeByDefault>\n            </activation>\n            <properties>\n                <it.local.mode>true</it.local.mode>\n            </properties>\n            <dependencies>\n                <dependency>\n                    <groupId>com.walmartlabs.concord.server</groupId>\n                    <artifactId>concord-server-impl</artifactId>\n                    <scope>test</scope>\n                </dependency>\n                <dependency>\n                    <groupId>com.walmartlabs.concord.server</groupId>\n                    <artifactId>concord-server</artifactId>\n                    <scope>test</scope>\n                </dependency>\n                <dependency>\n                    <groupId>com.walmartlabs.concord</groupId>\n                    <artifactId>concord-agent</artifactId>\n                    <scope>test</scope>\n                </dependency>\n            </dependencies>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-dependency-plugin</artifactId>\n                        <executions>\n                            <execution>\n                                <id>copy-runner-jar</id>\n                                <phase>process-test-resources</phase>\n                                <goals>\n                                    <goal>copy</goal>\n                                </goals>\n                                <configuration>\n                                    <overWriteIfNewer>true</overWriteIfNewer>\n                                    <artifactItems>\n                                        <artifactItem>\n                                            <groupId>com.walmartlabs.concord.runtime.v1</groupId>\n                                            <artifactId>concord-runtime-impl-v1</artifactId>\n                                            <classifier>jar-with-dependencies</classifier>\n                                            <destFileName>runner-v1.jar</destFileName>\n                                        </artifactItem>\n                                    </artifactItems>\n                                    <outputDirectory>${project.build.directory}</outputDirectory>\n                                </configuration>\n                            </execution>\n                        </executions>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n    </profiles>\n</project>\n"
  },
  {
    "path": "it/runtime-v1/src/test/filtered-resources/testcontainers.properties",
    "content": "ryuk.container.image = ${ryuk.image}"
  },
  {
    "path": "it/runtime-v1/src/test/filtered-resources/version.properties",
    "content": "project.version = ${project.version}"
  },
  {
    "path": "it/runtime-v1/src/test/java/com/walmartlabs/concord/it/runtime/v1/ConcordConfiguration.java",
    "content": "package com.walmartlabs.concord.it.runtime.v1;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport org.testcontainers.images.PullPolicy;\n\npublic final class ConcordConfiguration {\n\n    public static ConcordRule configure() {\n        ConcordRule concord = new ConcordRule()\n                .pathToRunnerV1(\"target/runner-v1.jar\")\n                .pathToRunnerV2(null)\n                .dbImage(System.getProperty(\"db.image\", \"library/postgres:14\"))\n                .serverImage(System.getProperty(\"server.image\", \"walmartlabs/concord-server\"))\n                .agentImage(System.getProperty(\"agent.image\", \"walmartlabs/concord-agent\"))\n                .pullPolicy(PullPolicy.defaultPolicy())\n                .streamServerLogs(true)\n                .streamAgentLogs(true)\n                .useLocalMavenRepository(true)\n                .extraConfigurationSupplier(() -> \"\"\"\n                        concord-server {\n                            queue {\n                                enqueuePollInterval = \"250 milliseconds\"\n                                dispatcher {\n                                    pollDelay = \"250 milliseconds\"\n                                }\n                            }\n                        }\n                        concord-agent {\n                            dependencyResolveTimeout = \"30 seconds\"\n                            logMaxDelay = \"250 milliseconds\"\n                            pollInterval = \"250 milliseconds\"\n                            prefork {\n                                enabled = true\n                            }\n                        }\n                        \"\"\");\n\n        boolean localMode = Boolean.parseBoolean(System.getProperty(\"it.local.mode\"));\n        if (localMode) {\n            concord.mode(ConcordRule.Mode.LOCAL);\n        } else if (Boolean.parseBoolean(System.getProperty(\"it.remote.mode\"))) {\n            concord.mode(ConcordRule.Mode.REMOTE);\n            concord.apiToken(System.getProperty(\"it.remote.token\"));\n            concord.apiBaseUrl(System.getProperty(\"it.remote.baseUrl\"));\n        } else {\n            concord.mode(ConcordRule.Mode.DOCKER);\n        }\n\n        return concord;\n    }\n\n    private ConcordConfiguration() {\n    }\n}\n"
  },
  {
    "path": "it/runtime-v1/src/test/java/com/walmartlabs/concord/it/runtime/v1/ITConstants.java",
    "content": "package com.walmartlabs.concord.it.runtime.v1;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.Properties;\n\npublic final class ITConstants {\n\n    public static final String PROJECT_VERSION;\n    public static final long DEFAULT_TEST_TIMEOUT = 120000;\n\n    static {\n        PROJECT_VERSION = getProperties(\"version.properties\").getProperty(\"project.version\");\n    }\n\n    private static Properties getProperties(String path) {\n        try (InputStream in = ClassLoader.getSystemResourceAsStream(path)) {\n            Properties props = new Properties();\n            props.load(in);\n            return props;\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private ITConstants() {\n    }\n}\n"
  },
  {
    "path": "it/runtime-v1/src/test/java/com/walmartlabs/concord/it/runtime/v1/ProcessIT.java",
    "content": "package com.walmartlabs.concord.it.runtime.v1;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.ConcordProcess;\nimport ca.ibodrov.concord.testcontainers.Payload;\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport com.google.common.base.Charsets;\nimport com.google.common.io.Resources;\nimport com.walmartlabs.concord.client2.FormListEntry;\nimport com.walmartlabs.concord.client2.FormSubmitResponse;\nimport com.walmartlabs.concord.client2.ProcessApi;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.ProcessEntry.StatusEnum;\nimport com.walmartlabs.concord.client2.ProcessEventEntry;\nimport com.walmartlabs.concord.client2.ProcessEventsApi;\nimport com.walmartlabs.concord.client2.ProcessListFilter;\nimport com.walmartlabs.concord.it.common.JGitUtils;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.net.URL;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.TimeUnit;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ITUtils.randomString;\nimport static com.walmartlabs.concord.it.runtime.v1.ITConstants.DEFAULT_TEST_TIMEOUT;\nimport static org.junit.jupiter.api.Assertions.*;\n\n@Timeout(value = DEFAULT_TEST_TIMEOUT, unit = TimeUnit.MILLISECONDS)\npublic class ProcessIT {\n\n    @RegisterExtension\n    public static final ConcordRule concord = ConcordConfiguration.configure();\n\n    @BeforeAll\n    public static void init() {\n        JGitUtils.applyWorkarounds();\n    }\n\n    @Test\n    public void testUploadAndRun() throws Exception {\n        // prepare the payload\n        byte[] archive = archive(ProcessIT.class.getResource(\"example\").toURI());\n\n        // start the process\n        ConcordProcess proc = concord.processes().start(new Payload().archive(archive));\n\n        // wait for completion\n        proc.expectStatus(StatusEnum.FINISHED);\n\n        proc.assertLog(\".*Hello, world.*\");\n        proc.assertLog(\".*Hello, local files!.*\");\n    }\n\n    @Test\n    public void testDefaultEntryPoint() throws Exception {\n        byte[] archive = archive(ProcessIT.class.getResource(\"defaultEntryPoint\").toURI());\n\n        ConcordProcess proc = concord.processes().start(new Payload().archive(archive));\n\n        // wait for completion\n        proc.expectStatus(StatusEnum.FINISHED);\n\n        proc.assertLog(\".*Hello, Concord!.*\");\n    }\n\n    @Test\n    public void testInterpolation() throws Exception {\n        byte[] archive = archive(ProcessIT.class.getResource(\"interpolation\").toURI());\n\n        // ---\n\n        ConcordProcess proc = concord.processes().start(new Payload().archive(archive));\n\n        // ---\n\n        proc.expectStatus(StatusEnum.FINISHED);\n\n        // ---\n\n        proc.assertLog(\".*Hello, world.*\");\n    }\n\n    @Test\n    public void testErrorHandling() throws Exception {\n        byte[] archive = archive(ProcessIT.class.getResource(\"errorHandling\").toURI());\n\n        ConcordProcess proc = concord.processes().start(new Payload().archive(archive));\n\n        proc.expectStatus(StatusEnum.FINISHED);\n\n        proc.assertLog(\".*Kaboom.*\");\n        proc.assertLog(\".*We got:.*java.lang.RuntimeException.*\");\n    }\n\n    @Test\n    public void testStartupProblem() throws Exception {\n        byte[] archive = archive(ProcessIT.class.getResource(\"startupProblem\").toURI());\n\n        ConcordProcess proc = concord.processes().start(new Payload().archive(archive));\n\n        proc.expectStatus(StatusEnum.FAILED);\n\n        proc.assertLogAtLeast(\".*gaaarbage.*\", 1);\n    }\n\n    @Test\n    public void testMultipart() throws Exception {\n        String zVal = \"z\" + randomString();\n        String myFileVal = \"myFile\" + randomString();\n        byte[] archive = archive(ProcessIT.class.getResource(\"multipart\").toURI());\n\n\n        // ---\n\n        Payload payload = new Payload()\n                .archive(archive)\n                .entryPoint(\"main\")\n                .arg(\"z\", zVal)\n                .file(\"myfile.txt\", myFileVal.getBytes());\n\n        ConcordProcess proc = concord.processes().start(payload);\n\n        // ---\n\n        proc.expectStatus(StatusEnum.FINISHED);\n\n        proc.assertLog(\".*x=123.*\");\n        proc.assertLog(\".*y=abc.*\");\n        proc.assertLog(\".*z=\" + zVal + \".*\");\n        proc.assertLog(\".*myfile=\" + myFileVal + \".*\");\n    }\n\n    @Test\n    public void testWorkDir() throws Exception {\n        byte[] archive = archive(ProcessIT.class.getResource(\"workDir\").toURI());\n\n        ConcordProcess proc = concord.processes().start(new Payload().archive(archive));\n\n        proc.expectStatus(StatusEnum.SUSPENDED);\n        proc.assertLog(\".*Hello!\");\n        proc.assertLog(\".*Bye!\");\n\n        // ---\n\n        List<FormListEntry> forms = proc.forms();\n        assertEquals(1, forms.size());\n\n        FormListEntry f = forms.get(0);\n        FormSubmitResponse fsr = proc.submitForm(f.getName(), Collections.singletonMap(\"name\", \"test\"));\n        assertNull(fsr.getErrors());\n\n        // ---\n\n        proc.expectStatus(StatusEnum.FINISHED);\n\n        proc.assertLogAtLeast(\".*Hello!\", 2);\n        proc.assertLogAtLeast(\".*Bye!\", 2);\n    }\n\n    @Test\n    public void testSwitch() throws Exception {\n        byte[] archive = archive(ProcessIT.class.getResource(\"switchCase\").toURI());\n\n        ConcordProcess proc = concord.processes().start(new Payload().archive(archive));\n\n        // ---\n\n        proc.expectStatus(StatusEnum.FINISHED);\n\n        // ---\n\n        proc.assertLog(\".*234234.*\");\n        proc.assertLog(\".*Hello, Concord.*\");\n        proc.assertLog(\".*Bye!.*\");\n    }\n\n    @Test\n    public void testTags() throws Exception {\n        byte[] archive = archive(ProcessIT.class.getResource(\"example\").toURI());\n\n        ConcordProcess parent = concord.processes().start(new Payload().archive(archive));\n\n        // ---\n\n        parent.expectStatus(StatusEnum.FINISHED);\n\n        // ---\n\n        archive = archive(ProcessIT.class.getResource(\"tags\").toURI());\n\n        Payload payload = new Payload()\n                .archive(archive)\n                .parent(parent.instanceId());\n\n        ConcordProcess child = concord.processes().start(payload);\n\n        // ---\n\n        child.expectStatus(StatusEnum.FINISHED);\n\n        // ---\n\n        List<ProcessEntry> l = parent.subprocesses(\"abc\");\n        assertTrue(l.isEmpty());\n\n        l = parent.subprocesses(\"test\");\n        assertEquals(1, l.size());\n\n        ProcessEntry e = l.get(0);\n        assertEquals(child.instanceId(), e.getInstanceId());\n\n        // ---\n\n        l = concord.processes().list(ProcessListFilter.builder().addTags(\"xyz\").build());\n        assertTrue(l.isEmpty());\n\n        l = concord.processes().list(ProcessListFilter.builder().addTags(\"IT\").build());\n        assertEquals(1, l.size());\n\n        e = l.get(0);\n        assertEquals(child.instanceId(), e.getInstanceId());\n    }\n\n    @Test\n    public void testGetProcessForChildIds() throws Exception {\n        byte[] archive = archive(ProcessIT.class.getResource(\"processWithChildren\").toURI());\n\n        ConcordProcess proc = concord.processes().start(new Payload().archive(archive));\n\n        // ---\n\n        proc.expectStatus(StatusEnum.FINISHED);\n\n        assertEquals(3, proc.getEntry(\"childrenIds\").getChildrenIds().size());\n    }\n\n    @Test\n    public void testGetProcessForChildIdsAfterSuspend() throws Exception {\n        byte[] archive = archive(ProcessIT.class.getResource(\"processWithChildrenSuspend\").toURI());\n\n        ConcordProcess proc = concord.processes().start(new Payload().archive(archive));\n\n        // ---\n\n        proc.expectStatus(StatusEnum.FINISHED);\n\n        assertEquals(3, proc.getEntry(\"childrenIds\").getChildrenIds().size());\n    }\n\n    @Test\n    public void testGetProcessForChildIdAfterSuspend() throws Exception {\n        byte[] archive = archive(ProcessIT.class.getResource(\"processWithChildSuspend\").toURI());\n\n        ConcordProcess proc = concord.processes().start(new Payload().archive(archive));\n\n        // ---\n\n        proc.expectStatus(StatusEnum.FINISHED);\n\n        proc.assertLog(\".*Got 1 ids.*\");\n    }\n\n    @Test\n    public void testGetProcessForChildIdAfterSuspendWithoutOut() throws Exception {\n        byte[] archive = archive(ProcessIT.class.getResource(\"processWithChildSuspendWithoutOut\").toURI());\n\n        ConcordProcess proc = concord.processes().start(new Payload().archive(archive));\n\n        // ---\n\n        proc.expectStatus(StatusEnum.FINISHED);\n\n        proc.assertLog(\".*Got 1 ids.*\");\n    }\n\n    @Test\n    public void testKillCascade() throws Exception {\n        byte[] archive = archive(ProcessIT.class.getResource(\"killCascade\").toURI());\n\n        ConcordProcess proc = concord.processes().start(new Payload().archive(archive));\n\n        proc.waitForChildStatus(StatusEnum.ENQUEUED, StatusEnum.PREPARING, StatusEnum.STARTING, StatusEnum.RUNNING);\n\n        proc.killCascade();\n\n        proc.waitForChildStatus(StatusEnum.CANCELLED, StatusEnum.FINISHED, StatusEnum.FAILED);\n\n        List<ProcessEntry> processEntryList = proc.subprocesses();\n        for (ProcessEntry pe : processEntryList) {\n            assertEquals(StatusEnum.CANCELLED, pe.getStatus());\n        }\n    }\n\n    @Test\n    public void testActiveProfiles() throws Exception {\n        byte[] archive = archive(ProcessIT.class.getResource(\"activeProfiles\").toURI());\n\n        Payload payload = new Payload()\n                .archive(archive)\n                .activeProfiles(\"profileA,profileB\");\n\n        ConcordProcess proc = concord.processes().start(payload);\n\n        // ---\n\n        proc.expectStatus(StatusEnum.FINISHED);\n\n        // ---\n\n        proc.assertLog(\".*Hello from A\\\\+B.*\");\n        proc.assertLog(\".*We got \\\\[profileA, profileB].*\");\n    }\n\n    @Test\n    public void testGetProcessErrorMessageFromRuntime() throws Exception {\n        byte[] archive = archive(ProcessIT.class.getResource(\"throwRuntime\").toURI());\n\n        // start the process\n\n        ConcordProcess proc = concord.processes().start(new Payload().archive(archive));\n\n        // wait for completion\n\n        ProcessEntry pe = proc.expectStatus(StatusEnum.FAILED);\n\n        assertProcessErrorMessage(pe, \"BOOOM\");\n    }\n\n    @Test\n    public void testGetProcessErrorMessageFromBpmnError() throws Exception {\n        byte[] archive = archive(ProcessIT.class.getResource(\"throwBpmnError\").toURI());\n\n        // start the process\n\n        ConcordProcess proc = concord.processes().start(new Payload().archive(archive));\n\n        // wait for completion\n\n        ProcessEntry pe = proc.expectStatus(StatusEnum.FAILED);\n\n        assertProcessErrorMessage(pe, \".*myBnpmError.*\");\n    }\n\n    @Test\n    public void testInvalidEntryPointError() throws Exception {\n        byte[] archive = archive(ProcessIT.class.getResource(\"multipart\").toURI());\n\n        // ---\n\n        Payload payload = new Payload()\n                .archive(archive)\n                .entryPoint(\"not-found\");\n\n        ConcordProcess proc = concord.processes().start(payload);\n\n        // wait for completion\n\n        ProcessEntry pe = proc.expectStatus(StatusEnum.FAILED);\n\n        assertProcessErrorMessage(pe, \"Process 'not-found' not found\");\n    }\n\n    @Test\n    public void testFileUploadWithNonRootPath() throws Exception {\n        byte[] archive = archive(ProcessIT.class.getResource(\"fileupload\").toURI());\n\n        // ---\n\n        Payload payload = new Payload()\n                .archive(archive)\n                .file(\"target/file1\", \"test from file\".getBytes());\n\n        // ---\n\n        ConcordProcess proc = concord.processes().start(payload);\n\n        // ---\n\n        proc.expectStatus(StatusEnum.FINISHED);\n\n        // ---\n\n        proc.assertLog(\".*test from file.*\");\n    }\n\n    @Test\n    public void testRunnerLogLevel() throws Exception {\n        byte[] archive = archive(ProcessIT.class.getResource(\"runnerLogLevel\").toURI());\n\n        // start the process\n\n        ConcordProcess proc = concord.processes().start(new Payload().archive(archive));\n\n        // wait for completion\n\n        proc.expectStatus(StatusEnum.FINISHED);\n\n        // check the logs\n\n        proc.assertNoLog(\".*I AM A DEBUG MESSAGE.*\");\n        proc.assertNoLog(\".*I AM AN INFO MESSAGE.*\");\n        proc.assertLog(\".*I AM A WARNING.*\");\n        proc.assertLog(\".*I AM AN ERROR.*\");\n    }\n\n    @Test\n    public void testDisableProcess() throws Exception {\n        byte[] archive = archive(ProcessIT.class.getResource(\"disableProcess\").toURI());\n\n        // ---\n\n        ConcordProcess proc = concord.processes().start(new Payload().archive(archive));\n\n        // ---\n\n        proc.expectStatus(StatusEnum.FINISHED);\n\n        // ---\n\n        ProcessEntry pe = proc.disable();\n\n        assertTrue(pe.getDisabled());\n    }\n\n    @Test\n    public void testCustomJvmArgs() throws Exception {\n        byte[] archive = archive(ProcessIT.class.getResource(\"customJvmArgs\").toURI());\n\n        // ---\n\n        ConcordProcess proc = concord.processes().start(new Payload().archive(archive));\n\n        // ---\n\n        proc.expectStatus(StatusEnum.FINISHED);\n\n        // ---\n\n        proc.assertLog(\".*Got: hello.*\");\n    }\n\n    @Test\n    public void testInterpolateWithVariables() throws Exception {\n        byte[] archive = archive(ProcessIT.class.getResource(\"interpolateWithVars\").toURI());\n\n        // ---\n\n        ConcordProcess proc = concord.processes().start(new Payload().archive(archive));\n\n        // ---\n\n        proc.expectStatus(StatusEnum.FINISHED);\n\n        proc.assertLog(\".*two: 2*\");\n    }\n\n    /**\n     * Verifies that variables changed in runtime are available in onFailure flows.\n     */\n    @Test\n    public void testOnFailureVariables() throws Exception {\n        ConcordProcess proc = concord.processes().start(new Payload()\n                .archive(resource(\"onFailureVars\")));\n\n        proc.expectStatus(StatusEnum.FAILED);\n\n\n        // wait for the onFailure process\n        ConcordProcess onFailureProc;\n        while (true) {\n            List<ProcessEntry> l = proc.subprocesses();\n            if (!l.isEmpty()) {\n                onFailureProc = concord.processes().get(l.get(0).getInstanceId());\n                break;\n            }\n\n            Thread.sleep(1000);\n        }\n\n        onFailureProc.expectStatus(StatusEnum.FINISHED);\n        onFailureProc.assertLog(\".*I've got xyz.*\");\n        onFailureProc.assertLog(\".*Last error was:.*Boom!.*\");\n    }\n\n    @Test\n    public void testOnFailureVariables2() throws Exception {\n        ConcordProcess proc = concord.processes().start(new Payload()\n                .archive(resource(\"onFailureVars2\")));\n\n        proc.expectStatus(StatusEnum.FAILED);\n\n        // wait for the onFailure process\n        ConcordProcess onFailureProc;\n        while (true) {\n            List<ProcessEntry> l = proc.subprocesses();\n            if (!l.isEmpty()) {\n                onFailureProc = concord.processes().get(l.get(0).getInstanceId());\n                break;\n            }\n\n            Thread.sleep(1000);\n        }\n\n        onFailureProc.expectStatus(StatusEnum.FINISHED);\n        onFailureProc.assertLog(\".*abc: Hello.*\");\n        onFailureProc.assertLog(\".*Last error was:.*PropertyNotFoundException.*\");\n    }\n\n    @Test\n    public void testYamlRootFile() throws Exception {\n        ConcordProcess proc = concord.processes().start(new Payload()\n                .archive(resource(\"yamlRootFile\")));\n\n        proc.expectStatus(StatusEnum.FINISHED);\n        proc.assertLog(\".*Hello, Concord!*\");\n    }\n\n    @Test\n    public void testMetadataWithWithItems() throws Exception {\n        ConcordProcess proc = concord.processes().start(new Payload()\n                .archive(resource(\"processMetadataWithItems\")));\n\n        ProcessEntry pe = proc.expectStatus(StatusEnum.FINISHED);\n        assertNotNull(pe.getMeta());\n        assertEquals(\"c\", pe.getMeta().get(\"var\"));\n    }\n\n    @Test\n    public void testMetadataUpdateOnlyOnEnd() throws Exception {\n        // Hard to validate the exact number of times POST /meta is called.\n        // This should only call it once, while testMetadataWithWithItems calls it 3 times.\n        // This at least validates the runner events config option is valid\n        // and doesn't break meta reporting\n        ConcordProcess proc = concord.processes().start(new Payload()\n                .archive(resource(\"processMetadataAfterExecution\")));\n\n        ProcessEntry pe = proc.expectStatus(StatusEnum.FINISHED);\n        assertNotNull(pe.getMeta());\n        assertEquals(\"c\", pe.getMeta().get(\"var\"));\n    }\n\n    @Test\n    public void testEmptyExclusiveGroup() throws Exception {\n        ConcordProcess proc = concord.processes().start(new Payload()\n                .archive(resource(\"emptyExclusiveGroup\")));\n\n        proc.expectStatus(StatusEnum.FAILED);\n        proc.assertLog(\".*Invalid exclusive mode.*\");\n    }\n\n    /**\n     * Tests process event batch flushing when a long-running task executes.\n     */\n    @Test\n    public void testEventBatchingShortTimer() throws Exception {\n        Payload payload = new Payload()\n                .activeProfiles(\"shortFlush\")\n                .archive(resource(\"eventBatchingTimer\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        ProcessEntry pe = proc.expectStatus(StatusEnum.RUNNING);\n\n        // let it run at least long enough to report an event batch (1-second interval)\n        Thread.sleep(1_500);\n\n        ProcessEventsApi processEventsApi = new ProcessEventsApi(concord.apiClient());\n\n        // At this point the process is still executing the sleep task.\n        // We set a 1-second batch duration, so we can not expect a batch to have\n        // been reported even though the max batch size (100) was not met.\n\n        // ---\n        List<ProcessEventEntry> events = processEventsApi.listProcessEvents(proc.instanceId(), \"ELEMENT\", null, null, null, \"pre\", null, null);\n\n        // clean up\n        new ProcessApi(concord.apiClient()).kill(pe.getInstanceId());\n\n        // ---\n        assertNotNull(events);\n        assertFalse(events.isEmpty());\n        assertEquals(1, events.size());\n\n        ProcessEventEntry sleepEvent = events.get(0);\n\n        assertEquals(\"sleep\", sleepEvent.getData().get(\"name\"));\n    }\n\n    /**\n     * Demonstrates what happens if process event batching flush timer is too long,\n     * or effectively doesn't exist.\n     */\n    @Test\n    public void testEventBatchingLongTimer() throws Exception {\n        Payload payload = new Payload()\n                .activeProfiles(\"longFlush\")\n                .archive(resource(\"eventBatchingTimer\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        ProcessEntry pe = proc.expectStatus(StatusEnum.RUNNING);\n\n        // let it run long enough to prove events aren't going to update any time soon\n        Thread.sleep(1_500);\n\n        ProcessEventsApi processEventsApi = new ProcessEventsApi(concord.apiClient());\n\n        // ---\n        List<ProcessEventEntry> events = processEventsApi.listProcessEvents(proc.instanceId(), \"ELEMENT\", null, null, null, \"pre\", null, null);\n        assertNotNull(events);\n        // No events because batch is still waiting to get large enough to report\n        assertTrue(events.isEmpty());\n\n        // clean up\n        new ProcessApi(concord.apiClient()).kill(pe.getInstanceId());\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static void assertProcessErrorMessage(ProcessEntry p, String expected) {\n        assertNotNull(p);\n\n        Map<String, Object> meta = p.getMeta();\n        assertNotNull(meta);\n\n        Map<String, Object> out = (Map<String, Object>) meta.get(\"out\");\n        assertNotNull(out);\n\n        Map<String, Object> error = (Map<String, Object>) out.get(Constants.Context.LAST_ERROR_KEY);\n        assertNotNull(error);\n\n        assertTrue(error.get(\"message\").toString().matches(expected));\n    }\n\n    private static URI resource(String name) throws URISyntaxException {\n        return ProcessIT.class.getResource(name).toURI();\n    }\n\n    private static String resourceToString(URL resource) throws IOException {\n        return Resources.toString(resource, Charsets.UTF_8);\n    }\n}\n"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/activeProfiles/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Hello from ${x}+${y}\"\n  - log: \"We got ${processInfo.activeProfiles}\"\n\nprofiles:\n  profileA:\n    configuration:\n      arguments:\n        x: \"A\"\n        y: \"oops\"\n  profileB:\n    configuration:\n      arguments:\n        y: \"B\"\n"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/customJvmArgs/concord.yml",
    "content": "configuration:\n  requirements:\n    jvm:\n      extraArgs:\n        - \"-DcustomProp=hello\"\n\nflows:\n  default:\n    - script: js\n      body: |\n        var System = Java.type('java.lang.System');\n        print(\"Got: \" + System.getProperty(\"customProp\"));\n"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/defaultEntryPoint/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Hello, Concord!\""
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/disableProcess/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Test disable process!\""
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/emptyExclusiveGroup/concord.yml",
    "content": "configuration:\n  exclusive:\n    group: \"\"\n    mode: wait\n\nflows:\n  default:\n    - log: \"Hello\"\n"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/errorHandling/_main.json",
    "content": "{\n  \"entryPoint\": \"main\"\n}"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/errorHandling/processes/test.yml",
    "content": "main:\n- expr: ${misc.throwBpmnError('boom')}\n  error:\n  - log: \"Ka${lastError.errorRef}\"\n- expr: ${misc.throwRuntimeException('DEAD')}\n  error:\n  - log: \"We got: ${lastError.cause}\"\n"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/eventBatchingTimer/concord.yml",
    "content": "profiles:\n  shortFlush:\n    configuration:\n      runner:\n        events:\n          batchSize: 100\n          batchFlushInterval: 1\n\n  longFlush:\n    configuration:\n      runner:\n        events:\n          batchSize: 100\n          batchFlushInterval: 120\n\nflows:\n  default:\n    - task: sleep\n      in:\n        duration: 120\n"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/example/_main.json",
    "content": "{\n  \"entryPoint\": \"main\",\n  \"arguments\": {\n    \"myName\": \"world\"\n  }\n}"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/example/processes/test.yml",
    "content": "main:\n- expr: ${log.info(\"test\", \"Hello, \".concat(myName))}\n- expr: ${log.info(\"test\", resource.asString(workDir.concat(\"/something.txt\")))}"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/example/something.txt",
    "content": "Hello, local files!"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/fileupload/concord.yml",
    "content": "flows:\n  default:\n  - log: \"${resource.asString('target/file1')}\"\n"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/interpolateWithVars/concord.yml",
    "content": "flows:\n  default:\n    - set:\n        two: \"boo\"\n        vars:\n          two: 2\n\n    - log: \"two: ${context.interpolate('${two}', vars)}\"\n"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/interpolation/_main.json",
    "content": "{\n  \"entryPoint\": \"main\",\n  \"arguments\": {\n    \"myName\": \"world\",\n    \"myRealName\": \"${myName}\"\n  }\n}"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/interpolation/processes/test.yml",
    "content": "main:\n- ${execution.setVariable(\"name\", myRealName)}\n- log: \"Hello, ${name}\"\n"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/killCascade/concord.yml",
    "content": "flows:\n  default:\n\n  - task: concord\n    in:\n      action: fork\n      disableOnCancel: true\n      forks:\n      - entryPoint: aJob\n        arguments:\n          color: \"red\"\n\n  - \"${concord.waitForCompletion(jobs)}\"\n\n  aJob:\n  - log: \"FORK (${color}) starting...\"\n  - ${sleep.ms(10000)}\n  - log: \"Done! \""
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/multipart/.concord.yml",
    "content": "flows:\n  main:\n  - log: \"x=${x}\"\n  - log: \"y=${y}\"\n  - log: \"z=${z}\"\n  - log: \"myfile=${resource.asString('myfile.txt')}\"\n\nvariables:\n  arguments:\n    x: 123"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/multipart/_main.json",
    "content": "{\n  \"arguments\": {\n    \"y\": \"abc\",\n    \"z\": \"n/a\"\n  }\n}"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/onFailureVars/concord.yml",
    "content": "flows:\n  default:\n    - set:\n        myVar: \"xyz\"\n\n    - log: \"The default flow got ${myVar}\"\n\n    - throw: \"Boom!\"\n\n  onFailure:\n    - log: \"I've got ${myVar}\"\n    - log: \"Last error was: ${lastError}\"\n\nconfiguration:\n  arguments:\n    myVar: \"abc\"\n"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/onFailureVars2/concord.yml",
    "content": "flows:\n  default:\n    - set:\n        abc: \"Hello!\"\n\n    - if: \"${xyz.success}\" # xyz is undefined\n      then:\n        - log: \"shouldn't work anyway\"\n\n  onFailure:\n    - log: \"abc: ${abc}\"\n    - log: \"Last error was: ${lastError}\""
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/processMetadataAfterExecution/concord.yml",
    "content": "configuration:\n  runner:\n    events:\n      updateMetaOnAllEvents: false\n  meta:\n    var: value\n\nflows:\n  default:\n    - call: anotherFlow\n      withItems:\n        - a\n        - b\n        - c\n\n    - log: ${var}\n\n  anotherFlow:\n    - set:\n        var: ${item}\n"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/processMetadataWithItems/concord.yml",
    "content": "configuration:\n  meta:\n    var: value\n\nflows:\n  default:\n    - call: anotherFlow\n      withItems:\n        - a\n        - b\n        - c\n\n    - log: ${var}\n\n  anotherFlow:\n    - set:\n        var: ${item}\n"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/processWithChildSuspend/concord.yml",
    "content": "flows:\n  default:\n    - task: concord\n      in:\n        action: start\n        payload: myPayload\n        sync: true\n        suspend: true\n        # disable the `onCancel` handler, because it's going to handle\n        # the parent's cancellation only\n        disableOnCancel: true\n        entryPoint: aJob\n        arguments:\n          color: \"red\"\n\n      # out variable \"myJobs\" will contain a list of process IDs\n      out:\n        myJobs: ${jobs}\n\n    - log: \"Got ${myJobs.size()} ids\"\n"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/processWithChildSuspend/myPayload/concord.yml",
    "content": "flows:\n  aJob:\n    - log: \"FORK (${color}) starting...\"\n    - log: \"...done!\""
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/processWithChildSuspendWithoutOut/concord.yml",
    "content": "flows:\n  default:\n    - task: concord\n      in:\n        action: start\n        payload: myPayload\n        sync: true\n        suspend: true\n        # disable the `onCancel` handler, because it's going to handle\n        # the parent's cancellation only\n        disableOnCancel: true\n        entryPoint: aJob\n        arguments:\n          color: \"red\"\n\n    - log: \"Got ${jobs.size()} ids\"\n"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/processWithChildSuspendWithoutOut/myPayload/concord.yml",
    "content": "flows:\n  aJob:\n    - log: \"FORK (${color}) starting...\"\n    - log: \"...done!\""
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/processWithChildren/concord.yml",
    "content": "flows:\n  default:\n\n\n  - task: concord\n    in:\n      action: fork\n      tags: forkJoinChild\n      # disable the `onCancel` handler, because it's going to handle\n      # the parent's cancellation only\n      disableOnCancel: true\n      forks:\n      # spawn multiple jobs with different parameters\n      - entryPoint: aJob\n        arguments:\n          color: \"red\"\n      - entryPoint: aJob\n        arguments:\n          color: \"green\"\n      - entryPoint: aJob\n        arguments:\n            color: \"blue\"\n\n\n    # out variable \"myJobs\" will contain a list of process IDs\n    out:\n      myJobs: ${jobs}\n\n  - log: \"Done! Status of the jobs: ${concord.waitForCompletion(myJobs)}\"\n\n  aJob:\n  - log: \"FORK (${color}) starting...\"\n  - log: \"...done!\""
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/processWithChildrenSuspend/concord.yml",
    "content": "flows:\n  default:\n\n\n  - task: concord\n    in:\n      action: fork\n      tags: forkJoinChild\n      sync: true\n      suspend: true\n      # disable the `onCancel` handler, because it's going to handle\n      # the parent's cancellation only\n      disableOnCancel: true\n      forks:\n      # spawn multiple jobs with different parameters\n      - entryPoint: aJob\n        arguments:\n          color: \"red\"\n      - entryPoint: aJob\n        arguments:\n          color: \"green\"\n      - entryPoint: aJob\n        arguments:\n            color: \"blue\"\n\n\n    # out variable \"myJobs\" will contain a list of process IDs\n    out:\n      myJobs: ${jobs}\n\n  - log: \"Done! Status of the jobs: ${concord.waitForCompletion(myJobs)}\"\n\n  aJob:\n  - log: \"FORK (${color}) starting...\"\n  - log: \"...done!\""
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/runnerLogLevel/concord.yml",
    "content": "configuration:\n  runner:\n    logLevel: \"WARN\"\n\nflows:\n  default:\n    - \"${log.debug('I AM A DEBUG MESSAGE')}\" # won't show\n    - \"${log.info('I AM AN INFO MESSAGE')}\" # won't show\n    - \"${log.warn('I AM A WARNING')}\" # will be printed out\n    - \"${log.error('I AM AN ERROR')}\" # will be printed out\n"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/startupProblem/_main.json",
    "content": "{\n  \"entryPoint\": \"main\",\n  \"dependencies\": [\"gaaarbage\"]\n}"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/startupProblem/processes/test.yml",
    "content": "main:\n- log: this is never going to run\n"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/switchCase/concord.yml",
    "content": "flows:\n  main:\n  - switch: ${myVar}\n    false:\n      - log: \"123123\"\n    default:\n      - log: \"234234\"\n\n  - switch: ${myVar}\n    true:\n      - log: \"Hello, Concord\"\n\n  - log: \"Bye!\"\n\nvariables:\n  entryPoint: main\n  arguments:\n    myVar: true"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/tags/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Hello!\"\n\nconfiguration:\n  tags:\n    - \"test\"\n    - \"IT\"\n    - \"tagging\""
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/throwBpmnError/concord.yml",
    "content": "flows:\n  default:\n  - ${misc.throwBpmnError('myBnpmError')}\n"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/throwRuntime/concord.yml",
    "content": "flows:\n  default:\n  - ${misc.throwRuntimeException(\"BOOOM\")}\n"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/timeout/_main.json",
    "content": "{\n  \"entryPoint\": \"main\"\n}"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/timeout/processes/test.yml",
    "content": "main:\n- expr: ${sleep.ms(15000)}\n"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/workDir/.concord.yml",
    "content": "flows:\n  main:\n  - log: ${resource.asString(workDir += '/test1.txt')}\n  - log: ${resource.asString(workDir += '/test2.txt')}\n  - form: myForm\n  - log: ${resource.asString(workDir += '/test1.txt')}\n  - log: ${resource.asString(workDir += '/test2.txt')}\n\nforms:\n  myForm:\n  - name: {type: \"string\"}\n\nvariables:\n  entryPoint: \"main\""
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/workDir/test1.txt",
    "content": "Hello!"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/workDir/test2.txt",
    "content": "Bye!"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/yamlRootFile/concord/extra.yaml",
    "content": "configuration:\n  arguments:\n    name: \"Concord\"\n"
  },
  {
    "path": "it/runtime-v1/src/test/resources/com/walmartlabs/concord/it/runtime/v1/yamlRootFile/concord.yaml",
    "content": "flows:\n  default:\n    - log: \"Hello, ${name}!\"\n"
  },
  {
    "path": "it/runtime-v1/src/test/resources/logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss} [%-5level] %logger{12} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "it/runtime-v2/README.md",
    "content": "# Integration Tests for Concord Runtime v2"
  },
  {
    "path": "it/runtime-v2/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.it</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-runtime-v2-it</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <properties>\n        <it.local.mode>false</it.local.mode>\n        <server.image>walmartlabs/concord-server</server.image>\n        <agent.image>walmartlabs/concord-agent</agent.image>\n        <ryuk.image>testcontainers/ryuk:0.6.0</ryuk.image>\n        <sshd.image>testcontainers/sshd:1.2.0</sshd.image>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-client2</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.it</groupId>\n            <artifactId>concord-common-it</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-compress</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.icegreen</groupId>\n            <artifactId>greenmail</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.icegreen</groupId>\n            <artifactId>greenmail-junit5</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.guava</groupId>\n            <artifactId>guava</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>ca.ibodrov.concord</groupId>\n            <artifactId>testcontainers-concord-core</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>ca.ibodrov.concord</groupId>\n            <artifactId>testcontainers-concord-junit5</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.testcontainers</groupId>\n            <artifactId>testcontainers</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.sun.mail</groupId>\n            <artifactId>javax.mail</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.wiremock</groupId>\n            <artifactId>wiremock-jetty12</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>ch.qos.logback</groupId>\n            <artifactId>logback-classic</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <!-- to test the scripting feature -->\n        <dependency>\n            <groupId>org.apache.groovy</groupId>\n            <artifactId>groovy-all</artifactId>\n            <type>pom</type>\n            <scope>test</scope>\n            <exclusions>\n                <exclusion>\n                    <groupId>org.apache.groovy</groupId>\n                    <artifactId>groovy-test</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>org.apache.groovy</groupId>\n                    <artifactId>groovy-test-junit5</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n\n        <!-- Node Roster stuff -->\n        <dependency>\n            <groupId>com.walmartlabs.concord.server.plugins.noderoster</groupId>\n            <artifactId>concord-noderoster-plugin-client2</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <resources>\n            <resource>\n                <filtering>false</filtering>\n                <directory>${project.basedir}/src/test/resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n            <resource>\n                <filtering>true</filtering>\n                <directory>${project.basedir}/src/test/filtered-resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n        </resources>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-failsafe-plugin</artifactId>\n                <configuration>\n                    <skipTests>${skip.it.tests}</skipTests>\n                    <systemProperties>\n                        <it.local.mode>${it.local.mode}</it.local.mode>\n                        <db.image>${db.image}</db.image>\n                        <server.image>${server.image}</server.image>\n                        <agent.image>${agent.image}</agent.image>\n                    </systemProperties>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n    <profiles>\n        <profile>\n            <id>local</id>\n            <activation>\n                <activeByDefault>false</activeByDefault>\n            </activation>\n            <properties>\n                <it.local.mode>true</it.local.mode>\n            </properties>\n            <dependencies>\n                <dependency>\n                    <groupId>com.walmartlabs.concord.server</groupId>\n                    <artifactId>concord-server-impl</artifactId>\n                    <scope>test</scope>\n                </dependency>\n                <dependency>\n                    <groupId>com.walmartlabs.concord.server</groupId>\n                    <artifactId>concord-server</artifactId>\n                    <scope>test</scope>\n                </dependency>\n                <dependency>\n                    <groupId>com.walmartlabs.concord</groupId>\n                    <artifactId>concord-agent</artifactId>\n                    <scope>test</scope>\n                </dependency>\n            </dependencies>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-dependency-plugin</artifactId>\n                        <executions>\n                            <execution>\n                                <id>copy-runner-jar</id>\n                                <phase>process-test-resources</phase>\n                                <goals>\n                                    <goal>copy</goal>\n                                </goals>\n                                <configuration>\n                                    <overWriteIfNewer>true</overWriteIfNewer>\n                                    <artifactItems>\n                                        <artifactItem>\n                                            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n                                            <artifactId>concord-runner-v2</artifactId>\n                                            <classifier>jar-with-dependencies</classifier>\n                                            <destFileName>runner-v2.jar</destFileName>\n                                        </artifactItem>\n                                    </artifactItems>\n                                    <outputDirectory>${project.build.directory}</outputDirectory>\n                                </configuration>\n                            </execution>\n                        </executions>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n    </profiles>\n</project>\n"
  },
  {
    "path": "it/runtime-v2/src/test/filtered-resources/testcontainers.properties",
    "content": "ryuk.container.image=${ryuk.image}\nsshd.container.image=${sshd.image}"
  },
  {
    "path": "it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/AbstractTest.java",
    "content": "package com.walmartlabs.concord.it.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.ConcordProcess;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.it.common.JGitUtils;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Timeout;\n\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.net.URL;\nimport java.util.concurrent.TimeUnit;\n\nimport static com.walmartlabs.concord.it.runtime.v2.ITConstants.DEFAULT_TEST_TIMEOUT;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n@Timeout(value = DEFAULT_TEST_TIMEOUT, unit = TimeUnit.MILLISECONDS)\npublic abstract class AbstractTest {\n\n    @BeforeAll\n    public static void init() {\n        JGitUtils.applyWorkarounds();\n    }\n\n    protected static ProcessEntry expectStatus(ConcordProcess proc, ProcessEntry.StatusEnum status, ProcessEntry.StatusEnum... more) throws ApiException {\n        try {\n            return proc.expectStatus(status, more);\n        } catch (Exception e) {\n            System.out.println(\"Process log:\");\n            System.out.println(new String(proc.getLog()));\n            throw e;\n        }\n    }\n\n    protected URI resource(String name) throws URISyntaxException {\n        URL url = getClass().getResource(name);\n        assertNotNull(url, \"can't find '\" + name + \"'\");\n        return url.toURI();\n    }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/ConcordConfiguration.java",
    "content": "package com.walmartlabs.concord.it.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport org.testcontainers.images.PullPolicy;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.security.KeyPair;\nimport java.security.KeyPairGenerator;\nimport java.util.Base64;\n\npublic final class ConcordConfiguration {\n\n    private static final String DEPENDENCY_RESOLVE_TIMEOUT = System.getProperty(\"it.runtime.v2.dependencyResolveTimeout\", \"2 minutes\");\n\n    private static final Path sharedDir = Paths.get(System.getProperty(\"java.io.tmpdir\")).resolve(\"concord-it\");\n\n    public static Path sharedDir() {\n        return sharedDir;\n    }\n\n    static {\n        if (Files.notExists(sharedDir)) {\n            try {\n                Files.createDirectories(sharedDir);\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        writePrivateKey(sharedDir.resolve(\"signing.pem\"));\n    }\n\n    public static ConcordRule configure() {\n        ConcordRule concord = new ConcordRule()\n                .pathToRunnerV1(null)\n                .pathToRunnerV2(\"target/runner-v2.jar\")\n                .dbImage(System.getProperty(\"db.image\", \"library/postgres:14\"))\n                .serverImage(System.getProperty(\"server.image\", \"walmartlabs/concord-server\"))\n                .agentImage(System.getProperty(\"agent.image\", \"walmartlabs/concord-agent\"))\n                .pullPolicy(PullPolicy.defaultPolicy())\n                .streamServerLogs(true)\n                .streamAgentLogs(true)\n                .sharedContainerDir(sharedDir)\n                .useLocalMavenRepository(true)\n                .extraConfigurationSupplier(() -> \"\"\"\n                    concord-server {\n                        queue {\n                            enqueuePollInterval = \"250 milliseconds\"\n                            dispatcher {\n                                pollDelay = \"250 milliseconds\"\n                            }\n                        }\n                        process {\n                            signingKeyPath = \"%%sharedDir%%/signing.pem\"\n                        }\n                    }\n                    concord-agent {\n                        dependencyResolveTimeout = \"%%dependencyResolveTimeout%%\"\n                        logMaxDelay = \"250 milliseconds\"\n                        pollInterval = \"250 milliseconds\"\n                        prefork {\n                            enabled = true\n                        }\n                    }\n                    \"\"\"\n                        .replace(\"%%sharedDir%%\", sharedDir().toString())\n                        .replace(\"%%dependencyResolveTimeout%%\", DEPENDENCY_RESOLVE_TIMEOUT));\n\n        boolean localMode = Boolean.parseBoolean(System.getProperty(\"it.local.mode\"));\n        if (localMode) {\n            concord.mode(ConcordRule.Mode.LOCAL);\n        } else {\n            boolean remoteMode = Boolean.parseBoolean(System.getProperty(\"it.remote.mode\"));\n            if (remoteMode) {\n                concord.mode(ConcordRule.Mode.REMOTE);\n                concord.apiToken(System.getProperty(\"it.remote.token\"));\n                concord.apiBaseUrl(System.getProperty(\"it.remote.baseUrl\"));\n            }\n        }\n\n        return concord;\n    }\n\n    // TODO: move to testcontainers\n    public static String getServerUrlForAgent(ConcordRule concord) {\n        switch (concord.mode()) {\n            case LOCAL:\n                return \"http://localhost:8001\";\n            case REMOTE:\n                return System.getProperty(\"it.remote.baseUrl\");\n            case DOCKER:\n                return \"http://server:8001\";\n            default:\n                throw new IllegalArgumentException(\"Unknown mode: \" + concord.mode());\n        }\n    }\n\n    private static Path writePrivateKey(Path targetFile) {\n        try {\n            return Files.writeString(targetFile, generatePkcs8PemPrivateKey());\n        } catch (Exception e) {\n            throw new IllegalStateException(\"Error writing server username signing key.\", e);\n        }\n    }\n\n    private static String generatePkcs8PemPrivateKey() throws Exception {\n        KeyPairGenerator keyGen = KeyPairGenerator.getInstance(\"RSA\");\n        keyGen.initialize(2048);\n        KeyPair pair = keyGen.generateKeyPair();\n        byte[] pkcs8 = pair.getPrivate().getEncoded();\n        String base64 = Base64.getMimeEncoder(64, \"\\n\".getBytes()).encodeToString(pkcs8);\n        return \"-----BEGIN PRIVATE KEY-----\\n\" + base64 + \"\\n-----END PRIVATE KEY-----\";\n    }\n\n    private ConcordConfiguration() {\n    }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/ConcordTaskIT.java",
    "content": "package com.walmartlabs.concord.it.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.ConcordProcess;\nimport ca.ibodrov.concord.testcontainers.Payload;\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport java.util.Base64;\nimport java.util.List;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.randomString;\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ConcordTaskIT extends AbstractTest {\n\n    @RegisterExtension\n    public static final ConcordRule concord = ConcordConfiguration.configure();\n\n    /**\n     * Test for concord/project-task\n     */\n    @Test\n    public void testCreateProject() throws Exception {\n        String projectName = \"project_\" + randomString();\n\n        Payload payload = new Payload()\n                .archive(resource(\"concord/projectTask\"))\n                .arg(\"newProjectName\", projectName);\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n        proc.assertLog(\".*Done!.*\");\n    }\n\n    /**\n     * start process with api key\n     */\n    @Test\n    public void testExternalApiToken() throws Exception {\n        String username = \"user_\" + randomString();\n\n        UsersApi usersApi = new UsersApi(concord.apiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(username)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeysApi = new ApiKeysApi(concord.apiClient());\n        CreateApiKeyResponse cakr = apiKeysApi.createUserApiKey(new CreateApiKeyRequest()\n                .username(username));\n\n        // ---\n        Payload payload = new Payload()\n                .archive(resource(\"concord/concordTaskApiKey\"))\n                .arg(\"myApiKey\", cakr.getKey());\n\n        ConcordProcess proc = concord.processes().start(payload);\n\n        // ---\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n        proc.assertLog(\".*Hello, Concord!. From: .*\" + username + \".*\");\n    }\n\n    @Test\n    public void testSuspendParentProcess() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"concord/concordTaskSuspendParentProcess\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n        proc.assertLog(\".*Hello, Concord!.*\");\n    }\n\n    @Test\n    public void testForkWithArguments() throws Exception {\n        String orgName = \"org_\" + randomString();\n        concord.organizations().create(orgName);\n\n        String projectName = \"project_\" + randomString();\n        concord.projects().create(orgName, projectName);\n\n        Payload payload = new Payload()\n                .archive(resource(\"concord/concordTaskForkWithArguments\"))\n                .org(orgName)\n                .project(projectName);\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n        ProcessEntry processEntry = proc.getEntry(\"childrenIds\");\n\n        // ---\n        assertEquals(1, processEntry.getChildrenIds().size());\n\n        ConcordProcess child = concord.processes().get(processEntry.getChildrenIds().iterator().next());\n\n        // ---\n        assertNotNull(child);\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, child.getEntry().getStatus());\n\n        // ---\n\n        child.assertLog(\".*Hello from a subprocess.*\");\n        child.assertLog(\".*Concord Fork Process 123.*\");\n    }\n\n    @Test\n    public void testForkSuspend() throws Exception {\n        String nameVar = \"name_\" + randomString();\n\n        String orgName = \"org_\" + randomString();\n        concord.organizations().create(orgName);\n\n        String projectName = \"project_\" + randomString();\n        concord.projects().create(orgName, projectName);\n\n        Payload payload = new Payload()\n                .archive(resource(\"concord/concordTaskForkSuspend\"))\n                .org(orgName)\n                .project(projectName)\n                .arg(\"name\", nameVar);\n\n        ConcordProcess proc = concord.processes().start(payload);\n\n        // ---\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n\n        proc.assertLog(\".*\\\\{varFromFork=Hello, \" + nameVar + \"}.*\");\n        proc.assertLog(\".*\\\\{varFromFork=Bye, \" + nameVar + \"}.*\");\n    }\n\n    @Test\n    public void testSubprocessIgnoreFail() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"concord/concordSubIgnoreFail\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n\n        // ---\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n        proc.assertLog(\".*Done!.*\");\n    }\n\n    @Test\n    public void testOutVarsNotFound() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"concord/concordOutVars\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n\n        // ---\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n        proc.assertLog(\".*Done!.*\");\n    }\n\n    /**\n     * Test for concord/repositoryRefresh-task\n     */\n    @Test\n    public void testRepositoryRefresh() throws Exception {\n\n        Payload payload = new Payload()\n                .archive(resource(\"concord/repositoryRefreshTask\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n        proc.assertLog(\".*Done!.*\");\n    }\n\n    @Test\n    public void testDryRunForChildProcess() throws Exception {\n        Payload payload = new Payload()\n                .parameter(Constants.Request.DRY_RUN_MODE_KEY, true)\n                .archive(resource(\"concord/concordSubDryRun\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n        proc.assertLog(\".*Done!.*\");\n    }\n\n    @Test\n    public void testCreateApiKey() throws Exception {\n        String apiKeyName1 = \"test1_\" + randomString();\n        String apiKeyName2 = \"test2_\" + randomString();\n        String apiKeyValue2 = Base64.getEncoder().encodeToString((\"foo_\" + randomString()).getBytes(UTF_8));\n\n        Payload payload = new Payload()\n                .archive(resource(\"concord/createApiKey\"))\n                .arg(\"apiKeyName1\", apiKeyName1)\n                .arg(\"apiKeyName2\", apiKeyName2)\n                .arg(\"apiKeyValue2\", apiKeyValue2);\n\n        // ---\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n\n        proc.assertLog(\".*result1=.*\");\n        proc.assertNoLog(\".*result2=.*\");\n        proc.assertLog(\".*error=.*\");\n        proc.assertLog(\".*result3=.*\");\n        proc.assertLog(\".*result4=.*key=\" + apiKeyValue2 + \".*\");\n\n        // ---\n\n        UUID adminId = UUID.fromString(\"230c5c9c-d9a7-11e6-bcfd-bb681c07b26c\");\n\n        ApiKeysApi apiKeysApi = new ApiKeysApi(concord.apiClient());\n        List<ApiKeyEntry> apiKeys = apiKeysApi.listUserApiKeys(adminId);\n        assertTrue(apiKeys.stream().anyMatch(k -> k.getName().equals(apiKeyName1)));\n        assertTrue(apiKeys.stream().anyMatch(k -> k.getName().equals(apiKeyName2)));\n\n        int apiKeyCount = apiKeys.size();\n\n        // ---\n\n        apiKeysApi = new ApiKeysApi(concord.apiClient().setApiKey(apiKeyValue2));\n        apiKeys = apiKeysApi.listUserApiKeys(adminId);\n        assertEquals(apiKeyCount, apiKeys.size());\n    }\n\n    @Test\n    public void testCreateOrUpdateApiKey() throws Exception {\n        String username = \"user_\" + randomString();\n\n        UsersApi usersApi = new UsersApi(concord.apiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(username)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        // --\n\n        String apiKeyValue = Base64.getEncoder().encodeToString((\"foo_\" + randomString()).getBytes(UTF_8));\n\n        Payload payload = new Payload()\n                .archive(resource(\"concord/createOrUpdateApiKey\"))\n                .arg(\"apiKeyValue\", apiKeyValue)\n                .arg(\"apiKeyUsername\", username);\n\n        // ---\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n\n        proc.assertLog(\".*result1=.*\");\n        proc.assertLog(\".*result2=.*\");\n        proc.assertLog(\".*result3=.*\");\n\n        // ---\n\n        ApiKeysApi apiKeysApi = new ApiKeysApi(concord.apiClient().setApiKey(apiKeyValue));\n        List<ApiKeyEntry> apiKeys = apiKeysApi.listUserApiKeys(null);\n        assertEquals(1, apiKeys.size());\n    }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/CryptoIT.java",
    "content": "package com.walmartlabs.concord.it.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.ConcordProcess;\nimport ca.ibodrov.concord.testcontainers.NewSecretQuery;\nimport ca.ibodrov.concord.testcontainers.Payload;\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport java.util.Collections;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.randomPwd;\nimport static com.walmartlabs.concord.it.common.ITUtils.randomString;\n\npublic class CryptoIT extends AbstractTest {\n\n    @RegisterExtension\n    public static final ConcordRule concord = ConcordConfiguration.configure();\n\n    /**\n     * Tests various methods of the 'crypto' plugin.\n     */\n    @Test\n    public void test() throws Exception {\n        ApiClient apiClient = concord.apiClient();\n\n        String orgName = \"org_\" + randomString();\n        String projectName = \"project_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(apiClient);\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        ProjectsApi projectsApi = new ProjectsApi(apiClient);\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.OWNERS));\n\n        // ---\n\n        String mySecretPwd = \"pwd_\" + randomPwd();\n\n        String myStringSecretName = \"secret_\" + randomString();\n        String myStringSecretValue = \"value_\" + randomString();\n        concord.secrets().createSecret(NewSecretQuery.builder()\n                        .org(orgName)\n                        .name(myStringSecretName)\n                        .storePassword(mySecretPwd)\n                        .build(),\n                myStringSecretValue.getBytes());\n\n        String myKeypairName = \"secret_\" + randomString();\n        concord.secrets().generateKeyPair(NewSecretQuery.builder()\n                .org(orgName)\n                .name(myKeypairName)\n                .storePassword(mySecretPwd)\n                .build());\n\n        String myCredentialsName = \"secret_\" + randomString();\n        String myUsername = \"username_\" + randomString();\n        String myPassword = \"password_\" + randomPwd();\n        concord.secrets().createSecret(NewSecretQuery.builder()\n                        .org(orgName)\n                        .name(myCredentialsName)\n                        .storePassword(mySecretPwd).build(),\n                myUsername, myPassword);\n\n        String mySecretFileName = \"secret_\" + randomString();\n        String mySecretFileValue = \"value_\" + randomString();\n        concord.secrets().createSecret(NewSecretQuery.builder()\n                        .org(orgName)\n                        .name(mySecretFileName)\n                        .storePassword(mySecretPwd)\n                        .build(),\n                mySecretFileValue.getBytes());\n\n        String myRawString = \"raw_\" + randomString();\n\n        // ---\n\n        Payload payload = new Payload()\n                .org(orgName)\n                .project(projectName)\n                .archive(resource(\"crypto\"))\n                .arg(\"myOrg\", orgName)\n                .arg(\"mySecretPwd\", mySecretPwd)\n                .arg(\"myStringSecret\", myStringSecretName)\n                .arg(\"myKeypair\", myKeypairName)\n                .arg(\"myCredentials\", myCredentialsName)\n                .arg(\"mySecretFile\", mySecretFileName)\n                .arg(\"myRawString\", myRawString);\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".*String: \" + myStringSecretValue.replaceAll(\".\", \"$0 \") + \".*\");\n        proc.assertLog(\".*Keypair: \\\\{.*private.*}.*\");\n        proc.assertLog(\".*Credentials: .*\" + myPassword.replaceAll(\".\", \"$0 \") + \".*\");\n        proc.assertLog(\".*File: .*\");\n        proc.assertLog(\".*Encrypted string: \" + myRawString.replaceAll(\".\", \"$0 \") + \".*\");\n    }\n\n    @Test\n    public void testCreate() throws Exception {\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi orgApi = new OrganizationsApi(concord.apiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        Payload payload = new Payload()\n                .arg(\"org\", orgName)\n                .arg(\"secretName\", \"secret_\" + randomString())\n                .archive(resource(\"cryptoCreate\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".*result.ok: true.*\");\n        proc.assertLog(\".*result.password: pAss123qweasd.*\");\n        proc.assertLog(\".*credentials-masked: .*password=\\\\*\\\\*\\\\*\\\\*\\\\*\\\\*.*\");\n        proc.assertLog(\".*credentials: .*password=\" + \"123\".replaceAll(\".\", \"$0 \") + \".*\");\n    }\n\n    @Test\n    public void testMasked() throws Exception {\n        ApiClient apiClient = concord.apiClient();\n\n        String orgName = \"org_\" + randomString();\n        String projectName = \"project_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(apiClient);\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        ProjectsApi projectsApi = new ProjectsApi(apiClient);\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.OWNERS));\n\n        // ---\n\n        String mySecretPwd = \"pwd_\" + randomPwd();\n\n        String myStringSecretName = \"secret_\" + randomString();\n        String myStringSecretValue = \"value_\" + randomString();\n        concord.secrets().createSecret(NewSecretQuery.builder()\n                        .org(orgName)\n                        .name(myStringSecretName)\n                        .storePassword(mySecretPwd)\n                        .build(),\n                myStringSecretValue.getBytes());\n\n        // ---\n\n        Payload payload = new Payload()\n                .org(orgName)\n                .project(projectName)\n                .archive(resource(\"crypto-masked\"))\n                .arg(\"myOrg\", orgName)\n                .arg(\"mySecretPwd\", mySecretPwd)\n                .arg(\"myStringSecret\", myStringSecretName);\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.SUSPENDED);\n\n        proc.assertLog(\".*String: \\\\*\\\\*\\\\*\\\\*\\\\*\\\\*.*\");\n\n        proc.submitForm(\"myForm\", Collections.singletonMap(\"name\", \"test\"));\n\n        proc.expectStatus(ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".*String after suspend: \\\\*\\\\*\\\\*\\\\*\\\\*\\\\*.*\");\n    }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/FlowEventsIT.java",
    "content": "package com.walmartlabs.concord.it.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.ConcordProcess;\nimport ca.ibodrov.concord.testcontainers.Payload;\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.ProcessEventEntry;\nimport com.walmartlabs.concord.client2.ProcessEventsApi;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class FlowEventsIT extends AbstractTest {\n\n    @RegisterExtension\n    public static final ConcordRule concord = ConcordConfiguration.configure();\n\n    @Test\n    public void test() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"flowEvents\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n\n        ProcessEventsApi processEventsApi = new ProcessEventsApi(concord.apiClient());\n        List<ProcessEventEntry> events = processEventsApi.listProcessEvents(proc.instanceId(), \"ELEMENT\", null, null, null, null, null, null);\n        assertNotNull(events);\n\n        // ---\n        // expression:\n        // - ${log.info('BOO')}\n        assertEvent(events, 0, new EventData()\n                .pre()\n                .correlationId()\n                .location(9, 7, \"concord.yml\")\n                .flow(\"default\")\n                .name(\"log\")\n                .method(\"info\")\n                .description(\"Task: log\"));\n\n        assertEvent(events, 1, new EventData()\n                .post()\n                .duration()\n                .correlationId()\n                .location(9, 7, \"concord.yml\")\n                .flow(\"default\")\n                .name(\"log\")\n                .method(\"info\")\n                .description(\"Task: log\"));\n\n        // task full form:\n        // - task: log\n        //   in:\n        //     msg: \"test\"\n        // pre\n        assertEvent(events, 2, new EventData()\n                .pre()\n                .correlationId()\n                .location(12, 7, \"concord.yml\")\n                .flow(\"default\")\n                .name(\"log\")\n                .description(\"Task: log\"));\n        // post\n        assertEvent(events, 3, new EventData()\n                .post()\n                .duration()\n                .correlationId()\n                .location(12, 7, \"concord.yml\")\n                .flow(\"default\")\n                .name(\"log\")\n                .description(\"Task: log\"));\n\n        // script:\n        // - script: js\n        assertEvent(events, 4, new EventData()\n                .correlationId()\n                .location(17, 7, \"concord.yml\")\n                .flow(\"default\")\n                .description(\"Script: js\"));\n\n        // if\n        // - if: ${1 == 1}\n        assertEvent(events, 5, new EventData()\n                .correlationId()\n                .location(22, 7, \"concord.yml\")\n                .flow(\"default\")\n                .description(\"Check: ${1 == 1}\"));\n\n        // - log: \"It's true!\"\n        // pre\n        assertEvent(events, 6, new EventData()\n                .pre()\n                .correlationId()\n                .location(24, 11, \"concord.yml\")\n                .flow(\"default\")\n                .name(\"log\")\n                .description(\"Task: log\"));\n        // post\n        assertEvent(events, 7, new EventData()\n                .post()\n                .correlationId()\n                .duration()\n                .location(24, 11, \"concord.yml\")\n                .flow(\"default\")\n                .name(\"log\")\n                .description(\"Task: log\"));\n\n        // - switch: ${myVar}\n        assertEvent(events, 8, new EventData()\n                .correlationId()\n                .location(26, 7, \"concord.yml\")\n                .flow(\"default\")\n                .description(\"Switch: ${myVar}\"));\n\n        // - log: \"It's red!\"\n        // pre\n        assertEvent(events, 9, new EventData()\n                .pre()\n                .correlationId()\n                .location(28, 11, \"concord.yml\")\n                .flow(\"default\")\n                .name(\"log\")\n                .description(\"Task: log\"));\n        // post\n        assertEvent(events, 10, new EventData()\n                .post()\n                .correlationId()\n                .duration()\n                .location(28, 11, \"concord.yml\")\n                .flow(\"default\")\n                .name(\"log\")\n                .description(\"Task: log\"));\n\n        // set variables\n        assertEvent(events, 11, new EventData()\n                .correlationId()\n                .location(30, 7, \"concord.yml\")\n                .flow(\"default\")\n                .description(\"Set variables\"));\n\n        // flow call\n        assertEvent(events, 12, new EventData()\n                .correlationId()\n                .location(33, 7, \"concord.yml\")\n                .flow(\"default\")\n                .description(\"Flow call: returnFlow\"));\n\n        // return\n        assertEvent(events, 13, new EventData()\n                .correlationId()\n                .location(38, 7, \"concord.yml\")\n                .flow(\"returnFlow\")\n                .description(\"Return\"));\n\n        // flow call\n        assertEvent(events, 14, new EventData()\n                .correlationId()\n                .location(35, 7, \"concord.yml\")\n                .flow(\"default\")\n                .description(\"Flow call: exitFlow\"));\n\n        // exit\n        assertEvent(events, 15, new EventData()\n                .correlationId()\n                .location(41, 7, \"concord.yml\")\n                .flow(\"exitFlow\")\n                .description(\"Exit\"));\n    }\n\n    private static void assertEvent(List<ProcessEventEntry> events, int index, EventData expected) {\n        assertTrue(index < events.size());\n\n        Set<String> toIntKeys = new HashSet<>(Arrays.asList(\"line\", \"column\"));\n        Map<String, Object> actual = events.get(index).getData().entrySet().stream()\n                .map(e -> {\n                    if (toIntKeys.contains(e.getKey())) {\n                        return new AbstractMap.SimpleEntry<>(e.getKey(), ((Number) e.getValue()).intValue());\n                    }\n                    return e;\n                })\n                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));\n\n        assertEquals(expected, actual);\n    }\n\n    static class EventData extends HashMap<String, Object> {\n\n        private static final long serialVersionUID = 1L;\n\n        public EventData location(int line, int column, String fileName) {\n            put(\"line\", line);\n            put(\"column\", column);\n            put(\"fileName\", fileName);\n            return this;\n        }\n\n        public EventData flow(String flow) {\n            put(\"processDefinitionId\", flow);\n            return this;\n        }\n\n        public EventData description(String description) {\n            put(\"description\", description);\n            return this;\n        }\n\n        public EventData pre() {\n            put(\"phase\", \"pre\");\n            return this;\n        }\n\n        public EventData post() {\n            put(\"phase\", \"post\");\n            return this;\n        }\n\n        public EventData name(String name) {\n            put(\"name\", name);\n            return this;\n        }\n\n        public EventData correlationId() {\n            put(\"correlationId\", new Object() {\n                @Override\n                public boolean equals(Object obj) {\n                    return true;\n                }\n            });\n            return this;\n        }\n\n        public EventData duration() {\n            put(\"duration\", new Object() {\n                @Override\n                public boolean equals(Object obj) {\n                    return true;\n                }\n            });\n            return this;\n        }\n\n        public EventData method(String name) {\n            put(\"method\", name);\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/FormIT.java",
    "content": "package com.walmartlabs.concord.it.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.ConcordProcess;\nimport ca.ibodrov.concord.testcontainers.Payload;\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport com.google.common.io.CharStreams;\nimport com.walmartlabs.concord.client2.FormListEntry;\nimport com.walmartlabs.concord.client2.FormSubmitResponse;\nimport com.walmartlabs.concord.client2.ProcessApi;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport org.testcontainers.shaded.com.google.common.collect.ImmutableMap;\n\nimport javax.script.ScriptEngine;\nimport javax.script.ScriptEngineManager;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.nio.charset.StandardCharsets;\nimport java.util.*;\nimport java.util.concurrent.ThreadLocalRandom;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.randomString;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class FormIT extends AbstractTest {\n\n    @RegisterExtension\n    public static final ConcordRule concord = ConcordConfiguration.configure();\n\n    /**\n     * A straightforward single form process.\n     */\n    @Test\n    public void test() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"form\"));\n\n        // ---\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.SUSPENDED);\n\n        // ---\n\n        List<FormListEntry> forms = proc.forms();\n        assertEquals(1, forms.size());\n\n        // ---\n\n        FormListEntry myForm = forms.get(0);\n        assertFalse(myForm.getCustom());\n\n        String formName = myForm.getName();\n\n        String firstName = \"john_\" + randomString();\n        String lastName = \"smith_\" + randomString();\n        int age = ThreadLocalRandom.current().nextInt(100);\n\n        Map<String, Object> data = new HashMap<>();\n        data.put(\"lastName\", lastName);\n        data.put(\"firstName\", firstName);\n        data.put(\"age\", age);\n\n        FormSubmitResponse fsr = proc.submitForm(formName, data);\n        assertTrue(fsr.getOk());\n        assertTrue(fsr.getErrors() == null || fsr.getErrors().isEmpty());\n\n        assertEquals(0, proc.forms().size());\n\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n\n        proc.assertLog(\".*firstName=\" + firstName + \".*\");\n        proc.assertLog(\".*lastName=\" + lastName + \".*\");\n        proc.assertLog(\".*age=\" + age + \".*\");\n    }\n\n    /**\n     * Start a process with a form and a sleep task. Cancel the process while sleeping\n     * and check the onCancel process for variables. We expect the submitted values\n     * to be available in the onCancel flow.\n     */\n    @Test\n    public void testFormOnCancel() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"formOnCancel\"));\n\n        // ---\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.SUSPENDED);\n\n        List<FormListEntry> forms = proc.forms();\n        assertEquals(1, forms.size());\n\n        FormListEntry myForm = forms.get(0);\n\n        String firstName = \"john_\" + randomString();\n        int age = ThreadLocalRandom.current().nextInt(100);\n\n        Map<String, Object> data = new HashMap<>();\n        data.put(\"firstName\", firstName);\n        data.put(\"age\", age);\n\n        FormSubmitResponse fsr = proc.submitForm(myForm.getName(), data);\n        assertTrue(fsr.getOk());\n\n        // ---\n\n        expectStatus(proc, ProcessEntry.StatusEnum.RUNNING);\n        proc.kill();\n        expectStatus(proc, ProcessEntry.StatusEnum.CANCELLED);\n\n        ConcordProcess child = concord.processes().get(proc.waitForChildStatus(ProcessEntry.StatusEnum.FINISHED).getInstanceId());\n        child.assertLog(\".*myForm.firstName: \" + firstName + \".*\");\n        child.assertLog(\".*myForm.age: \" + age + \".*\");\n    }\n\n    @Test\n    public void testFormValues() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"customFormValues\"));\n\n        // ---\n\n        assertFormValues(payload);\n    }\n\n    @Test\n    public void testFormExpressionValues() throws Exception {\n        Payload payload = new Payload()\n                .entryPoint(\"callExpressions\")\n                .archive(resource(\"customFormValues\"));\n\n        // ---\n\n        assertFormValues(payload);\n    }\n\n    private void assertFormValues(Payload payload) throws Exception {\n        ConcordProcess proc = concord.processes().start(payload);\n        ProcessEntry pe = expectStatus(proc, ProcessEntry.StatusEnum.SUSPENDED);\n\n        // ---\n\n        List<FormListEntry> forms = proc.forms();\n        assertEquals(1, forms.size());\n\n        // ---\n        FormListEntry form = forms.get(0);\n        String formName = form.getName();\n        assertEquals(\"myForm\", formName);\n\n        // runAs username from expression\n        Map<String, Object> runAs = form.getRunAs();\n        assertNotNull(runAs);\n        assertEquals(\"admin\", runAs.get(\"username\"));\n\n        // start session\n        startCustomFormSession(concord, pe.getInstanceId(), formName);\n\n        // get data.js\n        Map<String, Object> dataJs = getDataJs(concord, pe.getInstanceId(), formName);\n        Map<String, Object> values = MapUtils.get(dataJs, \"values\", Collections.emptyMap());\n\n        assertEquals(4, values.size());\n        assertEquals(\"Moo\", values.get(\"firstName\"));\n        assertEquals(\"Xaa\", values.get(\"lastName\"));\n        assertEquals(3, values.get(\"sum\"));\n        assertEquals(ImmutableMap.of(\"city\", \"Toronto\", \"province\", \"Ontario\"), values.get(\"address\"));\n    }\n\n    @Test\n    public void testSubmitInInvalidProcessState() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"form\"));\n\n        // ---\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.SUSPENDED);\n\n        // ---\n\n        List<FormListEntry> forms = proc.forms();\n        assertEquals(1, forms.size());\n\n        // change process status to emulate resuming from another form\n        ProcessApi api = new ProcessApi(concord.apiClient());\n        api.updateStatus(proc.instanceId(), UUID.randomUUID().toString(), ProcessEntry.StatusEnum.RESUMING.getValue());\n\n        // form data\n        String firstName = \"john_\" + randomString();\n        String lastName = \"smith_\" + randomString();\n        int age = ThreadLocalRandom.current().nextInt(100);\n\n        Map<String, Object> data = new HashMap<>();\n        data.put(\"lastName\", lastName);\n        data.put(\"firstName\", firstName);\n        data.put(\"age\", age);\n\n        try {\n            proc.submitForm(forms.get(0).getName(), data);\n            fail(\"exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        // change process status back to emulate another form submitted\n        api.updateStatus(proc.instanceId(), UUID.randomUUID().toString(), ProcessEntry.StatusEnum.SUSPENDED.getValue());\n\n        forms = proc.forms();\n        assertEquals(1, forms.size());\n\n        FormListEntry myForm = forms.get(0);\n        assertFalse(myForm.getCustom());\n\n        String formName = myForm.getName();\n        assertEquals(\"myForm\", formName);\n\n        FormSubmitResponse fsr = proc.submitForm(formName, data);\n        assertTrue(fsr.getOk());\n        assertTrue(fsr.getErrors() == null || fsr.getErrors().isEmpty());\n\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n\n        proc.assertLog(\".*firstName=\" + firstName + \".*\");\n        proc.assertLog(\".*lastName=\" + lastName + \".*\");\n        proc.assertLog(\".*age=\" + age + \".*\");\n    }\n\n    private static void startCustomFormSession(ConcordRule concord, UUID instanceId, String formName) throws Exception {\n        URL url = new URL(concord.apiBaseUrl() + \"/api/service/custom_form/\" + instanceId + \"/\" + formName + \"/start\");\n        HttpURLConnection http = (HttpURLConnection) url.openConnection();\n        http.setRequestProperty(\"Authorization\", concord.environment().apiToken());\n        http.setRequestMethod(\"POST\");\n        http.setDoOutput(true);\n        http.connect();\n\n        assertEquals(200, http.getResponseCode());\n\n        http.disconnect();\n    }\n\n    @SuppressWarnings({\"unchecked\", \"UnstableApiUsage\"})\n    private static Map<String, Object> getDataJs(ConcordRule concord, UUID instanceId, String formName) throws Exception {\n        URL url = new URL(concord.apiBaseUrl() + \"/forms/\" + instanceId + \"/\" + formName + \"/form/data.js\");\n        HttpURLConnection http = (HttpURLConnection) url.openConnection();\n        http.setRequestProperty(\"Authorization\", concord.environment().apiToken());\n        http.connect();\n\n        assertEquals(200, http.getResponseCode());\n\n        try (InputStream is = http.getInputStream()) {\n            String str = CharStreams.toString(new InputStreamReader(is, StandardCharsets.UTF_8));\n            ScriptEngine se = new ScriptEngineManager().getEngineByName(\"js\");\n            Object result = se.eval(str);\n            assertTrue(result instanceof Map);\n            return (Map<String, Object>) result;\n        } finally {\n            http.disconnect();\n        }\n    }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/GitHubTriggersV2IT.java",
    "content": "package com.walmartlabs.concord.it.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.ConcordProcess;\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport com.google.common.collect.ImmutableMap;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.it.common.GitHubUtils;\nimport com.walmartlabs.concord.it.common.GitUtils;\nimport com.walmartlabs.concord.it.common.ITUtils;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;\n\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.OffsetDateTime;\nimport java.time.temporal.ChronoUnit;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.randomString;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class GitHubTriggersV2IT extends AbstractTest {\n\n    @RegisterExtension\n    public static final ConcordRule concord = ConcordConfiguration.configure();\n\n    /**\n     * Test subscription to unknown repositories only:\n     * <pre>\n     * # project A\n     * # a default onPush trigger for the default branch\n     * triggers:\n     *   - github:\n     *       entryPoint: onPush\n     *\n     * # project G\n     * # accepts only specific commit authors\n     * triggers:\n     *   - github:\n     *       author: \".*xyz.*\"\n     *       entryPoint: onPush\n     * </pre>\n     */\n    @Test\n    public void testFilterBySender() throws Exception {\n        String orgXName = \"orgX_\" + randomString();\n        concord.organizations().create(orgXName);\n\n        Path repo = initRepo(\"triggers/github/repos/v2/defaultTrigger\");\n        String branch = \"branch_\" + randomString();\n        createNewBranch(repo, branch, \"triggers/github/repos/v2/defaultTriggerWithSender\");\n\n        // Project A\n        // master branch + a default trigger\n        String projectAName = \"projectA_\" + randomString();\n        String repoAName = \"repoA_\" + randomString();\n        Path projectARepo = initProjectAndRepo(orgXName, projectAName, repoAName, null, initRepo(\"triggers/github/repos/v2/defaultTrigger\"));\n        refreshRepo(orgXName, projectAName, repoAName);\n\n        // Project G\n        // accepts only specific commit authors\n        String projectGName = \"projectG_\" + randomString();\n        String repoGName = \"repoG_\" + randomString();\n        Path projectBRepo = initProjectAndRepo(orgXName, projectGName, repoGName, null, initRepo(\"triggers/github/repos/v2/defaultTriggerWithSender\"));\n        refreshRepo(orgXName, projectGName, repoGName);\n\n        // ---\n\n        sendEvent(\"triggers/github/events/direct_branch_push.json\", \"push\",\n                \"_FULL_REPO_NAME\", toRepoName(projectARepo),\n                \"_REF\", \"refs/heads/master\",\n                \"_USER_NAME\", \"aknowndude\",\n                \"_USER_LDAP_DN\", \"\");\n\n        // A's triggers should be activated\n        waitForAProcess(orgXName, projectAName, \"github\");\n        expectNoProcesses(orgXName, projectGName, null);\n\n        // ---\n\n        // see https://github.com/walmartlabs/concord/issues/435\n        // wait a bit to reliably filter out subsequent processes of projectA\n        Thread.sleep(1000);\n        OffsetDateTime now = OffsetDateTime.now().truncatedTo(ChronoUnit.SECONDS);\n\n        // ---\n\n        sendEvent(\"triggers/github/events/direct_branch_push.json\", \"push\",\n                \"_FULL_REPO_NAME\", toRepoName(projectBRepo),\n                \"_REF\", \"refs/heads/master\",\n                \"_USER_NAME\", \"somecooldude\",\n                \"_USER_LDAP_DN\", \"\");\n\n        // G's triggers should be activated\n        waitForAProcess(orgXName, projectGName, \"github\");\n\n        // no A's are expected\n        expectNoProcesses(orgXName, projectAName, now);\n    }\n\n    @Test\n    public void testOnPushWithFullTriggerParams() throws Exception {\n        String orgXName = \"orgX_\" + randomString();\n        concord.organizations().create(orgXName);\n\n        // Project A\n        // master branch + a default trigger\n        String projectAName = \"projectA_\" + randomString();\n        String repoAName = \"repoA_\" + randomString();\n        initProjectAndRepo(orgXName, projectAName, repoAName, null, initRepo(\"triggers/github/repos/v2/allParamsTrigger\"));\n        refreshRepo(orgXName, projectAName, repoAName);\n\n        // ---\n\n        sendEvent(\"triggers/github/events/direct_branch_push.json\", \"push\",\n                \"_FULL_REPO_NAME\", \"devtools/concord\",\n                \"_REF\", \"refs/heads/master\",\n                \"_USER_NAME\", \"vasia\",\n                \"_USER_LDAP_DN\", \"\");\n\n        // A's trigger should be activated\n        waitForAProcess(orgXName, projectAName, \"github\");\n    }\n\n    @Test\n    public void testOnPushWithUseEventCommitId() throws Exception {\n        //\n        Path repo = initRepo(\"triggers/github/repos/v2/useEventCommitIdTrigger\");\n        String branch = \"branch_\" + randomString();\n        String commitId = createNewBranch(repo, branch, \"triggers/github/repos/v2/defaultTrigger\");\n\n        //\n        String orgName = \"orgX_\" + randomString();\n        concord.organizations().create(orgName);\n\n        //\n        String projectName = \"project_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n        initProjectAndRepo(orgName, projectName, repoName, null, repo);\n        refreshRepo(orgName, projectName, repoName);\n\n        // ---\n\n        sendEvent(\"triggers/github/events/direct_branch_push_commit_id.json\", \"push\",\n                \"_FULL_REPO_NAME\", toRepoName(repo),\n                \"_REF\", \"refs/heads/\" + branch,\n                \"_USER_NAME\", \"vasia\",\n                \"_USER_LDAP_DN\", \"\",\n                \"_COMMIT_ID\", commitId);\n\n        //\n        ProcessEntry pe = waitForAProcess(orgName, projectName, \"github\");\n        ConcordProcess process = concord.processes().get(pe.getInstanceId());\n        process.assertLog(\".*onPush: .*\" + commitId + \".*\");\n    }\n\n    @Test\n    public void testOnPushWithFilesCondition() throws Exception {\n        String orgXName = \"orgX_\" + randomString();\n        concord.organizations().create(orgXName);\n\n        // Project A\n        // master branch + a trigger with \"files\" condition\n        String projectAName = \"projectA_\" + randomString();\n        String repoAName = \"repoA_\" + randomString();\n        initProjectAndRepo(orgXName, projectAName, repoAName, null, initRepo(\"triggers/github/repos/v2/filesTrigger\"));\n        refreshRepo(orgXName, projectAName, repoAName);\n\n        // ---\n\n        sendEvent(\"triggers/github/events/direct_branch_push.json\", \"push\",\n                \"_FULL_REPO_NAME\", \"devtools/concord\",\n                \"_REF\", \"refs/heads/master\",\n                \"_USER_NAME\", \"vasia\",\n                \"_USER_LDAP_DN\", \"\");\n\n        // A's trigger should be activated\n        waitForAProcess(orgXName, projectAName, \"github\");\n    }\n\n    private Path initRepo(String resource) throws Exception {\n        Path src = Paths.get(resource(resource));\n        return GitUtils.createBareRepository(src, concord.sharedContainerDir());\n    }\n\n    private String createNewBranch(Path bareRepo, String branch, String resource) throws Exception {\n        Path src = Paths.get(resource(resource));\n        return GitUtils.createNewBranch(bareRepo, branch, src);\n    }\n\n    private static Path initProjectAndRepo(String orgName, String projectName, String repoName, String repoBranch, Path bareRepo) throws Exception {\n        // TODO: up concord test rule for projects with repository\n        ProjectsApi projectsApi = new ProjectsApi(apiClient());\n\n        RepositoryEntry repo = new RepositoryEntry()\n                .branch(repoBranch != null ? repoBranch : \"master\")\n                .url(bareRepo.toAbsolutePath().toString());\n\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE)\n                .repositories(ImmutableMap.of(repoName, repo)));\n\n        return bareRepo;\n    }\n\n    private static void refreshRepo(String orgName, String projectName, String repoName) throws Exception {\n        RepositoriesApi repoApi = new RepositoriesApi(apiClient());\n        repoApi.refreshRepository(orgName, projectName, repoName, true);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static void sendEvent(String resource, String eventName, String... params) throws Exception {\n        String payload = resourceToString(resource);\n        if (params != null) {\n            for (int i = 0; i < params.length; i += 2) {\n                String k = params[i];\n                String v = params[i + 1];\n                payload = payload.replaceAll(k, v);\n            }\n        }\n\n        Map<String, Object> event = apiClient().getObjectMapper().readValue(payload, Map.class);\n        payload = apiClient().getObjectMapper().writeValueAsString(event);\n\n        ApiClient client = apiClient();\n        client.addDefaultHeader(\"X-Hub-Signature\", \"sha1=\" + GitHubUtils.sign(payload));\n\n        GitHubEventsApi eventsApi = new GitHubEventsApi(client);\n        eventsApi.onEvent( null, \"abc\", eventName, new ObjectMapper().readValue(payload, Map.class));\n    }\n\n    private static String resourceToString(String resource) throws Exception {\n        return ITUtils.resourceToString(GitHubTriggersV2IT.class, resource);\n    }\n\n    private static String toRepoName(Path p) {\n        return p.getParent().getFileName() + \"/\" + p.getFileName();\n    }\n\n    private static ProcessEntry waitForAProcess(String orgName, String projectName, String initiator) throws Exception {\n        ProcessListFilter q = ProcessListFilter.builder()\n                .orgName(orgName)\n                .projectName(projectName)\n                .initiator(initiator)\n                .build();\n\n        while (!Thread.currentThread().isInterrupted()) {\n            List<ProcessEntry> l = concord.processes().list(q);\n            if (l.size() == 1 && isFinished(l.get(0).getStatus())) {\n                return l.get(0);\n            }\n\n            Thread.sleep(1000);\n        }\n\n        throw new RuntimeException(\"Process wait interrupted\");\n    }\n\n    private static boolean isFinished(ProcessEntry.StatusEnum status) {\n        return status == ProcessEntry.StatusEnum.CANCELLED ||\n                status == ProcessEntry.StatusEnum.FAILED ||\n                status == ProcessEntry.StatusEnum.FINISHED ||\n                status == ProcessEntry.StatusEnum.TIMED_OUT;\n    }\n\n    private static void expectNoProcesses(String orgName, String projectName, OffsetDateTime afterCreatedAt) throws Exception {\n        ProcessListFilter q = ProcessListFilter.builder()\n                .orgName(orgName)\n                .projectName(projectName)\n                .afterCreatedAt(afterCreatedAt)\n                .build();\n\n        List<ProcessEntry> l = concord.processes().list(q);\n        assertEquals(0, l.size());\n    }\n\n    private static ApiClient apiClient() {\n        return concord.apiClient();\n    }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/ITConstants.java",
    "content": "package com.walmartlabs.concord.it.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic final class ITConstants {\n\n    public static final long DEFAULT_TEST_TIMEOUT = 120000;\n\n    private ITConstants() {\n    }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/ImportsIT.java",
    "content": "package com.walmartlabs.concord.it.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.ConcordProcess;\nimport ca.ibodrov.concord.testcontainers.Payload;\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.common.Posix;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport org.junit.jupiter.api.parallel.Execution;\nimport org.junit.jupiter.api.parallel.ExecutionMode;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.randomString;\n\n@Execution(ExecutionMode.SAME_THREAD)\npublic class ImportsIT extends AbstractTest {\n\n    @RegisterExtension\n    public static final ConcordRule concord = ConcordConfiguration.configure()\n            .extraConfigurationSupplier(() -> \"concord-server { imports { disabledProcessors = [] } }\\n\" +\n                    \"concord-agent { imports { disabledProcessors = [] } }\");\n\n    @Test\n    public void testDir() throws Exception {\n        Path tmpDir = Files.createTempDirectory(ConcordConfiguration.sharedDir(), \"test\");\n        Files.setPosixFilePermissions(tmpDir, Posix.posix(0755));\n\n        Path src = Paths.get(resource(\"dirImport/other.concord.yml\"));\n        Path dst = tmpDir.resolve(\"other.concord.yml\");\n        Files.copy(src, dst);\n        Files.setPosixFilePermissions(dst, Posix.posix(0644));\n\n        String name = \"name_\" + randomString();\n        Payload payload = new Payload()\n                .arg(\"name\", name)\n                .concordYml(\"configuration:\\n\" +\n                        \"  runtime: concord-v2\\n\" +\n                        \"imports:\\n\" +\n                        \"  - dir:\\n\" +\n                        \"      src: \" + dst.getParent().toAbsolutePath() + \"\\n\" +\n                        \"      dest: concord\");\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".*Hello, \" + name + \".*\");\n    }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/JsonStoreIT.java",
    "content": "package com.walmartlabs.concord.it.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.ConcordProcess;\nimport ca.ibodrov.concord.testcontainers.Payload;\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.randomString;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class JsonStoreIT extends AbstractTest {\n\n    @RegisterExtension\n    public static final ConcordRule concord = ConcordConfiguration.configure();\n\n    /**\n     * Tests various methods of the 'jsonStore' plugin.\n     */\n    @Test\n    @SuppressWarnings(\"unchecked\")\n    public void test() throws Exception {\n        ApiClient apiClient = concord.apiClient();\n\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi orgApi = new OrganizationsApi(apiClient);\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + randomString();\n        ProjectsApi projectsApi = new ProjectsApi(apiClient);\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.OWNERS));\n\n        String storeName = \"store_\" + randomString();\n\n        // ---\n\n        Payload payload = new Payload()\n                .org(orgName)\n                .project(projectName)\n                .archive(resource(\"jsonStore\"))\n                .arg(\"storeName\", storeName);\n\n        ConcordProcess proc = concord.processes().start(payload);\n\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".*the store doesn't exist.*\");\n        proc.assertLog(\".*the item doesn't exist.*\");\n        proc.assertLog(\".*the store exists now.*\");\n        proc.assertLog(\".*the item exists now.*\");\n\n        proc.assertLog(\".*empty: ==.*\");\n        proc.assertLog(\".*get: \\\\{x=1}.*\");\n        proc.assertLog(\".*Updating item 'test2'.*\" + orgName + \".*\" + storeName + \".*\");\n\n        // ---\n\n        JsonStoreDataApi jsonStoreDataApi = new JsonStoreDataApi(apiClient);\n        Object test = jsonStoreDataApi.getJsonStoreData(orgName, storeName, \"test2\");\n        assertNotNull(test);\n        assertTrue(test instanceof Map);\n\n        Map<String, Object> m = (Map<String, Object>) test;\n        assertEquals(m.get(\"x\"), \"1\");\n    }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/KvTaskIT.java",
    "content": "package com.walmartlabs.concord.it.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.ConcordProcess;\nimport ca.ibodrov.concord.testcontainers.Payload;\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.randomString;\n\npublic class KvTaskIT extends AbstractTest {\n\n    @RegisterExtension\n    public static final ConcordRule concord = ConcordConfiguration.configure();\n\n    /**\n     * Tests various methods of the 'kv' plugin.\n     */\n    @Test\n    public void test() throws Exception {\n        ApiClient apiClient = concord.apiClient();\n\n        String orgName = \"org_\" + randomString();\n        String projectName = \"project_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(apiClient);\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        ProjectsApi projectsApi = new ProjectsApi(apiClient);\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.OWNERS));\n\n        // ---\n\n        Payload payload = new Payload()\n                .org(orgName)\n                .project(projectName)\n                .archive(resource(\"kv\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".*msg: Hello!.*\");\n        proc.assertLog(\".*msg \\\\(removed\\\\): \\\\[].*\");\n        proc.assertLog(\".*x: 123.*\");\n        proc.assertLog(\".*x \\\\(updated\\\\): 124.*\");\n    }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/NodeRosterIT.java",
    "content": "package com.walmartlabs.concord.it.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.ConcordProcess;\nimport ca.ibodrov.concord.testcontainers.Payload;\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport com.walmartlabs.concord.client2.HostEntry;\nimport com.walmartlabs.concord.client2.NodeRosterHostsApi;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.it.common.Version;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\n\nimport static org.junit.jupiter.api.Assertions.fail;\n\nimport static com.walmartlabs.concord.it.runtime.v2.Utils.resourceToString;\n\npublic class NodeRosterIT extends AbstractTest {\n\n    @RegisterExtension\n    public static final ConcordRule concord = ConcordConfiguration.configure();\n\n    /**\n     * Tests various methods of the 'noderoster' plugin.\n     */\n    @Test\n    @Timeout(value = 5, unit = TimeUnit.MINUTES)\n    public void test() throws Exception {\n        // run the Ansible flow first to get some data\n\n        String concordYml = resourceToString(NodeRosterIT.class.getResource(\"noderoster/ansible.yml\"))\n                .replaceAll(\"PROJECT_VERSION\", Version.PROJECT_VERSION);\n\n        ConcordProcess proc = concord.processes().start(new Payload()\n                .concordYml(concordYml)\n                .resource(\"playbook.yml\", NodeRosterIT.class.getResource(\"noderoster/playbook.yml\")));\n\n        ProcessEntry pe = expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // wait for the Node Roster data to appear\n\n        NodeRosterHostsApi hostsApi = new NodeRosterHostsApi(concord.apiClient());\n        long deadline = System.nanoTime() + TimeUnit.MINUTES.toNanos(3);\n        boolean hasHosts = false;\n        while (System.nanoTime() < deadline) {\n            List<HostEntry> l = hostsApi.listKnownHosts(null, null, pe.getInstanceId(), null, 10, 0);\n            if (!l.isEmpty()) {\n                hasHosts = true;\n                break;\n            }\n\n            Thread.sleep(1000);\n        }\n        if (!hasHosts) {\n            fail(\"Timed out waiting for Node Roster data for process \" + pe.getInstanceId());\n        }\n\n        // run the Node Roster flow next to test the plugin\n\n        concordYml = resourceToString(ProcessIT.class.getResource(\"noderoster/noderoster.yml\"))\n                .replaceAll(\"PROJECT_VERSION\", Version.PROJECT_VERSION);\n\n        proc = concord.processes().start(new Payload()\n                .concordYml(concordYml));\n\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".*hostsWithArtifacts:.*ok=true.*\");\n        proc.assertLog(\".*ansible_dns=.*\");\n        proc.assertLog(\".*deployedOnHost:.*ok=true.*\");\n    }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/ProcessIT.java",
    "content": "package com.walmartlabs.concord.it.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.ConcordProcess;\nimport ca.ibodrov.concord.testcontainers.Payload;\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.it.common.Version;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport java.util.*;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.randomString;\nimport static com.walmartlabs.concord.it.runtime.v2.Utils.resourceToString;\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class ProcessIT extends AbstractTest {\n\n    @RegisterExtension\n    public static final ConcordRule concord = ConcordConfiguration.configure();\n\n    /**\n     * Argument passing.\n     */\n    @Test\n    public void testArgs() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"args\"))\n                .arg(\"name\", \"Concord\");\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n\n        proc.assertLog(\".*Runtime: concord-v2.*\");\n        proc.assertLog(\".*Hello, Concord!.*\");\n    }\n\n    /**\n     * Username signature generation.\n     */\n    @Test\n    public void testUsernameSignature() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"usernameSignature\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n\n        proc.assertNoLog(\".*signature: null.*\");\n        proc.assertLog(\".*signature: (?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?\\\\..*\");\n    }\n\n    /**\n     * Groovy script execution.\n     */\n    @Test\n    public void testGroovyScripts() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"scriptGroovy\"))\n                .arg(\"name\", \"Concord\");\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".*Runtime: concord-v2.*\");\n        proc.assertLog(\".*log from script: 123.*\");\n    }\n\n    /**\n     * Js script execution.\n     */\n    @Test\n    public void testJsScripts() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"scriptJs\"))\n                .arg(\"arg\", \"12345\")\n                .arg(\"pattern\", \".234.\");\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".*matches: true.*\");\n    }\n\n    /**\n     * Ruby script execution.\n     */\n    @Test\n    public void testRubyScripts() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"scriptRuby\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".*scriptTask: A1:A2*\");\n        proc.assertLog(\".*scriptTask: B1:B2*\");\n        proc.assertLog(\".*scriptTask: result: A1-Ruby*\");\n        proc.assertLog(\".*scriptTask: result: B1-Ruby*\");\n    }\n\n    /**\n     * Test the process metadata.\n     */\n    @Test\n    public void testMetaUpdate() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"meta\"))\n                .arg(\"name\", \"Concord\");\n\n        ConcordProcess proc = concord.processes().start(payload);\n\n        ProcessEntry pe = expectStatus(proc, ProcessEntry.StatusEnum.SUSPENDED);\n\n        // ---\n\n        proc.assertLog(\".*Runtime: concord-v2.*\");\n        proc.assertLog(\".*Hello, Concord!.*\");\n\n        assertNotNull(pe.getMeta());\n        assertEquals(4, pe.getMeta().size()); // 2 + plus system meta + entryPoint\n        assertEquals(\"init-value\", pe.getMeta().get(\"test\"));\n        assertEquals(\"xxx\", pe.getMeta().get(\"myForm.action\"));\n        assertEquals(\"default\", pe.getMeta().get(\"entryPoint\"));\n\n        // ---\n\n        List<FormListEntry> forms = proc.forms();\n        assertEquals(1, forms.size());\n\n        Map<String, Object> data = new HashMap<>();\n        data.put(\"action\", \"Reject\");\n\n        FormSubmitResponse fsr = proc.submitForm(forms.get(0).getName(), data);\n        assertTrue(fsr.getOk());\n\n        pe = expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n\n        proc.assertLog(\".*Action: Reject.*\");\n\n        assertNotNull(pe.getMeta());\n        assertEquals(4, pe.getMeta().size()); // 2 + plus system meta + entryPoint\n        assertEquals(\"init-value\", pe.getMeta().get(\"test\"));\n        assertEquals(\"Reject\", pe.getMeta().get(\"myForm.action\"));\n        assertEquals(\"default\", pe.getMeta().get(\"entryPoint\"));\n    }\n\n    /**\n     * Test the process metadata with exit step.\n     */\n    @Test\n    public void testMetaWithExit() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"exitWithMeta\"))\n                .arg(\"name\", \"Concord\");\n\n        ConcordProcess proc = concord.processes().start(payload);\n\n        ProcessEntry pe = expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n\n        proc.assertLog(\".*Hello, Concord!.*\");\n\n        assertNotNull(pe.getMeta());\n        assertEquals(\"init-value\", pe.getMeta().get(\"test\"));\n    }\n\n    @Test\n    public void testOutVariables() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"out\"))\n                .out(\"x\", \"y.some.boolean\", \"z\");\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n\n        Map<String, Object> data = proc.getOutVariables();\n        assertNotNull(data);\n\n        assertEquals(123, data.get(\"x\"));\n        assertEquals(true, data.get(\"y.some.boolean\"));\n        assertFalse(data.containsKey(\"z\"));\n    }\n\n    @Test\n    public void testOutVariablesForFailedProcess() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"outForFailed\"))\n                .out(\"x\", \"y.some.boolean\", \"z\");\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FAILED);\n\n        // ---\n\n        Map<String, Object> data = proc.getOutVariables();\n        assertNotNull(data);\n\n        assertEquals(123, data.get(\"x\"));\n        assertEquals(true, data.get(\"y.some.boolean\"));\n        assertFalse(data.containsKey(\"z\"));\n    }\n\n    @Test\n    public void testThrowWithPayload() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"throwWithPayload\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FAILED);\n\n        // ---\n\n        Map<String, Object> data = proc.getOutVariables();\n        assertNotNull(data);\n\n        assertEquals(\"BOOM\", ConfigurationUtils.get(data, \"lastError\", \"message\"));\n        assertEquals(Map.of(\"key\", \"value\", \"key2\", \"value2\"), ConfigurationUtils.get(data, \"lastError\", \"payload\"));\n    }\n\n    @Test\n    public void testLogsFromExpressions() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"logExpression\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".*log from expression short.*\");\n        proc.assertLog(\".*log from expression full form.*\");\n    }\n\n    @Test\n    public void testProjectInfo() throws Exception {\n        String orgName = \"org_\" + randomString();\n        concord.organizations().create(orgName);\n\n        String projectName = \"project_\" + randomString();\n        concord.projects().create(orgName, projectName);\n\n        Payload payload = new Payload()\n                .org(orgName)\n                .project(projectName)\n                .archive(resource(\"projectInfo\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".*orgName=\" + orgName + \".*\");\n        proc.assertLog(\".*projectName=\" + projectName + \".*\");\n    }\n\n    @Test\n    public void testCheckpoints() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"checkpoints\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".*#1.*x=123.*\");\n        proc.assertLog(\".*#2.*y=234.*\");\n        proc.assertLog(\".*#3.*y=345.*\");\n        proc.assertLog(\".*same workDir: true.*\");\n\n        // ---\n\n        List<ProcessCheckpointEntry> checkpoints = proc.checkpoints();\n        assertEquals(2, checkpoints.size());\n\n        checkpoints.sort(Comparator.comparing(ProcessCheckpointEntry::getCreatedAt));\n\n        ProcessCheckpointEntry firstCheckpoint = checkpoints.get(0);\n        assertEquals(\"first\", firstCheckpoint.getName());\n\n        ProcessCheckpointEntry secondCheckpoint = checkpoints.get(1);\n        assertEquals(\"second\", secondCheckpoint.getName());\n\n        // ---\n\n        // restore from the first checkpoint\n\n        proc.restoreCheckpoint(firstCheckpoint.getId());\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // we should see the second checkpoint being saved the second time\n\n        checkpoints = proc.checkpoints();\n        assertEquals(3, checkpoints.size());\n\n        checkpoints.sort(Comparator.comparing(ProcessCheckpointEntry::getCreatedAt));\n\n        assertEquals(\"second\", checkpoints.get(1).getName());\n        assertEquals(\"second\", checkpoints.get(2).getName());\n\n        proc.assertLog(\".*#1.*x=123.*\");\n        proc.assertLogAtLeast(\".*#3.*y=345.*\", 2);\n        proc.assertLog(\".*same workDir: false.*\");\n    }\n\n    @Test\n    public void testCheckpointsParallel() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"checkpointsParallel\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n        proc.assertLogAtLeast(\".*#1 \\\\{x=123}.*\", 1);\n        proc.assertLogAtLeast(\".*#2 \\\\{x=123, y=234}.*\", 1);\n        proc.assertLogAtLeast(\".*#3 \\\\{x=123, z=345}.*\", 1);\n        proc.assertLogAtLeast(\".*#4 \\\\{x=123}.*\", 1);\n\n        // ---\n\n        List<ProcessCheckpointEntry> checkpoints = proc.checkpoints();\n        assertEquals(3, checkpoints.size());\n\n        checkpoints.sort(Comparator.comparing(ProcessCheckpointEntry::getName));\n        assertEquals(\"aaa\", checkpoints.get(0).getName());\n        assertEquals(\"bbb\", checkpoints.get(1).getName());\n        assertEquals(\"ccc\", checkpoints.get(2).getName());\n\n        // ---\n\n        proc.restoreCheckpoint(checkpoints.get(1).getId());\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLogAtLeast(\".*#4 \\\\{x=123}.*\", 2);\n    }\n\n    @Test\n    public void testNoStateAfterCheckpoint() throws Exception {\n        String concordYml = resourceToString(ProcessIT.class.getResource(\"checkpointState/concord.yml\"))\n                .replaceAll(\"PROJECT_VERSION\", Version.PROJECT_VERSION);\n\n        Payload payload = new Payload().concordYml(concordYml);\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n\n        List<ProcessCheckpointEntry> checkpoints = proc.checkpoints();\n        assertEquals(1, checkpoints.size());\n\n        proc.assertLog(\".*#1 BEFORE: false.*\");\n        proc.assertLog(\".*#2 AFTER: false.*\");\n    }\n\n    @Test\n    public void testForkCheckpoints() throws Exception {\n        String forkTag = \"fork_\" + randomString();\n\n        Payload payload = new Payload()\n                .arg(\"forkTag\", forkTag)\n                .archive(resource(\"forkCheckpoints\"));\n\n        ConcordProcess parent = concord.processes().start(payload);\n        expectStatus(parent, ProcessEntry.StatusEnum.FINISHED);\n        parent.assertLog(\".*#1.*\");\n        parent.assertLog(\".*#2.*\");\n\n        // ---\n\n        List<ProcessEntry> children = concord.processes().list(ProcessListFilter.builder()\n                .parentInstanceId(parent.instanceId())\n                .limit(10)\n                .build());\n\n        assertEquals(1, children.size());\n\n        ProcessEntry fork = children.get(0);\n        assertEquals(fork.getTags().iterator().next(), forkTag);\n\n        // ---\n\n        List<ProcessCheckpointEntry> checkpoints = parent.checkpoints();\n        assertEquals(1, checkpoints.size());\n\n        parent.restoreCheckpoint(checkpoints.get(0).getId());\n        expectStatus(parent, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n\n        children = concord.processes().list(ProcessListFilter.builder()\n                .parentInstanceId(parent.instanceId())\n                .limit(10)\n                .build());\n\n        assertEquals(2, children.size());\n\n        // ---\n\n        for (ProcessEntry child : children) {\n            ConcordProcess proc = concord.processes().get(child.getInstanceId());\n            proc.assertNoLog(\".*#1.*\");\n            proc.assertNoLog(\".*#2.*\");\n            proc.assertLog(\".*#3.*\");\n        }\n    }\n\n    @Test\n    public void testCheckpointsWith3rdPartyClasses() throws Exception {\n        String concordYml = resourceToString(NodeRosterIT.class.getResource(\"checkpointClasses/concord.yml\"))\n                .replaceAll(\"PROJECT_VERSION\", Version.PROJECT_VERSION);\n\n        ConcordProcess proc = concord.processes().start(new Payload()\n                .concordYml(concordYml));\n\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n\n        List<ProcessCheckpointEntry> checkpoints = proc.checkpoints();\n        assertEquals(1, checkpoints.size());\n\n        proc.restoreCheckpoint(checkpoints.get(0).getId());\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n\n        proc.assertLog(\".*1: Hello!.*\");\n        proc.assertLogAtLeast(\".*2: Hello!.*\", 2);\n    }\n\n    @Test\n    public void testLastErrorSave() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"failProcess\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FAILED);\n\n        // ---\n\n        Map<String, Object> data = proc.getOutVariables();\n        assertNotNull(data);\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"@id\", 1);\n        m.put(\"message\", \"BOOM\");\n        assertEquals(m, data.get(\"lastError\"));\n    }\n\n    @Test\n    public void testSuspendTimeoutFromPayload() throws Exception {\n        Payload payload = new Payload()\n                .parameter(\"suspendTimeout\", \"PT1S\")\n                .archive(resource(\"form\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.TIMED_OUT);\n    }\n\n    @Test\n    public void testSuspendTimeout() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"formWithTimeout\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.TIMED_OUT);\n    }\n\n    @Test\n    public void testYamlRootFile() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"yamlRootFile\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n        proc.assertLog(\".*Hello, Concord!*\");\n    }\n\n    @Test\n    public void testMetadataWithWithItems() throws Exception {\n        ConcordProcess proc = concord.processes().start(new Payload()\n                .archive(resource(\"processMetadataWithItems\")));\n\n        ProcessEntry pe = expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n        assertNotNull(pe.getMeta());\n        assertEquals(\"c\", pe.getMeta().get(\"var\"));\n    }\n\n    @Test\n    public void testMetadataUpdateOnlyOnEnd() throws Exception {\n        ConcordProcess proc = concord.processes().start(new Payload()\n                .activeProfiles(\"disableMetaUpdates\")\n                .archive(resource(\"processMetadataSend\")));\n\n        ProcessEntry pe = proc.expectStatus(ProcessEntry.StatusEnum.FINISHED);\n        assertNotNull(pe.getMeta());\n        assertEquals(\"c\", pe.getMeta().get(\"var\"));\n\n        // expect one update\n        int sendCount = proc.getLogLines(line -> line.matches(\".*sending process meta.*\")).size();\n        assertEquals(1, sendCount);\n    }\n\n    @Test\n    public void testMetadataUpdateSuspendAndEnd() throws Exception {\n        ConcordProcess proc = concord.processes().start(new Payload()\n                .activeProfiles(\"disableMetaUpdates\")\n                .arg(\"doSuspend\", true)\n                .archive(resource(\"processMetadataSend\")));\n\n        ProcessEntry pe = proc.expectStatus(ProcessEntry.StatusEnum.FINISHED);\n        assertNotNull(pe.getMeta());\n        assertEquals(\"c\", pe.getMeta().get(\"var\"));\n\n        // expect two updates (one on suspend, one on finish)\n        int sendCount = proc.getLogLines(line -> line.matches(\".*sending process meta.*\")).size();\n        assertEquals(2, sendCount);\n    }\n\n    @Test\n    public void testMetadataUpdateDefault() throws Exception {\n        ConcordProcess proc = concord.processes().start(new Payload()\n                .archive(resource(\"processMetadataSend\")));\n\n        ProcessEntry pe = proc.expectStatus(ProcessEntry.StatusEnum.FINISHED);\n        assertNotNull(pe.getMeta());\n        assertEquals(\"c\", pe.getMeta().get(\"var\"));\n\n        // expect five updates (three set calls in a loop, plus two more)\n        int sendCount = proc.getLogLines(line -> line.matches(\".*sending process meta.*\")).size();\n        assertEquals(5, sendCount);\n    }\n\n    @Test\n    public void testEmptyExclusiveGroup() throws Exception {\n        ConcordProcess proc = concord.processes().start(new Payload()\n                .archive(resource(\"emptyExclusiveGroup\")));\n\n        expectStatus(proc, ProcessEntry.StatusEnum.FAILED);\n        proc.assertLog(\".*Invalid exclusive mode.*\");\n    }\n\n    @Test\n    public void testNullCallInputParam() throws Exception {\n        ConcordProcess proc = concord.processes().start(new Payload()\n                .archive(resource(\"nullCallInputParam\")));\n\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n        proc.assertLog(\".*nullParam: ''.*\");\n    }\n\n    @Test\n    public void testForkVariablesAfterForm() throws Exception {\n        ConcordProcess proc = concord.processes().start(new Payload()\n                .archive(resource(\"forkAfterForm\")));\n\n        expectStatus(proc, ProcessEntry.StatusEnum.SUSPENDED);\n\n        proc.submitForm(\"myForm\", Collections.singletonMap(\"name\", \"test\"));\n\n        proc.expectStatus(ProcessEntry.StatusEnum.FINISHED);\n\n        ProcessEntry forkEntry = proc.waitForChildStatus(ProcessEntry.StatusEnum.FINISHED);\n        ConcordProcess fork = concord.processes().get(forkEntry.getInstanceId());\n\n        fork.assertLog(\".*parentInstanceId: \" + proc.instanceId() + \".*\");\n        fork.assertLog(\".*txId: \" + fork.instanceId() + \".*\");\n    }\n\n    @Test\n    public void testRestart() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"args\"))\n                .arg(\"name\", \"Concord\");\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n\n        proc.assertLog(\".*Runtime: concord-v2.*\");\n        proc.assertLog(\".*Hello, Concord!.*\");\n\n        // restart\n        ProcessApi processApi = new ProcessApi(concord.apiClient());\n        processApi.restartProcess(proc.instanceId());\n\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLogAtLeast(\".*Runtime: concord-v2.*\", 2);\n        proc.assertLogAtLeast(\".*Hello, Concord!.*\", 2);\n\n        // ---\n        ProcessEventsApi processEventsApi = new ProcessEventsApi(concord.apiClient());\n        List<ProcessEventEntry> events = processEventsApi.listProcessEvents(proc.instanceId(), \"PROCESS_STATUS\", null, null, null, null, null, null);\n        assertNotNull(events);\n\n        // 2 NEW events\n        long eventsCount = events.stream().filter(e -> \"NEW\".equals(MapUtils.assertString(e.getData(), \"status\"))).count();\n        assertEquals(2, eventsCount, \"\" + events);\n\n        // empty wait conditions\n        ProcessWaitEntry waitConditions = processApi.getWait(proc.instanceId());\n        assertFalse(waitConditions.getIsWaiting());\n        assertNull(waitConditions.getWaits());\n    }\n\n    @Test\n    public void metaAfterSuspend() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"metaAfterSuspend\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        ProcessEntry pe = expectStatus(proc, ProcessEntry.StatusEnum.FAILED);\n\n        // ---\n        Object myMetaValue = pe.getMeta().get(\"myMetaVar\");\n        assertEquals(\"myMetaVarValue\", myMetaValue);\n    }\n\n    /**\n     * Tests process event batch flushing when a long-running task executes.\n     */\n    @Test\n    public void testEventBatchingShortTimer() throws Exception {\n        Payload payload = new Payload()\n                .activeProfiles(\"shortFlush\")\n                .archive(resource(\"eventBatchingTimer\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        ProcessEntry pe = expectStatus(proc, ProcessEntry.StatusEnum.RUNNING);\n\n        // let it run at least long enough to report an event batch (1-second interval)\n        Thread.sleep(1_500);\n\n        // At this point the process is still executing the sleep task.\n        // We set a 1-second batch duration, so we can not expect a batch to have\n        // been reported even though the max batch size (100) was not met.\n\n        // ---\n        List<ProcessEventEntry> events = getProcessElementEvents(proc);\n\n        // clean up\n        new ProcessApi(concord.apiClient()).kill(pe.getInstanceId());\n\n        // ---\n        assertNotNull(events);\n        assertFalse(events.isEmpty());\n        assertEquals(1, events.size());\n\n        ProcessEventEntry sleepEvent = events.get(0);\n\n        assertEquals(\"sleep\", sleepEvent.getData().get(\"name\"));\n    }\n\n    /**\n     * Demonstrates what happens if process event batching flush timer is too long,\n     * or effectively doesn't exist.\n     */\n    @Test\n    public void testEventBatchingLongTimer() throws Exception {\n        Payload payload = new Payload()\n                .activeProfiles(\"longFlush\")\n                .archive(resource(\"eventBatchingTimer\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        ProcessEntry pe = expectStatus(proc, ProcessEntry.StatusEnum.RUNNING);\n\n        // let it run long enough to prove events aren't going to update any time soon\n        Thread.sleep(1_500);\n\n        // ---\n        List<ProcessEventEntry> events = getProcessElementEvents(proc);\n        assertNotNull(events);\n        // No events because batch is still waiting to get large enough to report\n        assertTrue(events.isEmpty());\n\n        // clean up\n        new ProcessApi(concord.apiClient()).kill(pe.getInstanceId());\n    }\n\n    /**\n     * Executes a flow that will over-fill process event queue if not properly synchronized\n     */\n    @Test\n    public void testEventBatchingParallel() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"eventBatchingParallel\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n        List<ProcessEventEntry> events = getProcessElementEvents(proc);\n        assertNotNull(events);\n        assertFalse(events.isEmpty());\n    }\n\n    @Test\n    public void testSimpleDryRun() throws Exception {\n        Payload payload = new Payload()\n                .parameter(Constants.Request.DRY_RUN_MODE_KEY, true)\n                .archive(resource(\"dryRun\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".* Running in dry-run mode: Skipping sending request.*\");\n        proc.assertLog(\".* isDryRun: true.*\");\n    }\n\n    @Test\n    public void testDryRunModeNotSupportedByScript() throws Exception {\n        Payload payload = new Payload()\n                .parameter(Constants.Request.DRY_RUN_MODE_KEY, true)\n                .archive(resource(\"scriptJs\"))\n                .arg(\"arg\", \"12345\")\n                .arg(\"pattern\", \".234.\");\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FAILED);\n\n        proc.assertLog(\".*Error @ line: 6, col: 7. Dry-run mode is not supported for this 'script' step.*\");\n    }\n\n    @Test\n    public void testThrowParallelWithPayload() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"parallelExceptionPayload\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n\n        proc.assertLogAtLeast(\".*BOOM1.*\", 1);\n        proc.assertLogAtLeast(\".*BOOM2.*\", 1);\n\n        Map<String, Object> data = proc.getOutVariables();\n        List<Map<String, Object>> exceptions = MapUtils.getList(data, \"exceptions\", List.of());\n\n        assertNotNull(exceptions);\n        assertEquals(List.of(\"BOOM1\", \"BOOM2\"), exceptions.stream().map(e -> e.get(\"message\")).toList());\n        assertEquals(List.of(Map.of(\"key\", 1), Map.of(\"key\", 2)), exceptions.stream().map(e -> e.get(\"payload\")).toList());\n    }\n\n    private List<ProcessEventEntry> getProcessElementEvents(ConcordProcess proc) throws Exception {\n        ProcessEventsApi processEventsApi = new ProcessEventsApi(concord.apiClient());\n        return processEventsApi.listProcessEvents(proc.instanceId(), \"ELEMENT\", null, null, null, \"pre\", null, null);\n    }\n\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/ProfilesIT.java",
    "content": "package com.walmartlabs.concord.it.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.ConcordProcess;\nimport ca.ibodrov.concord.testcontainers.Payload;\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport com.walmartlabs.concord.client2.FormListEntry;\nimport com.walmartlabs.concord.client2.FormSubmitResponse;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.randomString;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ProfilesIT extends AbstractTest {\n\n    @RegisterExtension\n    public static final ConcordRule concord = ConcordConfiguration.configure();\n\n    /**\n     * Override flows from active profiles.\n     */\n    @Test\n    public void testFlowOverride() throws Exception {\n        Payload payload = new Payload()\n                .activeProfiles(\"stranger\")\n                .archive(resource(\"profileFlow\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n\n        proc.assertLog(\".*From profile: Hello, stranger.*\");\n    }\n\n    /**\n     * Override/define forms from profiles.\n     */\n    @Test\n    public void testFormOverride() throws Exception {\n        Payload payload = new Payload()\n                .activeProfiles(\"stranger\")\n                .archive(resource(\"profileForm\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n\n        // ---\n\n        expectStatus(proc, ProcessEntry.StatusEnum.SUSPENDED);\n\n        // ---\n\n        List<FormListEntry> forms = proc.forms();\n        assertEquals(1, forms.size());\n\n        FormListEntry myForm = forms.get(0);\n        assertNotNull(myForm);\n\n        String firstName = \"john_\" + randomString();\n        String lastName = \"smith_\" + randomString();\n        Map<String, Object> data = new HashMap<>();\n        data.put(\"lastName\", lastName);\n        data.put(\"firstName\", firstName);\n\n        FormSubmitResponse fsr = proc.submitForm(myForm.getName(), data);\n        assertTrue(fsr.getOk());\n        assertTrue(fsr.getErrors() == null || fsr.getErrors().isEmpty());\n\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".*firstName=\" + firstName + \".*\");\n        proc.assertLog(\".*lastName=\" + lastName + \".*\");\n    }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/ResourceTaskIT.java",
    "content": "package com.walmartlabs.concord.it.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.ConcordProcess;\nimport ca.ibodrov.concord.testcontainers.Payload;\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nclass ResourceTaskIT extends AbstractTest {\n\n    @RegisterExtension\n    public static final ConcordRule concord = ConcordConfiguration.configure();\n\n    @Test\n    void testReadAsJson() throws Exception {\n        basicAssert(test(\"resourceReadAsJson\"));\n    }\n\n    @Test\n    void testFromJsonString() throws Exception {\n        basicAssert(test(\"resourceReadFromJsonString\"));\n    }\n\n    @Test\n    void testReadAsString() throws Exception {\n        basicAssert(test(\"resourceReadAsString\"));\n    }\n\n    @Test\n    void testWriteAsJson() throws Exception {\n        basicAssert(test(\"resourceWriteAsJson\"));\n    }\n\n    @Test\n    void testWriteAsString() throws Exception {\n        basicAssert(test(\"resourceWriteAsString\"));\n    }\n\n    @Test\n    void testWriteAsYaml() throws Exception {\n        basicAssert(test(\"resourceWriteAsYaml\"));\n    }\n\n    @Test\n    void testPrintJson() throws Exception {\n        ConcordProcess proc = test(\"resourcePrintJson\");\n\n        // ---\n\n        Map<String, Object> out = proc.getOutVariables();\n\n        String condensedResult = (String) out.get(\"condensedResult\");\n        assertFalse(condensedResult.contains(\"\\n\"));\n        assertTrue(condensedResult.contains(\"\\\"x\\\":123\"));\n        assertTrue(condensedResult.contains(\"\\\"y\\\":\\\"hello\"));\n\n        String prettyResult = (String) out.get(\"prettyResult\");\n        assertTrue(prettyResult.contains(\"\\n\"));\n        assertTrue(prettyResult.contains(\"\\\"x\\\" : 123\"));\n        assertTrue(prettyResult.contains(\"\\\"y\\\" : \\\"hello\\\"\"));\n    }\n\n    private ConcordProcess test(String resource) throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(resource));\n\n        ConcordProcess proc = concord.processes().start(payload);\n\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        return proc;\n    }\n\n    private void basicAssert(ConcordProcess proc) throws ApiException {\n        proc.assertLog(\".*Runtime: concord-v2.*\");\n        proc.assertLog(\".*Hello Concord!.*\");\n    }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/RestartIT.java",
    "content": "package com.walmartlabs.concord.it.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.ConcordProcess;\nimport ca.ibodrov.concord.testcontainers.Payload;\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport com.google.common.collect.ImmutableMap;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.it.common.GitUtils;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.randomString;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class RestartIT extends AbstractTest {\n\n    @RegisterExtension\n    public static final ConcordRule concord = ConcordConfiguration.configure();\n\n    @Test\n    public void testRestartWithDeletedRepo() throws Exception {\n        // repo\n        Path src = Paths.get(resource(\"restartWithDeletedRepo\"));\n        Path bareRepo = GitUtils.createBareRepository(src, concord.sharedContainerDir());\n\n        // org + project + repository\n        String orgName = \"org_\" + randomString();\n        concord.organizations().create(orgName);\n\n        String projectName = \"project_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(concord.apiClient());\n        RepositoryEntry repo = new RepositoryEntry()\n                .branch(\"master\")\n                .url(bareRepo.toAbsolutePath().toString());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE)\n                .repositories(ImmutableMap.of(repoName, repo)));\n\n        // ---\n        ConcordProcess proc = concord.processes().start(new Payload()\n                .org(orgName)\n                .project(projectName)\n                .parameter(\"repo\", repoName));\n\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n        proc.assertLog(\".*Hello from repo process!.*\");\n\n        // --\n        RepositoriesApi repoApi = new RepositoriesApi(concord.apiClient());\n        repoApi.deleteRepository(orgName, projectName, repoName);\n\n        // restart should fail because the repository no longer exists\n        ProcessApi processApi = new ProcessApi(concord.apiClient());\n        processApi.restartProcess(proc.instanceId());\n\n        expectStatus(proc, ProcessEntry.StatusEnum.FAILED);\n        proc.assertLog(\".*Repository not found.*\");\n    }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/SessionStateFilesIT.java",
    "content": "package com.walmartlabs.concord.it.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.ConcordProcess;\nimport ca.ibodrov.concord.testcontainers.Payload;\nimport ca.ibodrov.concord.testcontainers.Processes;\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.ZipUtils;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.randomString;\nimport static com.walmartlabs.concord.it.runtime.v2.ConcordConfiguration.getServerUrlForAgent;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class SessionStateFilesIT extends AbstractTest {\n\n    @RegisterExtension\n    public static final ConcordRule concord = ConcordConfiguration.configure();\n\n    @Test\n    public void testSessionFileAccess() throws Exception {\n        String username = \"user_\" + randomString();\n\n        UsersApi usersApi = new UsersApi(concord.apiClient());\n        CreateUserResponse user = usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(username)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeysApi = new ApiKeysApi(concord.apiClient());\n        CreateApiKeyResponse cakr = apiKeysApi.createUserApiKey(new CreateApiKeyRequest()\n                        .userId(user.getId()));\n\n        // ---\n        Payload payload = new Payload()\n                .archive(resource(\"sessionFileAccess\"))\n                .arg(\"baseUrl\", getServerUrlForAgent(concord));\n\n        ApiClient client = concord.apiClient().setApiKey(cakr.getKey());\n        ConcordProcess proc = new Processes(client)\n                .start(payload);\n\n        // ---\n        expectStatus(proc, ProcessEntry.StatusEnum.SUSPENDED);\n\n        // ---\n        Assertions.assertEquals(username, proc.getEntry().getInitiator());\n\n        ProcessApi processApiAdmin = new ProcessApi(concord.apiClient());\n        ProcessApi processApiInitiator = new ProcessApi(client);\n\n        String file = \"sensitive_data.txt\";\n\n        // ---\n        assertCantDownloadFile(processApiAdmin, proc.instanceId(), file);\n\n        // ---\n        assertCantDownloadFile(processApiInitiator, proc.instanceId(), file);\n\n        FormSubmitResponse fsr = proc.submitForm(\"myForm\", Collections.emptyMap());\n        assertNull(fsr.getErrors());\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n        proc.assertLog(\".*Secret: top-secret-value.*\");\n    }\n\n    private static void assertCantDownloadFile(ProcessApi processApi, UUID instanceId, String file) throws Exception {\n        try {\n            processApi.downloadAttachment(instanceId, Constants.Files.JOB_SESSION_FILES_DIR_NAME + \"/\" + file);\n            fail(\"exception expected\");\n        } catch (ApiException e) {\n            assertEquals(403, e.getCode());\n        }\n\n        String fullFileName = Constants.Files.JOB_ATTACHMENTS_DIR_NAME + \"/\" + Constants.Files.JOB_SESSION_FILES_DIR_NAME + \"/\" + file;\n\n        // ---\n        try (InputStream state = processApi.downloadState(instanceId)) {\n            assertNoFileInState(fullFileName, state);\n        }\n\n        // ---\n        try {\n            processApi.downloadStateFile(instanceId, fullFileName);\n            fail(\"exception expected\");\n        } catch (ApiException e) {\n            assertEquals(403, e.getCode());\n        }\n    }\n\n    private static void assertNoFileInState(String file, InputStream state) throws IOException  {\n        Path target = Files.createTempDirectory(\"state-unzip\");\n        ZipUtils.unzip(state, target, false, (sourceFile, dstFile) -> {\n            assertNotEquals(file, target.relativize(dstFile).toString());\n        });\n    }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/SmtpIT.java",
    "content": "package com.walmartlabs.concord.it.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.ConcordProcess;\nimport ca.ibodrov.concord.testcontainers.ContainerListener;\nimport ca.ibodrov.concord.testcontainers.ContainerType;\nimport ca.ibodrov.concord.testcontainers.Payload;\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport com.icegreen.greenmail.junit5.GreenMailExtension;\nimport com.icegreen.greenmail.util.ServerSetup;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.it.common.Version;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport org.testcontainers.Testcontainers;\n\nimport javax.mail.internet.MimeMessage;\n\nimport static com.walmartlabs.concord.it.runtime.v2.Utils.resourceToString;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class SmtpIT extends AbstractTest {\n\n    @RegisterExtension\n    public static final GreenMailExtension mailServer = new GreenMailExtension(new ServerSetup(0, \"0.0.0.0\", ServerSetup.PROTOCOL_SMTP))\n            .withPerMethodLifecycle(false);\n\n    @RegisterExtension\n    public static final ConcordRule concord = ConcordConfiguration.configure()\n            .containerListener(new ContainerListener() {\n                @Override\n                public void beforeStart(ContainerType type) {\n                    // use container listener to expose the SMTP server's port right before the container starts\n                    if (type == ContainerType.AGENT) {\n                        Testcontainers.exposeHostPorts(mailServer.getSmtp().getPort());\n                    }\n                }\n            });\n\n    @Test\n    public void test() throws Exception {\n        String concordYml = resourceToString(ProcessIT.class.getResource(\"smtp/concord.yml\"));\n\n        // SMTP host and port must be accessible by the process\n        // i.e. when running in a container the host must point to the docker host's address\n        concordYml = concordYml.replaceAll(\"PROJECT_VERSION\", Version.PROJECT_VERSION)\n                .replaceAll(\"SMTP_HOST\", concord.hostAddressAccessibleByContainers())\n                .replaceAll(\"SMTP_PORT\", String.valueOf(mailServer.getSmtp().getPort()));\n\n        Payload payload = new Payload().concordYml(concordYml);\n        ConcordProcess proc = concord.processes().start(payload);\n\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n        proc.assertLog(\".*Done!.*\");\n\n        // ---\n\n        MimeMessage[] messages = mailServer.getReceivedMessages();\n        assertEquals(1, messages.length);\n\n        MimeMessage msg = messages[0];\n        assertEquals(\"Hey! How are you?\\r\\n\", msg.getContent());\n        assertEquals(\"me@localhost\", msg.getFrom()[0].toString());\n    }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/TaskSchemaValidationIT.java",
    "content": "package com.walmartlabs.concord.it.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.ConcordProcess;\nimport ca.ibodrov.concord.testcontainers.Payload;\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.it.common.Version;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport static com.walmartlabs.concord.it.runtime.v2.Utils.resourceToString;\n\npublic class TaskSchemaValidationIT extends AbstractTest {\n\n    @RegisterExtension\n    public static final ConcordRule concord = ConcordConfiguration.configure();\n\n    /**\n     * Test that valid input passes validation.\n     */\n    @Test\n    public void testValidInput() throws Exception {\n        String concordYml = resourceToString(TaskSchemaValidationIT.class.getResource(\"taskSchemaValidation/validInput/concord.yml\"))\n                .replaceAll(\"PROJECT_VERSION\", Version.PROJECT_VERSION);\n\n        Payload payload = new Payload().concordYml(concordYml);\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".*SchemaTestTask: message=hello, count=5.*\");\n        proc.assertLog(\".*Result: hello.*\");\n    }\n\n    /**\n     * Test that invalid input with FAIL mode throws and process fails.\n     */\n    @Test\n    public void testInvalidInputFail() throws Exception {\n        String concordYml = resourceToString(TaskSchemaValidationIT.class.getResource(\"taskSchemaValidation/invalidInputFail/concord.yml\"))\n                .replaceAll(\"PROJECT_VERSION\", Version.PROJECT_VERSION);\n\n        Payload payload = new Payload().concordYml(concordYml);\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FAILED);\n\n        proc.assertLog(\".*Task 'schemaTest' in validation failed.*\");\n        proc.assertLog(\".*message.*\");\n    }\n\n    /**\n     * Test that invalid input with WARN mode logs warning but process completes.\n     */\n    @Test\n    public void testInvalidInputWarn() throws Exception {\n        String concordYml = resourceToString(TaskSchemaValidationIT.class.getResource(\"taskSchemaValidation/invalidInputWarn/concord.yml\"))\n                .replaceAll(\"PROJECT_VERSION\", Version.PROJECT_VERSION);\n\n        Payload payload = new Payload().concordYml(concordYml);\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".*validation errors.*\");\n        proc.assertLog(\".*Result: default.*\");\n    }\n\n    /**\n     * Test that DISABLED mode skips validation entirely.\n     */\n    @Test\n    public void testValidationDisabled() throws Exception {\n        String concordYml = resourceToString(TaskSchemaValidationIT.class.getResource(\"taskSchemaValidation/validationDisabled/concord.yml\"))\n                .replaceAll(\"PROJECT_VERSION\", Version.PROJECT_VERSION);\n\n        Payload payload = new Payload().concordYml(concordYml);\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".*Result: hello.*\");\n    }\n\n    /**\n     * Test that task without schema works normally.\n     */\n    @Test\n    public void testNoSchema() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"taskSchemaValidation/noSchema\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".*hello from log task.*\");\n    }\n\n    /**\n     * Test that multiple validation errors are collected.\n     */\n    @Test\n    public void testMultipleErrors() throws Exception {\n        String concordYml = resourceToString(TaskSchemaValidationIT.class.getResource(\"taskSchemaValidation/multipleErrors/concord.yml\"))\n                .replaceAll(\"PROJECT_VERSION\", Version.PROJECT_VERSION);\n\n        Payload payload = new Payload().concordYml(concordYml);\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FAILED);\n\n        proc.assertLog(\".*Task 'schemaTest' in validation failed.*\");\n    }\n\n    /**\n     * Test that invalid output with FAIL mode throws and process fails.\n     */\n    @Test\n    public void testInvalidOutputFail() throws Exception {\n        String concordYml = resourceToString(TaskSchemaValidationIT.class.getResource(\"taskSchemaValidation/invalidOutputFail/concord.yml\"))\n                .replaceAll(\"PROJECT_VERSION\", Version.PROJECT_VERSION);\n\n        Payload payload = new Payload().concordYml(concordYml);\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FAILED);\n\n        proc.assertLog(\".*Task 'schemaTest' out validation failed.*\");\n        proc.assertLog(\".*echo.*\");\n    }\n\n    /**\n     * Test that invalid output with WARN mode logs warning but process completes.\n     */\n    @Test\n    public void testInvalidOutputWarn() throws Exception {\n        String concordYml = resourceToString(TaskSchemaValidationIT.class.getResource(\"taskSchemaValidation/invalidOutputWarn/concord.yml\"))\n                .replaceAll(\"PROJECT_VERSION\", Version.PROJECT_VERSION);\n\n        Payload payload = new Payload().concordYml(concordYml);\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".*validation errors.*\");\n        proc.assertLog(\".*Done.*\");\n    }\n\n    /**\n     * Test that an invalid schema resource fails in FAIL mode.\n     */\n    @Test\n    public void testInvalidSchemaFail() throws Exception {\n        String concordYml = resourceToString(TaskSchemaValidationIT.class.getResource(\"taskSchemaValidation/invalidSchemaFail/concord.yml\"))\n                .replaceAll(\"PROJECT_VERSION\", Version.PROJECT_VERSION);\n\n        Payload payload = new Payload().concordYml(concordYml);\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FAILED);\n\n        proc.assertLog(\".*Task 'invalidSchema' in validation failed.*\");\n        proc.assertLog(\".*\\\\[WARN \\\\] Failed to load schema resource.*\");\n    }\n\n    /**\n     * Test that an invalid schema resource only warns in WARN mode.\n     */\n    @Test\n    public void testInvalidSchemaWarn() throws Exception {\n        String concordYml = resourceToString(TaskSchemaValidationIT.class.getResource(\"taskSchemaValidation/invalidSchemaWarn/concord.yml\"))\n                .replaceAll(\"PROJECT_VERSION\", Version.PROJECT_VERSION);\n\n        Payload payload = new Payload().concordYml(concordYml);\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".*validation errors.*\");\n        proc.assertLog(\".*InvalidSchemaTask executed.*\");\n        proc.assertLog(\".*Done.*\");\n    }\n\n    /**\n     * Test that valid concord task input passes validation, including startExternal without baseUrl.\n     */\n    @Test\n    public void testConcordTaskValidInput() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"taskSchemaValidation/concordTaskValid\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        proc.assertLog(\".*Done.*\");\n    }\n\n    /**\n     * Test that invalid concord task input fails validation.\n     */\n    @Test\n    public void testConcordTaskInvalidInput() throws Exception {\n        Payload payload = new Payload()\n                .archive(resource(\"taskSchemaValidation/concordTaskInvalid\"));\n\n        ConcordProcess proc = concord.processes().start(payload);\n        expectStatus(proc, ProcessEntry.StatusEnum.FAILED);\n\n        proc.assertLog(\".*Task 'concord' in validation failed.*\");\n        proc.assertLog(\".*action.*\");\n    }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/TemplateIT.java",
    "content": "package com.walmartlabs.concord.it.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.testcontainers.ConcordProcess;\nimport ca.ibodrov.concord.testcontainers.ContainerListener;\nimport ca.ibodrov.concord.testcontainers.ContainerType;\nimport ca.ibodrov.concord.testcontainers.junit5.ConcordRule;\nimport com.github.tomakehurst.wiremock.client.WireMock;\nimport com.github.tomakehurst.wiremock.junit5.WireMockExtension;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.ZipUtils;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\nimport org.testcontainers.Testcontainers;\n\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;\nimport static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;\nimport static com.walmartlabs.concord.common.PathUtils.createTempFile;\nimport static com.walmartlabs.concord.it.common.ITUtils.randomString;\n\npublic class TemplateIT extends AbstractTest {\n\n    @RegisterExtension\n    public static WireMockExtension rule = WireMockExtension.newInstance()\n            .options(wireMockConfig()\n                    .dynamicPort()\n                    .globalTemplating(true))\n            .build();\n\n    @RegisterExtension\n    public static final ConcordRule concord = ConcordConfiguration\n            .configure()\n            .containerListener(new ContainerListener() {\n                @Override\n                public void beforeStart(ContainerType type) {\n                    if (type == ContainerType.SERVER) {\n                        Testcontainers.exposeHostPorts(rule.getPort());\n                    }\n                }\n            });\n\n    @Test\n    public void testTemplate() throws Exception {\n        Path templatePath = createTemplate(Paths.get(resource(\"template\")));\n        String templateUrl = stubForGetTemplate(templatePath.toAbsolutePath());\n        String templateAlias = \"template_\" + randomString();\n\n        TemplateAliasApi templateAliasResource = new TemplateAliasApi(concord.apiClient());\n        templateAliasResource.createOrUpdateTemplate(new TemplateAliasEntry()\n                .alias(templateAlias)\n                .url(templateUrl));\n\n        // ---\n\n        String orgName = \"Default\";\n        String projectName = \"project_\" + randomString();\n        String myName = \"myName_\" + randomString();\n\n        // ---\n\n        ProjectsApi projectsApi = new ProjectsApi(concord.apiClient());\n        Map<String, Object> cfg = new HashMap<>();\n        cfg.put(Constants.Request.TEMPLATE_KEY, templateAlias);\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .cfg(cfg)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"name\", myName);\n\n        ConcordProcess proc = concord.processes().start(input);\n\n        // ---\n\n        expectStatus(proc, ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n\n        proc.assertLog(\".*Hello, \" + myName + \".*\");\n    }\n\n    /**\n     * Creates a stub for serving a template file\n     *\n     * @param tPath Path to the local template file\n     * @return URL to wiremock location of the template file\n     */\n    private static String stubForGetTemplate(Path tPath) throws MalformedURLException {\n        try (InputStream is = new FileInputStream(tPath.toFile())) {\n            rule.stubFor(WireMock.get(urlPathEqualTo(tPath.toString()))\n                    .willReturn(WireMock.aResponse()\n                            .withStatus(200)\n                            .withHeader(\"Content-Type\", \"application/octet-stream\")\n                            .withBody(is.readAllBytes())\n                    )\n            );\n        } catch (Exception e) {\n            throw new RuntimeException(\"Failed to stub for template file download\" + e.getMessage());\n        }\n\n        return new URL(\"http\", concord.hostAddressAccessibleByContainers(), rule.getPort(), tPath.toString()).toString();\n    }\n\n    /**\n     * Creates a zip file for using as a template\n     *\n     * @param templateDir Directory containing template files\n     * @return Path to the tempalte zip file\n     * @throws IOException when template zip file cannot be created\n     */\n    private static Path createTemplate(Path templateDir) throws IOException {\n        Path tmpZip = createTempFile(\"runtime-v2Template\", \".zip\");\n        try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(Files.newOutputStream(tmpZip))) {\n            ZipUtils.zip(zip, templateDir);\n        }\n\n        if (!tmpZip.toFile().setReadable(true, false)) {\n            throw new RuntimeException(\"Cannot set readable permissions for template file: \" + tmpZip);\n        }\n\n        return tmpZip;\n    }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/java/com/walmartlabs/concord/it/runtime/v2/Utils.java",
    "content": "package com.walmartlabs.concord.it.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.base.Charsets;\nimport com.google.common.io.Resources;\n\nimport java.io.IOException;\nimport java.net.URL;\n\npublic final class Utils {\n\n    public static String resourceToString(URL resource) throws IOException {\n        return Resources.toString(resource, Charsets.UTF_8);\n    }\n\n    private Utils() {\n    }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/args/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - log: \"Hello, ${name}!\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/checkpointClasses/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.it.tasks:serialization-test:PROJECT_VERSION\"\n\nflows:\n  default:\n    - task: customBean\n      in:\n        msg: \"Hello!\"\n      out: myResult\n\n    - log: \"1: ${myResult.value.value}\"\n\n    - checkpoint: test\n\n    - log: \"2: ${myResult.value.value}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/checkpointState/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.plugins.basic:file-tasks:PROJECT_VERSION\"\n\nflows:\n  default:\n    - log: \"#1 BEFORE: ${files.exists('_attachments')}\"\n\n    - checkpoint: \"first\"\n\n    - log: \"#2 AFTER: ${files.exists('_attachments')}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/checkpoints/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  arguments:\n    aVar:\n      x: 123\n\nflows:\n  default:\n    - log: \"#1 ${aVar}\" # {x=123}\n\n    - set:\n        oldWorkDir: \"${workDir}\"\n\n    - checkpoint: \"first\"\n\n    - set:\n        aVar.y: 234\n\n    - log: \"#2 ${aVar}\" # {x=123, y=234}\n\n    - checkpoint: \"second\"\n\n    - set:\n        aVar.y: 345\n\n    - log: \"#3 ${aVar}\" # ${x=123, y=345}\n\n    - log: \"same workDir: ${workDir == oldWorkDir}\" # 'true' first time, 'false' after restoring a checkpoint\n\n    # see also the description of \"prefork.enable\" parameter in concord-agent.conf\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/checkpointsParallel/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  arguments:\n    aVar:\n      x: 123\n\nflows:\n  default:\n    - log: \"#1 ${aVar}\"\n\n    - parallel:\n        - block:\n            - set:\n                aVar.y: 234\n            - log: \"#2 ${aVar}\"\n            - checkpoint: \"aaa\"\n        - block:\n            - set:\n                aVar.z: 345\n            - log: \"#3 ${aVar}\"\n            - checkpoint: \"bbb\"\n\n    - checkpoint: \"ccc\"\n\n    - log: \"#4 ${aVar}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/concord/concordOutVars/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - task: concord\n      out: jobOut\n      in:\n        action: start\n        payload: myPayload\n        sync: true\n        outVars:\n          - xyz\n\n    - log: \"jobOut: ${jobOut}\"\n    - log: \"Done!\""
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/concord/concordOutVars/myPayload/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - log: \"hello!\""
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/concord/concordSubDryRun/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - if: \"${isDryRun() == false}\"\n      then:\n        - throw: \"Dry run mode expected\"\n\n    - task: concord\n      in:\n        action: start\n        payload: myPayload\n        sync: true\n        suspend: true\n\n    - log: \"Done!\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/concord/concordSubDryRun/myPayload/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - if: \"${isDryRun() == false}\"\n      then:\n        - throw: \"Dry run mode expected\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/concord/concordSubIgnoreFail/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - task: concord\n      in:\n        action: start\n        payload: myPayload\n        sync: true\n        ignoreFailures: true\n    - log: \"Done!\""
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/concord/concordSubIgnoreFail/myPayload/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n  - throw: boom!"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/concord/concordTaskApiKey/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - task: concord\n      in:\n        apiKey: \"${myApiKey}\"\n        action: startExternal\n        payload: \"payload\"\n        sync: true\n        arguments:\n          name: \"Concord\"\n        outVars:\n          - xyz\n      out: jobOut\n\n    - log: \"${jobOut.xyz}\""
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/concord/concordTaskApiKey/payload/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - set:\n        xyz: \"Hello, ${name}!. From: ${initiator}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/concord/concordTaskForkSuspend/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - task: concord\n      in:\n        action: fork\n        forks:\n          - entryPoint: onFork\n            arguments:\n              msg: \"Hello\"\n          - entryPoint: onFork\n            arguments:\n              msg: \"Bye\"\n        sync: true\n        suspend: true\n        outVars:\n          - varFromFork\n      out: jobOut\n\n    - log: \"${jobOut}\"\n\n  onFork:\n    - log: \"Running in a fork\"\n    - set:\n        varFromFork: \"${msg}, ${name}\""
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/concord/concordTaskForkWithArguments/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  arguments:\n    x: 123\n\nflows:\n  default:\n    - set:\n        x: 234\n    - task: concord\n      in:\n        action: fork\n        entryPoint: sayHello\n        sync: true\n\n  sayHello:\n    - log: \"Hello from a subprocess!\"\n    - log: \"Concord Fork Process ${x}\""
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/concord/concordTaskSuspendParentProcess/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - task: concord\n      in:\n        action: start\n        payload: payload\n        sync: true\n        suspend: true\n        arguments:\n          name: \"Concord\"\n        outVars:\n          - xyz\n      out: jobOut\n\n    - log: \"${jobOut.xyz}\""
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/concord/concordTaskSuspendParentProcess/payload/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - set:\n        xyz: \"Hello, ${name}!\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/concord/createApiKey/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    # create a new API key\n    - task: concord\n      in:\n        action: createApiKey\n        username: admin\n        name: ${apiKeyName1}\n      out: result1\n    - log: \"result1=${result1}\"\n\n    # try creating a new API key with the same name\n    - try:\n        - task: concord\n          in:\n            action: createApiKey\n            username: admin\n            name: ${apiKeyName1}\n        - log: \"result2=${result2}\"\n      error:\n        - log: \"error=${lastError}\"\n\n    # try creating a new API key with the same name but ignore existings\n    - task: concord\n      in:\n        action: createApiKey\n        username: admin\n        name: ${apiKeyName1}\n        ignoreExisting: true\n      out: result3\n    - log: \"result3=${result3}\"\n\n    # create a new API key with predefined value\n    - task: concord\n      in:\n        action: createApiKey\n        username: admin\n        name: ${apiKeyName2}\n        key: ${apiKeyValue2}\n      out: result4\n    - log: \"result4=${result4}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/concord/createOrUpdateApiKey/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    # create a new API key\n    - task: concord\n      in:\n        action: createOrUpdateApiKey\n        username: ${apiKeyUsername}\n      out: result1\n    - log: \"result1=${result1}\"\n\n    # update the API key\n    - task: concord\n      in:\n        action: createOrUpdateApiKey\n        username: ${apiKeyUsername}\n        name: ${result1.name}\n      out: result2\n    - log: \"result2=${result2}\"\n\n    # update the key again, now using the predefined value\n    - task: concord\n      in:\n        action: createOrUpdateApiKey\n        username: ${apiKeyUsername}\n        name: ${result1.name}\n        key: ${apiKeyValue}\n      out: result3\n    - log: \"result3=${result3}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/concord/projectTask/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n  - task: project\n    in:\n      action: create\n      org: Default\n      name: ${newProjectName}\n  - log: \"Done!\""
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/concord/repositoryRefreshTask/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n  - task: repositoryRefresh\n    in:\n      repositoryInfo:\n        - repositoryId: \"b31b0b06-c33c-11e7-b0e9-8702fc03629f\"\n          repository: \"triggers\"\n  - log: \"Done!\""
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/crypto/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - log: \"String: ${crypto.exportAsString(myOrg, myStringSecret, mySecretPwd).replaceAll('.', '$0 ')}\"\n    - log: \"Keypair: ${crypto.exportKeyAsFile(myOrg, myKeypair, mySecretPwd)}\"\n    - log: \"Credentials: ${crypto.exportCredentials(myOrg, myCredentials, mySecretPwd).get('password').replaceAll('.', '$0 ')}\"\n    - log: \"File: ${crypto.exportAsFile(myOrg, mySecretFile, mySecretPwd)}\"\n    - log: \"Encrypted string: ${crypto.decryptString(crypto.encryptString(myRawString)).replaceAll('.', '$0 ')}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/crypto-masked/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - set:\n        s: \"${crypto.exportAsString(myOrg, myStringSecret, mySecretPwd)}\"\n    - log: \"String: ${s}\"\n\n    - form: myForm\n\n    - log: \"String after suspend: ${s}\"\n\nforms:\n  myForm:\n    - firstName: { label: \"First name\", type: \"string\", value: \"John\" }"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/cryptoCreate/concord.yml",
    "content": "flows:\n  default:\n    - task: crypto\n      in:\n        action: create\n        org: ${org}\n        secretName: \"${secretName}\"\n        generatePwd: \"false\"\n        storePassword: \"pAss123qweasd\"\n        usernamePassword:\n          username: superuser\n          password: \"123\"\n      out: result\n\n    - log: \"result.ok: ${result.ok}\"\n    - log: \"result.password: ${result.password}\"\n    - log: \"credentials: password=${crypto.exportCredentials(org, secretName, 'pAss123qweasd').get('password').replaceAll('.', '$0 ')}\"\n    - log: \"credentials-masked: ${crypto.exportCredentials(org, secretName, 'pAss123qweasd')}\"\n\nconfiguration:\n  runtime: concord-v2\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/customFormValues/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  arguments:\n    runAsUser: 'admin'\n    formValues:\n      firstName: \"Moo\"\n      lastName: \"Xaa\"\n      sum: \"${1 + 2}\"\n      address:\n        city: Toronto\n        province: Ontario\n    formRunAs:\n      username: \"${runAsUser}\"\n\nflows:\n  default:\n    - form: myForm\n      # form calls can override form values or provide additional data\n      values:\n        firstName: \"Moo\"\n        lastName: \"Xaa\"\n        sum: \"${1 + 2}\"\n        address:\n          city: Toronto\n          province: Ontario\n      runAs:\n        username: \"${runAsUser}\"\n\n  callExpressions:\n    - form: myForm\n      # form calls can override form values or provide additional data\n      values: \"${formValues}\"\n      runAs: \"${formRunAs}\"\n\nforms:\n  myForm:\n    - firstName: { label: \"First name\", type: \"string\", value: \"John\" }\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/customFormValues/forms/myForm/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n</head>\n<body>\n    JUST FOR TEST\n</body>\n</html>"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/dirImport/other.concord.yml",
    "content": "flows:\n  default:\n    - log: \"Hello, ${name}!\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/dryRun/concord.yaml",
    "content": "flows:\n  default:\n    - task: http\n      in:\n        method: \"DELETE\"\n        url: \"http://localhost\"\n      out: result\n\n    - log: \"result: ${result}\"\n\n    - log: \"isDryRun: ${isDryRun()}\"\n\nconfiguration:\n  runtime: concord-v2\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/emptyExclusiveGroup/concord.yml",
    "content": "configuration:\n  exclusive:\n    group: \"\"\n    mode: wait\n\nflows:\n  default:\n    - log: \"Hello\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/eventBatchingParallel/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n  events:\n    batchSize: 5\n\nflows:\n  default:\n    - parallel:\n        - call: doALoop\n        - call: doALoop\n        - call: doALoop\n        - call: doALoop\n        - call: doALoop\n        - call: doALoop\n        - call: doALoop\n        - call: doALoop\n        - call: doALoop\n        - call: doALoop\n\n  doALoop:\n    - call: logSomething\n      in:\n        something: \"${item}\"\n      loop:\n        items:\n          - a\n          - b\n          - c\n\n  logSomething:\n    - log: \"hello ${something}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/eventBatchingTimer/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n\nprofiles:\n  shortFlush:\n    configuration:\n      events:\n        batchSize: 100\n        batchFlushInterval: 1\n\n  longFlush:\n    configuration:\n      events:\n        batchSize: 100\n        batchFlushInterval: 120\n\nflows:\n  default:\n    - task: sleep\n      in:\n        duration: 120\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/exitWithMeta/concord.yml",
    "content": "flows:\n  default:\n    - log: \"Hello, ${name}!\"\n    - exit\n\nconfiguration:\n  runtime: \"concord-v2\"\n  arguments:\n    name: \"Concord\"\n  meta:\n    test: \"init-value\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/failProcess/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n  - throw: \"BOOM\""
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/flowEvents/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n  arguments:\n    myVar: \"red\"\n\nflows:\n  default:\n    # expression\n    - ${log.info('BOO')}\n\n    # task full form\n    - task: log\n      in:\n        msg: \"test\"\n\n    # script\n    - script: js\n      body: |\n        print(\"Hello script\")\n\n    # if\n    - if: ${1 == 1}\n      then:\n        - log: \"It's true!\"\n\n    - switch: ${myVar}\n      red:\n        - log: \"It's red!\"\n\n    - set:\n        k: \"v\"\n\n    - call: returnFlow\n\n    - call: exitFlow\n\n  returnFlow:\n    - return\n\n  exitFlow:\n    - exit"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/forkAfterForm/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n\nforms:\n  myForm:\n    - name: { type: \"string\" }\n\nflows:\n  default:\n    - form: myForm\n\n    - task: concord\n      in:\n        action: fork\n        entryPoint: newProcess\n\n  newProcess:\n    - log: |\n        parentInstanceId: ${parentInstanceId}\n        txId: ${txId}:"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/forkCheckpoints/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n\nflows:\n  default:\n    - log: \"#1\"\n    - checkpoint: \"abc\"\n    - task: concord\n      in:\n        action: fork\n        sync: true\n        forks:\n          - entryPoint: onFork\n            tags:\n              - ${forkTag}\n    - log: \"#2\"\n\n  onFork:\n    - log: \"#3\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/form/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - form: myForm\n      fields:\n        - firstName: { type: \"string\" }\n        - lastName: { type: \"string\" }\n        - age: { type: \"int\" }\n\n    - log: \"myForm: ${myForm}\""
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/formOnCancel/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - log: \"before form\"\n    - form: \"myForm\"\n      fields:\n        - firstName: { type: \"string\" }\n        - age: { type: \"int\" }\n      yield: true\n    - log: \"after form\"\n    - \"${sleep.ms(30000)}\"\n    - log: \"after sleep\"\n\n  onCancel:\n    - log: |\n        myForm.firstName: ${myForm.firstName}\n        myForm.age: ${myForm.age}\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/formWithTimeout/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  suspendTimeout: \"PT1S\"\n\nflows:\n  default:\n    - form: myForm\n      fields:\n        - firstName: { type: \"string\" }\n        - lastName: { type: \"string\" }\n        - age: { type: \"int\" }\n\n    - log: \"myForm: ${myForm}\""
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/jsonStore/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - if: ${jsonStore.isStoreExists(storeName)}\n      then:\n          - throw: \"The store shouldn't exist at this point\"\n      else:\n        - log: \"OK: the store doesn't exist\"\n\n    - if: ${jsonStore.isExists(storeName, 'test')}\n      then:\n        - throw: \"The store and the item shouldn't exist at this point\"\n      else:\n        - log: \"OK: the item doesn't exist\"\n\n    - \"${jsonStore.upsert(storeName, 'test', {'test': 123})}\"\n\n    - if: ${jsonStore.isStoreExists(storeName)}\n      then:\n        - log: \"OK: the store exists now\"\n\n    - if: ${jsonStore.isExists(storeName, 'test')}\n      then:\n        - log: \"OK: the item exists now\"\n\n    - log: \"item: ${jsonStore.get(storeName, 'test')}\"\n\n    - log: \"empty: =${jsonStore.get(storeName, 'xxx')}=\"\n\n    - \"${jsonStore.put(storeName, 'test2', {'x': '1'})}\"\n\n    - expr: \"${jsonStore.get(storeName, 'test2')}\"\n      out: myVar\n\n    - log: \"get: ${myVar}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/kv/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - \"${kv.putString('msg', 'Hello!')}\"\n    - log: \"msg: ${kv.getString('msg')}\"\n    - \"${kv.remove('msg')}\"\n    - log: \"msg (removed): [${kv.getString('msg')}]\"\n    - \"${kv.putLong('x', 123)}\"\n    - log: \"x: ${kv.getLong('x')}\"\n    - \"${kv.incLong('x')}\"\n    - log: \"x (updated): ${kv.getLong('x')}\"\n\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/logExpression/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n  - ${log.info('log from expression short')}\n  - expr: \"${log.info('log from expression full form')}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/meta/concord.yml",
    "content": "flows:\n  default:\n    - log: \"Hello, ${name}!\"\n    - form: myForm\n    - log: \"Action: ${myForm.action}!\"\n\nforms:\n  myForm:\n    - action: {label: \"Action\", type: \"string?\", allow: [\"Approve\", \"Reject\"]}\n\nconfiguration:\n  runtime: \"concord-v2\"\n  arguments:\n    name: \"Concord\"\n  meta:\n    test: \"init-value\"\n    myForm.action: \"xxx\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/metaAfterSuspend/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  meta:\n    myMetaVar: \"n/a\"\n\nflows:\n  default:\n    - set:\n        myMetaVar: \"myMetaVarValue\"\n\n    - task: concord\n      in:\n        action: start\n        payload: payload\n        sync: true\n        suspend: true\n        arguments:\n          name: \"Concord\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/metaAfterSuspend/payload/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - throw: \"BOOM\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/noderoster/ansible.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\"\n\nflows:\n  default:\n    - task: ansible\n      in:\n        playbook: \"playbook.yml\"\n        inventory:\n          myHosts:\n            hosts:\n              - \"abc\"\n              - \"xyz\"\n            vars:\n              ansible_connection: \"local\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/noderoster/noderoster.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.plugins.basic:noderoster-tasks:PROJECT_VERSION\"\n\nflows:\n  default:\n    - task: nodeRoster\n      in:\n        action: \"hostsWithArtifacts\"\n        artifactPattern: \".*my-app-1.0.0.jar\"\n      out: myOut\n\n    - log: \"hostsWithArtifacts: ${myOut}\" # should be empty\n\n    - task: nodeRoster\n      in:\n        action: \"facts\"\n        hostName: \"xyz\"\n      out: myOut\n\n    - log: \"facts: ${myOut}\"\n\n    - task: nodeRoster\n      in:\n        action: \"deployedOnHost\"\n        hostName: \"myhost.example.com\"\n      out: myOut\n\n    - log: \"deployedOnHost: ${myOut}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/noderoster/playbook.yml",
    "content": "---\n- hosts: all\n  tasks:\n    - debug:\n        msg: \"Hello!\"\n        verbosity: 0\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/nullCallInputParam/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n  arguments:\n    arg:\n      k: \"v\"\n\nflows:\n  default:\n    - call: test\n      in:\n        nullParam: ${arg.a}\n\n  test:\n    - log: \"nullParam: '${nullParam}'\""
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/out/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n  - set:\n      x: 123\n      y:\n        some:\n          nested: [\"data\", \"in\", \"arrays\"]\n          boolean: true\n          number: 234\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/outForFailed/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n  - set:\n      x: 123\n      y:\n        some:\n          nested: [\"data\", \"in\", \"arrays\"]\n          boolean: true\n          number: 234\n\n  - throw: BOOM\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/parallelExceptionPayload/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  out:\n    - exceptions\n\nflows:\n  default:\n  - try:\n    - task: \"throw\"\n      in:\n        exception: \"BOOM${item}\"\n        payload:\n          key: \"${item}\"\n    loop:\n      items:\n        - 1\n        - 2\n      mode: parallel\n      parallelism: 2\n    error:\n      - set:\n          exceptions: ${lastError.exceptions.stream().toList()}\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/processMetadataSend/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n  requirements:\n    jvm:\n      extraArgs: # enable debug to capture metadata send log message\n        - \"-Dlogback.configurationFile=debug_logback.xml\"\n  meta:\n    var: default_value\n    tmpVar: default_value\n  arguments:\n    doSuspend: false # for ensuring meta is updated on SUSPENDED as well as FINISHED\n\nprofiles:\n  disableMetaUpdates:\n    configuration:\n      events:\n        updateMetaOnAllEvents: false\n\nflows:\n  default:\n    - call: anotherFlow\n      out: tmpVar\n      loop:\n        items:\n          - a\n          - b\n          - c\n\n    - set:\n        var: \"${tmpVar[1]}\"\n\n    - if: \"${doSuspend}\"\n      then:\n        - task: sleep\n          in:\n            suspend: true\n            duration: 1\n\n    - set:\n        var: \"${tmpVar[2]}\"\n\n  anotherFlow:\n    - set:\n        tmpVar: ${item}\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/processMetadataSend/debug_logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <filter class=\"com.walmartlabs.concord.runtime.v2.runner.logging.LogLevelFilter\" />\n\n        <encoder class=\"com.walmartlabs.concord.runtime.v2.runner.logging.ConcordLogEncoder\">\n            <layout class=\"com.walmartlabs.concord.runtime.v2.runner.logging.CustomLayout\">\n                <!-- the UI expects log timestamps in a specific format to be able to convert it to the local time -->\n                <pattern>%date{yyyy-MM-dd'T'HH:mm:ss.SSSZ, UTC} [%-5level] %msg%n%rEx{full, com.sun, sun}</pattern>\n            </layout>\n        </encoder>\n    </appender>\n\n    <logger name=\"com.walmartlabs.concord.runtime.v2.runner.MetadataProcessor\" level=\"DEBUG\"/>\n    <logger name=\"com.walmartlabs.concord\" level=\"INFO\"/>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/processMetadataWithItems/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n  meta:\n    var: value\n\nflows:\n  default:\n    - call: anotherFlow\n      out: var\n      withItems:\n        - a\n        - b\n        - c\n\n    - log: ${var}\n\n  anotherFlow:\n    - set:\n        var: ${item}\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/profileFlow/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  arguments:\n    name: \"Concord\"\n\nprofiles:\n  stranger:\n    configuration:\n      arguments:\n        name: \"stranger\"\n    flows:\n      default:\n        - log: \"From profile: Hello, ${name}\"\n\nflows:\n  default:\n    - log: \"Hello, ${name}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/profileForm/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nprofiles:\n  stranger:\n    configuration:\n      arguments:\n        name: \"stranger\"\n    forms:\n      myForm:\n        - firstName: { type: \"string\" }\n        - lastName: { type: \"string\" }\n\nflows:\n  default:\n    - form: myForm\n\n    - log: \"myForm: ${myForm}\"\n\nforms:\n  myForm:\n    - firstName: { type: \"string\" }\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/projectInfo/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - script: js\n      body: |\n        log.info(\"project info: {}\", context.processConfiguration().projectInfo());"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/resourcePrintJson/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n  out:\n    - condensedResult\n    - prettyResult\n\nflows:\n  default:\n    - set:\n        m:\n          x: 123\n          y: \"hello\"\n    - set:\n        condensedResult: \"${resource.printJson(m)}\"\n        prettyResult: \"${resource.prettyPrintJson(m)}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/resourceReadAsJson/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - expr: ${resource.asJson('sample.json')}\n      out: jsonObj\n    - log: \"Hello ${jsonObj.name}!\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/resourceReadAsJson/sample.json",
    "content": "{\n  \"name\": \"Concord\"\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/resourceReadAsString/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - log: ${resource.asString('sample.txt')}\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/resourceReadAsString/sample.txt",
    "content": "Hello Concord!"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/resourceReadFromJsonString/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - set:\n        jsonString: '{\"name\":\"Concord\"}'\n    - expr: ${resource.fromJsonString(jsonString)}\n      out: jsonObj\n    - log: \"Hello ${jsonObj.name}!\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/resourceWriteAsJson/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - set:\n        myObj:\n          name: Concord!\n\n    - expr: \"${resource.writeAsJson(myObj)}\"\n      out: path\n\n    - expr: ${resource.asJson(path)}\n      out: jsonObj\n\n    - log: \"Hello ${jsonObj.name}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/resourceWriteAsString/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - expr: ${resource.writeAsString('Hello Concord!')}\n      out: path\n\n    - log: \"${resource.asString(path)}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/resourceWriteAsYaml/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - set:\n        myObj:\n          name: Concord!\n\n    - expr: \"${resource.writeAsYaml(myObj)}\"\n      out: path\n\n    - expr: ${resource.asYaml(path)}\n      out: yamlObj\n\n    - log: \"Hello ${yamlObj.name}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/restartWithDeletedRepo/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - log: \"Hello from repo process!\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/scriptGroovy/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  dependencies:\n    - \"mvn://org.apache.groovy:groovy-all:pom:5.0.4\"\n\nflows:\n  default:\n    - script: groovy\n      body: |\n        execution.variables().set(\"x\", 123)\n\n    - script: groovy\n      body: |\n        println \"log from script: \" + x"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/scriptJs/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n\nflows:\n  default:\n    - script: js\n      body: |\n        print(\"matches: \" + arg.matches(pattern));"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/scriptRuby/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  dependencies:\n    - \"mvn://org.jruby:jruby:9.1.17.0\"\n\nflows:\n  default:\n    - parallel:\n        - call: scriptTask\n          in:\n            var1: \"A1\"\n            var2: \"A2\"\n\n        - call: scriptTask\n          in:\n            var1: \"B1\"\n            var2: \"B2\"\n\n  scriptTask:\n    - log: \"scriptTask: ${var1}:${var2}\"\n\n    - script: ruby\n      body: |\n        v = $execution.variables().get('var1')\n        v = v + \"-Ruby\"\n        $execution.variables().set(\"result\", v)\n\n    - log: \"scriptTask: result: ${result}\""
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/sessionFileAccess/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n\nflows:\n  default:\n    - script: js\n      body: |\n        var persistenceServiceType = Java.type('com.walmartlabs.concord.runtime.v2.runner.PersistenceService');\n\n        var w  = new persistenceServiceType.Writer() {\n          write: function(out) {\n            out.write('top-secret-value'.getBytes());\n          }\n        };\n\n        var persistenceService = context.execution().runtime().getService(persistenceServiceType)\n        persistenceService.persistSessionFile('sensitive_data.txt', w);\n\n    - form: myForm\n      fields:\n        - firstName: { type: \"string?\" }\n\n    - task: http\n      in:\n        method: GET\n        url: ${baseUrl}/api/v1/process/${txId}/state/snapshot/_attachments/_session_files/sensitive_data.txt\n        headers:\n          X-Concord-SessionToken: ${processInfo.sessionToken}\n      out: result\n\n    - if: ${result.statusCode != 200}\n      then:\n        - throw: \"Can't load file from snapshot: ${result}\"\n\n    - script: js\n      body: |\n        var persistenceServiceType = Java.type('com.walmartlabs.concord.runtime.v2.runner.PersistenceService');\n\n        var r  = new persistenceServiceType.Converter() {\n          apply: function() {\n            var String = Java.type('java.lang.String');\n            var IOUtils = Java.type('com.walmartlabs.concord.common.IOUtils');\n            var bytes = IOUtils.toByteArray(arguments[0]);\n            var result = new String(bytes);\n            return result;\n          }\n        };\n\n        var persistenceService = context.execution().runtime().getService(persistenceServiceType)\n        var topSecretValue = persistenceService.loadPersistedSessionFile('sensitive_data.txt', r);\n        context.variables().set('topSecret', topSecretValue);\n\n    - log: \"Secret: ${topSecret}\""
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/smtp/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.plugins.basic:smtp-tasks:PROJECT_VERSION\"\n  arguments:\n    smtpParams:\n      host: \"SMTP_HOST\"\n      port: SMTP_PORT\n\nflows:\n  default:\n    - task: smtp\n      in:\n        mail:\n          from: \"me@localhost\"\n          to: \"you@localhost\"\n          subject: \"Hello!\"\n          message: |\n            Hey! How are you?\n\n    - log: \"Done!\""
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/taskSchemaValidation/concordTaskInvalid/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  validation:\n    taskCalls:\n      in: fail\n\nflows:\n  default:\n    # Missing required 'action' parameter - should fail validation\n    - task: concord\n      in:\n        project: \"test\"\n      out: result\n    - log: \"Result: ${result}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/taskSchemaValidation/concordTaskValid/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  validation:\n    taskCalls:\n      in: fail\n\nflows:\n  default:\n    - task: concord\n      in:\n        action: STARTEXTERNAL\n        project: \"test\"\n        apiKey: \"dummy\"\n        activeProfiles: \"profileA\"\n        tags: \"schema-test\"\n        sync: false\n      out: result\n      error:\n        - log: \"Expected error: ${lastError}\"\n    - log: \"Done\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/taskSchemaValidation/invalidInputFail/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.it.tasks:schema-test:PROJECT_VERSION\"\n  validation:\n    taskCalls:\n      in: fail\n\nflows:\n  default:\n    # Missing required 'message' parameter - should fail validation\n    - task: schemaTest\n      in:\n        count: 5\n      out: result\n    - log: \"Result: ${result.echo}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/taskSchemaValidation/invalidInputWarn/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.it.tasks:schema-test:PROJECT_VERSION\"\n  validation:\n    taskCalls:\n      in: warn\n\nflows:\n  default:\n    # Missing required 'message' - should log warning but continue\n    # The task will use default value\n    - task: schemaTest\n      in:\n        message: \"default\"\n        count: -1  # Invalid: count must be >= 0\n      out: result\n    - log: \"Result: ${result.echo}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/taskSchemaValidation/invalidOutputFail/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.it.tasks:schema-test:PROJECT_VERSION\"\n  validation:\n    taskCalls:\n      out: fail\n\nflows:\n  default:\n    # badOutput=true causes the task to omit required 'echo' field\n    - task: schemaTest\n      in:\n        message: \"hello\"\n        badOutput: true\n      out: result\n    - log: \"Result: ${result}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/taskSchemaValidation/invalidOutputWarn/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.it.tasks:schema-test:PROJECT_VERSION\"\n  validation:\n    taskCalls:\n      out: warn\n\nflows:\n  default:\n    # badOutput=true causes the task to omit required 'echo' field\n    - task: schemaTest\n      in:\n        message: \"hello\"\n        badOutput: true\n      out: result\n    - log: \"Done\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/taskSchemaValidation/invalidSchemaFail/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.it.tasks:schema-test:PROJECT_VERSION\"\n  validation:\n    taskCalls:\n      in: fail\n\nflows:\n  default:\n    - task: invalidSchema\n      in:\n        message: \"hello\"\n    - log: \"Done\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/taskSchemaValidation/invalidSchemaWarn/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.it.tasks:schema-test:PROJECT_VERSION\"\n  validation:\n    taskCalls:\n      in: warn\n\nflows:\n  default:\n    - task: invalidSchema\n      in:\n        message: \"hello\"\n    - log: \"Done\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/taskSchemaValidation/multipleErrors/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.it.tasks:schema-test:PROJECT_VERSION\"\n  validation:\n    taskCalls:\n      in: fail\n\nflows:\n  default:\n    # Multiple validation errors: missing 'message' and invalid count type\n    - task: schemaTest\n      in:\n        count: \"not-a-number\"\n      out: result\n    - log: \"Result: ${result.echo}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/taskSchemaValidation/noSchema/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  validation:\n    taskCalls:\n      in: fail\n      out: fail\n\nflows:\n  default:\n    # Log task has no schema - should work normally even with validation enabled\n    - log: \"hello from log task\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/taskSchemaValidation/validInput/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.it.tasks:schema-test:PROJECT_VERSION\"\n  validation:\n    taskCalls:\n      in: fail\n      out: warn\n\nflows:\n  default:\n    - task: schemaTest\n      in:\n        message: \"hello\"\n        count: 5\n      out: result\n    - log: \"Result: ${result.echo}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/taskSchemaValidation/validationDisabled/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  dependencies:\n    - \"mvn://com.walmartlabs.concord.it.tasks:schema-test:PROJECT_VERSION\"\n  # validation disabled by default (no validation block)\n\nflows:\n  default:\n    - task: schemaTest\n      in:\n        message: \"hello\"\n        count: 5\n      out: result\n    - log: \"Result: ${result.echo}\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/template/_main.js",
    "content": "({runtime: \"concord-v2\", entryPoint: \"main\", arguments: { greeting: \"Hello, \" + _input.name }})"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/template/concord/hello.concord.yml",
    "content": "flows:\n  main:\n    - log: ${greeting}"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/throwWithPayload/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n  - task: \"throw\"\n    in:\n      exception: \"BOOM\"\n      payload:\n        key: \"value\"\n        key2: \"value2\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/triggers/github/events/direct_branch_push.json",
    "content": "{\n  \"ref\": \"_REF\",\n  \"before\": \"1083c12463e8122e1ff7adf06ec8aabbe998fec4\",\n  \"after\": \"a68f9e81f70b5ac645739f244cdc95a0da24310e\",\n  \"created\": false,\n  \"deleted\": false,\n  \"forced\": false,\n  \"base_ref\": null,\n  \"compare\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/compare/1083c12463e8...a68f9e81f70b\",\n  \"commits\": [\n    {\n      \"id\": \"a68f9e81f70b5ac645739f244cdc95a0da24310e\",\n      \"tree_id\": \"5e0785caefd8d107f75d18a26907f68c7168a8cc\",\n      \"distinct\": true,\n      \"message\": \"bump\",\n      \"timestamp\": \"2019-03-12T12:59:05-04:00\",\n      \"url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/commit/a68f9e81f70b5ac645739f244cdc95a0da24310e\",\n      \"author\": {\n        \"name\": \"John Smith\",\n        \"email\": \"_USER_NAME@example.com\",\n        \"username\": \"_USER_NAME\"\n      },\n      \"committer\": {\n        \"name\": \"John Smith\",\n        \"email\": \"_USER_NAME@example.com\",\n        \"username\": \"_USER_NAME\"\n      },\n      \"added\": [],\n      \"removed\": [],\n      \"modified\": [\n        \"concord.yml\"\n      ]\n    }\n  ],\n  \"head_commit\": {\n    \"id\": \"a68f9e81f70b5ac645739f244cdc95a0da24310e\",\n    \"tree_id\": \"5e0785caefd8d107f75d18a26907f68c7168a8cc\",\n    \"distinct\": true,\n    \"message\": \"bump\",\n    \"timestamp\": \"2019-03-12T12:59:05-04:00\",\n    \"url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/commit/a68f9e81f70b5ac645739f244cdc95a0da24310e\",\n    \"author\": {\n      \"name\": \"John Smith\",\n      \"email\": \"_USER_NAME@example.com\",\n      \"username\": \"_USER_NAME\"\n    },\n    \"committer\": {\n      \"name\": \"John Smith\",\n      \"email\": \"_USER_NAME@example.com\",\n      \"username\": \"_USER_NAME\"\n    },\n    \"added\": [],\n    \"removed\": [],\n    \"modified\": [\n      \"concord.yml\"\n    ]\n  },\n  \"repository\": {\n    \"id\": 80654,\n    \"node_id\": \"MDEwOlJlcG9zaXRvcnk4MDY1NA==\",\n    \"name\": \"_REPO_NAME\",\n    \"full_name\": \"_FULL_REPO_NAME\",\n    \"owner\": {\n      \"name\": \"_ORG_NAME\",\n      \"email\": \"_ORG_NAME@email.example.com\",\n      \"login\": \"_ORG_NAME\",\n      \"id\": 6582,\n      \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n      \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n      \"html_url\": \"https://github.example.com/_ORG_NAME\",\n      \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n      \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n      \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n      \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n      \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n      \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n      \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n      \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n      \"type\": \"Organization\",\n      \"site_admin\": false\n    },\n    \"private\": false,\n    \"html_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"description\": null,\n    \"fork\": false,\n    \"url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"forks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/forks\",\n    \"keys_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/keys{/key_id}\",\n    \"collaborators_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/collaborators{/collaborator}\",\n    \"teams_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/teams\",\n    \"hooks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/hooks\",\n    \"issue_events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/events{/number}\",\n    \"events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/events\",\n    \"assignees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/assignees{/user}\",\n    \"branches_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/branches{/branch}\",\n    \"tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/tags\",\n    \"blobs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/blobs{/sha}\",\n    \"git_tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/tags{/sha}\",\n    \"git_refs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/refs{/sha}\",\n    \"trees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/trees{/sha}\",\n    \"statuses_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/{sha}\",\n    \"languages_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/languages\",\n    \"stargazers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/stargazers\",\n    \"contributors_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contributors\",\n    \"subscribers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscribers\",\n    \"subscription_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscription\",\n    \"commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/commits{/sha}\",\n    \"git_commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/commits{/sha}\",\n    \"comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/comments{/number}\",\n    \"issue_comment_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/comments{/number}\",\n    \"contents_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contents/{+path}\",\n    \"compare_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/compare/{base}...{head}\",\n    \"merges_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/merges\",\n    \"archive_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/{archive_format}{/ref}\",\n    \"downloads_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/downloads\",\n    \"issues_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues{/number}\",\n    \"pulls_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls{/number}\",\n    \"milestones_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/milestones{/number}\",\n    \"notifications_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/notifications{?since,all,participating}\",\n    \"labels_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/labels{/name}\",\n    \"releases_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/releases{/id}\",\n    \"deployments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/deployments\",\n    \"created_at\": 1509843534,\n    \"updated_at\": \"2019-03-07T15:23:22Z\",\n    \"pushed_at\": 1552409951,\n    \"git_url\": \"git://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n    \"ssh_url\": \"git@github.example.com:_ORG_NAME/_REPO_NAME.git\",\n    \"clone_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n    \"svn_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"homepage\": null,\n    \"size\": 79,\n    \"stargazers_count\": 0,\n    \"watchers_count\": 0,\n    \"language\": null,\n    \"has_issues\": true,\n    \"has_projects\": true,\n    \"has_downloads\": true,\n    \"has_wiki\": true,\n    \"has_pages\": false,\n    \"forks_count\": 2,\n    \"mirror_url\": null,\n    \"archived\": false,\n    \"open_issues_count\": 3,\n    \"license\": null,\n    \"forks\": 2,\n    \"open_issues\": 3,\n    \"watchers\": 0,\n    \"default_branch\": \"master\",\n    \"stargazers\": 0,\n    \"master_branch\": \"master\",\n    \"organization\": \"_ORG_NAME\"\n  },\n  \"pusher\": {\n    \"name\": \"_USER_NAME\",\n    \"email\": \"_USER_NAME@example.com\"\n  },\n  \"organization\": {\n    \"login\": \"_ORG_NAME\",\n    \"id\": 6582,\n    \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n    \"url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME\",\n    \"repos_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/repos\",\n    \"events_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/events\",\n    \"hooks_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/hooks\",\n    \"issues_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/issues\",\n    \"members_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/members{/member}\",\n    \"public_members_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/public_members{/member}\",\n    \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n    \"description\": \"\"\n  },\n  \"sender\": {\n    \"login\": \"_USER_NAME\",\n    \"id\": 8935,\n    \"node_id\": \"MDQ6VXNlcjg5MzU=\",\n    \"avatar_url\": \"https://github.example.com/avatars/u/8935?\",\n    \"gravatar_id\": \"\",\n    \"url\": \"https://github.example.com/api/v3/users/_USER_NAME\",\n    \"html_url\": \"https://github.example.com/_USER_NAME\",\n    \"followers_url\": \"https://github.example.com/api/v3/users/_USER_NAME/followers\",\n    \"following_url\": \"https://github.example.com/api/v3/users/_USER_NAME/following{/other_user}\",\n    \"gists_url\": \"https://github.example.com/api/v3/users/_USER_NAME/gists{/gist_id}\",\n    \"starred_url\": \"https://github.example.com/api/v3/users/_USER_NAME/starred{/owner}{/repo}\",\n    \"subscriptions_url\": \"https://github.example.com/api/v3/users/_USER_NAME/subscriptions\",\n    \"organizations_url\": \"https://github.example.com/api/v3/users/_USER_NAME/orgs\",\n    \"repos_url\": \"https://github.example.com/api/v3/users/_USER_NAME/repos\",\n    \"events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/events{/privacy}\",\n    \"received_events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/received_events\",\n    \"type\": \"User\",\n    \"site_admin\": false,\n    \"ldap_dn\": \"_USER_LDAP_DN\"\n  }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/triggers/github/events/direct_branch_push_commit_id.json",
    "content": "{\n  \"ref\": \"_REF\",\n  \"before\": \"1083c12463e8122e1ff7adf06ec8aabbe998fec4\",\n  \"after\": \"_COMMIT_ID\",\n  \"created\": false,\n  \"deleted\": false,\n  \"forced\": false,\n  \"base_ref\": null,\n  \"compare\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/compare/1083c12463e8...a68f9e81f70b\",\n  \"commits\": [\n    {\n      \"id\": \"a68f9e81f70b5ac645739f244cdc95a0da24310e\",\n      \"tree_id\": \"5e0785caefd8d107f75d18a26907f68c7168a8cc\",\n      \"distinct\": true,\n      \"message\": \"bump\",\n      \"timestamp\": \"2019-03-12T12:59:05-04:00\",\n      \"url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/commit/a68f9e81f70b5ac645739f244cdc95a0da24310e\",\n      \"author\": {\n        \"name\": \"John Smith\",\n        \"email\": \"_USER_NAME@example.com\",\n        \"username\": \"_USER_NAME\"\n      },\n      \"committer\": {\n        \"name\": \"John Smith\",\n        \"email\": \"_USER_NAME@example.com\",\n        \"username\": \"_USER_NAME\"\n      },\n      \"added\": [],\n      \"removed\": [],\n      \"modified\": [\n        \"concord.yml\"\n      ]\n    }\n  ],\n  \"head_commit\": {\n    \"id\": \"a68f9e81f70b5ac645739f244cdc95a0da24310e\",\n    \"tree_id\": \"5e0785caefd8d107f75d18a26907f68c7168a8cc\",\n    \"distinct\": true,\n    \"message\": \"bump\",\n    \"timestamp\": \"2019-03-12T12:59:05-04:00\",\n    \"url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/commit/a68f9e81f70b5ac645739f244cdc95a0da24310e\",\n    \"author\": {\n      \"name\": \"John Smith\",\n      \"email\": \"_USER_NAME@example.com\",\n      \"username\": \"_USER_NAME\"\n    },\n    \"committer\": {\n      \"name\": \"John Smith\",\n      \"email\": \"_USER_NAME@example.com\",\n      \"username\": \"_USER_NAME\"\n    },\n    \"added\": [],\n    \"removed\": [],\n    \"modified\": [\n      \"concord.yml\"\n    ]\n  },\n  \"repository\": {\n    \"id\": 80654,\n    \"node_id\": \"MDEwOlJlcG9zaXRvcnk4MDY1NA==\",\n    \"name\": \"_REPO_NAME\",\n    \"full_name\": \"_FULL_REPO_NAME\",\n    \"owner\": {\n      \"name\": \"_ORG_NAME\",\n      \"email\": \"_ORG_NAME@email.example.com\",\n      \"login\": \"_ORG_NAME\",\n      \"id\": 6582,\n      \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n      \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n      \"html_url\": \"https://github.example.com/_ORG_NAME\",\n      \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n      \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n      \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n      \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n      \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n      \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n      \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n      \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n      \"type\": \"Organization\",\n      \"site_admin\": false\n    },\n    \"private\": false,\n    \"html_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"description\": null,\n    \"fork\": false,\n    \"url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"forks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/forks\",\n    \"keys_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/keys{/key_id}\",\n    \"collaborators_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/collaborators{/collaborator}\",\n    \"teams_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/teams\",\n    \"hooks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/hooks\",\n    \"issue_events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/events{/number}\",\n    \"events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/events\",\n    \"assignees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/assignees{/user}\",\n    \"branches_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/branches{/branch}\",\n    \"tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/tags\",\n    \"blobs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/blobs{/sha}\",\n    \"git_tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/tags{/sha}\",\n    \"git_refs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/refs{/sha}\",\n    \"trees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/trees{/sha}\",\n    \"statuses_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/{sha}\",\n    \"languages_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/languages\",\n    \"stargazers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/stargazers\",\n    \"contributors_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contributors\",\n    \"subscribers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscribers\",\n    \"subscription_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscription\",\n    \"commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/commits{/sha}\",\n    \"git_commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/commits{/sha}\",\n    \"comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/comments{/number}\",\n    \"issue_comment_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/comments{/number}\",\n    \"contents_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contents/{+path}\",\n    \"compare_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/compare/{base}...{head}\",\n    \"merges_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/merges\",\n    \"archive_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/{archive_format}{/ref}\",\n    \"downloads_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/downloads\",\n    \"issues_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues{/number}\",\n    \"pulls_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls{/number}\",\n    \"milestones_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/milestones{/number}\",\n    \"notifications_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/notifications{?since,all,participating}\",\n    \"labels_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/labels{/name}\",\n    \"releases_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/releases{/id}\",\n    \"deployments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/deployments\",\n    \"created_at\": 1509843534,\n    \"updated_at\": \"2019-03-07T15:23:22Z\",\n    \"pushed_at\": 1552409951,\n    \"git_url\": \"git://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n    \"ssh_url\": \"git@github.example.com:_ORG_NAME/_REPO_NAME.git\",\n    \"clone_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n    \"svn_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"homepage\": null,\n    \"size\": 79,\n    \"stargazers_count\": 0,\n    \"watchers_count\": 0,\n    \"language\": null,\n    \"has_issues\": true,\n    \"has_projects\": true,\n    \"has_downloads\": true,\n    \"has_wiki\": true,\n    \"has_pages\": false,\n    \"forks_count\": 2,\n    \"mirror_url\": null,\n    \"archived\": false,\n    \"open_issues_count\": 3,\n    \"license\": null,\n    \"forks\": 2,\n    \"open_issues\": 3,\n    \"watchers\": 0,\n    \"default_branch\": \"master\",\n    \"stargazers\": 0,\n    \"master_branch\": \"master\",\n    \"organization\": \"_ORG_NAME\"\n  },\n  \"pusher\": {\n    \"name\": \"_USER_NAME\",\n    \"email\": \"_USER_NAME@example.com\"\n  },\n  \"organization\": {\n    \"login\": \"_ORG_NAME\",\n    \"id\": 6582,\n    \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n    \"url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME\",\n    \"repos_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/repos\",\n    \"events_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/events\",\n    \"hooks_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/hooks\",\n    \"issues_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/issues\",\n    \"members_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/members{/member}\",\n    \"public_members_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/public_members{/member}\",\n    \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n    \"description\": \"\"\n  },\n  \"sender\": {\n    \"login\": \"_USER_NAME\",\n    \"id\": 8935,\n    \"node_id\": \"MDQ6VXNlcjg5MzU=\",\n    \"avatar_url\": \"https://github.example.com/avatars/u/8935?\",\n    \"gravatar_id\": \"\",\n    \"url\": \"https://github.example.com/api/v3/users/_USER_NAME\",\n    \"html_url\": \"https://github.example.com/_USER_NAME\",\n    \"followers_url\": \"https://github.example.com/api/v3/users/_USER_NAME/followers\",\n    \"following_url\": \"https://github.example.com/api/v3/users/_USER_NAME/following{/other_user}\",\n    \"gists_url\": \"https://github.example.com/api/v3/users/_USER_NAME/gists{/gist_id}\",\n    \"starred_url\": \"https://github.example.com/api/v3/users/_USER_NAME/starred{/owner}{/repo}\",\n    \"subscriptions_url\": \"https://github.example.com/api/v3/users/_USER_NAME/subscriptions\",\n    \"organizations_url\": \"https://github.example.com/api/v3/users/_USER_NAME/orgs\",\n    \"repos_url\": \"https://github.example.com/api/v3/users/_USER_NAME/repos\",\n    \"events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/events{/privacy}\",\n    \"received_events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/received_events\",\n    \"type\": \"User\",\n    \"site_admin\": false,\n    \"ldap_dn\": \"_USER_LDAP_DN\"\n  }\n}\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/triggers/github/events/pr_close.json",
    "content": "{\n  \"action\": \"closed\",\n  \"number\": 12,\n  \"pull_request\": {\n    \"url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12\",\n    \"id\": 1010948,\n    \"node_id\": \"MDExOlB1bGxSZXF1ZXN0MTAxMDk0OA==\",\n    \"html_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/pull/12\",\n    \"diff_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/pull/12.diff\",\n    \"patch_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/pull/12.patch\",\n    \"issue_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/12\",\n    \"number\": 12,\n    \"state\": \"closed\",\n    \"locked\": false,\n    \"title\": \"bump\",\n    \"user\": {\n      \"login\": \"_USER_NAME\",\n      \"id\": 8935,\n      \"node_id\": \"MDQ6VXNlcjg5MzU=\",\n      \"avatar_url\": \"https://github.example.com/avatars/u/8935?\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://github.example.com/api/v3/users/_USER_NAME\",\n      \"html_url\": \"https://github.example.com/_USER_NAME\",\n      \"followers_url\": \"https://github.example.com/api/v3/users/_USER_NAME/followers\",\n      \"following_url\": \"https://github.example.com/api/v3/users/_USER_NAME/following{/other_user}\",\n      \"gists_url\": \"https://github.example.com/api/v3/users/_USER_NAME/gists{/gist_id}\",\n      \"starred_url\": \"https://github.example.com/api/v3/users/_USER_NAME/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://github.example.com/api/v3/users/_USER_NAME/subscriptions\",\n      \"organizations_url\": \"https://github.example.com/api/v3/users/_USER_NAME/orgs\",\n      \"repos_url\": \"https://github.example.com/api/v3/users/_USER_NAME/repos\",\n      \"events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/events{/privacy}\",\n      \"received_events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false,\n      \"ldap_dn\": \"_USER_LDAP_DN\"\n    },\n    \"body\": \"\",\n    \"created_at\": \"2019-03-12T17:56:16Z\",\n    \"updated_at\": \"2019-03-12T17:59:46Z\",\n    \"closed_at\": \"2019-03-12T17:59:46Z\",\n    \"merged_at\": \"2019-03-12T17:59:46Z\",\n    \"merge_commit_sha\": \"ea21d5e14c6b6c8313f0920113d46afc8e6dc04d\",\n    \"assignee\": null,\n    \"assignees\": [],\n    \"requested_reviewers\": [],\n    \"requested_teams\": [],\n    \"labels\": [],\n    \"milestone\": null,\n    \"commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12/commits\",\n    \"review_comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12/comments\",\n    \"review_comment_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/comments{/number}\",\n    \"comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/12/comments\",\n    \"statuses_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/85e9a1f7e757aa6b82ea66206910c8ae758b3b0d\",\n    \"head\": {\n      \"label\": \"_ORG_NAME:pr-test-3\",\n      \"ref\": \"pr-test-3\",\n      \"sha\": \"85e9a1f7e757aa6b82ea66206910c8ae758b3b0d\",\n      \"user\": {\n        \"login\": \"_ORG_NAME\",\n        \"id\": 6582,\n        \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n        \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n        \"gravatar_id\": \"\",\n        \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n        \"html_url\": \"https://github.example.com/_ORG_NAME\",\n        \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n        \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n        \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n        \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n        \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n        \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n        \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n        \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n        \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n        \"type\": \"Organization\",\n        \"site_admin\": false\n      },\n      \"repo\": {\n        \"id\": 80654,\n        \"node_id\": \"MDEwOlJlcG9zaXRvcnk4MDY1NA==\",\n        \"name\": \"_REPO_NAME\",\n        \"full_name\": \"_FULL_REPO_NAME\",\n        \"owner\": {\n          \"login\": \"_ORG_NAME\",\n          \"id\": 6582,\n          \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n          \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n          \"gravatar_id\": \"\",\n          \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n          \"html_url\": \"https://github.example.com/_ORG_NAME\",\n          \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n          \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n          \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n          \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n          \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n          \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n          \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n          \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n          \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n          \"type\": \"Organization\",\n          \"site_admin\": false\n        },\n        \"private\": false,\n        \"html_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n        \"description\": null,\n        \"fork\": false,\n        \"url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME\",\n        \"forks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/forks\",\n        \"keys_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/keys{/key_id}\",\n        \"collaborators_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/collaborators{/collaborator}\",\n        \"teams_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/teams\",\n        \"hooks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/hooks\",\n        \"issue_events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/events{/number}\",\n        \"events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/events\",\n        \"assignees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/assignees{/user}\",\n        \"branches_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/branches{/branch}\",\n        \"tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/tags\",\n        \"blobs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/blobs{/sha}\",\n        \"git_tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/tags{/sha}\",\n        \"git_refs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/refs{/sha}\",\n        \"trees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/trees{/sha}\",\n        \"statuses_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/{sha}\",\n        \"languages_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/languages\",\n        \"stargazers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/stargazers\",\n        \"contributors_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contributors\",\n        \"subscribers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscribers\",\n        \"subscription_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscription\",\n        \"commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/commits{/sha}\",\n        \"git_commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/commits{/sha}\",\n        \"comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/comments{/number}\",\n        \"issue_comment_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/comments{/number}\",\n        \"contents_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contents/{+path}\",\n        \"compare_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/compare/{base}...{head}\",\n        \"merges_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/merges\",\n        \"archive_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/{archive_format}{/ref}\",\n        \"downloads_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/downloads\",\n        \"issues_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues{/number}\",\n        \"pulls_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls{/number}\",\n        \"milestones_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/milestones{/number}\",\n        \"notifications_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/notifications{?since,all,participating}\",\n        \"labels_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/labels{/name}\",\n        \"releases_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/releases{/id}\",\n        \"deployments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/deployments\",\n        \"created_at\": \"2017-11-05T00:58:54Z\",\n        \"updated_at\": \"2019-03-12T17:55:49Z\",\n        \"pushed_at\": \"2019-03-12T17:59:46Z\",\n        \"git_url\": \"git://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n        \"ssh_url\": \"git@github.example.com:_ORG_NAME/_REPO_NAME.git\",\n        \"clone_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n        \"svn_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n        \"homepage\": null,\n        \"size\": 79,\n        \"stargazers_count\": 0,\n        \"watchers_count\": 0,\n        \"language\": null,\n        \"has_issues\": true,\n        \"has_projects\": true,\n        \"has_downloads\": true,\n        \"has_wiki\": true,\n        \"has_pages\": false,\n        \"forks_count\": 2,\n        \"mirror_url\": null,\n        \"archived\": false,\n        \"open_issues_count\": 3,\n        \"license\": null,\n        \"forks\": 2,\n        \"open_issues\": 3,\n        \"watchers\": 0,\n        \"default_branch\": \"master\"\n      }\n    },\n    \"base\": {\n      \"label\": \"_ORG_NAME:master\",\n      \"ref\": \"master\",\n      \"sha\": \"ce08c4051a9a7d7e34e12f717a15bf788ce2a3a4\",\n      \"user\": {\n        \"login\": \"_ORG_NAME\",\n        \"id\": 6582,\n        \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n        \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n        \"gravatar_id\": \"\",\n        \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n        \"html_url\": \"https://github.example.com/_ORG_NAME\",\n        \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n        \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n        \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n        \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n        \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n        \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n        \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n        \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n        \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n        \"type\": \"Organization\",\n        \"site_admin\": false\n      },\n      \"repo\": {\n        \"id\": 80654,\n        \"node_id\": \"MDEwOlJlcG9zaXRvcnk4MDY1NA==\",\n        \"name\": \"_REPO_NAME\",\n        \"full_name\": \"_FULL_REPO_NAME\",\n        \"owner\": {\n          \"login\": \"_ORG_NAME\",\n          \"id\": 6582,\n          \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n          \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n          \"gravatar_id\": \"\",\n          \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n          \"html_url\": \"https://github.example.com/_ORG_NAME\",\n          \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n          \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n          \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n          \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n          \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n          \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n          \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n          \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n          \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n          \"type\": \"Organization\",\n          \"site_admin\": false\n        },\n        \"private\": false,\n        \"html_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n        \"description\": null,\n        \"fork\": false,\n        \"url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME\",\n        \"forks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/forks\",\n        \"keys_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/keys{/key_id}\",\n        \"collaborators_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/collaborators{/collaborator}\",\n        \"teams_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/teams\",\n        \"hooks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/hooks\",\n        \"issue_events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/events{/number}\",\n        \"events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/events\",\n        \"assignees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/assignees{/user}\",\n        \"branches_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/branches{/branch}\",\n        \"tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/tags\",\n        \"blobs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/blobs{/sha}\",\n        \"git_tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/tags{/sha}\",\n        \"git_refs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/refs{/sha}\",\n        \"trees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/trees{/sha}\",\n        \"statuses_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/{sha}\",\n        \"languages_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/languages\",\n        \"stargazers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/stargazers\",\n        \"contributors_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contributors\",\n        \"subscribers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscribers\",\n        \"subscription_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscription\",\n        \"commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/commits{/sha}\",\n        \"git_commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/commits{/sha}\",\n        \"comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/comments{/number}\",\n        \"issue_comment_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/comments{/number}\",\n        \"contents_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contents/{+path}\",\n        \"compare_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/compare/{base}...{head}\",\n        \"merges_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/merges\",\n        \"archive_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/{archive_format}{/ref}\",\n        \"downloads_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/downloads\",\n        \"issues_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues{/number}\",\n        \"pulls_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls{/number}\",\n        \"milestones_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/milestones{/number}\",\n        \"notifications_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/notifications{?since,all,participating}\",\n        \"labels_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/labels{/name}\",\n        \"releases_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/releases{/id}\",\n        \"deployments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/deployments\",\n        \"created_at\": \"2017-11-05T00:58:54Z\",\n        \"updated_at\": \"2019-03-12T17:55:49Z\",\n        \"pushed_at\": \"2019-03-12T17:59:46Z\",\n        \"git_url\": \"git://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n        \"ssh_url\": \"git@github.example.com:_ORG_NAME/_REPO_NAME.git\",\n        \"clone_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n        \"svn_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n        \"homepage\": null,\n        \"size\": 79,\n        \"stargazers_count\": 0,\n        \"watchers_count\": 0,\n        \"language\": null,\n        \"has_issues\": true,\n        \"has_projects\": true,\n        \"has_downloads\": true,\n        \"has_wiki\": true,\n        \"has_pages\": false,\n        \"forks_count\": 2,\n        \"mirror_url\": null,\n        \"archived\": false,\n        \"open_issues_count\": 3,\n        \"license\": null,\n        \"forks\": 2,\n        \"open_issues\": 3,\n        \"watchers\": 0,\n        \"default_branch\": \"master\"\n      }\n    },\n    \"_links\": {\n      \"self\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12\"\n      },\n      \"html\": {\n        \"href\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/pull/12\"\n      },\n      \"issue\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/12\"\n      },\n      \"comments\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/12/comments\"\n      },\n      \"review_comments\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12/comments\"\n      },\n      \"review_comment\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/comments{/number}\"\n      },\n      \"commits\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12/commits\"\n      },\n      \"statuses\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/85e9a1f7e757aa6b82ea66206910c8ae758b3b0d\"\n      }\n    },\n    \"author_association\": \"COLLABORATOR\",\n    \"merged\": true,\n    \"mergeable\": null,\n    \"rebaseable\": null,\n    \"mergeable_state\": \"unknown\",\n    \"merged_by\": {\n      \"login\": \"_USER_NAME\",\n      \"id\": 8935,\n      \"node_id\": \"MDQ6VXNlcjg5MzU=\",\n      \"avatar_url\": \"https://github.example.com/avatars/u/8935?\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://github.example.com/api/v3/users/_USER_NAME\",\n      \"html_url\": \"https://github.example.com/_USER_NAME\",\n      \"followers_url\": \"https://github.example.com/api/v3/users/_USER_NAME/followers\",\n      \"following_url\": \"https://github.example.com/api/v3/users/_USER_NAME/following{/other_user}\",\n      \"gists_url\": \"https://github.example.com/api/v3/users/_USER_NAME/gists{/gist_id}\",\n      \"starred_url\": \"https://github.example.com/api/v3/users/_USER_NAME/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://github.example.com/api/v3/users/_USER_NAME/subscriptions\",\n      \"organizations_url\": \"https://github.example.com/api/v3/users/_USER_NAME/orgs\",\n      \"repos_url\": \"https://github.example.com/api/v3/users/_USER_NAME/repos\",\n      \"events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/events{/privacy}\",\n      \"received_events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false,\n      \"ldap_dn\": \"_USER_LDAP_DN\"\n    },\n    \"comments\": 0,\n    \"review_comments\": 0,\n    \"maintainer_can_modify\": false,\n    \"commits\": 1,\n    \"additions\": 0,\n    \"deletions\": 1,\n    \"changed_files\": 1\n  },\n  \"repository\": {\n    \"id\": 80654,\n    \"node_id\": \"MDEwOlJlcG9zaXRvcnk4MDY1NA==\",\n    \"name\": \"_REPO_NAME\",\n    \"full_name\": \"_FULL_REPO_NAME\",\n    \"owner\": {\n      \"login\": \"_ORG_NAME\",\n      \"id\": 6582,\n      \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n      \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n      \"html_url\": \"https://github.example.com/_ORG_NAME\",\n      \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n      \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n      \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n      \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n      \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n      \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n      \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n      \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n      \"type\": \"Organization\",\n      \"site_admin\": false\n    },\n    \"private\": false,\n    \"html_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"description\": null,\n    \"fork\": false,\n    \"url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME\",\n    \"forks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/forks\",\n    \"keys_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/keys{/key_id}\",\n    \"collaborators_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/collaborators{/collaborator}\",\n    \"teams_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/teams\",\n    \"hooks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/hooks\",\n    \"issue_events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/events{/number}\",\n    \"events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/events\",\n    \"assignees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/assignees{/user}\",\n    \"branches_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/branches{/branch}\",\n    \"tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/tags\",\n    \"blobs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/blobs{/sha}\",\n    \"git_tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/tags{/sha}\",\n    \"git_refs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/refs{/sha}\",\n    \"trees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/trees{/sha}\",\n    \"statuses_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/{sha}\",\n    \"languages_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/languages\",\n    \"stargazers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/stargazers\",\n    \"contributors_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contributors\",\n    \"subscribers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscribers\",\n    \"subscription_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscription\",\n    \"commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/commits{/sha}\",\n    \"git_commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/commits{/sha}\",\n    \"comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/comments{/number}\",\n    \"issue_comment_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/comments{/number}\",\n    \"contents_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contents/{+path}\",\n    \"compare_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/compare/{base}...{head}\",\n    \"merges_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/merges\",\n    \"archive_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/{archive_format}{/ref}\",\n    \"downloads_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/downloads\",\n    \"issues_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues{/number}\",\n    \"pulls_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls{/number}\",\n    \"milestones_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/milestones{/number}\",\n    \"notifications_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/notifications{?since,all,participating}\",\n    \"labels_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/labels{/name}\",\n    \"releases_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/releases{/id}\",\n    \"deployments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/deployments\",\n    \"created_at\": \"2017-11-05T00:58:54Z\",\n    \"updated_at\": \"2019-03-12T17:55:49Z\",\n    \"pushed_at\": \"2019-03-12T17:59:46Z\",\n    \"git_url\": \"git://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n    \"ssh_url\": \"git@github.example.com:_ORG_NAME/_REPO_NAME.git\",\n    \"clone_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n    \"svn_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"homepage\": null,\n    \"size\": 79,\n    \"stargazers_count\": 0,\n    \"watchers_count\": 0,\n    \"language\": null,\n    \"has_issues\": true,\n    \"has_projects\": true,\n    \"has_downloads\": true,\n    \"has_wiki\": true,\n    \"has_pages\": false,\n    \"forks_count\": 2,\n    \"mirror_url\": null,\n    \"archived\": false,\n    \"open_issues_count\": 3,\n    \"license\": null,\n    \"forks\": 2,\n    \"open_issues\": 3,\n    \"watchers\": 0,\n    \"default_branch\": \"master\"\n  },\n  \"organization\": {\n    \"login\": \"_ORG_NAME\",\n    \"id\": 6582,\n    \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n    \"url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME\",\n    \"repos_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/repos\",\n    \"events_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/events\",\n    \"hooks_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/hooks\",\n    \"issues_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/issues\",\n    \"members_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/members{/member}\",\n    \"public_members_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/public_members{/member}\",\n    \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n    \"description\": \"\"\n  },\n  \"sender\": {\n    \"login\": \"_USER_NAME\",\n    \"id\": 8935,\n    \"node_id\": \"MDQ6VXNlcjg5MzU=\",\n    \"avatar_url\": \"https://github.example.com/avatars/u/8935?\",\n    \"gravatar_id\": \"\",\n    \"url\": \"https://github.example.com/api/v3/users/_USER_NAME\",\n    \"html_url\": \"https://github.example.com/_USER_NAME\",\n    \"followers_url\": \"https://github.example.com/api/v3/users/_USER_NAME/followers\",\n    \"following_url\": \"https://github.example.com/api/v3/users/_USER_NAME/following{/other_user}\",\n    \"gists_url\": \"https://github.example.com/api/v3/users/_USER_NAME/gists{/gist_id}\",\n    \"starred_url\": \"https://github.example.com/api/v3/users/_USER_NAME/starred{/owner}{/repo}\",\n    \"subscriptions_url\": \"https://github.example.com/api/v3/users/_USER_NAME/subscriptions\",\n    \"organizations_url\": \"https://github.example.com/api/v3/users/_USER_NAME/orgs\",\n    \"repos_url\": \"https://github.example.com/api/v3/users/_USER_NAME/repos\",\n    \"events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/events{/privacy}\",\n    \"received_events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/received_events\",\n    \"type\": \"User\",\n    \"site_admin\": false,\n    \"ldap_dn\": \"_USER_LDAP_DN\"\n  }\n}"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/triggers/github/events/pr_open.json",
    "content": "{\n  \"action\": \"opened\",\n  \"number\": 12,\n  \"pull_request\": {\n    \"url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12\",\n    \"id\": 1010948,\n    \"node_id\": \"MDExOlB1bGxSZXF1ZXN0MTAxMDk0OA==\",\n    \"html_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/pull/12\",\n    \"diff_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/pull/12.diff\",\n    \"patch_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/pull/12.patch\",\n    \"issue_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/12\",\n    \"number\": 12,\n    \"state\": \"open\",\n    \"locked\": false,\n    \"title\": \"bump\",\n    \"user\": {\n      \"login\": \"_USER_NAME\",\n      \"id\": 8935,\n      \"node_id\": \"MDQ6VXNlcjg5MzU=\",\n      \"avatar_url\": \"https://github.example.com/avatars/u/8935?\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://github.example.com/api/v3/users/_USER_NAME\",\n      \"html_url\": \"https://github.example.com/_USER_NAME\",\n      \"followers_url\": \"https://github.example.com/api/v3/users/_USER_NAME/followers\",\n      \"following_url\": \"https://github.example.com/api/v3/users/_USER_NAME/following{/other_user}\",\n      \"gists_url\": \"https://github.example.com/api/v3/users/_USER_NAME/gists{/gist_id}\",\n      \"starred_url\": \"https://github.example.com/api/v3/users/_USER_NAME/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://github.example.com/api/v3/users/_USER_NAME/subscriptions\",\n      \"organizations_url\": \"https://github.example.com/api/v3/users/_USER_NAME/orgs\",\n      \"repos_url\": \"https://github.example.com/api/v3/users/_USER_NAME/repos\",\n      \"events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/events{/privacy}\",\n      \"received_events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false,\n      \"ldap_dn\": \"_USER_LDAP_DN\"\n    },\n    \"body\": \"\",\n    \"created_at\": \"2019-03-12T17:56:16Z\",\n    \"updated_at\": \"2019-03-12T17:56:16Z\",\n    \"closed_at\": null,\n    \"merged_at\": null,\n    \"merge_commit_sha\": null,\n    \"assignee\": null,\n    \"assignees\": [],\n    \"requested_reviewers\": [],\n    \"requested_teams\": [],\n    \"labels\": [],\n    \"milestone\": null,\n    \"commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12/commits\",\n    \"review_comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12/comments\",\n    \"review_comment_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/comments{/number}\",\n    \"comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/12/comments\",\n    \"statuses_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/85e9a1f7e757aa6b82ea66206910c8ae758b3b0d\",\n    \"head\": {\n      \"label\": \"_ORG_NAME:pr-test-3\",\n      \"ref\": \"pr-test-3\",\n      \"sha\": \"85e9a1f7e757aa6b82ea66206910c8ae758b3b0d\",\n      \"user\": {\n        \"login\": \"_ORG_NAME\",\n        \"id\": 6582,\n        \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n        \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n        \"gravatar_id\": \"\",\n        \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n        \"html_url\": \"https://github.example.com/_ORG_NAME\",\n        \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n        \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n        \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n        \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n        \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n        \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n        \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n        \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n        \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n        \"type\": \"Organization\",\n        \"site_admin\": false\n      },\n      \"repo\": {\n        \"id\": 80654,\n        \"node_id\": \"MDEwOlJlcG9zaXRvcnk4MDY1NA==\",\n        \"name\": \"_REPO_NAME\",\n        \"full_name\": \"_FULL_REPO_NAME\",\n        \"owner\": {\n          \"login\": \"_ORG_NAME\",\n          \"id\": 6582,\n          \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n          \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n          \"gravatar_id\": \"\",\n          \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n          \"html_url\": \"https://github.example.com/_ORG_NAME\",\n          \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n          \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n          \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n          \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n          \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n          \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n          \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n          \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n          \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n          \"type\": \"Organization\",\n          \"site_admin\": false\n        },\n        \"private\": false,\n        \"html_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n        \"description\": null,\n        \"fork\": false,\n        \"url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME\",\n        \"forks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/forks\",\n        \"keys_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/keys{/key_id}\",\n        \"collaborators_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/collaborators{/collaborator}\",\n        \"teams_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/teams\",\n        \"hooks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/hooks\",\n        \"issue_events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/events{/number}\",\n        \"events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/events\",\n        \"assignees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/assignees{/user}\",\n        \"branches_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/branches{/branch}\",\n        \"tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/tags\",\n        \"blobs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/blobs{/sha}\",\n        \"git_tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/tags{/sha}\",\n        \"git_refs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/refs{/sha}\",\n        \"trees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/trees{/sha}\",\n        \"statuses_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/{sha}\",\n        \"languages_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/languages\",\n        \"stargazers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/stargazers\",\n        \"contributors_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contributors\",\n        \"subscribers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscribers\",\n        \"subscription_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscription\",\n        \"commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/commits{/sha}\",\n        \"git_commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/commits{/sha}\",\n        \"comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/comments{/number}\",\n        \"issue_comment_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/comments{/number}\",\n        \"contents_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contents/{+path}\",\n        \"compare_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/compare/{base}...{head}\",\n        \"merges_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/merges\",\n        \"archive_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/{archive_format}{/ref}\",\n        \"downloads_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/downloads\",\n        \"issues_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues{/number}\",\n        \"pulls_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls{/number}\",\n        \"milestones_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/milestones{/number}\",\n        \"notifications_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/notifications{?since,all,participating}\",\n        \"labels_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/labels{/name}\",\n        \"releases_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/releases{/id}\",\n        \"deployments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/deployments\",\n        \"created_at\": \"2017-11-05T00:58:54Z\",\n        \"updated_at\": \"2019-03-12T17:55:49Z\",\n        \"pushed_at\": \"2019-03-12T17:56:12Z\",\n        \"git_url\": \"git://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n        \"ssh_url\": \"git@github.example.com:_ORG_NAME/_REPO_NAME.git\",\n        \"clone_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n        \"svn_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n        \"homepage\": null,\n        \"size\": 79,\n        \"stargazers_count\": 0,\n        \"watchers_count\": 0,\n        \"language\": null,\n        \"has_issues\": true,\n        \"has_projects\": true,\n        \"has_downloads\": true,\n        \"has_wiki\": true,\n        \"has_pages\": false,\n        \"forks_count\": 2,\n        \"mirror_url\": null,\n        \"archived\": false,\n        \"open_issues_count\": 4,\n        \"license\": null,\n        \"forks\": 2,\n        \"open_issues\": 4,\n        \"watchers\": 0,\n        \"default_branch\": \"master\"\n      }\n    },\n    \"base\": {\n      \"label\": \"_ORG_NAME:master\",\n      \"ref\": \"master\",\n      \"sha\": \"ce08c4051a9a7d7e34e12f717a15bf788ce2a3a4\",\n      \"user\": {\n        \"login\": \"_ORG_NAME\",\n        \"id\": 6582,\n        \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n        \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n        \"gravatar_id\": \"\",\n        \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n        \"html_url\": \"https://github.example.com/_ORG_NAME\",\n        \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n        \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n        \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n        \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n        \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n        \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n        \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n        \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n        \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n        \"type\": \"Organization\",\n        \"site_admin\": false\n      },\n      \"repo\": {\n        \"id\": 80654,\n        \"node_id\": \"MDEwOlJlcG9zaXRvcnk4MDY1NA==\",\n        \"name\": \"_REPO_NAME\",\n        \"full_name\": \"_FULL_REPO_NAME\",\n        \"owner\": {\n          \"login\": \"_ORG_NAME\",\n          \"id\": 6582,\n          \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n          \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n          \"gravatar_id\": \"\",\n          \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n          \"html_url\": \"https://github.example.com/_ORG_NAME\",\n          \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n          \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n          \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n          \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n          \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n          \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n          \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n          \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n          \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n          \"type\": \"Organization\",\n          \"site_admin\": false\n        },\n        \"private\": false,\n        \"html_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n        \"description\": null,\n        \"fork\": false,\n        \"url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME\",\n        \"forks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/forks\",\n        \"keys_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/keys{/key_id}\",\n        \"collaborators_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/collaborators{/collaborator}\",\n        \"teams_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/teams\",\n        \"hooks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/hooks\",\n        \"issue_events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/events{/number}\",\n        \"events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/events\",\n        \"assignees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/assignees{/user}\",\n        \"branches_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/branches{/branch}\",\n        \"tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/tags\",\n        \"blobs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/blobs{/sha}\",\n        \"git_tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/tags{/sha}\",\n        \"git_refs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/refs{/sha}\",\n        \"trees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/trees{/sha}\",\n        \"statuses_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/{sha}\",\n        \"languages_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/languages\",\n        \"stargazers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/stargazers\",\n        \"contributors_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contributors\",\n        \"subscribers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscribers\",\n        \"subscription_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscription\",\n        \"commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/commits{/sha}\",\n        \"git_commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/commits{/sha}\",\n        \"comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/comments{/number}\",\n        \"issue_comment_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/comments{/number}\",\n        \"contents_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contents/{+path}\",\n        \"compare_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/compare/{base}...{head}\",\n        \"merges_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/merges\",\n        \"archive_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/{archive_format}{/ref}\",\n        \"downloads_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/downloads\",\n        \"issues_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues{/number}\",\n        \"pulls_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls{/number}\",\n        \"milestones_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/milestones{/number}\",\n        \"notifications_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/notifications{?since,all,participating}\",\n        \"labels_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/labels{/name}\",\n        \"releases_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/releases{/id}\",\n        \"deployments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/deployments\",\n        \"created_at\": \"2017-11-05T00:58:54Z\",\n        \"updated_at\": \"2019-03-12T17:55:49Z\",\n        \"pushed_at\": \"2019-03-12T17:56:12Z\",\n        \"git_url\": \"git://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n        \"ssh_url\": \"git@github.example.com:_ORG_NAME/_REPO_NAME.git\",\n        \"clone_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n        \"svn_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n        \"homepage\": null,\n        \"size\": 79,\n        \"stargazers_count\": 0,\n        \"watchers_count\": 0,\n        \"language\": null,\n        \"has_issues\": true,\n        \"has_projects\": true,\n        \"has_downloads\": true,\n        \"has_wiki\": true,\n        \"has_pages\": false,\n        \"forks_count\": 2,\n        \"mirror_url\": null,\n        \"archived\": false,\n        \"open_issues_count\": 4,\n        \"license\": null,\n        \"forks\": 2,\n        \"open_issues\": 4,\n        \"watchers\": 0,\n        \"default_branch\": \"master\"\n      }\n    },\n    \"_links\": {\n      \"self\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12\"\n      },\n      \"html\": {\n        \"href\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/pull/12\"\n      },\n      \"issue\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/12\"\n      },\n      \"comments\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/12/comments\"\n      },\n      \"review_comments\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12/comments\"\n      },\n      \"review_comment\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/comments{/number}\"\n      },\n      \"commits\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12/commits\"\n      },\n      \"statuses\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/85e9a1f7e757aa6b82ea66206910c8ae758b3b0d\"\n      }\n    },\n    \"author_association\": \"COLLABORATOR\",\n    \"merged\": false,\n    \"mergeable\": null,\n    \"rebaseable\": null,\n    \"mergeable_state\": \"unknown\",\n    \"merged_by\": null,\n    \"comments\": 0,\n    \"review_comments\": 0,\n    \"maintainer_can_modify\": false,\n    \"commits\": 1,\n    \"additions\": 0,\n    \"deletions\": 1,\n    \"changed_files\": 1\n  },\n  \"repository\": {\n    \"id\": 80654,\n    \"node_id\": \"MDEwOlJlcG9zaXRvcnk4MDY1NA==\",\n    \"name\": \"_REPO_NAME\",\n    \"full_name\": \"_FULL_REPO_NAME\",\n    \"owner\": {\n      \"login\": \"_ORG_NAME\",\n      \"id\": 6582,\n      \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n      \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n      \"html_url\": \"https://github.example.com/_ORG_NAME\",\n      \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n      \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n      \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n      \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n      \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n      \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n      \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n      \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n      \"type\": \"Organization\",\n      \"site_admin\": false\n    },\n    \"private\": false,\n    \"html_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"description\": null,\n    \"fork\": false,\n    \"url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME\",\n    \"forks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/forks\",\n    \"keys_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/keys{/key_id}\",\n    \"collaborators_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/collaborators{/collaborator}\",\n    \"teams_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/teams\",\n    \"hooks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/hooks\",\n    \"issue_events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/events{/number}\",\n    \"events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/events\",\n    \"assignees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/assignees{/user}\",\n    \"branches_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/branches{/branch}\",\n    \"tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/tags\",\n    \"blobs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/blobs{/sha}\",\n    \"git_tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/tags{/sha}\",\n    \"git_refs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/refs{/sha}\",\n    \"trees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/trees{/sha}\",\n    \"statuses_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/{sha}\",\n    \"languages_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/languages\",\n    \"stargazers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/stargazers\",\n    \"contributors_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contributors\",\n    \"subscribers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscribers\",\n    \"subscription_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscription\",\n    \"commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/commits{/sha}\",\n    \"git_commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/commits{/sha}\",\n    \"comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/comments{/number}\",\n    \"issue_comment_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/comments{/number}\",\n    \"contents_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contents/{+path}\",\n    \"compare_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/compare/{base}...{head}\",\n    \"merges_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/merges\",\n    \"archive_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/{archive_format}{/ref}\",\n    \"downloads_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/downloads\",\n    \"issues_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues{/number}\",\n    \"pulls_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls{/number}\",\n    \"milestones_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/milestones{/number}\",\n    \"notifications_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/notifications{?since,all,participating}\",\n    \"labels_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/labels{/name}\",\n    \"releases_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/releases{/id}\",\n    \"deployments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/deployments\",\n    \"created_at\": \"2017-11-05T00:58:54Z\",\n    \"updated_at\": \"2019-03-12T17:55:49Z\",\n    \"pushed_at\": \"2019-03-12T17:56:12Z\",\n    \"git_url\": \"git://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n    \"ssh_url\": \"git@github.example.com:_ORG_NAME/_REPO_NAME.git\",\n    \"clone_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n    \"svn_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"homepage\": null,\n    \"size\": 79,\n    \"stargazers_count\": 0,\n    \"watchers_count\": 0,\n    \"language\": null,\n    \"has_issues\": true,\n    \"has_projects\": true,\n    \"has_downloads\": true,\n    \"has_wiki\": true,\n    \"has_pages\": false,\n    \"forks_count\": 2,\n    \"mirror_url\": null,\n    \"archived\": false,\n    \"open_issues_count\": 4,\n    \"license\": null,\n    \"forks\": 2,\n    \"open_issues\": 4,\n    \"watchers\": 0,\n    \"default_branch\": \"master\"\n  },\n  \"organization\": {\n    \"login\": \"_ORG_NAME\",\n    \"id\": 6582,\n    \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n    \"url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME\",\n    \"repos_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/repos\",\n    \"events_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/events\",\n    \"hooks_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/hooks\",\n    \"issues_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/issues\",\n    \"members_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/members{/member}\",\n    \"public_members_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/public_members{/member}\",\n    \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n    \"description\": \"\"\n  },\n  \"sender\": {\n    \"login\": \"_USER_NAME\",\n    \"id\": 8935,\n    \"node_id\": \"MDQ6VXNlcjg5MzU=\",\n    \"avatar_url\": \"https://github.example.com/avatars/u/8935?\",\n    \"gravatar_id\": \"\",\n    \"url\": \"https://github.example.com/api/v3/users/_USER_NAME\",\n    \"html_url\": \"https://github.example.com/_USER_NAME\",\n    \"followers_url\": \"https://github.example.com/api/v3/users/_USER_NAME/followers\",\n    \"following_url\": \"https://github.example.com/api/v3/users/_USER_NAME/following{/other_user}\",\n    \"gists_url\": \"https://github.example.com/api/v3/users/_USER_NAME/gists{/gist_id}\",\n    \"starred_url\": \"https://github.example.com/api/v3/users/_USER_NAME/starred{/owner}{/repo}\",\n    \"subscriptions_url\": \"https://github.example.com/api/v3/users/_USER_NAME/subscriptions\",\n    \"organizations_url\": \"https://github.example.com/api/v3/users/_USER_NAME/orgs\",\n    \"repos_url\": \"https://github.example.com/api/v3/users/_USER_NAME/repos\",\n    \"events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/events{/privacy}\",\n    \"received_events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/received_events\",\n    \"type\": \"User\",\n    \"site_admin\": false,\n    \"ldap_dn\": \"_USER_LDAP_DN\"\n  }\n}"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/triggers/github/repos/v2/allParamsTrigger/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  onPush:\n    - log: \"onPush: ${event}\"\n\ntriggers:\n  - github:\n      entryPoint: \"onPush\"\n      version: 2\n      conditions:\n        type: \"push\"\n        githubOrg: \"devtools\"\n        githubRepo: \"concord\"\n        #      githubHost: \"\"\n        branch: \"master\"\n        sender: \"vasia\"\n\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/triggers/github/repos/v2/defaultTrigger/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  onPush:\n    - log: \"onPush: ${event}\"\n\ntriggers:\n  - github:\n      entryPoint: \"onPush\"\n      version: 2\n      conditions:\n        type: \"push\""
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/triggers/github/repos/v2/defaultTriggerWithSender/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  onPush:\n    - log: \"onPush (author filter): ${event}\"\n\ntriggers:\n  - github:\n      entryPoint: \"onPush\"\n      version: 2\n      conditions:\n        type: \"push\"\n        sender: \".*some.*dude.*\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/triggers/github/repos/v2/filesTrigger/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  onPush:\n    - log: \"onPush: ${event}\"\n\ntriggers:\n  - github:\n      entryPoint: \"onPush\"\n      version: 2\n      conditions:\n        type: \"push\"\n        githubOrg: \"devtools\"\n        githubRepo: \"concord\"\n        branch: \"master\"\n        files:\n          any:\n            - \"concord.yml\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/triggers/github/repos/v2/useEventCommitIdTrigger/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  onPush:\n    - log: \"onPush commitId: ${event.commitId}\"\n\ntriggers:\n  - github:\n      entryPoint: \"onPush\"\n      version: 2\n      useEventCommitId: true\n      conditions:\n        type: \"push\"\n        branch: \".*\""
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/usernameSignature/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - log: \"signature: ${initiator.usernameSignature == null ? 'null' : initiator.usernameSignature}.\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/yamlRootFile/concord/extra.concord.yaml",
    "content": "configuration:\n  arguments:\n    name: \"Concord\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/com/walmartlabs/concord/it/runtime/v2/yamlRootFile/concord.yaml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - log: \"Hello, ${name}!\"\n"
  },
  {
    "path": "it/runtime-v2/src/test/resources/logback-test.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss} [%-5level] %logger{12} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "it/server/README.md",
    "content": "# Integration Tests for Concord Server/Agent\n\n## Running\n\n```\n$ ./mvnw clean install -Pdocker -Pit\n```\n\n## Running Locally\n\n## OpenLDAP tests\n\nTo run tests such as [LdapIT](./src/test/java/com/walmartlabs/concord/it/server/LdapIT.java)\nlocally:\n- start the OpenLDAP docker container:\n  ```\n  $ docker run -it --rm -p 1389:389 osixia/openldap\n  ```\n- start the DB and Concord Agent as usual;\n- run the server with the following `ldap` configuration:\n  ```\n  ldap {\n      url = \"ldap://localhost:1389\"\n      searchBase = \"dc=example,dc=org\"\n      principalSearchFilter = \"(cn={0})\"\n      userSearchFilter = \"(cn=*{0}*)\"\n      returningAttributes = [\"cn\",\"memberof\",\"objectClass\",\"sn\",\"uid\"]\n      usernameProperty = \"cn\"\n      mailProperty = \"mail\"\n      systemUsername = \"cn=admin,dc=example,dc=org\"\n      systemPassword = \"admin\"\n  }\n  ```\n- run the test with `IT_LDAP_URL` environment variable:\n  ```\n  IT_LDAP_URL=ldap://localhost:1389\n  ```\n "
  },
  {
    "path": "it/server/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.it</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-server-it</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <properties>\n        <agent.image>walmartlabs/concord-agent</agent.image>\n        <ansible.image>walmartlabs/concord-ansible</ansible.image>\n        <base.dir>${basedir}</base.dir>\n        <deps.dir.src.mount>${project.basedir}/target/deps</deps.dir.src.mount>\n        <deps.dir>${project.basedir}/target/deps</deps.dir>\n        <dind.image>docker:dind</dind.image>\n        <docker.daemon.addr>tcp://127.0.0.1:2375</docker.daemon.addr>\n        <docker.host.addr>localhost</docker.host.addr>\n        <is.docker.profile>false</is.docker.profile>\n        <it.db.addr>db-node:5432</it.db.addr>\n        <it.docker.port>2375</it.docker.port>\n        <it.ldap.addr>ldap-node:389</it.ldap.addr>\n        <it.server.network.mode>custom</it.server.network.mode>\n        <it.server.port>8001</it.server.port>\n        <local.repository.src.mount>${settings.localRepository}</local.repository.src.mount>\n        <network>net-server-it</network>\n        <oldap.image>osixia/openldap</oldap.image>\n        <server.image>walmartlabs/concord-server</server.image>\n        <skip.socat>true</skip.socat>\n        <socat.image>alpine/socat</socat.image>\n        <tmp.dir.src.mount>${java.io.tmpdir}</tmp.dir.src.mount>\n        <tmp.dir>${java.io.tmpdir}</tmp.dir>\n        <agent.work.dir.base>${tmp.dir}/concord-agent/workDirs</agent.work.dir.base>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-client2</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server.plugins.ansible</groupId>\n            <artifactId>concord-ansible-plugin-client2</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server.plugins.noderoster</groupId>\n            <artifactId>concord-noderoster-plugin-client2</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.it</groupId>\n            <artifactId>concord-common-it</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>com.google.guava</groupId>\n            <artifactId>guava</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.wiremock</groupId>\n            <artifactId>wiremock-jetty12</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>ansible-template</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-engine</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jgit</groupId>\n            <artifactId>org.eclipse.jgit</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-core</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.sshd</groupId>\n            <artifactId>sshd-core</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.sshd</groupId>\n            <artifactId>sshd-git</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.github.mwiede</groupId>\n            <artifactId>jsch</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.icegreen</groupId>\n            <artifactId>greenmail</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.icegreen</groupId>\n            <artifactId>greenmail-junit5</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.sun.mail</groupId>\n            <artifactId>javax.mail</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>ch.qos.logback</groupId>\n            <artifactId>logback-classic</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.groovy</groupId>\n            <artifactId>groovy</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-compress</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.hamcrest</groupId>\n            <artifactId>hamcrest</artifactId>\n            <version>2.2</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty</groupId>\n            <artifactId>jetty-util</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-core</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jsr310</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.dataformat</groupId>\n            <artifactId>jackson-dataformat-yaml</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.jetbrains</groupId>\n            <artifactId>annotations</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <!-- JDK9 compatibility -->\n        <dependency>\n            <groupId>javax.xml.bind</groupId>\n            <artifactId>jaxb-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.sun.xml.bind</groupId>\n            <artifactId>jaxb-impl</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <!-- download large dependencies beforehand -->\n        <dependency>\n            <groupId>org.apache.groovy</groupId>\n            <artifactId>groovy-all</artifactId>\n            <type>pom</type>\n            <scope>provided</scope>\n            <exclusions>\n                <exclusion>\n                    <groupId>org.apache.groovy</groupId>\n                    <artifactId>groovy-test</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>org.apache.groovy</groupId>\n                    <artifactId>groovy-test-junit5</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n        <dependency>\n            <groupId>org.python</groupId>\n            <artifactId>jython-standalone</artifactId>\n            <version>2.7.4</version>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <resources>\n            <resource>\n                <filtering>false</filtering>\n                <directory>${project.basedir}/src/test/resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n        </resources>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-failsafe-plugin</artifactId>\n                <configuration>\n                    <skipTests>${skip.it.tests}</skipTests>\n                    <environmentVariables>\n                        <CONCORD_TMP_DIR>${tmp.dir}</CONCORD_TMP_DIR>\n                        <IT_CUSTOM_AGENTS>true</IT_CUSTOM_AGENTS>\n                        <IT_DEFAULT_API_KEY>cTFxMXExcTE=</IT_DEFAULT_API_KEY>\n                        <IT_DEPS_DIR>${deps.dir}</IT_DEPS_DIR>\n                        <IT_DOCKER_ANSIBLE_IMAGE>${ansible.image}</IT_DOCKER_ANSIBLE_IMAGE>\n                        <IT_DOCKER_HOST_ADDR>${docker.host.addr}</IT_DOCKER_HOST_ADDR>\n                        <IT_LDAP_URL>ldap://localhost:${it.ldap.port}</IT_LDAP_URL>\n                        <IT_PROJECT_VERSION>${project.version}</IT_PROJECT_VERSION>\n                        <IT_SERVER_PORT>${it.server.port}</IT_SERVER_PORT>\n                    </environmentVariables>\n                    <systemProperties>\n                        <java.io.tmpdir>${tmp.dir}</java.io.tmpdir>\n                        <isDocker>${is.docker.profile}</isDocker>\n                        <local.mvn.repo>${local.repository.src.mount}</local.mvn.repo>\n                    </systemProperties>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-dependency-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>copy</id>\n                        <phase>process-resources</phase>\n                        <goals>\n                            <goal>copy</goal>\n                        </goals>\n                        <configuration>\n                            <overWriteIfNewer>true</overWriteIfNewer>\n                            <artifactItems>\n                                <artifactItem>\n                                    <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n                                    <artifactId>ansible-template</artifactId>\n                                    <version>${project.version}</version>\n                                    <destFileName>ansible-template.jar</destFileName>\n                                </artifactItem>\n\n                                <!-- used by DependenciesIT as example dependencies -->\n                                <artifactItem>\n                                    <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n                                    <artifactId>example-tasks</artifactId>\n                                    <version>${project.version}</version>\n                                    <destFileName>example.jar</destFileName>\n                                </artifactItem>\n                                <dependency>\n                                    <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n                                    <artifactId>ansible-tasks</artifactId>\n                                    <version>2.15.0</version>\n                                </dependency>\n                                <dependency>\n                                    <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n                                    <artifactId>ansible-tasks</artifactId>\n                                    <version>2.16.0</version>\n                                </dependency>\n                                <dependency>\n                                    <groupId>com.walmartlabs.concord.plugins</groupId>\n                                    <artifactId>confluence-task</artifactId>\n                                    <version>2.5.0</version>\n                                </dependency>\n                            </artifactItems>\n                            <outputDirectory>${deps.dir}</outputDirectory>\n                        </configuration>\n                    </execution>\n                    <execution>\n                        <id>ensure-available</id>\n                        <phase>process-resources</phase>\n                        <goals>\n                            <goal>copy</goal>\n                        </goals>\n                        <configuration>\n                            <!-- to test broken classpaths -->\n                            <artifactItems>\n                                <artifactItem>\n                                    <groupId>io.takari.bpm</groupId>\n                                    <artifactId>bpm-engine-api</artifactId>\n                                    <version>0.10.0</version>\n                                </artifactItem>\n                                <artifactItem>\n                                    <groupId>io.takari.bpm</groupId>\n                                    <artifactId>bpm-engine-api</artifactId>\n                                    <version>0.10.0</version>\n                                    <type>pom</type>\n                                </artifactItem>\n                            </artifactItems>\n                            <outputDirectory>${tmp.dir}</outputDirectory>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n\n    <profiles>\n        <profile>\n            <id>mac-os</id>\n            <activation>\n                <os>\n                    <family>mac</family>\n                </os>\n            </activation>\n            <properties>\n                <tmp.dir>/tmp</tmp.dir>\n                <tmp.dir.src.mount>/tmp</tmp.dir.src.mount>\n                <docker.host.addr>host.docker.internal</docker.host.addr>\n                <skip.socat>false</skip.socat>\n            </properties>\n        </profile>\n        <profile>\n            <id>not-mac-os</id>\n            <activation>\n                <os>\n                    <family>\n                        !mac\n                    </family>\n                </os>\n            </activation>\n            <properties>\n                <it.server.network.mode>host</it.server.network.mode>\n                <it.server.port>8001</it.server.port>\n                <it.db.addr>localhost:${it.db.port}</it.db.addr>\n                <it.ldap.addr>localhost:${it.ldap.port}</it.ldap.addr>\n            </properties>\n        </profile>\n        <profile>\n            <id>docker</id>\n            <activation>\n                <activeByDefault>false</activeByDefault>\n            </activation>\n            <properties>\n                <is.docker.profile>true</is.docker.profile>\n            </properties>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>io.fabric8</groupId>\n                        <artifactId>docker-maven-plugin</artifactId>\n                        <extensions>true</extensions>\n                        <executions>\n                            <execution>\n                                <id>start</id>\n                                <phase>process-resources</phase>\n                                <goals>\n                                    <goal>start</goal>\n                                </goals>\n                            </execution>\n                            <execution>\n                                <id>stop</id>\n                                <phase>post-integration-test</phase>\n                                <goals>\n                                    <goal>stop</goal>\n                                </goals>\n                            </execution>\n                        </executions>\n                        <configuration>\n                            <skipRun>${skip.it.tests}</skipRun>\n                            <autoCreateCustomNetworks>true</autoCreateCustomNetworks>\n                            <images>\n                                <image>\n                                    <name>${socat.image}</name>\n                                    <alias>socat</alias>\n                                    <run>\n                                        <skip>${skip.socat}</skip>\n                                        <network>\n                                            <mode>host</mode>\n                                        </network>\n                                        <volumes>\n                                            <bind>\n                                                <volume>/var/run/docker.sock:/var/run/docker.sock</volume>\n                                            </bind>\n                                        </volumes>\n                                        <labels>\n                                            <concordTxId>${txId}</concordTxId>\n                                            <com.walmartlabs.concord.it.name>socat</com.walmartlabs.concord.it.name>\n                                        </labels>\n                                        <cmd> TCP-LISTEN:${it.docker.port},fork UNIX-CONNECT:/var/run/docker.sock</cmd>\n                                    </run>\n                                </image>\n                                <image>\n                                    <name>${oldap.image}</name>\n                                    <alias>openldap</alias>\n                                    <run>\n                                        <ports>\n                                            <port>${it.ldap.port}:389</port>\n                                        </ports>\n                                        <network>\n                                            <mode>custom</mode>\n                                            <name>${network}</name>\n                                            <alias>ldap-node</alias>\n                                        </network>\n                                        <wait>\n                                            <log>(?s).*slapd starting*</log>\n                                            <time>60000</time>\n                                        </wait>\n                                        <labels>\n                                            <com.walmartlabs.concord.it.name>oldap</com.walmartlabs.concord.it.name>\n                                        </labels>\n                                    </run>\n                                </image>\n                                <image>\n                                    <name>${db.image}</name>\n                                    <alias>db</alias>\n                                    <run>\n                                        <ports>\n                                            <port>it.db.port:5432</port>\n                                        </ports>\n                                        <network>\n                                            <mode>custom</mode>\n                                            <name>${network}</name>\n                                            <alias>db-node</alias>\n                                        </network>\n                                        <env>\n                                            <POSTGRES_PASSWORD>it</POSTGRES_PASSWORD>\n                                            <POSTGRES_INITDB_ARGS>--no-sync</POSTGRES_INITDB_ARGS>\n                                        </env>\n                                        <wait>\n                                            <log>(?s).*ready for start up.*ready to accept connections.*</log>\n                                            <time>60000</time>\n                                        </wait>\n                                        <labels>\n                                            <concordTxId>${txId}</concordTxId>\n                                            <com.walmartlabs.concord.it.name>db</com.walmartlabs.concord.it.name>\n                                        </labels>\n                                    </run>\n                                </image>\n                                <image>\n                                    <name>${server.image}</name>\n                                    <alias>server</alias>\n                                    <run>\n                                        <ports>\n                                            <port>it.server.port:8001</port>\n                                        </ports>\n                                        <network>\n                                            <mode>${it.server.network.mode}</mode>\n                                            <name>${network}</name>\n                                            <alias>server-node</alias>\n                                        </network>\n                                        <volumes>\n                                            <bind>\n                                                <!-- to share files between tests and the server -->\n                                                <volume>${tmp.dir.src.mount}:${tmp.dir}</volume>\n                                                <!-- allows the server to pick up local dependencies -->\n                                                <volume>${deps.dir.src.mount}:${deps.dir}:ro</volume>\n                                                <!-- provide the server's cfg -->\n                                                <volume>${base.dir}/src/test/resources/server.conf:/opt/concord/conf/server.conf:ro</volume>\n                                                <!-- provide the default process variables cfg -->\n                                                <volume>${base.dir}/src/test/resources/default_vars.yml:/opt/concord/conf/default_vars.yml:ro</volume>\n                                                <!-- share host artifacts -->\n                                                <volume>${local.repository.src.mount}:/host/.m2/repository:ro</volume>\n                                                <volume>${base.dir}/src/test/resources/mvn.json:/opt/concord/conf/mvn.json:ro</volume>\n                                            </bind>\n                                        </volumes>\n                                        <env>\n                                            <CONCORD_CFG_FILE>/opt/concord/conf/server.conf</CONCORD_CFG_FILE>\n                                            <CONCORD_TMP_DIR>${tmp.dir}</CONCORD_TMP_DIR>\n                                            <DB_INVENTORY_PASSWORD>it</DB_INVENTORY_PASSWORD>\n                                            <DB_INVENTORY_USERNAME>postgres</DB_INVENTORY_USERNAME>\n                                            <DB_PASSWORD>it</DB_PASSWORD>\n                                            <DB_URL>jdbc:postgresql://${it.db.addr}/postgres</DB_URL>\n                                            <DB_USERNAME>postgres</DB_USERNAME>\n                                            <LDAP_URL>ldap://${it.ldap.addr}</LDAP_URL>\n                                            <NODEROSTER_DB_PASSWORD>it</NODEROSTER_DB_PASSWORD>\n                                            <NODEROSTER_DB_URL>jdbc:postgresql://${it.db.addr}/postgres</NODEROSTER_DB_URL>\n                                            <NODEROSTER_DB_USERNAME>postgres</NODEROSTER_DB_USERNAME>\n                                            <CONCORD_MAVEN_CFG>/opt/concord/conf/mvn.json</CONCORD_MAVEN_CFG>\n                                        </env>\n                                        <wait>\n                                            <!--suppress MavenModelInspection -->\n                                            <http>\n                                                <url>http://localhost:${it.server.port}/api/v1/server/ping</url>\n                                            </http>\n                                            <time>60000</time>\n                                        </wait>\n                                        <labels>\n                                            <concordTxId>${txId}</concordTxId>\n                                            <com.walmartlabs.concord.it.name>server</com.walmartlabs.concord.it.name>\n                                        </labels>\n                                        <log>\n                                            <prefix>SERVER </prefix>\n                                            <color>cyan</color>\n                                        </log>\n                                    </run>\n                                </image>\n                                <!-- TODO start DIND? -->\n                                <image>\n                                    <name>${agent.image}</name>\n                                    <alias>agent</alias>\n                                    <run>\n                                        <network>\n                                            <mode>host</mode>\n                                        </network>\n                                        <volumes>\n                                            <bind>\n                                                <!-- provide the agents's cfg -->\n                                                <volume>${base.dir}/src/test/resources/agent.conf:/opt/concord/conf/agent.conf:ro</volume>\n                                                <!-- to share files between process containers -->\n                                                <volume>${tmp.dir.src.mount}:${tmp.dir}</volume>\n                                                <!-- allows the agent to pick up local dependencies -->\n                                                <volume>${deps.dir.src.mount}:${deps.dir}:ro</volume>\n                                                <!-- share host artifacts -->\n                                                <volume>${local.repository.src.mount}:/host/.m2/repository:ro</volume>\n                                                <volume>${base.dir}/src/test/resources/mvn.json:/opt/concord/conf/mvn.json:ro</volume>\n                                            </bind>\n                                        </volumes>\n                                        <env>\n                                            <CONCORD_CFG_FILE>/opt/concord/conf/agent.conf</CONCORD_CFG_FILE>\n                                            <CONCORD_DOCKER_LOCAL_MODE>false</CONCORD_DOCKER_LOCAL_MODE>\n                                            <CONCORD_MAVEN_CFG>/opt/concord/conf/mvn.json</CONCORD_MAVEN_CFG>\n                                            <CONCORD_TMP_DIR>${tmp.dir}</CONCORD_TMP_DIR>\n                                            <DOCKER_HOST>${docker.daemon.addr}</DOCKER_HOST>\n                                            <SERVER_API_BASE_URL>http://localhost:${it.server.port}</SERVER_API_BASE_URL>\n                                            <SERVER_WEBSOCKET_URL>ws://localhost:${it.server.port}/websocket</SERVER_WEBSOCKET_URL>\n                                            <WORK_DIR_BASE>${agent.work.dir.base}</WORK_DIR_BASE>\n                                            <WORKERS_COUNT>3</WORKERS_COUNT>\n                                        </env>\n                                        <labels>\n                                            <concordTxId>${txId}</concordTxId>\n                                            <com.walmartlabs.concord.it.name>agent</com.walmartlabs.concord.it.name>\n                                        </labels>\n                                    </run>\n                                </image>\n                            </images>\n                        </configuration>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n    </profiles>\n</project>\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/AbstractGeneralTriggerIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.client2.ProcessListFilter;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForStatus;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class AbstractGeneralTriggerIT extends AbstractServerIT {\n\n    protected Map<ProcessEntry.StatusEnum, ProcessEntry> waitProcesses(\n            String orgName, String projectName, ProcessEntry.StatusEnum first, ProcessEntry.StatusEnum... more) throws Exception {\n        ProcessV2Api processApi = new ProcessV2Api(getApiClient());\n\n        ProcessListFilter filter = ProcessListFilter.builder()\n                .orgName(orgName)\n                .projectName(projectName)\n                .build();\n\n        List<ProcessEntry> processes;\n        while (true) {\n            processes = processApi.listProcesses(filter);\n\n            if (processes.size() == 1 + (more != null ? more.length : 0)) {\n                break;\n            }\n            Thread.sleep(1000);\n        }\n\n        Map<ProcessEntry.StatusEnum, ProcessEntry> ps = new HashMap<>();\n        for (ProcessEntry p : processes) {\n            ProcessEntry pir = waitForStatus(getApiClient(), p.getInstanceId(), first, more);\n            ProcessEntry pe = ps.put(pir.getStatus(), pir);\n            if (pe != null) {\n                throw new RuntimeException(\"already got process with '\" + pe.getStatus() + \"' status, id: \" + pe.getInstanceId());\n            }\n        }\n        return ps;\n    }\n\n    protected void assertProcessLog(ProcessEntry pir, String log) throws Exception {\n        assertNotNull(pir);\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(log, ab);\n    }\n\n    protected List<TriggerEntry> waitForTriggers(String orgName, String projectName, String repoName, int expectedCount) throws Exception {\n        TriggersApi triggerResource = new TriggersApi(getApiClient());\n        while (true) {\n            List<TriggerEntry> l = triggerResource.listTriggers(orgName, projectName, repoName);\n            if (l != null && l.size() == expectedCount) {\n                return l;\n            }\n\n            Thread.sleep(1000);\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/AbstractGitHubTriggersIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.collect.ImmutableMap;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.client2.ProcessEntry.StatusEnum;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.it.common.GitHubUtils;\nimport com.walmartlabs.concord.it.common.GitUtils;\nimport com.walmartlabs.concord.it.common.ITUtils;\nimport com.walmartlabs.concord.it.common.ServerClient;\nimport org.eclipse.jgit.api.Git;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardOpenOption;\nimport java.time.OffsetDateTime;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic abstract class AbstractGitHubTriggersIT extends AbstractServerIT {\n\n    protected static String toRepoName(Path p) {\n        return p.getParent().getFileName() + \"/\" + p.getFileName();\n    }\n\n    protected Path initRepo(String resource) throws Exception {\n        Path src = Paths.get(AbstractGitHubTriggersIT.class.getResource(resource).toURI());\n        return GitUtils.createBareRepository(src);\n    }\n\n    protected Path initRepo(String resource, String leaf) throws Exception {\n        Path src = Paths.get(AbstractGitHubTriggersIT.class.getResource(resource).toURI());\n        return GitUtils.createBareRepository(src, \"init\", null, leaf);\n    }\n\n    protected Path initProjectAndRepo(String orgName, String projectName, String repoName, String repoBranch, Path bareRepo) throws Exception {\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n\n        RepositoryEntry repo = new RepositoryEntry()\n                .branch(repoBranch != null ? repoBranch : \"master\")\n                .url(bareRepo.toAbsolutePath().toString());\n\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE)\n                .repositories(ImmutableMap.of(repoName, repo)));\n\n        return bareRepo;\n    }\n\n    protected String createNewBranch(Path bareRepo, String branch, String resource) throws Exception {\n        Path src = Paths.get(AbstractGitHubTriggersIT.class.getResource(resource).toURI());\n        return GitUtils.createNewBranch(bareRepo, branch, src);\n    }\n\n    protected void updateConcordYml(Path bareRepo, Map<String, String> values) throws Exception {\n        Path dir = PathUtils.createTempDir(\"git\");\n\n        Git git = Git.cloneRepository()\n                .setDirectory(dir.toFile())\n                .setURI(bareRepo.toAbsolutePath().toString())\n                .call();\n\n        Path concordYml = dir.resolve(\"concord.yml\");\n        String s = new String(Files.readAllBytes(concordYml));\n        for (Map.Entry<String, String> e : values.entrySet()) {\n            s = s.replaceAll(e.getKey(), e.getValue());\n        }\n        Files.write(concordYml, s.getBytes(), StandardOpenOption.TRUNCATE_EXISTING);\n\n        git.add()\n                .addFilepattern(\".\")\n                .call();\n\n        git.commit()\n                .setMessage(\"updating concord.yml\")\n                .call();\n\n        git.push()\n                .call();\n    }\n\n    protected void refreshRepo(String orgName, String projectName, String repoName) throws Exception {\n        RepositoriesApi repoApi = new RepositoriesApi(getApiClient());\n        repoApi.refreshRepository(orgName, projectName, repoName, true);\n    }\n\n    protected ProcessEntry waitForAProcess(String orgName, String projectName, String initiator) throws Exception {\n        ProcessV2Api processApi = new ProcessV2Api(getApiClient());\n        ProcessListFilter filter = ProcessListFilter.builder()\n                .orgName(orgName)\n                .projectName(projectName)\n                .initiator(initiator)\n                .build();\n\n        while (!Thread.currentThread().isInterrupted()) {\n            List<ProcessEntry> l = processApi.listProcesses(filter);\n            if (l.size() == 1 && isFinished(l.get(0).getStatus())) {\n                return l.get(0);\n            }\n\n            Thread.sleep(1000);\n        }\n\n        throw new RuntimeException(\"Process wait interrupted\");\n    }\n\n    protected int waitForProcessesToFinish() throws Exception {\n        ProcessV2Api processApi = new ProcessV2Api(getApiClient());\n        while (true) {\n            List<ProcessEntry> l = processApi.listProcesses(ProcessListFilter.builder().build());\n\n            boolean allDone = true;\n            for (ProcessEntry e : l) {\n                if (e.getStatus() == StatusEnum.NEW\n                        || e.getStatus() == StatusEnum.PREPARING\n                        || e.getStatus() == StatusEnum.ENQUEUED\n                        || e.getStatus() == StatusEnum.STARTING\n                        || e.getStatus() == StatusEnum.RUNNING) {\n\n                    allDone = false;\n                    break;\n                }\n            }\n\n            if (allDone) {\n                return l.size();\n            }\n\n            Thread.sleep(1000);\n        }\n    }\n\n    protected void expectNoProcesses(String orgName, String projectName, OffsetDateTime afterCreatedAt) throws Exception {\n        ProcessV2Api processApi = new ProcessV2Api(getApiClient());\n        ProcessListFilter filter = ProcessListFilter.builder()\n                .orgName(orgName)\n                .projectName(projectName)\n                .afterCreatedAt(afterCreatedAt)\n                .build();\n\n        List<ProcessEntry> l = processApi.listProcesses(filter);\n        assertEquals(0, l.size());\n    }\n\n    protected String sendEvent(String resource, String event, String... params) throws Exception {\n        return sendEvent(resource, event, Collections.emptyMap(), params);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    protected String sendEvent(String resource, String event, Map<String, String> queryParams, String... params) throws Exception {\n        String payload = resourceToString(resource);\n        if (params != null) {\n            for (int i = 0; i < params.length; i += 2) {\n                String k = params[i];\n                String v = params[i + 1];\n                payload = payload.replaceAll(k, v);\n            }\n        }\n\n        ApiClient client = getApiClient();\n        Map<String, Object> payloadMap = client.getObjectMapper().readValue(payload, Map.class);\n        payload = client.getObjectMapper().writeValueAsString(payloadMap);\n\n        client.addDefaultHeader(\"X-Hub-Signature\", \"sha1=\" + GitHubUtils.sign(payload));\n\n        GitHubEventsApi eventsApi = new GitHubEventsApi(client);\n        if (queryParams.isEmpty()) {\n            return eventsApi.onEvent(Collections.emptyMap(), \"abc\", event, payloadMap);\n        } else {\n            return sendWithQueryParams(eventsApi, payloadMap, event, queryParams);\n        }\n    }\n\n    private String sendWithQueryParams(GitHubEventsApi eventsApi, Map<String, Object> payload, String event, Map<String, String> queryParams) throws ApiException {\n        GitHubEventsApi api = new GitHubEventsApi(getApiClient());\n        return api.onEvent(queryParams, \"abc\", event, payload);\n    }\n\n    protected void assertLog(ProcessEntry entry, String pattern) throws Exception {\n        byte[] ab = getLog(entry.getInstanceId());\n        ServerClient.assertLog(pattern, ab);\n    }\n\n    protected void waitForCompletion(ProcessEntry entry) throws Exception {\n        ServerClient.waitForCompletion(getApiClient(), entry.getInstanceId());\n    }\n\n    protected void deleteOrg(String orgName) throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.deleteOrg(orgName, \"yes\");\n    }\n\n    protected static String resourceToString(String resource) throws Exception {\n        return ITUtils.resourceToString(AbstractGitHubTriggersIT.class, resource);\n    }\n\n    protected boolean isFinished(StatusEnum status) {\n        return status == StatusEnum.CANCELLED ||\n                status == StatusEnum.FAILED ||\n                status == StatusEnum.FINISHED ||\n                status == StatusEnum.TIMED_OUT;\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/AbstractOneOpsTriggerIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.google.common.collect.ImmutableMap;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.it.common.GitUtils;\n\nimport java.io.InputStream;\nimport java.net.URL;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForStatus;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic abstract class AbstractOneOpsTriggerIT extends AbstractServerIT {\n\n    protected void sendOneOpsEvent(String payloadPath) throws Exception {\n        ExternalEventsApi api = new ExternalEventsApi(getApiClient());\n\n        api.externalEvent(\"oneops\", resourceToMap(payloadPath));\n    }\n\n    protected void assertProcessLog(ProcessEntry pir, String log) throws Exception {\n        assertNotNull(pir);\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(log, ab);\n    }\n\n    protected Map<ProcessEntry.StatusEnum, ProcessEntry> waitProcesses(\n            String orgName, String projectName, ProcessEntry.StatusEnum first, ProcessEntry.StatusEnum... more) throws Exception {\n        ProcessV2Api processApi = new ProcessV2Api(getApiClient());\n\n        List<ProcessEntry> processes;\n        while (true) {\n            processes = processApi.listProcesses(ProcessListFilter.builder()\n                            .orgName(orgName)\n                            .projectName(projectName)\n                    .build());\n            if (processes.size() == 1 + (more != null ? more.length : 0)) {\n                break;\n            }\n            Thread.sleep(1000);\n        }\n\n        Map<ProcessEntry.StatusEnum, ProcessEntry> ps = new HashMap<>();\n        for (ProcessEntry p : processes) {\n            ProcessEntry pir = waitForStatus(getApiClient(), p.getInstanceId(), first, more);\n            ProcessEntry pe = ps.put(pir.getStatus(), pir);\n            if (pe != null) {\n                throw new RuntimeException(\"already got process with '\" + pe.getStatus() + \"' status, id: \" + pe.getInstanceId());\n            }\n        }\n        return ps;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    protected static Map<String, Object> resourceToMap(String resource) throws Exception {\n        URL url = AbstractOneOpsTriggerIT.class.getResource(resource);\n        try (InputStream in = url.openStream()) {\n            return new ObjectMapper().readValue(in, Map.class);\n        }\n    }\n\n    protected void refreshRepo(String orgName, String projectName, String repoName) throws Exception {\n        RepositoriesApi repoApi = new RepositoriesApi(getApiClient());\n        repoApi.refreshRepository(orgName, projectName, repoName, true);\n    }\n\n    protected Path initRepo(String resource) throws Exception {\n        Path src = Paths.get(AbstractGitHubTriggersIT.class.getResource(resource).toURI());\n        return GitUtils.createBareRepository(src);\n    }\n\n    protected Path initProjectAndRepo(String orgName, String projectName, String repoName, Path bareRepo) throws Exception {\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n\n        RepositoryEntry repo = new RepositoryEntry()\n                .branch(\"master\")\n                .url(bareRepo.toAbsolutePath().toString());\n\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PUBLIC)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE)\n                .repositories(ImmutableMap.of(repoName, repo)));\n\n        return bareRepo;\n    }\n\n    protected List<TriggerEntry> waitForTriggers(String orgName, String projectName, String repoName, int expectedCount) throws Exception {\n        TriggersApi triggerResource = new TriggersApi(getApiClient());\n        while (true) {\n            List<TriggerEntry> l = triggerResource.listTriggers(orgName, projectName, repoName);\n\n            if (l != null && l.size() == expectedCount) {\n                return l;\n            }\n\n            Thread.sleep(1000);\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/AbstractServerIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.it.common.ITUtils;\nimport com.walmartlabs.concord.it.common.JGitUtils;\nimport com.walmartlabs.concord.it.common.ServerClient;\nimport org.eclipse.jgit.api.Git;\nimport org.intellij.lang.annotations.Language;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Timeout;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.attribute.PosixFilePermissions;\nimport java.util.*;\nimport java.util.concurrent.TimeUnit;\n\nimport static com.walmartlabs.concord.it.server.AbstractServerIT.DEFAULT_TEST_TIMEOUT;\n\n@Timeout(value = DEFAULT_TEST_TIMEOUT, unit = TimeUnit.MILLISECONDS)\npublic abstract class AbstractServerIT {\n\n    public static final long DEFAULT_TEST_TIMEOUT = 180000;\n\n    private ServerClient serverClient;\n\n    @BeforeAll\n    public static void _initJGit() {\n        JGitUtils.applyWorkarounds();\n    }\n\n    @BeforeEach\n    public void _init() {\n        serverClient = new ServerClient(ITConstants.SERVER_URL);\n    }\n\n    protected ApiClient getApiClient() {\n        return serverClient.getClient();\n    }\n\n    protected ApiClient getApiClientForKey(String apiKey) {\n        return serverClient.getClientForApiKey(apiKey);\n    }\n\n    protected StartProcessResponse start(String orgName, String projectName, String repoName, String entryPoint, byte[] payload) throws ApiException {\n        Map<String, Object> input = new HashMap<>();\n        if (orgName != null) {\n            input.put(\"org\", orgName);\n        }\n        if (projectName != null) {\n            input.put(\"project\", projectName);\n        }\n        if (repoName != null) {\n            input.put(\"repo\", repoName);\n        }\n        if (entryPoint != null) {\n            input.put(\"entryPoint\", entryPoint);\n        }\n        if (payload != null) {\n            input.put(\"archive\", payload);\n        }\n        return start(input);\n    }\n\n    protected StartProcessResponse start(String entryPoint, byte[] payload) throws ApiException {\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"entryPoint\", entryPoint);\n        input.put(\"archive\", payload);\n        return start(input);\n    }\n\n    protected StartProcessResponse start(String entryPoint) throws ApiException {\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"entryPoint\", entryPoint);\n        return start(input);\n    }\n\n    protected StartProcessResponse start(byte[] payload) throws ApiException {\n        return start(Collections.singletonMap(\"archive\", payload));\n    }\n\n    protected StartProcessResponse start(InputStream in) throws ApiException {\n        return start(Collections.singletonMap(\"archive\", in));\n    }\n\n    protected StartProcessResponse start(Map<String, Object> input) throws ApiException {\n        return serverClient.start(input);\n    }\n\n    protected SecretOperationResponse addPlainSecret(String orgName, String name, boolean generatePassword, String storePassword, byte[] secret) throws ApiException {\n        return serverClient.addPlainSecret(orgName, name, null, generatePassword, storePassword, secret);\n    }\n\n    protected SecretOperationResponse addPlainSecretWithProjectNames(String orgName, String name, Set<String> projectNames, boolean generatePassword, String storePassword, byte[] secret) throws ApiException {\n        return serverClient.addPlainSecret(orgName, name, projectNames, null, generatePassword, storePassword, secret);\n    }\n\n    protected SecretOperationResponse addUsernamePassword(String orgName, String name, boolean generatePassword, String storePassword, String username, String password) throws ApiException {\n        return serverClient.addUsernamePassword(orgName, null, name, generatePassword, storePassword, username, password);\n    }\n\n    protected SecretOperationResponse addUsernamePassword(String orgName, String projectName, String name, boolean generatePassword, String storePassword, String username, String password) throws ApiException {\n        return serverClient.addUsernamePassword(orgName, projectName, name, generatePassword, storePassword, username, password);\n    }\n\n    protected SecretOperationResponse generateKeyPair(String orgName, String name, boolean generatePassword, String storePassword) throws ApiException {\n        return serverClient.generateKeyPair(orgName, null, name, generatePassword, storePassword);\n    }\n\n    protected SecretOperationResponse generateKeyPair(String orgName, String projectName, String name, boolean generatePassword, String storePassword) throws ApiException {\n        return serverClient.generateKeyPair(orgName, projectName, name, generatePassword, storePassword);\n    }\n\n    protected SecretOperationResponse generateKeyPairWithProjectNames(String orgName, Set<String> projectNames, String name, boolean generatePassword, String storePassword) throws ApiException {\n        return serverClient.generateKeyPair(orgName, projectNames, null, name, generatePassword, storePassword);\n    }\n\n    protected SecretOperationResponse generateKeyPairWithProjectIds(String orgName, Set<UUID> projectIds, String name, boolean generatePassword, String storePassword) throws ApiException {\n        return serverClient.generateKeyPair(orgName, null, projectIds, name, generatePassword, storePassword);\n    }\n\n    protected byte[] getLog(UUID instanceId) throws ApiException {\n        return serverClient.getLog(instanceId);\n    }\n\n    protected void resetApiKey() {\n        serverClient.resetApiKey();\n    }\n\n    protected void setApiKey(String apiKey) {\n        serverClient.setApiKey(apiKey);\n    }\n\n    protected void setGithubKey(String key) {\n        serverClient.setGithubKey(key);\n    }\n\n    protected void waitForLog(UUID instanceId, @Language(\"RegExp\") String pattern) throws IOException, InterruptedException, ApiException {\n        serverClient.waitForLog(instanceId, pattern);\n    }\n\n    protected void waitForLog(UUID instanceId, int retries, @Language(\"RegExp\") String pattern) throws IOException, InterruptedException, ApiException {\n        serverClient.waitForLog(instanceId, retries, pattern);\n    }\n\n    protected static String randomString() {\n        return ITUtils.randomString();\n    }\n\n    protected static String randomPwd() {\n        return ITUtils.randomPwd();\n    }\n\n    protected static Path createTempDir() throws IOException {\n        return ITUtils.createTempDir();\n    }\n\n    protected static Path createTempFile(String suffix) throws IOException {\n        Path tmpFile = PathUtils.createTempFile(\"test\", suffix);\n        Files.setPosixFilePermissions(tmpFile, PosixFilePermissions.fromString(\"rw-r--r--\"));\n        return tmpFile;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    protected Map<String, Object> fromJson(File f) throws IOException {\n        return fromJson(f, Map.class);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    protected Map<String, Object> fromJson(InputStream is) throws IOException {\n        return getApiClient().getObjectMapper().readValue(is, Map.class);\n    }\n\n    protected <T> T fromJson(File f, Class<T> classOfT) throws IOException {\n        return getApiClient().getObjectMapper().readValue(f, classOfT);\n    }\n\n    protected <T> T fromJson(InputStream is, Class<T> classOfT) throws IOException {\n        return getApiClient().getObjectMapper().readValue(is, classOfT);\n    }\n\n    protected String createRepo(String resource) throws Exception {\n        Path tmpDir = createTempDir();\n\n        File src = new File(AbstractServerIT.class.getResource(resource).toURI());\n        PathUtils.copy(src.toPath(), tmpDir);\n\n        try (Git repo = Git.init().setInitialBranch(\"master\").setDirectory(tmpDir.toFile()).call()) {\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"import\").call();\n        }\n\n        return tmpDir.toAbsolutePath().toString();\n    }\n\n    protected static String env(String k, String def) {\n        String v = System.getenv(k);\n        if (v == null) {\n            return def;\n        }\n        return v;\n    }\n\n    protected void withOrg(Consumer<String> consumer) throws Exception {\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        try {\n            orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n            consumer.accept(orgName);\n        } finally {\n            orgApi.deleteOrg(orgName, \"yes\");\n        }\n    }\n\n    protected void withProject(String orgName, Consumer<String> consumer) throws Exception {\n        String projectName = \"project_\" + randomString();\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        try {\n            projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                    .name(projectName)\n                    .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n            consumer.accept(projectName);\n        } finally {\n            projectsApi.deleteProject(orgName, projectName);\n        }\n    }\n\n    @FunctionalInterface\n    public interface Consumer<T> {\n        void accept(T t) throws Exception;\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/AnsibleEventIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.net.URI;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class AnsibleEventIT extends AbstractServerIT {\n\n    @Test\n    @SuppressWarnings(\"unchecked\")\n    public void testEvent() throws Exception {\n        URI uri = AnsibleEventIT.class.getResource(\"ansibleEvent\").toURI();\n        byte[] payload = archive(uri);\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        ProcessEventsApi eventsApi = new ProcessEventsApi(getApiClient());\n        List<ProcessEventEntry> l = eventsApi.listProcessEvents(pir.getInstanceId(), null, null, null, null, null, null, -1);\n        assertFalse(l.isEmpty());\n\n        long cnt = l.stream().filter(e -> {\n            if (e.getData() == null) {\n                return false;\n            }\n\n            Map<String, Object> m = e.getData();\n\n            Map<String, Object> r = (Map<String, Object>) m.get(\"result\");\n            if (r == null) {\n                return false;\n            }\n\n            String msg = (String) r.get(\"msg\");\n            if (msg == null) {\n                return false;\n            }\n\n            return msg.equals(\"Hi there!\");\n        }).count();\n\n        assertEquals(1, cnt);\n    }\n\n    @Test\n    public void testIgnoredFailures() throws Exception {\n        URI uri = AnsibleEventIT.class.getResource(\"ansibleIgnoredFailures\").toURI();\n        byte[] payload = archive(uri);\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        ProcessEventsApi eventsApi = new ProcessEventsApi(getApiClient());\n        List<ProcessEventEntry> l = eventsApi.listProcessEvents(pir.getInstanceId(), null, null, null, null, null, null, -1);\n        assertFalse(l.isEmpty());\n\n        long cnt = l.stream().filter(e -> {\n            if (e.getData() == null) {\n                return false;\n            }\n\n            Map<String, Object> m = e.getData();\n\n            Object ignoreErrors = m.get(\"ignore_errors\");\n            if (ignoreErrors == null) {\n                return false;\n            }\n\n            return Boolean.TRUE.equals(ignoreErrors);\n        }).count();\n\n        assertEquals(1, cnt);\n    }\n\n    /**\n     * Runs a playbook that fails on one of the steps.\n     * Verifies that failed host events are correctly recorded.\n     */\n    @Test\n    public void testFailedHosts() throws Exception {\n        URI uri = AnsibleEventIT.class.getResource(\"ansibleFailedHosts\").toURI();\n        byte[] payload = archive(uri);\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FAILED, pe.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLogAtLeast(\".*'msg' is undefined.*\", 1, ab);\n\n        // ---\n\n        ProcessEventsApi eventsApi = new ProcessEventsApi(getApiClient());\n        List<ProcessEventEntry> l = eventsApi.listProcessEvents(pe.getInstanceId(), \"ANSIBLE\", null, null, null, \"post\", null, -1);\n        assertFalse(l.isEmpty());\n\n        for (ProcessEventEntry e : l) {\n            Map<String, Object> m = e.getData();\n            if (m.get(\"task\").equals(\"fail\") && m.get(\"status\").equals(\"FAILED\")) {\n                return;\n            }\n        }\n\n        fail(\"Can't find the required events\");\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/AnsibleEventProcessorIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.net.URI;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.Callable;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class AnsibleEventProcessorIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        URI uri = AnsibleEventProcessorIT.class.getResource(\"ansibleEventProcessor\").toURI();\n        byte[] payload = archive(uri);\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        Assertions.assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        AnsibleProcessApi ansibleApi = new AnsibleProcessApi(getApiClient());\n\n        PlaybookEntry playbook = assertPlaybook(ansibleApi, pir.getInstanceId());\n        assertEquals(\"playbook/hello.yml\", playbook.getName());\n        assertEquals(1L, playbook.getHostsCount().longValue());\n        assertEquals(1, playbook.getPlaysCount().intValue());\n\n        PlayInfo play = assertPlay(ansibleApi, pir.getInstanceId(), playbook.getId());\n        assertEquals(\"local\", play.getPlayName());\n        assertEquals(2L, play.getTaskCount().longValue());\n    }\n\n    @Test\n    public void testLongNames() throws Exception {\n        URI uri = AnsibleEventProcessorIT.class.getResource(\"ansibleEventProcessor\").toURI();\n        byte[] payload = archive(uri);\n\n        // ---\n\n        StartProcessResponse spr = start(\"emitLongNames\", payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        Assertions.assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        AnsibleProcessApi ansibleApi = new AnsibleProcessApi(getApiClient());\n\n        PlaybookEntry playbook = assertPlaybook(ansibleApi, pir.getInstanceId());\n        assertEquals(\"playbook/large_play_and_task_names.yml\", playbook.getName());\n        assertEquals(50L, playbook.getHostsCount().longValue());\n        assertEquals(1, playbook.getPlaysCount().intValue());\n\n        PlayInfo play = assertPlay(ansibleApi, pir.getInstanceId(), playbook.getId());\n        TaskInfo task = assertTask(ansibleApi, pir.getInstanceId(), play.getPlayId());\n        assertTrue(play.getPlayName().matches(\"\\\\['my-inventory-host-001', 'my-inventory-host-002'.*\\\\.\\\\.\\\\.$\"));\n        assertFalse(play.getPlayName().contains(\"my-inventory-host-048\"));\n        assertEquals(1L, play.getTaskCount().longValue());\n        assertEquals(1024, play.getPlayName().length());\n        assertEquals(1024, task.getTaskName().length());\n    }\n\n    @Test\n    public void testUnicodeSanitization() throws Exception {\n        URI uri = AnsibleEventProcessorIT.class.getResource(\"ansibleEventProcessor\").toURI();\n        byte[] payload = archive(uri);\n\n        // ---\n\n        StartProcessResponse spr = start(\"generateNonPrintableChars\", payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        Assertions.assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        AnsibleProcessApi ansibleApi = new AnsibleProcessApi(getApiClient());\n\n        PlaybookEntry playbook = assertPlaybook(ansibleApi, pir.getInstanceId());\n        assertEquals(\"playbook/unicode_sanitization.yml\", playbook.getName());\n        assertEquals(1L, playbook.getHostsCount().longValue());\n        assertEquals(1, playbook.getPlaysCount().intValue());\n\n        PlayInfo play = assertPlay(ansibleApi, pir.getInstanceId(), playbook.getId());\n        List<ProcessEventEntry> events = assertEvents(ansibleApi, pir.getInstanceId(), playbook.getId());\n\n        assertEquals(0, play.getTaskStats().get(\"failed\"));\n        assertEquals(1, play.getTaskStats().get(\"ok\"));\n\n        Map<?, ?> result = Assertions.assertInstanceOf(Map.class, events.get(0).getData().get(\"result\"));\n        String stdout = result.get(\"stdout\").toString();\n\n        // expect actual escaped NUL to be removed. Escaped SOH and not-really-NUL should be preserved.\n        assertEquals(\"aNul\\naSoh\\u0001\\nNotNul\\\\u0000\", stdout);\n    }\n\n    private static PlaybookEntry assertPlaybook(AnsibleProcessApi ansibleApi, UUID instanceId) throws Exception {\n        List<PlaybookEntry> playbooks = poll(() -> ansibleApi.listPlaybooks(instanceId));\n        assertEquals(1, playbooks.size());\n        return playbooks.get(0);\n    }\n\n    private static PlayInfo assertPlay(AnsibleProcessApi ansibleApi, UUID instanceId, UUID playbookId) throws Exception {\n        List<PlayInfo> plays = poll(() -> ansibleApi.listPlays(instanceId, playbookId));\n        assertEquals(1, plays.size());\n        return plays.get(0);\n    }\n\n    private static TaskInfo assertTask(AnsibleProcessApi ansibleApi, UUID instanceId, UUID playId) throws Exception {\n        List<TaskInfo> tasks = poll(() -> ansibleApi.listTasks(instanceId, playId));\n        assertFalse(tasks.isEmpty());\n        return tasks.get(0);\n    }\n\n    private static List<ProcessEventEntry> assertEvents(AnsibleProcessApi ansibleApi, UUID instanceId, UUID playbookId) throws Exception {\n        List<ProcessEventEntry> events = poll(() -> ansibleApi.listEvents(instanceId, \"127.0.0.1\", \"local\", \"OK\", playbookId));\n        assertFalse(events.isEmpty());\n        return events;\n    }\n\n    private static <T> List<T> poll(Callable<List<T>> call) throws Exception {\n        while (true) {\n            List<T> result = call.call();\n            if (result != null && !result.isEmpty()) {\n                return result;\n            }\n\n            Thread.sleep(1000);\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/AnsibleIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.io.Files;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.InputStream;\nimport java.net.URI;\nimport java.net.URL;\nimport java.nio.file.Paths;\nimport java.util.*;\nimport java.util.regex.Pattern;\n\nimport static com.walmartlabs.concord.common.GrepUtils.grep;\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class AnsibleIT extends AbstractServerIT {\n\n    @Test\n    public void testHello() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansible\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*\\\"msg\\\":.*Hello, world.*\", ab);\n    }\n\n    @Test\n    public void testConfigFile() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleConfigFile\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*\\\"msg\\\":.*Hello, world.*\", ab);\n    }\n\n    @Test\n    public void testSkipTags() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleSkipTags\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*\\\"msg\\\":.*Hello2, world.*\", ab);\n        assertEquals(0, grep(\".*Hello, world.*\", ab).size(), \"unexpected 'Hello, world' log\");\n    }\n\n    @Test\n    public void testVault() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleVault\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hello, Concord.*\", ab);\n    }\n\n    @Test\n    public void testVaultWithMultiplePasswords() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleVaultMultiplePasswords\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hello, Concord.*\", ab);\n    }\n\n    @Test\n    public void testVaultWithMultiplePasswordFiles() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleVaultMultiplePasswordFiles\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hello, Concord.*\", ab);\n    }\n\n    @Test\n    @SuppressWarnings(\"unchecked\")\n    public void testTwoAnsibleRuns() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"twoAnsible\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*\\\"msg\\\":.*Hello!.*\", ab);\n        assertLog(\".*\\\"msg\\\":.*Bye-bye!.*\", ab);\n\n        // ---\n\n        try (InputStream resp = processApi.downloadAttachment(spr.getInstanceId(), \"ansible_stats_v2.json\")) {\n            assertNotNull(resp);\n\n            List<Map<String, Object>> stats = fromJson(resp, List.class);\n\n            assertEquals(2, stats.size());\n\n            assertEquals(\"playbook/hello.yml\", stats.get(0).get(\"playbook\"));\n            Collection<String> oks = (Collection<String>)ConfigurationUtils.get(stats.get(0), \"stats\", \"ok\");\n            assertNotNull(oks);\n            assertEquals(1, oks.size());\n            assertEquals(\"127.0.0.1\", oks.iterator().next());\n        }\n    }\n\n    @Test\n    public void testWithForm() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleWithForm\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n\n        // ---\n\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n        List<FormListEntry> forms = formsApi.listProcessForms(pir.getInstanceId());\n        assertEquals(1, forms.size());\n\n        formsApi.submitForm(pir.getInstanceId(), forms.get(0).getName(), Collections.singletonMap(\"msg\", \"Hello!\"));\n\n        // ---\n\n        pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*\\\"msg\\\":.*Hello!.*\", ab);\n    }\n\n    @Test\n    public void testWithFormSuspensionPostAnsible() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleWithPostFormSuspension/payload\").toURI();\n        byte[] payload = archive(dir);\n\n        // --\n\n        URI playbookUri = AnsibleIT.class.getResource(\"ansibleWithPostFormSuspension/playbook/hello.yml\").toURI();\n        File playbookFileContent = new File(playbookUri);\n        InputStream playbookStream = Files.asByteSource(playbookFileContent).openStream();\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"playbook.yml\", playbookStream);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n\n        // ---\n\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n        List<FormListEntry> forms = formsApi.listProcessForms(pir.getInstanceId());\n        assertEquals(1, forms.size());\n\n        formsApi.submitForm(pir.getInstanceId(), forms.get(0).getName(), Collections.singletonMap(\"msg\", \"Hello!\"));\n\n        // ---\n\n        pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hello!.*\", ab);\n    }\n\n    @Test\n    public void testExtenalPlaybook() throws Exception {\n\n        URI dir = AnsibleIT.class.getResource(\"ansibleExternalPlaybook/payload\").toURI();\n        URL playbookUrl = AnsibleIT.class.getResource(\"ansibleExternalPlaybook/playbook/hello.yml\");\n        byte[] payload = archive(dir);\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        File playbook = Paths.get(playbookUrl.toURI()).toFile();\n        input.put(\"myplaybook.yml\", new FileInputStream(playbook));\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n    }\n\n    @Test\n    public void testMergeDefaults() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleMergeDefaults\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*_callbacks:myCallbackDir.*\", ab);\n    }\n\n    @Test\n    public void testGroupVars() throws Exception {\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        String secretName = \"secret_\" + randomString();\n        addPlainSecret(orgName, secretName, false, null, \"greetings: \\\"Hi there!\\\"\".getBytes());\n\n        // ---\n\n        URI dir = AnsibleIT.class.getResource(\"ansibleGroupVars\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.myOrgName\", orgName);\n        input.put(\"arguments.mySecretName\", secretName);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*\\\"msg\\\":.*Hi there!.*\", ab);\n    }\n\n    @Test\n    public void testOutVars() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleOutVars\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*\\\"msg\\\":.*First hello from ansible.*\", ab);\n        assertLog(\".*\\\"msg\\\":.*Second message.*\", ab);\n    }\n\n    @Test\n    public void testStats() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleStats\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*OK:.*127.0.0.1.*\", ab);\n    }\n\n    @Test\n    public void testBadStrings() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleBadStrings\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*THIS TASK CONTAINS SENSITIVE INFORMATION.*\", ab);\n    }\n\n    @Test\n    public void testRawStrings() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleRawStrings\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*\" + Pattern.quote(\"message '{{.lalala}}'\") + \".*\", ab);\n        assertLog(\".*\" + Pattern.quote(\"message {{.unsafe}}\") + \".*\", ab);\n    }\n\n    @Test\n    @SuppressWarnings(\"unchecked\")\n    public void testTemplateArgs() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleTemplateArgs\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*message iddqd.*\", ab);\n\n        // ---\n        ProcessEventsApi eventsApi = new ProcessEventsApi(getApiClient());\n        List<ProcessEventEntry> events = eventsApi.listProcessEvents(pir.getInstanceId(), \"ANSIBLE\", null, null, null, null, null, null);\n        assertNotNull(events);\n        // one pre and one post event\n        assertEquals(2, events.size());\n        Map<String, Object> data = events.stream()\n                .filter(e -> \"post\".equals(e.getData().get(\"phase\")))\n                .findAny()\n                .orElseThrow(() -> new RuntimeException(\"post event not found\"))\n                .getData();\n        assertEquals(\"message iddqd\", ((Map<String, Object>) data.get(\"result\")).get(\"msg\"));\n    }\n\n    @Test\n    public void testExtraVarsFiles() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleExtraVarsFiles\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hello from a JSON file!.*\", ab);\n        assertLog(\".*Hello from a YAML file!.*\", ab);\n    }\n\n    @Test\n    public void testMultiInventories() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleMultiInventory\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hello aaa.*\", ab);\n        assertLog(\".*Hello bbb.*\", ab);\n    }\n\n    @Test\n    public void testMultiInventoryFiles() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleMultiInventoryFile\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hello aaa.*\", ab);\n        assertLog(\".*Hello bbb.*\", ab);\n    }\n\n    @Test\n    public void testLimitWithMultipleHost() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleLimitWithMultipleHost\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLogAtLeast(\".*Hello aaa.*\", 2, ab);\n        assertLogAtLeast(\".*Hello ccc.*\", 2, ab);\n        assertNoLog(\".*Hello bbb.*\", ab);\n    }\n\n    @Test\n    public void testInventoryMixMatch() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleInventoryMix\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hello!.*\", ab);\n    }\n\n    @Test\n    public void testInventoryNameInvalidChars() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleInventoryName\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hello!.*\", ab);\n    }\n\n    @Test\n    public void testLogFiltering() throws Exception {\n        // run w/o filtering first\n\n        URI dir = AnsibleIT.class.getResource(\"ansibleLogFiltering\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"arguments.doFilter\", false);\n        input.put(\"archive\", payload);\n\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hello! my_password=.*\", ab);\n\n        // and then run with the filtering enabled\n\n        input = new HashMap<>();\n        input.put(\"arguments.doFilter\", true);\n        input.put(\"archive\", payload);\n\n        spr = start(input);\n\n        // ---\n\n        pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        ab = getLog(pir.getInstanceId());\n        assertNoLog(\".*Hello! my_password=.*\", ab);\n        assertLog(\".*SENSITIVE INFORMATION.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/AnsibleLookupIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.net.URI;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class AnsibleLookupIT extends AbstractServerIT {\n\n    @Test\n    public void testSecrets() throws Exception {\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String secretName = \"mySecret\";\n        String secretValue = \"value_\" + randomString();\n        String secretPwd = randomPwd();\n        addPlainSecret(orgName, secretName, false, secretPwd, secretValue.getBytes());\n\n        String projectName = \"project_\" + randomString();\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n\n        URI dir = AnsibleLookupIT.class.getResource(\"ansibleLookupSecret\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"archive\", payload);\n        input.put(\"arguments.orgName\", orgName);\n        input.put(\"arguments.secretPwd\", secretPwd);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertNoLog(\".*Explicit org \" + secretValue + \".*\", ab);\n        assertNoLog(\".*Implicit org \" + secretValue + \".*\", ab);\n        assertLogAtLeast(\".*ENABLING NO_LOG.*\", 2, ab);\n    }\n\n    @Test\n    public void testSecretData() throws Exception {\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String secretName = \"mySecret\";\n        String secretValue = \"value_\" + randomString();\n        String secretPwd = randomPwd();\n        addPlainSecret(orgName, secretName, false, secretPwd, secretValue.getBytes());\n\n        String projectName = \"project_\" + randomString();\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n\n        URI dir = AnsibleLookupIT.class.getResource(\"ansibleLookupSecretData\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"archive\", payload);\n        input.put(\"arguments.orgName\", orgName);\n        input.put(\"arguments.secretPwd\", secretPwd);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertNoLog(\".*Explicit org \" + secretValue + \".*\", ab);\n        assertNoLog(\".*Implicit org \" + secretValue + \".*\", ab);\n        assertLogAtLeast(\".*ENABLING NO_LOG.*\", 2, ab);\n    }\n\n    @Test\n    public void testSecretDataNoPassword() throws Exception {\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String secretName = \"mySecret\";\n        String secretValue = \"value_\" + randomString();\n        addPlainSecret(orgName, secretName, false, null, secretValue.getBytes());\n\n        String projectName = \"project_\" + randomString();\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n\n        URI dir = AnsibleLookupIT.class.getResource(\"ansibleLookupSecretDataNoPassword\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"archive\", payload);\n        input.put(\"arguments.orgName\", orgName);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertNoLog(\".*Explicit org \" + secretValue + \".*\", ab);\n        assertNoLog(\".*Implicit org \" + secretValue + \".*\", ab);\n    }\n\n    @Test\n    public void testPublickey() throws Exception {\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String secretName = \"mySecret_\" + randomString();\n        generateKeyPair(orgName, secretName, false, null);\n\n        String projectName = \"project_\" + randomString();\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n\n        URI dir = AnsibleLookupIT.class.getResource(\"ansibleLookupPublicKey\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"archive\", payload);\n        input.put(\"arguments.orgName\", orgName);\n        input.put(\"arguments.secretName\", secretName);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertNoLog(\".*Explicit org: ssh-rsa\" + \".*\", ab);\n        assertNoLog(\".*Implicit org: ssh-rsa\" + \".*\", ab);\n        assertLogAtLeast(\".*ENABLING NO_LOG.*\", 2, ab);\n    }\n\n    /**\n     * Verify that {@code lookup('concord_data_secret', ...)} returns a valid value.\n     */\n    @Test\n    public void testSecretValue() throws Exception {\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String secretName = \"mySecret_\" + randomString();\n        String secretValue = \"hello_\" + randomString();\n        addPlainSecret(orgName, secretName, false, null, secretValue.getBytes());\n\n        // ---\n\n        byte[] payload = archive(AnsibleLookupIT.class.getResource(\"ansibleLookupSecretDataValue\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.orgName\", orgName);\n        input.put(\"arguments.secretName\", secretName);\n\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLog(\".*Value: \" + secretValue + \".*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/AnsiblePolicyIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.net.URI;\nimport java.net.URL;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class AnsiblePolicyIT extends AbstractServerIT {\n\n    @Test\n    public void testTaskDeny() throws Exception {\n        // ---\n\n        String orgName = \"Default\";\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n\n        String policyName = \"test_policy_\" + randomString();\n        PolicyEntry policy = new PolicyEntry()\n                .name(policyName)\n                .rules(readPolicy(\"ansiblePolicyTaskDeny/test-policy.json\"));\n\n        PolicyApi policyApi = new PolicyApi(getApiClient());\n        policyApi.createOrUpdatePolicy(policy);\n        policyApi.linkPolicy(policyName, new PolicyLinkEntry()\n                .orgName(orgName)\n                .projectName(projectName));\n\n        URI dir = AnsiblePolicyIT.class.getResource(\"ansiblePolicyTaskDeny\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(orgName, projectName, null, null, payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FAILED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Task 'Copy a local file \\\\(copy\\\\)' is forbidden by the task policy.*\", ab);\n    }\n\n    private Map<String, Object> readPolicy(String file) throws Exception {\n        URL url = AnsiblePolicyIT.class.getResource(file);\n        return fromJson(new File(url.toURI()));\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/AnsiblePolicyVerboseLimitIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.net.URI;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static java.util.Collections.singletonMap;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class AnsiblePolicyVerboseLimitIT extends AbstractServerIT {\n\n    private String orgName;\n    private String projectName;\n\n    @BeforeEach\n    public void setup() throws Exception {\n\n        // -- Add policy to restrict verbose logging\n\n        orgName = \"org_\" + randomString();\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        projectName = \"project_\" + randomString();\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        Map<String, Object> ansibleVerboseLimits = new HashMap<>();\n        ansibleVerboseLimits.put(\"maxHosts\", 1);\n        ansibleVerboseLimits.put(\"maxTotalWork\", 2);\n\n        String policyName = \"policy_\" + randomString();\n        PolicyApi policyApi = new PolicyApi(getApiClient());\n        policyApi.createOrUpdatePolicy(new PolicyEntry()\n                .name(policyName)\n                .rules(singletonMap(\"processCfg\",\n                        singletonMap(\"arguments\",\n                                singletonMap(\"ansibleVerboseLimits\",\n                                        ansibleVerboseLimits)))));\n\n        policyApi.linkPolicy(policyName, new PolicyLinkEntry()\n                .orgName(orgName)\n                .projectName(projectName));\n    }\n\n    @Test\n    public void testLargeInventoryLimitedToGroup() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleLargeVerbose\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"arguments.playbook\", \"playbook_single.yml\");\n        input.put(\"arguments.verboseLevel\", \"1\");\n        input.put(\"arguments.invFile\", \"inventory_limit.ini\");\n        input.put(\"arguments.groupLimit\", \"dev\");\n        input.put(\"archive\", payload);\n\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus(), \"Large inventory limited to small group must FINISH\");\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*ansible completed successfully.*\", ab);\n    }\n\n    @Test\n    public void testVerboseTooManyImportedTasks() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleLargeVerbose\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"arguments.playbook\", \"playbook_include.yml\");\n        input.put(\"arguments.verboseLevel\", \"4\");\n        input.put(\"arguments.invFile\", \"inventory_small.ini\");\n        input.put(\"archive\", payload);\n\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus(),\n                \"Imported tasks exceeding max work must FINISH\");\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Disabling verbose output. Too much work.*\", ab);\n    }\n\n    @Test\n    public void testVerboseTooManyHosts() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleLargeVerbose\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"arguments.playbook\", \"playbook_single.yml\");\n        input.put(\"arguments.verboseLevel\", \"1\");\n        input.put(\"arguments.invFile\", \"inventory_large.ini\");\n        input.put(\"archive\", payload);\n\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus(),\n                \"Large inventory with verbose logging must FINISH\");\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Disabling verbose output. Too many hosts.*\", ab);\n    }\n\n    @Test\n    public void testVerboseTooMuchWork() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleLargeVerbose\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"arguments.playbook\", \"playbook_multi.yml\");\n        input.put(\"arguments.verboseLevel\", \"1\");\n        input.put(\"arguments.invFile\", \"inventory_small.ini\");\n        input.put(\"archive\", payload);\n\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus(),\n                \"Small inventory with many calls and verbose logging must FINISH\");\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Disabling verbose output. Too much work.*\", ab);\n    }\n\n    @Test\n    public void testNoVerboseLargeInventory() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleLargeVerbose\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"arguments.playbook\", \"playbook_single.yml\");\n        input.put(\"arguments.verboseLevel\", \"0\");\n        input.put(\"arguments.invFile\", \"inventory_large.ini\");\n        input.put(\"archive\", payload);\n\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus(),\n                \"Large inventory with standard logging must FINISH\");\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*ansible completed successfully.*\", ab);\n    }\n\n    @Test\n    public void testVerboseSmallInventory() throws Exception {\n        URI dir = AnsibleIT.class.getResource(\"ansibleLargeVerbose\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"arguments.playbook\", \"playbook_single.yml\");\n        input.put(\"arguments.verboseLevel\", \"3\");\n        input.put(\"arguments.invFile\", \"inventory_small.ini\");\n        input.put(\"archive\", payload);\n\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus(),\n                \"Small inventory with verbose logging must FINISH\");\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        // only shows with verbose logging enabled\n        // TODO may be flaky? no guarantee it'll *always* be in every ansible version\n        assertLog(\".*Using .* as config file.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/AnsibleProjectIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.it.common.GitUtils;\nimport com.walmartlabs.concord.it.common.MockGitSshServer;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.InputStream;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static java.util.Collections.singletonMap;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n/**\n * @see \"docs/examples/ansible_project\"\n */\npublic class AnsibleProjectIT extends AbstractServerIT {\n\n    private MockGitSshServer gitServer;\n    private int gitPort;\n\n    @BeforeEach\n    public void setUp() throws Exception {\n        Path data = Paths.get(AnsibleProjectIT.class.getResource(\"ansibleproject/git\").toURI());\n        Path repo = GitUtils.createBareRepository(data);\n\n        gitServer = new MockGitSshServer(0, repo);\n        gitServer.start();\n\n        gitPort = gitServer.getPort();\n    }\n\n    @AfterEach\n    public void tearDown() throws Exception {\n        gitServer.stop();\n    }\n\n    @Test\n    public void test() throws Exception {\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"request\", resource(\"ansibleproject/request.json\"));\n        input.put(\"inventory\", resource(\"ansibleproject/inventory.ini\"));\n        test(input);\n    }\n\n    @Test\n    public void testInlineInventory() throws Exception {\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"request\", resource(\"ansibleproject/requestInline.json\"));\n        test(input);\n    }\n\n    @Test\n    @SuppressWarnings(\"unchecked\")\n    public void testFailure() throws Exception {\n        String orgName = \"Default\";\n\n        // ---\n\n        String templatePath = \"file://\" + ITConstants.DEPENDENCIES_DIR + \"/ansible-template.jar\";\n\n        String projectName = \"project_\" + randomString();\n        String repoSecretName = \"repoSecret_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n        String repoUrl = String.format(ITConstants.GIT_SERVER_URL_PATTERN, gitPort);\n\n        // ---\n\n        generateKeyPair(orgName, repoSecretName, false, null);\n\n        // ---\n\n        RepositoryEntry repo = new RepositoryEntry()\n                .name(repoName)\n                .url(repoUrl)\n                .branch(\"master\")\n                .secretName(repoSecretName);\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        Map<String, Object> cfg = Collections.singletonMap(Constants.Request.TEMPLATE_KEY, templatePath);\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .repositories(singletonMap(repoName, repo))\n                .cfg(cfg)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"request\", resource(\"ansibleproject/requestFailure.json\"));\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"repo\", repoName);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.FAILED);\n\n        // ---\n\n        try (InputStream resp = downloadAttachment(spr.getInstanceId(), \"ansible_stats.json\")) {\n            assertNotNull(resp);\n\n            Map<String, Object> stats = fromJson(resp);\n            Collection<String> failures = (Collection<String>) stats.get(\"failures\");\n            assertNotNull(failures);\n            assertEquals(1, failures.size());\n            assertEquals(\"128.0.0.1\", failures.iterator().next());\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public void test(Map<String, Object> input) throws Exception {\n        String orgName = \"Default\";\n\n        // ---\n\n        String templatePath = \"file://\" + ITConstants.DEPENDENCIES_DIR + \"/ansible-template.jar\";\n\n        String projectName = \"project_\" + randomString();\n        String repoSecretName = \"repoSecret_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n        String repoUrl = String.format(ITConstants.GIT_SERVER_URL_PATTERN, gitPort);\n\n        // ---\n\n        generateKeyPair(orgName, repoSecretName, false, null);\n\n        // ---\n\n        RepositoryEntry repo = new RepositoryEntry()\n                .name(repoName)\n                .url(repoUrl)\n                .branch(\"master\")\n                .secretName(repoSecretName);\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        Map<String, Object> cfg = Collections.singletonMap(Constants.Request.TEMPLATE_KEY, templatePath);\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .repositories(singletonMap(repoName, repo))\n                .cfg(cfg));\n\n        // ---\n\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"repo\", repoName);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry psr = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(psr.getInstanceId());\n        assertLog(\".*\\\"msg\\\":.*Hello, world.*\", ab);\n\n        // check if `force_color` is working\n        assertLogAtLeast(\".*\\\\[0;32m.*\", 3, ab);\n\n        // ---\n\n        try (InputStream resp = downloadAttachment(spr.getInstanceId(), \"ansible_stats.json\")) {\n            assertNotNull(resp);\n            Map<String, Object> stats = fromJson(resp);\n\n            Collection<String> oks = (Collection<String>) stats.get(\"ok\");\n            assertNotNull(oks);\n            assertEquals(1, oks.size());\n            assertEquals(\"127.0.0.1\", oks.iterator().next());\n        }\n    }\n\n    private static InputStream resource(String path) {\n        return AnsibleProjectIT.class.getResourceAsStream(path);\n    }\n\n    private InputStream downloadAttachment(UUID instanceId, String name) throws InterruptedException, ApiException {\n        int attemptsLeft = 3;\n        while (true) {\n            try {\n                return new ProcessApi(getApiClient()).downloadAttachment(instanceId, name);\n            } catch (ApiException e) {\n                if (attemptsLeft-- > 0 && (e.getCode() == 404 || e.getCode() >= 500)) {\n                    Thread.sleep(500);\n                } else {\n                    throw e;\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/AnsibleRetryIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessApi;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.InputStream;\nimport java.net.URI;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class AnsibleRetryIT extends AbstractServerIT {\n\n    @Test\n    public void testSaveRetry() throws Exception {\n        URI uri = ProcessIT.class.getResource(\"ansibleSaveRetry\").toURI();\n        byte[] payload = archive(uri);\n\n        // start the process\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n\n        // wait for completion\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FAILED, pir.getStatus());\n\n        // retrieve the retry file\n\n        try (InputStream is = processApi.downloadAttachment(pir.getInstanceId(), \"hello.retry\")) {\n            assertNotNull(is);\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ApiKeyIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ApiKeyIT extends AbstractServerIT {\n\n    @Test\n    public void testOwner() throws Exception {\n        String userAName = \"userA_\" + randomString();\n        String userBName = \"userB_\" + randomString();\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userAName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userBName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        // ---\n\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse cakr = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userAName));\n        assertTrue(cakr.getOk());\n\n        // ---\n\n        setApiKey(cakr.getKey());\n\n        try {\n            apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userBName));\n            fail(\"Should fail\");\n        } catch (ApiException e) {\n        }\n\n        // ---\n\n        cakr = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userAName));\n        assertTrue(cakr.getOk());\n    }\n\n    @Test\n    public void testCreatingKeyWithoutUsername() throws Exception {\n        String userName = \"userA_\" + randomString();\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        CreateUserResponse user = usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        // the new user has no api keys initially\n\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n        List<ApiKeyEntry> keys = apiKeyResource.listUserApiKeys(user.getId());\n        assertEquals(0, keys.size());\n\n        // admin creates a new api key for the new user\n\n        CreateApiKeyResponse cakr = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userName));\n        assertTrue(cakr.getOk());\n        keys = apiKeyResource.listUserApiKeys(user.getId());\n        assertEquals(1, keys.size());\n\n        // the new user creates another api key for themselves\n\n        setApiKey(cakr.getKey());\n        cakr = apiKeyResource.createUserApiKey(new CreateApiKeyRequest());\n        assertTrue(cakr.getOk());\n\n        // the new user lists all their api keys (should be 2)\n\n        keys = apiKeyResource.listUserApiKeys(user.getId());\n        assertEquals(2, keys.size());\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/AttachmentRbacIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.InputStream;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForStatus;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class AttachmentRbacIT extends AbstractServerIT {\n\n    private static final Logger log = LoggerFactory.getLogger(AttachmentRbacIT.class);\n\n    @Test\n    public void test() throws Exception {\n\n        // create a new org\n\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // add the users A, B and C\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        String userAName = \"userA_\" + randomString();\n        usersApi.createOrUpdateUser(new CreateUserRequest().\n                username(userAName).type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse apiKeyA = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userAName));\n\n        String userBName = \"userB_\" + randomString();\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userBName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        CreateApiKeyResponse apiKeyB = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userBName));\n\n        String userCName = \"userC_\" + randomString();\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userCName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n        UUID userCUUID = usersApi.findByUsername(userCName).getId();\n        CreateApiKeyResponse apiKeyC = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userCName));\n\n        // create the user A's team\n\n        String teamName = \"team_\" + randomString();\n\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n        CreateTeamResponse ctr = teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamName));\n\n        teamsApi.addUsersToTeam(orgName, teamName, false, Collections.singletonList(new TeamUserEntry()\n                .username(userAName)\n                .role(TeamUserEntry.RoleEnum.MEMBER)));\n\n        teamsApi.addUsersToTeam(orgName, teamName, false, Collections.singletonList(new TeamUserEntry()\n                .username(userCName)\n                .role(TeamUserEntry.RoleEnum.OWNER)));\n\n        teamsApi.addUsersToTeam(orgName, teamName, false, Collections.singletonList(new TeamUserEntry()\n                .username(userBName)\n                .role(TeamUserEntry.RoleEnum.MEMBER)));\n\n        // switch to the user A and create a new private project\n\n        setApiKey(apiKeyA.getKey());\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PUBLIC)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // grant the team access to the project\n        projectsApi.updateProjectAccessLevel(orgName, projectName, new ResourceAccessEntry()\n                .teamId(ctr.getId())\n                .orgName(orgName)\n                .teamName(teamName)\n                .level(ResourceAccessEntry.LevelEnum.READER));\n\n        // start a new process using the project as the user A\n\n        byte[] payload = archive(AttachmentRbacIT.class.getResource(\"ansibleEvent\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n\n        StartProcessResponse spr = start(input);\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.FINISHED);\n\n        log.info(\"The initiator shall be able to list attachments\");\n        List<String> attachments = processApi.listAttachments(spr.getInstanceId());\n        assertNotNull(attachments, \"Attachments shall not be null for initiator\");\n        assertSame(2, attachments.size(), \"Attachment size shall be 2 for initiator\");\n\n        try (InputStream is = processApi.downloadAttachment(spr.getInstanceId(), attachments.get(0))) {\n            assertNotNull(is, \"File object shall not be null for initiator\");\n        }\n\n        // switch to admin and add the user B\n\n        setApiKey(apiKeyA.getKey());\n\n        ProjectEntry projectEntry = projectsApi.getProject(orgName, projectName);\n\n        // Update userC as owner\n        EntityOwner projectOwner = projectEntry.getOwner();\n\n        EntityOwner entityOwner = new EntityOwner()\n                .id(userCUUID)\n                .username(projectOwner.getUsername())\n                .userDomain(projectOwner.getUserDomain())\n                .userType(projectOwner.getUserType())\n                .displayName(projectOwner.getDisplayName());\n\n        projectEntry.setOwner(entityOwner);\n        projectsApi.createOrUpdateProject(orgName, projectEntry);\n\n        log.info(\"The admin shall be able to list attachments\");\n        // Admin shall be also able to list the attachments\n\n        resetApiKey();\n        attachments = processApi.listAttachments(spr.getInstanceId());\n        assertNotNull(attachments, \"Attachments shall not be null for admin\");\n        assertSame(2, attachments.size(), \"Attachment size shall be 2 for admin\");\n\n        try (InputStream is = processApi.downloadAttachment(spr.getInstanceId(), attachments.get(0))) {\n            assertNotNull(is, \"File object shall not be null for admin\");\n        }\n\n        // switch to the user B (non admin) and try to list and download the attachments\n\n        setApiKey(apiKeyB.getKey());\n\n        // Non-admin who is only a member shall not able to list the attachments\n        try {\n            processApi.listAttachments(spr.getInstanceId());\n            fail(\"Should fail when listing attachments for non-admin\");\n        } catch (Exception e) {\n            assertNotNull(e, \"Exception shall not be null\");\n            assertTrue(e.getMessage().contains(\"doesn't have the necessary access level\"), \"Exception doesn't match\");\n        }\n\n        // Non-admin who is only a member shall not able to download the attachments\n        try (InputStream is = processApi.downloadAttachment(spr.getInstanceId(), attachments.get(0))) {\n            fail(\"Should fail when downloading attachments for non-admin\");\n        } catch (Exception e) {\n            assertNotNull(e, \"Exception shall not be null\");\n            assertTrue(e.getMessage().contains(\"doesn't have the necessary access level\"), \"Exception doesn't match\");\n        }\n\n        // Switch to userC who should be able to list and download the attachments since its\n        // set to owner of the project\n        setApiKey(apiKeyC.getKey());\n\n        attachments = processApi.listAttachments(spr.getInstanceId());\n        assertNotNull(attachments, \"Attachments shall not be null for non-admin who is a owner\");\n        assertSame(2, attachments.size(),\n                \"Attachment size shall be 2 for non-admin who is a owner\");\n\n        try (InputStream is = processApi.downloadAttachment(spr.getInstanceId(), attachments.get(0))) {\n            assertNotNull(is, \"File object shall not be null for non-admin who is a owner\");\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/CheckpointsIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.collect.ImmutableMap;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class CheckpointsIT extends AbstractServerIT {\n\n    @Test\n    public void testCheckpoint() throws Exception {\n        // prepare the payload\n\n        byte[] payload = archive(CheckpointsIT.class.getResource(\"checkpoints\").toURI());\n\n        // start the process\n\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // wait for completion\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // check the logs\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*==Start.*\", ab);\n        assertLog(\".*==Middle.*\", ab);\n        assertLog(\".*==End.*\", ab);\n\n        // restore from TWO checkpoint\n        restoreFromCheckpoint(pir.getInstanceId(), \"TWO\");\n\n        waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*==Start.*\", ab);\n        assertLog(\".*==Middle.*\", ab);\n        assertLog(\".*==End.*\", 2, ab);\n\n        // restore from ONE checkpoint\n        restoreFromCheckpoint(pir.getInstanceId(), \"ONE\");\n\n        waitForCompletion(getApiClient(), spr.getInstanceId());\n        ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*==Start.*\", ab);\n        assertLog(\".*==Middle.*\", 2, ab);\n        assertLog(\".*==End.*\", 3, ab);\n    }\n\n    @Test\n    public void testRestoreCheckpointWithGetByName() throws Exception {\n        byte[] payload = archive(CheckpointsIT.class.getResource(\"oneCheckpoint\").toURI());\n\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*aaa before.*\", ab);\n        assertLog(\".*bbb after.*\", ab);\n\n        // ---\n\n        CheckpointV2Api checkpointV2Api = new CheckpointV2Api(getApiClient());\n        checkpointV2Api.processCheckpoint(pir.getInstanceId(), \"one\", \"restore\");\n\n        // ---\n\n        waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        ab = getLog(pir.getInstanceId());\n        assertLog(\".*aaa before.*\", ab);\n        assertLog(\".*bbb after.*\", 2, ab);\n    }\n\n    @Test\n    public void testRestoreCheckpointWithEventNameV1() throws Exception {\n        testRestoreCheckpointWithEventName(\"checkpointsWithEventName\");\n    }\n\n    @Test\n    public void testRestoreCheckpointWithEventNameV2() throws Exception {\n        testRestoreCheckpointWithEventName(\"checkpointsWithEventNameV2\");\n    }\n\n    private void testRestoreCheckpointWithEventName(String resource) throws Exception {\n        // prepare the payload\n\n        byte[] payload = archive(CheckpointsIT.class.getResource(resource).toURI());\n\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // restore from first checkpoint\n        CheckpointV2Api checkpointV2Api = new CheckpointV2Api(getApiClient());\n        checkpointV2Api.processCheckpoint(pir.getInstanceId(), \"first\", \"restore\");\n\n        // ---\n\n        waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Event Name: first*\", ab);\n\n        // restore from second checkpoint\n        checkpointV2Api.processCheckpoint(pir.getInstanceId(), \"second\", \"restore\");\n\n        waitForCompletion(getApiClient(), spr.getInstanceId());\n        ab = getLog(pir.getInstanceId());\n        assertLog(\".*Event Name: second.*\", ab);\n    }\n\n    @Test\n    public void testCheckpointWithError() throws Exception {\n        // prepare the payload\n\n        byte[] payload = archive(CheckpointsIT.class.getResource(\"checkpointsWithError\").toURI());\n\n        // start the process\n\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n\n        // ---\n        Map<String, Object> data = Collections.singletonMap(\"shouldFail\", true);\n        submitForm(spr.getInstanceId(), data);\n\n        // ---\n        ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.FAILED);\n\n        // check error message\n        assertProcessErrorMessage(pir, \"As you wish\");\n\n        // ---\n        restoreFromCheckpoint(pir.getInstanceId(), \"ONE\");\n\n        pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n\n        // no error message after process restored\n        assertNoProcessErrorMessage(pir);\n\n        // ---\n        data = Collections.singletonMap(\"shouldFail\", false);\n        submitForm(spr.getInstanceId(), data);\n\n        waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*==Start.*\", ab);\n        assertLog(\".*==Middle.*\", ab);\n        assertLog(\".*==End.*\", ab);\n    }\n\n    @Test\n    public void testCheckpointWithArgs() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        String value = \"value_\" + randomString();\n\n        EncryptValueResponse evr = projectsApi.encrypt(orgName, projectName, value);\n        assertTrue(evr.getOk());\n\n        // prepare the payload\n\n        byte[] payload = archive(CheckpointsIT.class.getResource(\"checkpointsWithArgs\").toURI());\n\n        // start the process\n\n        StartProcessResponse spr = start(ImmutableMap.of(\n                \"org\", orgName,\n                \"project\", projectName,\n                \"archive\", payload,\n                \"arguments.encrypted\", evr.getData()));\n        assertNotNull(spr.getInstanceId());\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*hello, World.*\", 2, ab);\n        assertLog(\".*\" + value + \".*\", 4, ab);\n        assertLog(\".*checkpoint pointA.*\", ab);\n    }\n\n    @Test\n    public void testExpressions() throws Exception {\n        String xValue = \"x_\" + randomString();\n\n        // ---\n\n        byte[] payload = archive(CheckpointsIT.class.getResource(\"checkpointExpressions\").toURI());\n\n        StartProcessResponse spr = start(ImmutableMap.of(\n                \"arguments.x\", xValue,\n                \"archive\", payload));\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*checkpoint test \" + xValue + \".*\", 1, ab);\n    }\n\n    /**\n     * Verifies the {@code LogTagMetadataProvider} feature.\n     */\n    @Test\n    public void testTags() throws Exception {\n        byte[] payload = archive(CheckpointsIT.class.getResource(\"checkpoints\").toURI());\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*__logTag.*phase.*pre.*meta.*checkpointName.*ONE.*\", ab);\n        assertLog(\".*__logTag.*phase.*post.*meta.*checkpointName.*ONE.*\", ab);\n        assertLog(\".*__logTag.*phase.*pre.*meta.*checkpointName.*TWO.*\", ab);\n        assertLog(\".*__logTag.*phase.*post.*meta.*checkpointName.*TWO.*\", ab);\n    }\n\n    private void restoreFromCheckpoint(UUID instanceId, String name) throws ApiException {\n        CheckpointApi checkpointApi = new CheckpointApi(getApiClient());\n        ProcessEventsApi eventsApi = new ProcessEventsApi(getApiClient());\n        List<ProcessEventEntry> processEvents = eventsApi.listProcessEvents(instanceId, null, null, null, null, null, true, null);\n        assertNotNull(processEvents);\n\n        // restore from ONE checkpoint\n        String checkpointId = assertCheckpoint(name, processEvents);\n\n        ResumeProcessResponse resp = checkpointApi.restore(instanceId,\n                new RestoreCheckpointRequest().id(UUID.fromString(checkpointId)));\n\n        assertNotNull(resp);\n    }\n\n    private void submitForm(UUID instanceId, Map<String, Object> data) throws ApiException {\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n\n        List<FormListEntry> forms = formsApi.listProcessForms(instanceId);\n        assertEquals(1, forms.size());\n\n        FormListEntry f = forms.get(0);\n        FormSubmitResponse fsr = formsApi.submitForm(instanceId, f.getName(), data);\n        assertTrue(fsr.getOk());\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static String assertCheckpoint(String name, List<ProcessEventEntry> processEvents) {\n        String checkpointDescription = \"Checkpoint: \" + name;\n\n        for (ProcessEventEntry e : processEvents) {\n            Map<String, Object> data = e.getData();\n            if (data == null) {\n                continue;\n            }\n            if (\"post\".equals(data.get(\"phase\")) && checkpointDescription.equals(data.get(\"description\"))) {\n                List<Map<String, Object>> out = (List<Map<String, Object>>) data.get(\"out\");\n                if (out == null || out.size() < 2) {\n                    continue;\n                }\n\n                String checkpointId = assertParam(\"checkpointId\", out.get(0));\n                String checkpointName = assertParam(\"checkpointName\", out.get(1));\n                if (name.equals(checkpointName)) {\n                    return checkpointId;\n                }\n            }\n        }\n\n        throw new IllegalStateException(\"can't find \" + name + \" checkpoint\");\n    }\n\n    private static String assertParam(String paramName, Map<String, Object> params) {\n        assertEquals(paramName, params.get(\"source\"));\n        assertEquals(paramName, params.get(\"target\"));\n        assertNotNull(params.get(\"resolved\"));\n        return (String) params.get(\"resolved\");\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static void assertNoProcessErrorMessage(ProcessEntry p) {\n        assertNotNull(p);\n\n        Map<String, Object> meta = p.getMeta();\n        if (meta == null || meta.isEmpty()) {\n            return;\n        }\n\n        Map<String, Object> out = (Map<String, Object>) meta.get(\"out\");\n        if (out == null || out.isEmpty()) {\n            return;\n        }\n\n        Map<String, Object> error = (Map<String, Object>) out.get(Constants.Context.LAST_ERROR_KEY);\n        assertNull(error);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static void assertProcessErrorMessage(ProcessEntry p, String expected) {\n        assertNotNull(p);\n\n        Map<String, Object> meta = p.getMeta();\n        assertNotNull(meta);\n\n        Map<String, Object> out = (Map<String, Object>) meta.get(\"out\");\n        assertNotNull(out);\n\n        Map<String, Object> error = (Map<String, Object>) out.get(Constants.Context.LAST_ERROR_KEY);\n        assertNotNull(error);\n\n        assertEquals(expected, error.get(\"message\"));\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ClasspathIsolationIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\n\npublic class ClasspathIsolationIT extends AbstractServerIT {\n\n    @Test\n    public void testBrokenDeps() throws Exception {\n        byte[] payload = archive(ClasspathIsolationIT.class.getResource(\"brokenDeps\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n\n        Map<String, Object> cfg = new HashMap<>();\n        cfg.put(\"dependencies\", new String[]{\"mvn://com.walmartlabs.concord.it.tasks:broken-deps:\" + ITConstants.PROJECT_VERSION});\n        input.put(\"request\", cfg);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*hello!.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ClasspathRepoIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\n\nimport static com.walmartlabs.concord.client2.ProjectEntry.RawPayloadModeEnum.DISABLED;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class ClasspathRepoIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        String url = \"classpath://com/walmartlabs/concord/server/selfcheck/concord.yml\";\n        String projectName = \"project_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(\"Default\", new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(DISABLED)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .name(repoName)\n                        .url(url)\n                        .branch(\"master\"))));\n\n        // ---\n\n        StartProcessResponse spr = start(\"Default\", projectName, repoName, null, null);\n        assertTrue(spr.getOk());\n\n        // ---\n\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pe.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLog(\"^.*\\\\[INFO \\\\] OK$\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ConcordTaskForkFromGitRepoIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.it.common.GitUtils;\nimport com.walmartlabs.concord.it.common.MockGitSshServer;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static java.util.Collections.singletonMap;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class ConcordTaskForkFromGitRepoIT extends AbstractServerIT {\n\n    private MockGitSshServer gitServer;\n    private int gitPort;\n\n    @BeforeEach\n    public void setUp() throws Exception {\n        Path data = Paths.get(ConcordTaskForkFromGitRepoIT.class.getResource(\"concordTaskFork\").toURI());\n        Path repo = GitUtils.createBareRepository(data);\n\n        gitServer = new MockGitSshServer(0, repo);\n        gitServer.start();\n\n        gitPort = gitServer.getPort();\n    }\n\n    @AfterEach\n    public void tearDown() throws Exception {\n        gitServer.stop();\n    }\n\n    @Test\n    public void testFork() throws Exception {\n        String orgName = \"Default\";\n\n        // ---\n\n        String projectName = \"project@\" + randomString();\n        String repoSecretName = \"repoSecret@\" + randomString();\n        String repoName = \"repo@\" + randomString();\n        String repoUrl = String.format(ITConstants.GIT_SERVER_URL_PATTERN, gitPort);\n\n        // ---\n\n        SecretOperationResponse response = generateKeyPair(orgName, repoSecretName, false, null);\n\n        // ---\n\n        RepositoryEntry repo = new RepositoryEntry()\n                .name(repoName)\n                .url(repoUrl)\n                .branch(\"master\")\n                .secretId(response.getId());\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .repositories(singletonMap(repoName, repo)));\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"repo\", repoName);\n        StartProcessResponse parentSpr = start(input);\n\n        // ---\n\n        ProcessV2Api processApi = new ProcessV2Api(getApiClient());\n        ProcessEntry parentProcessEntry = waitForCompletion(getApiClient(), parentSpr.getInstanceId());\n\n        byte[] ab = getLog(parentProcessEntry.getInstanceId());\n        assertLog(\".*repoCommitMessage: initial message.*\", ab);\n\n        ProcessEntry processEntry = processApi.getProcess(parentSpr.getInstanceId(), Collections.singleton(\"childrenIds\"));\n        assertEquals(1, processEntry.getChildrenIds().size());\n\n        ProcessEntry child = processApi.getProcess(processEntry.getChildrenIds().iterator().next(), Collections.singleton(\"childrenIds\"));\n        assertNotNull(child);\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, child.getStatus());\n\n        ab = getLog(child.getInstanceId());\n        assertLog(\".*repoCommitMessage: initial message.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ConcordTaskIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport javax.xml.bind.DatatypeConverter;\nimport java.io.InputStream;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.common.GrepUtils.grep;\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ConcordTaskIT extends AbstractServerIT {\n\n    @Test\n    public void testStartArchive() throws Exception {\n        // create a new org\n\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // add the user A\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n\n        String userAName = \"userA_\" + randomString();\n        usersApi.createOrUpdateUser(new CreateUserRequest().username(userAName).type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse apiKeyA = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userAName));\n\n        // create the user A's team\n\n        String teamName = \"team_\" + randomString();\n\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n        teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamName));\n\n        teamsApi.addUsersToTeam(orgName, teamName, false, Collections.singletonList(new TeamUserEntry()\n                .username(userAName)\n                .role(TeamUserEntry.RoleEnum.MEMBER)));\n\n        // switch to the user A and create a new private project\n\n        setApiKey(apiKeyA.getKey());\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PRIVATE)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // grant the team access to the project\n\n        projectsApi.updateProjectAccessLevel(orgName, projectName, new ResourceAccessEntry()\n                .orgName(orgName)\n                .teamName(teamName)\n                .level(ResourceAccessEntry.LevelEnum.READER));\n\n        // start a new process using the project as the user A\n\n        byte[] payload = archive(ConcordTaskIT.class.getResource(\"concordTask\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.FINISHED);\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Done!.*\", ab);\n    }\n\n    @Test\n    public void testStartDirectory() throws Exception {\n        byte[] payload = archive(ConcordTaskIT.class.getResource(\"concordDirTask\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Done! Hello! Good Bye.*\", ab);\n    }\n\n    @Test\n    public void testCreateProject() throws Exception {\n        String projectName = \"project_\" + randomString();\n\n        byte[] payload = archive(ConcordTaskIT.class.getResource(\"concordProjectTask\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.newProjectName\", projectName);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Done!.*\", ab);\n    }\n\n    @Test\n    public void testStartAt() throws Exception {\n        byte[] payload = archive(ConcordTaskIT.class.getResource(\"concordDirTask\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n\n        Calendar c = Calendar.getInstance();\n        c.add(Calendar.SECOND, 30);\n        input.put(\"arguments.startAt\", DatatypeConverter.printDateTime(c));\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Done! Hello!.*\", ab);\n    }\n\n    @Test\n    public void testOutVarsNotFound() throws Exception {\n        byte[] payload = archive(ConcordTaskIT.class.getResource(\"concordOutVars\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Done!.*\", ab);\n    }\n\n    @Test\n    public void testSubprocessFail() throws Exception {\n        byte[] payload = archive(ConcordTaskIT.class.getResource(\"concordSubFail\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Fail!.*\", ab);\n        assertNoLog(\".*Done!.*\", ab);\n    }\n\n    @Test\n    public void testSubprocessIgnoreFail() throws Exception {\n        byte[] payload = archive(ConcordTaskIT.class.getResource(\"concordSubIgnoreFail\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Done!.*\", ab);\n    }\n\n    @Test\n    public void testStartChildFinishedWithError() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PUBLIC)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        byte[] payload = archive(ConcordTaskIT.class.getResource(\"concordTaskFailChild\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.FAILED);\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Child process.*FAILED.*BOOOM.*\", ab);\n    }\n\n    @Test\n    public void testForkWithItemsWithOutVariable() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PUBLIC)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        byte[] payload = archive(ConcordTaskIT.class.getResource(\"concordTaskForkWithItemsWithOut\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.FAILED);\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*color=RED.*\", ab);\n        assertLog(\".*color=WHITE.*\", ab);\n        assertLog(\".*Done.*\\\\[\\\\[.*\\\\], \\\\[.*\\\\]\\\\] is completed.*\", ab);\n    }\n\n    @Test\n    public void testForkWithItems() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PUBLIC)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        byte[] payload = archive(ConcordTaskIT.class.getResource(\"concordTaskForkWithItems\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.FAILED);\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*color=RED.*\", ab);\n        assertLog(\".*color=WHITE.*\", ab);\n        assertLog(\".*Done.*\\\\[\\\\[.*\\\\], \\\\[.*\\\\]\\\\] is completed.*\", ab);\n    }\n\n    @Test\n    public void testExternalApiToken() throws Exception {\n        String username = \"user_\" + randomString();\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(username)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeysApi = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse cakr = apiKeysApi.createUserApiKey(new CreateApiKeyRequest()\n                .username(username));\n\n        // ---\n\n        byte[] payload = archive(ConcordTaskIT.class.getResource(\"concordTaskApiKey\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.myApiKey\", cakr.getKey());\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLog(\".*Hello, Concord!.*\", ab);\n    }\n\n    @Test\n    public void testSuspendParentProcess() throws Exception {\n        String username = \"user_\" + randomString();\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(username)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeysApi = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse cakr = apiKeysApi.createUserApiKey(new CreateApiKeyRequest()\n                .username(username));\n\n        // ---\n\n        byte[] payload = archive(ConcordTaskIT.class.getResource(\"concordTaskSuspendParentProcess\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.myApiKey\", cakr.getKey());\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLog(\".*Hello, Concord!.*\", ab);\n    }\n\n    @Test\n    public void testSubWithNullArgValue() throws Exception {\n        byte[] payload = archive(ConcordTaskIT.class.getResource(\"concordSubWithNullArg\").toURI());\n\n        StartProcessResponse parentSpr = start(payload);\n\n        ProcessV2Api processApi = new ProcessV2Api(getApiClient());\n        waitForCompletion(getApiClient(), parentSpr.getInstanceId());\n\n        ProcessEntry processEntry = processApi.getProcess(parentSpr.getInstanceId(), Collections.singleton(\"childrenIds\"));\n        assertEquals(1, processEntry.getChildrenIds().size());\n\n        ProcessEntry child = processApi.getProcess(processEntry.getChildrenIds().iterator().next(), Collections.singleton(\"childrenIds\"));\n        assertNotNull(child);\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, child.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(child.getInstanceId());\n        assertLog(\".*Child process, nullValue: ''.*\", ab);\n        assertLog(\".*Child process, nullValue == null: 'true'.*\", ab);\n        assertLog(\".*Child process, hasVariable\\\\('nullValue'\\\\): true.*\", ab);\n    }\n\n    @Test\n    public void testForkWithArguments() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PUBLIC)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        byte[] payload = archive(ConcordTaskIT.class.getResource(\"concordTaskForkWithArguments\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n\n        StartProcessResponse parentSpr = start(input);\n\n        ProcessV2Api processApi = new ProcessV2Api(getApiClient());\n        waitForCompletion(getApiClient(), parentSpr.getInstanceId());\n\n        ProcessEntry processEntry = processApi.getProcess(parentSpr.getInstanceId(), Collections.singleton(\"childrenIds\"));\n        assertEquals(1, processEntry.getChildrenIds().size());\n\n        ProcessEntry child = processApi.getProcess(processEntry.getChildrenIds().iterator().next(), Collections.singleton(\"childrenIds\"));\n        assertNotNull(child);\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, child.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(child.getInstanceId());\n        assertLog(\".*Hello from a subprocess.*\", ab);\n        assertLog(\".*Concord Fork Process 123.*\", ab);\n    }\n\n    @Test\n    public void testForkWithForm() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PUBLIC)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        byte[] payload = archive(ConcordTaskIT.class.getResource(\"concordTaskForkWithForm\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n\n        StartProcessResponse parentSpr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForStatus(getApiClient(), parentSpr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n\n        // ---\n\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n\n        List<FormListEntry> forms = formsApi.listProcessForms(parentSpr.getInstanceId());\n        assertEquals(1, forms.size());\n\n        Map<String, Object> data = new HashMap<>();\n        data.put(\"firstName\", \"Boo\");\n        FormSubmitResponse fsr = formsApi.submitForm(pir.getInstanceId(), \"myForm\", data);\n        assertTrue(fsr.getOk());\n\n        waitForCompletion(getApiClient(), parentSpr.getInstanceId());\n\n        // ---\n        ProcessV2Api processApi = new ProcessV2Api(getApiClient());\n\n        ProcessEntry processEntry = processApi.getProcess(parentSpr.getInstanceId(), Collections.singleton(\"childrenIds\"));\n        assertEquals(1, processEntry.getChildrenIds().size());\n\n        ProcessEntry child = processApi.getProcess(processEntry.getChildrenIds().iterator().next(), Collections.singleton(\"childrenIds\"));\n        assertNotNull(child);\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, child.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(child.getInstanceId());\n        assertLog(\".*Hello from a subprocess.*\", ab);\n        assertLog(\".*Concord Fork Process 234.*\", ab);\n    }\n\n    @Test\n    public void testForkSuspend() throws Exception {\n        String nameVar = \"name_\" + randomString();\n\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PUBLIC)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        byte[] payload = archive(ConcordTaskIT.class.getResource(\"concordTaskForkSuspend\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"arguments.name\", nameVar);\n\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLog(\".*\\\\{varFromFork=Hello, \" + nameVar + \"\\\\}.*\", ab);\n        assertLog(\".*\\\\{varFromFork=Bye, \" + nameVar + \"\\\\}.*\", ab);\n    }\n\n    @Test\n    public void testForkAsyncGrabOutVars() throws Exception {\n        byte[] payload = archive(ConcordTaskIT.class.getResource(\"concordTaskForkAsyncGrabOutVars\").toURI());\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n        byte[] ab = getLog(pe.getInstanceId());\n        if (grep(\".*\\\\{x=1, y=2, z=3\\\\}.*\", ab).isEmpty()\n                || grep(\".*\\\\{a=4, b=5, c=6\\\\}.*\", ab).isEmpty()) {\n\n            for (UUID id : pe.getChildrenIds()) {\n                ProcessEntry pp = new ProcessV2Api(getApiClient()).getProcess(id, Collections.singleton(\"childrenIds\"));\n                System.out.println(\"process: \" + pp.getInstanceId() + \", status: \" + pp.getStatus() + \", out: \" + getOutVars(id, processApi));\n                System.out.println(\">>>\");\n            }\n        }\n        assertLog(\".*\\\\{x=1, y=2, z=3\\\\}.*\", ab);\n        assertLog(\".*\\\\{a=4, b=5, c=6\\\\}.*\", ab);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Map<String, Object> getOutVars(UUID id, ProcessApi processApi) throws Exception {\n        try (InputStream is = processApi.downloadAttachment(id, \"out.json\")) {\n            ObjectMapper om = new ObjectMapper();\n            return om.readValue(is, Map.class);\n        } catch (ApiException e) {\n            if (e.getCode() == 404) {\n                return null;\n            }\n            throw e;\n        }\n    }\n\n    @Test\n    public void testParentInstanceId() throws Exception {\n        byte[] payload = archive(ConcordTaskIT.class.getResource(\"parentInstanceId\").toURI());\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        ProcessV2Api processV2Api = new ProcessV2Api(getApiClient());\n        ProcessListFilter filter = ProcessListFilter.builder()\n                .parentInstanceId(pe.getInstanceId())\n                .build();\n\n        List<ProcessEntry> l = processV2Api.listProcesses(filter);\n        assertEquals(2, l.size());\n\n        for (ProcessEntry e : l) {\n            byte[] ab = getLog(e.getInstanceId());\n            assertLog(\".*parentInstanceId: \" + pe.getInstanceId() + \".*\", ab);\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ConfigurableResourcesIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.junit.jupiter.api.Test;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class ConfigurableResourcesIT extends AbstractServerIT {\n\n    @Test\n    public void testProfiles() throws Exception {\n        // prepare the payload\n\n        byte[] payload = archive(ConfigurableResourcesIT.class.getResource(\"configurableProfilesDirectory\").toURI());\n\n        // start the process\n\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // wait for completion\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // get the name of the agent's log file\n\n        assertNotNull(pir.getLogFileName());\n\n        // check the logs\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Hello, world.*\", ab);\n    }\n\n    @Test\n    public void testFlows() throws Exception {\n        // prepare the payload\n\n        byte[] payload = archive(ConfigurableResourcesIT.class.getResource(\"configurableFlowsDirectory\").toURI());\n\n        // start the process\n\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // wait for completion\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // get the name of the agent's log file\n\n        assertNotNull(pir.getLogFileName());\n\n        // check the logs\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*External flow!.*\", ab);\n    }\n\n    @Test\n    public void testDisabledProfiles() throws Exception {\n        // prepare the payload\n\n        byte[] payload = archive(ConfigurableResourcesIT.class.getResource(\"disableProfilesDirectory\").toURI());\n\n        // start the process\n\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // wait for completion\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // get the name of the agent's log file\n\n        assertNotNull(pir.getLogFileName());\n\n        // check the logs\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Hello, stranger.*\", ab);\n    }\n\n    @Test\n    public void testInvalidDir() throws Exception {\n        byte[] payload = archive(ConfigurableResourcesIT.class.getResource(\"invalidResourcesPath\").toURI());\n\n        // ---\n\n        try {\n            start(payload);\n        } catch (ApiException e) {\n            String msg = e.getMessage();\n            assertTrue(msg.contains(\"../../etc\"));\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/CronIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.client2.CreateSecretRequest;\nimport com.walmartlabs.concord.client2.ProcessListFilter;\nimport com.walmartlabs.concord.common.PathUtils;\nimport org.eclipse.jgit.api.Git;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\n\nimport java.io.File;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.concurrent.TimeUnit;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport static com.walmartlabs.concord.common.GrepUtils.grep;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForStatus;\nimport static com.walmartlabs.concord.it.server.AbstractServerIT.DEFAULT_TEST_TIMEOUT;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\n\n@Timeout(value = 2 * DEFAULT_TEST_TIMEOUT, unit = TimeUnit.MILLISECONDS)\npublic class CronIT extends AbstractServerIT {\n\n    @Test\n    public void testProfiles() throws Exception {\n        String gitUrl = initRepo(\"cronProfiles\");\n\n        // ---\n\n        String orgName = \"org_\" + randomString();\n        String projectName = \"project_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        if (orgApi.getOrg(orgName) == null) {\n            orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n        }\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PUBLIC)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .url(gitUrl)\n                        .branch(\"master\"))));\n\n        // ---\n\n        waitForTriggers(orgName, projectName, repoName, 2);\n\n        // ---\n\n        UUID aaaProcessId = null;\n        UUID bbbProcessId = null;\n\n        while (true) {\n            Thread.sleep(1000);\n\n            List<ProcessEntry> aaaProcesses = listCronProcesses(orgName, projectName, repoName, \"AAA\");\n            List<ProcessEntry> bbbProcesses = listCronProcesses(orgName, projectName, repoName, \"BBB\");\n\n            if (aaaProcesses.isEmpty() || bbbProcesses.isEmpty()) {\n                continue;\n            }\n\n            List<ProcessEntry> processes = Stream.concat(aaaProcesses.stream(), bbbProcesses.stream())\n                    .collect(Collectors.toList());\n\n            for (ProcessEntry e : processes) {\n                assertNotEquals(ProcessEntry.StatusEnum.FAILED, e.getStatus());\n            }\n\n            if (aaaProcessId == null) {\n                aaaProcessId = findProcessWithLogEntry(aaaProcesses, \".*Hello, AAA!.*\");\n            }\n\n            if (bbbProcessId == null) {\n                bbbProcessId = findProcessWithLogEntry(bbbProcesses, \".*Hello, BBB!.*\");\n            }\n\n            if (aaaProcessId != null && bbbProcessId != null) {\n                break;\n            }\n        }\n\n        // --- clean up\n\n        ProcessEntry aaaProcess = waitForStatus(getApiClient(), aaaProcessId, ProcessEntry.StatusEnum.FINISHED);\n        ProcessEntry bbbProcess = waitForStatus(getApiClient(), bbbProcessId, ProcessEntry.StatusEnum.FINISHED);\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, aaaProcess.getStatus());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, bbbProcess.getStatus());\n\n        projectsApi.deleteProject(orgName, projectName);\n    }\n\n    @Test\n    public void testRunAs() throws Exception {\n        String gitUrl = initRepo(\"cronRunAs\");\n\n        // ---\n\n        String orgName = \"org_\" + randomString();\n        String projectName = \"project_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        if (orgApi.getOrg(orgName) == null) {\n            orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n        }\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PUBLIC)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .url(gitUrl)\n                        .branch(\"master\"))));\n\n        // ---\n\n        String username = \"user_\" + randomString();\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        CreateUserResponse cur = usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(username)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse apiKeyResponse = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(username));\n\n        com.walmartlabs.concord.client2.SecretClient secretsApi = new com.walmartlabs.concord.client2.SecretClient(getApiClient());\n        CreateSecretRequest secret = CreateSecretRequest.builder()\n                .org(orgName)\n                .addProjectNames(projectName)\n                .name(\"test-run-as\")\n                .data(apiKeyResponse.getKey().getBytes(StandardCharsets.UTF_8))\n                .build();\n        secretsApi.createSecret(secret);\n\n        waitForTriggers(orgName, projectName, repoName, 1);\n\n        // ---\n\n        ProcessV2Api processApi = new ProcessV2Api(getApiClient());\n        ProcessListFilter filter = ProcessListFilter.builder()\n                .orgName(orgName)\n                .projectName(projectName)\n                .build();\n\n        while (true) {\n            Thread.sleep(1000);\n\n            List<ProcessEntry> processes = processApi.listProcesses(filter);\n            if (processes.size() != 1) {\n                continue;\n            }\n\n            ProcessEntry pe = processes.get(0);\n            assertNotEquals(ProcessEntry.StatusEnum.FAILED, pe.getStatus());\n            pe = waitForStatus(getApiClient(), pe.getInstanceId(), ProcessEntry.StatusEnum.FINISHED);\n            assertEquals(ProcessEntry.StatusEnum.FINISHED, pe.getStatus());\n            assertEquals(cur.getId(), pe.getInitiatorId());\n            assertEquals(username, pe.getInitiator());\n            break;\n        }\n\n        // ---\n\n        projectsApi.deleteProject(orgName, projectName);\n    }\n\n    private static String initRepo(String initResource) throws Exception {\n        Path tmpDir = createTempDir();\n\n        File src = new File(TriggersRefreshIT.class.getResource(initResource).toURI());\n        PathUtils.copy(src.toPath(), tmpDir);\n\n        try (Git repo = Git.init().setInitialBranch(\"master\").setDirectory(tmpDir.toFile()).call()) {\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"import\").call();\n            return tmpDir.toAbsolutePath().toString();\n        }\n    }\n\n    private List<ProcessEntry> listCronProcesses(String o, String p, String r, String tag) throws ApiException {\n        ProcessV2Api processV2Api = new ProcessV2Api(getApiClient());\n\n        ProcessListFilter filter = ProcessListFilter.builder()\n                .orgName(o)\n                .projectName(p)\n                .repoName(r)\n                .initiator(\"cron\")\n                .tags(Collections.singleton(tag))\n                .build();\n\n        return processV2Api.listProcesses(filter);\n    }\n\n    private List<TriggerEntry> waitForTriggers(String orgName, String projectName, String repoName, int expectedCount) throws Exception {\n        TriggersApi triggerResource = new TriggersApi(getApiClient());\n        while (true) {\n            List<TriggerEntry> l = triggerResource.listTriggers(orgName, projectName, repoName);\n            if (l != null && l.size() == expectedCount) {\n                return l;\n            }\n\n            Thread.sleep(1000);\n        }\n    }\n\n    private boolean hasLogEntry(ProcessEntry e, String pattern) throws Exception {\n        byte[] ab = getLog(e.getInstanceId());\n        List<String> l = grep(pattern, ab);\n        return !l.isEmpty();\n    }\n\n    private UUID findProcessWithLogEntry(List<ProcessEntry> processes, String pattern) throws Exception {\n        for (ProcessEntry e : processes) {\n            if (hasLogEntry(e, pattern)) {\n                return e.getInstanceId();\n            }\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/CrudIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.parallel.Execution;\nimport org.junit.jupiter.api.parallel.ExecutionMode;\n\nimport java.util.*;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n@Execution(ExecutionMode.CONCURRENT)\npublic class CrudIT extends AbstractServerIT {\n\n    @Test\n    public void testOrgUpdate() throws Exception {\n        String orgName = \"org_\" + randomString();\n        Map<String, Object> meta = Collections.singletonMap(\"x\", \"123\");\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry()\n                .name(orgName)\n                .meta(meta)\n                .visibility(OrganizationEntry.VisibilityEnum.PUBLIC));\n\n        // ---\n\n        OrganizationEntry e = orgApi.getOrg(orgName);\n        assertNotNull(e.getMeta());\n        assertEquals(\"123\", e.getMeta().get(\"x\"));\n\n        // ---\n\n        orgApi.createOrUpdateOrg(e.meta(Collections.singletonMap(\"x\", \"234\")));\n\n        // ---\n\n        e = orgApi.getOrg(orgName);\n        assertNotNull(e.getMeta());\n        assertEquals(\"234\", e.getMeta().get(\"x\"));\n    }\n\n    @Test\n    public void testProject() throws Exception {\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n\n        String orgName = \"Default\";\n        String projectName = \"project_\" + randomString();\n        String updateProjectName = \"updateProject_\" + randomString();\n\n        // --- create\n\n        ProjectOperationResponse createResp = projectsApi.createOrUpdateProject(orgName, new ProjectEntry().name(projectName));\n        assertTrue(createResp.getOk());\n        assertNotNull(createResp.getId());\n\n        // --- update\n\n        ProjectOperationResponse updateResp = projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .id(createResp.getId())\n                .name(updateProjectName));\n        assertEquals(ProjectOperationResponse.ResultEnum.UPDATED, updateResp.getResult());\n        assertEquals(createResp.getId(), updateResp.getId());\n\n        // --- get\n\n        ProjectEntry projectEntry = projectsApi.getProject(orgName, updateProjectName);\n        assertEquals(projectEntry.getId(), createResp.getId());\n\n        // --- create\n\n        createResp = projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .repositories(Collections.emptyMap()));\n        assertTrue(createResp.getOk());\n\n        // --- list\n\n        List<ProjectEntry> projectList = projectsApi.findProjects(orgName, null, null, null);\n        projectEntry = findProject(projectList, projectName);\n        assertNotNull(projectEntry);\n\n        // --- update project's organization id\n\n        String newOrgName = \"org_\" + randomString();\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        CreateOrganizationResponse createOrganizationResponse =\n                orgApi.createOrUpdateOrg(new OrganizationEntry().name(newOrgName));\n        assertTrue(createOrganizationResponse.getOk());\n        assertNotNull(createOrganizationResponse.getId());\n\n        ProjectOperationResponse moveResp = projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .id(createResp.getId())\n                .orgId(createOrganizationResponse.getId())\n        );\n        assertTrue(moveResp.getOk());\n        assertEquals(ProjectOperationResponse.ResultEnum.UPDATED, moveResp.getResult());\n\n        // --- error - empty name\n\n        try {\n            projectsApi.createOrUpdateProject(orgName, new ProjectEntry().name(\"\"));\n            fail(\"Project name should not be empty string\");\n        } catch (ApiException e) {\n            assertTrue(e.getMessage().contains(\"must match\"), e::getMessage);\n        }\n\n        // --- error - null name and id\n\n        try {\n            projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                    .name(null)\n                    .id(null)\n            );\n            fail(\"Project name should not be empty string\");\n        } catch (ApiException e) {\n            assertTrue(e.getMessage().contains(\"'name' is required\"));\n        }\n\n        // --- update project's organization name\n\n        moveResp = projectsApi.createOrUpdateProject(newOrgName, new ProjectEntry()\n                .name(projectName)\n                .orgName(orgName)\n        );\n        assertTrue(moveResp.getOk());\n        assertEquals(ProjectOperationResponse.ResultEnum.UPDATED, moveResp.getResult());\n\n        // --- delete\n\n        GenericOperationResult deleteResp = projectsApi.deleteProject(orgName, projectName);\n        assertTrue(deleteResp.getOk());\n    }\n\n    @Test\n    public void testNonUniqueRepositoryNames() throws Exception {\n        String orgName = \"Default\";\n\n        String projectName1 = \"project1_\" + randomString();\n        String projectName2 = \"project2_\" + randomString();\n\n        String repoName = \"repo_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName1)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .name(repoName)\n                        .url(createRepo(\"repositoryRefresh\"))\n                        .branch(\"master\"))));\n\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName2)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .name(repoName)\n                        .url(createRepo(\"repositoryRefresh\"))\n                        .branch(\"master\"))));\n    }\n\n    @Test\n    public void testInventory() throws Exception {\n        InventoriesApi inventoriesApi = new InventoriesApi(getApiClient());\n\n        String orgName = \"Default\";\n        String inventoryName = \"inventory_\" + randomString();\n        String updatedInventoryName = \"updateInventory_\" + randomString();\n\n        // --- create\n\n        CreateInventoryResponse cir = inventoriesApi.createOrUpdateInventory(orgName, new InventoryEntry().name(inventoryName));\n        assertTrue(cir.getOk());\n        assertNotNull(cir.getId());\n\n        // --- update\n\n        CreateInventoryResponse updateInventoryResponse = inventoriesApi.createOrUpdateInventory(orgName,\n                new InventoryEntry()\n                        .id(cir.getId())\n                        .name(updatedInventoryName));\n        assertEquals(updateInventoryResponse.getResult(), CreateInventoryResponse.ResultEnum.UPDATED);\n        assertEquals(updateInventoryResponse.getId(), cir.getId());\n\n        // --- get\n\n        InventoryEntry inventoryEntry = inventoriesApi.getInventory(orgName, updatedInventoryName);\n        assertNotNull(inventoryEntry);\n\n        // -- list\n\n        List<InventoryEntry> l = inventoriesApi.listInventories(orgName);\n        assertTrue(l.stream().anyMatch(e -> updatedInventoryName.equals(e.getName()) && orgName.equals(e.getOrgName())));\n\n        // --- delete\n\n        GenericOperationResult deleteInventoryResponse = inventoriesApi.deleteInventory(orgName, updatedInventoryName);\n        assertTrue(deleteInventoryResponse.getResult() == GenericOperationResult.ResultEnum.DELETED);\n    }\n\n    @Test\n    public void testInventoryData() throws Exception {\n        InventoryDataApi dataApi = new InventoryDataApi(getApiClient());\n\n        String orgName = \"Default\";\n        String inventoryName = \"inventory_\" + randomString();\n        String itemPath = \"/a\";\n        Map<String, Object> data = Collections.singletonMap(\"k\", \"v\");\n\n        InventoriesApi inventoriesApi = new InventoriesApi(getApiClient());\n        inventoriesApi.createOrUpdateInventory(orgName, new InventoryEntry().name(inventoryName));\n\n        // --- create\n\n        @SuppressWarnings(\"unchecked\")\n        Map<String, Object> result = (Map<String, Object>) dataApi.updateInventoryData(orgName, inventoryName, itemPath, data);\n        assertNotNull(result);\n        assertEquals(Collections.singletonMap(\"a\", data), result);\n\n        // --- get\n\n        @SuppressWarnings(\"unchecked\")\n        Map<String, Object> result2 = (Map<String, Object>) dataApi.getInventoryData(orgName, inventoryName, itemPath, false);\n        assertNotNull(result2);\n        assertEquals(Collections.singletonMap(\"a\", data), result);\n\n        // --- delete\n\n        DeleteInventoryDataResponse didr = dataApi.deleteInventoryData(orgName, inventoryName, itemPath);\n        assertNotNull(didr);\n        assertTrue(didr.getOk());\n    }\n\n    @Test\n    @SuppressWarnings(\"unchecked\")\n    public void testInventoryQuery() throws Exception {\n        InventoryQueriesApi queriesApi = new InventoryQueriesApi(getApiClient());\n\n        String orgName = \"Default\";\n        String inventoryName = \"inventory_\" + randomString();\n        String queryName = \"queryName_\" + randomString();\n        String text = \"select * from test_\" + randomString();\n\n        InventoriesApi inventoriesApi = new InventoriesApi(getApiClient());\n        inventoriesApi.createOrUpdateInventory(orgName, new InventoryEntry().name(inventoryName));\n\n        // ---\n\n        InventoryDataApi dataApi = new InventoryDataApi(getApiClient());\n        dataApi.updateInventoryData(orgName, inventoryName, \"/test\", Collections.singletonMap(\"k\", \"v\"));\n\n        // --- create\n\n        CreateInventoryQueryResponse cqr = queriesApi.createOrUpdateInventoryQuery(orgName, inventoryName, queryName, text);\n        assertTrue(cqr.getOk());\n        assertNotNull(cqr.getId());\n\n        // --- update\n        String updatedText = \"select item_data::text from inventory_data\";\n        CreateInventoryQueryResponse uqr = queriesApi.createOrUpdateInventoryQuery(orgName, inventoryName, queryName, updatedText);\n        assertTrue(uqr.getOk());\n        assertNotNull(uqr.getId());\n\n        // --- get\n        InventoryQueryEntry e1 = queriesApi.getInventoryQuery(orgName, inventoryName, queryName);\n        assertNotNull(e1);\n        assertNotNull(e1.getId());\n        assertEquals(queryName, e1.getName());\n        assertEquals(updatedText, e1.getText());\n\n        // --- list\n        List<InventoryQueryEntry> list = queriesApi.listInventoryQueries(orgName, inventoryName);\n        assertTrue(list.stream().anyMatch(e -> queryName.equals(e.getName()) && updatedText.equals(e.getText())));\n\n        // --- exec\n        @SuppressWarnings(\"unchecked\")\n        List<Object> result = queriesApi.executeInventoryQuery(orgName, inventoryName, queryName, null);\n        assertNotNull(result);\n        Map<String, Object> m = (Map<String, Object>) result.get(0);\n        assertEquals(Collections.singletonMap(\"k\", \"v\"), m);\n\n        // --- delete\n        DeleteInventoryQueryResponse dqr = queriesApi.deleteInventoryQuery(orgName, inventoryName, queryName);\n        assertNotNull(dqr);\n        assertTrue(dqr.getOk());\n    }\n\n    @Test\n    public void testInvalidQueryName() throws Exception {\n        InventoryQueriesApi queriesApi = new InventoryQueriesApi(getApiClient());\n\n        String orgName = \"Default\";\n        String inventoryName = \"inventory_\" + randomString();\n        String queryName = \"queryName_\" + randomString();\n\n        InventoriesApi inventoriesApi = new InventoriesApi(getApiClient());\n        inventoriesApi.createOrUpdateInventory(orgName, new InventoryEntry().name(inventoryName));\n\n        // ---\n\n        try {\n            queriesApi.executeInventoryQuery(orgName, inventoryName, queryName, null);\n            fail(\"should fail\");\n        } catch (ApiException e) {\n            assertTrue(e.getMessage().contains(\"not found\") && e.getMessage().contains(queryName));\n        }\n    }\n\n    @Test\n    public void testDashes() throws Exception {\n        String orgName = randomString() + \"-test~\";\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        orgApi.getOrg(orgName);\n\n        // ---\n\n        String teamName = randomString() + \"-test~\";\n\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n        teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamName));\n\n        teamsApi.getTeam(orgName, teamName);\n\n        // ---\n\n        String projectName = randomString() + \"-test~\";\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry().name(projectName));\n\n        projectsApi.getProject(orgName, projectName);\n\n        // ---\n\n        String secretName = randomString() + \"-test~\";\n        addPlainSecret(orgName, secretName, true, null, new byte[]{0, 1, 2, 3});\n\n        SecretsApi secretResource = new SecretsApi(getApiClient());\n        secretResource.delete(orgName, secretName);\n    }\n\n    @Test\n    public void testTeam() throws Exception {\n        String teamName = \"team_\" + randomString();\n        String orgName = \"Default\";\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n\n        // Create\n        CreateTeamResponse teamResponse = teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamName));\n        assertTrue(teamResponse.getOk());\n        assertNotNull(teamResponse.getId());\n        assertEquals(teamResponse.getResult(), CreateTeamResponse.ResultEnum.CREATED);\n\n        // Update Team by Name\n        CreateTeamResponse updateTeamResponse = teamsApi.createOrUpdateTeam(orgName, new TeamEntry()\n                .name(teamName)\n                .description(\"Update Description\"));\n        assertEquals(updateTeamResponse.getId(), teamResponse.getId());\n        assertEquals(updateTeamResponse.getResult(), CreateTeamResponse.ResultEnum.UPDATED);\n\n        // Update Team by ID\n        String updatedTeamName = \"UpdatedName_\" + randomString();\n        CreateTeamResponse updateTeamById = teamsApi.createOrUpdateTeam(orgName, new TeamEntry()\n                .id(teamResponse.getId())\n                .name(updatedTeamName)\n                .description(\"Name is updated\"));\n        assertEquals(teamResponse.getId(), updateTeamById.getId());\n        assertEquals(updateTeamResponse.getResult(), CreateTeamResponse.ResultEnum.UPDATED);\n\n        // Get\n        TeamEntry teamEntry = teamsApi.getTeam(orgName, updatedTeamName);\n        assertEquals(teamResponse.getId(), teamEntry.getId());\n        assertEquals(updatedTeamName, teamEntry.getName());\n    }\n\n    @Test\n    public void testTeamManagementWithUserIds() throws Exception {\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n\n        String orgName = \"org_\" + randomString();\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry()\n                .name(orgName));\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n\n        String userAName = \"userA_\" + randomString();\n        CreateUserResponse curA = usersApi.createOrUpdateUser(new CreateUserRequest()\n                .type(CreateUserRequest.TypeEnum.LOCAL)\n                .username(userAName));\n\n        String userBName = \"userB_\" + randomString();\n        CreateUserResponse curB = usersApi.createOrUpdateUser(new CreateUserRequest()\n                .type(CreateUserRequest.TypeEnum.LOCAL)\n                .username(userBName));\n\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n\n        String teamName = \"team_\" + randomString();\n        teamsApi.createOrUpdateTeam(orgName, new TeamEntry()\n                .name(teamName));\n\n        // ---\n\n        List<TeamUserEntry> l = teamsApi.listUserTeams(orgName, teamName);\n        assertEquals(1, l.size());\n        assertEquals(\"admin\", l.get(0).getUsername());\n\n        // ---\n\n        teamsApi.addUsersToTeam(orgName, teamName, true, Arrays.asList(\n                new TeamUserEntry().userId(curA.getId()),\n                new TeamUserEntry().userId(curB.getId())));\n\n        // ---\n\n        l = teamsApi.listUserTeams(orgName, teamName);\n        assertEquals(2, l.size());\n        assertEquals(userAName.toLowerCase(), l.get(0).getUsername());\n        assertEquals(userBName.toLowerCase(), l.get(1).getUsername());\n    }\n\n    @Test\n    public void testSecrets() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        CreateOrganizationResponse orgResponse = orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        String secretName = \"secret_\" + randomString();\n        addUsernamePassword(orgName, secretName, false, null, \"username\", \"password\");\n\n        // ---\n\n        String projectName = \"project_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        ProjectOperationResponse projectResp = projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .name(repoName)\n                        .url(createRepo(\"repositoryRefresh\"))\n                        .branch(\"master\")\n                        .secretName(secretName))));\n        UUID projectId = projectResp.getId();\n\n        // ---\n\n        SecretsApi secretsApi = new SecretsApi(getApiClient());\n        SecretUpdateRequest updateReq;\n\n        // --  Update secret project name\n\n        updateReq = new SecretUpdateRequest();\n        updateReq.projectName(projectName).orgId(orgResponse.getId());\n        secretsApi.updateSecretV1(orgName, secretName, updateReq);\n        updateReq.setProjectName(\"\");\n        secretsApi.updateSecretV1(orgName, secretName, updateReq);\n        updateReq.setProjectName(null);\n        secretsApi.updateSecretV1(orgName, secretName, updateReq);\n\n        // --  Update secret project ID\n\n        updateReq = new SecretUpdateRequest();\n        updateReq.setProjectId(projectId);\n        secretsApi.updateSecretV1(orgName, secretName, updateReq);\n        updateReq.setProjectId(null);\n        secretsApi.updateSecretV1(orgName, secretName, updateReq);\n\n        // --  Delete secret\n\n        secretsApi.delete(orgName, secretName);\n\n        /// ---\n\n        ProjectEntry projectEntry = projectsApi.getProject(orgName, projectName);\n        assertNull(projectEntry.getRepositories());\n\n        List<RepositoryEntry> repos = new RepositoriesApi(getApiClient()).listRepositories(orgName, projectName, null, null, null);\n        assertEquals(1, repos.size());\n\n        RepositoryEntry repo = repos.get(0);\n        assertNotNull(repo);\n        assertNull(repo.getSecretName());\n    }\n\n    private static ProjectEntry findProject(List<ProjectEntry> l, String name) {\n        return l.stream().filter(e -> name.equals(e.getName())).findAny().get();\n    }\n\n    @Test\n    public void testOrganization() throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgName = \"org_\" + randomString();\n        String updatedOrgName = \"updateOrg_\" + randomString();\n\n        // --- create\n\n        CreateOrganizationResponse createOrganizationResponse = orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n        assertTrue(createOrganizationResponse.getOk());\n        assertNotNull(createOrganizationResponse.getId());\n\n        // --- update\n\n        CreateOrganizationResponse updateOrganizationResponse = orgApi.createOrUpdateOrg(new OrganizationEntry()\n                .id(createOrganizationResponse.getId())\n                .name(updatedOrgName));\n        assertEquals(updateOrganizationResponse.getResult(), CreateOrganizationResponse.ResultEnum.UPDATED);\n        assertEquals(updateOrganizationResponse.getId(), createOrganizationResponse.getId());\n\n        // --- get\n\n        OrganizationEntry organizationEntry = orgApi.getOrg(updatedOrgName);\n        assertNotNull(organizationEntry);\n        assertEquals(createOrganizationResponse.getId(), organizationEntry.getId());\n\n        // --- list\n\n        List<OrganizationEntry> organizationEntryList = orgApi.listOrgs(true, null, null, null);\n        assertNotNull(organizationEntryList);\n        organizationEntry = findOrganization(organizationEntryList, updatedOrgName);\n        assertNotNull(organizationEntry);\n    }\n\n    @Test\n    public void testOrgVisibility() throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgName = \"org_\" + randomString();\n\n        // create private org\n\n        CreateOrganizationResponse createOrganizationResponse = orgApi.createOrUpdateOrg(new OrganizationEntry()\n                .name(orgName)\n                .visibility(OrganizationEntry.VisibilityEnum.PRIVATE));\n        assertTrue(createOrganizationResponse.getOk());\n        assertNotNull(createOrganizationResponse.getId());\n\n        // --- private org available for admin\n\n        List<OrganizationEntry> orgs = orgApi.listOrgs(false, null, null, null);\n        assertTrue(orgs.stream().anyMatch(e -> e.getId().equals(createOrganizationResponse.getId())));\n\n        // add the user A\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n\n        String userAName = \"userA_\" + randomString();\n        usersApi.createOrUpdateUser(new CreateUserRequest().username(userAName).type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse apiKeyA = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userAName));\n\n        setApiKey(apiKeyA.getKey());\n\n        orgs = orgApi.listOrgs(true, null, null, null);\n        assertTrue(orgs.stream().noneMatch(e -> e.getId().equals(createOrganizationResponse.getId())));\n    }\n\n    @Test\n    public void testOrgMeta() throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgName = \"org_\" + randomString();\n        Map<String, Object> meta = Collections.singletonMap(\"x\", true);\n\n        CreateOrganizationResponse cor = orgApi.createOrUpdateOrg(new OrganizationEntry()\n                .name(orgName)\n                .meta(meta));\n\n        // ---\n\n        OrganizationEntry e = orgApi.getOrg(orgName);\n        assertNotNull(e);\n\n        Map<String, Object> meta2 = e.getMeta();\n        assertNotNull(meta2);\n        assertEquals(meta.get(\"x\"), meta2.get(\"x\"));\n\n        // ---\n\n        meta = Collections.singletonMap(\"y\", 123.0);\n        orgApi.createOrUpdateOrg(new OrganizationEntry()\n                .id(cor.getId())\n                .meta(meta));\n\n        e = orgApi.getOrg(orgName);\n        assertNotNull(e);\n\n        Map<String, Object> meta3 = e.getMeta();\n        assertNotNull(meta3);\n        assertEquals(1, meta3.size());\n        assertEquals(meta.get(\"y\"), meta3.get(\"y\"));\n    }\n\n    @Test\n    public void testPolicies() throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgName = \"org_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n\n        String projectName = \"project_\" + randomString();\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry().name(projectName));\n\n        // ---\n\n        String userName = \"user_\" + randomString();\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        CreateUserResponse createUserResponse = usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        // ---\n\n        PolicyApi policyResource = new PolicyApi(getApiClient());\n\n        String policyName = \"policy_\" + randomString();\n        Map<String, Object> policyRules = Collections.singletonMap(\"x\", 123);\n\n        PolicyOperationResponse por = policyResource.createOrUpdatePolicy(new PolicyEntry()\n                .name(policyName)\n                .rules(policyRules));\n\n        String newPolicyName = \"policy2_\" + randomString();\n        policyResource.createOrUpdatePolicy(new PolicyEntry()\n                .id(por.getId())\n                .name(newPolicyName)\n                .rules(policyRules));\n\n        String policyForUser = \"policy3_\" + randomString();\n        policyResource.createOrUpdatePolicy(new PolicyEntry()\n                .name(policyForUser)\n                .rules(policyRules));\n\n        policyResource.createOrUpdatePolicy(new PolicyEntry()\n                .id(por.getId())\n                .name(policyName)\n                .rules(policyRules));\n\n        // ---\n\n        PolicyEntry pe = policyResource.getPolicy(policyName);\n        assertNotNull(pe);\n\n        // ---\n\n        policyResource.linkPolicy(policyName, new PolicyLinkEntry()\n                .orgName(orgName)\n                .projectName(projectName));\n\n        List<PolicyEntry> l = policyResource.listPolicies(orgName, projectName, null, null, null);\n        assertEquals(1, l.size());\n        assertEquals(policyName, l.get(0).getName());\n\n        // ---\n\n        policyResource.linkPolicy(policyName, new PolicyLinkEntry().orgName(orgName));\n\n        l = policyResource.listPolicies(orgName, null, null, null, null);\n        assertEquals(1, l.size());\n        assertEquals(policyName, l.get(0).getName());\n\n        // ---\n\n        policyResource.linkPolicy(policyName, new PolicyLinkEntry().userName(userName));\n\n        l = policyResource.listPolicies(null, null, userName, null, null);\n        assertEquals(1, l.size());\n        assertEquals(policyName, l.get(0).getName());\n\n        // ---\n\n        policyResource.linkPolicy(policyForUser, new PolicyLinkEntry()\n                .orgName(orgName)\n                .projectName(projectName)\n                .userName(userName));\n\n        l = policyResource.listPolicies(orgName, projectName, userName, null, null);\n        assertEquals(1, l.size());\n        assertEquals(policyForUser, l.get(0).getName());\n\n        // ---\n\n        policyResource.unlinkPolicy(policyName, orgName, projectName, null, null, null);\n        l = policyResource.listPolicies(orgName, projectName, null, null, null);\n        assertEquals(1, l.size());\n        l = policyResource.listPolicies(orgName, null, null, null, null);\n        assertEquals(1, l.size());\n\n        // ---\n\n        policyResource.unlinkPolicy(policyName, orgName, null, null, null, null);\n        l = policyResource.listPolicies(orgName, projectName, null, null, null);\n        assertEquals(0, l.size());\n        l = policyResource.listPolicies(orgName, null, null, null, null);\n        assertEquals(0, l.size());\n\n        // ---\n\n        policyResource.unlinkPolicy(policyName, null, null, userName, null, null);\n        l = policyResource.listPolicies(null, null, userName, null, null);\n        assertEquals(0, l.size());\n        l = policyResource.listPolicies(orgName, null, null, null, null);\n        assertEquals(0, l.size());\n\n        // ---\n\n        policyResource.unlinkPolicy(policyForUser, orgName, projectName, userName, null, null);\n        l = policyResource.listPolicies(orgName, projectName, userName, null, null);\n        assertEquals(0, l.size());\n        l = policyResource.listPolicies(orgName, null, null, null, null);\n        assertEquals(0, l.size());\n\n        // ---\n\n        policyResource.deletePolicy(policyName);\n        l = policyResource.listPolicies(null, null, null, null, null);\n        for (PolicyEntry e : l) {\n            if (policyName.equals(e.getName())) {\n                fail(\"Should've been removed: \" + e.getName());\n            }\n        }\n\n        usersApi.deleteUser(createUserResponse.getId());\n\n        policyResource.deletePolicy(policyForUser);\n    }\n\n    @Test\n    public void testRoles() throws Exception {\n        RolesApi rolesApi = new RolesApi(getApiClient());\n\n        String roleName = \"role_\" + randomString();\n\n        // --- create\n\n        RoleOperationResponse createRoleResponse = rolesApi.createOrUpdateRole(new RoleEntry().name(roleName));\n        assertEquals(RoleOperationResponse.ResultEnum.CREATED, createRoleResponse.getResult());\n        assertNotNull(createRoleResponse.getId());\n\n        // --- update\n\n        RoleOperationResponse updateRoleResponse = rolesApi.createOrUpdateRole(new RoleEntry()\n                .id(createRoleResponse.getId())\n                .name(roleName)\n                .permissions(Collections.singleton(\"getProcessQueueAllOrgs\")));\n        assertEquals(RoleOperationResponse.ResultEnum.UPDATED, updateRoleResponse.getResult());\n\n        // --- get\n\n        RoleEntry roleEntry = rolesApi.getRole(roleName);\n        assertNotNull(roleEntry);\n        assertEquals(createRoleResponse.getId(), roleEntry.getId());\n        assertEquals(\"getProcessQueueAllOrgs\", roleEntry.getPermissions().iterator().next());\n\n        // --- list\n\n        List<RoleEntry> roleEntryList = rolesApi.listRoles();\n        assertNotNull(roleEntryList);\n\n        GenericOperationResult r = rolesApi.deleteRole(roleName);\n        assertEquals(GenericOperationResult.ResultEnum.DELETED, r.getResult());\n    }\n\n    @Test\n    public void testChangeOrganization() throws Exception {\n\n        String orgName = \"Default\";\n        String secondOrgName = \"SecondOrg_\" + randomString();\n\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n\n        UUID defaultOrgId = organizationsApi.getOrg(orgName).getId();\n\n        // create a second organization\n        CreateOrganizationResponse createOrgResponse = organizationsApi.createOrUpdateOrg(new OrganizationEntry()\n                .name(secondOrgName)\n                .visibility(OrganizationEntry.VisibilityEnum.PUBLIC));\n        assertTrue(createOrgResponse.getOk());\n        assertNotNull(createOrgResponse.getId());\n\n        // -- test change org for project\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        String projectName = \"project_\" + randomString();\n\n        // create a project in Default org\n        ProjectOperationResponse createResp = projectsApi.createOrUpdateProject(orgName, new ProjectEntry().name(projectName));\n        assertTrue(createResp.getOk());\n        assertNotNull(createResp.getId());\n\n        // -- move project to second organization by org Name\n        ProjectOperationResponse moveResponse = projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .orgName(secondOrgName));\n        assertTrue(moveResponse.getOk());\n        assertEquals(\"UPDATED\", moveResponse.getResult().getValue());\n        assertNotNull(projectsApi.getProject(secondOrgName, projectName));\n\n        // -- move project back to Default organization by org Id\n        moveResponse = projectsApi.createOrUpdateProject(secondOrgName, new ProjectEntry()\n                .name(projectName)\n                .orgId(defaultOrgId));\n        assertTrue(moveResponse.getOk());\n        assertEquals(\"UPDATED\", moveResponse.getResult().getValue());\n        assertNotNull(projectsApi.getProject(orgName, projectName));\n\n        // -- test change org for secret\n        SecretsApi secretsApi = new SecretsApi(getApiClient());\n        String secretName = \"secret_\" + randomString();\n\n        // create a secret in Default org\n        addPlainSecret(orgName, secretName, false, null, \"hey\".getBytes());\n\n        // -- move secret to second organization by org Name\n        GenericOperationResult moveSecretResponse = secretsApi.updateSecretV1(orgName, secretName,\n                new SecretUpdateRequest().orgName(secondOrgName));\n        assertTrue(moveSecretResponse.getOk());\n        assertEquals(\"UPDATED\", moveSecretResponse.getResult().getValue());\n        assertNotNull(new SecretsV2Api(getApiClient()).getSecret(secondOrgName, secretName));\n\n        // -- move secret back to Default organization by org Id\n        moveSecretResponse = secretsApi.updateSecretV1(secondOrgName, secretName, new SecretUpdateRequest().orgId(defaultOrgId));\n        assertTrue(moveSecretResponse.getOk());\n        assertEquals(\"UPDATED\", moveSecretResponse.getResult().getValue());\n        assertNotNull(new SecretsV2Api(getApiClient()).getSecret(orgName, secretName));\n    }\n\n    private static OrganizationEntry findOrganization(List<OrganizationEntry> l, String name) {\n        return l.stream().filter(e -> name.equals(e.getName())).findAny().get();\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/CryptoIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.collect.ImmutableMap;\nimport com.walmartlabs.concord.client2.*;\nimport org.intellij.lang.annotations.Language;\nimport org.junit.jupiter.api.Test;\n\nimport javax.xml.bind.DatatypeConverter;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Pattern;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class CryptoIT extends AbstractServerIT {\n\n    @Test\n    public void testPlain() throws Exception {\n        String orgName = \"Default\";\n\n        // ---\n\n        String secretName = \"secret@\" + randomString();\n        String secretValue = \"value@\" + randomString();\n        String storePassword = \"store1A@\" + randomString();\n\n        addPlainSecret(orgName, secretName, false, storePassword, secretValue.getBytes());\n\n        // ---\n\n        test(\"cryptoPlain\", secretName, storePassword, \".*value=\" + secretValue + \".*\");\n    }\n\n    @Test\n    public void testUsernamePassword() throws Exception {\n        String orgName = \"Default\";\n\n        // ---\n\n        String secretName = \"secret@\" + randomString();\n        String secretUsername = \"username@\" + randomString();\n        String secretPassword = \"password@\" + randomString();\n        String storePassword = \"store1A@\" + randomString();\n\n        addUsernamePassword(orgName, secretName, false, storePassword, secretUsername, secretPassword);\n\n        // ---\n\n        test(\"cryptoPwd\", secretName, storePassword, \".*\" + secretUsername + \" \" + secretPassword + \".*\");\n    }\n\n    @Test\n    public void testExportAsFile() throws Exception {\n        String orgName = \"Default\";\n\n        // ---\n\n        String secretName = \"secret@\" + randomString();\n        String secretValue = \"value@\" + randomString();\n        String storePassword = \"store1A@\" + randomString();\n\n        addPlainSecret(orgName, secretName, false, storePassword, secretValue.getBytes());\n\n        // ---\n\n        test(\"cryptoFile\", secretName, storePassword, \".*We got \" + secretValue + \".*\");\n    }\n\n    @Test\n    public void testExportAsFileWithOrg() throws Exception {\n        String orgName = \"org@\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        String secretName = \"secret@\" + randomString();\n        String secretValue = \"value@\" + randomString();\n        String storePassword = \"store1A@\" + randomString();\n\n        addPlainSecret(orgName, secretName, false, storePassword, secretValue.getBytes());\n\n        // ---\n\n        byte[] payload = archive(CryptoIT.class.getResource(\"cryptoFileWithOrg\").toURI());\n\n        StartProcessResponse spr = start(ImmutableMap.of(\n                \"archive\", payload,\n                \"arguments.secretName\", secretName,\n                \"arguments.pwd\", storePassword,\n                \"arguments.secretOrgName\", orgName));\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*We got \" + secretValue + \".*\", ab);\n    }\n\n    @Test\n    public void testWithoutPassword() throws Exception {\n        String orgName = \"org@\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        String secretName = \"secret@\" + randomString();\n        String secretValue = \"value@\" + randomString();\n\n        addPlainSecret(orgName, secretName, false, null, secretValue.getBytes());\n\n        // ---\n\n        byte[] payload = archive(CryptoIT.class.getResource(\"cryptoWithoutPassword\").toURI());\n\n        StartProcessResponse spr = start(ImmutableMap.of(\n                \"archive\", payload,\n                \"arguments.secretName\", secretName,\n                \"arguments.secretOrgName\", orgName));\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*We got \" + secretValue + \".*\", ab);\n    }\n\n    @Test\n    public void testEncryptString() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n\n        String value = \"value_\" + randomString();\n\n        EncryptValueResponse evr = projectsApi.encrypt(orgName, projectName, value);\n        assertTrue(evr.getOk());\n\n        // ---\n\n        byte[] payload = archive(CryptoIT.class.getResource(\"encryptString\").toURI());\n\n        StartProcessResponse spr = start(ImmutableMap.of(\n                \"org\", orgName,\n                \"project\", projectName,\n                \"archive\", payload,\n                \"arguments.decryptedValue\", value));\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*We got \" + Pattern.quote(evr.getData()) + \".*\", ab);\n    }\n\n    @Test\n    public void testDecryptString() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n\n        String value = \"value_\" + randomString();\n\n        EncryptValueResponse evr = projectsApi.encrypt(orgName, projectName, value);\n        assertTrue(evr.getOk());\n\n        // ---\n\n        byte[] payload = archive(CryptoIT.class.getResource(\"decryptString\").toURI());\n\n        StartProcessResponse spr = start(ImmutableMap.of(\n                \"org\", orgName,\n                \"project\", projectName,\n                \"archive\", payload,\n                \"arguments.encryptedValue\", evr.getData()));\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*We got \" + value + \".*\", ab);\n    }\n\n    @Test\n    public void testDecryptStringTooBig() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n\n        String value = \"value_\" + randomString();\n\n        EncryptValueResponse evr = projectsApi.encrypt(orgName, projectName, value);\n        assertTrue(evr.getOk());\n\n        // ---\n\n        byte[] payload = archive(CryptoIT.class.getResource(\"decryptStringTooBig\").toURI());\n\n        StartProcessResponse spr = start(ImmutableMap.of(\n                \"org\", orgName,\n                \"project\", projectName,\n                \"archive\", payload));\n\n        // ---\n\n        ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.FAILED);\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLogAtLeast(\".*too big.*\", 1, ab);\n    }\n\n    @Test\n    public void testDecryptInvalidString() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n\n        byte[] payload = archive(CryptoIT.class.getResource(\"decryptString\").toURI());\n\n        StartProcessResponse spr = start(ImmutableMap.of(\n                \"org\", orgName,\n                \"project\", projectName,\n                \"archive\", payload,\n                \"arguments.encryptedValue\", DatatypeConverter.printBase64Binary(new byte[]{0, 1, 2})));\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FAILED, pir.getStatus());\n\n        // ---\n\n        spr = start(ImmutableMap.of(\n                \"org\", orgName,\n                \"project\", projectName,\n                \"archive\", payload,\n                \"arguments.encryptedValue\", \"W+YrVH9Q0YKDZ5j8UytRAQ==\")); // junk\n\n        // ---\n\n        pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FAILED, pir.getStatus(),\n                \"Process logs: \" + new String(getLog(pir.getInstanceId())));\n    }\n\n    @Test\n    public void testProjectSecret() throws Exception {\n        String orgName = \"Default\";\n\n        // ---\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n\n        String secretName = \"secret@\" + randomString();\n        String secretUsername = \"username@\" + randomString();\n        String secretPassword = \"password@\" + randomString();\n        String storePassword = \"store1A@\" + randomString();\n\n        addUsernamePassword(orgName, projectName, secretName, false, storePassword, secretUsername, secretPassword);\n\n        // ---\n\n        String username = \"user_\" + randomString();\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(username)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeysApi = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse cakr = apiKeysApi.createUserApiKey(new CreateApiKeyRequest()\n                .username(username));\n\n        // ---\n\n        setApiKey(cakr.getKey());\n\n        // ---\n\n        test(\"cryptoPwd\", secretName, storePassword, \".*secrets can only be accessed within the project they belong to.*\" + secretName + \".*\");\n    }\n\n    /**\n     * If the organization is not specified the crypto task must pick the current organization.\n     * It works because Concord provides {@code projectInfo} variable that can be used to determine\n     * the current organization.\n     * The test uses a form to verify that \"organization auto select\" works\n     * for new and resumed processes.\n     */\n    @Test\n    public void testCurrentOrg() throws Exception {\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + randomString();\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.OWNERS));\n\n        String secretName = \"secret_\" + randomString();\n\n        String orgSecretValue = \"org value\";\n        addPlainSecret(orgName, secretName, false, null, orgSecretValue.getBytes());\n\n        String defaultSecretValue = \"default value\";\n        addPlainSecret(\"Default\", secretName, false, null, defaultSecretValue.getBytes());\n\n        // ---\n\n        byte[] payload = archive(CryptoIT.class.getResource(\"currentOrgCrypto\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.mySecretName\", secretName);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pe = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n        assertEquals(ProcessEntry.StatusEnum.SUSPENDED, pe.getStatus());\n\n        // ---\n\n        ProcessFormsApi processFormsApi = new ProcessFormsApi(getApiClient());\n        List<FormListEntry> forms = processFormsApi.listProcessForms(pe.getInstanceId());\n        assertEquals(1, forms.size());\n\n        FormListEntry form = forms.get(0);\n        processFormsApi.submitForm(pe.getInstanceId(), form.getName(), Collections.singletonMap(\"x\", \"123\"));\n\n        pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pe.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLog(\".*Before form: \" + orgSecretValue + \".*\", ab);\n        assertLog(\".*After form: \" + orgSecretValue + \".*\", ab);\n    }\n\n    private void test(String project, String secretName, String storePassword, @Language(\"RegExp\") String log) throws Exception {\n        byte[] payload = archive(CryptoIT.class.getResource(project).toURI());\n\n        StartProcessResponse spr = start(ImmutableMap.of(\n                \"archive\", payload,\n                \"arguments.secretName\", secretName,\n                \"arguments.pwd\", storePassword));\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLogAtLeast(log, 1, ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/DefaultProcessVariablesIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class DefaultProcessVariablesIT extends AbstractServerIT {\n\n    private static final String POLICY_NAME = \"default-vars-from-test\";\n\n    @BeforeEach\n    public void precondition() throws Exception {\n        Map<String, Object> defVars = new HashMap<>();\n        defVars.put(\"var1\", \"value1\");\n        defVars.put(\"var2\", \"value2\");\n\n        PolicyApi policyApi = new PolicyApi(getApiClient());\n        PolicyEntry policy = new PolicyEntry();\n        policy.setName(POLICY_NAME);\n        policy.setRules(Collections.singletonMap(\"defaultProcessCfg\", Collections.singletonMap(\"defaultTaskVariables\",\n                Collections.singletonMap(\"testDefaultVars\", defVars))));\n\n        policyApi.createOrUpdatePolicy(policy);\n        PolicyLinkEntry link = new PolicyLinkEntry();\n        policyApi.linkPolicy(POLICY_NAME, link);\n        policyApi.refreshPolicy();\n    }\n\n    @AfterEach\n    public void cleanup() {\n        PolicyApi policyApi = new PolicyApi(getApiClient());\n        try {\n            policyApi.deletePolicy(POLICY_NAME);\n        } catch (Exception e) {\n            // ignore\n        }\n    }\n\n    @Test\n    public void testDefaultVarsAccess() throws Exception {\n        // prepare the payload\n\n        byte[] payload = archive(DefaultProcessVariablesIT.class.getResource(\"defaultVars\").toURI());\n\n        // start the process\n\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // wait for completion\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // get the name of the agent's log file\n\n        assertNotNull(pir.getLogFileName());\n\n        // check the logs\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Default vars: value1.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/DependenciesIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport com.walmartlabs.concord.common.ZipUtils;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.ByteArrayOutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Collections;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class DependenciesIT extends AbstractServerIT {\n\n    @Test\n    public void testUploadAndRun() throws Exception {\n        String dep = \"file:///\" + ITConstants.DEPENDENCIES_DIR + \"/example.jar\";\n\n        String request = \"{ \\\"entryPoint\\\": \\\"main\\\", \\\"dependencies\\\": [ \\\"\" + dep + \"\\\" ] }\";\n        Path tmpDir = createTempDir();\n        Path requestFile = tmpDir.resolve(Constants.Files.CONFIGURATION_FILE_NAME);\n        Files.write(requestFile, Collections.singletonList(request));\n\n        Path src = Paths.get(DependenciesIT.class.getResource(\"deps\").toURI());\n        ByteArrayOutputStream baos = new ByteArrayOutputStream();\n        try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(baos)) {\n            ZipUtils.zip(zip, src);\n            ZipUtils.zip(zip, tmpDir);\n        }\n\n        byte[] payload = baos.toByteArray();\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        ProcessEntry psr = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, psr.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(psr.getInstanceId());\n        assertLog(\".*Hello!.*\", ab);\n    }\n\n    @Test\n    public void testMaven() throws Exception {\n        byte[] payload = archive(DependenciesIT.class.getResource(\"mvnDeps\").toURI());\n\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Hello, Concord.*\", ab);\n    }\n\n    @Test\n    public void testExtraDependencies() throws Exception {\n        byte[] payload = archive(DependenciesIT.class.getResource(\"extraDeps\").toURI());\n\n        // do the first run with the default profile\n\n        StartProcessResponse spr = start(payload);\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.15.0.*\", ab);\n\n        // then add \"foo\" profile\n\n        spr = start(Map.of(\"archive\", payload, \"activeProfiles\", \"foo\"));\n        pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        ab = getLog(pir.getInstanceId());\n        assertLog(\".*mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.16.0.*\", ab);\n        assertNoLog(\".*confluence-task.*\", ab);\n\n        // then both \"foo\" and \"bar\" profiles\n\n        spr = start(Map.of(\"archive\", payload, \"activeProfiles\", \"foo,bar\"));\n        pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        ab = getLog(pir.getInstanceId());\n        assertLog(\".*mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.16.0.*\", ab);\n        assertLog(\".*mvn://com.walmartlabs.concord.plugins:confluence-task:2.5.0.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/DependencyManagerIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.github.tomakehurst.wiremock.junit5.WireMockExtension;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.github.tomakehurst.wiremock.client.WireMock.*;\nimport static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForStatus;\n\n@Disabled(\"needs a fix for new wiremock version\")\npublic class DependencyManagerIT extends AbstractServerIT {\n\n    @RegisterExtension\n    static WireMockExtension rule = WireMockExtension.newInstance()\n            .options(wireMockConfig()\n                    .dynamicPort()\n                    .globalTemplating(true)\n                    .extensions(new HttpTaskIT.RequestHeaders()))\n            .build();\n\n    @BeforeAll\n    public static void setUp() {\n        rule.stubFor(get(urlEqualTo(\"/item.txt\"))\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withHeader(\"Content-Type\", \"application/octet-stream\")\n                        .withBody(\"Hello!\".getBytes()))\n        );\n    }\n\n    @AfterAll\n    public static void tearDown() {\n        rule.shutdownServer();\n    }\n\n    @Test\n    public void test() throws Exception {\n        byte[] payload = archive(DependencyManagerIT.class.getResource(\"dependencyManager\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n\n        Map<String, Object> cfg = new HashMap<>();\n        cfg.put(\"dependencies\", new String[]{\"mvn://com.walmartlabs.concord.it.tasks:dependency-manager-test:\" + ITConstants.PROJECT_VERSION});\n        String url = \"http://\" + env(\"IT_DOCKER_HOST_ADDR\", \"localhost\") + \":\" + rule.getPort() + \"/item.txt\";\n        cfg.put(\"arguments\", Collections.singletonMap(\"url\", url));\n\n        input.put(\"request\", cfg);\n\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*downloading.*\", ab);\n        assertLog(\".*using a cached copy.*\", ab);\n        assertLog(\".*Got: Hello!.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/DispatcherIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessApi;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.ProcessV2Api;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class DispatcherIT extends AbstractServerIT {\n\n    /**\n     * Tests the behaviour of the process queue dispatcher when one of\n     * the required agent types is not available.\n     */\n    @Test\n    public void testUnknownFlavor() throws Exception {\n        byte[] payload = archive(DispatcherIT.class.getResource(\"unknownFlavor\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"requirements.agent.type\", randomString());\n        input.put(\"archive\", payload);\n\n        StartProcessResponse unknownFlavor = start(input);\n\n        // ---\n\n        input.put(\"requirements.agent.type\", \"test\"); // as in it/server/src/test/resources/agent.conf\n\n        StartProcessResponse knownFlavor = start(input);\n\n        ProcessEntry pe = waitForCompletion(getApiClient(), knownFlavor.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pe.getStatus());\n\n        // ---\n\n        ProcessV2Api processApi = new ProcessV2Api(getApiClient());\n        pe = processApi.getProcess(unknownFlavor.getInstanceId(), Collections.emptySet());\n        assertEquals(ProcessEntry.StatusEnum.ENQUEUED, pe.getStatus());\n\n        new ProcessApi(getApiClient()).kill(pe.getInstanceId());\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/DockerAnsibleIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessApi;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;\n\nimport java.io.InputStream;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n@DisabledIfEnvironmentVariable(named = \"SKIP_DOCKER_TESTS\", matches = \"true\", disabledReason = \"Requires dockerd listening on a tcp socket. Not available in a typical CI environment\")\npublic class DockerAnsibleIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        byte[] payload = archive(DockerAnsibleIT.class.getResource(\"dockerAnsible\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.image\", ITConstants.DOCKER_ANSIBLE_IMAGE);\n        StartProcessResponse spr = start(input);\n\n        // --\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertNotNull(pir.getLogFileName());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*\\\"msg\\\": \\\"Hello from Docker!\\\".*\", ab);\n\n        // --\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        try (InputStream is = processApi.downloadAttachment(pir.getInstanceId(), \"ansible_stats.json\")) {\n            assertNotNull(is);\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/DockerIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.condition.DisabledIf;\nimport org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n@DisabledIfEnvironmentVariable(named = \"SKIP_DOCKER_TESTS\", matches = \"true\", disabledReason = \"Requires dockerd listening on a tcp socket. Not available in a typical CI environment\")\npublic class DockerIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        byte[] payload = archive(DockerIT.class.getResource(\"docker\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.image\", ITConstants.DOCKER_ANSIBLE_IMAGE);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertNotNull(pir.getLogFileName());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*DOCKER: Hello, world.*\", ab);\n    }\n\n    @Test\n    public void testOut() throws Exception {\n        byte[] payload = archive(DockerIT.class.getResource(\"dockerOut\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.image\", ITConstants.DOCKER_ANSIBLE_IMAGE);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertNotNull(pir.getLogFileName());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*!! Hello, world !!.*\", ab);\n        assertLog(\".*DOCKER: STDERR STILL WORKS.*\", ab);\n    }\n\n    @Test\n    public void testTaskSyntaxOut() throws Exception {\n        byte[] payload = archive(DockerIT.class.getResource(\"dockerTaskSyntaxOut\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.image\", ITConstants.DOCKER_ANSIBLE_IMAGE);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertNotNull(pir.getLogFileName());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*!! Hello, world.*\", ab);\n        assertLog(\".*DOCKER: STDERR STILL WORKS.*\", ab);\n    }\n\n    @Test\n    public void testNoLogWithStdOut() throws Exception {\n        byte[] payload = archive(DockerIT.class.getResource(\"dockerNoLogWithStdOut\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.image\", ITConstants.DOCKER_ANSIBLE_IMAGE);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertNotNull(pir.getLogFileName());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertNoLog(\".*!! Hello, world .*\", ab);\n        assertNoLog(\".*STDERR WORKS !!.*\", ab);\n    }\n\n    @Test\n    public void testLogWithoutStdOut() throws Exception {\n        byte[] payload = archive(DockerIT.class.getResource(\"dockerLogWithoutStdOut\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.image\", ITConstants.DOCKER_ANSIBLE_IMAGE);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertNotNull(pir.getLogFileName());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*DOCKER: Hello, world.*\", ab);\n        assertLog(\".*DOCKER: STDERR WORKS.*\", ab);\n    }\n\n    @Test\n    public void testLogWithStdErr() throws Exception {\n        byte[] payload = archive(DockerIT.class.getResource(\"dockerLogWithStdErr\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.image\", ITConstants.DOCKER_ANSIBLE_IMAGE);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertNotNull(pir.getLogFileName());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*STDERR: STDERR WORKS.*\", ab);\n    }\n\n    @Test\n    public void testPullRetry() throws Exception {\n        byte[] payload = archive(DockerIT.class.getResource(\"dockerPullRetry\").toURI());\n\n        StartProcessResponse spr = start(payload);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLogAtLeast(\".*Error pulling the image.*\", 2, ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/DynamicFormIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class DynamicFormIT extends AbstractServerIT {\n\n    @Test\n    public void testWithGroovy() throws Exception {\n        String firstName = \"firstName_\" + randomString();\n        String lastName = \"lastName_\" + randomString();\n\n        // ---\n\n        byte[] payload = archive(DynamicFormIT.class.getResource(\"dynamicFormWithGroovy\").toURI());\n\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // ---\n\n        ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n\n        // ---\n\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n\n        List<FormListEntry> forms = formsApi.listProcessForms(spr.getInstanceId());\n        assertEquals(1, forms.size());\n\n        // ---\n\n        FormListEntry f0 = forms.get(0);\n        assertFalse(f0.getCustom());\n\n        String formName = f0.getName();\n\n        Map<String, Object> data = new HashMap<>();\n        data.put(\"firstName\", firstName);\n        data.put(\"lastName\", lastName);\n        FormSubmitResponse fsr = formsApi.submitForm(pir.getInstanceId(), formName, data);\n        assertTrue(fsr.getOk());\n\n        // ---\n\n        pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // --\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*firstName.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/DynamicTaskIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessApi;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.junit.jupiter.api.Test;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class DynamicTaskIT extends AbstractServerIT {\n\n    @Test\n    public void testDynamicTask() throws Exception {\n        // prepare the payload\n\n        byte[] payload = archive(DynamicTaskIT.class.getResource(\"dynamicTask\").toURI());\n\n        // start the process\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Hey, world.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/EntityOwnerPolicyIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.junit.jupiter.api.Assertions.fail;\n\npublic class EntityOwnerPolicyIT extends AbstractServerIT {\n\n    private final String userOwner = \"ownerUser_\" + randomString();\n    private final String policyName = \"policy_\" + randomString();\n\n    @BeforeEach\n    public void init() throws Exception {\n\n        // --- policy\n        Map<String, Object> ownerConditions = new HashMap<>();\n        ownerConditions.put(\"username\", userOwner);\n        ownerConditions.put(\"userType\", \"LOCAL\");\n        Map<String, Object> deny = new HashMap<>();\n        deny.put(\"deny\", Collections.singletonList(createRule(\"create\", \"org\",\n                Collections.singletonMap(\"owner\", ownerConditions))));\n        Map<String, Object> rules = new HashMap<>();\n        rules.put(\"entity\", deny);\n        createPolicy(null, null, rules);\n    }\n\n    @AfterEach\n    public void cleanup() throws Exception {\n        PolicyApi policyApi = new PolicyApi(getApiClient());\n        policyApi.deletePolicy(policyName);\n    }\n\n    @Test\n    public void testOrgCreation() throws Exception {\n        // --- user\n        UsersApi usersApi = new UsersApi(getApiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userOwner)\n                .email(\"owner@mail.com\")\n                .displayName(\"Test Owner\")\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse cakr = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userOwner));\n        assertTrue(cakr.getOk());\n\n        usersApi.updateUserRoles(userOwner, new UpdateUserRolesRequest()\n                .roles(Collections.singleton(\"concordAdmin\")));\n\n        // ---\n\n        setApiKey(cakr.getKey());\n\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        EntityOwner owner = new EntityOwner();\n        owner.setUsername(userOwner);\n\n        try {\n            orgApi.createOrUpdateOrg(new OrganizationEntry().owner(owner).name(orgName));\n            fail(\"exception expected\");\n        } catch (ApiException e) {\n            assertTrue(e.getResponseBody().contains(\"Action forbidden: test-rule\"));\n        }\n    }\n\n    private Map<String, Object> createRule(String action, String entity, Map<String, Object> conditions) {\n        Map<String, Object> result = new HashMap<>();\n        result.put(\"msg\", \"test-rule\");\n        result.put(\"action\", action);\n        result.put(\"entity\", entity);\n        result.put(\"conditions\", conditions);\n        return result;\n    }\n\n    private String createPolicy(String orgName, String projectName, Map<String, Object> rules) throws ApiException {\n        PolicyApi policyApi = new PolicyApi(getApiClient());\n        policyApi.createOrUpdatePolicy(new PolicyEntry()\n                .name(policyName)\n                .rules(rules));\n\n        policyApi.linkPolicy(policyName, new PolicyLinkEntry()\n                .orgName(orgName)\n                .projectName(projectName));\n\n        return policyName;\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/EscapeGitCommitMessageIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.it.common.GitUtils;\nimport com.walmartlabs.concord.it.common.MockGitSshServer;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static java.util.Collections.singletonMap;\n\npublic class EscapeGitCommitMessageIT extends AbstractServerIT {\n\n    private MockGitSshServer gitServer;\n    private int gitPort;\n\n    @BeforeEach\n    public void setUp() throws Exception {\n        Path data = Paths.get(EscapeGitCommitMessageIT.class.getResource(\"escapeCommitMessage\").toURI());\n        Path repo = GitUtils.createBareRepository(data, \"oops ${booom}\");\n\n        gitServer = new MockGitSshServer(0, repo);\n        gitServer.start();\n\n        gitPort = gitServer.getPort();\n    }\n\n    @AfterEach\n    public void tearDown() throws Exception {\n        gitServer.stop();\n    }\n\n    @Test\n    public void test() throws Exception {\n        String orgName = \"Default\";\n\n        // ---\n\n        String projectName = \"project@\" + randomString();\n        String repoSecretName = \"repoSecret@\" + randomString();\n        String repoName = \"repo@\" + randomString();\n        String repoUrl = String.format(ITConstants.GIT_SERVER_URL_PATTERN, gitPort);\n\n        // ---\n\n        SecretOperationResponse response = generateKeyPair(orgName, repoSecretName, false, null);\n\n        // ---\n\n        RepositoryEntry repo = new RepositoryEntry()\n                .name(repoName)\n                .url(repoUrl)\n                .branch(\"master\")\n                .secretId(response.getId());\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .repositories(singletonMap(repoName, repo)));\n\n        // ---\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"repo\", repoName);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry psr = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(psr.getInstanceId());\n        assertLog(\".*Hello, Vasia.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ExclusiveProcessIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.client2.ProcessEntry.StatusEnum;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForStatus;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class ExclusiveProcessIT extends AbstractServerIT {\n\n    @Test\n    public void testExclusiveCancelOld() throws Exception {\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + randomString();\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        byte[] payload = archive(ExclusiveProcessIT.class.getResource(\"exclusiveCancelOld\").toURI());\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"archive\", payload);\n        input.put(\"arguments.time\", \"60000\");\n\n        StartProcessResponse spr1 = start(input);\n        waitForStatus(getApiClient(), spr1.getInstanceId(), StatusEnum.RUNNING);\n\n        input.put(\"arguments.time\", \"1\");\n        StartProcessResponse spr2 = start(input);\n\n        ProcessEntry p1 = waitForStatus(getApiClient(), spr1.getInstanceId(), StatusEnum.CANCELLED);\n        ProcessEntry p2 = waitForStatus(getApiClient(), spr2.getInstanceId(), StatusEnum.FINISHED);\n\n        System.out.println(\"p1: createdAt: \" + p1.getCreatedAt() + \", status: \" + p1.getStatus());\n        System.out.println(\"p2: createdAt: \" + p2.getCreatedAt() + \", status: \" + p2.getStatus());\n        if (p1.getStatus() != StatusEnum.CANCELLED) {\n            List<ProcessStatusHistoryEntry> p1History = processApi.getStatusHistory(p1.getInstanceId());\n            List<ProcessStatusHistoryEntry> p2History = processApi.getStatusHistory(p2.getInstanceId());\n\n            System.out.println(\"p1 history: \" + p1History);\n            System.out.println(\"p2 history: \" + p2History);\n            System.out.println(\"p1 log:\" + new String(getLog(p1.getInstanceId())));\n            System.out.println(\"p2 log:\" + new String(getLog(p2.getInstanceId())));\n        }\n\n        assertTrue(p1.getCreatedAt().isEqual(p2.getCreatedAt()) || p1.getCreatedAt().isBefore(p2.getCreatedAt()));\n        assertEquals(StatusEnum.CANCELLED, p1.getStatus());\n        assertEquals(StatusEnum.FINISHED, p2.getStatus());\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ExpressionResolveOrderIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.junit.jupiter.api.Test;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class ExpressionResolveOrderIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        // prepare the payload\n\n        byte[] payload = archive(ExpressionResolveOrderIT.class.getResource(\"resolveOrder\").toURI());\n\n        // start the process\n\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // wait for completion\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // get the name of the agent's log file\n\n        assertNotNull(pir.getLogFileName());\n\n        // check the logs\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*sleep time: 1 hour.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ExternalImportsIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.client2.ProcessListFilter;\nimport com.walmartlabs.concord.common.PathUtils;\nimport org.eclipse.jgit.api.Git;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.*;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ExternalImportsIT extends AbstractServerIT {\n\n    @Test\n    public void testExternalImportWithForm() throws Exception {\n        String repoUrl = initRepo(\"externalImportWithForm\");\n\n        // prepare the payload\n        Path payloadDir = createPayload(\"externalImportMain\", repoUrl);\n        byte[] payload = archive(payloadDir.toUri());\n\n        // start the process\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // wait for suspend\n\n        ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n        List<FormListEntry> forms = formsApi.listProcessForms(pir.getInstanceId());\n        assertEquals(1, forms.size());\n\n        formsApi.submitForm(pir.getInstanceId(), forms.get(0).getName(), Collections.singletonMap(\"name\", \"boo\"));\n\n        // wait process finished\n        pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // get the name of the agent's log file\n\n        assertNotNull(pir.getLogFileName());\n\n        // check the logs\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Hello, Concord!.*\", ab);\n        assertLog(\".*Hello from Template, Concord!.*\", ab);\n        assertLog(\".*Template form submitted: boo.*\", ab);\n    }\n\n    @Test\n    public void testExternalImportWithDefaults() throws Exception {\n        String repoUrl = initRepo(\"externalImport\");\n\n        // prepare the payload\n        Path payloadDir = createPayload(\"externalImportMain\", repoUrl);\n        byte[] payload = archive(payloadDir.toUri());\n\n        // start the process\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // wait for completion\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // get the name of the agent's log file\n\n        assertNotNull(pir.getLogFileName());\n\n        // check the logs\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Hello, Concord!.*\", ab);\n        assertLog(\".*Hello from Template, Concord!.*\", ab);\n    }\n\n    @Test\n    public void testExternalImportWithPath() throws Exception {\n        String repoUrl = initRepo(\"externalImportWithDir\");\n\n        // prepare the payload\n        Path payloadDir = createPayload(\"externalImportMainWithPath\", repoUrl);\n        byte[] payload = archive(payloadDir.toUri());\n\n        // start the process\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // wait for completion\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // get the name of the agent's log file\n\n        assertNotNull(pir.getLogFileName());\n\n        // check the logs\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Hello, Concord!.*\", ab);\n        assertLog(\".*Hello from Template DIR, Concord!.*\", ab);\n    }\n\n    @Test\n    public void testExternalImportWithConfigurationInImport() throws Exception {\n        String repoUrl = initRepo(\"externalImportWithConfiguration\");\n\n        // prepare the payload\n        Path payloadDir = createPayload(\"externalImportMain\", repoUrl);\n        byte[] payload = archive(payloadDir.toUri());\n\n        // start the process\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // wait for completion\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // get the name of the agent's log file\n\n        assertNotNull(pir.getLogFileName());\n\n        // check the logs\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Hello, Concord!.*\", ab);\n        assertLog(\".*Hello from Template, Concord!.*\", ab);\n    }\n\n    // payload with concord/concord.yml and import with concord/concord.yml,\n    // concord.yml from import will use.\n    @Test\n    public void testExternalImportWithConcordDirReplace() throws Exception {\n        String repoUrl = initRepo(\"externalImport\");\n\n        // prepare the payload\n        Path payloadDir = createPayload(\"externalImportMainWithFlow\", repoUrl);\n        byte[] payload = archive(payloadDir.toUri());\n\n        // start the process\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // wait for completion\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // get the name of the agent's log file\n\n        assertNotNull(pir.getLogFileName());\n\n        // check the logs\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Hello from Template, Concord!.*\", ab);\n    }\n\n    @Test\n    public void testExternalImportWithOnFailure() throws Exception {\n        String repoUrl = initRepo(\"externalImportFailHandler\");\n\n        // prepare the payload\n        Path payloadDir = createPayload(\"externalImportMainFailed\", repoUrl);\n        byte[] payload = archive(payloadDir.toUri());\n\n        // start the process\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // wait for completion\n\n        ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.FAILED);\n\n        ProcessEntry child = waitForChild(processApi, spr.getInstanceId(), ProcessEntry.KindEnum.FAILURE_HANDLER, ProcessEntry.StatusEnum.FINISHED);\n\n        // check the logs\n\n        byte[] ab = getLog(child.getInstanceId());\n\n        assertLog(\".*oh, handled.*\", ab);\n    }\n\n    @Test\n    public void testExternalImportWithForks() throws Exception {\n        String repoUrl = initRepo(\"externalImportWithForks\");\n\n        // prepare the payload\n        Path payloadDir = createPayload(\"externalImportMainWithForks\", repoUrl);\n        byte[] payload = archive(payloadDir.toUri());\n\n        // start the process\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // wait for completion\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        ProcessEntry child = waitForChild(processApi, pir.getInstanceId(), ProcessEntry.KindEnum.DEFAULT, ProcessEntry.StatusEnum.FINISHED);\n\n        // check the logs\n\n        byte[] ab = getLog(pir.getInstanceId());\n        byte[] cd = getLog(child.getInstanceId());\n\n        assertLog(\".*Hello, Concord!.*\", ab);\n        assertLog(\".*Hello from Concord, imports!.*\", cd);\n    }\n\n    @Test\n    public void testExternalImportValidation() throws Exception {\n        String importRepoUrl = initRepo(\"externalImport\");\n\n        String userRepoUrl = initRepo(\"externalImportTriggerReference\");\n        replace(Paths.get(userRepoUrl, \"concord.yml\"), \"{{gitUrl}}\", importRepoUrl);\n        commit(Paths.get(userRepoUrl).toFile());\n\n        // ---\n\n        assertExternalImportValidation(userRepoUrl);\n    }\n\n    @Test\n    public void testExternalImportWithSymlink() throws Exception {\n        // Init repo and add symlink\n        String importRepoUrl = initRepo(\"externalImportSymlink\", importRepo -> {\n            try {\n                Path target = importRepo.resolve(\"concord.txt\");\n                Path linkDir = Files.createDirectory(importRepo.resolve(\"link_dir\"));\n                Path link = linkDir.resolve(\"concord.yml\");\n                Files.createSymbolicLink(link, linkDir.relativize(target));\n            } catch (IOException e) {\n                fail(\"Error while creating symlink\");\n            }\n        });\n\n        String userRepoUrl = initRepo(\"externalImportTriggerReference\");\n        replace(Paths.get(userRepoUrl, \"concord.yml\"), \"{{gitUrl}}\", importRepoUrl);\n        commit(Paths.get(userRepoUrl).toFile());\n\n        // ---\n\n        assertExternalImportValidation(userRepoUrl);\n    }\n\n    private void assertExternalImportValidation(String userRepoUrl) throws Exception {\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"prj_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .url(userRepoUrl)\n                        .branch(\"master\"))));\n\n        RepositoriesApi repositoriesApi = new RepositoriesApi(getApiClient());\n        GenericOperationResult refreshResp = repositoriesApi.refreshRepository(orgName, projectName, repoName, true);\n        assertTrue(refreshResp.getOk());\n        RepositoryValidationResponse resp = repositoriesApi.validateRepository(orgName, projectName, repoName);\n        assertTrue(resp.getOk());\n    }\n\n    @Test\n    public void testExternalImportWithExcludeFullDir() throws Exception {\n        String repoUrl = initRepo(\"externalImportWithDir\");\n\n        // prepare the payload\n        Path payloadDir = createPayload(\"externalImportMainWithExclude\", repoUrl, \"dir\");\n        byte[] payload = archive(payloadDir.toUri());\n\n        // start the process\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // wait for completion\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // get the name of the agent's log file\n\n        assertNotNull(pir.getLogFileName());\n\n        // check the logs\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Hello, Concord!.*\", ab);\n        assertLog(\".*Hello from Template, Concord!.*\", ab);\n    }\n\n    @Test\n    public void testExternalImportWithExcludeFileFromDir() throws Exception {\n        String repoUrl = initRepo(\"externalImportWithDir\");\n\n        // prepare the payload\n        Path payloadDir = createPayload(\"externalImportMainWithExclude\", repoUrl, \"dir/concord.yml\");\n        byte[] payload = archive(payloadDir.toUri());\n\n        // start the process\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // wait for completion\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // get the name of the agent's log file\n\n        assertNotNull(pir.getLogFileName());\n\n        // check the logs\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Hello, Concord!.*\", ab);\n        assertLog(\".*Hello from Template, Concord!.*\", ab);\n    }\n\n    @Test\n    public void testExternalImportWithExcludeFile() throws Exception {\n        String repoUrl = initRepo(\"externalImportWithDir\");\n\n        // prepare the payload\n        Path payloadDir = createPayload(\"externalImportMainWithExclude\", repoUrl, \"concord.yml\");\n        byte[] payload = archive(payloadDir.toUri());\n\n        // start the process\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // wait for completion\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // get the name of the agent's log file\n\n        assertNotNull(pir.getLogFileName());\n\n        // check the logs\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Hello, Concord!.*\", ab);\n        assertLog(\".*Hello from Template DIR, Concord!.*\", ab);\n    }\n\n    @Test\n    public void testImportWithTriggers() throws Exception {\n        String importRepoUrl = initRepo(\"testTrigger\");\n        String clientRepoUrl = initRepo(\"importATrigger\", \"concord.yml\", \"{{gitUrl}}\", importRepoUrl);\n\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .url(clientRepoUrl)\n                        .branch(\"master\"))));\n\n        // ---\n\n        TriggersApi triggersApi = new TriggersApi(getApiClient());\n        while (true) {\n            List<TriggerEntry> triggers = triggersApi.listTriggers(orgName, projectName, repoName);\n            if (triggers != null && triggers.size() == 1 && triggers.get(0).getEventSource().equals(\"test\")) {\n                break;\n            }\n\n            Thread.sleep(1000);\n        }\n\n        // ---\n\n        ExternalEventsApi externalEventsApi = new ExternalEventsApi(getApiClient());\n        externalEventsApi.externalEvent(\"test\", Collections.emptyMap());\n\n        // ---\n\n        ProcessEntry pe;\n\n        ProcessV2Api processApi = new ProcessV2Api(getApiClient());\n        ProcessListFilter filter = ProcessListFilter.builder()\n                .orgName(orgName)\n                .projectName(projectName)\n                .build();\n\n        while (true) {\n            List<ProcessEntry> l = processApi.listProcesses(filter);\n\n            Optional<ProcessEntry> o = l.stream().filter(e -> e.getTriggeredBy().getTrigger().getEventSource().equals(\"test\")).findFirst();\n            if (o.isPresent()) {\n                pe = o.get();\n                break;\n            }\n\n            Thread.sleep(1000);\n        }\n\n        // ---\n\n        waitForCompletion(getApiClient(), pe.getInstanceId());\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLog(\".*Hello, Concord.*\", ab);\n    }\n\n    @Test\n    public void testDependencyMerging() throws Exception {\n        String repoUrl = initRepo(\"externalImportWithDeps\");\n\n        Path payloadDir = createPayload(\"externalImportMainWithDeps\", repoUrl, \"concord.yml\");\n        byte[] payload = archive(payloadDir.toUri());\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Hello from Groovy!.*\", ab);\n        assertLog(\".*Hello from Python!.*\", ab);\n    }\n\n    @Test\n    public void testExternalImportState() throws Exception {\n        String repoUrl = initRepo(\"externalImportWithDir\");\n\n        // prepare the payload\n        Path payloadDir = createPayload(\"externalImportMainStateTest\", repoUrl);\n        byte[] payload = archive(payloadDir.toUri());\n\n        // start the process\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n        waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.ENQUEUED);\n\n        try {\n            processApi.downloadStateFile(spr.getInstanceId(), \"import_data/concord.yml\");\n            fail(\"exception expected\");\n        } catch (ApiException e) {\n            assertEquals(404, e.getCode());\n        }\n\n        processApi.kill(spr.getInstanceId());\n    }\n\n    @Test\n    public void testGitImportWithCommitAsVersion() throws Exception {\n        String repoUrl = initRepo(\"externalImport\");\n        Git repo = Git.open(new File(repoUrl));\n        String commitId = repo.log().setMaxCount(1).call().iterator().next().name();\n        // add new commitId:\n        Files.write(Paths.get(repoUrl).resolve(\"concord.yml\"), \"trash\".getBytes());\n        repo.add().addFilepattern(\".\").call();\n        repo.commit().setMessage(\"up\").call();\n\n        // prepare the payload\n        Map<String, String> replacements = new HashMap<>();\n        replacements.put(\"{{gitUrl}}\", repoUrl);\n        replacements.put(\"{{version}}\", commitId);\n        Path payloadDir = createPayload(\"externalImportMainWithVersion\", replacements);\n        byte[] payload = archive(payloadDir.toUri());\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Hello from Template, Concord!.*\", ab);\n    }\n\n    private static String initRepo(String resourceName) throws Exception {\n        return initRepo(resourceName, null, null, null);\n    }\n\n    private static String initRepo(String resourceName, Consumer<Path> fileAdder) throws Exception {\n        return initRepo(resourceName, null, null, null, fileAdder);\n    }\n\n    private static String initRepo(String resourceName, String path, String find, String replace) throws Exception {\n        return initRepo(resourceName, path, find, replace, p -> {});\n    }\n\n    private static String initRepo(String resourceName, String path, String find, String replace, Consumer<Path> fileAdder) throws Exception {\n\n        Path tmpDir = createTempDir();\n\n        File src = new File(ExternalImportsIT.class.getResource(resourceName).toURI());\n        PathUtils.copy(src.toPath(), tmpDir);\n\n        // add more files\n        fileAdder.accept(tmpDir);\n\n        if (path != null) {\n            Path p = tmpDir.resolve(path);\n            String s = new String(Files.readAllBytes(p));\n            Files.write(p, s.replace(find, replace).getBytes());\n        }\n\n        try (Git repo = Git.init().setInitialBranch(\"master\").setDirectory(tmpDir.toFile()).call()) {\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"import\").call();\n            repo.branchCreate().setName(\"main\").call();\n            return tmpDir.toAbsolutePath().toString();\n        }\n    }\n\n    private static Path createPayload(String resourceName, String repoUrl) throws Exception {\n        return createPayload(resourceName, Collections.singletonMap(\"{{gitUrl}}\", repoUrl));\n    }\n\n    private static Path createPayload(String resourceName, String repoUrl, String exclude) throws Exception {\n        Map<String, String> replacements = new HashMap<>();\n        replacements.put(\"{{gitUrl}}\", repoUrl);\n        replacements.put(\"{{exclude}}\", exclude);\n        return createPayload(resourceName, replacements);\n    }\n\n    private static Path createPayload(String resourceName, Map<String, String> replacements) throws Exception {\n        Path tmpDir = createTempDir();\n        File src = new File(ExternalImportsIT.class.getResource(resourceName).toURI());\n        PathUtils.copy(src.toPath(), tmpDir);\n        Path concordFile = tmpDir.resolve(\"concord.yml\");\n        replacements.forEach((k, v) -> replace(concordFile, k, v));\n        return tmpDir;\n    }\n\n    private static void replace(Path concord, String what, String newValue) {\n        try {\n            List<String> fileContent = Files.readAllLines(concord, StandardCharsets.UTF_8).stream()\n                    .map(l -> l.replaceAll(Pattern.quote(what), newValue))\n                    .collect(Collectors.toList());\n\n            Files.write(concord, fileContent, StandardCharsets.UTF_8);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static void commit(File dir) throws Exception {\n        Git repo = Git.open(dir);\n        repo.add().addFilepattern(\".\").call();\n        repo.commit().setMessage(\"import\").call();\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/FailureHandlingIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.client2.ProcessEntry.StatusEnum;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class FailureHandlingIT extends AbstractServerIT {\n\n    @Test\n    public void testFailure() throws Exception {\n        ProcessApi processApi = new ProcessApi(getApiClient());\n\n        // prepare the payload\n\n        byte[] payload = archive(ProcessIT.class.getResource(\"failureHandling\").toURI());\n\n        // start the process and wait for it to fail\n\n        StartProcessResponse spr = start(payload);\n\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.SUSPENDED);\n\n        // ---\n\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n\n        List<FormListEntry> forms = formsApi.listProcessForms(spr.getInstanceId());\n        assertEquals(1, forms.size());\n\n        Map<String, Object> data = Collections.singletonMap(\"firstName\", \"first-name\");\n        FormSubmitResponse fsr = formsApi.submitFormAsMultipart(spr.getInstanceId(), forms.get(0).getName(), data);\n        assertTrue(fsr.getOk());\n\n        ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.FAILED);\n\n        // check the logs for the error message\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLogAtLeast(\".*boom!.*\", 1, ab);\n\n        // find the child processes\n\n        ProcessEntry child = waitForChild(processApi, spr.getInstanceId(), ProcessEntry.KindEnum.FAILURE_HANDLER, StatusEnum.FINISHED);\n\n        // check the logs for the successful message\n\n        ab = getLog(child.getInstanceId());\n        assertLog(\".*lastError:.*boom.*\", ab);\n        assertLog(\".*projectInfo: \\\\{.*orgName=Default.*\\\\}.*\", ab);\n        assertLog(\".*processInfo: \\\\{.*sessionKey=.*\\\\}.*\", ab);\n        assertLog(\".*initiator: \\\\{.*username=.*\\\\}.*\", ab);\n    }\n\n    @Test\n    public void testFailureHandlingError() throws Exception {\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        byte[] payload = archive(ProcessIT.class.getResource(\"failureHandlingError\").toURI());\n\n        StartProcessResponse spr = start(payload);\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.FAILED);\n\n        // find the child processes\n\n        waitForChild(processApi, spr.getInstanceId(), ProcessEntry.KindEnum.FAILURE_HANDLER, StatusEnum.FAILED);\n    }\n\n    @Test\n    public void testOnFailureDependencies() throws Exception {\n        String msg = \"msg_\" + randomString();\n        byte[] payload = archive(ProcessIT.class.getResource(\"onFailureDependencies\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.msg\", msg);\n\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        ProcessEntry pe = waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.FAILED);\n\n        // ---\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLogAtLeast(\".*\" + msg + \".*\", 1, ab);\n\n        // ---\n\n        ProcessEntry child = waitForChild(processApi, spr.getInstanceId(), ProcessEntry.KindEnum.FAILURE_HANDLER, StatusEnum.FAILED, StatusEnum.FINISHED);\n        assertEquals(StatusEnum.FINISHED, child.getStatus());\n\n        // check the logs for the successful message\n\n        ab = getLog(child.getInstanceId());\n        assertLog(\".*Hello!\", ab);\n    }\n\n    @Test\n    public void testCancel() throws Exception {\n        byte[] payload = archive(ProcessIT.class.getResource(\"cancelHandling\").toURI());\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.RUNNING);\n\n        // cancel the running process\n\n        processApi.kill(spr.getInstanceId());\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.CANCELLED);\n\n        // find the child processes\n\n        ProcessEntry child = waitForChild(processApi, spr.getInstanceId(), ProcessEntry.KindEnum.CANCEL_HANDLER, StatusEnum.FINISHED);\n\n        // check the logs for the successful message\n\n        byte[] ab = getLog(child.getInstanceId());\n        assertLog(\".*initiator is admin.*\", ab);\n    }\n\n    @Test\n    public void testCancelSuspended() throws Exception {\n        String aValue = \"value_\" + randomString();\n        byte[] payload = archive(ProcessIT.class.getResource(\"cancelSuspendHandling\").toURI());\n\n        // start the process\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.aValue\", aValue);\n        StartProcessResponse spr = start(input);\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.SUSPENDED);\n\n        // check if the form is there\n        ProcessFormsApi processFormsApi = new ProcessFormsApi(getApiClient());\n        List<FormListEntry> forms = processFormsApi.listProcessForms(spr.getInstanceId());\n        assertEquals(1, forms.size());\n\n        // cancel the suspended process\n\n        processApi.kill(spr.getInstanceId());\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.CANCELLED);\n\n        // find the child processes\n\n        ProcessEntry child = waitForChild(processApi, spr.getInstanceId(), ProcessEntry.KindEnum.CANCEL_HANDLER, StatusEnum.FINISHED);\n\n        // check the logs for the successful message\n\n        byte[] ab = getLog(child.getInstanceId());\n        assertLog(\".*\" + aValue + \" still here.*\", ab);\n    }\n\n    @Test\n    public void testCancelSuspendedAfterTwoForms() throws Exception {\n        byte[] payload = archive(ProcessIT.class.getResource(\"cancelSuspendAfterTwoForms\").toURI());\n\n        // start the process\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.SUSPENDED);\n\n        // check if the first form is there\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n        List<FormListEntry> forms = formsApi.listProcessForms(spr.getInstanceId());\n        assertEquals(1, forms.size());\n\n        // submit the first form\n        formsApi.submitForm(spr.getInstanceId(), \"myForm1\", Collections.singletonMap(\"x\", \"123\"));\n\n        // wait for the process to suspend\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.SUSPENDED);\n\n        // check if the second form's ready\n        forms = formsApi.listProcessForms(spr.getInstanceId());\n        assertEquals(1, forms.size());\n\n        // cancel the process\n        processApi.kill(spr.getInstanceId());\n\n        // find the child processes\n        ProcessEntry child = waitForChild(processApi, spr.getInstanceId(), ProcessEntry.KindEnum.CANCEL_HANDLER,\n                StatusEnum.FINISHED, StatusEnum.FAILED);\n        assertEquals(StatusEnum.FINISHED, child.getStatus());\n\n        // check the logs for the successful message\n        byte[] ab = getLog(child.getInstanceId());\n        assertLog(\".*Hello!.*\", ab);\n    }\n\n    @Test\n    public void testOnFailureForForks() throws Exception {\n        byte[] payload = archive(ProcessIT.class.getResource(\"forkOnFailure\").toURI());\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        List<ProcessEntry> children = processApi.listSubprocesses(pe.getInstanceId(), null);\n        assertEquals(2, children.size());\n\n        ProcessEntry childWithOnFailure = children.stream().filter(p -> p.getHandlers() != null && p.getHandlers().contains(\"onFailure\"))\n                .findFirst().orElseThrow(() -> new IllegalStateException(\"Can't find a child with an onFailure handler\"));\n\n        ProcessEntry childWithoutOnFailure = children.stream().filter(p -> p.getHandlers() == null || p.getHandlers().isEmpty())\n                .findFirst().orElseThrow(() -> new IllegalStateException(\"Can't find a child without an onFailure handler\"));\n\n        // ---\n\n        ProcessEntry onFailureProc = waitForChild(processApi, childWithOnFailure.getInstanceId(), ProcessEntry.KindEnum.FAILURE_HANDLER, StatusEnum.FINISHED);\n        byte[] ab = getLog(onFailureProc.getInstanceId());\n        assertLog(\".*Got.*aFork!.*\", ab);\n\n        // ---\n\n        List<ProcessEntry> childWithoutOnFailureChildren = processApi.listSubprocesses(childWithoutOnFailure.getInstanceId(), null);\n        assertEquals(0, childWithoutOnFailureChildren.size());\n    }\n\n    @Test\n    void testNoHandlingForDisabledUser() throws Exception {\n        // create user to be disabled\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n\n        String userAName = \"user_\" + randomString();\n        var user = usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userAName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse apiKeyA = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userAName));\n\n        // switch to temp user's api key\n\n        setApiKey(apiKeyA.getKey());\n\n        // start the process as temp user\n\n        byte[] payload = archive(ProcessIT.class.getResource(\"cancelHandling\").toURI());\n        StartProcessResponse spr = start(payload);\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.RUNNING);\n\n        // back to admin api token, disable the user\n\n        resetApiKey();\n        usersApi.disableUser(user.getId(), true);\n\n        // cancel the running process\n\n        new ProcessApi(getApiClient()).kill(spr.getInstanceId());\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.CANCELLED);\n\n        // expect error starting child process, may take 3+ seconds for the watchdog to do it's work\n\n        waitForLog(spr.getInstanceId(), 15, \".*Error while starting onCancel handler: initiator is disabled.*\");\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/FilePermissionsIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.ZipUtils;\nimport org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.ByteArrayOutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.attribute.PosixFilePermission;\nimport java.util.HashSet;\nimport java.util.Set;\n\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class FilePermissionsIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        Path src = Paths.get(FilePermissionsIT.class.getResource(\"filePerm\").toURI());\n\n        Path tmpDir = createTempDir();\n        PathUtils.copy(src, tmpDir);\n\n        Path testFile = tmpDir.resolve(\"test.sh\");\n        Set<PosixFilePermission> permissions = new HashSet<>(Files.getPosixFilePermissions(testFile));\n        permissions.add(PosixFilePermission.OWNER_EXECUTE);\n        Files.setPosixFilePermissions(testFile, permissions);\n\n        ByteArrayOutputStream payload = new ByteArrayOutputStream();\n        try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(payload)) {\n            ZipUtils.zip(zip, tmpDir);\n        }\n\n        // ---\n\n        StartProcessResponse spr = start(payload.toByteArray());\n        assertNotNull(spr.getInstanceId());\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Hello!.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ForceSuspendIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessApi;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\n\npublic class ForceSuspendIT extends AbstractServerIT {\n\n    @Test\n    public void testTask() throws Exception {\n        String eventName = \"ev_\" + randomString();\n\n        byte[] payload = archive(ForceSuspendIT.class.getResource(\"suspendTask\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.eventName\", eventName);\n\n        Map<String, Object> cfg = new HashMap<>();\n        cfg.put(\"dependencies\", new String[]{\"mvn://com.walmartlabs.concord.it.tasks:suspend-test:\" + ITConstants.PROJECT_VERSION});\n        input.put(\"request\", cfg);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Requesting suspend.*\", ab);\n        assertLog(\".*Whoa!.*\", 0, ab);\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        processApi.resume(pir.getInstanceId(), eventName, null, null);\n        pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Whoa!.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/FormIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.client2.ProcessEntry.StatusEnum;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\n\nimport java.util.*;\nimport java.util.concurrent.TimeUnit;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class FormIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        String firstName = \"john_\" + randomString();\n        String lastName = \"smith_\" + randomString();\n        byte[] payload = archive(FormIT.class.getResource(\"form\").toURI());\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        ProcessEntry pe = waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.SUSPENDED);\n        assertEquals(StatusEnum.SUSPENDED, pe.getStatus());\n\n        // ---\n\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n\n        List<FormListEntry> forms = formsApi.listProcessForms(spr.getInstanceId());\n        assertEquals(1, forms.size());\n\n        // ---\n\n        FormListEntry f0 = forms.get(0);\n        assertFalse(f0.getCustom());\n\n        String formName = f0.getName();\n\n        Map<String, Object> data = Collections.singletonMap(\"firstName\", firstName);\n        FormSubmitResponse fsr = formsApi.submitForm(spr.getInstanceId(), formName, data);\n        assertTrue(fsr.getOk());\n\n        ProcessEntry psr = waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.SUSPENDED);\n\n        byte[] ab = getLog(psr.getInstanceId());\n        assertLog(\".*100223.*\", ab);\n\n        // ---\n\n        forms = formsApi.listProcessForms(spr.getInstanceId());\n        assertEquals(1, forms.size());\n\n        // ---\n\n        formName = forms.get(0).getName();\n\n        data = new HashMap<>();\n        data.put(\"lastName\", lastName);\n        data.put(\"rememberMe\", true);\n        data.put(\"file\", \"file-content\");\n\n        fsr = formsApi.submitForm(spr.getInstanceId(), formName, data);\n        assertTrue(fsr.getOk());\n        assertTrue(fsr.getErrors() == null || fsr.getErrors().isEmpty());\n\n        psr = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(StatusEnum.FINISHED, psr.getStatus());\n\n        // ---\n\n        ab = getLog(psr.getInstanceId());\n        assertLog(\".*\" + firstName + \" \" + lastName + \".*\", ab);\n        assertLog(\".*100323.*\", ab);\n        assertLog(\".*r3d.*\", ab);\n        assertLog(\".*FILE_PATH _form_files/myForm2/file.*\", ab);\n        assertLog(\".*FILE file-content.*\", ab);\n        assertLog(\".*AAA true.*\", ab);\n    }\n\n    @Test\n    public void testSubmitMultipart() throws Exception {\n        String firstName = \"john_\" + randomString();\n        String lastName = \"smith_\" + randomString();\n        byte[] payload = archive(FormIT.class.getResource(\"form\").toURI());\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.SUSPENDED);\n\n        // ---\n\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n\n        List<FormListEntry> forms = formsApi.listProcessForms(spr.getInstanceId());\n        assertEquals(1, forms.size());\n\n        // ---\n\n        FormListEntry f0 = forms.get(0);\n        assertFalse(f0.getCustom());\n\n        String formName = f0.getName();\n\n        Map<String, Object> data = Collections.singletonMap(\"firstName\", firstName);\n        FormSubmitResponse fsr = formsApi.submitFormAsMultipart(spr.getInstanceId(), formName, data);\n        assertTrue(fsr.getOk());\n\n        ProcessEntry psr = waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.SUSPENDED);\n\n        byte[] ab = getLog(psr.getInstanceId());\n        assertLog(\".*100223.*\", ab);\n\n        // ---\n\n        forms = formsApi.listProcessForms(spr.getInstanceId());\n        assertEquals(1, forms.size());\n\n        // ---\n\n        formName = forms.get(0).getName();\n\n        data = new HashMap<>();\n        data.put(\"lastName\", lastName);\n        data.put(\"rememberMe\", true);\n        data.put(\"file\", \"file-content\".getBytes());\n\n        fsr = formsApi.submitFormAsMultipart(spr.getInstanceId(), formName, data);\n        assertTrue(fsr.getOk());\n        assertTrue(fsr.getErrors() == null || fsr.getErrors().isEmpty());\n\n        psr = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(StatusEnum.FINISHED, psr.getStatus());\n\n        // ---\n\n        ab = getLog(psr.getInstanceId());\n        assertLog(\".*\" + firstName + \" \" + lastName + \".*\", ab);\n        assertLog(\".*100323.*\", ab);\n        assertLog(\".*r3d.*\", ab);\n        assertLog(\".*FILE file-content.*\", ab);\n        assertLog(\".*AAA true.*\", ab);\n    }\n\n    @Test\n    public void testValues() throws Exception {\n        byte[] payload = archive(FormIT.class.getResource(\"formValues\").toURI());\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.SUSPENDED);\n\n        // ---\n\n        ProcessFormsApi formResource = new ProcessFormsApi(getApiClient());\n\n        List<FormListEntry> forms = formResource.listProcessForms(spr.getInstanceId());\n\n        FormListEntry f0 = forms.get(0);\n        String formName = f0.getName();\n\n        Map<String, Object> data = Collections.singletonMap(\"name\", \"Concord\");\n        FormSubmitResponse fsr = formResource.submitForm(spr.getInstanceId(), formName, data);\n        assertTrue(fsr.getOk());\n\n        // ---\n\n        ProcessEntry psr = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(StatusEnum.FINISHED, psr.getStatus());\n\n        byte[] ab = getLog(psr.getInstanceId());\n        assertLog(\".*Hello, Concord.*\", ab);\n    }\n\n    @Test\n    public void testAdditionalValuesSubmit() throws Exception {\n        byte[] payload = archive(FormIT.class.getResource(\"formValuesSubmit\").toURI());\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.SUSPENDED);\n\n        // ---\n\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n\n        List<FormListEntry> forms = formsApi.listProcessForms(spr.getInstanceId());\n        FormListEntry f0 = forms.get(0);\n        String formName = f0.getName();\n\n        FormSubmitResponse fsr = formsApi.submitForm(spr.getInstanceId(), formName, Collections.emptyMap());\n        assertTrue(fsr.getOk());\n\n        // ---\n\n        ProcessEntry psr = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(StatusEnum.FINISHED, psr.getStatus());\n\n        byte[] ab = getLog(psr.getInstanceId());\n        assertLog(\".*we got 123 hello 234.*\", ab);\n    }\n\n    @Test\n    public void testExternalFormFile() throws Exception {\n        String fieldValue = \"value_\" + randomString();\n\n        byte[] payload = archive(FormIT.class.getResource(\"formExternal\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.SUSPENDED);\n\n        // ---\n\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n\n        List<FormListEntry> forms = formsApi.listProcessForms(spr.getInstanceId());\n        assertEquals(1, forms.size());\n\n        // ---\n\n        FormListEntry f0 = forms.get(0);\n        assertFalse(f0.getCustom());\n\n        String formName = f0.getName();\n\n        Map<String, Object> data = Collections.singletonMap(\"myField\", fieldValue);\n        FormSubmitResponse fsr = formsApi.submitForm(spr.getInstanceId(), formName, data);\n        assertTrue(fsr.getOk());\n\n        ProcessEntry psr = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        byte[] ab = getLog(psr.getInstanceId());\n        assertLog(\".*We got \" + fieldValue + \".*\", ab);\n    }\n\n    @Test\n    public void testMultiValueInput() throws Exception {\n        byte[] payload = archive(FormIT.class.getResource(\"formMultiValue\").toURI());\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.SUSPENDED);\n\n        // ---\n\n        ProcessFormsApi formResource = new ProcessFormsApi(getApiClient());\n\n        List<FormListEntry> forms = formResource.listProcessForms(spr.getInstanceId());\n\n        FormListEntry f0 = forms.get(0);\n        String formName = f0.getName();\n\n        List<String> skills = new ArrayList<>();\n        skills.add(\"angular\");\n        skills.add(\"react\");\n        Map<String, Object> data = Collections.singletonMap(\"skills\", skills);\n        FormSubmitResponse fsr = formResource.submitForm(spr.getInstanceId(), formName, data);\n        assertTrue(fsr.getOk());\n\n        // ---\n\n        ProcessEntry psr = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(StatusEnum.FINISHED, psr.getStatus());\n\n        byte[] ab = getLog(psr.getInstanceId());\n        assertLog(\".*(Skills ->) \\\\[(angular|react), (react|angular)\\\\].*\", ab);\n    }\n\n    @Test\n    @Timeout(value = 60000, unit= TimeUnit.MILLISECONDS)\n    public void testDynamicFields() throws Exception {\n        byte[] payload = archive(FormIT.class.getResource(\"dynamicFormFields\").toURI());\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.SUSPENDED);\n\n        // ---\n\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n\n        List<FormListEntry> forms = formsApi.listProcessForms(spr.getInstanceId());\n        assertEquals(1, forms.size());\n\n        // ---\n\n        FormListEntry f0 = forms.get(0);\n        String formName = f0.getName();\n\n        String fieldValue = \"test_\" + randomString();\n        Map<String, Object> data = Collections.singletonMap(\"x\", fieldValue);\n        FormSubmitResponse fsr = formsApi.submitForm(spr.getInstanceId(), formName, data);\n        assertTrue(fsr.getOk());\n\n        ProcessEntry psr = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(StatusEnum.FINISHED, psr.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(psr.getInstanceId());\n        assertLog(\".*We got: \" + fieldValue + \".*\", ab);\n    }\n\n    @Test\n    public void testReadonlyField() throws Exception {\n        byte[] payload = archive(FormIT.class.getResource(\"formReadonlyField\").toURI());\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.SUSPENDED);\n\n        // ---\n\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n\n        List<FormListEntry> forms = formsApi.listProcessForms(spr.getInstanceId());\n        assertEquals(1, forms.size());\n\n        // ---\n\n        FormListEntry form = forms.get(0);\n        String formName = form.getName();\n\n        String fieldValue = \"test_\" + randomString();\n        Map<String, Object> data = Collections.singletonMap(\"myValue\", fieldValue);\n        FormSubmitResponse fsr = formsApi.submitForm(spr.getInstanceId(), formName, data);\n        assertTrue(fsr.getOk());\n\n        ProcessEntry psr = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(StatusEnum.FINISHED, psr.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(psr.getInstanceId());\n        assertLog(\".*default value.*\", ab);\n    }\n\n    @Test\n    public void testOptionalFileTypeField() throws Exception {\n        byte[] payload = archive(FormIT.class.getResource(\"formOptionalFileTypeField\").toURI());\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.SUSPENDED);\n\n        // ---\n\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n\n        List<FormListEntry> forms = formsApi.listProcessForms(spr.getInstanceId());\n        assertEquals(1, forms.size());\n\n        // ---\n\n        FormListEntry form = forms.get(0);\n        String formName = form.getName();\n\n        Map<String, Object> data = Collections.emptyMap();\n        FormSubmitResponse fsr = formsApi.submitForm(spr.getInstanceId(), formName, data);\n        assertTrue(fsr.getOk());\n\n        ProcessEntry psr = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        assertEquals(StatusEnum.FINISHED, psr.getStatus());\n    }\n\n    @Test\n    public void testFormCallWithExpression() throws Exception {\n        byte[] payload = archive(FormIT.class.getResource(\"formCallWithExpression\").toURI());\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.formNameVar\", \"myForm\");\n        StartProcessResponse spr = start(input);\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.SUSPENDED);\n\n        // ---\n\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n\n        List<FormListEntry> forms = formsApi.listProcessForms(spr.getInstanceId());\n\n        FormListEntry f0 = forms.get(0);\n        String formName = f0.getName();\n\n        Map<String, Object> data = Collections.singletonMap(\"name\", \"Concord\");\n        FormSubmitResponse fsr = formsApi.submitForm(spr.getInstanceId(), formName, data);\n        assertTrue(fsr.getOk());\n\n        // ---\n\n        ProcessEntry psr = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(StatusEnum.FINISHED, psr.getStatus());\n\n        byte[] ab = getLog(psr.getInstanceId());\n        assertLog(\".*Hello, Concord.*\", ab);\n    }\n\n    @Test\n    public void testFormLabelInterpolation() throws Exception {\n        String xValue = \"x_\" + randomString();\n\n        // ---\n\n        byte[] payload = archive(FormIT.class.getResource(\"formLabelExpression\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.x\", xValue);\n\n        StartProcessResponse spr = start(input);\n\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.SUSPENDED);\n\n        // ---\n\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n        List<FormListEntry> l = formsApi.listProcessForms(spr.getInstanceId());\n        assertEquals(1, l.size());\n\n        FormInstanceEntry f = formsApi.getProcessForm(spr.getInstanceId(), l.get(0).getName());\n        assertNotNull(f);\n\n        assertEquals(1, f.getFields().size());\n        Field field = f.getFields().get(0);\n        assertEquals(xValue, field.getLabel());\n    }\n\n    @Test\n    public void testSingleExpressionAllowedValue() throws Exception {\n        byte[] payload = archive(FormIT.class.getResource(\"formSingleAllowedValue\").toURI());\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.SUSPENDED);\n\n        // ---\n\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n\n        List<FormListEntry> forms = formsApi.listProcessForms(spr.getInstanceId());\n\n        FormListEntry f0 = forms.get(0);\n        String formName = f0.getName();\n\n        Map<String, Object> data = Collections.emptyMap();\n        FormSubmitResponse fsr = formsApi.submitForm(spr.getInstanceId(), formName, data);\n        assertTrue(fsr.getOk());\n\n        // ---\n\n        ProcessEntry psr = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(StatusEnum.FINISHED, psr.getStatus());\n\n        byte[] ab = getLog(psr.getInstanceId());\n        assertLog(\".*field1: one.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/GeneralTriggerIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.PathUtils;\nimport org.eclipse.jgit.api.Git;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class GeneralTriggerIT extends AbstractGeneralTriggerIT {\n\n    @Test\n    public void testExclusive() throws Exception {\n        Path tmpDir = createTempDir();\n\n        File src = new File(TriggersRefreshIT.class.getResource(\"generalExclusiveTrigger\").toURI());\n        PathUtils.copy(src.toPath(), tmpDir);\n\n        try (Git repo = Git.init().setInitialBranch(\"master\").setDirectory(tmpDir.toFile()).call()) {\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"import\").call();\n        }\n\n        String gitUrl = tmpDir.toAbsolutePath().toString();\n\n        // ---\n\n        String orgName = \"org_\" + randomString();\n        String projectName = \"project_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PUBLIC)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .url(gitUrl)\n                        .branch(\"master\"))));\n\n        // ---\n\n        waitForTriggers(orgName, projectName, repoName, 1);\n\n        // ---\n\n        ExternalEventsApi eea = new ExternalEventsApi(getApiClient());\n        Map<String, Object> eventParam = new HashMap<>();\n        eventParam.put(\"key1\", \"value1\");\n\n        // first process\n        eea.externalEvent(\"testTrigger\", eventParam);\n\n        // second process\n        eea.externalEvent(\"testTrigger\", eventParam);\n\n        Map<ProcessEntry.StatusEnum, ProcessEntry> ps = waitProcesses(orgName, projectName, ProcessEntry.StatusEnum.FINISHED, ProcessEntry.StatusEnum.CANCELLED);\n        assertProcessLog(ps.get(ProcessEntry.StatusEnum.FINISHED), \".*Hello from exclusive trigger.*\");\n        assertProcessLog(ps.get(ProcessEntry.StatusEnum.CANCELLED), \".*Process\\\\(es\\\\) with exclusive group 'RED' is already in the queue. Current process has been cancelled.*\");\n\n        // ---\n        orgApi.deleteOrg(orgName, \"yes\");\n    }\n\n    @Test\n    public void testExclusiveFromConfiguration() throws Exception {\n        Path tmpDir = createTempDir();\n\n        File src = new File(TriggersRefreshIT.class.getResource(\"generalTriggerWithExclusiveCfg\").toURI());\n        PathUtils.copy(src.toPath(), tmpDir);\n\n        try (Git repo = Git.init().setInitialBranch(\"master\").setDirectory(tmpDir.toFile()).call()) {\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"import\").call();\n        }\n\n        String gitUrl = tmpDir.toAbsolutePath().toString();\n\n        // ---\n\n        String orgName = \"org_\" + randomString();\n        String projectName = \"project_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PUBLIC)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .url(gitUrl)\n                        .branch(\"master\"))));\n\n        // ---\n\n        waitForTriggers(orgName, projectName, repoName, 1);\n\n        // ---\n\n        ExternalEventsApi eea = new ExternalEventsApi(getApiClient());\n        Map<String, Object> eventParam = new HashMap<>();\n        eventParam.put(\"key1\", \"value1\");\n\n        // first process\n        eea.externalEvent(\"testTrigger\", eventParam);\n\n        // second process\n        // we assume that the first process is in the RUNNING status when the second process is created\n        eea.externalEvent(\"testTrigger\", eventParam);\n\n        Map<ProcessEntry.StatusEnum, ProcessEntry> ps = waitProcesses(orgName, projectName, ProcessEntry.StatusEnum.FINISHED, ProcessEntry.StatusEnum.CANCELLED);\n        assertProcessLog(ps.get(ProcessEntry.StatusEnum.FINISHED), \".*Hello from exclusive trigger.*\");\n        assertProcessLog(ps.get(ProcessEntry.StatusEnum.CANCELLED), \".*Process\\\\(es\\\\) with exclusive group 'RED' is already in the queue. Current process has been cancelled.*\");\n\n        // ---\n        orgApi.deleteOrg(orgName, \"yes\");\n    }\n\n    @Test\n    public void testExclusiveWithTriggerOverride() throws Exception {\n        Path tmpDir = createTempDir();\n\n        File src = new File(TriggersRefreshIT.class.getResource(\"generalTriggerWithExclusiveOverride\").toURI());\n        PathUtils.copy(src.toPath(), tmpDir);\n\n        try (Git repo = Git.init().setInitialBranch(\"master\").setDirectory(tmpDir.toFile()).call()) {\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"import\").call();\n        }\n\n        String gitUrl = tmpDir.toAbsolutePath().toString();\n\n        // ---\n\n        String orgName = \"org_\" + randomString();\n        String projectName = \"project_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PUBLIC)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .url(gitUrl)\n                        .branch(\"master\"))));\n\n        // ---\n\n        waitForTriggers(orgName, projectName, repoName, 1);\n\n        // ---\n\n        ExternalEventsApi eea = new ExternalEventsApi(getApiClient());\n        Map<String, Object> eventParam = new HashMap<>();\n        eventParam.put(\"key1\", \"value1\");\n\n        // first process\n        eea.externalEvent(\"testTrigger\", eventParam);\n\n        // second process\n        eea.externalEvent(\"testTrigger\", eventParam);\n\n        Map<ProcessEntry.StatusEnum, ProcessEntry> ps = waitProcesses(orgName, projectName, ProcessEntry.StatusEnum.FINISHED, ProcessEntry.StatusEnum.CANCELLED);\n        assertProcessLog(ps.get(ProcessEntry.StatusEnum.FINISHED), \".*Hello from exclusive trigger.*\");\n        assertProcessLog(ps.get(ProcessEntry.StatusEnum.CANCELLED), \".*Process\\\\(es\\\\) with exclusive group 'TRIGGER' is already in the queue. Current process has been cancelled.*\");\n\n        // ---\n        orgApi.deleteOrg(orgName, \"yes\");\n    }\n}"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/GeneralTriggerV2IT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.PathUtils;\nimport org.eclipse.jgit.api.Git;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class GeneralTriggerV2IT extends AbstractGeneralTriggerIT {\n\n    private String orgName;\n    private String projectName;\n    private String repoName;\n    private OrganizationsApi orgApi;\n\n    private void setup(String yamlPath) throws Exception {\n        Path tmpDir = createTempDir();\n\n        File src = new File(TriggersRefreshIT.class.getResource(yamlPath).toURI());\n        PathUtils.copy(src.toPath(), tmpDir);\n\n        try (Git repo = Git.init().setInitialBranch(\"master\").setDirectory(tmpDir.toFile()).call()) {\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"import\").call();\n        }\n\n        String gitUrl = tmpDir.toAbsolutePath().toString();\n\n        // ---\n\n        orgName = \"org_\" + randomString();\n        projectName = \"project_\" + randomString();\n        repoName = \"repo_\" + randomString();\n\n        orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PUBLIC)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .url(gitUrl)\n                        .branch(\"master\"))));\n    }\n\n    private void cleanup() throws ApiException {\n        orgApi.deleteOrg(orgName, \"yes\");\n    }\n\n    @Test\n    public void testExclusiveV2() throws Exception {\n        setup(\"generalExclusiveTriggerv2\");\n\n        waitForTriggers(orgName, projectName, repoName, 2);\n\n        ExternalEventsApi eea = new ExternalEventsApi(getApiClient());\n        Map<String, Object> eventParam = new HashMap<>();\n        eventParam.put(\"key1\", \"value2\");\n\n        // first process\n        eea.externalEvent(\"testTriggerv2\", eventParam);\n\n        // second process\n        eea.externalEvent(\"testTriggerv2\", eventParam);\n\n        Map<ProcessEntry.StatusEnum, ProcessEntry> ps = waitProcesses(orgName, projectName, ProcessEntry.StatusEnum.FINISHED, ProcessEntry.StatusEnum.CANCELLED);\n        assertProcessLog(ps.get(ProcessEntry.StatusEnum.FINISHED), \".*Hello from exclusive trigger v2.*\");\n        assertProcessLog(ps.get(ProcessEntry.StatusEnum.CANCELLED), \".*Process\\\\(es\\\\) with exclusive group 'RED' is already in the queue. Current process has been cancelled.*\");\n\n        cleanup();\n    }\n\n    @Test\n    public void testExclusiveFromConfigurationV2() throws Exception {\n        setup(\"generalTriggerWithExclusiveCfgv2\");\n\n        waitForTriggers(orgName, projectName, repoName, 2);\n\n        // ---\n\n        ExternalEventsApi eea = new ExternalEventsApi(getApiClient());\n        Map<String, Object> eventParam = new HashMap<>();\n        eventParam.put(\"key1\", \"value2\");\n\n        // first process\n        eea.externalEvent(\"testTriggerv2\", eventParam);\n\n        // second process\n        eea.externalEvent(\"testTriggerv2\", eventParam);\n\n        Map<ProcessEntry.StatusEnum, ProcessEntry> ps = waitProcesses(orgName, projectName, ProcessEntry.StatusEnum.FINISHED, ProcessEntry.StatusEnum.CANCELLED);\n        assertProcessLog(ps.get(ProcessEntry.StatusEnum.FINISHED), \".*Hello from exclusive trigger v2.*\");\n        assertProcessLog(ps.get(ProcessEntry.StatusEnum.CANCELLED), \".*Process\\\\(es\\\\) with exclusive group 'RED' is already in the queue. Current process has been cancelled.*\");\n\n        // ---\n\n        cleanup();\n    }\n\n    @Test\n    public void testExclusiveWithTriggerOverrideV2() throws Exception {\n        setup(\"generalTriggerWithExclusiveOverridev2\");\n\n        waitForTriggers(orgName, projectName, repoName, 2);\n\n        // ---\n\n        ExternalEventsApi eea = new ExternalEventsApi(getApiClient());\n        Map<String, Object> eventParam = new HashMap<>();\n        eventParam.put(\"key1\", \"value2\");\n\n        // first process\n        eea.externalEvent(\"testTriggerv2\", eventParam);\n\n        // second process\n        eea.externalEvent(\"testTriggerv2\", eventParam);\n\n        Map<ProcessEntry.StatusEnum, ProcessEntry> ps = waitProcesses(orgName, projectName, ProcessEntry.StatusEnum.FINISHED, ProcessEntry.StatusEnum.CANCELLED);\n        assertProcessLog(ps.get(ProcessEntry.StatusEnum.FINISHED), \".*Hello from exclusive trigger v2.*\");\n        assertProcessLog(ps.get(ProcessEntry.StatusEnum.CANCELLED), \".*Process\\\\(es\\\\) with exclusive group 'TRIGGER' is already in the queue. Current process has been cancelled.*\");\n\n        // ---\n\n        cleanup();\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/GitBranchesIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.it.common.MockGitSshServer;\nimport org.eclipse.jgit.api.Git;\nimport org.eclipse.jgit.transport.RefSpec;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardCopyOption;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class GitBranchesIT extends AbstractServerIT {\n\n    private MockGitSshServer gitServer;\n\n    @BeforeEach\n    public void setUp() throws Exception {\n        Path bareRepo = createTempDir();\n        try (Git git = Git.init().setInitialBranch(\"master\").setBare(true).setDirectory(bareRepo.toFile()).call()) {\n        }\n\n        Path workdir = createTempDir();\n        try (Git git = Git.cloneRepository()\n                .setDirectory(workdir.toFile())\n                .setURI(\"file://\" + bareRepo)\n                .call()) {\n\n            Path initialData = Paths.get(GitBranchesIT.class.getResource(\"gitBranches/qa\").toURI());\n            PathUtils.copy(initialData, workdir);\n\n            git.add().addFilepattern(\".\").call();\n            git.commit().setMessage(\"initial commit\").call();\n\n            git.checkout().setCreateBranch(true).setName(\"qa\").call();\n            git.push().setRefSpecs(new RefSpec(\"qa:qa\")).call();\n\n            git.checkout().setCreateBranch(true).setName(\"dev\").call();\n\n            Path devData = Paths.get(GitBranchesIT.class.getResource(\"gitBranches/dev\").toURI());\n            PathUtils.copy(devData, workdir, StandardCopyOption.REPLACE_EXISTING);\n\n            git.add().addFilepattern(\".\").call();\n            git.commit().setMessage(\"dev commit\").call();\n            git.push().setRefSpecs(new RefSpec(\"dev:dev\")).call();\n        }\n\n        gitServer = new MockGitSshServer(0, bareRepo);\n        gitServer.start();\n    }\n\n    @AfterEach\n    public void tearDown() throws Exception {\n        gitServer.stop();\n    }\n\n    @Test\n    public void test() throws Exception {\n        String orgName = \"Default\";\n        String projectName = \"project_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n        String repoUrl = String.format(ITConstants.GIT_SERVER_URL_PATTERN, gitServer.getPort());\n        String repoSecret = \"secret_\" + randomString();\n\n        SecretOperationResponse sor = generateKeyPair(orgName, repoSecret, false, null);\n        assertEquals(SecretOperationResponse.ResultEnum.CREATED, sor.getResult());\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .url(repoUrl)\n                        .secretId(sor.getId())\n                        .branch(\"qa\"))));\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"repo\", repoName);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pe.getStatus());\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLog(\".*running qa.*\", ab);\n\n        // ---\n\n        RepositoriesApi repositoriesApi = new RepositoriesApi(getApiClient());\n        repositoriesApi.createOrUpdateRepository(orgName, projectName, new RepositoryEntry()\n                .name(repoName)\n                .url(repoUrl)\n                .secretId(sor.getId())\n                .branch(\"dev\"));\n\n        // ---\n\n        input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"repo\", repoName);\n        spr = start(input);\n\n        // ---\n\n        pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pe.getStatus());\n\n        ab = getLog(pe.getInstanceId());\n        assertLog(\".*running dev.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/GitHubNonOrgEventIt.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.client2.ProcessListFilter;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.it.common.GitHubUtils;\nimport org.eclipse.jgit.api.Git;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class GitHubNonOrgEventIt extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        Path tmpDir = createTempDir();\n\n        File src = new File(GitHubNonOrgEventIt.class.getResource(\"githubNonRepoEvent\").toURI());\n        PathUtils.copy(src.toPath(), tmpDir);\n\n        try (Git repo = Git.init().setInitialBranch(\"master\").setDirectory(tmpDir.toFile()).call()) {\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"import\").call();\n        }\n\n        String gitUrl = tmpDir.toAbsolutePath().toString();\n\n        // ---\n\n        String orgName = \"org_\" + randomString();\n        String projectName = \"test_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry()\n                .name(orgName));\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .url(gitUrl))));\n\n        // ---\n\n        TriggersApi triggersApi = new TriggersApi(getApiClient());\n\n        while (!Thread.currentThread().isInterrupted()) {\n            List<TriggerEntry> triggers = triggersApi.listTriggers(orgName, projectName, repoName);\n            if (!triggers.isEmpty()) {\n                break;\n            }\n\n            Thread.sleep(1000);\n        }\n\n        // ---\n\n        githubEvent(\"githubNonRepoEvent/event.json\", null, \"team\");\n\n        List<ProcessEntry> processes;\n\n        ProcessV2Api processV2Api = new ProcessV2Api(getApiClient());\n        ProcessListFilter filter = ProcessListFilter.builder()\n                .orgName(orgName)\n                .projectName(projectName)\n                .status(ProcessEntry.StatusEnum.FINISHED)\n                .build();\n\n        while (true) {\n            processes = processV2Api.listProcesses(filter);\n            if (!processes.isEmpty()) {\n                break;\n            }\n\n            Thread.sleep(1000);\n        }\n\n        assertEquals(1, processes.size());\n\n        // ---\n\n        ProcessEntry pe = processes.get(0);\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLog(\".*EVENT:.*added_to_repository.*\", ab);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private void githubEvent(String eventFile, String repoName, String eventName) throws Exception {\n        String eventStr = new String(Files.readAllBytes(Paths.get(GitHubNonOrgEventIt.class.getResource(eventFile).toURI())));\n        if (repoName != null) {\n            eventStr = eventStr.replace(\"org-repo\", repoName);\n        }\n\n        Map<String, Object> event = getApiClient().getObjectMapper().readValue(eventStr, Map.class);\n        eventStr = getApiClient().getObjectMapper().writeValueAsString(event);\n\n        ApiClient client = getApiClient();\n        client.addDefaultHeader(\"X-Hub-Signature\", \"sha1=\" + GitHubUtils.sign(eventStr));\n\n        GitHubEventsApi gitHubEvents = new GitHubEventsApi(client);\n\n        String result = gitHubEvents.onEvent(null, \"abc\", eventName, event);\n        assertEquals(\"ok\", result);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/GitHubTriggersV2IT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\n\nimport javax.naming.NameAlreadyBoundException;\nimport javax.naming.directory.*;\nimport java.nio.file.Path;\nimport java.time.OffsetDateTime;\nimport java.time.temporal.ChronoUnit;\nimport java.util.Collections;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class GitHubTriggersV2IT extends AbstractGitHubTriggersIT {\n\n    @AfterEach\n    public void tearDown() throws Exception {\n        waitForProcessesToFinish();\n    }\n\n    /**\n     * Checks if \"conditions->sender\" works.\n     * Tests filtering by \"sender\" in events that originate from \"known\"\n     * and \"unknown\" (not registered in any Concord project) GitHub repositories.\n     */\n    @Test\n    public void testFilterBySender() throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgXName = \"orgX_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgXName));\n\n        // project A uses a default trigger without conditions\n        // should trigger on any event for the matching \"known\" repository\n        // i.e. the event must come from a GitHub URL that is added to a Concord project\n        String projectAName = \"projectA_\" + randomString();\n        String repoAName = \"repoA_\" + randomString();\n        Path projectARepo = initProjectAndRepo(orgXName, projectAName, repoAName, null, initRepo(\"githubTests/repos/v2/defaultTrigger\"));\n        refreshRepo(orgXName, projectAName, repoAName);\n\n        // project G accepts only specific event senders but from any repository\n        // (including events from GitHub URLs that are not registered in any Concord project)\n        String projectGName = \"projectG_\" + randomString();\n        String repoGName = \"repoG_\" + randomString();\n        initProjectAndRepo(orgXName, projectGName, repoGName, null, initRepo(\"githubTests/repos/v2/anyRepoWithSender\"));\n        refreshRepo(orgXName, projectGName, repoGName);\n\n        // send an a projectARepo event from a random sender\n        sendEvent(\"githubTests/events/direct_branch_push.json\", \"push\",\n                \"_FULL_REPO_NAME\", toRepoName(projectARepo),\n                \"_REF\", \"refs/heads/master\",\n                \"_USER_NAME\", \"arandomdude\" + randomString(),\n                \"_USER_LDAP_DN\", \"\");\n\n        // A's triggers should be activated\n        waitForAProcess(orgXName, projectAName, \"github\");\n        // G's triggers should NOT be activated, the event is not from not the right sender\n        expectNoProcesses(orgXName, projectGName, null);\n\n        // see https://github.com/walmartlabs/concord/issues/435\n        // wait a bit to reliably filter out subsequent processes of projectA\n        Thread.sleep(1000);\n        OffsetDateTime now = OffsetDateTime.now().truncatedTo(ChronoUnit.SECONDS);\n\n        // send an a projectARepo event (again), this time from the user expected by the G trigger\n        sendEvent(\"githubTests/events/direct_branch_push.json\", \"push\",\n                \"_FULL_REPO_NAME\", \"org\" + randomString() + \"/\" + \"repo\" + randomString(), // a random (\"unknown\") GitHub repository\n                \"_REF\", \"refs/heads/master\",\n                \"_USER_NAME\", \"somecooldude\",\n                \"_USER_LDAP_DN\", \"\");\n\n        // G's triggers should be activated\n        waitForAProcess(orgXName, projectGName, \"github\");\n\n        // no A's are expected\n        expectNoProcesses(orgXName, projectAName, now);\n\n        // clean up\n\n        deleteOrg(orgXName);\n    }\n\n    /**\n     * Tests the default branch behaviour:\n     * <pre>\n     * # project A\n     * # a default onPush trigger for the default branch\n     * triggers:\n     *   - github:\n     *       entryPoint: onPush\n     * </pre>\n     */\n    @Test\n    public void testDefaultBranch() throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgXName = \"orgX_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgXName));\n\n        // Project A\n        // master branch + a default trigger\n        String projectAName = \"projectA_\" + randomString();\n        String repoAName = \"repoA_\" + randomString();\n        Path projectARepo = initProjectAndRepo(orgXName, projectAName, repoAName, null, initRepo(\"githubTests/repos/v2/defaultTrigger\"));\n        refreshRepo(orgXName, projectAName, repoAName);\n\n        // ---\n\n        sendEvent(\"githubTests/events/direct_branch_push.json\", \"push\",\n                \"_FULL_REPO_NAME\", toRepoName(projectARepo),\n                \"_USER_LDAP_DN\", \"\",\n                \"_REF\", \"refs/heads/master\");\n\n        // A's trigger should be activated\n        waitForAProcess(orgXName, projectAName, \"github\");\n\n        // ---\n\n        deleteOrg(orgXName);\n    }\n\n    @Test\n    public void testOnPushWithFullTriggerParams() throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgXName = \"orgX_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgXName));\n\n        // Project A\n        // master branch + a default trigger\n        String projectAName = \"projectA_\" + randomString();\n        String repoAName = \"repoA_\" + randomString();\n        initProjectAndRepo(orgXName, projectAName, repoAName, null, initRepo(\"githubTests/repos/v2/allParamsTrigger\"));\n        refreshRepo(orgXName, projectAName, repoAName);\n\n        // ---\n\n        sendEvent(\"githubTests/events/direct_branch_push.json\", \"push\",\n                \"_FULL_REPO_NAME\", \"devtools/concord\",\n                \"_REF\", \"refs/heads/master\",\n                \"_USER_NAME\", \"vasia\",\n                \"_USER_LDAP_DN\", \"\");\n\n        // A's trigger should be activated\n        waitForAProcess(orgXName, projectAName, \"github\");\n\n        // ---\n\n        deleteOrg(orgXName);\n    }\n\n    @Test\n    public void testOnPushWithFiles() throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgXName = \"orgX_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgXName));\n\n        // Project A\n        // master branch + a default trigger\n        String projectAName = \"projectA_\" + randomString();\n        String repoAName = \"repoA_\" + randomString();\n        initProjectAndRepo(orgXName, projectAName, repoAName, null, initRepo(\"githubTests/repos/v2/files\"));\n        refreshRepo(orgXName, projectAName, repoAName);\n\n        // ---\n\n        sendEvent(\"githubTests/events/direct_branch_push.json\", \"push\",\n                \"_FULL_REPO_NAME\", \"devtools/concord\",\n                \"_REF\", \"refs/heads/master\",\n                \"_USER_LDAP_DN\", \"\");\n\n        // A's trigger should be activated\n        waitForAProcess(orgXName, projectAName, \"github\");\n\n        // ---\n\n        deleteOrg(orgXName);\n    }\n\n    /**\n     * Verify that the \"requestInfo\" variable is available for GitHub processes\n     * (should be empty).\n     */\n    @Test\n    public void testRequestInfo() throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgXName = \"orgX_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgXName));\n\n        // Project A\n        // master branch + a default trigger\n        String projectAName = \"projectA_\" + randomString();\n        String repoAName = \"repoA_\" + randomString();\n        Path projectARepo = initProjectAndRepo(orgXName, projectAName, repoAName, null, initRepo(\"githubTests/repos/v2/requestInfo\"));\n        refreshRepo(orgXName, projectAName, repoAName);\n\n        // ---\n\n        sendEvent(\"githubTests/events/direct_branch_push.json\", \"push\",\n                \"_FULL_REPO_NAME\", toRepoName(projectARepo),\n                \"_REF\", \"refs/heads/master\",\n                \"_USER_LDAP_DN\", \"\");\n\n        // A's trigger should be activated\n        ProcessEntry pe = waitForAProcess(orgXName, projectAName, \"github\");\n\n        assertLog(pe, \".*Hello, !.*\");\n\n        // ---\n\n        deleteOrg(orgXName);\n    }\n\n    @Test\n    public void testQueryParamsCondition() throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgXName = \"orgX_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgXName));\n\n        // Project A\n        // master branch + a default trigger\n        String projectAName = \"projectA_\" + randomString();\n        String repoAName = \"repoA_\" + randomString();\n        Path projectARepo = initProjectAndRepo(orgXName, projectAName, repoAName, null, initRepo(\"githubTests/repos/v2/queryParams\"));\n        refreshRepo(orgXName, projectAName, repoAName);\n\n        // ---\n\n        sendEvent(\"githubTests/events/direct_branch_push.json\", \"push\",\n                Collections.singletonMap(\"param1\", \"value1\"),\n                \"_FULL_REPO_NAME\", toRepoName(projectARepo),\n                \"_REF\", \"refs/heads/master\",\n                \"_USER_LDAP_DN\", \"\");\n\n        // A's trigger should be activated\n        ProcessEntry pe = waitForAProcess(orgXName, projectAName, \"github\");\n\n        assertLog(pe, \".*Hello, value1!.*\");\n\n        // ---\n\n        deleteOrg(orgXName);\n    }\n\n    @Test\n    public void testIgnoreEmptyPush() throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgName = \"org_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n        Path projectRepo = initProjectAndRepo(orgName, projectName, repoName, null, initRepo(\"githubTests/repos/v2/ignoreEmptyPushTrigger\"));\n        refreshRepo(orgName, projectName, repoName);\n\n        // ---\n\n        sendEvent(\"githubTests/events/empty_push.json\", \"push\",\n                \"_FULL_REPO_NAME\", toRepoName(projectRepo),\n                \"_REF\", \"refs/heads/master\",\n                \"_USER_LDAP_DN\", \"\");\n\n        ProcessEntry pe = waitForAProcess(orgName, projectName, \"github\");\n        assertLog(pe, \".*onEmpty: .*\");\n\n        ProcessV2Api processApi = new ProcessV2Api(getApiClient());\n        ProcessListFilter filter = ProcessListFilter.builder()\n                .orgName(orgName)\n                .projectName(projectName)\n                .build();\n\n        List<ProcessEntry> list = processApi.listProcesses(filter);\n        assertEquals(1, list.size());\n\n        // ---\n\n        sendEvent(\"githubTests/events/direct_branch_push.json\", \"push\",\n                \"_FULL_REPO_NAME\", toRepoName(projectRepo),\n                \"_REF\", \"refs/heads/master\",\n                \"_USER_LDAP_DN\", \"\");\n\n        while (true) {\n            list = processApi.listProcesses(filter);\n            if (list.size() == 3) {\n                break;\n            }\n\n            Thread.sleep(1000);\n        }\n    }\n\n    @Test\n    public void testRefreshOnGitHubEvent() throws Exception {\n        String username = createUser();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgName = \"org_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // -- create two projects to hold two similarly named repos\n\n        String projectName1 = \"project_\" + randomString();\n        String projectName2 = \"project_\" + randomString();\n        String repoNameShort = \"repo_\" + randomString();\n        String repoNameLong = repoNameShort + \"-two\";\n\n        Path repoPathShort = initRepo(\"githubTests/repos/v2/defaultTrigger\", orgName + \"/\" + repoNameShort);\n        Path repoPathLong = initRepo(\"githubTests/repos/v2/defaultTrigger\", orgName + \"/\" + repoNameLong);\n\n        Path projectRepoShort = initProjectAndRepo(orgName, projectName1, repoNameShort, null, repoPathShort);\n        Path projectRepoLong = initProjectAndRepo(orgName, projectName2, repoNameLong, null, repoPathLong);\n        waitForProcessesToFinish();\n\n        // -- send GitHub event to trigger refresh\n\n        OffsetDateTime after = OffsetDateTime.now();\n        sendEvent(\"githubTests/events/direct_branch_push.json\", \"push\",\n                \"_USER_LDAP_DN\", \"\",\n                \"_USER_NAME\", username,\n                \"_ORG_NAME\", orgName,\n                \"_FULL_REPO_NAME\", toRepoName(projectRepoShort), // must be before _REPO_NAME\n                \"_REPO_NAME\", repoNameShort,\n                \"_REF\", \"refs/heads/master\");\n\n        // -- locate and wait for repository refresh process\n\n        ProcessV2Api processApi = new ProcessV2Api(getApiClient());\n        ProcessListFilter filter = ProcessListFilter.builder()\n                .orgName(\"ConcordSystem\")\n                .projectName(\"concordTriggers\")\n                .limit(1)\n                .offset(0)\n                .afterCreatedAt(after)\n                .build();\n        ProcessEntry refreshProc;\n\n        while (true) {\n            refreshProc = processApi.listProcesses(filter).stream()\n                    .filter(e -> e.getInitiator().equals(\"github\"))\n                    .findFirst()\n                    .orElse(null);\n\n            if (refreshProc != null && refreshProc.getStatus() == ProcessEntry.StatusEnum.FINISHED) {\n                break;\n            }\n            Thread.sleep(500);\n        }\n\n        assertNotNull(refreshProc, \"Must find repository refresh process.\");\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, refreshProc.getStatus(),\n                \"Repository refresh process must finish successfully.\");\n\n        // -- process log should indicate only one repo was refreshed\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        ProjectEntry p = projectsApi.getProject(orgName, projectName1);\n\n        RepositoriesApi repositoriesApi = new RepositoriesApi(getApiClient());\n        List<RepositoryEntry> repos = repositoriesApi.listRepositories(orgName, projectName1, null, null, null);\n\n        RepositoryEntry repo = repos.stream()\n                .filter(e -> e.getName().equals(repoNameShort))\n                .findFirst()\n                .orElseThrow(() -> new Exception(\"Unable to locate repository\"));\n\n        assertLog(refreshProc, \".*Repository ids to refresh: \\\\[\" + repo.getId().toString() + \"\\\\].*\");\n    }\n\n    @Test\n    public void testBranchDeleteRefresh() throws Exception {\n        String username = createUser();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgName = \"org_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // -- create two projects to hold two similarly named repos\n\n        String projectNameMaster = \"project_master\" + randomString();\n        String projectNameDev = \"project_dev_\" + randomString();\n        String projectNameTmp = \"project_tmp_\" + randomString();\n        String repoMaster = \"repo_master_\" + randomString();\n        String repoDev = \"repo_dev_\" + randomString();\n        String repoTmp = \"repo_tmp_\" + randomString();\n        String devBranch = \"dev\";\n        String tmpBranch = \"tmp\";\n\n        Path repoPath = initRepo(\"githubTests/repos/v2/defaultTrigger\", orgName + \"/\" + repoMaster);\n        // we only need the repo to exist enough to create the concord repo\n        createNewBranch(repoPath, devBranch, \"githubTests/repos/v2/defaultTrigger\");\n        createNewBranch(repoPath, tmpBranch, \"githubTests/repos/v2/defaultTrigger\");\n\n        Path projectRepoMain = initProjectAndRepo(orgName, projectNameMaster, repoMaster, null, repoPath);\n        Path projectRepoDev = initProjectAndRepo(orgName, projectNameDev, repoDev, devBranch, repoPath);\n        Path projectRepoTmp = initProjectAndRepo(orgName, projectNameTmp, repoTmp, tmpBranch, repoPath);\n        waitForProcessesToFinish();\n\n        // -- send GitHub event to trigger refresh\n\n        OffsetDateTime after = OffsetDateTime.now();\n        sendEvent(\"githubTests/events/direct_branch_push_delete.json\", \"push\",\n                \"_USER_LDAP_DN\", \"\",\n                \"_USER_NAME\", username,\n                \"_ORG_NAME\", orgName,\n                \"_FULL_REPO_NAME\", toRepoName(projectRepoMain), // must be before _REPO_NAME\n                \"_REPO_NAME\", repoMaster,\n                \"_REF\", \"refs/heads/tmp\");\n\n        // -- locate and wait for repository refresh process\n        ProcessV2Api processApi = new ProcessV2Api(getApiClient());\n        ProcessListFilter filter = ProcessListFilter.builder()\n                .orgName(\"ConcordSystem\")\n                .projectName(\"concordTriggers\")\n                .limit(1)\n                .offset(0)\n                .afterCreatedAt(after)\n                .build();\n        ProcessEntry refreshProc;\n\n        // not great, but we need to ensure no processes were generated\n        Thread.sleep(3000);\n\n        refreshProc = processApi.listProcesses(filter).stream()\n                    .filter(e -> e.getInitiator().equals(\"github\"))\n                    .findFirst()\n                    .orElse(null);\n\n        assertNull(refreshProc, \"Must NOT find repository refresh process.\");\n    }\n\n    @Test\n    public void testBranchRefreshMatchingOnly() throws Exception {\n        String username = createUser();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgName = \"org_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // -- create two projects to hold two similarly named repos\n\n        String projectName1 = \"project_master\" + randomString();\n        String projectName2 = \"project_dev_\" + randomString();\n        String repoMaster = \"repo_main_\" + randomString();\n        String repoDev = \"repo_dev_\" + randomString();\n        String devBranch = \"dev\";\n\n        Path repoPath = initRepo(\"githubTests/repos/v2/defaultTrigger\", orgName + \"/\" + repoMaster);\n        // we only need the repo to exist enough to create the concord repo\n        createNewBranch(repoPath, devBranch, \"githubTests/repos/v2/defaultTrigger\");\n\n        Path projectRepoMain = initProjectAndRepo(orgName, projectName1, repoMaster, null, repoPath);\n        Path projectRepoDev = initProjectAndRepo(orgName, projectName2, repoDev, devBranch, repoPath);\n        waitForProcessesToFinish();\n\n        // -- send GitHub event to trigger refresh\n\n        OffsetDateTime after = OffsetDateTime.now();\n        sendEvent(\"githubTests/events/direct_branch_push.json\", \"push\",\n                \"_USER_LDAP_DN\", \"\",\n                \"_USER_NAME\", username,\n                \"_ORG_NAME\", orgName,\n                \"_FULL_REPO_NAME\", toRepoName(projectRepoMain), // must be before _REPO_NAME\n                \"_REPO_NAME\", repoMaster,\n                \"_REF\", \"refs/heads/master\");\n\n        // -- locate and wait for repository refresh process\n\n        ProcessV2Api processApi = new ProcessV2Api(getApiClient());\n        ProcessListFilter filter = ProcessListFilter.builder()\n                .orgName(\"ConcordSystem\")\n                .projectName(\"concordTriggers\")\n                .limit(1)\n                .offset(0)\n                .afterCreatedAt(after)\n                .build();\n        ProcessEntry refreshProc;\n\n        while (true) {\n            refreshProc = processApi.listProcesses(filter).stream()\n                    .filter(e -> e.getInitiator().equals(\"github\"))\n                    .findFirst()\n                    .orElse(null);\n\n            if (refreshProc != null && refreshProc.getStatus() == ProcessEntry.StatusEnum.FINISHED) {\n                break;\n            }\n            Thread.sleep(500);\n        }\n\n        assertNotNull(refreshProc, \"Must find repository refresh process.\");\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, refreshProc.getStatus(),\n                \"Repository refresh process must finish successfully.\");\n\n        // -- process log should indicate only default repo was refreshed\n\n        RepositoriesApi repositoriesApi = new RepositoriesApi(getApiClient());\n        List<RepositoryEntry> repos = repositoriesApi.listRepositories(orgName, projectName1, null, null, null);\n\n        RepositoryEntry repo = repos.stream()\n                .filter(e -> e.getName().equals(repoMaster))\n                .findFirst()\n                .orElseThrow(() -> new Exception(\"Unable to locate repository\"));\n\n        assertLog(refreshProc, \".*Repository ids to refresh: \\\\[\" + repo.getId().toString() + \"\\\\].*\");\n    }\n\n    @Test\n    public void testUseInitiatorFromSender() throws Exception {\n        String username = createUser();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgName = \"org_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n        Path projectRepo = initProjectAndRepo(orgName, projectName, repoName, null, initRepo(\"githubTests/repos/v2/useInitiatorTrigger\"));\n        refreshRepo(orgName, projectName, repoName);\n\n        // ---\n\n        sendEvent(\"githubTests/events/direct_branch_push.json\", \"push\",\n                \"_USER_LDAP_DN\", \"\",\n                \"_USER_NAME\", username,\n                \"_FULL_REPO_NAME\", toRepoName(projectRepo),\n                \"_REF\", \"refs/heads/master\");\n\n        ProcessEntry pe = waitForAProcess(orgName, projectName, username);\n        assertEquals(username, pe.getInitiator());\n    }\n\n    @Test\n    public void testUseInitiatorFromSenderLdapDn() throws Exception {\n        String username = createUser();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgName = \"org_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n        Path projectRepo = initProjectAndRepo(orgName, projectName, repoName, null, initRepo(\"githubTests/repos/v2/useInitiatorTrigger\"));\n        refreshRepo(orgName, projectName, repoName);\n\n        // ---\n\n        sendEvent(\"githubTests/events/direct_branch_push.json\", \"push\",\n                \"_USER_LDAP_DN\", \"cn=\" + username + \",dc=example,dc=org\",\n                \"_FULL_REPO_NAME\", toRepoName(projectRepo),\n                \"_REF\", \"refs/heads/master\");\n\n        ProcessEntry pe = waitForAProcess(orgName, projectName, username);\n        assertEquals(username, pe.getInitiator());\n    }\n\n    @Test\n    public void testExclusiveGroupByBranch() throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgXName = \"orgX_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgXName));\n\n        // Project A\n        // master branch + a default trigger\n        String projectAName = \"projectA_\" + randomString();\n        String repoAName = \"repoA_\" + randomString();\n        Path projectARepo = initProjectAndRepo(orgXName, projectAName, repoAName, null, initRepo(\"githubTests/repos/v2/groupByBranchTrigger\"));\n        refreshRepo(orgXName, projectAName, repoAName);\n\n        // ---\n\n        sendEvent(\"githubTests/events/pr_open.json\", \"pull_request\",\n                \"_FULL_REPO_NAME\", toRepoName(projectARepo),\n                \"_USER_LDAP_DN\", \"\",\n                \"_REF\", \"refs/heads/master\");\n\n        // A's trigger should be activated\n        ProcessEntry pe = waitForAProcess(orgXName, projectAName, \"github\");\n\n        assertLog(pe, \".*Process' exclusive group: master.*\");\n\n        // ---\n\n        deleteOrg(orgXName);\n    }\n\n    @Test\n    public void testExclusiveGroupByEventAttr() throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgXName = \"orgX_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgXName));\n\n        // Project A\n        // master branch + a default trigger\n        String projectAName = \"projectA_\" + randomString();\n        String repoAName = \"repoA_\" + randomString();\n        Path projectARepo = initProjectAndRepo(orgXName, projectAName, repoAName, null, initRepo(\"githubTests/repos/v2/groupByEventAttrTrigger\"));\n        refreshRepo(orgXName, projectAName, repoAName);\n\n        // ---\n\n        sendEvent(\"githubTests/events/pr_open.json\", \"pull_request\",\n                \"_FULL_REPO_NAME\", toRepoName(projectARepo),\n                \"_USER_LDAP_DN\", \"\",\n                \"_REF\", \"refs/heads/master\");\n\n        // A's trigger should be activated\n        ProcessEntry pe = waitForAProcess(orgXName, projectAName, \"github\");\n\n        assertLog(pe, \".*Process' exclusive group: pr-test-3.*\");\n\n        // ---\n\n        deleteOrg(orgXName);\n    }\n\n    private String createUser() throws Exception {\n        assertNotNull(System.getenv(\"IT_LDAP_URL\"));\n\n        String username = \"user_\" + randomString();\n\n        DirContext ldapCtx = LdapIT.createContext();\n        createLdapUser(ldapCtx, username);\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(username)\n                .type(CreateUserRequest.TypeEnum.LDAP));\n        return username;\n    }\n\n    static void createLdapUser(DirContext ldapCtx, String username) throws Exception {\n        String dn = \"cn=\" + username + \",dc=example,dc=org\";\n        Attributes attributes = new BasicAttributes();\n\n        Attribute cn = new BasicAttribute(\"cn\", username);\n        Attribute sn = new BasicAttribute(\"sn\", username);\n\n        Attribute objectClass = new BasicAttribute(\"objectClass\");\n        objectClass.add(\"top\");\n        objectClass.add(\"organizationalPerson\");\n\n        attributes.put(cn);\n        attributes.put(sn);\n        attributes.put(objectClass);\n\n        try {\n            ldapCtx.createSubcontext(dn, attributes);\n        } catch (NameAlreadyBoundException e) {\n            System.err.println(\"createLdapUser -> \" + e.getMessage());\n            // already exists, ignore\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/GitRepositoryIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.it.common.MockGitSshServer;\nimport org.eclipse.jgit.api.Git;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class GitRepositoryIT extends AbstractServerIT {\n\n    private MockGitSshServer gitServer;\n\n    @BeforeEach\n    public void setUp() throws Exception {\n        Path bareRepo = createTempDir();\n        try (Git git = Git.init().setInitialBranch(\"master\").setBare(true).setDirectory(bareRepo.toFile()).call()) {\n        }\n\n        Path workdir = createTempDir();\n        try (Git git = Git.cloneRepository()\n                .setDirectory(workdir.toFile())\n                .setURI(\"file://\" + bareRepo.toString())\n                .call()) {\n\n            Path initialData = Paths.get(GitRepositoryIT.class.getResource(\"gitRepository\").toURI());\n            PathUtils.copy(initialData, workdir);\n\n            git.add().addFilepattern(\".\").call();\n            git.commit().setMessage(\"initial commit\").call();\n\n            git.push().call();\n        }\n\n        gitServer = new MockGitSshServer(0, bareRepo);\n        gitServer.start();\n    }\n\n    @AfterEach\n    public void tearDown() throws Exception {\n        gitServer.stop();\n    }\n\n    @Test\n    public void testGitUrlWithDifferentUser() throws Exception {\n        String orgName = \"Default\";\n        String projectName = \"project_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n\n        String repoUrl = String.format(ITConstants.CUSTOM_GIT_SERVER_URL_PATTERN, gitServer.getPort());\n        String repoSecret = \"secret_\" + randomString();\n\n\n        SecretOperationResponse sor = generateKeyPair(orgName, repoSecret, false, null);\n        assertEquals(SecretOperationResponse.ResultEnum.CREATED, sor.getResult());\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .url(repoUrl)\n                        .branch(\"master\")\n                        .secretId(sor.getId())\n                )));\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"repo\", repoName);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pe.getStatus());\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLog(\".*Hello Concord!.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/GroovyIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class GroovyIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        String username = \"user_\" + randomString();\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(username)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse cakr = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(username));\n\n        setApiKey(cakr.getKey());\n\n        // ---\n\n        byte[] payload = archive(GroovyIT.class.getResource(\"groovy\").toURI());\n\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hello, \" + username + \".*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/HttpTaskIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder;\nimport com.github.tomakehurst.wiremock.common.FileSource;\nimport com.github.tomakehurst.wiremock.extension.Parameters;\nimport com.github.tomakehurst.wiremock.extension.ResponseDefinitionTransformer;\nimport com.github.tomakehurst.wiremock.http.HttpHeader;\nimport com.github.tomakehurst.wiremock.http.Request;\nimport com.github.tomakehurst.wiremock.http.ResponseDefinition;\nimport com.github.tomakehurst.wiremock.junit5.WireMockExtension;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport java.net.URI;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\nimport static com.github.tomakehurst.wiremock.client.WireMock.*;\nimport static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class HttpTaskIT extends AbstractServerIT {\n\n    private static final String mockHttpBaseUrl;\n    private static final String mockHttpAuthToken = \"Y249dGVzdDpwYXNzd29yZA==\";\n    private static final String mockHttpAuthUser = \"cn=test\";\n    private static final String mockHttpAuthPassword = \"password\";\n    private static final String mockHttpPathPing = \"/api/v1/server/ping\";\n    private static final String mockHttpPathQueryParams = \"/query\";\n    private static final String mockHttpPathToken = \"/token\";\n    private static final String mockHttpPathPassword = \"/password\";\n    private static final String mockHttpPathHeaders = \"/headers\";\n    private static final String mockHttpPathUnauthorized = \"/unauthorized\";\n    private static final String mockHttpPathEmpty = \"/empty\";\n    private static final String mockHttpPathFormUrlEncoded = \"/formUrlEncode\";\n    private static final String mockHttpPathFollowRedirects = \"/followRedirects\";\n\n    private static final String SERVER_URL;\n\n    static {\n        SERVER_URL = \"http://localhost\" + \":\" + env(\"IT_SERVER_PORT\", \"8001\");\n        mockHttpBaseUrl = \"http://\" + env(\"IT_DOCKER_HOST_ADDR\", \"localhost\") + \":\";\n    }\n\n    @RegisterExtension\n    final WireMockExtension rule = WireMockExtension.newInstance()\n            .options(wireMockConfig()\n                    .dynamicPort()\n                    .globalTemplating(true)\n                    .extensions(new RequestHeaders()))\n            .build();\n\n    @BeforeEach\n    public void setup() {\n        stubForGetAsStringEndpoint(mockHttpPathPing);\n        stubForGetWithQueryEndpoint(mockHttpPathQueryParams);\n        stubForGetSecureEndpoint(mockHttpAuthUser, mockHttpAuthPassword, mockHttpPathPassword);\n        stubForPostSecureEndpoint(mockHttpAuthUser, mockHttpAuthPassword, mockHttpPathPassword);\n        stubForGetSecureTokenEndpoint(mockHttpAuthToken, mockHttpPathToken);\n        stubForPostSecureTokenEndpoint(mockHttpAuthToken, mockHttpPathToken);\n        stubForPatchSecureTokenEndpoint(mockHttpAuthToken, mockHttpPathPassword);\n        stubForHeadersEndpoint(mockHttpPathHeaders);\n        stubForUnAuthorizedRequestEndpoint(mockHttpPathUnauthorized);\n        stubForEmptyResponse(mockHttpPathEmpty);\n        stubForFormUrlEncodedEndpoint(mockHttpPathFormUrlEncoded);\n        stubForRedirects(mockHttpPathFollowRedirects);\n    }\n\n    @AfterEach\n    public void tearDown() {\n        rule.shutdownServer();\n    }\n\n    @Test\n    public void testGetAsString() throws Exception {\n        URI dir = HttpTaskIT.class.getResource(\"httpGetAsString\").toURI();\n        byte[] payload = archive(dir);\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.url\", SERVER_URL + mockHttpPathPing);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Success response.*\", ab);\n    }\n\n    @Test\n    public void testGet() throws Exception {\n        URI dir = HttpTaskIT.class.getResource(\"httpGet\").toURI();\n        byte[] payload = archive(dir);\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.url\", SERVER_URL + mockHttpPathPing);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Success response.*\", ab);\n        assertLog(\".*Out Response: true*\", ab);\n    }\n\n    @Test\n    public void testGetAsDefaultMethod() throws Exception {\n        URI dir = HttpTaskIT.class.getResource(\"httpGetAsDefaultMethod\").toURI();\n        byte[] payload = archive(dir);\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.url\", SERVER_URL + mockHttpPathPing);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Request method: GET*\", ab);\n        assertLog(\".*Success response.*\", ab);\n        assertLog(\".*Out Response: true*\", ab);\n    }\n\n    @Test\n    public void testGetWithQueryParams() throws Exception {\n        URI dir = HttpTaskIT.class.getResource(\"httpGetWithQueryParams\").toURI();\n        byte[] payload = archive(dir);\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.url\", mockHttpBaseUrl + rule.getPort() + mockHttpPathQueryParams);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Success response.*\", ab);\n        assertLog(\".*message: hello concord!*\", ab);\n        assertLog(\".*multi-value-1: value1*\", ab);\n        assertLog(\".*multi-value-2: value2*\", ab);\n    }\n\n    @Test\n    public void testGetWithAuthUsingPassword() throws Exception {\n\n        URI dir = HttpTaskIT.class.getResource(\"httpGetWithAuthUsingPassword\").toURI();\n        byte[] payload = archive(dir);\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.user\", mockHttpAuthUser);\n        input.put(\"arguments.password\", mockHttpAuthPassword);\n        input.put(\"arguments.url\", mockHttpBaseUrl + rule.getPort() + mockHttpPathPassword);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Success response.*\", ab);\n        assertLog(\".*Out Response: true*\", ab);\n    }\n\n    @Test\n    public void testGetWithAuthUsingToken() throws Exception {\n        URI dir = HttpTaskIT.class.getResource(\"httpGetWithAuthUsingToken\").toURI();\n        byte[] payload = archive(dir);\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.authToken\", mockHttpAuthToken);\n        input.put(\"arguments.url\", mockHttpBaseUrl + rule.getPort() + mockHttpPathToken);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Success response.*\", ab);\n        assertLog(\".*Out Response: true*\", ab);\n    }\n\n    @Test\n    public void testPost() throws Exception {\n        URI dir = HttpTaskIT.class.getResource(\"httpPost\").toURI();\n        byte[] payload = archive(dir);\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.user\", mockHttpAuthUser);\n        input.put(\"arguments.password\", mockHttpAuthPassword);\n        input.put(\"arguments.url\", mockHttpBaseUrl + rule.getPort() + mockHttpPathPassword);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Success response.*\", ab);\n        assertLog(\".*Out Response: true*\", ab);\n    }\n\n    @Test\n    public void testPostWithArrayBody() throws Exception {\n        URI dir = HttpTaskIT.class.getResource(\"httpPostArray\").toURI();\n        byte[] payload = archive(dir);\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.user\", mockHttpAuthUser);\n        input.put(\"arguments.password\", mockHttpAuthPassword);\n        input.put(\"arguments.url\", mockHttpBaseUrl + rule.getPort() + mockHttpPathPassword);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Success response.*\", ab);\n        assertLog(\".*Out Response: true*\", ab);\n    }\n\n    @Test\n    public void testPostWithDebug() throws Exception {\n        URI dir = HttpTaskIT.class.getResource(\"httpPostWithDebug\").toURI();\n        byte[] payload = archive(dir);\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.user\", mockHttpAuthUser);\n        input.put(\"arguments.password\", mockHttpAuthPassword);\n        input.put(\"arguments.url\", mockHttpBaseUrl + rule.getPort() + mockHttpPathPassword);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*requestInfo.*\", ab);\n        assertLog(\".*responseInfo.*\", ab);\n    }\n\n    @Test\n    public void testPostWithFormUrlEncoded() throws Exception {\n        URI dir = HttpTaskIT.class.getResource(\"httpPostWithFormUrlEncoded\").toURI();\n        byte[] payload = archive(dir);\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.url\", mockHttpBaseUrl + rule.getPort() + mockHttpPathFormUrlEncoded);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Success response.*\", ab);\n        assertLog(\".*Out Response: true*\", ab);\n    }\n\n    @Test\n    public void testPatch() throws Exception {\n        URI dir = HttpTaskIT.class.getResource(\"httpPatch\").toURI();\n        byte[] payload = archive(dir);\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.user\", mockHttpAuthUser);\n        input.put(\"arguments.password\", mockHttpAuthPassword);\n        input.put(\"arguments.url\", mockHttpBaseUrl + rule.getPort() + mockHttpPathPassword);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Success response.*\", ab);\n        assertLog(\".*Out Response: true*\", ab);\n    }\n\n    @Test\n    public void testPostWithAuthUsingToken() throws Exception {\n        URI dir = HttpTaskIT.class.getResource(\"httpPostWithAuthUsingToken\").toURI();\n        byte[] payload = archive(dir);\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.authToken\", mockHttpAuthToken);\n        input.put(\"arguments.url\", mockHttpBaseUrl + rule.getPort() + mockHttpPathToken);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Success response.*\", ab);\n        assertLog(\".*Out Response: true*\", ab);\n    }\n\n    @Test\n    public void testGetWithInvalidUrl() throws Exception {\n        URI dir = HttpTaskIT.class.getResource(\"httpGetWithInvalidUrl\").toURI();\n        byte[] payload = archive(dir);\n\n        StartProcessResponse spr = start(payload);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FAILED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*server not exists.*\", ab);\n    }\n\n    @Test\n    public void testGetWithHeaders() throws Exception {\n\n        URI dir = HttpTaskIT.class.getResource(\"httpGetWithHeaders\").toURI();\n        byte[] payload = archive(dir);\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.url\", mockHttpBaseUrl + rule.getPort() + mockHttpPathHeaders);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Success response.*\", ab);\n        assertLog(\".*Out Response: true*\", ab);\n        assertLog(\".*Response content: request headers:.*h1=v1.*\", ab);\n        assertLog(\".*Response content: request headers:.*h2=v2.*\", ab);\n    }\n\n    @Test\n    public void testGetWithIgnoreErrors() throws Exception {\n        URI dir = HttpTaskIT.class.getResource(\"httpGetWithIgnoreErrors\").toURI();\n        byte[] payload = archive(dir);\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.user\", \"wrongUsername\");\n        input.put(\"arguments.password\", \"wrongPassword\");\n        input.put(\"arguments.url\", mockHttpBaseUrl + rule.getPort() + mockHttpPathUnauthorized);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*statusCode: 401*\", ab);\n        assertLog(\".*Success response: false*\", ab);\n    }\n\n    @Test\n    public void testGetEmptyResponse() throws Exception {\n        URI dir = HttpTaskIT.class.getResource(\"httpGetEmpty\").toURI();\n        byte[] payload = archive(dir);\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.url\", mockHttpBaseUrl + rule.getPort() + mockHttpPathEmpty);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Success response: true.*\", ab);\n        assertLog(\".*Content is NULL: true.*\", ab);\n    }\n\n    @Test\n    public void testFollowRedirects() throws Exception {\n        URI dir = HttpTaskIT.class.getResource(\"httpFollowRedirects\").toURI();\n        byte[] payload = archive(dir);\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.authToken\", mockHttpAuthToken);\n        input.put(\"arguments.url\", mockHttpBaseUrl + rule.getPort() + mockHttpPathFollowRedirects);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Response status code: 302.*\", ab);\n    }\n\n    private void stubForGetAsStringEndpoint(String url) {\n        rule.stubFor(get(urlEqualTo(url))\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withBody(\"{\\n\" +\n                                  \"  \\\"Success\\\": \\\"true\\\"\\n\" +\n                                  \"}\"))\n        );\n    }\n\n    private void stubForGetWithQueryEndpoint(String url) {\n        // Use response templating because of multi-value params limitation in WireMock\n        rule.stubFor(get(urlPathEqualTo(url))\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withBody(\"{\\n\" +\n                                  \" \\\"message\\\": \\\"{{request.requestLine.query.message}}\\\", \" +\n                                  \" \\\"multiValue1\\\": \\\"{{request.requestLine.query.multiValue.[0]}}\\\", \" +\n                                  \" \\\"multiValue2\\\": \\\"{{request.requestLine.query.multiValue.[1]}}\\\"\" +\n                                  \"}\")\n                        .withTransformers(\"response-template\"))\n        );\n\n    }\n\n    private void stubForGetSecureEndpoint(String user, String password, String url) {\n        rule.stubFor(get(urlEqualTo(url))\n                .withBasicAuth(user, password)\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withBody(\"{\\n\" +\n                                  \"  \\\"Authorized\\\": \\\"true\\\"\\n\" +\n                                  \"}\"))\n        );\n    }\n\n    private void stubForGetSecureTokenEndpoint(String authToken, String url) {\n        rule.stubFor(get(urlEqualTo(url))\n                .withHeader(\"Authorization\", equalTo(\"Basic \" + authToken))\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withBody(\"{\\n\" +\n                                  \"  \\\"Authorized\\\": \\\"true\\\"\\n\" +\n                                  \"}\"))\n        );\n    }\n\n    private void stubForPostSecureEndpoint(String user, String password, String url) {\n        rule.stubFor(post(urlEqualTo(url))\n                .withBasicAuth(user, password)\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withBody(\"{\\n\" +\n                                  \"  \\\"Authorized\\\": \\\"true\\\"\\n\" +\n                                  \"}\"))\n        );\n    }\n\n    private void stubForPostSecureTokenEndpoint(String authToken, String url) {\n        rule.stubFor(post(urlEqualTo(url))\n                .withHeader(\"Authorization\", equalTo(\"Basic \" + authToken))\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withBody(\"{\\n\" +\n                                  \"  \\\"Authorized\\\": \\\"true\\\"\\n\" +\n                                  \"}\"))\n        );\n    }\n\n    private void stubForPatchSecureTokenEndpoint(String authToken, String url) {\n        rule.stubFor(patch(urlEqualTo(url))\n                .withHeader(\"Authorization\", equalTo(\"Basic \" + authToken))\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withBody(\"{\\n\" +\n                                  \"  \\\"Authorized\\\": \\\"true\\\"\\n\" +\n                                  \"}\"))\n        );\n    }\n\n    private void stubForHeadersEndpoint(String url) {\n        rule.stubFor(post(urlEqualTo(url))\n                .willReturn(aResponse()\n                        .withTransformers(\"request-headers\"))\n        );\n    }\n\n    private void stubForUnAuthorizedRequestEndpoint(String url) {\n        rule.stubFor(get(urlEqualTo(url))\n                .willReturn(aResponse()\n                        .withStatus(401)\n                        .withBody(\"{\\n\" +\n                                  \"  \\\"Authorized\\\": \\\"false\\\"\\n\" +\n                                  \"}\"))\n        );\n    }\n\n    private void stubForEmptyResponse(String url) {\n        rule.stubFor(get(urlEqualTo(url))\n                .willReturn(aResponse()\n                        .withStatus(204)));\n    }\n\n    private void stubForFormUrlEncodedEndpoint(String url) {\n        rule.stubFor(post(urlEqualTo(url))\n                .withHeader(\"Content-Type\", equalTo(\"application/x-www-form-urlencoded\"))\n                .withRequestBody(containing(\"message=Hello+Concord%21\"))\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withBody(\"{\\n\" +\n                                  \"  \\\"Success\\\": \\\"true\\\"\\n\" +\n                                  \"}\")));\n    }\n\n    private void stubForRedirects(String url) {\n        rule.stubFor(get(urlEqualTo(url))\n                .willReturn(temporaryRedirect(\"/temporaryRedirect\")));\n    }\n\n    public static class RequestHeaders extends ResponseDefinitionTransformer {\n\n        @Override\n        public ResponseDefinition transform(Request request, ResponseDefinition responseDefinition, FileSource files, Parameters parameters) {\n            String headers = \"\";\n            if (request.getHeaders() != null) {\n                headers = request.getHeaders().all().stream()\n                        .map(this::toString)\n                        .collect(Collectors.joining(\", \"));\n            }\n            return new ResponseDefinitionBuilder()\n                    .withStatus(200)\n                    .withHeader(\"Content-Type\", \"text/plain\")\n                    .withBody(\"request headers: \" + headers)\n                    .build();\n        }\n\n        private String toString(HttpHeader h) {\n            String value = \"\";\n            if (h.isPresent()) {\n                value = h.firstValue();\n                if (h.values().size() > 1) {\n                    value = String.join(\",\", h.values());\n                }\n            }\n            return h.key() + \"=\" + value;\n        }\n\n        @Override\n        public String getName() {\n            return \"request-headers\";\n        }\n\n        @Override\n        public boolean applyGlobally() {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ITConstants.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.base.Strings;\n\npublic final class ITConstants {\n\n    public static final String PROJECT_VERSION;\n    public static final String SERVER_URL;\n    public static final String DEPENDENCIES_DIR;\n    public static final String GIT_SERVER_URL_PATTERN;\n    public static final String SMTP_SERVER_HOST;\n    public static final String DOCKER_ANSIBLE_IMAGE;\n    public static final Integer GIT_WEBHOOK_MOCK_PORT;\n    public static final String CUSTOM_GIT_SERVER_URL_PATTERN;\n\n    static {\n        PROJECT_VERSION = env(\"IT_PROJECT_VERSION\", \"LATEST\");\n\n        SERVER_URL = \"http://localhost:\" + env(\"IT_SERVER_PORT\", \"8001\");\n        DEPENDENCIES_DIR = System.getenv(\"IT_DEPS_DIR\");\n\n        String dockerAddr = env(\"IT_DOCKER_HOST_ADDR\", \"127.0.0.1\");\n        String gitHost = dockerAddr != null ? dockerAddr : \"localhost\";\n        GIT_SERVER_URL_PATTERN = \"ssh://git@\" + gitHost + \":%d/\";\n\n        CUSTOM_GIT_SERVER_URL_PATTERN = \"ssh://user@\" + gitHost + \":%d/\";\n\n        SMTP_SERVER_HOST = dockerAddr;\n\n        DOCKER_ANSIBLE_IMAGE = env(\"IT_DOCKER_ANSIBLE_IMAGE\", \"walmartlabs/concord-ansible\");\n\n        GIT_WEBHOOK_MOCK_PORT = Integer.parseInt(env(\"IT_GIT_WEBHOOK_MOCK_PORT\", \"4567\"));\n    }\n\n    private static String env(String k, String def) {\n        String v = System.getenv(k);\n        if (Strings.isNullOrEmpty(v)) {\n            return def;\n        }\n        return v;\n    }\n\n    private ITConstants() {\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/InitiatorIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class InitiatorIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        String username = \"user_\" + randomString();\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(username)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse cakr = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(username));\n\n        setApiKey(cakr.getKey());\n\n        // ---\n\n        byte[] payload = archive(InitiatorIT.class.getResource(\"initiator\").toURI());\n\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hello, \" + username + \".*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/InventoryIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\n\npublic class InventoryIT extends AbstractServerIT {\n\n    @Test\n    public void testQuery() throws Exception {\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + randomString();\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        String inventoryName = \"inventory_\" + randomString();\n        InventoriesApi inventoriesApi = new InventoriesApi(getApiClient());\n        inventoriesApi.createOrUpdateInventory(orgName, new InventoryEntry().name(inventoryName)\n                .orgName(orgName)\n                .visibility(InventoryEntry.VisibilityEnum.PUBLIC));\n\n        String queryName = \"query_\" + randomString();\n        InventoryQueriesApi queriesApi = new InventoryQueriesApi(getApiClient());\n        queriesApi.createOrUpdateInventoryQuery(orgName, inventoryName, queryName, \"select cast(to_json(item_data) as varchar) from inventory_data where item_path like '%/testPath'\");\n\n        InventoryDataApi dataApi = new InventoryDataApi(getApiClient());\n        dataApi.updateInventoryData(orgName, inventoryName, \"/testPath\", \"{\\\"data\\\": \\\"testData\\\"}\");\n\n        // ---\n\n        byte[] payload = archive(ProcessIT.class.getResource(\"inventoryQuery\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"arguments.inventoryName\", inventoryName);\n        input.put(\"arguments.queryName\", queryName);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Inventory Item_data: testData.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/InventoryQueryIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.InputStream;\nimport java.net.URI;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.util.HashMap;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class InventoryQueryIT extends AbstractServerIT {\n\n    @Test\n    public void testQueryWithEmptyParams() throws Exception {\n        InventoryQueriesApi resource = new InventoryQueriesApi(getApiClient());\n\n        String orgName = \"Default\";\n        String inventoryName = \"inventory\" + randomString();\n        String queryName = \"query\" + randomString();\n        String text = \"SELECT CAST(json_build_object('host', item_data->'host', 'ansible_host', item_data->'ip', \" +\n                \"'ooInstanceName', item_data->'ooInstanceName', 'type', item_data->'type', 'profile', \" +\n                \"item_data->'profile', 'zone', item_data->'zone', 'clusterInventoryRef', \" +\n                \"a.item_data->'clusterInventoryRef') AS varchar) \" +\n                \"FROM inventory_data a \" +\n                \"WHERE item_data @> ?::jsonb\";\n\n        InventoriesApi inventoryResource = new InventoriesApi(getApiClient());\n        inventoryResource.createOrUpdateInventory(orgName, new InventoryEntry().name(inventoryName));\n\n        CreateInventoryQueryResponse cqr = resource.createOrUpdateInventoryQuery(orgName, inventoryName, queryName, text);\n        assertTrue(cqr.getOk());\n        assertNotNull(cqr.getId());\n\n        List<Object> resp = resource.executeInventoryQuery(orgName, inventoryName, queryName, new HashMap<>());\n        assertNotNull(resp);\n    }\n\n    @Test\n    public void testDifferentContentTypes() throws Exception {\n        ApiClient client = getApiClient();\n\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n\n        String orgName = \"org_\" + randomString();\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry()\n                .name(orgName));\n\n        InventoriesApi inventoriesApi = new InventoriesApi(getApiClient());\n\n        String inventoryName = \"inventory_\" + randomString();\n        inventoriesApi.createOrUpdateInventory(orgName, new InventoryEntry()\n                .name(inventoryName));\n\n        // ---\n\n        assertQuery(client, orgName, inventoryName, \"q1_\" + randomString(), \"text/plain\");\n        assertQuery(client, orgName, inventoryName, \"q2_\" + randomString(), \"application/json\");\n    }\n\n    private static void assertQuery(ApiClient client, String orgName, String inventoryName, String queryName, String contentType) throws Exception {\n        String data = \"select * from inventory_data\";\n\n        HttpRequest.Builder requestBuilder = client.requestBuilder();\n\n        String localVarPath = \"/api/v1/org/{orgName}/inventory/{inventoryName}/query/{queryName}\"\n                .replace(\"{orgName}\", ApiClient.urlEncode(orgName))\n                .replace(\"{inventoryName}\", ApiClient.urlEncode(inventoryName))\n                .replace(\"{queryName}\", ApiClient.urlEncode(queryName));\n\n        requestBuilder.uri(URI.create(client.getBaseUri() + localVarPath));\n\n        requestBuilder.header(\"Content-Type\", contentType);\n        String acceptHeaderValue = \"application/json\";\n        acceptHeaderValue += \",application/vnd.concord-validation-errors-v1+json\";\n        requestBuilder.header(\"Accept\", acceptHeaderValue);\n        requestBuilder.method(\"POST\", HttpRequest.BodyPublishers.ofString(data));\n\n        HttpResponse<InputStream> response = client.getHttpClient().send(\n                requestBuilder.build(),\n                HttpResponse.BodyHandlers.ofInputStream());\n\n        assertEquals(200, response.statusCode());\n\n        new InventoryQueriesApi(client).createOrUpdateInventoryQuery(orgName, inventoryName, queryName, data);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/JsonStoreIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class JsonStoreIT extends AbstractServerIT {\n\n    @Test\n    public void testValidationJsonStoreRequest() throws Exception {\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        JsonStoreApi api = new JsonStoreApi(getApiClient());\n        try {\n            api.createOrUpdateJsonStore(orgName, new JsonStoreRequest().name(\"<script></script>\"));\n            fail(\"exception expected\");\n        } catch (ApiException e) {\n            assertEquals(400, e.getCode());\n            assertEquals(\"[{\\\"id\\\":\\\"ImmutableJsonStoreRequest.createOrUpdate.arg1.name\\\",\\\"message\\\":\\\"must match \\\\\\\"^[0-9a-zA-Z][0-9a-zA-Z_@.\\\\\\\\-~]{2,127}$\\\\\\\"\\\"}]\", e.getResponseBody());\n        }\n    }\n\n    @Test\n    public void testBulkAccessUpdate() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        String storeName = \"store_\" + randomString();\n\n        JsonStoreApi jsonStoreApi = new JsonStoreApi(getApiClient());\n        jsonStoreApi.createOrUpdateJsonStore(orgName, new JsonStoreRequest()\n                .name(storeName));\n\n        // ---\n\n        String teamName = \"team_\" + randomString();\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n        CreateTeamResponse teamResp = teamsApi.createOrUpdateTeam(orgName, new TeamEntry()\n                .name(teamName));\n\n        // --- Typical one-or-more teams bulk access update\n\n        List<ResourceAccessEntry> teams = new ArrayList<>(1);\n        teams.add(new ResourceAccessEntry()\n                .orgName(orgName)\n                .teamId(teamResp.getId())\n                .teamName(teamName)\n                .level(ResourceAccessEntry.LevelEnum.OWNER));\n        GenericOperationResult addTeamsResult = jsonStoreApi.bulkUpdateJsonStoreAccessLevel(orgName, storeName, teams);\n        assertNotNull(addTeamsResult);\n        assertTrue(addTeamsResult.getOk());\n\n        List<ResourceAccessEntry> currentTeams = jsonStoreApi.getJsonStoreAccessLevel(orgName, storeName);\n        assertNotNull(currentTeams);\n        assertEquals(1, currentTeams.size());\n\n        // --- Empty teams list clears all\n\n        GenericOperationResult clearTeamsResult = jsonStoreApi.bulkUpdateJsonStoreAccessLevel(orgName, storeName, Collections.emptyList());\n        assertNotNull(clearTeamsResult);\n        assertTrue(clearTeamsResult.getOk());\n\n        currentTeams = jsonStoreApi.getJsonStoreAccessLevel(orgName, storeName);\n        assertNotNull(currentTeams);\n        assertEquals(0, currentTeams.size());\n\n        // --- Null list not allowed, throws error\n\n        try {\n            jsonStoreApi.bulkUpdateJsonStoreAccessLevel(orgName, storeName, null);\n        } catch (ApiException expected) {\n            assertEquals(400, expected.getCode());\n            assertTrue(expected.getResponseBody().contains(\"List of teams is null\"));\n        } catch (Exception e) {\n            fail(\"Expected ApiException. Got \" + e.getClass().toString());\n        }\n\n        // ---\n\n        teamsApi.deleteTeam(orgName, teamName);\n        jsonStoreApi.deleteJsonStore(orgName, storeName);\n        orgApi.deleteOrg(orgName, \"yes\");\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/JsonStoreTaskIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\n\npublic class JsonStoreTaskIT extends AbstractServerIT {\n\n    @Test\n    public void testStoreOperations() throws Exception {\n        withOrg(orgName -> {\n            withProject(orgName, projectName -> {\n                byte[] payload = archive(ProcessIT.class.getResource(\"jsonStoreTaskStoreTest\").toURI());\n\n                Map<String, Object> input = new HashMap<>();\n                input.put(\"archive\", payload);\n                input.put(\"org\", orgName);\n                input.put(\"project\", projectName);\n                input.put(\"arguments.storeName\", \"store_\" + randomString());\n                input.put(\"arguments.itemPath\", \"item_\" + randomString());\n\n                StartProcessResponse spr = start(input);\n\n                ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n                // ---\n\n                byte[] ab = getLog(pir.getInstanceId());\n                assertLog(\".*the store doesn't exist.*\", ab);\n                assertLog(\".*the item doesn't exist.*\", ab);\n                assertLog(\".*the store exists now.*\", ab);\n                assertLog(\".*the item exists now.*\", ab);\n                assertLog(\".*item: \\\\{test=123}.*\", ab);\n            });\n        });\n    }\n\n    @Test\n    public void testPutGetData() throws Exception {\n        withOrg(orgName -> {\n            withProject(orgName, projectName -> {\n                withStore(orgName, storeName -> {\n\n                    byte[] payload = archive(ProcessIT.class.getResource(\"jsonStoreTask\").toURI());\n\n                    Map<String, Object> input = new HashMap<>();\n                    input.put(\"archive\", payload);\n                    input.put(\"org\", orgName);\n                    input.put(\"project\", projectName);\n                    input.put(\"arguments.storeName\", storeName);\n\n                    StartProcessResponse spr = start(input);\n\n                    ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n                    // ---\n\n                    byte[] ab = getLog(pir.getInstanceId());\n                    assertLog(\".*empty: $\", ab);\n                    assertLog(\".*get: \\\\{x=1}*\", ab);\n                });\n            });\n        });\n\n    }\n\n    private void withStore(String orgName, Consumer<String> consumer) throws Exception {\n        String storageName = \"storage_\" + randomString();\n        JsonStoreApi storageApi = new JsonStoreApi(getApiClient());\n        try {\n            storageApi.createOrUpdateJsonStore(orgName, new JsonStoreRequest()\n                    .name(storageName)\n                    .visibility(JsonStoreRequest.VisibilityEnum.PUBLIC));\n            consumer.accept(storageName);\n        } finally {\n            storageApi.deleteJsonStore(orgName, storageName);\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/KvPolicyIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.net.URL;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class KvPolicyIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n\n        String policyName = \"policy_\" + randomString();\n\n        PolicyApi policyResource = new PolicyApi(getApiClient());\n        policyResource.createOrUpdatePolicy(new PolicyEntry().name(policyName).rules(readPolicy(\"kvPolicy/test-policy.json\")));\n        policyResource.linkPolicy(policyName, new PolicyLinkEntry().orgName(orgName));\n\n        // ---\n\n        byte[] payload = archive(KvPolicyIT.class.getResource(\"kvPolicy\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"archive\", payload);\n\n\n        StartProcessResponse spr = start(input);\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FAILED, pe.getStatus());\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLog(\".*kv\\\\.putLong\\\\('two', 444\\\\).*Found KV policy violations: Maximum KV entries exceeded: current 1, limit 1.*\", ab);\n        // ---\n\n        policyResource.createOrUpdatePolicy(new PolicyEntry().name(policyName).rules(readPolicy(\"kvPolicy/test-policy-relaxed.json\")));\n\n        // ---\n\n        spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Hello!.*\", ab);\n    }\n\n\n    private Map<String, Object> readPolicy(String file) throws Exception {\n        URL url = KvPolicyIT.class.getResource(file);\n        return fromJson(new File(url.toURI()));\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/KvServiceIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.collect.ImmutableMap;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.ZipUtils;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.FileWriter;\nimport java.io.Writer;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.regex.Pattern;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class KvServiceIT extends AbstractServerIT {\n\n    @Test\n    public void testKv() throws Exception {\n        String testKey = \"key_\" + randomString();\n\n        byte[] ab = test(\"kvInc\", \"main\", testKey);\n        assertLog(\".*x=[0-9]+.*\", ab);\n        assertLog(\".*abc123.*\", ab);\n        assertLog(\".*Hello, world.*\", ab);\n\n        // ---\n\n        ab = test(\"kvInc\", \"verify\", testKey);\n        assertLog(\".*Hello again, world.*\", ab);\n\n        // ---\n\n        ab = test(\"kvInc\", \"verify2\", testKey);\n        assertLog(\".*xyz.*\", ab);\n    }\n\n    @Test\n    public void testKvLong() throws Exception {\n        String testKey = \"key_\" + randomString();\n\n        byte[] ab = test(\"kvInc\", \"testLong\", testKey);\n        assertLog(\".*x=1.*\", ab);\n        assertLog(\".*y=1.*\", ab);\n        assertLog(\".*a=2.*\", ab);\n        assertLog(\".*b=2.*\", ab);\n        assertLog(\".*c=234.*\", ab);\n        assertLog(\".*d=235.*\", ab);\n    }\n\n    @Test\n    public void testKvWithSpecialString() throws Exception {\n        String testKey = \"key_\" + randomString();\n\n        byte[] ab = test(\"kvSpecialString\", \"default\", testKey);\n\n        assertLog(\".*\" + Pattern.quote(\"#aaa#bbb\") + \".*\", ab);\n    }\n\n    @Test\n    public void testInvalidKeys() throws Exception {\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n        String orgName = \"org_\" + randomString();\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry()\n                .name(orgName));\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        String projectName = \"project_\" + randomString();\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.OWNERS));\n\n        // ---\n\n        byte[] payload = archive(KvServiceIT.class.getResource(\"kvInvalidKeys\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FAILED, pe.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLogAtLeast(\".*Keys cannot be empty or null.*\", 1, ab);\n    }\n\n    @Test\n    public void testCallFromScript() throws Exception {\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n        String orgName = \"org_\" + randomString();\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry()\n                .name(orgName));\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        String projectName = \"project_\" + randomString();\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.OWNERS));\n\n        // ---\n\n        byte[] payload = archive(KvServiceIT.class.getResource(\"kvScript\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLog(\".*got myKey: myValue*\", ab);\n    }\n\n    private byte[] test(String process, String entryPoint, String testKey) throws Exception {\n        Map<String, Object> args = ImmutableMap.of(\"testKey\", testKey);\n        byte[] payload = createPayload(process, entryPoint, args);\n\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        return getLog(pir.getInstanceId());\n    }\n\n    private byte[] createPayload(String process, String entryPoint, Map<String, Object> args) throws Exception {\n        Path src = Paths.get(KvServiceIT.class.getResource(process).toURI());\n\n        Path tmpDir = createTempDir();\n        PathUtils.copy(src, tmpDir);\n\n        Map<String, Object> req = ImmutableMap.of(\"entryPoint\", entryPoint,\n                \"arguments\", args);\n\n        Path reqFile = tmpDir.resolve(Constants.Files.CONFIGURATION_FILE_NAME);\n        try (Writer w = new FileWriter(reqFile.toFile())) {\n            getApiClient().getObjectMapper().writeValue(w, req);\n        }\n\n        ByteArrayOutputStream baos = new ByteArrayOutputStream();\n        try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(baos)) {\n            ZipUtils.zip(zip, tmpDir);\n        }\n\n        return baos.toByteArray();\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/LdapIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport javax.naming.Context;\nimport javax.naming.NameAlreadyBoundException;\nimport javax.naming.directory.*;\nimport java.io.InputStream;\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.util.Base64;\nimport java.util.Map;\nimport java.util.Properties;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForStatus;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.junit.jupiter.api.Assumptions.assumeTrue;\n\npublic class LdapIT extends AbstractServerIT {\n\n    private static final String OBJECT_CLASS = \"objectClass\";\n    private static final String COMMON_NAME = \"cn\";\n    private static final String GROUP_OU = \"ou=groups,dc=example,dc=org\";\n    private static final String USER_OU = \"ou=users,dc=example,dc=org\";\n    private static DirContext ldapCtx;\n    private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder().build();\n\n    @BeforeAll\n    public static void createLdapStructure() throws Exception {\n        assumeTrue(System.getenv(\"IT_LDAP_URL\") != null);\n\n        ldapCtx = createContext();\n\n        //create organization units\n        createLdapOrganizationalUnits(GROUP_OU, \"groups\");\n        createLdapOrganizationalUnits(USER_OU, \"users\");\n    }\n\n    @Test\n    public void testLdapUserGroups() throws Exception {\n        // create user in ldap\n        String username = \"tester\";\n        createLdapUser(username);\n\n        // create group\n        String groupName = \"testerGroup\";\n        createLdapGroupWithUser(groupName, username);\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(username)\n                .type(CreateUserRequest.TypeEnum.LDAP));\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse cakr = apiKeyResource.createUserApiKey(new CreateApiKeyRequest()\n                .username(username)\n                .userType(CreateApiKeyRequest.UserTypeEnum.LDAP));\n\n        setApiKey(cakr.getKey());\n\n        // ---\n\n        byte[] payload = archive(LdapIT.class.getResource(\"ldapInitiator\").toURI());\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        String groupDn = \"cn=\" + groupName + \",\" + GROUP_OU;\n        assertLog(\".*\" + groupDn + \".*\", ab);\n    }\n\n    @Test\n    public void testCaseInsensitive() throws Exception {\n        String username = \"testUser_\" + randomString();\n        createLdapUser(username);\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(username)\n                .type(CreateUserRequest.TypeEnum.LDAP));\n\n        UserEntry ue = usersApi.findByUsername(username.toLowerCase());\n        assertNotNull(ue);\n        assertEquals(ue.getName(), username.toLowerCase());\n    }\n\n    @Test\n    void testDisableLdapUser() throws Exception {\n        String username = \"tester_\" + randomString();\n        createLdapUser(username);\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(username)\n                .type(CreateUserRequest.TypeEnum.LDAP));\n\n        UserEntry ue = usersApi.findByUsername(username);\n        assertNotNull(ue);\n        assertFalse(ue.getDisabled());\n        assertFalse(ue.getPermanentlyDisabled());\n\n        ue = usersApi.disableUser(ue.getId(), false);\n        assertNotNull(ue);\n        assertTrue(ue.getDisabled());\n        assertFalse(ue.getPermanentlyDisabled());\n\n        ue = usersApi.disableUser(ue.getId(), true);\n        assertNotNull(ue);\n        assertTrue(ue.getDisabled());\n        assertTrue(ue.getPermanentlyDisabled());\n    }\n\n    @Test\n    void testSubmitFormRunAsGroupWithApiKey() throws Exception {\n        // create users in ldap\n        String noGroupUser = \"noGroupUser\" + randomString();\n        createLdapUser(noGroupUser);\n\n        String username = \"runAsUser\" + randomString();\n        createLdapUser(username);\n\n        // create group\n        String groupName = \"RunAsGroup\" + randomString();\n        createLdapGroupWithUser(groupName, username);\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(username)\n                .type(CreateUserRequest.TypeEnum.LDAP));\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(noGroupUser)\n                .type(CreateUserRequest.TypeEnum.LDAP));\n\n        String noGroupApiKey = createApiKey(noGroupUser);\n        String validUserApiKey = createApiKey(username);\n\n        setApiKey(validUserApiKey);\n\n        // --- execute form\n\n        byte[] payload = archive(LdapIT.class.getResource(\"ldapFormRunAs\").toURI());\n        StartProcessResponse spr = start(Map.of(\n                \"archive\", payload,\n                \"arguments.ldapGroupName\", groupName\n        ));\n        assertNotNull(spr.getInstanceId());\n\n\n        // ---\n\n        ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n\n        // --- try to get with user not in group (expect no permission)\n\n        ApiException noGroupEx = assertThrows(ApiException.class, () ->\n                new ProcessFormsApi(getApiClientForKey(noGroupApiKey)).getProcessForm(pir.getInstanceId(), \"myForm\"));\n\n        assertEquals(403, noGroupEx.getCode());\n        assertTrue(noGroupEx.getMessage().contains(\"doesn't have the necessary permissions to resume process. Expected LDAP group(s) '[CN=RunAsGroup\"));\n\n        // --- get form with user in expected ldap group\n\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClientForKey(validUserApiKey));\n\n        FormInstanceEntry form = formsApi.getProcessForm(pir.getInstanceId(), \"myForm\");\n\n        assertEquals(\"myForm\", form.getName());\n        assertEquals(1, form.getFields().size());\n        assertEquals(\"inputName\", form.getFields().get(0).getName());\n        assertEquals(\"string\", form.getFields().get(0).getType());\n\n        // --- submit form with user in expected ldap group\n\n        formsApi.submitForm(pir.getInstanceId(), \"myForm\", Map.of(\"inputName\", \"testuser\"));\n\n        waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.FINISHED);\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Submitted name: testuser.*\", ab);\n    }\n\n    @Test\n    void testSubmitFormRunAsGroupWithPassword() throws Exception {\n        // create users in ldap\n        String noGroupUser = \"noGroupUser\" + randomString();\n        createLdapUser(noGroupUser);\n\n        String username = \"runAsUser\" + randomString();\n        createLdapUser(username);\n\n        // create group\n        String groupName = \"RunAsGroup\" + randomString();\n        createLdapGroupWithUser(groupName, username);\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(username)\n                .type(CreateUserRequest.TypeEnum.LDAP));\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(noGroupUser)\n                .type(CreateUserRequest.TypeEnum.LDAP));\n\n        String validUserApiKey = createApiKey(username);\n\n        setApiKey(validUserApiKey);\n\n        // --- execute form\n\n        byte[] payload = archive(LdapIT.class.getResource(\"ldapFormRunAs\").toURI());\n        StartProcessResponse spr = start(Map.of(\n                \"archive\", payload,\n                \"arguments.ldapGroupName\", groupName\n        ));\n        assertNotNull(spr.getInstanceId());\n\n        // ---\n\n        ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n\n        // --- try to get with user not in group (expect no permission)\n\n        ApiException noGroupEx = assertThrows(ApiException.class, () ->\n                getFormHttpClient(getApiClient().getBaseUrl(), pir.getInstanceId(), \"myForm\", noGroupUser, noGroupUser));\n\n        assertEquals(403, noGroupEx.getCode());\n        assertTrue(noGroupEx.getResponseBody().contains(\"doesn't have the necessary permissions to resume process. Expected LDAP group(s) '[CN=RunAsGroup\"));\n\n        // --- get form with user in expected ldap group\n\n        FormInstanceEntry form = getFormHttpClient(getApiClient().getBaseUrl(), pir.getInstanceId(), \"myForm\", username, username);\n\n        assertEquals(\"myForm\", form.getName());\n        assertEquals(1, form.getFields().size());\n        assertEquals(\"inputName\", form.getFields().get(0).getName());\n        assertEquals(\"string\", form.getFields().get(0).getType());\n\n        // --- submit form with user in expected ldap group\n\n        submitFormHttpClient(getApiClient().getBaseUrl(), pir.getInstanceId(), \"myForm\", Map.of(\"inputName\", \"testuser\"), username, username);\n\n        waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.FINISHED);\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Submitted name: testuser.*\", ab);\n    }\n\n    public static DirContext createContext() throws Exception {\n        String url = System.getenv(\"IT_LDAP_URL\");\n        String connectionType = \"simple\";\n        String dn = \"cn=admin,dc=example,dc=org\";\n        String credentials = \"admin\";\n\n        Properties environment = new Properties();\n        environment.put(Context.INITIAL_CONTEXT_FACTORY, \"com.sun.jndi.ldap.LdapCtxFactory\");\n        environment.put(Context.PROVIDER_URL, url);\n        environment.put(Context.SECURITY_AUTHENTICATION, connectionType);\n        environment.put(Context.SECURITY_PRINCIPAL, dn);\n        environment.put(Context.SECURITY_CREDENTIALS, credentials);\n\n        return new InitialDirContext(environment);\n    }\n\n    private static void createLdapOrganizationalUnits(String dn, String name) throws Exception {\n        Attributes attributes = new BasicAttributes();\n\n        Attribute ou = new BasicAttribute(\"ou\", name);\n        Attribute objectClass = new BasicAttribute(OBJECT_CLASS, \"organizationalUnit\");\n\n        attributes.put(ou);\n        attributes.put(objectClass);\n\n        try {\n            ldapCtx.createSubcontext(dn, attributes);\n        } catch (NameAlreadyBoundException e) {\n            System.err.println(\"createLdapOrganizationalUnits -> \" + e.getMessage());\n            // already exists, ignore\n        }\n    }\n\n    private static void createLdapUser(String username) throws Exception {\n        String dn = \"uid=\" + username + \",\" + USER_OU;\n        Attributes attributes = new BasicAttributes();\n\n        Attribute uid = new BasicAttribute(\"uid\", username);\n        Attribute cn = new BasicAttribute(COMMON_NAME, username);\n        Attribute sn = new BasicAttribute(\"sn\", username);\n        Attribute userPassword = new BasicAttribute(\"userPassword\", username);\n\n        Attribute objectClass = new BasicAttribute(OBJECT_CLASS);\n        objectClass.add(\"top\");\n        objectClass.add(\"organizationalPerson\");\n        objectClass.add(\"person\");\n        objectClass.add(\"inetOrgPerson\");\n\n        attributes.put(uid);\n        attributes.put(cn);\n        attributes.put(sn);\n        attributes.put(userPassword);\n        attributes.put(objectClass);\n\n        try {\n            ldapCtx.createSubcontext(dn, attributes);\n        } catch (NameAlreadyBoundException e) {\n            System.err.println(\"createLdapUser -> \" + e.getMessage());\n            // already exists, ignore\n        }\n    }\n\n    private static void createLdapGroupWithUser(String groupName, String username) throws Exception {\n        String groupDn = \"cn=\" + groupName + \",\" + GROUP_OU;\n        String userDn = \"uid=\" + username + \",\" + USER_OU;\n        Attributes attributes = new BasicAttributes();\n\n        Attribute cn = new BasicAttribute(COMMON_NAME, groupName);\n        Attribute uniqueMember = new BasicAttribute(\"uniqueMember\", userDn);\n        Attribute objectClass = new BasicAttribute(OBJECT_CLASS, \"groupOfUniqueNames\");\n\n        attributes.put(cn);\n        attributes.put(uniqueMember);\n        attributes.put(objectClass);\n\n        try {\n            ldapCtx.createSubcontext(groupDn, attributes);\n        } catch (NameAlreadyBoundException e) {\n            System.err.println(\"createLdapGroupWithUser -> \" + e.getMessage());\n            // already exists, ignore\n        }\n    }\n\n    private String createApiKey(String username) throws Exception {\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse cakr = apiKeyResource.createUserApiKey(new CreateApiKeyRequest()\n                .username(username)\n                .userType(CreateApiKeyRequest.UserTypeEnum.LDAP));\n\n        return cakr.getKey();\n    }\n\n    private FormInstanceEntry getFormHttpClient(String baseUrl, UUID instanceId, String formName, String username, String password) throws Exception {\n        HttpRequest req = HttpRequest.newBuilder(URI.create(baseUrl + \"/api/v1/process/\" + instanceId + \"/form/\" + formName))\n                .GET()\n                .header(\"Authorization\", \"Basic \" + Base64.getEncoder().encodeToString((username + \":\" + password).getBytes()))\n                .build();\n\n        HttpResponse<InputStream> resp = HTTP_CLIENT.send(req, HttpResponse.BodyHandlers.ofInputStream());\n\n        try (InputStream is = resp.body()) {\n            if (resp.statusCode() != 200) {\n                throw new ApiException(resp.statusCode(), resp.headers(), new String(is.readAllBytes()));\n            }\n\n            return new ObjectMapper().readValue(resp.body(), FormInstanceEntry.class);\n        }\n    }\n\n    private void submitFormHttpClient(String baseUrl, UUID instanceId, String formName, Map<String, Object> data, String username, String password) throws Exception {\n        ObjectMapper mapper = new ObjectMapper();\n        String requestBody = mapper.writeValueAsString(data);\n\n        HttpRequest req = HttpRequest.newBuilder(URI.create(baseUrl + \"/api/v1/process/\" + instanceId + \"/form/\" + formName))\n                .POST(HttpRequest.BodyPublishers.ofString(requestBody))\n                .header(\"Content-Type\", \"application/json\")\n                .header(\"Authorization\", \"Basic \" + Base64.getEncoder().encodeToString((username + \":\" + password).getBytes()))\n                .build();\n\n        HttpResponse<InputStream> resp = HTTP_CLIENT.send(req, HttpResponse.BodyHandlers.ofInputStream());\n\n        try (InputStream is = resp.body()) {\n            if (resp.statusCode() != 200) {\n                throw new ApiException(resp.statusCode(), resp.headers(), new String(resp.body().readAllBytes()));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/MavenRepoIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.PathUtils;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardCopyOption;\nimport java.util.Collections;\n\nimport static com.walmartlabs.concord.client2.ProjectEntry.RawPayloadModeEnum.DISABLED;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class MavenRepoIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        // prepare local mvn repo\n        Path localMavenRepo;\n        try {\n            localMavenRepo = Path.of(System.getProperty(\"local.mvn.repo\"));\n        } catch (NullPointerException e) {\n            localMavenRepo = Path.of(System.getProperty(\"user.home\")).resolve(\".m2/repository\");\n        }\n        Path mvnDirectory = localMavenRepo.resolve(\"com/walmartlabs/concord/mvn-concord/0.0.1\");\n\n        Path src = Paths.get(MavenRepoIT.class.getResource(\"mvnRepoFiles\").toURI());\n        PathUtils.copy(src, mvnDirectory, StandardCopyOption.REPLACE_EXISTING);\n\n        String url = \"mvn://com.walmartlabs.concord:mvn-concord:zip\";\n        String projectName = \"project_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(\"Default\", new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(DISABLED)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .name(repoName)\n                        .url(url)\n                        .branch(\"0.0.1\"))));\n\n        // ---\n\n        StartProcessResponse spr = start(\"Default\", projectName, repoName, null, null);\n        assertTrue(spr.getOk());\n\n        // ---\n\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pe.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLog(\"^.*\\\\[INFO \\\\] OK$\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/MultipleProjectFilesIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessApi;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.ZipUtils;\nimport org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.attribute.PosixFilePermission;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Set;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\n\npublic class MultipleProjectFilesIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        Path template = zip(Paths.get(MultipleProjectFilesIT.class.getResource(\"multiProjectTemplate/template\").toURI()));\n        String templateUrl = \"file://\" + template.toAbsolutePath();\n\n        // ---\n\n        byte[] payload = archive(MultipleProjectFilesIT.class.getResource(\"multiProjectTemplate/user\").toURI());\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"template\", templateUrl);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hello, Concord!.*\", ab);\n    }\n\n    private static Path zip(Path src) throws IOException {\n        Path dst = PathUtils.createTempFile(\"template\", \".zip\");\n        try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(Files.newOutputStream(dst))) {\n            ZipUtils.zip(zip, src);\n        }\n\n        Set<PosixFilePermission> s = new HashSet<>();\n        s.add(PosixFilePermission.OWNER_READ);\n        s.add(PosixFilePermission.GROUP_READ);\n        s.add(PosixFilePermission.OTHERS_READ);\n        Files.setPosixFilePermissions(dst, s);\n\n        return dst;\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/NodeRosterIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.github.tomakehurst.wiremock.junit5.WireMockExtension;\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static com.github.tomakehurst.wiremock.client.WireMock.*;\nimport static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class NodeRosterIT extends AbstractServerIT {\n\n    @RegisterExtension\n    static WireMockExtension rule = WireMockExtension.newInstance()\n            .options(wireMockConfig()\n                    .dynamicPort())\n            .build();\n\n    @BeforeEach\n    public void setUp() {\n        rule.stubFor(get(urlEqualTo(\"/test.txt\"))\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withBody(\"Hello!\")));\n    }\n\n    @Test\n    public void testE2e() throws Exception {\n        String hostA = \"hostA_\" + randomString();\n        String hostB = \"hostB_\" + randomString();\n\n        // run an Ansible playbook to get some events\n\n        byte[] payload = archive(NodeRosterIT.class.getResource(\"nodeRoster\").toURI());\n\n        String artifactUrl = \"http://\" + env(\"IT_DOCKER_HOST_ADDR\", \"localhost\") + \":\" + rule.getPort() + \"/test.txt\";\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.artifactUrl\", artifactUrl);\n        input.put(\"arguments.hostA\", hostA);\n        input.put(\"arguments.hostB\", hostB);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*\" + hostA + \".*failed=0.*\", ab);\n        assertLog(\".*\" + hostB + \".*failed=0.*\", ab);\n\n        // wait for events to be processes and check for known hosts\n\n        NodeRosterHostsApi hostsApi = new NodeRosterHostsApi(getApiClient());\n\n        UUID hostAId = findHost(hostA, hostsApi);\n        UUID hostBId = findHost(hostB, hostsApi);\n\n        // check if the artifact was deployed to our hosts\n\n        NodeRosterArtifactsApi artifactsApi = new NodeRosterArtifactsApi(getApiClient());\n        while (true) {\n            List<ArtifactEntry> artifactHostsA = artifactsApi.listHostArtifacts(hostAId, null, artifactUrl, 1000, 0); // TODO might require paging\n            if (artifactHostsA != null && artifactHostsA.size() == 1) {\n                break;\n            }\n        }\n\n        // check if we know who deployed to our hosts\n\n        List<ProcessEntry> hostAProcesses = listHostProcesses(hostAId);\n        assertEquals(\"admin\", hostAProcesses.get(0).getInitiator());\n\n        List<com.walmartlabs.concord.client2.ProcessEntry> hostBProcesses = listHostProcesses(hostBId);\n        assertEquals(\"admin\", hostBProcesses.get(0).getInitiator());\n\n        // check the host facts\n        NodeRosterFactsApi factsApi = new NodeRosterFactsApi(getApiClient());\n        assertNotNull(factsApi.getFacts(hostAId, null));\n        assertNotNull(factsApi.getFacts(hostBId, null));\n\n        // let's test the task\n\n        payload = archive(ProcessIT.class.getResource(\"nodeRosterTask\").toURI());\n\n        input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.artifactUrl\", artifactUrl);\n        spr = start(input);\n\n        pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        ab = getLog(pir.getInstanceId());\n        assertLog(\".*ok=true.*\", ab);\n        assertLog(\".*name: \" + hostA.toLowerCase() + \".*\", ab);\n        assertLog(\".*name: \" + hostB.toLowerCase() + \".*\", ab);\n    }\n\n    @Test\n    public void testMultipleFactsPerHost() throws Exception {\n        byte[] payload = archive(ProcessIT.class.getResource(\"nodeRosterMultiFacts\").toURI());\n\n        String host = \"host_\" + randomString();\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.host\", host);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*\" + host + \".*failed=0.*\", ab);\n\n        // ---\n\n        NodeRosterHostsApi hostsApi = new NodeRosterHostsApi(getApiClient());\n\n        UUID hostId = findHost(host, hostsApi);\n        assertNotNull(getFacts(hostId));\n    }\n\n    private static UUID findHost(String host, NodeRosterHostsApi hostsApi) throws InterruptedException, ApiException {\n        while (true) {\n            List<HostEntry> l = hostsApi.listKnownHosts(host, null, null, null, 10, 0);\n            HostEntry e = l.stream().filter(h -> h.getName().equalsIgnoreCase(host)).findFirst().orElse(null);\n\n            if (e != null) {\n                return e.getId();\n            }\n\n            Thread.sleep(1000);\n        }\n    }\n\n    private List<com.walmartlabs.concord.client2.ProcessEntry> listHostProcesses(UUID hostAId) throws Exception {\n        NodeRosterProcessesApi nrProcessApi = new NodeRosterProcessesApi(getApiClient());\n        while (true) {\n            List<com.walmartlabs.concord.client2.ProcessEntry> result = nrProcessApi.listHosts(hostAId, null, 1000, 0);\n            if (!result.isEmpty()) {\n                return result;\n            }\n\n            Thread.sleep(1000);\n        }\n    }\n\n    private Object getFacts(UUID hostId) throws Exception {\n        NodeRosterFactsApi factsApi = new NodeRosterFactsApi(getApiClient());\n        while (true) {\n            Object facts = factsApi.getFacts(hostId, null);\n            if (facts != null) {\n                return facts;\n            }\n            \n            Thread.sleep(1000);\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/OneOpsTriggerIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.client2.OrganizationsApi;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.file.Path;\nimport java.util.Map;\n\npublic class OneOpsTriggerIT extends AbstractOneOpsTriggerIT {\n\n    private OrganizationsApi orgApi;\n    private String orgName;\n    private String projectName;\n    private String repoName;\n\n    @BeforeEach\n    public void setup() throws Exception {\n        orgApi = new OrganizationsApi(getApiClient());\n        orgName = \"org_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        Path repo = initRepo(\"oneopsTests/trigger\");\n        projectName = \"project_\" + randomString();\n        repoName = \"repo_\" + randomString();\n        initProjectAndRepo(orgName, projectName, repoName, repo);\n        refreshRepo(orgName, projectName, repoName);\n    }\n\n    @AfterEach\n    public void tearDown() throws ApiException {\n        orgApi.deleteOrg(orgName, \"yes\");\n    }\n\n    @Test\n    public void testOneOpsTriggerv1() throws Exception {\n        waitForTriggers(orgName, projectName, repoName, 2);\n\n        sendOneOpsEvent(\"oneopsTests/events/oneops_deployment_complete.json\");\n\n        Map<ProcessEntry.StatusEnum, ProcessEntry> ps = waitProcesses(orgName, projectName, ProcessEntry.StatusEnum.FINISHED);\n        assertProcessLog(ps.get(ProcessEntry.StatusEnum.FINISHED), \".*Oneops has completed a deployment trigger version 1*\");\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/OneOpsTriggerITV2.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.client2.OrganizationsApi;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.file.Path;\nimport java.util.Map;\n\npublic class OneOpsTriggerITV2 extends AbstractOneOpsTriggerIT {\n\n    private OrganizationsApi orgApi;\n    private String orgName;\n    private String projectName;\n    private String repoName;\n\n    @BeforeEach\n    public void setup() throws Exception {\n        orgApi = new OrganizationsApi(getApiClient());\n        orgName = \"org_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        Path repo = initRepo(\"oneopsTests/trigger\");\n        projectName = \"project_\" + randomString();\n        repoName = \"repo_\" + randomString();\n        initProjectAndRepo(orgName, projectName, repoName, repo);\n        refreshRepo(orgName, projectName, repoName);\n\n    }\n\n    @AfterEach\n    public void tearDown() throws ApiException {\n        orgApi.deleteOrg(orgName, \"yes\");\n    }\n\n    @Test\n    public void testOneOpsTriggerv2() throws Exception {\n        sendOneOpsEvent(\"oneopsTests/events/oneops_deployment_qa.json\");\n\n        waitForTriggers(orgName, projectName, repoName, 2);\n\n        Map<ProcessEntry.StatusEnum, ProcessEntry> ps = waitProcesses(orgName, projectName, ProcessEntry.StatusEnum.FINISHED);\n        assertProcessLog(ps.get(ProcessEntry.StatusEnum.FINISHED), \".*Oneops has completed a deployment trigger version 2*\");\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/OutVariablesIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessApi;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.InputStream;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class OutVariablesIT extends AbstractServerIT {\n\n    @Test\n    public void testRequestParamAndPredefined() throws Exception {\n        byte[] payload = archive(OutVariablesIT.class.getResource(\"out\").toURI());\n        String[] out = {\"x\", \"y.some.boolean\", \"z\"};\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"out\", out);\n        StartProcessResponse spr = start(input);\n\n        Map<String, Object> data = getOutVars(spr.getInstanceId());\n        assertNotNull(data);\n\n        assertEquals(123, data.get(\"x\"));\n        assertEquals(true, data.get(\"y.some.boolean\"));\n        assertFalse(data.containsKey(\"z\"));\n    }\n\n    @Test\n    public void testPredefined() throws Exception {\n        byte[] payload = archive(OutVariablesIT.class.getResource(\"out\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"request\", Collections.singletonMap(\"activeProfiles\", Arrays.asList(\"predefinedOut\")));\n        input.put(\"sync\", false);\n        StartProcessResponse spr = start(input);\n\n        Map<String, Object> data = getOutVars(spr.getInstanceId());\n        assertNotNull(data);\n\n        assertEquals(123, data.get(\"x\"));\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private Map<String, Object> getOutVars(UUID instanceId) throws Exception {\n        waitForCompletion(getApiClient(), instanceId);\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        try (InputStream outJson = processApi.downloadAttachment(instanceId, \"out.json\")) {\n            return getApiClient().getObjectMapper().readValue(outJson, Map.class);\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/OutVariablesProjectIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.client2.ProjectEntry.RawPayloadModeEnum.EVERYONE;\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class OutVariablesProjectIT extends AbstractServerIT {\n\n    @Test\n    public void testOutVars() throws Exception {\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n\n        String orgName = \"Default\";\n        String projectName = \"project_\" + System.currentTimeMillis();\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .rawPayloadMode(EVERYONE)\n                .outVariablesMode(ProjectEntry.OutVariablesModeEnum.EVERYONE)\n                .name(projectName));\n\n        // ---\n\n        byte[] payload = archive(OutVariablesProjectIT.class.getResource(\"example\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"archive\", payload);\n        input.put(\"out\", \"myName\");\n        Map<String, Object> cfg = new HashMap<>();\n        cfg.put(\"out\", Collections.singletonList(\"myBool\"));\n        input.put(\"request\", cfg);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        Map<String, Object> out = (Map<String, Object>) pe.getMeta().get(\"out\");\n        assertEquals(\"world\", out.get(\"myName\"));\n        assertEquals(true, out.get(\"myBool\"));\n    }\n\n    @Test\n    public void testReject() throws Exception {\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n\n        String orgName = \"Default\";\n        String projectName = \"project_\" + System.currentTimeMillis();\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .rawPayloadMode(EVERYONE)\n                .name(projectName));\n\n        // ---\n\n        byte[] payload = archive(OutVariablesProjectIT.class.getResource(\"example\").toURI());\n\n        try {\n            Map<String, Object> input = new HashMap<>();\n            input.put(\"org\", orgName);\n            input.put(\"project\", projectName);\n            input.put(\"archive\", payload);\n            input.put(\"out\", \"myName,myBool\");\n            start(input);\n            fail(\"should fail\");\n        } catch (ApiException e) {\n            assertTrue(e.getMessage().contains(\"The project is not accepting custom out variables\"));\n        }\n    }\n\n    @Test\n    public void testRejectFromRequest() throws Exception {\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n\n        String orgName = \"Default\";\n        String projectName = \"project_\" + System.currentTimeMillis();\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .rawPayloadMode(EVERYONE)\n                .name(projectName));\n\n        // ---\n        byte[] payload = archive(OutVariablesProjectIT.class.getResource(\"example\").toURI());\n        try {\n            Map<String, Object> input = new HashMap<>();\n            input.put(\"org\", orgName);\n            input.put(\"project\", projectName);\n            input.put(\"archive\", payload);\n            Map<String, Object> cfg = new HashMap<>();\n            cfg.put(\"out\", Collections.singletonList(\"myName\"));\n            input.put(\"request\", cfg);\n            start(input);\n\n            StartProcessResponse spr = start(input);\n            ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n            assertEquals(ProcessEntry.StatusEnum.FAILED, pe.getStatus());\n            byte[] ab = getLog(pe.getInstanceId());\n            assertLog(\".*The project is not accepting custom out variables.*\", ab);\n\n        } catch (ApiException e) {\n            assertTrue(e.getMessage().contains(\"The project is not accepting custom out variables\"));\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/PermissionIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.*;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class PermissionIT extends AbstractServerIT {\n\n    @Test\n    public void testCreateOrgPermission() throws Exception {\n        String userName = \"user_\" + randomString();\n        String roleName = \"role_\" + randomString();\n        String orgNameA = \"org_\" + randomString();\n        String orgNameB = \"org_\" + randomString();\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        RolesApi rolesApi = new RolesApi(getApiClient());\n\n        // -- Create role with permission to create orgs\n\n        Set<String> permissions = new HashSet<>();\n        permissions.add(\"createOrg\");\n\n        RoleOperationResponse rop = rolesApi.createOrUpdateRole(new RoleEntry()\n                .name(roleName)\n                .permissions(permissions));\n\n        // -- Create user with the role\n\n        Set<String> roles = new HashSet<>();\n        roles.add(roleName);\n\n        CreateUserResponse cur = usersApi.createOrUpdateUser(new CreateUserRequest()\n                .type(CreateUserRequest.TypeEnum.LOCAL)\n                .username(userName)\n                .roles(roles));\n\n\n        // -- Switch to new user's api key\n        ApiKeysApi apiKeysApi = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse apiKeyA = apiKeysApi.createUserApiKey(new CreateApiKeyRequest().username(userName));\n\n        setApiKey(apiKeyA.getKey());\n\n\n        // -- Create the org\n\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n        CreateOrganizationResponse cor1 = organizationsApi.createOrUpdateOrg(new OrganizationEntry().name(orgNameA));\n\n        assertTrue(cor1.getOk());\n\n\n        // -- Remove role (and permission) from user\n\n        resetApiKey();\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .type(CreateUserRequest.TypeEnum.LOCAL)\n                .username(userName)\n                .roles(Collections.emptySet())\n        );\n\n\n        // -- Org creation must be denied\n\n        setApiKey(apiKeyA.getKey());\n        try {\n            organizationsApi.createOrUpdateOrg(new OrganizationEntry().name(orgNameB));\n            fail(\"users without creatOrg permissions must not be allowed to create orgs.\");\n        } catch (Exception e) {\n            assertTrue(e.getMessage().contains(\"'createOrg' permission\"));\n        }\n    }\n\n    @Test\n    public void testUpdateOrgPermission() throws Exception {\n        String userNameA = \"userA_\" + randomString();\n        String userNameB = \"userB_\" + randomString();\n        String roleName = \"role_\" + randomString();\n        String orgName = \"org_\" + randomString();\n\n\n        // -- Create role with permission to update orgs\n\n        RolesApi rolesApi = new RolesApi(getApiClient());\n        RoleOperationResponse role = rolesApi.createOrUpdateRole(new RoleEntry()\n                .name(roleName)\n                .permissions(Collections.singleton(\"updateOrg\")));\n\n        // -- Create user with the role\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        CreateUserResponse userA = usersApi.createOrUpdateUser(new CreateUserRequest()\n                .type(CreateUserRequest.TypeEnum.LOCAL)\n                .username(userNameA)\n                .roles(Collections.singleton(roleName)));\n\n        // -- Create the org\n\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n        CreateOrganizationResponse cor1 = organizationsApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        assertTrue(cor1.getOk());\n        assertEquals(\"admin\", organizationsApi.getOrg(orgName).getOwner().getUsername());\n\n        // -- Switch to new user's api key\n        ApiKeysApi apiKeysApi = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse userAKey = apiKeysApi.createUserApiKey(new CreateApiKeyRequest().username(userNameA));\n\n        setApiKey(userAKey.getKey());\n\n        // update org owner\n\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry()\n                .name(orgName)\n                .owner(new EntityOwner().id(userA.getId())));\n\n        assertEquals(userA.getId(), organizationsApi.getOrg(orgName).getOwner().getId());\n\n        // -- Remove role (and permission) from user\n\n        resetApiKey();\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .type(CreateUserRequest.TypeEnum.LOCAL)\n                .username(userNameA)\n                .roles(Collections.emptySet())\n        );\n\n        // new user - userB\n\n        CreateUserResponse userB = usersApi.createOrUpdateUser(new CreateUserRequest()\n                .type(CreateUserRequest.TypeEnum.LOCAL)\n                .username(userNameB));\n\n        // change owner: userA -> userB\n\n        setApiKey(userAKey.getKey());\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry()\n                .name(orgName)\n                .owner(new EntityOwner().id(userB.getId())));\n\n        assertEquals(userB.getId(), organizationsApi.getOrg(orgName).getOwner().getId());\n\n        // -- Org update must be denied\n\n        try {\n            organizationsApi.createOrUpdateOrg(new OrganizationEntry().name(orgName).visibility(OrganizationEntry.VisibilityEnum.PRIVATE));\n            fail(\"users without updateOrg permissions must not be allowed to create orgs.\");\n        } catch (Exception e) {\n            assertTrue(e.getMessage().contains(\"'updateOrg' permission\"));\n        }\n\n        // cleanup\n        resetApiKey();\n        organizationsApi.deleteOrg(orgName, \"yes\");\n        usersApi.deleteUser(userA.getId());\n        usersApi.deleteUser(userB.getId());\n        rolesApi.deleteRole(roleName);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/PolicyIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.client2.ProcessEntry.StatusEnum;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static java.util.Collections.singletonMap;\nimport static org.junit.jupiter.api.Assertions.assertDoesNotThrow;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class PolicyIT extends AbstractServerIT {\n\n    @Test\n    public void testCfg() throws Exception {\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + randomString();\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        String policyName = \"policy_\" + randomString();\n        PolicyApi policyApi = new PolicyApi(getApiClient());\n        policyApi.createOrUpdatePolicy(new PolicyEntry()\n                .name(policyName)\n                .rules(singletonMap(\"processCfg\",\n                        singletonMap(\"arguments\",\n                                singletonMap(\"x\",\n                                        singletonMap(\"name\", \"Concord\"))))));\n\n        policyApi.linkPolicy(policyName, new PolicyLinkEntry()\n                .orgName(orgName)\n                .projectName(projectName));\n\n        // ---\n\n        byte[] payload = archive(ProcessIT.class.getResource(\"policyCfg\").toURI());\n\n        StartProcessResponse spr = start(payload);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hello, Stranger.*\", ab);\n\n        // ---\n\n        spr = start(orgName, projectName, null, null, payload);\n\n        pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hello, Concord.*\", ab);\n    }\n\n    @Test\n    public void testMaxForkDepth() throws Exception {\n        String orgName = createOrg();\n        String projectName = createProject(orgName);\n\n        Map<String, Object> queueRules = new HashMap<>();\n        queueRules.put(\"forkDepth\", singletonMap(\"max\", 2));\n\n        Map<String, Object> rules = singletonMap(\"queue\", queueRules);\n\n        createPolicy(orgName, projectName, rules);\n\n        // ---\n        byte[] payload = archive(ProcessIT.class.getResource(\"forkDepth\").toURI());\n\n        StartProcessResponse spr = start(orgName, projectName, null, null, payload);\n\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.RUNNING);\n\n        waitForCompletion(getApiClient(), spr.getInstanceId());\n    }\n\n    @Test\n    public void testConcurrentProcess() throws Exception {\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + randomString();\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        String policyName = \"policy_\" + randomString();\n        PolicyApi policyApi = new PolicyApi(getApiClient());\n        Map<String, Object> queueRules = new HashMap<>();\n        queueRules.put(\"concurrent\", singletonMap(\"max\", 1));\n\n        Map<String, Object> rules = singletonMap(\"queue\", queueRules);\n        policyApi.createOrUpdatePolicy(new PolicyEntry()\n                .name(policyName)\n                .rules(rules));\n\n        policyApi.linkPolicy(policyName, new PolicyLinkEntry()\n                .orgName(orgName)\n                .projectName(projectName));\n\n        // ---\n\n        byte[] payload = archive(ProcessIT.class.getResource(\"withDelay\").toURI());\n\n        Map<String, Object> input1 = new HashMap<>();\n        input1.put(\"archive\", payload);\n        input1.put(\"org\", orgName);\n        input1.put(\"project\", projectName);\n        input1.put(\"request\", singletonMap(\"arguments\", singletonMap(\"delayValue\", 10000)));\n\n        StartProcessResponse spr1 = start(input1);\n\n        waitForStatus(getApiClient(), spr1.getInstanceId(), StatusEnum.RUNNING);\n\n        // --- start second process\n\n        Map<String, Object> input2 = new HashMap<>(input1);\n        input2.put(\"request\", singletonMap(\"arguments\", singletonMap(\"delayValue\", 0)));\n\n        StartProcessResponse spr2 = start(input2);\n\n        // ---\n\n        waitForCompletion(getApiClient(), spr1.getInstanceId());\n\n        // ---\n\n        waitForCompletion(getApiClient(), spr2.getInstanceId());\n    }\n\n    @Test\n    public void testConcurrentWithSuspendedProcess() throws Exception {\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + randomString();\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        String policyName = \"policy_\" + randomString();\n        PolicyApi policyApi = new PolicyApi(getApiClient());\n        Map<String, Object> queueRules = new HashMap<>();\n        queueRules.put(\"concurrent\", singletonMap(\"max\", 1));\n\n        Map<String, Object> rules = singletonMap(\"queue\", queueRules);\n        policyApi.createOrUpdatePolicy(new PolicyEntry()\n                .name(policyName)\n                .rules(rules));\n\n        policyApi.linkPolicy(policyName, new PolicyLinkEntry()\n                .orgName(orgName)\n                .projectName(projectName));\n\n        // ---\n\n        byte[] payload = archive(ProcessIT.class.getResource(\"withForm\").toURI());\n\n        Map<String, Object> input1 = new HashMap<>();\n        input1.put(\"archive\", payload);\n        input1.put(\"org\", orgName);\n        input1.put(\"project\", projectName);\n        input1.put(\"request\", singletonMap(\"arguments\", singletonMap(\"delayValue\", 10000)));\n\n        StartProcessResponse spr1 = start(input1);\n\n        waitForStatus(getApiClient(), spr1.getInstanceId(), StatusEnum.RUNNING);\n\n        // --- start the second process\n\n        byte[] payload2 = archive(ProcessIT.class.getResource(\"withDelay\").toURI());\n        Map<String, Object> input2 = new HashMap<>();\n        input2.put(\"archive\", payload2);\n        input2.put(\"org\", orgName);\n        input2.put(\"project\", projectName);\n        input2.put(\"request\", singletonMap(\"arguments\", singletonMap(\"delayValue\", 0)));\n\n        StartProcessResponse spr2 = start(input2);\n\n        // ---\n\n        waitForCompletion(getApiClient(), spr2.getInstanceId());\n    }\n\n    @Test\n    public void testMaxProcessTimeout() throws Exception {\n        String orgName = createOrg();\n        String projectName = createProject(orgName);\n\n        Map<String, Object> queueRules = new HashMap<>();\n        queueRules.put(\"processTimeout\", singletonMap(\"max\", \"PT1M\"));\n\n        Map<String, Object> rules = singletonMap(\"queue\", queueRules);\n\n        createPolicy(orgName, projectName, rules);\n\n        // ---\n        byte[] payload = archive(ProcessIT.class.getResource(\"process\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"request\", singletonMap(\"processTimeout\", \"PT10M\"));\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(StatusEnum.FAILED, pe.getStatus());\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLog(\".*Maximum processTimeout value exceeded.*\", ab);\n    }\n\n    @Test\n    public void testEffectiveYamlMaxSizeAllowed() throws Exception {\n        long maxSize = 1024 * 4; // 4KB\n        ProcessEntry pir = startEffectiveYamlProcess(Map.of(\"maxSizeInBytes\", maxSize));\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        byte[] contents = assertDoesNotThrow(\n                () -> getEffectiveYaml(processApi, pir.getInstanceId()));\n\n        assertTrue(contents.length < maxSize);\n    }\n\n    @Test\n    public void testEffectiveYamlMaxSizeTooLarge() throws Exception {\n        ProcessEntry pir = startEffectiveYamlProcess(Map.of(\"maxSizeInBytes\", 512));\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        ApiException ex = assertThrows(ApiException.class,\n                () -> getEffectiveYaml(processApi, pir.getInstanceId()));\n\n        assertEquals(404, ex.getCode());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Effective concord.yml is too large to persist \\\\(\\\\d+ bytes\\\\), skipping.*\", 1, ab);\n    }\n\n    @Test\n    public void testEffectiveYamlMaxSizeTooLargeNoLogging() throws Exception {\n        ProcessEntry pir = startEffectiveYamlProcess(Map.of(\"maxSizeInBytes\", 512, \"logWarning\", false));\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        ApiException ex = assertThrows(ApiException.class,\n                () -> getEffectiveYaml(processApi, pir.getInstanceId()));\n\n        assertEquals(404, ex.getCode());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Effective concord.yml is too large to persist\\\\(\\\\d+ bytes\\\\), skipping.*\", 0, ab);\n    }\n\n    @Test\n    public void testEffectiveYamlDisabled() throws Exception {\n        ProcessEntry pir = startEffectiveYamlProcess(Map.of(\"renderEffectiveYaml\", false));\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        ApiException ex = assertThrows(ApiException.class,\n                () -> getEffectiveYaml(processApi, pir.getInstanceId()));\n\n        assertEquals(404, ex.getCode());\n    }\n\n    private String createOrg() throws ApiException {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        return orgName;\n    }\n\n    private String createProject(String orgName) throws ApiException {\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        return projectName;\n    }\n\n    private String createPolicy(String orgName, String projectName, Map<String, Object> rules) throws ApiException {\n        String policyName = \"policy_\" + randomString();\n\n        PolicyApi policyApi = new PolicyApi(getApiClient());\n        policyApi.createOrUpdatePolicy(new PolicyEntry()\n                .name(policyName)\n                .rules(rules));\n\n        policyApi.linkPolicy(policyName, new PolicyLinkEntry()\n                .orgName(orgName)\n                .projectName(projectName));\n\n        return policyName;\n    }\n\n    private ProcessEntry startEffectiveYamlProcess(Map<String, Object> rule) throws Exception {\n        String orgName = createOrg();\n        String projectName = createProject(orgName);\n\n        Map<String, Object> rules = Map.of(\"effectiveYaml\", rule);\n\n        createPolicy(orgName, projectName, rules);\n\n        // ---\n\n        byte[] payload = archive(ProcessIT.class.getResource(\"effectiveYaml\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(StatusEnum.FINISHED, pir.getStatus());\n\n        return pir;\n    }\n\n    private byte[] getEffectiveYaml(ProcessApi processApi, UUID instanceId) throws ApiException, IOException {\n        try (var response = processApi.downloadStateFile(instanceId, \".concord/effective.concord.yml\")) {\n            return response.readAllBytes();\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/PortalIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.it.common.GitUtils;\nimport com.walmartlabs.concord.it.common.MockGitSshServer;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.List;\n\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static java.util.Collections.singletonMap;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class PortalIT extends AbstractServerIT {\n\n    private MockGitSshServer gitServer;\n    private int gitPort;\n\n    @BeforeEach\n    public void setUp() throws Exception {\n        Path data = Paths.get(PortalIT.class.getResource(\"portal\").toURI());\n        Path repo = GitUtils.createBareRepository(data);\n\n        gitServer = new MockGitSshServer(0, repo);\n        gitServer.start();\n\n        gitPort = gitServer.getPort();\n    }\n\n    @AfterEach\n    public void tearDown() throws Exception {\n        gitServer.stop();\n    }\n\n    @Test\n    public void test() throws Exception {\n        String orgName = \"Default\";\n\n        // ---\n\n        String projectName = \"project@\" + randomString();\n        String repoSecretName = \"repoSecret@\" + randomString();\n        String repoName = \"repo@\" + randomString();\n        String repoUrl = String.format(ITConstants.GIT_SERVER_URL_PATTERN, gitPort);\n\n        // ---\n\n        SecretOperationResponse response = generateKeyPair(orgName, repoSecretName, false, null);\n\n        // ---\n\n        RepositoryEntry repo = new RepositoryEntry()\n                .name(repoName)\n                .url(repoUrl)\n                .branch(\"master\")\n                .secretId(response.getId());\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        ProjectOperationResponse por = projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .repositories(singletonMap(repoName, repo)));\n\n        // ---\n\n        ProjectProcessesApi portalService = new ProjectProcessesApi(getApiClient());\n        portalService.startProjectProcess(orgName, projectName, repoName, \"main\", null, null, \"test1,test2\");\n\n        // ---\n\n        ProcessV2Api processApi = new ProcessV2Api(getApiClient());\n        List<ProcessEntry> l = processApi.listProcesses(ProcessListFilter.builder()\n                .projectId(por.getId())\n                .limit(10)\n                .build());\n        assertEquals(1, l.size());\n\n        // ---\n\n        ProcessEntry pe = l.get(0);\n        pe = waitForCompletion(getApiClient(), pe.getInstanceId());\n\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pe.getStatus());\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/PrincipalPermissionIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class PrincipalPermissionIT extends AbstractServerIT {\n\n\n    @Test\n    public void testPrincipalPermission() throws Exception {\n\n        byte[] payload = archive(PrincipalPermissionIT.class.getResource(\"principalPermission\").toURI());\n\n        InventoryQueriesApi resource = new InventoryQueriesApi(getApiClient());\n\n        String orgName = \"Default\";\n        String inventoryName = \"inventory\" + randomString();\n        String queryName = \"query\" + randomString();\n        String text = \"select item_path from inventory_data where item_path like '%/testPath'\";\n\n        InventoriesApi inventoryResource = new InventoriesApi(getApiClient());\n        inventoryResource.createOrUpdateInventory(orgName, new InventoryEntry().name(inventoryName));\n\n        CreateInventoryQueryResponse cqr = resource.createOrUpdateInventoryQuery(orgName, inventoryName, queryName, text);\n        assertTrue(cqr.getOk());\n        assertNotNull(cqr.getId());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.inventoryName\", inventoryName);\n        input.put(\"arguments.orgName\", orgName);\n        input.put(\"arguments.queryName\", queryName);\n        StartProcessResponse spr = start(input);\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        List<ProcessEntry> processEntryList = processApi.listSubprocesses(spr.getInstanceId(), null);\n        for (ProcessEntry pe : processEntryList) {\n            assertEquals(ProcessEntry.StatusEnum.FINISHED, pe.getStatus());\n        }\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Done!.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ProcessCardIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiException;\nimport com.walmartlabs.concord.client2.ProcessCardAccessEntry;\nimport com.walmartlabs.concord.client2.ProcessCardEntry;\nimport com.walmartlabs.concord.client2.ProcessCardsApi;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.InputStream;\nimport java.nio.charset.StandardCharsets;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ProcessCardIT extends AbstractServerIT {\n\n    @Test\n    public void testInvalidRequests() throws Exception {\n        ProcessCardsApi api = new ProcessCardsApi(getApiClient());\n\n        // no cards\n        List<ProcessCardEntry> cards = api.listUserProcessCards();\n        assertNotNull(cards);\n        assertTrue(cards.isEmpty());\n\n        // unknown card id -> 404 for form\n        try {\n            api.getProcessCardForm(UUID.randomUUID());\n            fail(\"exception expected\");\n        } catch (ApiException e) {\n            assertEquals(404, e.getCode());\n        }\n\n        // unknown card id -> 404 for form data\n        try {\n            api.getProcessCardFormData(UUID.randomUUID());\n            fail(\"exception expected\");\n        } catch (ApiException e) {\n            assertEquals(404, e.getCode());\n        }\n\n        // unknown card id -> 404 for delete card\n        try {\n            api.deleteProcessCard(UUID.randomUUID());\n            fail(\"exception expected\");\n        } catch (ApiException e) {\n            assertEquals(404, e.getCode());\n        }\n    }\n\n    @Test\n    public void testCRUD() throws Exception {\n        ProcessCardsApi api = new ProcessCardsApi(getApiClient());\n\n        withOrg(orgName -> {\n            withProject(orgName, projectName -> {\n                var r = new ProcessCardsApi.CreateOrUpdateProcessCardRequest()\n                        .org(orgName)\n                        .project(projectName)\n                        .name(\"test\")\n                        .description(\"test description\")\n                        .data(Map.of(\"myKey\", \"myValue\"))\n                        .form(\"hello world\")\n                        .icon(\"test icon\");\n\n                // create card\n                UUID cardId = api.createOrUpdateProcessCard(r.asMap()).getId();\n\n                ProcessCardEntry entry = api.getProcessCard(cardId);\n                assertEquals(orgName, entry.getOrgName());\n                assertEquals(projectName, entry.getProjectName());\n                assertEquals(\"test\", entry.getName());\n                assertEquals(\"test description\", entry.getDescription());\n\n                // -- list cards\n                List<ProcessCardEntry> cards = api.listUserProcessCards();\n                assertEquals(1, cards.size());\n\n                try (InputStream is = api.getProcessCardForm(cardId)) {\n                    assertEquals(\"hello world\", new String(is.readAllBytes(), StandardCharsets.UTF_8));\n                }\n\n                try (InputStream is = api.getProcessCardFormData(cardId)) {\n                    String data = new String(is.readAllBytes(), StandardCharsets.UTF_8);\n                    assertTrue(data.startsWith(\"data = \"), data);\n                    assertTrue(data.contains(\"myKey\"), data);\n                    assertTrue(data.contains(\"myValue\"), data);\n                }\n\n                // update card\n                r = new ProcessCardsApi.CreateOrUpdateProcessCardRequest()\n                        .id(cardId)\n                        .org(orgName)\n                        .project(projectName)\n                        .name(\"test update\")\n                        .description(\"test description update\");\n\n                UUID cardIdAfterUpdate = api.createOrUpdateProcessCard(r.asMap()).getId();\n                assertEquals(cardId, cardIdAfterUpdate);\n\n                entry = api.getProcessCard(cardId);\n                assertEquals(orgName, entry.getOrgName());\n                assertEquals(projectName, entry.getProjectName());\n                assertEquals(\"test update\", entry.getName());\n                assertEquals(\"test description update\", entry.getDescription());\n\n                // delete card\n                api.deleteProcessCard(cardId);\n            });\n        });\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ProcessContainerIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport com.walmartlabs.concord.common.PathUtils;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n@Disabled(\"Requires configuration changes for ITs\")\npublic class ProcessContainerIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        Path src = Paths.get(ProcessContainerIT.class.getResource(\"processContainer\").toURI());\n        Path dst = PathUtils.createTempDir(\"test\");\n        PathUtils.copy(src, dst);\n\n        Path concordYml = dst.resolve(\"concord.yml\");\n        String s = new String(Files.readAllBytes(concordYml));\n        s = s.replaceAll(\"%%image%%\", ITConstants.DOCKER_ANSIBLE_IMAGE);\n        Files.write(concordYml, s.getBytes());\n\n        // ---\n\n        byte[] payload = archive(dst.toUri());\n\n        StartProcessResponse spr = start(payload);\n\n        // --\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertNotNull(pir.getLogFileName());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hello from Docker!.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ProcessCountIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.client2.ProcessListFilter;\nimport com.walmartlabs.concord.common.PathUtils;\nimport org.eclipse.jgit.api.Git;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class ProcessCountIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        Path tmpDir = createTempDir();\n\n        File src = new File(TriggersRefreshIT.class.getResource(\"processCount\").toURI());\n        PathUtils.copy(src.toPath(), tmpDir);\n\n        try (Git repo = Git.init().setInitialBranch(\"master\").setDirectory(tmpDir.toFile()).call()) {\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"import\").call();\n        }\n\n        String gitUrl = tmpDir.toAbsolutePath().toString();\n\n        // ---\n\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n\n        System.out.println(\">>>\" + orgName + \"/\" + projectName);\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .branch(\"master\")\n                        .url(gitUrl))));\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"repo\", repoName);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pe.getStatus());\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLog(\".*Hello!.*\", ab);\n\n        // ---\n\n        ProcessV2Api processV2Api = new ProcessV2Api(getApiClient());\n        ProcessListFilter filter = ProcessListFilter.builder()\n                .orgName(orgName)\n                .projectName(projectName)\n                .repoName(repoName)\n                .build();\n\n        List<ProcessEntry> l = processV2Api.listProcesses(filter);\n        assertEquals(1, l.size());\n        assertEquals(pe.getInstanceId(), l.get(0).getInstanceId());\n\n        filter = ProcessListFilter.builder()\n                .orgName(orgName)\n                .projectName(projectName)\n                .repoName(repoName + randomString())\n                .build();\n\n        // specifying an invalid repository name should return a 404 response\n        try {\n            processV2Api.listProcesses(filter);\n        } catch (ApiException e) {\n            assertEquals(404, e.getCode());\n        }\n\n        // ---\n\n        int i = processV2Api.countProcesses(null, orgName, null, projectName, null, repoName, null, null, null, null, null, null);\n        assertEquals(1, i);\n\n        // specifying an invalid repository name should return a 404 response\n        try {\n            processV2Api.countProcesses(null, orgName, null, projectName, null, repoName + randomString(), null, null, null, null, null, null);\n        } catch (ApiException e) {\n            assertEquals(404, e.getCode());\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ProcessEventsIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ProcessEventsIT extends AbstractServerIT {\n\n    @Test\n    public void testIncludeAllPermissions() throws Exception {\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n\n        String orgName = \"org_\" + randomString();\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry()\n                .name(orgName));\n\n        // ---\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n\n        String projectName = \"project_\" + randomString();\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.OWNERS));\n\n        // ---\n\n        byte[] payload = archive(ProcessEventsIT.class.getResource(\"runnerEvents\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pe.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLog(\".*Hello!.*\", ab);\n\n        // ---\n\n        ProcessEventsApi eventsApi = new ProcessEventsApi(getApiClient());\n\n        List<ProcessEventEntry> events = eventsApi.listProcessEvents(pe.getInstanceId(), \"ELEMENT\", null, null, null, \"pre\", true, 10);\n        assertEquals(1, events.size());\n\n        ProcessEventEntry ev = events.get(0);\n        assertInParameter(ev, \"msg\", \"Hello!\");\n\n        // ---\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n\n        String userName = \"user_\" + randomString();\n        CreateUserResponse cur = usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeysApi = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse car = apiKeysApi.createUserApiKey(new CreateApiKeyRequest()\n                .userId(cur.getId()));\n\n        // ---\n\n        setApiKey(car.getKey());\n\n        // ---\n\n        try {\n            eventsApi.listProcessEvents(pe.getInstanceId(), \"ELEMENT\", null, null, null, \"pre\", true, 10);\n            fail(\"should fail\");\n        } catch (ApiException e) {\n            assertEquals(403, e.getCode());\n        }\n\n        // ---\n\n        resetApiKey();\n\n        // ---\n\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n\n        String teamName = \"team_\" + randomString();\n        CreateTeamResponse ctr = teamsApi.createOrUpdateTeam(orgName, new TeamEntry()\n                .name(teamName));\n\n        teamsApi.addUsersToTeam(orgName, teamName, false, Collections.singletonList(new TeamUserEntry()\n                .userId(cur.getId())\n                .role(TeamUserEntry.RoleEnum.MEMBER)));\n\n        projectsApi.updateProjectAccessLevel(orgName, projectName, new ResourceAccessEntry()\n                .teamId(ctr.getId())\n                .level(ResourceAccessEntry.LevelEnum.WRITER));\n\n        // ---\n\n        setApiKey(car.getKey());\n\n        events = eventsApi.listProcessEvents(pe.getInstanceId(), \"ELEMENT\", null, null, null, \"pre\", true, 10);\n        assertEquals(1, events.size());\n\n        ev = events.get(0);\n        assertInParameter(ev, \"msg\", \"Hello!\");\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static void assertInParameter(ProcessEventEntry ev, String target, String resolvedValue) {\n        Map<String, Object> data = ev.getData();\n        assertTrue(data.containsKey(\"in\"));\n\n        List<Map<String, Object>> in = (List<Map<String, Object>>) data.get(\"in\");\n        for (Map<String, Object> var : in) {\n            String t = (String) var.get(\"target\");\n            String r = (String) var.get(\"resolved\");\n            if (target.equals(t) && resolvedValue.equals(r)) {\n                return;\n            }\n        }\n\n        fail(\"Can't find \" + target + \" -> '\" + resolvedValue + \"' in the event's in parameters\");\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ProcessExecModeIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.client2.CreateUserRequest.TypeEnum.LOCAL;\nimport static com.walmartlabs.concord.client2.ProjectEntry.ProcessExecModeEnum.*;\nimport static com.walmartlabs.concord.client2.ProjectEntry.RawPayloadModeEnum.EVERYONE;\nimport static com.walmartlabs.concord.client2.ProjectEntry.VisibilityEnum.PRIVATE;\nimport static com.walmartlabs.concord.client2.ProjectEntry.VisibilityEnum.PUBLIC;\nimport static com.walmartlabs.concord.client2.ResourceAccessEntry.LevelEnum.READER;\nimport static com.walmartlabs.concord.client2.ResourceAccessEntry.LevelEnum.WRITER;\nimport static com.walmartlabs.concord.client2.TeamUserEntry.RoleEnum.MEMBER;\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.fail;\n\npublic class ProcessExecModeIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        var projectsApi = new ProjectsApi(getApiClient());\n        var usersApi = new UsersApi(getApiClient());\n        var apiKeyResource = new ApiKeysApi(getApiClient());\n        var teamsApi = new TeamsApi(getApiClient());\n\n        // ---\n\n        var orgName = \"Default\";\n\n        // create userA\n\n        var userAName = \"userA_\" + randomString();\n        usersApi.createOrUpdateUser(new CreateUserRequest().username(userAName).type(LOCAL));\n        var apiKeyA = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userAName));\n\n        // create userB\n\n        var userBName = \"userB_\" + randomString();\n        usersApi.createOrUpdateUser(new CreateUserRequest().username(userBName).type(LOCAL));\n        var apiKeyB = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userBName));\n\n        // create a new team and add userB\n\n        var teamName = \"team_\" + randomString();\n        var team = teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamName));\n        teamsApi.addUsersToTeam(orgName, teamName, false, List.of(new TeamUserEntry().username(userBName).role(MEMBER)));\n\n        // create the project and assign the team to it\n\n        var projectName = \"project_\" + randomString();\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(PUBLIC)\n                .rawPayloadMode(EVERYONE));\n\n        projectsApi.updateProjectAccessLevel(orgName, projectName, new ResourceAccessEntry()\n                .teamId(team.getId())\n                .orgName(orgName)\n                .teamName(teamName)\n                .level(READER));\n\n        // check the project defaults\n\n        var projectEntry = projectsApi.getProject(orgName, projectName);\n        assertEquals(READERS, projectEntry.getProcessExecMode());\n\n        // switch to userA, no extra permissions or teams assigned\n        // the project is public, the user should be able to execute a process within the context of the project\n\n        setApiKey(apiKeyA.getKey());\n        execProcess(orgName, projectName);\n\n        // switch to userB\n        // the project is still public, the user B should be able to execute a process too\n\n        setApiKey(apiKeyB.getKey());\n        execProcess(orgName, projectName);\n\n        // switch back to admin and make the project private\n\n        resetApiKey();\n        projectsApi.createOrUpdateProject(orgName, projectEntry.visibility(PRIVATE));\n\n        // switch to userA\n        // the project is private now, userA should NOT be able to execute any process within the project\n\n        setApiKey(apiKeyA.getKey());\n        try {\n            execProcess(orgName, projectName);\n            fail(\"exec attempt must be rejected\");\n        } catch (ApiException e) {\n        }\n\n        // switch to userB\n        // the project is still private, but userB is in the team that has READER privileges and thus should be able to execute a process\n\n        setApiKey(apiKeyB.getKey());\n        execProcess(orgName, projectName);\n\n        // switch back to admin and update the project and set the exec mode to WRITERs only\n\n        resetApiKey();\n        projectsApi.createOrUpdateProject(orgName, projectEntry.visibility(PRIVATE).processExecMode(WRITERS));\n\n        // switch to userB\n        // the project now requires WRITER privileges to execute processes, the attempt must fail\n\n        setApiKey(apiKeyB.getKey());\n        try {\n            execProcess(orgName, projectName);\n            fail(\"exec attempt must be rejected\");\n        } catch (ApiException e) {\n        }\n\n        // switch back to admin and update the team's privileges\n\n        resetApiKey();\n        projectsApi.updateProjectAccessLevel(orgName, projectName, new ResourceAccessEntry()\n                .teamId(team.getId())\n                .orgName(orgName)\n                .teamName(teamName)\n                .level(WRITER));\n\n        // switch to userB\n        // the userB's team now has the necessary privileges and should be able to execute a process\n\n        setApiKey(apiKeyB.getKey());\n        execProcess(orgName, projectName);\n\n        // switch back to admin and disable process execution altogether\n\n        projectsApi.createOrUpdateProject(orgName, projectEntry.visibility(PRIVATE).processExecMode(DISABLED));\n\n        // switch to userB and try starting a process\n\n        setApiKey(apiKeyB.getKey());\n        try {\n            execProcess(orgName, projectName);\n            fail(\"exec attempt must be rejected\");\n        } catch (ApiException e) {\n        }\n    }\n\n    private void execProcess(String orgName, String projectName) throws Exception {\n        byte[] payload = archive(ProcessExecModeIT.class.getResource(\"processModeExec\").toURI());\n        start(Map.of(\"org\", orgName,\n                \"project\", projectName,\n                \"archive\", payload));\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ProcessIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLFactory;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.client2.ProcessEntry.StatusEnum;\nimport com.walmartlabs.concord.client2.ProcessListFilter;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.common.PathUtils;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ProcessIT extends AbstractServerIT {\n\n    @Test\n    @Disabled\n    public void testLotsOfProcesses() throws Exception {\n        byte[] payload = archive(ProcessIT.class.getResource(\"example\").toURI());\n\n        int count = 100;\n        for (int i = 0; i < count; i++) {\n            start(payload);\n        }\n    }\n\n    @Test\n    public void testTimeout() throws Exception {\n        byte[] payload = archive(ProcessIT.class.getResource(\"timeout\").toURI());\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n\n        try {\n            processApi.waitForCompletion(spr.getInstanceId(), 3000L);\n            fail(\"should fail\");\n        } catch (ApiException e) {\n            String s = e.getResponseBody();\n            ProcessEntry pir = getApiClient().getObjectMapper().readValue(s, ProcessEntry.class);\n            assertTrue(StatusEnum.RUNNING.equals(pir.getStatus())\n                    || StatusEnum.ENQUEUED.equals(pir.getStatus())\n                    || StatusEnum.PREPARING.equals(pir.getStatus())\n                    || StatusEnum.STARTING.equals(pir.getStatus()),\n                    \"Unexpected status: \" + pir.getStatus());\n        }\n\n        processApi.kill(spr.getInstanceId());\n\n        waitForStatus(getApiClient(), spr.getInstanceId(), StatusEnum.CANCELLED, StatusEnum.FAILED, StatusEnum.FINISHED);\n    }\n\n    @Test\n    public void testTaskOut() throws Exception {\n        byte[] payload = archive(ProcessIT.class.getResource(\"taskOut\").toURI());\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*I said: Hello!.*\", ab);\n    }\n\n    @Test\n    public void testDelegateOut() throws Exception {\n        byte[] payload = archive(ProcessIT.class.getResource(\"delegateOut\").toURI());\n\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*I said: Hello!.*\", ab);\n    }\n\n    @Test\n    public void testProjectId() throws Exception {\n        String orgName = \"Default\";\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        ProjectOperationResponse por1 = projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n\n        byte[] payload = archive(ProcessIT.class.getResource(\"example\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"archive\", payload);\n        StartProcessResponse sprA = start(input);\n        waitForCompletion(getApiClient(), sprA.getInstanceId());\n\n        // ---\n\n        StartProcessResponse sprB = start(payload);\n        waitForCompletion(getApiClient(), sprB.getInstanceId());\n\n        // ---\n\n        String anotherProjectName = \"another_\" + randomString();\n        ProjectOperationResponse por2 = projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(anotherProjectName));\n\n        ProcessV2Api processV2Api = new ProcessV2Api(getApiClient());\n        ProcessListFilter filter = ProcessListFilter.builder()\n                .projectId(por2.getId())\n                .limit(30)\n                .build();\n\n        List<ProcessEntry> l = processV2Api.listProcesses(filter);\n        assertTrue(l.isEmpty());\n\n        filter = ProcessListFilter.builder()\n                .projectId(por1.getId())\n                .limit(30)\n                .build();\n\n        l = processV2Api.listProcesses(filter);\n        assertEquals(1, l.size());\n\n        filter = ProcessListFilter.builder()\n                .limit(30)\n                .build();\n\n        l = processV2Api.listProcesses(filter);\n        ProcessEntry p = null;\n        for (ProcessEntry e : l) {\n            if (e.getInstanceId().equals(sprB.getInstanceId())) {\n                p = e;\n                break;\n            }\n        }\n        assertNotNull(p);\n    }\n\n    @Test\n    public void testGetAllProcessesForChildIds() throws Exception {\n        // create a new org\n\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // add the user A\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n\n        String userAName = \"userA_\" + randomString();\n        usersApi.createOrUpdateUser(new CreateUserRequest().username(userAName).type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse apiKeyA = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userAName));\n\n        // create the user A's team\n\n        String teamName = \"team_\" + randomString();\n\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n        CreateTeamResponse ctr = teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamName));\n\n        teamsApi.addUsersToTeam(orgName, teamName, false, Collections.singletonList(new TeamUserEntry()\n                .username(userAName)\n                .role(TeamUserEntry.RoleEnum.MEMBER)));\n\n        // switch to the user A and create a new private project\n\n        setApiKey(apiKeyA.getKey());\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        ProjectOperationResponse por = projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PRIVATE)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // grant the team access to the project\n\n        projectsApi.updateProjectAccessLevel(orgName, projectName, new ResourceAccessEntry()\n                .teamId(ctr.getId())\n                .orgName(orgName)\n                .teamName(teamName)\n                .level(ResourceAccessEntry.LevelEnum.READER));\n\n        //Start a process with zero child\n\n        byte[] payload = archive(ProcessIT.class.getResource(\"process\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n\n        StartProcessResponse singleNodeProcess = start(input);\n        waitForCompletion(getApiClient(), singleNodeProcess.getInstanceId());\n\n        // Start a process with children\n\n        payload = archive(ProcessIT.class.getResource(\"processWithChildren\").toURI());\n        input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n\n        StartProcessResponse parentSpr = start(input);\n        waitForCompletion(getApiClient(), parentSpr.getInstanceId());\n\n        // ---\n        ProcessListFilter filter = ProcessListFilter.builder()\n                .projectId(por.getId())\n                .limit(10)\n                .addInclude(ProcessDataInclude.CHILDREN_IDS)\n                .build();\n        List<ProcessEntry> processEntry = new ProcessV2Api(getApiClient()).listProcesses(filter);\n        for (ProcessEntry pe : processEntry) {\n            if (pe.getInstanceId().equals(singleNodeProcess.getInstanceId())) {\n                assertTrue(pe.getChildrenIds() == null || pe.getChildrenIds().isEmpty());\n            } else if (pe.getInstanceId().equals(parentSpr.getInstanceId())) {\n                assertEquals(3, pe.getChildrenIds() != null ? pe.getChildrenIds().size() : 0);\n            }\n        }\n    }\n\n    @Test\n    public void testForkInitiatorFromApiKey() throws Exception {\n        // add the user A\n        UsersApi usersApi = new UsersApi(getApiClient());\n\n        String userAName = \"userA_\" + randomString();\n        usersApi.createOrUpdateUser(new CreateUserRequest().username(userAName).type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse apiKeyA = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userAName));\n\n        String yaml = new String(Files.readAllBytes(Paths.get(ProcessIT.class.getResource(\"forkInitiator\").toURI()).resolve(\"concord.yml\")));\n        yaml = yaml.replace(\"{{apiKey}}\", apiKeyA.getKey());\n\n        Path tmp = Files.createTempDirectory(\"concord-it-fork\");\n        Files.write(tmp.resolve(\"concord.yml\"), yaml.getBytes());\n\n        byte[] payload = archive(tmp.toUri());\n        PathUtils.deleteRecursively(tmp);\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n\n        StartProcessResponse parentSpr = start(input);\n\n        ProcessEntry processEntry = waitForCompletion(getApiClient(), parentSpr.getInstanceId());\n        assertEquals(StatusEnum.FINISHED, processEntry.getStatus(), () -> {\n            try {\n                return new String(getLog(processEntry.getInstanceId()));\n            } catch (ApiException e) {\n                throw new RuntimeException(e);\n            }\n        });\n        assertEquals(1, processEntry.getChildrenIds().size());\n\n        ProcessEntry child = waitForCompletion(getApiClient(), processEntry.getChildrenIds().iterator().next());\n        assertEquals(StatusEnum.FINISHED, child.getStatus());\n\n        {\n            byte[] ab = getLog(processEntry.getInstanceId());\n            assertLog(\".*initiator: .*admin.*\", ab);\n        }\n        {\n            byte[] ab = getLog(child.getInstanceId());\n            assertLog(\".*initiator: .*\" + userAName.toLowerCase() + \".*\", ab);\n        }\n    }\n\n    @Test\n    @SuppressWarnings(\"unchecked\")\n    public void testEffectiveYaml() throws Exception {\n        byte[] payload = archive(ProcessIT.class.getResource(\"effectiveYaml\").toURI());\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(\"test\", payload);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(StatusEnum.FINISHED, pir.getStatus());\n\n        try (InputStream effectiveYaml = processApi.downloadStateFile(pir.getInstanceId(), \".concord/effective.concord.yml\")) {\n            assertNotNull(effectiveYaml);\n\n            Map<String, Object> m = new ObjectMapper(new YAMLFactory()).readValue(effectiveYaml, Map.class);\n            String entryPoint = (String) ConfigurationUtils.get(m, \"configuration\", \"entryPoint\");\n            assertEquals(\"test\", entryPoint);\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ProcessLocksIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.client2.ProcessEntry.StatusEnum;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForStatus;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class ProcessLocksIT extends AbstractServerIT {\n\n    @Test\n    public void testOrgScope() throws Exception {\n        String orgName = \"org_\" + randomString();\n        String projectName = \"project_\" + randomString();\n\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry().name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n\n        byte[] payload = archive(ProcessLocksIT.class.getResource(\"processLocks\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"archive\", payload);\n\n        StartProcessResponse sprA = start(input);\n\n        ProcessEntry pirA = waitForStatus(getApiClient(), sprA.getInstanceId(), StatusEnum.FAILED, StatusEnum.RUNNING);\n        assertEquals(StatusEnum.RUNNING, pirA.getStatus());\n        waitForLog(pirA.getInstanceId(), \".*locked!.*\");\n\n        // ---\n\n        input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"archive\", payload);\n\n        StartProcessResponse sprB = start(input);\n\n        ProcessEntry pirB = waitForStatus(getApiClient(), sprB.getInstanceId(), StatusEnum.FAILED, StatusEnum.SUSPENDED);\n        assertEquals(StatusEnum.SUSPENDED, pirB.getStatus());\n\n        // ---\n\n        pirA = waitForStatus(getApiClient(), sprA.getInstanceId(), StatusEnum.FAILED, StatusEnum.FINISHED);\n        assertEquals(StatusEnum.FINISHED, pirA.getStatus());\n\n        pirB = waitForStatus(getApiClient(), sprB.getInstanceId(), StatusEnum.FAILED, StatusEnum.FINISHED);\n        assertEquals(StatusEnum.FINISHED, pirB.getStatus());\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ProcessMetadataIT.java",
    "content": "package com.walmartlabs.concord.it.server;//package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.client2.ProcessListFilter;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class ProcessMetadataIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        String orgName = \"Default\";\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n        // ---\n\n        String randomStringA = \"randomA_\" + randomString();\n        StartProcessResponse sprA = start(orgName, projectName, randomStringA);\n\n        String randomStringB = \"randomB_\" + randomString();\n        StartProcessResponse sprB = start(orgName, projectName, randomStringB);\n\n        // ---\n\n        waitForCompletion(getApiClient(), sprA.getInstanceId());\n        waitForCompletion(getApiClient(), sprB.getInstanceId());\n\n        // ---\n\n        List<ProcessEntry> l = list(orgName, projectName, \"meta.x\", \"123\");\n        assertEquals(2, l.size());\n\n        l = list(orgName, projectName, \"meta.x.startsWith\", \"123\");\n        assertEquals(2, l.size());\n\n        l = list(orgName, projectName, \"meta.y.endsWith\", \"abc\");\n        assertEquals(2, l.size());\n\n        l = list(orgName, projectName, \"meta.z.startsWith\", \"randomA\");\n        assertEquals(1, l.size());\n        assertEquals(sprA.getInstanceId(), l.get(0).getInstanceId());\n\n        l = list(orgName, projectName, \"meta.z.startsWith\", \"randomB\");\n        assertEquals(1, l.size());\n        assertEquals(sprB.getInstanceId(), l.get(0).getInstanceId());\n\n        l = list(orgName, projectName, \"meta.z.startsWith\", \"random\");\n        assertEquals(2, l.size());\n\n        l = list(orgName, projectName, \"meta.z.eq\", randomStringA);\n        assertEquals(1, l.size());\n        assertEquals(sprA.getInstanceId(), l.get(0).getInstanceId());\n\n        l = list(orgName, projectName, \"meta.z.eq\", randomStringB);\n        assertEquals(1, l.size());\n        assertEquals(sprB.getInstanceId(), l.get(0).getInstanceId());\n\n        l = list(orgName, projectName, \"meta.z.notEq\", randomStringA);\n        assertEquals(1, l.size());\n        assertEquals(sprB.getInstanceId(), l.get(0).getInstanceId());\n    }\n\n    private StartProcessResponse start(String orgName, String projectName, String var) throws Exception {\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", archive(ProcessMetadataIT.class.getResource(\"processMetadata\").toURI()));\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"arguments.myVar\", var);\n        return start(input);\n    }\n\n    private List<ProcessEntry> list(String orgName, String projectName, String... kv) throws Exception {\n        Map<String, String> meta = new HashMap<>();\n\n        for (int i = 0; i < kv.length - 1; i += 2) {\n            meta.put(kv[i], kv[i + 1]);\n        }\n\n        ProcessListFilter filter = ProcessListFilter.builder()\n                .orgName(orgName)\n                .projectName(projectName)\n                .meta(meta)\n                .build();\n\n        return new ProcessV2Api(getApiClient()).listProcesses(filter);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ProcessRbacIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForStatus;\nimport static org.junit.jupiter.api.Assertions.fail;\n\npublic class ProcessRbacIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        // create a new org\n\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // add the user A\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n\n        String userAName = \"userA_\" + randomString();\n        usersApi.createOrUpdateUser(new CreateUserRequest().username(userAName).type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse apiKeyA = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userAName));\n\n        // create the user A's team\n\n        String teamName = \"team_\" + randomString();\n\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n        CreateTeamResponse ctr = teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamName));\n\n        teamsApi.addUsersToTeam(orgName, teamName, false, Collections.singletonList(new TeamUserEntry()\n                .username(userAName)\n                .role(TeamUserEntry.RoleEnum.MEMBER)));\n\n        // switch to the user A and create a new private project\n\n        setApiKey(apiKeyA.getKey());\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PRIVATE)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // grant the team access to the project\n\n        projectsApi.updateProjectAccessLevel(orgName, projectName, new ResourceAccessEntry()\n                .teamId(ctr.getId())\n                .orgName(orgName)\n                .teamName(teamName)\n                .level(ResourceAccessEntry.LevelEnum.READER));\n\n        // start a new process using the project as the user A\n\n        byte[] payload = archive(ProcessRbacIT.class.getResource(\"processRbac\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n\n        StartProcessResponse spr = start(input);\n\n        waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.FINISHED);\n\n        // switch to admin and add the user B\n\n        resetApiKey();\n\n        String userBName = \"userB_\" + randomString();\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userBName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        CreateApiKeyResponse apiKeyB = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userBName));\n\n        // switch to the user B and try starting a process using the project\n\n        setApiKey(apiKeyB.getKey());\n\n        try {\n            start(input);\n            fail(\"Should fail\");\n        } catch (ApiException e) {\n        }\n\n        // switch to admin and add the user B to the team\n\n        resetApiKey();\n\n        teamsApi.addUsersToTeam(orgName, teamName, false, Collections.singletonList(new TeamUserEntry()\n                .username(userBName)\n                .role(TeamUserEntry.RoleEnum.MEMBER)));\n\n        // switch to the user B and start a process using the project\n\n        setApiKey(apiKeyB.getKey());\n\n        spr = start(input);\n        waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.FINISHED);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ProcessStateIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessApi;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.InputStream;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class ProcessStateIT extends AbstractServerIT {\n\n    @Test\n    public void testSingleFile() throws Exception {\n        byte[] payload = archive(ProcessStateIT.class.getResource(\"stateSingleFile\").toURI());\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pe.getStatus());\n\n        // ---\n\n        try (InputStream resp = processApi.downloadStateFile(spr.getInstanceId(), \"concord.yml\")) {\n            assertNotNull(resp);\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ProjectDeleteIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ProjectDeleteIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        String orgName = \"Default\";\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        ProjectOperationResponse cpr = projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n        assertTrue(cpr.getOk());\n\n        // ---\n\n        byte[] payload = archive(ProjectDeleteIT.class.getResource(\"simple\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pe.getStatus());\n        assertEquals(cpr.getId(), pe.getProjectId());\n\n        // ---\n\n        GenericOperationResult dpr = projectsApi.deleteProject(orgName, projectName);\n        assertTrue(dpr.getOk());\n\n        try {\n            projectsApi.getProject(orgName, projectName);\n            fail(\"Should fail\");\n        } catch (ApiException e) {\n        }\n\n        // ---\n\n        ProcessV2Api processApi = new ProcessV2Api(getApiClient());\n        pe = processApi.getProcess(spr.getInstanceId(), Collections.emptySet());\n        assertNull(pe.getProjectId());\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ProjectFileIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.ZipUtils;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.BufferedReader;\nimport java.io.ByteArrayOutputStream;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class ProjectFileIT extends AbstractServerIT {\n\n    @Test\n    public void testSingleProfile() throws Exception {\n        simpleTest(\"projectfile/singleprofile\", \".*Hello, world.*\", \".*xyz54321abc.*\");\n    }\n\n    @Test\n    public void testSingleProfileUsingConfiguration() throws Exception {\n        simpleTest(\"projectfile/singleprofilecfg\", \".*Hello, world.*\", \".*54321.*\");\n    }\n\n    @Test\n    public void testExternalProfile() throws Exception {\n        simpleTest(\"projectfile/externalprofile\", \".*Hello, world.*\");\n    }\n\n    @Test\n    public void testAltName() throws Exception {\n        simpleTest(\"projectfile/altname\", \".*Hello, world.*\");\n    }\n\n    @Test\n    public void testOverrideFlow() throws Exception {\n        simpleTest(\"projectfile/overrideflow\", \".*Hello, world.*\");\n    }\n\n    @Test\n    public void testExpressionsInVariables() throws Exception {\n        simpleTest(\"projectfile/expr\");\n    }\n\n    @Test\n    public void testExternalScript() throws Exception {\n        simpleTest(\"projectfile/externalscript\", \".*hello!.*\", \".*bye!.*\");\n    }\n\n    @Test\n    public void testExternalScriptWithErrorBlock() throws Exception {\n        simpleTest(\"projectfile/scriptWithErrorBlock\", \".*error occurred!.*\");\n    }\n\n    @Test\n    public void testDependencies() throws Exception {\n        String dep = \"file:///\" + ITConstants.DEPENDENCIES_DIR + \"/example.jar\";\n        Path tmpDir = createTempDir();\n\n        // prepare .concord.yml\n        try (InputStream in = ProjectFileIT.class.getResourceAsStream(\"projectfile/deps/.template.yml\");\n             BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {\n\n            List<String> l = new ArrayList<>();\n\n            String line;\n            while ((line = reader.readLine()) != null) {\n                l.add(line.replaceAll(\"WILL_BE_REPLACED\", dep));\n            }\n\n            Path p = tmpDir.resolve(\".concord.yml\");\n            Files.write(p, l);\n        }\n\n        // create the payload\n\n        String request = \"{ \\\"entryPoint\\\": \\\"main\\\" }\";\n        Path requestFile = tmpDir.resolve(Constants.Files.CONFIGURATION_FILE_NAME);\n        Files.write(requestFile, Collections.singletonList(request));\n\n        Path src = Paths.get(DependenciesIT.class.getResource(\"projectfile/deps\").toURI());\n        ByteArrayOutputStream baos = new ByteArrayOutputStream();\n        try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(baos)) {\n            ZipUtils.zip(zip, src);\n            ZipUtils.zip(zip, tmpDir);\n        }\n\n        byte[] payload = baos.toByteArray();\n\n        // send the request\n\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        ProcessEntry psr = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, psr.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(psr.getInstanceId());\n        assertLog(\".*Hello!.*\", ab);\n    }\n\n    @Test\n    public void testArchiveOverride() throws Exception {\n        String orgName = \"Default\";\n\n        // ---\n\n        String projectName = \"project_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n        String secretName = \"secret_\" + randomString();\n\n        generateKeyPair(orgName, secretName, false, null);\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .name(repoName)\n                        .url(createRepo(\"repositoryRefresh\"))\n                        .branch(\"master\")\n                        .secretName(secretName))));\n\n        // ---\n\n        byte[] payload = archive(ProjectFileIT.class.getResource(\"projectfile/singleprofile\").toURI());\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        StartProcessResponse spr = start(input);\n        assertNotNull(spr.getInstanceId());\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*54321.*\", ab);\n    }\n\n    @Test\n    public void testExpressionScriptName() throws Exception {\n        simpleTest(\"projectfile/expressionscript\", \".*hello!.*\", \".*bye!.*\");\n    }\n\n    private void simpleTest(String resource, String... logPatterns) throws Exception {\n        byte[] payload = archive(ProjectFileIT.class.getResource(resource).toURI());\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        if (logPatterns == null || logPatterns.length == 0) {\n            return;\n        }\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        for (String p : logPatterns) {\n            assertLog(p, ab);\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ProjectIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.eclipse.jgit.api.Git;\nimport org.eclipse.jgit.lib.Ref;\nimport org.eclipse.jgit.revwalk.RevCommit;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.nio.file.Path;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ProjectIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        Path tmpDir = createTempDir();\n\n        File src = new File(ProjectIT.class.getResource(\"project\").toURI());\n        PathUtils.copy(src.toPath(), tmpDir);\n\n        try (Git repo = initRepo(tmpDir)) {\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"import\").call();\n        }\n\n        String gitUrl = tmpDir.toAbsolutePath().toString();\n\n        // ---\n\n        String projectName = \"myProject_\" + randomString();\n        String username = \"myUser_\" + randomString();\n        Set<String> permissions = Collections.emptySet();\n        String repoName = \"myRepo_\" + randomString();\n        String repoUrl = gitUrl;\n        String entryPoint = \"main\";\n        String greeting = \"Hello, _\" + randomString();\n        Map<String, Object> args = Collections.singletonMap(Constants.Request.ARGUMENTS_KEY,\n                Collections.singletonMap(\"greeting\", greeting));\n\n        // ---\n\n        ProcessEntry psr = doTest(projectName, username, permissions, repoName, repoUrl, entryPoint, args, false);\n\n        byte[] ab = getLog(psr.getInstanceId());\n        assertLog(\".*\" + greeting + \".*\", ab);\n    }\n\n    private static Git initRepo(Path initDir) throws Exception {\n        return Git.init().setInitialBranch(\"master\").setDirectory(initDir.toFile()).call();\n    }\n\n    @Test\n    public void testEntryPointFromYml() throws Exception {\n        Path tmpDir = createTempDir();\n\n        File src = new File(ProjectIT.class.getResource(\"projectEntryPoint\").toURI());\n        PathUtils.copy(src.toPath(), tmpDir);\n\n        try (Git repo = initRepo(tmpDir)) {\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"import\").call();\n        }\n\n        String gitUrl = tmpDir.toAbsolutePath().toString();\n\n        // ---\n\n        String projectName = \"myProject_\" + randomString();\n        String username = \"myUser_\" + randomString();\n        Set<String> permissions = Collections.emptySet();\n        String repoName = \"myRepo_\" + randomString();\n        String repoUrl = gitUrl;\n\n        // ---\n\n        ProcessEntry psr = doTest(projectName, username, permissions, repoName, repoUrl, null, Collections.emptyMap(), false);\n\n        byte[] ab = getLog(psr.getInstanceId());\n        assertLog(\".*Hello, Concord.*\", ab);\n    }\n\n    @Test\n    public void testWithCommitId() throws Exception {\n        Path tmpDir = createTempDir();\n\n        File src = new File(ProjectIT.class.getResource(\"project\").toURI());\n        PathUtils.copy(src.toPath(), tmpDir);\n\n        try (Git repo = initRepo(tmpDir)) {\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"import\").call();\n\n            // commit-1\n            PathUtils.deleteRecursively(tmpDir.resolve(\"processes\"));\n            src = new File(ProjectIT.class.getResource(\"project-commit-id\").toURI());\n            PathUtils.copy(src.toPath().resolve(\"1\"), tmpDir);\n            Ref result = repo.checkout().setName(\"test-branch\").setCreateBranch(true).call();\n            assertNotNull(result);\n\n            repo.add().addFilepattern(\".\").call();\n            RevCommit cmt = repo.commit().setMessage(\"commit-1\").call();\n            String commitId = cmt.getId().getName();\n\n            // commit-2\n            result = repo.checkout().setName(\"master\").call();\n            PathUtils.deleteRecursively(tmpDir.resolve(\"processes\"));\n            src = new File(ProjectIT.class.getResource(\"project-commit-id\").toURI());\n            PathUtils.copy(src.toPath().resolve(\"2\"), tmpDir);\n            assertNotNull(result);\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"commit-2\").call();\n\n            String gitUrl = tmpDir.toAbsolutePath().toString();\n\n            // ---\n\n            String projectName = \"myProject_\" + randomString();\n            String username = \"myUser_\" + randomString();\n            Set<String> permissions = Collections.emptySet();\n            String repoName = \"myRepo_\" + randomString();\n            String repoUrl = gitUrl;\n            String entryPoint = \"main\";\n            String greeting = \"Hello, _\" + randomString();\n            Map<String, Object> args = Collections.singletonMap(Constants.Request.ARGUMENTS_KEY,\n                    Collections.singletonMap(\"greeting\", greeting));\n\n            // ---\n            ProcessEntry psr = doTest(projectName, username, permissions, repoName, repoUrl, entryPoint, args, commitId, null, false);\n\n            byte[] ab = getLog(psr.getInstanceId());\n            assertLog(\".*test-commit-1.*\" + greeting + \".*\", ab);\n        }\n    }\n\n    @Test\n    public void testWithTag() throws Exception {\n        Path tmpDir = createTempDir();\n\n        File src = new File(ProjectIT.class.getResource(\"project\").toURI());\n        PathUtils.copy(src.toPath(), tmpDir);\n\n        String tag = \"tag_for_testing\";\n\n        try (Git repo = initRepo(tmpDir)) {\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"import\").call();\n\n            // commit-1\n            PathUtils.deleteRecursively(tmpDir.resolve(\"processes\"));\n            src = new File(ProjectIT.class.getResource(\"project-commit-id\").toURI());\n            PathUtils.copy(src.toPath().resolve(\"1\"), tmpDir);\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"commit-1\").call();\n\n            repo.tag().setName(tag).call();\n\n            // commit-2\n            PathUtils.deleteRecursively(tmpDir.resolve(\"processes\"));\n            src = new File(ProjectIT.class.getResource(\"project-commit-id\").toURI());\n            PathUtils.copy(src.toPath().resolve(\"2\"), tmpDir);\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"commit-2\").call();\n        }\n\n        String gitUrl = tmpDir.toAbsolutePath().toString();\n\n        // ---\n        String projectName = \"myProject_\" + randomString();\n        String username = \"myUser_\" + randomString();\n        Set<String> permissions = Collections.emptySet();\n        String repoName = \"myRepo_\" + randomString();\n        String repoUrl = gitUrl;\n        String entryPoint = \"main\";\n        String greeting = \"Hello, _\" + randomString();\n        Map<String, Object> args = Collections.singletonMap(Constants.Request.ARGUMENTS_KEY,\n                Collections.singletonMap(\"greeting\", greeting));\n\n        // ---\n        ProcessEntry psr = doTest(projectName, username, permissions, repoName, repoUrl, entryPoint, args, null, tag, false);\n\n        byte[] ab = getLog(psr.getInstanceId());\n        assertLog(\".*test-commit-1.*\" + greeting + \".*\", ab);\n    }\n\n    @Test\n    public void testInitImport() throws Exception {\n        Path tmpDir = createTempDir();\n\n        File src = new File(ProjectIT.class.getResource(\"project-triggers\").toURI());\n        PathUtils.copy(src.toPath(), tmpDir);\n\n        try (Git repo = initRepo(tmpDir)) {\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"import\").call();\n        }\n\n        String gitUrl = tmpDir.toAbsolutePath().toString();\n\n        // ---\n\n        String projectName = \"myProject_\" + randomString();\n        String username = \"myUser_\" + randomString();\n        Set<String> permissions = Collections.emptySet();\n        String repoName = \"myRepo_\" + randomString();\n        String repoUrl = gitUrl;\n\n        createProjectAndRepo(projectName, username, permissions, repoName, repoUrl, null, null);\n\n        TriggersApi triggersApi = new TriggersApi(getApiClient());\n        while (true) {\n            List<TriggerEntry> triggers = triggersApi.listTriggers(\"Default\", projectName, repoName);\n            if (hasCondition(\"github\", \"branch\", \"foo\", triggers) &&\n                    hasCondition(\"github\", \"branch\", \"foo2\", triggers) &&\n                    hasCondition(\"oneops\", \"org\", \"myOrg\", triggers)) {\n                break;\n            }\n\n            Thread.sleep(1_000);\n        }\n    }\n\n    @Test\n    public void testRepositoryValidation() throws Exception {\n        Path tmpDir = createTempDir();\n\n        File src = new File(ProjectIT.class.getResource(\"repositoryValidation\").toURI());\n        PathUtils.copy(src.toPath(), tmpDir);\n\n        try (Git repo = initRepo(tmpDir)) {\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"import\").call();\n        }\n\n        String gitUrl = tmpDir.toAbsolutePath().toString();\n\n        // ---\n\n        String projectName = \"myProject_\" + randomString();\n        String username = \"myUser_\" + randomString();\n        Set<String> permissions = Collections.emptySet();\n        String repoName = \"myRepo_\" + randomString();\n        String repoUrl = gitUrl;\n\n        // ---\n\n        createProjectAndRepo(projectName, username, permissions, repoName, repoUrl, null, null);\n\n        // ---\n\n        RepositoriesApi repositoriesApi = new RepositoriesApi(getApiClient());\n        RepositoryValidationResponse result = repositoriesApi.validateRepository(\"Default\", projectName, repoName);\n        assertTrue(result.getOk());\n    }\n\n    @Test\n    public void testRepositoryValidationForEmptyFlow() throws Exception {\n        assertThrows(Exception.class, () -> {\n            Path tmpDir = createTempDir();\n\n            File src = new File(ProjectIT.class.getResource(\"repositoryValidationEmptyFlow\").toURI());\n            PathUtils.copy(src.toPath(), tmpDir);\n\n            try (Git repo = initRepo(tmpDir)) {\n                repo.add().addFilepattern(\".\").call();\n                repo.commit().setMessage(\"import\").call();\n            }\n\n            String gitUrl = tmpDir.toAbsolutePath().toString();\n\n            // ---\n\n            String projectName = \"myProject_\" + randomString();\n            String username = \"myUser_\" + randomString();\n            Set<String> permissions = Collections.emptySet();\n            String repoName = \"myRepo_\" + randomString();\n            String repoUrl = gitUrl;\n\n            // ---\n\n            createProjectAndRepo(projectName, username, permissions, repoName, repoUrl, null, null);\n\n            // ---\n\n            RepositoriesApi repositoriesApi = new RepositoriesApi(getApiClient());\n            RepositoryValidationResponse resp = repositoriesApi.validateRepository(\"Default\", projectName, repoName);\n            assertTrue(resp.getOk());\n        });\n    }\n\n    @Test\n    public void testRepositoryValidationForEmptyForm() throws Exception {\n        assertThrows(Exception.class, () -> {\n            Path tmpDir = createTempDir();\n\n            File src = new File(ProjectIT.class.getResource(\"repositoryValidationEmptyForm\").toURI());\n            PathUtils.copy(src.toPath(), tmpDir);\n\n            try (Git repo = initRepo(tmpDir)) {\n                repo.add().addFilepattern(\".\").call();\n                repo.commit().setMessage(\"import\").call();\n            }\n\n            String gitUrl = tmpDir.toAbsolutePath().toString();\n\n            // ---\n\n            String projectName = \"myProject_\" + randomString();\n            String username = \"myUser_\" + randomString();\n            Set<String> permissions = Collections.emptySet();\n            String repoName = \"myRepo_\" + randomString();\n            String repoUrl = gitUrl;\n\n            // ---\n\n            createProjectAndRepo(projectName, username, permissions, repoName, repoUrl, null, null);\n\n            // ---\n\n            RepositoriesApi repositoriesApi = new RepositoriesApi(getApiClient());\n            RepositoryValidationResponse resp = repositoriesApi.validateRepository(\"Default\", projectName, repoName);\n            assertTrue(resp.getOk());\n        });\n    }\n\n    @Test\n    public void testDisabledRepository() throws Exception {\n        assertThrows(Exception.class, () -> {\n            Path tmpDir = createTempDir();\n\n            File src = new File(ProjectIT.class.getResource(\"ProcessDisabledRepo\").toURI());\n            PathUtils.copy(src.toPath(), tmpDir);\n\n            try (Git repo = initRepo(tmpDir)) {\n                repo.add().addFilepattern(\".\").call();\n                repo.commit().setMessage(\"import\").call();\n            }\n\n            String gitUrl = tmpDir.toAbsolutePath().toString();\n\n            // ---\n            String orgName = \"Default\";\n            String projectName = \"myProject_\" + randomString();\n            String repoName = \"myRepo_\" + randomString();\n            String repoUrl = gitUrl;\n\n            ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n            projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                    .name(projectName)\n                    .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                            .name(repoName).url(repoUrl)\n                            .disabled(true))));\n\n            // ---\n            Map<String, Object> input = new HashMap<>();\n            input.put(\"org\", orgName);\n            input.put(\"project\", projectName);\n            input.put(\"repo\", repoName);\n\n            start(input);\n        });\n    }\n\n    @Test\n    public void testBulkAccessUpdate() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName));\n\n        // ---\n\n        String teamName = \"team_\" + randomString();\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n        CreateTeamResponse teamResp = teamsApi.createOrUpdateTeam(orgName, new TeamEntry()\n                .name(teamName));\n\n        // --- Typical one-or-more teams bulk access update\n\n        List<ResourceAccessEntry> teams = new ArrayList<>(1);\n        teams.add(new ResourceAccessEntry()\n                .orgName(orgName)\n                .teamId(teamResp.getId())\n                .teamName(teamName)\n                .level(ResourceAccessEntry.LevelEnum.OWNER));\n        GenericOperationResult addTeamsResult = projectsApi.updateProjectAccessLevelBulk(orgName, projectName, teams);\n        assertNotNull(addTeamsResult);\n        assertTrue(addTeamsResult.getOk());\n\n        List<ResourceAccessEntry> currentTeams = projectsApi.getProjectAccessLevel(orgName, projectName);\n        assertNotNull(currentTeams);\n        assertEquals(1, currentTeams.size());\n\n        // --- Empty teams list clears all\n\n        GenericOperationResult clearTeamsResult = projectsApi.updateProjectAccessLevelBulk(orgName, projectName, Collections.emptyList());\n        assertNotNull(clearTeamsResult);\n        assertTrue(clearTeamsResult.getOk());\n\n        currentTeams = projectsApi.getProjectAccessLevel(orgName, projectName);\n        assertNotNull(currentTeams);\n        assertEquals(0, currentTeams.size());\n\n        // --- Null list not allowed, throws error\n\n        try {\n            projectsApi.updateProjectAccessLevelBulk(orgName, projectName, null);\n        } catch (ApiException expected) {\n            assertEquals(400, expected.getCode());\n            assertTrue(expected.getResponseBody().contains(\"List of teams is null\"));\n        } catch (Exception e) {\n            fail(\"Expected ApiException. Got \" + e.getClass().toString());\n        }\n\n        // ---\n\n        teamsApi.deleteTeam(orgName, teamName);\n        projectsApi.deleteProject(orgName, projectName);\n        orgApi.deleteOrg(orgName, \"yes\");\n    }\n\n    private static boolean hasCondition(String src, String k, Object v, Collection<TriggerEntry> entries) {\n        for (TriggerEntry e : entries) {\n            Map<String, Object> c = e.getConditions();\n            if (c == null || c.isEmpty()) {\n                continue;\n            }\n\n            if (!src.equals(e.getEventSource())) {\n                continue;\n            }\n\n            if (v.equals(c.get(k))) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    protected void createProjectAndRepo(String projectName,\n                                        String username,\n                                        Set<String> permissions,\n                                        String repoName,\n                                        String repoUrl,\n                                        String commitId,\n                                        String tag) throws Exception {\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        CreateUserResponse cur = usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(username)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n        assertTrue(cur.getOk());\n\n        UUID userId = cur.getId();\n\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse cakr = apiKeyResource.createUserApiKey(new CreateApiKeyRequest()\n                .userId(userId)\n                .userType(CreateApiKeyRequest.UserTypeEnum.LOCAL));\n        assertTrue(cakr.getOk());\n\n        String apiKey = cakr.getKey();\n\n        // ---\n\n        setApiKey(apiKey);\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        ProjectOperationResponse cpr = projectsApi.createOrUpdateProject(\"Default\", new ProjectEntry()\n                .name(projectName)\n                .repositories(Collections.singletonMap(repoName,\n                        new RepositoryEntry()\n                                .name(repoName)\n                                .url(repoUrl)\n                                .branch(tag != null ? tag : \"master\")\n                                .commitId(commitId))));\n        assertTrue(cpr.getOk());\n    }\n\n    protected ProcessEntry doTest(String projectName,\n                                  String username, Set<String> permissions,\n                                  String repoName, String repoUrl,\n                                  String entryPoint, Map<String, Object> args,\n                                  boolean sync) throws Exception {\n        return doTest(projectName, username, permissions, repoName, repoUrl,\n                entryPoint, args, null, null, sync);\n    }\n\n    protected ProcessEntry doTest(String projectName,\n                                  String username, Set<String> permissions,\n                                  String repoName, String repoUrl,\n                                  String entryPoint, Map<String, Object> args,\n                                  String commitId, String tag,\n                                  boolean sync) throws Exception {\n\n        // ---\n\n        createProjectAndRepo(projectName, username, permissions, repoName, repoUrl, commitId, tag);\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        Map<String, Object> input = new HashMap<>();\n        if (projectName != null) {\n            input.put(\"org\", \"Default\");\n            input.put(\"project\", projectName);\n        }\n        if (repoName != null) {\n            input.put(\"repo\", repoName);\n        }\n        if (entryPoint != null) {\n            input.put(\"entryPoint\", entryPoint);\n        }\n        input.put(\"request\", args);\n        input.put(\"sync\", sync);\n        StartProcessResponse spr = start(input);\n        assertTrue(spr.getOk());\n\n        UUID instanceId = spr.getInstanceId();\n\n        // ---\n\n        ProcessEntry psr = waitForCompletion(getApiClient(), instanceId);\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, psr.getStatus());\n\n        // ---\n\n        return psr;\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ProjectInfoIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class ProjectInfoIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        String orgName = \"Default\";\n        UUID orgId = UUID.fromString(\"0fac1b18-d179-11e7-b3e7-d7df4543ed4f\");\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        ProjectOperationResponse cpr = projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n\n        byte[] payload = archive(ProjectInfoIT.class.getResource(\"projectInfo\").toURI());\n\n        // ---\n\n        StartProcessResponse spr = start(orgName, projectName, null, null, payload);\n        assertNotNull(spr.getInstanceId());\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Org ID:.*\" + orgId + \".*\", ab);\n        assertLog(\".*Project ID:.*\" + cpr.getId() + \".*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ProjectTaskIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.it.common.MockGitSshServer;\nimport org.eclipse.jgit.api.Git;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\n\npublic class ProjectTaskIT extends AbstractServerIT {\n\n    private MockGitSshServer gitServer;\n\n    @BeforeEach\n    public void setUp() throws Exception {\n        Path bareRepo = createTempDir();\n        try (Git git = Git.init().setInitialBranch(\"master\").setBare(true).setDirectory(bareRepo.toFile()).call()) {\n        }\n\n        Path workdir = createTempDir();\n        try (Git git = Git.cloneRepository()\n                .setDirectory(workdir.toFile())\n                .setURI(\"file://\" + bareRepo)\n                .call()) {\n\n            Path initialData = Paths.get(GitRepositoryIT.class.getResource(\"gitRepository\").toURI());\n            PathUtils.copy(initialData, workdir);\n\n            git.add().addFilepattern(\".\").call();\n            git.commit().setMessage(\"initial commit\").call();\n\n            git.push().call();\n        }\n\n        gitServer = new MockGitSshServer(0, bareRepo);\n        gitServer.start();\n    }\n\n    @AfterEach\n    public void tearDown() throws Exception {\n        gitServer.stop();\n    }\n\n    @Test\n    public void testCreate() throws Exception {\n        String orgName = \"Default\";\n\n        // ---\n\n        String projectName = \"project_\" + System.currentTimeMillis();\n        String repoName = \"repo_\" + System.currentTimeMillis();\n        String repoUrl = String.format(ITConstants.CUSTOM_GIT_SERVER_URL_PATTERN, gitServer.getPort());\n        String repoSecret = \"secret_\" + System.currentTimeMillis();\n        generateKeyPair(orgName, repoSecret, false, null);\n\n        // ---\n\n        byte[] payload = archive(ProjectTaskIT.class.getResource(\"projectTask\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"projectName\", projectName);\n        args.put(\"repoName\", repoName);\n        args.put(\"repoUrl\", repoUrl);\n        args.put(\"repoSecret\", repoSecret);\n\n        input.put(\"request\", Collections.singletonMap(\"arguments\", args));\n\n        // ---\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*CREATED.*\", ab);\n        assertLog(\".*Done!.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/PublicFlowsIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class PublicFlowsIT extends AbstractServerIT {\n\n    /**\n     * Verifies that {@code publicFlow} values are merged across profiles\n     * and a {@code publicFlow} from a profile can be used an the {@code entryPoint}\n     */\n    @Test\n    public void testProfiles() throws Exception {\n        byte[] payload = archive(PublicFlowsIT.class.getResource(\"publicFlowsInProfiles\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        byte[] ab = getLog(pe.getInstanceId());\n\n        assertLog(\".*Hello A.*\", ab);\n        assertLog(\".*Hello B.*\", ab);\n        assertLog(\".*Hello C.*\", ab);\n\n        // ---\n\n        input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"activeProfiles\", \"profileA\");\n        input.put(\"entryPoint\", \"flowA\");\n\n        spr = start(input);\n\n        pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        ab = getLog(pe.getInstanceId());\n\n        assertLog(\".*Hello A.*\", ab);\n        assertNoLog(\".*Hello B.*\", ab);\n        assertNoLog(\".*Hello C.*\", ab);\n\n        // ---\n\n        input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"entryPoint\", \"flowC\");\n\n        spr = start(input);\n\n        pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FAILED, pe.getStatus());\n\n        ab = getLog(pe.getInstanceId());\n\n        assertLogAtLeast(\".*not a public flow.*\", 1, ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/RawPayloadProjectIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.client2.ProjectEntry.RawPayloadModeEnum.DISABLED;\nimport static com.walmartlabs.concord.client2.ProjectEntry.RawPayloadModeEnum.ORG_MEMBERS;\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.fail;\n\npublic class RawPayloadProjectIT extends AbstractServerIT {\n\n    @Test\n    public void testReject() throws Exception {\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n\n        String orgName = \"Default\";\n        String projectName = \"project_\" + System.currentTimeMillis();\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName));\n\n        ProjectEntry projectEntry = projectsApi.getProject(orgName, projectName);\n        // check the defaults\n        assertEquals(DISABLED, projectEntry.getRawPayloadMode());\n\n        // ---\n\n        byte[] payload = archive(RawPayloadProjectIT.class.getResource(\"example\").toURI());\n\n        try {\n            Map<String, Object> input = new HashMap<>();\n            input.put(\"org\", orgName);\n            input.put(\"project\", projectName);\n            input.put(\"archive\", payload);\n            StartProcessResponse process = start(input);\n            System.out.println(\"process: \" + process);\n            fail(\"should fail\");\n        } catch (ApiException e) {\n        }\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/RepositoryRefreshIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\n\npublic class RepositoryRefreshIT extends AbstractServerIT {\n    /**\n     * Test case is ignored as repository refresh task is enabled only for concord runtime-v2\n     * @throws Exception\n     */\n    @Disabled\n    @Test\n    public void test() throws Exception {\n        String orgName = \"ConcordSystem\";\n        String projectName = \"concordTriggers\";\n        String repoName = \"triggers\";\n\n        // ---\n\n        byte[] payload = archive(RepositoryRefreshIT.class.getResource(\"repositoryRefresh\").toURI());\n\n        Map<String, Object> req = new HashMap<>();\n        req.put(\"archive\", payload);\n\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"orgName\", orgName);\n        args.put(\"projectName\", projectName);\n        args.put(\"repoName\", repoName);\n\n        req.put(\"request\", Collections.singletonMap(\"arguments\", args));\n\n        StartProcessResponse spr = start(req);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        TriggersApi triggerResource = new TriggersApi(getApiClient());\n        List<TriggerEntry> list = triggerResource.listTriggers(orgName, projectName, repoName);\n        assertFalse(list.isEmpty());\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/RequirementsIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.junit.jupiter.api.Assumptions.assumeTrue;\n\n// requires an agent with custom \"capabilities\" configured\nclass RequirementsIT extends AbstractServerIT {\n\n    @BeforeAll\n    static void setUp() {\n        assumeTrue(System.getenv(\"IT_CUSTOM_AGENTS\") != null);\n    }\n\n    @Test\n    void testRequirementsRegex() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PUBLIC)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        byte[] payload = archive(RequirementsIT.class.getResource(\"processRequirements\").toURI());\n        Map<String, Object> input = Map.of(\n                \"archive\", payload,\n                \"org\", orgName,\n                \"project\", projectName\n        );\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertNotNull(pe.getRequirements());\n        assertFalse(pe.getRequirements().isEmpty());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pe.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLog(\".*Hello from a process with requirements.*\", ab);\n    }\n\n    @Test\n    void testRequirementsInvalidRegex() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PUBLIC)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        byte[] payload = archive(RequirementsIT.class.getResource(\"processRequirements\").toURI());\n        Map<String, Object> input = Map.of(\n                \"archive\", payload,\n                \"org\", orgName,\n                \"project\", projectName,\n                \"activeProfiles\", \"invalidRegex\"\n        );\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertNotNull(pe.getRequirements());\n        assertFalse(pe.getRequirements().isEmpty());\n        assertEquals(ProcessEntry.StatusEnum.FAILED, pe.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLog(\".*Invalid regex in requested agent capabilities.*\", ab);\n    }\n\n    @Test\n    void testForkWithRequirements() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PUBLIC)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        byte[] payload = archive(RequirementsIT.class.getResource(\"concordTaskForkWithRequirements\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n\n        StartProcessResponse parentSpr = start(input);\n\n        ProcessEntry pe = waitForCompletion(getApiClient(), parentSpr.getInstanceId());\n        assertNotNull(pe.getRequirements());\n        assertFalse(pe.getRequirements().isEmpty());\n\n        ProcessV2Api processApi = new ProcessV2Api(getApiClient());\n        ProcessEntry processEntry = processApi.getProcess(parentSpr.getInstanceId(), Collections.singleton(\"childrenIds\"));\n        assertEquals(1, processEntry.getChildrenIds().size());\n\n        ProcessEntry child = processApi.getProcess(processEntry.getChildrenIds().iterator().next(), Collections.emptySet());\n        assertNotNull(child);\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, child.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(child.getInstanceId());\n        assertLog(\".*Hello from a subprocess.*\", ab);\n\n        // ---\n        assertNotNull(child.getRequirements());\n        assertEquals(pe.getRequirements(), child.getRequirements());\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ResourceIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.intellij.lang.annotations.Language;\nimport org.junit.jupiter.api.Test;\n\nimport java.net.URI;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ResourceIT extends AbstractServerIT {\n\n    @Test\n    public void testReadAsJson() throws Exception {\n        basicAssert(test(\"resourceReadAsJson\"), \".*Hello Concord!\");\n    }\n\n    @Test\n    public void testFromJsonString() throws Exception {\n        basicAssert(test(\"resourceReadFromJsonString\"), \".*Hello Concord!\");\n    }\n\n    @Test\n    public void testReadAsString() throws Exception {\n        basicAssert(test(\"resourceReadAsString\"), \".*Hello Concord!\");\n    }\n\n    @Test\n    public void testWriteAsJson() throws Exception {\n        basicAssert(test(\"resourceWriteAsJson\"), \".*Hello Concord!\");\n    }\n\n    @Test\n    public void testWriteAsString() throws Exception {\n        basicAssert(test(\"resourceWriteAsString\"), \".*Hello Concord!\");\n    }\n\n    @Test\n    public void testWriteAsYaml() throws Exception {\n        basicAssert(test(\"resourceWriteAsYaml\"), \".*Hello Concord!\");\n    }\n\n    @Test\n    void testPrintJson() throws Exception {\n        ProcessEntry pir = test(\"resourcePrintJson\");\n\n        // ---\n\n        Map<String, Object> meta = pir.getMeta();\n\n        String condensedResult = (String) meta.get(\"condensedResult\");\n        assertFalse(condensedResult.contains(\"\\n\"));\n        assertTrue(condensedResult.contains(\"\\\"x\\\":123\"));\n        assertTrue(condensedResult.contains(\"\\\"y\\\":\\\"hello\"));\n\n        String prettyResult = (String) meta.get(\"prettyResult\");\n        assertTrue(prettyResult.contains(\"\\n\"));\n        assertTrue(prettyResult.contains(\"\\\"x\\\" : 123\"));\n        assertTrue(prettyResult.contains(\"\\\"y\\\" : \\\"hello\\\"\"));\n    }\n\n    private ProcessEntry test(String resource) throws Exception {\n        URI dir = ResourceIT.class.getResource(resource).toURI();\n        byte[] payload = archive(dir);\n\n        StartProcessResponse spr = start(payload);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        return pir;\n    }\n\n    private void basicAssert(ProcessEntry pir, @Language(\"RegExp\") String pattern) throws Exception {\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(pattern, ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/RunAsIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class RunAsIT extends AbstractServerIT {\n\n    @Test\n    public void testSwitchCurrentUser() throws Exception {\n        // create a new org\n\n        String orgName = \"org_\" + randomString();\n        createOrg(orgName);\n\n        // add the user A\n\n        String userAName = \"userA_\" + randomString();\n        CreateApiKeyResponse apiKeyA = addUser(userAName);\n\n        // create the user A's team\n\n        String teamName = \"team_\" + randomString();\n        UUID teamId = createTeam(orgName, teamName, userAName);\n\n        // switch to the user A and create a new project\n\n        setApiKey(apiKeyA.getKey());\n\n        String projectName = \"project_\" + randomString();\n        createProject(orgName, projectName);\n\n        // grant the team access to the project\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.updateProjectAccessLevel(orgName, projectName, new ResourceAccessEntry()\n                .teamId(teamId)\n                .orgName(orgName)\n                .teamName(teamName)\n                .level(ResourceAccessEntry.LevelEnum.READER));\n\n        // Start a process\n\n        byte[] payload = archive(RunAsIT.class.getResource(\"runas\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n\n        StartProcessResponse p = start(input);\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        ProcessEntry pe = waitForStatus(getApiClient(), p.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n\n        byte[] ab = getLog(pe.getInstanceId());\n        // assume Concord forces all user/domain names to lower case\n        assertLog(\".*username=\" + userAName.toLowerCase() + \".*==.*username=\" + userAName.toLowerCase() + \".*\", ab);\n\n        String formName = findForm(p.getInstanceId());\n\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n\n        Map<String, Object> data = Collections.singletonMap(\"firstName\", \"xxx\");\n\n        // try submit as a wrong user\n\n        try {\n            formsApi.submitForm(p.getInstanceId(), formName, data);\n            fail(\"exception expected\");\n        } catch (ApiException e) {\n            // ignore\n        }\n\n        // submit as the proper user\n\n        resetApiKey();\n\n        FormSubmitResponse fsr = formsApi.submitForm(p.getInstanceId(), formName, data);\n        assertTrue(fsr.getOk());\n\n        pe = waitForCompletion(getApiClient(), p.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pe.getStatus());\n\n        ab = getLog(pe.getInstanceId());\n        assertLog(\".*Now we are running as admin. Initiator: \" + userAName.toLowerCase() + \".*\", ab);\n    }\n\n    @Test\n    public void testWithMultipleUsers() throws Exception {\n        // create a new org\n\n        String orgName = \"org_\" + randomString();\n        createOrg(orgName);\n\n        // add the user A\n\n        String userAName = \"userA_\" + randomString();\n        CreateApiKeyResponse apiKeyA = addUser(userAName);\n\n        // add the user B\n\n        String userBName = \"userB_\" + randomString();\n        CreateApiKeyResponse apiKeyB = addUser(userBName);\n\n        // create the user's team\n\n        String teamName = \"team_\" + randomString();\n        UUID teamId = createTeam(orgName, teamName, userAName, userBName);\n\n        // switch to the user A and create a new project\n\n        setApiKey(apiKeyA.getKey());\n\n        String projectName = \"project_\" + randomString();\n        createProject(orgName, projectName);\n\n        // grant the team access to the project\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.updateProjectAccessLevel(orgName, projectName, new ResourceAccessEntry()\n                .teamId(teamId)\n                .orgName(orgName)\n                .teamName(teamName)\n                .level(ResourceAccessEntry.LevelEnum.READER));\n\n        // Start a process\n\n        byte[] payload = archive(RunAsIT.class.getResource(\"runAsMultipleUsers\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"arguments.testUser\", userBName.toLowerCase());\n\n        StartProcessResponse p = start(input);\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        ProcessEntry pe = waitForStatus(getApiClient(), p.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n\n        byte[] ab = getLog(pe.getInstanceId());\n        // assume Concord forces all user/domain names to lower case\n        assertLog(\".*username=\" + userAName.toLowerCase() + \".*==.*username=\" + userAName.toLowerCase() + \".*\", ab);\n\n        String formName = findForm(p.getInstanceId());\n\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n\n        Map<String, Object> data = Collections.singletonMap(\"firstName\", \"xxx\");\n\n        // try submit as a wrong user\n\n        try {\n            formsApi.submitForm(p.getInstanceId(), formName, data);\n            fail(\"exception expected\");\n        } catch (ApiException e) {\n            // ignore\n        }\n\n        // switch to the user B and submit the form\n        setApiKey(apiKeyB.getKey());\n\n        FormSubmitResponse fsr = formsApi.submitForm(p.getInstanceId(), formName, data);\n        assertTrue(fsr.getOk());\n\n        pe = waitForCompletion(getApiClient(), p.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pe.getStatus());\n\n        // starting from 1.39.0 the log endpoint performs additional RBAC checks\n        // in this case user B doesn't have permissions to access the log\n        try {\n            getLog(pe.getInstanceId());\n            fail(\"should fail\");\n        } catch (ApiException e) {\n            assertEquals(403, e.getCode());\n        }\n\n        // switch to the user A and fetch the log again\n\n        setApiKey(apiKeyA.getKey());\n        ab = getLog(pe.getInstanceId());\n\n        assertLog(\".*Now we are running as \" + userBName.toLowerCase() + \".*\", ab);\n    }\n\n    @Test\n    public void testPayload() throws Exception {\n        // create a new org\n\n        String orgName = \"org_\" + randomString();\n        createOrg(orgName);\n\n        // add users\n\n        String userAName = \"userA_\" + randomString();\n        String userBName = \"userB_\" + randomString();\n\n        CreateApiKeyResponse apiKeyA = addUser(userAName);\n        CreateApiKeyResponse apiKeyB = addUser(userBName);\n\n        // start a new process\n\n        setApiKey(apiKeyA.getKey());\n\n        byte[] payload = archive(RunAsIT.class.getResource(\"runAsPayload\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.sudoUser\", userBName.toLowerCase());\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pe = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n\n        byte[] ab = getLog(pe.getInstanceId());\n        // assume Concord forces all user/domain names to lower case\n        assertLog(\".*AAA: \" + userAName.toLowerCase() + \".*\", ab);\n\n        // submit the form\n\n        setApiKey(apiKeyB.getKey());\n\n        String formName = findForm(pe.getInstanceId());\n\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n        Map<String, Object> data = Collections.singletonMap(\"msg\", \"Hello!\");\n        formsApi.submitForm(pe.getInstanceId(), formName, data);\n\n        // User B can submit the runAs form, but the process has no project and is still owned by user A.\n        setApiKey(apiKeyA.getKey());\n\n        // wait for the process to finish\n        pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // check the logs\n        resetApiKey();\n\n        ab = getLog(pe.getInstanceId());\n        assertLog(\".*BBB: Hello!.*\", ab);\n        // assume Concord forces all user/domain names to lower case\n        assertLog(\".*CCC: \" + userAName.toLowerCase() + \".*\", ab);\n    }\n\n    private void createOrg(String orgName) throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        CreateOrganizationResponse r = orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n        assertTrue(r.getOk());\n    }\n\n    private UUID createTeam(String orgName, String teamName, String... username) throws Exception {\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n        CreateTeamResponse ctr = teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamName));\n\n        teamsApi.addUsersToTeam(orgName, teamName, false, Arrays.stream(username)\n                .map(u -> new TeamUserEntry()\n                        .username(u)\n                        .role(TeamUserEntry.RoleEnum.MEMBER))\n                .collect(Collectors.toList()));\n\n        return ctr.getId();\n    }\n\n    private CreateApiKeyResponse addUser(String userAName) throws ApiException {\n        UsersApi usersApi = new UsersApi(getApiClient());\n\n        usersApi.createOrUpdateUser(new CreateUserRequest().username(userAName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n        return apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userAName));\n    }\n\n    private void createProject(String orgName, String projectName) throws ApiException {\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        ProjectOperationResponse por = projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PRIVATE)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n        assertTrue(por.getOk());\n    }\n\n    private String findForm(UUID instanceId) throws Exception {\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n\n        List<FormListEntry> forms = formsApi.listProcessForms(instanceId);\n        assertEquals(1, forms.size());\n\n        // ---\n\n        FormListEntry f0 = forms.get(0);\n        assertFalse(f0.getCustom());\n\n        return f0.getName();\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/SecretIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.client2.CreateSecretRequest;\nimport com.walmartlabs.concord.common.secret.KeyPair;\nimport com.walmartlabs.concord.common.secret.UsernamePassword;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.*;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.containsString;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class SecretIT extends AbstractServerIT {\n\n    @Test\n    public void testOwnerChange() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry().name(projectName));\n\n        // ---\n\n        String secretName = \"secret_\" + randomString();\n        generateKeyPair(orgName, projectName, secretName, false, null);\n\n        // ---\n\n        String userName = \"myUser_\" + randomString();\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        CreateUserResponse cur = usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        SecretsApi secretsApi = new SecretsApi(getApiClient());\n        SecretUpdateRequest req = new SecretUpdateRequest();\n        req.setOwner(new EntityOwner().id(cur.getId()));\n        secretsApi.updateSecretV1(orgName, secretName, req);\n\n        PublicKeyResponse pkr = secretsApi.getPublicKey(orgName, secretName);\n\n        assertNotNull(pkr);\n        assertNotNull(pkr.getPublicKey());\n\n        // ---\n\n        secretsApi.delete(orgName, secretName);\n        projectsApi.deleteProject(orgName, projectName);\n        orgApi.deleteOrg(orgName, \"yes\");\n    }\n\n    @Test\n    public void testBulkAccessUpdate() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName));\n\n        // ---\n\n        String secretName = \"secret_\" + randomString();\n        generateKeyPair(orgName, projectName, secretName, false, null);\n\n\n        SecretsApi secretsApi = new SecretsApi(getApiClient());\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n\n        // ---\n\n        String teamName = \"team_\" + randomString();\n        CreateTeamResponse teamResp = teamsApi.createOrUpdateTeam(orgName, new TeamEntry()\n                .name(teamName));\n\n        // --- Typical one-or-more teams bulk access update\n\n        List<ResourceAccessEntry> teams = new ArrayList<>(1);\n        teams.add(new ResourceAccessEntry()\n                .orgName(orgName)\n                .teamId(teamResp.getId())\n                .teamName(teamName)\n                .level(ResourceAccessEntry.LevelEnum.OWNER));\n        GenericOperationResult addTeamsResult = secretsApi.updateSecretAccessLevelBulk(orgName, secretName, teams);\n        assertNotNull(addTeamsResult);\n        assertTrue(addTeamsResult.getOk());\n\n        List<ResourceAccessEntry> currentTeams = secretsApi.getSecretAccessLevel(orgName, secretName);\n        assertNotNull(currentTeams);\n        assertEquals(1, currentTeams.size());\n\n        // --- Empty teams list clears all\n\n        GenericOperationResult clearTeamsResult = secretsApi.updateSecretAccessLevelBulk(orgName, secretName, Collections.emptyList());\n        assertNotNull(clearTeamsResult);\n        assertTrue(clearTeamsResult.getOk());\n\n        // --- Null list not allowed, throws error\n\n        try {\n            secretsApi.updateSecretAccessLevelBulk(orgName, secretName, null);\n        } catch (ApiException expected) {\n            assertEquals(400, expected.getCode());\n            assertTrue(expected.getResponseBody().contains(\"List of teams is null\"));\n        } catch (Exception e) {\n            fail(\"Expected ApiException. Got \" + e.getClass().toString());\n        }\n\n        // ---\n\n        teamsApi.deleteTeam(orgName, teamName);\n        secretsApi.delete(orgName, secretName);\n        projectsApi.deleteProject(orgName, projectName);\n        orgApi.deleteOrg(orgName, \"yes\");\n    }\n\n    @Test\n    public void testSecretUpdate() throws Exception {\n        String orgNameInit = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgNameInit));\n\n        // ---\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgNameInit, new ProjectEntry()\n                .name(projectName));\n\n        // ---\n\n        String secretName = \"secret_\" + randomString();\n        generateKeyPair(orgNameInit, projectName, secretName, false, null);\n\n        // ---\n\n        String userName = \"myUser_\" + randomString();\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        CreateUserResponse cur = usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        // ---\n\n        String newOrgName = \"org_\" + randomString();\n        UUID newOrgId = orgApi.createOrUpdateOrg(new OrganizationEntry().name(newOrgName)).getId();\n        String newSecretName = \"name_\" + randomString();\n\n        com.walmartlabs.concord.client2.UpdateSecretRequest request = com.walmartlabs.concord.client2.UpdateSecretRequest.builder()\n                .newOrgId(newOrgId)\n                .newOwnerId(cur.getId())\n                .newVisibility(SecretEntryV2.VisibilityEnum.PRIVATE)\n                .newName(newSecretName)\n                .build();\n\n        com.walmartlabs.concord.client2.SecretClient secretClient = new com.walmartlabs.concord.client2.SecretClient(getApiClient());\n        secretClient.updateSecret(orgNameInit, secretName, request);\n\n        SecretsApi secretsApi = new SecretsApi(getApiClient());\n\n        PublicKeyResponse pkr = secretsApi.getPublicKey(newOrgName, newSecretName);\n\n        assertNotNull(pkr);\n        assertNotNull(pkr.getPublicKey());\n\n        SecretEntryV2 secret = new SecretsV2Api(getApiClient()).getSecret(newOrgName, newSecretName);\n\n        assertNotNull(secret);\n        assertEquals(cur.getId(), secret.getOwner().getId());\n        assertTrue(secret.getProjects() == null || secret.getProjects().isEmpty());\n        assertEquals(SecretEntryV2.VisibilityEnum.PRIVATE, secret.getVisibility());\n\n        // ---\n\n        orgApi.deleteOrg(orgNameInit, \"yes\");\n        orgApi.deleteOrg(newOrgName, \"yes\");\n    }\n\n    @Test\n    public void testSecretPasswordUpdate() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        String initPassword = \"q1q1Q1Q1\";\n\n        String secretName = \"secret_\" + randomString();\n        generateKeyPair(orgName, null, secretName, false, initPassword);\n\n        // ---\n        String newPassword = \"q2q2Q2Q2\";\n\n        com.walmartlabs.concord.client2.UpdateSecretRequest request = com.walmartlabs.concord.client2.UpdateSecretRequest.builder()\n                .currentPassword(initPassword)\n                .newPassword(newPassword)\n                .build();\n\n        // ---\n\n        com.walmartlabs.concord.client2.SecretClient secretsApi = new com.walmartlabs.concord.client2.SecretClient(getApiClient());\n        secretsApi.updateSecret(orgName, secretName, request);\n\n        KeyPair kp = secretsApi.getData(orgName, secretName, newPassword, SecretEntryV2.TypeEnum.KEY_PAIR);\n\n        assertNotNull(kp);\n    }\n\n    @Test\n    public void testSecretDataUpdate() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        String secretName = \"secret_\" + randomString();\n        generateKeyPair(orgName, null, secretName, false, null);\n\n        // ---\n\n        com.walmartlabs.concord.client2.UpdateSecretRequest request = com.walmartlabs.concord.client2.UpdateSecretRequest.builder()\n                .usernamePassword(CreateSecretRequest.UsernamePassword.of(\"test\", \"q1\"))\n                .build();\n\n        com.walmartlabs.concord.client2.SecretClient secretClient = new com.walmartlabs.concord.client2.SecretClient(getApiClient());\n        secretClient.updateSecret(orgName, secretName, request);\n\n        UsernamePassword up = secretClient.getData(orgName, secretName, null, SecretEntryV2.TypeEnum.USERNAME_PASSWORD);\n\n        assertNotNull(up);\n        assertEquals(\"test\", up.getUsername());\n        assertArrayEquals(\"q1\".toCharArray(), up.getPassword());\n    }\n\n    @Test\n    public void testUpdateNonUniqueName() throws Exception {\n        String orgName1 = \"org_\" + randomString();\n        String orgName2 = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName1));\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName2));\n\n        // ---\n\n        String secretName = \"secret_\" + randomString();\n        addUsernamePassword(orgName1, secretName, false, null, \"test\", \"q1\");\n        addUsernamePassword(orgName2, secretName, false, null, \"test\", \"q1\");\n\n        com.walmartlabs.concord.client2.UpdateSecretRequest request = com.walmartlabs.concord.client2.UpdateSecretRequest.builder()\n                .newOrgName(orgName2)\n                .build();\n\n        com.walmartlabs.concord.client2.SecretClient secretClient = new com.walmartlabs.concord.client2.SecretClient(getApiClient());\n        ApiException exception = Assertions.assertThrows(ApiException.class,\n                () -> secretClient.updateSecret(orgName1, secretName, request));\n        assertThat(exception.getMessage(), containsString(\"Secret already exists\"));\n    }\n\n    @Test\n    public void testCreateSecretWithMultipleProjectIds() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName1 = \"project_\" + randomString();\n        String projectName2 = \"proejct_\" + randomString();\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        ProjectOperationResponse response1 = projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName1));\n        ProjectOperationResponse response2 = projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName2));\n\n        // ---\n\n        String secretName = \"secret_\" + randomString();\n        SecretOperationResponse secretResponse = generateKeyPairWithProjectIds(orgName, new HashSet<>(Arrays.asList(response1.getId(), response2.getId())), secretName, false, null);\n        assertEquals(secretResponse.getResult().toString(), \"CREATED\");\n        SecretsApi secretsApi = new SecretsApi(getApiClient());\n        SecretsV2Api secretsV2Api = new SecretsV2Api(getApiClient());\n        SecretEntryV2 secretEntry = secretsV2Api.getSecret(orgName, secretName);\n        assertTrue(secretEntry.getProjects().stream().map(ProjectEntry::getName).anyMatch(projectName -> projectName.equals(projectName1)));\n        assertTrue(secretEntry.getProjects().stream().map(ProjectEntry::getName).anyMatch(projectName -> projectName.equals(projectName2)));\n\n\n        projectsApi.deleteProject(orgName, projectName1);\n        projectsApi.deleteProject(orgName, projectName2);\n        secretsApi.delete(orgName, secretName);\n        orgApi.deleteOrg(orgName, \"yes\");\n    }\n\n    @Test\n    public void testCreateSecretWithMultipleProjectNames() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName1 = \"project_\" + randomString();\n        String projectName2 = \"proejct_\" + randomString();\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        ProjectOperationResponse response1 = projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName1));\n        ProjectOperationResponse response2 = projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName2));\n\n        // ---\n\n        String secretName = \"secret_\" + randomString();\n        SecretOperationResponse secretResponse = generateKeyPairWithProjectNames(orgName, new HashSet<>(Arrays.asList(projectName1, projectName2)), secretName, false, null);\n        assertEquals(secretResponse.getResult().toString(), \"CREATED\");\n\n        SecretsApi secretsApi = new SecretsApi(getApiClient());\n        SecretsV2Api secretsV2Api = new SecretsV2Api(getApiClient());\n        SecretEntryV2 secretEntry = secretsV2Api.getSecret(orgName, secretName);\n        assertTrue(secretEntry.getProjects().stream().map(ProjectEntry::getName).anyMatch(projectName -> projectName.equals(projectName1)));\n        assertTrue(secretEntry.getProjects().stream().map(ProjectEntry::getName).anyMatch(projectName -> projectName.equals(projectName2)));\n\n        projectsApi.deleteProject(orgName, projectName1);\n        projectsApi.deleteProject(orgName, projectName2);\n        secretsApi.delete(orgName, secretName);\n        orgApi.deleteOrg(orgName, \"yes\");\n    }\n\n\n    @Test\n    public void testUpdateSecretWithMultipleProjectNames() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        String secretName = \"secret_\" + randomString();\n        SecretOperationResponse secretResponse = generateKeyPair(orgName, secretName, false, null);\n        assertEquals(secretResponse.getResult().toString(), \"CREATED\");\n\n        String projectName1 = \"project_\" + randomString();\n        String projectName2 = \"proejct_\" + randomString();\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        ProjectOperationResponse response1 = projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName1));\n        ProjectOperationResponse response2 = projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName2));\n\n        com.walmartlabs.concord.client2.SecretClient secretClient = new com.walmartlabs.concord.client2.SecretClient(getApiClient());\n        com.walmartlabs.concord.client2.UpdateSecretRequest request = UpdateSecretRequest.builder()\n                .newProjectIds(new HashSet<>(Arrays.asList(response1.getId(), response2.getId())))\n                .build();\n        secretClient.updateSecret(orgName, secretName, request);\n\n\n        SecretsApi secretsApi = new SecretsApi(getApiClient());\n        SecretsV2Api secretsV2Api = new SecretsV2Api(getApiClient());\n        SecretEntryV2 secretEntry = secretsV2Api.getSecret(orgName, secretName);\n        assertTrue(secretEntry.getProjects().stream().map(ProjectEntry::getName).anyMatch(projectName -> projectName.equals(projectName1)));\n        assertTrue(secretEntry.getProjects().stream().map(ProjectEntry::getName).anyMatch(projectName -> projectName.equals(projectName2)));\n\n        projectsApi.deleteProject(orgName, projectName1);\n        projectsApi.deleteProject(orgName, projectName2);\n        secretsApi.delete(orgName, secretName);\n        orgApi.deleteOrg(orgName, \"yes\");\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/SecretProjectsIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.*;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class SecretProjectsIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        String orgName = \"org_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        String projectName1 = \"project_\" + randomString();\n        String projectName2 = \"project_\" + randomString();\n        String projectName3 = \"project_\" + randomString();\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName1).rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName2).rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName3).rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        String secretName = \"secret_\" + randomString();\n        String storePassword = \"St0rePassword1\";\n        byte[] secret = \"C0nC0rD\".getBytes();\n        addPlainSecretWithProjectNames(orgName, secretName, new HashSet<>(Arrays.asList(projectName1, projectName2)), false, storePassword, secret);\n\n        byte[] payload = archive(SecretProjectsIT.class.getResource(\"secretProjects\").toURI());\n\n        // Creating non admin user\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n\n        String userName = \"userA_\" + randomString();\n        CreateUserResponse userResponse = usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n        CreateApiKeyResponse apiKeyResponse = apiKeyResource.createUserApiKey(new CreateApiKeyRequest()\n                .username(userName)\n                .userType(CreateApiKeyRequest.UserTypeEnum.LOCAL));\n\n        String teamName = \"team_\" + randomString();\n        teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamName));\n        teamsApi.addUsersToTeam(orgName, teamName, false, Collections.singletonList(new TeamUserEntry()\n                .username(userName)\n                .role(TeamUserEntry.RoleEnum.MEMBER)));\n\n        resetApiKey();\n        setApiKey(apiKeyResponse.getKey());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"arguments.secretName\", secretName);\n        input.put(\"arguments.storePassword\", storePassword);\n        input.put(\"arguments.orgName\", orgName);\n        input.put(\"project\", projectName3);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertNotNull(pir.getLogFileName());\n\n        byte[] bytes = getLog(pir.getInstanceId());\n        assertLog(\".*Project-scoped secrets can only be accessed within the project they belong to.*\", 2, bytes);\n\n        input.put(\"project\", projectName2);\n        spr = start(input);\n\n        pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertNotNull(pir.getLogFileName());\n\n        bytes = getLog(pir.getInstanceId());\n        assertLog(\".*C0nC0rD.*\", bytes);\n\n        input.put(\"project\", projectName1);\n        spr = start(input);\n\n        pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertNotNull(pir.getLogFileName());\n\n        bytes = getLog(pir.getInstanceId());\n        assertLog(\".*C0nC0rD.*\", bytes);\n\n        resetApiKey();\n        setApiKey(DEFAULT_API_KEY);\n        projectsApi.deleteProject(orgName, projectName1);\n        projectsApi.deleteProject(orgName, projectName2);\n        projectsApi.deleteProject(orgName, projectName3);\n\n        SecretsApi secretsApi = new SecretsApi(getApiClient());\n        secretsApi.delete(orgName, secretName);\n\n        usersApi.deleteUser(userResponse.getId());\n        teamsApi.deleteTeam(orgName, teamName);\n        orgApi.deleteOrg(orgName, \"yes\");\n\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/SecretsTaskIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class\nSecretsTaskIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n\n        String orgName = \"Default\";\n        String projectName1 = \"project_\"+ randomString();\n        String projectName2 = \"project_\" + randomString();\n\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName2).rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName1).rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        String secretName = \"secret_\" + randomString();\n\n        byte[] payload = archive(SecretsTaskIT.class.getResource(\"secretsTask\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName1);\n        input.put(\"arguments.secretName\", secretName);\n        input.put(\"arguments.projectName1\", projectName1);\n        input.put(\"arguments.projectName2\", projectName2);\n        input.put(\"arguments.orgName\", orgName);\n\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertNotNull(pir.getLogFileName());\n\n        byte[] bytes = getLog(pir.getInstanceId());\n        // System.out.println(new String(bytes));\n        assertLog(\".* Delete secret2.*\", bytes);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/SerializationIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class SerializationIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        byte[] payload = archive(SerializationIT.class.getResource(\"serialization\").toURI());\n\n        // ---\n\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        // ---\n\n        ProcessEntry pe = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n        assertEquals(ProcessEntry.StatusEnum.SUSPENDED, pe.getStatus());\n\n        // ---\n\n        ProcessFormsApi formsApi = new ProcessFormsApi(getApiClient());\n        List<FormListEntry> forms = formsApi.listProcessForms(spr.getInstanceId());\n        assertEquals(1, forms.size());\n\n        FormListEntry f = forms.get(0);\n        formsApi.submitForm(spr.getInstanceId(), f.getName(),\n                Collections.singletonMap(\"y\", \"hello\"));\n\n        // ---\n\n        pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        byte[] ab = getLog(pe.getInstanceId());\n\n        assertLog(\".*hello.*\", ab);\n    }\n\n    @Test\n    public void testNonSerializable() throws Exception {\n        byte[] payload = archive(SerializationIT.class.getResource(\"nonSerializableTest\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"request\", Collections.singletonMap(\"dependencies\",\n                new String[]{\"mvn://com.walmartlabs.concord.it.tasks:serialization-test:\" + ITConstants.PROJECT_VERSION}));\n\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.FAILED);\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Not serializable value: test.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/SimpleIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport java.net.URL;\nimport java.net.URLConnection;\n\npublic class SimpleIT {\n\n    @Test\n    public void test() throws Exception {\n        URL url = new URL(ITConstants.SERVER_URL + \"/api/v1/server/ping\");\n        URLConnection conn = url.openConnection();\n        System.out.println(conn.getContent());\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/SmtpIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.icegreen.greenmail.junit5.GreenMailExtension;\nimport com.icegreen.greenmail.util.ServerSetup;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport javax.mail.internet.MimeMessage;\nimport java.net.URI;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class SmtpIT extends AbstractServerIT {\n\n    @RegisterExtension\n    GreenMailExtension mail = new GreenMailExtension(new ServerSetup(0, \"0.0.0.0\", ServerSetup.PROTOCOL_SMTP));\n\n    @Test\n    public void testSimple() throws Exception {\n        URI dir = SmtpIT.class.getResource(\"smtp\").toURI();\n        byte[] payload = archive(dir);\n\n        // --\n\n        String orgName = \"Default\";\n        String projectName = \"project_\" + randomString();\n\n        Map<String, Object> smtpParams = new HashMap<>();\n        smtpParams.put(\"host\", ITConstants.SMTP_SERVER_HOST);\n        smtpParams.put(\"port\", mail.getSmtp().getPort());\n\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"smtpParams\", smtpParams);\n\n        Map<String, Object> cfg = new HashMap<>();\n        cfg.put(Constants.Request.ARGUMENTS_KEY, args);\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .cfg(cfg)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // --\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"archive\", payload);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        MimeMessage[] messages = mail.getReceivedMessages();\n        assertNotNull(messages);\n        assertEquals(1, messages.length);\n\n        MimeMessage msg = messages[0];\n        assertEquals(\"hi!\\r\\n\", msg.getContent());\n        assertEquals(\"me@localhost\", msg.getFrom()[0].toString());\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/StandardAuthenticationHandlersIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.junit.jupiter.api.Test;\n\nimport java.net.HttpURLConnection;\nimport java.net.URI;\nimport java.net.URL;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class StandardAuthenticationHandlersIT extends AbstractServerIT {\n\n    @Test\n    public void testBearerToken() throws Exception {\n        URL urlObj = new URL(ITConstants.SERVER_URL + \"/api/v1/org?limit=1\");\n        HttpURLConnection httpCon = (HttpURLConnection) urlObj.openConnection();\n\n        httpCon.setRequestProperty(\"Authorization\", \"Bearer \" + DEFAULT_API_KEY);\n\n        int responseCode = httpCon.getResponseCode();\n        assertEquals(HttpURLConnection.HTTP_OK, responseCode);\n    }\n\n    @Test\n    public void testSessionTokenAsUsername() throws Exception {\n        URI dir = SuspendIT.class.getResource(\"sessionTokenAsUsername\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        String targetUrl = getApiClient().getBaseUri() + \"/api/v1/org?limit=1\";\n        StartProcessResponse spr = start(Map.of(\n                \"archive\", payload,\n                \"arguments.targetUrl\", targetUrl));\n\n        // ---\n\n        ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.FINISHED);\n        assertLog(\".*statusCode=200.*\", getLog(pir.getInstanceId()));\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/SuspendIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessApi;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.junit.jupiter.api.Test;\n\nimport java.net.URI;\nimport java.util.*;\nimport java.util.regex.Pattern;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class SuspendIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        URI dir = SuspendIT.class.getResource(\"suspend\").toURI();\n        byte[] payload = archive(dir);\n\n        // ---\n\n        ProcessApi processApi = new ProcessApi(getApiClient());\n        StartProcessResponse spr = start(payload);\n\n        // ---\n\n        ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*aaaa.*\", ab);\n\n        // ---\n\n        String testValue = \"test#\" + randomString();\n        Map<String, Object> args = Collections.singletonMap(\"testValue\", testValue);\n        Map<String, Object> req = Collections.singletonMap(Constants.Request.ARGUMENTS_KEY, args);\n\n        processApi.resume(spr.getInstanceId(), \"ev1\", null, req);\n\n        pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        waitForLog(pir.getInstanceId(), \".*bbbb.*\");\n        waitForLog(pir.getInstanceId(), \".*\" + Pattern.quote(testValue) + \".*\");\n    }\n\n    @Test\n    public void testSuspendForCompletion() throws Exception {\n\n        // ---\n        byte[] payload = archive(SuspendIT.class.getResource(\"suspendForCompletion\").toURI());\n\n        StartProcessResponse parentSpr = start(payload);\n\n        // ---\n\n        ProcessEntry p = waitForStatus(getApiClient(), parentSpr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n        Set<UUID> childrenIds = p.getChildrenIds();\n        assertEquals(2, childrenIds.size());\n\n        for(UUID childId : childrenIds) {\n            waitForCompletion(getApiClient(), childId);\n        }\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), parentSpr.getInstanceId());\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*process is resumed.*\", ab);\n    }\n\n    @Test\n    public void testSuspendForForkedProcess() throws Exception {\n\n        // ---\n        byte[] payload = archive(SuspendIT.class.getResource(\"suspendForForkedProcesses\").toURI());\n\n        StartProcessResponse parentSpr = start(payload);\n\n        // ---\n\n        ProcessEntry p = waitForStatus(getApiClient(), parentSpr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n        Set<UUID> childrenIds = p.getChildrenIds();\n        assertEquals(3, childrenIds.size());\n\n        for(UUID childId : childrenIds) {\n            waitForCompletion(getApiClient(), childId);\n        }\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), parentSpr.getInstanceId());\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*task completed.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/TaskRetryIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessApi;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.junit.jupiter.api.Test;\n\nimport java.net.URI;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class TaskRetryIT extends AbstractServerIT {\n\n    @Test\n    public void testAnsibleRetry() throws Exception {\n        URI uri = TaskRetryIT.class.getResource(\"taskRetry\").toURI();\n        byte[] payload = archive(uri);\n\n        // start the process\n\n        StartProcessResponse spr = start(payload);\n\n        // wait for completion\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // check logs\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*msg\\\": \\\"Hi retry!\\\".*\", ab);\n    }\n\n    @Test\n    public void testAnsibleRetryWithExpression() throws Exception {\n        URI uri = TaskRetryIT.class.getResource(\"taskRetryWithExpression\").toURI();\n        byte[] payload = archive(uri);\n\n        // start the process\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.retryCount\", \"1\");\n        input.put(\"arguments.retryDelay\", \"2\");\n        StartProcessResponse spr = start(input);\n\n        // wait for completion\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // check logs\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*msg\\\": \\\"Hi retry!\\\".*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/TeamRbacIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class TeamRbacIT extends AbstractServerIT {\n\n    @Test\n    public void testOrgs() throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgAName = \"orgA_\" + randomString();\n        CreateOrganizationResponse orgA = orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgAName));\n\n        String orgBName = \"orgB_\" + randomString();\n        CreateOrganizationResponse orgB = orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgBName));\n\n        // ---\n\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n\n        String teamAName = \"teamA_\" + randomString();\n        teamsApi.createOrUpdateTeam(orgAName, new TeamEntry().name(teamAName));\n\n        String teamBName = \"teamB_\" + randomString();\n        teamsApi.createOrUpdateTeam(orgBName, new TeamEntry().name(teamBName));\n\n        // ---\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n\n        String userAName = \"userA_\" + randomString();\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userAName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n        CreateApiKeyResponse apiKeyA = apiKeyResource.createUserApiKey(new CreateApiKeyRequest()\n                .username(userAName)\n                .userType(CreateApiKeyRequest.UserTypeEnum.LOCAL));\n\n        teamsApi.addUsersToTeam(orgAName, teamAName, false, Collections.singletonList(new TeamUserEntry()\n                .username(userAName)\n                .role(TeamUserEntry.RoleEnum.MEMBER)));\n\n        String userBName = \"userB_\" + randomString();\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userBName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n        CreateApiKeyResponse apiKeyB = apiKeyResource.createUserApiKey(new CreateApiKeyRequest()\n                .username(userBName)\n                .userType(CreateApiKeyRequest.UserTypeEnum.LOCAL));\n\n        teamsApi.addUsersToTeam(orgBName, teamBName, false, Collections.singletonList(new TeamUserEntry()\n                .username(userBName)\n                .role(TeamUserEntry.RoleEnum.MEMBER)));\n\n        // ---\n\n\n        setApiKey(apiKeyA.getKey());\n\n        String projectAName = \"projectA_\" + randomString();\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgAName, new ProjectEntry().name(projectAName));\n\n        try {\n            String projectBName = \"projectB_\" + randomString();\n            projectsApi.createOrUpdateProject(orgBName, new ProjectEntry().name(projectBName));\n            fail(\"should fail\");\n        } catch (ApiException e) {\n        }\n\n        setApiKey(apiKeyB.getKey());\n\n        projectsApi = new ProjectsApi(getApiClient());\n        String projectBName = \"projectB_\" + randomString();\n        projectsApi.createOrUpdateProject(orgBName, new ProjectEntry().name(projectBName));\n    }\n\n    @Test\n    public void testTeamCreators() throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgName = \"orgA_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n\n        String teamAName = \"teamA_\" + randomString();\n        teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamAName));\n\n        // ---\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n\n        String userAName = \"userA_\" + randomString();\n        usersApi.createOrUpdateUser(new CreateUserRequest().username(userAName).type(CreateUserRequest.TypeEnum.LOCAL));\n        CreateApiKeyResponse apiKeyA = apiKeyResource.createUserApiKey(new CreateApiKeyRequest()\n                .username(userAName)\n                .userType(CreateApiKeyRequest.UserTypeEnum.LOCAL));\n\n        // ---\n\n        setApiKey(apiKeyA.getKey());\n\n        try {\n            teamsApi.createOrUpdateTeam(orgName, new TeamEntry()\n                    .name(teamAName)\n                    .description(\"test\"));\n            fail(\"Should fail\");\n        } catch (ApiException e) {\n        }\n\n        // ---\n\n        resetApiKey();\n\n        teamsApi.addUsersToTeam(orgName, teamAName, false, Collections.singletonList(new TeamUserEntry()\n                .username(userAName)\n                .role(TeamUserEntry.RoleEnum.MAINTAINER)));\n\n        // ---\n\n        setApiKey(apiKeyA.getKey());\n\n        teamsApi.createOrUpdateTeam(orgName, new TeamEntry()\n                .name(teamAName)\n                .description(\"test\"));\n\n        // ---\n\n        String teamBName = \"teamB_\" + randomString();\n\n        try {\n            teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamBName));\n            fail(\"Should fail\");\n        } catch (ApiException e) {\n        }\n\n        // ---\n\n        resetApiKey();\n\n        teamsApi.addUsersToTeam(orgName, teamAName, false, Collections.singletonList(new TeamUserEntry()\n                .username(userAName)\n                .role(TeamUserEntry.RoleEnum.OWNER)));\n\n        // ---\n\n        teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamBName));\n    }\n\n    @Test\n    public void testTeamMaintainers() throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgName = \"orgA_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n\n        String teamName = \"teamA_\" + randomString();\n        teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamName));\n\n        // ---\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n\n        String userAName = \"userA_\" + randomString();\n        usersApi.createOrUpdateUser(new CreateUserRequest().username(userAName).type(CreateUserRequest.TypeEnum.LOCAL));\n        CreateApiKeyResponse apiKeyA = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userAName));\n\n        teamsApi.addUsersToTeam(orgName, teamName, false, Collections.singletonList(new TeamUserEntry()\n                .username(userAName)\n                .role(TeamUserEntry.RoleEnum.MAINTAINER)));\n\n        String userBName = \"userB_\" + randomString();\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userBName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n        CreateApiKeyResponse apiKeyB = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userBName));\n\n        // ---\n\n        setApiKey(apiKeyB.getKey());\n\n        try {\n            teamsApi.addUsersToTeam(orgName, teamName, false, Collections.singletonList(new TeamUserEntry()\n                    .username(userBName)\n                    .role(TeamUserEntry.RoleEnum.MEMBER)));\n            fail(\"should fail\");\n        } catch (ApiException e) {\n        }\n\n        // ---\n\n        setApiKey(apiKeyA.getKey());\n        teamsApi.addUsersToTeam(orgName, teamName, false, Collections.singletonList(new TeamUserEntry()\n                .username(userBName)\n                .role(TeamUserEntry.RoleEnum.MEMBER)));\n    }\n\n    @Test\n    public void testNewTeamOwner() throws Exception {\n        String userA = \"userA_\" + randomString();\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n\n        usersApi.createOrUpdateUser(new CreateUserRequest().username(userA).type(CreateUserRequest.TypeEnum.LOCAL));\n        CreateApiKeyResponse userAKey = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userA));\n\n        String userB = \"userA_\" + randomString();\n\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userB)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n        CreateApiKeyResponse userBKey = apiKeyResource.createUserApiKey(new CreateApiKeyRequest()\n                .username(userB));\n\n        // ---\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgName = \"orgA_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n        teamsApi.addUsersToTeam(orgName, \"default\", false, Collections.singletonList(new TeamUserEntry()\n                .username(userA)\n                .role(TeamUserEntry.RoleEnum.OWNER)));\n\n        // ---\n\n        setApiKey(userBKey.getKey());\n\n        // ---\n\n        String teamName = \"teamA_\" + randomString();\n        try {\n            teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamName));\n            fail(\"Should fail\");\n        } catch (ApiException e) {\n        }\n\n        // ---\n\n        setApiKey(userAKey.getKey());\n\n        // ---\n\n        teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamName));\n        teamsApi.addUsersToTeam(orgName, teamName, false, Collections.singletonList(new TeamUserEntry()\n                .username(userB)\n                .role(TeamUserEntry.RoleEnum.MEMBER)));\n    }\n\n    @Test\n    public void testTeamDelete() throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgName = \"orgA_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n\n        String teamName = \"teamA_\" + randomString();\n        teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamName));\n\n        // ---\n\n        teamsApi.addUsersToTeam(orgName, teamName, false, Collections.singletonList(new TeamUserEntry()\n                .username(\"admin\")\n                .role(TeamUserEntry.RoleEnum.OWNER)));\n\n        // ---\n\n        List<TeamEntry> l = teamsApi.listTeams(orgName);\n        assertEquals(2, l.size());\n\n        // ---\n\n        teamsApi.deleteTeam(orgName, teamName);\n\n        // ---\n\n        l = teamsApi.listTeams(orgName);\n        assertEquals(1, l.size());\n    }\n\n    @Test\n    public void testOrgProjects() throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgName = \"orgA_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n\n        String teamName = \"teamA_\" + randomString();\n        CreateTeamResponse ctr = teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamName));\n\n        // ---\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n\n        String userAName = \"userA_\" + randomString();\n        usersApi.createOrUpdateUser(new CreateUserRequest().username(userAName).type(CreateUserRequest.TypeEnum.LOCAL));\n        CreateApiKeyResponse apiKeyA = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userAName));\n\n        String userBName = \"userB_\" + randomString();\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userBName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n        CreateApiKeyResponse apiKeyB = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userBName));\n\n        // ---\n\n        setApiKey(apiKeyA.getKey());\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n\n        String projectName = \"projectA_\" + randomString();\n        try {\n            projectsApi.createOrUpdateProject(orgName, new ProjectEntry().name(projectName));\n            fail(\"should fail\");\n        } catch (ApiException e) {\n        }\n\n        // ---\n\n        resetApiKey();\n        teamsApi.addUsersToTeam(orgName, teamName, false, Collections.singletonList(new TeamUserEntry()\n                .username(userAName)\n                .role(TeamUserEntry.RoleEnum.MEMBER)));\n\n        // ---\n\n        setApiKey(apiKeyA.getKey());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry().name(projectName));\n\n        // ---\n\n        setApiKey(apiKeyB.getKey());\n\n        try {\n            projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                    .name(projectName)\n                    .description(\"new description\")\n                    .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n            fail(\"should fail\");\n        } catch (ApiException e) {\n        }\n\n        // ---\n\n        setApiKey(apiKeyA.getKey());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .description(\"new description\")\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n\n        setApiKey(apiKeyA.getKey());\n        projectsApi.updateProjectAccessLevel(orgName, projectName, new ResourceAccessEntry()\n                .teamId(ctr.getId())\n                .orgName(orgName)\n                .teamName(teamName)\n                .level(ResourceAccessEntry.LevelEnum.WRITER));\n\n        // ---\n\n        setApiKey(apiKeyB.getKey());\n\n        try {\n            projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                    .name(projectName)\n                    .description(\"another description\")\n                    .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n            fail(\"should fail\");\n        } catch (ApiException e) {\n        }\n\n        // ---\n\n        resetApiKey();\n        teamsApi.addUsersToTeam(orgName, teamName, false, Collections.singletonList(new TeamUserEntry()\n                .username(userBName)\n                .role(TeamUserEntry.RoleEnum.MEMBER)));\n\n        // ---\n\n        setApiKey(apiKeyB.getKey());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .description(\"another description\")\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n    }\n\n    @Test\n    public void testOrgPublicSecrets() throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n\n        String orgAName = \"orgA_\" + randomString();\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgAName));\n\n        // ---\n\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n\n        String teamAName = \"teamA_\" + randomString();\n        teamsApi.createOrUpdateTeam(orgAName, new TeamEntry().name(teamAName));\n\n        // ---\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n\n        String userAName = \"userA_\" + randomString();\n        usersApi.createOrUpdateUser(new CreateUserRequest().username(userAName).type(CreateUserRequest.TypeEnum.LOCAL));\n        CreateApiKeyResponse apiKeyA = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userAName));\n\n        String userBName = \"userB_\" + randomString();\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userBName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n        CreateApiKeyResponse apiKeyB = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userBName));\n\n        // ---\n\n        setApiKey(apiKeyA.getKey());\n\n        String secretAName = \"secretA_\" + randomString();\n        try {\n            generateKeyPair(orgAName, secretAName, false, null);\n            fail(\"should fail\");\n        } catch (ApiException e) {\n        }\n\n        // ---\n\n        resetApiKey();\n        teamsApi.addUsersToTeam(orgAName, teamAName, false, Collections.singletonList(new TeamUserEntry()\n                .username(userAName)\n                .role(TeamUserEntry.RoleEnum.MEMBER)));\n\n        // ---\n\n        setApiKey(apiKeyA.getKey());\n        generateKeyPair(orgAName, secretAName, false, null);\n\n        // ---\n\n        SecretsApi secretResource = new SecretsApi(getApiClient());\n\n        setApiKey(apiKeyB.getKey());\n        secretResource.getPublicKey(orgAName, secretAName);\n\n        // ---\n\n        setApiKey(apiKeyB.getKey());\n\n        try {\n            secretResource.delete(orgAName, secretAName);\n            fail(\"should fail\");\n        } catch (ApiException e) {\n        }\n\n        // ---\n\n        setApiKey(apiKeyA.getKey());\n        secretResource.delete(orgAName, secretAName);\n    }\n\n    @Test\n    public void testInventory() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        String teamName = \"teamA_\" + randomString();\n\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n        teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamName));\n\n        // ---\n\n        String inventoryName = \"inv_\" + randomString();\n\n        InventoriesApi inventoryResource = new InventoriesApi(getApiClient());\n        inventoryResource.createOrUpdateInventory(orgName, new InventoryEntry()\n                .name(inventoryName)\n                .visibility(InventoryEntry.VisibilityEnum.PRIVATE));\n\n        // ---\n\n        inventoryResource.updateInventoryAccessLevel(orgName, inventoryName, new ResourceAccessEntry()\n                .orgName(orgName)\n                .teamName(teamName)\n                .level(ResourceAccessEntry.LevelEnum.READER));\n\n        // ---\n\n        String userAName = \"userA_\" + randomString();\n        String userBName = \"userB_\" + randomString();\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest().username(userAName).type(CreateUserRequest.TypeEnum.LOCAL));\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userBName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        // ---\n\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse cakrA = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userAName));\n        CreateApiKeyResponse cakrB = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userBName));\n\n        // ---\n\n        teamsApi.addUsersToTeam(orgName, teamName, false, Collections.singletonList(new TeamUserEntry()\n                .username(userAName)\n                .role(TeamUserEntry.RoleEnum.MEMBER)));\n\n        // ---\n\n        setApiKey(cakrA.getKey());\n\n        inventoryResource.getInventory(orgName, inventoryName);\n\n        // ---\n\n        setApiKey(cakrB.getKey());\n\n        try {\n            inventoryResource.getInventory(orgName, inventoryName);\n            fail(\"Should fail\");\n        } catch (ApiException e) {\n        }\n\n        // ---\n\n        resetApiKey();\n\n        teamsApi.addUsersToTeam(orgName, teamName, false, Collections.singletonList(new TeamUserEntry()\n                .username(userBName)\n                .role(TeamUserEntry.RoleEnum.MEMBER)));\n\n        // ---\n\n        setApiKey(cakrB.getKey());\n\n        inventoryResource.getInventory(orgName, inventoryName);\n    }\n\n    @Test\n    public void testTeamUsersUpsert() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        String teamName = \"team_\" + randomString();\n\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n        teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamName));\n\n        // ---\n\n        String username = \"user_\" + randomString();\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(username)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        // ---\n\n        teamsApi.addUsersToTeam(orgName, teamName, false, Collections.singletonList(new TeamUserEntry()\n                .username(username)\n                .role(TeamUserEntry.RoleEnum.MEMBER)));\n        teamsApi.addUsersToTeam(orgName, teamName, false, Collections.singletonList(new TeamUserEntry()\n                .username(username)\n                .role(TeamUserEntry.RoleEnum.MAINTAINER)));\n    }\n\n    @Test\n    public void testSecretAccessLevels() throws Exception {\n        SecretsApi secretResource = new SecretsApi(getApiClient());\n\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        String secretName = \"secret_\" + randomString();\n        SecretOperationResponse sor = addPlainSecret(orgName, secretName, false, null, new byte[]{0, 1, 2});\n        secretResource.updateSecretV1(orgName, secretName, new SecretUpdateRequest()\n                .id(sor.getId())\n                .visibility(SecretUpdateRequest.VisibilityEnum.PRIVATE));\n\n        // ---\n\n        String username = \"user_\" + randomString();\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(username)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse cakr = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(username));\n\n        // ---\n\n        setApiKey(cakr.getKey());\n\n        try {\n            new SecretsV2Api(getApiClient()).getSecret(orgName, secretName);\n            fail(\"Should fail\");\n        } catch (ApiException e) {\n        }\n\n        // ---\n\n        resetApiKey();\n\n        // ---\n\n        String teamName = \"team_\" + randomString();\n\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n        teamsApi.createOrUpdateTeam(orgName, new TeamEntry().name(teamName));\n\n        teamsApi.addUsersToTeam(orgName, teamName, false, Collections.singletonList(new TeamUserEntry()\n                .username(username)\n                .role(TeamUserEntry.RoleEnum.MEMBER)));\n\n        // ---\n\n        setApiKey(cakr.getKey());\n\n        try {\n            new SecretsV2Api(getApiClient()).getSecret(orgName, secretName);\n            fail(\"Should fail\");\n        } catch (ApiException e) {\n        }\n\n        // ---\n\n        resetApiKey();\n\n        secretResource.updateSecretAccessLevel(orgName, secretName, new ResourceAccessEntry()\n                .teamName(teamName)\n                .level(ResourceAccessEntry.LevelEnum.READER));\n\n        // ---\n\n        setApiKey(cakr.getKey());\n\n        SecretEntryV2 s = new SecretsV2Api(getApiClient()).getSecret(orgName, secretName);\n        assertEquals(secretName, s.getName());\n\n        try {\n            secretResource.delete(orgName, secretName);\n            fail(\"Should fail\");\n        } catch (ApiException e) {\n        }\n\n        // ---\n\n        resetApiKey();\n\n        secretResource.updateSecretAccessLevel(orgName, secretName, new ResourceAccessEntry()\n                .teamName(teamName)\n                .level(ResourceAccessEntry.LevelEnum.OWNER));\n\n        // ---\n\n        setApiKey(cakr.getKey());\n\n        GenericOperationResult r = secretResource.delete(orgName, secretName);\n        assertEquals(GenericOperationResult.ResultEnum.DELETED, r.getResult());\n    }\n\n    /**\n     * Public organizations must be visible\n     * regardless of whether the user is in the org or not.\n     */\n    @Test\n    public void testPublicOrgVisibility() throws Exception {\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry()\n                .name(orgName)\n                .visibility(OrganizationEntry.VisibilityEnum.PUBLIC));\n\n        assertTrue(organizationsApi.listOrgs(true, null, null, null).stream().anyMatch(o -> o.getName().equals(orgName)));\n\n        // ---\n\n        String userName = \"user_\" + randomString();\n        UsersApi usersApi = new UsersApi(getApiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeysApi = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse cakr = apiKeysApi.createUserApiKey(new CreateApiKeyRequest()\n                .username(userName)\n                .userType(CreateApiKeyRequest.UserTypeEnum.LOCAL));\n\n        setApiKey(cakr.getKey());\n\n        assertTrue(organizationsApi.listOrgs(false, null, null, null).stream().anyMatch(o -> o.getName().equals(orgName)));\n\n        // ---\n\n        resetApiKey();\n        organizationsApi = new OrganizationsApi(getApiClient());\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry()\n                .name(orgName)\n                .visibility(OrganizationEntry.VisibilityEnum.PRIVATE));\n\n        assertTrue(organizationsApi.listOrgs(true, null, null, null).stream().anyMatch(o -> o.getName().equals(orgName)));\n\n        // ---\n\n        setApiKey(cakr.getKey());\n        organizationsApi = new OrganizationsApi(getApiClient());\n        assertFalse(organizationsApi.listOrgs(true, null, null, null).stream().anyMatch(o -> o.getName().equals(orgName)));\n    }\n\n    /**\n     * Organization owners should see the organization and all resources\n     * regardless of whether they're in the org (team) or not.\n     */\n    @Test\n    public void testOwnersVisibility() throws Exception {\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry()\n                .name(orgName)\n                .visibility(OrganizationEntry.VisibilityEnum.PRIVATE));\n\n        // ---\n\n        String userName = \"user_\" + randomString();\n        UsersApi usersApi = new UsersApi(getApiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeysApi = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse cakr = apiKeysApi.createUserApiKey(new CreateApiKeyRequest()\n                .username(userName)\n                .userType(CreateApiKeyRequest.UserTypeEnum.LOCAL));\n\n        setApiKey(cakr.getKey());\n\n        assertFalse(organizationsApi.listOrgs(true, null, null, null).stream().anyMatch(o -> o.getName().equals(orgName)));\n\n        // ---\n\n        resetApiKey();\n\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry()\n                .name(orgName)\n                .owner(new EntityOwner()\n                        .username(userName)\n                        .userType(EntityOwner.UserTypeEnum.LOCAL)));\n\n        // ---\n\n        setApiKey(cakr.getKey());\n\n        assertTrue(organizationsApi.listOrgs(true, null, null, null).stream().anyMatch(o -> o.getName().equals(orgName)));\n\n        // ---\n\n        resetApiKey();\n\n        String projectName = \"project_\" + randomString();\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PRIVATE));\n\n        String secretName = \"secret_\" + randomString();\n        addPlainSecret(orgName, secretName, false, null, \"hello!\".getBytes());\n\n        String jsonStoreName = \"store_\" + randomString();\n        JsonStoreApi jsonStoreApi = new JsonStoreApi(getApiClient());\n        jsonStoreApi.createOrUpdateJsonStore(orgName, new JsonStoreRequest()\n                .name(jsonStoreName)\n                .visibility(JsonStoreRequest.VisibilityEnum.PRIVATE));\n\n        // ---\n\n        setApiKey(cakr.getKey());\n\n        assertTrue(projectsApi.findProjects(orgName, null, null, null).stream().anyMatch(p -> p.getName().equals(projectName)));\n\n        assertTrue(new SecretsV2Api(getApiClient()).listSecrets(orgName, null, null, null).stream().anyMatch(s -> s.getName().equals(secretName)));\n\n        assertTrue(jsonStoreApi.listJsonStores(orgName, null, null, null).stream().anyMatch(p -> p.getName().equals(jsonStoreName)));\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/TemplateIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.ZipUtils;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;\nimport org.eclipse.jgit.api.Git;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class TemplateIT extends AbstractServerIT {\n\n    private static final String MAIN_JS = \"({ entryPoint: \\\"main\\\", arguments: { greeting: \\\"Hello, \\\" + _input.name }})\";\n\n    @Test\n    public void test() throws Exception {\n        final String processYml = \"main:\\n- expr: ${log.info(\\\"test\\\", greeting)}\";\n\n        String templateAlias = \"template_\" + randomString();\n        Path templatePath = createTemplate(processYml, MAIN_JS);\n\n        TemplateAliasApi templateAliasResource = new TemplateAliasApi(getApiClient());\n        templateAliasResource.createOrUpdateTemplate(new TemplateAliasEntry()\n                .alias(templateAlias)\n                .url(templatePath.toUri().toString()));\n\n        // ---\n\n        String orgName = \"Default\";\n        String projectName = \"project_\" + randomString();\n        String myName = \"myName_\" + randomString();\n\n        // ---\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        Map<String, Object> cfg = new HashMap<>();\n        cfg.put(Constants.Request.TEMPLATE_KEY, templateAlias);\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .cfg(cfg)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"name\", myName);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hello, \" + myName + \".*\", ab);\n    }\n\n    @Test\n    public void testInputVariablesStillPresent() throws Exception {\n        final String processYml = \"main:\\n- expr: ${log.info(\\\"test\\\", xxx)}\";\n\n        String templateAlias = \"template_\" + randomString();\n        Path templatePath = createTemplate(processYml, MAIN_JS);\n\n        TemplateAliasApi templateAliasResource = new TemplateAliasApi(getApiClient());\n        templateAliasResource.createOrUpdateTemplate(new TemplateAliasEntry()\n                .alias(templateAlias)\n                .url(templatePath.toUri().toString()));\n\n        // ---\n\n        String orgName = \"Default\";\n        String projectName = \"project_\" + randomString();\n        String myName = \"myName_\" + randomString();\n\n        // ---\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        Map<String, Object> cfg = new HashMap<>();\n        cfg.put(Constants.Request.TEMPLATE_KEY, templateAlias);\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .cfg(cfg)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n        Map<String, Object> args = Collections.singletonMap(Constants.Request.ARGUMENTS_KEY,\n                Collections.singletonMap(\"xxx\", \"BOO\"));\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"name\", myName);\n        input.put(\"request\", args);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*BOO.*\", ab);\n    }\n\n    @Test\n    public void testEntryPointReference() throws Exception {\n        final String processYml = \"fromTemplate:\\n- log: \\\"hello!\\\"\";\n        Path templatePath = createTemplate(processYml, null);\n\n        Path tmpDir = createTempDir();\n\n        File src = new File(TemplateIT.class.getResource(\"repositoryValidationTemplateRef\").toURI());\n        PathUtils.copy(src.toPath(), tmpDir);\n\n        Path concordYml = tmpDir.resolve(\"concord.yml\");\n        String s = new String(Files.readAllBytes(concordYml))\n                .replace(\"{{ template }}\", \"file://\" + templatePath.toAbsolutePath().toString());\n        Files.write(concordYml, s.getBytes());\n\n        try (Git repo = Git.init().setInitialBranch(\"master\").setDirectory(tmpDir.toFile()).call()) {\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"import\").call();\n        }\n\n        String gitUrl = tmpDir.toAbsolutePath().toString();\n\n        // ---\n\n        String orgName = \"org_\" + randomString();\n        String projectName = \"project_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n\n        // ---\n\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .url(gitUrl)\n                        .branch(\"master\"))));\n\n        // ---\n\n        RepositoriesApi repositoriesApi = new RepositoriesApi(getApiClient());\n        RepositoryValidationResponse resp = repositoriesApi.validateRepository(orgName, projectName, repoName);\n        assertTrue(resp.getOk());\n        assertFalse(resp.getWarnings().isEmpty());\n    }\n\n    private static Path createTemplate(String process, String mainJs) throws IOException {\n        Path tmpDir = createTempDir();\n\n        if (mainJs != null) {\n            Path metaPath = tmpDir.resolve(\"_main.js\");\n            Files.write(metaPath, mainJs.getBytes());\n        }\n\n        Path processesPath = tmpDir.resolve(\"flows\");\n        Files.createDirectories(processesPath);\n\n        Path procPath = processesPath.resolve(\"hello.yml\");\n        Files.write(procPath, process.getBytes());\n\n        Path tmpZip = createTempFile(\".zip\");\n        try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(Files.newOutputStream(tmpZip))) {\n            ZipUtils.zip(zip, tmpDir);\n        }\n\n        return tmpZip;\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/TemplateMergeIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class TemplateMergeIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        Path template = createTemplate();\n        byte[] payload = archive(TemplateMergeIT.class.getResource(\"templateMerge/process\").toURI());\n\n        // ---\n\n        String orgName = \"Default\";\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .cfg(Collections.singletonMap(Constants.Request.TEMPLATE_KEY, template.toUri().toString()))\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"archive\", payload);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hello, Concord.*\", ab);\n    }\n\n    private static Path createTemplate() throws Exception {\n        byte[] ab = archive(TemplateMergeIT.class.getResource(\"templateMerge/template\").toURI());\n        Path p = createTempFile(\".zip\");\n        Files.write(p, ab);\n        return p;\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ThrowExceptionTaskIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.junit.jupiter.api.Test;\n\nimport java.net.URI;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class ThrowExceptionTaskIT extends AbstractServerIT {\n\n    @Test\n    public void testThrowException() throws Exception {\n        URI uri = ThrowExceptionTaskIT.class.getResource(\"throwExceptionTask\").toURI();\n        byte[] payload = archive(uri);\n\n        // start the process\n\n        StartProcessResponse spr = start(payload);\n\n        // wait for completion\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FAILED, pir.getStatus());\n\n        // check logs\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Catch that!.*\", 3, ab);\n    }\n\n    @Test\n    public void testThrowExceptionMessage() throws Exception {\n        URI uri = ThrowExceptionTaskIT.class.getResource(\"throwExceptionMessage\").toURI();\n        byte[] payload = archive(uri);\n\n        // start the process\n\n        StartProcessResponse spr = start(payload);\n\n        // wait for completion\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FAILED, pir.getStatus());\n\n        // check logs\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Kaboom!! Error occurred.*\", 2, ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/TimeoutHandlingIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessApi;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.junit.jupiter.api.Test;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\n\npublic class TimeoutHandlingIT extends AbstractServerIT {\n\n    @Test\n    public void testTimeout() throws Exception {\n        ProcessApi processApi = new ProcessApi(getApiClient());\n\n        // prepare the payload\n\n        byte[] payload = archive(TimeoutHandlingIT.class.getResource(\"timeoutHandling\").toURI());\n\n        // start the process and wait for it to fail\n\n        StartProcessResponse spr = start(payload);\n\n        waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.TIMED_OUT);\n\n        // find the child processes\n\n        ProcessEntry child = waitForChild(processApi, spr.getInstanceId(), ProcessEntry.KindEnum.TIMEOUT_HANDLER, ProcessEntry.StatusEnum.FINISHED);\n\n        // check the handler's logs for expected messages\n\n        byte[] ab = getLog(child.getInstanceId());\n        assertLog(\".*projectInfo: \\\\{.*orgName=Default.*\\\\}.*\", ab);\n        assertLog(\".*processInfo: \\\\{.*sessionKey=.*\\\\}.*\", ab);\n        assertLog(\".*initiator: \\\\{.*username=.*\\\\}.*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/TriggerIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.client2.ProcessEntry.StatusEnum;\nimport com.walmartlabs.concord.client2.ProcessListFilter;\nimport com.walmartlabs.concord.common.PathUtils;\nimport org.eclipse.jgit.api.Git;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.net.URL;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\n\npublic class TriggerIT extends AbstractServerIT {\n\n    @Test\n    public void testTriggerProcessStartupFailure() throws Exception {\n        String orgName = \"org_\" + randomString();\n        String projectAName = \"projectA_\" + randomString();\n        String projectBName = \"projectA_\" + randomString();\n\n        ProjectOperationResponse porA = register(orgName, projectAName, \"invalidTriggersBrokenProcess/a\", 1);\n        ProjectOperationResponse porB = register(orgName, projectBName, \"invalidTriggersBrokenProcess/b\", 1);\n\n        // ---\n\n        String policyName = \"policy_\" + randomString();\n\n        PolicyApi policyApi = new PolicyApi(getApiClient());\n        policyApi.createOrUpdatePolicy(new PolicyEntry()\n                .name(policyName)\n                .rules(readPolicy(\"invalidTriggersBrokenProcess/policy.json\")));\n\n        policyApi.linkPolicy(policyName, new PolicyLinkEntry().orgName(orgName));\n\n        // ---\n\n        ExternalEventsApi eventResource = new ExternalEventsApi(getApiClient());\n        eventResource.externalEvent(\"testTrigger2\", Collections.singletonMap(\"x\", \"abc\"));\n\n        // ---\n\n        waitForProcs(porA.getId(), 1, StatusEnum.FAILED);\n        waitForProcs(porB.getId(), 1, StatusEnum.FINISHED);\n    }\n\n    @Test\n    public void testTriggerProfiles() throws Exception {\n        String orgName = \"org_\" + randomString();\n        String projectName = \"project_\" + randomString();\n        ProjectOperationResponse por = register(orgName, projectName, \"triggerActiveProfiles\", 1);\n\n        // ---\n\n        ExternalEventsApi eventResource = new ExternalEventsApi(getApiClient());\n        eventResource.externalEvent(\"testTrigger\", Collections.emptyMap());\n\n        // ---\n\n        List<ProcessEntry> l = waitForProcs(por.getId(), 1, StatusEnum.FINISHED);\n        ProcessEntry pe = l.get(0);\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLog(\".*Hello, Concord.*\", ab);\n    }\n\n    private ProjectOperationResponse register(String orgName, String projectName, String repoResource, int expectedTriggerCount) throws Exception {\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        if (orgApi.getOrg(orgName) == null) {\n            orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n        }\n\n        // ---\n\n        String repoName = \"repo_\" + randomString();\n\n        ProjectOperationResponse por = createProject(orgName, projectName, repoName, repoResource);\n\n        // ---\n\n        if (expectedTriggerCount < 0) {\n            return por;\n        }\n\n        TriggersApi triggerResource = new TriggersApi(getApiClient());\n        while (true) {\n            List<TriggerEntry> triggers = triggerResource.listTriggers(orgName, projectName, repoName);\n            if (triggers != null && triggers.size() == expectedTriggerCount) {\n                break;\n            }\n\n            Thread.sleep(1000);\n        }\n\n        return por;\n    }\n\n    private ProjectOperationResponse createProject(String orgName, String projectName, String repoName, String repoResource) throws Exception {\n        Path tmpDir = createTempDir();\n\n        File src = new File(TriggerIT.class.getResource(repoResource).toURI());\n        PathUtils.copy(src.toPath(), tmpDir);\n\n        try (Git repo = Git.init().setInitialBranch(\"master\").setDirectory(tmpDir.toFile()).call()) {\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"import\").call();\n        }\n\n        String gitUrl = tmpDir.toAbsolutePath().toString();\n\n        // ---\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        return projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .url(gitUrl)\n                        .branch(\"master\"))));\n    }\n\n    private Map<String, Object> readPolicy(String file) throws Exception {\n        URL url = TriggerIT.class.getResource(file);\n        return fromJson(new File(url.toURI()));\n    }\n\n    private List<ProcessEntry> waitForProcs(UUID projectId, int expectedCount, StatusEnum expectedStatus) throws Exception {\n        ProcessV2Api processApi = new ProcessV2Api(getApiClient());\n\n        ProcessListFilter filter = ProcessListFilter.builder()\n                .projectId(projectId)\n                .limit(10)\n                .offset(0)\n                .build();\n\n        while (true) {\n            List<ProcessEntry> l = processApi.listProcesses(filter);\n            if (l != null && l.size() == expectedCount && allHasStatus(l, expectedStatus)) {\n                return l;\n            }\n\n            Thread.sleep(1000);\n        }\n    }\n\n    private static boolean allHasStatus(List<ProcessEntry> l, StatusEnum s) {\n        if (s == null) {\n            return true;\n        }\n        return l.stream().allMatch(e -> s.equals(e.getStatus()));\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/TriggersRefreshIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.eclipse.jgit.api.Git;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class TriggersRefreshIT extends AbstractServerIT {\n\n    @Test\n    public void testTriggerRepoRefresh() throws Exception {\n        Path tmpDir = createTempDir();\n\n        File src = new File(TriggersRefreshIT.class.getResource(\"triggerRepo\").toURI());\n        PathUtils.copy(src.toPath(), tmpDir);\n\n        try (Git repo = Git.init().setInitialBranch(\"master\").setDirectory(tmpDir.toFile()).call()) {\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"import\").call();\n        }\n\n        String gitUrl = tmpDir.toAbsolutePath().toString();\n\n        // ---\n\n        String orgName = \"org_\" + randomString();\n        String projectName = \"project_\" + randomString();\n        String repoName = \"repo_\" + randomString();\n        String username = \"user_\" + randomString();\n\n        // ---\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(username)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        ApiKeysApi apiKeysApi = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse cakr = apiKeysApi.createUserApiKey(new CreateApiKeyRequest()\n                .username(username));\n\n        // ---\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        if (orgApi.getOrg(orgName) == null) {\n            orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n        }\n\n        TeamsApi teamsApi = new TeamsApi(getApiClient());\n        teamsApi.addUsersToTeam(orgName, \"default\", false, Collections.singletonList(new TeamUserEntry()\n                .username(username)\n                .role(TeamUserEntry.RoleEnum.MEMBER)\n                .userType(TeamUserEntry.UserTypeEnum.LOCAL)));\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .visibility(ProjectEntry.VisibilityEnum.PUBLIC)\n                .repositories(Collections.singletonMap(repoName, new RepositoryEntry()\n                        .url(gitUrl)\n                        .branch(\"master\"))));\n\n        // ---\n\n        List<TriggerEntry> l = waitForTriggers(orgName, projectName, repoName, 1);\n        assertEquals(\"onTrigger\", getEntryPoint(l.get(0)));\n\n        // ---\n\n        Files.copy(tmpDir.resolve(\"new_concord.yml\"), tmpDir.resolve(\"concord.yml\"), StandardCopyOption.REPLACE_EXISTING);\n\n        try (Git repo = Git.open(tmpDir.toFile())) {\n            repo.add().addFilepattern(\".\").call();\n            repo.commit().setMessage(\"update\").call();\n        }\n\n        // ---\n\n        setApiKey(cakr.getKey());\n\n        // ---\n\n        RepositoriesApi repositoriesApi = new RepositoriesApi(getApiClient());\n        repositoriesApi.refreshRepository(orgName, projectName, repoName, true);\n\n        // ---\n\n        l = waitForTriggers(orgName, projectName, repoName, 2);\n        l.sort(Comparator.comparing(TriggersRefreshIT::getEntryPoint));\n\n        assertEquals(\"onTrigger\", getEntryPoint(l.get(0)));\n        assertEquals(\"onTrigger2\", getEntryPoint(l.get(1)));\n    }\n\n    private List<TriggerEntry> waitForTriggers(String orgName, String projectName, String repoName, int expectedCount) throws Exception {\n        TriggersApi triggerResource = new TriggersApi(getApiClient());\n        while (true) {\n            List<TriggerEntry> l = triggerResource.listTriggers(orgName, projectName, repoName);\n            if (l != null && l.size() == expectedCount) {\n                return l;\n            }\n\n            Thread.sleep(1000);\n        }\n    }\n\n    private static String getEntryPoint(TriggerEntry e) {\n        Map<String, Object> cfg = e.getCfg();\n        if (cfg == null) {\n            return null;\n        }\n        return (String) cfg.get(Constants.Request.ENTRY_POINT_KEY);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/UserManagementIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class UserManagementIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        UsersApi usersApi = new UsersApi(getApiClient());\n\n        String username = \"user_\" + randomString();\n\n        CreateUserResponse cur = usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(username)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n        assertTrue(cur.getOk());\n\n        // ---\n\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse cakr = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(username));\n        assertTrue(cakr.getOk());\n\n        // ---\n\n        usersApi.deleteUser(cur.getId());\n    }\n\n    @Test\n    public void testAdmins() throws Exception {\n        UsersApi usersApi = new UsersApi(getApiClient());\n\n        String userAName = \"userA_\" + randomString();\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userAName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        // ---\n\n        ApiKeysApi apiKeyResource = new ApiKeysApi(getApiClient());\n        CreateApiKeyResponse apiKey = apiKeyResource.createUserApiKey(new CreateApiKeyRequest().username(userAName));\n\n        // ---\n\n        setApiKey(apiKey.getKey());\n\n        String userBName = \"userB_\" + randomString();\n        try {\n            usersApi.createOrUpdateUser(new CreateUserRequest()\n                    .username(userBName)\n                    .type(CreateUserRequest.TypeEnum.LOCAL));\n            fail(\"should fail\");\n        } catch (ApiException e) {\n        }\n\n        // ---\n\n        resetApiKey();\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userAName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        usersApi.updateUserRoles(userAName, new UpdateUserRolesRequest()\n                .roles(Collections.singleton(\"concordAdmin\")));\n\n        // ---\n\n        setApiKey(apiKey.getKey());\n        usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userBName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n    }\n\n    @Test\n    public void testWithRoles() throws Exception {\n        UsersApi usersApi = new UsersApi(getApiClient());\n\n        String roleName = \"role_\" + randomString();\n        String username = \"user_\" + randomString();\n\n        RolesApi rolesApi = new RolesApi(getApiClient());\n        RoleOperationResponse ror = rolesApi.createOrUpdateRole(new RoleEntry().name(roleName));\n        assertEquals(RoleOperationResponse.ResultEnum.CREATED, ror.getResult());\n\n        CreateUserResponse cur = usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(username)\n                .type(CreateUserRequest.TypeEnum.LOCAL)\n                .roles(Collections.singleton(roleName)));\n        assertTrue(cur.getOk());\n\n        UserEntry userEntry = usersApi.findByUsername(username);\n        assertNotNull(userEntry);\n        assertEquals(roleName, userEntry.getRoles().iterator().next().getName());\n\n        // ---\n\n        DeleteUserResponse dur = usersApi.deleteUser(cur.getId());\n        assertTrue(dur.getOk());\n\n        GenericOperationResult delete = rolesApi.deleteRole(roleName);\n        assertEquals(GenericOperationResult.ResultEnum.DELETED, delete.getResult());\n    }\n\n    @Test\n    public void testSpecialCharactersInUsernames() throws Exception {\n        String userName = \"usEr_\" + randomString() + \"@domain.local\";\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n        CreateUserResponse cur = usersApi.createOrUpdateUser(new CreateUserRequest()\n                .username(userName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n        assertNotNull(cur.getId());\n\n        UserEntry e = usersApi.findByUsername(userName);\n        assertEquals(userName.toLowerCase(), e.getName());\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/UserResourceIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Comparator;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class UserResourceIT extends AbstractServerIT {\n\n    @Test\n    public void testUserList() throws Exception {\n        String orgName = \"org_\" + randomString();\n        OrganizationsApi organizationsApi = new OrganizationsApi(getApiClient());\n        organizationsApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // add the user A\n\n        String userAName = \"testUserList_userA_\" + randomString();\n        addUser(userAName);\n\n        // add the user B\n\n        String userBName = \"testUserList_userB_\" + randomString();\n        addUser(userBName);\n\n        // list users\n\n        UserV2Api userApi = new UserV2Api(getApiClient());\n        List<UserEntry> result = userApi.listUsersWithFilter(0, 2, \"testUserList_\");\n        assertEquals(2, result.size());\n        result.sort(Comparator.comparing(UserEntry::getName));\n        assertEquals(userAName.toLowerCase(), result.get(0).getName());\n        assertEquals(userBName.toLowerCase(), result.get(1).getName());\n    }\n\n    private void addUser(String userName) throws ApiException {\n        UsersApi usersApi = new UsersApi(getApiClient());\n        usersApi.createOrUpdateUser(new CreateUserRequest().username(userName)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/UserResourceV2IT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiException;\nimport com.walmartlabs.concord.client2.ApiKeysApi;\nimport com.walmartlabs.concord.client2.CreateApiKeyRequest;\nimport com.walmartlabs.concord.client2.CreateUserRequest;\nimport com.walmartlabs.concord.client2.UpdateUserRolesRequest;\nimport com.walmartlabs.concord.client2.UserEntry;\nimport com.walmartlabs.concord.client2.UserV2Api;\nimport com.walmartlabs.concord.client2.UsersApi;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Set;\nimport java.util.UUID;\n\nimport static org.junit.jupiter.api.Assertions.assertDoesNotThrow;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nclass UserResourceV2IT extends AbstractServerIT {\n\n    @Test\n    void testGetUser() throws Exception {\n        // user with no roles\n\n        var userAName = \"user_basic_\" + randomString();\n        var noRolesUser = addUser(userAName, Set.of());\n\n        // user with system reader role\n\n        var userBName = \"user_system_reader_\" + randomString();\n        var systemReaderUser = addUser(userBName, Set.of(\"concordSystemReader\"));\n\n        // get a user with insufficient privileges\n        var ex = assertThrows(ApiException.class, () -> getUser(noRolesUser, systemReaderUser.userId()));\n        assertTrue(ex.getMessage().contains(\"Users can only view their own information or must have admin privileges.\"));\n\n        // get a user with concordSystemReader role\n        var user = assertDoesNotThrow(() -> getUser(systemReaderUser, noRolesUser.userId()));\n        assertEquals(user.getId(), noRolesUser.userId());\n    }\n\n    private UserEntry getUser(UserInfo userInfo, UUID userToGet) throws ApiException {\n        var apiClient = new UserV2Api(getApiClientForKey(userInfo.apiKey()));\n\n        return apiClient.getUser(userToGet);\n    }\n\n    private UserInfo addUser(String username, Set<String> roles) throws ApiException {\n        var usersApi = new UsersApi(getApiClient());\n        var user = usersApi.createOrUpdateUser(new CreateUserRequest().username(username)\n                .type(CreateUserRequest.TypeEnum.LOCAL));\n\n        if (!roles.isEmpty()) {\n            usersApi.updateUserRoles(username, new UpdateUserRolesRequest()\n                    .roles(roles));\n        }\n\n        var apiKeysApi = new ApiKeysApi(getApiClient());\n        var apiKeyResp = apiKeysApi.createUserApiKey(new CreateApiKeyRequest()\n                .userId(user.getId()));\n\n        return new UserInfo(username, user.getId(), apiKeyResp.getKey());\n    }\n\n    private record UserInfo(String username, UUID userId, String apiKey) { }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/ValidationIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ValidationIT extends AbstractServerIT {\n\n    @Test\n    public void testValidationExceptionMapping() throws Exception {\n        var processApi = new ProcessApi(getApiClient());\n        var input = Map.<String, Object>of(Constants.Multipart.ORG_NAME, \"org_\" + randomString(),\n                Constants.Multipart.PROJECT_NAME, \"foo\",\n                \"archive\", archive(ProcessIT.class.getResource(\"example\").toURI()));\n        var ex = assertThrows(ApiException.class, () -> processApi.startProcess(input));\n        assertEquals(400, ex.getCode());\n        assertTrue(ex.getMessage().contains(\"Organization not found\"));\n    }\n\n    @Test\n    public void testProjectCreation() throws Exception {\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n\n        try {\n            ProjectEntry req = new ProjectEntry().name(\"@123_123\");\n            projectsApi.createOrUpdateProject(\"Default\", req);\n            fail(\"Should fail with a validation error\");\n        } catch (ApiException e) {\n            assertInvalidRequest(e);\n        }\n\n        ProjectEntry req = new ProjectEntry().name(\"aProperName@\" + System.currentTimeMillis());\n        projectsApi.createOrUpdateProject(\"Default\", req);\n    }\n\n    @Test\n    public void testInvalidUsername() {\n        String longUsername = \"01234567890123456789012345678901234567890123456789012345678901234567890123456789\" +\n                \"01234567890123456789012345678901234567890123456789012345678901234567890123456789\";\n\n        UsersApi usersApi = new UsersApi(getApiClient());\n\n        try {\n            usersApi.findByUsername(\"test@localhost\");\n            fail(\"Should fail with a validation error\");\n        } catch (ApiException e) {\n            assertInvalidRequest(e);\n        }\n\n        try {\n            usersApi.findByUsername(\"local\\\\test\");\n            fail(\"Should fail with a validation error\");\n        } catch (ApiException e) {\n            assertInvalidRequest(e);\n        }\n\n        try {\n            usersApi.findByUsername(longUsername);\n            fail(\"Should fail with a validation error\");\n        } catch (ApiException e) {\n            assertInvalidRequest(e);\n        }\n\n        try {\n            usersApi.findByUsername(\"test#\" + System.currentTimeMillis());\n            fail(\"Random valid username, should fail with 404\");\n        } catch (ApiException e) {\n            assertInvalidRequest(e);\n        }\n\n        try {\n            usersApi.createOrUpdateUser(new CreateUserRequest().username(longUsername)\n                    .type(CreateUserRequest.TypeEnum.LOCAL));\n            fail(\"Should fail with a validation error\");\n        } catch (ApiException e) {\n            assertInvalidRequest(e);\n        }\n    }\n\n    private static void assertInvalidRequest(ApiException e) {\n        int code = e.getCode();\n        assertTrue(code >= 400 && code < 500);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/VariablesIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.collect.ImmutableMap;\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\n\npublic class VariablesIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        String orgName = \"Default\";\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .cfg(ImmutableMap.of(\"arguments\",\n                        ImmutableMap.of(\"nested\",\n                                ImmutableMap.of(\n                                        \"y\", \"cba\",\n                                        \"z\", true))))\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n\n        byte[] payload = archive(VariablesIT.class.getResource(\"variables\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"archive\", payload);\n        StartProcessResponse spr = start(payload);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*x=123.*\", ab);\n        assertLog(\".*y=abc.*\", ab);\n        assertLog(\".*z=false.*\", ab);\n        assertLog(\".*nested.p=false.*\", ab);\n        assertLog(\".*nested.q=abc.*\", ab);\n        assertLog(\".*var1=var1-value.*\", ab);\n    }\n\n    @Test\n    public void testCrypto() throws Exception {\n        String orgName = \"Default\";\n        String projectName = \"project_\" + randomString();\n        String secretValue = \"secret_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        EncryptValueResponse evr = projectsApi.encrypt(orgName, projectName, secretValue);\n        String encryptedValue = evr.getData();\n\n        projectsApi.updateProjectConfiguration(orgName, projectName,\n                ImmutableMap.of(\"arguments\",\n                        ImmutableMap.of(\"mySecret\", encryptedValue)));\n\n        // ---\n\n        byte[] payload = archive(VariablesIT.class.getResource(\"crypto\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"archive\", payload);\n        StartProcessResponse spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*\" + secretValue + \".*\", ab);\n    }\n\n    @Test\n    public void testArrayInterpolation() throws Exception {\n        String varA = \"varA_\" + System.currentTimeMillis();\n        String varB = \"varB_\" + System.currentTimeMillis();\n\n        byte[] payload = archive(VariablesIT.class.getResource(\"arrayInterpolation\").toURI());\n\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"varA\", varA);\n        args.put(\"varB\", varB);\n\n        Map<String, Object> req = new HashMap<>();\n        req.put(\"arguments\", args);\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"request\", req);\n        input.put(\"archive\", payload);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*\" + varA + \".*\" + varB + \".*\", ab);\n    }\n\n    @Test\n    public void testSetVar() throws Exception {\n        byte[] payload = archive(VariablesIT.class.getResource(\"setVar\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*shouldBeNull: $\", ab);\n        assertLog(\".*nested\\\\.var: nested\\\\.var.*\", ab);\n    }\n\n    @Test\n    public void testGetNestedVar() throws Exception {\n        byte[] payload = archive(VariablesIT.class.getResource(\"getVar\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*param1: 1$\", ab);\n        assertLog(\".*defaultValue: 101$\", ab);\n        assertLog(\".*defaultValueFromUnknown: 102$\", ab);\n    }\n\n    @Test\n    public void testSetDependentVars() throws Exception {\n        byte[] payload = archive(VariablesIT.class.getResource(\"setVarNested\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"arguments.obj.x\", \"123\");\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*obj.x: 123$\", ab);\n        assertLog(\".*obj.name: Concord$\", ab);\n        assertLog(\".*obj.msg: Hello, Concord$\", ab);\n    }\n\n    @Test\n    public void testSetDependentVars2() throws Exception {\n        byte[] payload = archive(VariablesIT.class.getResource(\"setVarNested2\").toURI());\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*event: \\\\{branch=master\\\\}$\", ab);\n        assertLog(\".*commitEvent.event: push$\", ab);\n        assertLog(\".*commitEvent.branch: master$\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/VariablesInjectionIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.client2.StartProcessResponse;\nimport org.junit.jupiter.api.Test;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class VariablesInjectionIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        byte[] payload = archive(VariablesInjectionIT.class.getResource(\"inject\").toURI());\n\n        StartProcessResponse spr = start(payload);\n        assertNotNull(spr.getInstanceId());\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        byte[] ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Hello, Concord!.*\", ab);\n        assertLog(\".*Hello, world!!!.*\", ab);\n        assertLog(\".*Hello, world. \\\\(from method param\\\\).*\", ab);\n        assertLog(\".*Hello, world. \\\\(from injected var\\\\).*\", ab);\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/WithItemsIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.net.URI;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.*;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class WithItemsIT extends AbstractServerIT {\n\n    @Test\n    public void testAnsible() throws Exception {\n        URI uri = WithItemsIT.class.getResource(\"ansibleWithItems\").toURI();\n        byte[] payload = archive(uri);\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hello!.*\", ab);\n        assertLog(\".*Hi there!.*\", ab);\n        assertLog(\".*Howdy!.*\", ab);\n    }\n\n    @Test\n    public void testForms() throws Exception {\n        URI uri = WithItemsIT.class.getResource(\"formsWithItems\").toURI();\n        byte[] payload = archive(uri);\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n\n        // ---\n\n        ProcessFormsApi formResource = new ProcessFormsApi(getApiClient());\n        List<FormListEntry> forms = formResource.listProcessForms(spr.getInstanceId());\n        assertEquals(1, forms.size());\n\n        formResource.submitForm(spr.getInstanceId(), forms.get(0).getName(), Collections.emptyMap());\n\n        ProcessEntry pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.SUSPENDED);\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hello!.*\", ab);\n\n        // ---\n\n        forms = formResource.listProcessForms(spr.getInstanceId());\n        assertEquals(1, forms.size());\n\n        formResource.submitForm(spr.getInstanceId(), forms.get(0).getName(), Collections.emptyMap());\n\n        pir = waitForStatus(getApiClient(), spr.getInstanceId(), ProcessEntry.StatusEnum.FINISHED);\n        ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hi there!.*\", ab);\n    }\n\n    @Test\n    public void testExternalItems() throws Exception {\n        URI uri = WithItemsIT.class.getResource(\"externalWithItems\").toURI();\n        byte[] payload = archive(uri);\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"request\", Collections.singletonMap(\"arguments\",\n                Collections.singletonMap(\"myItems\",\n                        Arrays.asList(\"Hello!\", \"Hi there!\", \"Howdy!\"))));\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n\n        // ---\n\n        byte[] ab = getLog(pir.getInstanceId());\n        assertLog(\".*Hello!.*\", ab);\n        assertLog(\".*Hi there!.*\", ab);\n        assertLog(\".*Howdy!.*\", ab);\n    }\n\n    @Test\n    public void testLotsOfItems() throws Exception {\n        URI uri = WithItemsIT.class.getResource(\"externalWithItems\").toURI();\n        byte[] payload = archive(uri);\n\n        // ---\n\n        List<String> items = new ArrayList<>();\n        for (int i = 0; i < 1000; i++) {\n            items.add(\"item_\" + randomString());\n        }\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        input.put(\"request\", Collections.singletonMap(\"arguments\",\n                Collections.singletonMap(\"myItems\", items)));\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n    }\n\n    @Test\n    public void testSubsequentCalls() throws Exception {\n        URI uri = WithItemsIT.class.getResource(\"multipleWithItems\").toURI();\n        byte[] payload = archive(uri);\n\n        // ---\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"archive\", payload);\n        StartProcessResponse spr = start(input);\n\n        // ---\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FINISHED, pir.getStatus());\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/java/com/walmartlabs/concord/it/server/WorkspacePolicyIT.java",
    "content": "package com.walmartlabs.concord.it.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.net.URL;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.it.common.ITUtils.archive;\nimport static com.walmartlabs.concord.it.common.ServerClient.assertLog;\nimport static com.walmartlabs.concord.it.common.ServerClient.waitForCompletion;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class WorkspacePolicyIT extends AbstractServerIT {\n\n    @Test\n    public void test() throws Exception {\n        String orgName = \"org_\" + randomString();\n\n        OrganizationsApi orgApi = new OrganizationsApi(getApiClient());\n        orgApi.createOrUpdateOrg(new OrganizationEntry().name(orgName));\n\n        // ---\n\n        String projectName = \"project_\" + randomString();\n\n        ProjectsApi projectsApi = new ProjectsApi(getApiClient());\n        projectsApi.createOrUpdateProject(orgName, new ProjectEntry()\n                .name(projectName)\n                .rawPayloadMode(ProjectEntry.RawPayloadModeEnum.EVERYONE));\n\n        // ---\n\n        String policyName = \"policy_\" + randomString();\n\n        PolicyApi policyResource = new PolicyApi(getApiClient());\n        policyResource.createOrUpdatePolicy(new PolicyEntry().name(policyName).rules(readPolicy(\"workspacePolicy/test-policy.json\")));\n        policyResource.linkPolicy(policyName, new PolicyLinkEntry().orgName(orgName));\n\n        // ---\n\n        byte[] payload = archive(WorkspacePolicyIT.class.getResource(\"workspacePolicy\").toURI());\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"org\", orgName);\n        input.put(\"project\", projectName);\n        input.put(\"archive\", payload);\n\n        StartProcessResponse spr = start(input);\n        ProcessEntry pe = waitForCompletion(getApiClient(), spr.getInstanceId());\n        assertEquals(ProcessEntry.StatusEnum.FAILED, pe.getStatus());\n\n        byte[] ab = getLog(pe.getInstanceId());\n        assertLog(\".*Workspace policy violation.*\", ab);\n        // ---\n\n        policyResource.createOrUpdatePolicy(new PolicyEntry().name(policyName).rules(readPolicy(\"workspacePolicy/test-policy-relaxed.json\")));\n\n        // ---\n\n        spr = start(input);\n\n        ProcessEntry pir = waitForCompletion(getApiClient(), spr.getInstanceId());\n        ab = getLog(pir.getInstanceId());\n\n        assertLog(\".*Hello!.*\", ab);\n    }\n\n\n    private Map<String, Object> readPolicy(String file) throws Exception {\n        URL url = WorkspacePolicyIT.class.getResource(file);\n        return fromJson(new File(url.toURI()));\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/resources/agent.conf",
    "content": "concord-agent {\n\n    dependencyResolveTimeout = \"30 seconds\"\n    logMaxDelay = \"250 milliseconds\"\n    pollInterval = \"250 milliseconds\"\n\n    prefork {\n        enabled = true\n    }\n\n    capabilities = {\n        type = \"test\"\n    }\n\n    server {\n        apiKey = \"cTJxMnEycTI=\"\n        processRequestDelay = \"250 milliseconds\"\n    }\n}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ProcessDisabledRepo/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Hello!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansible/_main.json",
    "content": "{\n  \"dependencies\": [\n    \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\"\n  ],\n  \"entryPoint\": \"main\",\n  \"arguments\": {\n    \"ansibleParams\": {\n      \"playbook\": \"playbook/hello.yml\",\n      \"inventory\": {\n        \"local\": {\n          \"hosts\": [\"127.0.0.1\"],\n          \"vars\": {\n            \"ansible_connection\": \"local\"\n          }\n        }\n      },\n      \"extraVars\": {\n        \"greetings\": \"Hello, world\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansible/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ greetings }}\"\n      verbosity: 0"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansible/processes/main.yml",
    "content": "main:\n- expr: ${ansible.run(ansibleParams, workDir)}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleBadStrings/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      enableLogFiltering: true\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleBadStrings/playbook/blns.base64.json",
    "content": "[\n  \"\", \n  \"dW5kZWZpbmVk\", \n  \"dW5kZWY=\", \n  \"bnVsbA==\", \n  \"TlVMTA==\", \n  \"KG51bGwp\", \n  \"bmls\", \n  \"TklM\", \n  \"dHJ1ZQ==\", \n  \"ZmFsc2U=\", \n  \"VHJ1ZQ==\", \n  \"RmFsc2U=\", \n  \"VFJVRQ==\", \n  \"RkFMU0U=\", \n  \"Tm9uZQ==\", \n  \"aGFzT3duUHJvcGVydHk=\", \n  \"XA==\", \n  \"MA==\", \n  \"MQ==\", \n  \"MS4wMA==\", \n  \"JDEuMDA=\", \n  \"MS8y\", \n  \"MUUy\", \n  \"MUUwMg==\", \n  \"MUUrMDI=\", \n  \"LTE=\", \n  \"LTEuMDA=\", \n  \"LSQxLjAw\", \n  \"LTEvMg==\", \n  \"LTFFMg==\", \n  \"LTFFMDI=\", \n  \"LTFFKzAy\", \n  \"MS8w\", \n  \"MC8w\", \n  \"LTIxNDc0ODM2NDgvLTE=\", \n  \"LTkyMjMzNzIwMzY4NTQ3NzU4MDgvLTE=\", \n  \"LTA=\", \n  \"LTAuMA==\", \n  \"KzA=\", \n  \"KzAuMA==\", \n  \"MC4wMA==\", \n  \"MC4uMA==\", \n  \"Lg==\", \n  \"MC4wLjA=\", \n  \"MCwwMA==\", \n  \"MCwsMA==\", \n  \"LA==\", \n  \"MCwwLDA=\", \n  \"MC4wLzA=\", \n  \"MS4wLzAuMA==\", \n  \"MC4wLzAuMA==\", \n  \"MSwwLzAsMA==\", \n  \"MCwwLzAsMA==\", \n  \"LS0x\", \n  \"LQ==\", \n  \"LS4=\", \n  \"LSw=\", \n  \"OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5\", \n  \"OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5OTk5\", \n  \"TmFO\", \n  \"SW5maW5pdHk=\", \n  \"LUluZmluaXR5\", \n  \"SU5G\", \n  \"MSNJTkY=\", \n  \"LTEjSU5E\", \n  \"MSNRTkFO\", \n  \"MSNTTkFO\", \n  \"MSNJTkQ=\", \n  \"MHgw\", \n  \"MHhmZmZmZmZmZg==\", \n  \"MHhmZmZmZmZmZmZmZmZmZmZm\", \n  \"MHhhYmFkMWRlYQ==\", \n  \"MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5\", \n  \"MSwwMDAuMDA=\", \n  \"MSAwMDAuMDA=\", \n  \"MScwMDAuMDA=\", \n  \"MSwwMDAsMDAwLjAw\", \n  \"MSAwMDAgMDAwLjAw\", \n  \"MScwMDAnMDAwLjAw\", \n  \"MS4wMDAsMDA=\", \n  \"MSAwMDAsMDA=\", \n  \"MScwMDAsMDA=\", \n  \"MS4wMDAuMDAwLDAw\", \n  \"MSAwMDAgMDAwLDAw\", \n  \"MScwMDAnMDAwLDAw\", \n  \"MDEwMDA=\", \n  \"MDg=\", \n  \"MDk=\", \n  \"Mi4yMjUwNzM4NTg1MDcyMDExZS0zMDg=\", \n  \"LC4vOydbXS09\", \n  \"PD4/OiJ7fXxfKw==\", \n  \"IUAjJCVeJiooKWB+\", \n  \"AQIDBAUGBwgODxAREhMUFRYXGBkaGxwdHh9/\", \n  \"woDCgcKCwoPChMKGwofCiMKJworCi8KMwo3CjsKPwpDCkcKSwpPClMKVwpbCl8KYwpnCmsKbwpzC\", \n  \"ncKewp8=\", \n  \"CwwgwoXCoOGagOKAgOKAgeKAguKAg+KAhOKAheKAhuKAh+KAiOKAieKAiuKAi+KAqOKAqeKAr+KB\", \n  \"n+OAgA==\", \n  \"wq3YgNiB2ILYg9iE2IXYnNud3I/hoI7igIvigIzigI3igI7igI/igKrigKvigKzigK3igK7igaDi\", \n  \"gaHigaLigaPigaTigabigafigajiganigarigavigaziga3iga7iga/vu7/vv7nvv7rvv7vwkYK9\", \n  \"8JuyoPCbsqHwm7Ki8Juyo/CdhbPwnYW08J2FtfCdhbbwnYW38J2FuPCdhbnwnYW686CAgfOggKDz\", \n  \"oICh86CAovOggKPzoICk86CApfOggKbzoICn86CAqPOggKnzoICq86CAq/OggKzzoICt86CArvOg\", \n  \"gK/zoICw86CAsfOggLLzoICz86CAtPOggLXzoIC286CAt/OggLjzoIC586CAuvOggLvzoIC886CA\", \n  \"vfOggL7zoIC/86CBgPOggYHzoIGC86CBg/OggYTzoIGF86CBhvOggYfzoIGI86CBifOggYrzoIGL\", \n  \"86CBjPOggY3zoIGO86CBj/OggZDzoIGR86CBkvOggZPzoIGU86CBlfOggZbzoIGX86CBmPOggZnz\", \n  \"oIGa86CBm/OggZzzoIGd86CBnvOggZ/zoIGg86CBofOggaLzoIGj86CBpPOggaXzoIGm86CBp/Og\", \n  \"gajzoIGp86CBqvOggavzoIGs86CBrfOgga7zoIGv86CBsPOggbHzoIGy86CBs/OggbTzoIG186CB\", \n  \"tvOggbfzoIG486CBufOggbrzoIG786CBvPOggb3zoIG+86CBvw==\", \n  \"77u/\", \n  \"77++\", \n  \"zqniiYjDp+KImuKIq8ucwrXiiaTiiaXDtw==\", \n  \"w6XDn+KIgsaSwqnLmeKIhsuawqzigKbDpg==\", \n  \"xZPiiJHCtMKu4oCgwqXCqMuGw7jPgOKAnOKAmA==\", \n  \"wqHihKLCo8Ki4oiewqfCtuKAosKqwrrigJPiiaA=\", \n  \"wrjLm8OH4peKxLHLnMOCwq/LmMK/\", \n  \"w4XDjcOOw4/LncOTw5Tvo7/DksOaw4bimIM=\", \n  \"xZLigJ7CtOKAsMuHw4HCqMuGw5jiiI/igJ3igJk=\", \n  \"YOKBhOKCrOKAueKAuu+sge+sguKAocKwwrfigJrigJTCsQ==\", \n  \"4oWb4oWc4oWd4oWe\", \n  \"0IHQgtCD0ITQhdCG0IfQiNCJ0IrQi9CM0I3QjtCP0JDQkdCS0JPQlNCV0JbQl9CY0JnQmtCb0JzQ\", \n  \"ndCe0J/QoNCh0KLQo9Ck0KXQptCn0KjQqdCq0KvQrNCt0K7Qr9Cw0LHQstCz0LTQtdC20LfQuNC5\", \n  \"0LrQu9C80L3QvtC/0YDRgdGC0YPRhNGF0YbRh9GI0YnRitGL0YzRjdGO0Y8=\", \n  \"2aDZodmi2aPZpNml2abZp9mo2ak=\", \n  \"4oGw4oG04oG1\", \n  \"4oKA4oKB4oKC\", \n  \"4oGw4oG04oG14oKA4oKB4oKC\", \n  \"4LiU4LmJ4LmJ4LmJ4LmJ4LmJ4LmH4LmH4LmH4LmH4LmH4LmJ4LmJ4LmJ4LmJ4LmJ4LmH4LmH4LmH\", \n  \"4LmH4LmH4LmJ4LmJ4LmJ4LmJ4LmJ4LmJ4LmJ4LmJ4LmH4LmH4LmH4LmH4LmH4LmJ4LmJ4LmJ4LmJ\", \n  \"4LmJ4LmH4LmH4LmH4LmH4LmH4LmJ4LmJ4LmJ4LmJ4LmJ4LmJ4LmJ4LmJ4LmH4LmH4LmH4LmH4LmH\", \n  \"4LmJ4LmJ4LmJ4LmJ4LmJ4LmH4LmH4LmH4LmH4LmH4LmJ4LmJ4LmJ4LmJ4LmJ4LmJ4LmJ4LmJ4LmH\", \n  \"4LmH4LmH4LmH4LmH4LmJ4LmJ4LmJ4LmJ4LmJ4LmH4LmH4LmH4LmHIOC4lOC5ieC5ieC5ieC5ieC5\", \n  \"ieC5h+C5h+C5h+C5h+C5h+C5ieC5ieC5ieC5ieC5ieC5h+C5h+C5h+C5h+C5h+C5ieC5ieC5ieC5\", \n  \"ieC5ieC5ieC5ieC5ieC5h+C5h+C5h+C5h+C5h+C5ieC5ieC5ieC5ieC5ieC5h+C5h+C5h+C5h+C5\", \n  \"h+C5ieC5ieC5ieC5ieC5ieC5ieC5ieC5ieC5h+C5h+C5h+C5h+C5h+C5ieC5ieC5ieC5ieC5ieC5\", \n  \"h+C5h+C5h+C5h+C5h+C5ieC5ieC5ieC5ieC5ieC5ieC5ieC5ieC5h+C5h+C5h+C5h+C5h+C5ieC5\", \n  \"ieC5ieC5ieC5ieC5h+C5h+C5h+C5hyDguJTguYnguYnguYnguYnguYnguYfguYfguYfguYfguYfg\", \n  \"uYnguYnguYnguYnguYnguYfguYfguYfguYfguYfguYnguYnguYnguYnguYnguYnguYnguYnguYfg\", \n  \"uYfguYfguYfguYfguYnguYnguYnguYnguYnguYfguYfguYfguYfguYfguYnguYnguYnguYnguYng\", \n  \"uYnguYnguYnguYfguYfguYfguYfguYfguYnguYnguYnguYnguYnguYfguYfguYfguYfguYfguYng\", \n  \"uYnguYnguYnguYnguYnguYnguYnguYfguYfguYfguYfguYfguYnguYnguYnguYnguYnguYfguYfg\", \n  \"uYfguYc=\", \n  \"Jw==\", \n  \"Ig==\", \n  \"Jyc=\", \n  \"IiI=\", \n  \"JyIn\", \n  \"IicnJyciJyI=\", \n  \"IiciJyInJycnIg==\", \n  \"PGZvbyB2YWw94oCcYmFy4oCdIC8+\", \n  \"PGZvbyB2YWw94oCcYmFy4oCdIC8+\", \n  \"PGZvbyB2YWw94oCdYmFy4oCcIC8+\", \n  \"PGZvbyB2YWw9YGJhcicgLz4=\", \n  \"55Sw5Lit44GV44KT44Gr44GC44GS44Gm5LiL44GV44GE\", \n  \"44OR44O844OG44Kj44O844G46KGM44GL44Gq44GE44GL\", \n  \"5ZKM6KO95ryi6Kqe\", \n  \"6YOo6JC95qC8\", \n  \"7IKs7ZqM6rO87ZWZ7JuQIOyWtO2VmeyXsOq1rOyGjA==\", \n  \"7LCm7LCo66W8IO2DgOqzoCDsmKgg7Y6y7Iuc66eo6rO8IOyRm+uLpOumrCDrmKDrsKnqsIHtlZg=\", \n  \"56S+5pyD56eR5a246Zmi6Kqe5a2456CU56m25omA\", \n  \"7Jq4656A67CU7Yag66W0\", \n  \"8KCcjvCgnLHwoJ258KCxk/CgsbjwoLKW8KCzjw==\", \n  \"6KGo44Od44GCQem3l8WSw6nvvKLpgI3DnMOfwqrEhcOx5LiC45CA8KCAgA==\", \n  \"44O94Ly84LqI2YTNnOC6iOC8ve++iSDjg73gvLzguojZhM2c4LqI4Ly9776J\", \n  \"KO+9oeKXlSDiiIAg4peV772hKQ==\", \n  \"772A772oKMK04oiA772A4oip\", \n  \"X1/vvpsoLF8sKik=\", \n  \"44O7KO+/o+KIgO+/oynjg7s6Kjo=\", \n  \"776f772l4py/44O+4pWyKO+9oeKXleKAv+KXle+9oSnilbHinL/vvaXvvp8=\", \n  \"LOOAguODuzoqOuODu+OCnOKAmSgg4pi7IM+JIOKYuyAp44CC44O7Oio644O744Kc4oCZ\", \n  \"KOKVr8Kw4pahwrDvvInila/vuLUg4pS74pSB4pS7KQ==\", \n  \"KO++ieCypeebiuCype+8ie++ie+7vyDilLvilIHilLs=\", \n  \"4pSs4pSA4pSs44OOKCDCuiBfIMK644OOKQ==\", \n  \"KCDNocKwIM2cypYgzaHCsCk=\", \n  \"8J+YjQ==\", \n  \"8J+RqfCfj70=\", \n  \"8J+RviDwn5mHIPCfkoEg8J+ZhSDwn5mGIPCfmYsg8J+ZjiDwn5mN\", \n  \"8J+QtSDwn5mIIPCfmYkg8J+Zig==\", \n  \"4p2k77iPIPCfkpQg8J+SjCDwn5KVIPCfkp4g8J+SkyDwn5KXIPCfkpYg8J+SmCDwn5KdIPCfkp8g\", \n  \"8J+SnCDwn5KbIPCfkpog8J+SmQ==\", \n  \"4pyL8J+PvyDwn5Kq8J+PvyDwn5GQ8J+PvyDwn5mM8J+PvyDwn5GP8J+PvyDwn5mP8J+Pvw==\", \n  \"8J+aviDwn4aSIPCfhpMg8J+GlSDwn4aWIPCfhpcg8J+GmSDwn4+n\", \n  \"MO+4j+KDoyAx77iP4oOjIDLvuI/ig6MgM++4j+KDoyA077iP4oOjIDXvuI/ig6MgNu+4j+KDoyA3\", \n  \"77iP4oOjIDjvuI/ig6MgOe+4j+KDoyDwn5Sf\", \n  \"8J+HuvCfh7jwn4e38J+HuvCfh7gg8J+HpvCfh6vwn4em8J+HsvCfh7g=\", \n  \"8J+HuvCfh7jwn4e38J+HuvCfh7jwn4em8J+Hq/Cfh6bwn4ey\", \n  \"8J+HuvCfh7jwn4e38J+HuvCfh7jwn4em\", \n  \"77yR77yS77yT\", \n  \"2aHZotmj\", \n  \"2KvZhSDZhtmB2LMg2LPZgti32Kog2YjYqNin2YTYqtit2K/Zitiv2IwsINis2LLZitix2KrZiiDY\", \n  \"qNin2LPYqtiu2K/Yp9mFINij2YYg2K/ZhtmILiDYpdiwINmH2YbYp9ifINin2YTYs9iq2KfYsSDZ\", \n  \"iNiq2YbYtdmK2Kgg2YPYp9mGLiDYo9mH2ZHZhCDYp9mK2LfYp9mE2YrYp9iMINio2LHZiti32KfZ\", \n  \"htmK2Kct2YHYsdmG2LPYpyDZgtivINij2K7YsC4g2LPZhNmK2YXYp9mG2Iwg2KXYqtmB2KfZgtmK\", \n  \"2Kkg2KjZitmGINmF2KcsINmK2LDZg9ixINin2YTYrdiv2YjYryDYo9mKINio2LnYrywg2YXYudin\", \n  \"2YXZhNipINio2YjZhNmG2K/Yp9iMINin2YTYpdi32YTYp9mCINi52YQg2KXZitmILg==\", \n  \"15HWsNa816jWtdeQ16nWtNeB15nXqiwg15HWuNa816jWuNeQINeQ1rHXnNa515TWtNeZ150sINeQ\", \n  \"1rXXqiDXlNa316nWuNa814HXnta315nWtNedLCDXldaw15DWtdeqINeU1rjXkNa416jWttel\", \n  \"15TWuNeZ1rDXqta415R0ZXN02KfZhNi12YHYrdin2Kog2KfZhNiq2ZHYrdmI2YQ=\", \n  \"77e9\", \n  \"77e6\", \n  \"2YXZj9mG2Y7Yp9mC2Y7YtNmO2KnZjyDYs9mP2KjZj9mE2ZAg2KfZkNiz2ZLYqtmQ2K7Zktiv2Y7Y\", \n  \"p9mF2ZAg2KfZhNmE2ZHZj9i62Y7YqdmQINmB2ZDZiiDYp9mE2YbZkdmP2LjZj9mF2ZAg2KfZhNmS\", \n  \"2YLZjtin2KbZkNmF2Y7YqdmQINmI2Y7ZgdmQ2YrZhSDZitmO2K7Zj9i12ZHZjiDYp9mE2KrZkdmO\", \n  \"2LfZktio2ZDZitmC2Y7Yp9iq2Y8g2KfZhNmS2K3Yp9iz2Y/ZiNio2ZDZitmR2Y7YqdmP2Iw=\", \n  \"4oCq4oCqdGVzdOKAqg==\", \n  \"4oCrdGVzdOKAqw==\", \n  \"4oCpdGVzdOKAqQ==\", \n  \"dGVzdOKBoHRlc3TigKs=\", \n  \"4oGmdGVzdOKBpw==\", \n  \"4bmwzLrMusyVb82eIMy3acyyzKzNh8yqzZluzJ3Ml82VdsyfzJzMmMymzZ9vzLbMmcywzKBrw6jN\", \n  \"msyuzLrMqsy5zLHMpCDMlnTMnc2VzLPMo8y7zKrNnmjMvM2TzLLMpsyzzJjMsmXNh8yjzLDMpsys\", \n  \"zY4gzKLMvMy7zLHMmGjNms2OzZnMnMyjzLLNhWnMpsyyzKPMsMykdsy7zY1lzLrMrcyzzKrMsC1t\", \n  \"zKJpzYVuzJbMusyezLLMr8ywZMy1zLzMn82ZzKnMvMyYzLMgzJ7MpcyxzLPMrXLMm8yXzJhlzZlw\", \n  \"zaByzLzMnsy7zK3Ml2XMusygzKPNn3PMmM2HzLPNjcydzYllzYnMpcyvzJ7Mss2azKzNnMe5zKzN\", \n  \"js2OzJ/Mls2HzKR0zY3MrMykzZPMvMytzZjNhWnMqsyxbs2gZ8y0zYkgzY/Nic2FY8yszJ9ozaFh\", \n  \"zKvMu8yvzZhvzKvMn8yWzY3MmcydzYlzzJfMpsyyLsyozLnNiMyj\", \n  \"zKHNk8yezYVJzJfMmMymzZ1uzYfNh82ZdsyuzKtva8yyzKvMmc2IacyWzZnMrcy5zKDMnm7Mocy7\", \n  \"zK7Mo8y6Z8yyzYjNmcytzZnMrM2OIMywdM2UzKZozJ7MsmXMosykIM2NzKzMss2WZsy0zJjNlcyj\", \n  \"w6jNluG6ucylzKlszZbNlM2aac2TzZrMps2gbs2WzY3Ml82TzLPMrmfNjSDMqG/NmsyqzaFmzJjM\", \n  \"o8ysIMyWzJjNlsyfzZnMrmPSic2UzKvNls2TzYfNls2FaMy1zKTMo82azZTDocyXzLzNlc2Fb8y8\", \n  \"zKPMpXPMsc2IzLrMlsymzLvNoi7Mm8yWzJ7MoMyrzLA=\", \n  \"zJfMus2WzLnMr82T4bmuzKTNjcylzYfNiGjMssyBZc2PzZPMvMyXzJnMvMyjzZQgzYfMnMyxzKDN\", \n  \"k82NzYVOzZXNoGXMl8yxesyYzJ3MnMy6zZlwzKTMusy5zY3Mr82aZcygzLvMoM2ccsyozKTNjcy6\", \n  \"zJbNlMyWzJZkzKDMn8ytzKzMnc2facymzZbMqc2TzZTMpGHMoMyXzKzNicyZbs2azZwgzLvMnsyw\", \n  \"zZrNhWjMtc2JacyzzJ52zKLNh+G4mc2OzZ8t0onMrcypzLzNlG3MpMytzKtpzZXNh8ydzKZuzJfN\", \n  \"meG4jcyfIMyvzLLNlc2ex6vMn8yvzLDMss2ZzLvMnWYgzKrMsMywzJfMlsytzJjNmGPMps2NzLLM\", \n  \"ns2NzKnMmeG4pc2aYcyuzY7Mn8yZzZzGocypzLnNjnPMpC7MncydINKJWsyhzJbMnM2WzLDMo82J\", \n  \"zJxhzZbMsM2ZzKzNoWzMssyrzLPNjcypZ8yhzJ/MvMyxzZrMnsyszYVvzJfNnC7Mnw==\", \n  \"zKZIzKzMpMyXzKTNnWXNnCDMnMylzJ3Mu82NzJ/MgXfMlWjMlsyvzZNvzJ3NmcyWzY7MscyuINKJ\", \n  \"zLrMmcyezJ/NiFfMt8y8zK1hzLrMqs2NxK/NiM2VzK3NmcyvzJx0zLbMvMyuc8yYzZnNlsyVIMyg\", \n  \"zKvMoELMu82NzZnNicyzzYVlzLVozLXMrM2HzKvNmWnMuc2TzLPMs8yuzY7Mq8yVbs2fZMy0zKrM\", \n  \"nMyWIMywzYnMqc2HzZnMss2ezYVUzZbMvM2TzKrNomjNj82TzK7Mu2XMrMydzJ/NhSDMpMy5zJ1X\", \n  \"zZnMnsydzZTNh82dzYVhzY/Nk82UzLnMvMyjbMy0zZTMsMykzJ/NlOG4vcyrLs2V\", \n  \"WsyuzJ7MoM2ZzZTNheG4gMyXzJ7NiMy7zJfhuLbNmc2OzK/MucyezZNHzLtPzK3Ml8yu\", \n  \"y5nJkG5i4bSJbMmQIMmQdcaDyZDJryDHncm5b2xvcCDKh8edIMedyblvccmQbCDKh24gyod1bnDh\", \n  \"tIlw4bSJyZR14bSJIMm5b2TJr8edyocgcG/Jr3Nu4bSJx50gb3AgcMedcyAnyofhtIlsx50gxoN1\", \n  \"4bSJyZRz4bSJZOG0iXDJkCDJuW7Kh8edyofJlMedc3VvyZQgJ8qHx53Jr8mQIMqH4bSJcyDJuW9s\", \n  \"b3Agya9uc2ThtIkgya/Hncm5b8ul\", \n  \"MDDLmcaWJC0=\", \n  \"77y0772I772FIO+9ke+9le+9ie+9g++9iyDvvYLvvZLvvY/vvZfvvY4g772G772P772YIO+9iu+9\", \n  \"le+9je+9kO+9kyDvvY/vvZbvvYXvvZIg772U772I772FIO+9jO+9ge+9mu+9mSDvvYTvvY/vvYc=\", \n  \"8J2Qk/CdkKHwnZCeIPCdkKrwnZCu8J2QovCdkJzwnZCkIPCdkJvwnZCr8J2QqPCdkLDwnZCnIPCd\", \n  \"kJ/wnZCo8J2QsSDwnZCj8J2QrvCdkKbwnZCp8J2QrCDwnZCo8J2Qr/CdkJ7wnZCrIPCdkK3wnZCh\", \n  \"8J2QniDwnZCl8J2QmvCdkLPwnZCyIPCdkJ3wnZCo8J2QoA==\", \n  \"8J2Vv/Cdlo3wnZaKIPCdlpbwnZaa8J2WjvCdlojwnZaQIPCdlofwnZaX8J2WlPCdlpzwnZaTIPCd\", \n  \"lovwnZaU8J2WnSDwnZaP8J2WmvCdlpLwnZaV8J2WmCDwnZaU8J2Wm/CdlorwnZaXIPCdlpnwnZaN\", \n  \"8J2WiiDwnZaR8J2WhvCdlp/wnZaeIPCdlonwnZaU8J2WjA==\", \n  \"8J2Ru/CdkonwnZKGIPCdkpLwnZKW8J2SivCdkoTwnZKMIPCdkoPwnZKT8J2SkPCdkpjwnZKPIPCd\", \n  \"kofwnZKQ8J2SmSDwnZKL8J2SlvCdko7wnZKR8J2SlCDwnZKQ8J2Sl/CdkobwnZKTIPCdkpXwnZKJ\", \n  \"8J2ShiDwnZKN8J2SgvCdkpvwnZKaIPCdkoXwnZKQ8J2SiA==\", \n  \"8J2To/Cdk7HwnZOuIPCdk7rwnZO+8J2TsvCdk6zwnZO0IPCdk6vwnZO78J2TuPCdlIDwnZO3IPCd\", \n  \"k6/wnZO48J2UgSDwnZOz8J2TvvCdk7bwnZO58J2TvCDwnZO48J2Tv/Cdk67wnZO7IPCdk73wnZOx\", \n  \"8J2TriDwnZO18J2TqvCdlIPwnZSCIPCdk63wnZO48J2TsA==\", \n  \"8J2Vi/CdlZnwnZWWIPCdlaLwnZWm8J2VmvCdlZTwnZWcIPCdlZPwnZWj8J2VoPCdlajwnZWfIPCd\", \n  \"lZfwnZWg8J2VqSDwnZWb8J2VpvCdlZ7wnZWh8J2VpCDwnZWg8J2Vp/CdlZbwnZWjIPCdlaXwnZWZ\", \n  \"8J2VliDwnZWd8J2VkvCdlavwnZWqIPCdlZXwnZWg8J2VmA==\", \n  \"8J2ag/CdmpHwnZqOIPCdmprwnZqe8J2akvCdmozwnZqUIPCdmovwnZqb8J2amPCdmqDwnZqXIPCd\", \n  \"mo/wnZqY8J2aoSDwnZqT8J2anvCdmpbwnZqZ8J2anCDwnZqY8J2an/Cdmo7wnZqbIPCdmp3wnZqR\", \n  \"8J2ajiDwnZqV8J2aivCdmqPwnZqiIPCdmo3wnZqY8J2akA==\", \n  \"4pKv4pKj4pKgIOKSrOKSsOKSpOKSnuKSpiDikp3ikq3ikqrikrLikqkg4pKh4pKq4pKzIOKSpeKS\", \n  \"sOKSqOKSq+KSriDikqrikrHikqDikq0g4pKv4pKj4pKgIOKSp+KSnOKSteKStCDikp/ikqrikqI=\", \n  \"PHNjcmlwdD5hbGVydCgxMjMpPC9zY3JpcHQ+\", \n  \"Jmx0O3NjcmlwdCZndDthbGVydCgmIzM5OzEyMyYjMzk7KTsmbHQ7L3NjcmlwdCZndDs=\", \n  \"PGltZyBzcmM9eCBvbmVycm9yPWFsZXJ0KDEyMykgLz4=\", \n  \"PHN2Zz48c2NyaXB0PjEyMzwxPmFsZXJ0KDEyMyk8L3NjcmlwdD4=\", \n  \"Ij48c2NyaXB0PmFsZXJ0KDEyMyk8L3NjcmlwdD4=\", \n  \"Jz48c2NyaXB0PmFsZXJ0KDEyMyk8L3NjcmlwdD4=\", \n  \"PjxzY3JpcHQ+YWxlcnQoMTIzKTwvc2NyaXB0Pg==\", \n  \"PC9zY3JpcHQ+PHNjcmlwdD5hbGVydCgxMjMpPC9zY3JpcHQ+\", \n  \"PCAvIHNjcmlwdCA+PCBzY3JpcHQgPmFsZXJ0KDEyMyk8IC8gc2NyaXB0ID4=\", \n  \"b25mb2N1cz1KYVZhU0NyaXB0OmFsZXJ0KDEyMykgYXV0b2ZvY3Vz\", \n  \"IiBvbmZvY3VzPUphVmFTQ3JpcHQ6YWxlcnQoMTIzKSBhdXRvZm9jdXM=\", \n  \"JyBvbmZvY3VzPUphVmFTQ3JpcHQ6YWxlcnQoMTIzKSBhdXRvZm9jdXM=\", \n  \"77ycc2NyaXB077yeYWxlcnQoMTIzKe+8nC9zY3JpcHTvvJ4=\", \n  \"PHNjPHNjcmlwdD5yaXB0PmFsZXJ0KDEyMyk8L3NjPC9zY3JpcHQ+cmlwdD4=\", \n  \"LS0+PHNjcmlwdD5hbGVydCgxMjMpPC9zY3JpcHQ+\", \n  \"IjthbGVydCgxMjMpO3Q9Ig==\", \n  \"JzthbGVydCgxMjMpO3Q9Jw==\", \n  \"SmF2YVNDcmlwdDphbGVydCgxMjMp\", \n  \"O2FsZXJ0KDEyMyk7\", \n  \"c3JjPUphVmFTQ3JpcHQ6cHJvbXB0KDEzMik=\", \n  \"Ij48c2NyaXB0PmFsZXJ0KDEyMyk7PC9zY3JpcHQgeD0i\", \n  \"Jz48c2NyaXB0PmFsZXJ0KDEyMyk7PC9zY3JpcHQgeD0n\", \n  \"PjxzY3JpcHQ+YWxlcnQoMTIzKTs8L3NjcmlwdCB4PQ==\", \n  \"IiBhdXRvZm9jdXMgb25rZXl1cD0iamF2YXNjcmlwdDphbGVydCgxMjMp\", \n  \"JyBhdXRvZm9jdXMgb25rZXl1cD0namF2YXNjcmlwdDphbGVydCgxMjMp\", \n  \"PHNjcmlwdHgyMHR5cGU9InRleHQvamF2YXNjcmlwdCI+amF2YXNjcmlwdDphbGVydCgxKTs8L3Nj\", \n  \"cmlwdD4=\", \n  \"PHNjcmlwdHgzRXR5cGU9InRleHQvamF2YXNjcmlwdCI+amF2YXNjcmlwdDphbGVydCgxKTs8L3Nj\", \n  \"cmlwdD4=\", \n  \"PHNjcmlwdHgwRHR5cGU9InRleHQvamF2YXNjcmlwdCI+amF2YXNjcmlwdDphbGVydCgxKTs8L3Nj\", \n  \"cmlwdD4=\", \n  \"PHNjcmlwdHgwOXR5cGU9InRleHQvamF2YXNjcmlwdCI+amF2YXNjcmlwdDphbGVydCgxKTs8L3Nj\", \n  \"cmlwdD4=\", \n  \"PHNjcmlwdHgwQ3R5cGU9InRleHQvamF2YXNjcmlwdCI+amF2YXNjcmlwdDphbGVydCgxKTs8L3Nj\", \n  \"cmlwdD4=\", \n  \"PHNjcmlwdHgyRnR5cGU9InRleHQvamF2YXNjcmlwdCI+amF2YXNjcmlwdDphbGVydCgxKTs8L3Nj\", \n  \"cmlwdD4=\", \n  \"PHNjcmlwdHgwQXR5cGU9InRleHQvamF2YXNjcmlwdCI+amF2YXNjcmlwdDphbGVydCgxKTs8L3Nj\", \n  \"cmlwdD4=\", \n  \"J2AiPjx4M0NzY3JpcHQ+amF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"J2AiPjx4MDBzY3JpcHQ+amF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieHgzQWV4cHJlc3Npb24oamF2YXNjcmlwdDphbGVydCgxKSI+REVG\", \n  \"QUJDPGRpdiBzdHlsZT0ieDpleHByZXNzaW9ueDVDKGphdmFzY3JpcHQ6YWxlcnQoMSkiPkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDpleHByZXNzaW9ueDAwKGphdmFzY3JpcHQ6YWxlcnQoMSkiPkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDpleHB4MDByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSkiPkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDpleHB4NUNyZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSkiPkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4MEFleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSkiPkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4MDlleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSkiPkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4RTN4ODB4ODBleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSki\", \n  \"PkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4RTJ4ODB4ODRleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSki\", \n  \"PkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4QzJ4QTBleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSkiPkRF\", \n  \"Rg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4RTJ4ODB4ODBleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSki\", \n  \"PkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4RTJ4ODB4OEFleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSki\", \n  \"PkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4MERleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSkiPkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4MENleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSkiPkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4RTJ4ODB4ODdleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSki\", \n  \"PkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4RUZ4QkJ4QkZleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSki\", \n  \"PkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4MjBleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSkiPkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4RTJ4ODB4ODhleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSki\", \n  \"PkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4MDBleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSkiPkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4RTJ4ODB4OEJleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSki\", \n  \"PkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4RTJ4ODB4ODZleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSki\", \n  \"PkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4RTJ4ODB4ODVleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSki\", \n  \"PkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4RTJ4ODB4ODJleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSki\", \n  \"PkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4MEJleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSkiPkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4RTJ4ODB4ODFleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSki\", \n  \"PkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4RTJ4ODB4ODNleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSki\", \n  \"PkRFRg==\", \n  \"QUJDPGRpdiBzdHlsZT0ieDp4RTJ4ODB4ODlleHByZXNzaW9uKGphdmFzY3JpcHQ6YWxlcnQoMSki\", \n  \"PkRFRg==\", \n  \"PGEgaHJlZj0ieDBCamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDBGamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieEMyeEEwamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVs\", \n  \"ZW1lbnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDA1amF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieEUxeEEweDhFamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6\", \n  \"emVsZW1lbnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDE4amF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDExamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieEUyeDgweDg4amF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6\", \n  \"emVsZW1lbnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieEUyeDgweDg5amF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6\", \n  \"emVsZW1lbnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieEUyeDgweDgwamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6\", \n  \"emVsZW1lbnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDE3amF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDAzamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDBFamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDFBamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDAwamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDEwamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieEUyeDgweDgyamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6\", \n  \"emVsZW1lbnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDIwamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDEzamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDA5amF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieEUyeDgweDhBamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6\", \n  \"emVsZW1lbnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDE0amF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDE5amF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieEUyeDgweEFGamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6\", \n  \"emVsZW1lbnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDFGamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieEUyeDgweDgxamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6\", \n  \"emVsZW1lbnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDFEamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieEUyeDgweDg3amF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6\", \n  \"emVsZW1lbnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDA3amF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieEUxeDlBeDgwamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6\", \n  \"emVsZW1lbnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieEUyeDgweDgzamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6\", \n  \"emVsZW1lbnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDA0amF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDAxamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDA4amF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieEUyeDgweDg0amF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6\", \n  \"emVsZW1lbnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieEUyeDgweDg2amF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6\", \n  \"emVsZW1lbnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieEUzeDgweDgwamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6\", \n  \"emVsZW1lbnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDEyamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDBEamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDBBamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDBDamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDE1amF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieEUyeDgweEE4amF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6\", \n  \"emVsZW1lbnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDE2amF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDAyamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDFCamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDA2amF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieEUyeDgweEE5amF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6\", \n  \"emVsZW1lbnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieEUyeDgweDg1amF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6\", \n  \"emVsZW1lbnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDFFamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieEUyeDgxeDlGamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6\", \n  \"emVsZW1lbnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0ieDFDamF2YXNjcmlwdDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0iamF2YXNjcmlwdHgwMDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0iamF2YXNjcmlwdHgzQTpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0iamF2YXNjcmlwdHgwOTpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0iamF2YXNjcmlwdHgwRDpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"PGEgaHJlZj0iamF2YXNjcmlwdHgwQTpqYXZhc2NyaXB0OmFsZXJ0KDEpIiBpZD0iZnV6emVsZW1l\", \n  \"bnQxIj50ZXN0PC9hPg==\", \n  \"YCInPjxpbWcgc3JjPXh4eDp4IHgwQW9uZXJyb3I9amF2YXNjcmlwdDphbGVydCgxKT4=\", \n  \"YCInPjxpbWcgc3JjPXh4eDp4IHgyMm9uZXJyb3I9amF2YXNjcmlwdDphbGVydCgxKT4=\", \n  \"YCInPjxpbWcgc3JjPXh4eDp4IHgwQm9uZXJyb3I9amF2YXNjcmlwdDphbGVydCgxKT4=\", \n  \"YCInPjxpbWcgc3JjPXh4eDp4IHgwRG9uZXJyb3I9amF2YXNjcmlwdDphbGVydCgxKT4=\", \n  \"YCInPjxpbWcgc3JjPXh4eDp4IHgyRm9uZXJyb3I9amF2YXNjcmlwdDphbGVydCgxKT4=\", \n  \"YCInPjxpbWcgc3JjPXh4eDp4IHgwOW9uZXJyb3I9amF2YXNjcmlwdDphbGVydCgxKT4=\", \n  \"YCInPjxpbWcgc3JjPXh4eDp4IHgwQ29uZXJyb3I9amF2YXNjcmlwdDphbGVydCgxKT4=\", \n  \"YCInPjxpbWcgc3JjPXh4eDp4IHgwMG9uZXJyb3I9amF2YXNjcmlwdDphbGVydCgxKT4=\", \n  \"YCInPjxpbWcgc3JjPXh4eDp4IHgyN29uZXJyb3I9amF2YXNjcmlwdDphbGVydCgxKT4=\", \n  \"YCInPjxpbWcgc3JjPXh4eDp4IHgyMG9uZXJyb3I9amF2YXNjcmlwdDphbGVydCgxKT4=\", \n  \"ImAnPjxzY3JpcHQ+eDNCamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eDBEamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEVGeEJCeEJGamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEUyeDgweDgxamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEUyeDgweDg0amF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEUzeDgweDgwamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eDA5amF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEUyeDgweDg5amF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEUyeDgweDg1amF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEUyeDgweDg4amF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eDAwamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEUyeDgweEE4amF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEUyeDgweDhBamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEUxeDlBeDgwamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eDBDamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eDJCamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEYweDkweDk2eDlBamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+LWphdmFzY3JpcHQ6YWxlcnQoMSk8L3NjcmlwdD4=\", \n  \"ImAnPjxzY3JpcHQ+eDBBamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEUyeDgweEFGamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eDdFamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEUyeDgweDg3amF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEUyeDgxeDlGamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEUyeDgweEE5amF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEMyeDg1amF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEVGeEJGeEFFamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEUyeDgweDgzamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEUyeDgweDhCamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEVGeEJGeEJFamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEUyeDgweDgwamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eDIxamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEUyeDgweDgyamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEUyeDgweDg2amF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEUxeEEweDhFamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eDBCamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eDIwamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"ImAnPjxzY3JpcHQ+eEMyeEEwamF2YXNjcmlwdDphbGVydCgxKTwvc2NyaXB0Pg==\", \n  \"PGltZyB4MDBzcmM9eCBvbmVycm9yPSJhbGVydCgxKSI+\", \n  \"PGltZyB4NDdzcmM9eCBvbmVycm9yPSJqYXZhc2NyaXB0OmFsZXJ0KDEpIj4=\", \n  \"PGltZyB4MTFzcmM9eCBvbmVycm9yPSJqYXZhc2NyaXB0OmFsZXJ0KDEpIj4=\", \n  \"PGltZyB4MTJzcmM9eCBvbmVycm9yPSJqYXZhc2NyaXB0OmFsZXJ0KDEpIj4=\", \n  \"PGltZ3g0N3NyYz14IG9uZXJyb3I9ImphdmFzY3JpcHQ6YWxlcnQoMSkiPg==\", \n  \"PGltZ3gxMHNyYz14IG9uZXJyb3I9ImphdmFzY3JpcHQ6YWxlcnQoMSkiPg==\", \n  \"PGltZ3gxM3NyYz14IG9uZXJyb3I9ImphdmFzY3JpcHQ6YWxlcnQoMSkiPg==\", \n  \"PGltZ3gzMnNyYz14IG9uZXJyb3I9ImphdmFzY3JpcHQ6YWxlcnQoMSkiPg==\", \n  \"PGltZ3g0N3NyYz14IG9uZXJyb3I9ImphdmFzY3JpcHQ6YWxlcnQoMSkiPg==\", \n  \"PGltZ3gxMXNyYz14IG9uZXJyb3I9ImphdmFzY3JpcHQ6YWxlcnQoMSkiPg==\", \n  \"PGltZyB4NDdzcmM9eCBvbmVycm9yPSJqYXZhc2NyaXB0OmFsZXJ0KDEpIj4=\", \n  \"PGltZyB4MzRzcmM9eCBvbmVycm9yPSJqYXZhc2NyaXB0OmFsZXJ0KDEpIj4=\", \n  \"PGltZyB4MzlzcmM9eCBvbmVycm9yPSJqYXZhc2NyaXB0OmFsZXJ0KDEpIj4=\", \n  \"PGltZyB4MDBzcmM9eCBvbmVycm9yPSJqYXZhc2NyaXB0OmFsZXJ0KDEpIj4=\", \n  \"PGltZyBzcmN4MDk9eCBvbmVycm9yPSJqYXZhc2NyaXB0OmFsZXJ0KDEpIj4=\", \n  \"PGltZyBzcmN4MTA9eCBvbmVycm9yPSJqYXZhc2NyaXB0OmFsZXJ0KDEpIj4=\", \n  \"PGltZyBzcmN4MTM9eCBvbmVycm9yPSJqYXZhc2NyaXB0OmFsZXJ0KDEpIj4=\", \n  \"PGltZyBzcmN4MzI9eCBvbmVycm9yPSJqYXZhc2NyaXB0OmFsZXJ0KDEpIj4=\", \n  \"PGltZyBzcmN4MTI9eCBvbmVycm9yPSJqYXZhc2NyaXB0OmFsZXJ0KDEpIj4=\", \n  \"PGltZyBzcmN4MTE9eCBvbmVycm9yPSJqYXZhc2NyaXB0OmFsZXJ0KDEpIj4=\", \n  \"PGltZyBzcmN4MDA9eCBvbmVycm9yPSJqYXZhc2NyaXB0OmFsZXJ0KDEpIj4=\", \n  \"PGltZyBzcmN4NDc9eCBvbmVycm9yPSJqYXZhc2NyaXB0OmFsZXJ0KDEpIj4=\", \n  \"PGltZyBzcmM9eHgwOW9uZXJyb3I9ImphdmFzY3JpcHQ6YWxlcnQoMSkiPg==\", \n  \"PGltZyBzcmM9eHgxMG9uZXJyb3I9ImphdmFzY3JpcHQ6YWxlcnQoMSkiPg==\", \n  \"PGltZyBzcmM9eHgxMW9uZXJyb3I9ImphdmFzY3JpcHQ6YWxlcnQoMSkiPg==\", \n  \"PGltZyBzcmM9eHgxMm9uZXJyb3I9ImphdmFzY3JpcHQ6YWxlcnQoMSkiPg==\", \n  \"PGltZyBzcmM9eHgxM29uZXJyb3I9ImphdmFzY3JpcHQ6YWxlcnQoMSkiPg==\", \n  \"PGltZ1thXVtiXVtjXXNyY1tkXT14W2Vdb25lcnJvcj1bZl0iYWxlcnQoMSkiPg==\", \n  \"PGltZyBzcmM9eCBvbmVycm9yPXgwOSJqYXZhc2NyaXB0OmFsZXJ0KDEpIj4=\", \n  \"PGltZyBzcmM9eCBvbmVycm9yPXgxMCJqYXZhc2NyaXB0OmFsZXJ0KDEpIj4=\", \n  \"PGltZyBzcmM9eCBvbmVycm9yPXgxMSJqYXZhc2NyaXB0OmFsZXJ0KDEpIj4=\", \n  \"PGltZyBzcmM9eCBvbmVycm9yPXgxMiJqYXZhc2NyaXB0OmFsZXJ0KDEpIj4=\", \n  \"PGltZyBzcmM9eCBvbmVycm9yPXgzMiJqYXZhc2NyaXB0OmFsZXJ0KDEpIj4=\", \n  \"PGltZyBzcmM9eCBvbmVycm9yPXgwMCJqYXZhc2NyaXB0OmFsZXJ0KDEpIj4=\", \n  \"PGEgaHJlZj1qYXZhJiMxJiMyJiMzJiM0JiM1JiM2JiM3JiM4JiMxMSYjMTJzY3JpcHQ6amF2YXNj\", \n  \"cmlwdDphbGVydCgxKT5YWFg8L2E+\", \n  \"PGltZyBzcmM9InhgIGA8c2NyaXB0PmphdmFzY3JpcHQ6YWxlcnQoMSk8L3NjcmlwdD4iYCBgPg==\", \n  \"PGltZyBzcmMgb25lcnJvciAvIiAnIj0gYWx0PWphdmFzY3JpcHQ6YWxlcnQoMSkvLyI+\", \n  \"PHRpdGxlIG9ucHJvcGVydHljaGFuZ2U9amF2YXNjcmlwdDphbGVydCgxKT48L3RpdGxlPjx0aXRs\", \n  \"ZSB0aXRsZT0+\", \n  \"PGEgaHJlZj1odHRwOi8vZm9vLmJhci8jeD1geT48L2E+PGltZyBhbHQ9ImA+PGltZyBzcmM9eDp4\", \n  \"IG9uZXJyb3I9amF2YXNjcmlwdDphbGVydCgxKT48L2E+Ij4=\", \n  \"PCEtLVtpZl0+PHNjcmlwdD5qYXZhc2NyaXB0OmFsZXJ0KDEpPC9zY3JpcHQgLS0+\", \n  \"PCEtLVtpZjxpbWcgc3JjPXggb25lcnJvcj1qYXZhc2NyaXB0OmFsZXJ0KDEpLy9dPiAtLT4=\", \n  \"PHNjcmlwdCBzcmM9Ii8lKGpzY3JpcHQpcyI+PC9zY3JpcHQ+\", \n  \"PHNjcmlwdCBzcmM9IlwlKGpzY3JpcHQpcyI+PC9zY3JpcHQ+\", \n  \"PElNRyAiIiI+PFNDUklQVD5hbGVydCgiWFNTIik8L1NDUklQVD4iPg==\", \n  \"PElNRyBTUkM9amF2YXNjcmlwdDphbGVydChTdHJpbmcuZnJvbUNoYXJDb2RlKDg4LDgzLDgzKSk+\", \n  \"PElNRyBTUkM9IyBvbm1vdXNlb3Zlcj0iYWxlcnQoJ3h4cycpIj4=\", \n  \"PElNRyBTUkM9IG9ubW91c2VvdmVyPSJhbGVydCgneHhzJykiPg==\", \n  \"PElNRyBvbm1vdXNlb3Zlcj0iYWxlcnQoJ3h4cycpIj4=\", \n  \"PElNRyBTUkM9JiMxMDY7JiM5NzsmIzExODsmIzk3OyYjMTE1OyYjOTk7JiMxMTQ7JiMxMDU7JiMx\", \n  \"MTI7JiMxMTY7JiM1ODsmIzk3OyYjMTA4OyYjMTAxOyYjMTE0OyYjMTE2OyYjNDA7JiMzOTsmIzg4\", \n  \"OyYjODM7JiM4MzsmIzM5OyYjNDE7Pg==\", \n  \"PElNRyBTUkM9JiMwMDAwMTA2JiMwMDAwMDk3JiMwMDAwMTE4JiMwMDAwMDk3JiMwMDAwMTE1JiMw\", \n  \"MDAwMDk5JiMwMDAwMTE0JiMwMDAwMTA1JiMwMDAwMTEyJiMwMDAwMTE2JiMwMDAwMDU4JiMwMDAw\", \n  \"MDk3JiMwMDAwMTA4JiMwMDAwMTAxJiMwMDAwMTE0JiMwMDAwMTE2JiMwMDAwMDQwJiMwMDAwMDM5\", \n  \"JiMwMDAwMDg4JiMwMDAwMDgzJiMwMDAwMDgzJiMwMDAwMDM5JiMwMDAwMDQxPg==\", \n  \"PElNRyBTUkM9JiN4NkEmI3g2MSYjeDc2JiN4NjEmI3g3MyYjeDYzJiN4NzImI3g2OSYjeDcwJiN4\", \n  \"NzQmI3gzQSYjeDYxJiN4NkMmI3g2NSYjeDcyJiN4NzQmI3gyOCYjeDI3JiN4NTgmI3g1MyYjeDUz\", \n  \"JiN4MjcmI3gyOT4=\", \n  \"PElNRyBTUkM9ImphdiBhc2NyaXB0OmFsZXJ0KCdYU1MnKTsiPg==\", \n  \"PElNRyBTUkM9ImphdiYjeDA5O2FzY3JpcHQ6YWxlcnQoJ1hTUycpOyI+\", \n  \"PElNRyBTUkM9ImphdiYjeDBBO2FzY3JpcHQ6YWxlcnQoJ1hTUycpOyI+\", \n  \"PElNRyBTUkM9ImphdiYjeDBEO2FzY3JpcHQ6YWxlcnQoJ1hTUycpOyI+\", \n  \"cGVybCAtZSAncHJpbnQgIjxJTUcgU1JDPWphdmEwc2NyaXB0OmFsZXJ0KCJYU1MiKT4iOycgPiBv\", \n  \"dXQ=\", \n  \"PElNRyBTUkM9IiAmIzE0OyBqYXZhc2NyaXB0OmFsZXJ0KCdYU1MnKTsiPg==\", \n  \"PFNDUklQVC9YU1MgU1JDPSJodHRwOi8vaGEuY2tlcnMub3JnL3hzcy5qcyI+PC9TQ1JJUFQ+\", \n  \"PEJPRFkgb25sb2FkISMkJSYoKSp+Ky1fLiw6Oz9AWy98XV5gPWFsZXJ0KCJYU1MiKT4=\", \n  \"PFNDUklQVC9TUkM9Imh0dHA6Ly9oYS5ja2Vycy5vcmcveHNzLmpzIj48L1NDUklQVD4=\", \n  \"PDxTQ1JJUFQ+YWxlcnQoIlhTUyIpOy8vPDwvU0NSSVBUPg==\", \n  \"PFNDUklQVCBTUkM9aHR0cDovL2hhLmNrZXJzLm9yZy94c3MuanM/PCBCID4=\", \n  \"PFNDUklQVCBTUkM9Ly9oYS5ja2Vycy5vcmcvLmo+\", \n  \"PElNRyBTUkM9ImphdmFzY3JpcHQ6YWxlcnQoJ1hTUycpIg==\", \n  \"PGlmcmFtZSBzcmM9aHR0cDovL2hhLmNrZXJzLm9yZy9zY3JpcHRsZXQuaHRtbCA8\", \n  \"IjthbGVydCgnWFNTJyk7Ly8=\", \n  \"PHUgb25jb3B5PWFsZXJ0KCk+IENvcHkgbWU8L3U+\", \n  \"PGkgb253aGVlbD1hbGVydCgxKT4gU2Nyb2xsIG92ZXIgbWUgPC9pPg==\", \n  \"PHBsYWludGV4dD4=\", \n  \"aHR0cDovL2EvJSUzMCUzMA==\", \n  \"PC90ZXh0YXJlYT48c2NyaXB0PmFsZXJ0KDEyMyk8L3NjcmlwdD4=\", \n  \"MTtEUk9QIFRBQkxFIHVzZXJz\", \n  \"MSc7IERST1AgVEFCTEUgdXNlcnMtLSAx\", \n  \"JyBPUiAxPTEgLS0gMQ==\", \n  \"JyBPUiAnMSc9JzE=\", \n  \"JQ==\", \n  \"Xw==\", \n  \"LQ==\", \n  \"LS0=\", \n  \"LS12ZXJzaW9u\", \n  \"LS1oZWxw\", \n  \"JFVTRVI=\", \n  \"L2Rldi9udWxsOyB0b3VjaCAvdG1wL2JsbnMuZmFpbCA7IGVjaG8=\", \n  \"YHRvdWNoIC90bXAvYmxucy5mYWlsYA==\", \n  \"JCh0b3VjaCAvdG1wL2JsbnMuZmFpbCk=\", \n  \"QHtbc3lzdGVtICJ0b3VjaCAvdG1wL2JsbnMuZmFpbCJdfQ==\", \n  \"ZXZhbCgicHV0cyAnaGVsbG8gd29ybGQnIik=\", \n  \"U3lzdGVtKCJscyAtYWwgLyIp\", \n  \"YGxzIC1hbCAvYA==\", \n  \"S2VybmVsLmV4ZWMoImxzIC1hbCAvIik=\", \n  \"S2VybmVsLmV4aXQoMSk=\", \n  \"JXgoJ2xzIC1hbCAvJyk=\", \n  \"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iSVNPLTg4NTktMSI/PjwhRE9DVFlQRSBmb28g\", \n  \"WyA8IUVMRU1FTlQgZm9vIEFOWSA+PCFFTlRJVFkgeHhlIFNZU1RFTSAiZmlsZTovLy9ldGMvcGFz\", \n  \"c3dkIiA+XT48Zm9vPiZ4eGU7PC9mb28+\", \n  \"JEhPTUU=\", \n  \"JEVOVnsnSE9NRSd9\", \n  \"JWQ=\", \n  \"JXM=\", \n  \"ezB9\", \n  \"JSouKnM=\", \n  \"Li4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vZXRjL3Bhc3N3ZCUwMA==\", \n  \"Li4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vZXRjL2hvc3Rz\", \n  \"KCkgeyAwOyB9OyB0b3VjaCAvdG1wL2JsbnMuc2hlbGxzaG9jazEuZmFpbDs=\", \n  \"KCkgeyBfOyB9ID5fWyQoJCgpKV0geyB0b3VjaCAvdG1wL2JsbnMuc2hlbGxzaG9jazIuZmFpbDsg\", \n  \"fQ==\", \n  \"PDw8ICVzKHVuPSclcycpID0gJXU=\", \n  \"KysrQVRIMA==\", \n  \"Q09O\", \n  \"UFJO\", \n  \"QVVY\", \n  \"Q0xPQ0sk\", \n  \"TlVM\", \n  \"QTo=\", \n  \"Wlo6\", \n  \"Q09NMQ==\", \n  \"TFBUMQ==\", \n  \"TFBUMg==\", \n  \"TFBUMw==\", \n  \"Q09NMg==\", \n  \"Q09NMw==\", \n  \"Q09NNA==\", \n  \"RENDIFNFTkQgU1RBUlRLRVlMT0dHRVIgMCAwIDA=\", \n  \"U2N1bnRob3JwZSBHZW5lcmFsIEhvc3BpdGFs\", \n  \"UGVuaXN0b25lIENvbW11bml0eSBDaHVyY2g=\", \n  \"TGlnaHR3YXRlciBDb3VudHJ5IFBhcms=\", \n  \"SmltbXkgQ2xpdGhlcm9l\", \n  \"SG9ybmltYW4gTXVzZXVt\", \n  \"c2hpdGFrZSBtdXNocm9vbXM=\", \n  \"Um9tYW5zSW5TdXNzZXguY28udWs=\", \n  \"aHR0cDovL3d3dy5jdW0ucWMuY2Ev\", \n  \"Q3JhaWcgQ29ja2J1cm4sIFNvZnR3YXJlIFNwZWNpYWxpc3Q=\", \n  \"TGluZGEgQ2FsbGFoYW4=\", \n  \"RHIuIEhlcm1hbiBJLiBMaWJzaGl0eg==\", \n  \"bWFnbmEgY3VtIGxhdWRl\", \n  \"U3VwZXIgQm93bCBYWFg=\", \n  \"bWVkaWV2YWwgZXJlY3Rpb24gb2YgcGFyYXBldHM=\", \n  \"ZXZhbHVhdGU=\", \n  \"bW9jaGE=\", \n  \"ZXhwcmVzc2lvbg==\", \n  \"QXJzZW5hbCBjYW5hbA==\", \n  \"Y2xhc3NpYw==\", \n  \"VHlzb24gR2F5\", \n  \"RGljayBWYW4gRHlrZQ==\", \n  \"YmFzZW1lbnQ=\", \n  \"SWYgeW91J3JlIHJlYWRpbmcgdGhpcywgeW91J3ZlIGJlZW4gaW4gYSBjb21hIGZvciBhbG1vc3Qg\", \n  \"MjAgeWVhcnMgbm93LiBXZSdyZSB0cnlpbmcgYSBuZXcgdGVjaG5pcXVlLiBXZSBkb24ndCBrbm93\", \n  \"IHdoZXJlIHRoaXMgbWVzc2FnZSB3aWxsIGVuZCB1cCBpbiB5b3VyIGRyZWFtLCBidXQgd2UgaG9w\", \n  \"ZSBpdCB3b3Jrcy4gUGxlYXNlIHdha2UgdXAsIHdlIG1pc3MgeW91Lg==\", \n  \"Um9zZXMgYXJlIBtbMDszMW1yZWQbWzBtLCB2aW9sZXRzIGFyZSAbWzA7MzRtYmx1ZS4gSG9wZSB5\", \n  \"b3UgZW5qb3kgdGVybWluYWwgaHVl\", \n  \"QnV0IG5vdy4uLhtbMjBDZm9yIG15IGdyZWF0ZXN0IHRyaWNrLi4uG1s4bQ==\", \n  \"VGhlIHF1aWMICAgICAhrIGJyb3duIGZvBwcHBwcHBwcHBwd4Li4uIFtCZWVlZXBd\", \n  \"UG93ZXLZhNmP2YTZj9i12ZHYqNmP2YTZj9mE2LXZkdio2Y/Ysdix2Ysg4KWjIOClo2gg4KWjIOCl\", \n  \"o+WGlw==\"\n]"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleBadStrings/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"password\"\n      verbosity: 0\n\n  - debug:\n      msg: \"{{ item | b64decode | to_json }}\"\n      verbosity: 0\n    with_items: \"{{ lookup('file', 'blns.base64.json') | from_json }}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleConfigFile/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n  runtime: \"concord-v2\"\n\nflows:\n  default:\n    - task: ansible\n      in:\n        playbook: playbook/hello.yml\n        configFile: user.cfg\n        extraVars:\n          greetings: \"Hello, world\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleConfigFile/myInventory.ini",
    "content": "[local]\n127.0.0.1 ansible_connection=local\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleConfigFile/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ greetings }}\"\n      verbosity: 0\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleConfigFile/user.cfg",
    "content": "[defaults]\ninventory = ./myInventory.ini\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleEvent/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        msg: \"Hi there!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleEvent/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ msg }}\"\n      verbosity: 0"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleEventProcessor/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        msg: \"Hi there!\"\n\n  generateNonPrintableChars:\n    - task: ansible\n      in:\n        playbook: playbook/unicode_sanitization.yml\n        inventory:\n          local:\n            hosts:\n              - \"127.0.0.1\"\n            vars:\n              ansible_connection: \"local\"\n\n  emitLongNames:\n  - set:\n      localHosts:\n        - my-inventory-host-001\n        - my-inventory-host-002\n        - my-inventory-host-003\n        - my-inventory-host-004\n        - my-inventory-host-005\n        - my-inventory-host-006\n        - my-inventory-host-007\n        - my-inventory-host-008\n        - my-inventory-host-009\n        - my-inventory-host-010\n        - my-inventory-host-011\n        - my-inventory-host-012\n        - my-inventory-host-013\n        - my-inventory-host-014\n        - my-inventory-host-015\n        - my-inventory-host-016\n        - my-inventory-host-017\n        - my-inventory-host-018\n        - my-inventory-host-019\n        - my-inventory-host-020\n        - my-inventory-host-021\n        - my-inventory-host-022\n        - my-inventory-host-023\n        - my-inventory-host-024\n        - my-inventory-host-025\n        - my-inventory-host-026\n        - my-inventory-host-027\n        - my-inventory-host-028\n        - my-inventory-host-029\n        - my-inventory-host-030\n        - my-inventory-host-031\n        - my-inventory-host-032\n        - my-inventory-host-033\n        - my-inventory-host-034\n        - my-inventory-host-035\n        - my-inventory-host-036\n        - my-inventory-host-037\n        - my-inventory-host-038\n        - my-inventory-host-039\n        - my-inventory-host-040\n        - my-inventory-host-041\n        - my-inventory-host-042\n        - my-inventory-host-043\n        - my-inventory-host-044\n        - my-inventory-host-045\n        - my-inventory-host-046\n        - my-inventory-host-047\n        - my-inventory-host-048\n        - my-inventory-host-049\n        - my-inventory-host-050\n  - task: ansible\n    in:\n      playbook: playbook/large_play_and_task_names.yml\n      inventory:\n        local:\n          hosts: \"${localHosts}\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        msg: \"Hi there!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleEventProcessor/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ msg }}\"\n      verbosity: 0\n  - debug:\n      msg: \"{{ msg }}\"\n      verbosity: 0\n    when: false\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleEventProcessor/playbook/large_play_and_task_names.yml",
    "content": "---\n- hosts: \"{{ groups['local'] }}\"\n  serial: \"50%\"\n  gather_facts: false\n  tasks:\n    - name: \"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"\n      debug:\n        msg: \"{{ msg }}\"\n        verbosity: 0\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleEventProcessor/playbook/unicode_sanitization.yml",
    "content": "---\n- hosts: local\n  gather_facts: false\n  tasks:\n    - name: \"Print some non-printable unicode characters\"\n      ansible.builtin.shell:\n        cmd: bash -c 'echo -ne \"aNul\\x00\\naSoh\\x01\\nNotNul\\\\\\\\u0000\\n\"'\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleExternalPlaybook/payload/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: myplaybook.yml\n      inventory:\n        local:\n          hosts:\n          - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleExternalPlaybook/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"External playbook\"\n      verbosity: 0"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleExtraVarsFiles/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n    - task: ansible\n      in:\n        playbook: playbook/hello.yml\n        inventory:\n          local:\n            hosts:\n              - \"127.0.0.1\"\n            vars:\n              ansible_connection: \"local\"\n        extraVarsFiles:\n          - \"extra.json\"\n          - \"extra.yml\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleExtraVarsFiles/extra.json",
    "content": "{\n  \"msg1\": \"Hello from a JSON file!\"\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleExtraVarsFiles/extra.yml",
    "content": "msg2: \"Hello from a YAML file!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleExtraVarsFiles/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ msg1 }}\"\n      verbosity: 0\n  - debug:\n      msg: \"{{ msg2 }}\"\n      verbosity: 0\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleFailedHosts/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: \"playbook.yml\"\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleFailedHosts/playbook.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - fail:\n      msg: \"echo {{ msg }}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleGroupVars/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n    - task: ansible\n      in:\n        playbook: playbook/hello.yml\n        groupVars:\n        - local:\n            orgName: ${myOrgName}\n            secretName: ${mySecretName}\n        inventory:\n          local:\n            hosts:\n            - \"127.0.0.1\"\n            vars:\n              ansible_connection: \"local\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleGroupVars/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ greetings }}\"\n      verbosity: 0"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleIgnoredFailures/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        msg: \"hello\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleIgnoredFailures/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - fail:\n      msg: \"echo {{ msg }}\"\n    ignore_errors: yes"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleInventoryMix/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n    - task: ansible\n      in:\n        playbook: \"playbook.yml\"\n        inventory:\n          - \"inventory.ini\"\n          - local:\n              children:\n                - test\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleInventoryMix/inventory.ini",
    "content": "[test]\n127.0.0.1 ansible_connection=local"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleInventoryMix/playbook.yml",
    "content": "---\n- hosts: test\n  tasks:\n    - debug:\n        msg: \"Hello!\"\n        verbosity: 0\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleInventoryName/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n    - task: ansible\n      in:\n        debug: true\n        playbook: \"playbook.yml\"\n        inventoryFile: \"inventory'$( whoami )'&123.ini\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleInventoryName/inventory'$( whoami )'&123.ini",
    "content": "[test]\n127.0.0.1 ansible_connection=local"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleInventoryName/playbook.yml",
    "content": "---\n- hosts: test\n  tasks:\n    - debug:\n        msg: \"Hello!\"\n        verbosity: 0\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLargeVerbose/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n  arguments:\n    groupLimit: null\n\nflows:\n  default:\n    - task: ansible\n      in:\n        verbose: ${Integer.parseInt(verboseLevel)}\n        playbook: ${playbook}\n        limit: ${groupLimit}\n        inventoryFile: ${invFile}\n\n    - log: \"ansible completed successfully\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLargeVerbose/inventory_large.ini",
    "content": "[test]\nmyhost1.local ansible_connection=local\nmyhost2.local ansible_connection=local\nmyhost3.local ansible_connection=local\nmyhost4.local ansible_connection=local\nmyhost5.local ansible_connection=local\nmyhost6.local ansible_connection=local\nmyhost7.local ansible_connection=local\nmyhost8.local ansible_connection=local\nmyhost9.local ansible_connection=local\nmyhost10.local ansible_connection=local\nmyhost11.local ansible_connection=local\nmyhost12.local ansible_connection=local\nmyhost13.local ansible_connection=local\nmyhost14.local ansible_connection=local\nmyhost15.local ansible_connection=local\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLargeVerbose/inventory_limit.ini",
    "content": "[dev]\ndev01 ansible_connection=local\n\n[test]\ntest01 ansible_connection=local\ntest02 ansible_connection=local\ntest03 ansible_connection=local\ntest04 ansible_connection=local\ntest05 ansible_connection=local\ntest06 ansible_connection=local\ntest07 ansible_connection=local\ntest08 ansible_connection=local\ntest09 ansible_connection=local\ntest10 ansible_connection=local\ntest11 ansible_connection=local\n\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLargeVerbose/inventory_small.ini",
    "content": "[test]\n127.0.0.1 ansible_connection=local\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLargeVerbose/more_tasks.yml",
    "content": "---\n- debug:\n    msg: \"Debug one\"\n- debug:\n    msg: \"Debug two\"\n- debug:\n    msg: \"Debug three\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLargeVerbose/playbook_include.yml",
    "content": "---\n- hosts: all\n  tasks:\n  - include_tasks: more_tasks.yml\n    when: True\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLargeVerbose/playbook_multi.yml",
    "content": "---\n- hosts: all\n  tasks:\n    - debug:\n        msg: \"Debug one\"\n    - debug:\n        msg: \"Debug two\"\n    - debug:\n        msg: \"Debug three\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLargeVerbose/playbook_single.yml",
    "content": "---\n- hosts: all\n  gather_facts: no\n  tasks:\n    - debug:\n        msg: \"Hello!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLimitWithMultipleHost/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n    - task: ansible\n      withItems:\n        - \"${'aaa,ccc'.split(',')}\"\n        - [\"aaa\", \"ccc\"]\n      in:\n        playbook: playbook/hello.yml\n        inventory:\n          - aaa:\n              hosts:\n                - \"127.0.0.1\"\n              vars:\n                ansible_connection: \"local\"\n                msg: \"Hello aaa\"\n          - bbb:\n              hosts:\n                - \"127.0.0.2\"\n              vars:\n                ansible_connection: \"local\"\n                msg: \"Hello bbb\"\n          - ccc:\n              hosts:\n                - \"127.0.0.3\"\n              vars:\n                ansible_connection: \"local\"\n                msg: \"Hello ccc\"\n        limit: \"${item}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLimitWithMultipleHost/playbook/hello.yml",
    "content": "---\n- hosts: aaa\n  tasks:\n    - debug:\n        msg: \"{{ msg }}\"\n        verbosity: 0\n\n- hosts: bbb\n  tasks:\n    - debug:\n        msg: \"{{ msg }}\"\n        verbosity: 0\n\n- hosts: ccc\n  tasks:\n    - debug:\n        msg: \"{{ msg }}\"\n        verbosity: 0\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLogFiltering/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n    - task: ansible\n      in:\n        playbook: \"playbook.yml\"\n        enableLogFiltering: \"${Boolean.valueOf(doFilter)}\"\n        inventory:\n          local:\n            hosts:\n              - \"127.0.0.1\"\n            vars:\n              ansible_connection: \"local\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLogFiltering/playbook.yml",
    "content": "---\n- hosts: all\n  tasks:\n    - debug:\n        msg: \"Hello! my_password='n0N30fUr8uS1N3sS'\"\n        verbosity: 0\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLookupPublicKey/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      debug: true\n      enableLogFiltering: true\n      verbose: 3\n      playbook: \"playbook/hello.yml\"\n      inventory:\n        local:\n          hosts:\n          - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        orgName: ${orgName}\n        secretName: ${secretName}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLookupPublicKey/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"Explicit org: {{ lookup('concord_public_key_secret', orgName, secretName) }}\"\n      verbosity: 0\n  - debug:\n      msg: \"Implicit org: {{ lookup('concord_public_key_secret', secretName) }}\"\n      verbosity: 0\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLookupSecret/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      debug: true\n      enableLogFiltering: true\n      verbose: 3\n      playbook: \"playbook/hello.yml\"\n      inventory:\n        local:\n          hosts:\n          - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        orgName: ${orgName}\n        secretPwd: ${secretPwd}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLookupSecret/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"Explicit org {{ lookup('concord_secret', orgName, 'mySecret', secretPwd) }}\"\n      verbosity: 0\n  - debug:\n      msg: \"Implicit org {{ lookup('concord_secret', 'mySecret', secretPwd) }}\"\n      verbosity: 0\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLookupSecretData/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      debug: true\n      enableLogFiltering: true\n      verbose: 3\n      playbook: \"playbook/hello.yml\"\n      inventory:\n        local:\n          hosts:\n          - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        orgName: ${orgName}\n        secretPwd: ${secretPwd}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLookupSecretData/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"Explicit org {{ lookup('concord_data_secret', orgName, 'mySecret', secretPwd) }}\"\n      verbosity: 0\n  - debug:\n      msg: \"Implicit org {{ lookup('concord_data_secret', 'mySecret', secretPwd) }}\"\n      verbosity: 0\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLookupSecretDataNoPassword/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      debug: true\n      enableLogFiltering: true\n      verbose: 3\n      playbook: \"playbook/hello.yml\"\n      inventory:\n        local:\n          hosts:\n          - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        orgName: ${orgName}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLookupSecretDataNoPassword/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"Explicit org {{ lookup('concord_data_secret', orgName, 'mySecret', None) }}\"\n      verbosity: 0\n  - debug:\n      msg: \"Implicit org {{ lookup('concord_data_secret', 'mySecret', None) }}\"\n      verbosity: 0\n\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLookupSecretDataValue/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: \"playbook/hello.yml\"\n      inventory:\n        local:\n          hosts:\n          - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        orgName: \"${orgName}\"\n        secretName: \"${secretName}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleLookupSecretDataValue/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"Value: {{ lookup('concord_data_secret', orgName, secretName, None) }}\"\n      verbosity: 0\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleMergeDefaults/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      debug: true\n      config:\n        defaults:\n          callback_plugins: \"myCallbackDir\"\n      inventory:\n        local:\n          hosts:\n          - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        greetings: \"hello!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleMergeDefaults/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ greetings }}\"\n      verbosity: 0"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleMultiInventory/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n    - task: ansible\n      in:\n        playbook: playbook/hello.yml\n        inventory:\n          - aaa:\n              hosts:\n                - \"127.0.0.1\"\n              vars:\n                ansible_connection: \"local\"\n                msg: \"Hello aaa\"\n          - bbb:\n              hosts:\n                - \"127.0.0.2\"\n              vars:\n                ansible_connection: \"local\"\n                msg: \"Hello bbb\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleMultiInventory/playbook/hello.yml",
    "content": "---\n- hosts: aaa\n  tasks:\n    - debug:\n        msg: \"{{ msg }}\"\n        verbosity: 0\n\n- hosts: bbb\n  tasks:\n    - debug:\n        msg: \"{{ msg }}\"\n        verbosity: 0\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleMultiInventoryFile/aaa.ini",
    "content": "[aaa]\n127.0.0.1 ansible_connection=local msg=\"Hello aaa\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleMultiInventoryFile/bbb.ini",
    "content": "[bbb]\n127.0.0.2 ansible_connection=local msg=\"Hello bbb\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleMultiInventoryFile/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n    - task: ansible\n      in:\n        playbook: playbook/hello.yml\n        inventoryFile:\n          - \"aaa.ini\"\n          - \"bbb.ini\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleMultiInventoryFile/playbook/hello.yml",
    "content": "---\n- hosts: aaa\n  tasks:\n    - debug:\n        msg: \"{{ msg }}\"\n        verbosity: 0\n\n- hosts: bbb\n  tasks:\n    - debug:\n        msg: \"{{ msg }}\"\n        verbosity: 0\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleOutVars/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      outVars: \"message,message2\"\n\n  - log: ${message.get(\"127.0.0.1\").get(\"msg\")}\n  - log: ${message2.get(\"127.0.0.1\").get(\"msg\")}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleOutVars/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"First hello from ansible\"\n      verbosity: 0\n    register: message\n\n  - debug:\n      msg: \"Second message\"\n      verbosity: 0\n    register: message2\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansiblePolicyTaskDeny/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n  entryPoint: main\n\nflows:\n  main:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        greetings: \"Hello, world\"\n      enablePolicy: true\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansiblePolicyTaskDeny/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ greetings }}\"\n      verbosity: 0\n\n  - name: Copy a local file\n    copy:\n      src: super-secret-file.dat\n      dest: /tmp/my.ini\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansiblePolicyTaskDeny/test-policy.json",
    "content": "{\n  \"ansible\": {\n    \"deny\": [\n      {\n        \"action\": \"copy\",\n        \"params\": [\n          {\n            \"name\": \"src\",\n            \"values\": [\".*secret.*\"]\n          }\n        ]\n      }\n    ]\n  }\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleRawStrings/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleRawStrings/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"message {%raw%}'{{.lalala}}'{%endraw%}\"\n      verbosity: 0\n  - debug:\n      msg: !unsafe \"message {{.unsafe}}\"\n      verbosity: 0\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleSaveRetry/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      user: \"aRandomUser\"\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"garbage\" # to make it fail\n      saveRetryFile: true\n      extraVars:\n        msg: \"Hi there!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleSaveRetry/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ msg }}\"\n      verbosity: 0"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleSkipTags/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n  entryPoint: main\n\nflows:\n  main:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        greetings: \"Hello, world\"\n        greetings2: \"Hello2, world\"\n      skipTags:\n        - \"tag-1\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleSkipTags/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ greetings }}\"\n      verbosity: 0\n    tags:\n      - \"tag-1\"\n\n  - debug:\n      msg: \"{{ greetings2 }}\"\n      verbosity: 0\n    tags:\n      - \"tag-2\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleStats/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      outVars: \"_stats\"\n\n  - log: \"OK: ${ _stats.ok }\"\n\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleStats/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"Hello from ansible\"\n      verbosity: 0\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleTemplateArgs/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleTemplateArgs/playbook/hello.yml",
    "content": "---\n- hosts: local\n  gather_facts: no\n  vars:\n    myVar: 'iddqd'\n  tasks:\n  - debug:\n      msg: \"message {{ myVar }}\"\n      verbosity: 0\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleVault/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n  entryPoint: main\n\nflows:\n  main:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      vaultPasswordFile: get_password.py\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleVault/get_password.py",
    "content": "#!/usr/bin/python3\nprint(\"q1\")\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleVault/playbook/group_vars/all.yml",
    "content": "$ANSIBLE_VAULT;1.1;AES256\n64333662343066326434303130393832363030313236303961303663363336333136323431616263\n6663336536393132636461333831323963626539336439330a353430646635396663376435303439\n35636633383464656437343366613866363963613235323035316135633432356464396332303562\n3735626264393232370a386539643533356233653639653263633331343136323938646464326139\n33343465373866386461643662666461373562613139383531373162616138366363\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleVault/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ greetings }}\"\n      verbosity: 0"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleVaultMultiplePasswordFiles/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      vaultPasswordFile:\n         local: \"get_local_password.py\"\n         all: \"get_all_password.py\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleVaultMultiplePasswordFiles/get_all_password.py",
    "content": "#!/usr/bin/python3\nprint(\"q2\")\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleVaultMultiplePasswordFiles/get_local_password.py",
    "content": "#!/usr/bin/python3\nprint(\"q1\")\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleVaultMultiplePasswordFiles/playbook/group_vars/all.yml",
    "content": "$ANSIBLE_VAULT;1.1;AES256\n35333438663066653565323339616238333437626666636134326231373033376230303036323831\n6563366564363966356266613737383633343330313534640a616132643965666339626236333363\n62633261363339326364316566383730313462636234373037636361313235613134616333363533\n6238396231633832370a366533393539336636373064633562623031613533343939343333666231\n36366664663836316262353134353561633635633737326663383565383562643466\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleVaultMultiplePasswordFiles/playbook/group_vars/local.yml",
    "content": "$ANSIBLE_VAULT;1.1;AES256\n30353739353166323138373262303034333166656130643231626638383335396566653335326261\n3134643166356634373031333633356437636632333539610a383363623261373338303365356636\n35666666363732376461393832353062366238313365646162646337316564323136326364653764\n3561326131386561370a326239353665346666663965613937306231306230636537343764373739\n3339\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleVaultMultiplePasswordFiles/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ greetings }} {{ name }}\"\n      verbosity: 0"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleVaultMultiplePasswords/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      vaultPassword:\n         local: \"q1\"\n         all: \"q2\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleVaultMultiplePasswords/playbook/group_vars/all.yml",
    "content": "$ANSIBLE_VAULT;1.1;AES256\n30653265613665303864383037653234633263656133623266373366343732393536393865366339\n3130366230633237316232633361323631623065303332330a363637663631323464383538303861\n66303333313562333337313037353966643838373233356533396462623332633233646237373235\n3064636134656365360a663762383033663736626162616431616566663539373739333838363865\n32666463313739313835383238383432656436306435643736306562393739656332\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleVaultMultiplePasswords/playbook/group_vars/local.yml",
    "content": "$ANSIBLE_VAULT;1.1;AES256\n64363531303438336335326234613434343133343864323932623836656437346537626530656663\n3464373464393530313132643566366562646165376363340a663938653430393133336636396236\n32373135333761333934656137653631643632393638366330383434336133376661323532356535\n6163656533323832360a626566653232356665636465316131323934653731356137393337383337\n6138\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleVaultMultiplePasswords/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ greetings }} {{ name }}\"\n      verbosity: 0"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleWithForm/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - form: myForm\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n          - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        msg: ${myForm.msg}\n\nforms:\n  myForm:\n  - msg: { type: \"string\" }\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleWithForm/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ msg }}\"\n      verbosity: 0"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleWithItems/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        msg: \"${item}\"\n    withItems:\n    - \"Hello!\"\n    - \"Hi there!\"\n    - \"Howdy!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleWithItems/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ msg }}\"\n      verbosity: 0"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleWithPostFormSuspension/payload/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n    - call: runAnsible\n    - form: myForm\n    - log : \"${myForm.msg}\"\n\n  runAnsible:\n  - task: ansible\n    in:\n      playbook: playbook.yml\n      inventory:\n        local:\n          hosts:\n          - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n\nforms:\n  myForm:\n  - msg: { label: \"msg\", type: \"string\" }\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleWithPostFormSuspension/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"hello!\"\n      verbosity: 0"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleproject/git/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ greeting }}\"\n      verbosity: 0\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleproject/inventory.ini",
    "content": "[local]\n127.0.0.1\n\n[local:vars]\nansible_connection=local"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleproject/request.json",
    "content": "{\n  \"playbook\": \"playbook/hello.yml\",\n  \"extraVars\": {\n    \"greeting\": \"Hello, world\"\n  },\n  \"config\": {\n    \"defaults\": {\n      \"force_color\": 1\n    }\n  }\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleproject/requestFailure.json",
    "content": "{\n  \"playbook\": \"playbook/hello.yml\",\n  \"inventory\": {\n    \"local\": {\n      \"hosts\": [\"128.0.0.1\"],\n      \"vars\": {\n        \"ansible_connection\": \"local\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ansibleproject/requestInline.json",
    "content": "{\n  \"playbook\": \"playbook/hello.yml\",\n  \"inventory\": {\n    \"local\": {\n      \"hosts\": [\"127.0.0.1\"],\n      \"vars\": {\n        \"ansible_connection\": \"local\"\n      }\n    }\n  },\n  \"extraVars\": {\n    \"greeting\": \"Hello, world\"\n  },\n  \"config\": {\n    \"defaults\": {\n      \"force_color\": 1\n    }\n  }\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/arrayInterpolation/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:example-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - example: ['${varA}', '${varB}']\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/brokenDeps/concord.yml",
    "content": "flows:\n  default:\n  - task: brokenDeps\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/cancelHandling/.concord.yml",
    "content": "flows:\n  main:\n  - ${sleep.ms(30000)}\n\n  onCancel:\n  - log: \"initiator is ${initiator.username}\"\n\nvariables:\n  entryPoint: main"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/cancelSuspendAfterTwoForms/concord.yml",
    "content": "configuration:\n  arguments:\n    z: \"Hello!\"\n\nflows:\n  default:\n    - form: myForm1\n      fields:\n        - x: {type: \"string\"}\n    - form: myForm2\n      fields:\n        - y: {type: \"string\"}\n\n  onCancel:\n    - log: \"${z}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/cancelSuspendHandling/concord.yml",
    "content": "configuration:\n  arguments:\n    bValue: \"still here\"\n\nflows:\n  default:\n  - form: myForm\n\n  onCancel:\n  - call: testFlow\n\n  testFlow:\n  - log: \"${aValue} ${bValue}\"\n\nforms:\n  myForm:\n  - firstName: { type: \"string\" }\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/checkpointExpressions/concord.yml",
    "content": "configuration:\n  runner:\n    events:\n      recordTaskOutVars: true # test uses out vars to verify checkpoints\n\nflows:\n  default:\n    - log: \"before the checkpoint\"\n    - checkpoint: \"test ${x}\"\n    - log: \"after the checkpoint\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/checkpoints/concord.yml",
    "content": "configuration:\n  runner:\n    events:\n      recordTaskOutVars: true # test uses out vars to verify checkpoints\n\nflows:\n  default:\n    - log: \"==Start\"\n    - checkpoint: \"ONE\"\n    - log: \"==Middle\"\n    - checkpoint: \"TWO\"\n    - log: \"==End\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/checkpointsWithArgs/concord.yml",
    "content": "configuration:\n  runner:\n    events:\n      recordTaskOutVars: true # test uses out vars to verify checkpointsconfiguration:\n\n  arguments:\n    varA: \"hello, World!\"\n    varB: \"${crypto.decryptString(encrypted)}\"\n\nflows:\n  default:\n  - set:\n      keyVar: ${varB}\n  - log: ${varA}\n  - log: ${varB}\n  - log: ${keyVar}\n  - checkpoint: \"pointA\"\n  - log: ${varA}\n  - log: ${varB}\n  - log: ${keyVar}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/checkpointsWithError/concord.yml",
    "content": "configuration:\n  runner:\n    events:\n      recordTaskOutVars: true # test uses out vars to verify checkpoints\n\nflows:\n  default:\n  - log: \"==Start\"\n  - checkpoint: \"ONE\"\n  - form: myForm\n  - if: ${myForm.shouldFail}\n    then:\n    - throw: \"As you wish\"\n  - log: \"==Middle\"\n  - checkpoint: \"TWO\"\n  - log: \"==End\"\n\nforms:\n  myForm:\n  - shouldFail: {type: \"boolean\"}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/checkpointsWithEventName/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Starting the process...\"\n  - checkpoint: \"first\"\n  - log: \"Continuing the process...\"\n  - checkpoint: \"second\"\n  - log: \"Done!\"\n\n  - log: \"Event Name: ${resumeEventName}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/checkpointsWithEventNameV2/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n\nflows:\n  default:\n  - log: \"Starting the process...\"\n  - checkpoint: \"first\"\n  - log: \"Continuing the process...\"\n  - checkpoint: \"second\"\n  - log: \"Done!\"\n\n  - log: \"Event Name: ${resumeEvents[0]}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordDirTask/concord.yml",
    "content": "flows:\n  default:\n  - task: concord\n    in:\n      action: start\n      payload: myPayload\n      attachments:\n        - \"${workDir}/someDir/subFile.txt\"\n        - src: rootFile.txt\n          dest: someFile.txt\n      sync: true\n      outVars:\n        - msg\n        - file1\n        - file2\n  - log: \"Done! ${jobOut.msg} ${jobOut.file1} ${jobOut.file2}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordDirTask/myPayload/concord.yml",
    "content": "flows:\n  default:\n  - set:\n      msg: \"Hello!\"\n      file1: ${resource.asString(\"subFile.txt\")}\n      file2: ${resource.asString(\"someFile.txt\")}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordDirTask/rootFile.txt",
    "content": "Bye"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordDirTask/someDir/subFile.txt",
    "content": "Good"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordOutVars/concord.yml",
    "content": "flows:\n  default:\n  - task: concord\n    in:\n      action: start\n      payload: myPayload\n      sync: true\n  - log: \"Done!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordOutVars/myPayload/concord.yml",
    "content": "flows:\n  default:\n  - log: \"hello!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordProjectTask/concord.yml",
    "content": "flows:\n  default:\n  - task: project\n    in:\n      action: create\n      name: ${newProjectName}\n  - log: \"Done!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordStartAtTask/concord.yml",
    "content": "flows:\n  default:\n  - task: concord\n    in:\n      action: start\n      payload: myPayload\n      sync: true\n      startAt: ${startAt}\n      outVars:\n        - msg\n  - log: \"Done! ${jobOut.msg}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordStartAtTask/myPayload/concord.yml",
    "content": "flows:\n  default:\n  - set:\n      msg: \"Hello!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordSubFail/concord.yml",
    "content": "flows:\n  default:\n    - try:\n      - task: concord\n        in:\n          action: start\n          payload: myPayload\n          sync: true\n      error:\n      - log: \"Fail!\"\n      - return\n    - log: \"Done!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordSubFail/myPayload/concord.yml",
    "content": "flows:\n  default:\n  - return: boom!"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordSubIgnoreFail/concord.yml",
    "content": "flows:\n  default:\n    - task: concord\n      in:\n        action: start\n        payload: myPayload\n        sync: true\n        ignoreFailures: true\n    - log: \"Done!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordSubIgnoreFail/myPayload/concord.yml",
    "content": "flows:\n  default:\n  - return: boom!"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordSubWithNullArg/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n\nflows:\n  default:\n    - task: concord\n      in:\n        action: start\n        payload: myPayload\n        sync: true\n        suspend: true\n        arguments:\n          nullValue: ${null}\n    - log: \"Done!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordSubWithNullArg/myPayload/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n\nflows:\n  default:\n    - log: \"Child process, nullValue: '${nullValue}'\"\n    - log: \"Child process, nullValue == null: '${nullValue == null}'\"\n    - log: \"Child process, hasVariable('nullValue'): ${hasVariable('nullValue')}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordTask/concord.yml",
    "content": "flows:\n  default:\n  - task: concord\n    in:\n      action: start\n      project: ${projectInfo.projectName}\n      archive: test.zip\n      sync: true\n  - log: \"Done!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordTaskApiKey/concord.yml",
    "content": "flows:\n  default:\n    - task: concord\n      in:\n        apiKey: \"${myApiKey}\"\n        action: startExternal\n        payload: \"payload\"\n        sync: true\n        arguments:\n          name: \"Concord\"\n        outVars:\n          - xyz\n\n    - log: \"${jobOut.xyz}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordTaskApiKey/payload/concord.yml",
    "content": "flows:\n  default:\n    - set:\n        xyz: \"Hello, ${name}!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordTaskFailChild/concord.yml",
    "content": "flows:\n  default:\n  - task: concord\n    in:\n      action: start\n      project: ${projectInfo.projectName}\n      archive: test.zip\n      sync: true\n  - log: \"Done!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordTaskFork/concord.yml",
    "content": "configuration:\n  arguments:\n    myName: \"Concord\"\n\nflows:\n  default:\n    - log: \"repoCommitMessage: ${projectInfo.repoCommitMessage}\"\n\n    # \"forks\" the current process as a child process\n    - task: concord\n      in:\n        action: fork\n\n        # if not specified, the parent's entry point will be used\n        entryPoint: sayHello\n\n        # additional arguments\n        arguments:\n          otherName: \"${initiator.username}\"\n\n      # out variable \"myJobs\" will contain a list of process IDs\n      out:\n        myJobs: ${jobs}\n\n    - log: \"Done! Status of the jobs: ${concord.waitForCompletion(myJobs)}\"\n\n  sayHello:\n    # forked processes can access the latest snapshot of the parent's\n    # state in addition to the arguments provided by the parent task\n    - log: \"FORK: Hello, ${otherName}. I'm ${myName}\"\n\n    - log: \"repoCommitMessage: ${projectInfo.repoCommitMessage}\"\n\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordTaskForkAsyncGrabOutVars/concord.yml",
    "content": "flows:\n  default:\n    # empty list to store fork IDs\n    - set:\n        children: []\n\n    # start the first fork\n    - task: concord\n      in:\n        action: fork\n        entryPoint: forkA\n        sync: false\n        outVars:\n          - x\n          - y\n          - z\n\n    # save the first fork's ID\n    - ${children.addAll(jobs)}\n\n    # start the second fork\n    - task: concord\n      in:\n        action: fork\n        entryPoint: forkB\n        sync: false\n        outVars:\n          - a\n          - b\n          - c\n\n    # save the second fork's ID\n    - ${children.addAll(jobs)}\n\n    # grab out vars of the forks\n    - expr: ${concord.getOutVars(children)}\n      out: forkOutVars\n\n    # print out out vars grouped by fork\n    - log: \"${forkOutVars}\"\n\n  forkA:\n    - set:\n        x: 1\n        y: 2\n        z: 3\n\n  forkB:\n    - set:\n        a: 4\n        b: 5\n        c: 6"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordTaskForkSuspend/concord.yml",
    "content": "flows:\n  default:\n    - task: concord\n      in:\n        action: fork\n        forks:\n          - entryPoint: onFork\n            arguments:\n              msg: \"Hello\"\n          - entryPoint: onFork\n            arguments:\n              msg: \"Bye\"\n        sync: true\n        suspend: true\n        outVars:\n          - varFromFork\n\n    - log: \"${jobOut}\"\n\n  onFork:\n    - log: \"Running in a fork\"\n    - set:\n        varFromFork: \"${msg}, ${name}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordTaskForkWithArguments/concord.yml",
    "content": "configuration:\n  arguments:\n    x: 123\n\nflows:\n  default:\n    - set:\n        x: 234\n    - task: concord\n      in:\n        action: fork\n        entryPoint: sayHello\n        sync: true\n\n  sayHello:\n    - log: \"Hello from a subprocess!\"\n    - log: \"Concord Fork Process ${x}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordTaskForkWithForm/concord.yml",
    "content": "configuration:\n  arguments:\n    x: 123\n\nflows:\n  default:\n    - set:\n        x: 234\n\n    - form: myForm\n\n    - task: concord\n      in:\n        action: fork\n        entryPoint: sayHello\n        sync: true\n\n  sayHello:\n    - log: \"Hello from a subprocess!\"\n    - log: \"Concord Fork Process ${x}\"\n\nforms:\n  myForm:\n  - firstName: { label: \"First name\", type: \"string\"}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordTaskForkWithItems/concord.yml",
    "content": "configuration:\n  arguments:\n    forkItems:\n    - entryPoint: sayHello\n      arguments:\n        color: \"RED\"\n    - entryPoint: sayHello\n      arguments:\n        color: \"WHITE\"\n\nflows:\n  default:\n  - task: concord\n    in:\n      action: fork\n      debug: true # to see the arguments in the log\n      entryPoint: ${item.entryPoint}\n      arguments: ${item.arguments}\n    withItems: ${forkItems}\n\n  - log: \"Done! ${jobs} is completed\"\n\n  sayHello:\n  - log: \"FORK: Hello, ${color}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordTaskForkWithItemsWithOut/concord.yml",
    "content": "configuration:\n  arguments:\n    forkItems:\n    - entryPoint: sayHello\n      arguments:\n        color: \"RED\"\n    - entryPoint: sayHello\n      arguments:\n        color: \"WHITE\"\n\nflows:\n  default:\n  - task: concord\n    in:\n      action: fork\n      debug: true # to see the arguments in the log\n      entryPoint: ${item.entryPoint}\n      arguments: ${item.arguments}\n    out:\n      myJobs: ${jobs}\n    withItems: ${forkItems}\n\n  - log: \"Done! ${myJobs} is completed\"\n\n  sayHello:\n  - log: \"FORK: Hello, ${color}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordTaskForkWithRequirements/concord.yml",
    "content": "configuration:\n  requirements:\n    agent:\n      type: \"test\"\n\nflows:\n  default:\n    - task: concord\n      in:\n        action: fork\n        entryPoint: sayHello\n        sync: true\n\n  sayHello:\n    - log: \"Hello from a subprocess!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordTaskSuspendParentProcess/concord.yml",
    "content": "flows:\n  default:\n    - try:\n      - call: startChild\n        in:\n          enterHere: default\n      error:\n        - call: startChild\n          in:\n            enterHere: setHello\n    - log: \"${jobOut.xyz}\"\n\n  startChild:\n    - task: concord\n      in:\n        apiKey: \"${myApiKey}\"\n        action: start\n        payload: payload\n        sync: true\n        suspend: true\n        entryPoint: ${enterHere}\n        arguments:\n          name: \"Concord\"\n        outVars:\n          - xyz\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/concordTaskSuspendParentProcess/payload/concord.yml",
    "content": "flows:\n  default:\n    - throw: \"default flow throws an error\"\n\n  setHello:\n    - set:\n        xyz: \"Hello, ${name}!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/configurableFlowsDirectory/concord.yml",
    "content": "flows:\n  default:\n  - call: externalFlow\n\nresources:\n  flows: myFlows"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/configurableFlowsDirectory/myFlows/external.yml",
    "content": "externalFlow:\n- log: \"External flow!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/configurableProfilesDirectory/.concord.yml",
    "content": "configuration:\n  entryPoint: main\n  arguments:\n    name: \"stranger\"\n\nflows:\n  main:\n  - log: Hello, ${name}\n\n\nresources:\n  profiles: myProfiles\n\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/configurableProfilesDirectory/_main.json",
    "content": "{\n  \"activeProfiles\": [\"test\"]\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/configurableProfilesDirectory/myProfiles/test.yml",
    "content": "test:\n  configuration:\n    arguments:\n      name: \"world\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/cronProfiles/concord.yml",
    "content": "flows:\n  onCron:\n    - log: \"Hello, ${msg}!\"\n\nprofiles:\n  aaa:\n    configuration:\n      arguments:\n        msg: \"AAA\"\n      tags:\n        - \"AAA\"\n  bbb:\n    configuration:\n      arguments:\n        msg: \"BBB\"\n      tags:\n        - \"BBB\"\n\ntriggers:\n- cron:\n    spec: \"* * * * *\"\n    entryPoint: onCron\n    activeProfiles:\n      - \"aaa\"\n- cron:\n    spec: \"* * * * *\"\n    entryPoint: onCron\n    activeProfiles:\n      - \"bbb\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/cronRunAs/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n\nflows:\n  onCron:\n    - log: \"Hello, ${initiator}!\"\n\ntriggers:\n- cron:\n    spec: \"* * * * *\"\n    entryPoint: onCron\n    runAs:\n      withSecret: \"test-run-as\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/crypto/_main.json",
    "content": "{\n  \"entryPoint\": \"main\"\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/crypto/flows/test.yml",
    "content": "main:\n- log: \"${crypto.decryptString(mySecret)}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/cryptoFile/concord.yml",
    "content": "flows:\n  default:\n  - set:\n      path: ${crypto.exportAsFile(secretName, pwd)}\n  - log: \"Path ${path}\"\n  - log: \"We got ${resource.asString(path)}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/cryptoFileWithOrg/concord.yml",
    "content": "flows:\n  default:\n  - set:\n      path: ${crypto.exportAsFile(secretOrgName, secretName, pwd)}\n  - log: \"Path ${path}\"\n  - log: \"We got ${resource.asString(path)}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/cryptoPlain/.concord.yml",
    "content": "flows:\n  main:\n  - log: \"value=${crypto.exportAsString(secretName, pwd)}\"\n\nvariables:\n  entryPoint: main\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/cryptoPwd/.concord.yml",
    "content": "flows:\n  main:\n  - set:\n      value: ${crypto.exportCredentials(secretName, pwd)}\n  - log: \"${value.username} ${value.password}\"\n\nvariables:\n  entryPoint: main\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/cryptoWithoutPassword/concord.yml",
    "content": "flows:\n  default:\n  - log: \"We got ${crypto.exportAsString(secretOrgName, secretName, null)}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/currentOrgCrypto/concord.yml",
    "content": "flows:\n  default:\n    - log: \"Before form: ${crypto.exportAsString(mySecretName, null)}\"\n    - form: myForm\n      fields:\n        - x: { type: \"string\" }\n    - log: \"After form: ${crypto.exportAsString(mySecretName, null)}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/decryptString/concord.yml",
    "content": "flows:\n  default:\n  - log: \"${encryptedValue}\"\n  - log: \"We got ${crypto.decryptString(encryptedValue)}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/decryptStringTooBig/concord.yml",
    "content": "flows:\n  default:\n  - script: javascript\n    body: |\n      var t = \"\";\n      for (var i = 0; i < 256 * 1024; i++) {\n        t += \"z\";\n      }\n      execution.setVariable(\"encryptedValue\", t);\n\n  - log: \"We got ${crypto.decryptString(encryptedValue)}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/defaultEntryPoint/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Hello, Concord!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/defaultVars/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Default vars: ${testDefaultVars.var1}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/delegateOut/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:example-tasks:PROJECT_VERSION\n  entryPoint: main\n\nflows:\n  main:\n  - task: exampleDelegate\n    out:\n      msg: ${exampleOutput}\n  - log: \"I said: ${msg}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/dependencyManager/concord.yml",
    "content": "flows:\n  default:\n  - task: dependencyManagerTest\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/deps/processes/test.yml",
    "content": "main:\n- expr: ${example.hello()}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/disableProfilesDirectory/_main.json",
    "content": "{\n  \"activeProfiles\": [\"test\"]\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/disableProfilesDirectory/concord.yml",
    "content": "configuration:\n  entryPoint: main\n  arguments:\n    name: \"stranger\"\n\nflows:\n  main:\n  - log: Hello, ${name}\n\n\nresources:\n  disabled:\n    - profiles\n\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/disableProfilesDirectory/profiles/broken.yml",
    "content": "test:\n  configuration:\n    entryPoint: main\n    arguments:\n      x: {\n      name: \"stranger\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/docker/concord.yml",
    "content": "flows:\n  main:\n  - docker: ${image}\n    debug: true\n    forcePull: false\n    cmd: echo \"Hello, ${name}\"\n\nvariables:\n  entryPoint: main\n  arguments:\n    name: \"world\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/dockerAnsible/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      dockerImage: \"${image}\"\n      forcePull: false\n      playbook: \"playbook/hello.yml\"\n      debug: true\n      verbose: 3\n      inventory:\n        local:\n          hosts:\n          - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        msg: \"Hello from Docker!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/dockerAnsible/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ msg }}\"\n      verbosity: 0"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/dockerLogWithStdErr/concord.yml",
    "content": "flows:\n  main:\n  - docker: ${image}\n    debug: true\n    forcePull: false\n    cmd: echo \"Hello, ${name}\" && (>&2 echo \"STDERR WORKS\")\n    stderr: myErr\n\n  - log: \"STDERR: ${myErr}\"\n\nvariables:\n  entryPoint: main\n  arguments:\n    name: \"world\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/dockerLogWithoutStdOut/concord.yml",
    "content": "flows:\n  main:\n  - task: docker\n    in:\n      image: ${image}\n      debug: true\n      forcePull: false\n      cmd: echo \"Hello, ${name}\" && (>&2 echo \"STDERR WORKS\")\n\nvariables:\n  entryPoint: main\n  arguments:\n    name: \"world\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/dockerNoLogWithStdOut/concord.yml",
    "content": "flows:\n  main:\n  - task: docker\n    in:\n      image: ${image}\n      debug: true\n      forcePull: false\n      stdout: myStdout\n      cmd: echo \"Hello, ${name}\" && (>&2 echo \"STDERR WORKS\")\n\nvariables:\n  entryPoint: main\n  arguments:\n    name: \"world\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/dockerOut/concord.yml",
    "content": "flows:\n  main:\n  - docker: ${image}\n    debug: true\n    forcePull: false\n    stdout: myStdout\n    cmd: echo \"Hello, ${name}\" && (>&2 echo \"STDERR STILL WORKS\")\n  - log: \"!! ${myStdout.trim()} !!\"\n\nvariables:\n  entryPoint: main\n  arguments:\n    name: \"world\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/dockerPullRetry/concord.yml",
    "content": "flows:\n  default:\n    - task: docker\n      in:\n        image: \"localhost:12345/non-existing\"\n        pullRetryInterval: 1000\n        pullRetryCount: 2\n        cmd: |\n          echo 'Hi!'\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/dockerTaskSyntaxOut/concord.yml",
    "content": "flows:\n  main:\n  - task: docker\n    in:\n      image: ${image}\n      debug: true\n      forcePull: false\n      stdout: myStdout\n      cmd: echo \"Hello, ${name}\" && (>&2 echo \"STDERR STILL WORKS\")\n  - log: \"!! ${myStdout.trim()} !!\"\n\nvariables:\n  entryPoint: main\n  arguments:\n    name: \"world\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/dynamicFormFields/concord.yml",
    "content": "configuration:\n  arguments:\n    myFields:\n    - x: {type: \"string\"}\n\nflows:\n  default:\n  - form: myForm\n    fields: \"${myFields}\"\n  - log: \"We got: ${myForm.x}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/dynamicFormWithGroovy/concord.yml",
    "content": "configuration:\n  dependencies:\n    - \"mvn://org.apache.groovy:groovy-all:pom:5.0.4\"\n\nflows:\n  default:\n    - script: groovy\n      body: |\n        def myForm = [\n          \"fields\": [\n            [\"firstName\": [\"type\": \"string\"]],\n            [\"lastName\": [\"type\": \"string\"]]\n          ]\n        ]\n\n        execution.form('myForm', myForm);\n\n    - log: \"${myForm}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/dynamicTask/concord.yml",
    "content": "configuration:\n  dependencies:\n    - \"mvn://org.apache.groovy:groovy-all:pom:5.0.4\"\n    - \"mvn://com.walmartlabs.concord.plugins.basic:dynamic-tasks:PROJECT_VERSION\"\n\nflows:\n  default:\n    - loadTasks: \"tasks\"\n    - ${myTask.hey(\"world\")}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/dynamicTask/tasks/test.groovy",
    "content": "import com.walmartlabs.concord.sdk.Task\n\nimport javax.inject.Named\n\n@Named(\"myTask\")\nclass MyTask implements Task {\n\n    void hey(String name) {\n        println \"Hey, ${name}\"\n    }\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/effectiveYaml/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n\nflows:\n  default:\n    - log: \"default\"\n\n  test:\n    - log: \"test\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/encryptString/.concord.yml",
    "content": "flows:\n  default:\n    - log: \"${decryptedValue}\"\n    - log: \"We got ${crypto.encryptString(projectInfo.orgName, projectInfo.projectName, decryptedValue)}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/escapeCommitMessage/.concord.yml",
    "content": "flows:\n  default:\n    - log: \"Hello, ${name}\"\n\nvariables:\n  arguments:\n    name: \"Vasia\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/example/_main.json",
    "content": "{\n  \"entryPoint\": \"main\",\n  \"arguments\": {\n    \"myName\": \"world\",\n    \"myBool\": true\n  }\n}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/example/processes/test.yml",
    "content": "main:\n- expr: ${log.info(\"test\", \"Hello, \".concat(myName))}\n- expr: ${log.info(\"test\", resource.asString(workDir.concat(\"/something.txt\")))}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/example/something.txt",
    "content": "Hello, local files!"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/exclusiveCancelOld/concord.yml",
    "content": "flows:\n  default:\n    - log: \"sleep for ${time} ms...\"\n    - ${sleep.ms(time)}\n\nconfiguration:\n  exclusive:\n    group: \"RED\"\n    mode: \"cancelOld\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/externalImport/concord.yml",
    "content": "flows:\n  flowFromTemplate:\n    - log: \"Hello from Template, ${name}!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/externalImportFailHandler/concord.yml",
    "content": "flows:\n  onFailure:\n    - log: \"oh, handled\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/externalImportMain/concord.yml",
    "content": "imports:\n  - git:\n      url: \"{{gitUrl}}\"\n\nflows:\n  default:\n    - log: \"Hello, ${name}!\"\n    - call: flowFromTemplate\n\nconfiguration:\n  arguments:\n    name: \"Concord\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/externalImportMainFailed/concord.yml",
    "content": "imports:\n  - git:\n      url: \"{{gitUrl}}\"\n\nflows:\n  default:\n    - ${misc.throwBpmnError(\"boom!\")}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/externalImportMainStateTest/concord.yml",
    "content": "imports:\n  - git:\n      url: \"{{gitUrl}}\"\n      version: \"master\"\n      path: \"dir\"\n      dest: \"./import_data\"\n\nflows:\n  default:\n    - log: \"Hello, ${name}!\"\n    - call: flowFromTemplate\n\nconfiguration:\n  arguments:\n    name: \"Concord\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/externalImportMainWithDeps/concord.yml",
    "content": "imports:\n  - git:\n      url: \"{{gitUrl}}\"\n\nconfiguration:\n  dependencies:\n    - \"mvn://org.python:jython-standalone:2.7.4\"\n\nflows:\n  default:\n    - call: flowFromImport\n    - script: python\n      body: |\n        print \"Hello from Python!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/externalImportMainWithExclude/concord.yml",
    "content": "imports:\n  - git:\n      url: \"{{gitUrl}}\"\n      exclude:\n        - \"{{exclude}}\"\n\nflows:\n  default:\n    - log: \"Hello, ${name}!\"\n    - call: flowFromTemplate\n\nconfiguration:\n  arguments:\n    name: \"Concord\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/externalImportMainWithFlow/concord/concord.yml",
    "content": "flows:\n  flowFromTemplate:\n    - log: \"Hello from Local, ${name}!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/externalImportMainWithFlow/concord.yml",
    "content": "imports:\n  - git:\n      url: \"{{gitUrl}}\"\n#    - name: \"git-org/concord-template\"\n#      version: \"master\"\n#      path: \"dir\"\n#      dest: \"concord\"\n\nflows:\n  default:\n    - log: \"Hello, ${name}!\"\n    - call: flowFromTemplate\n\nconfiguration:\n  arguments:\n    name: \"Concord\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/externalImportMainWithForks/concord.yml",
    "content": "imports:\n  - git:\n      url: \"{{gitUrl}}\"\n\nflows:\n  default:\n  - log: \"Hello, ${name}!\"\n  - task: concord\n    in:\n      action: fork\n      entryPoint: forkWithImport\n      arguments:\n          name: \"imports\"\n\nconfiguration:\n  arguments:\n    name: \"Concord\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/externalImportMainWithPath/concord.yml",
    "content": "imports:\n  - git:\n      url: \"{{gitUrl}}\"\n      #    - name: \"git-org/concord-template\"\n      version: \"master\"\n      path: \"dir\"\n      dest: \"concord\"\n\nflows:\n  default:\n    - log: \"Hello, ${name}!\"\n    - call: flowFromTemplate\n\nconfiguration:\n  arguments:\n    name: \"Concord\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/externalImportMainWithVersion/concord.yml",
    "content": "imports:\n  - git:\n      url: \"{{gitUrl}}\"\n      version: \"{{version}}\"\n      dest: \"concord\"\n\nflows:\n  default:\n    - log: \"Hello, ${name}!\"\n    - call: flowFromTemplate\n\nconfiguration:\n  arguments:\n    name: \"Concord\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/externalImportSymlink/concord.txt",
    "content": "imports:\n  - git:\n      url: \"{{gitUrl}}\"\n\ntriggers:\n  - test:\n      entryPoint: \"flowFromTemplate\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/externalImportTriggerReference/concord.yml",
    "content": "imports:\n  - git:\n      url: \"{{gitUrl}}\"\n\ntriggers:\n  - test:\n      entryPoint: \"flowFromTemplate\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/externalImportWithConfiguration/concord.yml",
    "content": "flows:\n  flowFromTemplate:\n    - log: \"Hello from Template, ${nameInExternalImport}!\"\n\nconfiguration:\n  arguments:\n    nameInExternalImport: \"Concord\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/externalImportWithDeps/concord.yml",
    "content": "configuration:\n  dependencies:\n    - \"mvn://org.apache.groovy:groovy-all:pom:5.0.4\"\n\nflows:\n  flowFromImport:\n    - script: groovy\n      body: |\n        println \"Hello from Groovy!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/externalImportWithDir/concord.yml",
    "content": "flows:\n  flowFromTemplate:\n    - log: \"Hello from Template, ${name}!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/externalImportWithDir/dir/concord.yml",
    "content": "flows:\n  flowFromTemplate:\n    - log: \"Hello from Template DIR, ${name}!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/externalImportWithForks/concord.yml",
    "content": "flows:\n  forkWithImport:\n    - log: \"Hello from Concord, ${name}!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/externalImportWithForm/concord.yml",
    "content": "flows:\n  flowFromTemplate:\n    - log: \"Hello from Template, ${name}!\"\n    - form: myForm\n    - log: \"Template form submitted: ${myForm.name}\"\n\nforms:\n  myForm:\n    - name: {type: \"string\"}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/externalWithItems/concord.yml",
    "content": "flows:\n  default:\n  - task: log\n    in:\n      msg: ${item}\n    withItems: ${myItems}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/extraDeps/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.15.0\nprofiles:\n  foo:\n    configuration:\n      dependencies:\n        - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:2.16.0\n  bar:\n    configuration:\n      extraDependencies:\n        - mvn://com.walmartlabs.concord.plugins:confluence-task:2.5.0\nflows:\n  default:\n    - log: \"Hi!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/failureHandling/.concord.yml",
    "content": "flows:\n  main:\n  - form: myForm1\n  - ${misc.throwBpmnError(\"boom!\")}\n\n  onFailure:\n  - log: \"lastError: ${lastError}\"\n  - log: \"txId: ${txId}\"\n  - log: \"projectInfo: ${projectInfo}\"\n  - log: \"processInfo: ${processInfo}\"\n  - log: \"initiator: ${initiator}\"\n\nforms:\n  myForm1:\n  - firstName: { label: \"First Name\", type: \"string\" }\n\nvariables:\n  entryPoint: main"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/failureHandlingError/.concord.yml",
    "content": "flows:\n  main:\n  - ${misc.throwBpmnError(\"boom!\")}\n\n  onFailure:\n  - ${misc.throwBpmnError(\"kapow!\")}\n\nvariables:\n  entryPoint: main"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/filePerm/concord.yml",
    "content": "flows:\n  default:\n  - script: groovy\n    body: |\n      import java.nio.file.*\n      import java.nio.file.attribute.*\n\n      def p = Paths.get(\"test.sh\")\n      def ps = Files.getPosixFilePermissions(p)\n\n      println ps\n\n      if (!ps.contains(PosixFilePermission.OWNER_EXECUTE)) {\n        throw new RuntimeException(\"whoops\")\n      }\n\n  - log: \"Hello!\"\n\nconfiguration:\n  dependencies:\n  - \"mvn://org.apache.groovy:groovy-all:pom:5.0.4\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/filePerm/test.sh",
    "content": "#!/bin/bash\n\necho \"Hello!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/forkDepth/concord.yml",
    "content": "flows:\n  default:\n  - task: concord\n    in:\n      action: fork\n      entryPoint: fork1\n      sync: true\n\n  - log: \"Done! ${jobs} is completed\"\n\n  fork1:\n  - log: \"FORK1\"\n  - task: concord\n    in:\n      action: fork\n      sync: true\n      entryPoint: fork2\n\n  fork2:\n  - log: \"FORK2\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/forkInitiator/concord.yml",
    "content": "flows:\n  default:\n  - log: \"initiator: ${initiator}\"\n  - task: concord\n    in:\n      action: fork\n      apiKey: \"{{apiKey}}\"\n      entryPoint: fork1\n      sync: true\n\n  - log: \"Done! ${jobs} is completed\"\n\n  fork1:\n  - log: \"initiator: ${initiator}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/forkOnFailure/concord.yml",
    "content": "flows:\n  default:\n    - task: concord\n      in:\n        action: fork\n        entryPoint: aFork\n        arguments:\n          msg: \"aFork!\"\n        sync: true\n        ignoreFailures: true\n\n    - task: concord\n      in:\n        action: fork\n        entryPoint: aFork\n        disableOnFailure: true\n        arguments:\n          msg: \"aForkNoOnFailure!\"\n        sync: true\n        ignoreFailures: true\n\n\n  aFork:\n    - throw: \"${msg}!\"\n\n  onFailure:\n    - log: \"Got ${lastError}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/form/_main.json",
    "content": "{\n  \"entryPoint\": \"main\",\n  \"arguments\": {\n    \"myForm1\": {\n      \"x\": 100123\n    }\n  }\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/form/processes/test.yml",
    "content": "main:\n  - log: \"before form\"\n  - form: myForm1\n  - ${log.info(\"test\", myForm1.x + 100)}\n\n  - form: myForm2\n  - ${log.info(\"test\", myForm1.firstName.concat(' ').concat(myForm2.lastName))}\n  - ${log.info(\"test\", myForm2.age + 100)}\n\n  - ${log.info(\"test\", myForm1.x + 200)}\n\n  - ${log.info(\"test\", myForm2.color)}\n\n  - log: \"FILE_PATH ${myForm2.file}\"\n\n  - log: \"FILE ${resource.asString(myForm2.file)}\"\n\n  - log: \"AAA ${myForm2.rememberMe}\"\n\nform (myForm1):\n  - firstName: { label: \"First Name\", type: \"string\" }\n\nform (myForm2):\n  - lastName: { label: \"Last Name\", type: \"string\" }\n  - age: { label: \"Age\", type: \"int?\", min: 21, max: 120 }\n  - color: { label: \"Favorite color\", type: \"string\", value: \"r3d\" }\n  - rememberMe: { label: \"Remember me\", type: \"boolean\" }\n  - file: { label: \"file\", type: \"file\" }\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/formCallWithExpression/.concord.yml",
    "content": "flows:\n  default:\n  - set:\n      formNameVar: \"myForm\"\n  - form: ${formNameVar}\n  - log: \"Hello, ${myForm.name}\"\n\nforms:\n  myForm:\n  - name: {type: \"string\"}\n\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/formExternal/concord.yml",
    "content": "flows:\n  default:\n  - call: myFlow\n  - log: \"We got ${myForm.myField}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/formExternal/flows/external.yml",
    "content": "myFlow:\n- form: myForm\n\nform(myForm):\n- myField: {type: \"string\"}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/formLabelExpression/concord.yml",
    "content": "flows:\n  default:\n    - form: myForm\n      yield: true\n\nforms:\n  myForm:\n    - myField: { type: \"string\", label: \"${x}\" }\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/formMultiValue/.concord.yml",
    "content": "flows:\n  default:\n  - form: myForm\n  - log: \"Skills -> ${myForm.skills}\"\n\nforms:\n  myForm:\n  - skills: { type: \"string*\", allow: [\"css\", \"react\", \"angular\"] }\n\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/formOptionalFileTypeField/.concord.yml",
    "content": "flows:\n  default:\n  - call: formFlow\n\n  formFlow:\n  - form: myForm\n  - log: ${myForm}\n\nforms:\n  myForm:\n  - myFile: {label: \"upload file\", type: \"file?\"}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/formReadonlyField/.concord.yml",
    "content": "flows:\n  default:\n  - call: formFlow\n\n  formFlow:\n  - form: myForm\n  - log: ${myForm.myValue}\n\nforms:\n  myForm:\n  - myValue: {type: \"string\", value: \"default value\", readonly: true }"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/formSingleAllowedValue/concord.yml",
    "content": "flows:\n  default:\n  - set:\n      allowExpression:\n        - one\n\n  - form: myForm\n\n  - log: \"field1: ${myForm.field1}\"\n\nforms:\n  myForm:\n  - field1: { label: \"Expression\", type: \"string\", allow: \"${allowExpression}\", search: true }\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/formValues/.concord.yml",
    "content": "flows:\n  main:\n  - sub\n\n  sub:\n  - set:\n      message: \"Hello\"\n  - form: myForm\n  - log: \"${message}, ${myForm.name}\"\n\nforms:\n  myForm:\n  - name: {type: \"string\"}\n\nvariables:\n  entryPoint: main\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/formValuesSubmit/concord.yml",
    "content": "flows:\n  default:\n  - form: myForm\n    values:\n      a: 123\n      b: \"hello\"\n  - log: \"we got ${myForm.a} ${myForm.b} ${myForm.c}\"\n\nforms:\n  myForm:\n  - c: {type: \"string\", value: \"234\"}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/formsWithItems/concord.yml",
    "content": "flows:\n  default:\n  - call: formFlow\n    withItems:\n    - \"Hello!\"\n    - \"Hi there!\"\n\n  formFlow:\n  - form: myForm\n  - log: ${myForm.myValue}\n\nforms:\n  myForm:\n  - myValue: {type: \"string\", value: \"${item}\"}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/generalExclusiveTrigger/concord.yml",
    "content": "flows:\n  onTestTrigger:\n    - log: \"Hello from exclusive trigger!\"\n\ntriggers:\n  - testTrigger:\n      entryPoint: onTestTrigger\n      key1: \"value1\"\n      exclusive:\n        group: \"RED\"\n        mode: \"cancel\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/generalExclusiveTriggerv2/concord.yml",
    "content": "flows:\n  onTestTrigger:\n    - log: \"Hello from exclusive trigger!\"\n\n  onTestTriggerv2:\n    - log: \"Hello from exclusive trigger v2!\"\n\n\ntriggers:\n  - testTrigger:\n      entryPoint: onTestTrigger\n      key1: \"value1\"\n      exclusive:\n        group: \"RED\"\n        mode: \"cancel\"\n\n  - testTriggerv2:\n      entryPoint: onTestTriggerv2\n      version: 2\n      conditions:\n        key1: \"value2\"\n      exclusive:\n        group: \"RED\"\n        mode: \"cancel\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/generalTriggerWithExclusiveCfg/concord.yml",
    "content": "configuration:\n  exclusive:\n    group: \"RED\"\n    mode: \"cancel\"\n\nflows:\n  onTestTrigger:\n    - log: \"Hello from exclusive trigger!\"\n    - \"${sleep.ms(10000)}\"\n\ntriggers:\n  - testTrigger:\n      entryPoint: onTestTrigger\n      key1: \"value1\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/generalTriggerWithExclusiveCfgv2/concord.yml",
    "content": "configuration:\n  exclusive:\n    group: \"RED\"\n    mode: \"cancel\"\n\nflows:\n  onTestTrigger:\n    - log: \"Hello from exclusive trigger!\"\n\n  onTestTriggerv2:\n    - log: \"Hello from exclusive trigger v2!\"\n\ntriggers:\n  - testTrigger:\n      entryPoint: onTestTrigger\n      key1: \"value1\"\n\n  - testTriggerv2:\n      entryPoint: onTestTriggerv2\n      version: 2\n      conditions:\n        key1: \"value2\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/generalTriggerWithExclusiveOverride/concord.yml",
    "content": "configuration:\n  exclusive:\n    group: \"RED\"\n    mode: \"cancel\"\n\nflows:\n  onTestTrigger:\n    - log: \"Hello from exclusive trigger!\"\n\ntriggers:\n  - testTrigger:\n      entryPoint: onTestTrigger\n      key1: \"value1\"\n      exclusive:\n        group: \"TRIGGER\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/generalTriggerWithExclusiveOverridev2/concord.yml",
    "content": "configuration:\n  exclusive:\n    group: \"RED\"\n    mode: \"cancel\"\n\nflows:\n  onTestTrigger:\n    - log: \"Hello from exclusive trigger!\"\n\n  onTestTriggerv2:\n    - log: \"Hello from exclusive trigger v2!\"\n\n\ntriggers:\n  - testTrigger:\n      entryPoint: onTestTrigger\n      key1: \"value1\"\n      exclusive:\n        group: \"TRIGGER\"\n\n  - testTriggerv2:\n      entryPoint: onTestTriggerv2\n      version: 2\n      conditions:\n        key1: \"value2\"\n      exclusive:\n        group: \"TRIGGER\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/getVar/concord.yml",
    "content": "flows:\n  default:\n  - set:\n      cfg:\n        param1: 1\n\n  - log: \"param1: ${vars.get('cfg.param1', 100)}\"\n  - log: \"defaultValue: ${vars.get('cfg.param2', 101)}\"\n  - log: \"defaultValueFromUnknown: ${vars.get('xxx.yyy', 102)}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/gitBranches/dev/concord.yml",
    "content": "flows:\n  default:\n    - log: \"running dev\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/gitBranches/qa/concord.yml",
    "content": "flows:\n  default:\n    - log: \"running qa\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/gitRepository/concord.yml",
    "content": "flows:\n  default:\n    - log: \"Hello Concord!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/githubNonRepoEvent/concord.yml",
    "content": "flows:\n  onEvent:\n  - log: \"EVENT: ${event}\"\n\ntriggers:\n- github:\n    version: 2\n    entryPoint: onEvent\n    conditions:\n      type: \"team\"\n      githubOrg: \"it-githubNonRepoEvent\"\n      githubRepo: \".*\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/githubNonRepoEvent/event.json",
    "content": "{\n  \"action\": \"added_to_repository\",\n  \"team\": {\n    \"name\": \"github\"\n  },\n  \"repository\": {\n    \"full_name\": \"it-githubNonRepoEvent/Hello-World\"\n  },\n  \"organization\": {\n    \"login\": \"it-githubNonRepoEvent\"\n  },\n  \"sender\": {\n    \"login\": \"Codertocat\"\n  }\n}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/githubTests/events/direct_branch_push.json",
    "content": "{\n  \"ref\": \"_REF\",\n  \"before\": \"1083c12463e8122e1ff7adf06ec8aabbe998fec4\",\n  \"after\": \"a68f9e81f70b5ac645739f244cdc95a0da24310e\",\n  \"created\": false,\n  \"deleted\": false,\n  \"forced\": false,\n  \"base_ref\": null,\n  \"compare\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/compare/1083c12463e8...a68f9e81f70b\",\n  \"commits\": [\n    {\n      \"id\": \"a68f9e81f70b5ac645739f244cdc95a0da24310e\",\n      \"tree_id\": \"5e0785caefd8d107f75d18a26907f68c7168a8cc\",\n      \"distinct\": true,\n      \"message\": \"bump\",\n      \"timestamp\": \"2019-03-12T12:59:05-04:00\",\n      \"url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/commit/a68f9e81f70b5ac645739f244cdc95a0da24310e\",\n      \"author\": {\n        \"name\": \"John Smith\",\n        \"email\": \"_USER_NAME@example.com\",\n        \"username\": \"_USER_NAME\"\n      },\n      \"committer\": {\n        \"name\": \"John Smith\",\n        \"email\": \"_USER_NAME@example.com\",\n        \"username\": \"_USER_NAME\"\n      },\n      \"added\": [],\n      \"removed\": [],\n      \"modified\": [\n        \"concord.yml\"\n      ]\n    }\n  ],\n  \"head_commit\": {\n    \"id\": \"a68f9e81f70b5ac645739f244cdc95a0da24310e\",\n    \"tree_id\": \"5e0785caefd8d107f75d18a26907f68c7168a8cc\",\n    \"distinct\": true,\n    \"message\": \"bump\",\n    \"timestamp\": \"2019-03-12T12:59:05-04:00\",\n    \"url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/commit/a68f9e81f70b5ac645739f244cdc95a0da24310e\",\n    \"author\": {\n      \"name\": \"John Smith\",\n      \"email\": \"_USER_NAME@example.com\",\n      \"username\": \"_USER_NAME\"\n    },\n    \"committer\": {\n      \"name\": \"John Smith\",\n      \"email\": \"_USER_NAME@example.com\",\n      \"username\": \"_USER_NAME\"\n    },\n    \"added\": [],\n    \"removed\": [],\n    \"modified\": [\n      \"concord.yml\"\n    ]\n  },\n  \"repository\": {\n    \"id\": 80654,\n    \"node_id\": \"MDEwOlJlcG9zaXRvcnk4MDY1NA==\",\n    \"name\": \"_REPO_NAME\",\n    \"full_name\": \"_FULL_REPO_NAME\",\n    \"owner\": {\n      \"name\": \"_ORG_NAME\",\n      \"email\": \"_ORG_NAME@email.example.com\",\n      \"login\": \"_ORG_NAME\",\n      \"id\": 6582,\n      \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n      \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n      \"html_url\": \"https://github.example.com/_ORG_NAME\",\n      \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n      \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n      \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n      \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n      \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n      \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n      \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n      \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n      \"type\": \"Organization\",\n      \"site_admin\": false\n    },\n    \"private\": false,\n    \"html_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"description\": null,\n    \"fork\": false,\n    \"url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"forks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/forks\",\n    \"keys_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/keys{/key_id}\",\n    \"collaborators_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/collaborators{/collaborator}\",\n    \"teams_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/teams\",\n    \"hooks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/hooks\",\n    \"issue_events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/events{/number}\",\n    \"events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/events\",\n    \"assignees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/assignees{/user}\",\n    \"branches_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/branches{/branch}\",\n    \"tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/tags\",\n    \"blobs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/blobs{/sha}\",\n    \"git_tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/tags{/sha}\",\n    \"git_refs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/refs{/sha}\",\n    \"trees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/trees{/sha}\",\n    \"statuses_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/{sha}\",\n    \"languages_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/languages\",\n    \"stargazers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/stargazers\",\n    \"contributors_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contributors\",\n    \"subscribers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscribers\",\n    \"subscription_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscription\",\n    \"commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/commits{/sha}\",\n    \"git_commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/commits{/sha}\",\n    \"comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/comments{/number}\",\n    \"issue_comment_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/comments{/number}\",\n    \"contents_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contents/{+path}\",\n    \"compare_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/compare/{base}...{head}\",\n    \"merges_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/merges\",\n    \"archive_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/{archive_format}{/ref}\",\n    \"downloads_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/downloads\",\n    \"issues_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues{/number}\",\n    \"pulls_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls{/number}\",\n    \"milestones_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/milestones{/number}\",\n    \"notifications_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/notifications{?since,all,participating}\",\n    \"labels_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/labels{/name}\",\n    \"releases_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/releases{/id}\",\n    \"deployments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/deployments\",\n    \"created_at\": 1509843534,\n    \"updated_at\": \"2019-03-07T15:23:22Z\",\n    \"pushed_at\": 1552409951,\n    \"git_url\": \"git://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n    \"ssh_url\": \"git@github.example.com:_ORG_NAME/_REPO_NAME.git\",\n    \"clone_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n    \"svn_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"homepage\": null,\n    \"size\": 79,\n    \"stargazers_count\": 0,\n    \"watchers_count\": 0,\n    \"language\": null,\n    \"has_issues\": true,\n    \"has_projects\": true,\n    \"has_downloads\": true,\n    \"has_wiki\": true,\n    \"has_pages\": false,\n    \"forks_count\": 2,\n    \"mirror_url\": null,\n    \"archived\": false,\n    \"open_issues_count\": 3,\n    \"license\": null,\n    \"forks\": 2,\n    \"open_issues\": 3,\n    \"watchers\": 0,\n    \"default_branch\": \"master\",\n    \"stargazers\": 0,\n    \"master_branch\": \"master\",\n    \"organization\": \"_ORG_NAME\"\n  },\n  \"pusher\": {\n    \"name\": \"_USER_NAME\",\n    \"email\": \"_USER_NAME@example.com\"\n  },\n  \"organization\": {\n    \"login\": \"_ORG_NAME\",\n    \"id\": 6582,\n    \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n    \"url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME\",\n    \"repos_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/repos\",\n    \"events_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/events\",\n    \"hooks_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/hooks\",\n    \"issues_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/issues\",\n    \"members_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/members{/member}\",\n    \"public_members_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/public_members{/member}\",\n    \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n    \"description\": \"\"\n  },\n  \"sender\": {\n    \"login\": \"_USER_NAME\",\n    \"id\": 8935,\n    \"node_id\": \"MDQ6VXNlcjg5MzU=\",\n    \"avatar_url\": \"https://github.example.com/avatars/u/8935?\",\n    \"gravatar_id\": \"\",\n    \"url\": \"https://github.example.com/api/v3/users/_USER_NAME\",\n    \"html_url\": \"https://github.example.com/_USER_NAME\",\n    \"followers_url\": \"https://github.example.com/api/v3/users/_USER_NAME/followers\",\n    \"following_url\": \"https://github.example.com/api/v3/users/_USER_NAME/following{/other_user}\",\n    \"gists_url\": \"https://github.example.com/api/v3/users/_USER_NAME/gists{/gist_id}\",\n    \"starred_url\": \"https://github.example.com/api/v3/users/_USER_NAME/starred{/owner}{/repo}\",\n    \"subscriptions_url\": \"https://github.example.com/api/v3/users/_USER_NAME/subscriptions\",\n    \"organizations_url\": \"https://github.example.com/api/v3/users/_USER_NAME/orgs\",\n    \"repos_url\": \"https://github.example.com/api/v3/users/_USER_NAME/repos\",\n    \"events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/events{/privacy}\",\n    \"received_events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/received_events\",\n    \"type\": \"User\",\n    \"site_admin\": false,\n    \"ldap_dn\": \"_USER_LDAP_DN\"\n  }\n}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/githubTests/events/direct_branch_push_delete.json",
    "content": "{\n  \"ref\": \"_REF\",\n  \"before\": \"1083c12463e8122e1ff7adf06ec8aabbe998fec4\",\n  \"after\": \"0000000000000000000000000000000000000000\",\n  \"created\": false,\n  \"deleted\": true,\n  \"forced\": false,\n  \"base_ref\": null,\n  \"compare\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/compare/1083c12463e8...a68f9e81f70b\",\n  \"commits\": [],\n  \"head_commit\": null,\n  \"repository\": {\n    \"id\": 80654,\n    \"node_id\": \"MDEwOlJlcG9zaXRvcnk4MDY1NA==\",\n    \"name\": \"_REPO_NAME\",\n    \"full_name\": \"_FULL_REPO_NAME\",\n    \"owner\": {\n      \"name\": \"_ORG_NAME\",\n      \"email\": \"_ORG_NAME@email.example.com\",\n      \"login\": \"_ORG_NAME\",\n      \"id\": 6582,\n      \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n      \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n      \"html_url\": \"https://github.example.com/_ORG_NAME\",\n      \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n      \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n      \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n      \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n      \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n      \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n      \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n      \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n      \"type\": \"Organization\",\n      \"site_admin\": false\n    },\n    \"private\": false,\n    \"html_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"description\": null,\n    \"fork\": false,\n    \"url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"forks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/forks\",\n    \"keys_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/keys{/key_id}\",\n    \"collaborators_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/collaborators{/collaborator}\",\n    \"teams_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/teams\",\n    \"hooks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/hooks\",\n    \"issue_events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/events{/number}\",\n    \"events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/events\",\n    \"assignees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/assignees{/user}\",\n    \"branches_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/branches{/branch}\",\n    \"tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/tags\",\n    \"blobs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/blobs{/sha}\",\n    \"git_tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/tags{/sha}\",\n    \"git_refs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/refs{/sha}\",\n    \"trees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/trees{/sha}\",\n    \"statuses_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/{sha}\",\n    \"languages_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/languages\",\n    \"stargazers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/stargazers\",\n    \"contributors_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contributors\",\n    \"subscribers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscribers\",\n    \"subscription_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscription\",\n    \"commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/commits{/sha}\",\n    \"git_commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/commits{/sha}\",\n    \"comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/comments{/number}\",\n    \"issue_comment_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/comments{/number}\",\n    \"contents_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contents/{+path}\",\n    \"compare_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/compare/{base}...{head}\",\n    \"merges_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/merges\",\n    \"archive_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/{archive_format}{/ref}\",\n    \"downloads_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/downloads\",\n    \"issues_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues{/number}\",\n    \"pulls_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls{/number}\",\n    \"milestones_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/milestones{/number}\",\n    \"notifications_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/notifications{?since,all,participating}\",\n    \"labels_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/labels{/name}\",\n    \"releases_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/releases{/id}\",\n    \"deployments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/deployments\",\n    \"created_at\": 1509843534,\n    \"updated_at\": \"2019-03-07T15:23:22Z\",\n    \"pushed_at\": 1552409951,\n    \"git_url\": \"git://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n    \"ssh_url\": \"git@github.example.com:_ORG_NAME/_REPO_NAME.git\",\n    \"clone_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n    \"svn_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"homepage\": null,\n    \"size\": 79,\n    \"stargazers_count\": 0,\n    \"watchers_count\": 0,\n    \"language\": null,\n    \"has_issues\": true,\n    \"has_projects\": true,\n    \"has_downloads\": true,\n    \"has_wiki\": true,\n    \"has_pages\": false,\n    \"forks_count\": 2,\n    \"mirror_url\": null,\n    \"archived\": false,\n    \"open_issues_count\": 3,\n    \"license\": null,\n    \"forks\": 2,\n    \"open_issues\": 3,\n    \"watchers\": 0,\n    \"default_branch\": \"master\",\n    \"stargazers\": 0,\n    \"master_branch\": \"master\",\n    \"organization\": \"_ORG_NAME\"\n  },\n  \"pusher\": {\n    \"name\": \"_USER_NAME\",\n    \"email\": \"_USER_NAME@example.com\"\n  },\n  \"organization\": {\n    \"login\": \"_ORG_NAME\",\n    \"id\": 6582,\n    \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n    \"url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME\",\n    \"repos_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/repos\",\n    \"events_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/events\",\n    \"hooks_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/hooks\",\n    \"issues_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/issues\",\n    \"members_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/members{/member}\",\n    \"public_members_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/public_members{/member}\",\n    \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n    \"description\": \"\"\n  },\n  \"sender\": {\n    \"login\": \"_USER_NAME\",\n    \"id\": 8935,\n    \"node_id\": \"MDQ6VXNlcjg5MzU=\",\n    \"avatar_url\": \"https://github.example.com/avatars/u/8935?\",\n    \"gravatar_id\": \"\",\n    \"url\": \"https://github.example.com/api/v3/users/_USER_NAME\",\n    \"html_url\": \"https://github.example.com/_USER_NAME\",\n    \"followers_url\": \"https://github.example.com/api/v3/users/_USER_NAME/followers\",\n    \"following_url\": \"https://github.example.com/api/v3/users/_USER_NAME/following{/other_user}\",\n    \"gists_url\": \"https://github.example.com/api/v3/users/_USER_NAME/gists{/gist_id}\",\n    \"starred_url\": \"https://github.example.com/api/v3/users/_USER_NAME/starred{/owner}{/repo}\",\n    \"subscriptions_url\": \"https://github.example.com/api/v3/users/_USER_NAME/subscriptions\",\n    \"organizations_url\": \"https://github.example.com/api/v3/users/_USER_NAME/orgs\",\n    \"repos_url\": \"https://github.example.com/api/v3/users/_USER_NAME/repos\",\n    \"events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/events{/privacy}\",\n    \"received_events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/received_events\",\n    \"type\": \"User\",\n    \"site_admin\": false,\n    \"ldap_dn\": \"_USER_LDAP_DN\"\n  }\n}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/githubTests/events/empty_push.json",
    "content": "{\n  \"ref\": \"_REF\",\n  \"before\": \"a68f9e81f70b5ac645739f244cdc95a0da24310e\",\n  \"after\": \"a68f9e81f70b5ac645739f244cdc95a0da24310e\",\n  \"created\": false,\n  \"deleted\": false,\n  \"forced\": false,\n  \"base_ref\": null,\n  \"compare\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/compare/1083c12463e8...a68f9e81f70b\",\n  \"commits\": [],\n  \"head_commit\": {\n    \"id\": \"a68f9e81f70b5ac645739f244cdc95a0da24310e\",\n    \"tree_id\": \"5e0785caefd8d107f75d18a26907f68c7168a8cc\",\n    \"distinct\": true,\n    \"message\": \"bump\",\n    \"timestamp\": \"2019-03-12T12:59:05-04:00\",\n    \"url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/commit/a68f9e81f70b5ac645739f244cdc95a0da24310e\",\n    \"author\": {\n      \"name\": \"John Smith\",\n      \"email\": \"_USER_NAME@example.com\",\n      \"username\": \"_USER_NAME\"\n    },\n    \"committer\": {\n      \"name\": \"John Smith\",\n      \"email\": \"_USER_NAME@example.com\",\n      \"username\": \"_USER_NAME\"\n    },\n    \"added\": [],\n    \"removed\": [],\n    \"modified\": [\n      \"concord.yml\"\n    ]\n  },\n  \"repository\": {\n    \"id\": 80654,\n    \"node_id\": \"MDEwOlJlcG9zaXRvcnk4MDY1NA==\",\n    \"name\": \"_REPO_NAME\",\n    \"full_name\": \"_FULL_REPO_NAME\",\n    \"owner\": {\n      \"name\": \"_ORG_NAME\",\n      \"email\": \"_ORG_NAME@email.example.com\",\n      \"login\": \"_ORG_NAME\",\n      \"id\": 6582,\n      \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n      \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n      \"html_url\": \"https://github.example.com/_ORG_NAME\",\n      \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n      \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n      \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n      \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n      \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n      \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n      \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n      \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n      \"type\": \"Organization\",\n      \"site_admin\": false\n    },\n    \"private\": false,\n    \"html_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"description\": null,\n    \"fork\": false,\n    \"url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"forks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/forks\",\n    \"keys_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/keys{/key_id}\",\n    \"collaborators_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/collaborators{/collaborator}\",\n    \"teams_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/teams\",\n    \"hooks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/hooks\",\n    \"issue_events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/events{/number}\",\n    \"events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/events\",\n    \"assignees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/assignees{/user}\",\n    \"branches_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/branches{/branch}\",\n    \"tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/tags\",\n    \"blobs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/blobs{/sha}\",\n    \"git_tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/tags{/sha}\",\n    \"git_refs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/refs{/sha}\",\n    \"trees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/trees{/sha}\",\n    \"statuses_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/{sha}\",\n    \"languages_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/languages\",\n    \"stargazers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/stargazers\",\n    \"contributors_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contributors\",\n    \"subscribers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscribers\",\n    \"subscription_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscription\",\n    \"commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/commits{/sha}\",\n    \"git_commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/commits{/sha}\",\n    \"comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/comments{/number}\",\n    \"issue_comment_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/comments{/number}\",\n    \"contents_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contents/{+path}\",\n    \"compare_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/compare/{base}...{head}\",\n    \"merges_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/merges\",\n    \"archive_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/{archive_format}{/ref}\",\n    \"downloads_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/downloads\",\n    \"issues_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues{/number}\",\n    \"pulls_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls{/number}\",\n    \"milestones_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/milestones{/number}\",\n    \"notifications_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/notifications{?since,all,participating}\",\n    \"labels_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/labels{/name}\",\n    \"releases_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/releases{/id}\",\n    \"deployments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/deployments\",\n    \"created_at\": 1509843534,\n    \"updated_at\": \"2019-03-07T15:23:22Z\",\n    \"pushed_at\": 1552409951,\n    \"git_url\": \"git://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n    \"ssh_url\": \"git@github.example.com:_ORG_NAME/_REPO_NAME.git\",\n    \"clone_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n    \"svn_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"homepage\": null,\n    \"size\": 79,\n    \"stargazers_count\": 0,\n    \"watchers_count\": 0,\n    \"language\": null,\n    \"has_issues\": true,\n    \"has_projects\": true,\n    \"has_downloads\": true,\n    \"has_wiki\": true,\n    \"has_pages\": false,\n    \"forks_count\": 2,\n    \"mirror_url\": null,\n    \"archived\": false,\n    \"open_issues_count\": 3,\n    \"license\": null,\n    \"forks\": 2,\n    \"open_issues\": 3,\n    \"watchers\": 0,\n    \"default_branch\": \"master\",\n    \"stargazers\": 0,\n    \"master_branch\": \"master\",\n    \"organization\": \"_ORG_NAME\"\n  },\n  \"pusher\": {\n    \"name\": \"_USER_NAME\",\n    \"email\": \"_USER_NAME@example.com\"\n  },\n  \"organization\": {\n    \"login\": \"_ORG_NAME\",\n    \"id\": 6582,\n    \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n    \"url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME\",\n    \"repos_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/repos\",\n    \"events_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/events\",\n    \"hooks_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/hooks\",\n    \"issues_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/issues\",\n    \"members_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/members{/member}\",\n    \"public_members_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/public_members{/member}\",\n    \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n    \"description\": \"\"\n  },\n  \"sender\": {\n    \"login\": \"_USER_NAME\",\n    \"id\": 8935,\n    \"node_id\": \"MDQ6VXNlcjg5MzU=\",\n    \"avatar_url\": \"https://github.example.com/avatars/u/8935?\",\n    \"gravatar_id\": \"\",\n    \"url\": \"https://github.example.com/api/v3/users/_USER_NAME\",\n    \"html_url\": \"https://github.example.com/_USER_NAME\",\n    \"followers_url\": \"https://github.example.com/api/v3/users/_USER_NAME/followers\",\n    \"following_url\": \"https://github.example.com/api/v3/users/_USER_NAME/following{/other_user}\",\n    \"gists_url\": \"https://github.example.com/api/v3/users/_USER_NAME/gists{/gist_id}\",\n    \"starred_url\": \"https://github.example.com/api/v3/users/_USER_NAME/starred{/owner}{/repo}\",\n    \"subscriptions_url\": \"https://github.example.com/api/v3/users/_USER_NAME/subscriptions\",\n    \"organizations_url\": \"https://github.example.com/api/v3/users/_USER_NAME/orgs\",\n    \"repos_url\": \"https://github.example.com/api/v3/users/_USER_NAME/repos\",\n    \"events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/events{/privacy}\",\n    \"received_events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/received_events\",\n    \"type\": \"User\",\n    \"site_admin\": false,\n    \"ldap_dn\": \"_USER_LDAP_DN\"\n  }\n}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/githubTests/events/pr_close.json",
    "content": "{\n  \"action\": \"closed\",\n  \"number\": 12,\n  \"pull_request\": {\n    \"url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12\",\n    \"id\": 1010948,\n    \"node_id\": \"MDExOlB1bGxSZXF1ZXN0MTAxMDk0OA==\",\n    \"html_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/pull/12\",\n    \"diff_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/pull/12.diff\",\n    \"patch_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/pull/12.patch\",\n    \"issue_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/12\",\n    \"number\": 12,\n    \"state\": \"closed\",\n    \"locked\": false,\n    \"title\": \"bump\",\n    \"user\": {\n      \"login\": \"_USER_NAME\",\n      \"id\": 8935,\n      \"node_id\": \"MDQ6VXNlcjg5MzU=\",\n      \"avatar_url\": \"https://github.example.com/avatars/u/8935?\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://github.example.com/api/v3/users/_USER_NAME\",\n      \"html_url\": \"https://github.example.com/_USER_NAME\",\n      \"followers_url\": \"https://github.example.com/api/v3/users/_USER_NAME/followers\",\n      \"following_url\": \"https://github.example.com/api/v3/users/_USER_NAME/following{/other_user}\",\n      \"gists_url\": \"https://github.example.com/api/v3/users/_USER_NAME/gists{/gist_id}\",\n      \"starred_url\": \"https://github.example.com/api/v3/users/_USER_NAME/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://github.example.com/api/v3/users/_USER_NAME/subscriptions\",\n      \"organizations_url\": \"https://github.example.com/api/v3/users/_USER_NAME/orgs\",\n      \"repos_url\": \"https://github.example.com/api/v3/users/_USER_NAME/repos\",\n      \"events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/events{/privacy}\",\n      \"received_events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false,\n      \"ldap_dn\": \"_USER_LDAP_DN\"\n    },\n    \"body\": \"\",\n    \"created_at\": \"2019-03-12T17:56:16Z\",\n    \"updated_at\": \"2019-03-12T17:59:46Z\",\n    \"closed_at\": \"2019-03-12T17:59:46Z\",\n    \"merged_at\": \"2019-03-12T17:59:46Z\",\n    \"merge_commit_sha\": \"ea21d5e14c6b6c8313f0920113d46afc8e6dc04d\",\n    \"assignee\": null,\n    \"assignees\": [],\n    \"requested_reviewers\": [],\n    \"requested_teams\": [],\n    \"labels\": [],\n    \"milestone\": null,\n    \"commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12/commits\",\n    \"review_comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12/comments\",\n    \"review_comment_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/comments{/number}\",\n    \"comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/12/comments\",\n    \"statuses_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/85e9a1f7e757aa6b82ea66206910c8ae758b3b0d\",\n    \"head\": {\n      \"label\": \"_ORG_NAME:pr-test-3\",\n      \"ref\": \"pr-test-3\",\n      \"sha\": \"85e9a1f7e757aa6b82ea66206910c8ae758b3b0d\",\n      \"user\": {\n        \"login\": \"_ORG_NAME\",\n        \"id\": 6582,\n        \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n        \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n        \"gravatar_id\": \"\",\n        \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n        \"html_url\": \"https://github.example.com/_ORG_NAME\",\n        \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n        \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n        \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n        \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n        \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n        \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n        \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n        \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n        \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n        \"type\": \"Organization\",\n        \"site_admin\": false\n      },\n      \"repo\": {\n        \"id\": 80654,\n        \"node_id\": \"MDEwOlJlcG9zaXRvcnk4MDY1NA==\",\n        \"name\": \"_REPO_NAME\",\n        \"full_name\": \"_FULL_REPO_NAME\",\n        \"owner\": {\n          \"login\": \"_ORG_NAME\",\n          \"id\": 6582,\n          \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n          \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n          \"gravatar_id\": \"\",\n          \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n          \"html_url\": \"https://github.example.com/_ORG_NAME\",\n          \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n          \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n          \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n          \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n          \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n          \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n          \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n          \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n          \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n          \"type\": \"Organization\",\n          \"site_admin\": false\n        },\n        \"private\": false,\n        \"html_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n        \"description\": null,\n        \"fork\": false,\n        \"url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME\",\n        \"forks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/forks\",\n        \"keys_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/keys{/key_id}\",\n        \"collaborators_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/collaborators{/collaborator}\",\n        \"teams_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/teams\",\n        \"hooks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/hooks\",\n        \"issue_events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/events{/number}\",\n        \"events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/events\",\n        \"assignees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/assignees{/user}\",\n        \"branches_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/branches{/branch}\",\n        \"tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/tags\",\n        \"blobs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/blobs{/sha}\",\n        \"git_tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/tags{/sha}\",\n        \"git_refs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/refs{/sha}\",\n        \"trees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/trees{/sha}\",\n        \"statuses_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/{sha}\",\n        \"languages_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/languages\",\n        \"stargazers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/stargazers\",\n        \"contributors_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contributors\",\n        \"subscribers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscribers\",\n        \"subscription_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscription\",\n        \"commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/commits{/sha}\",\n        \"git_commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/commits{/sha}\",\n        \"comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/comments{/number}\",\n        \"issue_comment_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/comments{/number}\",\n        \"contents_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contents/{+path}\",\n        \"compare_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/compare/{base}...{head}\",\n        \"merges_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/merges\",\n        \"archive_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/{archive_format}{/ref}\",\n        \"downloads_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/downloads\",\n        \"issues_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues{/number}\",\n        \"pulls_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls{/number}\",\n        \"milestones_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/milestones{/number}\",\n        \"notifications_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/notifications{?since,all,participating}\",\n        \"labels_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/labels{/name}\",\n        \"releases_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/releases{/id}\",\n        \"deployments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/deployments\",\n        \"created_at\": \"2017-11-05T00:58:54Z\",\n        \"updated_at\": \"2019-03-12T17:55:49Z\",\n        \"pushed_at\": \"2019-03-12T17:59:46Z\",\n        \"git_url\": \"git://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n        \"ssh_url\": \"git@github.example.com:_ORG_NAME/_REPO_NAME.git\",\n        \"clone_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n        \"svn_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n        \"homepage\": null,\n        \"size\": 79,\n        \"stargazers_count\": 0,\n        \"watchers_count\": 0,\n        \"language\": null,\n        \"has_issues\": true,\n        \"has_projects\": true,\n        \"has_downloads\": true,\n        \"has_wiki\": true,\n        \"has_pages\": false,\n        \"forks_count\": 2,\n        \"mirror_url\": null,\n        \"archived\": false,\n        \"open_issues_count\": 3,\n        \"license\": null,\n        \"forks\": 2,\n        \"open_issues\": 3,\n        \"watchers\": 0,\n        \"default_branch\": \"master\"\n      }\n    },\n    \"base\": {\n      \"label\": \"_ORG_NAME:master\",\n      \"ref\": \"master\",\n      \"sha\": \"ce08c4051a9a7d7e34e12f717a15bf788ce2a3a4\",\n      \"user\": {\n        \"login\": \"_ORG_NAME\",\n        \"id\": 6582,\n        \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n        \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n        \"gravatar_id\": \"\",\n        \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n        \"html_url\": \"https://github.example.com/_ORG_NAME\",\n        \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n        \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n        \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n        \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n        \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n        \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n        \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n        \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n        \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n        \"type\": \"Organization\",\n        \"site_admin\": false\n      },\n      \"repo\": {\n        \"id\": 80654,\n        \"node_id\": \"MDEwOlJlcG9zaXRvcnk4MDY1NA==\",\n        \"name\": \"_REPO_NAME\",\n        \"full_name\": \"_FULL_REPO_NAME\",\n        \"owner\": {\n          \"login\": \"_ORG_NAME\",\n          \"id\": 6582,\n          \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n          \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n          \"gravatar_id\": \"\",\n          \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n          \"html_url\": \"https://github.example.com/_ORG_NAME\",\n          \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n          \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n          \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n          \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n          \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n          \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n          \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n          \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n          \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n          \"type\": \"Organization\",\n          \"site_admin\": false\n        },\n        \"private\": false,\n        \"html_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n        \"description\": null,\n        \"fork\": false,\n        \"url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME\",\n        \"forks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/forks\",\n        \"keys_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/keys{/key_id}\",\n        \"collaborators_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/collaborators{/collaborator}\",\n        \"teams_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/teams\",\n        \"hooks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/hooks\",\n        \"issue_events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/events{/number}\",\n        \"events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/events\",\n        \"assignees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/assignees{/user}\",\n        \"branches_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/branches{/branch}\",\n        \"tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/tags\",\n        \"blobs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/blobs{/sha}\",\n        \"git_tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/tags{/sha}\",\n        \"git_refs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/refs{/sha}\",\n        \"trees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/trees{/sha}\",\n        \"statuses_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/{sha}\",\n        \"languages_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/languages\",\n        \"stargazers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/stargazers\",\n        \"contributors_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contributors\",\n        \"subscribers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscribers\",\n        \"subscription_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscription\",\n        \"commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/commits{/sha}\",\n        \"git_commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/commits{/sha}\",\n        \"comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/comments{/number}\",\n        \"issue_comment_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/comments{/number}\",\n        \"contents_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contents/{+path}\",\n        \"compare_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/compare/{base}...{head}\",\n        \"merges_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/merges\",\n        \"archive_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/{archive_format}{/ref}\",\n        \"downloads_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/downloads\",\n        \"issues_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues{/number}\",\n        \"pulls_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls{/number}\",\n        \"milestones_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/milestones{/number}\",\n        \"notifications_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/notifications{?since,all,participating}\",\n        \"labels_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/labels{/name}\",\n        \"releases_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/releases{/id}\",\n        \"deployments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/deployments\",\n        \"created_at\": \"2017-11-05T00:58:54Z\",\n        \"updated_at\": \"2019-03-12T17:55:49Z\",\n        \"pushed_at\": \"2019-03-12T17:59:46Z\",\n        \"git_url\": \"git://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n        \"ssh_url\": \"git@github.example.com:_ORG_NAME/_REPO_NAME.git\",\n        \"clone_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n        \"svn_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n        \"homepage\": null,\n        \"size\": 79,\n        \"stargazers_count\": 0,\n        \"watchers_count\": 0,\n        \"language\": null,\n        \"has_issues\": true,\n        \"has_projects\": true,\n        \"has_downloads\": true,\n        \"has_wiki\": true,\n        \"has_pages\": false,\n        \"forks_count\": 2,\n        \"mirror_url\": null,\n        \"archived\": false,\n        \"open_issues_count\": 3,\n        \"license\": null,\n        \"forks\": 2,\n        \"open_issues\": 3,\n        \"watchers\": 0,\n        \"default_branch\": \"master\"\n      }\n    },\n    \"_links\": {\n      \"self\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12\"\n      },\n      \"html\": {\n        \"href\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/pull/12\"\n      },\n      \"issue\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/12\"\n      },\n      \"comments\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/12/comments\"\n      },\n      \"review_comments\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12/comments\"\n      },\n      \"review_comment\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/comments{/number}\"\n      },\n      \"commits\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12/commits\"\n      },\n      \"statuses\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/85e9a1f7e757aa6b82ea66206910c8ae758b3b0d\"\n      }\n    },\n    \"author_association\": \"COLLABORATOR\",\n    \"merged\": true,\n    \"mergeable\": null,\n    \"rebaseable\": null,\n    \"mergeable_state\": \"unknown\",\n    \"merged_by\": {\n      \"login\": \"_USER_NAME\",\n      \"id\": 8935,\n      \"node_id\": \"MDQ6VXNlcjg5MzU=\",\n      \"avatar_url\": \"https://github.example.com/avatars/u/8935?\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://github.example.com/api/v3/users/_USER_NAME\",\n      \"html_url\": \"https://github.example.com/_USER_NAME\",\n      \"followers_url\": \"https://github.example.com/api/v3/users/_USER_NAME/followers\",\n      \"following_url\": \"https://github.example.com/api/v3/users/_USER_NAME/following{/other_user}\",\n      \"gists_url\": \"https://github.example.com/api/v3/users/_USER_NAME/gists{/gist_id}\",\n      \"starred_url\": \"https://github.example.com/api/v3/users/_USER_NAME/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://github.example.com/api/v3/users/_USER_NAME/subscriptions\",\n      \"organizations_url\": \"https://github.example.com/api/v3/users/_USER_NAME/orgs\",\n      \"repos_url\": \"https://github.example.com/api/v3/users/_USER_NAME/repos\",\n      \"events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/events{/privacy}\",\n      \"received_events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false,\n      \"ldap_dn\": \"_USER_LDAP_DN\"\n    },\n    \"comments\": 0,\n    \"review_comments\": 0,\n    \"maintainer_can_modify\": false,\n    \"commits\": 1,\n    \"additions\": 0,\n    \"deletions\": 1,\n    \"changed_files\": 1\n  },\n  \"repository\": {\n    \"id\": 80654,\n    \"node_id\": \"MDEwOlJlcG9zaXRvcnk4MDY1NA==\",\n    \"name\": \"_REPO_NAME\",\n    \"full_name\": \"_FULL_REPO_NAME\",\n    \"owner\": {\n      \"login\": \"_ORG_NAME\",\n      \"id\": 6582,\n      \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n      \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n      \"html_url\": \"https://github.example.com/_ORG_NAME\",\n      \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n      \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n      \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n      \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n      \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n      \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n      \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n      \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n      \"type\": \"Organization\",\n      \"site_admin\": false\n    },\n    \"private\": false,\n    \"html_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"description\": null,\n    \"fork\": false,\n    \"url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME\",\n    \"forks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/forks\",\n    \"keys_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/keys{/key_id}\",\n    \"collaborators_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/collaborators{/collaborator}\",\n    \"teams_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/teams\",\n    \"hooks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/hooks\",\n    \"issue_events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/events{/number}\",\n    \"events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/events\",\n    \"assignees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/assignees{/user}\",\n    \"branches_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/branches{/branch}\",\n    \"tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/tags\",\n    \"blobs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/blobs{/sha}\",\n    \"git_tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/tags{/sha}\",\n    \"git_refs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/refs{/sha}\",\n    \"trees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/trees{/sha}\",\n    \"statuses_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/{sha}\",\n    \"languages_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/languages\",\n    \"stargazers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/stargazers\",\n    \"contributors_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contributors\",\n    \"subscribers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscribers\",\n    \"subscription_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscription\",\n    \"commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/commits{/sha}\",\n    \"git_commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/commits{/sha}\",\n    \"comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/comments{/number}\",\n    \"issue_comment_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/comments{/number}\",\n    \"contents_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contents/{+path}\",\n    \"compare_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/compare/{base}...{head}\",\n    \"merges_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/merges\",\n    \"archive_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/{archive_format}{/ref}\",\n    \"downloads_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/downloads\",\n    \"issues_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues{/number}\",\n    \"pulls_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls{/number}\",\n    \"milestones_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/milestones{/number}\",\n    \"notifications_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/notifications{?since,all,participating}\",\n    \"labels_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/labels{/name}\",\n    \"releases_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/releases{/id}\",\n    \"deployments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/deployments\",\n    \"created_at\": \"2017-11-05T00:58:54Z\",\n    \"updated_at\": \"2019-03-12T17:55:49Z\",\n    \"pushed_at\": \"2019-03-12T17:59:46Z\",\n    \"git_url\": \"git://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n    \"ssh_url\": \"git@github.example.com:_ORG_NAME/_REPO_NAME.git\",\n    \"clone_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n    \"svn_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"homepage\": null,\n    \"size\": 79,\n    \"stargazers_count\": 0,\n    \"watchers_count\": 0,\n    \"language\": null,\n    \"has_issues\": true,\n    \"has_projects\": true,\n    \"has_downloads\": true,\n    \"has_wiki\": true,\n    \"has_pages\": false,\n    \"forks_count\": 2,\n    \"mirror_url\": null,\n    \"archived\": false,\n    \"open_issues_count\": 3,\n    \"license\": null,\n    \"forks\": 2,\n    \"open_issues\": 3,\n    \"watchers\": 0,\n    \"default_branch\": \"master\"\n  },\n  \"organization\": {\n    \"login\": \"_ORG_NAME\",\n    \"id\": 6582,\n    \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n    \"url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME\",\n    \"repos_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/repos\",\n    \"events_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/events\",\n    \"hooks_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/hooks\",\n    \"issues_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/issues\",\n    \"members_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/members{/member}\",\n    \"public_members_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/public_members{/member}\",\n    \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n    \"description\": \"\"\n  },\n  \"sender\": {\n    \"login\": \"_USER_NAME\",\n    \"id\": 8935,\n    \"node_id\": \"MDQ6VXNlcjg5MzU=\",\n    \"avatar_url\": \"https://github.example.com/avatars/u/8935?\",\n    \"gravatar_id\": \"\",\n    \"url\": \"https://github.example.com/api/v3/users/_USER_NAME\",\n    \"html_url\": \"https://github.example.com/_USER_NAME\",\n    \"followers_url\": \"https://github.example.com/api/v3/users/_USER_NAME/followers\",\n    \"following_url\": \"https://github.example.com/api/v3/users/_USER_NAME/following{/other_user}\",\n    \"gists_url\": \"https://github.example.com/api/v3/users/_USER_NAME/gists{/gist_id}\",\n    \"starred_url\": \"https://github.example.com/api/v3/users/_USER_NAME/starred{/owner}{/repo}\",\n    \"subscriptions_url\": \"https://github.example.com/api/v3/users/_USER_NAME/subscriptions\",\n    \"organizations_url\": \"https://github.example.com/api/v3/users/_USER_NAME/orgs\",\n    \"repos_url\": \"https://github.example.com/api/v3/users/_USER_NAME/repos\",\n    \"events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/events{/privacy}\",\n    \"received_events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/received_events\",\n    \"type\": \"User\",\n    \"site_admin\": false,\n    \"ldap_dn\": \"_USER_LDAP_DN\"\n  }\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/githubTests/events/pr_open.json",
    "content": "{\n  \"action\": \"opened\",\n  \"number\": 12,\n  \"pull_request\": {\n    \"url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12\",\n    \"id\": 1010948,\n    \"node_id\": \"MDExOlB1bGxSZXF1ZXN0MTAxMDk0OA==\",\n    \"html_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/pull/12\",\n    \"diff_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/pull/12.diff\",\n    \"patch_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/pull/12.patch\",\n    \"issue_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/12\",\n    \"number\": 12,\n    \"state\": \"open\",\n    \"locked\": false,\n    \"title\": \"bump\",\n    \"user\": {\n      \"login\": \"_USER_NAME\",\n      \"id\": 8935,\n      \"node_id\": \"MDQ6VXNlcjg5MzU=\",\n      \"avatar_url\": \"https://github.example.com/avatars/u/8935?\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://github.example.com/api/v3/users/_USER_NAME\",\n      \"html_url\": \"https://github.example.com/_USER_NAME\",\n      \"followers_url\": \"https://github.example.com/api/v3/users/_USER_NAME/followers\",\n      \"following_url\": \"https://github.example.com/api/v3/users/_USER_NAME/following{/other_user}\",\n      \"gists_url\": \"https://github.example.com/api/v3/users/_USER_NAME/gists{/gist_id}\",\n      \"starred_url\": \"https://github.example.com/api/v3/users/_USER_NAME/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://github.example.com/api/v3/users/_USER_NAME/subscriptions\",\n      \"organizations_url\": \"https://github.example.com/api/v3/users/_USER_NAME/orgs\",\n      \"repos_url\": \"https://github.example.com/api/v3/users/_USER_NAME/repos\",\n      \"events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/events{/privacy}\",\n      \"received_events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false,\n      \"ldap_dn\": \"_USER_LDAP_DN\"\n    },\n    \"body\": \"\",\n    \"created_at\": \"2019-03-12T17:56:16Z\",\n    \"updated_at\": \"2019-03-12T17:56:16Z\",\n    \"closed_at\": null,\n    \"merged_at\": null,\n    \"merge_commit_sha\": null,\n    \"assignee\": null,\n    \"assignees\": [],\n    \"requested_reviewers\": [],\n    \"requested_teams\": [],\n    \"labels\": [],\n    \"milestone\": null,\n    \"commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12/commits\",\n    \"review_comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12/comments\",\n    \"review_comment_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/comments{/number}\",\n    \"comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/12/comments\",\n    \"statuses_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/85e9a1f7e757aa6b82ea66206910c8ae758b3b0d\",\n    \"head\": {\n      \"label\": \"_ORG_NAME:pr-test-3\",\n      \"ref\": \"pr-test-3\",\n      \"sha\": \"85e9a1f7e757aa6b82ea66206910c8ae758b3b0d\",\n      \"user\": {\n        \"login\": \"_ORG_NAME\",\n        \"id\": 6582,\n        \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n        \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n        \"gravatar_id\": \"\",\n        \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n        \"html_url\": \"https://github.example.com/_ORG_NAME\",\n        \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n        \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n        \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n        \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n        \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n        \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n        \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n        \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n        \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n        \"type\": \"Organization\",\n        \"site_admin\": false\n      },\n      \"repo\": {\n        \"id\": 80654,\n        \"node_id\": \"MDEwOlJlcG9zaXRvcnk4MDY1NA==\",\n        \"name\": \"_REPO_NAME\",\n        \"full_name\": \"_FULL_REPO_NAME\",\n        \"owner\": {\n          \"login\": \"_ORG_NAME\",\n          \"id\": 6582,\n          \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n          \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n          \"gravatar_id\": \"\",\n          \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n          \"html_url\": \"https://github.example.com/_ORG_NAME\",\n          \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n          \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n          \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n          \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n          \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n          \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n          \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n          \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n          \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n          \"type\": \"Organization\",\n          \"site_admin\": false\n        },\n        \"private\": false,\n        \"html_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n        \"description\": null,\n        \"fork\": false,\n        \"url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME\",\n        \"forks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/forks\",\n        \"keys_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/keys{/key_id}\",\n        \"collaborators_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/collaborators{/collaborator}\",\n        \"teams_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/teams\",\n        \"hooks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/hooks\",\n        \"issue_events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/events{/number}\",\n        \"events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/events\",\n        \"assignees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/assignees{/user}\",\n        \"branches_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/branches{/branch}\",\n        \"tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/tags\",\n        \"blobs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/blobs{/sha}\",\n        \"git_tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/tags{/sha}\",\n        \"git_refs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/refs{/sha}\",\n        \"trees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/trees{/sha}\",\n        \"statuses_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/{sha}\",\n        \"languages_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/languages\",\n        \"stargazers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/stargazers\",\n        \"contributors_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contributors\",\n        \"subscribers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscribers\",\n        \"subscription_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscription\",\n        \"commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/commits{/sha}\",\n        \"git_commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/commits{/sha}\",\n        \"comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/comments{/number}\",\n        \"issue_comment_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/comments{/number}\",\n        \"contents_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contents/{+path}\",\n        \"compare_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/compare/{base}...{head}\",\n        \"merges_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/merges\",\n        \"archive_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/{archive_format}{/ref}\",\n        \"downloads_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/downloads\",\n        \"issues_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues{/number}\",\n        \"pulls_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls{/number}\",\n        \"milestones_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/milestones{/number}\",\n        \"notifications_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/notifications{?since,all,participating}\",\n        \"labels_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/labels{/name}\",\n        \"releases_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/releases{/id}\",\n        \"deployments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/deployments\",\n        \"created_at\": \"2017-11-05T00:58:54Z\",\n        \"updated_at\": \"2019-03-12T17:55:49Z\",\n        \"pushed_at\": \"2019-03-12T17:56:12Z\",\n        \"git_url\": \"git://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n        \"ssh_url\": \"git@github.example.com:_ORG_NAME/_REPO_NAME.git\",\n        \"clone_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n        \"svn_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n        \"homepage\": null,\n        \"size\": 79,\n        \"stargazers_count\": 0,\n        \"watchers_count\": 0,\n        \"language\": null,\n        \"has_issues\": true,\n        \"has_projects\": true,\n        \"has_downloads\": true,\n        \"has_wiki\": true,\n        \"has_pages\": false,\n        \"forks_count\": 2,\n        \"mirror_url\": null,\n        \"archived\": false,\n        \"open_issues_count\": 4,\n        \"license\": null,\n        \"forks\": 2,\n        \"open_issues\": 4,\n        \"watchers\": 0,\n        \"default_branch\": \"master\"\n      }\n    },\n    \"base\": {\n      \"label\": \"_ORG_NAME:master\",\n      \"ref\": \"master\",\n      \"sha\": \"ce08c4051a9a7d7e34e12f717a15bf788ce2a3a4\",\n      \"user\": {\n        \"login\": \"_ORG_NAME\",\n        \"id\": 6582,\n        \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n        \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n        \"gravatar_id\": \"\",\n        \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n        \"html_url\": \"https://github.example.com/_ORG_NAME\",\n        \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n        \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n        \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n        \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n        \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n        \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n        \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n        \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n        \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n        \"type\": \"Organization\",\n        \"site_admin\": false\n      },\n      \"repo\": {\n        \"id\": 80654,\n        \"node_id\": \"MDEwOlJlcG9zaXRvcnk4MDY1NA==\",\n        \"name\": \"_REPO_NAME\",\n        \"full_name\": \"_FULL_REPO_NAME\",\n        \"owner\": {\n          \"login\": \"_ORG_NAME\",\n          \"id\": 6582,\n          \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n          \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n          \"gravatar_id\": \"\",\n          \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n          \"html_url\": \"https://github.example.com/_ORG_NAME\",\n          \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n          \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n          \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n          \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n          \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n          \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n          \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n          \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n          \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n          \"type\": \"Organization\",\n          \"site_admin\": false\n        },\n        \"private\": false,\n        \"html_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n        \"description\": null,\n        \"fork\": false,\n        \"url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME\",\n        \"forks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/forks\",\n        \"keys_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/keys{/key_id}\",\n        \"collaborators_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/collaborators{/collaborator}\",\n        \"teams_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/teams\",\n        \"hooks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/hooks\",\n        \"issue_events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/events{/number}\",\n        \"events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/events\",\n        \"assignees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/assignees{/user}\",\n        \"branches_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/branches{/branch}\",\n        \"tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/tags\",\n        \"blobs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/blobs{/sha}\",\n        \"git_tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/tags{/sha}\",\n        \"git_refs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/refs{/sha}\",\n        \"trees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/trees{/sha}\",\n        \"statuses_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/{sha}\",\n        \"languages_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/languages\",\n        \"stargazers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/stargazers\",\n        \"contributors_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contributors\",\n        \"subscribers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscribers\",\n        \"subscription_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscription\",\n        \"commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/commits{/sha}\",\n        \"git_commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/commits{/sha}\",\n        \"comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/comments{/number}\",\n        \"issue_comment_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/comments{/number}\",\n        \"contents_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contents/{+path}\",\n        \"compare_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/compare/{base}...{head}\",\n        \"merges_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/merges\",\n        \"archive_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/{archive_format}{/ref}\",\n        \"downloads_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/downloads\",\n        \"issues_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues{/number}\",\n        \"pulls_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls{/number}\",\n        \"milestones_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/milestones{/number}\",\n        \"notifications_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/notifications{?since,all,participating}\",\n        \"labels_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/labels{/name}\",\n        \"releases_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/releases{/id}\",\n        \"deployments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/deployments\",\n        \"created_at\": \"2017-11-05T00:58:54Z\",\n        \"updated_at\": \"2019-03-12T17:55:49Z\",\n        \"pushed_at\": \"2019-03-12T17:56:12Z\",\n        \"git_url\": \"git://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n        \"ssh_url\": \"git@github.example.com:_ORG_NAME/_REPO_NAME.git\",\n        \"clone_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n        \"svn_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n        \"homepage\": null,\n        \"size\": 79,\n        \"stargazers_count\": 0,\n        \"watchers_count\": 0,\n        \"language\": null,\n        \"has_issues\": true,\n        \"has_projects\": true,\n        \"has_downloads\": true,\n        \"has_wiki\": true,\n        \"has_pages\": false,\n        \"forks_count\": 2,\n        \"mirror_url\": null,\n        \"archived\": false,\n        \"open_issues_count\": 4,\n        \"license\": null,\n        \"forks\": 2,\n        \"open_issues\": 4,\n        \"watchers\": 0,\n        \"default_branch\": \"master\"\n      }\n    },\n    \"_links\": {\n      \"self\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12\"\n      },\n      \"html\": {\n        \"href\": \"https://github.example.com/_ORG_NAME/_REPO_NAME/pull/12\"\n      },\n      \"issue\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/12\"\n      },\n      \"comments\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/12/comments\"\n      },\n      \"review_comments\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12/comments\"\n      },\n      \"review_comment\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/comments{/number}\"\n      },\n      \"commits\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls/12/commits\"\n      },\n      \"statuses\": {\n        \"href\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/85e9a1f7e757aa6b82ea66206910c8ae758b3b0d\"\n      }\n    },\n    \"author_association\": \"COLLABORATOR\",\n    \"merged\": false,\n    \"mergeable\": null,\n    \"rebaseable\": null,\n    \"mergeable_state\": \"unknown\",\n    \"merged_by\": null,\n    \"comments\": 0,\n    \"review_comments\": 0,\n    \"maintainer_can_modify\": false,\n    \"commits\": 1,\n    \"additions\": 0,\n    \"deletions\": 1,\n    \"changed_files\": 1\n  },\n  \"repository\": {\n    \"id\": 80654,\n    \"node_id\": \"MDEwOlJlcG9zaXRvcnk4MDY1NA==\",\n    \"name\": \"_REPO_NAME\",\n    \"full_name\": \"_FULL_REPO_NAME\",\n    \"owner\": {\n      \"login\": \"_ORG_NAME\",\n      \"id\": 6582,\n      \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n      \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://github.example.com/api/v3/users/_ORG_NAME\",\n      \"html_url\": \"https://github.example.com/_ORG_NAME\",\n      \"followers_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/followers\",\n      \"following_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/following{/other_user}\",\n      \"gists_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/gists{/gist_id}\",\n      \"starred_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/subscriptions\",\n      \"organizations_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/orgs\",\n      \"repos_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/repos\",\n      \"events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/events{/privacy}\",\n      \"received_events_url\": \"https://github.example.com/api/v3/users/_ORG_NAME/received_events\",\n      \"type\": \"Organization\",\n      \"site_admin\": false\n    },\n    \"private\": false,\n    \"html_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"description\": null,\n    \"fork\": false,\n    \"url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME\",\n    \"forks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/forks\",\n    \"keys_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/keys{/key_id}\",\n    \"collaborators_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/collaborators{/collaborator}\",\n    \"teams_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/teams\",\n    \"hooks_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/hooks\",\n    \"issue_events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/events{/number}\",\n    \"events_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/events\",\n    \"assignees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/assignees{/user}\",\n    \"branches_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/branches{/branch}\",\n    \"tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/tags\",\n    \"blobs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/blobs{/sha}\",\n    \"git_tags_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/tags{/sha}\",\n    \"git_refs_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/refs{/sha}\",\n    \"trees_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/trees{/sha}\",\n    \"statuses_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/statuses/{sha}\",\n    \"languages_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/languages\",\n    \"stargazers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/stargazers\",\n    \"contributors_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contributors\",\n    \"subscribers_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscribers\",\n    \"subscription_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/subscription\",\n    \"commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/commits{/sha}\",\n    \"git_commits_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/git/commits{/sha}\",\n    \"comments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/comments{/number}\",\n    \"issue_comment_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues/comments{/number}\",\n    \"contents_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/contents/{+path}\",\n    \"compare_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/compare/{base}...{head}\",\n    \"merges_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/merges\",\n    \"archive_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/{archive_format}{/ref}\",\n    \"downloads_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/downloads\",\n    \"issues_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/issues{/number}\",\n    \"pulls_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/pulls{/number}\",\n    \"milestones_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/milestones{/number}\",\n    \"notifications_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/notifications{?since,all,participating}\",\n    \"labels_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/labels{/name}\",\n    \"releases_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/releases{/id}\",\n    \"deployments_url\": \"https://github.example.com/api/v3/repos/_ORG_NAME/_REPO_NAME/deployments\",\n    \"created_at\": \"2017-11-05T00:58:54Z\",\n    \"updated_at\": \"2019-03-12T17:55:49Z\",\n    \"pushed_at\": \"2019-03-12T17:56:12Z\",\n    \"git_url\": \"git://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n    \"ssh_url\": \"git@github.example.com:_ORG_NAME/_REPO_NAME.git\",\n    \"clone_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME.git\",\n    \"svn_url\": \"https://github.example.com/_ORG_NAME/_REPO_NAME\",\n    \"homepage\": null,\n    \"size\": 79,\n    \"stargazers_count\": 0,\n    \"watchers_count\": 0,\n    \"language\": null,\n    \"has_issues\": true,\n    \"has_projects\": true,\n    \"has_downloads\": true,\n    \"has_wiki\": true,\n    \"has_pages\": false,\n    \"forks_count\": 2,\n    \"mirror_url\": null,\n    \"archived\": false,\n    \"open_issues_count\": 4,\n    \"license\": null,\n    \"forks\": 2,\n    \"open_issues\": 4,\n    \"watchers\": 0,\n    \"default_branch\": \"master\"\n  },\n  \"organization\": {\n    \"login\": \"_ORG_NAME\",\n    \"id\": 6582,\n    \"node_id\": \"MDEyOk9yZ2FuaXphdGlvbjY1ODI=\",\n    \"url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME\",\n    \"repos_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/repos\",\n    \"events_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/events\",\n    \"hooks_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/hooks\",\n    \"issues_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/issues\",\n    \"members_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/members{/member}\",\n    \"public_members_url\": \"https://github.example.com/api/v3/orgs/_ORG_NAME/public_members{/member}\",\n    \"avatar_url\": \"https://github.example.com/avatars/u/6582?\",\n    \"description\": \"\"\n  },\n  \"sender\": {\n    \"login\": \"_USER_NAME\",\n    \"id\": 8935,\n    \"node_id\": \"MDQ6VXNlcjg5MzU=\",\n    \"avatar_url\": \"https://github.example.com/avatars/u/8935?\",\n    \"gravatar_id\": \"\",\n    \"url\": \"https://github.example.com/api/v3/users/_USER_NAME\",\n    \"html_url\": \"https://github.example.com/_USER_NAME\",\n    \"followers_url\": \"https://github.example.com/api/v3/users/_USER_NAME/followers\",\n    \"following_url\": \"https://github.example.com/api/v3/users/_USER_NAME/following{/other_user}\",\n    \"gists_url\": \"https://github.example.com/api/v3/users/_USER_NAME/gists{/gist_id}\",\n    \"starred_url\": \"https://github.example.com/api/v3/users/_USER_NAME/starred{/owner}{/repo}\",\n    \"subscriptions_url\": \"https://github.example.com/api/v3/users/_USER_NAME/subscriptions\",\n    \"organizations_url\": \"https://github.example.com/api/v3/users/_USER_NAME/orgs\",\n    \"repos_url\": \"https://github.example.com/api/v3/users/_USER_NAME/repos\",\n    \"events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/events{/privacy}\",\n    \"received_events_url\": \"https://github.example.com/api/v3/users/_USER_NAME/received_events\",\n    \"type\": \"User\",\n    \"site_admin\": false,\n    \"ldap_dn\": \"_USER_LDAP_DN\"\n  }\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/githubTests/repos/v2/allParamsTrigger/concord.yml",
    "content": "flows:\n  onPush:\n    - log: \"onPush: ${event}\"\n\ntriggers:\n  - github:\n      entryPoint: \"onPush\"\n      version: 2\n      conditions:\n        type: \"push\"\n        githubOrg: \"devtools\"\n        githubRepo: \"concord\"\n        #      githubHost: \"\"\n        branch: \"master\"\n        sender: \"vasia\"\n\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/githubTests/repos/v2/anyRepoWithSender/concord.yml",
    "content": "flows:\n  onPush:\n    - log: \"onPush (author filter): ${event}\"\n\ntriggers:\n  - github:\n      entryPoint: \"onPush\"\n      version: 2\n      conditions:\n        githubOrg: \".*\"\n        githubRepo: \".*\"\n        type: \".*\"\n        sender: \".*some.*dude.*\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/githubTests/repos/v2/defaultTrigger/concord.yml",
    "content": "flows:\n  onPush:\n    - log: \"onPush: ${event}\"\n\ntriggers:\n  - github:\n      entryPoint: \"onPush\"\n      version: 2"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/githubTests/repos/v2/files/concord.yml",
    "content": "flows:\n  onPush:\n    - log: \"onPush: ${event}\"\n\ntriggers:\n  - github:\n      entryPoint: \"onPush\"\n      version: 2\n      conditions:\n        type: \"push\"\n        githubOrg: \"devtools\"\n        githubRepo: \"concord\"\n        branch: \"master\"\n        files:\n          modified:\n            - \"concord.yml\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/githubTests/repos/v2/groupByBranchTrigger/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n\nflows:\n  onPush:\n    - log: \"onPush: ${event}\"\n\ntriggers:\n  - github:\n      entryPoint: \"onPush\"\n      version: 2\n      conditions:\n        type: \"pull_request\"\n      exclusive:\n        groupBy: branch\n        mode: cancelOld"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/githubTests/repos/v2/groupByEventAttrTrigger/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n\nflows:\n  onPush:\n    - log: \"onPush: ${event}\"\n\ntriggers:\n  - github:\n      entryPoint: \"onPush\"\n      version: 2\n      conditions:\n        type: \"pull_request\"\n      exclusive:\n        groupBy: \"event.pull_request.head.ref\"\n        mode: cancelOld"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/githubTests/repos/v2/ignoreEmptyPushTrigger/concord.yml",
    "content": "flows:\n  onRegular:\n    - log: \"onRegular: ${event}\"\n\n  onEmpty:\n    - log: \"onEmpty: ${event}\"\n\ntriggers:\n  - github:\n      version: 2\n      entryPoint: \"onRegular\"\n      ignoreEmptyPush: true\n      conditions:\n        type: \"push\"\n\n  - github:\n      version: 2\n      entryPoint: \"onEmpty\"\n      ignoreEmptyPush: false\n      conditions:\n        type: \"push\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/githubTests/repos/v2/queryParams/concord.yml",
    "content": "flows:\n  onPush:\n    - log: \"Hello, ${event.queryParams.param1[0]}!\"\n\ntriggers:\n  - github:\n      entryPoint: \"onPush\"\n      version: 2\n      conditions:\n        type: push\n        queryParams:\n          param1: \"value1\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/githubTests/repos/v2/requestInfo/concord.yml",
    "content": "flows:\n  onPush:\n    - log: \"Hello, ${requestInfo.headers.Host}!\"\n\ntriggers:\n  - github:\n      entryPoint: \"onPush\"\n      version: 2"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/githubTests/repos/v2/useInitiatorTrigger/concord.yml",
    "content": "flows:\n  onPush:\n    - log: \"onPush: ${event}\"\n\ntriggers:\n  - github:\n      entryPoint: \"onPush\"\n      version: 2\n      useInitiator: true\n      conditions:\n        type: \"push\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/groovy/concord.yml",
    "content": "configuration:\n  dependencies:\n    - \"mvn://org.apache.groovy:groovy-all:pom:5.0.4\"\n\nflows:\n  default:\n    - script: groovy\n      body: |\n        def username = initiator.username;\n        println \"Hello, ${username}\";\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/httpFollowRedirects/concord.yml",
    "content": "flows:\n  default:\n  - task: http\n    in:\n      method: GET\n      url: ${url}\n      response: string\n      followRedirects: false\n      out: rsp\n\nconfiguration:\n  arguments:\n    url: \"http://localhost:8001\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/httpGet/concord.yml",
    "content": "flows:\n  default:\n  - try:\n    - task: http\n      in:\n        method: GET\n        url: ${url}\n        response: json\n        out: rsp\n    - log: \"Out Response: ${rsp.success}\"\n    error:\n    - log: \"Gotcha! ${lastError}\"\n\nconfiguration:\n  arguments:\n    url: \"http://localhost:8001\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/httpGetAsDefaultMethod/concord.yml",
    "content": "flows:\n  default:\n  - try:\n    - task: http\n      in:\n        url: ${url}\n        response: json\n        out: rsp\n    - log: \"Out Response: ${rsp.success}\"\n    error:\n    - log: \"Gotcha! ${lastError}\"\n\nconfiguration:\n  arguments:\n    url: \"http://localhost:8001\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/httpGetAsString/concord.yml",
    "content": "flows:\n  default:\n  - try:\n    - log: \"Url is: ${url}\"\n    - log: \"${http.asString( url )}\"\n    error:\n    - log: \"Gotcha! ${lastError}\"\n\nconfiguration:\n  arguments:\n    url: \"http://localhost:8001\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/httpGetEmpty/concord.yml",
    "content": "flows:\n  default:\n\n  - task: http\n    in:\n      method: GET\n      url: ${url}\n      response: string\n      out: rsp\n\n  - log: \"Response: ${rsp.success}\"\n  - log: \"Content is NULL: ${rsp.content == null}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/httpGetWithAuthUsingPassword/concord.yml",
    "content": "flows:\n  default:\n  - try:\n    - log: \"Url is: ${url}\"\n    - task: http\n      in:\n       method: GET\n       auth:\n        basic:\n          username: ${user}\n          password: ${password}\n       url: ${url}\n       response: \"json\"\n       out: rsp\n    - log: \"Out Response: ${rsp.success}\"\n    error:\n    - log: \"Gotcha! ${lastError}\"\n\nconfiguration:\n  arguments:\n    url: \"http://localhost:8001\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/httpGetWithAuthUsingToken/concord.yml",
    "content": "flows:\n  default:\n  - task: http\n    in:\n     method: GET\n     auth:\n      basic:\n        token: ${authToken}\n     url: ${url}\n     response: \"json\"\n     out: rsp\n  - log: \"Out Response: ${rsp.success}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/httpGetWithHeaders/concord.yml",
    "content": "flows:\n  default:\n\n  - task: http\n    in:\n      method: POST\n      url: ${url}\n      headers:\n        h1: \"v1\"\n        h2: \"v2\"\n      body:\n        obj:\n          nestedVar: 123\n      request: json\n      response: string\n      out: rsp\n  - log: \"Out Response: ${rsp.success}\"\n  - log: \"Response content: ${rsp.content}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/httpGetWithIgnoreErrors/concord.yml",
    "content": "flows:\n  default:\n  - task: http\n    in:\n     method: GET\n     auth:\n      basic:\n        username: ${user}\n        password: ${password}\n     url: ${url}\n     response: \"json\"\n     out: rsp\n     ignoreErrors: true\n\n  - log: \"statusCode: ${rsp.statusCode}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/httpGetWithInvalidUrl/concord.yml",
    "content": "flows:\n  default:\n  - task: http\n    in:\n     method: GET\n     url: \"http://dummy/server/get/request\"\n     response: \"json\"\n     out: rsp\n    error:\n      - ${log.error(\"server not exists\")}\n  - log: \"Out Response: ${rsp.success}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/httpGetWithQueryParams/concord.yml",
    "content": "flows:\n  default:\n  - try:\n    - task: http\n      in:\n        method: GET\n        url: ${url}\n        query:\n          message: \"hello concord!\"\n          multiValue:\n          - \"value1\"\n          - \"value2\"\n        response: json\n        out: rsp\n    - log: \"message: ${rsp.content.message}\"\n    - log: \"multi-value-1: ${rsp.content.multiValue1}\"\n    - log: \"multi-value-2: ${rsp.content.multiValue2}\"\n    error:\n    - log: \"Gotcha! ${lastError}\"\n\nconfiguration:\n  arguments:\n    url: \"http://localhost:8001\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/httpPatch/concord.yml",
    "content": "flows:\n  default:\n\n  - task: http\n    in:\n      method: PATCH\n      url: ${url}\n      auth:\n        basic:\n          username: ${user}\n          password: ${password}\n      body:\n        obj:\n          nestedVar: 123\n      request: json\n      response: json\n      out: rsp\n  - log: \"Out Response: ${rsp.success}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/httpPost/concord.yml",
    "content": "flows:\n  default:\n\n  - task: http\n    in:\n      method: POST\n      url: ${url}\n      auth:\n        basic:\n          username: ${user}\n          password: ${password}\n      body:\n        obj:\n          nestedVar: 123\n      request: json\n      response: json\n      out: rsp\n  - log: \"Out Response: ${rsp.success}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/httpPostArray/concord.yml",
    "content": "flows:\n  default:\n\n  - task: http\n    in:\n      method: POST\n      url: ${url}\n      auth:\n        basic:\n          username: ${user}\n          password: ${password}\n      body:\n        - key: value\n          one: two\n      request: json\n      response: json\n      out: rsp\n  - log: \"Out Response: ${rsp.success}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/httpPostWithAuthUsingToken/concord.yml",
    "content": "flows:\n  default:\n\n  - task: http\n    in:\n      method: POST\n      url: ${url}\n      auth:\n        basic:\n          token: ${authToken}\n      body:\n        obj:\n          nestedVar: 123\n      request: json\n      response: json\n      out: rsp\n  - log: \"Out Response: ${rsp.success}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/httpPostWithDebug/concord.yml",
    "content": "flows:\n  default:\n\n  - task: http\n    in:\n      method: POST\n      url: ${url}\n      auth:\n        basic:\n          username: ${user}\n          password: ${password}\n      body:\n        obj:\n          nestedVar: 123\n      request: json\n      response: json\n      debug: true\n      out: rsp\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/httpPostWithFormUrlEncoded/concord.yml",
    "content": "flows:\n  default:\n\n  - task: http\n    in:\n      method: POST\n      url: ${url}\n      request: form\n      body:\n        message: \"Hello Concord!\"\n      response: json\n      out: rsp\n  - log: \"Out Response: ${rsp.success}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/importATrigger/concord.yml",
    "content": "imports:\n  - git:\n      url: \"{{gitUrl}}\"\n\nflows:\n  onTrigger:\n    - log: \"Hello, ${name}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/initiator/_main.json",
    "content": "{\n  \"entryPoint\": \"main\"\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/initiator/processes/test.yml",
    "content": "main:\n- log: Hello, ${initiator.username}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/inject/.concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:example-tasks:PROJECT_VERSION\n  entryPoint: main\n  arguments:\n    myName: \"Concord\"\n\nflows:\n  main:\n  - ${example.hello(\"myName\")}\n\n  - set:\n      myName: \"world\"\n  - ${example.helloButLouder()}\n\n  - task: example\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/interpolation/_main.json",
    "content": "{\n  \"entryPoint\": \"main\",\n  \"arguments\": {\n    \"myName\": \"world\",\n    \"myRealName\": \"${myName}\"\n  }\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/interpolation/processes/test.yml",
    "content": "main:\n- ${execution.setVariable(\"name\", myRealName)}\n- log: \"Hello, ${name}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/invalidResourcesPath/concord.yml",
    "content": "resources:\n  flows:\n    - \"myFlows\"\n    - \"../../../etc/\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/invalidTriggers/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Hello, default\"\n\n  onValidTrigger:\n  - log: \"Hello, valid trigger\"\n\n  onInvalidTrigger:\n  - log: \"Oh no\"\n\ntriggers:\n  - testTrigger:\n      x: \"${invalid}\"\n      entryPoint: onInvalidTrigger\n\n  - testTrigger:\n      x: \"abc\"\n      entryPoint: onValidTrigger\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/invalidTriggersBrokenProcess/a/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Hello, default\"\n\n  onValidTrigger:\n  - log: \"Hello, valid trigger\"\n\ntriggers:\n  - testTrigger2:\n      entryPoint: onValidTrigger\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/invalidTriggersBrokenProcess/a/makepolicyfail.txt",
    "content": "this file should be rejected by the policy created in the test"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/invalidTriggersBrokenProcess/b/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Hello, default\"\n\n  onValidTrigger:\n  - log: \"Hello, valid trigger\"\n\ntriggers:\n  - testTrigger2:\n      entryPoint: onValidTrigger\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/invalidTriggersBrokenProcess/policy.json",
    "content": "{\n  \"file\": {\n    \"deny\": [\n      {\n        \"names\": [ \"makepolicyfail.txt\" ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/inventoryQuery/concord.yml",
    "content": "flows:\n  default:\n  - set:\n      result: ${inventory.query(inventoryName, queryName, null)}\n  - log: \"Inventory Item_data: ${result.get(0).data}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/jsonStoreTask/concord.yml",
    "content": "flows:\n  default:\n  - log: \"empty: ${jsonStore.get(storeName, 'xxx')}\"\n  - \"${jsonStore.put(storeName, 'test', {'x':1})}\"\n  - log: \"get: ${jsonStore.get(storeName, 'test')}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/jsonStoreTaskStoreTest/concord.yml",
    "content": "flows:\n  default:\n    - if: ${jsonStore.isStoreExists(storeName)}\n      then:\n        - throw: \"The store shouldn't exist at this point\"\n      else:\n        - log: \"OK: the store doesn't exist\"\n\n    - if: ${jsonStore.isExists(storeName, itemPath)}\n      then:\n        - throw: \"The store and the item shouldn't exist at this point\"\n      else:\n        - log: \"OK: the item doesn't exist\"\n\n    - \"${jsonStore.upsert(storeName, itemPath, {'test': 123})}\"\n\n    - if: ${jsonStore.isStoreExists(storeName)}\n      then:\n        - log: \"OK: the store exists now\"\n\n    - if: ${jsonStore.isExists(storeName, itemPath)}\n      then:\n        - log: \"OK: the item exists now\"\n\n    - log: \"item: ${jsonStore.get(storeName, itemPath)}\"\n\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/kvInc/processes/test.yml",
    "content": "main:\n  - expr: ${kv.inc(\"x\")}\n    out: x\n  - log: \"x=${x}\"\n\n  - if: ${kv.getString(testKey) == null}\n    then:\n      - log: \"abc123\"\n\n  - ${kv.putString(testKey, \"world\")}\n  - log: \"Hello, ${kv.getString(testKey)}\"\n\nverify:\n  - log: \"Hello again, ${kv.getString(testKey)}\"\n  - ${kv.remove(testKey)}\n\nverify2:\n  - if: ${kv.getString(testKey) == null}\n    then:\n      - log: \"xyz\"\n\ntestLong:\n  - log: \"x=${kv.incLong(testKey)}\"\n  - log: \"y=${kv.getLong(testKey)}\"\n  - log: \"a=${kv.inc(testKey)}\"\n  - log: \"b=${kv.getLong(testKey)}\"\n  - ${kv.putLong(testKey, 234)}\n  - log: \"c=${kv.getLong(testKey)}\"\n  - log: \"d=${kv.inc(testKey)}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/kvInvalidKeys/concord.yml",
    "content": "configuration:\n  arguments:\n    myKey: null\n    myValue: \"Hello!\"\n\nflows:\n  default:\n    - \"${kv.putString(myKey, myValue)}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/kvPolicy/concord.yml",
    "content": "configuration:\n  arguments:\n    keyStorePassword: crypto.decryptString(\"<somestring>\")\n\nprofiles:\n  pci:\n    configuration:\n      arguments:\n        keyStorePassword: ''\n\nflows:\n  default:\n    - log: \"Hello!\"\n    - ${kv.putLong('one', 123)}\n    - ${kv.putLong('one', 321)}\n    - ${kv.putLong('two', 444)}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/kvPolicy/test-policy-relaxed.json",
    "content": "{\n  \"kv\": {\n    \"maxEntries\": 2\n  }\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/kvPolicy/test-policy.json",
    "content": "{\n  \"kv\": {\n    \"maxEntries\": 1\n  }\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/kvScript/concord.yml",
    "content": "configuration:\n  dependencies:\n    - \"mvn://org.apache.groovy:groovy-all:pom:5.0.4\"\n\nflows:\n  default:\n    - script: groovy\n      body: |\n        def kv = tasks.get('kv');\n        kv.putString(execution, 'myKey', 'myValue');\n\n    - log: \"got myKey: ${kv.getString('myKey')}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/kvSpecialString/concord.yml",
    "content": "flows:\n  default:\n    - ${kv.putString(testKey, \"#aaa#bbb\")}\n    - log: ${kv.getString(testKey)}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ldapFormRunAs/concord.yml",
    "content": "flows:\n  default:\n    - form: myForm\n      fields:\n        - inputName: { label: \"name\", type: \"string\" }\n      runAs:\n        ldap:\n          - group: \"CN=${ldapGroupName},.*\"\n    - log: \"Submitted name: ${myForm.inputName}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/ldapInitiator/concord.yml",
    "content": "flows:\n  default:\n    - log: \"Hi, ${initiator}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/multiProjectTemplate/template/concord/test.yml",
    "content": "flows:\n  default:\n  - log: \"Hello, ${name}!\"\n\nconfiguration:\n  arguments:\n    name: \"${initiator.username}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/multiProjectTemplate/template/test.txt",
    "content": "Concord"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/multiProjectTemplate/user/concord.yml",
    "content": "configuration:\n  arguments:\n    name: \"${resource.asString('test.txt')}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/multipart/.concord.yml",
    "content": "flows:\n  main:\n  - log: \"x=${x}\"\n  - log: \"y=${y}\"\n  - log: \"z=${z}\"\n  - log: \"myfile=${resource.asString('myfile.txt')}\"\n\nvariables:\n  arguments:\n    x: 123"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/multipart/_main.json",
    "content": "{\n  \"arguments\": {\n    \"y\": \"abc\",\n    \"z\": \"n/a\"\n  }\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/multipleWithItems/concord.yml",
    "content": "flows:\n  default:\n  - call: myFlow\n    withItems:\n    - 1\n    - 2\n    - 3\n\n  - call: myFlow\n    withItems:\n    - 4\n    - 5\n    - 6\n\n  myFlow:\n  - log: \"${item}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/mvnDeps/concord.yml",
    "content": "flows:\n  default:\n  - script: groovy\n    body: |\n      println \"Hello, Concord!\"\n\nconfiguration:\n  dependencies:\n  - \"mvn://org.apache.groovy:groovy-all:pom:5.0.4\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/nodeRoster/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook.yml\n      inventory:\n        local:\n          hosts:\n            - \"${hostA}\"\n            - \"${hostB}\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        artifactUrl: \"${artifactUrl}\"\n        artifactDest: \"${workDir}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/nodeRoster/playbook.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - get_url:\n      url: \"{{ artifactUrl }}\"\n      dest: \"{{ artifactDest }}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/nodeRosterMultiFacts/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible\n    in:\n      playbook: playbook.yml\n      inventory:\n        groupA:\n          hosts:\n            - \"${host}\"\n          vars:\n            ansible_connection: \"local\"\n        groupB:\n          hosts:\n            - \"${host}\"\n          vars:\n            ansible_connection: \"local\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/nodeRosterMultiFacts/playbook.yml",
    "content": "---\n- hosts: groupA\n  tasks:\n    - debug:\n        msg: \"Hello!\"\n        verbosity: 0\n\n- hosts: groupB\n  tasks:\n    - debug:\n        msg: \"Hello!\"\n        verbosity: 0\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/nodeRosterTask/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:noderoster-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: nodeRoster\n    in:\n      action: hostsWithArtifacts\n      artifactPattern: \"${artifactUrl}\"\n\n  - log: \"${result}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/nonSerializableTest/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.it.tasks:serialization-test:PROJECT_VERSION\n\nflows:\n  default:\n    - task: serializationTest\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/onFailureDependencies/concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://org.apache.groovy:groovy-all:pom:5.0.4\"\n\nflows:\n  default:\n  - script: groovy\n    body: |\n      throw new RuntimeException(msg)\n\n  onFailure:\n  - script: groovy\n    body: |\n      println \"Hello!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/oneCheckpoint/concord.yml",
    "content": "configuration:\n  runner:\n    events:\n      recordTaskOutVars: true # test uses out vars to verify checkpoints\n\nflows:\n  default:\n    - log: \"aaa before\"\n    - checkpoint: \"one\"\n    - log: \"bbb after\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/oneopsTests/events/oneops_deployment_complete.json",
    "content": "{\n  \"cmsId\":2201338847,\n  \"severity\":\"info\",\n  \"type\":\"deployment\",\n  \"source\":\"deployment\",\n  \"subject\":\"myorgname/myassemblyname/prod : Deployment completed successfully.\",\n  \"text\":\"16 work-orders relying on instance(s) in bad state.\",\n  \"nsPath\":\"/myorgname/myassemblyname/prod/bom\",\n  \"payload\":{\n    \"deploymentState\":\"complete\",\n    \"updatedBy\":\"oneops-system\",\n    \"comments\":\"\",\n    \"ops\":\"16 work-orders relying on instance(s) in bad state.\",\n    \"createdBy\":\"admin\",\n    \"deploymentId\":\"2201338847\",\n    \"organization\":{\n      \"owner\":\"testuser@my.org\",\n      \"name\":\"myorgname\",\n      \"id\":2119985.0,\n      \"tags\":{\n        \"CCCID\":\"05-US.07479\",\n        \"pillar\":\"Test Pillar\",\n        \"VP\":\"Test User\",\n        \"dept\":\"05\",\n        \"costcenter\":\"07479\",\n        \"CTOdirect\":\"Test CTODirect\",\n        \"CTO\":\"Test CTO King\"\n      }\n    },\n    \"assembly\":{\n      \"owner\":\"testuser@my.org\",\n      \"name\":\"myassemblyname\",\n      \"id\":1.3101082E7,\n      \"tags\":{\n        \"trproductid\":\"9\"\n      }\n    }\n  },\n  \"timestamp\":1580163213384,\n  \"manifestCiId\":0,\n  \"cis\":[\n    {\n      \"nsId\":30819132,\n      \"ciAttributes\":{\n        \"metadata\":\"{\\\"owner\\\":\\\"testuser@my.org\\\",\\\"mgmt_url\\\":\\\"https://web.prod.oneops.com\\\",\\\"organization\\\":\\\"myorg\\\",\\\"assembly\\\":\\\"myassembly\\\",\\\"environment\\\":\\\"prod\\\",\\\"platform\\\":\\\"myplatformname\\\",\\\"component\\\":\\\"30799465\\\",\\\"instance\\\":\\\"412832054\\\",\\\"trproductid\\\":\\\"9\\\"}\",\n        \"allow_rules\":\"[\\\"-p tcp --dport 22\\\"]\",\n        \"public_ip\":\"10.36.78.212\",\n        \"iptables_enabled\":\"false\",\n        \"timezone\":\"UTC\",\n        \"allow_loopback\":\"true\",\n        \"additional_search_domains\":\"[]\",\n        \"private_ip\":\"10.1.1.1\",\n        \"applied_compliance\":\"{}\",\n        \"dns_record\":\"10.1.1.1\",\n        \"hostname\":\"myhostname\",\n        \"cores\":\"4\",\n        \"accelerated_flag\":\"false\",\n        \"drop_policy\":\"true\",\n        \"hypervisor\":\"myhypervisor-test.com\",\n        \"instance_state\":\"ACTIVE\",\n        \"require_public_ip\":\"false\",\n        \"deny_rules\":\"[]\",\n        \"osname\":\"redhat-6.9 Linux 2.6.32-696.28.1.el6.x86_64 #1 SMP Thu Apr 26 04:27:41 EDT 2018 x86_64 x86_64 GNU/Linux\",\n        \"server_image_name\":\"RHEL-6.9-x86_64 RC12\",\n        \"limits\":\"{}\",\n        \"ram\":\"11532\",\n        \"server_image_id\":\"6c4e169a-d56b-4d85-9322-87978d2af20a\",\n        \"dhclient\":\"false\",\n        \"availability_zone\":\"az2\",\n        \"instance_name\":\"my-instancename-prod-412832054\",\n        \"hosts\":\"{}\",\n        \"task_state\":\"\",\n        \"sshd_config\":\"{\\\"Ciphers\\\":\\\"aes128-ctr,aes192-ctr,aes256-ctr,arcfour256,arcfour128,arcfour\\\",\\\"Macs\\\":\\\"hmac-sha1,hmac-ripemd160\\\"}\",\n        \"repo_list\":\"[]\",\n        \"ostype\":\"redhat-6.9\",\n        \"proxy_map\":\"{}\",\n        \"host_id\":\"34bcaee707889792351b20f1aad4bb9d54683981834ae4da53c59d2f\",\n        \"tags\":\"{}\",\n        \"vm_state\":\"active\",\n        \"sysctl\":\"{\\\"net.ipv4.tcp_mem\\\":\\\"1529280 2039040 3058560\\\",\\\"net.ipv4.udp_mem\\\":\\\"1529280 2039040 3058560\\\",\\\"fs.file-max\\\":\\\"1611021\\\"}\",\n        \"instance_id\":\"e618a6e7-69a4-46bc-b975-ad2e6bc3762d\",\n        \"pam_groupdn\":\"\",\n        \"size\":\"L\",\n        \"image_id\":\"\",\n        \"nat_rules\":\"[]\"\n      },\n      \"attrProps\":{\n      },\n      \"altNs\":{\n      },\n      \"ciId\":412832054,\n      \"ciName\":\"compute-355303109-23\",\n      \"ciClassName\":\"bom.Compute\",\n      \"impl\":\"oo::chef-11.4.0\",\n      \"nsPath\":\"/myorgname/myassemblyname/prod/bom/myplatformname/1\",\n      \"ciGoid\":\"30819132-1243-412832054\",\n      \"ciState\":\"default\",\n      \"lastAppliedRfcId\":831828210,\n      \"createdBy\":\"mytestuser\",\n      \"updatedBy\":\"mytestuser:controller\",\n      \"created\":1580162840629,\n      \"updated\":1580162840629\n    }\n  ]\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/oneopsTests/events/oneops_deployment_qa.json",
    "content": "{\n  \"cmsId\":2201338847,\n  \"severity\":\"info\",\n  \"type\":\"deployment\",\n  \"source\":\"deployment\",\n  \"subject\":\"myorgname/myassemblyname/qa : Deployment completed successfully.\",\n  \"text\":\"16 work-orders relying on instance(s) in bad state.\",\n  \"nsPath\":\"/myorgname/myassemblyname/qa/bom\",\n  \"payload\":{\n    \"deploymentState\":\"complete\",\n    \"updatedBy\":\"oneops-system\",\n    \"comments\":\"\",\n    \"ops\":\"16 work-orders relying on instance(s) in bad state.\",\n    \"createdBy\":\"admin\",\n    \"deploymentId\":\"2201338847\",\n    \"organization\":{\n      \"owner\":\"testuser@my.org\",\n      \"name\":\"myorgname\",\n      \"id\":2119985.0,\n      \"tags\":{\n        \"CCCID\":\"05-US.07479\",\n        \"pillar\":\"Test Pillar\",\n        \"VP\":\"Test User\",\n        \"dept\":\"05\",\n        \"costcenter\":\"07479\",\n        \"CTOdirect\":\"Test CTODirect\",\n        \"CTO\":\"Test CTO King\"\n      }\n    },\n    \"assembly\":{\n      \"owner\":\"testuser@my.org\",\n      \"name\":\"myassemblyname\",\n      \"id\":1.3101082E7,\n      \"tags\":{\n        \"trproductid\":\"9\"\n      }\n    }\n  },\n  \"timestamp\":1580163213384,\n  \"manifestCiId\":0,\n  \"cis\":[\n    {\n      \"nsId\":30819132,\n      \"ciAttributes\":{\n        \"metadata\":\"{\\\"owner\\\":\\\"testuser@my.org\\\",\\\"mgmt_url\\\":\\\"https://web.qa.oneops.com\\\",\\\"organization\\\":\\\"myorg\\\",\\\"assembly\\\":\\\"myassembly\\\",\\\"environment\\\":\\\"qa\\\",\\\"platform\\\":\\\"myplatformname\\\",\\\"component\\\":\\\"30799465\\\",\\\"instance\\\":\\\"412832054\\\",\\\"trproductid\\\":\\\"9\\\"}\",\n        \"allow_rules\":\"[\\\"-p tcp --dport 22\\\"]\",\n        \"public_ip\":\"10.36.78.212\",\n        \"iptables_enabled\":\"false\",\n        \"timezone\":\"UTC\",\n        \"allow_loopback\":\"true\",\n        \"additional_search_domains\":\"[]\",\n        \"private_ip\":\"10.1.1.1\",\n        \"applied_compliance\":\"{}\",\n        \"dns_record\":\"10.1.1.1\",\n        \"hostname\":\"myhostname\",\n        \"cores\":\"4\",\n        \"accelerated_flag\":\"false\",\n        \"drop_policy\":\"true\",\n        \"hypervisor\":\"myhypervisor-test.com\",\n        \"instance_state\":\"ACTIVE\",\n        \"require_public_ip\":\"false\",\n        \"deny_rules\":\"[]\",\n        \"osname\":\"redhat-6.9 Linux 2.6.32-696.28.1.el6.x86_64 #1 SMP Thu Apr 26 04:27:41 EDT 2018 x86_64 x86_64 GNU/Linux\",\n        \"server_image_name\":\"RHEL-6.9-x86_64 RC12\",\n        \"limits\":\"{}\",\n        \"ram\":\"11532\",\n        \"server_image_id\":\"6c4e169a-d56b-4d85-9322-87978d2af20a\",\n        \"dhclient\":\"false\",\n        \"availability_zone\":\"az2\",\n        \"instance_name\":\"my-instancename-qa-412832054\",\n        \"hosts\":\"{}\",\n        \"task_state\":\"\",\n        \"sshd_config\":\"{\\\"Ciphers\\\":\\\"aes128-ctr,aes192-ctr,aes256-ctr,arcfour256,arcfour128,arcfour\\\",\\\"Macs\\\":\\\"hmac-sha1,hmac-ripemd160\\\"}\",\n        \"repo_list\":\"[]\",\n        \"ostype\":\"redhat-6.9\",\n        \"proxy_map\":\"{}\",\n        \"host_id\":\"34bcaee707889792351b20f1aad4bb9d54683981834ae4da53c59d2f\",\n        \"tags\":\"{}\",\n        \"vm_state\":\"active\",\n        \"sysctl\":\"{\\\"net.ipv4.tcp_mem\\\":\\\"1529280 2039040 3058560\\\",\\\"net.ipv4.udp_mem\\\":\\\"1529280 2039040 3058560\\\",\\\"fs.file-max\\\":\\\"1611021\\\"}\",\n        \"instance_id\":\"e618a6e7-69a4-46bc-b975-ad2e6bc3762d\",\n        \"pam_groupdn\":\"\",\n        \"size\":\"L\",\n        \"image_id\":\"\",\n        \"nat_rules\":\"[]\"\n      },\n      \"attrProps\":{\n      },\n      \"altNs\":{\n      },\n      \"ciId\":412832054,\n      \"ciName\":\"compute-355303109-23\",\n      \"ciClassName\":\"bom.Compute\",\n      \"impl\":\"oo::chef-11.4.0\",\n      \"nsPath\":\"/myorgname/myassemblyname/qa/bom/myplatformname/1\",\n      \"ciGoid\":\"30819132-1243-412832054\",\n      \"ciState\":\"default\",\n      \"lastAppliedRfcId\":831828210,\n      \"createdBy\":\"mytestuser\",\n      \"updatedBy\":\"mytestuser:controller\",\n      \"created\":1580162840629,\n      \"updated\":1580162840629\n    }\n  ]\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/oneopsTests/trigger/concord.yml",
    "content": "triggers:\n  - oneops:\n      org: \"myorgname\"\n      asm: \"myassemblyname\"\n      env: \"prod\"\n      platform: \"myplatformname\"\n      type: \"deployment\"\n      deploymentState: \"complete\"\n      entryPoint: oneopsTriggerFlow\n\n  - oneops:\n      version: 2\n      conditions:\n        org: \"myorgname\"\n        asm: \"myassemblyname\"\n        env: \"qa\"\n        platform: \"myplatformname\"\n        type: \"deployment\"\n        deploymentState: \"complete\"\n      entryPoint: oneopsTriggerFlowV2\n\nflows:\n  oneopsTriggerFlow:\n    - log: \"Oneops has completed a deployment trigger version 1\"\n\n  oneopsTriggerFlowV2:\n    - log: \"Oneops has completed a deployment trigger version 2\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/out/concord.yml",
    "content": "flows:\n  default:\n  - set:\n      x: 123\n      y:\n        some:\n          nested: [\"data\", \"in\", \"arrays\"]\n          boolean: true\n          number: 234\n\nprofiles:\n  predefinedOut:\n    configuration:\n      out: [\"x\"]\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/parentInstanceId/concord.yml",
    "content": "flows:\n  default:\n    - task: concord\n      in:\n        action: fork\n        entryPoint: onFork\n        sync: true\n\n    - task: concord\n      in:\n        action: start\n        payload: myPayload\n        sync: true\n\n  onFork:\n    - log: \"parentInstanceId: ${parentInstanceId}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/parentInstanceId/myPayload/concord.yml",
    "content": "flows:\n  default:\n    - log: \"parentInstanceId: ${parentInstanceId}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/policyCfg/concord.yml",
    "content": "flows:\n  default:\n  - log: \"${x.msg}, ${x.name}\"\n\nconfiguration:\n  arguments:\n    x:\n      msg: \"Hello\"\n      name: \"Stranger\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/portal/.concord.yml",
    "content": "flows:\n  main:\n    - log: \"Hello, ${name}\"\n    - log: \"x=${x}\"\n\nvariables:\n  arguments:\n    name: \"stranger\"\n    x: 0\n\nprofiles:\n  test1:\n    variables:\n      arguments:\n        name: \"world\"\n  test2:\n    variables:\n      arguments:\n        x: 123\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/principalPermission/concord.yml",
    "content": "flows:\n  default:\n\n  - task: concord\n    in:\n      action: start\n      payload: payload\n      sync: true\n      arguments:\n        inventoryName: ${inventoryName}\n        queryName: ${queryName}\n\n  - log: \"Done! \"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/principalPermission/payload/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Executing Inventory Query for Principal Permission Integration Test\"\n  - log: \"${inventory.query(inventoryName, queryName, null)}\"\n\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/process/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Hello!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/processContainer/concord.yml",
    "content": "configuration:\n  container:\n    image: \"%%image%%\"\n\nflows:\n  default:\n    - log: \"Hello from Docker!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/processCount/concord.yml",
    "content": "flows:\n  default:\n    - log: \"Hello!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/processLocks/concord.yml",
    "content": "flows:\n  default:\n  - ${lock.lock(\"my-lock\", \"ORG\")}\n  - log: \"locked!\"\n  - ${sleep.ms(10000)}\n  - ${lock.unlock(\"my-lock\", \"ORG\")}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/processMetadata/concord.yml",
    "content": "configuration:\n  meta:\n    x: 0\n    y: \"n/a\"\n    z: \"n/a\"\n\nflows:\n  default:\n    - set:\n        x: 123\n        y: \"xyz 234 abc\"\n        z: \"${myVar}\"\n    - log: \"Bye!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/processModeExec/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n\nflows:\n  default:\n    - log: \"Hello!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/processRbac/concord.yml",
    "content": "flows:\n  default:\n  - log: \"hello!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/processRequirements/concord.yml",
    "content": "configuration:\n  requirements:\n    agent:\n      type: \"t.*t\"\n\nprofiles:\n  invalidRegex:\n    configuration:\n      requirements:\n        agent:\n          type: \"*test*\"\n\nflows:\n  default:\n    - log: \"Hello from a process with requirements!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/processWithChildren/concord.yml",
    "content": "flows:\n  default:\n\n\n  - task: concord\n    in:\n      action: fork\n      tags: forkJoinChild\n      # disable the `onCancel` handler, because it's going to handle\n      # the parent's cancellation only\n      disableOnCancel: true\n      forks:\n      # spawn multiple jobs with different parameters\n      - entryPoint: aJob\n        arguments:\n          color: \"red\"\n      - entryPoint: aJob\n        arguments:\n          color: \"green\"\n      - entryPoint: aJob\n        arguments:\n            color: \"blue\"\n\n\n    # out variable \"myJobs\" will contain a list of process IDs\n    out:\n      myJobs: ${jobs}\n\n  - log: \"Done! Status of the jobs: ${concord.waitForCompletion(myJobs)}\"\n\n  aJob:\n  - log: \"FORK (${color}) starting...\"\n  - log: \"...done!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/project/processes/test.yml",
    "content": "main:\n- expr: ${log.info(\"test\", greeting)}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/project-commit-id/1/processes/test.yml",
    "content": "main:\n- expr: ${log.info(\"test-commit-1\", greeting)}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/project-commit-id/2/processes/test.yml",
    "content": "main:\n- expr: ${log.info(\"test-commit-2\", greeting)}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/project-triggers/concord.yml",
    "content": "flows:\n  onPush:\n  - log: \"onPush!\"\n\ntriggers:\n  - github:\n      entryPoint: onPush\n      conditions:\n        type: \"push\"\n        branch: \"foo\"\n  - github:\n      entryPoint: onPush\n      conditions:\n        type: \"push\"\n        branch: \"foo2\"\n  - oneops:\n      org: \"myOrg\"\n      entryPoint: onPush"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectEntryPoint/.concord.yml",
    "content": "flows:\n  main:\n  - log: \"Hello, Concord\"\n\nvariables:\n  entryPoint: main"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectInfo/concord.yml",
    "content": "flows:\n  default:\n  - log: \"${projectInfo}\"\n  - log: \"Org ID: ${projectInfo.orgId}\"\n  - log: \"Project ID: ${projectInfo.projectId}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectTask/concord.yml",
    "content": "flows:\n  default:\n  - task: project\n    in:\n      action: create\n      name: ${projectName}\n      repositories:\n      - name: ${repoName}\n        url: ${repoUrl}\n        branch: \"master\"\n        secretName: ${repoSecret}\n  - log: \"Done!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/altname/concord.yml",
    "content": "flows:\n  main:\n  - log: Hello, ${name}\n\nvariables:\n  entryPoint: main\n  arguments:\n    name: \"world\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/deps/.template.yml",
    "content": "flows:\n  main:\n  - ${example.hello()}\n\nvariables:\n  dependencies: [\"WILL_BE_REPLACED\"]\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/expr/.concord.yml",
    "content": "flows:\n  main:\n  - log: Hello, ${name}\n\nvariables:\n  arguments:\n    myName: \"world\"\n\nprofiles:\n  test:\n    variables:\n      arguments:\n        name: \"${myName}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/expr/_main.json",
    "content": "{\n  \"entryPoint\": \"main\",\n  \"activeProfiles\": [\"test\"]\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/expressionscript/concord.yml",
    "content": "configuration:\n  arguments:\n    scriptName: \"scripts/test.js\"\n\nflows:\n  default:\n  - script: \"${scriptName}\"\n  - log: bye!\n\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/expressionscript/scripts/test.js",
    "content": "print(\"hello!\");"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/externalprofile/.concord.yml",
    "content": "flows:\n  main:\n  - log: Hello, ${name}\n\nvariables:\n  entryPoint: main\n  arguments:\n    name: \"stranger\"\n\nprofiles:\n  notUsed:\n    variables:\n      arguments:\n        name: \"there\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/externalprofile/_main.json",
    "content": "{\n  \"activeProfiles\": [\"test\"]\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/externalprofile/profiles/another.yml",
    "content": "another:\n  variables:\n    arguments:\n      name: \"loser\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/externalprofile/profiles/test.yml",
    "content": "test:\n  variables:\n    arguments:\n      name: \"world\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/externalscript/.concord.yml",
    "content": "flows:\n  main:\n  - script: scripts/test.js\n  - log: bye!\n\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/externalscript/_main.json",
    "content": "{\n  \"entryPoint\": \"main\"\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/externalscript/scripts/test.js",
    "content": "print(\"hello!\");"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/overrideflow/.concord.yml",
    "content": "flows:\n  main:\n  - myOtherFlow\n\n  myOtherFlow:\n  - log: Bye, ${name}\n\nvariables:\n  arguments:\n    name: \"world\"\n\nprofiles:\n  test:\n    flows:\n      myOtherFlow:\n      - log: Hello, ${name}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/overrideflow/_main.json",
    "content": "{\n  \"entryPoint\": \"main\",\n  \"activeProfiles\": [\"test\"]\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/scriptWithErrorBlock/.concord.yml",
    "content": "configuration:\n  dependencies:\n  - \"mvn://org.apache.groovy:groovy-all:pom:5.0.4\"\n\nflows:\n  main:\n  - script: \"scripts/myscript.groovy\"\n    error:\n      - log: \"error occurred!!\"\n  - log: bye!\n\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/scriptWithErrorBlock/_main.json",
    "content": "{\n  \"entryPoint\": \"main\"\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/scriptWithErrorBlock/scripts/myscript.groovy",
    "content": "throw new RuntimeException(\"kaboom!\")"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/singleprofile/.concord.yml",
    "content": "flows:\n  main:\n  - log: Hello, ${name}\n  - log: ${nested.val}\n\nvariables:\n  arguments:\n    name: \"stranger\"\n    nested:\n      val: \"xyz12345abc\"\n\nprofiles:\n  test:\n    variables:\n      arguments:\n        name: \"world\"\n        nested:\n          val: \"xyz54321abc\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/singleprofile/_main.json",
    "content": "{\n  \"entryPoint\": \"main\",\n  \"activeProfiles\": [\"test\"]\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/singleprofilecfg/.concord.yml",
    "content": "flows:\n  main:\n  - log: Hello, ${name}\n  - log: ${nested.val}\n\nconfiguration:\n  arguments:\n    name: \"stranger\"\n    nested:\n      val: 12345\n\nprofiles:\n  test:\n    configuration:\n      arguments:\n        name: \"world\"\n        nested:\n          val: 54321\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/projectfile/singleprofilecfg/_main.json",
    "content": "{\n  \"entryPoint\": \"main\",\n  \"activeProfiles\": [\"test\"]\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/publicFlowsInProfiles/concord/a.yml",
    "content": "profiles:\n  profileA:\n    publicFlows:\n      - \"flowA\"\n\nflows:\n  flowA:\n    - log: \"Hello A!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/publicFlowsInProfiles/concord/b.yml",
    "content": "profiles:\n  profileB:\n    publicFlows:\n      - \"flowB\"\n\nflows:\n  flowB:\n    - log: \"Hello B!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/publicFlowsInProfiles/concord.yml",
    "content": "publicFlows:\n  - \"default\"\n\nflows:\n  default:\n    - call: \"flowA\"\n    - call: \"flowB\"\n    - call: \"flowC\"\n\n  flowC:\n    - log: \"Hello C!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/repositoryRefresh/concord.yml",
    "content": "flows:\n  default:\n  - task: repositoryRefresh\n    in:\n      repositoryInfo:\n        - repositoryId: \"b31b0b06-c33c-11e7-b0e9-8702fc03629f\"\n          repository: \"triggers\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/repositoryValidation/concord.yml",
    "content": "flows:\n  default:\n  - log: \"default flow...\"\n\n  onCronEvent:\n  - log: \"on Cron event\"\n\n  onGitEvent:\n  - log: \"on Git event\"\n\ntriggers:\n  - cron:\n      spec: 30 * * * *\n      entryPoint: onCronEvent\n\n  - github:\n      project: \"myProject\"\n      repository: \"myRepository\"\n      entryPoint: onGitEvent\n\n  - eventSource:\n      spec: \"specs\"\n      param1: \".*123.*\"\n      param2: false\n      param3: 123\n      entryPoint: default\n      arguments:\n         array:\n          - val1\n          - val2\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/repositoryValidationEmptyFlow/concord.yml",
    "content": "flows:\n  default:"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/repositoryValidationEmptyForm/concord.yml",
    "content": "flows:\n  default:\n  - log: \"default flow\"\n  - form: testForm\nforms:\n  testForm:"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/repositoryValidationTemplateRef/concord.yml",
    "content": "configuration:\n  template: \"{{ template }}\"\n\ntriggers:\n  - test:\n      entryPoint: \"fromTemplate\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/resolveOrder/concord.yml",
    "content": "flows:\n  default:\n  - log: \"sleep time: ${sleep}\"\n\nvariables:\n  arguments:\n    sleep: \"1 hour\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/resourcePrintJson/concord.yml",
    "content": "configuration:\n  meta:\n    condensedResult: ''\n    prettyResult: ''\n\nflows:\n  default:\n    - set:\n        m:\n          x: 123\n          y: \"hello\"\n    - set:\n        condensedResult: \"${resource.printJson(m)}\"\n        prettyResult: \"${resource.prettyPrintJson(m)}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/resourceReadAsJson/concord.yml",
    "content": "flows:\n  default:\n    - expr: ${resource.asJson('sample.json')}\n      out: jsonObj\n    - log: \"Hello ${jsonObj.name}!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/resourceReadAsJson/sample.json",
    "content": "{\n  \"name\": \"Concord\"\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/resourceReadAsString/concord.yml",
    "content": "flows:\n  default:\n    - log: ${resource.asString('sample.txt')}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/resourceReadAsString/sample.txt",
    "content": "Hello Concord!"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/resourceReadFromJsonString/concord.yml",
    "content": "flows:\n  default:\n    - set:\n        jsonString: '{\"name\":\"Concord\"}'\n    - expr: ${resource.fromJsonString(jsonString)}\n      out: jsonObj\n    - log: \"Hello ${jsonObj.name}!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/resourceWriteAsJson/concord.yml",
    "content": "flows:\n  default:\n    - set:\n       myObj:\n         name: Concord!\n\n    - expr: \"${resource.writeAsJson(myObj)}\"\n      out: path\n\n    - expr: ${resource.asJson(path)}\n      out: jsonObj\n\n    - log: \"Hello ${jsonObj.name}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/resourceWriteAsString/concord.yml",
    "content": "flows:\n  default:\n    - expr: ${resource.writeAsString('Hello Concord!')}\n      out: path\n\n    - log: \"${resource.asString(path)}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/resourceWriteAsYaml/concord.yml",
    "content": "flows:\n  default:\n    - set:\n       myObj:\n         name: Concord!\n\n    - expr: \"${resource.writeAsYaml(myObj)}\"\n      out: path\n\n    - expr: ${resource.asYaml(path)}\n      out: yamlObj\n\n    - log: \"Hello ${yamlObj.name}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/runAsMultipleUsers/concord.yml",
    "content": "configuration:\n  arguments:\n    testUser: \"\"\nflows:\n  default:\n    - log: \"${initiator} == ${currentUser}\"\n    - form: myForm\n      runAs:\n        username:\n         - \"admin\"\n         - ${testUser}\n        keep: true\n    - log: \"Now we are running as ${currentUser.username}\"\n\nforms:\n  myForm:\n  - firstName: { label: \"First name\", type: \"string\"}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/runAsPayload/concord.yml",
    "content": "flows:\n  default:\n    - log: \"AAA: ${currentUser.username}\"\n    - form: myForm\n      runAs:\n        username: \"${sudoUser}\"\n        keep: false\n    - log: \"BBB: ${myForm.msg}\"\n    - log: \"CCC: ${currentUser.username}\"\n\nforms:\n  myForm:\n  - msg: { type: \"string\" }\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/runas/concord.yml",
    "content": "flows:\n  default:\n    - log: \"${initiator} == ${currentUser}\"        # currentUser is the initiator\n    - form: myForm\n      runAs:\n        username: \"admin\"                        # optional\n  #      ldap:                                      # optional\n  #        group: \"CN=Admin,DN=Concord\"\n        keep: true                                 # continue the execution as the specified user\n    - log: \"Now we are running as ${currentUser.username}. Initiator: ${initiator.username}\"  # switched to admin\n\nforms:\n  myForm:\n  - firstName: { label: \"First name\", type: \"string\"}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/runnerEvents/concord.yml",
    "content": "configuration:\n  runner:\n    events:\n      recordTaskInVars: true\n\nflows:\n  default:\n    - task: log\n      in:\n        msg: \"Hello!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/secretProjects/concord.yml",
    "content": "flows:\n  default:\n      - log: \"${crypto.exportAsString(orgName, secretName, storePassword)}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/secretsTask/concord.yml",
    "content": "flows:\n  default:\n    - call: delete-secret\n    - call: validate-secret-NOT_FOUND\n    - call: create-secret\n    - call: update-secret-projects\n    - call: validate-secret\n    - call: get-secret\n    - call: validate-secret-INVALID_REQUEST\n    - call: update-secret-password\n    - call: update-secret-data\n    - call: get-secret-again\n    - call: delete-secret\n    - call: update-secret-create\n    - call: delete-secret2\n\n  create-secret:\n    - log: \"Create secret\"\n    - task: concordSecrets\n      in:\n        action: create\n        name: ${secretName}\n        data: anders-test-value\n        storePassword: Dingo1234\n        projects:\n          - \"${projectName1}\"\n          - \"${projectName2}\"\n    - log: \"Create Result: ${result}\"\n\n  get-secret:\n    - log: \"Get secret\"\n    - task: concordSecrets\n      in:\n        name: ${secretName}\n        storePassword: Dingo1234\n    - log: \"Get Result: ${result}\"\n    - if: ${result.data != \"anders-test-value\"}\n      then:\n        - throw: \"Expected anders-test-value, got ${result}\"\n\n  validate-secret:\n    - log: \"Validate secret\"\n    - task: concordSecrets\n      in:\n        action: getAsString\n        name: ${secretName}\n        storePassword: Dingo1234\n    - log: \"Validate Result: ${result}\"\n    - if: ${result.status != \"OK\"}\n      then:\n        - throw: \"Expected 'OK', got ${result}\"\n\n  validate-secret-NOT_FOUND:\n    - log: \"Validate secret NOT_FOUND\"\n    - task: concordSecrets\n      in:\n        action: getAsString\n        name: anders-NOT_FOUND-secret\n        ignoreErrors: true\n    - log: \"Validate Result: ${result}\"\n    - if: ${result.status != \"NOT_FOUND\"}\n      then:\n        - throw: \"Expected 'NOT_FOUND', got ${result}\"\n\n  validate-secret-INVALID_REQUEST:\n    - log: \"Validate secret INVALID_REQUEST\"\n    - task: concordSecrets\n      in:\n        action: getAsString\n        name: ${secretName}\n        storePassword: wrong\n        ignoreErrors: true\n    - log: \"Validate Result: ${result}\"\n    - if: ${result.status != \"INVALID_REQUEST\"}\n      then:\n        - throw: \"Expected 'INVALID_REQUEST', got ${result}\"\n\n  update-secret-password:\n    - log: \"Update secret password\"\n    - task: concordSecrets\n      in:\n        action: update\n        name: ${secretName}\n        storePassword: Dingo1234\n        newStorePassword: Tapir1234\n\n  update-secret-data:\n    - log: \"Update secret data\"\n    - task: concordSecrets\n      in:\n        action: update\n        name: ${secretName}\n        storePassword: Tapir1234\n        data: anders-test-elephant\n\n  get-secret-again:\n    - log: \"Get secret (with new password and data)\"\n    - task: concordSecrets\n      in:\n        name: ${secretName}\n        storePassword: Tapir1234\n    - log: \"Get Result: ${result}\"\n    - if: ${result.data != \"anders-test-elephant\"}\n      then:\n        - throw: \"Expected anders-test-elephant, got ${result}\"\n\n  delete-secret:\n    - log: \"Delete secret\"\n    - task: concordSecrets\n      in:\n        action: delete\n        name: ${secretName}\n        ignoreErrors: true\n\n  update-secret-create:\n    - log: \"Update secret create\"\n    - task: concordSecrets\n      in:\n        action: update\n        name: ${secretName}2\n        storePassword: Tapir1234\n        data: anders-test-elephant\n        createIfMissing: true\n\n  delete-secret2:\n    - log: \"Delete secret2\"\n    - task: concordSecrets\n      in:\n        action: delete\n        name: ${secretName}2\n\n  update-secret-projects:\n    - log: \"Update secret projects\"\n    - task: concordSecrets\n      in:\n        action: update\n        name: \"${secretName}\"\n        projects:\n          - \"${projectName1}\"\n\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/serialization/.concord.yml",
    "content": "configuration:\n  dependencies:\n    - \"mvn://org.apache.groovy:groovy-all:pom:5.0.4\"\n    - \"mvn://com.walmartlabs.concord.plugins.basic:example-tasks:PROJECT_VERSION\"\n\nflows:\n  default:\n    - script: groovy\n      body: |\n        import com.walmartlabs.concord.plugins.example.ExampleBean\n        execution.setVariable(\"x\", new ExampleBean(\"hello\"))\n\n    - form: myForm\n\n    - log: \"${myForm.y}\"\n\nforms:\n  myForm:\n    - y: { type: \"string\" }\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/sessionToken/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n\nflows:\n  default:\n    - task: http\n      in:\n        method: GET\n        url: ${baseUrl.replace('://', '://' += ${processInfo.sessionToken})}/api/v1/process/${txId}\n      out: result\n\n    - log: \"${result}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/sessionTokenAsUsername/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\nflows:\n  default:\n    - script: js\n      body: |\n        const Base64 = Java.type('java.util.Base64');\n        const auth = processInfo.sessionToken + \":\";\n        result.set('auth', Base64.getEncoder().withoutPadding().encodeToString(auth.getBytes()));\n      out:\n        auth: ${result.auth}\n\n    - task: http\n      in:\n        method: GET\n        headers:\n          Authorization: \"Basic ${auth}\"\n        url: ${targetUrl}\n        ignoreErrors: false\n        response: json\n      out: response\n\n    - log: \"statusCode=${response.statusCode}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/setVar/concord.yml",
    "content": "flows:\n  default:\n  - set:\n      shouldBeNull: null\n      nested:\n        var: \"nested.var\"\n\n  - log: \"shouldBeNull: ${shouldBeNull}\"\n  - log: \"nested.var: ${nested.var}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/setVarNested/concord.yml",
    "content": "configuration:\n  arguments:\n    obj:\n      x: 123\nflows:\n  default:\n    - set:\n        obj.name: \"Concord\"\n        obj.msg: \"Hello, ${obj.name}\"\n\n    - log: \"obj.x: ${obj.x}\"\n    - log: \"obj.name: ${obj.name}\"\n    - log: \"obj.msg: ${obj.msg}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/setVarNested2/concord.yml",
    "content": "configuration:\n  arguments:\n    event:\n      branch: 'master'\n\nflows:\n  default:\n    - set:\n        commitEvent:\n          event: 'push'\n          branch: '${event.branch}'\n\n    - log: \"event: ${event}\"\n    - log: \"commitEvent.event: ${commitEvent.event}\"\n    - log: \"commitEvent.branch: ${commitEvent.branch}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/simple/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Hello!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/smtp/_main.json",
    "content": "{\n  \"dependencies\": [\n    \"mvn://com.walmartlabs.concord.plugins.basic:smtp-tasks:PROJECT_VERSION\"\n  ],\n  \"entryPoint\": \"main\",\n  \"arguments\": {\n    \"mailParams\": {\n      \"from\": \"me@localhost\",\n      \"to\": \"you@localhost\",\n      \"subject\": \"test\",\n      \"template\": \"test.mustache\"\n    }\n  }\n}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/smtp/processes/main.yml",
    "content": "main:\n  - smtp: [ \"${smtpParams}\", \"${mailParams}\" ]\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/smtp/test.mustache",
    "content": "hi!"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/stateSingleFile/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Hello!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/stateSingleFile/dir/test.txt",
    "content": "123"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/suspend/_main.json",
    "content": "{\n  \"entryPoint\": \"main\"\n}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/suspend/processes/main.yml",
    "content": "main:\n  - ${log.info(\"test\", \"aaaa\")}\n  - event: ev1\n  - ${log.info(\"test\", \"bbbb\")}\n  - ${log.info(\"test\", testValue)}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/suspendForCompletion/concord.yml",
    "content": "flows:\n  default:\n  - set:\n      children: []\n\n  - task: concord\n    in:\n      action: start\n      payload: payload\n\n  - ${children.addAll(jobs)}\n\n  - task: concord\n    in:\n      action: start\n      payload: payload\n\n  - ${children.addAll(jobs)}\n\n  - log: \"children: ${children}\"\n\n  - ${concord.suspendForCompletion(children)}\n\n  - log: \"process is resumed.\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/suspendForCompletion/payload/concord.yml",
    "content": "flows:\n  default:\n  - ${sleep.ms(3000)}\n  - log: \"Hello from child process.\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/suspendForForkedProcesses/concord.yml",
    "content": "flows:\n  default:\n  - task: concord\n    in:\n      action: fork\n      forks:\n        - entryPoint: sayHello\n        - entryPoint: sayHello\n        - entryPoint: sayHello\n      suspend: true\n      sync: true\n  - log: \"task completed\"\n  - log: \"jobs ${jobs}\"\n\n  sayHello:\n    - log: \"Hello from a subprocess!\"\n    - ${sleep.ms(3000)}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/suspendTask/concord.yml",
    "content": "flows:\n  default:\n  - task: suspendTest\n  - log: \"Whoa!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/taskOut/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:example-tasks:PROJECT_VERSION\n  entryPoint: main\n\nflows:\n  main:\n  - task: example\n    out:\n      msg: ${exampleOutput}\n  - log: \"I said: ${msg}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/taskRetry/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible2\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: local\n    retry:\n      in:\n        retry: true\n        extraVars:\n          msg: \"Hi retry!\"\n      times: 3\n      delay: 2\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/taskRetry/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ msg }}\"\n      verbosity: 0"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/taskRetryWithExpression/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - task: ansible2\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n            - \"127.0.0.1\"\n          vars:\n            ansible_connection: local\n    retry:\n      in:\n        retry: true\n        extraVars:\n          msg: \"Hi retry!\"\n      times: ${retryCount}\n      delay: ${retryDelay}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/taskRetryWithExpression/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ msg }}\"\n      verbosity: 0"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/templateMerge/process/.concord.yml",
    "content": "flows:\n  main:\n  - log: \"Hello, ${name}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/templateMerge/template/_main.js",
    "content": "({\n    entryPoint: \"main\",\n    arguments: {\n        name: \"Concord\"\n    }\n})"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/templateMerge/template/flows/main.yml",
    "content": "main:\n- log: \"Bye, ${name}\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/testTrigger/concord.yml",
    "content": "triggers:\n  - test:\n      entryPoint: onTrigger\n      arguments:\n        name: \"Concord\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/throwExceptionMessage/concord.yml",
    "content": "flows:\n  default:\n  - try:\n    - log: \"Running the default flow...\"\n    - expr: ${myTask.doSomethingDangerous()}\n    error:\n    - throw: \"Kaboom!! Error occurred.\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/throwExceptionTask/concord.yml",
    "content": "flows:\n  default:\n  - try:\n    - log: \"Running the default flow...\"\n    - expr: ${misc.throwBpmnError('Catch that!')}\n    error:\n    - throw: ${lastError}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/timeout/_main.json",
    "content": "{\n  \"entryPoint\": \"main\"\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/timeout/processes/test.yml",
    "content": "main:\n- expr: ${sleep.ms(15000)}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/timeoutHandling/.concord.yml",
    "content": "configuration:\n  processTimeout: \"PT1S\"\n  handlerProcessTimeout: \"PT1M\"\n  entryPoint: main\n\nflows:\n  main:\n  - expr: ${sleep.ms(15000)}\n\n  onTimeout:\n    - log: \"txId: ${txId}\"\n    - log: \"projectInfo: ${projectInfo}\"\n    - log: \"processInfo: ${processInfo}\"\n    - log: \"initiator: ${initiator}\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/triggerActiveProfiles/concord.yml",
    "content": "flows:\n  default:\n  - log: \"whoops\"\n\n  onTrigger:\n  - log: \"${msg}, ${name}\"\n\n  onInvalidTrigger:\n  - log: \"Oh no\"\n\ntriggers:\n  - testTrigger:\n      entryPoint: onTrigger\n      activeProfiles:\n      - testProfile\n      arguments:\n        name: \"Concord\"\n\nprofiles:\n  testProfile:\n    configuration:\n      arguments:\n        msg: \"Hello\"\n        name: \"stranger\"\n\nconfiguration:\n  arguments:\n    msg: \"Bye\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/triggerRepo/concord.yml",
    "content": "flows:\n  default:\n  - log: \"default\"\n\n  onTrigger:\n  - log: \"onTrigger\"\n\ntriggers:\n  - testTrigger:\n      entryPoint: onTrigger\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/triggerRepo/new_concord.yml",
    "content": "flows:\n  default:\n  - log: \"default\"\n\n  onTrigger:\n  - log: \"onTrigger\"\n\n  onTrigger2:\n  - log: \"onTrigger2\"\n\ntriggers:\n  - testTrigger:\n      entryPoint: onTrigger\n  - testTrigger:\n      entryPoint: onTrigger2\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/twoAnsible/concord.yml",
    "content": "configuration:\n  dependencies:\n    - mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:PROJECT_VERSION\n\nflows:\n  default:\n  - call: runAnsible\n    in:\n      msg: \"Hello!\"\n  - call: runAnsible\n    in:\n      msg: \"Bye-bye!\"\n\n  runAnsible:\n  - task: ansible\n    in:\n      playbook: playbook/hello.yml\n      inventory:\n        local:\n          hosts:\n          - \"127.0.0.1\"\n          vars:\n            ansible_connection: \"local\"\n      extraVars:\n        msg: ${msg}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/twoAnsible/playbook/hello.yml",
    "content": "---\n- hosts: local\n  tasks:\n  - debug:\n      msg: \"{{ msg }}\"\n      verbosity: 0"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/unknownFlavor/concord.yml",
    "content": "flows:\n  default:\n    - log: \"Hello!\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/variables/.concord.yml",
    "content": "flows:\n  main:\n  - set:\n      nested.p: false\n      nested.q: \"abc\"\n      var1: \"var1-value\"\n  - log: \"x=${nested.x}\"\n  - log: \"y=${nested.y}\"\n  - log: \"z=${nested.z}\"\n  - log: \"nested.p=${nested.p}\"\n  - log: \"nested.q=${nested.q}\"\n  - log: \"var1=${var1}\"\n  - log: \"Hello, ${name}\"\n\nvariables:\n  arguments:\n    nested:\n      z: false\n      p: true\n    name: \"stranger\""
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/variables/_main.json",
    "content": "{\n  \"entryPoint\": \"main\",\n  \"arguments\": {\n    \"nested\": {\n      \"x\": 123,\n      \"y\": \"abc\"\n    },\n    \"name\": \"world\"\n  }\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/withDelay/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Hello, Concord!\"\n  - ${sleep.ms(delayValue)}\n\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/withForm/concord.yml",
    "content": "flows:\n  default:\n    - ${sleep.ms(delayValue)}\n    - form: myForm\n    - log: \"${myForm.name}\"\n\nforms:\n  myForm:\n    - name: {type: \"string\"}\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/workspacePolicy/concord.yml",
    "content": "flows:\n  default:\n  - log: \"Hello!\"\n"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/workspacePolicy/test-policy-relaxed.json",
    "content": "{\n  \"workspace\": {\n    \"maxSizeInBytes\": 1000000\n  }\n}"
  },
  {
    "path": "it/server/src/test/resources/com/walmartlabs/concord/it/server/workspacePolicy/test-policy.json",
    "content": "{\n  \"workspace\": {\n    \"maxSizeInBytes\": 10\n  }\n}"
  },
  {
    "path": "it/server/src/test/resources/default_vars.yml",
    "content": "configuration:\n  arguments:\n    testDefaultVars:\n      var1: value1\n      var2: value2"
  },
  {
    "path": "it/server/src/test/resources/logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] [%-5level] %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"WARN\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "it/server/src/test/resources/mvn.json",
    "content": "{\n  \"repositories\": [\n    {\n      \"id\": \"host\",\n      \"url\": \"file:///host/.m2/repository\"\n    }\n  ]\n}\n"
  },
  {
    "path": "it/server/src/test/resources/server.conf",
    "content": "concord-server {\n    db {\n        changeLogParameters {\n            defaultAdminToken = \"cTFxMXExcTE=\"\n            defaultAgentToken = \"cTJxMnEycTI=\"\n        }\n    }\n\n    secretStore {\n        serverPassword = \"aGVsbG93b3JsZA==\"\n        secretStoreSalt = \"aGVsbG93b3JsZA==\"\n        projectSecretSalt = \"aGVsbG93b3JsZA==\"\n    }\n\n    queue {\n        enqueuePollInterval = \"250 milliseconds\"\n        dispatcher {\n            pollDelay = \"250 milliseconds\"\n        }\n    }\n\n    github {\n        secret = \"12345\"\n        useSenderLdapDn = true\n        disableReposOnDeletedRef = true\n    }\n\n    ldap {\n        searchBase = \"dc=example,dc=org\"\n        principalSearchFilter = \"(cn={0})\"\n        userSearchFilter = \"(cn=*{0}*)\"\n        returningAttributes = [\"cn\",\"memberof\",\"objectClass\",\"sn\",\"uid\"]\n        usernameProperty = \"cn\"\n        mailProperty = \"mail\"\n        systemUsername = \"cn=admin,dc=example,dc=org\"\n        systemPassword = \"admin\"\n    }\n\n    process {\n        checkLogPermissions = true\n    }\n}\n"
  },
  {
    "path": "it/tasks/broken-deps/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.it</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <groupId>com.walmartlabs.concord.it.tasks</groupId>\n    <artifactId>broken-deps</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <!-- use an old bpm engine version to reproduce the conflicting dependency issue -->\n        <dependency>\n            <groupId>io.takari.bpm</groupId>\n            <artifactId>bpm-engine-impl</artifactId>\n            <version>0.10.0</version>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "it/tasks/broken-deps/src/main/java/com/walmartlabs/concord/it/tasks/brokendeps/BrokenDepsTask.java",
    "content": "package com.walmartlabs.concord.it.tasks.brokendeps;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.Task;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Named;\n\n@Named(\"brokenDeps\")\npublic class BrokenDepsTask implements Task {\n\n    private static final Logger log = LoggerFactory.getLogger(BrokenDepsTask.class);\n\n    @Override\n    public void execute(Context ctx) throws Exception {\n        log.info(\"hello!\");\n    }\n}\n"
  },
  {
    "path": "it/tasks/dependency-manager-test/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.it</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <groupId>com.walmartlabs.concord.it.tasks</groupId>\n    <artifactId>dependency-manager-test</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "it/tasks/dependency-manager-test/src/main/java/com/walmartlabs/concord/it/tasks/dependencymanagertest/DependencyManagerTestTask.java",
    "content": "package com.walmartlabs.concord.it.tasks.dependencymanagertest;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.ContextUtils;\nimport com.walmartlabs.concord.sdk.DependencyManager;\nimport com.walmartlabs.concord.sdk.Task;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\n@Named(\"dependencyManagerTest\")\npublic class DependencyManagerTestTask implements Task {\n\n    private static final Logger log = LoggerFactory.getLogger(DependencyManagerTestTask.class);\n\n    private final DependencyManager dependencyManager;\n\n    @Inject\n    public DependencyManagerTestTask(DependencyManager dependencyManager) {\n        this.dependencyManager = dependencyManager;\n    }\n\n    @Override\n    public void execute(Context ctx) throws Exception {\n        String url = ContextUtils.assertString(ctx, \"url\");\n        log.info(\"Fetching {}...\", url);\n\n        // check if it actually returns the same artifact\n        Path p1 = dependencyManager.resolve(URI.create(url));\n        Path p2 = dependencyManager.resolve(URI.create(url));\n        if (!p1.equals(p2)) {\n            throw new RuntimeException(\"Got different results: \\n\" + p1 + \"\\n\" + p2);\n        }\n\n        String s = new String(Files.readAllBytes(p2));\n        log.info(\"Got: {}\", s);\n    }\n}\n"
  },
  {
    "path": "it/tasks/schema-test/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.it</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <groupId>com.walmartlabs.concord.it.tasks</groupId>\n    <artifactId>schema-test</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "it/tasks/schema-test/src/main/java/com/walmartlabs/concord/it/tasks/schematest/InvalidSchemaTask.java",
    "content": "package com.walmartlabs.concord.it.tasks.schematest;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Named;\n\n@Named(\"invalidSchema\")\npublic class InvalidSchemaTask implements Task {\n\n    private static final Logger log = LoggerFactory.getLogger(InvalidSchemaTask.class);\n\n    @Override\n    public TaskResult execute(Variables input) {\n        log.info(\"InvalidSchemaTask executed\");\n        return TaskResult.success();\n    }\n}\n"
  },
  {
    "path": "it/tasks/schema-test/src/main/java/com/walmartlabs/concord/it/tasks/schematest/SchemaTestTask.java",
    "content": "package com.walmartlabs.concord.it.tasks.schematest;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Named;\n\n/**\n * Test task for schema validation integration tests.\n */\n@Named(\"schemaTest\")\npublic class SchemaTestTask implements Task {\n\n    private static final Logger log = LoggerFactory.getLogger(SchemaTestTask.class);\n\n    @Override\n    public TaskResult execute(Variables input) {\n        String message = input.assertString(\"message\");\n        int count = input.getInt(\"count\", 0);\n        boolean badOutput = input.getBoolean(\"badOutput\", false);\n\n        log.info(\"SchemaTestTask: message={}, count={}\", message, count);\n\n        if (badOutput) {\n            // Return output missing required \"echo\" field to trigger output validation failure\n            return TaskResult.success()\n                    .value(\"count\", count);\n        }\n\n        return TaskResult.success()\n                .value(\"echo\", message)\n                .value(\"count\", count);\n    }\n}\n"
  },
  {
    "path": "it/tasks/schema-test/src/main/resources/com/walmartlabs/concord/it/tasks/schematest/invalidSchema.schema.json",
    "content": "{ \"in\":\n"
  },
  {
    "path": "it/tasks/schema-test/src/main/resources/com/walmartlabs/concord/it/tasks/schematest/schemaTest.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"SchemaTest Task Schema\",\n  \"description\": \"Schema for the schemaTest task used in integration tests\",\n  \"in\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"message\": {\n        \"type\": \"string\",\n        \"minLength\": 1,\n        \"description\": \"The message to echo back\"\n      },\n      \"count\": {\n        \"type\": \"integer\",\n        \"minimum\": 0,\n        \"description\": \"A count value\"\n      }\n    },\n    \"required\": [\"message\"],\n    \"additionalProperties\": true\n  },\n  \"out\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"ok\": {\n        \"type\": \"boolean\",\n        \"description\": \"Whether the task succeeded\"\n      },\n      \"echo\": {\n        \"type\": \"string\",\n        \"description\": \"The echoed message\"\n      },\n      \"count\": {\n        \"type\": \"integer\",\n        \"description\": \"The count value\"\n      }\n    },\n    \"required\": [\"ok\", \"echo\"]\n  }\n}\n"
  },
  {
    "path": "it/tasks/serialization-test/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.it</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <groupId>com.walmartlabs.concord.it.tasks</groupId>\n    <artifactId>serialization-test</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "it/tasks/serialization-test/src/main/java/com/walmartlabs/concord/it/tasks/serializationtest/CustomBean.java",
    "content": "package com.walmartlabs.concord.it.tasks.serializationtest;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\n\n/**\n * A custom class to test checkpointing with \"3rd party\" classes.\n */\npublic class CustomBean implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String value;\n\n    public CustomBean(String value) {\n        this.value = value;\n    }\n\n    public String getValue() {\n        return value;\n    }\n}\n"
  },
  {
    "path": "it/tasks/serialization-test/src/main/java/com/walmartlabs/concord/it/tasks/serializationtest/CustomBeanTask.java",
    "content": "package com.walmartlabs.concord.it.tasks.serializationtest;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport javax.inject.Named;\n\n/**\n * A custom task to test 3rd party classes being used as flow variables.\n */\n@Named(\"customBean\")\npublic class CustomBeanTask implements Task {\n\n    @Override\n    public TaskResult execute(Variables input) throws Exception {\n        return TaskResult.success()\n                .value(\"value\", new CustomBean(input.assertString(\"msg\")));\n    }\n}\n"
  },
  {
    "path": "it/tasks/serialization-test/src/main/java/com/walmartlabs/concord/it/tasks/serializationtest/NonSerializableThingy.java",
    "content": "package com.walmartlabs.concord.it.tasks.serializationtest;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class NonSerializableThingy {\n\n    private final String value;\n\n    public NonSerializableThingy(String value) {\n        this.value = value;\n    }\n\n    public String getValue() {\n        return value;\n    }\n}\n"
  },
  {
    "path": "it/tasks/serialization-test/src/main/java/com/walmartlabs/concord/it/tasks/serializationtest/SerializationTestTask.java",
    "content": "package com.walmartlabs.concord.it.tasks.serializationtest;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.Task;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Named;\n\n@Named(\"serializationTest\")\npublic class SerializationTestTask implements Task {\n\n    private static final Logger log = LoggerFactory.getLogger(SerializationTestTask.class);\n\n    @Override\n    public void execute(Context ctx) {\n        log.info(\"Setting a variable...\");\n        ctx.setVariable(\"test\", new NonSerializableThingy(\"Hello!\"));\n    }\n}\n"
  },
  {
    "path": "it/tasks/suspend-test/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.it</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <groupId>com.walmartlabs.concord.it.tasks</groupId>\n    <artifactId>suspend-test</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "it/tasks/suspend-test/src/main/java/com/walmartlabs/concord/it/tasks/suspendtest/SuspendTestTask.java",
    "content": "package com.walmartlabs.concord.it.tasks.suspendtest;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.Task;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Named;\n\n@Named(\"suspendTest\")\npublic class SuspendTestTask implements Task {\n\n    private static final Logger log = LoggerFactory.getLogger(SuspendTestTask.class);\n\n    @Override\n    public void execute(Context ctx) throws Exception {\n        log.info(\"Requesting suspend...\");\n        String eventName = (String) ctx.getVariable(\"eventName\");\n        ctx.suspend(eventName);\n    }\n}\n"
  },
  {
    "path": "it/testing-server/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.it</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>testing-concord-server</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <configuration>\n                    <excludes>\n                        <exclude>**/*IT.java</exclude>\n                    </excludes>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-failsafe-plugin</artifactId>\n                <configuration>\n                    <skipTests>${skip.it.tests}</skipTests>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server</artifactId>\n            <exclusions>\n                <exclusion>\n                    <groupId>com.walmartlabs.concord.server.plugins.ansible</groupId>\n                    <artifactId>concord-ansible-plugin</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>com.walmartlabs.concord.server.plugins</groupId>\n                    <artifactId>concord-oneops-plugin</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>com.walmartlabs.concord.server.plugins.noderoster</groupId>\n                    <artifactId>concord-noderoster-plugin</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>com.walmartlabs.concord.server.plugins</groupId>\n                    <artifactId>pfed-sso</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-agent</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.testcontainers</groupId>\n            <artifactId>testcontainers-postgresql</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.typesafe</groupId>\n            <artifactId>config</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-client2</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n</project>\n"
  },
  {
    "path": "it/testing-server/src/main/java/com/walmartlabs/concord/it/testingserver/TestingConcordAgent.java",
    "content": "package com.walmartlabs.concord.it.testingserver;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Guice;\nimport com.google.inject.Module;\nimport com.typesafe.config.Config;\nimport com.typesafe.config.ConfigFactory;\nimport com.typesafe.config.ConfigParseOptions;\nimport com.typesafe.config.ConfigResolveOptions;\nimport com.walmartlabs.concord.agent.Agent;\nimport com.walmartlabs.concord.agent.AgentModule;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Function;\nimport java.util.stream.Stream;\n\n/**\n * A helper class for running concord-agent.\n * The agent runs in the same JVM as TestingConcordAgent.\n */\npublic class TestingConcordAgent implements AutoCloseable {\n\n    private final Map<String, String> extraConfiguration;\n    private final List<Function<Config, Module>> extraModules;\n    private final int apiPort;\n    private final String agentApiKey;\n\n    private Agent agent;\n\n    public TestingConcordAgent(TestingConcordServer testingConcordServer) {\n        this(testingConcordServer, Map.of(), List.of());\n    }\n\n    public TestingConcordAgent(TestingConcordServer testingConcordServer, Map<String, String> extraConfiguration, List<Function<Config, Module>> extraModules) {\n        this.apiPort = testingConcordServer.getApiPort();\n        this.agentApiKey = testingConcordServer.getAgentApiKey();\n        this.extraConfiguration = extraConfiguration;\n        this.extraModules = extraModules;\n    }\n\n    public synchronized void start() {\n        var config = prepareConfig();\n        var system = new AgentModule(config);\n        var allModules = Stream.concat(extraModules.stream().map(f -> f.apply(config)), Stream.of(system)).toList();\n        var injector = Guice.createInjector(allModules);\n        agent = injector.getInstance(Agent.class);\n        agent.start();\n    }\n\n    public synchronized void stop() {\n        if (agent != null) {\n            agent.stop();\n            agent = null;\n        }\n    }\n\n    @Override\n    public void close() {\n        this.stop();\n    }\n\n    private Config prepareConfig() {\n        var extraConfig = ConfigFactory.parseMap(this.extraConfiguration);\n\n        var testConfig = ConfigFactory.parseMap(Map.of(\n                \"maintenanceModeListenerPort\", 0,\n                \"workDirBase\", \"workDirs\",\n                \"server.apiBaseUrl\", \"http://localhost:\" + apiPort,\n                \"server.websocketUrl\", \"ws://localhost:\" + apiPort + \"/websocket\",\n                \"server.apiKey\", agentApiKey\n        ));\n\n        var defaultConfig = ConfigFactory.load(\"concord-agent.conf\", ConfigParseOptions.defaults(), ConfigResolveOptions.defaults().setAllowUnresolved(true))\n                .getConfig(\"concord-agent\");\n\n        return extraConfig.withFallback(testConfig.withFallback(defaultConfig)).resolve();\n    }\n}\n"
  },
  {
    "path": "it/testing-server/src/main/java/com/walmartlabs/concord/it/testingserver/TestingConcordServer.java",
    "content": "package com.walmartlabs.concord.it.testingserver;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ca.ibodrov.concord.webapp.WebappPluginModule;\nimport com.google.common.collect.ImmutableMap;\nimport com.google.inject.Module;\nimport com.typesafe.config.Config;\nimport com.typesafe.config.ConfigFactory;\nimport com.typesafe.config.ConfigParseOptions;\nimport com.typesafe.config.ConfigResolveOptions;\nimport com.walmartlabs.concord.server.ConcordServer;\nimport com.walmartlabs.concord.server.ConcordServerModule;\nimport org.testcontainers.containers.PostgreSQLContainer;\n\nimport java.security.SecureRandom;\nimport java.util.Base64;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Function;\nimport java.util.stream.Stream;\n\nimport static com.google.common.base.Preconditions.checkArgument;\nimport static java.util.Objects.requireNonNull;\n\n/**\n * A helper class for running concord-server. It runs PostgreSQL in Docker\n * and the server runs in the same JVM as TestingConcordServer.\n */\npublic class TestingConcordServer implements AutoCloseable {\n\n    private final PostgreSQLContainer<?> db;\n    private final Map<String, Object> extraConfiguration;\n    private final List<Function<Config, Module>> extraModules;\n    private final int apiPort;\n    private final String adminApiKey;\n    private final String agentApiKey;\n\n    private ConcordServer server;\n\n    public TestingConcordServer(PostgreSQLContainer<?> db) {\n        this(db, 8001, Map.of(), List.of());\n    }\n\n    public TestingConcordServer(PostgreSQLContainer<?> db, int apiPort, Map<String, Object> extraConfiguration, List<Function<Config, Module>> extraModules) {\n        this.db = requireNonNull(db);\n        this.extraConfiguration = requireNonNull(extraConfiguration);\n        this.apiPort = apiPort;\n        this.extraModules = requireNonNull(extraModules);\n        this.adminApiKey = randomString(8);\n        this.agentApiKey = randomString(16);\n    }\n\n    public synchronized TestingConcordServer start() throws Exception {\n        checkArgument(db.isRunning(), \"The database container is not running\");\n\n        var config = prepareConfig(db);\n        var system = new ConcordServerModule(config);\n        var webapp = new WebappPluginModule();\n        var allModules = Stream.concat(extraModules.stream().map(f -> f.apply(config)), Stream.of(system, webapp)).toList();\n        server = ConcordServer.withModules(allModules)\n                .start();\n\n        return this;\n    }\n\n    public synchronized void stop() throws Exception {\n        if (server != null) {\n            server.stop();\n            server = null;\n        }\n    }\n\n    @Override\n    public void close() throws Exception {\n        this.stop();\n    }\n\n    public int getApiPort() {\n        return apiPort;\n    }\n\n    public String getApiBaseUrl() {\n        return \"http://localhost:\" + apiPort;\n    }\n\n    public ConcordServer getServer() {\n        return server;\n    }\n\n    public PostgreSQLContainer<?> getDb() {\n        return db;\n    }\n\n    public String getAdminApiKey() {\n        return adminApiKey;\n    }\n\n    public String getAgentApiKey() {\n        return agentApiKey;\n    }\n\n    private Config prepareConfig(PostgreSQLContainer<?> db) {\n        var extraConfig = ConfigFactory.parseMap(this.extraConfiguration);\n\n        var testConfig = ConfigFactory.parseMap(ImmutableMap.<String, String>builder()\n                .put(\"server.port\", String.valueOf(apiPort))\n                .put(\"db.url\", db.getJdbcUrl())\n                .put(\"db.appUsername\", db.getUsername())\n                .put(\"db.appPassword\", db.getPassword())\n                .put(\"db.inventoryUsername\", db.getUsername())\n                .put(\"db.inventoryPassword\", db.getPassword())\n                .put(\"db.changeLogParameters.defaultAdminToken\", adminApiKey)\n                .put(\"db.changeLogParameters.defaultAgentToken\", agentApiKey)\n                .put(\"secretStore.serverPassword\", randomString(64))\n                .put(\"secretStore.secretStoreSalt\", randomString(64))\n                .put(\"secretStore.projectSecretSalt\", randomString(64))\n                .build());\n\n        var defaultConfig = ConfigFactory.load(\"concord-server.conf\", ConfigParseOptions.defaults(), ConfigResolveOptions.defaults().setAllowUnresolved(true))\n                .getConfig(\"concord-server\");\n\n        return extraConfig.withFallback(testConfig.withFallback(defaultConfig)).resolve();\n    }\n\n    private static String randomString(int minLength) {\n        byte[] ab = new byte[minLength];\n        new SecureRandom().nextBytes(ab);\n        return Base64.getEncoder().encodeToString(ab);\n    }\n\n    /**\n     * Just an example.\n     */\n    public static void main(String[] args) throws Exception {\n        try (var db = new PostgreSQLContainer<>(\"postgres:15-alpine\");\n             var server = new TestingConcordServer(db, 8001, Map.of(\"process.watchdogPeriod\", \"10 seconds\"), List.of())) {\n            db.start();\n            server.start();\n            System.out.printf(\"\"\"\n                            ==============================================================\n                            \n                              UI: http://localhost:8001/\n                              DB:\n                                JDBC URL: %s\n                                username: %s\n                                password: %s\n                            \n                              admin API key: %s\n                              agent API key: %s\n                            %n\"\"\", db.getJdbcUrl(),\n                    db.getUsername(),\n                    db.getPassword(),\n                    server.getAdminApiKey(),\n                    server.getAgentApiKey());\n\n            Thread.currentThread().join();\n        }\n    }\n}\n\n"
  },
  {
    "path": "it/testing-server/src/test/java/com/walmartlabs/concord/it/testingserver/TestingConcordIT.java",
    "content": "package com.walmartlabs.concord.it.testingserver;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClientConfiguration;\nimport com.walmartlabs.concord.client2.DefaultApiClientFactory;\nimport com.walmartlabs.concord.client2.ProcessApi;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\nimport org.testcontainers.containers.PostgreSQLContainer;\n\nimport java.io.IOException;\nimport java.net.ServerSocket;\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.client2.ProcessEntry.StatusEnum.FINISHED;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\n/**\n * A test that tests TestingConcordServer and TestingConcordAgent can run together in the same JVM.\n */\npublic class TestingConcordIT {\n\n    private PostgreSQLContainer<?> db;\n    private TestingConcordServer concordServer;\n    private TestingConcordAgent concordAgent;\n\n    @BeforeEach\n    public void setUp() throws Exception {\n        db = new PostgreSQLContainer<>(\"postgres:15-alpine\");\n        db.start();\n\n        int apiPort = getFreePort();\n        concordServer = new TestingConcordServer(db, apiPort, Map.of(), List.of());\n        concordServer.start();\n\n        concordAgent = new TestingConcordAgent(concordServer);\n        concordAgent.start();\n    }\n\n    @AfterEach\n    public void tearDown() throws Exception {\n        if (concordAgent != null) {\n            concordAgent.close();\n            concordAgent = null;\n        }\n\n        if (concordServer != null) {\n            concordServer.close();\n            concordServer = null;\n        }\n\n        if (db != null) {\n            db.close();\n            db = null;\n        }\n    }\n\n    @Test\n    @Timeout(120)\n    public void testRunningSimpleProcess() throws Exception {\n        var client = new DefaultApiClientFactory(concordServer.getApiBaseUrl())\n                .create(ApiClientConfiguration.builder()\n                        .apiKey(concordServer.getAdminApiKey())\n                        .build());\n\n        var processApi = new ProcessApi(client);\n        var response = processApi.startProcess(Map.of(\"concord.yml\", \"\"\"\n                configuration:\n                  runtime: \"concord-v2\"\n                flows:\n                  default:\n                    - log: \"Hello!\"\n                \"\"\".getBytes()));\n        assertNotNull(response.getInstanceId());\n\n        var process = processApi.waitForCompletion(response.getInstanceId(), Duration.ofSeconds(60).toMillis());\n        assertEquals(FINISHED, process.getStatus());\n    }\n\n    private static int getFreePort() {\n        try (var socket = new ServerSocket(0)) {\n            return socket.getLocalPort();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "mvnw",
    "content": "#!/bin/sh\n# ----------------------------------------------------------------------------\n# Licensed to the Apache Software Foundation (ASF) under one\n# or more contributor license agreements.  See the NOTICE file\n# distributed with this work for additional information\n# regarding copyright ownership.  The ASF licenses this file\n# to you under the Apache License, Version 2.0 (the\n# \"License\"); you may not use this file except in compliance\n# with the License.  You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing,\n# software distributed under the License is distributed on an\n# \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n# KIND, either express or implied.  See the License for the\n# specific language governing permissions and limitations\n# under the License.\n# ----------------------------------------------------------------------------\n\n# ----------------------------------------------------------------------------\n# Apache Maven Wrapper startup batch script, version 3.3.2\n#\n# Optional ENV vars\n# -----------------\n#   JAVA_HOME - location of a JDK home dir, required when download maven via java source\n#   MVNW_REPOURL - repo url base for downloading maven distribution\n#   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven\n#   MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output\n# ----------------------------------------------------------------------------\n\nset -euf\n[ \"${MVNW_VERBOSE-}\" != debug ] || set -x\n\n# OS specific support.\nnative_path() { printf %s\\\\n \"$1\"; }\ncase \"$(uname)\" in\nCYGWIN* | MINGW*)\n  [ -z \"${JAVA_HOME-}\" ] || JAVA_HOME=\"$(cygpath --unix \"$JAVA_HOME\")\"\n  native_path() { cygpath --path --windows \"$1\"; }\n  ;;\nesac\n\n# set JAVACMD and JAVACCMD\nset_java_home() {\n  # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched\n  if [ -n \"${JAVA_HOME-}\" ]; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ]; then\n      # IBM's JDK on AIX uses strange locations for the executables\n      JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n      JAVACCMD=\"$JAVA_HOME/jre/sh/javac\"\n    else\n      JAVACMD=\"$JAVA_HOME/bin/java\"\n      JAVACCMD=\"$JAVA_HOME/bin/javac\"\n\n      if [ ! -x \"$JAVACMD\" ] || [ ! -x \"$JAVACCMD\" ]; then\n        echo \"The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run.\" >&2\n        echo \"JAVA_HOME is set to \\\"$JAVA_HOME\\\", but \\\"\\$JAVA_HOME/bin/java\\\" or \\\"\\$JAVA_HOME/bin/javac\\\" does not exist.\" >&2\n        return 1\n      fi\n    fi\n  else\n    JAVACMD=\"$(\n      'set' +e\n      'unset' -f command 2>/dev/null\n      'command' -v java\n    )\" || :\n    JAVACCMD=\"$(\n      'set' +e\n      'unset' -f command 2>/dev/null\n      'command' -v javac\n    )\" || :\n\n    if [ ! -x \"${JAVACMD-}\" ] || [ ! -x \"${JAVACCMD-}\" ]; then\n      echo \"The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run.\" >&2\n      return 1\n    fi\n  fi\n}\n\n# hash string like Java String::hashCode\nhash_string() {\n  str=\"${1:-}\" h=0\n  while [ -n \"$str\" ]; do\n    char=\"${str%\"${str#?}\"}\"\n    h=$(((h * 31 + $(LC_CTYPE=C printf %d \"'$char\")) % 4294967296))\n    str=\"${str#?}\"\n  done\n  printf %x\\\\n $h\n}\n\nverbose() { :; }\n[ \"${MVNW_VERBOSE-}\" != true ] || verbose() { printf %s\\\\n \"${1-}\"; }\n\ndie() {\n  printf %s\\\\n \"$1\" >&2\n  exit 1\n}\n\ntrim() {\n  # MWRAPPER-139:\n  #   Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.\n  #   Needed for removing poorly interpreted newline sequences when running in more\n  #   exotic environments such as mingw bash on Windows.\n  printf \"%s\" \"${1}\" | tr -d '[:space:]'\n}\n\n# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties\nwhile IFS=\"=\" read -r key value; do\n  case \"${key-}\" in\n  distributionUrl) distributionUrl=$(trim \"${value-}\") ;;\n  distributionSha256Sum) distributionSha256Sum=$(trim \"${value-}\") ;;\n  esac\ndone <\"${0%/*}/.mvn/wrapper/maven-wrapper.properties\"\n[ -n \"${distributionUrl-}\" ] || die \"cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties\"\n\ncase \"${distributionUrl##*/}\" in\nmaven-mvnd-*bin.*)\n  MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/\n  case \"${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)\" in\n  *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;\n  :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;\n  :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;\n  :Linux*x86_64*) distributionPlatform=linux-amd64 ;;\n  *)\n    echo \"Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version\" >&2\n    distributionPlatform=linux-amd64\n    ;;\n  esac\n  distributionUrl=\"${distributionUrl%-bin.*}-$distributionPlatform.zip\"\n  ;;\nmaven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;\n*) MVN_CMD=\"mvn${0##*/mvnw}\" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;\nesac\n\n# apply MVNW_REPOURL and calculate MAVEN_HOME\n# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>\n[ -z \"${MVNW_REPOURL-}\" ] || distributionUrl=\"$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*\"$_MVNW_REPO_PATTERN\"}\"\ndistributionUrlName=\"${distributionUrl##*/}\"\ndistributionUrlNameMain=\"${distributionUrlName%.*}\"\ndistributionUrlNameMain=\"${distributionUrlNameMain%-bin}\"\nMAVEN_USER_HOME=\"${MAVEN_USER_HOME:-${HOME}/.m2}\"\nMAVEN_HOME=\"${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string \"$distributionUrl\")\"\n\nexec_maven() {\n  unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :\n  exec \"$MAVEN_HOME/bin/$MVN_CMD\" \"$@\" || die \"cannot exec $MAVEN_HOME/bin/$MVN_CMD\"\n}\n\nif [ -d \"$MAVEN_HOME\" ]; then\n  verbose \"found existing MAVEN_HOME at $MAVEN_HOME\"\n  exec_maven \"$@\"\nfi\n\ncase \"${distributionUrl-}\" in\n*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;\n*) die \"distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'\" ;;\nesac\n\n# prepare tmp dir\nif TMP_DOWNLOAD_DIR=\"$(mktemp -d)\" && [ -d \"$TMP_DOWNLOAD_DIR\" ]; then\n  clean() { rm -rf -- \"$TMP_DOWNLOAD_DIR\"; }\n  trap clean HUP INT TERM EXIT\nelse\n  die \"cannot create temp dir\"\nfi\n\nmkdir -p -- \"${MAVEN_HOME%/*}\"\n\n# Download and Install Apache Maven\nverbose \"Couldn't find MAVEN_HOME, downloading and installing it ...\"\nverbose \"Downloading from: $distributionUrl\"\nverbose \"Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName\"\n\n# select .zip or .tar.gz\nif ! command -v unzip >/dev/null; then\n  distributionUrl=\"${distributionUrl%.zip}.tar.gz\"\n  distributionUrlName=\"${distributionUrl##*/}\"\nfi\n\n# verbose opt\n__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''\n[ \"${MVNW_VERBOSE-}\" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v\n\n# normalize http auth\ncase \"${MVNW_PASSWORD:+has-password}\" in\n'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;\nhas-password) [ -n \"${MVNW_USERNAME-}\" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;\nesac\n\nif [ -z \"${MVNW_USERNAME-}\" ] && command -v wget >/dev/null; then\n  verbose \"Found wget ... using wget\"\n  wget ${__MVNW_QUIET_WGET:+\"$__MVNW_QUIET_WGET\"} \"$distributionUrl\" -O \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" || die \"wget: Failed to fetch $distributionUrl\"\nelif [ -z \"${MVNW_USERNAME-}\" ] && command -v curl >/dev/null; then\n  verbose \"Found curl ... using curl\"\n  curl ${__MVNW_QUIET_CURL:+\"$__MVNW_QUIET_CURL\"} -f -L -o \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" \"$distributionUrl\" || die \"curl: Failed to fetch $distributionUrl\"\nelif set_java_home; then\n  verbose \"Falling back to use Java to download\"\n  javaSource=\"$TMP_DOWNLOAD_DIR/Downloader.java\"\n  targetZip=\"$TMP_DOWNLOAD_DIR/$distributionUrlName\"\n  cat >\"$javaSource\" <<-END\n\tpublic class Downloader extends java.net.Authenticator\n\t{\n\t  protected java.net.PasswordAuthentication getPasswordAuthentication()\n\t  {\n\t    return new java.net.PasswordAuthentication( System.getenv( \"MVNW_USERNAME\" ), System.getenv( \"MVNW_PASSWORD\" ).toCharArray() );\n\t  }\n\t  public static void main( String[] args ) throws Exception\n\t  {\n\t    setDefault( new Downloader() );\n\t    java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );\n\t  }\n\t}\n\tEND\n  # For Cygwin/MinGW, switch paths to Windows format before running javac and java\n  verbose \" - Compiling Downloader.java ...\"\n  \"$(native_path \"$JAVACCMD\")\" \"$(native_path \"$javaSource\")\" || die \"Failed to compile Downloader.java\"\n  verbose \" - Running Downloader.java ...\"\n  \"$(native_path \"$JAVACMD\")\" -cp \"$(native_path \"$TMP_DOWNLOAD_DIR\")\" Downloader \"$distributionUrl\" \"$(native_path \"$targetZip\")\"\nfi\n\n# If specified, validate the SHA-256 sum of the Maven distribution zip file\nif [ -n \"${distributionSha256Sum-}\" ]; then\n  distributionSha256Result=false\n  if [ \"$MVN_CMD\" = mvnd.sh ]; then\n    echo \"Checksum validation is not supported for maven-mvnd.\" >&2\n    echo \"Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties.\" >&2\n    exit 1\n  elif command -v sha256sum >/dev/null; then\n    if echo \"$distributionSha256Sum  $TMP_DOWNLOAD_DIR/$distributionUrlName\" | sha256sum -c >/dev/null 2>&1; then\n      distributionSha256Result=true\n    fi\n  elif command -v shasum >/dev/null; then\n    if echo \"$distributionSha256Sum  $TMP_DOWNLOAD_DIR/$distributionUrlName\" | shasum -a 256 -c >/dev/null 2>&1; then\n      distributionSha256Result=true\n    fi\n  else\n    echo \"Checksum validation was requested but neither 'sha256sum' or 'shasum' are available.\" >&2\n    echo \"Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties.\" >&2\n    exit 1\n  fi\n  if [ $distributionSha256Result = false ]; then\n    echo \"Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised.\" >&2\n    echo \"If you updated your Maven version, you need to update the specified distributionSha256Sum property.\" >&2\n    exit 1\n  fi\nfi\n\n# unzip and move\nif command -v unzip >/dev/null; then\n  unzip ${__MVNW_QUIET_UNZIP:+\"$__MVNW_QUIET_UNZIP\"} \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" -d \"$TMP_DOWNLOAD_DIR\" || die \"failed to unzip\"\nelse\n  tar xzf${__MVNW_QUIET_TAR:+\"$__MVNW_QUIET_TAR\"} \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" -C \"$TMP_DOWNLOAD_DIR\" || die \"failed to untar\"\nfi\nprintf %s\\\\n \"$distributionUrl\" >\"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url\"\nmv -- \"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain\" \"$MAVEN_HOME\" || [ -d \"$MAVEN_HOME\" ] || die \"fail to move MAVEN_HOME\"\n\nclean || :\nexec_maven \"$@\"\n"
  },
  {
    "path": "mvnw.cmd",
    "content": "<# : batch portion\r\n@REM ----------------------------------------------------------------------------\r\n@REM Licensed to the Apache Software Foundation (ASF) under one\r\n@REM or more contributor license agreements.  See the NOTICE file\r\n@REM distributed with this work for additional information\r\n@REM regarding copyright ownership.  The ASF licenses this file\r\n@REM to you under the Apache License, Version 2.0 (the\r\n@REM \"License\"); you may not use this file except in compliance\r\n@REM with the License.  You may obtain a copy of the License at\r\n@REM\r\n@REM    http://www.apache.org/licenses/LICENSE-2.0\r\n@REM\r\n@REM Unless required by applicable law or agreed to in writing,\r\n@REM software distributed under the License is distributed on an\r\n@REM \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\r\n@REM KIND, either express or implied.  See the License for the\r\n@REM specific language governing permissions and limitations\r\n@REM under the License.\r\n@REM ----------------------------------------------------------------------------\r\n\r\n@REM ----------------------------------------------------------------------------\r\n@REM Apache Maven Wrapper startup batch script, version 3.3.2\r\n@REM\r\n@REM Optional ENV vars\r\n@REM   MVNW_REPOURL - repo url base for downloading maven distribution\r\n@REM   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven\r\n@REM   MVNW_VERBOSE - true: enable verbose log; others: silence the output\r\n@REM ----------------------------------------------------------------------------\r\n\r\n@IF \"%__MVNW_ARG0_NAME__%\"==\"\" (SET __MVNW_ARG0_NAME__=%~nx0)\r\n@SET __MVNW_CMD__=\r\n@SET __MVNW_ERROR__=\r\n@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%\r\n@SET PSModulePath=\r\n@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 @(\r\n  IF \"%%A\"==\"MVN_CMD\" (set __MVNW_CMD__=%%B) ELSE IF \"%%B\"==\"\" (echo %%A) ELSE (echo %%A=%%B)\r\n)\r\n@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%\r\n@SET __MVNW_PSMODULEP_SAVE=\r\n@SET __MVNW_ARG0_NAME__=\r\n@SET MVNW_USERNAME=\r\n@SET MVNW_PASSWORD=\r\n@IF NOT \"%__MVNW_CMD__%\"==\"\" (%__MVNW_CMD__% %*)\r\n@echo Cannot start maven from wrapper >&2 && exit /b 1\r\n@GOTO :EOF\r\n: end batch / begin powershell #>\r\n\r\n$ErrorActionPreference = \"Stop\"\r\nif ($env:MVNW_VERBOSE -eq \"true\") {\r\n  $VerbosePreference = \"Continue\"\r\n}\r\n\r\n# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties\r\n$distributionUrl = (Get-Content -Raw \"$scriptDir/.mvn/wrapper/maven-wrapper.properties\" | ConvertFrom-StringData).distributionUrl\r\nif (!$distributionUrl) {\r\n  Write-Error \"cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties\"\r\n}\r\n\r\nswitch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {\r\n  \"maven-mvnd-*\" {\r\n    $USE_MVND = $true\r\n    $distributionUrl = $distributionUrl -replace '-bin\\.[^.]*$',\"-windows-amd64.zip\"\r\n    $MVN_CMD = \"mvnd.cmd\"\r\n    break\r\n  }\r\n  default {\r\n    $USE_MVND = $false\r\n    $MVN_CMD = $script -replace '^mvnw','mvn'\r\n    break\r\n  }\r\n}\r\n\r\n# apply MVNW_REPOURL and calculate MAVEN_HOME\r\n# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>\r\nif ($env:MVNW_REPOURL) {\r\n  $MVNW_REPO_PATTERN = if ($USE_MVND) { \"/org/apache/maven/\" } else { \"/maven/mvnd/\" }\r\n  $distributionUrl = \"$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')\"\r\n}\r\n$distributionUrlName = $distributionUrl -replace '^.*/',''\r\n$distributionUrlNameMain = $distributionUrlName -replace '\\.[^.]*$','' -replace '-bin$',''\r\n$MAVEN_HOME_PARENT = \"$HOME/.m2/wrapper/dists/$distributionUrlNameMain\"\r\nif ($env:MAVEN_USER_HOME) {\r\n  $MAVEN_HOME_PARENT = \"$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain\"\r\n}\r\n$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString(\"x2\")}) -join ''\r\n$MAVEN_HOME = \"$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME\"\r\n\r\nif (Test-Path -Path \"$MAVEN_HOME\" -PathType Container) {\r\n  Write-Verbose \"found existing MAVEN_HOME at $MAVEN_HOME\"\r\n  Write-Output \"MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD\"\r\n  exit $?\r\n}\r\n\r\nif (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {\r\n  Write-Error \"distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl\"\r\n}\r\n\r\n# prepare tmp dir\r\n$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile\r\n$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path \"$TMP_DOWNLOAD_DIR_HOLDER.dir\"\r\n$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null\r\ntrap {\r\n  if ($TMP_DOWNLOAD_DIR.Exists) {\r\n    try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }\r\n    catch { Write-Warning \"Cannot remove $TMP_DOWNLOAD_DIR\" }\r\n  }\r\n}\r\n\r\nNew-Item -Itemtype Directory -Path \"$MAVEN_HOME_PARENT\" -Force | Out-Null\r\n\r\n# Download and Install Apache Maven\r\nWrite-Verbose \"Couldn't find MAVEN_HOME, downloading and installing it ...\"\r\nWrite-Verbose \"Downloading from: $distributionUrl\"\r\nWrite-Verbose \"Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName\"\r\n\r\n$webclient = New-Object System.Net.WebClient\r\nif ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {\r\n  $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)\r\n}\r\n[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\r\n$webclient.DownloadFile($distributionUrl, \"$TMP_DOWNLOAD_DIR/$distributionUrlName\") | Out-Null\r\n\r\n# If specified, validate the SHA-256 sum of the Maven distribution zip file\r\n$distributionSha256Sum = (Get-Content -Raw \"$scriptDir/.mvn/wrapper/maven-wrapper.properties\" | ConvertFrom-StringData).distributionSha256Sum\r\nif ($distributionSha256Sum) {\r\n  if ($USE_MVND) {\r\n    Write-Error \"Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties.\"\r\n  }\r\n  Import-Module $PSHOME\\Modules\\Microsoft.PowerShell.Utility -Function Get-FileHash\r\n  if ((Get-FileHash \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {\r\n    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.\"\r\n  }\r\n}\r\n\r\n# unzip and move\r\nExpand-Archive \"$TMP_DOWNLOAD_DIR/$distributionUrlName\" -DestinationPath \"$TMP_DOWNLOAD_DIR\" | Out-Null\r\nRename-Item -Path \"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain\" -NewName $MAVEN_HOME_NAME | Out-Null\r\ntry {\r\n  Move-Item -Path \"$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME\" -Destination $MAVEN_HOME_PARENT | Out-Null\r\n} catch {\r\n  if (! (Test-Path -Path \"$MAVEN_HOME\" -PathType Container)) {\r\n    Write-Error \"fail to move MAVEN_HOME\"\r\n  }\r\n} finally {\r\n  try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }\r\n  catch { Write-Warning \"Cannot remove $TMP_DOWNLOAD_DIR\" }\r\n}\r\n\r\nWrite-Output \"MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD\"\r\n"
  },
  {
    "path": "plugins/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n    <artifactId>parent</artifactId>\n\n    <packaging>pom</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <modules>\n        <module>tasks/ansible</module>\n        <module>tasks/asserts</module>\n        <module>tasks/concord</module>\n        <module>tasks/crypto</module>\n        <module>tasks/docker</module>\n        <module>tasks/dynamic-tasks</module>\n        <module>tasks/example</module>\n        <module>tasks/http</module>\n        <module>tasks/kv</module>\n        <module>tasks/locale</module>\n        <module>tasks/lock</module>\n        <module>tasks/log</module>\n        <module>tasks/misc</module>\n        <module>tasks/mock</module>\n        <module>tasks/noderoster</module>\n        <module>tasks/resource</module>\n        <module>tasks/slack</module>\n        <module>tasks/sleep</module>\n        <module>tasks/smtp</module>\n        <module>tasks/throw</module>\n        <module>tasks/variables</module>\n        <module>templates/ansible</module>\n        <module>tasks/files</module>\n    </modules>\n</project>\n"
  },
  {
    "path": "plugins/tasks/ansible/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>ansible-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-client2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-model-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-vm-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jsr310</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <!-- Immutables -->\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.errorprone</groupId>\n            <artifactId>error_prone_annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.apache.kerby</groupId>\n            <artifactId>kerby-util</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.kerby</groupId>\n            <artifactId>kerb-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.kerby</groupId>\n            <artifactId>kerb-client</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.kerby</groupId>\n            <artifactId>kerb-util</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <dependencies>\n                    <dependency>\n                        <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n                        <artifactId>concord-runner-v2</artifactId>\n                        <version>${project.version}</version>\n                    </dependency>\n                </dependencies>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/AnsibleAuth.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface AnsibleAuth {\n\n    void prepare() throws Exception;\n\n    AnsibleAuth enrich(AnsibleEnv env, AnsibleContext context);\n\n    AnsibleAuth enrich(PlaybookScriptBuilder p);\n\n    void postProcess();\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/AnsibleAuthFactory.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.plugins.ansible.secrets.AnsibleSecretService;\nimport com.walmartlabs.concord.plugins.ansible.secrets.KeyPair;\nimport com.walmartlabs.concord.plugins.ansible.secrets.UsernamePassword;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.attribute.PosixFilePermission;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Set;\n\nimport static com.walmartlabs.concord.sdk.MapUtils.*;\n\npublic class AnsibleAuthFactory {\n\n    private static final Logger log = LoggerFactory.getLogger(AnsibleAuthFactory.class);\n\n    private final AnsibleSecretService secretService;\n\n    public AnsibleAuthFactory(AnsibleSecretService secretService) {\n        this.secretService = secretService;\n    }\n\n    public AnsibleAuth create(AnsibleContext context) {\n        Map<String, Map<String, Object>> authParams = getMap(context.args(), TaskParams.AUTH, Collections.emptyMap());\n        if (authParams.isEmpty()) {\n            return new NopAuth();\n        }\n\n        if (authParams.size() != 1) {\n            throw new RuntimeException(\"Invalid auth configuration. More that one auth type (expected one of 'krb5' or 'privateKey'): \" + authParams.keySet());\n        }\n\n        Map.Entry<String, Map<String, Object>> auth = authParams.entrySet().iterator().next();\n        switch (auth.getKey().toLowerCase()) {\n            case \"krb5\":\n                try {\n                    UsernamePassword cred = parseKerberosAuth(secretService, auth.getValue());\n                    log.info(\"Using the kerberos username: {}\", cred.username());\n                    return new KerberosAuth(cred.username(), cred.password(), context.tmpDir(), context.debug());\n                } catch (Exception e) {\n                    log.error(\"Error while fetching the kerberos credentials: {}\", e.getMessage(), e);\n                    throw new RuntimeException(\"Error while fetching the kerberos credentials: \" + e.getMessage());\n                }\n            case \"privatekey\":\n                try {\n                    PrivateKeyAuth privateKeyAuth = parsePrivateKeyAuth(secretService, context.workDir(), auth.getValue());\n                    log.info(\"Using the private key: {}\", privateKeyAuth.getKeyPath());\n                    return privateKeyAuth;\n                } catch (Exception e) {\n                    log.error(\"Error while fetching the private key: {}\", e.getMessage(), e);\n                    throw new RuntimeException(\"Error while fetching the private key: \" + e.getMessage());\n                }\n            default:\n                throw new IllegalArgumentException(\"Unknown auth type: \" + auth);\n        }\n    }\n\n    private static UsernamePassword parseKerberosAuth(AnsibleSecretService secretService,\n                                                      Map<String, Object> auth) throws Exception {\n        Map<String, Object> secretParams = getMap(auth, \"secret\", Collections.emptyMap());\n        if (!secretParams.isEmpty()) {\n            Secret secret = Secret.from(secretParams);\n            return secretService.exportCredentials(secret.getOrg(), secret.getName(), secret.getPassword());\n        }\n\n        return new UsernamePassword(assertString(auth, \"user\"), assertString(auth, \"password\"));\n    }\n\n    private static PrivateKeyAuth parsePrivateKeyAuth(AnsibleSecretService secretService,\n                                                      Path workDir,\n                                                      Map<String, Object> auth) throws Exception {\n\n        boolean removeAfter = true;\n\n        Path p;\n        Map<String, Object> secretParams = getMap(auth, \"secret\", Collections.emptyMap());\n        if (!secretParams.isEmpty()) {\n            Secret secret = Secret.from(secretParams);\n            KeyPair keyPair = secretService.exportKeyAsFile(secret.getOrg(), secret.getName(), secret.getPassword());\n            p = keyPair.privateKey();\n        } else {\n            p = ArgUtils.getPath(auth, \"path\", workDir);\n            removeAfter = false;\n        }\n\n        if (!Files.exists(p)) {\n            throw new IllegalArgumentException(\"Private key file not found: \" + p);\n        }\n\n        // ensure that the key has proper permissions (chmod 600)\n        Set<PosixFilePermission> perms = new HashSet<>();\n        perms.add(PosixFilePermission.OWNER_READ);\n        perms.add(PosixFilePermission.OWNER_WRITE);\n        Files.setPosixFilePermissions(p, perms);\n\n        String user = getString(auth, \"user\");\n        Path keyPath = p.toAbsolutePath();\n        return new PrivateKeyAuth(workDir, user, keyPath, removeAfter);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/AnsibleCallbacks.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessEventsApi;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\n\npublic class AnsibleCallbacks {\n\n    public static AnsibleCallbacks process(AnsibleContext ctx, AnsibleConfig config) {\n        return new AnsibleCallbacks(ctx.workDir(), ctx.tmpDir(), ctx.debug())\n                .parse(ctx.argsWithDefaults())\n                .enrich(config)\n                .write();\n    }\n\n    private static final Logger log = LoggerFactory.getLogger(AnsibleCallbacks.class);\n\n    private static final String CALLBACK_LOCATION = \"/com/walmartlabs/concord/plugins/ansible/callback\";\n    private static final String CALLBACK_PLUGINS_DIR = \"_callbacks\";\n\n    private static final String[] STATS_CALLBACKS = new String[]{\n            \"concord_trace.py\"\n    };\n\n    private static final String[] EVENTS_CALLBACKS = new String[]{\n            \"concord_events.py\",\n            \"concord_strategy_patch.py\"\n    };\n    private static final String[] OUTVARS_CALLBACKS = new String[]{\n            \"concord_out_vars.py\"\n    };\n    private static final String[] POLICY_CALLBACKS = new String[]{\n            \"concord_task_executor_patch.py\"\n    };\n    private static final String[] LOG_FILTERING_CALLBACKS = new String[]{\n            \"concord_protectdata.py\"\n    };\n    private static final String[] MODULE_DEFAULTS_CALLBACKS = new String[]{\n            \"concord_default_module_args.py\"\n    };\n\n    private final boolean debug;\n    private final Path workDir;\n    private final Path tmpDir;\n\n    private boolean disabled = false;\n    private boolean policyEnabled = false;\n    private boolean logFilteringEnabled = false;\n    private boolean eventsEnabled = false;\n    private boolean statsEnabled = false;\n    private boolean outVarsEnabled = false;\n    private boolean moduleDefaultsEnabled = false;\n\n    private Path eventsFile;\n    private EventSender eventSender;\n    private Future<?> eventSenderFuture;\n\n    public AnsibleCallbacks(Path workDir, Path tmpDir, boolean debug) {\n        this.debug = debug;\n        this.workDir = workDir;\n        this.tmpDir = tmpDir;\n    }\n\n    public AnsibleCallbacks parse(Map<String, Object> args) {\n        this.disabled = MapUtils.getBoolean(args, TaskParams.DISABLE_CONCORD_CALLBACKS_KEY, false);\n        this.policyEnabled = MapUtils.getBoolean(args, TaskParams.ENABLE_POLICY, false);\n        this.logFilteringEnabled = MapUtils.getBoolean(args, TaskParams.ENABLE_LOG_FILTERING, false);\n\n        this.eventsEnabled = MapUtils.getBoolean(args, TaskParams.ENABLE_EVENTS, true);\n        this.statsEnabled = MapUtils.getBoolean(args, TaskParams.ENABLE_STATS, true);\n        this.outVarsEnabled = MapUtils.getBoolean(args, TaskParams.ENABLE_OUT_VARS, true);\n        this.moduleDefaultsEnabled = MapUtils.getBoolean(args, TaskParams.ENABLE_MODULE_DEFAULTS, true);\n\n        return this;\n    }\n\n    public AnsibleCallbacks write() {\n        if (disabled) {\n            return this;\n        }\n\n        try {\n            if (policyEnabled) {\n                Resources.copy(CALLBACK_LOCATION, POLICY_CALLBACKS, getDir());\n            }\n\n            if (logFilteringEnabled) {\n                Resources.copy(CALLBACK_LOCATION, LOG_FILTERING_CALLBACKS, getDir());\n            }\n\n            if (eventsEnabled) {\n                Resources.copy(CALLBACK_LOCATION, EVENTS_CALLBACKS, getDir());\n            }\n\n            if (statsEnabled) {\n                Resources.copy(CALLBACK_LOCATION, STATS_CALLBACKS, getDir());\n            }\n\n            if (outVarsEnabled) {\n                Resources.copy(CALLBACK_LOCATION, OUTVARS_CALLBACKS, getDir());\n            }\n\n            if (moduleDefaultsEnabled) {\n                Resources.copy(CALLBACK_LOCATION, MODULE_DEFAULTS_CALLBACKS, getDir());\n            }\n        } catch (IOException e) {\n            log.error(\"Error while adding Concord callback plugins: {}\", e.getMessage(), e);\n            throw new RuntimeException(\"Error while adding Concord callback plugins: \" + e.getMessage());\n        }\n\n        return this;\n    }\n\n    public AnsibleCallbacks enrich(AnsibleConfig config) {\n        if (disabled) {\n            return this;\n        }\n\n        ConfigSection defaults = config.getDefaults()\n                .prependPath(\"callback_plugins\", CALLBACK_PLUGINS_DIR);\n\n        if (logFilteringEnabled) {\n            defaults.put(\"stdout_callback\", \"concord_protectdata\");\n        }\n\n        return this;\n    }\n\n    public AnsibleCallbacks startEventSender(UUID instanceId, ProcessEventsApi eventsApi) throws IOException {\n        if (disabled || !eventsEnabled) {\n            return this;\n        }\n\n        this.eventsFile = Files.createTempFile(tmpDir, \"events\", \".log\");\n        this.eventSender = new EventSender(debug, instanceId, eventsFile, eventsApi);\n        this.eventSenderFuture = eventSender.start();\n\n        return this;\n    }\n\n    public void stopEventSender() {\n        if (eventSender == null) {\n            return;\n        }\n\n        this.eventSender.stop();\n\n        try {\n            this.eventSenderFuture.get(1, TimeUnit.MINUTES);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        } catch (ExecutionException | TimeoutException e) {\n            log.warn(\"Error while stopping the event sending thread\", e);\n        }\n\n        this.eventSender = null;\n    }\n\n    public AnsibleCallbacks enrich(AnsibleEnv env) {\n        if (disabled) {\n            return this;\n        }\n\n        if (eventsFile != null) {\n            // must be a relative path to support Ansible containers\n            env.put(\"CONCORD_ANSIBLE_EVENTS_FILE\", workDir.relativize(eventsFile).toString());\n        }\n\n        return this;\n    }\n\n    private Path getDir() {\n        return tmpDir.resolve(CALLBACK_PLUGINS_DIR);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/AnsibleConfig.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.sdk.MapUtils.getMap;\nimport static com.walmartlabs.concord.sdk.MapUtils.getString;\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\npublic class AnsibleConfig {\n\n    private static final Logger log = LoggerFactory.getLogger(AnsibleConfig.class);\n\n    private static final String CFG_FILE_NAME = \"ansible.cfg\";\n\n    private final Path workDir;\n    private final Path tmpDir;\n    private final boolean debug;\n\n    private Map<String, Map<String, Object>> cfg = new HashMap<>();\n\n    public AnsibleConfig(AnsibleContext context) {\n        this.workDir = context.workDir();\n        this.tmpDir = context.tmpDir();\n        this.debug = context.debug();\n    }\n\n    public AnsibleConfig parse(Map<String, Object> args) {\n        String s = getString(args, TaskParams.CONFIG_FILE_KEY);\n        if (s != null) {\n            Path provided = workDir.resolve(s);\n            if (Files.exists(provided)) {\n                log.info(\"Using the provided configuration file: {}\", provided);\n                this.cfg = loadFromFile(provided);\n                return this;\n            }\n        }\n\n        Map<String, Object> userCfg = getMap(args, TaskParams.CONFIG_KEY, Collections.emptyMap());\n\n        this.cfg = makeAnsibleCfg(userCfg);\n\n        return this;\n    }\n\n    public Path write() {\n        StringBuilder b = new StringBuilder();\n\n        for (Map.Entry<String, Map<String, Object>> c : cfg.entrySet()) {\n            b = addCfgSection(b, c.getKey(), c.getValue());\n        }\n\n        if (debug) {\n            log.info(\"Using the configuration: \\n{}\", b);\n        }\n\n        Path cfgPath = getConfigPath();\n        try {\n            Files.write(cfgPath, b.toString().getBytes(UTF_8), StandardOpenOption.CREATE);\n        } catch (IOException e) {\n            log.error(\"Configuration write {} error\", CFG_FILE_NAME, e);\n            throw new RuntimeException(\"Configuration write error: \" + e.getMessage());\n        }\n        return workDir.relativize(cfgPath);\n    }\n\n    public AnsibleConfig enrich(AnsibleEnv env) {\n        env.put(\"ANSIBLE_CONFIG\", workDir.relativize(getConfigPath()).toString());\n        return this;\n    }\n\n    public ConfigSection getDefaults() {\n        return getSection(\"defaults\");\n    }\n\n    public ConfigSection getSection(String section) {\n        Map<String, Object> defaults = cfg.computeIfAbsent(section, s -> new HashMap<>());\n        return new ConfigSection(defaults);\n    }\n\n    private Path getConfigPath() {\n        return tmpDir.resolve(CFG_FILE_NAME);\n    }\n\n    private static Map<String, Object> makeDefaults() {\n        Map<String, Object> m = new HashMap<>();\n\n        // disable ssl host key checking by default\n        m.put(\"host_key_checking\", false);\n\n        // SSH timeout, default is 10 seconds and too slow for stores\n        m.put(\"timeout\", \"120\");\n\n        // Prepare for Ansible 2.8, ensure retry: continues to work\n        m.put(\"retry_files_enabled\", true);\n\n        // use a shorter path to store temporary files\n        m.put(\"remote_tmp\", \"/tmp/${USER}/ansible\");\n\n        return m;\n    }\n\n    private static Map<String, Map<String, Object>> makeAnsibleCfg(Map<String, Object> userCfg) {\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"defaults\", makeDefaults());\n        m.put(\"ssh_connection\", makeSshConnCfg());\n\n        m = ConfigurationUtils.deepMerge(m, userCfg);\n\n        return assertCfg(m);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Map<String, Map<String, Object>> assertCfg(Map<String, Object> cfg) {\n        Map<String, Map<String, Object>> result = new HashMap<>();\n\n        for (Map.Entry<String, Object> c : cfg.entrySet()) {\n            String k = c.getKey();\n            Object v = c.getValue();\n            if (!(v instanceof Map)) {\n                throw new IllegalArgumentException(\"Invalid configuration. Expected a Map object for key: \" + k + \", got: \" + v);\n            }\n\n            result.put(k, (Map<String, Object>) v);\n        }\n\n        return result;\n    }\n\n    private static Map<String, Object> makeSshConnCfg() {\n        Map<String, Object> m = new HashMap<>();\n\n        // default pipelining to True for better overall performance, compatibility\n        m.put(\"pipelining\", true);\n\n        return m;\n    }\n\n    private static StringBuilder addCfgSection(StringBuilder b, String name, Map<String, Object> m) {\n        if (m == null || m.isEmpty()) {\n            return b;\n        }\n\n        b.append(\"[\").append(name).append(\"]\\n\");\n        for (Map.Entry<String, Object> e : m.entrySet()) {\n            Object v = e.getValue();\n            if (v == null) {\n                continue;\n            }\n\n            b.append(e.getKey()).append(\" = \").append(v).append(\"\\n\");\n        }\n        return b;\n    }\n\n    private Map<String, Map<String, Object>> loadFromFile(Path file) {\n        try {\n            return parseIniFile(file);\n        } catch (IOException e) {\n            log.error(\"Configuration parse error: {}\", e.getMessage());\n            throw new RuntimeException(\"Configuration parse error \" + file + \": \" + e.getMessage());\n        }\n    }\n\n    private static Map<String, Map<String, Object>> parseIniFile(Path file) throws IOException {\n        Map<String, Map<String, Object>> result = new HashMap<>();\n        Map<String, Object> currentSection = null;\n        for (String line : Files.readAllLines(file, UTF_8)) {\n            line = line.trim();\n\n            if (line.isEmpty() || line.startsWith(\"#\") || line.startsWith(\";\")) {\n                continue;\n            }\n\n            if (line.startsWith(\"[\") && line.endsWith(\"]\")) {\n                String sectionName = line.substring(1, line.length() - 1).trim();\n                currentSection = new HashMap<>();\n                result.put(sectionName, currentSection);\n                continue;\n            }\n\n            if (currentSection != null && line.contains(\"=\")) {\n                int equalIndex = line.indexOf(\"=\");\n                String key = line.substring(0, equalIndex).trim();\n                String value = line.substring(equalIndex + 1).trim();\n\n                if ((value.startsWith(\"\\\"\") && value.endsWith(\"\\\"\")) ||\n                    (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n                    value = value.substring(1, value.length() - 1);\n                }\n\n                currentSection.put(key, value);\n            }\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/AnsibleContext.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AllowNulls;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.nio.file.Path;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.sdk.MapUtils.getBoolean;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface AnsibleContext {\n\n    UUID instanceId();\n\n    Path workDir();\n\n    Path tmpDir();\n\n    @Value.Default\n    default boolean debug() {\n        return getBoolean(argsWithDefaults(), TaskParams.DEBUG_KEY.getKey(), false);\n    }\n\n    @AllowNulls\n    Map<String, Object> defaults();\n\n    @AllowNulls\n    Map<String, Object> args();\n\n    String apiBaseUrl();\n\n    @Nullable\n    String sessionToken();\n\n    @Nullable\n    UUID eventCorrelationId();\n\n    @Nullable\n    String orgName();\n\n    @Nullable\n    Integer retryCount();\n\n    default Map<String, Object> argsWithDefaults() {\n        return ConfigurationUtils.deepMerge(defaults(), args());\n    }\n\n    static ImmutableAnsibleContext.Builder builder() {\n        return ImmutableAnsibleContext.builder();\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/AnsibleEnv.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.nio.file.Paths;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic class AnsibleEnv {\n\n    private static final Logger log = LoggerFactory.getLogger(AnsibleEnv.class);\n\n    private final String apiBaseUrl;\n    private final UUID instanceId;\n    private final String sessionToken;\n    private final UUID eventCorrelationId;\n    private final String orgName;\n    private final Integer retryCount;\n    private final boolean debug;\n\n    private Map<String, String> env = Collections.emptyMap();\n\n    public AnsibleEnv(AnsibleContext context) {\n        this.apiBaseUrl = context.apiBaseUrl();\n        this.instanceId = context.instanceId();\n        this.sessionToken = context.sessionToken();\n        this.eventCorrelationId = context.eventCorrelationId();\n        this.orgName = context.orgName();\n        this.retryCount = context.retryCount();\n        this.debug = context.debug();\n    }\n\n    public AnsibleEnv parse(Map<String, Object> args) {\n        env = mergeEnv(defaultEnv(), concordEnv(), args);\n\n        if (eventCorrelationId != null) {\n            env.put(\"CONCORD_EVENT_CORRELATION_ID\", eventCorrelationId.toString());\n        }\n\n        if (retryCount != null) {\n            env.put(\"CONCORD_CURRENT_RETRY_COUNT\", Integer.toString(retryCount));\n        }\n\n        return this;\n    }\n\n    public void write() {\n        if (debug) {\n            StringBuilder b = new StringBuilder();\n            env.forEach((k, v) -> b.append(k).append(\"=\").append(v).append('\\n'));\n            log.info(\"Using environment: {}\", b.toString());\n        }\n    }\n\n    public Map<String, String> get() {\n        return env;\n    }\n\n    public AnsibleEnv put(String key, String value) {\n        env.put(key, value);\n        return this;\n    }\n\n    public AnsibleEnv append(String key, String value, String delimiter) {\n        String current = env.get(key);\n        if (current == null) {\n            return put(key, value);\n        }\n\n        return put(key, value + delimiter + current);\n    }\n\n    /**\n     * Overridable environment variables.\n     */\n    private Map<String, String> defaultEnv() {\n        Map<String, String> env = new HashMap<>();\n        env.put(\"ANSIBLE_FORCE_COLOR\", \"true\");\n        return env;\n    }\n\n    /**\n     * Non-overridable environment variables.\n     */\n    private Map<String, String> concordEnv() {\n        Map<String, String> env = new HashMap<>();\n        env.put(\"CONCORD_INSTANCE_ID\", instanceId.toString());\n        env.put(\"CONCORD_BASE_URL\", apiBaseUrl);\n\n        if (sessionToken != null) {\n            env.put(\"CONCORD_SESSION_TOKEN\", sessionToken);\n        }\n\n        env.put(\"CONCORD_POLICY\", Paths.get(Constants.Files.CONCORD_SYSTEM_DIR_NAME, Constants.Files.POLICY_FILE_NAME).toString());\n\n        if (orgName != null) {\n            env.put(\"CONCORD_CURRENT_ORG_NAME\", orgName);\n        }\n\n        return env;\n    }\n\n    private static Map<String, String> mergeEnv(Map<String, String> defaultEnv, Map<String, String> concordEnv, Map<String, Object> args) {\n        Map<String, Object> extraEnv = MapUtils.getMap(args, TaskParams.EXTRA_ENV_KEY, Collections.emptyMap());\n\n        Map<String, String> result = new HashMap<>(defaultEnv.size() + concordEnv.size() + extraEnv.size());\n        result.putAll(defaultEnv);\n\n        extraEnv.forEach((k, v) -> result.put(k, v.toString()));\n\n        result.putAll(concordEnv);\n        return result;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/AnsibleInventory.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.BufferedWriter;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic class AnsibleInventory {\n\n    public static void process(AnsibleContext context, AnsibleConfig cfg, PlaybookScriptBuilder playbook) throws IOException {\n        List<String> inventories = new AnsibleInventory(context.workDir(), context.tmpDir(), context.debug())\n                .write(context.args(), cfg);\n\n        playbook.withInventories(inventories);\n    }\n\n    private static final Logger log = LoggerFactory.getLogger(AnsibleInventory.class);\n\n    private final Path workDir;\n    private final Path tmpDir;\n    private final boolean debug;\n\n    public AnsibleInventory(Path workDir, Path tmpDir, boolean debug) {\n        this.workDir = workDir;\n        this.tmpDir = tmpDir;\n        this.debug = debug;\n    }\n\n    public List<String> write(Map<String, Object> args, AnsibleConfig cfg) throws IOException {\n        List<Path> paths = writeInventory(args, cfg);\n\n        if (debug) {\n            for (Path p : paths) {\n                log.info(\"INVENTORY: {}\\n{}\", p, new String(Files.readAllBytes(p)));\n            }\n        }\n\n        return paths.stream()\n                .map(workDir::relativize)\n                .map(Path::toString)\n                .collect(Collectors.toList());\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private List<Path> writeInventory(Map<String, Object> args, AnsibleConfig cfg) throws IOException {\n        // try an \"inline\" inventory\n        Object v = MapUtils.get(args, TaskParams.INVENTORY_KEY.getKey(), null);\n\n        // check if there are multiple entries\n        if (v instanceof Collection) {\n            List<Path> l = new ArrayList<>();\n            for (Object vv : (Collection<Object>) v) {\n                if (vv instanceof String) {\n                    l.add(processInventoryFile((String) vv));\n                } else if (vv instanceof Map) {\n                    l.add(processInventoryObject((Map<String, Object>) vv));\n                } else {\n                    throw new IllegalArgumentException(\"Invalid '\" + TaskParams.INVENTORY_KEY.getKey() + \"' entry. Expected a map (YAML/JSON object) or a path to a file, got: (\" + vv.getClass() + \") \" + vv);\n                }\n            }\n            return l;\n        } else if (v instanceof Map) {\n            Path p = processInventoryObject((Map<String, Object>) v);\n            return Collections.singletonList(p);\n        } else if (v != null) {\n            throw new IllegalArgumentException(\"Unsupported '\" + TaskParams.INVENTORY_KEY.getKey() + \"' value. Expected an inventory object or a list of inventory objects, got: (\" + v.getClass() + \") \" + v);\n        }\n\n        // try a static inventory file\n        v = args.get(TaskParams.INVENTORY_FILE_KEY.getKey());\n        if (v instanceof Collection) {\n            List<Path> l = new ArrayList<>();\n            for (Object vv : (Collection<Object>) v) {\n                if (vv instanceof String) {\n                    l.add(processInventoryFile((String) vv));\n                } else {\n                    throw new IllegalArgumentException(\"Invalid '\" + TaskParams.INVENTORY_FILE_KEY + \"' entry. Expected a path to a file, got: (\" + vv.getClass() + \") \" + vv);\n                }\n            }\n            return l;\n        } else if (v instanceof String) {\n            Path p = workDir.resolve(v.toString());\n            if (!Files.exists(p) || !Files.isRegularFile(p)) {\n                throw new IllegalArgumentException(\"File not found: \" + v);\n            }\n\n            return Collections.singletonList(p);\n        } else if (v != null) {\n            throw new IllegalArgumentException(\"Unsupported '\" + TaskParams.INVENTORY_FILE_KEY + \"' value. Expected a path to an inventory file or a list of paths, got: (\" + v.getClass() + \") \" + v);\n        }\n\n        // try an \"old school\" inventory file\n        Path p = workDir.resolve(TaskParams.INVENTORY_FILE_NAME.getKey());\n        if (Files.exists(p)) {\n            log.warn(\"{} is deprecated: use '{}' parameter if you want to specify an existing inventory file\",\n                    TaskParams.INVENTORY_FILE_NAME, TaskParams.INVENTORY_FILE_KEY);\n            return Collections.singletonList(p);\n        }\n\n        // try a dynamic inventory script\n        v = args.get(TaskParams.DYNAMIC_INVENTORY_FILE_KEY.getKey());\n        if (v != null) {\n            p = workDir.resolve(v.toString());\n            Utils.updateScriptPermissions(p);\n            log.info(\"Using a dynamic inventory script: {}\", p);\n            return Collections.singletonList(p);\n        }\n\n        // try an \"old school\" dynamic inventory script\n        p = workDir.resolve(TaskParams.DYNAMIC_INVENTORY_FILE_NAME.getKey());\n        if (Files.exists(p)) {\n            Utils.updateScriptPermissions(p);\n            log.warn(\"Using a deprecated inventory source. Please use '{}', '{}' or '{}' parameter\",\n                    TaskParams.INVENTORY_KEY.getKey(), TaskParams.INVENTORY_FILE_KEY.getKey(),\n                    TaskParams.DYNAMIC_INVENTORY_FILE_KEY.getKey());\n            return Collections.singletonList(p);\n        }\n\n        // try the defaults from the configuration file\n        v = cfg.getDefaults().getString(\"inventory\");\n        if (v != null) {\n            p = workDir.resolve(v.toString());\n            if (Files.exists(p)) {\n                Utils.updateScriptPermissions(p);\n                log.info(\"Using the inventory specified in the configuration file: {}\", p);\n                return Collections.singletonList(p);\n            }\n        }\n\n        // we can't continue without an inventory\n        throw new IOException(\"'\" + TaskParams.INVENTORY_KEY.getKey() + \"', '\" + TaskParams.INVENTORY_FILE_KEY.getKey()\n                + \"' or '\" + TaskParams.DYNAMIC_INVENTORY_FILE_KEY.getKey() + \"' parameter is required\");\n    }\n\n    private Path processInventoryObject(Map<String, Object> m) throws IOException {\n        validateInventoryObject(m);\n\n        Path p = createInventoryFile(tmpDir, m);\n        Utils.updateScriptPermissions(p);\n        return p;\n    }\n\n    private Path processInventoryFile(String s) {\n        Path p = workDir.resolve(s);\n        if (!Files.exists(p) || !Files.isRegularFile(p)) {\n            throw new IllegalArgumentException(\"Inventory file not found: \" + s);\n        }\n        return p;\n    }\n\n    private Path createInventoryFile(Path tmpDir, Map<String, Object> m) throws IOException {\n        Path p = Files.createTempFile(tmpDir, \"inventory\", \".sh\");\n\n        try (BufferedWriter w = Files.newBufferedWriter(p, StandardOpenOption.CREATE)) {\n            w.write(\"#!/bin/sh\");\n            w.newLine();\n            w.write(\"cat << \\\"EOF\\\"\");\n            w.newLine();\n\n            ObjectMapper om = new ObjectMapper();\n            String s = om.writeValueAsString(m);\n            w.write(s);\n            w.newLine();\n\n            w.write(\"EOF\");\n            w.newLine();\n        }\n\n        return p;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static void validateInventoryObject(Map<String, Object> m) {\n        if (m.isEmpty()) {\n            throw new IllegalArgumentException(\"'\" + TaskParams.INVENTORY_KEY.getKey() + \"' object is empty. \" +\n                    \"Check the task's input parameters.\");\n        }\n\n        for (Map.Entry<String, Object> e : m.entrySet()) {\n            String hostGroup = e.getKey();\n\n            Object v = e.getValue();\n            if (v == null) {\n                throw new IllegalArgumentException(\"Invalid '\" + TaskParams.INVENTORY_KEY.getKey() + \"' value. \" +\n                        \"The '\" + hostGroup + \"' host group is empty. Check the task's input parameters.\");\n            }\n\n            if (!(v instanceof Map)) {\n                throw new IllegalArgumentException(\"Invalid '\" + TaskParams.INVENTORY_KEY.getKey() + \"' value. \" +\n                        \"The '\" + hostGroup + \"' host group must be a valid YAML/JSON object (Java Map instance), got: \" + v + \". \" +\n                        \"Check the task's input parameters.\");\n            }\n\n            Map<String, Object> mm = (Map<String, Object>) v;\n            Object vv = mm.get(\"hosts\");\n            if (vv == null) {\n                continue;\n            }\n\n            if (!(vv instanceof Collection)) {\n                throw new IllegalArgumentException(\"Invalid '\" + TaskParams.INVENTORY_KEY.getKey() + \"' value. \" +\n                        \"The '\" + hostGroup + \".hosts' parameter must be a list of strings (host names or IP addresses), got: \" + vv + \". \" +\n                        \"Check the task's input parameters.\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/AnsibleLibs.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\n\npublic class AnsibleLibs {\n\n    public static void process(AnsibleContext context, AnsibleEnv env) {\n        new AnsibleLibs(context.workDir(), context.tmpDir())\n                .enrichEnv(env)\n                .write();\n    }\n\n    private static final Logger log = LoggerFactory.getLogger(AnsibleLibs.class);\n\n    private static final String PYTHON_LIB_DIR = \"_python_lib\";\n\n    private static final String LIB_LOCATION = \"/com/walmartlabs/concord/plugins/ansible/lib\";\n    private static final String[] LIBS = new String[]{\"task_policy.py\", \"process_cfg_policy.py\", \"concord_ansible_stats.py\"};\n\n    private final Path workDir;\n    private final Path tmpDir;\n\n    public AnsibleLibs(Path workDir, Path tmpDir) {\n        this.workDir = workDir;\n        this.tmpDir = tmpDir;\n    }\n\n    public AnsibleLibs write() {\n        try {\n            Resources.copy(LIB_LOCATION, LIBS, tmpDir.resolve(PYTHON_LIB_DIR));\n        } catch (IOException e) {\n            log.error(\"Error while adding Concord-specific libraries: {}\", e.getMessage(), e);\n            throw new RuntimeException(\"Error while adding Concord-specific libraries: \" + e.getMessage());\n        }\n\n        return this;\n    }\n\n    public AnsibleLibs enrichEnv(AnsibleEnv env) {\n        env.append(\"PYTHONPATH\", workDir.relativize(tmpDir.resolve(PYTHON_LIB_DIR)).toString(), \":\");\n        return this;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/AnsibleLookup.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\n\npublic class AnsibleLookup {\n\n    public static void process(AnsibleContext context, AnsibleConfig cfg) {\n        new AnsibleLookup(context.tmpDir())\n                .enrich(cfg)\n                .write();\n    }\n\n    private static final Logger log = LoggerFactory.getLogger(AnsibleLookup.class);\n\n    private static final String LOOKUP_LOCATION = \"/com/walmartlabs/concord/plugins/ansible/lookup\";\n    private static final String[] LOOKUPS = new String[]{\n            \"concord_data_secret.py\",\n            \"concord_inventory.py\",\n            \"concord_public_key_secret.py\",\n            \"concord_secret.py\" };\n\n    private static final String LOOKUP_PLUGINS_DIR = \"_lookups\";\n\n    private final Path tmpDir;\n\n    public AnsibleLookup(Path tmpDir) {\n        this.tmpDir = tmpDir;\n    }\n\n    public AnsibleLookup write() {\n        try {\n            Resources.copy(LOOKUP_LOCATION, LOOKUPS, getDir());\n        } catch (IOException e) {\n            log.error(\"Error while adding Concord lookup plugins: {}\", e.getMessage(), e);\n            throw new RuntimeException(\"Error while adding Concord lookup plugins: \" + e.getMessage());\n        }\n\n        return this;\n    }\n\n    public AnsibleLookup enrich(AnsibleConfig config) {\n        config.getDefaults()\n                .prependPath(\"lookup_plugins\", LOOKUP_PLUGINS_DIR);\n        return this;\n    }\n\n    private Path getDir() {\n        return tmpDir.resolve(LOOKUP_PLUGINS_DIR);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/AnsibleRoles.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PrivilegedAction;\nimport com.walmartlabs.concord.common.TruncBufferedReader;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.BufferedReader;\nimport java.io.InputStreamReader;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.plugins.ansible.ArgUtils.assertString;\nimport static com.walmartlabs.concord.sdk.MapUtils.getList;\nimport static com.walmartlabs.concord.sdk.MapUtils.getString;\n\npublic class AnsibleRoles {\n\n    public static void process(AnsibleContext context, AnsibleConfig cfg) throws Exception {\n        new AnsibleRoles(context.workDir(), context.tmpDir(), context.defaults(), context.debug())\n                .parse(context.args())\n                .enrich(cfg)\n                .downloadRoles()\n                .validate();\n    }\n\n    private static final Logger log = LoggerFactory.getLogger(AnsibleRoles.class);\n\n    private static final int SUCCESS_EXIT_CODE = 0;\n    private static final String ROLE_DIR = \"_roles\";\n\n    private static final String ROLE_NAME_KEY = \"name\";\n    private static final String ROLE_SRC_KEY = \"src\";\n    private static final String ROLE_PATH_KEY = \"path\";\n    private static final String ROLE_VERSION_KEY = \"version\";\n\n    private final Path workDir;\n    private final Path tmpDir;\n    private final Map<String, Object> defaults;\n    private final boolean debug;\n\n    private List<Map<String, String>> roles = Collections.emptyList();\n\n    private AnsibleRoles(Path workDir, Path tmpDir, Map<String, Object> defaults, boolean debug) {\n        this.workDir = workDir;\n        this.tmpDir = tmpDir;\n        this.defaults = defaults;\n        this.debug = debug;\n    }\n\n    private AnsibleRoles parse(Map<String, Object> args) {\n        List<Map<String, Object>> in = getList(args, TaskParams.ROLES_KEY, Collections.emptyList());\n        if (in.isEmpty()) {\n            return this;\n        }\n\n        roles = in.stream()\n                .map(this::assertRole)\n                .collect(Collectors.toList());\n\n        return this;\n    }\n\n    private AnsibleRoles downloadRoles() throws Exception {\n        Path roleDir = workDir.relativize(tmpDir.resolve(ROLE_DIR));\n        for (Map<String, String> e : roles) {\n            String src = e.get(ROLE_SRC_KEY);\n            Path dest = roleDir.resolve(e.get(ROLE_NAME_KEY));\n\n            String[] cmd = new String[]{\"git\", \"clone\", src, dest.toString()};\n            executeCommand(workDir, cmd);\n\n            String version = e.get(ROLE_VERSION_KEY);\n            if (version != null) {\n                executeCommand(dest, new String[]{\"git\", \"checkout\", version});\n            }\n        }\n\n        return this;\n    }\n\n    private void executeCommand(Path workDir, String[] cmd) throws Exception {\n        ProcessBuilder b = new ProcessBuilder()\n                .command(cmd)\n                .directory(workDir.toFile())\n                .redirectErrorStream(true);\n\n        if (debug) {\n            log.info(\"execute -> cmd: {}\", String.join(\" \", cmd));\n        }\n\n        Process p = PrivilegedAction.perform(\"task\", b::start);\n\n        try (BufferedReader reader = new TruncBufferedReader(new InputStreamReader(p.getInputStream()))) {\n            String line;\n            while ((line = reader.readLine()) != null) {\n                log.info(\"GIT: {}\", hideSensitiveData(line));\n            }\n        }\n\n        int code = p.waitFor();\n        if (code != SUCCESS_EXIT_CODE) {\n            log.warn(\"GIT finished with a non-zero exit code: {}\", code);\n            throw new IllegalStateException(\"GIT finished with exit code \" + code);\n        }\n    }\n\n    private AnsibleRoles enrich(AnsibleConfig config) {\n        if (roles.isEmpty()) {\n            return this;\n        }\n\n        ConfigSection cfg = config.getDefaults();\n        cfg.prependPath(\"roles_path\", ROLE_DIR);\n        roles.forEach(r -> cfg.prependPath(\"roles_path\", Paths.get(ROLE_DIR, r.get(ROLE_PATH_KEY)).toString()));\n        return this;\n    }\n\n    private void validate() {\n        Path roleDir = workDir.resolve(tmpDir.resolve(ROLE_DIR));\n        for (Map<String, String> e : roles) {\n            String rolePath = e.get(ROLE_PATH_KEY);\n            if (rolePath == null) {\n                continue;\n            }\n\n            Path dest = roleDir.resolve(rolePath);\n            if (!Files.exists(dest)) {\n                throw new IllegalStateException(\"The specified role path doesn't exist: \" + rolePath);\n            }\n        }\n    }\n\n    private String getDefaultSrc() {\n        return getString(defaults, \"roleSrc\");\n    }\n\n    private String getPath(Map<String, Object> r, String name) {\n        String path = getString(r, ROLE_PATH_KEY);\n        if (path == null || path.isEmpty()) {\n            return name;\n        }\n\n        return name + \"/\" + path;\n    }\n\n    private String assertDefaultSrc() {\n        String src = assertString(\"'roleSrc' is required in default 'ansibleParams'\", defaults, \"roleSrc\");\n        if (!src.endsWith(\"/\")) {\n            src += \"/\";\n        }\n        return src;\n    }\n\n    private Map<String, String> assertRole(Map<String, Object> r) {\n        String name = assertString(\"Role 'name' is required\", r, ROLE_NAME_KEY);\n\n        String src = getString(r, ROLE_SRC_KEY);\n        if (src == null || src.isEmpty()) {\n            src = assertDefaultSrc() + name;\n            name = normalizeName(name);\n        }\n\n        Map<String, String> result = new HashMap<>();\n        result.put(ROLE_NAME_KEY, name);\n        result.put(ROLE_SRC_KEY, src);\n        result.put(ROLE_PATH_KEY, getPath(r, name));\n\n        String version = getString(r, ROLE_VERSION_KEY);\n        if (version != null && !version.isEmpty()) {\n            result.put(ROLE_VERSION_KEY, version);\n        }\n\n        return result;\n    }\n\n    private String hideSensitiveData(String s) {\n        if (s == null) {\n            return null;\n        }\n\n        String src = getDefaultSrc();\n        if (src != null) {\n            s = s.replaceAll(src, \"***\");\n        }\n\n        return s;\n    }\n\n    private static String normalizeName(String name) {\n        int pos = name.indexOf('/');\n        if (pos < 0) {\n            return name;\n        }\n        return name.substring(pos + 1);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/AnsibleTask.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.SerializationFeature;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.plugins.ansible.secrets.AnsibleSecretService;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.nio.file.StandardOpenOption;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.plugins.ansible.ArgUtils.getListAsString;\nimport static com.walmartlabs.concord.sdk.MapUtils.*;\n\npublic class AnsibleTask {\n\n    private static final Logger log = LoggerFactory.getLogger(AnsibleTask.class);\n\n    private static final int SUCCESS_EXIT_CODE = 0;\n\n    private final ApiClient apiClient;\n    private final AnsibleAuthFactory ansibleAuthFactory;\n    private final AnsibleSecretService secretService;\n\n    public AnsibleTask(ApiClient apiClient,\n                       AnsibleAuthFactory ansibleAuthFactory,\n                       AnsibleSecretService secretService) {\n\n        this.apiClient = apiClient;\n        this.ansibleAuthFactory = ansibleAuthFactory;\n        this.secretService = secretService;\n    }\n\n    public TaskResult.SimpleResult run(AnsibleContext context,\n                                       PlaybookProcessRunner playbookProcessRunner) throws Exception {\n\n        String playbook = assertString(context.args(), TaskParams.PLAYBOOK_KEY.getKey());\n        log.info(\"Using a playbook: {}\", playbook);\n\n        AnsibleEnv env = new AnsibleEnv(context)\n                .parse(context.args());\n\n        AnsibleConfig cfg = new AnsibleConfig(context)\n                .parse(context.args())\n                .enrich(env);\n\n        AnsibleCallbacks callbacks = AnsibleCallbacks.process(context, cfg)\n                .startEventSender(context.instanceId(), new ProcessEventsApi(apiClient))\n                .enrich(env);\n\n        AnsibleLibs.process(context, env);\n        AnsibleLookup.process(context, cfg);\n\n        PlaybookScriptBuilder b = new PlaybookScriptBuilder(context, playbook);\n\n        AnsibleInventory.process(context, cfg, b);\n\n        AnsibleVaultId.process(context, b);\n\n        AnsibleRoles.process(context, cfg);\n\n        GroupVarsProcessor groupVarsProcessor = new GroupVarsProcessor(secretService);\n        groupVarsProcessor.process(context, playbook);\n\n        OutVarsProcessor outVarsProcessor = new OutVarsProcessor();\n        outVarsProcessor.prepare(context, env.get());\n\n        AnsibleAuth auth = ansibleAuthFactory.create(context)\n                .enrich(env, context)\n                .enrich(b);\n\n        cfg.write();\n        env.write();\n\n        boolean checkMode = getBoolean(context.args(), TaskParams.CHECK_KEY.getKey(), false);\n        if (checkMode) {\n            log.warn(\"Running in the check mode. No changes will be made.\");\n        }\n\n        boolean syntaxCheck = getBoolean(context.args(), TaskParams.SYNTAX_CHECK_KEY.getKey(), false);\n        if (syntaxCheck) {\n            log.warn(\"Running in the syntax check mode. No changes will be made.\");\n        }\n\n        Virtualenv virtualenv = Virtualenv.create(context);\n\n        boolean skipCheckBinary = getBoolean(context.args(), TaskParams.SKIP_CHECK_BINARY.getKey(), false);\n\n        try {\n            Path workDir = context.workDir();\n            Path attachmentsPath = workDir.relativize(workDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME));\n\n            b = b.withAttachmentsDir(attachmentsPath.toString())\n                    .withDebug(context.debug())\n                    .withTags(getListAsString(context.args(), TaskParams.TAGS_KEY))\n                    .withSkipTags(getListAsString(context.args(), TaskParams.SKIP_TAGS_KEY))\n                    .withExtraVars(getMap(context.args(), TaskParams.EXTRA_VARS_KEY.getKey(), null))\n                    .withExtraVarsFiles(getList(context.args(), TaskParams.EXTRA_VARS_FILES_KEY.getKey(), null))\n                    .withLimit(getLimit(context.args(), playbook))\n                    .withVerboseLevel(getVerboseLevel(context.args()))\n                    .withCheck(checkMode)\n                    .withSyntaxCheck(syntaxCheck)\n                    .withEnv(env.get())\n                    .withVirtualenv(virtualenv)\n                    .withSkipCheckBinary(skipCheckBinary);\n\n            auth.prepare();\n\n            int code = playbookProcessRunner\n                    .withDebug(context.debug())\n                    .run(b.buildArgs(), b.buildEnv());\n\n            log.debug(\"execution -> done, code {}\", code);\n\n            updateAnsibleStats(workDir, code);\n            updateAnsibleStatsV2(workDir, code);\n\n            Map<String, Object> result = outVarsProcessor.process();\n\n            boolean success = code == SUCCESS_EXIT_CODE;\n            if (!success) {\n                saveRetryFile(context.args(), workDir);\n                log.warn(\"Playbook is finished with code {}\", code);\n            }\n\n            return TaskResult.of(success)\n                    .values(result)\n                    .value(\"exitCode\", code);\n        } finally {\n            callbacks.stopEventSender();\n\n            auth.postProcess();\n            groupVarsProcessor.postProcess();\n            outVarsProcessor.postProcess();\n\n            virtualenv.destroy();\n        }\n    }\n\n    private static String getLimit(Map<String, Object> args, String playbook) {\n        boolean debug = getBoolean(args, TaskParams.DEBUG_KEY.getKey(), false);\n\n        boolean retry = getBoolean(args, TaskParams.RETRY_KEY.getKey(), false);\n        if (retry) {\n            String s = \"@\" + getNameWithoutExtension(playbook) + \".retry\";\n            if (debug) {\n                log.info(\"Using a limit file: {}\", s);\n            }\n            return s;\n        }\n\n        String limit = getListAsString(args, TaskParams.LIMIT_KEY);\n        if (limit != null) {\n            if (debug) {\n                log.info(\"Using the limit value: {}\", limit);\n            }\n            return limit;\n        }\n\n        return null;\n    }\n\n    private static String getNameWithoutExtension(String fileName) {\n        int i = fileName.lastIndexOf('.');\n        return (i == -1) ? fileName : fileName.substring(0, i);\n    }\n\n    private static int getVerboseLevel(Map<String, Object> args) {\n        return getNumber(args, TaskParams.VERBOSE_LEVEL_KEY.getKey(), 0).intValue();\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    @Deprecated\n    private static void updateAnsibleStats(Path workDir, int code) throws IOException {\n        Path p = workDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(TaskParams.STATS_FILE_NAME.getKey());\n        if (!Files.exists(p)) {\n            return;\n        }\n\n        ObjectMapper om = new ObjectMapper()\n                .enable(SerializationFeature.INDENT_OUTPUT);\n\n        Map<String, Object> m = new HashMap<>();\n        try (InputStream in = Files.newInputStream(p)) {\n            Map<String, Object> mm = om.readValue(in, Map.class);\n            m.putAll(mm);\n        }\n\n        m.put(TaskParams.EXIT_CODE_KEY.getKey(), code);\n\n        try (OutputStream out = Files.newOutputStream(p, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {\n            om.writeValue(out, m);\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static void updateAnsibleStatsV2(Path workDir, int code) throws IOException {\n        Path p = workDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(\"ansible_stats_v2.json\");\n        if (!Files.exists(p)) {\n            return;\n        }\n\n        ObjectMapper om = new ObjectMapper()\n                .enable(SerializationFeature.INDENT_OUTPUT);\n\n        List<Map<String, Object>> stats = new ArrayList<>();\n        try (InputStream in = Files.newInputStream(p)) {\n            stats.addAll(om.readValue(in, List.class));\n        }\n\n        if (stats.isEmpty()) {\n            return;\n        }\n\n        Map<String, Object> currentStats = new HashMap<>(stats.get(stats.size() - 1));\n        currentStats.put(TaskParams.EXIT_CODE_KEY.getKey(), code);\n        stats.set(stats.size() - 1, currentStats);\n\n        try (OutputStream out = Files.newOutputStream(p, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {\n            om.writeValue(out, stats);\n        }\n    }\n\n    private static void saveRetryFile(Map<String, Object> args, Path workDir) throws IOException {\n        boolean saveRetryFiles = getBoolean(args, TaskParams.SAVE_RETRY_FILE.getKey(), false);\n        if (!saveRetryFiles) {\n            return;\n        }\n\n        String playbookName = getString(args, TaskParams.PLAYBOOK_KEY.getKey());\n        String retryFile = getNameWithoutExtension(playbookName) + \".retry\";\n\n        Path src = workDir.resolve(retryFile);\n        if (!Files.exists(src)) {\n            log.warn(\"Retry file not found: {}\", src);\n            return;\n        }\n\n        Path dst = workDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(src.getFileName());\n        Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);\n        log.info(\"The retry file was saved as: {}\", workDir.relativize(dst));\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/AnsibleVaultId.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class AnsibleVaultId {\n\n    public static void process(AnsibleContext context, PlaybookScriptBuilder playbook) {\n        Map<String, Path> vaultIds = new AnsibleVaultId(context.workDir(), context.tmpDir())\n                .parse(context.args())\n                .getVaultIds();\n\n        if (vaultIds != null) {\n            playbook.withVaultIds(vaultIds);\n        }\n    }\n\n    private static final Logger log = LoggerFactory.getLogger(AnsibleVaultId.class);\n\n    private final Path workDir;\n    private final Path tmpDir;\n\n    private Map<String, Path> vaultIds;\n\n    private AnsibleVaultId(Path workDir, Path tmpDir) {\n        this.workDir = workDir;\n        this.tmpDir = tmpDir;\n    }\n\n    private AnsibleVaultId parse(Map<String, Object> args) {\n        try {\n            vaultIds = getVaultIds(args);\n            return this;\n        } catch (IOException e) {\n            log.error(\"Error while parsing the vault password configuration: {}\", e.getMessage());\n            throw new RuntimeException(\"Error while parsing the vault password configuration: \" + e.getMessage());\n        }\n    }\n\n    private Map<String, Path> getVaultIds() {\n        if (vaultIds == null) {\n            return null;\n        }\n\n        Map<String, Path> m = new HashMap<>();\n        for (Map.Entry<String, Path> e : vaultIds.entrySet()) {\n            if (e.getValue() != null) {\n                m.put(e.getKey(), workDir.relativize(e.getValue()));\n            }\n        }\n\n        return m;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private Map<String, Path> getVaultIds(Map<String, Object> args) throws IOException {\n        Map<String, Path> paths = new HashMap<>();\n\n        // check if there is a path to a vault password file\n        Object v = args.get(TaskParams.VAULT_PASSWORD_FILE_KEY.getKey());\n        if (v instanceof String) {\n            String key = TaskParams.VAULT_PASSWORD_FILE_KEY.getKey();\n            paths.put(key, getVaultPasswordFilePath(key, v));\n            return paths;\n\n        } else if (v instanceof Map) {\n            Map<String, Object> m = (Map<String, Object>) v;\n            for (Map.Entry<String, Object> e : m.entrySet()) {\n                paths.put(e.getKey(), getVaultPasswordFilePath(e.getKey(), e.getValue()));\n            }\n            return paths;\n        }\n\n        // try an \"inline\" password\n        v = args.get(TaskParams.VAULT_PASSWORD_KEY.getKey());\n        if (v instanceof String) {\n            String key = TaskParams.VAULT_PASSWORD_KEY.getKey();\n            paths.put(key, storePassword(key, v));\n            return paths;\n        } else if (v instanceof Map) {\n            Map<String, Object> m = (Map<String, Object>) v;\n            for (Map.Entry<String, Object> e : m.entrySet()) {\n                paths.put(e.getKey(), storePassword(e.getKey(), e.getValue()));\n            }\n            return paths;\n        } else if (v != null) {\n            throw new IllegalArgumentException(\"Invalid '\" + TaskParams.VAULT_PASSWORD_KEY.getKey() + \"' type: \" + v);\n        }\n\n        Path p = workDir.resolve(TaskParams.VAULT_PASSWORD_FILE_PATH.getKey());\n        if (!Files.exists(p)) {\n            return null;\n        }\n\n        return paths;\n    }\n\n    private Path getVaultPasswordFilePath(String key, Object o) throws IOException {\n        Path p = ArgUtils.getPath(key, o, workDir);\n        if (p != null) {\n            if (isAScript(p)) {\n                Utils.updateScriptPermissions(p);\n            }\n\n            log.info(\"Using the provided vault password file: {}\", workDir.relativize(p));\n        }\n        return p;\n    }\n\n    private Path storePassword(String key, Object o) throws IOException {\n        if (o == null) {\n            throw new IllegalArgumentException(\"'\" + key + \"' vault's password is empty. \" +\n                    \"Check the task's '\" + TaskParams.VAULT_PASSWORD_KEY.getKey() + \"' parameter.\");\n        }\n\n        if (!(o instanceof String)) {\n            throw new IllegalArgumentException(\"Invalid '\" + key + \"' vault's password value. \" +\n                    \"Expected a string, got: \" + o + \". \" +\n                    \"Check the task's '\" + TaskParams.VAULT_PASSWORD_KEY.getKey() + \"' parameter.\");\n        }\n\n        Path p = Files.createTempFile(tmpDir, key, \".vault\");\n        Files.write(p, ((String) o).getBytes(), StandardOpenOption.CREATE);\n        return p;\n    }\n\n    private static boolean isAScript(Path p) {\n        String n = p.getFileName().toString().toLowerCase();\n        return n.endsWith(\".sh\") || n.endsWith(\".py\");\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/ArgUtils.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.MapUtils;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\npublic final class ArgUtils {\n\n    public static Path getPath(Map<String, Object> args, String key, Path workDir) {\n        Object v = args.get(key);\n        return getPath(key, v, workDir);\n    }\n\n    public static Path getPath(String key, Object o, Path workDir) {\n        Path p = null;\n\n        if (o instanceof String) {\n            p = workDir.resolve((String) o);\n        } else if (o instanceof Path) {\n            p = workDir.resolve((Path) o);\n        } else if (o != null) {\n            throw new IllegalArgumentException(\"'\" + key + \"' should be either a relative path: \" + o);\n        }\n\n        if (p != null && !Files.exists(p)) {\n            throw new IllegalArgumentException(\"File not found: \" + workDir.relativize(p));\n        }\n\n        return p;\n    }\n\n    public static String assertString(String assertionMessage, Map<String, Object> args, String key) {\n        String v = MapUtils.getString(args, key);\n        if (v == null) {\n            throw new IllegalArgumentException(assertionMessage);\n        }\n        return v;\n    }\n\n    public static String getListAsString(Map<String, Object> args, TaskParams c) {\n        Object v = args.get(c.getKey());\n        if (v == null) {\n            return null;\n        }\n\n        if (v instanceof String) {\n            return ((String) v).trim();\n        }\n\n        Collection<String> items = null;\n        if (v instanceof Collection) {\n            items = ((Collection<?>) v).stream().map(Object::toString).collect(Collectors.toList());\n        } else if (v.getClass().isArray()) {\n            items = Arrays.stream((Object[]) v).map(Object::toString).collect(Collectors.toList());\n        }\n\n        if (items != null) {\n            return String.join(\", \", items);\n        }\n\n        throw new IllegalArgumentException(\"Unexpected '\" + c.getKey() + \"' type: \" + v);\n    }\n\n    private ArgUtils() {\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/ConfigSection.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Map;\n\npublic class ConfigSection {\n\n    private final Map<String, Object> items;\n\n    public ConfigSection(Map<String, Object> items) {\n        this.items = items;\n    }\n\n    public ConfigSection prependPath(String key, String path) {\n        String current = getString(key);\n        if (current != null) {\n            items.put(key, path + \":\" + current);\n        } else {\n            items.put(key, path);\n        }\n        return this;\n    }\n\n    public ConfigSection put(String key, String value) {\n        items.put(key, value);\n        return this;\n    }\n\n    public String getString(String key) {\n        Object v = items.get(key);\n        if (v == null) {\n            return null;\n        }\n        if (v instanceof String s) {\n            return s;\n        }\n        throw new RuntimeException(\"Expected a string value in '%s', got %s (%s)\".formatted(key, v, v.getClass()));\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/DefaultPlaybookProcessRunner.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PrivilegedAction;\nimport com.walmartlabs.concord.common.TruncBufferedReader;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.Map;\n\npublic class DefaultPlaybookProcessRunner implements PlaybookProcessRunner {\n\n    private static final Logger log = LoggerFactory.getLogger(DefaultPlaybookProcessRunner.class);\n    private static final Logger processLog = LoggerFactory.getLogger(\"processLog\");\n\n    private final Path workDir;\n\n    private boolean debug;\n\n    public DefaultPlaybookProcessRunner(Path workDir) {\n        this.workDir = workDir;\n    }\n\n    public DefaultPlaybookProcessRunner withDebug(boolean debug) {\n        this.debug = debug;\n        return this;\n    }\n\n    @Override\n    public int run(List<String> args, Map<String, String> extraEnv) throws IOException, InterruptedException {\n        File pwd = workDir.toFile();\n        if (!pwd.exists()) {\n            throw new IOException(\"Working directory not found: \" + pwd);\n        }\n\n        if (debug) {\n            log.info(\"build -> working directory: {}\", pwd);\n        }\n\n        String[] cmd = args.toArray(new String[0]);\n\n        if (debug) {\n            log.info(\"build -> cmd: {}\", String.join(\" \", cmd));\n        }\n\n        java.lang.ProcessBuilder b = new java.lang.ProcessBuilder()\n                .command(cmd)\n                .directory(pwd)\n                .redirectErrorStream(true);\n\n        Map<String, String> env = b.environment();\n        env.putAll(extraEnv);\n\n        if (debug) {\n            log.info(\"build -> env: {}\", env);\n        }\n\n        Process p = PrivilegedAction.perform(\"task\", b::start);\n\n        BufferedReader reader = new TruncBufferedReader(new InputStreamReader(p.getInputStream()));\n        String line;\n        while ((line = reader.readLine()) != null) {\n            processLog.info(\"ANSIBLE: {}\", line);\n        }\n\n        return p.waitFor();\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/DeprecatedArgsProcessor.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\n@SuppressWarnings(\"deprecation\")\npublic class DeprecatedArgsProcessor {\n\n    private static final Logger log = LoggerFactory.getLogger(DeprecatedArgsProcessor.class);\n\n    public static Map<String, Object> process(Path workDir, Map<String, Object> args) {\n        Map<String, Object> result = new HashMap<>(args);\n\n        processPrivateKey(workDir, result);\n\n        return result;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static void processPrivateKey(Path workDir, Map<String, Object> args) {\n        Object privateKey = args.get(TaskParams.PRIVATE_KEY_FILE_KEY.getKey());\n        if (privateKey != null) {\n            log.warn(\"'{}' is deprecated, please use '{}.{}' parameter\", TaskParams.PRIVATE_KEY_FILE_KEY.getKey(), TaskParams.AUTH.getKey(), \"privateKey\");\n        }\n\n        Object user = args.get(TaskParams.USER_KEY.getKey());\n        if (user != null) {\n            log.warn(\"'{}' is deprecated, please use '{}.{}' parameter\", TaskParams.USER_KEY.getKey(), TaskParams.AUTH.getKey(), \"user\");\n        }\n\n        Map<String, Object> privateKeyParams = new HashMap<>();\n        if (privateKey instanceof Map) {\n\n            Map<String, Object> m = (Map<String, Object>) privateKey;\n            String name = (String) m.get(\"secretName\");\n            String password = (String) m.get(\"password\");\n            String orgName = (String) m.get(\"org\");\n\n            Map<String, Object> secretParams = new HashMap<>();\n            secretParams.put(\"org\", orgName);\n            secretParams.put(\"name\", name);\n            secretParams.put(\"password\", password);\n\n            privateKeyParams.put(\"secret\", secretParams);\n            privateKeyParams.put(\"user\", user);\n        } else {\n            String path = (String) privateKey;\n            if (path == null) {\n                path = TaskParams.PRIVATE_KEY_FILE_NAME.getKey();\n                if (!Files.exists(workDir.resolve(path))) {\n                    path = null;\n                } else {\n                    log.warn(\"'{}' is deprecated, please use '{}.{}' parameter\", TaskParams.PRIVATE_KEY_FILE_NAME.getKey(), TaskParams.AUTH.getKey(), \"privateKey\");\n                }\n            }\n            if (path != null) {\n                privateKeyParams.put(\"path\", path);\n                privateKeyParams.put(\"user\", user);\n            }\n        }\n\n        if (!privateKeyParams.isEmpty()) {\n            args.put(TaskParams.AUTH.getKey(), Collections.singletonMap(\"privateKey\", privateKeyParams));\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/DockerExtraHosts.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Map;\n\npublic class DockerExtraHosts {\n\n    @SuppressWarnings(\"unchecked\")\n    public static Collection<String> getHosts(Map<String, Object> options) {\n        if (options == null) {\n            return Collections.emptyList();\n        }\n\n        Object o = options.get(\"hosts\");\n        if (o == null) {\n            return Collections.emptyList();\n        }\n\n        if (o instanceof Collection) {\n            return (Collection<String>) o;\n        }\n\n        throw new IllegalArgumentException(\"Unexpected 'hosts' type: \" + o);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/DockerPlaybookProcessRunner.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.plugins.ansible.docker.AnsibleDockerService;\nimport com.walmartlabs.concord.plugins.ansible.docker.AnsibleDockerService.DockerContainerSpec;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\n\npublic class DockerPlaybookProcessRunner implements PlaybookProcessRunner {\n\n    private final AnsibleDockerService dockerService;\n    private final String image;\n\n    private Collection<String> hosts;\n    private boolean debug = false;\n    private boolean forcePull = true;\n    private int pullRetryCount = 0;\n    private long pullRetryInterval = 0;\n\n    public DockerPlaybookProcessRunner(AnsibleDockerService dockerService, String image) {\n        this.dockerService = dockerService;\n        this.image = image;\n    }\n\n    @Override\n    public PlaybookProcessRunner withDebug(boolean debug) {\n        this.debug = debug;\n        return this;\n    }\n\n    public DockerPlaybookProcessRunner withForcePull(boolean forcePull) {\n        this.forcePull = forcePull;\n        return this;\n    }\n\n    public DockerPlaybookProcessRunner withHosts(Collection<String> hosts) {\n        this.hosts = hosts;\n        return this;\n    }\n\n    public DockerPlaybookProcessRunner withPullRetryCount(int count) {\n        this.pullRetryCount = count;\n        return this;\n    }\n\n    public DockerPlaybookProcessRunner withPullRetryInterval(long interval) {\n        this.pullRetryInterval = interval;\n        return this;\n    }\n\n    @Override\n    public int run(List<String> args, Map<String, String> extraEnv) throws Exception {\n        return dockerService.start(new DockerContainerSpec()\n                .image(image)\n                .args(args)\n                .env(extraEnv)\n                .debug(debug)\n                .forcePull(forcePull)\n                .extraDockerHosts(hosts)\n                .pullRetryCount(pullRetryCount)\n                .pullRetryInterval(pullRetryInterval));\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/EventSender.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\nimport com.walmartlabs.concord.client2.ApiException;\nimport com.walmartlabs.concord.client2.ProcessEventRequest;\nimport com.walmartlabs.concord.client2.ProcessEventsApi;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.io.RandomAccessFile;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\n\n/**\n * Reads data recorded by concord_events.py and sends it to the Server.\n */\npublic class EventSender {\n\n    private static final Logger log = LoggerFactory.getLogger(EventSender.class);\n\n    private static final String EOL_MARKER = \"<~EOL~>\";\n\n    private static final long NO_DATA_DELAY = 1000;\n    private static final long API_ERROR_DELAY = 10000;\n    private static final long MAX_BATCH_SIZE = 10;\n    private static final long MAX_BATCH_AGE = 5000;\n\n    private final boolean debug;\n    private final UUID instanceId;\n    private final Path eventsFile;\n    private final ProcessEventsApi eventsApi;\n\n    private final ObjectMapper objectMapper = createObjectMapper();\n\n    private static ObjectMapper createObjectMapper() {\n        ObjectMapper om = new ObjectMapper();\n        om.registerModule(new JavaTimeModule());\n        return om;\n    }\n\n    private final ExecutorService executor = Executors.newSingleThreadExecutor();\n\n    private volatile boolean stop = false;\n\n    public EventSender(boolean debug, UUID instanceId, Path eventsFile, ProcessEventsApi eventsApi) {\n        this.debug = debug;\n        this.instanceId = instanceId;\n        this.eventsFile = eventsFile;\n        this.eventsApi = eventsApi;\n    }\n\n    public Future<?> start() {\n        return executor.submit(this::doRun);\n    }\n\n    public void stop() {\n        this.stop = true;\n    }\n\n    public void doRun() {\n        if (debug) {\n            log.info(\"run -> started...\");\n        }\n\n        try (RandomAccessFile f = new RandomAccessFile(eventsFile.toFile(), \"r\")) {\n            Batch batch = new Batch(instanceId, eventsApi);\n            long t1 = System.currentTimeMillis();\n\n            while (true) {\n                String line = f.readLine();\n\n                if (line == null || line.isEmpty()) {\n                    if (stop) {\n                        // looks like the end of the play\n\n                        // don't stop until we reach the end of the file\n                        if (Files.size(eventsFile) <= f.getFilePointer()) {\n                            break;\n                        }\n                    }\n\n                    // wait for more data\n                    sleep(NO_DATA_DELAY);\n                } else {\n                    if (line.endsWith(EOL_MARKER)) {\n                        String data = line.substring(0, line.length() - EOL_MARKER.length());\n                        ProcessEventRequest req = objectMapper.readValue(data, ProcessEventRequest.class);\n                        batch.add(req);\n                    } else {\n                        // partial line, re-read next time\n                        f.seek(f.getFilePointer() - line.length());\n                    }\n                }\n\n                long t2 = System.currentTimeMillis();\n                if (batch.size() >= MAX_BATCH_SIZE || t2 - t1 >= MAX_BATCH_AGE) {\n                    flush(batch);\n                    t1 = t2;\n                }\n            }\n\n            flush(batch);\n        } catch (IOException e) {\n            log.error(\"Error while reading the event file: {}\", e.getMessage(), e);\n        }\n\n        if (debug) {\n            log.info(\"run -> stopped...\");\n        }\n    }\n\n    private static void flush(Batch b) {\n        try {\n            b.flush();\n        } catch (ApiException e) {\n            log.warn(\"Error while sending the event to the server\", e);\n            sleep(API_ERROR_DELAY);\n        }\n    }\n\n    private static void sleep(long ms) {\n        try {\n            Thread.sleep(ms);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    private static class Batch {\n\n        private final UUID instanceId;\n        private final ProcessEventsApi eventsApi;\n\n        private final List<ProcessEventRequest> items = new ArrayList<>();\n\n        private Batch(UUID instanceId, ProcessEventsApi eventsApi) {\n            this.instanceId = instanceId;\n            this.eventsApi = eventsApi;\n        }\n\n        public void add(ProcessEventRequest req) {\n            items.add(req);\n        }\n\n        public void flush() throws ApiException {\n            eventsApi.batchEvent(instanceId, items);\n            items.clear();\n        }\n\n        public int size() {\n            return items.size();\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/GroupVarsProcessor.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.plugins.ansible.secrets.AnsibleSecretService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.sdk.MapUtils.assertString;\nimport static com.walmartlabs.concord.sdk.MapUtils.getString;\n\npublic class GroupVarsProcessor {\n\n    private final Logger log = LoggerFactory.getLogger(GroupVarsProcessor.class);\n\n    private final AnsibleSecretService secretService;\n\n    private final Collection<Path> exportedFiles = new HashSet<>();\n\n    public GroupVarsProcessor(AnsibleSecretService secretService) {\n        this.secretService = secretService;\n    }\n\n    public void process(AnsibleContext context, String playbook) throws Exception {\n        Collection<Ref> refs = toRefs(context.args());\n        if (refs == null) {\n            return;\n        }\n\n        Path playbookPath = context.workDir().resolve(playbook);\n\n        Path groupVarsBase = playbookPath.getParent().resolve(\"group_vars\");\n        if (Files.notExists(groupVarsBase)) {\n            Files.createDirectories(groupVarsBase);\n        }\n\n        for (Ref r : refs) {\n            export(r, groupVarsBase);\n        }\n    }\n\n    public void postProcess() throws Exception {\n        for (Path p : exportedFiles) {\n            Files.deleteIfExists(p);\n            log.info(\"Removed exported file {}\", p);\n        }\n    }\n\n    private void export(Ref r, Path groupVarsBase) throws Exception {\n        Path src = secretService.exportAsFile(r.orgName, r.secretName, r.password);\n\n        Path dst = groupVarsBase.resolve(r.groupName + \".\" + r.type);\n        if (Files.exists(dst)) {\n            throw new IllegalArgumentException(\"Can't export a group_vars file, the destination file already exists: \" + dst);\n        }\n\n        Files.move(src, dst);\n        exportedFiles.add(dst);\n\n        log.info(\"Exported secret '{}' into {}\", r.secretName, dst);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Collection<Ref> toRefs(Map<String, Object> args) {\n        Object v = args.get(TaskParams.GROUP_VARS_KEY.getKey());\n        if (v == null) {\n            return null;\n        }\n\n        if (!(v instanceof Collection)) {\n            throw new IllegalArgumentException(\"'\" + TaskParams.GROUP_VARS_KEY.getKey() + \"' must be a list of group_vars references\");\n        }\n\n        Collection<Object> refs = (Collection<Object>) v;\n        if (refs.isEmpty()) {\n            return null;\n        }\n\n        return refs.stream()\n                .map(GroupVarsProcessor::toRef)\n                .collect(Collectors.toList());\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Ref toRef(Object o) {\n        if (!(o instanceof Map)) {\n            throw new IllegalArgumentException(\"Unsupported object type in '\" + TaskParams.GROUP_VARS_KEY.getKey() + \"', got: \" + o);\n        }\n\n        Map<String, Object> r = (Map<String, Object>) o;\n        if (r.size() != 1) {\n            throw new IllegalArgumentException(\"Invalid reference format in '\" + TaskParams.GROUP_VARS_KEY.getKey() + \"', expected a single value, got: \" + r);\n        }\n\n        Map.Entry<String, Object> e = r.entrySet().iterator().next();\n\n        String groupName = e.getKey();\n\n        Object v = e.getValue();\n        if (!(v instanceof Map)) {\n            throw new IllegalArgumentException(\"Invalid reference value in '\" + TaskParams.GROUP_VARS_KEY.getKey() + \"', expected an object, got: \" + v);\n        }\n\n        Map<String, Object> params = (Map<String, Object>) v;\n\n        String orgName = getString(params, \"orgName\", null);\n        orgName = getString(params, \"org\", orgName); // alternative parameter name\n\n        String secretName = assertString(params, \"secretName\");\n\n        String password = getString(params, \"password\", null);\n        String type = getString(params, \"type\", \"yml\");\n\n        return new Ref(groupName, orgName, secretName, password, type);\n    }\n\n    private static class Ref {\n\n        private final String groupName;\n        private final String orgName;\n        private final String secretName;\n        private final String password;\n        private final String type;\n\n        private Ref(String groupName, String orgName, String secretName, String password, String type) {\n            this.groupName = groupName;\n            this.orgName = orgName;\n            this.secretName = secretName;\n            this.password = password;\n            this.type = type;\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/KerberosAuth.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.apache.kerby.KOptions;\nimport org.apache.kerby.kerberos.kerb.KrbException;\nimport org.apache.kerby.kerberos.kerb.ccache.CredentialCache;\nimport org.apache.kerby.kerberos.kerb.client.ClientUtil;\nimport org.apache.kerby.kerberos.kerb.client.KrbClient;\nimport org.apache.kerby.kerberos.kerb.client.KrbKdcOption;\nimport org.apache.kerby.kerberos.kerb.client.KrbOption;\nimport org.apache.kerby.kerberos.kerb.type.ticket.TgtTicket;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Date;\nimport java.util.concurrent.TimeUnit;\n\nimport static com.walmartlabs.concord.sdk.MapUtils.getString;\nimport static java.nio.file.StandardCopyOption.ATOMIC_MOVE;\nimport static java.nio.file.StandardCopyOption.REPLACE_EXISTING;\n\npublic class KerberosAuth implements AnsibleAuth {\n\n    private static final Logger log = LoggerFactory.getLogger(KerberosAuth.class);\n\n    private static final long ERROR_RETRY = TimeUnit.SECONDS.toMillis(30);\n    private static final long RENEW = TimeUnit.MINUTES.toMillis(1);\n\n    private final String username;\n    private final String password;\n    private final Path tgtCacheFile;\n    private final Path tgtTmpCacheFile;\n\n    private Thread renewThread;\n\n    public KerberosAuth(String username, String password, Path tmpDir, boolean debug) {\n        this.username = username;\n        this.password = password;\n        this.tgtCacheFile = tmpDir.resolve(\"tgt-ticket\");\n        this.tgtTmpCacheFile = tmpDir.resolve(\"tmp-tgt-ticket\");\n        if (debug) {\n            System.setProperty(\"sun.security.krb5.debug\", \"true\");\n        }\n    }\n\n    @Override\n    public KerberosAuth enrich(AnsibleEnv env, AnsibleContext context) {\n        String dockerImage = getString(context.args(), TaskParams.DOCKER_IMAGE_KEY.getKey());\n        if (dockerImage != null) {\n            env.put(\"KRB5CCNAME\", Paths.get(\"/workspace\").resolve(context.workDir().relativize(tgtCacheFile)).toString());\n        }\n        else {\n            env.put(\"KRB5CCNAME\", tgtCacheFile.toString());\n        }\n        return this;\n    }\n\n    @Override\n    public KerberosAuth enrich(PlaybookScriptBuilder p) {\n        p.withExtraSshArgs(\"-o GSSAPIAuthentication=yes\")\n                .withUser(username);\n        return this;\n    }\n\n    @Override\n    public void prepare() throws Exception {\n        long expiredAt = storeTgt(tgtCacheFile);\n        log.info(\"TGT obtained, expired at '{}'\", new Date(expiredAt));\n\n        renewThread = new Thread(new TgtRenew(tgtTmpCacheFile, tgtCacheFile, expiredAt), \"tgt-renew\");\n        renewThread.start();\n    }\n\n    public void postProcess() {\n        if (renewThread != null) {\n            renewThread.interrupt();\n            try {\n                renewThread.join();\n            } catch (Exception e) {\n                log.warn(\"postProcess -> error\", e);\n            }\n            renewThread = null;\n        }\n\n        try {\n            Files.deleteIfExists(tgtCacheFile);\n        } catch (Exception e) {\n            log.warn(\"postProcess -> error\", e);\n        }\n\n        try {\n            Files.deleteIfExists(tgtTmpCacheFile);\n        } catch (Exception e) {\n            log.warn(\"postProcess -> error\", e);\n        }\n    }\n\n    private long storeTgt(Path dest) throws IOException, KrbException {\n        Files.deleteIfExists(dest);\n\n        KOptions requestOptions = new KOptions();\n        requestOptions.add(KrbOption.CLIENT_PRINCIPAL, username);\n        requestOptions.add(KrbOption.USE_PASSWD, true);\n        requestOptions.add(KrbOption.USER_PASSWD, password);\n        requestOptions.add(KrbOption.CONN_TIMEOUT, 30000);\n        requestOptions.add(KrbKdcOption.FORWARDABLE, false);\n        requestOptions.add(KrbKdcOption.PROXIABLE, false);\n\n        KrbClient krbClient = new KrbClient(ClientUtil.getDefaultConfig());\n        krbClient.init();\n\n        TgtTicket ticket = krbClient.requestTgt(requestOptions);\n\n        CredentialCache cache = new CredentialCache(ticket);\n        try {\n            cache.store(dest.toFile());\n        } catch (IOException e) {\n            throw new KrbException(\"Failed to store tgt\", e);\n        }\n\n        return ticket.getEncKdcRepPart().getEndTime().getTime();\n    }\n\n    private class TgtRenew implements Runnable {\n\n        private final Path tgtTmpCacheFile;\n        private final Path tgtCacheFile;\n\n        private long expiredAt;\n\n        public TgtRenew(Path tgtTmpCacheFile, Path tgtCacheFile, long expiredAt) {\n            this.tgtTmpCacheFile = tgtTmpCacheFile;\n            this.tgtCacheFile = tgtCacheFile;\n            this.expiredAt = expiredAt;\n        }\n\n        @Override\n        public void run() {\n            while (!Thread.currentThread().isInterrupted()) {\n                try {\n                    long now = System.currentTimeMillis();\n                    long renewAt = expiredAt - RENEW;\n                    if (renewAt <= now) {\n                        expiredAt = storeTgt(tgtTmpCacheFile);\n                        Files.move(tgtTmpCacheFile, tgtCacheFile, ATOMIC_MOVE, REPLACE_EXISTING);\n                        log.info(\"TGT obtained, expires at '{}'\", new Date(expiredAt));\n                    } else {\n                        long timeToSleep = renewAt - now;\n                        if (timeToSleep > 0) {\n                            log.info(\"TGT renew at {}\", new Date(renewAt));\n                            sleep(timeToSleep);\n                        }\n                    }\n                } catch (Exception e) {\n                    log.error(\"TGT get error: {}, retry in {} ms\", e.getMessage(), ERROR_RETRY);\n                    sleep(ERROR_RETRY);\n                }\n            }\n        }\n\n        private void sleep(long ms) {\n            try {\n                Thread.sleep(ms);\n            } catch (InterruptedException e) {\n                Thread.currentThread().interrupt();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/NopAuth.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class NopAuth implements AnsibleAuth {\n\n    @Override\n    public void prepare() {\n        // do nothing\n    }\n\n    @Override\n    public AnsibleAuth enrich(AnsibleEnv env, AnsibleContext context) {\n        // do nothing\n        return this;\n    }\n\n    @Override\n    public AnsibleAuth enrich(PlaybookScriptBuilder p) {\n        // do nothing\n        return this;\n    }\n\n    @Override\n    public void postProcess() {\n        // do nothing\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/OutVarsProcessor.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class OutVarsProcessor {\n\n    private Path outVarsFile;\n\n    public void prepare(AnsibleContext context, Map<String, String> env) {\n        String outVars = ArgUtils.getListAsString(context.args(), TaskParams.OUT_VARS_KEY);\n        if (outVars == null) {\n            return;\n        }\n        env.put(\"CONCORD_OUT_VARS\", outVars);\n\n        outVarsFile = context.tmpDir().resolve(\"out_vars.json\");\n        env.put(\"CONCORD_OUT_VARS_FILE\", context.workDir().relativize(outVarsFile).toString());\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public Map<String, Object> process() throws IOException {\n        if (outVarsFile == null || !Files.exists(outVarsFile)) {\n            return Collections.emptyMap();\n        }\n\n        ObjectMapper om = new ObjectMapper();\n        Map<String, Object> m = new HashMap<>();\n        try (InputStream in = Files.newInputStream(outVarsFile)) {\n            m.putAll(om.readValue(in, Map.class));\n        }\n        return m;\n    }\n\n    public void postProcess() throws IOException {\n        if (outVarsFile != null && Files.exists(outVarsFile)) {\n            Files.delete(outVarsFile);\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/PlaybookProcessRunner.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.List;\nimport java.util.Map;\n\npublic interface PlaybookProcessRunner {\n\n    PlaybookProcessRunner withDebug(boolean debug);\n\n    int run(List<String> args, Map<String, String> env) throws Exception;\n\n    interface LogCallback {\n\n        void onLog(String line);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/PlaybookProcessRunnerFactory.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.plugins.ansible.docker.AnsibleDockerService;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.nio.file.Path;\nimport java.util.Collection;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.sdk.MapUtils.*;\n\npublic final class PlaybookProcessRunnerFactory {\n\n    private static final Logger log = LoggerFactory.getLogger(PlaybookProcessRunnerFactory.class);\n\n    private final AnsibleDockerService dockerService;\n    private final Path workDir;\n\n    public PlaybookProcessRunnerFactory(AnsibleDockerService dockerService, Path workDir) {\n        this.dockerService = dockerService;\n        this.workDir = workDir;\n    }\n\n    public PlaybookProcessRunner create(Map<String, Object> args) {\n        String dockerImage = getString(args, TaskParams.DOCKER_IMAGE_KEY.getKey());\n        if (dockerImage != null) {\n            log.info(\"Using the docker image: {}\", dockerImage);\n\n            Collection<String> extraHosts = DockerExtraHosts.getHosts(getMap(args, TaskParams.DOCKER_OPTS_KEY, null));\n            log.info(\"Using extra /etc/hosts records: {}\", extraHosts);\n\n            int pullRetryCount = MapUtils.getInt(args, TaskParams.DOCKER_PULL_RETRY_COUNT.getKey(), 0);\n            long pullRetryInterval = MapUtils.getInt(args, TaskParams.DOCKER_PULL_RETRY_INTERVAL.getKey(), 0);\n\n            return new DockerPlaybookProcessRunner(dockerService, dockerImage)\n                            .withForcePull(getBoolean(args, TaskParams.FORCE_PULL_KEY, true))\n                            .withPullRetryCount(pullRetryCount)\n                            .withPullRetryInterval(pullRetryInterval)\n                            .withHosts(extraHosts);\n        } else {\n            return new DefaultPlaybookProcessRunner(workDir);\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/PlaybookScriptBuilder.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.util.*;\n\npublic class PlaybookScriptBuilder {\n\n    private static final Logger log = LoggerFactory.getLogger(PlaybookScriptBuilder.class);\n\n    private static final String ANSIBLE_CMD = \"ansible-playbook\";\n    private static final String MISSING_ANSIBLE_CHECK =\n            \"command -v \" + ANSIBLE_CMD + \" >/dev/null 2>&1 || { echo \\\"\" +\n            \"Can't find ansible-playbook binary in \\\\$PATH. Install a local copy or use \" +\n            \"'\" + TaskParams.DOCKER_IMAGE_KEY.getKey() + \"' or '\" + TaskParams.VIRTUALENV_KEY.getKey() + \"' options.\" +\n            \"\\\" ; exit 1; }\";\n\n    private static final String VIRTUALENV_CMD = \"virtualenv\";\n    private static final String MISSING_VIRTUALENV_CHECK =\n            \"command -v \" + VIRTUALENV_CMD + \" >/dev/null 2>&1 || { echo \\\"\" +\n            \"Can't find virtualenv binary in \\\\$PATH. Install a local copy or use \" +\n            \"'\" + TaskParams.DOCKER_IMAGE_KEY.getKey() + \"' option.\" +\n            \"\\\" ; exit 1; }\";\n\n    private final String playbook;\n    private List<String> inventories;\n    private final Path workDir;\n    private final Path tmpDir;\n\n    private boolean debug;\n    private String attachmentsDir;\n    private Map<String, Object> extraVars;\n    private List<String> extraVarsFiles;\n    private String extraSshArgs;\n    private String user;\n    private String tags;\n    private String skipTags;\n    private String privateKey;\n    private Map<String, Path> vaultIds;\n    private Map<String, String> extraEnv = Collections.emptyMap();\n    private String limit;\n    private boolean check;\n    private boolean syntaxCheck;\n    private boolean skipCheckBinary;\n    private int verboseLevel = 0;\n    private Virtualenv virtualenv;\n\n    public PlaybookScriptBuilder(AnsibleContext context, String playbook) {\n        this.playbook = playbook;\n        this.workDir = context.workDir();\n        this.tmpDir = context.tmpDir();\n    }\n\n    public PlaybookScriptBuilder withDebug(boolean debug) {\n        this.debug = debug;\n        return this;\n    }\n\n    public PlaybookScriptBuilder withInventories(List<String> inventories) {\n        this.inventories = inventories;\n        return this;\n    }\n\n    public PlaybookScriptBuilder withExtraVars(Map<String, Object> extraVars) {\n        this.extraVars = extraVars;\n        return this;\n    }\n\n    public PlaybookScriptBuilder withExtraSshArgs(String extraSshArgs) {\n        this.extraSshArgs = extraSshArgs;\n        return this;\n    }\n\n    public PlaybookScriptBuilder withExtraVarsFiles(List<String> extraVarsFiles) {\n        this.extraVarsFiles = extraVarsFiles;\n        return this;\n    }\n\n    public PlaybookScriptBuilder withUser(String user) {\n        this.user = user;\n        return this;\n    }\n\n    public PlaybookScriptBuilder withTags(String tags) {\n        this.tags = tags;\n        return this;\n    }\n\n    public PlaybookScriptBuilder withSkipTags(String skipTags) {\n        this.skipTags = skipTags;\n        return this;\n    }\n\n    public PlaybookScriptBuilder withPrivateKey(String privateKey) {\n        this.privateKey = privateKey;\n        return this;\n    }\n\n    public PlaybookScriptBuilder withAttachmentsDir(String attachmentsDir) {\n        this.attachmentsDir = attachmentsDir;\n        return this;\n    }\n\n    public PlaybookScriptBuilder withVaultIds(Map<String, Path> vaultIds) {\n        this.vaultIds = vaultIds;\n        return this;\n    }\n\n    public PlaybookScriptBuilder withEnv(Map<String, String> env) {\n        this.extraEnv = env;\n        return this;\n    }\n\n    public PlaybookScriptBuilder withVerboseLevel(int level) {\n        this.verboseLevel = level;\n        return this;\n    }\n\n    public PlaybookScriptBuilder withLimit(String limit) {\n        this.limit = limit;\n        return this;\n    }\n\n    public PlaybookScriptBuilder withCheck(boolean check) {\n        this.check = check;\n        return this;\n    }\n\n    public PlaybookScriptBuilder withSyntaxCheck(boolean syntaxCheck) {\n        this.syntaxCheck = syntaxCheck;\n        return this;\n    }\n\n    public PlaybookScriptBuilder withVirtualenv(Virtualenv virtualenv) {\n        this.virtualenv = virtualenv;\n        return this;\n    }\n\n    public PlaybookScriptBuilder withSkipCheckBinary(boolean skipCheckBinary) {\n        this.skipCheckBinary = skipCheckBinary;\n        return this;\n    }\n\n    private List<String> buildAnsibleArgs() throws IOException {\n        List<String> l = new ArrayList<>();\n        l.add(ANSIBLE_CMD);\n\n        for (String i : inventories) {\n            l.add(\"-i\");\n            l.add(quote(i));\n        }\n        l.add(quote(playbook));\n\n        if (extraVars != null && !extraVars.isEmpty()) {\n            l.add(\"--extra-vars\");\n            l.add(\"@\" + workDir.relativize(writeExtraVars(extraVars, tmpDir)));\n        }\n\n        if (extraVarsFiles != null) {\n            extraVarsFiles.forEach(extraVarsFile -> {\n                l.add(\"--extra-vars\");\n                l.add(\"@\" + extraVarsFile);\n            });\n        }\n\n        if (user != null) {\n            l.add(\"-u\");\n            l.add(quote(user));\n        }\n\n        if (tags != null) {\n            l.add(\"-t\");\n            l.add(quote(tags));\n        }\n\n        if (skipTags != null) {\n            l.add(\"--skip-tags\");\n            l.add(quote(skipTags));\n        }\n\n        if (privateKey != null) {\n            l.add(\"--private-key\");\n            l.add(quote(privateKey));\n        }\n\n        if (vaultIds != null) {\n            for (Map.Entry<String, Path> p : vaultIds.entrySet()) {\n                l.add(\"--vault-id\");\n                l.add(quote(p.getKey() + \"@\" + p.getValue().toString()));\n            }\n        }\n\n        if (limit != null) {\n            l.add(\"--limit\");\n            l.add(quote(limit));\n        }\n\n        if (check) {\n            l.add(\"--check\");\n        }\n\n        if (syntaxCheck) {\n            l.add(\"--syntax-check\");\n        }\n\n        if (extraSshArgs != null) {\n            l.add(\"--ssh-extra-args=\" + quote(extraSshArgs));\n        }\n\n        if (verboseLevel > 0) {\n            if (verboseLevel > 4) {\n                verboseLevel = 4;\n            }\n\n            StringBuilder b = new StringBuilder();\n            for (int i = 0; i < verboseLevel; i++) {\n                b.append(\"v\");\n            }\n\n            l.add(\"-\" + b);\n        }\n\n        return l;\n    }\n\n    public List<String> buildArgs() throws IOException {\n        StringBuilder sb = new StringBuilder(\"#!/bin/bash\").append(\"\\n\")\n                .append(\"set -e\").append(\"\\n\");\n\n        if (virtualenv.isEnabled()) {\n            if (!skipCheckBinary) {\n                sb.append(MISSING_VIRTUALENV_CHECK).append(\"\\n\");\n            }\n\n            String virtualenvDir = workDir.relativize(virtualenv.getTargetDir()).toString();\n\n            sb.append(\"virtualenv --no-download --system-site-packages \")\n                    .append(virtualenvDir)\n                    .append(\"\\n\");\n\n            sb.append(\"source \").append(virtualenvDir).append(\"/bin/activate\")\n                    .append(\"\\n\");\n\n            String indexUrl = virtualenv.getIndexUrl();\n\n            List<String> packages = virtualenv.getPackages();\n            if (packages != null && !packages.isEmpty()) {\n                sb.append(\"pip install \");\n                if (indexUrl != null) {\n                    sb.append(\"-i\").append(\" '\").append(indexUrl).append(\"' \");\n                }\n\n                packages.forEach(p -> sb.append(\"'\").append(p).append(\"' \"));\n\n                sb.append(\"\\n\");\n            }\n        }\n\n        if (!skipCheckBinary) {\n            sb.append(MISSING_ANSIBLE_CHECK).append(\"\\n\");\n        }\n\n        if (debug) {\n            sb.append(ANSIBLE_CMD).append(\" --version\").append(\"\\n\");\n        }\n\n        buildAnsibleArgs().forEach(arg -> sb.append(arg).append(\" \"));\n        sb.append(\"\\n\");\n\n        if (virtualenv.isEnabled()) {\n            sb.append(\"deactivate\").append(\"\\n\");\n        }\n\n        if (debug) {\n            log.info(\"Using the run script:\\n{}\", sb);\n        }\n\n        Path dst = tmpDir.resolve(\"run.sh\");\n        Files.write(dst, sb.toString().getBytes(), StandardOpenOption.CREATE_NEW);\n        return Arrays.asList(\"/bin/bash\", workDir.relativize(dst).toString());\n    }\n\n    public Map<String, String> buildEnv() {\n        Map<String, String> env = new HashMap<>();\n        if (attachmentsDir != null) {\n            env.put(\"_CONCORD_ATTACHMENTS_DIR\", attachmentsDir);\n        }\n        env.putAll(extraEnv);\n        return env;\n    }\n\n    private static Path writeExtraVars(Map<String, Object> extraVars, Path tmpDir) throws IOException {\n        Path result = tmpDir.resolve(\"ansible-extra-vars.json\");\n        new ObjectMapper().writeValue(result.toFile(), extraVars);\n        return result;\n    }\n\n    /**\n     * Puts the specified string into single quotes.\n     */\n    private static String quote(String s) {\n        return \"'\" + escapeQuote(s) + \"'\";\n    }\n\n    private static String escapeQuote(String s) {\n        return s.replace(\"'\", \"'\\\\''\");\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/PrivateKeyAuth.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic class PrivateKeyAuth implements AnsibleAuth {\n\n    private static final Logger log = LoggerFactory.getLogger(PrivateKeyAuth.class);\n\n    private final Path workDir;\n    private final String username;\n    private final Path keyPath;\n    private final boolean removeAfter;\n\n    public PrivateKeyAuth(Path workDir, String username, Path keyPath, boolean removeAfter) {\n        this.workDir = workDir;\n        this.username = username;\n        this.keyPath = keyPath;\n        this.removeAfter = removeAfter;\n    }\n\n    public Path getKeyPath() {\n        return keyPath;\n    }\n\n    @Override\n    public void prepare() {\n        // do nothing\n    }\n\n    @Override\n    public AnsibleAuth enrich(AnsibleEnv env, AnsibleContext context) {\n        // do nothing\n        return this;\n    }\n\n    @Override\n    public AnsibleAuth enrich(PlaybookScriptBuilder p) {\n        p.withUser(username)\n                .withPrivateKey(workDir.relativize(keyPath).toString());\n        return this;\n    }\n\n    @Override\n    public void postProcess() {\n        if (!removeAfter) {\n            return;\n        }\n\n        try {\n            Files.deleteIfExists(keyPath);\n        } catch (Exception e) {\n            log.warn(\"postProcess -> error\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/Resources.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.plugins.ansible.v1.RunPlaybookTask2;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardCopyOption;\n\npublic final class Resources {\n\n    public static void copy(String resourcesLocation, String[] files, Path dest) throws IOException {\n        Files.createDirectories(dest);\n\n        for (String f : files) {\n            copyResourceToFile(Paths.get(resourcesLocation, f).toString(), dest.resolve(f));\n        }\n    }\n\n    private static void copyResourceToFile(String resourceName, Path dest) throws IOException {\n        try (InputStream is = RunPlaybookTask2.class.getResourceAsStream(resourceName)) {\n            Files.copy(is, dest, StandardCopyOption.REPLACE_EXISTING);\n        }\n    }\n\n    private Resources() {\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/Secret.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.sdk.MapUtils.getString;\n\npublic class Secret {\n\n    public static Secret from(Map<String, Object> auth) {\n        String name = getString(auth, \"name\");\n        if (name == null) {\n            throw new IllegalArgumentException(\"Secret name is required \");\n        }\n\n        return new Secret(getString(auth, \"org\"), name, getString(auth, \"password\"));\n    }\n\n    private final String org;\n    private final String name;\n    private final String password;\n\n    public Secret(String org, String name, String password) {\n        this.org = org;\n        this.name = name;\n        this.password = password;\n    }\n\n    public String getOrg() {\n        return org;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public String getPassword() {\n        return password;\n    }\n\n    @Override\n    public String toString() {\n        return \"Secret{\" +\n                \"org='\" + org + '\\'' +\n                \", name='\" + name + '\\'' +\n                \", password='***\" +\n                '}';\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/TaskParams.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport com.walmartlabs.concord.sdk.HasKey;\n\npublic enum TaskParams implements HasKey {\n\n    AUTH(\"auth\"),\n\n    CHECK_KEY(\"check\"),\n\n    CONFIG_FILE_KEY(\"configFile\"),\n\n    CONFIG_KEY(\"config\"),\n\n    DEBUG_KEY(\"debug\"),\n\n    DISABLE_CONCORD_CALLBACKS_KEY(\"disableConcordCallbacks\"),\n\n    DOCKER_IMAGE_KEY(\"dockerImage\"),\n\n    DOCKER_OPTS_KEY(\"dockerOpts\"),\n\n    DOCKER_PULL_RETRY_COUNT(\"pullRetryCount\"),\n\n    DOCKER_PULL_RETRY_INTERVAL(\"pullRetryInterval\"),\n\n    DYNAMIC_INVENTORY_FILE_KEY(\"dynamicInventoryFile\"),\n\n    @Deprecated\n    DYNAMIC_INVENTORY_FILE_NAME(\"_dynamicInventory\"),\n\n    ENABLE_POLICY(\"enablePolicy\"),\n\n    ENABLE_LOG_FILTERING(\"enableLogFiltering\"),\n\n    ENABLE_EVENTS(\"enableEvents\"),\n\n    ENABLE_STATS(\"enableStats\"),\n\n    ENABLE_OUT_VARS(\"enableOutsVars\"),\n\n    ENABLE_MODULE_DEFAULTS(\"enableModuleDefaults\"),\n\n    EXIT_CODE_KEY(\"exitCode\"),\n\n    EXTRA_ENV_KEY(\"extraEnv\"),\n\n    EXTRA_VARS_KEY(\"extraVars\"),\n\n    EXTRA_VARS_FILES_KEY(\"extraVarsFiles\"),\n\n    FORCE_PULL_KEY(\"forcePull\"),\n\n    GROUP_VARS_KEY(\"groupVars\"),\n\n    INVENTORY_FILE_KEY(\"inventoryFile\"),\n\n    @Deprecated\n    INVENTORY_FILE_NAME(\"_inventory\"),\n\n    INVENTORY_KEY(\"inventory\"),\n\n    @Deprecated\n    LAST_RETRY_FILE(\"ansible.retry\"),\n\n    LIMIT_KEY(\"limit\"),\n\n    OUT_VARS_KEY(\"outVars\"),\n\n    PASSWORD(\"password\"),\n\n    PLAYBOOK_KEY(\"playbook\"),\n\n    /**\n     * @deprecated use {@link #AUTH}\n     */\n    @Deprecated\n    PRIVATE_KEY_FILE_KEY(\"privateKey\"),\n\n    /**\n     * @deprecated use {@link #AUTH}\n     */\n    @Deprecated\n    PRIVATE_KEY_FILE_NAME(\"_privateKey\"),\n\n    RETRY_KEY(\"retry\"),\n\n    ROLES_KEY(\"roles\"),\n\n    SAVE_RETRY_FILE(\"saveRetryFile\"),\n\n    SKIP_TAGS_KEY(\"skipTags\"),\n\n    // TODO not really a TaskParam\n    STATS_FILE_NAME(\"ansible_stats.json\"),\n\n    SYNTAX_CHECK_KEY(\"syntaxCheck\"),\n\n    TAGS_KEY(\"tags\"),\n\n    /**\n     * @deprecated use {@link #AUTH}\n     */\n    @Deprecated\n    USER_KEY(\"user\"),\n\n    VAULT_PASSWORD_FILE_KEY(\"vaultPasswordFile\"),\n\n    /**\n     * @deprecated set the path explicitly using\n     * {@link #VAULT_PASSWORD_FILE_KEY} input parameter\n     */\n    @Deprecated\n    VAULT_PASSWORD_FILE_PATH(\"_vaultPassword\"),\n\n    VAULT_PASSWORD_KEY(\"vaultPassword\"),\n\n    VERBOSE_LEVEL_KEY(\"verbose\"),\n\n    VIRTUALENV_KEY(\"virtualenv\"),\n\n    SKIP_CHECK_BINARY(\"skipCheckBinary\"),\n\n    WORK_DIR_KEY(\"workDir\");\n\n    private final String key;\n\n    TaskParams(String key) {\n        this.key = key;\n    }\n\n    @Override\n    public String getKey() {\n        return key;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/Utils.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.attribute.PosixFilePermission;\nimport java.util.HashSet;\nimport java.util.Set;\n\npublic final class Utils {\n\n    public static void updateScriptPermissions(Path p) throws IOException {\n        // ensure that the file has the executable bit set\n        Set<PosixFilePermission> perms = new HashSet<>();\n        perms.add(PosixFilePermission.OWNER_READ);\n        perms.add(PosixFilePermission.OWNER_EXECUTE);\n        perms.add(PosixFilePermission.OWNER_WRITE);\n        Files.setPosixFilePermissions(p, perms);\n    }\n\n    public static String assertArgSafe(String s) {\n        if (s == null) {\n            return null;\n        }\n\n        if (s.contains(\"'\")) {\n            throw new IllegalArgumentException(\"Illegal character \\\"'\\\" in value: \" + s);\n        }\n\n        return s;\n    }\n\n    private Utils() {\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/Virtualenv.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\npublic class Virtualenv {\n\n    private static final Logger log = LoggerFactory.getLogger(Virtualenv.class);\n\n    @SuppressWarnings(\"unchecked\")\n    public static Virtualenv create(AnsibleContext context) {\n        Virtualenv virtualenv = new Virtualenv(context.tmpDir().resolve(\"ve\")); // can't be \"virtualenv\", it breaks initial install\n\n        Object v = context.args().get(TaskParams.VIRTUALENV_KEY.getKey());\n        if (v == null) {\n            return virtualenv;\n        }\n\n        if (!(v instanceof Map)) {\n            throw new IllegalArgumentException(\"Invalid '\" + TaskParams.VIRTUALENV_KEY.getKey() + \"' value. \" +\n                    \"Expected a virtualenv definition, got: \" + v);\n        }\n\n        virtualenv.enabled = true;\n\n        Map<String, Object> m = (Map<String, Object>) v;\n\n        Object packages = m.get(\"packages\");\n        if (packages != null) {\n            if (!(packages instanceof List)) {\n                throw new IllegalArgumentException(\"Invalid virtualenv package list value. \" +\n                        \"Expected a list of package names, got: \" + packages);\n            }\n\n            List<Object> l = (List<Object>) packages;\n            l.forEach(p -> addPackage(virtualenv, p));\n        }\n\n        virtualenv.indexUrl = Utils.assertArgSafe(getIndexUrl(context.defaults(), m));\n\n        return virtualenv;\n    }\n\n    private final Path targetDir;\n    private final List<String> packages = new ArrayList<>();\n\n    private boolean enabled = false;\n    private String indexUrl;\n\n    private Virtualenv(Path targetDir) {\n        this.targetDir = targetDir;\n    }\n\n    public boolean isEnabled() {\n        return enabled;\n    }\n\n    public List<String> getPackages() {\n        return packages;\n    }\n\n    public String getIndexUrl() {\n        return indexUrl;\n    }\n\n    public Path getTargetDir() {\n        return targetDir;\n    }\n\n    public void destroy() {\n        try {\n            PathUtils.deleteRecursively(targetDir);\n        } catch (IOException e) {\n            log.warn(\"Error while removing the virtualenv's directory: {}\", e.getMessage());\n        }\n    }\n\n    private static void addPackage(Virtualenv v, Object p) {\n        if (!(p instanceof String)) {\n            throw new IllegalArgumentException(\"Invalid virtualenv package '\" + p + \"'\");\n        }\n\n        String s = Utils.assertArgSafe((String) p);\n        v.packages.add(s);\n    }\n\n    private static String getIndexUrl(Map<String, Object> defaults, Map<String, Object> m) {\n        Object indexUrl = m.get(\"indexUrl\");\n\n        if (indexUrl != null) {\n            if (!(indexUrl instanceof String)) {\n                throw new IllegalArgumentException(\"Invalid virtualenv 'indexUrl' value. \" +\n                        \"Expected a string, got: \" + indexUrl);\n            }\n\n            return (String) indexUrl;\n        }\n\n        return MapUtils.getString(MapUtils.getMap(defaults, \"virtualenv\", Collections.emptyMap()), \"indexUrl\", null);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/docker/AnsibleDockerService.java",
    "content": "package com.walmartlabs.concord.plugins.ansible.docker;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Common interface for v1 and v2 DockerService implementations.\n */\npublic interface AnsibleDockerService {\n\n    int start(DockerContainerSpec spec) throws Exception;\n\n    class DockerContainerSpec {\n\n        private String image;\n        private List<String> args;\n        private Map<String, String> env;\n        private boolean debug;\n        private boolean forcePull;\n        private Collection<String> extraDockerHosts;\n        private int pullRetryCount;\n        private long pullRetryInterval;\n\n        public String image() {\n            return image;\n        }\n\n        public DockerContainerSpec image(String image) {\n            this.image = image;\n            return this;\n        }\n\n        public List<String> args() {\n            return args;\n        }\n\n        public DockerContainerSpec args(List<String> args) {\n            this.args = args;\n            return this;\n        }\n\n        public Map<String, String> env() {\n            return this.env;\n        }\n\n        public DockerContainerSpec env(Map<String, String> env) {\n            this.env = env;\n            return this;\n        }\n\n        public boolean debug() {\n            return this.debug;\n        }\n\n        public DockerContainerSpec debug(boolean debug) {\n            this.debug = debug;\n            return this;\n        }\n\n        public boolean forcePull() {\n            return this.forcePull;\n        }\n\n        public DockerContainerSpec forcePull(boolean forcePull) {\n            this.forcePull = forcePull;\n            return this;\n        }\n\n        public Collection<String> extraDockerHosts() {\n            return this.extraDockerHosts;\n        }\n\n        public DockerContainerSpec extraDockerHosts(Collection<String> extraDockerHosts) {\n            this.extraDockerHosts = extraDockerHosts;\n            return this;\n        }\n\n        public int pullRetryCount() {\n            return this.pullRetryCount;\n        }\n\n        public DockerContainerSpec pullRetryCount(int pullRetryCount) {\n            this.pullRetryCount = pullRetryCount;\n            return this;\n        }\n\n        public long pullRetryInterval() {\n            return this.pullRetryInterval;\n        }\n\n        public DockerContainerSpec pullRetryInterval(long pullRetryInterval) {\n            this.pullRetryInterval = pullRetryInterval;\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/secrets/AnsibleSecretService.java",
    "content": "package com.walmartlabs.concord.plugins.ansible.secrets;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.nio.file.Path;\n\npublic interface AnsibleSecretService {\n\n    Path exportAsFile(String orgName, String secretName, String password) throws Exception;\n\n    UsernamePassword exportCredentials(String orgName, String secretName, String password) throws Exception;\n\n    KeyPair exportKeyAsFile(String orgName, String secretName, String password) throws Exception;\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/secrets/KeyPair.java",
    "content": "package com.walmartlabs.concord.plugins.ansible.secrets;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.nio.file.Path;\n\n/**\n * A type to use instead of v1 or v2 specific types.\n */\npublic class KeyPair {\n\n    private final Path privateKey;\n    private final Path publicKey;\n\n    public KeyPair(Path privateKey, Path publicKey) {\n        this.privateKey = privateKey;\n        this.publicKey = publicKey;\n    }\n\n    public Path privateKey() {\n        return privateKey;\n    }\n\n    public Path publicKey() {\n        return publicKey;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/secrets/UsernamePassword.java",
    "content": "package com.walmartlabs.concord.plugins.ansible.secrets;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n/**\n * A type to use instead of v1 or v2 specific types.\n */\npublic class UsernamePassword {\n\n    private final String username;\n    private final String password;\n\n    public UsernamePassword(String username, String password) {\n        this.username = username;\n        this.password = password;\n    }\n\n    public String username() {\n        return username;\n    }\n\n    public String password() {\n        return password;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/v1/AnsibleDockerServiceV1.java",
    "content": "package com.walmartlabs.concord.plugins.ansible.v1;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.plugins.ansible.docker.AnsibleDockerService;\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.DockerContainerSpec.Options;\nimport com.walmartlabs.concord.sdk.DockerService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class AnsibleDockerServiceV1 implements AnsibleDockerService {\n\n    private static final Logger processLog = LoggerFactory.getLogger(\"processLog\");\n\n    private final Context ctx;\n    private final DockerService delegate;\n\n    public AnsibleDockerServiceV1(Context ctx, DockerService delegate) {\n        this.ctx = ctx;\n        this.delegate = delegate;\n    }\n\n    @Override\n    public int start(DockerContainerSpec spec) throws Exception {\n        return delegate.start(ctx, com.walmartlabs.concord.sdk.DockerContainerSpec.builder()\n                .image(spec.image())\n                .args(spec.args())\n                .debug(spec.debug())\n                .forcePull(spec.forcePull())\n                .env(spec.env())\n                .pullRetryCount(spec.pullRetryCount())\n                .pullRetryInterval(spec.pullRetryInterval())\n                .workdir(\"/workspace\")\n                .options(Options.builder()\n                        .hosts(spec.extraDockerHosts())\n                        .build())\n                .build(), line -> processLog.info(\"ANSIBLE: {}\", line), null);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/v1/AnsibleSecretServiceV1.java",
    "content": "package com.walmartlabs.concord.plugins.ansible.v1;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.plugins.ansible.secrets.AnsibleSecretService;\nimport com.walmartlabs.concord.plugins.ansible.secrets.KeyPair;\nimport com.walmartlabs.concord.plugins.ansible.secrets.UsernamePassword;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.ContextUtils;\nimport com.walmartlabs.concord.sdk.SecretService;\n\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Map;\n\npublic class AnsibleSecretServiceV1 implements AnsibleSecretService {\n\n    private final Context ctx;\n    private final SecretService delegate;\n\n    public AnsibleSecretServiceV1(Context ctx, SecretService delegate) {\n        this.ctx = ctx;\n        this.delegate = delegate;\n    }\n\n    @Override\n    public Path exportAsFile(String orgName, String secretName, String password) throws Exception {\n        String instanceId = ContextUtils.assertString(ctx, Constants.Context.TX_ID_KEY);\n        String workDir = ContextUtils.assertString(ctx, Constants.Context.WORK_DIR_KEY);\n        String result = delegate.exportAsFile(ctx, instanceId, workDir, orgName, secretName, password);\n        return Paths.get(workDir, result);\n    }\n\n    @Override\n    public UsernamePassword exportCredentials(String orgName, String secretName, String password) throws Exception {\n        String instanceId = ContextUtils.assertString(ctx, Constants.Context.TX_ID_KEY);\n        String workDir = ContextUtils.assertString(ctx, Constants.Context.WORK_DIR_KEY);\n        Map<String, String> result = delegate.exportCredentials(ctx, instanceId, workDir, orgName, secretName, password);\n        return new UsernamePassword(result.get(\"username\"), result.get(\"password\"));\n    }\n\n    @Override\n    public KeyPair exportKeyAsFile(String orgName, String secretName, String password) throws Exception {\n        String instanceId = ContextUtils.assertString(ctx, Constants.Context.TX_ID_KEY);\n        String workDir = ContextUtils.assertString(ctx, Constants.Context.WORK_DIR_KEY);\n        Map<String, String> result = delegate.exportKeyAsFile(ctx, instanceId, workDir, orgName, secretName, password);\n        return new KeyPair(Paths.get(workDir, result.get(\"private\")), Paths.get(workDir, result.get(\"public\")));\n    }\n\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/v1/AnsibleTaskV1.java",
    "content": "package com.walmartlabs.concord.plugins.ansible.v1;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClientFactory;\nimport com.walmartlabs.concord.sdk.ApiConfiguration;\nimport com.walmartlabs.concord.sdk.DockerService;\nimport com.walmartlabs.concord.sdk.SecretService;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\n\n@Named(\"ansible\")\npublic class AnsibleTaskV1 extends RunPlaybookTask2 {\n\n    @Inject\n    public AnsibleTaskV1(ApiClientFactory apiClientFactory,\n                         ApiConfiguration apiCfg,\n                         SecretService secretService,\n                         DockerService dockerService) {\n\n        super(apiClientFactory, apiCfg, secretService, dockerService);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/v1/RunPlaybookTask2.java",
    "content": "package com.walmartlabs.concord.plugins.ansible.v1;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.plugins.ansible.*;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.sdk.*;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.stream.Stream;\n\nimport static com.walmartlabs.concord.sdk.ContextUtils.getMap;\n\n@Named(\"ansible2\")\npublic class RunPlaybookTask2 implements Task {\n\n    private final ApiClientFactory apiClientFactory;\n    private final ApiConfiguration apiCfg;\n    private final SecretService secretService;\n    private final DockerService dockerService;\n\n    @InjectVariable(Constants.Context.CONTEXT_KEY)\n    Context context;\n\n    @InjectVariable(Constants.Context.TX_ID_KEY)\n    String txId;\n\n    @InjectVariable(\"ansibleParams\")\n    Map<String, Object> defaults;\n\n    @Inject\n    public RunPlaybookTask2(ApiClientFactory apiClientFactory,\n                            ApiConfiguration apiCfg,\n                            SecretService secretService,\n                            DockerService dockerService) {\n\n        this.apiClientFactory = apiClientFactory;\n        this.apiCfg = apiCfg;\n        this.secretService = secretService;\n        this.dockerService = dockerService;\n    }\n\n    public void run(String dockerImageName, Map<String, Object> args, String payloadPath) throws Exception {\n        Map<String, Object> argsWithDocker = new HashMap<>(args);\n        argsWithDocker.put(TaskParams.DOCKER_IMAGE_KEY.getKey(), dockerImageName);\n        run(this.context, Paths.get(payloadPath), argsWithDocker);\n    }\n\n    public void run(Map<String, Object> args, String payloadPath) throws Exception {\n        run(this.context, Paths.get(payloadPath), args);\n    }\n\n    @Override\n    public void execute(Context ctx) throws Exception {\n        Map<String, Object> allArgs = new HashMap<>();\n\n        Stream.of(TaskParams.values()).forEach(c ->\n                addIfPresent(ctx, allArgs, c.getKey())\n        );\n\n        Path workDir = ContextUtils.getWorkDir(ctx);\n        Map<String, Object> args = DeprecatedArgsProcessor.process(workDir, allArgs);\n\n        run(ctx, workDir, args);\n    }\n\n    private void run(Context ctx, Path workDir, Map<String, Object> args) throws Exception {\n        ApiClient apiClient = apiClientFactory.create(ApiClientConfiguration.builder()\n                .sessionToken(ContextUtils.getSessionToken(context))\n                .build());\n\n        AnsibleSecretServiceV1 ansibleSecretService = new AnsibleSecretServiceV1(context, secretService);\n\n        AnsibleTask task = new AnsibleTask(apiClient,\n                new AnsibleAuthFactory(ansibleSecretService),\n                ansibleSecretService);\n\n        Map<String, Object> projectInfo = getMap(context, Constants.Request.PROJECT_INFO_KEY, null);\n        String orgName = projectInfo != null ? (String) projectInfo.get(\"orgName\") : null;\n\n        AnsibleContext context = AnsibleContext.builder()\n                .apiBaseUrl(apiClient.getBaseUrl())\n                .instanceId(UUID.fromString(txId))\n                .workDir(workDir)\n                .tmpDir(createTmpDir(workDir))\n                .defaults(defaults != null ? defaults : Collections.emptyMap())\n                .args(args)\n                .sessionToken(apiCfg.getSessionToken(ctx))\n                .eventCorrelationId(ctx.getEventCorrelationId())\n                .orgName(orgName)\n                .retryCount((Integer) ctx.getVariable(Constants.Context.CURRENT_RETRY_COUNTER))\n                .build();\n\n        PlaybookProcessRunner runner = new PlaybookProcessRunnerFactory(new AnsibleDockerServiceV1(ctx, dockerService), workDir)\n                .create(args);\n\n        TaskResult.SimpleResult result = task.run(context, runner);\n        result.values().forEach(ctx::setVariable);\n        if (!result.ok()) {\n            throw new IllegalStateException(\"Process finished with exit code \" + result.values().get(\"exitCode\"));\n        }\n    }\n\n    private static void addIfPresent(Context src, Map<String, Object> dst, String k) {\n        Object v = src.getVariable(k);\n        if (v != null) {\n            dst.put(k, v);\n        }\n    }\n\n    private static Path createTmpDir(Path workDir) throws IOException {\n        Path p = workDir.resolve(Constants.Files.CONCORD_TMP_DIR_NAME);\n        Files.createDirectories(p);\n        return Files.createTempDirectory(p, \"ansible\");\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/v2/AnsibleDockerServiceV2.java",
    "content": "package com.walmartlabs.concord.plugins.ansible.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.plugins.ansible.docker.AnsibleDockerService;\nimport com.walmartlabs.concord.runtime.v2.sdk.DockerContainerSpec.Options;\nimport com.walmartlabs.concord.runtime.v2.sdk.DockerService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class AnsibleDockerServiceV2 implements AnsibleDockerService {\n\n    private static final Logger processLog = LoggerFactory.getLogger(\"processLog\");\n\n    private final DockerService delegate;\n\n    public AnsibleDockerServiceV2(DockerService delegate) {\n        this.delegate = delegate;\n    }\n\n    @Override\n    public int start(DockerContainerSpec spec) throws Exception {\n        return delegate.start(com.walmartlabs.concord.runtime.v2.sdk.DockerContainerSpec.builder()\n                .image(spec.image())\n                .args(spec.args())\n                .debug(spec.debug())\n                .forcePull(spec.forcePull())\n                .env(spec.env())\n                .workdir(\"/workspace\")\n                .options(Options.builder()\n                        .hosts(spec.extraDockerHosts())\n                        .build())\n                .pullRetryCount(spec.pullRetryCount())\n                .pullRetryInterval(spec.pullRetryInterval())\n        .build(), line -> processLog.info(\"ANSIBLE: {}\", line), null);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/v2/AnsibleSecretServiceV2.java",
    "content": "package com.walmartlabs.concord.plugins.ansible.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.plugins.ansible.secrets.AnsibleSecretService;\nimport com.walmartlabs.concord.plugins.ansible.secrets.KeyPair;\nimport com.walmartlabs.concord.plugins.ansible.secrets.UsernamePassword;\nimport com.walmartlabs.concord.runtime.v2.sdk.SecretService;\n\nimport java.nio.file.Path;\n\npublic class AnsibleSecretServiceV2 implements AnsibleSecretService {\n\n    private final SecretService delegate;\n\n    public AnsibleSecretServiceV2(SecretService delegate) {\n        this.delegate = delegate;\n    }\n\n    @Override\n    public Path exportAsFile(String orgName, String secretName, String password) throws Exception {\n        return delegate.exportAsFile(orgName, secretName, password);\n    }\n\n    @Override\n    public UsernamePassword exportCredentials(String orgName, String secretName, String password) throws Exception {\n        SecretService.UsernamePassword result = delegate.exportCredentials(orgName, secretName, password);\n        return new UsernamePassword(result.username(), result.password());\n    }\n\n    @Override\n    public KeyPair exportKeyAsFile(String orgName, String secretName, String password) throws Exception {\n        SecretService.KeyPair result = delegate.exportKeyAsFile(orgName, secretName, password);\n        return new KeyPair(result.privateKey(), result.publicKey());\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/java/com/walmartlabs/concord/plugins/ansible/v2/AnsibleTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.ansible.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.plugins.ansible.*;\nimport com.walmartlabs.concord.plugins.ansible.secrets.AnsibleSecretService;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProjectInfo;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.nio.file.Path;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.sdk.MapUtils.getBoolean;\n\n@Named(\"ansible\")\npublic class AnsibleTaskV2 implements Task {\n\n    private final Context context;\n    private final ApiClient apiClient;\n    private final Map<String, Object> defaults;\n\n    @Inject\n    public AnsibleTaskV2(ApiClient apiClient, Context context) {\n        this.context = context;\n        this.apiClient = apiClient;\n        this.defaults = context.defaultVariables().toMap();\n    }\n\n    @Override\n    public TaskResult execute(Variables input) throws Exception {\n        Map<String, Object> in = input.toMap();\n        Path workDir = context.workingDirectory();\n\n        PlaybookProcessRunner runner = new PlaybookProcessRunnerFactory(new AnsibleDockerServiceV2(context.dockerService()), workDir)\n                .create(in);\n\n        AnsibleSecretService secretService = new AnsibleSecretServiceV2(context.secretService());\n        AnsibleTask task = new AnsibleTask(apiClient, new AnsibleAuthFactory(secretService), secretService);\n\n        UUID instanceId = Objects.requireNonNull(context.processInstanceId());\n        Path tmpDir = context.fileService().createTempDirectory(\"ansible\");\n\n        ProjectInfo projectInfo = context.processConfiguration().projectInfo();\n\n        boolean debug = getBoolean(ConfigurationUtils.deepMerge(defaults, in), TaskParams.DEBUG_KEY.getKey(), false)\n                || context.processConfiguration().debug();\n        AnsibleContext ctx = AnsibleContext.builder()\n                .apiBaseUrl(apiClient.getBaseUrl())\n                .instanceId(instanceId)\n                .workDir(workDir)\n                .tmpDir(tmpDir)\n                .defaults(defaults)\n                .args(in)\n                .sessionToken(context.processConfiguration().processInfo().sessionToken())\n                .eventCorrelationId(context.execution().correlationId())\n                .orgName(projectInfo != null ? projectInfo.orgName() : null)\n                .retryCount(ContextUtils.getCurrentRetryAttemptNumber(context))\n                .debug(debug)\n                .build();\n\n        TaskResult.SimpleResult result = task.run(ctx, runner);\n        if (!result.ok()) {\n            throw new IllegalStateException(\"Process finished with exit code \" + result.values().get(\"exitCode\"));\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/resources/com/walmartlabs/concord/plugins/ansible/callback/concord_default_module_args.py",
    "content": "from ansible.plugins.callback import CallbackBase\nfrom ansible.playbook.task import Task\nfrom ansible.inventory.host import Host\n\n\nclass CallbackModule(CallbackBase):\n    def v2_runner_on_start(self, host: Host, task: Task):\n        if task.resolved_action == 'ansible.builtin.gather_facts' or task.resolved_action == 'gather_facts':\n            # disable puppet / chef fact gathering, significant speed/performance increase - usually unneeded\n            # may eventually need !hardware for AIX/HPUX or set at runtime, Ansible 2.4 fixes many broken facts\n            module_defaults = self._get_defaults(task)\n\n            if ((task.resolved_action not in module_defaults) or\n                    module_defaults[task.resolved_action]['gather_subset'] is None):\n\n                # no module_defaults are set for this module\n                # now make sure there are no task args\n                if 'gather_subset' not in task.args or task.args['gather_subset'] is None:\n                    task.args['gather_subset'] = ['!facter', '!ohai']\n\n    def _get_defaults(self, task: Task):\n        for mod_def in task.module_defaults:\n            if task.resolved_action in mod_def and mod_def[task.resolved_action] is not None:\n                return mod_def\n\n        return dict()\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/resources/com/walmartlabs/concord/plugins/ansible/callback/concord_events.py",
    "content": "from __future__ import (absolute_import, division, print_function)\n__metaclass__ = type\n\nfrom ansible.plugins.callback import CallbackBase\nfrom ansible.module_utils.six import string_types\nfrom ansible.playbook.block import Block\nfrom ansible.playbook.handler import Handler\nfrom ansible.template import Templar\nfrom ansible.utils.unsafe_proxy import AnsibleUnsafeText\n\nimport time\nimport datetime\nimport uuid\n\nimport os\nimport json\n\nfrom process_cfg_policy import ProcessCfgPolicy\n\ndef to_millis(t):\n    return int(round(t * 1000))\n\nclass CallbackModule(CallbackBase):\n    CALLBACK_VERSION = 2.0\n    CALLBACK_NAME = 'concord_events'\n    CALLBACK_NEEDS_WHITELIST = False\n\n\n    MAX_STRING_LEN = 1024\n    HALF_MAX_STRING_LEN = int(MAX_STRING_LEN / 2)\n    OVERLIMIT_STRING_LEN = MAX_STRING_LEN / 10\n    MAX_ARRAY_LEN = 26\n    HALF_MAX_ARRAY_LENGTH = int(MAX_ARRAY_LEN / 2)\n    OVERLIMIT_ARRAY_LEN = MAX_ARRAY_LEN / 10\n\n    def __init__(self):\n        super(CallbackModule, self).__init__()\n\n        print(\"Ansible event recording started...\")\n\n        outFilePath = os.environ['CONCORD_ANSIBLE_EVENTS_FILE']\n        self.outFile = open(outFilePath, 'a')\n\n        # TODO could be moved into the task itself\n        self.currentRetryCount = None\n        if \"CONCORD_CURRENT_RETRY_COUNT\" in os.environ:\n            self.currentRetryCount = int(os.environ['CONCORD_CURRENT_RETRY_COUNT'])\n\n        # TODO could be moved into the task itself\n        self.eventCorrelationId = None\n        if \"CONCORD_EVENT_CORRELATION_ID\" in os.environ:\n            self.eventCorrelationId = os.environ['CONCORD_EVENT_CORRELATION_ID']\n\n        self.taskDurations = {}\n        self.playbookId = self._get_playbook_id()\n        self.process_cfg_policy = ProcessCfgPolicy()\n        self.work_completed = 0\n\n    def _get_playbook_id(self):\n        if self.eventCorrelationId is not None:\n            return self.eventCorrelationId\n        else:\n            return str(uuid.uuid4())\n\n    def _handle_event(self, type, event):\n        self.outFile.write(json.dumps({\n            'eventType': type,\n            'eventDate': datetime.datetime.utcnow().isoformat() + 'Z',\n            'data': dict(event, **{'parentCorrelationId': self.eventCorrelationId,\n                                   'playbookId': self.playbookId})\n        }))\n        self.outFile.write('<~EOL~>\\n')\n        self.outFile.flush()\n\n    def handle_event(self, event):\n        self._handle_event('ANSIBLE', event)\n\n    def cleanup_results(self, result):\n        abridged_result = self._strip_internal_keys(result)\n\n        if 'invocation' in result:\n            del abridged_result['invocation']\n\n        if 'diff' in result:\n            del abridged_result['diff']\n\n        if 'exception' in abridged_result:\n            del abridged_result['exception']\n\n        return self._trunc_long_items(abridged_result)\n\n    def _strip_internal_keys(self, dirty):\n        clean = dirty.copy()\n        for k in dirty.keys():\n            if isinstance(k, string_types) and k.startswith('_ansible_'):\n                del clean[k]\n            elif isinstance(dirty[k], dict):\n                clean[k] = self._strip_internal_keys(dirty[k])\n        return clean\n\n    def _trunc_long_items(self, obj):\n        if isinstance(obj, str):\n            overlimit = len(obj) - self.MAX_STRING_LEN\n            return (obj[:self.HALF_MAX_STRING_LEN] +\n                    '...[skipped ' + str(overlimit) + ' bytes]...' +\n                    obj[len(obj) - self.HALF_MAX_STRING_LEN:]) \\\n                if overlimit > self.OVERLIMIT_STRING_LEN else obj\n        elif isinstance(obj, list):\n            overlimit = len(obj) - self.MAX_ARRAY_LEN\n            if overlimit > self.OVERLIMIT_ARRAY_LEN:\n                copy = [self._trunc_long_items(o) for o in obj[:self.HALF_MAX_ARRAY_LENGTH]]\n                copy.append('[skipped ' + str(overlimit) + ' lines]')\n                copy += [self._trunc_long_items(o) for o in obj[len(obj) - self.HALF_MAX_ARRAY_LENGTH:]]\n                return copy\n            else:\n                return [self._trunc_long_items(o) for o in obj]\n        elif isinstance(obj, tuple):\n            return tuple(self._trunc_long_items(o) for o in obj)\n        elif isinstance(obj, dict):\n            return dict((k, self._trunc_long_items(v)) for (k,v) in obj.items())\n        elif isinstance(obj, AnsibleUnsafeText):\n            return self._trunc_long_items(str(obj))\n        else:\n            return obj\n\n    @staticmethod\n    def _host_correlation_id(host_group, host_uuid):\n        return host_group + host_uuid\n\n    @staticmethod\n    def _task_correlation_id(host_name, task_uuid):\n        return host_name + task_uuid\n\n    def _task_duration(self, correlation_id):\n        if correlation_id in self.taskDurations:\n            return to_millis(time.time()) - self.taskDurations.pop(correlation_id)\n        return 0\n\n    def _on_task_start(self, host, task):\n        self.process_cfg_policy.disable_verbose_after_too_much_work(self.work_completed)\n        self.work_completed += 1\n\n        task_correlation_id = self._task_correlation_id(host.name, task._uuid)\n        self.taskDurations[task_correlation_id] = to_millis(time.time())\n\n        data = {\n            'status': \"RUNNING\",\n            'playbook': self.playbook._file_name,\n            'playId': self.play._uuid,\n            'host': host.name,\n            'hostGroup': self.play.get_name(),\n            'taskId': task._uuid,\n            'task': task.get_name(),\n            'isHandler': isinstance(task, Handler),\n            'action': task.action,\n            'correlationId': task_correlation_id,\n            'phase': \"pre\"\n        }\n\n        self.handle_event(data)\n\n    def _on_task_skipped(self, result):\n        task_correlation_id = self._task_correlation_id(result._host.name, result._task._uuid)\n\n        data = {\n            'status': 'SKIPPED',\n            'playbook': self.playbook._file_name,\n            'playId': self.play._uuid,\n            'host': result._host.name,\n            'hostGroup': self.play.get_name(),\n            'taskId': result._task._uuid,\n            'task': result._task.get_name(),\n            'isHandler': isinstance(result._task, Handler),\n            'action': result._task.action,\n            'correlationId': task_correlation_id,\n            'phase': \"post\",\n            'duration': self._task_duration(task_correlation_id),\n            'result': self.cleanup_results(result._result)\n        }\n\n        self.handle_event(data)\n\n    def handle_playbook_start(self, playbook):\n        def _process_task(task):\n            result = []\n            if isinstance(task, Block):\n                result.extend(_process_block(task))\n            else:\n                if task.action == 'meta':\n                    return []\n\n                result.append({\n                    'id': task._uuid,\n                    'task': task.get_name()\n                })\n\n            return result\n\n        def _process_block(b):\n            result = []\n            for task in b.block:\n                result.extend(_process_task(task))\n\n            for task in b.rescue:\n                result.extend(_process_task(task))\n\n            for task in b.always:\n                result.extend(_process_task(task))\n\n            return result\n\n        def _collect_tasks(play):\n            result = []\n            for block in play.compile():\n                if not block.has_tasks():\n                    continue\n                tasks = _process_block(block)\n\n                for x in tasks:\n                    if x not in result:\n                        result.append(x)\n\n            return result\n\n        total_work = 0\n        hosts = set()\n        info = []\n        for play in playbook.get_plays():\n            loader = play._loader\n            if play._included_path is not None:\n                loader.set_basedir(play._included_path)\n            else:\n                loader.set_basedir(playbook._basedir)\n\n            all_vars = play.get_variable_manager().get_vars(play=play)\n            templar = Templar(loader=loader, variables=all_vars)\n            play.post_validate(templar)\n\n            play_hosts = play.get_variable_manager()._inventory.get_hosts(play.hosts)\n            play_hosts_count = len(play_hosts)\n            play_task = _collect_tasks(play)\n\n            play_info = {\n                'id': play._uuid,\n                'play': play.get_name(),\n                'hosts': play_hosts_count,\n                'tasks': play_task\n            }\n            info.append(play_info)\n            hosts.update(play_hosts)\n            total_work += play_hosts_count * len(play_task)\n\n        # Override verbose output (disable it) if policy is set to limit\n        self.process_cfg_policy.disable_verbose_on_start(len(hosts), total_work)\n\n        self._handle_event('ANSIBLE_PLAYBOOK_INFO', {'playbook':playbook._file_name,\n                                                     'currentRetryCount': self.currentRetryCount,\n                                                     'uniqueHosts': len(hosts),\n                                                     'totalWork': total_work,\n                                                     'plays': info})\n\n    @staticmethod\n    def _get_playbook_status(stats):\n        if len(stats.failures) > 0 or len(stats.dark) > 0:\n            return 'FAILED'\n        return 'OK'\n\n    ### Ansible callbacks ###\n\n    def v2_playbook_on_start(self, playbook):\n        self.playbook = playbook\n        self.handle_playbook_start(playbook)\n\n    def playbook_on_stats(self, stats):\n        status = self._get_playbook_status(stats)\n        self._handle_event('ANSIBLE_PLAYBOOK_RESULT', {'playbook': self.playbook._file_name,\n                                                       'status': status})\n\n        self.outFile.close()\n\n    def v2_runner_on_failed(self, result, ignore_errors=False):\n        task_correlation_id = self._task_correlation_id(result._host.name, result._task._uuid)\n\n        data = {\n            'status': \"FAILED\",\n            'playbook': self.playbook._file_name,\n            'playId': self.play._uuid,\n            'host': result._host.name,\n            'hostGroup': self.play.get_name(),\n            'taskId': result._task._uuid,\n            'task': result._task.get_name(),\n            'isHandler': isinstance(result._task, Handler),\n            'action': result._task.action,\n            'correlationId': task_correlation_id,\n            'phase': \"post\",\n            'duration': self._task_duration(task_correlation_id),\n            'result': self.cleanup_results(result._result),\n            'ignore_errors': isinstance(ignore_errors, bool) and ignore_errors\n        }\n\n        self.handle_event(data)\n\n    def v2_runner_on_ok(self, result, **kwargs):\n        task_correlation_id = self._task_correlation_id(result._host.name, result._task._uuid)\n\n        data = {\n            'status': \"OK\",\n            'playbook': self.playbook._file_name,\n            'playId': self.play._uuid,\n            'host': result._host.name,\n            'hostGroup': self.play.get_name(),\n            'taskId': result._task._uuid,\n            'task': result._task.get_name(),\n            'isHandler': isinstance(result._task, Handler),\n            'action': result._task.action,\n            'correlationId': task_correlation_id,\n            'phase': \"post\",\n            'duration': self._task_duration(task_correlation_id),\n            'result': self.cleanup_results(result._result)\n        }\n\n        self.handle_event(data)\n\n    def v2_runner_on_unreachable(self, result):\n        task_correlation_id = self._task_correlation_id(result._host.name, result._task._uuid)\n\n        data = {\n            'status': 'UNREACHABLE',\n            'playbook': self.playbook._file_name,\n            'playId': self.play._uuid,\n            'host': result._host.name,\n            'hostGroup': self.play.get_name(),\n            'taskId': result._task._uuid,\n            'task': result._task.get_name(),\n            'isHandler': isinstance(result._task, Handler),\n            'action': result._task.action,\n            'correlationId': task_correlation_id,\n            'phase': \"post\",\n            'duration': self._task_duration(task_correlation_id),\n            'result': self.cleanup_results(result._result)\n        }\n\n        self.handle_event(data)\n\n    def v2_runner_on_async_failed(self, result, **kwargs):\n        task_correlation_id = self._task_correlation_id(result._host.name, result._task._uuid)\n\n        data = {\n            'status': 'UNREACHABLE',\n            'playbook': self.playbook._file_name,\n            'playId': self.play._uuid,\n            'host': result._host.name,\n            'hostGroup': self.play.get_name(),\n            'taskId': result._task._uuid,\n            'task': result._task.get_name(),\n            'isHandler': isinstance(result._task, Handler),\n            'action': result._task.action,\n            'correlationId': task_correlation_id,\n            'phase': \"post\",\n            'duration': self._task_duration(task_correlation_id),\n            'result': self.cleanup_results(result._result)\n        }\n\n        self.handle_event(data)\n\n    def concord_on_task_start(self, host, task):\n        self._on_task_start(host, task)\n\n    def v2_runner_on_skipped(self, result):\n        self._on_task_skipped(result)\n\n    def v2_playbook_on_play_start(self, play):\n        self.play = play\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/resources/com/walmartlabs/concord/plugins/ansible/callback/concord_out_vars.py",
    "content": "import os\nimport json\nfrom ansible.plugins.callback import CallbackBase\nfrom concord_ansible_stats import ConcordAnsibleStats\n\ntry:\n    from __main__ import cli\nexcept ImportError:\n    cli = None\n\nclass CallbackModule(CallbackBase):\n    CALLBACK_VERSION = 2.0\n    CALLBACK_NAME = 'concord_out_vars'\n    CALLBACK_NEEDS_WHITELIST = False\n\n    def __init__(self):\n        super(CallbackModule, self).__init__()\n\n        self.out_vars = None\n        if \"CONCORD_OUT_VARS\" in os.environ:\n            self.out_vars = [v.strip() for v in os.environ[\"CONCORD_OUT_VARS\"].split(\",\")]\n\n        self.out_vars_file_name = None\n        if \"CONCORD_OUT_VARS_FILE\" in os.environ:\n            self.out_vars_file_name = os.environ[\"CONCORD_OUT_VARS_FILE\"]\n\n        print(\"Saving out variables:\", self.out_vars)\n\n    def playbook_on_stats(self, stats):\n        if not self.out_vars:\n            return\n\n        result = dict()\n\n        all_vars = self.var_manager._nonpersistent_fact_cache\n\n        for fact in self.out_vars:\n            fact_by_host = dict()\n            for host, vars in all_vars.items():\n                if fact in vars:\n                    fact_by_host[host] = vars[fact]\n            result[fact] = fact_by_host\n\n        if '_stats' in self.out_vars:\n            result['_stats'] = ConcordAnsibleStats.build_stats_data(stats)\n\n        target_file = open(self.out_vars_file_name, \"w\")\n        target_file.write(json.dumps(result, indent=2))\n        print(\"Variables saved to:\", self.out_vars_file_name)\n\n\n    def v2_playbook_on_play_start(self, play):\n        self.var_manager = play.get_variable_manager()\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/resources/com/walmartlabs/concord/plugins/ansible/callback/concord_protectdata.py",
    "content": "from __future__ import (absolute_import)\n__metaclass__ = type\n\nfrom ansible.plugins.callback.default import CallbackModule as CallbackModule_default\nfrom collections.abc import Mapping\n\ndef enc(s):\n    # we shouldn't use `str` as the input string can contain non-ascii characters\n    # and we can't use `s.encode('utf-8')` because the input can be a non string value\n    return repr(s)\n\nclass CallbackModule(CallbackModule_default):\n\n    '''\n    This is the callback interface, which will search for any sensitive data in stdout and enable no_log = true on that particular tasks.\n    Also this will search for all Key, Value pairs in the Ansible stdout and mask the value of corresponding keys that has got the sensitive information in it.\n    '''\n\n    CALLBACK_VERSION = 2.0\n    CALLBACK_TYPE = 'stdout'\n    CALLBACK_NAME = 'concord_protectdata'\n    CALLBACK_NEEDS_WHITELIST = False\n\n    def __init__(self):\n        super(CallbackModule, self).__init__()\n\n        self.secret_list = ['password', 'credentials', 'secret', 'ansible_password', 'vaultpassword']\n        print(\"Log filter is enabled...\")\n\n    def set_options(self, task_keys=None, var_options=None, direct=None):\n        super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)\n\n        self.set_option('display_ok_hosts', True)\n        self.set_option('show_per_host_start', False)\n\n    def hide_password(self, result):\n        ret = {}\n        for key, value in result.items():\n            if isinstance(value, Mapping):\n                ret[key] = self.hide_password(value)\n            elif any (x in enc(value).lower() for x in self.secret_list) or any (y in enc(key).lower() for y in self.secret_list):\n                    ret[key] = \"******\"\n            else:\n                    ret[key] = value\n        return ret\n\n    def v2_playbook_on_task_start(self, task, is_conditional):\n        print(\"TASK\", \"[\",task.get_name(),\"]\" , \"*************************************************************************\")\n        if hasattr(task, 'args'):\n            task_args = getattr(task, 'args', None)\n            for k, v in task_args.items():\n               if any(s in enc(v).lower() for s in self.secret_list):\n                 print(\"*********** THIS TASK CONTAINS SENSITIVE INFORMATION. ENABLING NO_LOG ******************\")\n                 task.no_log = True\n\n    def _dump_results(self, result,  indent=None, sort_keys=True, keep_invocation=False):\n         return super(CallbackModule, self)._dump_results(self.hide_password(result), indent, sort_keys, keep_invocation)\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/resources/com/walmartlabs/concord/plugins/ansible/callback/concord_strategy_patch.py",
    "content": "from __future__ import (absolute_import)\n__metaclass__ = type\n\nfrom ansible.plugins.callback import CallbackBase\nfrom ansible.plugins.strategy import StrategyBase\n\ndef _queue_task(self, host, task, task_vars, play_context):\n    self._tqm.send_callback('concord_on_task_start', host, task)\n\n    ansibleStrategyModuleQueueTask(self, host, task, task_vars, play_context)\n\n\nclass CallbackModule(CallbackBase):\n\n    CALLBACK_VERSION = 2.0\n    CALLBACK_NAME = 'concord_strategy_patch'\n    CALLBACK_NEEDS_WHITELIST = False\n\n    def __init__(self):\n        global ansibleStrategyModuleQueueTask\n        ansibleStrategyModuleQueueTask = StrategyBase._queue_task\n        StrategyBase._queue_task = _queue_task\n\n        super(CallbackModule, self).__init__()"
  },
  {
    "path": "plugins/tasks/ansible/src/main/resources/com/walmartlabs/concord/plugins/ansible/callback/concord_task_executor_patch.py",
    "content": "from __future__ import (absolute_import)\n__metaclass__ = type\n\nfrom ansible.template import Templar\nfrom ansible.plugins.callback import CallbackBase\nfrom ansible.executor.task_executor import TaskExecutor\nfrom ansible.errors import AnsibleError\n\ntry:\n    from __main__ import display\nexcept ImportError:\n    from ansible.utils.display import Display\n    display = Display()\n\n\nfrom task_policy import TaskPolicy\n\nimport copy\n\ntaskPolicy = TaskPolicy()\n\n### copied from ansible/executor/task_executor.py with added concord policy check logic\ndef _execute(self, variables=None):\n    if variables is None:\n        variables = self._job_vars\n\n    templar = Templar(loader=self._loader, shared_loader_obj=self._shared_loader_obj, variables=variables)\n\n    _task = copy.deepcopy(self._task)\n\n    context_validation_error = None\n    try:\n        # apply the given task's information to the connection info,\n        # which may override some fields already set by the play or\n        # the options specified on the command line\n        self._play_context = self._play_context.set_task_and_variable_override(task=_task, variables=variables, templar=templar)\n\n        # fields set from the play/task may be based on variables, so we have to\n        # do the same kind of post validation step on it here before we use it.\n        self._play_context.post_validate(templar=templar)\n\n        # now that the play context is finalized, if the remote_addr is not set\n        # default to using the host's address field as the remote address\n        if not self._play_context.remote_addr:\n            self._play_context.remote_addr = self._host.address\n\n        # We also add \"magic\" variables back into the variables dict to make sure\n        # a certain subset of variables exist.\n        self._play_context.update_vars(variables)\n\n        # FIXME: update connection/shell plugin options\n    except AnsibleError as e:\n        # save the error, which we'll raise later if we don't end up\n        # skipping this task during the conditional evaluation step\n        context_validation_error = e\n\n    # Evaluate the conditional (if any) for this task, which we do before running\n    # the final task post-validation. We do this before the post validation due to\n    # the fact that the conditional may specify that the task be skipped due to a\n    # variable not being present which would otherwise cause validation to fail\n    try:\n        if not _task.evaluate_conditional(templar, variables):\n            display.debug(\"when evaluation is False, skipping this task\")\n            return dict(changed=False, skipped=True, skip_reason='Conditional result was False', _ansible_no_log=self._play_context.no_log)\n    except AnsibleError:\n        # loop error takes precedence\n        if self._loop_eval_error is not None:\n            raise self._loop_eval_error  # pylint: disable=raising-bad-type\n        # skip conditional exception in the case of includes as the vars needed might not be available except in the included tasks or due to tags\n        if _task.action not in ['include', 'include_tasks', 'include_role']:\n            raise\n\n    # Not skipping, if we had loop error raised earlier we need to raise it now to halt the execution of this task\n    if self._loop_eval_error is not None:\n        raise self._loop_eval_error  # pylint: disable=raising-bad-type\n\n    # if we ran into an error while setting up the PlayContext, raise it now\n    if context_validation_error is not None:\n        raise context_validation_error  # pylint: disable=raising-bad-type\n\n    # if this task is a TaskInclude, we just return now with a success code so the\n    # main thread can expand the task list for the given host\n    if _task.action in ('include', 'include_tasks'):\n        include_variables = _task.args.copy()\n        include_file = include_variables.pop('_raw_params', None)\n        if not include_file:\n            return dict(failed=True, msg=\"No include file was specified to the include\")\n\n        include_file = templar.template(include_file)\n        return dict(include=include_file, include_variables=include_variables)\n\n    # if this task is a IncludeRole, we just return now with a success code so the main thread can expand the task list for the given host\n    elif _task.action == 'include_role':\n        include_variables = _task.args.copy()\n        return dict(include_variables=include_variables)\n\n    # Now we do final validation on the task, which sets all fields to their final values.\n    _task.post_validate(templar=templar)\n    if '_variable_params' in _task.args:\n        variable_params = _task.args.pop('_variable_params')\n        if isinstance(variable_params, dict):\n            display.deprecated(\"Using variables for task params is unsafe, especially if the variables come from an external source like facts\",\n                               version=\"2.6\")\n            variable_params.update(_task.args)\n            _task.args = variable_params\n\n    if taskPolicy.is_deny(_task):\n        self._task = copy.deepcopy(self._task)\n        self._task.action = \"fail\"\n        self._task.args = {\"msg\": \"Found forbidden tasks\"}\n\n    return ansibleTaskExecutorExecute(self, variables)\n\nclass CallbackModule(CallbackBase):\n\n    CALLBACK_VERSION = 2.0\n    CALLBACK_NAME = 'concord_task_executor_patch'\n    CALLBACK_NEEDS_WHITELIST = False\n\n    def __init__(self):\n        global ansibleTaskExecutorExecute\n        ansibleTaskExecutorExecute = TaskExecutor._execute\n        TaskExecutor._execute = _execute\n\n        super(CallbackModule, self).__init__()"
  },
  {
    "path": "plugins/tasks/ansible/src/main/resources/com/walmartlabs/concord/plugins/ansible/callback/concord_trace.py",
    "content": "import errno\nimport os\nimport json\nfrom ansible.plugins.callback import CallbackBase\nfrom concord_ansible_stats import ConcordAnsibleStats\n\ntry:\n    from __main__ import cli\nexcept ImportError:\n    cli = None\n\n\ndef mkdir_p(path):\n    try:\n        os.makedirs(path)\n    except OSError as exc:\n        if exc.errno == errno.EEXIST and os.path.isdir(path):\n            pass\n        else:\n            raise\n\n\nclass CallbackModule(CallbackBase):\n    CALLBACK_VERSION = 2.0\n    CALLBACK_NAME = 'concord_trace'\n    CALLBACK_NEEDS_WHITELIST = False\n\n    def __init__(self):\n        super(CallbackModule, self).__init__()\n        self.base_dir = os.environ['_CONCORD_ATTACHMENTS_DIR']\n        self.eventCorrelationId = None\n        if \"CONCORD_EVENT_CORRELATION_ID\" in os.environ:\n            self.eventCorrelationId = os.environ['CONCORD_EVENT_CORRELATION_ID']\n\n    def log(self, data):\n        target_dir = self.base_dir\n        mkdir_p(target_dir)\n\n        target_filename = target_dir + \"/ansible_stats.json\"\n        target_file = open(target_filename, \"w\")\n        target_file.write(json.dumps(data, indent=2))\n\n        print(\"Trace saved to:\", target_filename)\n\n        entry = {'playbook': self.playbook._file_name,\n                 'eventCorrelationId': self.eventCorrelationId,\n                 'stats': data}\n\n        target_filename = target_dir + \"/ansible_stats_v2.json\"\n        if not os.path.isfile(target_filename):\n            with open(target_filename, mode='w') as f:\n                f.write(json.dumps([entry], indent=2))\n        else:\n            with open(target_filename) as f:\n                stats = json.load(f)\n\n            stats.append(entry)\n            with open(target_filename, mode='w') as f:\n                f.write(json.dumps(stats, indent=2))\n\n        print(\"Trace saved to:\", target_filename)\n\n    def v2_playbook_on_start(self, playbook):\n        self.playbook = playbook\n\n    def playbook_on_stats(self, stats):\n        self.log(ConcordAnsibleStats.build_stats_data(stats))\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/resources/com/walmartlabs/concord/plugins/ansible/inventory.sh",
    "content": "#!/bin/bash\ncat << \"EOF\"\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/resources/com/walmartlabs/concord/plugins/ansible/lib/concord_ansible_stats.py",
    "content": "from __future__ import (absolute_import, division, print_function)\n__metaclass__ = type\n\nclass ConcordAnsibleStats:\n\n    @staticmethod\n    def build_stats_data(stats):\n        failures = list(stats.failures.keys())\n\n        unreachable = [e for e in stats.dark.keys()\n                       if e not in failures]\n\n        changed = [e for e in stats.changed.keys()\n                   if (e not in failures\n                       and e not in unreachable)]\n\n        ok = [e for e in stats.ok.keys()\n              if (e not in failures\n                  and e not in unreachable\n                  and e not in changed)]\n\n        skipped = [e for e in stats.skipped.keys()\n                   if (e not in failures\n                       and e not in unreachable\n                       and e not in changed\n                       and e not in ok)]\n\n        return {\n            'ok': ok,\n            'failures': failures,\n            'unreachable': unreachable,\n            'changed': changed,\n            'skipped': skipped\n        }\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/resources/com/walmartlabs/concord/plugins/ansible/lib/process_cfg_policy.py",
    "content": "from __future__ import (absolute_import, division, print_function)\n\nfrom ansible.utils.color import hostcolor\n__metaclass__ = type\n\ntry:\n    from __main__ import display\nexcept ImportError:\n    from ansible.utils.display import Display\n    display = Display()\n\nimport json\nimport os\nimport os.path\n\nclass ProcessCfgPolicy:\n\n    def __init__(self):\n        rule_file = os.environ['CONCORD_POLICY']\n\n        # default value\n        verbose_limits = {'maxHosts': None, 'maxTotalWork': None}\n\n        if os.path.isfile(rule_file):\n            print(\"Loading policy from {}\".format(rule_file))\n            policy_rules = json.load(open(rule_file))\n\n            try:\n                verbose_limits = policy_rules['processCfg']['arguments']['ansibleVerboseLimits']\n            except KeyError:\n                pass\n\n        self.max_hosts = verbose_limits.get('maxHosts')\n        self.max_total_work = verbose_limits.get('maxTotalWork')\n\n    def disable_verbose_after_too_much_work(self, completed_work):\n        '''\n        Methods for disabling verbose output after playbook starts execution. Useful\n        when the main playbook has few tasks but includes one or more include_tasks\n        calls which bypasses total_work calculations before playbook starts.\n        '''\n\n        if display.verbosity == 0:\n            return False\n\n        if self.max_total_work is not None and completed_work > self.max_total_work:\n            msg = \"\"\"\nDisabling verbose output. Too much work for verbose logging. Completed {0} work\nso far and Concord system policies forbid verbose logging for playbooks with\ngreater than {1} total work.\n            \"\"\".format(str(completed_work), self.max_total_work)\n            display.error(msg)\n            display.verbosity = 0\n\n    def disable_verbose_on_start(self, host_count, total_work):\n        '''\n        Disables verbose logging if policy exists to limit total number of inventory\n        hosts or total work.\n        '''\n\n        if display.verbosity == 0:\n            return\n\n        if self.max_hosts is not None and host_count > self.max_hosts:\n            msg = \"\"\"\nDisabling verbose output. Too many hosts for verbose logging. Inventory contains\n{0} hosts and Concord system policies forbid verbose logging for inventories\ngreater than {1} hosts.\n            \"\"\".format(host_count, str(self.max_hosts))\n            display.error(msg)\n            display.verbosity = 0\n            return\n\n        if self.max_total_work is not None and total_work > self.max_total_work:\n            msg = \"\"\"\nDisabling verbose output. Too much work for verbose logging. Expecting to perform\n{0} total work and Concord system policies forbid verbose logging for playbooks\nwith greater than {1} total work.\n            \"\"\".format(str(total_work), self.max_total_work)\n            display.error(msg)\n            display.verbosity = 0\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/resources/com/walmartlabs/concord/plugins/ansible/lib/task_policy.py",
    "content": "from __future__ import (absolute_import, division, print_function)\n__metaclass__ = type\n\ntry:\n    from __main__ import display\nexcept ImportError:\n    from ansible.utils.display import Display\n    display = Display()\n\nimport json\nimport re\nimport os\nimport os.path\nimport string\nimport ansible\n\nclass SafeDict(dict):\n    def __missing__(self, key):\n        return '{' + key + '}'\n\nclass TaskPolicy:\n\n    def __init__(self):\n        rule_file = os.environ['CONCORD_POLICY']\n\n        self.policy_rules = dict()\n\n        if os.path.isfile(rule_file):\n            print(\"Loading policy from {}\".format(rule_file))\n            self.policy_rules = json.load(open(rule_file))\n\n    def is_deny(self, task):\n        if 'ansible' not in self.policy_rules:\n            return False\n\n        ansible_rules = self.policy_rules['ansible']\n\n        action = task.action\n\n        if not hasattr(task, 'args'):\n            return False\n\n        task_args = getattr(task, 'args', None)\n        \n        args = self._enrich_args(action, task_args)\n\n        if 'allow' in ansible_rules:\n            for r in ansible_rules['allow']:\n                if self._match_ansible_rule(r, action, args):\n                    return False\n\n        if 'deny' in ansible_rules:\n            for r in ansible_rules['deny']:\n                if self._match_ansible_rule(r, action, args):\n                    display.error(\"Task '{0} ({1})' is forbidden by the task policy: {2}\".format(\n                        task.get_name(), action, self._format_rule_message(r, args)))\n                    return True\n\n        if 'warn' in ansible_rules:\n            for r in ansible_rules['warn']:\n                if self._match_ansible_rule(r, action, args):\n                    display.warning(\"Potentially restricted task '{0} ({1})' (task policy: {2})\".format(\n                        task.get_name(), action, self._format_rule_message(r, args)))\n                    return False\n\n        return False\n\n    @staticmethod\n    def _format_rule_message(rule, args):\n        if 'msg' not in rule:\n            return ''\n\n        args_dict = rule.copy()\n        for ta_name, ta_value in args.items():\n            args_dict[ta_name] = ta_value\n\n        return string.Formatter().vformat(rule['msg'], (), SafeDict(args_dict))\n\n    def _match_ansible_rule(self, rule, task_action, task_args):\n        display.vv(\"match_ansible_rule: {} on {}\".format(rule, task_action, task_args))\n\n        if not self._match(rule['action'], task_action):\n            return False\n\n        if 'params' in rule and not self._match_task_args(rule['params'], task_args):\n            return False\n\n        return True\n\n    def _match_task_args(self, rule_args, task_args):\n        display.vv(\"match_task_args: {} on {}\".format(rule_args, task_args))\n\n        matched = False\n        for a in rule_args:\n            for ta_name, ta_value in task_args.items():\n                if self._match(a['name'], ta_name):\n                    if self._match_values(a['values'], ta_value):\n                        matched = True\n                    else:\n                        return False\n                    break\n\n        return matched\n\n    def _match_values(self, patterns, value):\n        display.vv(\"match_values: {} on {}\".format(patterns, value))\n\n        if isinstance(value, ansible.parsing.yaml.objects.AnsibleUnicode):\n            value = str(value)\n\n        if isinstance(value, str):\n            for p in patterns:\n                if self._match(p, value):\n                    return True\n\n        return False\n\n    def _enrich_args(self, action, args):\n        display.vv(\"enrich_args: {} ({})\".format(action, args))\n\n        new_args = args\n        if action == 'maven_artifact':\n            new_args = args.copy()\n            new_args['artifact_url'] = self._build_artifact_url(args)\n\n        return new_args\n\n    # /$groupId[0]/../$groupId[n]/$artifactId/$version/$artifactId-$version-$classifier.$extension\n    def _build_artifact_url(self, args):\n        name = args['artifact_id'] + '-' + args['version']\n        if 'classifier' in args:\n            name = name + '-' + args['classifier']\n        name = name + '.' + args.get('extension', 'jar')\n\n        return args['repository_url'].rstrip('/') + '/' + args['group_id'].replace('.', '/') + '/' + args['artifact_id'] + '/' + args['version'] + '/' + name\n\n    @staticmethod\n    def _match(pattern, s):\n        return re.compile(pattern).match(s)\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/resources/com/walmartlabs/concord/plugins/ansible/lookup/concord_data_secret.py",
    "content": "from __future__ import (absolute_import, division, print_function)\n\n__metaclass__ = type\n\nfrom ansible.errors import AnsibleError\nfrom ansible.plugins.lookup import LookupBase\n\nimport os\nimport requests\n\nclass LookupModule(LookupBase):\n\n    def run(self, terms, variables, **kwargs):\n\n        ret = []\n\n        argsLen = len(terms)\n        if argsLen < 2:\n            raise AnsibleError('Invalid lookup format. Expected: [orgName], secretName, storePassword')\n        elif argsLen < 3:\n            orgName = os.environ['CONCORD_CURRENT_ORG_NAME']\n            secretName = terms[0]\n            storePassword = terms[1]\n        else:\n            orgName = terms[0]\n            secretName = terms[1]\n            storePassword = terms[2]\n\n        concordBaseUrl = os.environ['CONCORD_BASE_URL']\n        concordInstanceId = os.environ['CONCORD_INSTANCE_ID']\n        concordSessionToken = os.environ['CONCORD_SESSION_TOKEN']\n\n        headers = {'X-Concord-SessionToken': concordSessionToken,\n                   'User-Agent': 'ansible (txId: ' + concordInstanceId + ')'}\n        url = concordBaseUrl + '/api/v1/org/' + orgName + '/secret/' + secretName + '/data'\n        multipartInputDict = {}\n        if storePassword is not None:\n            multipartInputDict = {'storePassword' : storePassword}\n        else:\n            # this to make sure that multipart header of post request is correctly set\n            multipartInputDict = {'':''}\n\n        r = requests.post(url, headers=headers, files=multipartInputDict)\n\n        if r.status_code == requests.codes.not_found:\n            raise AnsibleError('Secret ' + orgName + '/' + secretName + ' not found')\n\n        if r.status_code != requests.codes.ok:\n            resp = self.get_json(r)\n\n            msg = 'Error accessing secret ' + orgName + '/' + secretName + ': '\n            if resp:\n                try:\n                    raise AnsibleError(msg + resp[0]['message'])\n                except (IndexError, KeyError, TypeError):\n                    pass\n\n            if r.text:\n                raise AnsibleError(msg + r.text)\n\n            raise AnsibleError(msg + 'Invalid server response: ' + str(r.status_code))\n\n        ret.append(str(r.text))\n\n        return ret\n\n    def get_json(self, r):\n        try:\n            return r.json()\n        except ValueError:\n            # no JSON returned\n            return\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/resources/com/walmartlabs/concord/plugins/ansible/lookup/concord_inventory.py",
    "content": "from __future__ import (absolute_import, division, print_function)\n\n__metaclass__ = type\n\nfrom ansible.errors import AnsibleError\nfrom ansible.plugins.lookup import LookupBase\nfrom ansible.parsing.splitter import parse_kv\n\nimport os\nimport requests\nimport json\n\ndef _parse_parameters(term):\n    first_split = term.split(' ', 1)\n    if len(first_split) <= 1:\n        # Only a single argument given, therefore it's a queryName\n        queryName = term\n        params = dict()\n    else:\n        queryName = first_split[0]\n        params = parse_kv(first_split[1])\n\n    params['result'] = int(params.get('result', -1))\n\n    return queryName, params\n\n\nclass LookupModule(LookupBase):\n\n    def run(self, terms, variables, **kwargs):\n\n        ret = []\n\n        argsLen = len(terms)\n        if argsLen < 2:\n            raise AnsibleError('Invalid lookup format. Expected: orgName, inventoryName, queryName')\n        elif argsLen <= 3:\n            orgName = os.environ['CONCORD_CURRENT_ORG_NAME']\n            inventoryName = terms[0]\n            queryName, resultParams = _parse_parameters(terms[1])\n            queryParams = terms[2]\n        else:\n            orgName = terms[0]\n            inventoryName = terms[1]\n            queryName, resultParams = _parse_parameters(terms[2])\n            queryParams = terms[3]\n\n        concordBaseUrl = os.environ['CONCORD_BASE_URL']\n        concordInstanceId = os.environ['CONCORD_INSTANCE_ID']\n        concordSessionToken = os.environ['CONCORD_SESSION_TOKEN']\n\n        headers = {'X-Concord-SessionToken': concordSessionToken,\n                   'Content-type': 'application/json',\n                   'User-Agent': 'ansible (txId: ' + concordInstanceId + ')'}\n        url = concordBaseUrl + '/api/v1/org/' + orgName + '/inventory/' + inventoryName + '/query/' + queryName + \"/exec\"\n\n        r = requests.post(url, headers=headers, data=json.dumps(queryParams))\n\n        if r.status_code != requests.codes.ok:\n            raise AnsibleError('Invalid server response: ' + str(r.status_code))\n\n        resultElement = resultParams['result']\n        if resultElement != -1:\n            ret.append(r.json()[0])\n        else:\n            ret.append(r.json())\n\n        return ret\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/resources/com/walmartlabs/concord/plugins/ansible/lookup/concord_public_key_secret.py",
    "content": "from __future__ import (absolute_import, division, print_function)\n\n__metaclass__ = type\n\nfrom ansible.errors import AnsibleError\nfrom ansible.plugins.lookup import LookupBase\n\nimport os\nimport requests\n\nclass LookupModule(LookupBase):\n\n    def run(self, terms, variables, **kwargs):\n        ret = []\n\n        argsLen = len(terms)\n        if argsLen == 1:\n            orgName = os.environ['CONCORD_CURRENT_ORG_NAME']\n            secretName = terms[0]\n        elif argsLen == 2:\n            orgName = terms[0]\n            secretName = terms[1]\n        else:\n            raise AnsibleError('Invalid lookup format. Expected: [orgName], secretName')\n\n        concordBaseUrl = os.environ['CONCORD_BASE_URL']\n        concordInstanceId = os.environ['CONCORD_INSTANCE_ID']\n        concordSessionToken = os.environ['CONCORD_SESSION_TOKEN']\n\n        headers = {'X-Concord-SessionToken': concordSessionToken,\n                   'User-Agent': 'ansible (txId: ' + concordInstanceId + ')'}\n        url = concordBaseUrl + '/api/v1/org/' + orgName + '/secret/' + secretName + '/public'\n\n        r = requests.get(url, headers=headers)\n\n        if r.status_code != requests.codes.ok:\n            resp = self.get_json(r)\n\n            msg = 'Error accessing public key ' + orgName + '/' + secretName + ': '\n            if resp:\n                try:\n                    raise AnsibleError(msg + resp[0]['message'])\n                except (IndexError, KeyError, TypeError):\n                    pass\n\n            if r.text:\n                raise AnsibleError(msg + r.text)\n\n            raise AnsibleError(msg + 'Invalid server response: ' + str(r.status_code))\n\n        data = r.json()\n        ret.append(str(data['publicKey']))\n\n        return ret\n\n    def get_json(self, r):\n        try:\n            return r.json()\n        except ValueError:\n            # no JSON returned\n            return\n"
  },
  {
    "path": "plugins/tasks/ansible/src/main/resources/com/walmartlabs/concord/plugins/ansible/lookup/concord_secret.py",
    "content": "from __future__ import (absolute_import, division, print_function)\n\n__metaclass__ = type\n\nimport os\nimport sys\n\nsys.path.insert(1, os.path.join(os.path.dirname(__file__), os.pardir, \"_lookups\"))\nfrom concord_data_secret import LookupModule as LookupSecret\n\n\n# This lookup plugin will be removed in future - Kindly use 'concord_data_secret'\nclass LookupModule(LookupSecret):\n    pass\n"
  },
  {
    "path": "plugins/tasks/ansible/src/test/java/com/walmartlabs/concord/plugins/ansible/AbstractTest.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic abstract class AbstractTest {\n\n    protected void assertFile(String resource, Path actual) throws Exception {\n        assertEquals(read(resource), new String(Files.readAllBytes(actual.toAbsolutePath())));\n    }\n\n    protected Path tempDir(String prefix) throws Exception {\n        return Files.createTempDirectory(Paths.get(System.getProperty(\"java.io.tmpdir\")), prefix);\n    }\n\n    protected String read(String f) throws Exception {\n        URI uri = getClass().getResource(f).toURI();\n        return new String(Files.readAllBytes(Paths.get(uri)));\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/test/java/com/walmartlabs/concord/plugins/ansible/AnsibleConfigTest.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.HashMap;\nimport java.util.UUID;\n\npublic class AnsibleConfigTest extends AbstractTest {\n\n    @Test\n    public void testDefaultConfig() throws Exception {\n        Path workDir = tempDir(\"ansible-config-workdir\");\n        Path tmpDir = workDir.resolve(\"tmp\");\n        Files.createDirectories(tmpDir);\n\n        AnsibleContext context = AnsibleContext.builder()\n                .apiBaseUrl(\"http://localhost:8001\")\n                .instanceId(UUID.randomUUID())\n                .workDir(workDir)\n                .tmpDir(tmpDir)\n                .build();\n\n        AnsibleConfig cfg = new AnsibleConfig(context);\n        cfg.parse(new HashMap<>());\n\n        new AnsibleCallbacks(workDir, tmpDir, false).enrich(cfg);\n        new AnsibleLookup(tmpDir).enrich(cfg);\n\n        Path cfgPath = cfg.write();\n        assertFile(\"ansible.cfg\", workDir.resolve(cfgPath));\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/test/java/com/walmartlabs/concord/plugins/ansible/KerberosTest.java",
    "content": "package com.walmartlabs.concord.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.concurrent.TimeUnit;\n\n@Disabled\npublic class KerberosTest {\n\n    @Test\n    public void a() throws Exception {\n        Path tmpPath = Files.createTempDirectory(\"krb-test\");\n        System.out.println(\">>\" + tmpPath);\n\n        String username = \"USER\";\n        String password = \"PASSWORD\";\n        KerberosAuth kerberos = new KerberosAuth(username, password, tmpPath, false);\n\n        kerberos.prepare();\n\n        Thread.sleep(TimeUnit.MINUTES.toMillis(5));\n\n        kerberos.postProcess();\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/ansible/src/test/resources/com/walmartlabs/concord/plugins/ansible/ansible.cfg",
    "content": "[defaults]\ncallback_plugins = _callbacks\nhost_key_checking = false\nretry_files_enabled = true\nremote_tmp = /tmp/${USER}/ansible\ntimeout = 120\nlookup_plugins = _lookups\n[ssh_connection]\npipelining = true\n"
  },
  {
    "path": "plugins/tasks/asserts/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>asserts-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.inject</groupId>\n            <artifactId>guice</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <dependencies>\n                    <dependency>\n                        <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n                        <artifactId>concord-runner-v2</artifactId>\n                        <version>${project.version}</version>\n                    </dependency>\n                </dependencies>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/tasks/asserts/src/main/java/com/walmartlabs/concord/plugins/asserts/AssertsTask.java",
    "content": "package com.walmartlabs.concord.plugins.asserts;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.DryRunReady;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport com.walmartlabs.concord.runtime.v2.sdk.UserDefinedException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.math.BigDecimal;\nimport java.math.BigInteger;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\n\n@Named(\"asserts\")\n@DryRunReady\n@SuppressWarnings(\"unused\")\npublic class AssertsTask implements Task {\n\n    private final static Logger log = LoggerFactory.getLogger(AssertsTask.class);\n\n    private final Context context;\n\n    @Inject\n    public AssertsTask(Context context) {\n        this.context = context;\n    }\n\n    public void hasVariable(String name) {\n        boolean present = context.eval(String.format(\"${hasVariable('%s')}\", name), Boolean.class);\n        if (!present) {\n            throw new UserDefinedException(\"Variable '\" + name + \"' not found\");\n        }\n\n        Object value = context.eval(\"${\" + name + \"}\", Object.class);\n        if (value == null) {\n            throw new UserDefinedException(\"Variable '\" + name + \"' is null value\");\n        }\n\n        if (value instanceof String) {\n            if (((String) value).isBlank()) {\n                throw new UserDefinedException(\"Variable '\" + name + \"' is empty\");\n            }\n        }\n    }\n\n    public void hasFile(String path) {\n        if (Files.notExists(Paths.get(path))) {\n            throw new UserDefinedException(\"File '\" + path + \"' does not exist\");\n        }\n    }\n\n    public void assertEquals(Object expected, Object actual) {\n        if (expected == null && actual == null) {\n            return;\n        }\n\n        if (expected == null) {\n            throw new UserDefinedException(\"Expected value to be 'null' but is '\" + actual + \"'\");\n        } else if (actual == null) {\n            throw new UserDefinedException(\"Expected value to be '\" + expected + \"' but is 'null'\");\n        }\n\n        if (expected instanceof Number && actual instanceof Number) {\n            if (numbersEquals((Number) expected, (Number) actual)) {\n                return;\n            }\n        } else if (expected.equals(actual)) {\n            return;\n        }\n\n        String msg = String.format(\"Expected value to be '%s' (class: %s) but is '%s' (class: %s)\",\n                expected, expected.getClass().getName(),\n                actual, actual.getClass().getName());\n\n        throw new UserDefinedException(msg);\n    }\n\n    public static void assertTrue(boolean condition) {\n        if (!condition) {\n            throw new UserDefinedException(\"Expected value to be true but is false\");\n        }\n    }\n\n    public static void assertTrue(String message, boolean condition) {\n        if (!condition) {\n            throw new UserDefinedException(message);\n        }\n    }\n\n    public void dryRunMode() {\n        if (!context.processConfiguration().dryRun()) {\n            throw new UserDefinedException(\"Expected to run in the dry-run mode, running in the regular mode instead.\");\n        }\n    }\n\n    private static boolean isSpecialNumber(Number x) {\n        boolean specialDouble = x instanceof Double\n                && (Double.isNaN((Double) x) || Double.isInfinite((Double) x));\n        boolean specialFloat = x instanceof Float\n                && (Float.isNaN((Float) x) || Float.isInfinite((Float) x));\n        return specialDouble || specialFloat;\n    }\n\n    private static BigDecimal toBigDecimal(Number number) {\n        if (number instanceof BigDecimal) {\n            return (BigDecimal) number;\n        } else if (number instanceof BigInteger) {\n            return new BigDecimal((BigInteger) number);\n        } else if (number instanceof Byte || number instanceof Short\n                || number instanceof Integer || number instanceof Long) {\n            return BigDecimal.valueOf(number.longValue());\n        } else if (number instanceof Float || number instanceof Double) {\n            return BigDecimal.valueOf(number.doubleValue());\n        }\n\n        try {\n            return new BigDecimal(number.toString());\n        } catch (NumberFormatException e) {\n            throw new RuntimeException(\"The given number (\\\"\" + number + \"\\\" of class \" + number.getClass().getName() + \") does not have a parsable string representation\", e);\n        }\n    }\n\n    private boolean numbersEquals(Number expected, Number actual) {\n        if (isSpecialNumber(expected) || isSpecialNumber(actual)) {\n            return Double.compare(expected.doubleValue(), actual.doubleValue()) == 0;\n        } else {\n            return toBigDecimal(expected).compareTo(toBigDecimal(actual)) == 0;\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-model-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-client2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <!-- JDK8 data types. e.g. Optional -->\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jdk8</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <!-- JDK9 compatibility -->\n        <dependency>\n            <groupId>javax.xml.bind</groupId>\n            <artifactId>jaxb-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-compress</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-core</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <dependencies>\n                    <dependency>\n                        <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n                        <artifactId>concord-runner-v2</artifactId>\n                        <version>${project.version}</version>\n                    </dependency>\n                </dependencies>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/AbstractConcordTask.java",
    "content": "package com.walmartlabs.concord.client;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.sdk.ApiConfiguration;\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.ContextUtils;\nimport com.walmartlabs.concord.sdk.Task;\n\nimport javax.inject.Inject;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.client.Keys.SESSION_TOKEN_KEY;\n\npublic abstract class AbstractConcordTask implements Task {\n\n    protected static final String API_KEY = \"apiKey\";\n\n    @Inject\n    ApiConfiguration apiCfg;\n\n    @Inject\n    ApiClientFactory apiClientFactory;\n\n    protected <T> T withClient(Context ctx, CheckedFunction<ApiClient, T> f) throws Exception {\n        return withClient(ctx, true, f);\n    }\n\n    protected <T> T withClient(Context ctx, boolean withApiKey, CheckedFunction<ApiClient, T> f) throws Exception {\n        ImmutableApiClientConfiguration.Builder builder = ApiClientConfiguration.builder()\n                .baseUrl(getBaseUrl(ctx))\n                .sessionToken(ContextUtils.getSessionToken(ctx));\n\n        if (withApiKey) {\n            builder.apiKey(getApiKey(ctx));\n        }\n\n        ImmutableApiClientConfiguration cfg = builder.build();\n\n        return f.apply(apiClientFactory.create(cfg));\n    }\n\n    private String getApiKey(Context ctx) {\n        return (String) ctx.getVariable(API_KEY);\n    }\n\n    // TODO move to ClientUtils?\n    @SuppressWarnings(\"unchecked\")\n    private static String extractMessage(Object details) {\n        if (details == null) {\n            return null;\n        }\n\n        if (details instanceof List) {\n            List<Object> l = (List<Object>) details;\n            if (!l.isEmpty()) {\n                Object o = l.get(0);\n                if (o instanceof Map) {\n                    Map<String, Object> m = (Map<String, Object>) o;\n                    Object msg = m.get(\"message\");\n                    if (msg != null) {\n                        return msg.toString();\n                    }\n                }\n            }\n        }\n\n        return details.toString();\n    }\n\n    protected Map<String, Object> createCfg(Context ctx, String... keys) {\n        Map<String, Object> m = new HashMap<>();\n\n        String sessionToken = apiCfg.getSessionToken(ctx);\n        if (sessionToken != null) {\n            m.put(SESSION_TOKEN_KEY, sessionToken);\n        }\n\n        for (String k : keys) {\n            Object v = ctx.getVariable(k);\n            if (v != null) {\n                m.put(k, v);\n            }\n        }\n\n        return m;\n    }\n\n    protected static String getBaseUrl(Context ctx) {\n        Object v = ctx.getVariable(Keys.BASE_URL_KEY);\n        if (v == null) {\n            return null;\n        }\n\n        if (!(v instanceof String)) {\n            throw new IllegalArgumentException(\"Expected a string value '\" + Keys.BASE_URL_KEY + \"', got: \" + v);\n        }\n\n        return (String) v;\n    }\n\n    @FunctionalInterface\n    protected interface CheckedFunction<T, R> {\n        R apply(T t) throws Exception;\n    }\n}"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/ConcordTask.java",
    "content": "package com.walmartlabs.concord.client;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.ZipUtils;\nimport com.walmartlabs.concord.sdk.*;\nimport org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Named;\nimport javax.xml.bind.DatatypeConverter;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.client.Keys.ACTION_KEY;\nimport static com.walmartlabs.concord.sdk.MapUtils.*;\n\n@Named(\"concord\")\n@SuppressWarnings(\"unused\")\npublic class ConcordTask extends AbstractConcordTask {\n\n    private static final Logger log = LoggerFactory.getLogger(ConcordTask.class);\n\n    private static final long DEFAULT_KILL_TIMEOUT = 10000;\n    private static final long DEFAULT_POLL_DELAY = 5000;\n\n    /**\n     * @deprecated use {@link #PAYLOAD_KEY}\n     */\n    @Deprecated\n    private static final String ARCHIVE_KEY = \"archive\";\n\n    /**\n     * @deprecated use {@link #REPO_KEY}\n     */\n    @Deprecated\n    private static final String REPOSITORY_KEY = \"repository\";\n\n    private static final String ACTIVE_PROFILES_KEY = \"activeProfiles\";\n    private static final String ARGUMENTS_KEY = \"arguments\";\n    private static final String BASE_URL_KEY = \"baseUrl\";\n    private static final String DEBUG_KEY = \"debug\";\n    private static final String DISABLE_ON_CANCEL_KEY = \"disableOnCancel\";\n    private static final String DISABLE_ON_FAILURE_KEY = \"disableOnFailure\";\n    private static final String ENTRY_POINT_KEY = \"entryPoint\";\n    private static final String EXCLUSIVE_KEY = \"exclusive\";\n    private static final String FORKS_KEY = \"forks\";\n    private static final String IGNORE_FAILURES_KEY = \"ignoreFailures\";\n    private static final String INSTANCE_ID_KEY = \"instanceId\";\n    private static final String INSTANCES_KEY = \"instances\";\n    private static final String JOB_OUT_KEY = \"jobOut\";\n    private static final String JOBS_KEY = \"jobs\";\n    private static final String ORG_KEY = \"org\";\n    private static final String OUT_VARS_KEY = \"outVars\";\n    private static final String PAYLOAD_KEY = \"payload\";\n    private static final String ATTACHMENTS_KEY = \"attachments\";\n    private static final String PROJECT_KEY = \"project\";\n    private static final String REPO_BRANCH_OR_TAG_KEY = \"repoBranchOrTag\";\n    private static final String REPO_COMMIT_ID_KEY = \"repoCommitId\";\n    private static final String REPO_KEY = \"repo\";\n    private static final String START_AT_KEY = \"startAt\";\n    private static final String SYNC_KEY = \"sync\";\n    private static final String TAGS_KEY = \"tags\";\n    private static final String SUSPEND_KEY = \"suspend\";\n    private static final String REQUIREMENTS_KEY = \"requirements\";\n    private static final String META_KEY = \"meta\";\n\n    private static final String SUSPEND_MARKER = \"__concordTaskSuspend\";\n    private static final String RESUME_EVENT_NAME = \"concordTask\";\n\n    private static final int MAX_EXECUTOR_THREADS = 20;\n\n    private static final Set<String> FAILED_STATUSES;\n\n    static {\n        FAILED_STATUSES = new HashSet<>();\n        FAILED_STATUSES.add(ProcessEntry.StatusEnum.FAILED.toString());\n        FAILED_STATUSES.add(ProcessEntry.StatusEnum.CANCELLED.toString());\n        FAILED_STATUSES.add(ProcessEntry.StatusEnum.TIMED_OUT.toString());\n    }\n\n    private final ExecutorService executor = new ThreadPoolExecutor(1, MAX_EXECUTOR_THREADS, 30, TimeUnit.SECONDS, new LinkedBlockingQueue<>());\n\n    @InjectVariable(\"uiLinks\")\n    Map<String, Object> uiLinks;\n\n    @InjectVariable(\"concord\")\n    Map<String, Object> defaults;\n\n    @Override\n    public void execute(Context ctx) throws Exception {\n        Action action = getAction(ctx);\n        switch (action) {\n            case START: {\n                if (ContextUtils.getBoolean(ctx, SUSPEND_MARKER, false)) {\n                    continueAfterSuspend(ctx);\n                } else {\n                    startChildProcess(ctx);\n                }\n                break;\n            }\n            case STARTEXTERNAL: {\n                startExternalProcess(ctx);\n                break;\n            }\n            case FORK: {\n                if (ContextUtils.getBoolean(ctx, SUSPEND_MARKER, false)) {\n                    continueAfterSuspend(ctx);\n                } else {\n                    fork(ctx);\n                }\n                break;\n            }\n            case KILL: {\n                kill(ctx);\n                break;\n            }\n            default:\n                throw new IllegalArgumentException(\"Unsupported action type: \" + action);\n        }\n    }\n\n    public List<String> listSubprocesses(@InjectVariable(\"context\") Context ctx, String instanceId, String... tags) throws Exception {\n        Map<String, Object> m = new HashMap<>();\n        m.put(INSTANCE_ID_KEY, instanceId);\n        if (tags != null) {\n            m.put(TAGS_KEY, new HashSet<>(Arrays.asList(tags)));\n        }\n        return listSubprocesses(ctx, createJobCfg(ctx, m));\n    }\n\n    public List<String> listSubprocesses(@InjectVariable(\"context\") Context ctx, Map<String, Object> cfg) throws Exception {\n        UUID instanceId = getUUID(cfg, INSTANCE_ID_KEY);\n        Set<String> tags = getSet(cfg, TAGS_KEY);\n\n        return withClient(ctx, client -> {\n            ProcessApi api = new ProcessApi(client);\n\n            List<ProcessEntry> result = api.listSubprocesses(instanceId, tags);\n\n            return result.stream()\n                    .map(ProcessEntry::getInstanceId)\n                    .map(UUID::toString)\n                    .collect(Collectors.toList());\n        });\n    }\n\n    public void suspendForCompletion(@InjectVariable(\"context\") Context ctx, List<String> ids) throws Exception {\n        suspend(ctx, ids, false);\n    }\n\n    public Map<String, ProcessEntry> waitForCompletion(@InjectVariable(\"context\") Context ctx, List<String> ids) throws Exception {\n        return waitForCompletion(ctx, ids, -1);\n    }\n\n    public Map<String, ProcessEntry> waitForCompletion(@InjectVariable(\"context\") Context ctx, List<String> ids, long timeout) {\n        return waitForCompletion(ctx, ids, timeout, p -> p);\n    }\n\n    public <T> Map<String, T> waitForCompletion(@InjectVariable(\"context\") Context ctx, List<String> ids, long timeout, Function<ProcessEntry, T> processor) {\n        Map<String, T> result = new ConcurrentHashMap<>();\n\n        ids.parallelStream().map(UUID::fromString).forEach(id -> {\n            log.info(\"Waiting for {}, URL: {}\", id, getProcessUrl(ctx, id));\n\n            long t1 = System.currentTimeMillis();\n            while (true) {\n                try {\n                    ProcessEntry e = ClientUtils.withRetry(3, 1000, () -> withClient(ctx, client -> {\n                        ProcessV2Api api = new ProcessV2Api(client);\n                        return api.getProcess(id, Collections.singleton(\"childrenIds\"));\n                    }));\n\n                    ProcessEntry.StatusEnum s = e.getStatus();\n\n                    if (isFinalStatus(s)) {\n                        T t = processor.apply(e);\n                        if (t != null) {\n                            result.put(id.toString(), t);\n                        }\n                        break;\n                    } else {\n                        long t2 = System.currentTimeMillis();\n                        if (timeout > 0) {\n                            long dt = t2 - t1;\n                            if (dt >= timeout) {\n                                throw new TimeoutException(\"Timeout waiting for \" + id + \": \" + dt);\n                            }\n                        }\n\n                        Thread.sleep(DEFAULT_POLL_DELAY);\n                    }\n                } catch (Exception e) {\n                    throw new RuntimeException(e);\n                }\n            }\n        });\n\n        return result;\n    }\n\n    @SuppressWarnings(\"rawtypes\")\n    public void kill(@InjectVariable(\"context\") Context ctx, Map<String, Object> cfg) throws Exception {\n        List<String> ids = new ArrayList<>();\n\n        Object v = cfg.get(INSTANCE_ID_KEY);\n        if (v instanceof String || v instanceof UUID) {\n            ids.add(v.toString());\n        } else if (v instanceof String[] || v instanceof UUID[]) {\n            Object[] os = (Object[]) v;\n            for (Object o : os) {\n                ids.add(o.toString());\n            }\n        } else if (v instanceof Collection) {\n            for (Object o : (Collection) v) {\n                if (o instanceof String || o instanceof UUID) {\n                    ids.add(o.toString());\n                } else {\n                    throw new IllegalArgumentException(\"'\" + INSTANCE_ID_KEY + \"' value should be a string or an UUID: \" + o);\n                }\n            }\n        } else {\n            throw new IllegalArgumentException(\"'\" + INSTANCE_ID_KEY + \"' should be a single string, an UUID value or an array of strings or UUIDs: \" + v);\n        }\n\n        killMany(ctx, cfg, ids);\n    }\n\n    public Map<String, Map<String, Object>> getOutVars(@InjectVariable(\"context\") Context ctx, List<String> ids) {\n        return getOutVars(ctx, ids, -1);\n    }\n\n    public Map<String, Map<String, Object>> getOutVars(@InjectVariable(\"context\") Context ctx, List<String> ids, long timeout) {\n        return waitForCompletion(ctx, ids, timeout, p -> {\n            try {\n                return getOutVars(ctx, p.getInstanceId());\n            } catch (RuntimeException e) {\n                throw e;\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n        });\n    }\n\n    private void startExternalProcess(Context ctx) throws Exception {\n        // just the validation. AbstractConcordTask#withClient will take care of passing the API key into the client\n        ContextUtils.assertString(\"'\" + API_KEY + \"' is required to start a process on an external Concord instance\",\n                ctx, API_KEY);\n\n        Map<String, Object> cfg = createJobCfg(ctx, defaults);\n        boolean sync = getBoolean(cfg, SYNC_KEY, false);\n        boolean suspend = getBoolean(cfg, SUSPEND_KEY, false);\n        if (sync && suspend) {\n            log.warn(\"Input parameter '{}' ignored for {} action\", SUSPEND_KEY, Action.STARTEXTERNAL);\n            cfg.put(SUSPEND_KEY, false);\n        }\n\n        start(ctx, cfg, null);\n    }\n\n    private void startChildProcess(Context ctx) throws Exception {\n        UUID parentInstanceId = ContextUtils.getTxId(ctx);\n        start(ctx, parentInstanceId);\n    }\n\n    private void start(Context ctx, UUID parentInstanceId) throws Exception {\n        Map<String, Object> cfg = createJobCfg(ctx, defaults);\n        start(ctx, cfg, parentInstanceId);\n    }\n\n    private void start(Context ctx, Map<String, Object> cfg, UUID parentInstanceId) throws Exception {\n        Map<String, Object> req = createRequest(cfg);\n\n        Path workDir = ContextUtils.getWorkDir(ctx);\n\n        Path archive = archivePayload(workDir, cfg);\n        String project = getString(cfg, PROJECT_KEY);\n        if (project == null && archive == null) {\n            throw new IllegalArgumentException(\"'\" + PAYLOAD_KEY + \"' and/or '\" + PROJECT_KEY + \"' are required\");\n        }\n\n        Map<String, Object> input = new HashMap<>();\n\n        if (archive != null) {\n            input.put(\"archive\", Files.readAllBytes(archive));\n        }\n\n        ObjectMapper om = new ObjectMapper();\n        input.put(\"request\", om.writeValueAsBytes(req));\n\n        String org = getOrg(ctx, cfg);\n        addIfNotNull(input, \"org\", org);\n        addIfNotNull(input, \"project\", project);\n\n        String repo = getRepo(cfg);\n        addIfNotNull(input, \"repo\", repo);\n\n        List<Object> attachments = getList(cfg, ATTACHMENTS_KEY, Collections.emptyList());\n        processAttachments(attachments).forEach((d, p) -> addIfNotNull(input, d, p));\n\n        String repoBranchOrTag = getString(cfg, REPO_BRANCH_OR_TAG_KEY);\n        addIfNotNull(input, \"repoBranchOrTag\", repoBranchOrTag);\n\n        String repoCommitId = getString(cfg, REPO_COMMIT_ID_KEY);\n        addIfNotNull(input, \"repoCommitId\", repoCommitId);\n\n        String startAt = getStartAt(cfg);\n        addIfNotNull(input, \"startAt\", startAt);\n\n        addIfNotNull(input, \"parentInstanceId\", parentInstanceId);\n\n        boolean sync = getBoolean(cfg, SYNC_KEY, false);\n        boolean debug = getBoolean(cfg, DEBUG_KEY, false);\n        if (parentInstanceId != null) {\n            if (debug) {\n                log.info(\"Starting a child process (org={}, project={}, repository={}, archive={}, sync={}, req={})\",\n                        org, project, repo, archive, sync, req);\n            } else {\n                log.info(\"Starting a child process (org={}, project={}, repository={})\", org, project, repo);\n            }\n        } else {\n            if (debug) {\n                log.info(\"Starting a new process (org={}, project={}, repository={}, archive={}, sync={}, req={}), on {}\",\n                        org, project, repo, archive, sync, req, ctx.getVariable(BASE_URL_KEY));\n            } else {\n                log.info(\"Starting a new process (org={}, project={}, repository={}), on {}\",\n                        org, project, repo, ctx.getVariable(BASE_URL_KEY));\n            }\n        }\n\n        StartProcessResponse resp = withClient(ctx, apiClient -> new ProcessApi(apiClient).startProcess(input));\n\n        UUID processId = resp.getInstanceId();\n\n        log.info(\"Started a process: {}, URL: {}\", processId, getProcessUrl(ctx, processId));\n\n        List<String> jobs = Collections.singletonList(processId.toString());\n        ctx.setVariable(JOBS_KEY, jobs);\n\n        if (sync) {\n            boolean suspend = getBoolean(cfg, SUSPEND_KEY, false);\n            if (suspend) {\n                log.info(\"Suspending the process until the child process ({}) is completed...\", processId);\n                suspend(ctx, jobs, true);\n                return;\n            }\n\n            Map<String, ProcessEntry> result = waitForCompletion(ctx, jobs);\n            handleResults(cfg, result);\n\n            Object out = null;\n            if (cfg.containsKey(OUT_VARS_KEY)) {\n                out = getOutVars(ctx, processId);\n            }\n            ctx.setVariable(JOB_OUT_KEY, out != null ? out : Collections.emptyMap());\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private void continueAfterSuspend(Context ctx) throws Exception {\n        ctx.removeVariable(SUSPEND_MARKER);\n\n        Map<String, Object> cfg = createJobCfg(ctx, defaults);\n\n        List<String> jobs = (List<String>) ctx.getVariable(JOBS_KEY);\n\n        List<Result> results = new ArrayList<>();\n        for (String processId : jobs) {\n            Result r = continueAfterSuspend(ctx, cfg, UUID.fromString(processId));\n            results.add(r);\n        }\n\n        Map<String, ProcessEntry> instances = new HashMap<>();\n        for (Result r : results) {\n            String id = r.processEntry.getInstanceId().toString();\n            instances.put(id, r.processEntry);\n        }\n\n        handleResults(cfg, instances);\n\n        boolean single = jobs.size() == 1;\n\n        if (single) {\n            // if only one job was started put all variables at the top level of the jobOut object\n            // e.g. jobOut.someVar\n            Map<String, Object> out = results.get(0).out;\n            ctx.setVariable(JOB_OUT_KEY, out != null ? out : Collections.emptyMap());\n        } else {\n            // for multiple jobs save their variable into a nested map\n            // e.g. jobOut['PROCESSID'].someVar\n            Map<String, Map<String, Object>> vars = new HashMap<>();\n            for (Result r : results) {\n                String id = r.processEntry.getInstanceId().toString();\n                if (r.out != null) {\n                    vars.put(id, r.out);\n                }\n            }\n            ctx.setVariable(JOB_OUT_KEY, vars);\n        }\n    }\n\n    private Result continueAfterSuspend(Context ctx, Map<String, Object> cfg, UUID processId) throws Exception {\n        ProcessEntry e = ClientUtils.withRetry(3, 1000, () -> withClient(ctx, client -> {\n            ProcessV2Api api = new ProcessV2Api(client);\n            return api.getProcess(processId, Collections.singleton(\"childrenIds\"));\n        }));\n\n        ProcessEntry.StatusEnum s = e.getStatus();\n        if (!isFinalStatus(s)) {\n            throw new IllegalStateException(\"Process '\" + processId + \"' not finished\");\n        }\n\n        if (cfg.containsKey(OUT_VARS_KEY)) {\n            return new Result(e, getOutVars(ctx, processId));\n        }\n\n        return new Result(e, Collections.emptyMap());\n    }\n\n    private static class Result {\n\n        private final ProcessEntry processEntry;\n        private final Map<String, Object> out;\n\n        private Result(ProcessEntry processEntry, Map<String, Object> out) {\n            this.processEntry = processEntry;\n            this.out = out;\n        }\n    }\n\n    private void suspend(Context ctx, List<String> jobs, boolean resumeFromSameStep) throws ApiException {\n        if (jobs.isEmpty()) {\n            return;\n        }\n\n        Map<String, Object> condition = new HashMap<>();\n        condition.put(\"type\", \"PROCESS_COMPLETION\");\n        condition.put(\"reason\", \"Waiting for a child process to end\");\n        condition.put(\"processes\", jobs);\n        condition.put(\"resumeEvent\", RESUME_EVENT_NAME);\n\n        ClientUtils.withRetry(3, 1000, () -> withClient(ctx, false, client -> {\n            ProcessApi api = new ProcessApi(client);\n            api.setWaitCondition(ContextUtils.getTxId(ctx), condition);\n            return null;\n        }));\n\n        if (resumeFromSameStep) {\n            ctx.setVariable(SUSPEND_MARKER, true);\n        }\n\n        ctx.suspend(RESUME_EVENT_NAME, null, resumeFromSameStep);\n    }\n\n    private static void handleResults(Map<String, Object> cfg, Map<String, ProcessEntry> m) {\n        StringBuilder errors = new StringBuilder();\n        boolean hasErrors = false;\n        boolean ignoreFailures = getBoolean(cfg, IGNORE_FAILURES_KEY, false);\n        for (Map.Entry<String, ProcessEntry> e : m.entrySet()) {\n            String id = e.getKey();\n            String status = e.getValue().getStatus().getValue();\n            if (FAILED_STATUSES.contains(status)) {\n                Map<String, Object> error = getError(e.getValue());\n                String errorMessage = \"\";\n                if (!error.isEmpty()) {\n                    errorMessage = \"(error: \" + error + \")\";\n                }\n\n                if (ignoreFailures) {\n                    log.warn(\"Child process {} {} {}, ignoring...\", id, status, errorMessage);\n                    continue;\n                }\n\n                errors.append(\"Child process \").append(id).append(\" \").append(status).append(\" \").append(errorMessage);\n                errors.append(\"\\n\");\n                hasErrors = true;\n            }\n        }\n\n        if (hasErrors) {\n            throw new IllegalStateException(errors.toString());\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Map<String, Object> getError(ProcessEntry p) {\n        Map<String, Object> meta = p.getMeta();\n        if (meta == null) {\n            return Collections.emptyMap();\n        }\n\n        Map<String, Object> out = (Map<String, Object>) meta.get(\"out\");\n        if (out == null) {\n            return Collections.emptyMap();\n        }\n\n        return (Map<String, Object>) out.getOrDefault(Constants.Context.LAST_ERROR_KEY, Collections.emptyMap());\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private Map<String, Object> getOutVars(Context ctx, UUID processId) throws Exception {\n        return withClient(ctx, client -> {\n            ProcessApi api = new ProcessApi(client);\n\n            try (InputStream is = api.downloadAttachment(processId, \"out.json\")){\n                ObjectMapper om = new ObjectMapper();\n                return om.readValue(is, Map.class);\n            } catch (ApiException e) {\n                if (e.getCode() == 404) {\n                    return null;\n                }\n                log.error(\"Error while reading the out variables\", e);\n                throw e;\n            }\n        });\n    }\n\n    private void fork(Context ctx) throws Exception {\n        List<Map<String, Object>> jobs = ContextUtils.getList(ctx, FORKS_KEY, null);\n        if (jobs == null) {\n            jobs = Collections.singletonList(createJobCfg(ctx, null));\n        }\n\n        if (jobs.isEmpty()) {\n            throw new IllegalArgumentException(\"'\" + FORKS_KEY + \"' can't be an empty list\");\n        }\n\n        List<String> jobIds = forkMany(ctx, jobs);\n        ctx.setVariable(JOBS_KEY, jobIds);\n    }\n\n    private List<String> forkMany(Context ctx, List<Map<String, Object>> jobs) throws Exception {\n        List<String> ids = new ArrayList<>();\n\n        List<Future<UUID>> futures = new ArrayList<>();\n        for (Map<String, Object> job : jobs) {\n            Map<String, Object> cfg = createJobCfg(ctx, job);\n            cfg.put(INSTANCE_ID_KEY, ctx.getVariable(Constants.Context.TX_ID_KEY));\n\n            int n = getInstances(cfg);\n            for (int i = 0; i < n; i++) {\n                futures.add(forkOne(ctx, cfg));\n            }\n\n        }\n\n        // collect all futures, effectively blocking until all forks are started\n        for (Future<UUID> f : futures) {\n            ids.add(f.get().toString());\n        }\n\n        Map<String, Object> cfg = createJobCfg(ctx, defaults);\n        boolean sync = getBoolean(cfg, SYNC_KEY, false);\n        if (sync) {\n            boolean suspend = getBoolean(cfg, SUSPEND_KEY, false);\n            if (suspend) {\n                log.info(\"Suspending the process until the fork processes ({}) are completed...\", ids);\n                suspend(ctx, ids, true);\n                return ids;\n            }\n\n            Map<String, ProcessEntry> result = waitForCompletion(ctx, ids);\n            handleResults(cfg, result);\n        }\n\n        return ids;\n    }\n\n    private Future<UUID> forkOne(Context ctx, Map<String, Object> cfg) {\n        if (cfg.containsKey(ARCHIVE_KEY)) {\n            log.warn(\"'\" + ARCHIVE_KEY + \"' parameter is not supported for fork action and will be ignored\");\n        }\n\n        if (!cfg.containsKey(ENTRY_POINT_KEY)) {\n            throw new IllegalArgumentException(\"'\" + ENTRY_POINT_KEY + \"' is required\");\n        }\n\n        Map<String, Object> req = createRequest(cfg);\n\n        UUID instanceId = assertUUID(cfg, INSTANCE_ID_KEY);\n\n        boolean sync = getBoolean(cfg, SYNC_KEY, false);\n        boolean debug = getBoolean(cfg, DEBUG_KEY, false);\n        if (debug) {\n            log.info(\"Forking the current instance (sync={}, req={})...\", sync, req);\n        }\n\n        return executor.submit(() -> withClient(ctx, client -> {\n            ProcessApi api = new ProcessApi(client);\n            StartProcessResponse resp = api.fork(instanceId, false, null, req);\n            log.info(\"Forked a child process: {} url: {}\", resp.getInstanceId(), getProcessUrl(ctx, resp.getInstanceId()));\n            return resp.getInstanceId();\n        }));\n    }\n\n    private void kill(Context ctx) throws Exception {\n        Map<String, Object> cfg = createCfg(ctx, INSTANCE_ID_KEY);\n        kill(ctx, cfg);\n    }\n\n    private void killMany(Context ctx, Map<String, Object> cfg, List<String> instanceIds) throws Exception {\n        if (instanceIds == null) {\n            throw new IllegalArgumentException(\"'\" + INSTANCE_ID_KEY + \"' should be a single value or an array of values: \" + instanceIds);\n        }\n\n        if (instanceIds.isEmpty()) {\n            log.warn(\"kill: no process IDs specified, nothing to do.\");\n            return;\n        }\n\n        for (String id : instanceIds) {\n            killOne(ctx, cfg, id);\n        }\n    }\n\n    private void killOne(Context ctx, Map<String, Object> cfg, String instanceId) throws Exception {\n        withClient(ctx, client -> {\n            ProcessApi api = new ProcessApi(client);\n            api.kill(UUID.fromString(instanceId));\n            return null;\n        });\n\n        boolean sync = getBoolean(cfg, SYNC_KEY, false);\n        if (sync) {\n            waitForCompletion(ctx, Collections.singletonList(instanceId), DEFAULT_KILL_TIMEOUT);\n        }\n    }\n\n    private Map<String, Object> createJobCfg(Context ctx, Map<String, Object> job) {\n        Map<String, Object> m = createCfg(ctx,\n                ACTIVE_PROFILES_KEY,\n                ARCHIVE_KEY,\n                ARGUMENTS_KEY,\n                DEBUG_KEY,\n                DISABLE_ON_CANCEL_KEY,\n                DISABLE_ON_FAILURE_KEY,\n                ENTRY_POINT_KEY,\n                EXCLUSIVE_KEY,\n                IGNORE_FAILURES_KEY,\n                INSTANCE_ID_KEY,\n                ORG_KEY,\n                OUT_VARS_KEY,\n                PAYLOAD_KEY,\n                ATTACHMENTS_KEY,\n                PROJECT_KEY,\n                REPO_BRANCH_OR_TAG_KEY,\n                REPO_COMMIT_ID_KEY,\n                REPO_KEY,\n                REPOSITORY_KEY,\n                START_AT_KEY,\n                SYNC_KEY,\n                TAGS_KEY,\n                SUSPEND_KEY,\n                REQUIREMENTS_KEY,\n                META_KEY);\n\n        if (job != null) {\n            m.putAll(job);\n        }\n\n        return m;\n    }\n\n    private String getProcessUrl(Context ctx, UUID processId) {\n        String action = ContextUtils.getString(ctx, ACTION_KEY);\n        if (Action.STARTEXTERNAL.name().equalsIgnoreCase(action) || uiLinks == null) {\n            return \"n/a\";\n        }\n\n        String processLinkTemplate = getString(uiLinks, \"process\");\n        if (processLinkTemplate == null) {\n            return \"n/a\";\n        }\n\n        String baseUrl = getBaseUrl(ctx);\n        if (action == null && baseUrl != null && !processLinkTemplate.startsWith(baseUrl)) {\n            return \"n/a\";\n        }\n\n        return String.format(processLinkTemplate, processId);\n    }\n\n    private static void addIfNotNull(Map<String, Object> m, String k, Object v) {\n        if (v == null) {\n            return;\n        }\n        m.put(k, v);\n    }\n\n    /**\n     * Processes a list of attachment parameters. An attachment may be a string which represents\n     * the path to a file or a Map which specifies the destination filename (key) and current filename (value)\n     *\n     * @param attachments List of attachments\n     * @return Map of attachments. Key is the destination filename. Value is path to the local file.\n     */\n    private static Map<String, Path> processAttachments(List<Object> attachments) {\n        if (attachments.isEmpty()) {\n            return Collections.emptyMap();\n        }\n\n        Map<String, Path> result = new HashMap<>(attachments.size());\n        for (Object o : attachments) {\n            if (o instanceof String) {\n                String key = ((String) o).replaceFirst(\"^.*/\", \"\");\n\n                result.put(key, Paths.get((String) o));\n            } else if (o instanceof Map) {\n                Map m = (Map) o;\n                result.put((String) m.get(\"dest\"), Paths.get((String) m.get(\"src\")));\n            } else {\n                throw new IllegalArgumentException(\"Unsupported attachment formatting provided. Must be a string or map. Received \" + o.getClass());\n            }\n        }\n\n        return result;\n    }\n\n    private static Path archivePayload(Path workDir, Map<String, Object> cfg) throws IOException {\n        String s = getString(cfg, PAYLOAD_KEY);\n        if (s == null) {\n            s = getString(cfg, ARCHIVE_KEY);\n            if (s != null) {\n                log.warn(\"'{}' is deprecated, please use '{}' parameter\", ARCHIVE_KEY, PAYLOAD_KEY);\n            }\n        }\n\n        if (s == null) {\n            return null;\n        }\n\n        Path path = workDir.resolve(s);\n        if (!Files.exists(path)) {\n            throw new IllegalArgumentException(\"File or directory not found: \" + path);\n        }\n\n        if (Files.isDirectory(path)) {\n            Path tmp = PathUtils.createTempFile(\"payload\", \".zip\");\n            try (ZipArchiveOutputStream out = new ZipArchiveOutputStream(Files.newOutputStream(tmp))) {\n                ZipUtils.zip(out, path);\n            }\n            return tmp;\n        }\n\n        return path;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Map<String, Object> createRequest(Map<String, Object> cfg) {\n        Map<String, Object> req = new HashMap<>();\n\n        Set<String> activeProfiles = getSet(cfg, ACTIVE_PROFILES_KEY);\n        if (activeProfiles != null) {\n            req.put(Constants.Request.ACTIVE_PROFILES_KEY, activeProfiles);\n        }\n\n        String entryPoint = getString(cfg, ENTRY_POINT_KEY);\n        if (entryPoint != null) {\n            req.put(Constants.Request.ENTRY_POINT_KEY, entryPoint);\n        }\n\n        if (cfg.get(EXCLUSIVE_KEY) != null) {\n            Map<String, Object> exclusive = MapUtils.getMap(cfg, EXCLUSIVE_KEY, null);\n            req.put(Constants.Request.EXCLUSIVE, exclusive);\n        }\n\n        Set<String> tags = getSet(cfg, TAGS_KEY);\n        if (tags != null) {\n            req.put(Constants.Request.TAGS_KEY, tags);\n        }\n\n        Map<String, Object> args = getMap(cfg, ARGUMENTS_KEY, Collections.emptyMap());\n        if (!args.isEmpty()) {\n            req.put(Constants.Request.ARGUMENTS_KEY, new HashMap<>(args));\n        }\n\n        if (getBoolean(cfg, DISABLE_ON_CANCEL_KEY, false)) {\n            req.put(Constants.Request.DISABLE_ON_CANCEL_KEY, true);\n        }\n\n        if (getBoolean(cfg, DISABLE_ON_FAILURE_KEY, false)) {\n            req.put(Constants.Request.DISABLE_ON_FAILURE_KEY, true);\n        }\n\n        Collection<String> outVars = (Collection<String>) cfg.get(OUT_VARS_KEY);\n        if (outVars != null && !outVars.isEmpty()) {\n            req.put(Constants.Request.OUT_EXPRESSIONS_KEY, outVars);\n        }\n\n        Object meta = cfg.get(META_KEY);\n        if (meta != null) {\n            req.put(Constants.Request.META, meta);\n        }\n\n        Map<String, Object> requirements = getMap(cfg, Constants.Request.REQUIREMENTS, Collections.emptyMap());\n        if (!requirements.isEmpty()) {\n            req.put(Constants.Request.REQUIREMENTS, new HashMap<>(requirements));\n        }\n\n        return req;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Set<String> getSet(Map<String, Object> cfg, String k) {\n        if (cfg == null) {\n            return null;\n        }\n\n        Object v = cfg.get(k);\n        if (v == null) {\n            return null;\n        }\n\n        if (v instanceof String) {\n            return Arrays.stream(((String) v)\n                            .split(\",\"))\n                    .map(String::trim)\n                    .collect(Collectors.toSet());\n        } else if (v instanceof String[]) {\n            return new HashSet<>(Arrays.asList((String[]) v));\n        } else if (v instanceof Collection) {\n            return new HashSet<>((Collection) v);\n        } else {\n            throw new IllegalArgumentException(\"'\" + k + \"' must a single string value or an array of strings: \" + v);\n        }\n    }\n\n    private static String getOrg(Context ctx, Map<String, Object> cfg) {\n        String org = getString(cfg, ORG_KEY);\n        if (org != null) {\n            return org;\n        }\n\n        ProjectInfo pi = ContextUtils.getProjectInfo(ctx);\n        if (pi != null) {\n            return pi.orgName();\n        }\n        return null;\n    }\n\n    private static String getRepo(Map<String, Object> cfg) {\n        String repo = getString(cfg, REPO_KEY);\n        if (repo != null) {\n            return repo;\n        }\n        repo = getString(cfg, REPOSITORY_KEY);\n        if (repo != null) {\n            log.warn(\"'{}' is deprecated, please use '{}' parameter\", REPOSITORY_KEY, REPO_KEY);\n        }\n        return repo;\n    }\n\n    private static Action getAction(Context ctx) {\n        String action = ContextUtils.assertString(ctx, ACTION_KEY);\n        try {\n            return Action.valueOf(action.trim().toUpperCase());\n        } catch (IllegalArgumentException e) {\n            throw new RuntimeException(\"Unknown action: '\" + action + \"'. Available actions: \" + Arrays.toString(Action.values()));\n        }\n    }\n\n    private static int getInstances(Map<String, Object> cfg) {\n        int i;\n\n        Object v = cfg.getOrDefault(INSTANCES_KEY, 1);\n        if (v instanceof Integer) {\n            i = (Integer) v;\n        } else if (v instanceof Long) {\n            i = ((Long) v).intValue();\n        } else {\n            throw new IllegalArgumentException(\"'\" + INSTANCES_KEY + \"' must be a number\");\n        }\n\n        if (i <= 0) {\n            throw new IllegalArgumentException(\"'\" + INSTANCES_KEY + \"' must be a positive number\");\n        }\n\n        return i;\n    }\n\n    private static String getStartAt(Map<String, Object> cfg) {\n        Object v = cfg.get(START_AT_KEY);\n        if (v == null) {\n            return null;\n        }\n\n        if (v instanceof String) {\n            return (String) v;\n        } else if (v instanceof Date) {\n            Calendar c = Calendar.getInstance();\n            c.setTime((Date) v);\n            return DatatypeConverter.printDateTime(c);\n        } else if (v instanceof Calendar) {\n            return DatatypeConverter.printDateTime((Calendar) v);\n        } else {\n            throw new IllegalArgumentException(\"'\" + START_AT_KEY + \"' must be a string, java.util.Date or java.util.Calendar value. Got: \" + v);\n        }\n    }\n\n    private static boolean isFinalStatus(ProcessEntry.StatusEnum s) {\n        return s == ProcessEntry.StatusEnum.FAILED\n                || s == ProcessEntry.StatusEnum.FINISHED\n                || s == ProcessEntry.StatusEnum.CANCELLED\n                || s == ProcessEntry.StatusEnum.TIMED_OUT;\n    }\n\n\n    private enum Action {\n\n        START,\n        STARTEXTERNAL,\n        FORK,\n        KILL\n    }\n}"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/ConcordTaskCommon.java",
    "content": "package com.walmartlabs.concord.client;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.ZipUtils;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.UserDefinedException;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.LogTags;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.OffsetDateTime;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.client.ConcordTaskParams.*;\n\npublic class ConcordTaskCommon {\n\n    private static final Logger log = LoggerFactory.getLogger(ConcordTaskCommon.class);\n\n    private static final long DEFAULT_KILL_TIMEOUT = 10000;\n    private static final long DEFAULT_POLL_DELAY = 5000;\n\n    private static final int MAX_EXECUTOR_THREADS = 20;\n\n    private static final Set<String> FAILED_STATUSES;\n\n    static {\n        FAILED_STATUSES = new HashSet<>();\n        FAILED_STATUSES.add(ProcessEntry.StatusEnum.FAILED.toString());\n        FAILED_STATUSES.add(ProcessEntry.StatusEnum.CANCELLED.toString());\n        FAILED_STATUSES.add(ProcessEntry.StatusEnum.TIMED_OUT.toString());\n    }\n\n    private final ExecutorService executor = new ThreadPoolExecutor(1, MAX_EXECUTOR_THREADS, 30, TimeUnit.SECONDS, new LinkedBlockingQueue<>());\n\n    private final String sessionToken;\n    private final ApiClientFactory apiClientFactory;\n    private final UUID currentProcessId;\n    private final String currentOrgName;\n    private final Path workDir;\n    private final boolean globalDebug;\n    private final boolean dryRun;\n\n    public ConcordTaskCommon(String sessionToken, ApiClientFactory apiClientFactory, UUID currentProcessId, String currentOrgName, Path workDir, boolean globalDebug, boolean dryRun) {\n        this.sessionToken = sessionToken;\n        this.apiClientFactory = apiClientFactory;\n        this.currentProcessId = currentProcessId;\n        this.currentOrgName = currentOrgName;\n        this.workDir = workDir;\n        this.globalDebug = globalDebug;\n        this.dryRun = dryRun;\n    }\n\n    public TaskResult execute(ConcordTaskParams in) throws Exception {\n        Action action = in.action();\n        return switch (action) {\n            case START -> startChildProcess((StartParams) in);\n            case STARTEXTERNAL -> startExternalProcess((StartExternalParams) in);\n            case FORK -> fork((ForkParams) in);\n            case KILL -> {\n                kill((KillParams) in);\n                yield TaskResult.success();\n            }\n            case CREATEAPIKEY -> createApiKey((CreateOrUpdateApiKeyParams) in);\n            case CREATEORUPDATEAPIKEY -> createOrUpdateApiKey((CreateOrUpdateApiKeyParams) in);\n        };\n    }\n\n    public List<ProcessEntry> listSubProcesses(ListSubProcesses in) throws Exception {\n        UUID instanceId = in.instanceId();\n        Set<String> tags = in.tags();\n\n        return withClient(client -> {\n            ProcessApi api = new ProcessApi(client);\n\n            return api.listSubprocesses(instanceId, tags);\n        });\n    }\n\n    public String suspendForCompletion(List<UUID> ids) throws Exception {\n        TaskResult result = suspend(new ResumePayload(null, null, false, ids, false), false);\n        if (!(result instanceof TaskResult.SuspendResult)) {\n            throw new RuntimeException(\"Invalid result type. This is most likely a bug.\");\n        }\n        return ((TaskResult.SuspendResult) result).eventName();\n    }\n\n    public <T> Map<String, T> waitForCompletion(List<UUID> ids, long timeout, Function<ProcessEntry, T> processor) {\n        Map<String, T> result = new ConcurrentHashMap<>();\n\n        ids.parallelStream().forEach(id -> {\n            log.info(\"Waiting for {}\", LogTags.instanceId(id));\n\n            long t1 = System.currentTimeMillis();\n            while (true) {\n                try {\n                    ProcessEntry e = ClientUtils.withRetry(3, 1000,\n                            () -> withClient(client -> {\n                                ProcessV2Api api = new ProcessV2Api(client);\n                                return api.getProcess(id, Collections.emptySet());\n                            }));\n\n                    ProcessEntry.StatusEnum s = e.getStatus();\n\n                    if (isFinalStatus(s)) {\n                        T t = processor.apply(e);\n                        if (t != null) {\n                            result.put(id.toString(), t);\n                        }\n                        break;\n                    } else {\n                        long t2 = System.currentTimeMillis();\n                        if (timeout > 0) {\n                            long dt = t2 - t1;\n                            if (dt >= timeout) {\n                                throw new TimeoutException(\"Timeout waiting for \" + id + \": \" + dt);\n                            }\n                        }\n\n                        Thread.sleep(DEFAULT_POLL_DELAY);\n                    }\n                } catch (Exception e) {\n                    throw new RuntimeException(e);\n                }\n            }\n        });\n\n        return result;\n    }\n\n    public void kill(KillParams in) throws Exception {\n        List<UUID> instanceIds = in.ids();\n        if (instanceIds.isEmpty()) {\n            log.warn(\"kill: no process IDs specified, nothing to do.\");\n            return;\n        }\n\n        for (UUID id : instanceIds) {\n            withClient(client -> {\n                ProcessApi api = new ProcessApi(client);\n                api.kill(id);\n                return null;\n            });\n\n            if (in.sync()) {\n                waitForCompletion(Collections.singletonList(id), DEFAULT_KILL_TIMEOUT, Function.identity());\n            }\n        }\n    }\n\n    public TaskResult createApiKey(CreateOrUpdateApiKeyParams in) throws Exception {\n        return withClient(in.baseUrl(), in.apiKey(), client -> {\n            log.info(\"Creating a new API key in {}\", client.getBaseUri());\n\n            UUID userId = assertUserId(client, in);\n\n            String keyName = in.name();\n            if (keyName != null) {\n                ApiKeysApi api = new ApiKeysApi(client);\n                List<ApiKeyEntry> existingKeys = api.listUserApiKeys(userId);\n                Optional<ApiKeyEntry> maybeExistingKey = existingKeys.stream().filter(k -> k.getName().equals(keyName)).findFirst();\n                if (maybeExistingKey.isPresent()) {\n                    if (in.ignoreExisting()) {\n                        log.info(\"API key '{}' already exists, nothing to do.\", keyName);\n                        ApiKeyEntry existingKey = maybeExistingKey.get();\n                        return TaskResult.success()\n                                .value(\"id\", existingKey.getId())\n                                .value(\"expiredAt\", Optional.ofNullable(existingKey.getExpiredAt()).map(OffsetDateTime::toString).orElse(null));\n                    } else {\n                        throw new IllegalArgumentException(\"API key '\" + keyName + \"' already exists.\");\n                    }\n                }\n            }\n\n            ApiKeysApi apiKeysApi = new ApiKeysApi(client);\n            CreateApiKeyResponse response = apiKeysApi.createUserApiKey(new CreateApiKeyRequest()\n                    .name(keyName)\n                    .userId(userId)\n                    .userDomain(in.userDomain())\n                    .userType(in.userType())\n                    .key(in.key()));\n\n            return TaskResult.success()\n                    .value(\"id\", response.getId())\n                    .value(\"name\", response.getName())\n                    .value(\"key\", response.getKey());\n        });\n    }\n\n    public TaskResult createOrUpdateApiKey(CreateOrUpdateApiKeyParams in) throws Exception {\n        return withClient(in.baseUrl(), in.apiKey(), client -> {\n            log.info(\"Creating or updating an API key in {}\", client.getBaseUri());\n\n            UUID userId = assertUserId(client, in);\n\n            ApiKeysV2Api apiKeysApi = new ApiKeysV2Api(client);\n            CreateApiKeyResponse response = apiKeysApi.createOrUpdateUserApiKey(new CreateApiKeyRequest()\n                    .name(in.name())\n                    .userId(userId)\n                    .userDomain(in.userDomain())\n                    .userType(in.userType())\n                    .key(in.key()));\n\n            return TaskResult.success()\n                    .value(\"id\", response.getId())\n                    .value(\"name\", response.getName())\n                    .value(\"key\", response.getKey())\n                    .value(\"result\", response.getResult().toString());\n        });\n    }\n\n    private UUID assertUserId(ApiClient client, CreateOrUpdateApiKeyParams in) throws ApiException {\n        UUID userId = in.userId();\n        if (userId == null) {\n            String username = in.username();\n            if (username == null) {\n                throw new IllegalArgumentException(\"User ID or user name is required\");\n            }\n\n            UsersApi usersApi = new UsersApi(client);\n            UserEntry user = usersApi.findByUsername(username);\n            if (user == null) {\n                throw new IllegalArgumentException(\"User '\" + username + \"' not found.\");\n            }\n            userId = user.getId();\n        }\n        return userId;\n    }\n\n    public Map<String, Map<String, Object>> getOutVars(String baseUrl, String apiKey, List<UUID> ids, long timeout) {\n        return waitForCompletion(ids, timeout, p -> {\n            try {\n                return getOutVars(baseUrl, apiKey, p.getInstanceId());\n            } catch (RuntimeException e) {\n                throw e;\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n        });\n    }\n\n    private TaskResult startExternalProcess(StartExternalParams in) throws Exception {\n        if (in.sync() && in.suspendRaw()) {\n            log.warn(\"Input parameter '{}' ignored for {} action\", StartParams.SUSPEND_KEY, Action.STARTEXTERNAL);\n        }\n\n        return start(in, null);\n    }\n\n    private TaskResult startChildProcess(StartParams in) throws Exception {\n        return start(in, currentProcessId);\n    }\n\n    private TaskResult start(StartParams in, UUID parentInstanceId) throws Exception {\n        Path archive = archivePayload(workDir, in);\n        String project = in.project();\n        if (project == null && archive == null) {\n            throw new IllegalArgumentException(\"'\" + StartParams.PAYLOAD_KEY + \"' and/or '\" + StartParams.PROJECT_KEY + \"' are required\");\n        }\n\n        Map<String, Object> input = new HashMap<>();\n\n        if (archive != null) {\n            input.put(\"archive\", archive);\n        }\n\n        Map<String, Object> req = createRequest(in);\n\n        ObjectMapper om = new ObjectMapper();\n        input.put(\"request\", om.writeValueAsBytes(req));\n\n        String org = getOrg(in);\n        addIfNotNull(input, \"org\", org);\n        addIfNotNull(input, \"project\", project);\n\n        addIfNotNull(input, \"repo\", in.repo());\n        addIfNotNull(input, \"repoBranchOrTag\", in.repoBranchOrTag());\n        addIfNotNull(input, \"repoCommitId\", in.repoCommitId());\n\n        Collection<Object> attachments = in.attachments();\n        processAttachments(attachments).forEach((d, p) -> addIfNotNull(input, d, p));\n\n        addIfNotNull(input, \"startAt\", in.startAt());\n\n        addIfNotNull(input, \"parentInstanceId\", parentInstanceId);\n\n        boolean sync = in.sync();\n        boolean debug = in.debug(globalDebug);\n        if (parentInstanceId != null) {\n            if (debug) {\n                log.info(\"Starting a child process (org={}, project={}, repository={}, archive={}, sync={}, req={})\",\n                        org, project, in.repo(), archive, sync, req);\n            } else {\n                log.info(\"Starting a child process (org={}, project={}, repository={})\", org, project, in.repo());\n            }\n        } else {\n            if (debug) {\n                log.info(\"Starting a new process (org={}, project={}, repository={}, archive={}, sync={}, req={}), on {}\",\n                        org, project, in.repo(), archive, sync, req, in.baseUrl());\n            } else {\n                log.info(\"Starting a new process (org={}, project={}, repository={}), on {}\",\n                        org, project, in.repo(), in.baseUrl());\n            }\n        }\n\n        StartProcessResponse resp = withClient(in.baseUrl(), in.apiKey(),\n                client -> new ProcessApi(client).startProcess(input));\n\n        UUID processId = resp.getInstanceId();\n\n        log.info(\"Started a process: {}\",\n                in.action() == Action.STARTEXTERNAL ? processId : LogTags.instanceId(processId));\n\n        if (sync) {\n            if (in.suspend()) {\n                log.info(\"Suspending the process until the child process ({}) is completed...\",\n                        in.action() == Action.STARTEXTERNAL ? processId : LogTags.instanceId(processId));\n\n                ResumePayload resume = new ResumePayload(\n                        in.baseUrl(), in.apiKey(), !in.outVars().isEmpty(),\n                        Collections.singletonList(processId), in.ignoreFailures());\n\n                return suspend(resume, true);\n            }\n\n            Map<String, ProcessEntry> result = waitForCompletion(Collections.singletonList(processId), -1, Function.identity());\n            handleResults(result, in.ignoreFailures());\n\n            Map<String, Object> out = Collections.emptyMap();\n            if (!in.outVars().isEmpty()) {\n                out = getOutVars(in.baseUrl(), in.apiKey(), processId);\n            }\n\n            return TaskResult.success()\n                    .value(\"id\", processId.toString())\n                    .values(out);\n        }\n\n        return TaskResult.success()\n                .value(\"id\", processId.toString());\n    }\n\n    public TaskResult continueAfterSuspend(ResumePayload payload) throws Exception {\n        List<Result> results = new ArrayList<>();\n        for (UUID processId : payload.jobs()) {\n            Result r = continueAfterSuspend(payload.baseUrl(), payload.apiKey(), processId, payload.collectOutVars());\n            results.add(r);\n        }\n\n        Map<String, ProcessEntry> instances = new HashMap<>();\n        for (Result r : results) {\n            UUID id = r.processEntry.getInstanceId();\n            instances.put(id.toString(), r.processEntry);\n        }\n\n        handleResults(instances, payload.ignoreFailures());\n\n        HashMap<String, Object> vars = new HashMap<>();\n        for (Result r : results) {\n            String id = r.processEntry.getInstanceId().toString();\n            if (r.out != null) {\n                vars.put(id, r.out);\n            }\n        }\n\n        TaskResult.SimpleResult result = TaskResult.success()\n                .value(\"ids\", payload.jobs().stream().map(UUID::toString).collect(Collectors.toList()))\n                .values(vars);\n\n        boolean single = payload.jobs().size() == 1;\n        if (single) {\n            // for single job also put all variables at the top level of the result\n            String id = payload.jobs().get(0).toString();\n            Map<String, Object> out = results.get(0).out;\n            result.value(\"id\", id)\n                    .values(out);\n        }\n\n        return result;\n    }\n\n    private Result continueAfterSuspend(String baseUrl, String apiKey, UUID processId, boolean collectOutVars) throws Exception {\n        ProcessEntry e = ClientUtils.withRetry(3, 1000,\n                () -> withClient(baseUrl, apiKey, client -> {\n                    ProcessV2Api api = new ProcessV2Api(client);\n                    return api.getProcess(processId, Collections.emptySet());\n                }));\n\n        ProcessEntry.StatusEnum s = e.getStatus();\n        if (!isFinalStatus(s)) {\n            throw new IllegalStateException(\"Process '\" + processId + \"' not finished\");\n        }\n\n        if (collectOutVars) {\n            return new Result(e, getOutVars(baseUrl, apiKey, processId));\n        }\n\n        return new Result(e, Collections.emptyMap());\n    }\n\n    private static class Result {\n\n        private final ProcessEntry processEntry;\n        private final Map<String, Object> out;\n\n        private Result(ProcessEntry processEntry, Map<String, Object> out) {\n            this.processEntry = processEntry;\n            this.out = out;\n        }\n    }\n\n    private TaskResult suspend(ResumePayload payload, boolean resumeFromSameStep) throws ApiException {\n        if (payload.jobs().isEmpty()) {\n            throw new RuntimeException(\"Jobs is empty\");\n        }\n\n        String eventName = UUID.randomUUID().toString();\n\n        Map<String, Object> condition = new HashMap<>();\n        condition.put(\"type\", \"PROCESS_COMPLETION\");\n        condition.put(\"reason\", \"Waiting for a child process to end\");\n        condition.put(\"processes\", payload.jobs());\n        condition.put(\"resumeEvent\", eventName);\n\n        ClientUtils.withRetry(3, 1000, () -> withClient(client -> {\n            ProcessApi api = new ProcessApi(client);\n            api.setWaitCondition(currentProcessId, condition);\n            return null;\n        }));\n\n        if (resumeFromSameStep) {\n            return TaskResult.reentrantSuspend(eventName, payload.asMap());\n        } else {\n            return TaskResult.suspend(eventName);\n        }\n    }\n\n    private static void handleResults(Map<String, ProcessEntry> m, boolean ignoreFailures) {\n        StringBuilder errors = new StringBuilder();\n        boolean hasErrors = false;\n        for (Map.Entry<String, ProcessEntry> e : m.entrySet()) {\n            String id = e.getKey();\n            String status = e.getValue().getStatus().getValue();\n            if (FAILED_STATUSES.contains(status)) {\n                Map<String, Object> error = getError(e.getValue());\n                String errorMessage = \"\";\n                if (!error.isEmpty()) {\n                    errorMessage = \"(error: \" + error + \")\";\n                }\n\n                if (ignoreFailures) {\n                    log.warn(\"Child process {} {} {}, ignoring...\", id, status, errorMessage);\n                    continue;\n                }\n\n                errors.append(\"Child process \").append(id).append(\" \").append(status).append(\" \").append(errorMessage);\n                errors.append(\"\\n\");\n                hasErrors = true;\n            }\n        }\n\n        if (hasErrors) {\n            throw new UserDefinedException(errors.toString());\n        }\n    }\n\n    private static Map<String, Object> getError(ProcessEntry p) {\n        Map<String, Object> meta = p.getMeta();\n        if (meta == null) {\n            return Collections.emptyMap();\n        }\n\n        Map<String, Object> out = MapUtils.getMap(meta, \"out\", Collections.emptyMap());\n        return MapUtils.getMap(out, Constants.Context.LAST_ERROR_KEY, Collections.emptyMap());\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private Map<String, Object> getOutVars(String baseUrl, String apiKey, UUID processId) throws Exception {\n        return withClient(baseUrl, apiKey, client -> {\n            ProcessApi api = new ProcessApi(client);\n\n            try (InputStream is = api.downloadAttachment(processId, \"out.json\")) {\n                ObjectMapper om = new ObjectMapper();\n                return om.readValue(is, Map.class);\n            } catch (ApiException e) {\n                if (e.getCode() == 404) {\n                    return Collections.emptyMap();\n                }\n                log.error(\"Error while reading the out variables\", e);\n                throw e;\n            }\n        });\n    }\n\n    private TaskResult fork(ForkParams in) throws Exception {\n        List<Future<UUID>> futures = new ArrayList<>();\n        for (ForkStartParams fork : in.forks()) {\n            int n = fork.getInstances();\n            for (int i = 0; i < n; i++) {\n                futures.add(forkOne(fork));\n            }\n        }\n\n        // collect all futures, effectively blocking until all forks are started\n        List<UUID> ids = new ArrayList<>();\n        for (Future<UUID> f : futures) {\n            ids.add(f.get());\n        }\n\n        boolean sync = in.sync();\n        if (sync) {\n            boolean suspend = in.suspend();\n            if (suspend) {\n                log.info(\"Suspending the process until the fork processes ({}) are completed...\",\n                        ids.stream().map(LogTags::instanceId).collect(Collectors.toList()));\n\n                ResumePayload resume = new ResumePayload(\n                        null, null, !in.outVars().isEmpty(), ids, in.ignoreFailures());\n\n                return suspend(resume, true);\n            }\n\n            Map<String, ProcessEntry> result = waitForCompletion(ids, -1, Function.identity());\n            handleResults(result, in.ignoreFailures());\n        }\n\n        TaskResult.SimpleResult result = TaskResult.success()\n                .value(\"ids\", ids.stream().map(UUID::toString).collect(Collectors.toList()));\n\n        boolean single = ids.size() == 1;\n        if (single) {\n            result.value(\"id\", ids.get(0).toString());\n        }\n        return result;\n    }\n\n    private Future<UUID> forkOne(ForkStartParams in) {\n        if (in.payload() != null) {\n            log.warn(\"'\" + StartParams.PAYLOAD_KEY + \"' parameter is not supported for fork action and will be ignored\");\n        }\n\n        Map<String, Object> req = createRequest(in);\n\n        if (in.debug(globalDebug)) {\n            log.info(\"Forking the current instance (sync={}, req={})...\", in.sync(), req);\n        }\n\n        return executor.submit(() -> withClient(in.apiKey(), client -> {\n            ProcessApi api = new ProcessApi(client);\n            StartProcessResponse resp = api.fork(currentProcessId, false, null, req);\n            log.info(\"Forked a child process: {}\", LogTags.instanceId(resp.getInstanceId()));\n            return resp.getInstanceId();\n        }));\n    }\n\n    private static void addIfNotNull(Map<String, Object> m, String k, Object v) {\n        if (v == null) {\n            return;\n        }\n        m.put(k, v);\n    }\n\n    /**\n     * Processes a list of attachment parameters. An attachment may be a string which represents\n     * the path to a file or a Map which specifies the destination filename (key) and current filename (value)\n     *\n     * @param attachments List of attachments\n     * @return Map of attachments. Key is the destination filename. Value is path to the local file.\n     */\n    @SuppressWarnings(\"rawtypes\")\n    private static Map<String, Path> processAttachments(Collection<Object> attachments) {\n        if (attachments.isEmpty()) {\n            return Collections.emptyMap();\n        }\n\n        Map<String, Path> result = new HashMap<>(attachments.size());\n        for (Object o : attachments) {\n            if (o instanceof String) {\n                String key = ((String) o).replaceFirst(\"^.*/\", \"\");\n\n                result.put(key, Paths.get((String) o));\n            } else if (o instanceof Map) {\n                Map m = (Map) o;\n                result.put((String) m.get(\"dest\"), Paths.get((String) m.get(\"src\")));\n            } else {\n                throw new IllegalArgumentException(\"Unsupported attachment formatting provided. Must be a string or map. Received \" + o.getClass());\n            }\n        }\n\n        return result;\n    }\n\n    private static Path archivePayload(Path workDir, StartParams in) throws IOException {\n        String s = in.payload();\n        if (s == null) {\n            return null;\n        }\n\n        Path path = workDir.resolve(s);\n        if (!Files.exists(path)) {\n            throw new IllegalArgumentException(\"File or directory not found: \" + path);\n        }\n\n        if (Files.isDirectory(path)) {\n            Path tmp = PathUtils.createTempFile(\"payload\", \".zip\");\n            try (ZipArchiveOutputStream out = new ZipArchiveOutputStream(Files.newOutputStream(tmp))) {\n                ZipUtils.zip(out, path);\n            }\n            return tmp;\n        }\n\n        return path;\n    }\n\n    private Map<String, Object> createRequest(StartParams in) {\n        Map<String, Object> req = new HashMap<>();\n\n        Set<String> activeProfiles = in.activeProfiles();\n        if (activeProfiles != null) {\n            req.put(Constants.Request.ACTIVE_PROFILES_KEY, activeProfiles);\n        }\n\n        String entryPoint = in.entryPoint();\n        if (entryPoint != null) {\n            req.put(Constants.Request.ENTRY_POINT_KEY, entryPoint);\n        }\n\n        Map<String, Object> exclusive = in.exclusive();\n        if (!exclusive.isEmpty()) {\n            req.put(Constants.Request.EXCLUSIVE, exclusive);\n        }\n\n        Set<String> tags = in.tags();\n        if (tags != null) {\n            req.put(Constants.Request.TAGS_KEY, tags);\n        }\n\n        Map<String, Object> args = in.arguments();\n        if (!args.isEmpty()) {\n            req.put(Constants.Request.ARGUMENTS_KEY, new HashMap<>(args));\n        }\n\n        boolean disableOnCancel = in.disableOnCancel();\n        if (disableOnCancel) {\n            req.put(Constants.Request.DISABLE_ON_CANCEL_KEY, true);\n        }\n\n        boolean disableOnFailure = in.disableOnFailure();\n        if (disableOnFailure) {\n            req.put(Constants.Request.DISABLE_ON_FAILURE_KEY, true);\n        }\n\n        Collection<String> outVars = in.outVars();\n        if (!outVars.isEmpty()) {\n            req.put(Constants.Request.OUT_EXPRESSIONS_KEY, outVars);\n        }\n\n        Object meta = in.meta();\n        if (meta != null) {\n            req.put(Constants.Request.META, meta);\n        }\n\n        Map<String, Object> requirements = in.requirements();\n        if (!requirements.isEmpty()) {\n            req.put(Constants.Request.REQUIREMENTS, new HashMap<>(requirements));\n        }\n\n        if (in.dryRunMode(dryRun)) {\n            req.put(Constants.Request.DRY_RUN_MODE_KEY, true);\n        }\n\n        return req;\n    }\n\n    private String getOrg(StartParams in) {\n        String org = in.org();\n        if (org != null) {\n            return org;\n        }\n\n        return currentOrgName;\n    }\n\n    private static boolean isFinalStatus(ProcessEntry.StatusEnum s) {\n        return s == ProcessEntry.StatusEnum.FAILED\n                || s == ProcessEntry.StatusEnum.FINISHED\n                || s == ProcessEntry.StatusEnum.CANCELLED\n                || s == ProcessEntry.StatusEnum.TIMED_OUT;\n    }\n\n    private <T> T withClient(CheckedFunction<ApiClient, T> f) throws Exception {\n        return withClient(ApiClientConfiguration.builder().build(), f);\n    }\n\n    private <T> T withClient(String apiKey, CheckedFunction<ApiClient, T> f) throws Exception {\n        return withClient(ApiClientConfiguration.builder()\n                .apiKey(apiKey)\n                .build(), f);\n    }\n\n    private <T> T withClient(String baseUrl, String apiKey, CheckedFunction<ApiClient, T> f) throws Exception {\n        return withClient(ApiClientConfiguration.builder()\n                .baseUrl(baseUrl)\n                .apiKey(apiKey)\n                .build(), f);\n    }\n\n    private <T> T withClient(ApiClientConfiguration cfg, CheckedFunction<ApiClient, T> f) throws Exception {\n        return f.apply(apiClientFactory.create(ApiClientConfiguration.builder().from(cfg).sessionToken(sessionToken).build()));\n    }\n\n    @FunctionalInterface\n    interface CheckedFunction<T, R> {\n        R apply(T t) throws Exception;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/ConcordTaskParams.java",
    "content": "package com.walmartlabs.concord.client;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.CreateApiKeyRequest;\nimport com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport javax.xml.bind.DatatypeConverter;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic class ConcordTaskParams {\n\n    public static ConcordTaskParams of(Variables input, Map<String, Object> defaults) {\n        Map<String, Object> variablesMap = new HashMap<>(defaults != null ? defaults : Collections.emptyMap());\n        variablesMap.putAll(input.toMap());\n\n        Variables variables = new MapBackedVariables(variablesMap);\n\n        ConcordTaskParams p = new ConcordTaskParams(variables);\n        return switch (p.action()) {\n            case START -> new StartParams(variables);\n            case STARTEXTERNAL -> new StartExternalParams(variables);\n            case FORK -> new ForkParams(variables);\n            case KILL -> new KillParams(variables);\n            case CREATEAPIKEY, CREATEORUPDATEAPIKEY -> new CreateOrUpdateApiKeyParams(variables);\n        };\n    }\n\n    protected final Variables variables;\n\n    ConcordTaskParams(Variables variables) {\n        this.variables = variables;\n    }\n\n    public Action action() {\n        String action = variables.assertString(Keys.ACTION_KEY);\n        try {\n            return Action.valueOf(action.trim().toUpperCase());\n        } catch (IllegalArgumentException e) {\n            throw new RuntimeException(\"Unknown action: '\" + action + \"'. Available actions: \" + Arrays.toString(Action.values()));\n        }\n    }\n\n    public static class ListSubProcesses {\n\n        public static ListSubProcesses of(UUID instanceId, String... tags) {\n            Map<String, Object> vars = new HashMap<>();\n            vars.put(INSTANCE_ID_KEY, instanceId);\n            if (tags != null) {\n                vars.put(TAGS_KEY, Arrays.asList(tags));\n            }\n            return new ListSubProcesses(new MapBackedVariables(vars));\n        }\n\n        private static final String INSTANCE_ID_KEY = \"instanceId\";\n        private static final String TAGS_KEY = \"tags\";\n\n        private final Variables variables;\n\n        public ListSubProcesses(Variables variables) {\n            this.variables = variables;\n        }\n\n        public UUID instanceId() {\n            return variables.assertUUID(INSTANCE_ID_KEY);\n        }\n\n        public Set<String> tags() {\n            return getSet(variables, TAGS_KEY);\n        }\n    }\n\n    public static class StartParams extends ConcordTaskParams {\n\n        private static final String ACTIVE_PROFILES_KEY = \"activeProfiles\";\n        protected static final String API_KEY = \"apiKey\";\n        private static final String ARGUMENTS_KEY = \"arguments\";\n        private static final String ATTACHMENTS_KEY = \"attachments\";\n        private static final String BASE_URL_KEY = \"baseUrl\";\n        private static final String DEBUG_KEY = \"debug\";\n        private static final String DISABLE_ON_CANCEL_KEY = \"disableOnCancel\";\n        private static final String DISABLE_ON_FAILURE_KEY = \"disableOnFailure\";\n        private static final String DRY_RUN_MODE_KEY = \"dryRunMode\";\n        private static final String EXCLUSIVE_KEY = \"exclusive\";\n        private static final String IGNORE_FAILURES_KEY = \"ignoreFailures\";\n        private static final String META_KEY = \"meta\";\n        private static final String ORG_KEY = \"org\";\n        private static final String OUT_VARS_KEY = \"outVars\";\n        private static final String REPO_BRANCH_OR_TAG_KEY = \"repoBranchOrTag\";\n        private static final String REPO_COMMIT_ID_KEY = \"repoCommitId\";\n        private static final String REQUIREMENTS_KEY = \"requirements\";\n        private static final String START_AT_KEY = \"startAt\";\n        public static final String SUSPEND_KEY = \"suspend\";\n        private static final String SYNC_KEY = \"sync\";\n        private static final String TAGS_KEY = \"tags\";\n        protected static final String ENTRY_POINT_KEY = \"entryPoint\";\n        public static final String PAYLOAD_KEY = \"payload\";\n        public static final String PROJECT_KEY = \"project\";\n        public static final String REPO_KEY = \"repo\";\n\n        StartParams(Variables variables) {\n            super(variables);\n        }\n\n        public String payload() {\n            return variables.getString(PAYLOAD_KEY);\n        }\n\n        public Set<String> activeProfiles() {\n            return getSet(variables, ACTIVE_PROFILES_KEY);\n        }\n\n        public String entryPoint() {\n            return variables.getString(ENTRY_POINT_KEY);\n        }\n\n        public Map<String, Object> exclusive() {\n            return variables.getMap(EXCLUSIVE_KEY, Collections.emptyMap());\n        }\n\n        public Set<String> tags() {\n            return getSet(variables, TAGS_KEY);\n        }\n\n        public Map<String, Object> arguments() {\n            return variables.getMap(ARGUMENTS_KEY, Collections.emptyMap());\n        }\n\n        public boolean disableOnCancel() {\n            return variables.getBoolean(DISABLE_ON_CANCEL_KEY, false);\n        }\n\n        public boolean disableOnFailure() {\n            return variables.getBoolean(DISABLE_ON_FAILURE_KEY, false);\n        }\n\n        public Collection<String> outVars() {\n            return variables.getCollection(OUT_VARS_KEY, Collections.emptyList());\n        }\n\n        public Object meta() {\n            return variables.get(META_KEY);\n        }\n\n        public Map<String, Object> requirements() {\n            return variables.getMap(REQUIREMENTS_KEY, Collections.emptyMap());\n        }\n\n        public String repo() {\n            return variables.getString(REPO_KEY);\n        }\n\n        public Collection<Object> attachments() {\n            return variables.getCollection(ATTACHMENTS_KEY, Collections.emptyList());\n        }\n\n        public String repoBranchOrTag() {\n            return variables.getString(REPO_BRANCH_OR_TAG_KEY);\n        }\n\n        public String repoCommitId() {\n            return variables.getString(REPO_COMMIT_ID_KEY);\n        }\n\n        public String startAt() {\n            Object v = variables.get(START_AT_KEY);\n            if (v == null) {\n                return null;\n            }\n\n            if (v instanceof String) {\n                return (String) v;\n            } else if (v instanceof Date) {\n                Calendar c = Calendar.getInstance();\n                c.setTime((Date) v);\n                return DatatypeConverter.printDateTime(c);\n            } else if (v instanceof Calendar) {\n                return DatatypeConverter.printDateTime((Calendar) v);\n            } else {\n                throw new IllegalArgumentException(\"'\" + START_AT_KEY + \"' must be a string, java.util.Date or java.util.Calendar value. Got: \" + v);\n            }\n        }\n\n        public boolean sync() {\n            return variables.getBoolean(SYNC_KEY, false);\n        }\n\n        public boolean debug(boolean defaultValue) {\n            return variables.getBoolean(DEBUG_KEY, defaultValue);\n        }\n\n        public boolean dryRunMode(boolean defaultValue) {\n            return variables.get(DRY_RUN_MODE_KEY, defaultValue, Boolean.class);\n        }\n\n        public String org() {\n            return variables.getString(ORG_KEY);\n        }\n\n        public boolean suspend() {\n            return variables.getBoolean(SUSPEND_KEY, false);\n        }\n\n        public boolean ignoreFailures() {\n            return variables.getBoolean(IGNORE_FAILURES_KEY, false);\n        }\n\n        public String project() {\n            return variables.getString(PROJECT_KEY);\n        }\n\n        public String baseUrl() {\n            return variables.getString(BASE_URL_KEY);\n        }\n\n        public String apiKey() {\n            return variables.getString(API_KEY);\n        }\n    }\n\n    static class StartExternalParams extends StartParams {\n\n        StartExternalParams(Variables variables) {\n            super(variables);\n        }\n\n        @Override\n        public String apiKey() {\n            return variables.assertString(\"'\" + API_KEY + \"' is required to start a process on an external Concord instance\", API_KEY);\n        }\n\n        @Override\n        public boolean suspend() {\n            return false;\n        }\n\n        public boolean suspendRaw() {\n            return super.suspend();\n        }\n    }\n\n    static class ForkParams extends ConcordTaskParams {\n\n        private static final String IGNORE_FAILURES_KEY = \"ignoreFailures\";\n        private static final String SYNC_KEY = \"sync\";\n        private static final String FORKS_KEY = \"forks\";\n        private static final String SUSPEND_KEY = \"suspend\";\n        private static final String OUT_VARS_KEY = \"outVars\";\n\n        ForkParams(Variables variables) {\n            super(variables);\n        }\n\n        public Collection<String> outVars() {\n            return variables.getCollection(OUT_VARS_KEY, Collections.emptyList());\n        }\n\n        public List<ForkStartParams> forks() {\n            List<ForkStartParams> forks;\n            Collection<Map<String, Object>> forksValue = variables.getCollection(FORKS_KEY, null);\n            if (forksValue == null) {\n                forks = Collections.singletonList(new ForkStartParams(variables));\n            } else {\n                forks = forksValue.stream()\n                        .map(f -> {\n                            // some parameters (e.g. tags) can be defined for either an \"forks\" entry or \"globally\"\n                            // for example:\n                            // - task: concord\n                            //   in:\n                            //     tags: [\"red\"]\n                            //     forks:\n                            //       - entryPoint: \"x\" // inherits tags value\n                            //       - entryPoint: \"y\"\n                            //         tags: [\"green\"] // provides its own tags\n                            //\n                            // that's why here we're using DelegateVariables which looks for keys in\n                            // the \"forks\" entry first and then falls back to the \"global\" section.\n                            Variables vars = new DelegateVariables(new MapBackedVariables(f), variables);\n                            return new ForkStartParams(vars, outVars());\n                        })\n                        .collect(Collectors.toList());\n            }\n\n            if (forks.isEmpty()) {\n                throw new IllegalArgumentException(\"'\" + FORKS_KEY + \"' can't be an empty list\");\n            }\n\n            return forks;\n        }\n\n        public boolean sync() {\n            return variables.getBoolean(SYNC_KEY, false);\n        }\n\n        public boolean ignoreFailures() {\n            return variables.getBoolean(IGNORE_FAILURES_KEY, false);\n        }\n\n        public boolean suspend() {\n            return variables.getBoolean(SUSPEND_KEY, false);\n        }\n    }\n\n    static class ForkStartParams extends StartParams {\n\n        private static final String INSTANCES_KEY = \"instances\";\n        private final Collection<String> outVars;\n\n        ForkStartParams(Variables variables) {\n            this(variables, Collections.emptyList());\n        }\n\n        ForkStartParams(Variables variables, Collection<String> outVars) {\n            super(variables);\n            this.outVars = outVars;\n        }\n\n        @Override\n        public Action action() {\n            return Action.FORK;\n        }\n\n        @Override\n        public String entryPoint() {\n            String entryPoint = super.entryPoint();\n            if (entryPoint != null) {\n                return entryPoint;\n            }\n\n            throw new IllegalArgumentException(\"'\" + ENTRY_POINT_KEY + \"' is required\");\n        }\n\n        @Override\n        public Collection<String> outVars() {\n            Collection<String> outVars = super.outVars();\n            if (!outVars.isEmpty()) {\n                return outVars;\n            }\n            return this.outVars;\n        }\n\n        public int getInstances() {\n            Object v = variables.get(INSTANCES_KEY);\n            if (v == null) {\n                return 1;\n            }\n\n            int i;\n            if (v instanceof Integer) {\n                i = (Integer) v;\n            } else if (v instanceof Long) {\n                i = ((Long) v).intValue();\n            } else {\n                throw new IllegalArgumentException(\"'\" + INSTANCES_KEY + \"' must be a number\");\n            }\n\n            if (i <= 0) {\n                throw new IllegalArgumentException(\"'\" + INSTANCES_KEY + \"' must be a positive number\");\n            }\n\n            return i;\n        }\n    }\n\n    public static class KillParams extends ConcordTaskParams {\n\n        private static final String INSTANCE_ID_KEY = \"instanceId\";\n        private static final String SYNC_KEY = \"sync\";\n\n        public KillParams(Variables variables) {\n            super(variables);\n        }\n\n        @Override\n        public Action action() {\n            return Action.KILL;\n        }\n\n        @SuppressWarnings(\"rawtypes\")\n        public List<UUID> ids() {\n            List<UUID> ids = new ArrayList<>();\n\n            Object v = variables.get(INSTANCE_ID_KEY);\n            if (v instanceof String || v instanceof UUID) {\n                ids.add(toUUID(v));\n            } else if (v instanceof String[] || v instanceof UUID[]) {\n                Object[] os = (Object[]) v;\n                for (Object o : os) {\n                    ids.add(toUUID(o));\n                }\n            } else if (v instanceof Collection) {\n                for (Object o : (Collection) v) {\n                    if (o instanceof String || o instanceof UUID) {\n                        ids.add(toUUID(o));\n                    } else {\n                        throw new IllegalArgumentException(\"'\" + INSTANCE_ID_KEY + \"' value should be a string or an UUID: \" + o);\n                    }\n                }\n            } else {\n                throw new IllegalArgumentException(\"'\" + INSTANCE_ID_KEY + \"' should be a single string, an UUID value or an array of strings or UUIDs: \" + v);\n            }\n\n            return ids;\n        }\n\n        public boolean sync() {\n            return variables.getBoolean(SYNC_KEY, false);\n        }\n    }\n\n    public static class CreateOrUpdateApiKeyParams extends ConcordTaskParams {\n\n        private static final String BASE_URL_KEY = \"baseUrl\";\n        private static final String API_KEY = \"apiKey\";\n        private static final String USER_ID_KEY = \"userId\";\n        private static final String USERNAME_KEY = \"username\";\n        private static final String USER_DOMAIN_KEY = \"userDomain\";\n        private static final String USER_TYPE_KEY = \"userType\";\n        private static final String NAME_KEY = \"name\";\n        private static final String IGNORE_EXISTING_KEY = \"ignoreExisting\";\n        private static final String KEY = \"key\";\n\n        CreateOrUpdateApiKeyParams(Variables variables) {\n            super(variables);\n        }\n\n        public String baseUrl() {\n            return variables.getString(BASE_URL_KEY);\n        }\n\n        public String apiKey() {\n            return variables.getString(API_KEY);\n        }\n\n        public UUID userId() {\n            String value = variables.getString(USER_ID_KEY);\n            if (value == null) {\n                return null;\n            }\n            try {\n                return UUID.fromString(value);\n            } catch (IllegalArgumentException e) {\n                throw new IllegalArgumentException(\"Expected a user ID, got: \" + value);\n            }\n        }\n\n        public String username() {\n            return variables.getString(USERNAME_KEY);\n        }\n\n        public String userDomain() {\n            return variables.getString(USER_DOMAIN_KEY);\n        }\n\n        public CreateApiKeyRequest.UserTypeEnum userType() {\n            String value = variables.getString(USER_TYPE_KEY);\n            if (value == null) {\n                return null;\n            }\n            try {\n                return CreateApiKeyRequest.UserTypeEnum.valueOf(value.toUpperCase());\n            } catch (IllegalArgumentException e) {\n                String validTypes = Arrays.stream(CreateApiKeyRequest.UserTypeEnum.values())\n                        .map(CreateApiKeyRequest.UserTypeEnum::toString).collect(Collectors.joining(\", \"));\n                throw new IllegalArgumentException(\"Invalid user type. Expected one of \" + validTypes + \", got: \" + value);\n            }\n        }\n\n        public String name() {\n            return variables.getString(NAME_KEY);\n        }\n\n        public boolean ignoreExisting() {\n            return variables.getBoolean(IGNORE_EXISTING_KEY, false);\n        }\n\n        public String key() {\n            return variables.getString(KEY);\n        }\n    }\n\n    private static UUID toUUID(Object v) {\n        if (v instanceof String) {\n            return UUID.fromString(v.toString());\n        } else if (v instanceof UUID) {\n            return (UUID) v;\n        }\n        throw new IllegalArgumentException(\"Invalid value type: expected UUID or String, got: \" + v.getClass());\n    }\n\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    private static Set<String> getSet(Variables variables, String key) {\n        Object v = variables.get(key);\n        if (v == null) {\n            return null;\n        }\n\n        if (v instanceof String) {\n            return Arrays.stream(((String) v)\n                    .split(\",\"))\n                    .map(String::trim)\n                    .collect(Collectors.toSet());\n        } else if (v instanceof String[]) {\n            return new HashSet<>(Arrays.asList((String[]) v));\n        } else if (v instanceof Collection) {\n            return new HashSet<>((Collection) v);\n        } else {\n            throw new IllegalArgumentException(\"'\" + key + \"' must a single string value or an array of strings: \" + v);\n        }\n    }\n\n    public enum Action {\n\n        START,\n        STARTEXTERNAL,\n        FORK,\n        KILL,\n        CREATEAPIKEY,\n        CREATEORUPDATEAPIKEY,\n    }\n\n    private static class DelegateVariables implements Variables {\n\n        private final Variables[] delegates;\n\n        public DelegateVariables(Variables... delegates) {\n            this.delegates = delegates;\n        }\n\n        @Override\n        public Object get(String key) {\n            for (Variables delegate : delegates) {\n                if (delegate.has(key)) {\n                    return delegate.get(key);\n                }\n            }\n            return null;\n        }\n\n        @Override\n        public void set(String key, Object value) {\n            throw new IllegalStateException(\"Not supported\");\n        }\n\n        @Override\n        public boolean has(String key) {\n            for (Variables delegate : delegates) {\n                if (delegate.has(key)) {\n                    return true;\n                }\n            }\n            return false;\n        }\n\n        @Override\n        public Map<String, Object> toMap() {\n            throw new IllegalStateException(\"Not supported\");\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/InventoryTask.java",
    "content": "package com.walmartlabs.concord.client;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.InjectVariable;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Named;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n@Named(\"inventory\")\npublic class InventoryTask extends AbstractConcordTask {\n\n    private static final Logger log = LoggerFactory.getLogger(InventoryTask.class);\n\n    private static final int RETRY_COUNT = 3;\n    private static final long RETRY_INTERVAL = 5000;\n\n    public Map<String, Object> ansible(@InjectVariable(\"context\") Context ctx,\n                                       String inventoryName,\n                                       String hostGroupName, String queryName, Map<String, Object> params) throws Exception {\n\n        String orgName = getOrg(ctx);\n        return ansible(ctx, orgName, inventoryName, hostGroupName, queryName, params, null);\n    }\n\n    public Map<String, Object> ansible(@InjectVariable(\"context\") Context ctx,\n                                       String orgName, String inventoryName,\n                                       String hostGroupName, String queryName, Map<String, Object> params) throws Exception {\n\n        return ansible(ctx, orgName, inventoryName, hostGroupName, queryName, params, null);\n    }\n\n    public Map<String, Object> ansible(@InjectVariable(\"context\") Context ctx,\n                                       String orgName, String inventoryName,\n                                       String hostGroupName, String queryName) throws Exception {\n\n        return ansible(ctx, orgName, inventoryName, hostGroupName, queryName, null, null);\n    }\n\n    public Map<String, Object> ansible(@InjectVariable(\"context\") Context ctx,\n                                       String inventoryName,\n                                       String hostGroupName, String queryName) throws Exception {\n\n        String orgName = getOrg(ctx);\n        return ansible(ctx, orgName, inventoryName, hostGroupName, queryName, null, null);\n    }\n\n    public Map<String, Object> ansible(@InjectVariable(\"context\") Context ctx,\n                                       String orgName, String inventoryName,\n                                       String hostGroupName, String queryName, Map<String, Object> params,\n                                       Map<String, Object> vars) throws Exception {\n\n        List<Object> data = execQuery(ctx, orgName, inventoryName, queryName, params);\n\n        Map<String, Object> hostVars = toHostVars(data);\n\n        Map<String, Object> meta = new HashMap<>();\n        meta.put(\"hostvars\", hostVars);\n\n        Map<String, Object> result = new HashMap<>();\n        result.put(\"_meta\", meta);\n\n        Map<String, Object> hostGroup = new HashMap<>();\n        hostGroup.put(\"hosts\", hostVars.keySet());\n        if (vars != null && !vars.isEmpty()) {\n            hostGroup.put(\"vars\", vars);\n        }\n\n        result.put(hostGroupName, hostGroup);\n\n        log.info(\"ansible ['{}', '{}', '{}', '{}', '{}'] -> done\", orgName, inventoryName, hostGroupName, queryName, params);\n\n        return result;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Map<String, Object> toHostVars(List<Object> data) {\n        if (data == null || data.isEmpty()) {\n            return Collections.emptyMap();\n        }\n\n        Map<String, Object> result = new HashMap<>();\n\n        for (Object o : data) {\n            if (o instanceof String) {\n                result.put((String) o, Collections.emptyMap());\n            } else if (o instanceof Map) {\n                Map<String, Object> m = (Map<String, Object>) o;\n                Object h = m.get(\"host\");\n                if (h instanceof String) {\n                    m.remove(\"host\");\n                    result.put((String) h, m);\n                } else {\n                    throw new IllegalArgumentException(\"Expected a record with a \\\"host\\\" value, got \" + o);\n                }\n            } else {\n                throw new IllegalArgumentException(\"Expected a string value or a record, got \" + o);\n            }\n        }\n\n        return result;\n    }\n\n    public List<Object> query(@InjectVariable(\"context\") Context ctx,\n                              String inventoryName, String queryName, Map<String, Object> params) throws Exception {\n\n        String orgName = getOrg(ctx);\n        return query(ctx, orgName, inventoryName, queryName, params);\n    }\n\n    public List<Object> query(@InjectVariable(\"context\") Context ctx,\n                              String orgName, String inventoryName, String queryName, Map<String, Object> params) throws Exception {\n\n        List<Object> result = execQuery(ctx, orgName, inventoryName, queryName, params);\n\n        log.info(\"query ['{}', '{}', '{}', '{}'] -> {}\", orgName, inventoryName, queryName, params, result != null);\n\n        return result;\n    }\n\n    private List<Object> execQuery(Context ctx, String orgName, String inventoryName, String queryName, Map<String, Object> params) throws Exception {\n        return ClientUtils.withRetry(RETRY_COUNT, RETRY_INTERVAL, () -> withClient(ctx, client -> {\n            InventoryQueriesApi api = new InventoryQueriesApi(client);\n            return api.executeInventoryQuery(orgName, inventoryName, queryName, params);\n        }));\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static String getOrg(Context ctx) {\n        Map<String, Object> m = (Map<String, Object>) ctx.getVariable(Constants.Request.PROJECT_INFO_KEY);\n        if (m == null) {\n            return null;\n        }\n\n        return (String) m.get(\"orgName\");\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/JsonStoreTask.java",
    "content": "package com.walmartlabs.concord.client;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.ContextUtils;\nimport com.walmartlabs.concord.sdk.InjectVariable;\nimport com.walmartlabs.concord.sdk.ProjectInfo;\n\nimport javax.inject.Named;\nimport java.util.List;\nimport java.util.Map;\n\n@Named(\"jsonStore\")\npublic class JsonStoreTask extends AbstractConcordTask {\n\n    public boolean isStoreExists(@InjectVariable(\"context\") Context ctx, String storeName) throws Exception {\n        return isStoreExists(ctx, assertOrg(ctx), storeName);\n    }\n\n    public boolean isStoreExists(@InjectVariable(\"context\") Context ctx, String orgName, String storeName) throws Exception {\n        return withClient(ctx, client -> new JsonStoreTaskCommon(client).isStoreExists(orgName, storeName));\n    }\n\n    public boolean isExists(@InjectVariable(\"context\") Context ctx, String storeName, String itemPath) throws Exception {\n        return isExists(ctx, assertOrg(ctx), storeName, itemPath);\n    }\n\n    public boolean isExists(@InjectVariable(\"context\") Context ctx, String orgName, String storeName, String itemPath) throws Exception {\n        return withClient(ctx, client -> new JsonStoreTaskCommon(client).isExists(orgName, storeName, itemPath));\n    }\n\n    public void upsert(@InjectVariable(\"context\") Context ctx, String storeName, String itemPath, Object data) throws Exception {\n        upsert(ctx, assertOrg(ctx), storeName, itemPath, data);\n    }\n\n    public void upsert(@InjectVariable(\"context\") Context ctx, String orgName, String storeName, String itemPath, Object data) throws Exception {\n        withClient(ctx, client -> {\n            new JsonStoreTaskCommon(client).put(orgName, storeName, itemPath, data, true);\n            return null;\n        });\n    }\n\n    public void put(@InjectVariable(\"context\") Context ctx, String storeName, String itemPath, Object data) throws Exception {\n        put(ctx, assertOrg(ctx), storeName, itemPath, data);\n    }\n\n    public void put(@InjectVariable(\"context\") Context ctx, String orgName, String storeName, String itemPath, Object data) throws Exception {\n        withClient(ctx, client -> {\n            new JsonStoreTaskCommon(client).put(orgName, storeName, itemPath, data, false);\n            return null;\n        });\n    }\n\n    public Object get(@InjectVariable(\"context\") Context ctx, String storageName, String itemPath) throws Exception {\n        return get(ctx, assertOrg(ctx), storageName, itemPath);\n    }\n\n    public Object get(@InjectVariable(\"context\") Context ctx, String orgName, String storeName, String itemPath) throws Exception {\n        return withClient(ctx, client -> new JsonStoreTaskCommon(client)\n                .get(orgName, storeName, itemPath));\n    }\n\n    public Object delete(@InjectVariable(\"context\") Context ctx, String storeName, String itemPath) throws Exception {\n        return delete(ctx, assertOrg(ctx), storeName, itemPath);\n    }\n\n    public boolean delete(@InjectVariable(\"context\") Context ctx, String orgName, String storeName, String itemPath) throws Exception {\n        return withClient(ctx, client -> new JsonStoreTaskCommon(client)\n                .delete(orgName, storeName, itemPath));\n    }\n\n    public void upsertQuery(@InjectVariable(\"context\") Context ctx, String storeName, String queryName, String queryText) throws Exception {\n        upsertQuery(ctx, assertOrg(ctx), storeName, queryName, queryText);\n    }\n\n    public void upsertQuery(@InjectVariable(\"context\") Context ctx, String orgName, String storeName, String queryName, String queryText) throws Exception {\n        withClient(ctx, client -> {\n            new JsonStoreTaskCommon(client)\n                    .createOrUpdateQuery(orgName, storeName, queryName, queryText);\n            return null;\n        });\n    }\n\n    public List<Object> executeQuery(@InjectVariable(\"context\") Context ctx, String storeName, String queryName) throws Exception {\n        return executeQuery(ctx, storeName, queryName, (Map<String, Object>) null);\n    }\n\n    public List<Object> executeQuery(@InjectVariable(\"context\") Context ctx, String orgName, String storeName, String queryName) throws Exception {\n        return executeQuery(ctx, orgName, storeName, queryName, null);\n    }\n\n    public List<Object> executeQuery(@InjectVariable(\"context\") Context ctx, String storeName, String queryName, Map<String, Object> params) throws Exception {\n        String orgName = assertOrg(ctx);\n        return executeQuery(ctx, orgName, storeName, queryName, params);\n    }\n\n    public List<Object> executeQuery(@InjectVariable(\"context\") Context ctx, String orgName, String storeName, String queryName, Map<String, Object> params) throws Exception {\n        return withClient(ctx, client -> new JsonStoreTaskCommon(client)\n                .executeQuery(orgName, storeName, queryName, params));\n    }\n\n    private static String assertOrg(Context ctx) {\n        ProjectInfo projectInfo = ContextUtils.getProjectInfo(ctx);\n        if (projectInfo != null) {\n            return projectInfo.orgName();\n        }\n\n        throw new RuntimeException(\"Can't determine the current organization name. \" +\n                \"Please specify it explicitly or run your process in a project.\");\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/JsonStoreTaskCommon.java",
    "content": "package com.walmartlabs.concord.client;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Common code for both v1 and v2 versions of the JSON store task.\n */\npublic class JsonStoreTaskCommon {\n\n    private static final Logger log = LoggerFactory.getLogger(JsonStoreTaskCommon.class);\n\n    private static final int RETRY_COUNT = 3;\n    private static final long RETRY_INTERVAL = 5000;\n\n    private final ApiClient apiClient;\n    private final boolean dryRunMode;\n\n    public JsonStoreTaskCommon(ApiClient apiClient) {\n        this(apiClient, false);\n    }\n\n    public JsonStoreTaskCommon(ApiClient apiClient, boolean dryRunMode) {\n        this.apiClient = apiClient;\n        this.dryRunMode = dryRunMode;\n    }\n\n    /**\n     * Returns {@code true} if the specified JSON store exists.\n     */\n    public boolean isStoreExists(String orgName, String storeName) throws ApiException {\n        assertNotEmpty(\"Organization name\", orgName);\n        assertNotEmpty(\"Store name\", storeName);\n\n        JsonStoreEntry entry = ClientUtils.withRetry(RETRY_COUNT, RETRY_INTERVAL, () -> {\n            try {\n                JsonStoreApi api = new JsonStoreApi(apiClient);\n                return api.getJsonStore(orgName, storeName);\n            } catch (ApiException e) {\n                if (e.getCode() == 404) {\n                    return null;\n                }\n\n                throw e;\n            }\n        });\n\n        return entry != null;\n    }\n\n    /**\n     * Returns {@code true} if the specified item and JSON store exists.\n     * <p/>\n     * The difference between this method and {@code get(orgName, storeName, itemPath) != null}\n     * is that this method doesn't throw exceptions if the specified organization or the store\n     * don't exist.\n     */\n    public boolean isExists(String orgName, String storeName, String itemPath) throws ApiException {\n        try {\n            return get(orgName, storeName, itemPath) != null;\n        } catch (ApiException e) {\n            if (e.getCode() == 404) {\n                return false;\n            }\n\n            throw e;\n        }\n    }\n\n    /**\n     * Creates a new JSON store or updates an existing one.\n     */\n    public void createOrUpdateStore(String orgName, JsonStoreRequest request) throws ApiException {\n        if (dryRunMode) {\n            log.info(\"Dry-run mode enabled: Skipping creation or update of store '{}'\", request.getName());\n            return;\n        }\n\n        ClientUtils.withRetry(RETRY_COUNT, RETRY_INTERVAL, () -> {\n            JsonStoreApi api = new JsonStoreApi(apiClient);\n            GenericOperationResult result = api.createOrUpdateJsonStore(orgName, request);\n            log.info(\"The store '{}' has been successfully {}\", request.getName(), result.getResult());\n            return null;\n        });\n    }\n\n    /**\n     * Creates a new JSON store query or update an existing one.\n     */\n    public void createOrUpdateQuery(String orgName, String storeName, String queryName, String queryText) throws ApiException {\n        if (dryRunMode) {\n            log.info(\"Dry-run mode enabled: Skipping creation or update of query '{}' in store '{}'\", queryName, storeName);\n            return;\n        }\n\n        ClientUtils.withRetry(RETRY_COUNT, RETRY_INTERVAL, () -> {\n            JsonStoreQueryApi api = new JsonStoreQueryApi(apiClient);\n            JsonStoreQueryRequest request = new JsonStoreQueryRequest()\n                    .name(queryName)\n                    .text(queryText);\n            GenericOperationResult result = api.createOrUpdateJsonStoreQuery(orgName, storeName, request);\n            log.info(\"The query '{}' in store '{}' has been successfully {}\", queryName, storeName, result.getResult());\n            return null;\n        });\n    }\n\n    /**\n     * Inserts a new item or replaces an existing one.\n     * <p/>\n     * If {@code createStore} is {@code true} the store will be automatically created.\n     */\n    public void put(String orgName, String storeName, String itemPath, Object data, boolean createStore) throws ApiException {\n        if (data == null) {\n            throw new IllegalArgumentException(\"Data cannot be null.\");\n        }\n\n        if (!(data instanceof Map)) {\n            throw new IllegalArgumentException(\"Data must be a valid JSON object, represented by a Java Map instance. Got: \" + data.getClass());\n        }\n\n        assertNotEmpty(\"Organization name\", orgName);\n        assertNotEmpty(\"Store name\", storeName);\n        assertNotEmpty(\"Item path\", itemPath);\n\n        if (createStore && !isStoreExists(orgName, storeName)) {\n            createOrUpdateStore(orgName, new JsonStoreRequest().name(storeName));\n        }\n\n        if (dryRunMode) {\n            log.info(\"Dry-run mode enabled: Skipping Updating item '{}' (org={}, store={})\", itemPath, orgName, storeName);\n            return;\n        }\n\n        log.info(\"Updating item '{}' (org={}, store={})\", itemPath, orgName, storeName);\n\n        ClientUtils.withRetry(RETRY_COUNT, RETRY_INTERVAL, () -> {\n            JsonStoreDataApi api = new JsonStoreDataApi(apiClient);\n            return api.updateJsonStoreData(orgName, storeName, itemPath, data);\n        });\n    }\n\n    /**\n     * Returns an existing item or {@code null} if the specified path doesn't exist.\n     */\n    public Object get(String orgName, String storeName, String itemPath) throws ApiException {\n        assertNotEmpty(\"Organization name\", orgName);\n        assertNotEmpty(\"Store name\", storeName);\n        assertNotEmpty(\"Item path\", itemPath);\n\n        log.info(\"Getting item '{}' (org='{}', store='{}')\", itemPath, orgName, storeName);\n\n        // we need to deserialize the response using Jackson instead of GSON to avoid\n        // differences between two libraries (e.g. deserialization of integers/decimals)\n        // hence we're using custom \"request\" method instead of the standard swagger-codegen client\n        return ClientUtils.withRetry(RETRY_COUNT, RETRY_INTERVAL, () ->\n                new JsonStoreDataApi(apiClient).getJsonStoreData(orgName, storeName, itemPath));\n    }\n\n    /**\n     * Removes an existing item. Returns {@code true} if the item existed and was successfully removed.\n     */\n    public boolean delete(String orgName, String storeName, String itemPath) throws ApiException {\n        assertNotEmpty(\"Organization name\", orgName);\n        assertNotEmpty(\"Store name\", storeName);\n        assertNotEmpty(\"Item path\", itemPath);\n\n        if (dryRunMode) {\n            log.info(\"Running in dry-run mode: Skipping removing item '{}' (org='{}', store='{}')\", itemPath, orgName, storeName);\n            return true;\n        }\n\n        log.info(\"Removing item '{}' (org='{}', store='{}')\", itemPath, orgName, storeName);\n\n        return ClientUtils.withRetry(RETRY_COUNT, RETRY_INTERVAL, () -> {\n            JsonStoreDataApi api = new JsonStoreDataApi(apiClient);\n            GenericOperationResult result = api.deleteJsonStoreDataItem(orgName, storeName, itemPath);\n            return result != null && result.getResult() == GenericOperationResult.ResultEnum.DELETED;\n        });\n    }\n\n    /**\n     * Executes a named query and returns the list of results.\n     */\n    public List<Object> executeQuery(String orgName, String storeName, String queryName, Map<String, Object> params) throws ApiException {\n        assertNotEmpty(\"Organization name\", orgName);\n        assertNotEmpty(\"Store name\", storeName);\n        assertNotEmpty(\"Query name\", queryName);\n\n        log.info(\"Executing query '{}' (org='{}', store='{}') with parameters '{}'\", queryName, orgName, storeName, params);\n\n        return ClientUtils.withRetry(RETRY_COUNT, RETRY_INTERVAL, () -> {\n            JsonStoreQueryApi api = new JsonStoreQueryApi(apiClient);\n            return api.execJsonStoreQuery(orgName, storeName, queryName, params);\n        });\n    }\n\n    private static void assertNotEmpty(String what, String s) {\n        if (s == null || s.isEmpty()) {\n            throw new IllegalArgumentException(what + \" cannot be empty or null\");\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/Keys.java",
    "content": "package com.walmartlabs.concord.client;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic final class Keys {\n\n    public static final String ACTION_KEY = \"action\";\n    public static final String BASE_URL_KEY = \"baseUrl\";\n    public static final String SESSION_TOKEN_KEY = \"sessionToken\";\n\n    private Keys() {\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/ProjectTask.java",
    "content": "package com.walmartlabs.concord.client;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client.v1.ContextBackedVariables;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.ContextUtils;\n\nimport javax.inject.Named;\nimport java.util.Collections;\nimport java.util.Map;\n\n@Named(\"project\")\npublic class ProjectTask extends AbstractConcordTask {\n\n    @Override\n    public void execute(Context ctx) throws Exception {\n        ProjectTaskParams in = new ProjectTaskParams(new ContextBackedVariables(ctx));\n\n        withClient(ctx, client -> {\n            new ProjectTaskCommon(client, getProcessOrgName(ctx)).execute(in);\n            return null;\n        });\n    }\n\n    private static String getProcessOrgName(Context ctx) {\n        Map<String, Object> projectInfo = ContextUtils.getMap(ctx, Constants.Request.PROJECT_INFO_KEY, Collections.emptyMap());\n        return (String) projectInfo.get(\"orgName\");\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/ProjectTaskCommon.java",
    "content": "package com.walmartlabs.concord.client;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport static com.walmartlabs.concord.client.ProjectTaskParams.Action;\n\npublic class ProjectTaskCommon {\n\n    private static final Logger log = LoggerFactory.getLogger(ProjectTaskCommon.class);\n\n    private final ProjectsApi api;\n    private final String defaultOrg;\n\n    public ProjectTaskCommon(ApiClient apiClient, String defaultOrg) {\n        this.api = new ProjectsApi(apiClient);\n        this.defaultOrg = defaultOrg;\n    }\n\n    public void execute(ProjectTaskParams in) throws Exception {\n        Action action = in.action();\n        switch (action) {\n            case CREATE: {\n                create(in);\n                break;\n            }\n            default:\n                throw new IllegalArgumentException(\"Unsupported action type: \" + action);\n        }\n    }\n\n    private void create(ProjectTaskParams in) throws Exception {\n        ProjectEntry entry = new ProjectEntry();\n        entry.setName(in.projectName());\n        entry.setRepositories(in.repositories());\n\n        ProjectOperationResponse resp = api.createOrUpdateProject(in.orgName(defaultOrg), entry);\n        log.info(\"The project was created (or updated): {}\", resp);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/ProjectTaskParams.java",
    "content": "package com.walmartlabs.concord.client;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.client2.RepositoryEntry;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\nimport com.walmartlabs.concord.sdk.MapUtils;\n\nimport java.util.*;\n\npublic class ProjectTaskParams {\n\n    private static final String ORG_KEY = \"org\";\n    private static final String NAME_KEY = \"name\";\n    private static final String REPOSITORIES_KEY = \"repositories\";\n\n    private final ObjectMapper mapper;\n    private final Variables variables;\n\n    public ProjectTaskParams(Variables variables) {\n        this.mapper = new ObjectMapper();\n        this.variables = variables;\n    }\n\n    public Action action() {\n        String v = variables.assertString(Keys.ACTION_KEY);\n        try {\n            return Action.valueOf(v.trim().toUpperCase());\n        } catch (IllegalArgumentException e) {\n            throw new RuntimeException(\"Unknown action: '\" + v + \"'. Available actions: \" + Arrays.toString(Action.values()));\n        }\n    }\n\n    public String orgName(String defaultOrg) {\n        String org = variables.getString(ORG_KEY);\n        if (org != null) {\n            return org;\n        }\n\n        if (defaultOrg != null) {\n            return defaultOrg;\n        }\n\n        throw new IllegalArgumentException(\"'\" + ORG_KEY + \"' is required\");\n    }\n\n    public String projectName() {\n        return variables.getString(NAME_KEY, null);\n    }\n\n    public Map<String, RepositoryEntry> repositories() {\n        Collection<Object> o = variables.getCollection(REPOSITORIES_KEY, Collections.emptyList());\n        return toRepositories(o);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private Map<String, RepositoryEntry> toRepositories(Collection<Object> items) {\n        if (items.isEmpty()) {\n            return Collections.emptyMap();\n        }\n\n        Map<String, RepositoryEntry> result = new HashMap<>();\n        for (Object i : items) {\n            if (!(i instanceof Map)) {\n                throw new IllegalArgumentException(\"Repository entry must be an object. Got: \" + i);\n            }\n\n            Map<String, Object> r = (Map<String, Object>) i;\n\n            String name = MapUtils.getString(r, NAME_KEY);\n            if (name == null) {\n                throw new IllegalArgumentException(\"Repository name is required\");\n            }\n\n            result.put(name, parseRepository(r));\n        }\n\n        return result;\n    }\n\n    private RepositoryEntry parseRepository(Map<String, Object> r) {\n        return mapper.convertValue(r, RepositoryEntry.class);\n    }\n\n\n    public enum Action {\n\n        CREATE\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/RepositoryRefreshTaskCommon.java",
    "content": "package com.walmartlabs.concord.client;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport com.walmartlabs.concord.client.model.EventRepository;\nimport com.walmartlabs.concord.client.model.RefreshEvent;\nimport com.walmartlabs.concord.client2.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic class RepositoryRefreshTaskCommon {\n\n    private static final Logger log = LoggerFactory.getLogger(RepositoryRefreshTaskCommon.class);\n\n    private final RepositoriesV2Api apiV2;\n    private final ObjectMapper mapper;\n\n    public RepositoryRefreshTaskCommon(ApiClient client) {\n        this.apiV2 = new RepositoriesV2Api(client);\n        this.mapper = new ObjectMapper()\n                .registerModule(new Jdk8Module()); // for Optional usage\n    }\n\n    public void execute(RepositoryRefreshTaskParams in) throws ApiException {\n        Map<String, Object> rawEvent = in.event();\n        if (!\"push\".equals(rawEvent.get(\"type\"))) {\n            // very odd, but glad we checked\n            log.warn(\"Non-push event received: {}\", rawEvent.get(\"type\"));\n            return;\n        }\n\n        RefreshEvent event = mapper.convertValue(rawEvent, RefreshEvent.class);\n\n        if (event.payload().deleted()) {\n            log.warn(\"Event ref was deleted. Skip refresh.\");\n            return;\n        }\n\n        List<UUID> repositoriesUUIDs = event.repositoryInfo().stream()\n                .filter(EventRepository::enabled)\n                // TODO validate: is always branch? event if configured for commit id? tag?\n                .filter(repo -> event.branch().equals(repo.branch().orElse(null)))\n                .map(EventRepository::repositoryId)\n                .toList();\n\n        refresh(repositoriesUUIDs);\n    }\n\n    void refresh(List<UUID> repositoriesUUIDs) throws ApiException {\n        if (repositoriesUUIDs.isEmpty()) {\n            log.warn(\"No applicable repository IDs. Skip refresh.\");\n            return;\n        }\n\n        log.info(\"Repository ids to refresh: {}\", repositoriesUUIDs);\n        apiV2.refreshRepositoryV2(repositoriesUUIDs);\n        log.info(\"Repository refresh completed\");\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/RepositoryRefreshTaskParams.java",
    "content": "package com.walmartlabs.concord.client;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\nimport com.walmartlabs.concord.sdk.MapUtils;\n\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic class RepositoryRefreshTaskParams {\n\n    private static final String REPOSITORY_INFO = \"repositoryInfo\";\n    private static final String EVENT = \"event\";\n\n    private final Variables variables;\n\n    public RepositoryRefreshTaskParams(Variables variables) {\n        this.variables = variables;\n    }\n\n    public Map<String, Object> event() {\n        return variables.getMap(EVENT, Collections.emptyMap());\n    }\n\n    public List<UUID> repositories() {\n        Collection<Object> o = variables.getCollection(REPOSITORY_INFO, Collections.emptyList());\n        return toUUIDs(o);\n    }\n\n    private List<UUID> toUUIDs(Collection<Object> items) {\n        if (items.isEmpty()) {\n            throw new IllegalArgumentException(\"Invalid in parameters: no repository info\");\n        }\n        return items.stream().map(v -> MapUtils.getUUID((Map<String, Object>)v, \"repositoryId\")).collect(Collectors.toList());\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/ResumePayload.java",
    "content": "package com.walmartlabs.concord.client;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.MapUtils;\n\nimport java.io.Serializable;\nimport java.util.*;\n\npublic class ResumePayload implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String baseUrl;\n    private final String apiKey;\n    private final boolean collectOutVars;\n    private final List<UUID> jobs;\n    private final boolean ignoreFailures;\n\n    public ResumePayload(String baseUrl, String apiKey, boolean collectOutVars, List<UUID> jobs, boolean ignoreFailures) {\n        this.baseUrl = baseUrl;\n        this.apiKey = apiKey;\n        this.collectOutVars = collectOutVars;\n        this.jobs = jobs;\n        this.ignoreFailures = ignoreFailures;\n    }\n\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    public ResumePayload(Map items) {\n        this(MapUtils.getString(items, \"baseUrl\"),\n                MapUtils.getString(items, \"apiKey\"),\n                MapUtils.getBoolean(items, \"collectOutVars\", false),\n                new ArrayList<>(MapUtils.getList(items, \"jobs\", Collections.emptyList())),\n                MapUtils.getBoolean(items, \"ignoreFailures\", false));\n    }\n\n    public Map<String, Serializable> asMap() {\n        Map<String, Serializable> result = new HashMap<>();\n        result.put(\"baseUrl\", baseUrl);\n        result.put(\"apiKey\", apiKey);\n        result.put(\"collectOutVars\", collectOutVars);\n        if (jobs != null) {\n            result.put(\"jobs\", new ArrayList<>(jobs));\n        }\n        result.put(\"ignoreFailures\", ignoreFailures);\n        return result;\n    }\n\n    public String baseUrl() {\n        return baseUrl;\n    }\n\n    public String apiKey() {\n        return apiKey;\n    }\n\n    public boolean collectOutVars() {\n        return collectOutVars;\n    }\n\n    public List<UUID> jobs() {\n        return jobs;\n    }\n\n    public boolean ignoreFailures() {\n        return ignoreFailures;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/SecretsTask.java",
    "content": "package com.walmartlabs.concord.client;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client.v1.ContextBackedVariables;\nimport com.walmartlabs.concord.client2.ApiClientConfiguration;\nimport com.walmartlabs.concord.client2.ApiClientFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.ContextUtils;\nimport com.walmartlabs.concord.sdk.Task;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.util.Collections;\nimport java.util.Map;\n\n@Named(\"concordSecrets\")\npublic class SecretsTask implements Task {\n\n    public static final String RESULT_KEY = \"result\";\n\n    private final ApiClientFactory clientFactory;\n\n    @Inject\n    public SecretsTask(ApiClientFactory clientFactory) {\n        this.clientFactory = clientFactory;\n    }\n\n    @Override\n    public void execute(Context ctx) throws Exception {\n        ApiClientConfiguration c = ApiClientConfiguration.builder()\n                .sessionToken(ContextUtils.getSessionToken(ctx))\n                .build();\n\n        SecretsTaskParams in = SecretsTaskParams.of(new ContextBackedVariables(ctx));\n        TaskResult.SimpleResult result = new SecretsTaskCommon(clientFactory.create(c), getProcessOrgName(ctx))\n                .execute(in);\n        ctx.setVariable(RESULT_KEY, result.toMap());\n    }\n\n    private static String getProcessOrgName(Context ctx) {\n        Map<String, Object> projectInfo = ContextUtils.getMap(ctx, Constants.Request.PROJECT_INFO_KEY, Collections.emptyMap());\n        return (String) projectInfo.get(\"orgName\");\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/SecretsTaskCommon.java",
    "content": "package com.walmartlabs.concord.client;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.charset.StandardCharsets;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.client.SecretsTaskParams.*;\n\npublic class SecretsTaskCommon {\n\n    private static final Logger log = LoggerFactory.getLogger(SecretsTaskCommon.class);\n\n    private static final int RETRY_COUNT = 3;\n    private static final long RETRY_INTERVAL = 5000;\n\n    private final ApiClient apiClient;\n    private final String defaultOrg;\n    private final boolean dryRunMode;\n\n    public SecretsTaskCommon(ApiClient apiClient, String defaultOrg) {\n        this(apiClient, defaultOrg, false);\n    }\n\n    public SecretsTaskCommon(ApiClient apiClient, String defaultOrg, boolean dryRunMode) {\n        this.apiClient = apiClient;\n        this.defaultOrg = defaultOrg;\n        this.dryRunMode = dryRunMode;\n    }\n\n    public TaskResult.SimpleResult execute(SecretsTaskParams in) throws Exception {\n        Action action = in.action();\n\n        TaskResult.SimpleResult result;\n        switch (action) {\n            case GETASSTRING: {\n                result = getAsString((AsStringParams)in);\n                break;\n            }\n            case CREATE: {\n                result = create((CreateParams)in);\n                break;\n            }\n            case UPDATE: {\n                result = update((UpdateParams)in);\n                break;\n            }\n            case DELETE: {\n                result = delete(in);\n                break;\n            }\n            default: {\n                throw new IllegalArgumentException(\"Unsupported action type: \" + action);\n            }\n        }\n\n        return result;\n    }\n\n    private TaskResult.SimpleResult getAsString(AsStringParams in) {\n        String orgName = in.orgName(defaultOrg);\n        String secretName = in.secretName();\n\n        // TODO allow empty multipart requests?\n        Map<String, Object> params = new HashMap<>();\n        params.put(Constants.Multipart.NAME, secretName);\n\n        addIfPresent(params, Constants.Multipart.STORE_PASSWORD, in.storePassword());\n\n        SecretsApi api = new SecretsApi(apiClient);\n        try (InputStream is = ClientUtils.withRetry(RETRY_COUNT, RETRY_INTERVAL, () -> api.getSecretData(orgName, secretName, params))) {\n            String data = new String(is.readAllBytes());\n            return Result.ok(data);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        } catch (ApiException e) {\n            return handleErrors(in, secretName, e);\n        }\n    }\n\n    private Map<String, Object> makeCreateParams(CreateParams in) {\n        Map<String, Object> m = new HashMap<>();\n\n        String orgName = in.orgName(defaultOrg);\n        m.put(Constants.Multipart.ORG_NAME, orgName);\n\n        String secretName = in.secretName();\n        m.put(Constants.Multipart.NAME, secretName);\n\n        SecretEntryV2.TypeEnum secretType = in.secretType();\n        m.put(Constants.Multipart.TYPE, secretType.toString());\n\n        switch (secretType) {\n            case DATA:\n                m.put(Constants.Multipart.DATA, in.data());\n                break;\n            case KEY_PAIR:\n                m.put(Constants.Multipart.PUBLIC, in.publicKey());\n                m.put(Constants.Multipart.PRIVATE, in.privateKey());\n                break;\n            case USERNAME_PASSWORD:\n                m.put(Constants.Multipart.USERNAME, in.userName());\n                m.put(Constants.Multipart.PASSWORD, in.password());\n                break;\n        }\n\n        addIfPresent(m, Constants.Multipart.STORE_PASSWORD, in.storePassword());\n        addIfPresent(m, Constants.Multipart.GENERATE_PASSWORD, in.generatePassword());\n        addIfPresent(m, Constants.Multipart.VISIBILITY, in.visibility());\n        addIfPresent(m, Constants.Multipart.PROJECT_NAME, in.projectName());\n\n        return m;\n    }\n\n    private TaskResult.SimpleResult create(CreateParams in) throws Exception {\n        Map<String, Object> params = makeCreateParams(in);\n        return create(in, params);\n    }\n\n    private TaskResult.SimpleResult create(CreateParams in, Map<String, Object> params) throws Exception {\n        String orgName = in.orgName(defaultOrg);\n\n        if (dryRunMode) {\n            log.info(\"Dry-run mode enabled: Skipping creation of secret '{}'\", params.get(Constants.Multipart.NAME));\n            return Result.ok();\n        }\n\n        new SecretsApi(apiClient).createSecret(orgName, params);\n\n        log.info(\"New secret was successfully created: {}\", params.get(Constants.Multipart.NAME));\n        return Result.ok();\n    }\n\n    private TaskResult.SimpleResult update(UpdateParams in) throws Exception {\n\n        String newData = null;\n        Object data = in.data();\n\n        if (data != null) {\n            if (data instanceof String) {\n                String s = data.toString();\n                newData = Base64.getEncoder().encodeToString(s.getBytes(StandardCharsets.UTF_8));\n            } else if (data instanceof byte[]) {\n                byte[] ab = (byte[]) data;\n                newData = Base64.getEncoder().encodeToString(ab);\n            } else {\n                throw new RuntimeException(\"Unsupported '\" + Constants.Multipart.DATA + \"' type: \" + data.getClass() + \". \" +\n                        \"Only string values and byte arrays are allowed.\");\n            }\n        }\n\n        String storePassword = in.storePassword();\n        String newStorePassword = in.newStorePassword();\n\n        if (newData == null && newStorePassword == null) {\n            log.warn(\"Not updating anything since nothing has changed.\");\n            return Result.ok();\n        }\n\n        String orgName = in.orgName(defaultOrg);\n        String secretName = in.secretName();\n        List<String> projectNames = in.projectNames();\n        List<UUID> projectIds = in.projectIds();\n\n        try {\n            Map<String,Object> params = new HashMap<>();\n            addIfPresent(params, Constants.Multipart.DATA, data);\n            addIfPresent(params, Constants.Multipart.STORE_PASSWORD, storePassword);\n            addIfPresent(params, Constants.Multipart.NEW_STORE_PASSWORD, newStorePassword);\n            addIfPresent(params, Constants.Multipart.PROJECT_IDS, projectIds);\n            addIfPresent(params, Constants.Multipart.PROJECT_NAMES, projectNames);\n            if(data != null){\n                addIfPresent(params, Constants.Multipart.TYPE, in.secretType());\n            }\n\n            if (dryRunMode) {\n                log.info(\"Dry-run mode enabled: Skipping updating of secret '{}'\", secretName);\n                return Result.ok();\n            }\n\n            new SecretsV2Api(apiClient).updateSecret(orgName, secretName, params);\n\n            log.info(\"The secret was successfully updated: {}\", secretName);\n            return Result.ok();\n        } catch (ApiException e) {\n            if (e.getCode() == 404) {\n                boolean createIfMissing = in.createIfMissing();\n                if (createIfMissing) {\n                    return create(in);\n                }\n            }\n\n            return handleErrors(in, secretName, e.getCode(), e.getResponseBody());\n        }\n    }\n\n    private TaskResult.SimpleResult delete(SecretsTaskParams in) {\n        String secretName = in.secretName();\n\n        if (dryRunMode) {\n            log.info(\"Dry-run mode enabled: Skipping deleting of secret '{}'\", secretName);\n            // TODO: check secret exists?\n            return Result.ok();\n        }\n\n        try {\n            SecretsApi api = new SecretsApi(apiClient);\n            GenericOperationResult result = api.delete(in.orgName(defaultOrg), secretName);\n            switch (result.getResult()) {\n                case DELETED: {\n                    log.info(\"The secret was successfully deleted: {}\", secretName);\n                    return Result.ok();\n                }\n                case NOT_FOUND: {\n                    log.warn(\"Secret not found: {}\", secretName);\n                    return Result.notFound();\n                }\n                default: {\n                    return Result.invalidRequest();\n                }\n            }\n        } catch (ApiException e) {\n            return handleErrors(in, secretName, e.getCode(), e.getResponseBody());\n        }\n    }\n\n    private static void addIfPresent(Map<String, Object> m, String key, Object value) {\n        if (value != null) {\n            m.put(key, value);\n        }\n    }\n\n    private static TaskResult.SimpleResult handleErrors(SecretsTaskParams in, String secretName, ApiException e) {\n        return handleErrors(in, secretName, e.getCode(), e.getResponseBody());\n    }\n\n    private static TaskResult.SimpleResult handleErrors(SecretsTaskParams in, String secretName, int code, String responseBody) {\n        boolean ignoreErrors = in.ignoreErrors();\n\n        if (code == 401) {\n            if (ignoreErrors) {\n                log.warn(\"Access denied to secret '{}' \", secretName);\n                return Result.accessDenied();\n            }\n            throw new RuntimeException(\"Access denied: \" + secretName);\n        } else if (code == 404) {\n            if (ignoreErrors) {\n                log.warn(\"Secret '{}' not found\", secretName);\n                return Result.notFound();\n            }\n            throw new RuntimeException(\"Secret not found: \" + secretName);\n        } else if (code >= 400 && code < 500 && ignoreErrors) {\n            log.warn(\"Invalid request for secret '{}': {}\", secretName, code);\n            return Result.invalidRequest();\n        }\n\n        throw new RuntimeException(\"Error while requesting the secret '\" + secretName + \"': (code=\" + code + \"): \" + responseBody);\n    }\n\n    public enum Status {\n        OK,\n        NOT_FOUND,\n        INVALID_REQUEST,\n        ACCESS_DENIED\n    }\n\n    static class Result {\n\n        private static TaskResult.SimpleResult ok() {\n            return result(true, Status.OK, null);\n        }\n\n        private static TaskResult.SimpleResult ok(String data) {\n            return result(true, Status.OK, data);\n        }\n\n        private static TaskResult.SimpleResult notFound() {\n            return result(false, Status.NOT_FOUND, null);\n        }\n\n        private static TaskResult.SimpleResult invalidRequest() {\n            return result(false, Status.INVALID_REQUEST, null);\n        }\n\n        private static TaskResult.SimpleResult accessDenied() {\n            return result(false, Status.ACCESS_DENIED, null);\n        }\n\n        private static TaskResult.SimpleResult result(boolean ok, Status status, String data) {\n            return TaskResult.of(ok)\n                    .value(\"status\", status.toString())\n                    .value(\"data\", data);\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/SecretsTaskParams.java",
    "content": "package com.walmartlabs.concord.client;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.SecretEntryV2;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\nimport com.walmartlabs.concord.sdk.Constants;\n\nimport java.util.*;\n\npublic class SecretsTaskParams {\n\n    public static SecretsTaskParams of(Variables variables) {\n        SecretsTaskParams p = new SecretsTaskParams(variables);\n        switch (p.action()) {\n            case GETASSTRING: {\n                return new AsStringParams(variables);\n            }\n            case CREATE: {\n                return new CreateParams(variables);\n            }\n            case UPDATE: {\n                return new UpdateParams(variables);\n            }\n            case DELETE: {\n                return p;\n            }\n            default: {\n                throw new IllegalArgumentException(\"Unsupported action type: \" + p.action());\n            }\n        }\n    }\n\n    static final String ORG_KEY = \"org\";\n    static final String SECRET_NAME_KEY = \"name\";\n    static final String IGNORE_ERRORS_KEY = \"ignoreErrors\";\n\n    protected final Variables variables;\n\n    private SecretsTaskParams(Variables variables) {\n        this.variables = variables;\n    }\n\n    public Action action() {\n        String v = variables.getString(Keys.ACTION_KEY, Action.GETASSTRING.toString());\n        try {\n            return Action.valueOf(v.trim().toUpperCase());\n        } catch (IllegalArgumentException e) {\n            throw new RuntimeException(\"Unknown action: '\" + v + \"'. Available actions: \" + Arrays.toString(Action.values()));\n        }\n    }\n\n    public boolean ignoreErrors() {\n        return variables.getBoolean(IGNORE_ERRORS_KEY, false);\n    }\n\n    public String orgName(String defaultOrg) {\n        String org = variables.getString(ORG_KEY);\n        if (org != null) {\n            return org;\n        }\n\n        if (defaultOrg != null) {\n            return defaultOrg;\n        }\n\n        throw new IllegalArgumentException(\"'\" + ORG_KEY + \"' is required\");\n    }\n\n    public String secretName() {\n        return variables.assertString(SECRET_NAME_KEY);\n    }\n\n    static class AsStringParams extends SecretsTaskParams {\n\n        static String STORE_PASSWORD_KEY = \"storePassword\";\n\n        private AsStringParams(Variables variables) {\n            super(variables);\n        }\n\n        public String storePassword() {\n            return variables.getString(STORE_PASSWORD_KEY);\n        }\n    }\n\n    static class CreateParams extends SecretsTaskParams {\n\n        static String DATA_KEY = \"data\";\n        static final String PUBLIC_KEY = \"public\";\n        static final String PRIVATE_KEY = \"private\";\n        static final String USERNAME_KEY = \"username\";\n        static final String PASSWORD_KEY = \"password\"; // NOSONAR\n        static final String STORE_PASSWORD_KEY = \"storePassword\";\n        static final String GENERATE_PASSWORD_KEY = \"generatePassword\"; // NOSONAR\n        static final String VISIBILITY_KEY = \"visibility\";\n        static final String PROJECT_NAME_KEY = \"project\";\n        static final String PROJECT_NAMES_KEY = \"projects\";\n        static final String PROJECT_IDS_KEY = \"projectIds\";\n\n        private CreateParams(Variables variables) {\n            super(variables);\n        }\n\n        public SecretEntryV2.TypeEnum secretType() {\n            String type = variables.getString(Constants.Multipart.TYPE, SecretEntryV2.TypeEnum.DATA.toString());\n            try {\n                return SecretEntryV2.TypeEnum.valueOf(type.trim().toUpperCase());\n            } catch (Exception e) {\n                String message = String.format(\"Invalid argument '%s', allowed values are: 'data' (default), 'key_pair' and 'username_password'\", type);\n                throw new IllegalArgumentException(message);\n            }\n        }\n\n        public Object data() {\n            return variables.get(DATA_KEY, null, Object.class);\n        }\n\n        public String publicKey() {\n            return variables.assertString(PUBLIC_KEY);\n        }\n\n        public String privateKey() {\n            return variables.assertString(PRIVATE_KEY);\n        }\n\n        public String userName() {\n            return variables.assertString(USERNAME_KEY);\n        }\n\n        public Object password() {\n            return variables.assertString(PASSWORD_KEY);\n        }\n\n        public String storePassword() {\n            return variables.getString(STORE_PASSWORD_KEY);\n        }\n\n        public Object generatePassword() {\n            return variables.get(GENERATE_PASSWORD_KEY);\n        }\n\n        public Object visibility() {\n            return variables.get(VISIBILITY_KEY);\n        }\n\n        public String projectName() {\n            return variables.getString(PROJECT_NAME_KEY);\n        }\n\n        public List<String> projectNames() {\n            return variables.getList(PROJECT_NAMES_KEY, null);\n        }\n\n        public List<UUID> projectIds() {\n            return variables.getList(PROJECT_IDS_KEY, null);\n        }\n    }\n\n    static class UpdateParams extends CreateParams {\n\n        static final String NEW_STORE_PASSWORD_KEY = \"newStorePassword\"; // NOSONAR\n        static final String CREATE_IF_MISSING_KEY = \"createIfMissing\";\n\n        private UpdateParams(Variables variables) {\n            super(variables);\n        }\n\n        public String newStorePassword() {\n            return variables.getString(NEW_STORE_PASSWORD_KEY);\n        }\n\n        public boolean createIfMissing() {\n            return variables.getBoolean(CREATE_IF_MISSING_KEY, false);\n        }\n\n    }\n\n    public enum Action {\n        GETASSTRING,\n        CREATE,\n        UPDATE,\n        DELETE\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/model/EventRepository.java",
    "content": "package com.walmartlabs.concord.client.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport org.immutables.value.Value;\n\nimport java.util.Optional;\nimport java.util.UUID;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonIgnoreProperties(ignoreUnknown = true)\n@JsonDeserialize(as = ImmutableEventRepository.class)\npublic interface EventRepository {\n    UUID repositoryId();\n\n    String repository();\n\n    UUID projectId();\n\n    Optional<String> branch();\n\n    boolean enabled();\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/model/PushEvent.java",
    "content": "package com.walmartlabs.concord.client.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport org.immutables.value.Value;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonIgnoreProperties(ignoreUnknown = true)\n@JsonDeserialize(as = ImmutablePushEvent.class)\npublic interface PushEvent {\n\n    boolean deleted();\n\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/model/RefreshEvent.java",
    "content": "package com.walmartlabs.concord.client.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport org.immutables.value.Value;\n\nimport java.util.List;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonIgnoreProperties(ignoreUnknown = true)\n@JsonDeserialize(as = ImmutableRefreshEvent.class)\npublic interface RefreshEvent {\n\n    String type();\n\n    String  branch();\n\n    PushEvent payload();\n\n    @Value.Default\n    default List<EventRepository> repositoryInfo() {\n        return List.of();\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/v1/ContextBackedVariables.java",
    "content": "package com.walmartlabs.concord.client.v1;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\nimport com.walmartlabs.concord.sdk.Context;\n\nimport java.util.Map;\n\npublic class ContextBackedVariables implements Variables {\n\n    private final Context ctx;\n\n    public ContextBackedVariables(Context ctx) {\n        this.ctx = ctx;\n    }\n\n    @Override\n    public Object get(String key) {\n        return ctx.getVariable(key);\n    }\n\n    @Override\n    public void set(String key, Object value) {\n        throw new IllegalStateException(\"Not supported\");\n    }\n\n    @Override\n    public boolean has(String key) {\n        return ctx.getVariable(key) != null;\n    }\n\n    @Override\n    public Map<String, Object> toMap() {\n        throw new IllegalStateException(\"Not supported\");\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/v2/ConcordTaskV2.java",
    "content": "package com.walmartlabs.concord.client.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client.*;\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\nimport com.walmartlabs.concord.sdk.LogTags;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.client.ConcordTaskParams.KillParams;\nimport static com.walmartlabs.concord.client.ConcordTaskParams.ListSubProcesses;\n\n@Named(\"concord\")\n@DryRunReady\n@SuppressWarnings(\"unused\")\npublic class ConcordTaskV2 implements ReentrantTask {\n\n    private static final Logger log = LoggerFactory.getLogger(ConcordTaskV2.class);\n\n    private final ApiClientFactory apiClientFactory;\n    private final Context context;\n\n    @Inject\n    public ConcordTaskV2(ApiClientFactory apiClientFactory, Context context) {\n        this.apiClientFactory = apiClientFactory;\n        this.context = context;\n    }\n\n    @Override\n    public TaskResult execute(Variables in) throws Exception {\n        return delegate().execute(ConcordTaskParams.of(in, context.defaultVariables().toMap()));\n    }\n\n    @Override\n    public TaskResult resume(ResumeEvent event) throws Exception {\n        return delegate().continueAfterSuspend(new ResumePayload(event.state()));\n    }\n\n    public List<String> listSubprocesses(String instanceId, String... tags) throws Exception {\n        return delegate().listSubProcesses(ListSubProcesses.of(UUID.fromString(instanceId), tags)).stream()\n                .map(e -> e.getInstanceId().toString())\n                .collect(Collectors.toList());\n    }\n\n    public List<String> listSubprocesses(Map<String, Object> cfg) throws Exception {\n        return delegate().listSubProcesses(new ListSubProcesses(new MapBackedVariables(cfg))).stream()\n                .map(e -> e.getInstanceId().toString())\n                .collect(Collectors.toList());\n    }\n\n    public void suspendForCompletion(List<String> ids) throws Exception {\n        if (ids.isEmpty()) {\n            return;\n        }\n\n        String eventName = delegate().suspendForCompletion(ids.stream()\n                .map(UUID::fromString)\n                .collect(Collectors.toList()));\n        context.suspend(eventName);\n    }\n\n    public Map<String, ProcessEntry> waitForCompletion(String id) {\n        return waitForCompletion(Collections.singletonList(id));\n    }\n\n    public Map<String, ProcessEntry> waitForCompletion(List<String> ids) {\n        return waitForCompletion(ids, -1);\n    }\n\n    public Map<String, ProcessEntry> waitForCompletion(String id, long timeout) {\n        return waitForCompletion(Collections.singletonList(id), timeout);\n    }\n\n    public Map<String, ProcessEntry> waitForCompletion(List<String> ids, long timeout) {\n        return waitForCompletion(ids, timeout, Function.identity());\n    }\n\n    public <T> Map<String, T> waitForCompletion(List<String> ids, long timeout, Function<ProcessEntry, T> processor) {\n        return delegate().waitForCompletion(toUUIDs(ids), timeout, processor);\n    }\n\n    public void assertFinished(List<String> ids) {\n        var result = waitForCompletion(ids);\n        boolean hasFailed = false;\n        for (var p : result.values()) {\n            if (p.getStatus() == ProcessEntry.StatusEnum.FAILED) {\n                log.info(\"Failed process: {}\", LogTags.instanceId(p.getInstanceId()));\n                hasFailed = true;\n            }\n        }\n\n        if (hasFailed) {\n            throw new UserDefinedException(\"Found failed processes\");\n        }\n    }\n\n    public void kill(Map<String, Object> cfg) throws Exception {\n        delegate().kill(new KillParams(new MapBackedVariables(cfg)));\n    }\n\n    public Map<String, Map<String, Object>> getOutVars(String id) {\n        return getOutVars(Collections.singletonList(id));\n    }\n\n    public Map<String, Map<String, Object>> getOutVars(List<String> ids) {\n        return getOutVars(ids, -1);\n    }\n\n    public Map<String, Map<String, Object>> getOutVars(String id, long timeout) {\n        return getOutVars(Collections.singletonList(id), timeout);\n    }\n\n    public Map<String, Map<String, Object>> getOutVars(List<String> ids, long timeout) {\n        return delegate().getOutVars(null, null, toUUIDs(ids), timeout);\n    }\n\n    public void suspend(String eventId) {\n        this.context.suspend(eventId);\n    }\n\n    public void resume(String processId, String eventId, String saveAs, Map<String, Object> payload) throws ApiException {\n        ClientUtils.withRetry(3, 15000, () -> { // TODO: input args?\n            var api = new ProcessApi(apiClientFactory.create(ApiClientConfiguration.builder().build()));\n            api.resume(UUID.fromString(processId), eventId, saveAs, payload);\n            return null;\n        });\n    }\n\n    private ConcordTaskCommon delegate() {\n        String sessionToken = context.processConfiguration().processInfo().sessionToken();\n        UUID instanceId = context.processInstanceId();\n        ProjectInfo projectInfo = context.processConfiguration().projectInfo();\n        return new ConcordTaskCommon(sessionToken, apiClientFactory, instanceId, projectInfo.orgName(),\n                context.workingDirectory(), context.processConfiguration().debug(), context.processConfiguration().dryRun());\n    }\n\n    private static List<UUID> toUUIDs(List<String> ids) {\n        return ids.stream().map(UUID::fromString).collect(Collectors.toList());\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/v2/JsonStoreTaskV2.java",
    "content": "package com.walmartlabs.concord.client.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.client.JsonStoreTaskCommon;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.DryRunReady;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.util.List;\nimport java.util.Map;\n\n@Named(\"jsonStore\")\n@DryRunReady\n@SuppressWarnings(\"unused\")\npublic class JsonStoreTaskV2 implements Task {\n\n    private final JsonStoreTaskCommon delegate;\n    private final String processOrg;\n\n    @Inject\n    public JsonStoreTaskV2(ApiClient apiClient, Context context) {\n        this.delegate = new JsonStoreTaskCommon(apiClient, context.processConfiguration().dryRun());\n        this.processOrg = context.processConfiguration().projectInfo().orgName();\n    }\n\n    public boolean isStoreExists(String storeName) throws Exception {\n        return isStoreExists(assertOrg(processOrg), storeName);\n    }\n\n    public boolean isStoreExists(String orgName, String storeName) throws Exception {\n        return delegate.isStoreExists(orgName, storeName);\n    }\n\n    public boolean isExists(String storeName, String itemPath) throws Exception {\n        return isExists(assertOrg(processOrg), storeName, itemPath);\n    }\n\n    public boolean isExists(String orgName, String storeName, String itemPath) throws Exception {\n        return delegate.isExists(orgName, storeName, itemPath);\n    }\n\n    public void upsert(String storeName, String itemPath, Object data) throws Exception {\n        upsert(assertOrg(processOrg), storeName, itemPath, data);\n    }\n\n    public void upsert(String orgName, String storeName, String itemPath, Object data) throws Exception {\n        delegate.put(orgName, storeName, itemPath, data, true);\n    }\n\n    public void put(String storeName, String itemPath, Object data) throws Exception {\n        put(assertOrg(processOrg), storeName, itemPath, data);\n    }\n\n    public void put(String orgName, String storeName, String itemPath, Object data) throws Exception {\n        delegate.put(orgName, storeName, itemPath, data, false);\n    }\n\n    public Object get(String storageName, String itemPath) throws Exception {\n        return get(assertOrg(processOrg), storageName, itemPath);\n    }\n\n    public Object get(String orgName, String storeName, String itemPath) throws Exception {\n        return delegate.get(orgName, storeName, itemPath);\n    }\n\n    public Object delete(String storeName, String itemPath) throws Exception {\n        return delete(assertOrg(processOrg), storeName, itemPath);\n    }\n\n    public boolean delete(String orgName, String storeName, String itemPath) throws Exception {\n        return delegate.delete(orgName, storeName, itemPath);\n    }\n\n    public void upsertQuery(String storeName, String queryName, String queryText) throws Exception {\n        upsertQuery(assertOrg(processOrg), storeName, queryName, queryText);\n    }\n\n    public void upsertQuery(String orgName, String storeName, String queryName, String queryText) throws Exception {\n        delegate.createOrUpdateQuery(orgName, storeName, queryName, queryText);\n    }\n\n    public List<Object> executeQuery(String storeName, String queryName) throws Exception {\n        return executeQuery(storeName, queryName, (Map<String, Object>) null);\n    }\n\n    public List<Object> executeQuery(String orgName, String storeName, String queryName) throws Exception {\n        return executeQuery(orgName, storeName, queryName, null);\n    }\n\n    public List<Object> executeQuery(String storeName, String queryName, Map<String, Object> params) throws Exception {\n        return executeQuery(assertOrg(processOrg), storeName, queryName, params);\n    }\n\n    public List<Object> executeQuery(String orgName, String storeName, String queryName, Map<String, Object> params) throws Exception {\n        return delegate.executeQuery(orgName, storeName, queryName, params);\n    }\n\n    private static String assertOrg(String org) {\n        if (org != null) {\n            return org;\n        }\n\n        throw new RuntimeException(\"Can't determine the current organization name. \" +\n                \"Please specify it explicitly or run your process in a project.\");\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/v2/ProjectTaskV2.java",
    "content": "package com.walmartlabs.concord.client.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.client.ProjectTaskCommon;\nimport com.walmartlabs.concord.client.ProjectTaskParams;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\n\n@Named(\"project\")\npublic class ProjectTaskV2 implements Task {\n\n    private final ProjectTaskCommon delegate;\n\n    @Inject\n    public ProjectTaskV2(ApiClient apiClient, Context context) {\n        this.delegate = new ProjectTaskCommon(apiClient, context.processConfiguration().projectInfo().orgName());\n    }\n\n    @Override\n    public TaskResult execute(Variables input) throws Exception {\n        delegate.execute(new ProjectTaskParams(input));\n        return TaskResult.success();\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/v2/RepositoryRefreshTaskV2.java",
    "content": "package com.walmartlabs.concord.client.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.client.RepositoryRefreshTaskCommon;\nimport com.walmartlabs.concord.client.RepositoryRefreshTaskParams;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\n\n@Named(\"repositoryRefresh\")\npublic class RepositoryRefreshTaskV2 implements Task {\n\n    private final RepositoryRefreshTaskCommon delegate;\n\n    @Inject\n    public RepositoryRefreshTaskV2(ApiClient apiClient) {\n        this.delegate = new RepositoryRefreshTaskCommon(apiClient);\n    }\n\n    @Override\n    public TaskResult execute(Variables input) throws Exception {\n        delegate.execute(new RepositoryRefreshTaskParams(input));\n        return TaskResult.success();\n    }\n\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/java/com/walmartlabs/concord/client/v2/SecretsTaskV2.java",
    "content": "package com.walmartlabs.concord.client.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.client.SecretsTaskCommon;\nimport com.walmartlabs.concord.client.SecretsTaskParams;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\n\n@Named(\"concordSecrets\")\npublic class SecretsTaskV2 implements Task {\n\n    private final SecretsTaskCommon delegate;\n\n    @Inject\n    public SecretsTaskV2(ApiClient apiClient, Context context) {\n        this.delegate = new SecretsTaskCommon(apiClient,\n                context.processConfiguration().projectInfo().orgName(),\n                context.processConfiguration().dryRun());\n    }\n\n    @Override\n    public TaskResult execute(Variables input) throws Exception {\n        return delegate.execute(SecretsTaskParams.of(input));\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/main/resources/com/walmartlabs/concord/client/v2/concord.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"Concord Task Schema\",\n  \"description\": \"Schema for the concord task - start/fork processes, manage API keys\",\n  \"definitions\": {\n    \"action\": {\n      \"type\": \"string\",\n      \"pattern\": \"^(?:[sS][tT][aA][rR][tT]|[sS][tT][aA][rR][tT][eE][xX][tT][eE][rR][nN][aA][lL]|[fF][oO][rR][kK]|[kK][iI][lL][lL]|[cC][rR][eE][aA][tT][eE][aA][pP][iI][kK][eE][yY]|[cC][rR][eE][aA][tT][eE][oO][rR][uU][pP][dD][aA][tT][eE][aA][pP][iI][kK][eE][yY])$\",\n      \"description\": \"The action to perform\"\n    },\n    \"actionStart\": {\n      \"type\": \"string\",\n      \"pattern\": \"^[sS][tT][aA][rR][tT]$\"\n    },\n    \"actionStartExternal\": {\n      \"type\": \"string\",\n      \"pattern\": \"^[sS][tT][aA][rR][tT][eE][xX][tT][eE][rR][nN][aA][lL]$\"\n    },\n    \"actionFork\": {\n      \"type\": \"string\",\n      \"pattern\": \"^[fF][oO][rR][kK]$\"\n    },\n    \"actionKill\": {\n      \"type\": \"string\",\n      \"pattern\": \"^[kK][iI][lL][lL]$\"\n    },\n    \"actionApiKey\": {\n      \"type\": \"string\",\n      \"pattern\": \"^(?:[cC][rR][eE][aA][tT][eE][aA][pP][iI][kK][eE][yY]|[cC][rR][eE][aA][tT][eE][oO][rR][uU][pP][dD][aA][tT][eE][aA][pP][iI][kK][eE][yY])$\"\n    },\n    \"stringOrStringArray\": {\n      \"oneOf\": [\n        { \"type\": \"string\" },\n        { \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n      ]\n    },\n    \"attachment\": {\n      \"oneOf\": [\n        { \"type\": \"string\" },\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"src\": {\n              \"type\": \"string\",\n              \"description\": \"Source file path\"\n            },\n            \"dest\": {\n              \"type\": \"string\",\n              \"description\": \"Destination file path\"\n            }\n          },\n          \"required\": [\"src\", \"dest\"],\n          \"additionalProperties\": true\n        }\n      ]\n    },\n    \"startParamsBase\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"project\": {\n          \"type\": \"string\",\n          \"description\": \"Project name\"\n        },\n        \"org\": {\n          \"type\": \"string\",\n          \"description\": \"Organization name\"\n        },\n        \"repo\": {\n          \"type\": \"string\",\n          \"description\": \"Repository name\"\n        },\n        \"repoBranchOrTag\": {\n          \"type\": \"string\",\n          \"description\": \"Git branch or tag\"\n        },\n        \"repoCommitId\": {\n          \"type\": \"string\",\n          \"description\": \"Git commit ID\"\n        },\n        \"payload\": {\n          \"type\": \"string\",\n          \"description\": \"Path to directory or zip file\"\n        },\n        \"entryPoint\": {\n          \"type\": \"string\",\n          \"description\": \"Entry flow name\"\n        },\n        \"activeProfiles\": {\n          \"allOf\": [{ \"$ref\": \"#/definitions/stringOrStringArray\" }],\n          \"description\": \"Active profiles\"\n        },\n        \"arguments\": {\n          \"type\": \"object\",\n          \"description\": \"Process arguments\",\n          \"additionalProperties\": true\n        },\n        \"tags\": {\n          \"allOf\": [{ \"$ref\": \"#/definitions/stringOrStringArray\" }],\n          \"description\": \"Process tags\"\n        },\n        \"meta\": {\n          \"description\": \"Metadata\"\n        },\n        \"requirements\": {\n          \"type\": \"object\",\n          \"description\": \"Agent requirements\",\n          \"additionalProperties\": true\n        },\n        \"attachments\": {\n          \"type\": \"array\",\n          \"items\": { \"$ref\": \"#/definitions/attachment\" },\n          \"description\": \"Files to attach\"\n        },\n        \"exclusive\": {\n          \"type\": \"object\",\n          \"description\": \"Exclusive mode settings\",\n          \"additionalProperties\": true\n        },\n        \"startAt\": {\n          \"type\": \"string\",\n          \"description\": \"Scheduled start time\"\n        },\n        \"disableOnCancel\": {\n          \"type\": \"boolean\",\n          \"default\": false,\n          \"description\": \"Disable onCancel handler\"\n        },\n        \"disableOnFailure\": {\n          \"type\": \"boolean\",\n          \"default\": false,\n          \"description\": \"Disable onFailure handler\"\n        },\n        \"outVars\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"string\" },\n          \"description\": \"Output variable names to retrieve\"\n        },\n        \"sync\": {\n          \"type\": \"boolean\",\n          \"default\": false,\n          \"description\": \"Wait for completion\"\n        },\n        \"suspend\": {\n          \"type\": \"boolean\",\n          \"default\": false,\n          \"description\": \"Suspend until completion\"\n        },\n        \"debug\": {\n          \"type\": \"boolean\",\n          \"description\": \"Enable debug logging\"\n        },\n        \"dryRunMode\": {\n          \"type\": \"boolean\",\n          \"description\": \"Run in dry-run mode\"\n        },\n        \"ignoreFailures\": {\n          \"type\": \"boolean\",\n          \"default\": false,\n          \"description\": \"Ignore child process failures\"\n        },\n        \"baseUrl\": {\n          \"type\": \"string\",\n          \"description\": \"Concord server URL\"\n        },\n        \"apiKey\": {\n          \"type\": \"string\",\n          \"description\": \"API key for authentication\"\n        }\n      },\n      \"additionalProperties\": true\n    },\n    \"startParams\": {\n      \"description\": \"Parameters for starting a process. At least one of 'project' or 'payload' is required.\",\n      \"allOf\": [\n        { \"$ref\": \"#/definitions/startParamsBase\" },\n        {\n          \"anyOf\": [\n            { \"required\": [\"project\"] },\n            { \"required\": [\"payload\"] }\n          ]\n        }\n      ]\n    },\n    \"forkEntry\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"entryPoint\": {\n          \"type\": \"string\",\n          \"description\": \"Entry flow name (required)\"\n        },\n        \"instances\": {\n          \"type\": \"integer\",\n          \"minimum\": 1,\n          \"default\": 1,\n          \"description\": \"Number of instances to spawn\"\n        },\n        \"arguments\": {\n          \"type\": \"object\",\n          \"description\": \"Process arguments\",\n          \"additionalProperties\": true\n        },\n        \"tags\": {\n          \"allOf\": [{ \"$ref\": \"#/definitions/stringOrStringArray\" }],\n          \"description\": \"Process tags\"\n        },\n        \"outVars\": {\n          \"type\": \"array\",\n          \"items\": { \"type\": \"string\" },\n          \"description\": \"Output variable names\"\n        },\n        \"activeProfiles\": {\n          \"allOf\": [{ \"$ref\": \"#/definitions/stringOrStringArray\" }],\n          \"description\": \"Active profiles\"\n        },\n        \"exclusive\": {\n          \"type\": \"object\",\n          \"description\": \"Exclusive mode settings\",\n          \"additionalProperties\": true\n        },\n        \"meta\": {\n          \"description\": \"Metadata\"\n        },\n        \"requirements\": {\n          \"type\": \"object\",\n          \"description\": \"Agent requirements\",\n          \"additionalProperties\": true\n        },\n        \"disableOnCancel\": {\n          \"type\": \"boolean\",\n          \"default\": false,\n          \"description\": \"Disable onCancel handler\"\n        },\n        \"disableOnFailure\": {\n          \"type\": \"boolean\",\n          \"default\": false,\n          \"description\": \"Disable onFailure handler\"\n        },\n        \"baseUrl\": {\n          \"type\": \"string\",\n          \"description\": \"Concord server URL\"\n        },\n        \"apiKey\": {\n          \"type\": \"string\",\n          \"description\": \"API key for authentication\"\n        }\n      },\n      \"required\": [\"entryPoint\"],\n      \"additionalProperties\": true\n    }\n  },\n  \"in\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"action\": {\n        \"$ref\": \"#/definitions/action\"\n      }\n    },\n    \"required\": [\"action\"],\n    \"allOf\": [\n      {\n        \"if\": {\n          \"properties\": { \"action\": { \"$ref\": \"#/definitions/actionStart\" } },\n          \"required\": [\"action\"]\n        },\n        \"then\": {\n          \"$ref\": \"#/definitions/startParams\"\n        }\n      },\n      {\n        \"if\": {\n          \"properties\": { \"action\": { \"$ref\": \"#/definitions/actionStartExternal\" } },\n          \"required\": [\"action\"]\n        },\n        \"then\": {\n          \"allOf\": [\n            { \"$ref\": \"#/definitions/startParams\" },\n            { \"required\": [\"apiKey\"] }\n          ]\n        }\n      },\n      {\n        \"if\": {\n          \"properties\": { \"action\": { \"$ref\": \"#/definitions/actionFork\" } },\n          \"required\": [\"action\"]\n        },\n        \"then\": {\n          \"properties\": {\n            \"forks\": {\n              \"type\": \"array\",\n              \"items\": { \"$ref\": \"#/definitions/forkEntry\" },\n              \"minItems\": 1,\n              \"description\": \"Fork configurations (cannot be empty)\"\n            },\n            \"entryPoint\": {\n              \"type\": \"string\",\n              \"description\": \"Entry flow name (required when not using forks array)\"\n            },\n            \"sync\": {\n              \"type\": \"boolean\",\n              \"default\": false,\n              \"description\": \"Wait for completion\"\n            },\n            \"suspend\": {\n              \"type\": \"boolean\",\n              \"default\": false,\n              \"description\": \"Suspend until completion\"\n            },\n            \"ignoreFailures\": {\n              \"type\": \"boolean\",\n              \"default\": false,\n              \"description\": \"Ignore child process failures\"\n            },\n            \"outVars\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"string\" },\n              \"description\": \"Output variable names to retrieve\"\n            },\n            \"tags\": {\n              \"allOf\": [{ \"$ref\": \"#/definitions/stringOrStringArray\" }],\n              \"description\": \"Process tags (inherited by forks)\"\n            },\n            \"arguments\": {\n              \"type\": \"object\",\n              \"description\": \"Process arguments (inherited by forks)\",\n              \"additionalProperties\": true\n            },\n            \"activeProfiles\": {\n              \"allOf\": [{ \"$ref\": \"#/definitions/stringOrStringArray\" }],\n              \"description\": \"Active profiles (inherited by forks)\"\n            },\n            \"exclusive\": {\n              \"type\": \"object\",\n              \"description\": \"Exclusive mode settings (inherited by forks)\",\n              \"additionalProperties\": true\n            },\n            \"meta\": {\n              \"description\": \"Metadata (inherited by forks)\"\n            },\n            \"requirements\": {\n              \"type\": \"object\",\n              \"description\": \"Agent requirements (inherited by forks)\",\n              \"additionalProperties\": true\n            },\n            \"disableOnCancel\": {\n              \"type\": \"boolean\",\n              \"default\": false,\n              \"description\": \"Disable onCancel handler (inherited by forks)\"\n            },\n            \"disableOnFailure\": {\n              \"type\": \"boolean\",\n              \"default\": false,\n              \"description\": \"Disable onFailure handler (inherited by forks)\"\n            },\n            \"baseUrl\": {\n              \"type\": \"string\",\n              \"description\": \"Concord server URL\"\n            },\n            \"apiKey\": {\n              \"type\": \"string\",\n              \"description\": \"API key for authentication\"\n            }\n          },\n          \"anyOf\": [\n            { \"required\": [\"forks\"] },\n            { \"required\": [\"entryPoint\"] }\n          ],\n          \"additionalProperties\": true\n        }\n      },\n      {\n        \"if\": {\n          \"properties\": { \"action\": { \"$ref\": \"#/definitions/actionKill\" } },\n          \"required\": [\"action\"]\n        },\n        \"then\": {\n          \"properties\": {\n            \"instanceId\": {\n              \"oneOf\": [\n                { \"type\": \"string\" },\n                { \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n              ],\n              \"description\": \"Process ID(s) to kill\"\n            },\n            \"sync\": {\n              \"type\": \"boolean\",\n              \"default\": false,\n              \"description\": \"Wait for process termination\"\n            }\n          },\n          \"required\": [\"instanceId\"],\n          \"additionalProperties\": true\n        }\n      },\n      {\n        \"if\": {\n          \"properties\": { \"action\": { \"$ref\": \"#/definitions/actionApiKey\" } },\n          \"required\": [\"action\"]\n        },\n        \"then\": {\n          \"properties\": {\n            \"baseUrl\": {\n              \"type\": \"string\",\n              \"description\": \"Concord server URL\"\n            },\n            \"apiKey\": {\n              \"type\": \"string\",\n              \"description\": \"API key for authentication\"\n            },\n            \"userId\": {\n              \"type\": \"string\",\n              \"description\": \"Target user ID\"\n            },\n            \"username\": {\n              \"type\": \"string\",\n              \"description\": \"Target username\"\n            },\n            \"userDomain\": {\n              \"type\": \"string\",\n              \"description\": \"User domain\"\n            },\n            \"userType\": {\n              \"type\": \"string\",\n              \"pattern\": \"^(?:[lL][oO][cC][aA][lL]|[lL][dD][aA][pP]|[oO][iI][dD][cC])$\",\n              \"description\": \"User type\"\n            },\n            \"name\": {\n              \"type\": \"string\",\n              \"description\": \"API key name\"\n            },\n            \"key\": {\n              \"type\": \"string\",\n              \"description\": \"Specific API key value\"\n            },\n            \"ignoreExisting\": {\n              \"type\": \"boolean\",\n              \"default\": false,\n              \"description\": \"Ignore if API key already exists\"\n            }\n          },\n          \"anyOf\": [\n            { \"required\": [\"userId\"] },\n            { \"required\": [\"username\"] }\n          ],\n          \"additionalProperties\": true\n        }\n      }\n    ],\n    \"additionalProperties\": true\n  },\n  \"out\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"ok\": {\n        \"type\": \"boolean\",\n        \"description\": \"Whether the operation succeeded\"\n      },\n      \"id\": {\n        \"type\": \"string\",\n        \"description\": \"Instance ID (for start) or API key ID (for createApiKey)\"\n      },\n      \"ids\": {\n        \"type\": \"array\",\n        \"items\": { \"type\": \"string\" },\n        \"description\": \"Instance IDs (for fork with multiple processes)\"\n      },\n      \"name\": {\n        \"type\": \"string\",\n        \"description\": \"API key name\"\n      },\n      \"key\": {\n        \"type\": \"string\",\n        \"description\": \"The actual API key value\"\n      },\n      \"result\": {\n        \"type\": \"string\",\n        \"description\": \"Result status (CREATED or UPDATED for createOrUpdateApiKey)\"\n      },\n      \"expiredAt\": {\n        \"type\": [\"string\", \"null\"],\n        \"description\": \"API key expiration timestamp\"\n      }\n    },\n    \"required\": [\"ok\"],\n    \"additionalProperties\": true\n  }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/test/java/com/walmartlabs/concord/client/ConcordTaskParamsTest.java",
    "content": "package com.walmartlabs.concord.client;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client.ConcordTaskParams.ForkParams;\nimport com.walmartlabs.concord.client.ConcordTaskParams.ForkStartParams;\nimport com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.*;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class ConcordTaskParamsTest {\n\n    @Test\n    public void testForks() {\n        List<String> tags = Arrays.asList(\"x\", \"y\", \"z\");\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"action\", \"fork\");\n        input.put(\"tags\", tags);\n        input.put(\"forks\", Arrays.asList(\n                Collections.singletonMap(\"entryPoint\", \"aaa\"),\n                Collections.singletonMap(\"entryPoint\", \"bbb\")\n        ));\n\n        ForkParams params = (ForkParams) ConcordTaskParams.of(new MapBackedVariables(input), Collections.emptyMap());\n\n        List<ForkStartParams> forks = params.forks();\n        assertEquals(2, forks.size());\n\n        ForkStartParams f1 = forks.get(0);\n        assertEquals(\"aaa\", f1.entryPoint());\n        assertTrue(f1.tags().containsAll(tags));\n\n        ForkStartParams f2 = forks.get(1);\n        assertEquals(\"bbb\", f2.entryPoint());\n        assertTrue(f2.tags().containsAll(tags));\n\n        System.out.println(params);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/concord/src/test/java/com/walmartlabs/concord/client/RepositoryRefreshCommonTest.java",
    "content": "package com.walmartlabs.concord.client;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport org.mockito.ArgumentCaptor;\nimport org.mockito.Mockito;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static org.junit.jupiter.api.Assertions.assertDoesNotThrow;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.*;\n\nclass RepositoryRefreshCommonTest {\n\n    private static final UUID MOCK_REPO_ID_1 = UUID.randomUUID();\n    private static final UUID MOCK_REPO_ID_2 = UUID.randomUUID();\n\n    ArgumentCaptor<List<UUID>> listCaptor;\n\n    @BeforeEach\n    @SuppressWarnings(\"unchecked\")\n    void setup() {\n        listCaptor = ArgumentCaptor.forClass(List.class);\n    }\n\n    @Test\n    void testPushMain() throws Exception {\n        RepositoryRefreshTaskCommon common = getCommon();\n\n        common.execute(params(\"main\", false, true));\n\n        verify(common, times(1))\n                .refresh(listCaptor.capture());\n\n        assertEquals(1, listCaptor.getValue().size());\n        assertEquals(MOCK_REPO_ID_2, listCaptor.getValue().get(0));\n    }\n\n    @Test\n    void testPushDev() throws Exception {\n        RepositoryRefreshTaskCommon common = getCommon();\n\n        common.execute(params(\"dev\", false, true));\n\n        verify(common, times(1))\n                .refresh(listCaptor.capture());\n\n        assertEquals(1, listCaptor.getValue().size());\n        assertEquals(MOCK_REPO_ID_1, listCaptor.getValue().get(0));\n    }\n\n    @Test\n    void testPushDevDisabled() throws Exception {\n        RepositoryRefreshTaskCommon common = getCommon();\n\n        common.execute(params(\"dev\", false, false));\n\n        verify(common, times(1))\n                .refresh(listCaptor.capture());\n\n        assertEquals(0, listCaptor.getValue().size());\n    }\n\n    @Test\n    void testPushDeleteDev() throws Exception {\n        RepositoryRefreshTaskCommon common = getCommon();\n\n        common.execute(params(\"dev\", true, true));\n\n        verify(common, times(0))\n                .refresh(listCaptor.capture());\n    }\n\n    private static RepositoryRefreshTaskCommon getCommon() {\n        ApiClient apiClient = Mockito.mock(ApiClient.class);\n        RepositoryRefreshTaskCommon common = spy(new RepositoryRefreshTaskCommon(apiClient));\n\n        assertDoesNotThrow(() -> doNothing().when(common).refresh(any()));\n\n        return common;\n    }\n\n    private static RepositoryRefreshTaskParams params(String branch, boolean deleted, boolean devEnabled) {\n        Map<String, Object> rawParams = Map.of(\n                \"event\", Map.of(\n                        \"type\", \"push\",\n                        \"branch\", branch,\n                        \"payload\", Map.of(\n                                \"deleted\", deleted\n                        ),\n                        \"repositoryInfo\", List.of(\n                                Map.of(\n                                        \"repositoryId\", MOCK_REPO_ID_1,\n                                        \"repository\", \"broken_repo\",\n                                        \"projectId\", UUID.randomUUID(),\n                                        \"branch\", \"dev\",\n                                        \"enabled\", devEnabled\n                                ),\n                                Map.of(\n                                        \"repositoryId\", MOCK_REPO_ID_2,\n                                        \"repository\", \"main\",\n                                        \"projectId\", UUID.randomUUID(),\n                                        \"branch\", \"main\",\n                                        \"enabled\", true\n                                )\n                        )\n                )\n        );\n\n        return new RepositoryRefreshTaskParams(new MapBackedVariables(rawParams));\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/crypto/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>crypto-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <configuration>\n                    <!-- no verify, as this plugin depends on runtime-v2 -->\n                    <skipVerify>true</skipVerify>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/tasks/crypto/src/main/java/com/walmartlabs/concord/plugins/crypto/CryptoTask.java",
    "content": "package com.walmartlabs.concord.plugins.crypto;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.*;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.util.Map;\n\n@Named(\"crypto\")\npublic class CryptoTask implements Task {\n\n   private final SecretService secretService;\n\n    @InjectVariable(Constants.Context.CONTEXT_KEY)\n    Context context;\n\n    @Inject\n    public CryptoTask(SecretService secretService) {\n        this.secretService = secretService;\n    }\n\n    public String exportAsString(@InjectVariable(\"txId\") String instanceId,\n                                 String name,\n                                 String password) throws Exception {\n\n        return exportAsString(instanceId, null, name, password);\n    }\n\n    public String exportAsString(@InjectVariable(\"txId\") String instanceId,\n                                 String orgName,\n                                 String name,\n                                 String password) throws Exception {\n        return secretService.exportAsString(context, instanceId, orgName, name, password);\n    }\n\n    public Map<String, String> exportKeyAsFile(@InjectVariable(\"txId\") String instanceId,\n                                               @InjectVariable(\"workDir\") String workDir,\n                                               String name,\n                                               String password) throws Exception {\n\n        return exportKeyAsFile(instanceId, workDir, null, name, password);\n    }\n\n    public Map<String, String> exportKeyAsFile(@InjectVariable(\"txId\") String instanceId,\n                                               @InjectVariable(\"workDir\") String workDir,\n                                               String orgName,\n                                               String name,\n                                               String password) throws Exception {\n        return secretService.exportKeyAsFile(context, instanceId, workDir, orgName, name, password);\n    }\n\n    public Map<String, String> exportCredentials(@InjectVariable(\"txId\") String instanceId,\n                                                 @InjectVariable(\"workDir\") String workDir,\n                                                 String name,\n                                                 String password) throws Exception {\n\n        return exportCredentials(instanceId, workDir, null, name, password);\n    }\n\n    public Map<String, String> exportCredentials(@InjectVariable(\"txId\") String instanceId,\n                                                 @InjectVariable(\"workDir\") String workDir,\n                                                 String orgName,\n                                                 String name,\n                                                 String password) throws Exception {\n        return secretService.exportCredentials(context, instanceId, workDir, orgName, name, password);\n    }\n\n    public String exportAsFile(@InjectVariable(\"txId\") String instanceId,\n                               @InjectVariable(\"workDir\") String workDir,\n                               String name,\n                               String password) throws Exception {\n\n        return exportAsFile(instanceId, workDir, null, name, password);\n    }\n\n    public String exportAsFile(@InjectVariable(\"txId\") String instanceId,\n                               @InjectVariable(\"workDir\") String workDir,\n                               String orgName,\n                               String name,\n                               String password) throws Exception {\n        return secretService.exportAsFile(context, instanceId, workDir, orgName, name, password);\n    }\n\n    public String decryptString(@InjectVariable(\"txId\") String instanceId, String s) throws Exception {\n        return secretService.decryptString(context, instanceId, s);\n    }\n\n    public String encryptString(@InjectVariable(\"txId\") String instanceId, String orgName, String projName, String value) throws Exception {\n        return secretService.encryptString(context, instanceId, orgName, projName, value);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/crypto/src/main/java/com/walmartlabs/concord/plugins/crypto/CryptoTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.crypto;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProjectInfo;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\nimport com.walmartlabs.concord.runtime.v2.sdk.SecretService.KeyPair;\nimport com.walmartlabs.concord.runtime.v2.sdk.SecretService.SecretCreationResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.SecretService.UsernamePassword;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.util.HashMap;\nimport java.util.Map;\n\n@Named(\"crypto\")\n@SuppressWarnings(\"unused\")\npublic class CryptoTaskV2 implements Task {\n\n    private static final Logger log = LoggerFactory.getLogger(CryptoTaskV2.class);\n\n    private static final int RETRY_COUNT = 3;\n    private static final int RETRY_INTERVAL = 5000;\n\n    private final SecretService secretService;\n    private final Path workDir;\n    private final ProcessConfiguration processCfg;\n    private final String processOrg;\n    private final boolean dryRunMode;\n\n    @Inject\n    public CryptoTaskV2(Context context) {\n        this.secretService = context.secretService();\n        this.workDir = context.workingDirectory();\n        this.processCfg = context.processConfiguration();\n\n        ProjectInfo projectInfo = processCfg.projectInfo();\n        this.processOrg = projectInfo != null ? projectInfo.orgName() : null;\n        this.dryRunMode = processCfg.dryRun();\n    }\n\n    @SensitiveData\n    public String exportAsString(String orgName, String name, @SensitiveData String password) throws Exception {\n        return secretService.exportAsString(orgName, name, password);\n    }\n\n    public Map<String, String> exportKeyAsFile(String orgName, String name, @SensitiveData String password) throws Exception {\n        KeyPair keyPair = secretService.exportKeyAsFile(orgName, name, password);\n\n        Path baseDir = workDir;\n\n        Map<String, String> m = new HashMap<>();\n        m.put(\"private\", baseDir.relativize(keyPair.privateKey()).toString());\n        m.put(\"public\", baseDir.relativize(keyPair.publicKey()).toString());\n        return m;\n    }\n\n    @SensitiveData(keys = {\"password\"})\n    public Map<String, String> exportCredentials(String orgName, String name, @SensitiveData String password) throws Exception {\n        UsernamePassword credentials = secretService.exportCredentials(orgName, name, password);\n\n        Map<String, String> m = new HashMap<>();\n        m.put(\"username\", credentials.username());\n        m.put(\"password\", credentials.password());\n        return m;\n    }\n\n    public String exportAsFile(String orgName, String name, @SensitiveData String password) throws Exception {\n        Path path = secretService.exportAsFile(orgName, name, password);\n        return workDir.relativize(path).toString();\n    }\n\n    public String exportAsFile(String exportDir, String orgName, String name, @SensitiveData String password) throws Exception {\n        Path path = secretService.exportAsFile(orgName, name, password);\n        Path dest = workDir.resolve(exportDir);\n\n        Files.createDirectories(dest);\n\n        Path destFileName = dest.resolve(path.getFileName());\n        Files.move(path, destFileName, StandardCopyOption.REPLACE_EXISTING);\n\n        return workDir.relativize(destFileName).toString();\n    }\n\n    public String encryptString(String value) throws Exception {\n        ProjectInfo projectInfo = processCfg.projectInfo();\n\n        String orgName = projectInfo.orgName();\n        String projectName = projectInfo.projectName();\n        if (orgName == null || projectName == null) {\n            throw new IllegalStateException(\"The process must run in a project in order to be able to encrypt strings.\");\n        }\n\n        return secretService.encryptString(orgName, projectName, value);\n    }\n\n    @SensitiveData\n    public String decryptString(String s) throws Exception {\n        return secretService.decryptString(s);\n    }\n\n    @Override\n    public TaskResult execute(Variables input) throws Exception {\n        TaskParams params = new TaskParams(input);\n        TaskParams.Action action = params.action();\n        switch (action) {\n            case CREATE: {\n                SecretCreationResult result = createSecret(params);\n                if (result == null) {\n                    return TaskResult.success();\n                }\n\n                log.info(\"The secret '{}/{}' was created successfully\", params.orgOrDefault(processOrg), params.secretName());\n                return TaskResult.success()\n                        .value(\"password\", result.password());\n            }\n            default:\n                throw new IllegalArgumentException(\"Unsupported action type: \" + action);\n        }\n    }\n\n    private SecretCreationResult createSecret(TaskParams in) throws Exception {\n        SecretService.SecretParams secret = SecretService.SecretParams.builder()\n                .orgName(in.orgOrDefault(processOrg))\n                .secretName(in.secretName())\n                .generatePassword(in.generatePassword())\n                .storePassword(in.storePassword())\n                .visibility(in.visibility() != null ? SecretService.SecretParams.Visibility.valueOf(in.visibility()) : null)\n                .project(in.project())\n                .build();\n\n        String data = in.data();\n        TaskParams.KeyPair kp = in.keyPair();\n        TaskParams.UsernamePassword up = in.usernamePassword();\n\n        if (data != null) {\n            if (dryRunMode) {\n                log.info(\"Dry-run mode enabled: Skipping creating of data secret '{}'\", in.secretName());\n                return null;\n            }\n\n            return secretService.createData(secret, readFile(toPath(data)));\n        } else if (kp != null) {\n            Path publicKey = toPath(kp.publicKey());\n            Path privateKey = toPath(kp.privateKey());\n\n            if (dryRunMode) {\n                log.info(\"Dry-run mode enabled: Skipping creating of key-pair secret '{}'\", in.secretName());\n                return null;\n            }\n\n            return secretService.createKeyPair(secret, KeyPair.builder()\n                    .publicKey(publicKey)\n                    .privateKey(privateKey)\n                    .build());\n        } else if (up != null) {\n            if (dryRunMode) {\n                log.info(\"Dry-run mode enabled: Skipping creating of username secret '{}'\", in.secretName());\n                return null;\n            }\n\n            return secretService.createUsernamePassword(secret, UsernamePassword.of(up.username(), up.password()));\n        } else {\n            throw new IllegalArgumentException(\"A path to the secret's data, a key pair or a username/password pair must be specified.\");\n        }\n    }\n\n    private Path toPath(String value) {\n        Path p = workDir.resolve(value).normalize();\n\n        if (!p.startsWith(workDir)) {\n            throw new IllegalArgumentException(\"Can't use paths outside of the working directory: \" + p.toAbsolutePath());\n        }\n\n        return p;\n    }\n\n    private static byte[] readFile(Path file) throws IOException {\n        if (Files.notExists(file)) {\n            throw new RuntimeException(\"File '\" + file + \"' not found\");\n        }\n\n        return Files.readAllBytes(file);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/crypto/src/main/java/com/walmartlabs/concord/plugins/crypto/TaskParams.java",
    "content": "package com.walmartlabs.concord.plugins.crypto;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport java.util.Arrays;\nimport java.util.Collections;\n\npublic class TaskParams {\n\n    private static final String ACTION_KEY = \"action\";\n    private static final String SECRET_NAME_KEY = \"secretName\";\n    private static final String GENERATE_PASSWORD_KEY = \"generatePassword\";\n    private static final String STORE_PASSWORD_KEY = \"storePassword\";\n    private static final String VISIBILITY_KEY = \"visibility\";\n    private static final String ORG_KEY = \"org\";\n    private static final String PROJECT_KEY = \"project\";\n    private static final String KEY_PAIR_KEY = \"keyPair\";\n    private static final String USERNAME_PASSWORD_KEY = \"usernamePassword\";\n    private static final String DATA_KEY = \"data\";\n\n    private final Variables variables;\n\n    public TaskParams(Variables variables) {\n        this.variables = variables;\n    }\n\n    public Action action() {\n        String action = variables.assertString(ACTION_KEY);\n        try {\n            return Action.valueOf(action.trim().toUpperCase());\n        } catch (IllegalArgumentException e) {\n            throw new RuntimeException(\"Unknown action: '\" + action + \"'. Available actions: \" + Arrays.toString(Action.values()));\n        }\n    }\n\n    public String secretName() {\n        return variables.assertString(SECRET_NAME_KEY);\n    }\n\n    public boolean generatePassword() {\n        return variables.getBoolean(GENERATE_PASSWORD_KEY, false);\n    }\n\n    public String storePassword() {\n        return variables.getString(STORE_PASSWORD_KEY);\n    }\n\n    public String visibility() {\n        return variables.getString(VISIBILITY_KEY);\n    }\n\n    public String orgOrDefault(String defaultValue) {\n        String org = variables.getString(ORG_KEY, defaultValue);\n        if (org == null) {\n            throw new IllegalArgumentException(\"An organization name is required.\");\n        }\n        return org;\n    }\n\n    public String project() {\n        return variables.getString(PROJECT_KEY);\n    }\n\n    public KeyPair keyPair() {\n        if (variables.has(KEY_PAIR_KEY)) {\n            return new KeyPair(new MapBackedVariables(variables.getMap(KEY_PAIR_KEY, Collections.emptyMap())));\n        }\n        return null;\n    }\n\n    public String data() {\n        return variables.getString(DATA_KEY);\n    }\n\n    public UsernamePassword usernamePassword() {\n        if (variables.has(USERNAME_PASSWORD_KEY)) {\n            return new UsernamePassword(new MapBackedVariables(variables.getMap(USERNAME_PASSWORD_KEY, Collections.emptyMap())));\n        }\n        return null;\n    }\n\n    public static class KeyPair {\n\n        private static final String PUBLIC_KEY = \"public\";\n        private static final String PRIVATE_KEY = \"private\";\n\n        private final Variables variables;\n\n        public KeyPair(Variables variables) {\n            this.variables = variables;\n        }\n\n        public String publicKey() {\n            return variables.assertString(PUBLIC_KEY);\n        }\n\n        public String privateKey() {\n            return variables.assertString(PRIVATE_KEY);\n        }\n    }\n\n    public static class UsernamePassword {\n\n        private static final String USERNAME_KEY = \"username\";\n        private static final String PASSWORD_KEY = \"password\";\n\n        private final Variables variables;\n\n        public UsernamePassword(Variables variables) {\n            this.variables = variables;\n        }\n\n        public String username() {\n            return variables.assertString(USERNAME_KEY);\n        }\n\n        public String password() {\n            return variables.assertString(PASSWORD_KEY);\n        }\n    }\n\n    public enum Action {\n        CREATE\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/docker/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>docker-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <configuration>\n                    <!-- no verify, as this plugin depends on runtime-v2 -->\n                    <skipVerify>true</skipVerify>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/tasks/docker/src/main/java/com/walmartlabs/concord/plugins/docker/DockerConstants.java",
    "content": "package com.walmartlabs.concord.plugins.docker;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class DockerConstants {\n\n    private DockerConstants() {}\n\n    public static final int SUCCESS_EXIT_CODE = 0;\n    public static final String VOLUME_CONTAINER_DEST = \"/workspace\";\n\n    public static final String CMD_KEY = \"cmd\";\n    public static final String IMAGE_KEY = \"image\";\n    public static final String ENV_KEY = \"env\";\n    public static final String ENV_FILE_KEY = \"envFile\";\n    public static final String HOSTS_KEY = \"hosts\";\n    public static final String FORCE_PULL_KEY = \"forcePull\";\n    public static final String DEBUG_KEY = \"debug\";\n    public static final String PULL_RETRY_COUNT_KEY = \"pullRetryCount\";\n    public static final String PULL_RETRY_INTERVAL_KEY = \"pullRetryInterval\";\n}\n"
  },
  {
    "path": "plugins/tasks/docker/src/main/java/com/walmartlabs/concord/plugins/docker/DockerTask.java",
    "content": "package com.walmartlabs.concord.plugins.docker;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.plugins.docker.DockerConstants.*;\n\n@Named(\"docker\")\npublic class DockerTask implements Task {\n\n    private static final Logger log = LoggerFactory.getLogger(DockerTask.class);\n    private static final Logger processLog = LoggerFactory.getLogger(\"processLog\");\n\n    private static final String STDOUT_KEY = \"stdout\";\n    private static final String STDERR_KEY = \"stderr\";\n\n    private static final String[] ALL_KEYS = {\n            CMD_KEY, IMAGE_KEY, ENV_KEY, ENV_FILE_KEY, HOSTS_KEY, FORCE_PULL_KEY,\n            DEBUG_KEY, PULL_RETRY_COUNT_KEY, PULL_RETRY_INTERVAL_KEY, STDOUT_KEY, STDERR_KEY\n    };\n\n    @Inject\n    private DockerService dockerService;\n\n    @Override\n    public void execute(Context ctx) throws Exception {\n        Path workDir = ContextUtils.getWorkDir(ctx);\n        TaskParams params = new TaskParams(createInput(ctx));\n\n        String stdOutVar = ContextUtils.getString(ctx, STDOUT_KEY);\n        String stdErrVar = ContextUtils.getString(ctx, STDERR_KEY);\n\n        // redirect stderr to stdout\n        boolean redirectErrorStream = stdErrVar == null && stdOutVar == null;\n\n        // redirect stdout to log\n        boolean logStdOut = redirectErrorStream || stdOutVar == null;\n\n        // redirect stderr to log\n        boolean logStdErr = !logStdOut || stdErrVar != null;\n\n        // save stderr to a variable\n        boolean storeStdErr = stdErrVar != null;\n\n        String stdOutFilePath = null;\n        if (stdOutVar != null) {\n            Path logFile = DockerTaskCommon.createTmpFile(workDir, \"stdout\", \".log\");\n            stdOutFilePath = workDir.relativize(logFile).toString();\n        }\n\n        DockerContainerSpec spec = DockerContainerSpec.builder()\n                .image(params.image())\n                .env(DockerTaskCommon.stringify(params.env()))\n                .envFile(DockerTaskCommon.getEnvFile(workDir, params))\n                .entryPoint(DockerTaskCommon.prepareEntryPoint(workDir, params))\n                .forcePull(params.forcePull())\n                .options(DockerContainerSpec.Options.builder().hosts(params.hosts()).build())\n                .debug(params.debug(false))\n                .redirectErrorStream(redirectErrorStream)\n                .stdOutFilePath(stdOutFilePath)\n                .pullRetryCount(params.pullRetryCount())\n                .pullRetryInterval(params.pullRetryInterval())\n                .build();\n\n        StringBuilder stdErr = new StringBuilder();\n        int code = dockerService.start(ctx, spec,\n                logStdOut ? line -> processLog.info(\"DOCKER: {}\", line) : null,\n                logStdErr ? line -> {\n                    if (storeStdErr) {\n                        stdErr.append(line).append(\"\\n\");\n                    }\n\n                    processLog.info(\"DOCKER: {}\", line);\n                } : null);\n\n        String stdOut = null;\n        if (stdOutFilePath != null) {\n            InputStream inputStream = Files.newInputStream(Paths.get(stdOutFilePath));\n            stdOut = DockerTaskCommon.toString(inputStream);\n        }\n\n        if (stdOutVar != null) {\n            ctx.setVariable(stdOutVar, stdOut);\n        }\n\n        if (stdErrVar != null) {\n            ctx.setVariable(stdErrVar, stdErr.toString());\n        }\n\n        if (code != SUCCESS_EXIT_CODE) {\n            log.warn(\"call ['{}', '{}', '{}'] -> finished with code {}\", params.image(), params.cmd(), workDir, code);\n            throw new RuntimeException(\"Docker process finished with with exit code \" + code);\n        }\n\n        log.info(\"call ['{}', '{}', '{}', '{}'] -> done\", params.image(), params.cmd(), workDir, params.hosts());\n    }\n\n    private static Map<String, Object> createInput(Context ctx) {\n        Map<String, Object> result = new HashMap<>();\n        for (String k : ALL_KEYS) {\n            result.put(k, ctx.getVariable(k));\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/docker/src/main/java/com/walmartlabs/concord/plugins/docker/DockerTaskCommon.java",
    "content": "package com.walmartlabs.concord.plugins.docker;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardOpenOption;\nimport java.nio.file.attribute.PosixFilePermission;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.plugins.docker.DockerConstants.ENV_FILE_KEY;\nimport static com.walmartlabs.concord.plugins.docker.DockerConstants.VOLUME_CONTAINER_DEST;\n\npublic final class DockerTaskCommon {\n\n    public static String prepareEntryPoint(Path workDir, TaskParams params) throws IOException {\n        if (params.cmd() == null) {\n            return null;\n        }\n\n        // create a script containing the specified \"cmd\"\n        Path p = createTmpFile(workDir, \"docker\", \".sh\");\n        Path runScript = createRunScript(p, params.cmd());\n        return Paths.get(VOLUME_CONTAINER_DEST).resolve(workDir.relativize(runScript))\n                .toAbsolutePath()\n                .toString();\n    }\n\n    public static Path createTmpFile(Path workDir, String prefix, String suffix) throws IOException {\n        Path tmpDir = workDir.resolve(Constants.Files.CONCORD_SYSTEM_DIR_NAME);\n        if (!Files.exists(tmpDir)) {\n            Files.createDirectories(tmpDir);\n        }\n\n        return Files.createTempFile(tmpDir, prefix, suffix);\n    }\n\n    public static String getEnvFile(Path workDir, TaskParams params) {\n        if (params.envFile() == null) {\n            return null;\n        }\n\n        Path p = workDir.resolve(params.envFile());\n        if (!Files.exists(p)) {\n            throw new IllegalArgumentException(\"'\" + ENV_FILE_KEY + \"' file not found: \" + params.envFile());\n        }\n        return p.toAbsolutePath().toString();\n    }\n\n    public static Map<String, String> stringify(Map<String, Object> m) {\n        if (m == null || m.isEmpty()) {\n            return Collections.emptyMap();\n        }\n\n        Map<String, String> result = new HashMap<>();\n        for (Map.Entry<String, Object> e : m.entrySet()) {\n            Object v = e.getValue();\n            if (v == null) {\n                continue;\n            }\n\n            result.put(e.getKey(), v.toString());\n        }\n\n        return result;\n    }\n\n    public static Path createRunScript(Path file, String cmd) throws IOException {\n        String script = \"#!/bin/sh\\n\" +\n                \"cd \" + VOLUME_CONTAINER_DEST + \"\\n\" +\n                cmd;\n\n        Files.write(file, script.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);\n        updateScriptPermissions(file);\n\n        return file;\n    }\n\n    public static void updateScriptPermissions(Path p) throws IOException {\n        Set<PosixFilePermission> perms = new HashSet<>();\n        perms.add(PosixFilePermission.OWNER_READ);\n        perms.add(PosixFilePermission.OWNER_WRITE);\n        perms.add(PosixFilePermission.OWNER_EXECUTE);\n        Files.setPosixFilePermissions(p, perms);\n    }\n\n    public static String toString(InputStream in) throws IOException {\n        ByteArrayOutputStream out = new ByteArrayOutputStream(8192);\n\n        byte[] ab = new byte[1024];\n        int read;\n        while ((read = in.read(ab)) > 0) {\n            out.write(ab, 0, read);\n        }\n\n        return new String(out.toByteArray());\n    }\n\n    private DockerTaskCommon() {\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/docker/src/main/java/com/walmartlabs/concord/plugins/docker/DockerTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.docker;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\nimport static com.walmartlabs.concord.plugins.docker.DockerConstants.SUCCESS_EXIT_CODE;\n\n@Named(\"docker\")\npublic class DockerTaskV2 implements Task {\n\n    private static final Logger log = LoggerFactory.getLogger(DockerTask.class);\n    private static final Logger processLog = LoggerFactory.getLogger(\"processLog\");\n\n    private static final String REDIRECT_ERROR_STREAM_KEY = \"redirectErrorStream\";\n    private static final String LOG_STD_OUT_KEY = \"logOut\";\n    private static final String LOG_STD_ERR_KEY = \"logErr\";\n    private static final String SAVE_STD_OUT_KEY = \"saveOut\";\n    private static final String SAVE_STD_ERR_KEY = \"saveErr\";\n\n    private final Context context;\n    private final WorkingDirectory workDir;\n    private final DockerService dockerService;\n\n    @Inject\n    public DockerTaskV2(Context context, WorkingDirectory workDir, DockerService dockerService) {\n        this.context = context;\n        this.workDir = workDir;\n        this.dockerService = dockerService;\n    }\n\n    @Override\n    public TaskResult execute(Variables input) throws Exception {\n        Path workDir = this.workDir.getValue();\n        TaskParams params = new TaskParams(input);\n\n        boolean logStdOut = input.getBoolean(LOG_STD_OUT_KEY, true);\n        boolean logStdError = input.getBoolean(LOG_STD_ERR_KEY, true);\n        boolean saveStdOut = input.getBoolean(SAVE_STD_OUT_KEY, false);\n        boolean saveStdError = input.getBoolean(SAVE_STD_ERR_KEY, false);\n        boolean redirectErrorStream = input.getBoolean(REDIRECT_ERROR_STREAM_KEY, false);\n\n        String stdOutFilePath = null;\n        if (saveStdOut) {\n            Path logFile = DockerTaskCommon.createTmpFile(workDir, \"stdout\", \".log\");\n            stdOutFilePath = workDir.relativize(logFile).toString();\n        }\n\n        DockerContainerSpec spec = DockerContainerSpec.builder()\n                .image(params.image())\n                .env(DockerTaskCommon.stringify(params.env()))\n                .envFile(DockerTaskCommon.getEnvFile(workDir, params))\n                .entryPoint(DockerTaskCommon.prepareEntryPoint(workDir, params))\n                .forcePull(params.forcePull())\n                .options(DockerContainerSpec.Options.builder().hosts(params.hosts()).build())\n                .debug(params.debug(context.processConfiguration().debug()))\n                .redirectErrorStream(redirectErrorStream)\n                .stdOutFilePath(stdOutFilePath)\n                .pullRetryCount(params.pullRetryCount())\n                .pullRetryInterval(params.pullRetryInterval())\n                .build();\n\n        StringBuilder stdErr = new StringBuilder();\n        int code = dockerService.start(spec,\n                logStdOut ? line -> processLog.info(\"DOCKER: {}\", line) : null,\n                logStdError || saveStdError ? line -> {\n                    if (logStdError) {\n                        processLog.info(\"DOCKER: {}\", line);\n                    }\n                    if (saveStdError) {\n                        stdErr.append(line).append(\"\\n\");\n                    }\n                } : null);\n\n        String stdOut = null;\n        if (stdOutFilePath != null) {\n            InputStream inputStream = Files.newInputStream(Paths.get(stdOutFilePath));\n            stdOut = DockerTaskCommon.toString(inputStream);\n        }\n\n        if (code != SUCCESS_EXIT_CODE) {\n            log.warn(\"call ['{}', '{}', '{}'] -> finished with code {}\", params.image(), params.cmd(), workDir, code);\n            return TaskResult.fail(\"Docker process finished with exit code \" + code)\n                    .value(\"stdout\", stdOut)\n                    .value(\"stderr\", stdErr.toString());\n        }\n\n        log.info(\"call ['{}', '{}', '{}', '{}'] -> done\", params.image(), params.cmd(), workDir, params.hosts());\n        return TaskResult.success()\n                .value(\"stdout\", stdOut)\n                .value(\"stderr\", stdErr.toString());\n    }\n}\n\n"
  },
  {
    "path": "plugins/tasks/docker/src/main/java/com/walmartlabs/concord/plugins/docker/TaskParams.java",
    "content": "package com.walmartlabs.concord.plugins.docker;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport java.util.Collection;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.plugins.docker.DockerConstants.*;\n\npublic class TaskParams {\n\n    private final Variables variables;\n\n    public TaskParams(Map<String, Object> in) {\n        this(new MapBackedVariables(in));\n    }\n\n    public TaskParams(Variables variables) {\n        this.variables = variables;\n    }\n\n    public String image() {\n        return variables.assertString(IMAGE_KEY);\n    }\n\n    public String cmd() {\n        return variables.getString(CMD_KEY, null);\n    }\n\n    public Map<String, Object> env() {\n        return variables.getMap(ENV_KEY, null);\n    }\n\n    public String envFile() {\n        return variables.getString(ENV_FILE_KEY, null);\n    }\n\n    public Collection<String> hosts() {\n        return variables.getCollection(HOSTS_KEY, null);\n    }\n\n    public boolean forcePull() {\n        return variables.getBoolean(FORCE_PULL_KEY, true);\n    }\n\n    public boolean debug(boolean defaultValue) {\n        return variables.getBoolean(DEBUG_KEY, defaultValue);\n    }\n\n    public int pullRetryCount(){\n        return variables.getInt(PULL_RETRY_COUNT_KEY, 3);\n    }\n\n    public long pullRetryInterval() {\n        return variables.getLong(PULL_RETRY_INTERVAL_KEY, 10_000L);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/dynamic-tasks/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>dynamic-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.groovy</groupId>\n            <artifactId>groovy</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.inject</groupId>\n            <artifactId>guice</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <dependencies>\n                    <dependency>\n                        <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n                        <artifactId>concord-runner-v2</artifactId>\n                        <version>${project.version}</version>\n                    </dependency>\n                </dependencies>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/tasks/dynamic-tasks/src/main/java/com/walmartlabs/concord/plugins/dynamic/LoadTasksTask.java",
    "content": "package com.walmartlabs.concord.plugins.dynamic;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.DynamicTaskRegistry;\nimport com.walmartlabs.concord.sdk.*;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\n@Named(\"loadTasks\")\n@SuppressWarnings(\"unused\")\npublic class LoadTasksTask implements Task {\n\n    private final TaskRegistry taskRegistry;\n\n    @InjectVariable(Constants.Context.CONTEXT_KEY)\n    private Context ctx;\n\n    @Inject\n    public LoadTasksTask(DynamicTaskRegistry registry) {\n        this.taskRegistry = registry::register;\n    }\n\n    public void call(String path) throws Exception {\n        Path workDir = Paths.get(ContextUtils.getString(ctx, Constants.Context.WORK_DIR_KEY));\n\n        Path src = workDir.resolve(path);\n        if (!Files.exists(src) || !Files.isDirectory(src)) {\n            throw new RuntimeException(\"Path not found or not a directory: \" + workDir.relativize(src));\n        }\n\n        new TaskLoader(taskRegistry).load(src);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/dynamic-tasks/src/main/java/com/walmartlabs/concord/plugins/dynamic/LoadTasksTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.dynamic;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Injector;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\n@Named(\"loadTasks\")\n@DryRunReady\n@SuppressWarnings(\"unused\")\npublic class LoadTasksTaskV2 implements Task {\n\n    private final Context context;\n    private final TaskRegistry taskRegistry;\n\n    @Inject\n    @SuppressWarnings(\"unchecked\")\n    public LoadTasksTaskV2(Injector injector, Context context) {\n        this.taskRegistry = clazz -> {\n            if (Task.class.isAssignableFrom(clazz)) {\n                injector.getBinding(clazz);\n            } else {\n                throw new RuntimeException(\"Unknown task type: \" + clazz);\n            }\n        };\n        this.context = context;\n    }\n\n    @Override\n    public TaskResult execute(Variables input) throws Exception {\n        String path = input.assertString(\"path\");\n\n        Path src = context.workingDirectory().resolve(path);\n        if (!Files.exists(src) || !Files.isDirectory(src)) {\n            throw new RuntimeException(\"Path not found or not a directory: \" + context.workingDirectory().relativize(src));\n        }\n\n        new TaskLoader(taskRegistry).load(src);\n        return TaskResult.success();\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/dynamic-tasks/src/main/java/com/walmartlabs/concord/plugins/dynamic/TaskLoader.java",
    "content": "package com.walmartlabs.concord.plugins.dynamic;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport groovy.lang.GroovyClassLoader;\n\nimport java.nio.file.FileVisitResult;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.SimpleFileVisitor;\nimport java.nio.file.attribute.BasicFileAttributes;\n\npublic class TaskLoader {\n\n    private final TaskRegistry taskRegistry;\n    private final GroovyClassLoader classLoader;\n\n    public TaskLoader(TaskRegistry taskRegistry) {\n        this.taskRegistry = taskRegistry;\n        this.classLoader = new GroovyClassLoader();\n    }\n\n    @SuppressWarnings(\"rawtypes\")\n    public void load(Path path) throws Exception {\n        Files.walkFileTree(path, new SimpleFileVisitor<Path>() {\n\n            @Override\n            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {\n                try {\n                    Class clazz = classLoader.parseClass(file.toFile());\n                    taskRegistry.register(clazz);\n                } catch (Exception e) {\n                    throw new RuntimeException(\"Error while loading a task: \" + file, e);\n                }\n\n                return FileVisitResult.CONTINUE;\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/dynamic-tasks/src/main/java/com/walmartlabs/concord/plugins/dynamic/TaskRegistry.java",
    "content": "package com.walmartlabs.concord.plugins.dynamic;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface TaskRegistry {\n\n    @SuppressWarnings(\"rawtypes\")\n    void register(Class clazz);\n}\n"
  },
  {
    "path": "plugins/tasks/example/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>example-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <!-- deprecated -->\n        <dependency>\n            <groupId>io.takari.bpm</groupId>\n            <artifactId>bpm-engine-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <dependencies>\n                    <dependency>\n                        <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n                        <artifactId>concord-runner-v2</artifactId>\n                        <version>${project.version}</version>\n                    </dependency>\n                </dependencies>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/tasks/example/src/main/java/com/walmartlabs/concord/plugins/example/ExampleBean.java",
    "content": "package com.walmartlabs.concord.plugins.example;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\n\npublic class ExampleBean implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String value;\n\n    public ExampleBean(String value) {\n        this.value = value;\n    }\n\n    public String getValue() {\n        return value;\n    }\n\n    @Override\n    public String toString() {\n        return \"ExampleBean{\" +\n                \"value='\" + value + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/example/src/main/java/com/walmartlabs/concord/plugins/example/ExampleDelegate.java",
    "content": "package com.walmartlabs.concord.plugins.example;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.Task;\nimport io.takari.bpm.api.ExecutionContext;\nimport io.takari.bpm.api.JavaDelegate;\n\nimport javax.inject.Named;\n\n/**\n * @deprecated use {@link Task#execute(Context)}\n */\n@Named(\"exampleDelegate\")\n@Deprecated\npublic class ExampleDelegate implements Task, JavaDelegate {\n\n    @Override\n    public void execute(ExecutionContext ctx) throws Exception {\n        ctx.setVariable(\"exampleOutput\", \"Hello!\");\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/example/src/main/java/com/walmartlabs/concord/plugins/example/ExampleTask.java",
    "content": "package com.walmartlabs.concord.plugins.example;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.InjectVariable;\nimport com.walmartlabs.concord.sdk.Task;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Named;\n\n@Named(\"example\")\npublic class ExampleTask implements Task {\n\n    private static final Logger log = LoggerFactory.getLogger(ExampleTask.class);\n\n    @InjectVariable(\"context\")\n    private Context context;\n\n    public void hello() {\n        log.info(\"Hello!\");\n    }\n\n    public void hello(String k) {\n        Object o = context.getVariable(k);\n        log.info(\"Hello, {}!\", o);\n    }\n\n    public void helloButLouder(@InjectVariable(\"myName\") String name) {\n        log.info(\"Hello, {}!!!\", name);\n    }\n\n    @Override\n    public void execute(Context ctx) throws Exception {\n        log.info(\"Hello, {}. (from method param)\", ctx.getVariable(\"myName\"));\n        log.info(\"Hello, {}. (from injected var)\", context.getVariable(\"myName\"));\n        ctx.setVariable(\"exampleOutput\", \"Hello!\");\n    }\n\n    public void call(String a, String b) {\n        log.info(\"We got {} and {}\", a, b);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/files/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>file-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <dependencies>\n                    <dependency>\n                        <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n                        <artifactId>concord-runner-v2</artifactId>\n                        <version>${project.version}</version>\n                    </dependency>\n                </dependencies>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/tasks/files/src/main/java/com/walmartlabs/concord/plugins/file/v2/FilesTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.file.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.DryRunReady;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\n\n@Named(\"files\")\n@DryRunReady\n@SuppressWarnings(\"unused\")\npublic class FilesTaskV2 implements Task {\n\n    private final Path workDir;\n\n    @Inject\n    public FilesTaskV2(Context context) {\n        this.workDir = context.workingDirectory();\n    }\n\n    public boolean exists(String path) {\n        return Files.exists(assertPath(workDir, path));\n    }\n\n    public boolean notExists(String path) {\n        return Files.notExists(assertPath(workDir, path));\n    }\n\n    public String moveFile(String source, String targetDir) throws IOException {\n        Path src = assertPath(workDir, source);\n        Path dest = workDir.resolve(targetDir);\n\n        Files.createDirectories(dest);\n\n        Path destFileName = dest.resolve(src.getFileName());\n        Files.move(src, destFileName, StandardCopyOption.REPLACE_EXISTING);\n\n        return workDir.relativize(destFileName).toString();\n    }\n\n    public String relativize(String src, String other) {\n        return workDir.resolve(src).relativize(workDir.resolve(other)).toString();\n    }\n\n    private static Path assertPath(Path workDir, String path) {\n        Path result = workDir.resolve(path).normalize().toAbsolutePath();\n        if (!result.startsWith(workDir)) {\n            throw new IllegalArgumentException(\"The path must be within the working directory: \" + path);\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/http/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>http-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.jboss.spec.javax.ws.rs</groupId>\n            <artifactId>jboss-jaxrs-api_2.1_spec</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.httpcomponents</groupId>\n            <artifactId>httpclient</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.httpcomponents</groupId>\n            <artifactId>httpcore</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.httpcomponents</groupId>\n            <artifactId>httpmime</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.wiremock</groupId>\n            <artifactId>wiremock-jetty12</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-core</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-junit-jupiter</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>ch.qos.logback</groupId>\n            <artifactId>logback-classic</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <resources>\n            <resource>\n                <filtering>false</filtering>\n                <directory>${project.basedir}/src/main/resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n            <resource>\n                <filtering>true</filtering>\n                <directory>${project.basedir}/src/main/filtered-resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n        </resources>\n\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <dependencies>\n                    <dependency>\n                        <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n                        <artifactId>concord-runner-v2</artifactId>\n                        <version>${project.version}</version>\n                    </dependency>\n                </dependencies>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n"
  },
  {
    "path": "plugins/tasks/http/src/main/filtered-resources/com/walmartlabs/concord/plugins/http/version.properties",
    "content": "version=${project.version}\n"
  },
  {
    "path": "plugins/tasks/http/src/main/java/com/walmartlabs/concord/plugins/http/Configuration.java",
    "content": "package com.walmartlabs.concord.plugins.http;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.plugins.http.HttpTask.RequestType;\nimport com.walmartlabs.concord.plugins.http.HttpTask.ResponseType;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport org.apache.http.client.utils.URIBuilder;\n\nimport java.util.*;\n\nimport static com.walmartlabs.concord.plugins.http.HttpTask.HttpTaskConstant.*;\nimport static com.walmartlabs.concord.plugins.http.HttpTask.RequestMethodType;\nimport static javax.xml.transform.OutputKeys.METHOD;\n\n/**\n * Configuration for {@link SimpleHttpClient}\n *\n * @see SimpleHttpClient\n */\npublic class Configuration {\n\n    private final String url;\n    private final String encodedAuthToken;\n    private final RequestType requestType;\n    private final ResponseType responseType;\n    private final String workDir;\n    private final RequestMethodType methodType;\n    private final Map<String, String> requestHeaders;\n    private final Object body;\n    private final int connectTimeout;\n    private final int socketTimeout;\n    private final int requestTimeout;\n    private final boolean ignoreErrors;\n    private final String proxy;\n    private final String proxyUser;\n    private final char[] proxyPassword;\n    private final boolean debug;\n    private final boolean followRedirects;\n    private final String keyStorePath;\n    private final String keyStorePassword;\n    private final boolean strictSsl;\n    private final String trustStorePath;\n    private final String trustStorePassword;\n\n\n    private Configuration(RequestMethodType methodType,\n                          String url,\n                          String authToken,\n                          RequestType requestType,\n                          ResponseType responseType,\n                          String workDir,\n                          Map<String, String> requestHeaders,\n                          Object body,\n                          int connectTimeout,\n                          int socketTimeout,\n                          int requestTimeout,\n                          boolean ignoreErrors,\n                          String proxy,\n                          String proxyUser,\n                          char[] proxyPassword,\n                          boolean debug,\n                          boolean followRedirects,\n                          String keyStorePath,\n                          String keyStorePassword,\n                          boolean strictSsl,\n                          String trustStorePath,\n                          String trustStorePassword) {\n\n        this.methodType = methodType;\n        this.url = url;\n        this.encodedAuthToken = authToken;\n        this.requestType = requestType;\n        this.responseType = responseType;\n        this.workDir = workDir;\n        this.requestHeaders = requestHeaders;\n        this.body = body;\n        this.connectTimeout = connectTimeout;\n        this.socketTimeout = socketTimeout;\n        this.requestTimeout = requestTimeout;\n        this.ignoreErrors = ignoreErrors;\n        this.proxy = proxy;\n        this.proxyUser = proxyUser;\n        this.proxyPassword = proxyPassword;\n        this.debug = debug;\n        this.followRedirects = followRedirects;\n        this.keyStorePath = keyStorePath;\n        this.keyStorePassword = keyStorePassword;\n        this.strictSsl = strictSsl;\n        this.trustStorePath = trustStorePath;\n        this.trustStorePassword = trustStorePassword;\n    }\n\n    /**\n     * Method to get a new instance of builder\n     *\n     * @return new instance of builder\n     */\n    public static Builder custom() {\n        return new Builder();\n    }\n\n    /**\n     * Method to get the method type\n     *\n     * @return RequestMethodType methodType\n     */\n    public RequestMethodType getMethodType() {\n        return methodType;\n    }\n\n    /**\n     * Method to get the encoded auth token\n     *\n     * @return encoded auth token\n     */\n    public String getEncodedAuthToken() {\n        return encodedAuthToken;\n    }\n\n    /**\n     * Method to get the url\n     *\n     * @return url\n     */\n    public String getUrl() {\n        return url;\n    }\n\n    /**\n     * Method to get the body\n     *\n     * @return body {@link Object}\n     */\n    public Object getBody() {\n        return body;\n    }\n\n    /**\n     * Method to get the request type\n     *\n     * @return RequestType requestType\n     */\n    @SuppressWarnings(\"unused\")\n    public RequestType getRequestType() {\n        return requestType;\n    }\n\n    /**\n     * Method to get the response type\n     *\n     * @return ResponseType responseType\n     */\n    public ResponseType getResponseType() {\n        return responseType;\n    }\n\n    /**\n     * Method to get the working directory\n     *\n     * @return working directory\n     */\n    public String getWorkDir() {\n        return workDir;\n    }\n\n    public Map<String, String> getRequestHeaders() {\n        return requestHeaders;\n    }\n\n    public int getConnectTimeout() {\n        return connectTimeout;\n    }\n\n    public int getSocketTimeout() {\n        return socketTimeout;\n    }\n\n    public int getRequestTimeout() {\n        return requestTimeout;\n    }\n\n    public boolean isIgnoreErrors() {\n        return ignoreErrors;\n    }\n\n    public String getProxy() {\n        return proxy;\n    }\n\n    public String getProxyUser() {\n        return proxyUser;\n    }\n\n    public char[] getProxyPassword() {\n        return proxyPassword;\n    }\n\n    public boolean isDebug() {\n        return debug;\n    }\n\n    public boolean isFollowRedirects() {\n        return followRedirects;\n    }\n\n    public String keyStorePath() {\n        return keyStorePath;\n    }\n\n    public String keyStorePassword() {\n        return keyStorePassword;\n    }\n\n    public boolean isStrictSsl() {\n        return strictSsl;\n    }\n\n    public String trustStorePath() {\n        return trustStorePath;\n    }\n\n    public String trustStorePassword() {\n        return trustStorePassword;\n    }\n\n    public static class Builder {\n\n        private String url;\n        private String encodedAuthToken;\n        private RequestType requestType;\n        private ResponseType responseType;\n        private String workDir;\n        private RequestMethodType methodType = RequestMethodType.GET;\n        private Map<String, String> requestHeaders;\n        private Object body;\n        private Integer connectTimeout = 30000;\n        private Integer socketTimeout = -1;\n        private Integer requestTimeout = 0;\n        private boolean ignoreErrors;\n        private String proxy;\n        private String proxyUser;\n        private char[] proxyPassword;\n        private boolean debug;\n        private boolean followRedirects = true;\n        private String keyStorePath;\n        private String keyStorePassword;\n        private boolean strictSsl = false;\n        private String trustStorePath;\n        private String trustStorePassword;\n\n        /**\n         * Used to specify the url which will later use to create {@link org.apache.http.client.methods.HttpUriRequest}\n         *\n         * @param url url\n         * @return instance of this {@link Builder}\n         */\n        public Builder withUrl(String url) {\n            this.url = url;\n            return this;\n        }\n\n        /**\n         * Used to specify the method type\n         *\n         * @param methodType Http request methods\n         * @return insance of this {@link Builder}\n         */\n        public Builder withMethodType(RequestMethodType methodType) {\n            this.methodType = methodType;\n            return this;\n        }\n\n        /**\n         * Used to specify the encoded authentication token which later use in the Authorization\n         * Header\n         *\n         * @param encodedAuthToken Base64 encoded string\n         * @return instance of this {@link Builder}\n         */\n        public Builder withEncodedAuthToken(String encodedAuthToken) {\n            this.encodedAuthToken = encodedAuthToken;\n            return this;\n        }\n\n        /**\n         * Used to specify the request type which later maps to Content-Type header of the request.\n         *\n         * @param requestType {@link RequestType} type of request (file, json, string)\n         * @return instance of this {@link Builder}\n         */\n        public Builder withRequestType(RequestType requestType) {\n            this.requestType = requestType;\n            return this;\n        }\n\n        /**\n         * Used to specify the response type, which later use to parse the response from endpoint.\n         *\n         * @param responseType {@link ResponseType} type of the response (file, json, string)\n         * @return instance of this {@link Builder}\n         */\n        public Builder withResponseType(ResponseType responseType) {\n            this.responseType = responseType;\n            return this;\n        }\n\n        /**\n         * Used to specify the working directory. This will be used to store the http response in temporary file\n         *\n         * @param workDir current working directory\n         * @return instance of this {@link Builder}\n         */\n        public Builder withWorkingDirectory(String workDir) {\n            this.workDir = workDir;\n            return this;\n        }\n\n        /**\n         * Used to specify the body\n         *\n         * @param body complex(map), raw body or relative path\n         * @return instance of this {@link Builder}\n         */\n        public Builder withBody(Object body) {\n            this.body = body;\n            return this;\n        }\n\n        /**\n         * Used to specify the connection timeout (in ms).\n         * A timeout value of zero is interpreted as an infinite timeout.\n         * A negative value is interpreted as undefined (system default).\n         * <p>\n         * Default value is {@code 30000}\n         * </p>\n         *\n         * @param connectTimeout\n         * @return instance of this {@link Builder}\n         */\n        public Builder withConnectTimeout(int connectTimeout) {\n            this.connectTimeout = connectTimeout;\n            return this;\n        }\n\n        /**\n         * Used to specify the socket timeout (in ms).\n         * A timeout value of zero is interpreted as an infinite timeout.\n         * A negative value is interpreted as undefined (system default).\n         * <p>\n         * Default value is {@code -1}\n         * </p>\n         *\n         * @param socketTimeout\n         * @return instance of this {@link Builder}\n         */\n        public Builder withSocketTimeout(int socketTimeout) {\n            this.socketTimeout = socketTimeout;\n            return this;\n        }\n\n        /**\n         * Used to specify the request timeout (in ms).\n         * A timeout value of zero is interpreted as an infinite timeout.\n         *\n         * @param requestTimeout\n         * @return instance of this {@link Builder}\n         */\n        public Builder withRequestTimeout(int requestTimeout) {\n            this.requestTimeout = requestTimeout;\n            return this;\n        }\n\n        /**\n         * Used to ignore the errors produced by the http task in flow\n         *\n         * @param ignoreErrors\n         * @return instance of this {@link Builder}\n         */\n        public Builder withIgnoreErrors(boolean ignoreErrors) {\n            this.ignoreErrors = ignoreErrors;\n            return this;\n        }\n\n        public Builder withProxy(String proxy) {\n            this.proxy = proxy;\n            return this;\n        }\n\n        public Builder withDebug(boolean debug) {\n            this.debug = debug;\n            return this;\n        }\n\n        public Builder withFollowRedirects(boolean followRedirects) {\n            this.followRedirects = followRedirects;\n            return this;\n        }\n\n        public Builder withKeyStore(String path, String password) {\n            this.keyStorePath = path;\n            this.keyStorePassword = password;\n            return this;\n        }\n\n        public Builder setStrictSsl(boolean strictSsl) {\n            this.strictSsl = strictSsl;\n            return this;\n        }\n\n        public Builder withTrustStore(String path, String password) {\n            this.trustStorePath = path;\n            this.trustStorePassword = password;\n            return this;\n        }\n\n        /**\n         * Invoking this method will result in a new configuration\n         *\n         * @return new instance of this {@link Configuration}\n         */\n        public Configuration build() {\n            if (this.url == null || this.url.isEmpty()) {\n                throw new IllegalArgumentException(\"URL is missing\");\n            } else if (responseType == ResponseType.FILE && (workDir == null || workDir.isEmpty())) {\n                throw new IllegalArgumentException(\"Working directory is mandatory for ResponseType FILE\");\n            } else if (this.methodType == RequestMethodType.POST && (this.body == null)) {\n                throw new IllegalArgumentException(\"Body is missing for Post method\");\n            } else if (this.methodType == RequestMethodType.PUT && (this.body == null)) {\n                throw new IllegalArgumentException(\"Body is missing for Put method\");\n            }\n\n            return new Configuration(methodType, url, encodedAuthToken, requestType, responseType, workDir,\n                    requestHeaders, body, connectTimeout, socketTimeout, requestTimeout, ignoreErrors,\n                    proxy, proxyUser, proxyPassword, debug, followRedirects,\n                    keyStorePath, keyStorePassword, strictSsl, trustStorePath, trustStorePassword);\n        }\n\n        /**\n         * Invoking this method will result in a new configuration.\n         *\n         * @param ctx context use to build the configuration\n         * @return new instance of this {@link Configuration}\n         * @throws Exception\n         */\n        public Configuration build(Context ctx) throws Exception {\n            String workDir = (String) ctx.getVariable(Constants.Context.WORK_DIR_KEY);\n\n            Map<String, Object> input = new HashMap<>(ALL_KEYS.length);\n            for (String k : ALL_KEYS) {\n                Object v = ctx.getVariable(k);\n                if (v != null) {\n                    input.put(k, v);\n                }\n            }\n\n            return build(workDir, input, false);\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        public Configuration build(String workDir, Map<String, Object> input, boolean globalDebug) throws Exception {\n            validateMandatory(input);\n\n            this.url = MapUtils.getString(input, URL_KEY);\n\n            // query params are optional\n            Map<String, Object> queryParams = MapUtils.getMap(input, QUERY_KEY, null);\n            if (queryParams != null) {\n                URIBuilder uriBuilder = new URIBuilder(url);\n                queryParams.forEach((k, v) -> {\n                    if (v instanceof Collection) {\n                        ((Collection<Object>) v).forEach(item -> uriBuilder.addParameter(k, item.toString()));\n                    } else {\n                        uriBuilder.setParameter(k, v.toString());\n                    }\n                });\n\n                this.url = uriBuilder.build().toURL().toString();\n            }\n\n            // method param is optional\n            String method = MapUtils.getString(input, METHOD_KEY, null);\n            if (method != null) {\n                if (RequestMethodType.isMember(method)) {\n                    this.methodType = RequestMethodType.valueOf(method.toUpperCase());\n                } else {\n                    throw new IllegalArgumentException(\"'\" + METHOD_KEY + \": \" + method + \"' is not a supported HTTP method\");\n                }\n            }\n\n            // auth param is optional\n            Map<String, Object> authParams = MapUtils.getMap(input, AUTH_KEY, null);\n            if (authParams != null) {\n                this.encodedAuthToken = HttpTaskUtils.getBasicAuthorization(MapUtils.assertMap(authParams, BASIC_KEY));\n            }\n\n            // request param is optional\n            String request = MapUtils.getString(input, REQUEST_KEY);\n            if (request != null) {\n                if (RequestType.isMember(request)) {\n                    this.requestType = RequestType.valueOf(request.toUpperCase());\n                } else {\n                    throw new IllegalArgumentException(\"'\" + REQUEST_KEY + \": \" + request + \"' is not a supported request type\");\n                }\n            }\n\n            // response param is optional\n            String response = MapUtils.getString(input, RESPONSE_KEY);\n            if (response != null) {\n                if (ResponseType.isMember(response)) {\n                    this.responseType = ResponseType.valueOf(response.toUpperCase());\n                } else {\n                    throw new IllegalArgumentException(\"'\" + RESPONSE_KEY + \": \" + response + \"' is not a supported response type\");\n                }\n            }\n\n            if (responseType == ResponseType.FILE && (workDir == null || workDir.isEmpty())) {\n                throw new IllegalArgumentException(\"Working directory is mandatory for ResponseType FILE\");\n            }\n\n            this.requestHeaders = MapUtils.getMap(input, HEADERS_KEY, null);\n\n            this.body = input.get(BODY_KEY);\n\n            this.ignoreErrors = MapUtils.getBoolean(input, IGNORE_ERRORS_KEY, false);\n\n            this.connectTimeout = MapUtils.getInt(input, CONNECT_TIMEOUT_KEY, 0);\n\n            this.socketTimeout = MapUtils.getInt(input, SOCKET_TIMEOUT_KEY, 0);\n\n            this.requestTimeout = MapUtils.getInt(input, REQUEST_TIMEOUT_KEY, 0);\n\n            this.proxy = MapUtils.getString(input, PROXY_KEY);\n            Map<String, Object> proxyAuth = MapUtils.getMap(input, PROXY_AUTH_KEY, Collections.emptyMap());\n            this.proxyUser = MapUtils.getString(proxyAuth, PROXY_USER_KEY);\n            this.proxyPassword = Optional.ofNullable(MapUtils.getString(proxyAuth, PROXY_PASSWORD_KEY))\n                    .map(String::toCharArray)\n                    .orElse(null);\n\n            this.debug = MapUtils.getBoolean(input, DEBUG_KEY, globalDebug);\n\n            this.followRedirects = MapUtils.getBoolean(input, FOLLOW_REDIRECTS_KEY, true);\n\n            this.keyStorePath = MapUtils.getString(input, KEYSTORE_PATH);\n            this.keyStorePassword = MapUtils.getString(input, KEYSTORE_PASSWD);\n\n            this.strictSsl = MapUtils.getBoolean(input, STRICT_SSL, false);\n\n            this.trustStorePath = MapUtils.getString(input, TRUSTSTORE_PATH);\n            this.trustStorePassword = MapUtils.getString(input, TRUSTSTORE_PASSWD);\n\n            return new Configuration(methodType, url, encodedAuthToken, requestType, responseType, workDir,\n                    requestHeaders, body, connectTimeout, socketTimeout, requestTimeout, ignoreErrors,\n                    proxy, proxyUser, proxyPassword, debug, followRedirects,\n                    keyStorePath, keyStorePassword, strictSsl, trustStorePath, trustStorePassword);\n        }\n\n        private static void validateMandatory(Map<String, Object> m) {\n            if (m.get(URL_KEY) == null) {\n                throw new IllegalArgumentException(\"('\" + URL_KEY + \"') argument is missing\");\n            } else if (POST_METHOD.equals(m.get(METHOD_KEY)) && m.get(REQUEST_KEY) == null) {\n                throw new IllegalArgumentException(\"('\" + REQUEST_KEY + \"') argument is missing for ('\" + POST_METHOD + \"') method\");\n            } else if (POST_METHOD.equals(m.get(METHOD)) && m.get(BODY_KEY) == null) {\n                throw new IllegalArgumentException(\"('\" + BODY_KEY + \"') argument is missing for ('\" + POST_METHOD + \"') method\");\n            } else if (PUT_METHOD.equals(m.get(METHOD_KEY)) && m.get(REQUEST_KEY) == null) {\n                throw new IllegalArgumentException(\"('\" + REQUEST_KEY + \"') argument is missing for ('\" + PUT_METHOD + \"') method\");\n            } else if (PUT_METHOD.equals(m.get(METHOD)) && m.get(BODY_KEY) == null) {\n                throw new IllegalArgumentException(\"('\" + BODY_KEY + \"') argument is missing for ('\" + PUT_METHOD + \"') method\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/http/src/main/java/com/walmartlabs/concord/plugins/http/HttpTask.java",
    "content": "package com.walmartlabs.concord.plugins.http;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.Task;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Named;\nimport java.io.IOException;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.plugins.http.HttpTask.HttpTaskConstant.*;\n\n/**\n * Http task to support the direct http calls from concord.yml file. It uses the Apache HttpClient to\n * call the restful endpoints. This task is capable of storing the response in temporary file and returning the\n * response in string or JSON format.\n */\n@Named(\"http\")\npublic class HttpTask implements Task {\n\n    private static final Logger log = LoggerFactory.getLogger(HttpTask.class);\n\n    private static final String DEFAULT_OUT_VAR = \"response\";\n\n    @Override\n    public void execute(Context ctx) throws Exception {\n        Configuration config = Configuration.custom().build(ctx);\n\n        setOutVariable(ctx, executeRequest(config));\n    }\n\n    /**\n     * Method to get the resource from the given URL as a String.\n     *\n     * @param url resource URL\n     * @return Response as {@link String}\n     * @throws IOException exception\n     */\n    public String asString(String url) throws Exception {\n        Configuration config = Configuration.custom().withUrl(url).build();\n\n        return (String) executeRequest(config).get(CONTENT_KEY);\n    }\n\n    /**\n     * Method to execute the request from the given configuration\n     *\n     * @param config {@link Configuration}\n     * @return Map\n     * @throws Exception exception\n     */\n    Map<String, Object> executeRequest(Configuration config) throws Exception {\n        log.info(\"Request method: {}\", config.getMethodType());\n\n        Map<String, Object> response = SimpleHttpClient.create(config, false).execute().getResponse();\n        log.info(\"Response status code: {}\", response.get(STATUS_CODE_PARAM));\n        log.info(\"Success response: {}\", response.get(SUCCESS_PARAM));\n\n        return response;\n    }\n\n    /**\n     * Method to set the response in the output variable\n     *\n     * @param ctx            {@link Context}\n     * @param returnResponse response returned from endpoint\n     */\n    private void setOutVariable(Context ctx, Map<String, Object> returnResponse) {\n        String key = (String) ctx.getVariable(OUT_KEY);\n        if (key == null) {\n            key = DEFAULT_OUT_VAR;\n        }\n        ctx.setVariable(key, returnResponse);\n    }\n\n    public enum RequestMethodType {\n        DELETE,\n        GET,\n        POST,\n        PUT,\n        PATCH;\n\n        public static boolean isMember(String name) {\n            for (RequestMethodType t : values()) {\n                if (t.name().equalsIgnoreCase(name)) {\n                    return true;\n                }\n            }\n            return false;\n        }\n    }\n\n    public enum RequestType {\n        JSON,\n        FILE,\n        STRING,\n        FORM,\n        FORMDATA;\n\n        public static boolean isMember(String aName) {\n            RequestType[] requestTypes = RequestType.values();\n            for (RequestType requestType : requestTypes) {\n                if (requestType.name().equalsIgnoreCase(aName)) {\n                    return true;\n                }\n            }\n            return false;\n        }\n    }\n\n    public enum ResponseType {\n        JSON,\n        FILE,\n        STRING,\n        ANY;\n\n        public static boolean isMember(String aName) {\n            ResponseType[] responseTypes = ResponseType.values();\n            for (ResponseType responseType : responseTypes) {\n                if (responseType.name().equalsIgnoreCase(aName)) {\n                    return true;\n                }\n            }\n            return false;\n        }\n    }\n\n    /**\n     * Http Task Constant Class.\n     */\n    static final class HttpTaskConstant {\n\n        // input parameters\n        static final String AUTH_KEY = \"auth\";\n        static final String BASIC_KEY = \"basic\";\n        static final String BODY_KEY = \"body\";\n        static final String CONNECT_TIMEOUT_KEY = \"connectTimeout\";\n        static final String CONTENT_KEY = \"content\";\n        static final String DEBUG_KEY = \"debug\";\n        static final String FOLLOW_REDIRECTS_KEY = \"followRedirects\";\n        static final String HEADERS_KEY = \"headers\";\n        static final String IGNORE_ERRORS_KEY = \"ignoreErrors\";\n        static final String METHOD_KEY = \"method\";\n        static final String OUT_KEY = \"out\";\n        static final String PASSWORD_KEY = \"password\"; // NOSONAR\n        static final String PROXY_KEY = \"proxy\";\n        static final String PROXY_AUTH_KEY = \"proxyAuth\";\n        static final String PROXY_USER_KEY = \"user\";\n        static final String PROXY_PASSWORD_KEY = \"password\";\n        static final String QUERY_KEY = \"query\";\n        static final String REQUEST_KEY = \"request\";\n        static final String REQUEST_TIMEOUT_KEY = \"requestTimeout\";\n        static final String RESPONSE_KEY = \"response\";\n        static final String SOCKET_TIMEOUT_KEY = \"socketTimeout\";\n        static final String TOKEN_KEY = \"token\";\n        static final String URL_KEY = \"url\";\n        static final String USERNAME_KEY = \"username\";\n        static final String KEYSTORE_PATH = \"keystorePath\";\n        static final String KEYSTORE_PASSWD = \"keystorePassword\";\n        static final String STRICT_SSL = \"strictSsl\";\n        static final String TRUSTSTORE_PATH = \"truststorePath\";\n        static final String TRUSTSTORE_PASSWD = \"truststorePassword\";\n\n        static final String[] ALL_KEYS = {\n                AUTH_KEY,\n                BASIC_KEY,\n                BODY_KEY,\n                CONNECT_TIMEOUT_KEY,\n                CONTENT_KEY,\n                DEBUG_KEY,\n                FOLLOW_REDIRECTS_KEY,\n                HEADERS_KEY,\n                IGNORE_ERRORS_KEY,\n                METHOD_KEY,\n                OUT_KEY,\n                PASSWORD_KEY,\n                PROXY_KEY,\n                PROXY_AUTH_KEY,\n                QUERY_KEY,\n                REQUEST_KEY,\n                REQUEST_TIMEOUT_KEY,\n                RESPONSE_KEY,\n                SOCKET_TIMEOUT_KEY,\n                TOKEN_KEY,\n                URL_KEY,\n                USERNAME_KEY,\n                KEYSTORE_PATH,\n                KEYSTORE_PASSWD,\n                STRICT_SSL,\n                TRUSTSTORE_PATH,\n                TRUSTSTORE_PASSWD\n        };\n\n        // internal constants\n        static final String POST_METHOD = \"POST\";\n        static final String PUT_METHOD = \"PUT\";\n\n        static final String SUCCESS_PARAM = \"success\";\n        static final String STATUS_CODE_PARAM = \"statusCode\";\n\n        private HttpTaskConstant() {\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/http/src/main/java/com/walmartlabs/concord/plugins/http/HttpTaskUtils.java",
    "content": "package com.walmartlabs.concord.plugins.http;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.plugins.http.HttpTask.RequestType;\nimport org.apache.http.HttpEntity;\nimport org.apache.http.NameValuePair;\nimport org.apache.http.client.entity.UrlEncodedFormEntity;\nimport org.apache.http.entity.ContentType;\nimport org.apache.http.entity.FileEntity;\nimport org.apache.http.entity.StringEntity;\nimport org.apache.http.entity.mime.MultipartEntityBuilder;\nimport org.apache.http.entity.mime.content.AbstractContentBody;\nimport org.apache.http.entity.mime.content.FileBody;\nimport org.apache.http.entity.mime.content.StringBody;\nimport org.apache.http.message.BasicNameValuePair;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.File;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.plugins.http.HttpTask.HttpTaskConstant.*;\n\n/**\n * Utility class which contains helper methods for {@link HttpTask}\n */\npublic final class HttpTaskUtils {\n\n    private static final Logger log = LoggerFactory.getLogger(HttpTaskUtils.class);\n\n    /**\n     * Method to get the basic authorization header entry using the basic authorization params provided in the concord.yml\n     * file. It will use either the token key or the username and password key to generate the header entry. If token key\n     * is provided then it must be in Base64 encoded string just like curl\n     *\n     * @param basicAuthParams use to generate the basic authorization header entry\n     * @return String (\"Basic BASE64_TOKEN\")\n     */\n    static String getBasicAuthorization(Map<String, String> basicAuthParams) {\n        if (basicAuthParams.get(TOKEN_KEY) != null) {\n            String token = String.valueOf(basicAuthParams.get(TOKEN_KEY));\n            try {\n                Base64.getDecoder().decode(token);\n            } catch (IllegalArgumentException e) {\n                throw new RuntimeException(\"Invalid auth token value. Expected a base64 encoded string, got: \" + token);\n            }\n            return token;\n        } else {\n            return getBasicAuthorization(basicAuthParams.get(USERNAME_KEY), basicAuthParams.get(PASSWORD_KEY));\n        }\n    }\n\n    /**\n     * Method to encode the given username and password\n     *\n     * @param username username\n     * @param password password\n     * @return Base64 encoded {@link String}\n     */\n    private static String getBasicAuthorization(String username, String password) {\n        return Base64.getEncoder().encodeToString((username + \":\" + password).getBytes());\n    }\n\n    /**\n     * Method to get the HttpEntity using the request type.\n     *\n     * @param body        Contain complex object or relative path as string\n     * @param requestType type of request\n     * @return FileEntity for RequestType.FILE with ContentType.APPLICATION_OCTET_STREAM otherwise return {@link StringEntity}\n     * @throws Exception exception\n     */\n    @SuppressWarnings(\"unchecked\")\n    static HttpEntity getHttpEntity(Object body, RequestType requestType) throws Exception {\n        if ((RequestType.FILE == requestType) && (body instanceof String)) {\n            String filePath = (String) body;\n            File newFile = new File(filePath);\n\n            if (!newFile.exists()) {\n                throw new FileNotFoundException(\"File: \" + filePath + \" not found\");\n            }\n\n            return new FileEntity(newFile, ContentType.APPLICATION_OCTET_STREAM);\n\n        } else if ((RequestType.FORM == requestType) && (body instanceof Map)) {\n            List<NameValuePair> params = new ArrayList<>();\n\n            Map<String, Object> bodyParams = (Map<String, Object>) body;\n            bodyParams.forEach((k, v) -> {\n                if (v instanceof Collection) {\n                    ((Collection<Object>) v).forEach(item -> params.add(new BasicNameValuePair(k, item.toString())));\n                } else {\n                    params.add(new BasicNameValuePair(k, v.toString()));\n                }\n            });\n\n            return new UrlEncodedFormEntity(params);\n        } else if ((RequestType.FORMDATA == requestType) && body instanceof Map) {\n            return createMultipartBody((Map<String, Object>) body);\n        } else if ((RequestType.JSON == requestType)) {\n            if (body instanceof String) {\n                String strBody = (String) body;\n                if (isValidJSON(strBody)) {\n                    return new StringEntity(strBody, StandardCharsets.UTF_8);\n                }\n            } else {\n                return new StringEntity(new ObjectMapper().writeValueAsString(body), StandardCharsets.UTF_8);\n            }\n        } else if ((RequestType.STRING == requestType) && (body instanceof String)) {\n            return new StringEntity((String) body, StandardCharsets.UTF_8);\n        }\n\n        throw new IllegalArgumentException(\"'\" + REQUEST_KEY + \": \" + requestType.toString().toLowerCase() + \"' is not compatible with '\" + BODY_KEY + \"'\");\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static HttpEntity createMultipartBody(Map<String, Object> body) {\n        MultipartEntityBuilder builder = MultipartEntityBuilder.create();\n\n        for (Map.Entry<String, Object> e : body.entrySet()) {\n            String k = e.getKey();\n            Object v = e.getValue();\n\n            if (v instanceof String value) {\n                if (value.startsWith(\"@\")) {\n                    builder.addPart(k, createContentBody(value, ContentType.APPLICATION_OCTET_STREAM));\n                } else {\n                    builder.addPart(k, createContentBody(value, ContentType.TEXT_PLAIN));\n                }\n            } else if (v instanceof Map) {\n                Map<String, String> field = (Map<String, String>) v;\n                String type = field.get(\"type\");\n                String data = field.get(\"data\");\n\n                if (data == null) {\n                    throw new IllegalArgumentException(\"field -> \" + k + \" missing request data\");\n                }\n\n                builder.addPart(k, createContentBody(data, ContentType.parse(type)));\n            } else if (v instanceof Number) {\n                builder.addPart(k, createContentBody(String.valueOf(v), ContentType.TEXT_PLAIN));\n            } else {\n                log.warn(\"Skipping value for key '{}': unsupported type {}. Expected String, Number, or Map<String, String>.\", k, v == null ? \"null\" : v.getClass().getSimpleName());\n            }\n        }\n\n        return builder.build();\n\n    }\n\n    private static AbstractContentBody createContentBody(String value, ContentType type) {\n        if (ContentType.APPLICATION_OCTET_STREAM.getMimeType().equals(type.getMimeType())) {\n            String filePath = value.substring(1);\n            File file = new File(filePath);\n            if (!file.exists()) {\n                throw new IllegalArgumentException(\"file -> \" + filePath + \" does not exists\");\n            }\n\n            return new FileBody(file);\n        }\n\n        return new StringBody(value, type);\n    }\n\n    /**\n     * Method to validate the json string\n     *\n     * @param body json string\n     * @return true if json is valid\n     */\n    private static boolean isValidJSON(String body) {\n        ObjectMapper om = new ObjectMapper();\n        try {\n            om.readTree(body);\n        } catch (IOException e) {\n            log.error(\"invalid json body '{}': {}\", body, e.getMessage());\n            return false;\n        }\n\n        return true;\n    }\n\n    private HttpTaskUtils() {\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/http/src/main/java/com/walmartlabs/concord/plugins/http/HttpTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.http;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.util.HashMap;\n\n@Named(\"http\")\n@DryRunReady\npublic class HttpTaskV2 implements Task {\n\n    private final Context context;\n\n    @Inject\n    public HttpTaskV2(Context context) {\n        this.context = context;\n    }\n\n    @Override\n    public TaskResult execute(Variables input) throws Exception {\n        var inputMap = new HashMap<>(context.defaultVariables().toMap());\n        inputMap.putAll(input.toMap());\n\n        var config = Configuration.custom().build(context.workingDirectory().toString(), inputMap, context.processConfiguration().debug());\n\n        var response = new HashMap<>(SimpleHttpClient.create(config, context.processConfiguration().dryRun()).execute().getResponse());\n        return TaskResult.of((boolean)response.remove(\"success\"), (String)response.remove(\"errorString\"), response);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/http/src/main/java/com/walmartlabs/concord/plugins/http/SimpleHttpClient.java",
    "content": "package com.walmartlabs.concord.plugins.http;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.plugins.http.HttpTask.RequestType;\nimport com.walmartlabs.concord.plugins.http.exception.RequestTimeoutException;\nimport com.walmartlabs.concord.plugins.http.exception.UnauthorizedException;\nimport com.walmartlabs.concord.plugins.http.request.HttpTaskRequest;\nimport org.apache.http.*;\nimport org.apache.http.auth.AuthScope;\nimport org.apache.http.auth.UsernamePasswordCredentials;\nimport org.apache.http.client.CredentialsProvider;\nimport org.apache.http.client.config.RequestConfig;\nimport org.apache.http.client.methods.CloseableHttpResponse;\nimport org.apache.http.client.methods.HttpGet;\nimport org.apache.http.client.methods.HttpUriRequest;\nimport org.apache.http.config.Registry;\nimport org.apache.http.config.RegistryBuilder;\nimport org.apache.http.conn.HttpClientConnectionManager;\nimport org.apache.http.conn.socket.ConnectionSocketFactory;\nimport org.apache.http.conn.socket.PlainConnectionSocketFactory;\nimport org.apache.http.conn.ssl.DefaultHostnameVerifier;\nimport org.apache.http.conn.ssl.NoopHostnameVerifier;\nimport org.apache.http.conn.ssl.SSLConnectionSocketFactory;\nimport org.apache.http.conn.ssl.TrustAllStrategy;\nimport org.apache.http.impl.client.BasicCredentialsProvider;\nimport org.apache.http.impl.client.CloseableHttpClient;\nimport org.apache.http.impl.client.HttpClientBuilder;\nimport org.apache.http.impl.client.LaxRedirectStrategy;\nimport org.apache.http.impl.conn.PoolingHttpClientConnectionManager;\nimport org.apache.http.ssl.SSLContextBuilder;\nimport org.apache.http.util.EntityUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.net.ssl.HostnameVerifier;\nimport javax.ws.rs.core.Response.Status.Family;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.attribute.FileAttribute;\nimport java.security.KeyManagementException;\nimport java.security.KeyStore;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.UnrecoverableKeyException;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.*;\n\nimport static com.walmartlabs.concord.plugins.http.HttpTask.ResponseType;\nimport static com.walmartlabs.concord.plugins.http.HttpTaskUtils.getHttpEntity;\n\npublic class SimpleHttpClient {\n\n    private static final Logger log = LoggerFactory.getLogger(SimpleHttpClient.class);\n\n    private final Configuration config;\n    private final CloseableHttpClient client;\n    private final HttpUriRequest request;\n    private final ObjectMapper objectMapper = new ObjectMapper();\n    private final ExecutorService executorService = Executors.newSingleThreadExecutor();\n    private final boolean dryRunMode;\n\n    private SimpleHttpClient(Configuration config, boolean dryRunMode) throws Exception {\n        this.config = config;\n        this.client = createClient(config);\n        this.request = buildHttpUriRequest(config);\n        this.dryRunMode = dryRunMode;\n    }\n\n    /**\n     * Factory method to create {@link SimpleHttpClient} objects\n     *\n     * @param config {@link Configuration}\n     * @return SimpleHttpClient\n     */\n    public static SimpleHttpClient create(Configuration config, boolean dryRunMode) throws Exception {\n        return new SimpleHttpClient(config, dryRunMode);\n    }\n\n    /**\n     * Execute request in the {@link #config Configuration}, resulting in {@link ClientResponse}\n     *\n     * @return ClientResponse response result of the execution\n     * @throws Exception exception\n     */\n    public ClientResponse execute() throws Exception {\n        CloseableHttpResponse httpResponse = null;\n        Object content = \"\";\n        try {\n            if (config.isDebug()) {\n                logRequest(request);\n            }\n\n            if (dryRunMode && !HttpGet.METHOD_NAME.equals(request.getMethod())) {\n                log.info(\"Running in dry-run mode: Skipping sending request\");\n                return new ClientResponse(Map.of(\"success\", true, \"statusCode\", 200));\n            }\n\n            httpResponse = callWithTimeout(() -> this.client.execute(request), config.getRequestTimeout());\n\n            int code = httpResponse.getStatusLine().getStatusCode();\n            if (isUnauthorized(code) && !config.isIgnoreErrors()) {\n                throw new UnauthorizedException(\"Authorization required for \" + request.getURI().toURL() + \"(code: \" + code + \")\");\n            }\n\n            Family statusCodeFamily = Family.familyOf(httpResponse.getStatusLine().getStatusCode());\n            boolean isSuccess = Family.SUCCESSFUL == statusCodeFamily;\n\n            Map<String, Object> response = new HashMap<>();\n            if (isSuccess) {\n                content = processResponse(httpResponse, config);\n                response.put(\"content\", content);\n            } else {\n                content = httpResponse.getEntity() != null ? EntityUtils.toString(httpResponse.getEntity()) : \"\";\n                // for backward compatibility\n                response.put(\"content\", \"\");\n                response.put(\"errorString\", content);\n            }\n\n            response.put(\"success\", isSuccess);\n            response.put(\"statusCode\", httpResponse.getStatusLine().getStatusCode());\n            response.put(\"headers\", getHeaders(httpResponse.getAllHeaders()));\n\n            // TODO return a proper structure, convert later\n            return new ClientResponse(response);\n        } catch (RequestTimeoutException | IOException | UnauthorizedException e) {\n            if (!config.isIgnoreErrors()) {\n                throw e;\n            }\n\n            Map<String, Object> response = new HashMap<>();\n            response.put(\"success\", false);\n            response.put(\"errorString\", e.getMessage());\n            if (httpResponse != null) {\n                response.put(\"statusCode\", httpResponse.getStatusLine().getStatusCode());\n            }\n\n            // TODO return a proper structure, convert later\n            return new ClientResponse(response);\n        } finally {\n            if (httpResponse != null) {\n                if (config.isDebug()) {\n                    logResponse(httpResponse, content);\n                }\n\n                httpResponse.close();\n            }\n\n            this.client.close();\n        }\n    }\n\n    private <T> T callWithTimeout(Callable<T> callable, long timeoutDurationMs) throws Exception {\n        Future<T> future = executorService.submit(callable);\n        try {\n            if (timeoutDurationMs > 0) {\n                return future.get(timeoutDurationMs, TimeUnit.MILLISECONDS);\n            } else {\n                return future.get();\n            }\n        } catch (TimeoutException e) {\n            future.cancel(true);\n\n            if (!request.isAborted()) {\n                request.abort();\n            }\n\n            throw new RequestTimeoutException(\"Request timeout after \" + timeoutDurationMs + \"ms\");\n        } catch (ExecutionException e) {\n            return unwrapException(e);\n        }\n    }\n\n    private <T> T unwrapException(ExecutionException e) throws Exception {\n        if (e.getCause() instanceof IOException) {\n            throw (IOException) e.getCause();\n        } else {\n            throw new Exception(e.getCause());\n        }\n    }\n\n    private Object processResponse(HttpResponse r, Configuration cfg) throws IOException {\n        ResponseType t = cfg.getResponseType();\n        if (t == null) {\n            t = ResponseType.STRING;\n        }\n\n        HttpEntity e = r.getEntity();\n        if (e == null) {\n            return null;\n        }\n\n        switch (t) {\n            case FILE:\n                return storeFile(e);\n            case JSON:\n                return parseJson(e);\n            default:\n                return EntityUtils.toString(e);\n        }\n    }\n\n    private void logRequest(HttpUriRequest request) throws IOException {\n        Map<String, Object> debugInfo = new HashMap<>();\n\n        Map<String, Object> requestInfo = buildRequestInfo(request);\n        debugInfo.put(\"requestInfo\", requestInfo);\n\n        log.info(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(debugInfo));\n    }\n\n    private void logResponse(CloseableHttpResponse httpResponse, Object content) throws IOException {\n        Map<String, Object> debugInfo = new HashMap<>();\n\n        Map<String, Object> responseInfo = buildResponseInfo(httpResponse, content);\n        debugInfo.put(\"responseInfo\", responseInfo);\n\n        log.info(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(debugInfo));\n    }\n\n    /**\n     * Method to store the file into the .tmp folder under working directory\n     *\n     * @param entity {@link HttpEntity}\n     * @return File relative path in {@link String}\n     * @throws IOException exception from {@link Files#createTempFile(Path, String, String, FileAttribute[])}\n     */\n    private String storeFile(HttpEntity entity) throws IOException {\n        Path baseDir = Paths.get(config.getWorkDir());\n        Path tmpDir = assertTempDir(baseDir);\n        Path tempFile = uriToPath(this.request.getURI(), tmpDir);\n\n        entity.writeTo(new FileOutputStream(tempFile.toFile()));\n        // Return the relative path instead of absolute path\n        return baseDir.relativize(tempFile.toAbsolutePath()).toString();\n    }\n\n    /**\n     * Method to make sure the .tmp directory exists in the working directory\n     *\n     * @param baseDir working directory\n     * @return Path of the .tmp directory\n     * @throws IOException from {@link Files#createDirectories(Path, FileAttribute[])}\n     */\n    private Path assertTempDir(Path baseDir) throws IOException {\n        Path p = baseDir.resolve(\".tmp\");\n        if (!Files.exists(p)) {\n            Files.createDirectories(p);\n        }\n        return p.resolve(Files.createTempDirectory(p, \"tmpdir_\"));\n    }\n\n    /**\n     * Converts a URI to a temporary Path. If no filename can be parsed from the URI, then a filename will be generated\n     *\n     * @param uri URI of the file\n     * @param dir Directory in which to save the file\n     * @return Path object representing the file\n     * @throws IOException when the file cannot be created with {@link Files#createFile(Path, FileAttribute[])} or\n     *                     {@link Files#createTempFile(Path, String, String, FileAttribute[])}\n     */\n    private Path uriToPath(java.net.URI uri, Path dir) throws IOException {\n        String filename = null;\n        String s = uri.toString();\n        Path path;\n\n        final int lastSlashIndex = s.lastIndexOf('/');\n        if (lastSlashIndex > 0) {\n            filename = s.substring(lastSlashIndex + 1);\n        }\n\n        if (filename == null || filename.isEmpty()) {\n            path = Files.createTempFile(dir, \"tmpfile_\", \".tmp\");\n        } else {\n            path = Files.createFile(dir.resolve(filename));\n        }\n\n        return path;\n    }\n\n    /**\n     * Method to parse the json response\n     *\n     * @param entity {@link HttpEntity}\n     * @return parsed Json {@link Object}\n     * @throws RuntimeException\n     */\n    private Object parseJson(HttpEntity entity) {\n        try {\n            return objectMapper.readValue(entity.getContent(), Object.class);\n        } catch (IOException e) {\n            throw new RuntimeException(\"Invalid JSON response: \" + e.getMessage());\n        }\n    }\n\n    /**\n     * Method to check whether the status code is Authorized or not\n     *\n     * @param statusCode http status code\n     * @return true if statusCode is UNAUTHORIZED (401)\n     */\n    private boolean isUnauthorized(int statusCode) {\n        return HttpStatus.SC_UNAUTHORIZED == statusCode;\n    }\n\n    /**\n     * Method to create {@link CloseableHttpClient client} with custom connection manager\n     *\n     * @return CloseableHttpClient client\n     * @throws Exception exception\n     */\n    private static CloseableHttpClient createClient(Configuration cfg) throws Exception {\n        RequestConfig.Builder c = RequestConfig.custom()\n                .setConnectTimeout(cfg.getConnectTimeout())\n                .setSocketTimeout(cfg.getSocketTimeout())\n                .setRedirectsEnabled(cfg.isFollowRedirects());\n\n        HttpClientBuilder clientBuilder = HttpClientBuilder.create();\n\n        String proxy = cfg.getProxy();\n        if (proxy != null) {\n            log.info(\"Using proxy: {}\", proxy);\n            HttpHost proxyHost = HttpHost.create(proxy);\n            c.setProxy(proxyHost);\n\n            if (cfg.getProxyUser() != null) {\n                log.info(\"Using proxy auth: {}:***\", cfg.getProxyUser());\n\n                CredentialsProvider proxyCredsProvider = new BasicCredentialsProvider();\n                proxyCredsProvider.setCredentials(\n                        new AuthScope(proxyHost.getHostName(), proxyHost.getPort()),\n                        new UsernamePasswordCredentials(cfg.getProxyUser(), new String(cfg.getProxyPassword())));\n\n                clientBuilder.setDefaultCredentialsProvider(proxyCredsProvider);\n            }\n        }\n\n        return clientBuilder\n                .setConnectionManager(buildConnectionManager(cfg))\n                .setDefaultRequestConfig(c.build())\n                .setRedirectStrategy(new LaxRedirectStrategy())\n                .build();\n    }\n\n    /**\n     * Method to build the connection manager\n     *\n     * @return HttpClientConnectionManager\n     * @throws KeyManagementException   keyManagementException\n     * @throws NoSuchAlgorithmException noSuchAlgorithmException\n     */\n    private static HttpClientConnectionManager buildConnectionManager(Configuration cfg) throws Exception {\n        SSLContextBuilder builder = new SSLContextBuilder();\n\n        if (!cfg.isStrictSsl()) {\n            builder.loadTrustMaterial(new TrustAllStrategy());\n        }\n\n        if (cfg.keyStorePath() != null) {\n            Path keystorePath = Paths.get(cfg.getWorkDir()).resolve(cfg.keyStorePath());\n            if (Files.notExists(keystorePath)) {\n                throw new RuntimeException(\"Keystore '\" + cfg.keyStorePath() + \"' not found\");\n            }\n\n            char[] keystorePass = cfg.keyStorePassword() != null ? cfg.keyStorePassword().toCharArray() : null;\n\n            KeyStore keyStore = KeyStore.getInstance(\"pkcs12\");\n            try (InputStream keyStoreInput = Files.newInputStream(keystorePath)) {\n                keyStore.load(keyStoreInput, keystorePass);\n            }\n\n            if (keyStore.size() == 0) {\n                throw new RuntimeException(\"Keystore is empty. Remove keystore input parameters or fix keystore\");\n            }\n\n            try {\n                builder.loadKeyMaterial(keyStore, keystorePass);\n            } catch (UnrecoverableKeyException e) {\n                throw new RuntimeException(\"Get key failed. Check keystore password\", e);\n            }\n        }\n\n        if (cfg.trustStorePath() != null) {\n            Path trustStorePath = Paths.get(cfg.getWorkDir()).resolve(cfg.trustStorePath());\n            if (Files.notExists(trustStorePath)) {\n                throw new RuntimeException(\"TrustStore '\" + cfg.trustStorePath() + \"' not found\");\n            }\n\n            char[] trustStorePass = cfg.trustStorePassword() != null ? cfg.trustStorePassword().toCharArray() : null;\n\n            try {\n                builder.loadTrustMaterial(trustStorePath.toFile(), trustStorePass);\n            } catch (Exception e) {\n                throw new RuntimeException(\"load trustStore failed. Check truststore password\", e);\n            }\n        }\n\n        SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(builder.build(), assertHostNameVerifier(cfg));\n\n        Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()\n                .register(\"http\", PlainConnectionSocketFactory.INSTANCE)\n                .register(\"https\", socketFactory)\n                .build();\n\n        return new PoolingHttpClientConnectionManager(registry);\n    }\n\n\n    private HttpUriRequest buildHttpUriRequest(Configuration cfg) throws Exception {\n        switch (cfg.getMethodType()) {\n            case DELETE:\n                return buildDeleteRequest(cfg);\n            case POST:\n                return buildPostRequest(cfg);\n            case GET:\n                return buildGetRequest(cfg);\n            case PUT:\n                return buildPutRequest(cfg);\n            case PATCH:\n                return buildPatchRequest(cfg);\n            default:\n                throw new IllegalArgumentException(\"Unsupported method type: \" + cfg.getMethodType());\n        }\n    }\n\n    /**\n     * Method to build the delete request using the given configuration\n     *\n     * @param cfg {@link Configuration}\n     * @return HttpUriRequest\n     */\n    private HttpUriRequest buildDeleteRequest(Configuration cfg) {\n        return HttpTaskRequest.delete(cfg.getUrl())\n                .withBasicAuth(cfg.getEncodedAuthToken())\n                .withResponseType(ResponseType.ANY)\n                .withHeaders(cfg.getRequestHeaders())\n                .get();\n    }\n\n    /**\n     * Method to build the post request using the given configuration\n     *\n     * @param cfg {@link Configuration}\n     * @return HttpUriRequest\n     * @throws Exception thrown by {@link HttpTaskUtils#getHttpEntity(Object, RequestType)} method\n     */\n    private HttpUriRequest buildPostRequest(Configuration cfg) throws Exception {\n        return HttpTaskRequest.post(cfg.getUrl())\n                .withBasicAuth(cfg.getEncodedAuthToken())\n                .withRequestType(cfg.getRequestType())\n                .withResponseType(ResponseType.ANY)\n                .withHeaders(cfg.getRequestHeaders())\n                .withBody(getHttpEntity(cfg.getBody(), cfg.getRequestType()))\n                .get();\n    }\n\n    /**\n     * Method to build the get request using the given configuration\n     *\n     * @param cfg {@link Configuration}\n     * @return HttpUriRequest\n     */\n    private HttpUriRequest buildGetRequest(Configuration cfg) {\n        return HttpTaskRequest.get(cfg.getUrl())\n                .withBasicAuth(cfg.getEncodedAuthToken())\n                .withRequestType(cfg.getRequestType())\n                .withResponseType(ResponseType.ANY)\n                .withHeaders(cfg.getRequestHeaders())\n                .get();\n    }\n\n    /**\n     * Method to build the patch request using the given configuration\n     *\n     * @param cfg {@link Configuration}\n     * @return HttpUriRequest\n     * @throws Exception thrown by {@link HttpTaskUtils#getHttpEntity(Object, RequestType)} method\n     */\n    private HttpUriRequest buildPatchRequest(Configuration cfg) throws Exception {\n        return HttpTaskRequest.patch(cfg.getUrl())\n                .withBasicAuth(cfg.getEncodedAuthToken())\n                .withRequestType(cfg.getRequestType())\n                .withResponseType(ResponseType.ANY)\n                .withHeaders(cfg.getRequestHeaders())\n                .withBody(getHttpEntity(cfg.getBody(), cfg.getRequestType()))\n                .get();\n    }\n\n    /**\n     * Method to build the put request using the given configuration\n     *\n     * @param cfg {@link Configuration}\n     * @return HttpUriRequest\n     * @throws Exception thrown by {@link HttpTaskUtils#getHttpEntity(Object, RequestType)} method\n     */\n    private HttpUriRequest buildPutRequest(Configuration cfg) throws Exception {\n        return HttpTaskRequest.put(cfg.getUrl())\n                .withBasicAuth(cfg.getEncodedAuthToken())\n                .withRequestType(cfg.getRequestType())\n                .withResponseType(ResponseType.ANY)\n                .withHeaders(cfg.getRequestHeaders())\n                .withBody(getHttpEntity(cfg.getBody(), cfg.getRequestType()))\n                .get();\n    }\n\n    private Map<String, Object> buildRequestInfo(HttpUriRequest request) throws IOException {\n        Map<String, Object> requestInfo = new HashMap<>();\n        requestInfo.put(\"headers\", getHeaders(request.getAllHeaders()));\n        requestInfo.put(\"method\", request.getMethod());\n        requestInfo.put(\"url\", request.getURI().toString());\n\n        if ((config.getRequestType() != RequestType.FILE) && (request instanceof HttpEntityEnclosingRequest)) {\n            try {\n                String rsp = EntityUtils.toString(((HttpEntityEnclosingRequest) request).getEntity());\n                requestInfo.put(\"body\", rsp);\n            } catch (ContentTooLongException e) {\n                requestInfo.put(\"body\", \"...too long to dump...\");\n            }\n        }\n\n        return requestInfo;\n    }\n\n    private Map<String, Object> buildResponseInfo(CloseableHttpResponse httpResponse, Object content) {\n        Map<String, Object> responseInfo = new HashMap<>();\n        responseInfo.put(\"headers\", getHeaders(httpResponse.getAllHeaders()));\n        responseInfo.put(\"status\", httpResponse.getStatusLine().getStatusCode());\n        responseInfo.put(\"response\", content);\n\n        return responseInfo;\n    }\n\n    private static Map<String, String> getHeaders(Header[] headers) {\n        if (headers == null) {\n            return Collections.emptyMap();\n        }\n\n        Map<String, String> result = new HashMap<>();\n        for (Header h : headers) {\n            result.put(h.getName(), h.getValue());\n        }\n        return result;\n    }\n\n    /**\n     * ClientResponse which wraps the response map. It is used to prevent the user to call the getResponse Method\n     * without executing the request.\n     */\n    public class ClientResponse {\n        private final Map<String, Object> response;\n\n        private ClientResponse(Map<String, Object> response) {\n            this.response = response;\n        }\n\n        public Map<String, Object> getResponse() {\n            return response;\n        }\n    }\n\n    private static HostnameVerifier assertHostNameVerifier(Configuration cfg) {\n        if (cfg.isStrictSsl()) {\n            return new DefaultHostnameVerifier(null);\n        }\n\n        return NoopHostnameVerifier.INSTANCE;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/http/src/main/java/com/walmartlabs/concord/plugins/http/Version.java",
    "content": "package com.walmartlabs.concord.plugins.http;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.Properties;\n\npublic final class Version {\n\n    private static final Version INSTANCE;\n\n    static {\n        Properties props = new Properties();\n\n        try (InputStream in = Version.class.getResourceAsStream(\"version.properties\")) {\n            props.load(in);\n        } catch (IOException e) {\n            throw new RuntimeException(\"version load error\", e);\n        }\n\n        String version = props.getProperty(\"version\");\n\n        INSTANCE = new Version(version);\n    }\n\n    public static String get() {\n        return INSTANCE.getVersion();\n    }\n\n    private final String version;\n\n    public Version(String version) {\n        this.version = version;\n    }\n\n    public String getVersion() {\n        return version;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/http/src/main/java/com/walmartlabs/concord/plugins/http/exception/RequestTimeoutException.java",
    "content": "package com.walmartlabs.concord.plugins.http.exception;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class RequestTimeoutException extends Exception {\n\n    private static final long serialVersionUID = 1L;\n\n    public RequestTimeoutException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/http/src/main/java/com/walmartlabs/concord/plugins/http/exception/UnauthorizedException.java",
    "content": "package com.walmartlabs.concord.plugins.http.exception;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class UnauthorizedException extends RuntimeException {\n\n    private static final long serialVersionUID = 1L;\n\n    public UnauthorizedException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/http/src/main/java/com/walmartlabs/concord/plugins/http/request/HttpTaskRequest.java",
    "content": "package com.walmartlabs.concord.plugins.http.request;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.plugins.http.HttpTask.RequestType;\nimport com.walmartlabs.concord.plugins.http.HttpTask.ResponseType;\nimport com.walmartlabs.concord.plugins.http.Version;\nimport org.apache.http.HttpEntity;\nimport org.apache.http.HttpHeaders;\nimport org.apache.http.client.methods.*;\n\nimport javax.ws.rs.core.MediaType;\nimport java.util.Map;\n\n/**\n * Implementation of {@link Request} use to create Http request object\n */\npublic class HttpTaskRequest implements Request {\n\n    private HttpUriRequest request;\n\n    private HttpTaskRequest(HttpUriRequest request) {\n        this.request = request;\n        this.request.setHeader(HttpHeaders.USER_AGENT, \"Concord (http-task version '\" + Version.get() + \"')\");\n    }\n\n    /**\n     * Method to create {@link HttpDelete} request\n     *\n     * @param url Endpoint\n     * @return HttpTaskRequest\n     */\n    public static HttpTaskRequest delete(String url) {\n        return new HttpTaskRequest(new HttpDelete(url));\n    }\n\n    /**\n     * Method to create {@link HttpPost} request\n     *\n     * @param url Endpoint\n     * @return HttpTaskRequest\n     */\n    public static HttpTaskRequest post(String url) {\n        return new HttpTaskRequest(new HttpPost(url));\n    }\n\n    /**\n     * Method to create {@link HttpPut} request\n     *\n     * @param url Endpoint\n     * @return HttpTaskRequest\n     */\n    public static HttpTaskRequest put(String url) {\n        return new HttpTaskRequest(new HttpPut(url));\n    }\n\n    /**\n     * Method to create {@link HttpPatch} request\n     *\n     * @param url Endpoint\n     * @return HttpTaskRequest\n     */\n    public static HttpTaskRequest patch(String url) {\n        return new HttpTaskRequest(new HttpPatch(url));\n    }\n\n    /**\n     * Method to create {@link HttpGet} request\n     *\n     * @param url Endpoint\n     * @return HttpTaskRequest\n     */\n    public static HttpTaskRequest get(String url) {\n        return new HttpTaskRequest(new HttpGet(url));\n    }\n\n    @Override\n    public Request withBasicAuth(String encodedToken) {\n        if (encodedToken != null) {\n            this.request.setHeader(HttpHeaders.AUTHORIZATION, \"Basic \" + encodedToken);\n        }\n\n        return this;\n    }\n\n    @Override\n    public Request withRequestType(RequestType requestType) {\n        String contentType = getContentType(requestType);\n        if (contentType != null && !contentType.isEmpty()) {\n            this.request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);\n        }\n\n        return this;\n    }\n\n    @Override\n    public Request withResponseType(ResponseType responseType) {\n        String acceptType = getAcceptType(responseType);\n        if (acceptType != null && !acceptType.isEmpty()) {\n            this.request.setHeader(HttpHeaders.ACCEPT, acceptType);\n        }\n\n        return this;\n    }\n\n    @Override\n    public Request withHeaders(Map<String, String> headers) {\n        if (headers != null) {\n            headers.forEach(this.request::setHeader);\n        }\n        return this;\n    }\n\n    @Override\n    public Request withBody(HttpEntity body) {\n        if (this.request instanceof HttpPost) {\n            ((HttpPost) this.request).setEntity(body);\n        } else if (this.request instanceof HttpPut) {\n            ((HttpPut) this.request).setEntity(body);\n        } else if (this.request instanceof HttpPatch) {\n            ((HttpPatch) this.request).setEntity(body);\n        }\n\n        return this;\n    }\n\n    @Override\n    public HttpUriRequest get() {\n        return this.request;\n    }\n\n    /**\n     * Method to get the Content-Type header using the given request type\n     *\n     * @param requestType type of request\n     * @return Specific {@link MediaType} or null in case of invalid {@link RequestType}\n     */\n    private String getContentType(RequestType requestType) {\n        if (RequestType.JSON == requestType) {\n            return MediaType.APPLICATION_JSON;\n        } else if (RequestType.FILE == requestType) {\n            return MediaType.APPLICATION_OCTET_STREAM;\n        } else if (RequestType.STRING == requestType) {\n            return MediaType.TEXT_PLAIN;\n        } else if (RequestType.FORM == requestType) {\n            return MediaType.APPLICATION_FORM_URLENCODED;\n        }\n\n        return null;\n    }\n\n    /**\n     * Method to get the Accept header using the given response type\n     *\n     * @param responseType type of response\n     * @return Specific {@link MediaType} or null in case of invalid {@link ResponseType}\n     */\n    private String getAcceptType(ResponseType responseType) {\n        if (ResponseType.ANY == responseType) {\n            return MediaType.WILDCARD;\n        }\n\n        return null;\n    }\n\n\n}\n"
  },
  {
    "path": "plugins/tasks/http/src/main/java/com/walmartlabs/concord/plugins/http/request/Request.java",
    "content": "package com.walmartlabs.concord.plugins.http.request;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.plugins.http.HttpTask.RequestType;\nimport com.walmartlabs.concord.plugins.http.HttpTask.ResponseType;\nimport org.apache.http.HttpEntity;\nimport org.apache.http.client.methods.HttpUriRequest;\n\nimport java.util.Map;\n\npublic interface Request {\n\n    /**\n     * Method to set the Authorization header in the request\n     *\n     * @param encodedToken Base64 encoded token\n     * @return Instance of this Request\n     */\n    Request withBasicAuth(String encodedToken);\n\n    /**\n     * Method to set the Content-Type header in the request\n     *\n     * @param requestType {@link RequestType}\n     * @return Instance of this Request\n     */\n    Request withRequestType(RequestType requestType);\n\n    /**\n     * Method to set the response type, which will later use to parse the\n     * response from endpoint\n     *\n     * @param responseType {@link ResponseType}\n     * @return Instance of this Request\n     */\n    Request withResponseType(ResponseType responseType);\n\n\n    /**\n     * Method to set the request headers\n     *\n     * @param headers\n     * @return Instance of this Request\n     */\n    Request withHeaders(Map<String, String> headers);\n\n    /**\n     * Method to set the Body of the request\n     *\n     * @param body request body\n     * @return Instance of this Request\n     */\n    Request withBody(HttpEntity body);\n\n    /**\n     * Method to get the underlying {@link HttpUriRequest}\n     *\n     * @return HttpUriRequest\n     */\n    HttpUriRequest get();\n}\n"
  },
  {
    "path": "plugins/tasks/http/src/test/java/com/walmartlabs/concord/plugins/http/HttpTaskTest.java",
    "content": "package com.walmartlabs.concord.plugins.http;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.MockContext;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nclass HttpTaskTest {\n\n    @Test\n    void testExecute() {\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"url\", \"https://mock.local\");\n\n        var ctx = new MockContext(input);\n        assertDoesNotThrow(() -> new MockHttpTask().execute(ctx));\n\n        assertNotNull(ctx.getVariable(\"response\"));\n    }\n\n    @Test\n    void testExecuteCustomOutVar() {\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"url\", \"https://mock.local\");\n        input.put(\"out\", \"myOut\");\n\n        var ctx = new MockContext(input);\n        assertDoesNotThrow(() -> new MockHttpTask().execute(ctx));\n\n        assertNotNull(ctx.getVariable(\"myOut\"));\n    }\n\n    @Test\n    void testAsString() {\n        var out = assertDoesNotThrow(() -> new MockHttpTask().asString(\"https://mock.local\"));\n        assertEquals(\"ok\", out);\n    }\n\n    private static class MockHttpTask extends HttpTask {\n\n        @Override\n        Map<String, Object> executeRequest(Configuration config) {\n            return Map.of(\"success\", true, \"content\", \"ok\");\n        }\n    }\n\n}\n"
  },
  {
    "path": "plugins/tasks/http/src/test/java/com/walmartlabs/concord/plugins/http/HttpTaskV2Test.java",
    "content": "package com.walmartlabs.concord.plugins.http;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.junit.jupiter.api.io.TempDir;\nimport org.mockito.Answers;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\nimport java.nio.file.Path;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertDoesNotThrow;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.when;\n\n@ExtendWith(MockitoExtension.class)\nclass HttpTaskV2Test {\n\n    @TempDir\n    Path tempDir;\n\n    @Mock(answer = Answers.RETURNS_DEEP_STUBS)\n    Context ctx;\n\n    HttpTaskV2 task;\n\n    @BeforeEach\n    void setUp() {\n        when(ctx.processConfiguration().dryRun()).thenReturn(true);\n        when(ctx.workingDirectory()).thenReturn(tempDir);\n        task = new HttpTaskV2(ctx);\n    }\n\n    @Test\n    void testExecute() {\n        var input = defaultInput();\n\n        var result = execute(input);\n\n        assertTrue(result.ok());\n        assertEquals(200, result.values().get(\"statusCode\"));\n    }\n\n    private static Map<String, Object> defaultInput() {\n        Map<String, Object> defaultInput = new HashMap<>();\n        defaultInput.put(\"url\", \"https://mock.local\");\n        defaultInput.put(\"method\", \"POST\");\n        defaultInput.put(\"request\", \"json\");\n        defaultInput.put(\"body\", \"{}\");\n\n        return defaultInput;\n    }\n\n    private TaskResult.SimpleResult execute(Map<String, Object> input) {\n        var result = assertDoesNotThrow(() -> task.execute(new MapBackedVariables(input)));\n        return assertInstanceOf(TaskResult.SimpleResult.class, result);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/http/src/test/java/com/walmartlabs/concord/plugins/http/SimpleHttpClientTest.java",
    "content": "package com.walmartlabs.concord.plugins.http;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.github.tomakehurst.wiremock.http.RequestMethod;\nimport com.walmartlabs.concord.plugins.http.exception.RequestTimeoutException;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.File;\nimport java.net.URLEncoder;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Paths;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.github.tomakehurst.wiremock.client.WireMock.deleteRequestedFor;\nimport static com.github.tomakehurst.wiremock.client.WireMock.equalTo;\nimport static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;\nimport static com.github.tomakehurst.wiremock.client.WireMock.patchRequestedFor;\nimport static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;\nimport static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;\nimport static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;\nimport static org.junit.jupiter.api.Assertions.assertDoesNotThrow;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nclass SimpleHttpClientTest extends WiremockTest {\n\n    @Test\n    void testExecuteGetRequestForJson() throws Exception {\n        var config = initCfgForRequest(\"GET\", \"JSON\", \"JSON\",\n                \"http://localhost:\" + wireMockServer().port() + \"/json\", false, 0, true);\n\n        var response = SimpleHttpClient.create(config, false)\n                .execute()\n                .getResponse();\n\n        wireMockServer().verify(getRequestedFor(urlEqualTo(\"/json\")));\n        assertEquals(200, response.get(\"statusCode\"));\n        assertNull(response.get(\"errorString\"));\n    }\n\n    @Test\n    void testExecuteGetRequestForString() throws Exception {\n        var config = initCfgForRequest(\"GET\", \"STRING\", \"STRING\",\n                \"http://localhost:\" + wireMockServer().port() + \"/string\", false, 0, true);\n        SimpleHttpClient.create(config, false)\n                .execute();\n        wireMockServer().verify(getRequestedFor(urlEqualTo(\"/string\")));\n    }\n\n    @Test\n    void testExecuteGetRequestWithQueryParams() throws Exception {\n        Map<String, Object> queryParams = new HashMap<>();\n        queryParams.put(\"key\", \"value with space\");\n\n        var config = initCfgForRequest(\"GET\", null, \"STRING\",\n                \"http://localhost:\" + wireMockServer().port() + \"/query\", false, 0, true,\n                Map.of(\"query\", queryParams ));\n        SimpleHttpClient.create(config, false)\n                .execute();\n        wireMockServer().verify(getRequestedFor(urlPathEqualTo(\"/query\")).withQueryParam(\"key\", equalTo(\"value with space\")));\n    }\n\n    @Test\n    void testExecutePostRequestWithFormUrlEncoded() throws Exception {\n        var config = initCfgForRequest(\"POST\", \"form\", \"json\",\n                \"http://localhost:\" + wireMockServer().port() + \"/post\", false, 0, true,\n                Map.of(\"body\", Map.of(\"message\", \"Hello Concord!\")));\n        SimpleHttpClient.create(config, false)\n                        .execute();\n\n        var req = wireMockServer().getAllServeEvents().get(0).getRequest();\n        assertEquals(RequestMethod.POST, req.getMethod());\n        assertEquals(\"message=\" + URLEncoder.encode(\"Hello Concord!\", StandardCharsets.UTF_8), req.getBodyAsString());\n    }\n\n    @Test\n    void testExecutePostRequestForJson() throws Exception {\n        var cfg = initCfgForRequest(\"POST\", \"json\", \"json\",\n                \"http://localhost:\" + wireMockServer().port() + \"/post\", false, 0, true,\n                Map.of(\"body\", \"{ \\\"request\\\": \\\"PostTest\\\" }\"));\n        var out = SimpleHttpClient.create(cfg, false)\n                .execute();\n\n        assertNotNull(out.getResponse());\n        wireMockServer().verify(postRequestedFor(urlEqualTo(\"/post\"))\n                .withHeader(\"Content-Type\", equalTo(\"application/json\")));\n    }\n\n    @Test\n    void testExecutePostRequestForComplexObject() throws Exception {\n        HashMap<String, Object> complexObject = new HashMap<>();\n        HashMap<String, Object> nestedObject = new HashMap<>();\n        nestedObject.put(\"nestedVar\", 123);\n        complexObject.put(\"myObject\", nestedObject);\n\n        var cfg = initCfgForRequest(\"POST\", \"json\", \"json\",\n                \"http://localhost:\" + wireMockServer().port() + \"/post\", false, 0, true,\n                Map.of(\"body\", complexObject));\n        var out = SimpleHttpClient.create(cfg, false)\n                .execute();\n\n        assertNotNull(out.getResponse());\n        wireMockServer().verify(postRequestedFor(urlEqualTo(\"/post\"))\n                .withHeader(\"Content-Type\", equalTo(\"application/json\")));\n    }\n\n    @Test\n    void testExecutePostRequestForMultipart() throws Exception {\n        HashMap<String, Object> formInput = new HashMap<>();\n        HashMap<String, Object> field = new HashMap<>();\n        field.put(\"type\", \"text/plain\");\n        field.put(\"data\", \"\\\"string value\\\"\");\n\n        formInput.put(\"field1\", \"string value\");\n        formInput.put(\"field2\", \"src/test/resources/__files/file.bin\");\n        formInput.put(\"field3\", field);\n\n        var cfg = initCfgForRequest(\"POST\", \"formData\", \"json\",\n                \"http://localhost:\" + wireMockServer().port() + \"/post\", false, 0, true,\n                Map.of(\"body\", formInput));\n\n        var out = SimpleHttpClient.create(cfg, false).execute();\n\n        assertNotNull(out.getResponse());\n        assertTrue((Boolean) out.getResponse().get(\"success\"));\n    }\n\n    @Test\n    void testExecuteForException() throws Exception {\n        var cfg = initCfgForRequest(\"GET\", \"string\", \"string\",\n                \"http://localhost:\" + wireMockServer().port() + \"/fault\", false, 0, true);\n        var client = SimpleHttpClient.create(cfg, false);\n        var ex = assertThrows(Exception.class, client::execute);\n\n        assertTrue(ex.getMessage().contains(\"failed to respond\"));\n    }\n\n    @Test\n    void testExecuteWithIgnoreError() throws Exception {\n        var throwingCfg = initCfgForRequest(\"GET\", \"string\", \"string\",\n                \"http://localhost:\" + wireMockServer().port() + \"/fault\", false, 0, true);\n        var throwingClient = SimpleHttpClient.create(throwingCfg, false);\n        assertThrows(Exception.class, throwingClient::execute);\n\n        var ignoringCfg = initCfgForRequest(\"GET\", \"string\", \"string\",\n                \"http://localhost:\" + wireMockServer().port() + \"/fault\", true, 0, true);\n        var ignoringClient = SimpleHttpClient.create(ignoringCfg, false);\n        assertDoesNotThrow(ignoringClient::execute);\n        wireMockServer().verify(8, getRequestedFor(urlEqualTo(\"/fault\")));\n    }\n\n    @Test\n    void testExecuteGetRequestWithoutFollowRedirect() throws Exception {\n        var cfg = initCfgForRequest(\"GET\", \"string\", \"string\",\n                \"http://localhost:\" + wireMockServer().port() + \"/followRedirects\", false, 0, false);\n        var out = SimpleHttpClient.create(cfg, false)\n                .execute();\n\n        assertEquals(301, out.getResponse().get(\"statusCode\"));\n        wireMockServer().verify(1, getRequestedFor(urlEqualTo(\"/followRedirects\")));\n        wireMockServer().verify(0, getRequestedFor(urlEqualTo(\"/string\")));\n    }\n\n    @Test\n    void testExecuteGetRequestWithFollowRedirect() throws Exception {\n        var cfg = initCfgForRequest(\"GET\", \"string\", \"string\",\n                \"http://localhost:\" + wireMockServer().port() + \"/followRedirects\", false, 0, true);\n        var out = SimpleHttpClient.create(cfg, false)\n                .execute();\n\n        assertEquals(200, out.getResponse().get(\"statusCode\"));\n        wireMockServer().verify(1, getRequestedFor(urlEqualTo(\"/followRedirects\")));\n        wireMockServer().verify(1, getRequestedFor(urlEqualTo(\"/string\")));\n    }\n\n    @Test\n    void testExecutePostRequestWithoutFollowRedirect() throws Exception {\n        var cfg = initCfgForRequest(\"POST\", \"json\", \"json\",\n                \"http://localhost:\" + wireMockServer().port() + \"/followRedirectsPost\", false, 0, false,\n                Map.of(\"body\", \"{}\"));\n        var out = SimpleHttpClient.create(cfg, false)\n                .execute();\n\n        assertEquals(301, out.getResponse().get(\"statusCode\"));\n        wireMockServer().verify(1, postRequestedFor(urlEqualTo(\"/followRedirectsPost\")));\n        wireMockServer().verify(0, postRequestedFor(urlEqualTo(\"/string\")));\n    }\n\n    @Test\n    void testExecutePostRequestWithFollowRedirect() throws Exception {\n        var cfg = initCfgForRequest(\"POST\", \"json\", \"string\",\n                \"http://localhost:\" + wireMockServer().port() + \"/followRedirectsPost\", false, 0, true,\n                Map.of(\"body\", \"{}\"));\n        var out = SimpleHttpClient.create(cfg, false)\n                .execute();\n\n        assertEquals(200, out.getResponse().get(\"statusCode\"));\n        wireMockServer().verify(1, postRequestedFor(urlEqualTo(\"/followRedirectsPost\")));\n        wireMockServer().verify(1, getRequestedFor(urlEqualTo(\"/string\")));\n    }\n\n    @Test\n    void testIllegalArgumentExceptionForRequest() {\n        var ex = assertThrows(IllegalArgumentException.class, () -> initCfgForRequest(\"GET\", null, \"json\",\n                null, false, 0, true));\n\n        assertTrue(ex.getMessage().contains(\"('url') argument is missing\"));\n    }\n\n    @Test\n    void testGetAsDefaultRequestMethod() throws Exception {\n        var cfg = initCfgForRequest(null, \"json\", \"json\",\n                \"http://localhost:\" + wireMockServer().port() + \"/json\", false, 0, true);\n        SimpleHttpClient.create(cfg, false)\n                .execute();\n\n        assertEquals(HttpTask.RequestMethodType.GET, cfg.getMethodType());\n        wireMockServer().verify(1, getRequestedFor(urlEqualTo(\"/json\")));\n    }\n\n    @Test\n    void testGetRequestForResponseContent() throws Exception {\n        var cfg = initCfgForRequest(\"GET\", \"json\", \"json\",\n                \"http://localhost:\" + wireMockServer().port() + \"/json\", false, 0, true);\n        var out = SimpleHttpClient.create(cfg, false)\n                .execute();\n\n        assertNotNull(out.getResponse());\n        assertNotNull(out.getResponse().get(\"content\"));\n        wireMockServer().verify(1, getRequestedFor(urlEqualTo(\"/json\")));\n    }\n\n    @Test\n    void testUnsuccessfulResponse() throws Exception {\n        var cfg = initCfgForRequest(\"GET\", \"json\", \"json\",\n                \"http://localhost:\" + wireMockServer().port() + \"/unsuccessful\", false, 0, true);\n        var out = SimpleHttpClient.create(cfg, false)\n                .execute();\n\n        assertNotNull(out.getResponse());\n        assertFalse((Boolean) out.getResponse().get(\"success\"));\n        String errorString = assertInstanceOf(String.class, out.getResponse().get(\"errorString\"));\n        assertFalse(errorString.isEmpty());\n        wireMockServer().verify(1, getRequestedFor(urlEqualTo(\"/unsuccessful\")));\n    }\n\n    @Test\n    void testFilePostRequest() throws Exception {\n        var cfg = initCfgForRequest(\"POST\", \"file\", \"json\",\n                \"http://localhost:\" + wireMockServer().port() + \"/file\", false, 0, true,\n                Map.of(\"body\", \"src/test/resources/__files/file.bin\"));\n        var out = SimpleHttpClient.create(cfg, false)\n                .execute();\n\n        assertNotNull(out.getResponse());\n        assertTrue((Boolean) out.getResponse().get(\"success\"));\n        wireMockServer().verify(1, postRequestedFor(urlEqualTo(\"/file\")));\n    }\n\n    @Test\n    void testForMissingWorkDirForFileGetRequest() throws Exception {\n        // Working directory is mandatory for response type file\n        workDir = Paths.get(\"\");\n        var ex = assertThrows(IllegalArgumentException.class, () -> initCfgForRequest(\"GET\", \"json\", \"file\",\n                \"http://localhost:\" + wireMockServer().port() + \"/stringFile\", false, 0, true));\n\n        assertTrue(ex.getMessage().contains(\"Working directory is mandatory for ResponseType FILE\"));\n    }\n\n    @Test\n    void testFileGetRequestWithWorkDir() throws Exception {\n        var cfg = initCfgForRequest(\"GET\", \"json\", \"file\",\n                \"http://localhost:\" + wireMockServer().port() + \"/stringFile\", false, 0, true);\n        var out = SimpleHttpClient.create(cfg, false)\n                .execute();\n\n        assertNotNull(out.getResponse());\n        assertTrue((Boolean) out.getResponse().get(\"success\"));\n        assertEquals(\"stringFile\", (new File((String) out.getResponse().get(\"content\"))).getName());\n        wireMockServer().verify(1, getRequestedFor(urlEqualTo(\"/stringFile\")));\n    }\n\n    @Test\n    void testFileGetRequestWithNoFilename() throws Exception {\n        var cfg = initCfgForRequest(\"GET\", \"json\", \"file\",\n                \"http://localhost:\" + wireMockServer().port() + \"/fileUrlWithoutName/\", false, 0, true);\n        var out = SimpleHttpClient.create(cfg, false)\n                .execute();\n\n        assertNotNull(out.getResponse());\n        assertTrue((Boolean) out.getResponse().get(\"success\"));\n        assertTrue(((String) out.getResponse().get(\"content\")).matches(\".*/tmpfile_.*\\\\.tmp$\"));\n        wireMockServer().verify(1, getRequestedFor(urlEqualTo(\"/fileUrlWithoutName/\")));\n    }\n\n    @Test\n    void testFileGetWithResponseTypeString() throws Exception {\n        var cfg = initCfgForRequest(\"GET\", \"json\", \"string\",\n                \"http://localhost:\" + wireMockServer().port() + \"/stringFile\", false, 0, true);\n        var out = SimpleHttpClient.create(cfg, false)\n                .execute();\n\n        assertNotNull(out.getResponse());\n        wireMockServer().verify(1, getRequestedFor(urlEqualTo(\"/stringFile\")));\n    }\n\n    @Test\n    void testFileGetWithResponseTypeJSON() throws Exception {\n        var cfg = initCfgForRequest(\"GET\", \"json\", \"json\",\n                \"http://localhost:\" + wireMockServer().port() + \"/JSONFile\", false, 0, true);\n        var out = SimpleHttpClient.create(cfg, false)\n                .execute();\n\n        assertNotNull(out.getResponse());\n        assertTrue((Boolean) out.getResponse().get(\"success\"));\n        wireMockServer().verify(1, getRequestedFor(urlEqualTo(\"/JSONFile\")));\n    }\n\n    @Test\n    void testPostJsonRequestForIncompatibleBody() throws Exception {\n        var cfg = initCfgForRequest(\"POST\", \"json\", \"string\",\n                \"http://localhost:\" + wireMockServer().port() + \"/post\", false, 0, true,\n                Map.of(\"body\", \"src/test/resources/__files/file.bin\"));\n        var ex = assertThrows(IllegalArgumentException.class, () -> SimpleHttpClient.create(cfg, false));\n\n        assertTrue(ex.getMessage().contains(\"'request: json' is not compatible with 'body'\"));\n    }\n\n    @Test\n    void testPostStringRequestForIncompatibleComplexBody() throws Exception {\n        var cfg = initCfgForRequest(\"POST\", \"string\", \"string\",\n                \"http://localhost:\" + wireMockServer().port() + \"/post\", false, 0, true,\n                Map.of(\"body\", Map.of()));\n        var ex = assertThrows(IllegalArgumentException.class, () -> SimpleHttpClient.create(cfg, false));\n\n        assertTrue(ex.getMessage().contains(\"'request: string' is not compatible with 'body'\"));\n    }\n\n    @Test\n    void testPostFileRequestForIncompatibleComplexBody() throws Exception {\n        var cfg = initCfgForRequest(\"POST\", \"file\", \"string\",\n                \"http://localhost:\" + wireMockServer().port() + \"/post\", false, 0, true,\n                Map.of(\"body\", Map.of()));\n        var ex = assertThrows(IllegalArgumentException.class, () -> SimpleHttpClient.create(cfg, false));\n\n        assertTrue(ex.getMessage().contains(\"'request: file' is not compatible with 'body'\"));\n    }\n\n    @Test\n    void testInvalidRequestMethodType() {\n        var ex = assertThrows(IllegalArgumentException.class, () -> initCfgForRequest(\"GET1\", \"json\", \"file\",\n                \"http://localhost:\" + wireMockServer().port() + \"/file\", false, 0, true));\n\n        assertTrue(ex.getMessage().contains(\"'method: GET1' is not a supported HTTP method\"));\n    }\n\n    @Test\n    void testInvalidRequestType() {\n        var ex = assertThrows(IllegalArgumentException.class, () -> initCfgForRequest(\"GET\", \"json1\", \"file\",\n                \"http://localhost:\" + wireMockServer().port() + \"/file\", false, 0, true));\n\n        assertTrue(ex.getMessage().contains(\"'request: json1' is not a supported request type\"));\n    }\n\n    @Test\n    void testInvalidResponseType() {\n        var ex = assertThrows(IllegalArgumentException.class, () -> initCfgForRequest(\"GET\", \"json\", \"file1\",\n                \"http://localhost:\" + wireMockServer().port() + \"/file\", false, 0, true));\n\n        assertTrue(ex.getMessage().contains(\"'response: file1' is not a supported response type\"));\n    }\n\n    @Test\n    void testOptionalResponseType() throws Exception {\n        var cfg = initCfgForRequest(\"GET\", \"string\", null,\n                \"http://localhost:\" + wireMockServer().port() + \"/string\", false, 0, true);\n        var out = SimpleHttpClient.create(cfg, false)\n                .execute();\n\n        assertTrue((Boolean) out.getResponse().get(\"success\"));\n        wireMockServer().verify(getRequestedFor(urlEqualTo(\"/string\")));\n    }\n\n    @Test\n    void testDelete() throws Exception {\n        var cfg = initCfgForRequest(\"DELETE\", \"string\", \"json\",\n                \"http://localhost:\" + wireMockServer().port() + \"/delete\", false, 0, true);\n        var out = SimpleHttpClient.create(cfg, false)\n                .execute();\n\n        wireMockServer().verify(deleteRequestedFor(urlEqualTo(\"/delete\")));\n        assertNotNull(out.getResponse());\n        Map<?, ?> content = assertInstanceOf(Map.class, out.getResponse().get(\"content\"));\n        assertEquals(\"Success\", content.get(\"message\"));\n    }\n\n    @Test\n    void testPatch() throws Exception {\n        var cfg = initCfgForRequest(\"PATCH\", \"json\", \"json\",\n                \"http://localhost:\" + wireMockServer().port() + \"/patch\", false, 0, true,\n                Map.of(\"body\", \"{ \\\"request\\\": \\\"PatchTest\\\" }\"));\n        var out = SimpleHttpClient.create(cfg, false)\n                .execute();\n\n        wireMockServer().verify(patchRequestedFor(urlEqualTo(\"/patch\")));\n        assertNotNull(out.getResponse());\n        Map<?, ?> content = assertInstanceOf(Map.class, out.getResponse().get(\"content\"));\n        assertEquals(\"Success\", content.get(\"message\"));\n    }\n\n    @Test\n    void testRequestTimeoutException() throws Exception {\n        var cfg = initCfgForRequest(\"GET\", \"string\", \"string\",\n                \"http://localhost:\" + wireMockServer().port() + \"/requestTimeout\", false, 250, true);\n\n        assertThrows(RequestTimeoutException.class, () -> SimpleHttpClient.create(cfg, false)\n                .execute());\n    }\n\n    @Test\n    void testInvalidJsonResponse() throws Exception {\n        var cfg = initCfgForRequest(\"GET\", \"json\", \"json\",\n                \"http://localhost:\" + wireMockServer().port() + \"/invalid/json\", false, 0, true);\n        var client =  SimpleHttpClient.create(cfg, false);\n        var ex = assertThrows(RuntimeException.class, client::execute);\n\n        assertTrue(ex.getMessage().contains(\"Invalid JSON response\"));\n    }\n\n    @Test\n    void testInvalidRequestMethod() {\n        var ex = assertThrows(IllegalArgumentException.class, () -> initCfgForRequest(null, \"json\", \"json\",\n                \"http://localhost:\" + wireMockServer().port() + \"/json\", false, 0, true,\n                Map.of(\"method\", 123)));\n\n        assertTrue(ex.getMessage().contains(\"Invalid variable 'method' type\"));\n    }\n\n    @Test\n    void testInvalidRequest() {\n        var ex = assertThrows(IllegalArgumentException.class, () -> initCfgForRequest(\"GET\", null, \"json\",\n                \"http://localhost:\" + wireMockServer().port() + \"/json\", false, 0, true,\n                Map.of(\"request\", 123)));\n\n        assertTrue(ex.getMessage().contains(\"Invalid variable 'request' type\"));\n    }\n\n    @Test\n    void testInvalidResponse() {\n        var ex = assertThrows(IllegalArgumentException.class, () -> initCfgForRequest(\"GET\", \"json\", null,\n                \"http://localhost:\" + wireMockServer().port() + \"/json\", false, 0, true,\n                Map.of(\"response\", 123)));\n\n        assertTrue(ex.getMessage().contains(\"Invalid variable 'response' type\"));\n    }\n\n}\n"
  },
  {
    "path": "plugins/tasks/http/src/test/java/com/walmartlabs/concord/plugins/http/WiremockTest.java",
    "content": "package com.walmartlabs.concord.plugins.http;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.github.tomakehurst.wiremock.WireMockServer;\nimport com.github.tomakehurst.wiremock.core.WireMockConfiguration;\nimport com.github.tomakehurst.wiremock.http.Fault;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.io.TempDir;\n\nimport java.nio.file.Path;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.github.tomakehurst.wiremock.client.WireMock.*;\n\npublic abstract class WiremockTest {\n\n    private WireMockServer wireMockServer;\n\n    @TempDir\n    protected Path workDir;\n\n    @BeforeEach\n    public void setup() {\n        wireMockServer = new WireMockServer(WireMockConfiguration.options()\n                .dynamicPort());\n        wireMockServer.start();\n\n        stubForJsonResponse();\n        stubForStringResponse();\n        stubForPostRequest();\n        stubForGetSecureEndpoint();\n        stubForGetWithQueryParams();\n        stubForPostSecureEndpoint();\n        stubForPostRequestForRequestTypeFile();\n        stubForGetRequestForResponseTypeStringFile();\n        stubForGetRequestForResponseTypeStringFileWithNoFilename();\n        stubForGetRequestForResponseTypeJSONFile();\n        stubForUnsuccessfulResponse();\n        stubForDeleteRequest();\n        stubForPatchRequest();\n        stubForFault();\n        stubForRequestTimeout();\n        stubForInvalidJsonResponse();\n        stubForFollowRedirect();\n        stubForFollowRedirectPost();\n    }\n\n    @AfterEach\n    public void tearDown() {\n        wireMockServer.shutdown();\n    }\n\n    protected WireMockServer wireMockServer() {\n        return this.wireMockServer;\n    }\n\n    protected Configuration initCfgForRequest(String requestMethod, String requestType,\n                                              String responseType, String url,\n                                              boolean ignoreErrors, int requestTimeout,\n                                              boolean followRedirects) throws Exception {\n\n        return initCfgForRequest(requestMethod, requestType, responseType, url,\n                ignoreErrors, requestTimeout, followRedirects, Map.of());\n    }\n\n    protected Configuration initCfgForRequest(String requestMethod, String requestType,\n                                              String responseType, String url,\n                                              boolean ignoreErrors, int requestTimeout,\n                                              boolean followRedirects, Map<String, Object> extra) throws Exception {\n\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"url\", url);\n        input.put(\"method\", requestMethod);\n        input.put(\"request\", requestType);\n        input.put(\"response\", responseType);\n        input.put(\"ignoreErrors\", ignoreErrors);\n        input.put(\"requestTimeout\", requestTimeout);\n        input.put(\"followRedirects\", followRedirects);\n\n        if (extra != null) {\n            input.putAll(extra);\n        }\n\n        return Configuration.custom()\n                .build(workDir.toString(), input, false);\n    }\n\n    protected void stubForJsonResponse() {\n        wireMockServer.stubFor(get(urlEqualTo(\"/json\"))\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withHeader(\"Content-Type\", \"application/json\")\n                        .withHeader(\"Accept\", \"application/json\")\n                        .withBody(\"\"\"\n                                [\n                                    {\n                                        \"id\": 1,\n                                        \"version\": \"1.0\"\n                                    },\n                                    {\n                                        \"id\": 2,\n                                        \"test\": \"1.1\"\n                                    }\n                                ]\"\"\"))\n        );\n    }\n\n    protected void stubForInvalidJsonResponse() {\n        wireMockServer.stubFor(get(urlEqualTo(\"/invalid/json\"))\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withHeader(\"Content-Type\", \"application/json\")\n                        .withHeader(\"Accept\", \"application/json\")\n                        .withBody(\"\"))\n        );\n    }\n\n    protected void stubForFault() {\n        wireMockServer.stubFor(get(urlEqualTo(\"/fault\"))\n                .willReturn(aResponse()\n                        .withFault(Fault.EMPTY_RESPONSE)\n                        .withHeader(\"Content-Type\", \"text/plain\")\n                        .withHeader(\"Accept\", \"text/plain\"))\n        );\n    }\n\n    protected void stubForStringResponse() {\n        wireMockServer.stubFor(get(urlEqualTo(\"/string\"))\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withHeader(\"Content-Type\", \"text/plain\")\n                        .withHeader(\"Accept\", \"text/plain\")\n                        .withBody(\"Test\"))\n        );\n    }\n\n    protected void stubForPostRequest() {\n        wireMockServer.stubFor(post(urlEqualTo(\"/post\"))\n                .willReturn(aResponse()\n                        .withStatus(201)\n                        .withHeader(\"Content-Type\", \"application/json\")\n                        .withHeader(\"Accept\", \"application/json\")\n                        .withBody(\"\"\"\n                                {\n                                  \"message\": \"Success\"\n                                }\"\"\"))\n        );\n    }\n\n    protected void stubForGetSecureEndpoint() {\n        wireMockServer.stubFor(get(urlEqualTo(\"/secure\"))\n                .withBasicAuth(\"cn=test\", \"password\")\n                .willReturn(aResponse()\n                        .withStatus(401)\n                        .withBody(\"Unauthorized\"))\n        );\n    }\n\n    protected void stubForGetWithQueryParams() {\n        wireMockServer.stubFor(get(urlPathEqualTo(\"/query\"))\n                .withQueryParam(\"key\", equalTo(\"value with space\"))\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withBody(\"Success response\"))\n        );\n    }\n\n    protected void stubForPostSecureEndpoint() {\n        wireMockServer.stubFor(post(urlEqualTo(\"/secure\"))\n                .withBasicAuth(\"cn=test\", \"password\")\n                .willReturn(aResponse()\n                        .withStatus(201)\n                        .withBody(\"Success response\"))\n        );\n    }\n\n    protected void stubForPostRequestForRequestTypeFile() {\n        wireMockServer.stubFor(post(urlEqualTo(\"/file\"))\n                .withHeader(\"Content-Type\", equalTo(\"application/octet-stream\"))\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withHeader(\"Content-Type\", \"application/json\")\n                        .withBody(\"\"\"\n                                {\n                                  \"testObject\": {\n                                    \"testString\": \"hello\",\n                                    \"testInteger\": \"2\"\n                                  }\n                                }\"\"\"))\n        );\n    }\n\n\n    protected void stubForGetRequestForResponseTypeStringFile() {\n        wireMockServer.stubFor(get(urlEqualTo(\"/stringFile\"))\n                .withHeader(\"Accept\", equalTo(\"*/*\"))\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withHeader(\"Content-Type\", \"application/octet-stream\")\n                        .withBodyFile(\"file.bin\"))\n        );\n    }\n\n    protected void stubForGetRequestForResponseTypeStringFileWithNoFilename() {\n        wireMockServer.stubFor(get(urlEqualTo(\"/fileUrlWithoutName/\"))\n                .withHeader(\"Accept\", equalTo(\"*/*\"))\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withHeader(\"Content-Type\", \"application/octet-stream\")\n                        .withBodyFile(\"file.bin\"))\n        );\n    }\n\n    protected void stubForGetRequestForResponseTypeJSONFile() {\n        wireMockServer.stubFor(get(urlEqualTo(\"/JSONFile\"))\n                .withHeader(\"Accept\", equalTo(\"*/*\"))\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withHeader(\"Content-Type\", \"application/octet-stream\")\n                        .withBodyFile(\"jsonFile.bin\"))\n        );\n    }\n\n\n    protected void stubForUnsuccessfulResponse() {\n        wireMockServer.stubFor(get(urlEqualTo(\"/unsuccessful\"))\n                .willReturn(aResponse()\n                        .withStatus(400)\n                        .withBody(\"Error string\"))\n        );\n    }\n\n    protected void stubForDeleteRequest() {\n        wireMockServer.stubFor(delete(urlEqualTo(\"/delete\"))\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withHeader(\"Content-Type\", \"application/json\")\n                        .withHeader(\"Accept\", \"application/json\")\n                        .withBody(\"\"\"\n                                {\n                                  \"message\": \"Success\"\n                                }\"\"\"))\n        );\n    }\n\n    protected void stubForPatchRequest() {\n        wireMockServer.stubFor(patch(urlEqualTo(\"/patch\"))\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withHeader(\"Content-Type\", \"application/json\")\n                        .withHeader(\"Accept\", \"application/json\")\n                        .withBody(\"\"\"\n                                {\n                                  \"message\": \"Success\"\n                                }\"\"\"))\n        );\n    }\n\n    protected void stubForRequestTimeout() {\n        wireMockServer.stubFor(get(urlEqualTo(\"/requestTimeout\"))\n                .willReturn(aResponse()\n                        .withStatus(200)\n                        .withHeader(\"Content-Type\", \"text/plain\")\n                        .withHeader(\"Accept\", \"text/plain\")\n                        .withBody(\"Request timeout\")\n                        .withFixedDelay(8000))\n        );\n    }\n\n    protected void stubForFollowRedirect() {\n        wireMockServer.stubFor(get(urlEqualTo(\"/followRedirects\"))\n                .willReturn(permanentRedirect(url(wireMockServer(), \"/string\")))\n        );\n    }\n\n    protected void stubForFollowRedirectPost() {\n        wireMockServer.stubFor(post(urlEqualTo(\"/followRedirectsPost\"))\n                .willReturn(permanentRedirect(url(wireMockServer(), \"/string\")))\n        );\n    }\n\n    private static String url(WireMockServer wireMockServer, String path) {\n        if (!path.startsWith(\"/\")) {\n            path = \"/\" + path;\n        }\n\n        return String.format(\"%s%s\", \"http://localhost:\" + wireMockServer.port(), path);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/http/src/test/resources/logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss} [%-5level] %logger{12} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <logger name=\"org.eclipse.jetty\" level=\"ERROR\"/>\n    <logger name=\"com.walmartlabs.concord\" level=\"INFO\"/>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "plugins/tasks/kv/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>kv-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-client2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <configuration>\n                    <!-- no verify, as this plugin depends on runtime-v2 -->\n                    <skipVerify>true</skipVerify>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/tasks/kv/src/main/java/com/walmartlabs/concord/plugins/kv/Constants.java",
    "content": "package com.walmartlabs.concord.plugins.kv;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic final class Constants {\n\n    public static final int RETRY_COUNT = 3;\n    public static final long RETRY_INTERVAL = 5000;\n\n    private Constants() {\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/kv/src/main/java/com/walmartlabs/concord/plugins/kv/KvTask.java",
    "content": "package com.walmartlabs.concord.plugins.kv;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClientConfiguration;\nimport com.walmartlabs.concord.client2.ApiClientFactory;\nimport com.walmartlabs.concord.client2.ProcessKvStoreApi;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.*;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.util.UUID;\n\n@Named(\"kv\")\npublic class KvTask implements Task {\n\n    private final ApiClientFactory apiClientFactory;\n\n    @InjectVariable(Constants.Context.CONTEXT_KEY)\n    private Context context;\n\n    @Inject\n    public KvTask(ApiClientFactory apiClientFactory) {\n        this.apiClientFactory = apiClientFactory;\n    }\n\n    /**\n     * @deprecated left for backward compatibility, prefer {@link #remove(Context, String)}\n     */\n    @Deprecated\n    public void remove(@InjectVariable(\"txId\") String instanceId, String key) throws Exception {\n        remove(context, key);\n    }\n\n    public void remove(@InjectVariable(\"context\") Context ctx, String key) throws Exception {\n        ProcessKvStoreApi api = getApi(ctx);\n        UUID txId = ContextUtils.getTxId(ctx);\n        KvTaskUtils.remove(api, txId, key);\n    }\n\n    /**\n     * @deprecated left for backward compatibility, prefer {@link #putString(Context, String, String)}\n     */\n    @Deprecated\n    public void putString(@InjectVariable(\"txId\") String instanceId, String key, String value) throws Exception {\n        putString(context, key, value);\n    }\n\n    public void putString(@InjectVariable(\"context\") Context ctx, String key, String value) throws Exception {\n        ProcessKvStoreApi api = getApi(ctx);\n        UUID txId = ContextUtils.getTxId(ctx);\n        KvTaskUtils.putString(api, txId, key, value);\n\n    }\n\n    /**\n     * @deprecated left for backward compatibility, prefer {@link #getString(Context, String)}\n     */\n    @Deprecated\n    public String getString(@InjectVariable(\"txId\") String instanceId, String key) throws Exception {\n        return getString(context, key);\n    }\n\n    public String getString(@InjectVariable(\"context\") Context ctx, String key) throws Exception {\n        ProcessKvStoreApi api = getApi(ctx);\n        UUID txId = ContextUtils.getTxId(ctx);\n        return KvTaskUtils.getString(api, txId, key);\n    }\n\n    /**\n     * @deprecated left for backward compatibility, prefer {@link #putLong(Context, String, Long)}\n     */\n    @Deprecated\n    public void putLong(@InjectVariable(\"txId\") String instanceId, String key, Long value) throws Exception {\n        putLong(context, key, value);\n    }\n\n    public void putLong(@InjectVariable(\"context\") Context ctx, String key, Long value) throws Exception {\n        ProcessKvStoreApi api = getApi(ctx);\n        UUID txId = ContextUtils.getTxId(ctx);\n        KvTaskUtils.putLong(api, txId, key, value);\n    }\n\n    /**\n     * @deprecated left for backward compatibility, prefer {@link #getLong(Context, String)}\n     */\n    @Deprecated\n    public Long getLong(@InjectVariable(\"txId\") String instanceId, String key) throws Exception {\n        return getLong(context, key);\n    }\n\n    public Long getLong(@InjectVariable(\"context\") Context ctx, String key) throws Exception {\n        ProcessKvStoreApi api = getApi(ctx);\n        UUID txId = ContextUtils.getTxId(ctx);\n        return KvTaskUtils.getLong(api, txId, key);\n    }\n\n    /**\n     * @deprecated left for backward compatibility, prefer {@link #inc(Context, String)}\n     */\n    @Deprecated\n    public long inc(@InjectVariable(\"txId\") String instanceId, String key) throws Exception {\n        return inc(context, key);\n    }\n\n    public long inc(@InjectVariable(\"context\") Context ctx, String key) throws Exception {\n        return incLong(ctx, key);\n    }\n\n    /**\n     * @deprecated left for backward compatibility, prefer {@link #incLong(Context, String)}\n     */\n    @Deprecated\n    public long incLong(@InjectVariable(\"txId\") String instanceId, String key) throws Exception {\n        return incLong(context, key);\n    }\n\n    public long incLong(@InjectVariable(\"context\") Context ctx, String key) throws Exception {\n        ProcessKvStoreApi api = getApi(ctx);\n        UUID txId = ContextUtils.getTxId(ctx);\n        return KvTaskUtils.incLong(api, txId, key);\n    }\n\n    private ProcessKvStoreApi getApi(Context ctx) {\n        return new ProcessKvStoreApi(apiClientFactory.create(ApiClientConfiguration.builder()\n                .sessionToken(ContextUtils.getSessionToken(ctx))\n                .build()));\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/kv/src/main/java/com/walmartlabs/concord/plugins/kv/KvTaskUtils.java",
    "content": "package com.walmartlabs.concord.plugins.kv;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ClientUtils;\nimport com.walmartlabs.concord.client2.ProcessKvStoreApi;\n\nimport java.util.UUID;\n\npublic final class KvTaskUtils {\n\n    public static void remove(ProcessKvStoreApi api, UUID txId, String key) throws Exception {\n        assertValidKey(key);\n        ClientUtils.withRetry(Constants.RETRY_COUNT, Constants.RETRY_INTERVAL, () -> {\n            api.deleteKv(txId, key);\n            return null;\n        });\n    }\n\n    public static void putString(ProcessKvStoreApi api, UUID txId, String key, String value) throws Exception {\n        assertValidKey(key);\n        ClientUtils.withRetry(Constants.RETRY_COUNT, Constants.RETRY_INTERVAL, () -> {\n            api.putKvString(txId, key, value);\n            return null;\n        });\n    }\n\n    public static String getString(ProcessKvStoreApi api, UUID txId, String key) throws Exception {\n        assertValidKey(key);\n        return ClientUtils.withRetry(Constants.RETRY_COUNT, Constants.RETRY_INTERVAL, () ->\n                api.getKvString(txId, key));\n    }\n\n    public static void putLong(ProcessKvStoreApi api, UUID txId, String key, Long value) throws Exception {\n        assertValidKey(key);\n        ClientUtils.withRetry(Constants.RETRY_COUNT, Constants.RETRY_INTERVAL, () -> {\n            api.putKvLong(txId, key, value);\n            return null;\n        });\n    }\n\n    public static Long getLong(ProcessKvStoreApi api, UUID txId, String key) throws Exception {\n        assertValidKey(key);\n        return ClientUtils.withRetry(Constants.RETRY_COUNT, Constants.RETRY_INTERVAL, () ->\n                api.getKvLong(txId, key));\n    }\n\n    public static long incLong(ProcessKvStoreApi api, UUID txId, String key) throws Exception {\n        assertValidKey(key);\n        return ClientUtils.withRetry(Constants.RETRY_COUNT, Constants.RETRY_INTERVAL, () ->\n                api.incKvLong(txId, key));\n    }\n\n    private static void assertValidKey(String s) {\n        if (s == null || s.isEmpty()) {\n            throw new IllegalArgumentException(\"Keys cannot be empty or null\");\n        }\n    }\n\n    private KvTaskUtils() {\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/kv/src/main/java/com/walmartlabs/concord/plugins/kv/KvTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.kv;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.client2.ProcessKvStoreApi;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.DryRunReady;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.util.UUID;\n\n@Named(\"kv\")\n@SuppressWarnings(\"unused\")\n@DryRunReady\npublic class KvTaskV2 implements Task {\n\n    private static final Logger log = LoggerFactory.getLogger(KvTaskV2.class);\n\n    private final ApiClient apiClient;\n    private final UUID processInstanceId;\n    private final boolean dryRunMode;\n\n    @Inject\n    public KvTaskV2(ApiClient apiClient, Context context) {\n        this.apiClient = apiClient;\n        this.processInstanceId = context.processInstanceId();\n        this.dryRunMode = context.processConfiguration().dryRun();\n    }\n\n    public void remove(String key) throws Exception {\n        if (dryRunMode) {\n            log.info(\"Running in dry-run mode: Skipping removing key '{}'\", key);\n            return;\n        }\n\n        ProcessKvStoreApi api = new ProcessKvStoreApi(apiClient);\n        KvTaskUtils.remove(api, processInstanceId, key);\n    }\n\n    public void putString(String key, String value) throws Exception {\n        if (dryRunMode) {\n            log.info(\"Running in dry-run mode: Skipping putString with key '{}'\", key);\n            return;\n        }\n\n        ProcessKvStoreApi api = new ProcessKvStoreApi(apiClient);\n        KvTaskUtils.putString(api, processInstanceId, key, value);\n    }\n\n    public String getString(String key) throws Exception {\n        ProcessKvStoreApi api = new ProcessKvStoreApi(apiClient);\n        return KvTaskUtils.getString(api, processInstanceId, key);\n    }\n\n    public void putLong(String key, Long value) throws Exception {\n        if (dryRunMode) {\n            log.info(\"Running in dry-run mode: Skipping putLong with key '{}'\", key);\n            return;\n        }\n\n        ProcessKvStoreApi api = new ProcessKvStoreApi(apiClient);\n        KvTaskUtils.putLong(api, processInstanceId, key, value);\n    }\n\n    public Long getLong(String key) throws Exception {\n        ProcessKvStoreApi api = new ProcessKvStoreApi(apiClient);\n        return KvTaskUtils.getLong(api, processInstanceId, key);\n    }\n\n    public long inc(String key) throws Exception {\n        return incLong(key);\n    }\n\n    public long incLong(String key) throws Exception {\n        ProcessKvStoreApi api = new ProcessKvStoreApi(apiClient);\n        return KvTaskUtils.incLong(api, processInstanceId, key);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/locale/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>locale-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <configuration>\n                    <!-- no verify, as this plugin depends on runtime-v2 -->\n                    <skipVerify>true</skipVerify>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/tasks/locale/src/main/java/com/walmartlabs/concord/plugins/locale/LocaleTask.java",
    "content": "package com.walmartlabs.concord.plugins.locale;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Task;\n\nimport javax.inject.Named;\nimport java.util.Locale;\n\n@Named(\"locale\")\npublic class LocaleTask implements Task {\n\n    public String[] countries() {\n        return Locale.getISOCountries();\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/locale/src/main/java/com/walmartlabs/concord/plugins/locale/LocaleTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.locale;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.DryRunReady;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\n\nimport javax.inject.Named;\nimport java.util.Locale;\n\n@Named(\"locale\")\n@DryRunReady\npublic class LocaleTaskV2 implements Task {\n\n    public String[] countries() {\n        return Locale.getISOCountries();\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/lock/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>lock-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-client2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <configuration>\n                    <!-- no verify, as this plugin depends on runtime-v2 -->\n                    <skipVerify>true</skipVerify>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/tasks/lock/src/main/java/com/walmartlabs/concord/plugins/lock/Constants.java",
    "content": "package com.walmartlabs.concord.plugins.lock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic final class Constants {\n\n    public static final int RETRY_COUNT = 3;\n    public static final long RETRY_INTERVAL = 5000;\n\n    public static final String LOCK_NAME_KEY = \"name\";\n    public static final String SCOPE_KEY = \"scope\";\n    public static final String PROJECT_SCOPE = \"PROJECT\";\n\n    private Constants() {\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/lock/src/main/java/com/walmartlabs/concord/plugins/lock/LockTask.java",
    "content": "package com.walmartlabs.concord.plugins.lock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.client2.ApiClientConfiguration;\nimport com.walmartlabs.concord.client2.ApiClientFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.sdk.*;\nimport com.walmartlabs.concord.sdk.Constants;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.util.UUID;\n\n@Named(\"lock\")\n@SuppressWarnings(\"unused\")\npublic class LockTask implements Task {\n\n    private final ApiClientFactory apiClientFactory;\n\n    @InjectVariable(Constants.Context.CONTEXT_KEY)\n    Context context;\n\n    @Inject\n    public LockTask(ApiClientFactory apiClientFactory) {\n        this.apiClientFactory = apiClientFactory;\n    }\n\n    public void lock(@InjectVariable(\"txId\") String instanceId, String lockName, String scope) throws Exception {\n        ApiClient apiClient = apiClientFactory.create(ApiClientConfiguration.builder()\n                .sessionToken(ContextUtils.getSessionToken(context))\n                .build());\n\n        TaskResult taskResult = new LockTaskCommon(apiClient, UUID.fromString(instanceId))\n                .lock(lockName, scope);\n        if (taskResult instanceof TaskResult.SuspendResult) {\n            context.suspend(((TaskResult.SuspendResult) taskResult).eventName());\n        }\n    }\n\n    public void unlock(@InjectVariable(\"txId\") String instanceId, String lockName, String scope) throws Exception {\n        ApiClient apiClient = apiClientFactory.create(ApiClientConfiguration.builder()\n                .sessionToken(ContextUtils.getSessionToken(context))\n                .build());\n\n        new LockTaskCommon(apiClient, UUID.fromString(instanceId))\n                .unlock(lockName, scope);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/lock/src/main/java/com/walmartlabs/concord/plugins/lock/LockTaskCommon.java",
    "content": "package com.walmartlabs.concord.plugins.lock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.Callable;\n\nimport static com.walmartlabs.concord.plugins.lock.Constants.RETRY_COUNT;\nimport static com.walmartlabs.concord.plugins.lock.Constants.RETRY_INTERVAL;\n\npublic class LockTaskCommon {\n\n    private static final Logger log = LoggerFactory.getLogger(LockTaskCommon.class);\n\n    private final ProcessApi processApi;\n    private final ProcessLocksApi lockApi;\n    private final UUID instanceId;\n\n    public LockTaskCommon(ApiClient apiClient, UUID instanceId) {\n        this.processApi = new ProcessApi(apiClient);\n        this.lockApi = new ProcessLocksApi(apiClient);\n        this.instanceId = instanceId;\n    }\n\n    public TaskResult lock(String lockName, String lockScope) throws ApiException {\n        log.info(\"Locking '{}' with scope '{}'...\", lockName, lockScope);\n\n        if (lockName == null) {\n            throw new IllegalArgumentException(\"Mandatory variable 'lockName' is required\");\n        }\n\n        LockResult lock = withRetry(() -> lockApi.tryLock(instanceId, lockName, checkScope(lockScope)));\n\n        boolean result = lock.getAcquired();\n        if (!result) {\n            withRetry(() -> {\n                processApi.setWaitCondition(instanceId, createCondition(lock.getInfo()));\n                return null;\n            });\n\n            return TaskResult.suspend(lockName);\n        }\n        return TaskResult.success();\n    }\n\n    public void unlock(String lockName, String lockScope) throws ApiException {\n        log.info(\"Unlocking '{}' with scope '{}'...\", lockName, lockScope);\n\n        withRetry(() -> {\n            lockApi.unlock(instanceId, lockName, checkScope(lockScope));\n            return null;\n        });\n    }\n\n    private String checkScope(String scope) {\n        if (scope == null || scope.isEmpty()) {\n            return null;\n        }\n\n        if (!scope.equalsIgnoreCase(\"ORG\")\n                && !scope.equalsIgnoreCase(\"PROJECT\")) {\n\n            throw new IllegalArgumentException(\"Unknown scope '\" + scope + \"', expected: org or project\");\n        }\n\n        return scope.toUpperCase();\n    }\n\n    /**\n     *  @see com.walmartlabs.concord.server.process.waits.ProcessLockCondition\n      */\n    private static Map<String, Object> createCondition(LockEntry lock) {\n        Map<String, Object> condition = new HashMap<>();\n        condition.put(\"type\", \"PROCESS_LOCK\");\n        condition.put(\"instanceId\", lock.getInstanceId());\n        condition.put(\"orgId\", lock.getOrgId());\n        condition.put(\"projectId\", lock.getProjectId());\n        condition.put(\"scope\", lock.getScope());\n        condition.put(\"name\", lock.getName());\n        return condition;\n    }\n\n    private static <T> T withRetry(Callable<T> c) throws ApiException {\n        return ClientUtils.withRetry(RETRY_COUNT, RETRY_INTERVAL, c);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/lock/src/main/java/com/walmartlabs/concord/plugins/lock/TaskParams.java",
    "content": "package com.walmartlabs.concord.plugins.lock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport static com.walmartlabs.concord.plugins.lock.Constants.*;\n\npublic class TaskParams {\n\n    private final Variables input;\n\n    public TaskParams(Variables input) {\n        this.input = input;\n    }\n\n    public String lockName() {\n        return input.assertString(LOCK_NAME_KEY);\n    }\n\n    public String scope() {\n        return input.getString(SCOPE_KEY, PROJECT_SCOPE);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/lock/src/main/java/com/walmartlabs/concord/plugins/lock/v2/LockTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.lock.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.plugins.lock.LockTaskCommon;\nimport com.walmartlabs.concord.plugins.lock.TaskParams;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\n\n@Named(\"lock\")\n@DryRunReady\n@SuppressWarnings(\"unused\")\npublic class LockTaskV2 implements Task {\n\n    private final LockTaskCommon delegate;\n    private final Context context;\n\n    @Inject\n    public LockTaskV2(ApiClient apiClient, Context context) {\n        this.delegate = new LockTaskCommon(apiClient, context.processInstanceId());\n        this.context = context;\n    }\n\n    @Override\n    public TaskResult execute(Variables input) throws Exception {\n        TaskParams params = new TaskParams(input);\n\n        return delegate.lock(params.lockName(), params.scope());\n    }\n\n    public void lock(String lockName, String scope) throws Exception {\n        TaskResult taskResult = delegate.lock(lockName, scope);\n        if (taskResult instanceof TaskResult.SuspendResult) {\n            context.suspend(((TaskResult.SuspendResult) taskResult).eventName());\n        }\n    }\n\n    public void unlock(String lockName, String scope) throws Exception {\n        delegate.unlock(lockName, scope);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/lock/src/main/java/com/walmartlabs/concord/plugins/lock/v2/UnlockTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.lock.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.plugins.lock.LockTaskCommon;\nimport com.walmartlabs.concord.plugins.lock.TaskParams;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\n\n@Named(\"unlock\")\n@DryRunReady\npublic class UnlockTaskV2 implements Task {\n\n    private final LockTaskCommon delegate;\n\n    @Inject\n    public UnlockTaskV2(ApiClient apiClient, Context context) {\n        this.delegate = new LockTaskCommon(apiClient, context.processInstanceId());\n    }\n\n    public TaskResult execute(Variables input) throws Exception {\n        TaskParams params = new TaskParams(input);\n\n        delegate.unlock(params.lockName(), params.scope());\n\n        return TaskResult.success();\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/log/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>log-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-core</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.dataformat</groupId>\n            <artifactId>jackson-dataformat-yaml</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jsr310</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jdk8</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <configuration>\n                    <!-- no verify, as this plugin depends on runtime-v2 -->\n                    <skipVerify>true</skipVerify>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/tasks/log/src/main/java/com/walmartlabs/concord/plugins/log/LogDebugTask.java",
    "content": "package com.walmartlabs.concord.plugins.log;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.Task;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Named;\n\nimport static com.walmartlabs.concord.sdk.ContextUtils.assertString;\n\n@Named(\"logDebug\")\npublic class LogDebugTask implements Task {\n\n    public static final Logger log = LoggerFactory.getLogger(LogDebugTask.class);\n\n    public void call(String s) {\n        log.debug(s);\n    }\n\n    @Override\n    public void execute(Context ctx) {\n        String msg = assertString(ctx, \"msg\");\n        log.debug(msg);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/log/src/main/java/com/walmartlabs/concord/plugins/log/LogErrorTask.java",
    "content": "package com.walmartlabs.concord.plugins.log;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.Task;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Named;\n\nimport static com.walmartlabs.concord.sdk.ContextUtils.assertString;\n\n@Named(\"logError\")\npublic class LogErrorTask implements Task {\n\n    public static final Logger log = LoggerFactory.getLogger(LogErrorTask.class);\n\n    public void call(String s) {\n        log.error(s);\n    }\n\n    @Override\n    public void execute(Context ctx) {\n        String msg = assertString(ctx, \"msg\");\n        log.error(msg);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/log/src/main/java/com/walmartlabs/concord/plugins/log/LogUtils.java",
    "content": "package com.walmartlabs.concord.plugins.log;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic class LogUtils {\n\n    public static final Logger log = LoggerFactory.getLogger(LogUtils.class);\n\n    private LogUtils() { }\n\n    public static void error(String s) {\n        log.error(s);\n    }\n\n    public static void error(Object o) {\n        log.error(\"{}\", o);\n    }\n\n    public static void warn(String s) {\n        log.warn(s);\n    }\n\n    public static void warn(Object o) {\n        log.warn(\"{}\", o);\n    }\n\n    public static void debug(String s) {\n        log.debug(s);\n    }\n\n    public static void debug(Object o) {\n        log.debug(\"{}\", o);\n    }\n\n    public static void info(String s) {\n        log.info(s);\n    }\n\n    public static void info(Object o) {\n        log.info(\"{}\", o);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/log/src/main/java/com/walmartlabs/concord/plugins/log/LogWarnTask.java",
    "content": "package com.walmartlabs.concord.plugins.log;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.Task;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Named;\n\nimport static com.walmartlabs.concord.sdk.ContextUtils.assertString;\n\n@Named(\"logWarn\")\npublic class LogWarnTask implements Task {\n\n    public static final Logger log = LoggerFactory.getLogger(LogWarnTask.class);\n\n    public void call(String s) {\n        log.warn(s);\n    }\n\n    @Override\n    public void execute(Context ctx) {\n        String msg = assertString(ctx, \"msg\");\n        log.warn(msg);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/log/src/main/java/com/walmartlabs/concord/plugins/log/LoggingTask.java",
    "content": "package com.walmartlabs.concord.plugins.log;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.Task;\n\nimport javax.inject.Named;\n\n@Named(\"log\")\npublic class LoggingTask implements Task {\n\n    public void debug(String s) {\n        LogUtils.debug(s);\n    }\n\n    public void info(String s) {\n        LogUtils.info(s);\n    }\n\n    /**\n     * @deprecated use {@link #info(String)} or {@link #call(String)}\n     */\n    @Deprecated\n    public void info(String logName, String s) {\n        LogUtils.info(String.format(\"%s - %s\", logName, s));\n    }\n\n    public void warn(String s) {\n        LogUtils.warn(s);\n    }\n\n    /**\n     * @deprecated use {@link #warn(String)} or {@link #call(String)}\n     */\n    @Deprecated\n    public void warn(String logName, String s) {\n        LogUtils.warn(String.format(\"%s - %s\", logName, s));\n    }\n\n    public void error(String s) {\n        LogUtils.error(s);\n    }\n\n    public void error(String logName, String s) {\n        LogUtils.error(String.format(\"%s - %s\", logName, s));\n    }\n\n    public void call(String s) {\n        LogUtils.info(s);\n    }\n\n    @Override\n    public void execute(Context ctx) {\n        String msg = assertString(ctx, \"msg\");\n        LogUtils.info(msg);\n    }\n\n    private static String assertString(Context ctx, String k) {\n        Object v = ctx.getVariable(k);\n        if (v == null) {\n            throw new IllegalArgumentException(\"Required parameter '\" + k + \"' is missing\");\n        }\n        return v.toString();\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/log/src/main/java/com/walmartlabs/concord/plugins/log/LoggingTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.log;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonFactory;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLFactory;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport com.walmartlabs.concord.runtime.v2.sdk.DryRunReady;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\n\nimport javax.inject.Named;\n\n@Named(\"log\")\n@DryRunReady\npublic class LoggingTaskV2 implements Task {\n\n    private static final ObjectMapper yamlObjectMapper = createYamlObjectMapper();\n\n    @Override\n    public TaskResult execute(Variables variables) {\n        Object msg = variables.get(\"msg\");\n        String format = variables.getString(\"format\");\n\n        String logLevel = variables.getString(\"level\", \"INFO\");\n        switch (logLevel.toUpperCase()) {\n            case \"DEBUG\": {\n                LogUtils.debug(formatMessage(format, msg));\n                break;\n            }\n            case \"INFO\": {\n                LogUtils.info(formatMessage(format, msg));\n                break;\n            }\n            case \"WARN\": {\n                LogUtils.warn(formatMessage(format, msg));\n                break;\n            }\n            case \"ERROR\": {\n                LogUtils.error(formatMessage(format, msg));\n                break;\n            }\n            default:\n                LogUtils.info(formatMessage(format, msg));\n        }\n\n        return TaskResult.success();\n    }\n\n    public static void info(String s) {\n        LogUtils.info(s);\n    }\n\n    public static void info(Object o) {\n        LogUtils.info(o);\n    }\n\n    public static void debug(String s) {\n        LogUtils.debug(s);\n    }\n\n    public static void debug(Object o) {\n        LogUtils.debug(o);\n    }\n\n    public static void warn(String s) {\n        LogUtils.warn(s);\n    }\n\n    public static void warn(Object o) {\n        LogUtils.warn(o);\n    }\n\n    public static void error(String s) {\n        LogUtils.error(s);\n    }\n\n    public static void error(Object o) {\n        LogUtils.error(o);\n    }\n\n    public void call(String s) {\n        LogUtils.info(s);\n    }\n\n    private static Object formatMessage(String format, Object msg) {\n        if (format == null || format.trim().isEmpty()) {\n            return msg;\n        }\n\n        if (\"yaml\".equalsIgnoreCase(format)) {\n            try {\n                return \"\\n\" + yamlObjectMapper.writerWithDefaultPrettyPrinter()\n                        .writeValueAsString(msg);\n            } catch (Exception e) {\n                throw new RuntimeException(\"Invalid yaml:\" + e.getMessage());\n            }\n        }\n\n        throw new IllegalArgumentException(\"Unknown format '\" + format + \"'\");\n    }\n\n    private static ObjectMapper createYamlObjectMapper() {\n        return defaultObjectMapper(new YAMLFactory()\n                                   .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)\n                                   .disable(YAMLGenerator.Feature.SPLIT_LINES));\n    }\n\n    private static ObjectMapper defaultObjectMapper(JsonFactory jf) {\n        ObjectMapper om = new ObjectMapper(jf);\n        om.registerModule(new Jdk8Module());\n        om.registerModule(new JavaTimeModule());\n        return om;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/misc/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>misc-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>io.takari.bpm</groupId>\n            <artifactId>bpm-engine-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <configuration>\n                    <!-- no verify, as this plugin depends on runtime-v2 -->\n                    <skipVerify>true</skipVerify>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/tasks/misc/src/main/java/com/walmartlabs/concord/plugins/misc/Base64TaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.misc;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.DryRunReady;\nimport com.walmartlabs.concord.runtime.v2.sdk.SensitiveDataHolder;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.util.Base64;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\n@Named(\"base64\")\n@DryRunReady\npublic class Base64TaskV2 implements Task {\n\n    private final SensitiveDataHolder sensitiveDataHolder;\n\n    @Inject\n    public Base64TaskV2(SensitiveDataHolder sensitiveDataHolder) {\n        this.sensitiveDataHolder = sensitiveDataHolder;\n    }\n\n    /**\n     * Encodes bytes of a UTF-8 string as a base64 string.\n     */\n    public String encode(String raw) {\n        var result = Base64.getEncoder().encodeToString(raw.getBytes(UTF_8));\n        var isSensitive = sensitiveDataHolder.get().contains(raw);\n        if (isSensitive) {\n            sensitiveDataHolder.add(result);\n        }\n        return result;\n    }\n\n    /**\n     * Decodes a base64-encoded string.\n     */\n    public String decode(String base64) {\n        var result = new String(Base64.getDecoder().decode(base64), UTF_8);\n        var isSensitive = sensitiveDataHolder.get().contains(base64);\n        if (isSensitive) {\n            sensitiveDataHolder.add(result);\n        }\n        return result;\n\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/misc/src/main/java/com/walmartlabs/concord/plugins/misc/CollectionsTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.misc;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.collect.Lists;\nimport com.walmartlabs.concord.runtime.v2.sdk.DryRunReady;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\n\nimport javax.inject.Named;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\n/**\n * Provides utility methods for working with collections\n */\n@Named(\"collections\")\n@DryRunReady\npublic class CollectionsTaskV2 implements Task {\n\n    /**\n     * Concatenates multiple lists into a single list, preserving the order of elements.\n     * Ignores any null lists.\n     *\n     * @param lists one or more lists to concatenate\n     * @return a new list containing all elements from the input lists\n     */\n    @SafeVarargs\n    public static <T> List<T> concat(List<T> ... lists) {\n        return Arrays.stream(lists)\n                .filter(Objects::nonNull)\n                .flatMap(List::stream)\n                .collect(Collectors.toList());\n    }\n\n    /**\n     * Concatenates multiple lists into a single set, eliminating duplicate elements.\n     * Ignores any null lists.\n     *\n     * @param lists one or more lists to concatenate\n     * @return a new set containing all unique elements from the input lists\n     */\n    @SafeVarargs\n    public static <T> Set<T> concatAsSet(List<T> ... lists) {\n        return Arrays.stream(lists)\n                .filter(Objects::nonNull)\n                .flatMap(List::stream)\n                .collect(Collectors.toSet());\n    }\n\n    /**\n     * Reverses the order of elements in the given list.\n     *\n     * @param list the list to reverse\n     * @return a new list with the elements in reverse order\n     */\n    public static <T> List<T> reverse(List<T> list) {\n        return Lists.reverse(list);\n    }\n\n    /**\n     * Creates a new list of integers from 0 to the specified size (exclusive).\n     * Each element in the list corresponds to its index.\n     *\n     * @param size the size of the list to create\n     * @return a list of integers from 0 to (size - 1)\n     */\n    public static List<Integer> range(int size) {\n        List<Integer> result = new ArrayList<>(size);\n        for (int i = 0; i < size; i++) {\n            result.add(i);\n        }\n        return result;\n    }\n\n    /**\n     * Creates a new instance of {@link LinkedHashMap} with {@code String} keys and {@code Object} values.\n     *\n     * @return a new, empty {@code LinkedHashMap<String, Object>}\n     */\n    public static Map<String, Object> newMap() {\n        return new LinkedHashMap<>();\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/misc/src/main/java/com/walmartlabs/concord/plugins/misc/DateTimeTask.java",
    "content": "package com.walmartlabs.concord.plugins.misc;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Task;\n\nimport javax.inject.Named;\nimport java.text.ParseException;\nimport java.text.SimpleDateFormat;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Date;\n\n@Named(\"datetime\")\npublic class DateTimeTask implements Task {\n\n    public Date current() {\n        return new Date();\n    }\n\n    public String current(String pattern) {\n        return DateTimeFormatter.ofPattern(pattern).format(ZonedDateTime.now());\n    }\n\n    public String currentWithZone(String zone, String pattern) {\n        return DateTimeFormatter.ofPattern(pattern).format(ZonedDateTime.now(ZoneId.of(zone)));\n    }\n\n    public String format(Date date, String pattern) {\n        return new SimpleDateFormat(pattern).format(date);\n    }\n\n    public Date parse(String src, String pattern) throws ParseException {\n        return new SimpleDateFormat(pattern).parse(src);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/misc/src/main/java/com/walmartlabs/concord/plugins/misc/DateTimeTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.misc;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.DryRunReady;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\n\nimport javax.inject.Named;\nimport java.text.ParseException;\nimport java.text.SimpleDateFormat;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.time.format.DateTimeFormatter;\nimport java.util.Date;\n\n@Named(\"datetime\")\n@DryRunReady\npublic class DateTimeTaskV2 implements Task {\n\n    public Date current() {\n        return new Date();\n    }\n\n    public String current(String pattern) {\n        return DateTimeFormatter.ofPattern(pattern).format(ZonedDateTime.now());\n    }\n\n    public String currentISO() {\n        return current(\"yyyy-MM-dd'T'HH:mm:ss.SSSXXX\");\n    }\n\n    public String currentWithZone(String zone, String pattern) {\n        return DateTimeFormatter.ofPattern(pattern).format(ZonedDateTime.now(ZoneId.of(zone)));\n    }\n\n    public String format(Date date, String pattern) {\n        return new SimpleDateFormat(pattern).format(date);\n    }\n\n    public Date parse(String src, String pattern) throws ParseException {\n        return new SimpleDateFormat(pattern).parse(src);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/misc/src/main/java/com/walmartlabs/concord/plugins/misc/EnvTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.misc;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.DryRunReady;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\n\nimport javax.inject.Named;\n\n/**\n * Provides access to environment variables.\n */\n@Named(\"env\")\n@DryRunReady\npublic class EnvTaskV2 implements Task {\n\n    /**\n     * Retrieves the value of the environment variable for the given key.\n     * If the variable is not set, returns {@code null}.\n     *\n     * @param key the name of the environment variable to retrieve\n     * @return the value of the environment variable, or {@code null} if not set\n     */\n    public String get(String key) {\n        return getOrDefault(key, null);\n    }\n\n    /**\n     * Retrieves the value of the environment variable for the given key.\n     * If the variable is not set, returns the specified default value.\n     *\n     * @param key the name of the environment variable to retrieve\n     * @param defaultValue the value to return if the environment variable is not set\n     * @return the value of the environment variable, or the default value if not set\n     */\n    public String getOrDefault(String key, String defaultValue) {\n        String result = System.getenv(key);\n        if (result != null) {\n            return result;\n        }\n        return defaultValue;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/misc/src/main/java/com/walmartlabs/concord/plugins/misc/MiscTask.java",
    "content": "package com.walmartlabs.concord.plugins.misc;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Task;\nimport io.takari.bpm.api.BpmnError;\n\nimport javax.inject.Named;\n\n@Named(\"misc\")\npublic class MiscTask implements Task {\n\n    public void throwRuntimeException(String message) {\n        throw new RuntimeException(message);\n    }\n\n    public void throwBpmnError(String errorRef) {\n        throw new BpmnError(errorRef, new RuntimeException(\"Requested by the user. Error: \" + errorRef));\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/misc/src/main/java/com/walmartlabs/concord/plugins/misc/MiscTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.misc;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.DryRunReady;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\n\nimport javax.inject.Named;\n\n@Named(\"misc\")\n@SuppressWarnings(\"unused\")\n@DryRunReady\npublic class MiscTaskV2 implements Task {\n\n    public void throwRuntimeException(String message) {\n        throw new RuntimeException(message);\n    }\n\n    public String trim(String s, int length) {\n        if (s == null) {\n            return s;\n        }\n\n        if (s.length() <= length) {\n            return s;\n        }\n\n        int newLength = length - 3;\n        return s.substring(0, newLength) + \"...\";\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>mock-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runner-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-vm-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.inject</groupId>\n            <artifactId>guice</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <!-- Immutables -->\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>builder</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.errorprone</groupId>\n            <artifactId>error_prone_annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.eclipse.sisu</groupId>\n            <artifactId>org.eclipse.sisu.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runner-v2-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <dependencies>\n                    <dependency>\n                        <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n                        <artifactId>concord-runner-v2</artifactId>\n                        <version>${project.version}</version>\n                    </dependency>\n                </dependencies>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/InputSanitizer.java",
    "content": "package com.walmartlabs.concord.plugins.mock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic final class InputSanitizer {\n\n    private static final Set<Class<?>> INPUT_VARIABLE_TYPES = Set.of(\n            String.class, Boolean.class, Character.class, Byte.class, Short.class,\n            Integer.class, Long.class, Float.class, Double.class);\n\n    public static List<Object> sanitize(List<Object> input) {\n        var result = new ArrayList<>(input.size());\n        for (var item : input) {\n            result.add(sanitizeValue(item));\n        }\n        return result;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Object sanitizeValue(Object value) {\n        if (value == null) {\n            return null;\n        }\n\n        if (value.getClass().isPrimitive() || INPUT_VARIABLE_TYPES.contains(value.getClass())) {\n            return value;\n        }\n\n        if (value instanceof Set<?> setValue) {\n            return setValue.stream()\n                    .map(InputSanitizer::sanitizeValue)\n                    .filter(Objects::nonNull)\n                    .collect(Collectors.toSet());\n        } else if (value instanceof Collection<?> collectionValue) {\n            return collectionValue.stream()\n                    .map(InputSanitizer::sanitizeValue)\n                    .filter(Objects::nonNull)\n                    .collect(Collectors.toList());\n        } else if (value instanceof Map) {\n            Map<String, Object> mapValue = (Map<String, Object>) value;\n            Map<String, Object> result = new LinkedHashMap<>();\n            mapValue.forEach((k, v) -> result.put(k, sanitizeValue(v)));\n            return result;\n        } else if (value.getClass().isArray()) {\n            return Arrays.stream((Object[]) value)\n                    .map(InputSanitizer::sanitizeValue)\n                    .collect(Collectors.toCollection(ArrayList::new));\n        } else if (value instanceof Variables variables) {\n            return sanitizeValue(variables.toMap());\n        }\n\n        return null;\n    }\n\n    private InputSanitizer() {\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/Invocation.java",
    "content": "package com.walmartlabs.concord.plugins.mock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.common.AllowNulls;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.util.List;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonIgnoreProperties(ignoreUnknown = true)\n@JsonSerialize(as = ImmutableInvocation.class)\n@JsonDeserialize(as = ImmutableInvocation.class)\npublic interface Invocation {\n\n    @Nullable\n    String fileName();\n\n    int line();\n\n    String taskName();\n\n    String methodName();\n\n    @AllowNulls\n    @Value.Default\n    default List<Object> args() {\n        return List.of();\n    }\n\n    static ImmutableInvocation.Builder builder() {\n        return ImmutableInvocation.builder();\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/Invocations.java",
    "content": "package com.walmartlabs.concord.plugins.mock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLFactory;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;\nimport com.walmartlabs.concord.plugins.mock.matcher.ArgsMatcher;\nimport com.walmartlabs.concord.runtime.v2.runner.PersistenceService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Singleton;\nimport java.io.IOException;\nimport java.nio.file.StandardOpenOption;\nimport java.util.List;\n\n@Singleton\npublic class Invocations {\n\n    private static final Logger log = LoggerFactory.getLogger(Invocations.class);\n\n    private static final TypeReference<List<Invocation>> INVOCATIONS_TYPE = new TypeReference<>() {\n    };\n\n    private final PersistenceService persistenceService;\n    private final ObjectMapper objectMapper;\n\n    @Inject\n    public Invocations(PersistenceService persistenceService) {\n        this.persistenceService = persistenceService;\n        this.objectMapper = new ObjectMapper(\n                new YAMLFactory()\n                        .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER));\n    }\n\n    public synchronized void record(Invocation invocation) {\n        String taskName = invocation.taskName();\n        persistenceService.persistFile(\"invocations/\" + taskName,\n                out -> objectMapper.writeValue(out, List.of(invocation)),\n                StandardOpenOption.CREATE, StandardOpenOption.APPEND);\n    }\n\n    public synchronized List<Invocation> find(String taskName, String methodName, Object[] args) {\n        return find(taskName, methodName).stream()\n                .filter(i -> ArgsMatcher.match(i.args(), args))\n                .toList();\n    }\n\n    public synchronized List<Invocation> find(String taskName, String methodName) {\n        var result = persistenceService.loadPersistedFile(\"invocations/\" + taskName, in -> objectMapper.readValue(in, INVOCATIONS_TYPE));\n        if (result == null) {\n            return List.of();\n        }\n        return result.stream()\n                .filter(i -> i.methodName().equals(methodName))\n                .toList();\n    }\n\n    public synchronized void cleanup() {\n        try {\n            persistenceService.deletePersistedFile(\"invocations\");\n        } catch (IOException e) {\n            log.warn(\"Can't cleanup invocations from state: {}\", e.getMessage());\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/InvocationsCollector.java",
    "content": "package com.walmartlabs.concord.plugins.mock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Inject;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.runner.DefaultTaskVariablesService;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallEvent;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallListener;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\nimport com.walmartlabs.concord.svm.*;\nimport com.walmartlabs.concord.svm.Runtime;\n\nimport java.util.List;\n\npublic class InvocationsCollector implements TaskCallListener, ExecutionListener {\n\n    private final InvocationsCollectorParams params;\n    private final Invocations invocations;\n\n    @Inject\n    public InvocationsCollector(DefaultTaskVariablesService defaultTaskVariablesService,\n                                Invocations invocations) {\n\n        this.params = new InvocationsCollectorParams(defaultTaskVariablesService.get(\"invocations\"));\n        this.invocations = invocations;\n    }\n\n    @Override\n    public void onEvent(TaskCallEvent event) {\n        if (!params.enabled() || event.phase() != TaskCallEvent.Phase.PRE) {\n            return;\n        }\n\n        Step currentStep = event.currentStep();\n\n        invocations.record(Invocation.builder()\n                .fileName(currentStep.getLocation().fileName())\n                .line(currentStep.getLocation().lineNum())\n                .taskName(event.taskName())\n                .methodName(event.methodName())\n                .args(sanitizeInput(event.methodName(), event.input()))\n                .build());\n    }\n\n    @Override\n    public void afterProcessEnds(Runtime runtime, State state, Frame lastFrame) {\n        if (!params.enabled()) {\n            return;\n        }\n\n        if (!isSuspended(state)) {\n            invocations.cleanup();\n        }\n    }\n\n    @Override\n    public void onProcessError(Runtime runtime, State state, Exception e) {\n        if (!params.enabled()) {\n            return;\n        }\n\n        invocations.cleanup();\n    }\n\n    private static List<Object> sanitizeInput(String methodName, List<Object> input) {\n        // task call without input -> omit variables so we can use\n        // ${verify.task('testTask', 1).execute()}\n        if (\"execute\".equals(methodName) && input.size() == 1 && input.get(0) instanceof Variables variables) {\n            var m = variables.toMap();\n            if (m.isEmpty()) {\n                return List.of();\n            }\n        }\n\n        return InputSanitizer.sanitize(input);\n    }\n\n    private static boolean isSuspended(State state) {\n        return state.threadStatus().entrySet().stream()\n                .anyMatch(e -> e.getValue() == ThreadStatus.SUSPENDED);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/InvocationsCollectorParams.java",
    "content": "package com.walmartlabs.concord.plugins.mock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.MapUtils;\n\nimport java.io.Serial;\nimport java.io.Serializable;\nimport java.util.Map;\n\npublic class InvocationsCollectorParams implements Serializable {\n\n    @Serial\n    private static final long serialVersionUID = 1L;\n\n    private final Map<String, Object> params;\n\n    public InvocationsCollectorParams(Map<String, Object> params) {\n        this.params = params;\n    }\n\n    public boolean enabled() {\n        return MapUtils.getBoolean(params, \"enabled\", true);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/MockDefinition.java",
    "content": "package com.walmartlabs.concord.plugins.mock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport com.walmartlabs.concord.sdk.UserDefinedException;\n\nimport java.io.Serializable;\nimport java.util.List;\nimport java.util.Map;\n\npublic class MockDefinition {\n\n    private final Map<String, Object> definition;\n\n    public MockDefinition(Map<String, Object> definition) {\n        this.definition = definition;\n    }\n\n    public String stepName() {\n        return MapUtils.getString(definition, \"stepName\");\n    }\n\n    public Map<String, Object> stepMeta() {\n        return MapUtils.getMap(definition, \"stepMeta\", Map.of());\n    }\n\n    public String task() {\n        try {\n            return MapUtils.assertString(definition, \"task\");\n        } catch (IllegalArgumentException e) {\n            throw new UserDefinedException(\"Invalid mock definition: \" + e.getMessage() + \"\\n\" + definition);\n        }\n    }\n\n    public Map<String, Object> input() {\n        return MapUtils.getMap(definition, \"in\", Map.of());\n    }\n\n    public Map<String, Object> out() {\n        return MapUtils.getMap(definition, \"out\", Map.of());\n    }\n\n    public String method() {\n        return MapUtils.getString(definition, \"method\");\n    }\n\n    public List<Object> args() {\n        return MapUtils.getList(definition, \"args\", List.of());\n    }\n\n    public Serializable result() {\n        return MapUtils.get(definition, \"result\", Serializable.class);\n    }\n\n    public String throwError() {\n        return MapUtils.getString(definition, \"throwError\");\n    }\n\n    public String executeFlow() {\n        return MapUtils.getString(definition, \"executeFlow\");\n    }\n\n    @Override\n    public String toString() {\n        return String.valueOf(definition);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/MockDefinitionContext.java",
    "content": "package com.walmartlabs.concord.plugins.mock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport java.util.Objects;\n\npublic record MockDefinitionContext(Step currentStep, String taskName, Variables input, String method, Object[] params) {\n\n    public static MockDefinitionContext task(Step currentStep, String taskName, Variables input) {\n        Objects.requireNonNull(taskName);\n        Objects.requireNonNull(input);\n\n        return new MockDefinitionContext(currentStep, taskName, input, null, null);\n    }\n\n    public static MockDefinitionContext method(Step currentStep, String taskName, String methodName, Object[] params) {\n        Objects.requireNonNull(taskName);\n        Objects.requireNonNull(methodName);\n        Objects.requireNonNull(params);\n        return new MockDefinitionContext(currentStep, taskName, null, methodName, params);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/MockDefinitionProvider.java",
    "content": "package com.walmartlabs.concord.plugins.mock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.plugins.mock.matcher.ArgsMatcher;\nimport com.walmartlabs.concord.runtime.v2.model.AbstractStep;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.LogUtils;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.UserDefinedException;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Singleton;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Predicate;\nimport java.util.stream.Stream;\n\n@Singleton\npublic class MockDefinitionProvider {\n\n    private static final Logger log = LoggerFactory.getLogger(MockDefinitionProvider.class);\n\n    private final MockDefinitionMatcher mockDefinitionMatcher = new MockDefinitionMatcher();\n\n    public MockDefinition find(Context ctx, String taskName, Variables input) {\n        return findMockDefinitions(ctx, mock ->\n                mockDefinitionMatcher.matches(MockDefinitionContext.task(ctx.execution().currentStep(), taskName, input), mock));\n    }\n\n    public MockDefinition find(Context ctx, String taskName, String method, Object[] params) {\n        return findMockDefinitions(ctx, mock ->\n                mockDefinitionMatcher.matches(MockDefinitionContext.method(ctx.execution().currentStep(), taskName, method, params), mock));\n    }\n\n    public boolean isTaskMocked(Context ctx, String taskName) {\n        return mocks(ctx).anyMatch(mock -> ArgsMatcher.match(taskName, mock.task()));\n    }\n\n    private static MockDefinition findMockDefinitions(Context ctx, Predicate<MockDefinition> predicate) {\n        var candidates = mocks(ctx).filter(predicate).toList();\n        if (candidates.isEmpty()) {\n            return null;\n        } else if (candidates.size() == 1) {\n            return candidates.get(0);\n        }\n        throw new UserDefinedException(\"Too many mocks: \" + candidates);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static Stream<MockDefinition> mocks(Context ctx) {\n        return ctx.variables().getList(\"mocks\", List.of()).stream()\n                .map(m -> new MockDefinition((Map<String, Object>) m));\n    }\n\n    public static class MockDefinitionMatcher {\n\n        private final List<Matcher> matchers = List.of(\n                new TaskNameMatcher(),\n                new MethodNameMatcher(),\n                new StepNameMatcher(),\n                new StepMetaMatcher(),\n                new TaskInputMatcher(),\n                new TaskArgsMatcher()\n        );\n\n        public boolean matches(MockDefinitionContext context, MockDefinition mock) {\n            for (Matcher matcher : matchers) {\n                if (!matcher.matches(context, mock)) {\n                    return false;\n                }\n            }\n            return true;\n        }\n    }\n\n    public interface Matcher {\n\n        boolean matches(MockDefinitionContext context, MockDefinition mock);\n    }\n\n    public static class TaskNameMatcher implements Matcher {\n\n        @Override\n        public boolean matches(MockDefinitionContext context, MockDefinition mock) {\n            return ArgsMatcher.match(context.taskName(), mock.task());\n        }\n    }\n\n    public static class MethodNameMatcher implements Matcher {\n\n        @Override\n        public boolean matches(MockDefinitionContext context, MockDefinition mock) {\n            if (context.method() == null) {\n                return true;\n            }\n\n            return ArgsMatcher.match(context.method(), mock.method());\n        }\n    }\n\n    public static class StepNameMatcher implements Matcher {\n\n        @Override\n        public boolean matches(MockDefinitionContext context, MockDefinition mock) {\n            if (mock.stepName() == null) {\n                return true;\n            }\n\n            var logContext = LogUtils.getContext();\n            return logContext != null && ArgsMatcher.match(logContext.segmentName(), mock.stepName());\n        }\n    }\n\n    public static class StepMetaMatcher implements Matcher {\n\n        @Override\n        public boolean matches(MockDefinitionContext context, MockDefinition mock) {\n            if (mock.stepMeta().isEmpty()) {\n                return true;\n            }\n\n            if (!(context.currentStep() instanceof AbstractStep<?> step)) {\n                return false;\n            }\n\n            var stepOptions = step.getOptions();\n            if (stepOptions == null) {\n                return false;\n            }\n\n            return ArgsMatcher.match(stepOptions.meta(), mock.stepMeta());\n        }\n    }\n\n    public static class TaskInputMatcher implements Matcher {\n\n        @Override\n        public boolean matches(MockDefinitionContext context, MockDefinition mock) {\n            if (context.input() == null) {\n                return true;\n            }\n\n            return ArgsMatcher.match(context.input().toMap(), mock.input());\n        }\n    }\n\n    public static class TaskArgsMatcher implements Matcher {\n\n        @Override\n        public boolean matches(MockDefinitionContext context, MockDefinition mock) {\n            if (context.params() == null) {\n                return true;\n            }\n\n            return ArgsMatcher.match(context.params(), mock.args());\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/MockModule.java",
    "content": "package com.walmartlabs.concord.plugins.mock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.multibindings.Multibinder;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallListener;\nimport com.walmartlabs.concord.runtime.v2.sdk.CustomBeanMethodResolver;\nimport com.walmartlabs.concord.runtime.v2.sdk.CustomTaskMethodResolver;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskProvider;\nimport com.walmartlabs.concord.svm.ExecutionListener;\n\nimport javax.inject.Named;\n\n@Named\npublic class MockModule implements com.google.inject.Module {\n\n    @Override\n    public void configure(Binder binder) {\n        var taskProviders = Multibinder.newSetBinder(binder, TaskProvider.class);\n        taskProviders.addBinding().to(MockTaskProvider.class);\n\n        var taskMethodResolvers = Multibinder.newSetBinder(binder, CustomTaskMethodResolver.class);\n        taskMethodResolvers.addBinding().to(MockTaskMethodResolver.class);\n\n        var beanMethodResolvers = Multibinder.newSetBinder(binder, CustomBeanMethodResolver.class);\n        beanMethodResolvers.addBinding().to(VerifierBeanMethodResolver.class);\n\n        var taskCallListeners = Multibinder.newSetBinder(binder, TaskCallListener.class);\n        taskCallListeners.addBinding().to(InvocationsCollector.class);\n\n        var executionListeners = Multibinder.newSetBinder(binder, ExecutionListener.class);\n        executionListeners.addBinding().to(InvocationsCollector.class);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/MockTask.java",
    "content": "package com.walmartlabs.concord.plugins.mock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.model.FlowCall;\nimport com.walmartlabs.concord.runtime.v2.model.FlowCallOptions;\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.VMUtils;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\nimport com.walmartlabs.concord.runtime.v2.sdk.Compiler;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.Serializable;\nimport java.util.Arrays;\nimport java.util.Map;\nimport java.util.function.Supplier;\n\npublic class MockTask implements Task {\n\n    private static final Logger log = LoggerFactory.getLogger(MockTask.class);\n\n    private final Context ctx;\n    private final String taskName;\n    private final MockDefinitionProvider mockDefinitionProvider;\n    private final Class<? extends Task> originalTaskClass;\n    private final Supplier<Task> delegate;\n    private final boolean debug;\n\n    public MockTask(Context ctx, String taskName,\n                    MockDefinitionProvider mockDefinitionProvider,\n                    Class<? extends Task> originalTaskClass,\n                    Supplier<Task> delegate) {\n        this.ctx = ctx;\n        this.taskName = taskName;\n        this.mockDefinitionProvider = mockDefinitionProvider;\n        this.originalTaskClass = originalTaskClass;\n        this.delegate = delegate;\n        this.debug = ctx.processConfiguration().debug();\n    }\n\n    @Override\n    public TaskResult execute(Variables input) throws Exception{\n        var mockDefinition = mockDefinitionProvider.find(ctx, taskName, input);\n        if (mockDefinition == null) {\n            if (debug) {\n                log.info(\"Arguments did not match for '{}' mocked task. Executing the real task\", taskName);\n                log.info(\"Mock definitions:\\n{}\", MockDefinitionProvider.mocks(ctx).toList());\n            }\n\n            return delegate.get().execute(input);\n        }\n\n        log.info(\"The actual task is not being executed; this is a mock\");\n\n        if (mockDefinition.throwError() != null) {\n            throw new UserDefinedException(mockDefinition.throwError());\n        }\n\n        var result = mockDefinition.out();\n        if (mockDefinition.executeFlow() != null) {\n            var flowResult = executeFlow(mockDefinition.executeFlow(), input.toMap());\n            result = assertMap(flowResult);\n        }\n\n        boolean success = MapUtils.getBoolean(result, \"ok\", true);\n        return TaskResult.of(success)\n                .values(result);\n    }\n\n    public Object call(InvocationContext ic, String method, Class<?>[] paramTypes, Object[] params) {\n        var mockDefinition = mockDefinitionProvider.find(ctx, taskName, method, params);\n        if (mockDefinition == null) {\n            if (debug) {\n                log.info(\"Arguments did not match for '{}.{}' mocked task call. Executing the real task.\", taskName, method);\n                log.info(\"Mock definitions:\\n{}\", MockDefinitionProvider.mocks(ctx).toList());\n            }\n\n            return ic.invoker().invoke(delegate.get(), method, paramTypes, params);\n        }\n\n        log.info(\"The actual '{}.{}()' is not being executed; this is a mock\", taskName, method);\n\n        if (mockDefinition.throwError() != null) {\n            throw new UserDefinedException(mockDefinition.throwError());\n        }\n\n        var result = mockDefinition.result();\n        if (mockDefinition.executeFlow() != null) {\n            result = executeFlow(mockDefinition.executeFlow(), toMap(params));\n        }\n\n        return result;\n    }\n\n    public String taskName() {\n        return taskName;\n    }\n\n    public Class<? extends Task> originalTaskClass() {\n        return originalTaskClass;\n    }\n\n    @SuppressWarnings({\"rawtypes\", \"unchecked\"})\n    private Serializable executeFlow(String flowName, Map<String, Object> input) {\n        log.info(\"Executing flow '{}' to get mock results\", flowName);\n\n        var runtime = ctx.execution().runtime();\n        var state = ctx.execution().state();\n        var compiler = runtime.getService(Compiler.class);\n        var pd = runtime.getService(ProcessDefinition.class);\n\n        var callOptions = FlowCallOptions.builder()\n                .input((Map)input)\n                .addOut(\"result\")\n                .build();\n        var flowCallCommand = compiler.compile(pd, new FlowCall(Location.builder().build(), flowName, callOptions));\n\n        var currentThreadId = ctx.execution().currentThreadId();\n        var forkThreadId = state.nextThreadId();\n\n        state.fork(currentThreadId, forkThreadId, flowCallCommand);\n\n        var targetFrame = state.peekFrame(forkThreadId);\n        VMUtils.putLocals(targetFrame, VMUtils.getCombinedLocals(state, currentThreadId));\n\n        try {\n            var result = runtime.eval(state, forkThreadId);\n            return result.lastFrame().getLocal(\"result\");\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @SuppressWarnings({\"unchecked\"})\n    private Map<String, Object> assertMap(Serializable maybeMap) {\n        if (maybeMap == null) {\n            return Map.of();\n        }\n\n        if (maybeMap instanceof Map<?,?>) {\n            return (Map<String, Object>) maybeMap;\n        }\n\n        throw new IllegalArgumentException(\"Flow should set result as Map. Actual: \" + maybeMap.getClass());\n    }\n\n    private static Map<String, Object> toMap(Object[] params) {\n        if (params == null || params.length == 0) {\n            return Map.of();\n        }\n\n        return Map.of(\"args\", Arrays.asList(params));\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/MockTaskMethodResolver.java",
    "content": "package com.walmartlabs.concord.plugins.mock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.CustomTaskMethodResolver;\nimport com.walmartlabs.concord.runtime.v2.sdk.InvocationContext;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\n\npublic class MockTaskMethodResolver implements CustomTaskMethodResolver {\n\n    @Override\n    public TaskInvocation resolve(Task base, String method, Class<?>[] paramTypes, Object[] params) {\n        if (base instanceof MockTask mockTask) {\n            return new TaskInvocation() {\n\n                @Override\n                public String taskName() {\n                    return mockTask.taskName();\n                }\n\n                @Override\n                public Class<? extends Task> taskClass() {\n                    return mockTask.originalTaskClass();\n                }\n\n                @Override\n                public Object invoke(InvocationContext context) {\n                    return mockTask.call(context, method, paramTypes, params);\n                }\n            };\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/MockTaskProvider.java",
    "content": "package com.walmartlabs.concord.plugins.mock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskProvider;\nimport com.walmartlabs.concord.runtime.v2.sdk.UserDefinedException;\nimport org.eclipse.sisu.Priority;\n\nimport javax.inject.Inject;\nimport javax.inject.Provider;\nimport javax.inject.Singleton;\nimport java.util.*;\nimport java.util.function.Function;\n\n@Priority(-1)\n@Singleton\npublic class MockTaskProvider implements TaskProvider {\n\n    private final MockDefinitionProvider mockDefinitionProvider;\n    private final Provider<List<TaskProvider>> taskProviderProvider;\n\n    @Inject\n    public MockTaskProvider(MockDefinitionProvider mockDefinitionProvider, Provider<List<TaskProvider>> taskProviderProvider) {\n        this.mockDefinitionProvider = mockDefinitionProvider;\n        this.taskProviderProvider = taskProviderProvider;\n    }\n\n    @Override\n    public Task createTask(Context ctx, String key) {\n        boolean mocked = mockDefinitionProvider.isTaskMocked(ctx, key);\n        if (mocked) {\n            return new MockTask(ctx, key, mockDefinitionProvider, originTaskClass(ctx, key), () -> originalTask(ctx, key));\n        }\n\n        return null;\n    }\n\n    @Override\n    public Class<? extends Task> getTaskClass(Context ctx, String key) {\n        return null;\n    }\n\n    @Override\n    public boolean hasTask(String key) {\n        return false;\n    }\n\n    @Override\n    public Set<String> names() {\n        return Collections.emptySet();\n    }\n\n    private Class<? extends Task> originTaskClass(Context ctx, String key) {\n        return findFirstMatchingTaskProvider((provider) -> provider.getTaskClass(ctx, key))\n                .orElseThrow(() -> new UserDefinedException(\"Task not found: '\" + key + \"'\"));\n    }\n\n    private Task originalTask(Context ctx, String key) {\n        return findFirstMatchingTaskProvider((provider) -> provider.createTask(ctx, key))\n                .orElseThrow(() -> new UserDefinedException(\"Task not found: '\" + key + \"'\"));\n    }\n\n    private <T> Optional<T> findFirstMatchingTaskProvider(Function<TaskProvider, T> taskFunction) {\n        return taskProviderProvider.get().stream()\n                .filter(MockTaskProvider::isNotMockProvider)\n                .sorted(Comparator.comparingInt(MockTaskProvider::getPriority))\n                .map(taskFunction)\n                .filter(Objects::nonNull)\n                .findFirst();\n    }\n\n    private static int getPriority(TaskProvider p) {\n        Class<? extends TaskProvider> klass = p.getClass();\n        Priority priority = klass.getDeclaredAnnotation(Priority.class);\n        if (priority == null) {\n            return 0;\n        }\n        return priority.value();\n    }\n\n    private static boolean isNotMockProvider(TaskProvider provider) {\n        return !(provider instanceof MockTaskProvider);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/MockUtilsTask.java",
    "content": "package com.walmartlabs.concord.plugins.mock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonValue;\nimport com.walmartlabs.concord.runtime.v2.sdk.DryRunReady;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\n\nimport javax.inject.Named;\nimport java.io.Serial;\nimport java.io.Serializable;\n\n@Named(\"mock\")\n@DryRunReady\npublic class MockUtilsTask implements Task {\n\n    public static Any any() {\n        return new Any();\n    }\n\n    public static class Any implements Serializable {\n\n        @Serial\n        private static final long serialVersionUID = 1L;\n\n        @Override\n        public boolean equals(Object obj) {\n            return true;\n        }\n\n        @Override\n        @JsonValue\n        public String toString() {\n            return \"any\";\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/VerifierBeanMethodResolver.java",
    "content": "package com.walmartlabs.concord.plugins.mock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.CustomBeanMethodResolver;\nimport com.walmartlabs.concord.runtime.v2.sdk.Invocation;\n\npublic class VerifierBeanMethodResolver implements CustomBeanMethodResolver {\n\n    @Override\n    public Invocation resolve(Object base, String method, Class<?>[] paramTypes, Object[] params) {\n        if (base instanceof VerifyTask.Verifier verifier) {\n            return context -> {\n                verifier.verify(method, params);\n                return null;\n            };\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/VerifyTask.java",
    "content": "package com.walmartlabs.concord.plugins.mock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.DryRunReady;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport com.walmartlabs.concord.runtime.v2.sdk.UserDefinedException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n@Named(\"verify\")\n@DryRunReady\npublic class VerifyTask implements Task {\n\n    private final ObjectMapper objectMapper;\n    private final Invocations invocations;\n    private final Location verifyStepLocation;\n\n    @Inject\n    public VerifyTask(ObjectMapper objectMapper, Invocations invocations, Context context) {\n        this.objectMapper = objectMapper;\n        this.invocations = invocations;\n\n        var currentStep = context.execution().currentStep();\n        this.verifyStepLocation = currentStep != null ? currentStep.getLocation() : null;\n    }\n\n    public Verifier task(String taskName, int count) {\n        return new Verifier(taskName, count);\n    }\n\n    public class Verifier {\n\n        private static final Logger log = LoggerFactory.getLogger(\"processLog\");\n\n        private final String taskName;\n        private final int count;\n\n        public Verifier(String taskName, int count) {\n            this.taskName = taskName;\n            this.count = count;\n        }\n\n        public void verify(String methodName, Object[] args) {\n            List<Invocation> result = invocations.find(taskName, methodName, args);\n            if (result.size() != count) {\n                dumpInvocations(result, taskName, methodName, args, count);\n\n                throw new UserDefinedException(\"verify failed\");\n            }\n        }\n\n        private void dumpInvocations(List<Invocation> matchedInvocations, String expectedTaskName, String expectedMethodName, Object[] args, int expectedCount) {\n            StringBuilder logMessage = new StringBuilder();\n\n            String invocation = String.format(\"%s.%s(%s) @ %s\",\n                    expectedTaskName, expectedMethodName,\n                    argsToString(args), locationToString(verifyStepLocation));\n\n            if (matchedInvocations.isEmpty()) {\n                logMessage.append(\"Wanted but not invoked:\\n\")\n                        .append(invocation).append('\\n');\n\n                appendTaskInteractions(logMessage, expectedMethodName);\n            } else {\n                logMessage.append(\"Wanted \").append(expectedCount).append(pluralize(\" time\", expectedCount))\n                        .append(\":\\n\")\n                        .append(invocation).append('\\n');\n\n                appendMatchedInvocations(logMessage, matchedInvocations);\n            }\n\n            log.info(\"\\n{}\", logMessage);\n        }\n\n        private void appendTaskInteractions(StringBuilder logMessage, String expectedMethodName) {\n            List<Invocation> taskInvocations = invocations.find(taskName, expectedMethodName);\n            if (!taskInvocations.isEmpty()) {\n                logMessage.append(\"However, there was \")\n                        .append(taskInvocations.size()).append(pluralize(\" interaction\", taskInvocations.size()))\n                        .append(\" with this task:\\n\");\n\n                taskInvocations.forEach(invocation ->\n                        logMessage.append(\"-> \").append(invocationToString(invocation)).append('\\n'));\n            }\n        }\n\n        private void appendMatchedInvocations(StringBuilder logMessage, List<Invocation> matchedInvocations) {\n            if (!matchedInvocations.isEmpty()) {\n                logMessage.append(\"But was \").append(matchedInvocations.size()).append(pluralize(\" time\", matchedInvocations.size()))\n                        .append(\":\\n\");\n\n                matchedInvocations.forEach(invocation ->\n                        logMessage.append(\"-> \").append(invocationToString(invocation)).append('\\n'));\n            } else {\n                logMessage.append(\"But no interactions recorded\\n\");\n            }\n        }\n\n        private String invocationToString(Invocation invocation) {\n            return String.format(\"%s:%d => %s.%s(%s)\", invocation.fileName(), invocation.line(),\n                    invocation.taskName(), invocation.methodName(), argsToString(invocation.args()));\n        }\n\n        private static String pluralize(String word, int count) {\n            return word + (count > 1 ? \"s\" : \"\");\n        }\n\n        private String argsToString(Object[] args) {\n            if (args == null || args.length == 0) {\n                return \"\";\n            }\n\n            return argsToString(Arrays.asList(args));\n        }\n\n        private String argsToString(List<Object> args) {\n            if (args.isEmpty()) {\n                return \"\";\n            }\n\n            return args.stream()\n                    .map(arg -> {\n                        try {\n                            return objectMapper.writeValueAsString(arg);\n                        } catch (JsonProcessingException e) {\n                            throw new RuntimeException(\"Can't serialize argument: \" + arg, e);\n                        }\n                    })\n                    .collect(Collectors.joining(\", \"));\n        }\n\n        private static String locationToString(Location location) {\n            if (location == null || location.fileName() == null) {\n                return \"\";\n            }\n\n            return location.fileName() + \":\" + location.lineNum();\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/matcher/AbstractMatcher.java",
    "content": "package com.walmartlabs.concord.plugins.mock.matcher;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic abstract class AbstractMatcher<E1, E2> implements Matcher<E1, E2> {\n\n    private final Class<E1> inputType;\n    private final Class<E2> mockInputType;\n\n    public AbstractMatcher(TypeReference<E1> inputTypeRef, TypeReference<E2> mockInputTypeRef) {\n        this.inputType = inputTypeRef.getRawType();\n        this.mockInputType = mockInputTypeRef.getRawType();\n    }\n\n    @Override\n    public boolean canHandle(Object input, Object mockInput) {\n        return inputType.isInstance(input) && mockInputType.isInstance(mockInput);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/matcher/ArgsMatcher.java",
    "content": "package com.walmartlabs.concord.plugins.mock.matcher;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.UUID;\n\npublic final class ArgsMatcher {\n\n    private static final List<Matcher<?, ?>> MATCHERS = List.of(\n            new MapMatcher(),\n            new CollectionMatcher(),\n            new StringValueMatcher(),\n            new ValueMatcher()\n    );\n\n    public static boolean match(Object input, Object mockInput) {\n        Object normalizedInput = normalizeValue(input);\n        Object normalizedMockInput = normalizeValue(mockInput);\n\n        return MATCHERS.stream()\n                .filter(matcher -> matcher.canHandle(normalizedInput, normalizedMockInput))\n                .findFirst()\n                .map(matcher -> invokeMatch(matcher, normalizedInput, normalizedMockInput))\n                .orElse(false);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static <E1, E2> boolean invokeMatch(Matcher<E1, E2> matcher, Object input, Object mockInput) {\n        return matcher.matches((E1) input, (E2) mockInput);\n    }\n\n    private static Object normalizeValue(Object value) {\n        if (value instanceof Integer) {\n            return ((Integer) value).longValue();\n        } else if (value instanceof UUID) {\n            return value.toString();\n        } else if (isArray(value)) {\n            return Arrays.asList((Object[]) value);\n        }\n        return value;\n    }\n\n    private static boolean isArray(Object obj) {\n        return obj != null && obj.getClass().isArray();\n    }\n\n    private ArgsMatcher() {}\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/matcher/CollectionMatcher.java",
    "content": "package com.walmartlabs.concord.plugins.mock.matcher;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Collection;\nimport java.util.Iterator;\n\npublic class CollectionMatcher extends AbstractMatcher<Collection<Object>, Collection<Object>> {\n\n    public CollectionMatcher() {\n        super(new TypeReference<>() {}, new TypeReference<>() {});\n    }\n\n    @Override\n    public boolean matches(Collection<Object> input, Collection<Object> mockInput) {\n        if (input.size() != mockInput.size()) {\n            return false;\n        }\n\n        Iterator<Object> inputIt = input.iterator();\n        Iterator<Object> mockIt = mockInput.iterator();\n        while (inputIt.hasNext() && mockIt.hasNext()) {\n            if (!ArgsMatcher.match(inputIt.next(), mockIt.next())) {\n                return false;\n            }\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/matcher/MapMatcher.java",
    "content": "package com.walmartlabs.concord.plugins.mock.matcher;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Map;\n\npublic class MapMatcher extends AbstractMatcher<Map<String, Object>, Map<String, Object>> {\n\n    public MapMatcher() {\n        super(new TypeReference<>() {}, new TypeReference<>() {});\n    }\n\n    @Override\n    public boolean matches(Map<String, Object> input, Map<String, Object> mockInput) {\n        return mockInput.entrySet().stream()\n                .allMatch(entry -> ArgsMatcher.match(input.get(entry.getKey()), entry.getValue()));\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/matcher/Matcher.java",
    "content": "package com.walmartlabs.concord.plugins.mock.matcher;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface Matcher<E1, E2> {\n\n    boolean matches(E1 input, E2 mockInput);\n\n    boolean canHandle(Object input, Object mockInput);\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/matcher/StringValueMatcher.java",
    "content": "package com.walmartlabs.concord.plugins.mock.matcher;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.regex.Pattern;\n\npublic class StringValueMatcher extends AbstractMatcher<String, String> {\n\n    public StringValueMatcher() {\n        super(new TypeReference<>() {}, new TypeReference<>() {});\n    }\n\n    @Override\n    public boolean matches(String input, String mockInput) {\n        return Pattern.compile(mockInput, Pattern.CASE_INSENSITIVE).matcher(input).matches();\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/matcher/TypeReference.java",
    "content": "package com.walmartlabs.concord.plugins.mock.matcher;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.lang.reflect.ParameterizedType;\nimport java.lang.reflect.Type;\n\npublic abstract class TypeReference<T> {\n\n    private final Type type;\n\n    protected TypeReference() {\n        Type superclass = getClass().getGenericSuperclass();\n        if (superclass instanceof ParameterizedType) {\n            this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0];\n        } else {\n            throw new IllegalStateException(\"TypeReference must be parameterized\");\n        }\n    }\n\n    public Type getType() {\n        return type;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public Class<T> getRawType() {\n        if (type instanceof ParameterizedType) {\n            return (Class<T>) ((ParameterizedType) type).getRawType();\n        } else if (type instanceof Class) {\n            return (Class<T>) type;\n        } else {\n            throw new IllegalStateException(\"Unable to determine raw type\");\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/main/java/com/walmartlabs/concord/plugins/mock/matcher/ValueMatcher.java",
    "content": "package com.walmartlabs.concord.plugins.mock.matcher;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class ValueMatcher implements Matcher<Object, Object> {\n\n    @Override\n    public boolean canHandle(Object input, Object mockInput) {\n        return true;\n    }\n\n    @Override\n    public boolean matches(Object input, Object mockInput) {\n        if (input == mockInput) {\n            return true;\n        }\n        if (input == null || mockInput == null) {\n            return false;\n        }\n        return mockInput.equals(input);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/test/java/com/walmartlabs/concord/plugins/mock/MockDefinitionMatcherTest.java",
    "content": "package com.walmartlabs.concord.plugins.mock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.model.TaskCall;\nimport com.walmartlabs.concord.runtime.v2.model.TaskCallOptions;\nimport com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.plugins.mock.MockDefinitionProvider.*;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\n\npublic class MockDefinitionMatcherTest {\n\n    private MockDefinitionMatcher mockDefinitionMatcher;\n\n    @BeforeEach\n    public void setUp() {\n        mockDefinitionMatcher = new MockDefinitionMatcher();\n    }\n\n    @Test\n    public void testTaskMatch() {\n        /*\n         task: myTask\n         in:\n           param1: value1\n           param2: value2\n         */\n        var context = MockDefinitionContext.task(mock(Step.class), \"myTask\", new MapBackedVariables(Map.of(\"param1\", \"value1\", \"param2\", \"value2\")));\n        var mock = new MockDefinition(Map.of(\n                \"task\", \"myTask\",\n                \"in\", Map.of(\"param1\", \"value1\")\n        ));\n\n        assertTrue(mockDefinitionMatcher.matches(context, mock));\n    }\n\n    @Test\n    public void testMatchOnlyByMeta() {\n        /*\n         task: myTask\n         in:\n           param1: value1\n           param2: value2\n         */\n        var currentStep = new TaskCall(Location.builder().build(), \"myTask\", TaskCallOptions.builder().meta(Map.of(\"taskId\", \"BOO\")).build());\n        var context = MockDefinitionContext.task(currentStep, \"myTask\", new MapBackedVariables(Map.of(\"param1\", \"value1\", \"param2\", \"value2\")));\n        var mock = new MockDefinition(Map.of(\n                \"task\", \"myTask\",\n                \"stepMeta\", Map.of(\"taskId\", \"BO.*\")\n        ));\n\n        assertTrue(mockDefinitionMatcher.matches(context, mock));\n    }\n\n    @Test\n    public void testTaskMatchByMeta() {\n        /*\n         task: myTask\n         in:\n           param1: value1\n         meta:\n           taskId: \"BOO\"\n         */\n        var currentStep = new TaskCall(Location.builder().build(), \"myTask\", TaskCallOptions.builder().meta(Map.of(\"taskId\", \"BOO\")).build());\n        var context = MockDefinitionContext.task(currentStep, \"myTask\", new MapBackedVariables(Map.of(\"param1\", \"value1\", \"param2\", \"value2\")));\n        var mock = new MockDefinition(Map.of(\n                \"task\", \"myTask\",\n                \"in\", Map.of(\"param1\", \"value1\"),\n                \"stepMeta\", Map.of(\"taskId\", \"BO.*\")\n        ));\n\n        assertTrue(mockDefinitionMatcher.matches(context, mock));\n    }\n\n    @Test\n    public void testTaskMethodMatch() {\n        // expr: ${myTask.myMethod(1, 2)}\n        var context = MockDefinitionContext.method(mock(Step.class), \"myTask\", \"myMethod\", new Object[] {1, 2});\n\n        var mock = new MockDefinition(Map.of(\n                \"task\", \"myTask\",\n                \"method\", \"myMethod\",\n                \"args\", List.of(1, 2)\n        ));\n\n        assertTrue(mockDefinitionMatcher.matches(context, mock));\n    }\n\n    @Test\n    public void testTaskMethodMatchByMeta() {\n        // expr: ${myTask.myMethod(1, 2)}\n        // meta:\n        //   taskId: \"BOO\"\n        var currentStep = new TaskCall(Location.builder().build(), \"myTask\", TaskCallOptions.builder().meta(Map.of(\"taskId\", \"BOO\")).build());\n        var context = MockDefinitionContext.method(currentStep, \"myTask\", \"myMethod\", new Object[] {1, 2});\n\n        var mock = new MockDefinition(Map.of(\n                \"task\", \"myTask\",\n                \"method\", \"myMethod\",\n                \"args\", List.of(1, 2),\n                \"stepMeta\", Map.of(\"taskId\", \"BO.*\")\n        ));\n\n        assertTrue(mockDefinitionMatcher.matches(context, mock));\n    }\n\n    @Test\n    public void testNotMatch_taskName() {\n        /*\n         task: myTask\n         in:\n           param1: value1\n           param2: value2\n         */\n        var context = MockDefinitionContext.task(mock(Step.class), \"myTask\", new MapBackedVariables(Map.of(\"param1\", \"value1\", \"param2\", \"value2\")));\n        var mock = new MockDefinition(Map.of(\n                \"task\", \"myTask2\",\n                \"in\", Map.of(\"param1\", \"value1\")\n        ));\n\n        // real taskName=myTask, mocked: \"myTask2\"\n        assertFalse(mockDefinitionMatcher.matches(context, mock));\n    }\n\n    @Test\n    public void testNotMatch_InputParams() {\n        /*\n         task: myTask\n         in:\n           param1: value1\n           param2: value2\n         */\n        var context = MockDefinitionContext.task(mock(Step.class), \"myTask\", new MapBackedVariables(Map.of(\"param1\", \"value1\", \"param2\", \"value2\")));\n        var mock = new MockDefinition(Map.of(\n                \"task\", \"myTask2\",\n                \"in\", Map.of(\"param3\", \"value3\")\n        ));\n\n        assertFalse(mockDefinitionMatcher.matches(context, mock));\n    }\n\n    @Test\n    public void testNotMatch_Meta() {\n        /*\n         task: myTask\n         in:\n           param1: value1\n           param2: value2\n         */\n        var context = MockDefinitionContext.task(mock(Step.class), \"myTask\", new MapBackedVariables(Map.of(\"param1\", \"value1\", \"param2\", \"value2\")));\n        var mock = new MockDefinition(Map.of(\n                \"task\", \"myTask2\",\n                \"stepMeta\", Map.of(\"taskId\", \"BO.*\")\n        ));\n\n        assertFalse(mockDefinitionMatcher.matches(context, mock));\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/test/java/com/walmartlabs/concord/plugins/mock/MockTest.java",
    "content": "package com.walmartlabs.concord.plugins.mock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.runner.TestRuntimeV2;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport java.util.regex.Pattern;\n\nimport static com.walmartlabs.concord.runtime.v2.runner.TestRuntimeV2.assertLog;\n\npublic class MockTest {\n\n    @RegisterExtension\n    private static final TestRuntimeV2 runtime = new TestRuntimeV2();\n\n    @Test\n    public void testTaskMock() throws Exception {\n        runtime.deploy(\"simple\");\n\n        byte[] log = runtime.run();\n        assertLog(log, \".*The actual task is not being executed; this is a mock.*\");\n        assertLog(log, \".*result.ok: true.*\");\n        assertLog(log, \".*result.fromMock: good.*\");\n    }\n\n    @Test\n    public void testMethodMock() throws Exception {\n        runtime.deploy(\"method-mock\");\n\n        byte[] log = runtime.run();\n        assertLog(log, \".*\" + Pattern.quote(\"The actual 'testTask.myMethod()' is not being executed; this is a mock\") + \".*\");\n        assertLog(log, \".*result.ok: BOO.*\");\n        assertLog(log, \".*original: original.*\");\n        assertLog(log, \".*original2: original2.*\");\n    }\n\n    @Test\n    public void testMethodMockWithFlowExecute() throws Exception {\n        runtime.deploy(\"method-mock-with-flow-execute\");\n\n        byte[] log = runtime.run();\n        assertLog(log, \".*\" + Pattern.quote(\"The actual 'testTask.myMethod()' is not being executed; this is a mock\") + \".*\");\n        assertLog(log, \".* Executing flow 'assertMyMethod' to get mock results.*\");\n        assertLog(log, \".*\" + Pattern.quote(\"flow can access method args: [1, b, false, [1, 2, 3], {k=v}]\") + \".*\");\n        assertLog(log, \".*result.ok: WOW.*\");\n    }\n\n    @Test\n    public void testMethodMockWithAny() throws Exception {\n        runtime.deploy(\"method-mock-with-any\");\n\n        byte[] log = runtime.run();\n        assertLog(log, \".*\" + Pattern.quote(\"The actual 'testTask.myMethod()' is not being executed; this is a mock\") + \".*\");\n        assertLog(log, \".*result.ok: BOO.*\");\n    }\n\n    @Test\n    public void testTaskMockWithFlowExecute() throws Exception {\n        runtime.deploy(\"task-mock-with-flow-execute\");\n\n        byte[] log = runtime.run();\n        assertLog(log, \".*testTaskLogic can access task input params: p1=value-1, p2=value-2.*\");\n        assertLog(log, \".*The actual task is not being executed; this is a mock.*\");\n        assertLog(log, \".*result.ok: .*fromMockAsFlow=WOW.*\");\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/test/java/com/walmartlabs/concord/plugins/mock/TestTask.java",
    "content": "package com.walmartlabs.concord.plugins.mock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport javax.inject.Named;\nimport java.util.List;\nimport java.util.Map;\n\n@Named(\"testTask\")\npublic class TestTask implements Task {\n\n    @Override\n    public TaskResult execute(Variables input) {\n        return TaskResult.success();\n    }\n\n    public String doAction(String input) {\n        return input;\n    }\n\n    public void myMethod(int a, String b, boolean c, List<Integer> d, Map<String, Object> e) {\n        throw new RuntimeException(\"myMethod not implemented\");\n    }\n\n    public void myMethod(int a, String b) {\n        throw new RuntimeException(\"myMethod not implemented\");\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/test/java/com/walmartlabs/concord/plugins/mock/VerifyTest.java",
    "content": "package com.walmartlabs.concord.plugins.mock;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.runner.DefaultPersistenceService;\nimport com.walmartlabs.concord.runtime.v2.runner.TestRuntimeV2;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\npublic class VerifyTest {\n\n    @RegisterExtension\n    private static final TestRuntimeV2 runtime = new TestRuntimeV2()\n            .withPersistenceService(DefaultPersistenceService.class);\n\n    @Test\n    public void testVerify() throws Exception {\n        runtime.deploy(\"simple-verify\");\n\n        runtime.run();\n    }\n\n    @Test\n    public void testVerifyMockedTask() throws Exception {\n        runtime.deploy(\"verify-mocked-task\");\n\n        runtime.run();\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/test/java/com/walmartlabs/concord/plugins/mock/matcher/ArgsMatcherTest.java",
    "content": "package com.walmartlabs.concord.plugins.mock.matcher;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.plugins.mock.MockUtilsTask;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class ArgsMatcherTest {\n\n    @Test\n    void testMatchTwoEqualMaps() {\n        Map<String, Object> map1 = Map.of(\"key1\", 123, \"key2\", \"Hello\");\n        Map<String, Object> map2 = Map.of(\"key1\", 123L, \"key2\", \"hello\");\n\n        assertTrue(ArgsMatcher.match(map1, map2));\n    }\n\n    @Test\n    void testMatchTwoDifferentMaps() {\n        Map<String, Object> map1 = Map.of(\"key1\", 123, \"key2\", \"Hello\");\n        Map<String, Object> map2 = Map.of(\"key1\", 123L, \"key2\", \"World\");\n\n        assertFalse(ArgsMatcher.match(map1, map2));\n    }\n\n    @Test\n    void testMatchTwoEqualLists() {\n        List<Object> list1 = List.of(1, 2, \"Test\");\n        List<Object> list2 = List.of(1L, 2, \"test\");\n\n        assertTrue(ArgsMatcher.match(list1, list2));\n    }\n\n    @Test\n    void testMatchTwoDifferentLists() {\n        List<Object> list1 = List.of(1, 2, \"Test\");\n        List<Object> list2 = List.of(1L, 2, \"Fail\");\n\n        assertFalse(ArgsMatcher.match(list1, list2));\n    }\n\n    @Test\n    void testMatchDifferentSizeLists() {\n        List<Object> list1 = List.of(1, 2);\n        List<Object> list2 = List.of(1L, 2, \"extra\");\n\n        assertFalse(ArgsMatcher.match(list1, list2));\n    }\n\n    @Test\n    void testMatchEqualValues() {\n        assertTrue(ArgsMatcher.match(123, 123L), \"Integer and Long should match\");\n        assertTrue(ArgsMatcher.match(UUID.fromString(\"d290f1ee-6c54-4b01-90e6-d701748f0851\"), \"d290f1ee-6c54-4b01-90e6-d701748f0851\"));\n    }\n\n    @Test\n    void testMatchDifferentValues() {\n        assertFalse(ArgsMatcher.match(123, 456));\n        assertFalse(ArgsMatcher.match(\"Hello\", \"World\"));\n    }\n\n    @Test\n    void testMatchNullValues() {\n        assertTrue(ArgsMatcher.match(null, null));\n        assertFalse(ArgsMatcher.match(null, \"Non-null\"));\n    }\n\n    @Test\n    void testMatchArrays() {\n        Object[] array1 = {1, 2, 3};\n        Object[] array2 = {1L, 2, 3};\n\n        assertTrue(ArgsMatcher.match(array1, array2));\n    }\n\n    @Test\n    void testMatchDifferentArrays() {\n        Object[] array1 = {1, 2};\n        Object[] array2 = {1, 2, 3};\n\n        assertFalse(ArgsMatcher.match(array1, array2));\n    }\n\n    @Test\n    void testMatchWithPatternStrings() {\n        assertTrue(ArgsMatcher.match(\"Hello\", \"hello\"));\n        assertTrue(ArgsMatcher.match(\"123abc\", \"\\\\d+abc\"));\n        assertFalse(ArgsMatcher.match(\"Hello\", \"world\"));\n    }\n\n    @Test\n    public void testArraysWithAny() {\n        assertTrue(ArgsMatcher.match(List.of(\"1\", 2), List.of(\"1\", new MockUtilsTask.Any())));\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/mock/src/test/resources/com/walmartlabs/concord/plugins/mock/method-mock/concord.yml",
    "content": "flows:\n  default:\n    - expr: \"${testTask.myMethod(1, 'b', false, [1, 2, 3], {'k': 'v'})}\"\n      out: result\n\n    - log: \"result.ok: ${result}\"\n\n    # call original method\n    - expr: \"${testTask.doAction('original')}\"\n      out: original\n\n    - log: \"original: ${original}\"\n\n    - set:\n        original2: \"${testTask.doAction('original2')}\"\n\n    - log: \"original2: ${original2}\"\n\nconfiguration:\n  arguments:\n    mocks:\n      - task: \"testTask\"\n        method: \"myMethod\"\n        args: [1, 'b', false, [1, 2, 3], {k: 'v'}]\n        result: \"BOO\"\n"
  },
  {
    "path": "plugins/tasks/mock/src/test/resources/com/walmartlabs/concord/plugins/mock/method-mock-with-any/concord.yml",
    "content": "flows:\n  default:\n    - expr: \"${testTask.myMethod(1, 'something')}\"\n      out: result\n\n    - log: \"result.ok: ${result}\"\n\nconfiguration:\n  arguments:\n    mocks:\n      - task: \"testTask\"\n        method: \"myMethod\"\n        args: [1, \"${mock.any()}\"]\n        result: \"BOO\"\n"
  },
  {
    "path": "plugins/tasks/mock/src/test/resources/com/walmartlabs/concord/plugins/mock/method-mock-with-flow-execute/concord.yml",
    "content": "flows:\n  default:\n    - expr: \"${testTask.myMethod(1, 'b', false, [1, 2, 3], {'k': 'v'})}\"\n      out: result\n\n    - log: \"result.ok: ${result}\"\n\n  assertMyMethod:\n    - log: \"flow can access method args: ${args}\"\n\n    - set:\n        result: \"WOW\"\n\nconfiguration:\n  arguments:\n    mocks:\n      - task: \"testTask\"\n        method: \"myMethod\"\n        args: [1, 'b', false, [1, 2, 3], {k: 'v'}]\n        executeFlow: \"assertMyMethod\"\n"
  },
  {
    "path": "plugins/tasks/mock/src/test/resources/com/walmartlabs/concord/plugins/mock/simple/concord.yml",
    "content": "flows:\n  default:\n    - task: testTask\n      out: result\n\n    - log: \"result.ok: ${result.ok}\"\n    - log: \"result.fromMock: ${result.fromMock}\"\n\nconfiguration:\n  arguments:\n    mocks:\n      - task: \"testTask\"\n        out:\n          fromMock: \"good\"\n"
  },
  {
    "path": "plugins/tasks/mock/src/test/resources/com/walmartlabs/concord/plugins/mock/simple-verify/concord.yaml",
    "content": "flows:\n  default:\n    - expr: \"${testTask.doAction('test')}\"\n\n    - task: \"testTask\"\n\n    - task: \"testTask\"\n      in:\n        k: v\n        k2:\n          k3: v3\n\n    - expr: \"${testTask.doAction('test_1')}\"\n    - expr: \"${testTask.doAction('test_2')}\"\n    - expr: \"${testTask.doAction('test_3')}\"\n\n    # expr\n    - expr: \"${verify.task('testTask', 1).doAction('test')}\"\n\n    # task without args\n    - expr: \"${verify.task('testTask', 1).execute()}\"\n\n    # task with args\n    - expr: \"${verify.task('testTask', 1).execute(mock.any())}\"\n    - expr: \"${verify.task('testTask', 1).execute({'k': 'v'})}\"\n    - expr: \"${verify.task('testTask', 1).execute({'k': 'v', 'k2': {'k3': 'v3'} })}\"\n\n    # expr\n    - expr: \"${verify.task('testTask', 3).doAction('test_.*')}\"\n"
  },
  {
    "path": "plugins/tasks/mock/src/test/resources/com/walmartlabs/concord/plugins/mock/task-mock-with-flow-execute/concord.yml",
    "content": "flows:\n  default:\n    - task: testTask\n      in:\n        p1: \"value-1\"\n        p2: \"value-2\"\n      out: result\n\n    - log: \"result.ok: ${result}\"\n\n  testTaskLogic:\n    - log: \"testTaskLogic can access task input params: p1=${p1}, p2=${p2}\"\n\n    - set:\n        result:\n          fromMockAsFlow: \"WOW\"\n\n    # and override result\n\nconfiguration:\n  arguments:\n    mocks:\n      - task: \"testTask\"\n        executeFlow: \"testTaskLogic\"\n"
  },
  {
    "path": "plugins/tasks/mock/src/test/resources/com/walmartlabs/concord/plugins/mock/verify-mocked-task/concord.yaml",
    "content": "flows:\n  default:\n    - set:\n        mocks:\n          - task: testTask\n            in:\n              action: \"clone\"\n\n    - task: testTask\n      in:\n        action: \"clone\"\n\n    - expr: \"${verify.task('testTask', 1).execute({'action': 'clone'})}\"\n"
  },
  {
    "path": "plugins/tasks/noderoster/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>noderoster-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-client2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server.plugins.noderoster</groupId>\n            <artifactId>concord-noderoster-plugin-client2</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <dependencies>\n                    <dependency>\n                        <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n                        <artifactId>concord-runner-v2</artifactId>\n                        <version>${project.version}</version>\n                    </dependency>\n                </dependencies>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/tasks/noderoster/src/main/java/com/walmartlabs/concord/plugins/noderoster/Constants.java",
    "content": "package com.walmartlabs.concord.plugins.noderoster;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic final class Constants {\n\n    public static final int RETRY_COUNT = 3;\n    public static final long RETRY_INTERVAL = 5000;\n\n    public static final String ACTION_KEY = \"action\";\n    public static final String BASE_URL_KEY = \"baseUrl\";\n    public static final String HOSTNAME_KEY = \"hostName\";\n    public static final String HOSTID_KEY = \"hostId\";\n    public static final String ARTIFACT_PATTERN_KEY = \"artifactPattern\";\n    public static final String PROJECT_ID_KEY = \"projectId\";\n    public static final String LIMIT_KEY = \"limit\";\n    public static final String OFFSET_KEY = \"offset\";\n    public static final String API_KEY = \"apiKey\";\n\n    public static final String[] ALL_IN_PARAMS = {\n            ACTION_KEY,\n            BASE_URL_KEY,\n            HOSTNAME_KEY,\n            HOSTID_KEY,\n            ARTIFACT_PATTERN_KEY,\n            PROJECT_ID_KEY,\n            LIMIT_KEY,\n            OFFSET_KEY,\n            API_KEY\n    };\n\n    private Constants() {\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/noderoster/src/main/java/com/walmartlabs/concord/plugins/noderoster/NodeRosterTask.java",
    "content": "package com.walmartlabs.concord.plugins.noderoster;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.ContextUtils;\nimport com.walmartlabs.concord.sdk.Task;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.plugins.noderoster.NodeRosterTaskUtils.*;\nimport static com.walmartlabs.concord.plugins.noderoster.Result.createResponse;\n\n@Named(\"nodeRoster\")\npublic class NodeRosterTask implements Task {\n\n    private final ApiClientFactory apiClientFactory;\n\n    @Inject\n    public NodeRosterTask(ApiClientFactory apiClientFactory) {\n        this.apiClientFactory = apiClientFactory;\n    }\n\n    @Override\n    public void execute(Context ctx) throws Exception {\n        Map<String, Object> paramsCfg = createParamsCfg(ctx);\n        Action action = getAction(paramsCfg);\n\n        switch (action) {\n            case HOSTSWITHARTIFACTS: {\n                findHostsWithArtifacts(ctx, paramsCfg);\n                break;\n            }\n            case FACTS: {\n                findFacts(ctx, paramsCfg);\n                break;\n            }\n            case DEPLOYEDONHOST: {\n                findDeployedArtifacts(ctx, paramsCfg);\n                break;\n            }\n\n            default:\n                throw new IllegalArgumentException(\"Unsupported action type: \" + action);\n        }\n    }\n\n    /**\n     * Find facts for a given host name or host id\n     */\n    public void findFacts(Context ctx, Map<String, Object> paramsCfg) throws Exception {\n        ApiClient clientConfig = clientConfig(ctx, paramsCfg);\n        NodeRosterFactsApi api = new NodeRosterFactsApi(clientConfig);\n        Object facts = NodeRosterTaskUtils.findFacts(api, paramsCfg);\n\n        ctx.setVariable(\"result\", createResponse(facts));\n    }\n\n    /**\n     * Find hosts with a deployed artifact\n     */\n    public void findHostsWithArtifacts(Context ctx, Map<String, Object> paramsCfg) throws Exception {\n        ApiClient clientConfig = clientConfig(ctx, paramsCfg);\n        NodeRosterHostsApi api = new NodeRosterHostsApi(clientConfig);\n        List<HostEntry> hosts = NodeRosterTaskUtils.findHostsWithArtifacts(api, paramsCfg);\n\n        ctx.setVariable(\"result\", createResponse(hosts));\n    }\n\n    /**\n     * Find artifacts deployed on a given host\n     */\n    public void findDeployedArtifacts(Context ctx, Map<String, Object> paramsCfg) throws Exception {\n        ApiClient clientConfig = clientConfig(ctx, paramsCfg);\n        NodeRosterArtifactsApi api = new NodeRosterArtifactsApi(clientConfig);\n        List<ArtifactEntry> deployedArtifacts = NodeRosterTaskUtils.findDeployedArtifacts(api, paramsCfg);\n\n        ctx.setVariable(\"result\", createResponse(deployedArtifacts));\n    }\n\n    private ApiClient clientConfig(Context ctx, Map<String, Object> paramsCfg) {\n        return apiClientFactory.create(ApiClientConfiguration.builder()\n                .baseUrl(getBaseUrl(paramsCfg))\n                .apiKey(getApiKey(paramsCfg))\n                .sessionToken(ContextUtils.sessionTokenOrNull(ctx))\n                .build());\n    }\n\n    private static Map<String, Object> createParamsCfg(Context ctx) {\n        Map<String, Object> m = new HashMap<>();\n        for (String k : Constants.ALL_IN_PARAMS) {\n            Object v = ctx.getVariable(k);\n            if (v != null) {\n                m.put(k, v);\n            }\n        }\n        return m;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/noderoster/src/main/java/com/walmartlabs/concord/plugins/noderoster/NodeRosterTaskUtils.java",
    "content": "package com.walmartlabs.concord.plugins.noderoster;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic class NodeRosterTaskUtils {\n\n    private static final Logger log = LoggerFactory.getLogger(NodeRosterTaskUtils.class);\n\n    public static String getBaseUrl(Map<String, Object> cfg) {\n        return MapUtils.getString(cfg, Constants.BASE_URL_KEY);\n    }\n\n    public static String getApiKey(Map<String, Object> cfg) {\n        return MapUtils.getString(cfg, Constants.API_KEY);\n    }\n\n    public static int getLimit(Map<String, Object> cfg) {\n        return MapUtils.getInt(cfg, Constants.LIMIT_KEY, 30);\n    }\n\n    public static int getOffset(Map<String, Object> cfg) {\n        return MapUtils.getInt(cfg, Constants.OFFSET_KEY, 0);\n    }\n\n    public static String getHostName(Map<String, Object> cfg) {\n        return MapUtils.getString(cfg, Constants.HOSTNAME_KEY);\n    }\n\n    public static UUID getHostId(Map<String, Object> cfg) {\n        return MapUtils.getUUID(cfg, Constants.HOSTID_KEY);\n    }\n\n    public static Action getAction(Map<String, Object> cfg) {\n        String action = MapUtils.assertString(cfg, Constants.ACTION_KEY);\n        return Action.valueOf(action.trim().toUpperCase());\n    }\n\n    public enum Action {\n        HOSTSWITHARTIFACTS,\n        FACTS,\n        DEPLOYEDONHOST\n    }\n\n    /**\n     * Find facts for a given host name or host id\n     */\n    public static Object findFacts(NodeRosterFactsApi api,\n                                   Map<String, Object> paramsCfg) throws Exception {\n        String hostName = getHostName(paramsCfg);\n        UUID hostId = getHostId(paramsCfg);\n\n        if (hostName != null) {\n            log.info(\"Finding facts for hostname {}\", hostName);\n        } else if (hostId != null) {\n            log.info(\"Finding facts for host id {}\", hostId);\n        } else {\n            throw new IllegalArgumentException(\"A 'hostName' or 'hostId' value is required\");\n        }\n\n        return ClientUtils.withRetry(Constants.RETRY_COUNT, Constants.RETRY_INTERVAL, () ->\n                api.getFacts(hostId, hostName));\n    }\n\n    /**\n     * Find hosts with a deployed artifact\n     */\n    public static List<HostEntry> findHostsWithArtifacts(NodeRosterHostsApi api,\n                                                         Map<String, Object> paramsCfg) throws Exception {\n\n        String artifactPattern = MapUtils.assertString(paramsCfg, Constants.ARTIFACT_PATTERN_KEY);\n        int limit = getLimit(paramsCfg);\n        int offset = getOffset(paramsCfg);\n\n        log.info(\"Finding hosts where artifact {} is deployed \" +\n                \"(limit: {}, offset: {})...\", artifactPattern, limit, offset);\n\n        return ClientUtils.withRetry(Constants.RETRY_COUNT, Constants.RETRY_INTERVAL, () ->\n                api.listKnownHosts(null, artifactPattern, null, null, limit, offset));\n    }\n\n    /**\n     * Find artifacts deployed on a given host\n     */\n    public static List<ArtifactEntry> findDeployedArtifacts(NodeRosterArtifactsApi api,\n                                                            Map<String, Object> paramsCfg) throws Exception {\n\n        String hostName = getHostName(paramsCfg);\n        UUID hostId = getHostId(paramsCfg);\n        int limit = getLimit(paramsCfg);\n        int offset = getOffset(paramsCfg);\n\n        if (hostName == null && hostId == null) {\n            throw new IllegalArgumentException(\"A 'hostName' or 'hostId' value is required\");\n        }\n\n        log.info(\"Finding artifacts deployed on a host (hostName: {}, hostId: {})...\", hostName, hostId);\n\n        return ClientUtils.withRetry(Constants.RETRY_COUNT, Constants.RETRY_INTERVAL, () ->\n                api.listHostArtifacts(hostId, hostName, null, limit, offset));\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/noderoster/src/main/java/com/walmartlabs/concord/plugins/noderoster/NodeRosterTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.noderoster;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.plugins.noderoster.NodeRosterTaskUtils.getAction;\n\n@Named(\"nodeRoster\")\npublic class NodeRosterTaskV2 implements Task {\n\n    private final ApiClient apiClient;\n\n    @Inject\n    public NodeRosterTaskV2(ApiClient apiClient) {\n        this.apiClient = apiClient;\n    }\n\n    @Override\n    public TaskResult execute(Variables input) throws Exception {\n        Map<String, Object> paramsCfg = input.toMap();\n        NodeRosterTaskUtils.Action action = getAction(paramsCfg);\n\n        switch (action) {\n            case HOSTSWITHARTIFACTS: {\n                return findHostsWithArtifacts(paramsCfg);\n            }\n            case FACTS: {\n                return findFacts(paramsCfg);\n            }\n            case DEPLOYEDONHOST: {\n                return findDeployedArtifacts(paramsCfg);\n            }\n            default:\n                throw new IllegalArgumentException(\"Unsupported action type: \" + action);\n        }\n    }\n\n    /**\n     * Find facts for a given host name or host id\n     */\n    public TaskResult findFacts(Map<String, Object> paramsCfg) throws Exception {\n        NodeRosterFactsApi api = new NodeRosterFactsApi(apiClient);\n        Object facts = NodeRosterTaskUtils.findFacts(api, paramsCfg);\n        return result(\"facts\", facts);\n    }\n\n    /**\n     * Find hosts with a deployed artifact\n     */\n    public TaskResult findHostsWithArtifacts(Map<String, Object> paramsCfg) throws Exception {\n        NodeRosterHostsApi api = new NodeRosterHostsApi(apiClient);\n        List<HostEntry> hosts = NodeRosterTaskUtils.findHostsWithArtifacts(api, paramsCfg);\n        return result(\"hosts\", hosts);\n    }\n\n    /**\n     * Find artifacts deployed on a given host\n     */\n    public TaskResult findDeployedArtifacts(Map<String, Object> paramsCfg) throws Exception {\n        NodeRosterArtifactsApi api = new NodeRosterArtifactsApi(apiClient);\n        List<ArtifactEntry> deployedArtifacts = NodeRosterTaskUtils.findDeployedArtifacts(api, paramsCfg);\n        return result(\"artifacts\", deployedArtifacts);\n    }\n\n    private static TaskResult result(String key, Object data) {\n        return TaskResult.of(data != null)\n                .value(key, data);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/noderoster/src/main/java/com/walmartlabs/concord/plugins/noderoster/Result.java",
    "content": "package com.walmartlabs.concord.plugins.noderoster;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\n\nimport java.io.Serializable;\n\n@JsonInclude(JsonInclude.Include.NON_NULL)\npublic class Result implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok;\n    private final Object data;\n\n    public static Result createResponse(Object data) {\n        return new Result(data != null, data);\n    }\n\n    public Result(boolean ok, Object data) {\n        this.ok = ok;\n        this.data = data;\n    }\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    public Object getData() {\n        return data;\n    }\n\n    @Override\n    public String toString() {\n        return \"Response{\" +\n                \"ok=\" + ok +\n                \", data='\" + data + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/resource/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>resource-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-core</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.dataformat</groupId>\n            <artifactId>jackson-dataformat-yaml</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jsr310</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jdk8</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <resources>\n            <resource>\n                <filtering>false</filtering>\n                <directory>${project.basedir}/src/main/resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n            <resource>\n                <filtering>true</filtering>\n                <directory>${project.basedir}/src/main/filtered-resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n        </resources>\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <configuration>\n                    <!-- no verify, as this plugin depends on runtime-v2 -->\n                    <skipVerify>true</skipVerify>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/tasks/resource/src/main/java/com/walmartlabs/concord/plugins/resource/Evaluator.java",
    "content": "package com.walmartlabs.concord.plugins.resource;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface Evaluator {\n\n    Object eval(Object v);\n}\n"
  },
  {
    "path": "plugins/tasks/resource/src/main/java/com/walmartlabs/concord/plugins/resource/FileService.java",
    "content": "package com.walmartlabs.concord.plugins.resource;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.nio.file.Path;\n\npublic interface FileService {\n\n    Path createTempFile(String prefix, String suffix) throws IOException;\n}\n"
  },
  {
    "path": "plugins/tasks/resource/src/main/java/com/walmartlabs/concord/plugins/resource/ResourceTask.java",
    "content": "package com.walmartlabs.concord.plugins.resource;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.InjectVariable;\nimport com.walmartlabs.concord.sdk.Task;\n\nimport javax.inject.Named;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\n@Named(\"resource\")\n@SuppressWarnings(\"unused\")\npublic class ResourceTask implements Task {\n\n    @InjectVariable(\"workDir\")\n    private String workDir;\n\n    public String asString(String path) throws IOException {\n        return delegate(null).asString(path);\n    }\n\n    public Object asJson(String path) throws IOException {\n        return asJson(null, path, false);\n    }\n\n    public Object asJson(@InjectVariable(\"context\") Context ctx, String path, boolean eval) throws IOException {\n        return delegate(ctx).asJson(path, eval);\n    }\n\n    public Object fromJsonString(String jsonString) throws IOException {\n        return fromJsonString(null, jsonString, false);\n    }\n\n    public Object fromJsonString(@InjectVariable(\"context\") Context ctx,\n                                 String jsonString, boolean eval) throws IOException {\n        return delegate(ctx).fromJsonString(jsonString, eval);\n    }\n\n    public Object fromYamlString(String yamlString) throws IOException {\n        return fromYamlString(null, yamlString, false);\n    }\n\n    public Object fromYamlString(@InjectVariable(\"context\") Context ctx,\n                                 String yamlString, boolean eval) throws IOException {\n        return delegate(ctx).fromYamlString(yamlString, eval);\n    }\n\n    public Object asYaml(String path) throws IOException {\n        return asYaml(null, path, false);\n    }\n\n    public Object asYaml(@InjectVariable(\"context\") Context ctx, String path, boolean eval) throws IOException {\n        return delegate(ctx).asYaml(path, eval);\n    }\n\n    public String writeAsJson(Object content, @InjectVariable(\"workDir\") String workDir) throws IOException {\n        return delegate(null).writeAsJson(content);\n    }\n\n    public String writeAsString(String content, @InjectVariable(\"workDir\") String workDir) throws IOException {\n        return delegate(null).writeAsString(content);\n    }\n\n    public String writeAsYaml(Object content, @InjectVariable(\"workDir\") String workDir) throws IOException {\n        return delegate(null).writeAsYaml(content);\n    }\n\n    public String printJson(Object value) throws IOException {\n        return ResourceTaskCommon.printJson(value);\n    }\n\n    public String prettyPrintJson(Object value) throws IOException {\n        return ResourceTaskCommon.prettyPrintJson(value);\n    }\n\n    public String prettyPrintYaml(Object value) throws IOException {\n        return ResourceTaskCommon.prettyPrintYaml(value);\n    }\n\n    public String prettyPrintYaml(Object value, int indent) throws IOException {\n        return ResourceTaskCommon.prettyPrintYaml(value, indent);\n    }\n\n    private ResourceTaskCommon delegate(Context ctx) {\n        Evaluator evaluator = null;\n        if (ctx != null) {\n            evaluator = ctx::interpolate;\n        }\n        Path wd = Paths.get(workDir);\n        return new ResourceTaskCommon(wd,\n                (prefix, suffix) -> createTempFile(wd, prefix, suffix), evaluator);\n    }\n\n    private static Path createTempFile(Path baseDir, String prefix, String suffix) throws IOException {\n        Path tempDir = assertTempDir(baseDir);\n        return Files.createTempFile(tempDir, prefix, suffix);\n    }\n\n    private static Path assertTempDir(Path baseDir) throws IOException {\n        Path p = baseDir.resolve(Constants.Files.CONCORD_TMP_DIR_NAME);\n        if (!p.toFile().exists()) {\n            Files.createDirectories(p);\n        }\n\n        return p;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/resource/src/main/java/com/walmartlabs/concord/plugins/resource/ResourceTaskCommon.java",
    "content": "package com.walmartlabs.concord.plugins.resource;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonFactory;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.ObjectWriter;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLFactory;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Arrays;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Properties;\n\nimport static com.walmartlabs.concord.common.PathUtils.assertInPath;\n\npublic class ResourceTaskCommon {\n\n    private static final String RESOURCE_PREFIX = \"resource_\";\n    private static final String TEXT_FILE_SUFFIX = \".txt\";\n    private static final String YAML_FILE_SUFFIX = \".yaml\";\n    private static final String JSON_FILE_SUFFIX = \".json\";\n\n    private final Path workDir;\n    private final FileService fileService;\n    private final Evaluator evaluator;\n\n    public ResourceTaskCommon(Path workDir, FileService fileService, Evaluator evaluator) {\n        this.workDir = workDir;\n        this.fileService = fileService;\n        this.evaluator = evaluator;\n    }\n\n    public String asString(String path) throws IOException {\n        byte[] ab = Files.readAllBytes(normalizePath(path));\n        return new String(ab);\n    }\n\n    public Object asJson(String path) throws IOException {\n        return asJson(path, false);\n    }\n\n    public Object asJson(String path, boolean eval) throws IOException {\n        try (InputStream in = Files.newInputStream(normalizePath(path))) {\n            Object result = createObjectMapper().readValue(in, Object.class);\n            if (eval) {\n                return evaluator.eval(result);\n            } else {\n                return result;\n            }\n        }\n    }\n\n    public Map<String, Object> asProperties(String path) throws IOException {\n        return asProperties(path, false);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public Map<String, Object> asProperties(String path, boolean eval) throws IOException {\n        try (InputStream in = Files.newInputStream(normalizePath(path))) {\n            Properties props = new Properties();\n            props.load(in);\n\n            HashMap<String, Object> result = new HashMap<>();\n            for (final String name : props.stringPropertyNames()) {\n                result.put(name, props.getProperty(name));\n            }\n\n            if (eval) {\n                return (Map<String, Object>) evaluator.eval(result);\n            } else {\n                return result;\n            }\n        }\n    }\n\n    public Object fromJsonString(String jsonsString) throws IOException {\n        return fromJsonString(jsonsString, false);\n    }\n\n    public Object fromJsonString(String jsonString, boolean eval) throws IOException {\n        Object result = createObjectMapper().readValue(jsonString, Object.class);\n        if (eval) {\n            return evaluator.eval(result);\n        } else {\n            return result;\n        }\n    }\n\n    public Object fromYamlString(String yamlString) throws IOException {\n        return fromYamlString(yamlString, false);\n    }\n\n    public Object fromYamlString(String yamlString, boolean eval) throws IOException {\n        Object result = createObjectMapper(new YAMLFactory()).readValue(yamlString, Object.class);\n        if (eval) {\n            return evaluator.eval(result);\n        } else {\n            return result;\n        }\n    }\n\n    public Object asYaml(String path) throws IOException {\n        return asYaml(path, false);\n    }\n\n    public Object asYaml(String path, boolean eval) throws IOException {\n        try (InputStream in = Files.newInputStream(normalizePath(path))) {\n            Object result = createObjectMapper(new YAMLFactory()).readValue(in, Object.class);\n            if (eval) {\n                return evaluator.eval(result);\n            } else {\n                return result;\n            }\n        }\n    }\n\n    public String writeAsJson(Object content) throws IOException {\n        Path tmpFile = fileService.createTempFile(RESOURCE_PREFIX, JSON_FILE_SUFFIX);\n        try (OutputStream out = Files.newOutputStream(tmpFile)) {\n            createObjectMapper().writerWithDefaultPrettyPrinter().writeValue(out, content);\n        }\n        return workDir.relativize(tmpFile.toAbsolutePath()).toString();\n    }\n\n    public String writeAsJson(Object content, String path) throws IOException {\n        Path dst = assertWorkDirPath(path);\n        try (OutputStream out = Files.newOutputStream(dst)) {\n            createObjectMapper().writerWithDefaultPrettyPrinter().writeValue(out, content);\n        }\n        return workDir.relativize(dst.toAbsolutePath()).toString();\n    }\n\n    public String writeAsString(String content) throws IOException {\n        Path tmpFile = fileService.createTempFile(RESOURCE_PREFIX, TEXT_FILE_SUFFIX);\n        Files.write(tmpFile, content.getBytes());\n        return workDir.relativize(tmpFile.toAbsolutePath()).toString();\n    }\n\n    public String writeAsString(String content, String path) throws IOException {\n        Path dst = assertWorkDirPath(path);\n        Files.write(dst, content.getBytes());\n        return workDir.relativize(dst.toAbsolutePath()).toString();\n    }\n\n    public String writeAsYaml(Object content) throws IOException {\n        Path tmpFile = fileService.createTempFile(RESOURCE_PREFIX, YAML_FILE_SUFFIX);\n        try (OutputStream out = Files.newOutputStream(tmpFile)) {\n            createYamlWriter().writeValue(out, content);\n        }\n        return workDir.relativize(tmpFile.toAbsolutePath()).toString();\n    }\n\n    public String writeAsYaml(Object content, String path) throws IOException {\n        Path dst = assertWorkDirPath(path);\n        try (OutputStream out = Files.newOutputStream(dst)) {\n            createYamlWriter().writeValue(out, content);\n        }\n        return workDir.relativize(dst.toAbsolutePath()).toString();\n    }\n\n    public static String printJson(Object value) throws IOException {\n        ObjectMapper mapper = createObjectMapper();\n\n        if (value instanceof String) {\n            // parse json string to object\n            value = mapper.readValue((String) value, Object.class);\n        }\n\n        return mapper.writeValueAsString(value);\n    }\n\n    public static String prettyPrintJson(Object value) throws IOException {\n        ObjectMapper mapper = createObjectMapper();\n\n        if (value instanceof String) {\n            // to add line feeds\n            value = mapper.readValue((String) value, Object.class);\n        }\n\n        return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(value);\n    }\n\n    public static String prettyPrintYaml(Object value) throws IOException {\n        return prettyPrintYaml(value, 0);\n    }\n\n    public static String prettyPrintYaml(Object value, int indent) throws IOException {\n        ObjectMapper mapper = createObjectMapper(new YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER));\n\n        if (value instanceof String) {\n            value = mapper.readValue((String) value, Object.class);\n        }\n\n        String prefix = null;\n        if (indent > 0) {\n            char[] ch = new char[indent + 1];\n            ch[0] = '\\n';\n            Arrays.fill(ch, 1, ch.length, ' ');\n            prefix = new String(ch);\n        }\n\n        String s = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(value);\n        if (prefix != null) {\n            s = prefix + s.replace(\"\\n\", prefix);\n        }\n        return s;\n    }\n\n    private Path normalizePath(String path) {\n        return assertWorkDirPath(path);\n    }\n\n    private Path assertWorkDirPath(String path) {\n        try {\n            return assertInPath(workDir,path);\n        } catch (IOException ex) {\n            throw new IllegalArgumentException(\"Not authorized to access file outside of working directory: \" + path);\n        }\n    }\n\n    private static ObjectWriter createYamlWriter() {\n        return createObjectMapper(new YAMLFactory()\n                .disable(YAMLGenerator.Feature.SPLIT_LINES))\n                .writerWithDefaultPrettyPrinter();\n    }\n\n    private static ObjectMapper createObjectMapper() {\n        return createObjectMapper(null);\n    }\n\n    private static ObjectMapper createObjectMapper(JsonFactory jf) {\n        ObjectMapper om = new ObjectMapper(jf);\n        om.registerModule(new Jdk8Module());\n        om.registerModule(new JavaTimeModule());\n        return om;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/resource/src/main/java/com/walmartlabs/concord/plugins/resource/v2/ResourceTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.resource.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.plugins.resource.ResourceTaskCommon;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.DryRunReady;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.io.IOException;\nimport java.util.Map;\n\n@Named(\"resource\")\n@DryRunReady\n@SuppressWarnings(\"unused\")\npublic class ResourceTaskV2 implements Task {\n\n    private final ResourceTaskCommon delegate;\n\n    @Inject\n    public ResourceTaskV2(Context ctx) {\n        this.delegate = new ResourceTaskCommon(ctx.workingDirectory(), ctx.fileService()::createTempFile, v -> ctx.eval(v, Object.class));\n    }\n\n    public String asString(String path) throws IOException {\n        return delegate.asString(path);\n    }\n\n    public Object asJson(String path) throws IOException {\n        return delegate.asJson(path);\n    }\n\n    public Object asJson(String path, boolean eval) throws IOException {\n        return delegate.asJson(path, eval);\n    }\n\n    public Map<String, Object> asProperties(String path) throws IOException {\n        return delegate.asProperties(path);\n    }\n\n    public Map<String, Object> asProperties(String path, boolean eval) throws IOException {\n        return delegate.asProperties(path, eval);\n    }\n\n    public Object fromJsonString(String jsonString) throws IOException {\n        return fromJsonString(jsonString, false);\n    }\n\n    public Object fromJsonString(String jsonString, boolean eval) throws IOException {\n        return delegate.fromJsonString(jsonString, eval);\n    }\n\n    public Object fromYamlString(String yamlString) throws IOException {\n        return fromYamlString(yamlString, false);\n    }\n\n    public Object fromYamlString(String yamlString, boolean eval) throws IOException {\n        return delegate.fromYamlString(yamlString, eval);\n    }\n\n    public Object asYaml(String path) throws IOException {\n        return delegate.asYaml(path);\n    }\n\n    public Object asYaml(String path, boolean eval) throws IOException {\n        return delegate.asYaml(path, eval);\n    }\n\n    public String writeAsJson(Object content) throws IOException {\n        return delegate.writeAsJson(content);\n    }\n\n    public String writeAsJson(Object content, String path) throws IOException {\n        return delegate.writeAsJson(content, path);\n    }\n\n    public String writeAsString(String content) throws IOException {\n        return delegate.writeAsString(content);\n    }\n\n    public String writeAsString(String content, String path) throws IOException {\n        return delegate.writeAsString(content, path);\n    }\n\n    public String writeAsYaml(Object content) throws IOException {\n        return delegate.writeAsYaml(content);\n    }\n\n    public String writeAsYaml(Object content, String path) throws IOException {\n        return delegate.writeAsYaml(content, path);\n    }\n\n    public String printJson(Object value) throws IOException {\n        return ResourceTaskCommon.printJson(value);\n    }\n\n    public String prettyPrintJson(Object value) throws IOException {\n        return ResourceTaskCommon.prettyPrintJson(value);\n    }\n\n    public String prettyPrintYaml(Object value) throws IOException {\n        return ResourceTaskCommon.prettyPrintYaml(value);\n    }\n\n    public String prettyPrintYaml(Object value, int indent) throws IOException {\n        return ResourceTaskCommon.prettyPrintYaml(value, indent);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/resource/src/test/java/com/walmartlabs/concord/plugins/resource/ResourceTaskCommonTest.java",
    "content": "package com.walmartlabs.concord.plugins.resource;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLFactory;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.io.TempDir;\n\nimport java.io.IOException;\nimport java.net.URISyntaxException;\nimport java.net.URL;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.OffsetDateTime;\nimport java.util.*;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nclass ResourceTaskCommonTest {\n\n    @Test\n    void testPrettyPrintYaml() throws Exception {\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"x\", 123);\n        m.put(\"y\", Collections.singletonMap(\"a\", false));\n        m.put(\"jsr310\", OffsetDateTime.now());\n\n        assertValidYaml(ResourceTaskCommon.prettyPrintYaml(m, 0));\n        assertValidYaml(String.format(\"value: %s\", ResourceTaskCommon.prettyPrintYaml(m, 2)));\n        assertValidYaml(String.format(\"value: %s\", ResourceTaskCommon.prettyPrintYaml(Arrays.asList(\"a\", \"b\", \"c\"), 2)));\n    }\n\n    @Test\n    void testPrintJson() throws IOException {\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"x\", 123);\n        m.put(\"y\", \"hello\");\n\n        String result = ResourceTaskCommon.printJson(m);\n\n        assertFalse(result.contains(\"\\n\"));\n        assertTrue(result.contains(\"\\\"x\\\":123\"));\n        assertTrue(result.contains(\"\\\"y\\\":\\\"hello\"));\n    }\n\n    @Test\n    void testPrettyPrintJson() throws IOException {\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"x\", 123);\n        m.put(\"y\", \"hello\");\n\n        String result = ResourceTaskCommon.prettyPrintJson(m);\n\n        assertTrue(result.contains(\"\\n\"));\n        assertTrue(result.contains(\"\\\"x\\\" : 123\"));\n        assertTrue(result.contains(\"\\\"y\\\" : \\\"hello\\\"\"));\n    }\n\n    @Test\n    @SuppressWarnings(\"rawtypes\")\n    void testFromJsonString() throws Exception {\n        String jsonString = \"{ \\\"stringKey\\\": \\\"stringValue\\\", \\\"listKey\\\": [1,2,3]}\";\n        Path workDir = Paths.get(System.getProperty(\"user.dir\"));\n\n        ResourceTaskCommon rsc = new ResourceTaskCommon(workDir,\n                (prefix, suffix) -> createTempFile(workDir, prefix, suffix), null);\n        Object obj = rsc.fromJsonString(jsonString);\n\n        assertTrue(obj instanceof Map);\n        assertEquals(\"stringValue\", ((Map) obj).get(\"stringKey\"));\n        List l = (List) ((Map) obj).get(\"listKey\");\n        assertEquals(3, l.size());\n        assertEquals(2, l.get(1));\n    }\n\n    @Test\n    void testFromYamlString() throws Exception {\n        var yamlString = \"\"\"\n                stringKey: stringValue\n                numList: [1, 2, 3]\n                strList:\n                  - a\n                  - b\n                  - c\n                map:\n                 key: value\n                 list:\n                   - 1\n                   - 2\n                 listOfMaps:\n                   - key1: value1\n                   - key2:\n                       key22: value2\"\"\";\n\n        var workDir = Paths.get(System.getProperty(\"user.dir\"));\n        var rsc = new ResourceTaskCommon(workDir,\n                (prefix, suffix) -> createTempFile(workDir, prefix, suffix), null);\n\n        var fromYaml = assertInstanceOf(Map.class, rsc.fromYamlString(yamlString));\n        assertEquals(\"stringValue\", fromYaml.get(\"stringKey\"));\n\n        var numList = assertInstanceOf(List.class, fromYaml.get(\"numList\"));\n        assertEquals(3, numList.size());\n        assertEquals(2, numList.get(1));\n\n        var strList = assertInstanceOf(List.class, fromYaml.get(\"strList\"));\n        assertEquals(\"a\", strList.get(0));\n\n        var map = assertInstanceOf(Map.class, fromYaml.get(\"map\"));\n        var listOfMaps = assertInstanceOf(List.class, map.get(\"listOfMaps\"));\n        assertEquals(\"value\", map.get(\"key\"));\n\n        var secondMap = assertInstanceOf(Map.class, listOfMaps.get(1));\n        var nestedMap = assertInstanceOf(Map.class, secondMap.get(\"key2\"));\n        assertEquals(\"value2\", nestedMap.get(\"key22\"));\n    }\n\n    @Test\n    void testAsProperties() throws Exception {\n        Path workDir = Paths.get(System.getProperty(\"user.dir\"));\n\n        ResourceTaskCommon rsc = new ResourceTaskCommon(workDir,\n                (prefix, suffix) -> createTempFile(workDir, prefix, suffix), null);\n\n        Map<String, Object> result = rsc.asProperties(resource(\"test.properties\").toString());\n\n        assertEquals(\"value2\", result.get(\"param2\"));\n    }\n\n    @Test\n    void testWriteToKnownPath(@TempDir Path workDir) throws Exception {\n        FileService unneededFileService = (prefix, suffix) -> {\n            throw new IllegalStateException();\n        };\n\n        ResourceTaskCommon task = new ResourceTaskCommon(workDir, unneededFileService, null);\n\n        String stringContent = \"Hello, world!\";\n        task.writeAsString(stringContent, \"test.txt\");\n\n        Object jsonContent = Map.of(\"key\", \"value\");\n        task.writeAsJson(jsonContent, \"test.json\");\n\n        Object yamlContent = Map.of(\"key\", \"value\", \"but\", \"worse\");\n        task.writeAsYaml(yamlContent, \"test.yaml\");\n\n        Path txt = workDir.resolve(\"test.txt\");\n        assertTrue(Files.exists(txt));\n        assertEquals(stringContent, Files.readString(txt));\n\n        Path json = workDir.resolve(\"test.json\");\n        assertTrue(Files.exists(json));\n        assertEquals(\"{\\n  \\\"key\\\" : \\\"value\\\"\\n}\", Files.readString(json));\n\n        Path yaml = workDir.resolve(\"test.yaml\");\n        assertTrue(Files.exists(yaml));\n        assertTrue(Files.readString(yaml).contains(\"but: \\\"worse\\\"\"));\n\n        // test bad paths\n\n        assertThrows(IllegalArgumentException.class, () -> task.writeAsString(stringContent, \"../test.txt\"));\n        assertThrows(IllegalArgumentException.class, () -> task.writeAsJson(jsonContent, workDir.toAbsolutePath() + \"./../../../../test.json\"));\n\n        // unicode paths\n\n        String path = \"z̶̧̛͖̯̞͈̗̼̦̫̱͕̤̱͐̀̄́̋̊̂̂́̆́̚̕͘ͅā̸͓͍̣͂͋̓́̈͆͋̀̿͑́͗͘̚ļ̸̙̙͔̦̺̙̘̻̗̿̓̄̅̿̓̿̈̿̽̊̿̂́̚͝͝g̴̡͇͚̩̻̮̙̻͔͆́͆̀̈́́̎͋o̷̪̖̥̹̲̣͇̣͂̇͆͆́̓̇͑̌\";\n        task.writeAsString(\"Hello!\", path);\n        assertTrue(Files.exists(workDir.resolve(path)));\n        assertEquals(\"Hello!\", Files.readString(workDir.resolve(path)));\n    }\n\n    private static void assertValidYaml(String s) throws IOException {\n        new ObjectMapper(new YAMLFactory()).readValue(s, Object.class);\n    }\n\n    private static Path createTempFile(Path baseDir, String prefix, String suffix) throws IOException {\n        Path tempDir = assertTempDir(baseDir);\n        return Files.createTempFile(tempDir, prefix, suffix);\n    }\n\n    private static Path assertTempDir(Path baseDir) throws IOException {\n        Path p = baseDir.resolve(Constants.Files.CONCORD_TMP_DIR_NAME);\n        if (!p.toFile().exists()) {\n            Files.createDirectories(p);\n        }\n\n        return p;\n    }\n\n    private static Path resource(String name) throws URISyntaxException {\n        URL url = ResourceTaskCommonTest.class.getResource(name);\n        assertNotNull(url, \"can't find '\" + name + \"'\");\n        return Paths.get(url.toURI());\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/resource/src/test/resources/com/walmartlabs/concord/plugins/resource/test.properties",
    "content": "param1=value1\nparam2=value2"
  },
  {
    "path": "plugins/tasks/slack/README.md",
    "content": "# slack-tasks\n\nSend messages and manage channels.\n\n## Required OAuth Permissions\n\nRequired `slack` task OAuth scopes:\n\n- [`chat:write`](https://api.slack.com/scopes/chat:write) - \n    For sending messages to channels.\n- [`chat:write.customize`](https://api.slack.com/scopes/chat:write.customize) -\n    For customizing message `username` and `iconEmoji`.\n- [`reactions:write`](https://docs.slack.dev/reference/scopes/reactions.write) -\n    For adding reactions to messages using the `addReaction` action.\n- [`chat:write.public`](https://api.slack.com/scopes/chat:write.public) -\n    Optional, for sending messages to public channels without membership.\n\nRequired `slackChannel` task OAuth scopes:\n\n- [`channels:manage`](https://api.slack.com/scopes/channels:manage) -\n    For creating and archiving public channels.\n- [`groups:write`](https://api.slack.com/scopes/groups:write) -\n    For creating and archiving private channels.\n"
  },
  {
    "path": "plugins/tasks/slack/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>slack-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.httpcomponents</groupId>\n            <artifactId>httpclient</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.httpcomponents</groupId>\n            <artifactId>httpcore</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <dependencies>\n                    <dependency>\n                        <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n                        <artifactId>concord-runner-v2</artifactId>\n                        <version>${project.version}</version>\n                    </dependency>\n                </dependencies>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/tasks/slack/src/main/java/com/walmartlabs/concord/plugins/slack/ContextVariables.java",
    "content": "package com.walmartlabs.concord.plugins.slack;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\nimport com.walmartlabs.concord.sdk.Context;\n\nimport java.util.Map;\n\npublic class ContextVariables implements Variables {\n\n    private final Context context;\n\n    public ContextVariables(Context context) {\n        this.context = context;\n    }\n\n    @Override\n    public Object get(String key) {\n        return context.getVariable(key);\n    }\n\n    @Override\n    public void set(String key, Object value) {\n        throw new IllegalStateException(\"Unsupported\");\n    }\n\n    @Override\n    public boolean has(String key) {\n        return context.getVariable(key) != null;\n    }\n\n    @Override\n    public Map<String, Object> toMap() {\n        return context.toMap();\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/slack/src/main/java/com/walmartlabs/concord/plugins/slack/Slack.java",
    "content": "package com.walmartlabs.concord.plugins.slack;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.plugins.slack.SlackClient.Response;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collection;\n\npublic class Slack {\n\n    private static final Logger log = LoggerFactory.getLogger(Slack.class);\n\n    public static Response sendMessage(SlackConfiguration slackCfg,\n                                       String channelId,\n                                       String ts,\n                                       boolean replyBroadcast,\n                                       String text,\n                                       String iconEmoji,\n                                       String username,\n                                       Collection<Object> attachments,\n                                       Collection<Object> blocks) throws Exception {\n\n        try (SlackClient client = new SlackClient(slackCfg)) {\n            Response r = client.message(channelId, ts, replyBroadcast, text, iconEmoji, username, attachments, blocks);\n            if (!r.isOk()) {\n                log.warn(\"Error sending a Slack message: {}\", r.getError());\n            } else {\n                log.info(\"Slack message sent into '{}' channel\", channelId);\n            }\n\n            return r;\n        }\n    }\n\n    private Slack() {\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/slack/src/main/java/com/walmartlabs/concord/plugins/slack/SlackChannelTask.java",
    "content": "package com.walmartlabs.concord.plugins.slack;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.ContextUtils;\nimport com.walmartlabs.concord.sdk.Task;\n\nimport javax.inject.Named;\nimport java.util.Collections;\nimport java.util.Map;\n\n@Named(\"slackChannel\")\n@SuppressWarnings(\"unused\")\npublic class SlackChannelTask implements Task {\n\n    private final SlackChannelTaskCommon delegate = new SlackChannelTaskCommon();\n\n    @Override\n    public void execute(Context ctx) throws Exception {\n        Map<String, Object> result = delegate.execute(SlackChannelTaskParams.of(new ContextVariables(ctx), defaults(ctx)));\n        result.forEach(ctx::setVariable);\n    }\n\n    private static Map<String, Object> defaults(Context ctx) {\n        return ContextUtils.getMap(ctx, \"slackCfg\", Collections.emptyMap());\n    }\n\n}\n"
  },
  {
    "path": "plugins/tasks/slack/src/main/java/com/walmartlabs/concord/plugins/slack/SlackChannelTaskCommon.java",
    "content": "package com.walmartlabs.concord.plugins.slack;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collections;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.plugins.slack.SlackChannelTaskParams.*;\nimport static com.walmartlabs.concord.plugins.slack.SlackClient.Response;\n\npublic class SlackChannelTaskCommon {\n\n    private static final Logger log = LoggerFactory.getLogger(SlackChannelTaskCommon.class);\n\n    public static final String SLACK_CHANNEL_ID_KEY = \"slackChannelId\";\n\n    public Map<String, Object> execute(SlackChannelTaskParams in) throws Exception {\n        log.debug(\"Starting '{}' action...\", in.action());\n\n        switch (in.action()) {\n            case CREATE: {\n                return createChannel((CreateChannelParams)in);\n            }\n            case CREATEGROUP: {\n                return createGroup((CreateGroupParams)in);\n            }\n            case ARCHIVE: {\n                archiveChannel((ArchiveChannelParams)in);\n                break;\n            }\n            case ARCHIVEGROUP: {\n                archiveGroup((ArchiveGroupParams)in);\n                break;\n            }\n            default:\n                throw new IllegalArgumentException(\"Unsupported action type: \" + in.action());\n        }\n        return Collections.emptyMap();\n    }\n\n    private static Map<String, Object> createChannel(CreateChannelParams in) throws Exception {\n        String channelName = in.channelName();\n\n        SlackConfiguration cfg = SlackConfiguration.from(in.cfg());\n        try (SlackClient client = new SlackClient(cfg)) {\n            Response r = client.createChannel(channelName);\n            handleError(in.action(), r, channelName);\n\n            String channelId = Utils.extractString(r, \"channel\", \"id\");\n            log.info(\"Slack channel created: {} -> {} (stored as '{}' variable)\", channelName, channelId, SLACK_CHANNEL_ID_KEY);\n            return Collections.singletonMap(SLACK_CHANNEL_ID_KEY, channelId);\n        }\n    }\n\n    private static Map<String, Object> createGroup(CreateGroupParams in) throws Exception {\n        String channelName = in.channelName();\n\n        SlackConfiguration cfg = SlackConfiguration.from(in.cfg());\n        try (SlackClient client = new SlackClient(cfg)) {\n            Response r = client.createGroupChannel(channelName);\n            handleError(in.action(), r, channelName);\n\n            String channelId = Utils.extractString(r, \"group\", \"id\");\n            log.info(\"Slack group created: {} -> {} (stored as '{}' variable)\", channelName, channelId, SLACK_CHANNEL_ID_KEY);\n            return Collections.singletonMap(SLACK_CHANNEL_ID_KEY, channelId);\n        }\n    }\n\n    private static void archiveChannel(ArchiveChannelParams in) throws Exception {\n        String channelId = in.channelId();\n\n        SlackConfiguration cfg = SlackConfiguration.from(in.cfg());\n        try (SlackClient client = new SlackClient(cfg)) {\n            Response r = cfg.isLegacy()\n                    ? client.archiveChannel(channelId)\n                    : client.archiveConversation(channelId);\n            handleError(in.action(), r, channelId);\n        }\n    }\n\n    private static void archiveGroup(ArchiveGroupParams in) throws Exception {\n        String channelId = in.channelId();\n\n        SlackConfiguration cfg = SlackConfiguration.from(in.cfg());\n        try (SlackClient client = new SlackClient(cfg)) {\n            Response r = client.archiveGroup(channelId);\n            handleError(in.action(), r, channelId);\n        }\n    }\n\n    private static void handleError(Action action, Response r, String channelId) {\n        if (!r.isOk()) {\n            throw new RuntimeException(action + \" error (channel: '\" + channelId + \"'): \" + r.getError());\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/slack/src/main/java/com/walmartlabs/concord/plugins/slack/SlackChannelTaskParams.java",
    "content": "package com.walmartlabs.concord.plugins.slack;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport java.util.Arrays;\nimport java.util.Map;\n\npublic class SlackChannelTaskParams {\n\n    public static SlackChannelTaskParams of(Variables input, Map<String, Object> defaults) {\n        Variables variables = Utils.merge(input, defaults);\n\n        SlackChannelTaskParams p = new SlackChannelTaskParams(variables);\n        switch (p.action()) {\n            case CREATE: {\n                return new CreateChannelParams(variables);\n            }\n            case CREATEGROUP: {\n                return new CreateGroupParams(variables);\n            }\n            case ARCHIVE: {\n                return new ArchiveChannelParams(variables);\n            }\n            case ARCHIVEGROUP: {\n                return new ArchiveGroupParams(variables);\n            }\n            default: {\n                throw new IllegalArgumentException(\"Unsupported action type: \" + p.action());\n            }\n        }\n    }\n\n    private static final String ACTION = \"action\";\n\n    protected final Variables variables;\n    private final SlackConfigurationParams cfg;\n\n    public SlackChannelTaskParams(Variables variables) {\n        this.variables = variables;\n        this.cfg = new SlackConfigurationParams(variables);\n    }\n\n    public Action action() {\n        String action = variables.assertString(ACTION);\n        try {\n            return Action.valueOf(action.trim().toUpperCase());\n        } catch (IllegalArgumentException e) {\n            throw new RuntimeException(\"Unknown action: '\" + action + \"'. Available actions: \" + Arrays.toString(SlackTaskParams.Action.values()));\n        }\n    }\n\n    public static class CreateChannelParams extends SlackChannelTaskParams {\n\n        private static final String CHANNEL_NAME = \"channelName\";\n\n        public CreateChannelParams(Variables variables) {\n            super(variables);\n        }\n\n        public String channelName() {\n            return variables.assertString(CHANNEL_NAME);\n        }\n    }\n\n    public static class CreateGroupParams extends SlackChannelTaskParams {\n\n        private static final String CHANNEL_NAME = \"channelName\";\n\n        public CreateGroupParams(Variables variables) {\n            super(variables);\n        }\n\n        public String channelName() {\n            return variables.assertString(CHANNEL_NAME);\n        }\n    }\n\n    public static class ArchiveChannelParams extends SlackChannelTaskParams {\n\n        private static final String CHANNEL_ID = \"channelId\";\n\n        public ArchiveChannelParams(Variables variables) {\n            super(variables);\n        }\n\n        public String channelId() {\n            return variables.assertString(CHANNEL_ID);\n        }\n    }\n\n    public static class ArchiveGroupParams extends SlackChannelTaskParams {\n\n        private static final String CHANNEL_ID = \"channelId\";\n\n        public ArchiveGroupParams(Variables variables) {\n            super(variables);\n        }\n\n        public String channelId() {\n            return variables.assertString(CHANNEL_ID);\n        }\n    }\n\n    public SlackConfigurationParams cfg() {\n        return cfg;\n    }\n\n    public enum Action {\n        CREATE,\n        CREATEGROUP,\n        ARCHIVE,\n        ARCHIVEGROUP\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/slack/src/main/java/com/walmartlabs/concord/plugins/slack/SlackClient.java",
    "content": "package com.walmartlabs.concord.plugins.slack;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.*;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.apache.http.*;\nimport org.apache.http.client.config.RequestConfig;\nimport org.apache.http.client.methods.CloseableHttpResponse;\nimport org.apache.http.client.methods.HttpPost;\nimport org.apache.http.config.Registry;\nimport org.apache.http.config.RegistryBuilder;\nimport org.apache.http.conn.HttpClientConnectionManager;\nimport org.apache.http.conn.socket.ConnectionSocketFactory;\nimport org.apache.http.conn.socket.PlainConnectionSocketFactory;\nimport org.apache.http.conn.ssl.NoopHostnameVerifier;\nimport org.apache.http.conn.ssl.SSLConnectionSocketFactory;\nimport org.apache.http.entity.ContentType;\nimport org.apache.http.entity.StringEntity;\nimport org.apache.http.impl.client.CloseableHttpClient;\nimport org.apache.http.impl.client.HttpClientBuilder;\nimport org.apache.http.impl.conn.PoolingHttpClientConnectionManager;\nimport org.apache.http.message.BasicHeader;\nimport org.apache.http.util.EntityUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.net.ssl.KeyManager;\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.TrustManager;\nimport javax.net.ssl.X509TrustManager;\nimport java.io.IOException;\nimport java.security.SecureRandom;\nimport java.security.cert.CertificateException;\nimport java.security.cert.X509Certificate;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class SlackClient implements AutoCloseable {\n\n    private static final Logger log = LoggerFactory.getLogger(SlackClient.class);\n\n    private static final String SLACK_API_ROOT = \"https://slack.com/api/\";\n    private static final String CHAT_POST_MESSAGE_CMD = \"chat.postMessage\";\n    private static final String CHAT_UPDATE_MESSAGE_CMD = \"chat.update\";\n    private static final String MESSAGE_ADD_REACTION_CMD = \"reactions.add\";\n    private static final String CREATE_CHANNEL_CMD = \"channels.create\";\n    private static final String CREATE_GROUP_CMD = \"groups.create\";\n    private static final String ARCHIVE_CHANNEL_CMD = \"channels.archive\";\n    private static final String ARCHIVE_CONVERSATION_CMD = \"conversations.archive\";\n    private static final String ARCHIVE_GROUP_CMD = \"groups.archive\";\n    private static final String AS_USER = \"as_user\";\n    private static final String CHANNEL = \"channel\";\n\n    private static final int TOO_MANY_REQUESTS_ERROR = 429;\n    private static final int DEFAULT_RETRY_AFTER = 10;\n\n    private final SlackConfiguration slackCfg;\n    private final int retryCount;\n    private final PoolingHttpClientConnectionManager connManager;\n    private final CloseableHttpClient client;\n\n    private final ObjectMapper objectMapper = new ObjectMapper();\n\n    public SlackClient(SlackConfiguration cfg) {\n        this.retryCount = cfg.getRetryCount();\n        this.connManager = createConnManager();\n        this.client = createClient(cfg, connManager);\n        this.slackCfg = cfg;\n    }\n\n    @Override\n    public void close() throws IOException {\n        client.close();\n        connManager.close();\n    }\n\n    public Response addReaction(String channelId, String ts, String reaction) throws IOException {\n        Map<String, Object> params = new HashMap<>();\n        params.put(CHANNEL, channelId);\n        params.put(\"timestamp\", ts);\n        params.put(\"name\", reaction);\n\n        return exec(MESSAGE_ADD_REACTION_CMD, params);\n    }\n\n    public Response postJsonMessage(String json) throws IOException {\n        return exec(CHAT_POST_MESSAGE_CMD, json);\n    }\n\n    public Response updateJsonMessage(String json) throws IOException {\n        return exec(CHAT_UPDATE_MESSAGE_CMD, json);\n    }\n\n    public Response message(String channelId, String ts, boolean replyBroadcast, String text, String iconEmoji, String username, Collection<Object> attachments, Collection<Object> blocks) throws IOException {\n        Map<String, Object> params = new HashMap<>();\n        params.put(CHANNEL, channelId);\n\n        if (slackCfg.isLegacy()) {\n            params.put(AS_USER, true);\n        }\n\n        params.put(\"text\", text);\n\n        if (ts != null) {\n            params.put(\"thread_ts\", ts);\n            params.put(\"reply_broadcast\", replyBroadcast);\n        }\n\n        if (iconEmoji != null) {\n            params.put(\"icon_emoji\", iconEmoji);\n            if (slackCfg.isLegacy()) {\n                params.put(AS_USER, false);\n            }\n        }\n\n        if (username != null) {\n            params.put(\"username\", username);\n            if (slackCfg.isLegacy()) {\n                params.put(AS_USER, false);\n            }\n        }\n\n        if (attachments != null) {\n            params.put(\"attachments\", attachments);\n        }\n\n        if (blocks != null && !blocks.isEmpty()) {\n            params.put(\"blocks\", blocks);\n        }\n\n        return exec(CHAT_POST_MESSAGE_CMD, params);\n    }\n\n    public Response createChannel(String channelName) throws IOException {\n        return exec(CREATE_CHANNEL_CMD, Collections.singletonMap(\"name\", channelName));\n    }\n\n    public Response createGroupChannel(String channelName) throws IOException {\n        return exec(CREATE_GROUP_CMD, Collections.singletonMap(\"name\", channelName));\n    }\n\n    public Response archiveChannel(String channelId) throws IOException {\n        return exec(ARCHIVE_CHANNEL_CMD, Collections.singletonMap(CHANNEL, channelId));\n    }\n\n    public Response archiveConversation(String id) throws IOException {\n        return exec(ARCHIVE_CONVERSATION_CMD, Map.of(CHANNEL, id));\n    }\n\n    public Response archiveGroup(String channelId) throws IOException {\n        return exec(ARCHIVE_GROUP_CMD, Collections.singletonMap(CHANNEL, channelId));\n    }\n\n    private Response exec(String command, Map<String, Object> params) throws IOException {\n        return exec(command, objectMapper.writeValueAsString(params));\n    }\n\n    private Response exec(String command, String json) throws IOException {\n        int retryAfter = DEFAULT_RETRY_AFTER;\n        IOException lastException = null;\n        Integer statusCode = null;\n\n        HttpPost request = new HttpPost(SLACK_API_ROOT + command);\n        request.setEntity(new StringEntity(json, ContentType.APPLICATION_JSON));\n\n        for (int attemptNo = 0; attemptNo <= retryCount; attemptNo++) {\n            if (attemptNo > 0) {\n                sleep(retryAfter * 1000L);\n                log.info(\"exec [{}, {}] -> attempt #{}. Retrying request after {} sec ...\",\n                        command, json, attemptNo, retryAfter);\n            }\n\n            try (CloseableHttpResponse response = client.execute(request)) {\n                statusCode = response.getStatusLine().getStatusCode();\n\n                if (statusCode == HttpStatus.SC_OK) {\n                    return parseResponse(response, command, json);\n                }\n\n                if (statusCode == TOO_MANY_REQUESTS_ERROR) {\n                    retryAfter = getRetryAfter(response);\n                }\n\n                log.warn(\"exec ['{}', '{}'] -> response code: {}\", command, json, statusCode);\n            } catch (IOException ie) {\n                log.error(\"exec [{}, {}] -> {}\", command, json, ie.getMessage());\n                lastException = ie;\n            }\n        }\n\n        if (lastException != null) {\n            throw lastException;\n        }\n\n        String error = \"Slack request not successful after \" + retryCount + \" retries. Last response code: \" + statusCode;\n        return new Response(false, null, error);\n    }\n\n    private Response parseResponse(CloseableHttpResponse response, String command,\n                                   String json) throws IOException {\n        Response r;\n\n        if (response.getEntity() == null) {\n            log.error(\"exec ['{}', '{}'] -> empty response\", command, json);\n            r = new Response(false, null, \"empty response\");\n        } else {\n            String s = EntityUtils.toString(response.getEntity());\n            r = objectMapper.readValue(s, Response.class);\n            log.debug(\"exec ['{}', '{}'] -> {}\", command, json, r);\n        }\n\n        return r;\n    }\n\n    private static int getRetryAfter(HttpResponse response) {\n        Header h = response.getFirstHeader(\"Retry-After\");\n        if (h == null) {\n            return DEFAULT_RETRY_AFTER;\n        }\n\n        try {\n            return Integer.parseInt(h.getValue());\n        } catch (Exception e) {\n            log.warn(\"getRetryAfter -> can't parse retry value '{}'\", h.getValue());\n            return DEFAULT_RETRY_AFTER;\n        }\n    }\n\n    private static void sleep(long t) {\n        try {\n            Thread.sleep(t);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    private static PoolingHttpClientConnectionManager createConnManager() {\n        try {\n            SSLContext sslContext = SSLContext.getInstance(\"TLS\");\n            sslContext.init(new KeyManager[0], new TrustManager[]{new DefaultTrustManager()}, new SecureRandom());\n\n            Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()\n                    .register(\"http\", PlainConnectionSocketFactory.INSTANCE)\n                    .register(\"https\", new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE))\n                    .build();\n\n            return new PoolingHttpClientConnectionManager(registry);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static CloseableHttpClient createClient(SlackConfiguration cfg, HttpClientConnectionManager connManager) {\n        Collection<Header> headers = Collections.singleton(new BasicHeader(HttpHeaders.AUTHORIZATION, \"Bearer \" + cfg.getAuthToken()));\n        return HttpClientBuilder.create()\n                .setDefaultRequestConfig(createConfig(cfg))\n                .setConnectionManager(connManager)\n                .setDefaultHeaders(headers)\n                .build();\n    }\n\n    private static RequestConfig createConfig(SlackConfiguration cfg) {\n        HttpHost proxy = null;\n        if (cfg.getProxyAddress() != null) {\n            proxy = new HttpHost(cfg.getProxyAddress(), cfg.getProxyPort(), \"http\");\n        }\n\n        return RequestConfig.custom()\n                .setConnectTimeout(cfg.getConnectTimeout())\n                .setSocketTimeout(cfg.getSoTimeout())\n                .setProxy(proxy)\n                .build();\n    }\n\n    @JsonInclude(JsonInclude.Include.NON_NULL)\n    @JsonIgnoreProperties(ignoreUnknown = true)\n    public static class Response {\n\n        private final boolean ok;\n        private final String ts;\n        private final String error;\n        private final Map<String, Object> params = new HashMap<>();\n\n        @JsonCreator\n        public Response(@JsonProperty(\"ok\") boolean ok,\n                        @JsonProperty(\"ts\") String ts,\n                        @JsonProperty(\"error\") String error) {\n\n            this.ok = ok;\n            this.ts = ts;\n            this.error = error;\n        }\n\n        public boolean isOk() {\n            return ok;\n        }\n\n        public String getError() {\n            return error;\n        }\n\n        public String getTs() {\n            return ts;\n        }\n\n        @JsonAnyGetter\n        public Map<String, Object> getParams() {\n            return params;\n        }\n\n        @JsonAnySetter\n        public void setParams(String name, Object value) {\n            params.put(name, value);\n        }\n\n        @Override\n        public String toString() {\n            return \"Response{\" +\n                    \"ok=\" + ok +\n                    \", ts=\" + ts +\n                    \", error='\" + error + '\\'' +\n                    '}';\n        }\n    }\n\n    private static class DefaultTrustManager implements X509TrustManager {\n\n        @Override\n        public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { // NOSONAR\n        }\n\n        @Override\n        public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { // NOSONAR\n        }\n\n        @Override\n        public X509Certificate[] getAcceptedIssuers() {\n            return new X509Certificate[0];\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/slack/src/main/java/com/walmartlabs/concord/plugins/slack/SlackConfiguration.java",
    "content": "package com.walmartlabs.concord.plugins.slack;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class SlackConfiguration {\n\n    public static SlackConfiguration from(SlackConfigurationParams in) {\n        String apiToken = in.apiToken();\n        boolean isLegacy = in.isLegacy();\n\n        SlackConfiguration cfg = new SlackConfiguration(apiToken, isLegacy);\n        cfg.setProxy(in.proxyAddress(), in.proxyPort(-1));\n        cfg.setConnectTimeout(in.connectTimeout(DEFAULT_CONNECT_TIMEOUT));\n        cfg.setSoTimeout(in.soTimeout(DEFAULT_SO_TIMEOUT));\n        cfg.setRetryCount(in.retryCount(DEFAULT_RETRY_COUNT));\n        return cfg;\n    }\n\n    private static final int DEFAULT_CONNECT_TIMEOUT = 30_000;\n    private static final int DEFAULT_SO_TIMEOUT = 30_000;\n    private static final int DEFAULT_RETRY_COUNT = 3;\n\n    private final String authToken;\n    private final boolean isLegacy;\n    private String proxyAddress;\n    private Integer proxyPort;\n    private int connectTimeout = DEFAULT_CONNECT_TIMEOUT;\n    private int soTimeout = DEFAULT_SO_TIMEOUT;\n    private int retryCount = DEFAULT_RETRY_COUNT;\n\n    public SlackConfiguration(String authToken) {\n        this(authToken, false);\n    }\n\n    public SlackConfiguration(String authToken, boolean isLegacy) {\n        this.authToken = authToken;\n        this.isLegacy = isLegacy;\n    }\n\n    private void setProxy(String proxyAddress, Integer proxyPort) {\n        this.proxyAddress = proxyAddress;\n        this.proxyPort = proxyPort;\n    }\n\n    private void setConnectTimeout(int connectTimeout) {\n        this.connectTimeout = connectTimeout;\n    }\n\n    private void setSoTimeout(int soTimeout) {\n        this.soTimeout = soTimeout;\n    }\n\n    private void setRetryCount(int retryCount) {\n        this.retryCount = retryCount;\n    }\n\n    public String getAuthToken() {\n        return authToken;\n    }\n\n    public boolean isLegacy() {\n        return isLegacy;\n    }\n\n    public String getProxyAddress() {\n        return proxyAddress;\n    }\n\n    public Integer getProxyPort() {\n        return proxyPort;\n    }\n\n    public int getConnectTimeout() {\n        return connectTimeout;\n    }\n\n    public int getSoTimeout() {\n        return soTimeout;\n    }\n\n    public int getRetryCount() {\n        return retryCount;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/slack/src/main/java/com/walmartlabs/concord/plugins/slack/SlackConfigurationParams.java",
    "content": "package com.walmartlabs.concord.plugins.slack;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport java.util.Map;\n\npublic class SlackConfigurationParams {\n\n    public static SlackConfigurationParams of(Variables input, Map<String, Object> defaults) {\n        return new SlackConfigurationParams(Utils.merge(input, defaults));\n    }\n\n    private static final String API_TOKEN = \"apiToken\";\n    private static final String IS_LEGACY = \"isLegacy\";\n    private static final String AUTH_TOKEN = \"authToken\";\n    private static final String CONNECT_TIMEOUT = \"connectTimeout\";\n    private static final String RETRY_COUNT = \"retryCount\";\n    private static final String SO_TIMEOUT = \"soTimeout\";\n    private static final String PROXY_ADDRESS = \"proxyAddress\";\n    private static final String PROXY_PORT = \"proxyPort\";\n\n    protected final Variables variables;\n\n    public SlackConfigurationParams(Variables variables) {\n        this.variables = variables;\n    }\n\n    public String apiToken() {\n        String apiToken = variables.getString(API_TOKEN);\n        if (apiToken == null) {\n            // fallback to the old \"authToken\" parameter\n            apiToken = variables.getString(AUTH_TOKEN);\n        }\n\n        if (apiToken == null) {\n            throw new IllegalStateException(\"'\" + API_TOKEN + \"' or '\" + AUTH_TOKEN + \"' is required.\");\n        }\n\n        return apiToken;\n    }\n\n    public boolean isLegacy() {\n        return variables.getBoolean(IS_LEGACY, false);\n    }\n\n    public int connectTimeout(int defaultValue) {\n        return variables.getInt(CONNECT_TIMEOUT, defaultValue);\n    }\n\n    public int soTimeout(int defaultValue) {\n        return variables.getInt(SO_TIMEOUT, defaultValue);\n    }\n\n    public int retryCount(int defaultValue) {\n        return variables.getInt(RETRY_COUNT, defaultValue);\n    }\n\n    public String proxyAddress() {\n        return variables.getString(PROXY_ADDRESS);\n    }\n\n    public int proxyPort(int defaultValue) {\n        return variables.getInt(PROXY_PORT, defaultValue);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/slack/src/main/java/com/walmartlabs/concord/plugins/slack/SlackTask.java",
    "content": "package com.walmartlabs.concord.plugins.slack;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.*;\n\nimport javax.inject.Named;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Map;\n\n@Named(\"slack\")\n@SuppressWarnings(\"unused\")\npublic class SlackTask implements Task {\n\n    private final SlackTaskCommon delegate = new SlackTaskCommon();\n\n    @Override\n    public void execute(Context ctx) {\n        Map<String, Object> result = delegate.execute(SlackTaskParams.of(new ContextVariables(ctx), defaults(ctx)));\n        ctx.setVariable(\"result\", result);\n    }\n\n    public void call(@InjectVariable(\"context\") Context ctx, String channelId, String text) {\n        call(ctx, channelId, null, false, text, null, null, null, false);\n    }\n\n    public void call(@InjectVariable(\"context\") Context ctx,\n                     String channelId, String text,\n                     String iconEmoji, String username, Collection<Object> attachments) {\n\n        call(ctx, channelId, null, false, text, iconEmoji, username, attachments, false);\n    }\n\n    public void call(@InjectVariable(\"context\") Context ctx,\n                     String channelId, String ts, String text,\n                     String iconEmoji, String username, Collection<Object> attachments,\n                     boolean ignoreErrors) {\n\n        SlackConfiguration slackCfg = SlackConfiguration.from(SlackConfigurationParams.of(new ContextVariables(ctx), defaults(ctx)));\n        delegate.sendMessage(slackCfg, channelId, ts, false, text, iconEmoji, username, attachments, null, ignoreErrors);\n    }\n\n    public void call(@InjectVariable(\"context\") Context ctx,\n                     String channelId, String ts, boolean replyBroadcast, String text,\n                     String iconEmoji, String username, Collection<Object> attachments,\n                     boolean ignoreErrors) {\n\n        SlackConfiguration slackCfg = SlackConfiguration.from(SlackConfigurationParams.of(new ContextVariables(ctx), defaults(ctx)));\n        delegate.sendMessage(slackCfg, channelId, ts, replyBroadcast, text, iconEmoji, username, attachments, null, ignoreErrors);\n    }\n\n    public void sendJsonMessage(@InjectVariable(\"context\") Context ctx, SlackConfiguration slackCfg, String json, boolean ignoreErrors) {\n        sendJsonMessage(ctx, slackCfg, json, ignoreErrors, false);\n    }\n\n    public void sendJsonMessage(@InjectVariable(\"context\") Context ctx, SlackConfiguration slackCfg, String json, boolean ignoreErrors, boolean update) {\n        Map<String, Object> result = delegate.sendJsonMessage(slackCfg, json, ignoreErrors, update);\n        ctx.setVariable(\"result\", result);\n    }\n\n    public void sendMessage(@InjectVariable(\"context\") Context ctx,\n                            SlackConfiguration slackCfg,\n                            String channelId,\n                            String ts,\n                            boolean replyBroadcast,\n                            String text,\n                            String iconEmoji,\n                            String username,\n                            Collection<Object> attachments,\n                            boolean ignoreErrors) {\n\n        Map<String, Object> result = delegate.sendMessage(slackCfg, channelId, ts, replyBroadcast, text, iconEmoji, username, attachments, null, ignoreErrors);\n        ctx.setVariable(\"result\", result);\n    }\n\n    private static Map<String, Object> defaults(Context ctx) {\n        return ContextUtils.getMap(ctx, \"slackCfg\", Collections.emptyMap());\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/slack/src/main/java/com/walmartlabs/concord/plugins/slack/SlackTaskCommon.java",
    "content": "package com.walmartlabs.concord.plugins.slack;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.plugins.slack.SlackTaskParams.*;\n\npublic class SlackTaskCommon {\n\n    private static final Logger log = LoggerFactory.getLogger(SlackTask.class);\n\n    public Map<String, Object> execute(SlackTaskParams in) {\n        Action action = in.action();\n\n        switch (action) {\n            case ADDREACTION: {\n                return addReaction((AddReactionParams)in);\n            }\n            case SENDMESSAGE: {\n                return sendMessage((SendMessageParams)in);\n            }\n            case UPDATEMESSAGE: {\n                return sendMessage((SendMessageParams)in, true);\n            }\n            default: {\n                throw new IllegalArgumentException(\"Unsupported action type: \" + action);\n            }\n        }\n    }\n\n    private Map<String, Object> sendMessage(SendMessageParams in) {\n        return sendMessage(in, false);\n    }\n\n    private Map<String, Object> sendMessage(SendMessageParams in, boolean update) {\n        SlackConfiguration slackCfg = SlackConfiguration.from(in.cfg());\n\n        String json = in.json();\n        if (json != null) {\n            return sendJsonMessage(slackCfg, json, in.ignoreErrors(), update);\n        } else {\n            return sendMessage(slackCfg, in.channelId(), in.ts(), in.replyBroadcast(), in.text(), in.iconEmoji(), in.username(), in.attachments(), in.blocks(), in.ignoreErrors());\n        }\n    }\n\n    public Map<String, Object> sendJsonMessage(SlackConfiguration slackCfg, String json, boolean ignoreErrors, boolean update) {\n        try (SlackClient client = new SlackClient(slackCfg)) {\n            SlackClient.Response r;\n            if (update) {\n                r = client.updateJsonMessage(json);\n            } else {\n                r = client.postJsonMessage(json);\n            }\n            if (!r.isOk()) {\n                log.warn(\"Error sending a Slack message: {}\", r.getError());\n            }\n            return result(r);\n        } catch (Exception e) {\n            if (!ignoreErrors) {\n                log.error(\"call ['{}', '{}'] -> error\", json, e);\n                throw new RuntimeException(\"slack task error: \", e);\n            }\n            log.warn(\"call ['{}', '{}'] -> error (ignoreErrors=true)\", json, e);\n            return errorResult(e);\n        }\n    }\n\n    public Map<String, Object> sendMessage(SlackConfiguration slackCfg,\n                            String channelId,\n                            String ts,\n                            boolean replyBroadcast,\n                            String text,\n                            String iconEmoji,\n                            String username,\n                            Collection<Object> attachments,\n                            Collection<Object> blocks,\n                            boolean ignoreErrors) {\n\n        try {\n            SlackClient.Response r = Slack.sendMessage(slackCfg, channelId, ts, replyBroadcast, text, iconEmoji, username, attachments, blocks);\n            return result(r);\n        } catch (Exception e) {\n            if (!ignoreErrors) {\n                log.error(\"call ['{}', '{}', '{}', '{}', '{}', '{}', '{}'] -> error\", channelId, ts, replyBroadcast, text, iconEmoji, username, attachments, e);\n                throw new RuntimeException(\"slack task error: \", e);\n            }\n\n            log.warn(\"call ['{}', '{}', '{}', '{}', '{}', '{}', '{}'] -> error (ignoreErrors=true)\", channelId, ts, replyBroadcast, text, iconEmoji, username, attachments, e);\n            return errorResult(e);\n        }\n    }\n\n    private Map<String, Object> addReaction(AddReactionParams in) {\n        SlackConfiguration slackCfg = SlackConfiguration.from(in.cfg());\n\n        String channelId = in.channelId();\n        String ts = in.ts();\n        String reaction = in.reaction();\n\n        try (SlackClient client = new SlackClient(slackCfg)) {\n            SlackClient.Response r = client.addReaction(channelId, ts, reaction);\n\n            if (!r.isOk()) {\n                log.warn(\"Error adding reaction to Slack message: {}\", r.getError());\n            } else {\n                log.info(\"Reaction '{}' added to the Slack message '{}'\", reaction, ts);\n            }\n\n            return result(r);\n        } catch (Exception e) {\n            if (!in.ignoreErrors()) {\n                log.error(\"callAddReaction ['{}', '{}', '{}'] -> error\", channelId, ts, reaction, e);\n                throw new RuntimeException(\"slack task error: \", e);\n            }\n\n            log.warn(\"callAddReaction ['{}', '{}', '{}', '{}'] -> error (ignoreErrors=true)\", channelId, ts, reaction, e);\n            return errorResult(e);\n        }\n    }\n\n    private static Map<String, Object> result(SlackClient.Response r) {\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"ok\", r.isOk());\n        m.put(\"error\", r.getError());\n        m.put(\"id\", Utils.extractString(r, \"channel\"));\n        m.put(\"ts\", r.getTs());\n        return m;\n    }\n\n    private static Map<String, Object> errorResult(Throwable t) {\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"ok\", false);\n        m.put(\"error\", t.getMessage());\n        return m;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/slack/src/main/java/com/walmartlabs/concord/plugins/slack/SlackTaskParams.java",
    "content": "package com.walmartlabs.concord.plugins.slack;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Map;\n\npublic class SlackTaskParams {\n\n    public static SlackTaskParams of(Variables input, Map<String, Object> defaults) {\n        Variables variables = Utils.merge(input, defaults);\n\n        SlackTaskParams p = new SlackTaskParams(variables);\n        switch (p.action()) {\n            case SENDMESSAGE:\n            case UPDATEMESSAGE: {\n                return new SendMessageParams(variables);\n            }\n            case ADDREACTION: {\n                return new AddReactionParams(variables);\n            }\n            default: {\n                throw new IllegalArgumentException(\"Unsupported action type: \" + p.action());\n            }\n        }\n    }\n\n    private static final String ACTION = \"action\";\n    private static final String IGNORE_ERRORS = \"ignoreErrors\";\n\n    protected final Variables variables;\n    private final SlackConfigurationParams cfg;\n\n    public SlackTaskParams(Variables variables) {\n        this.variables = variables;\n        this.cfg = new SlackConfigurationParams(variables);\n    }\n\n    public Action action() {\n        String action = variables.getString(ACTION, Action.SENDMESSAGE.name());\n        try {\n            return Action.valueOf(action.trim().toUpperCase());\n        } catch (IllegalArgumentException e) {\n            throw new RuntimeException(\"Unknown action: '\" + action + \"'. Available actions: \" + Arrays.toString(Action.values()));\n        }\n    }\n\n    public SlackConfigurationParams cfg() {\n        return cfg;\n    }\n\n    public static class AddReactionParams extends SlackTaskParams {\n\n        private static final String REACTION = \"reaction\";\n        private static final String CHANNEL_ID = \"channelId\";\n        private static final String TS = \"ts\";\n\n        public AddReactionParams(Variables variables) {\n            super(variables);\n        }\n\n        public String reaction() {\n            return variables.assertString(REACTION);\n        }\n\n        public String channelId() {\n            return variables.assertString(CHANNEL_ID);\n        }\n\n        public String ts() {\n            return variables.assertString(TS);\n        }\n    }\n\n    public static class SendMessageParams extends SlackTaskParams {\n\n        private static final String ATTACHMENTS = \"attachments\";\n        private static final String JSON = \"json\";\n        private static final String ICON_EMOJI = \"iconEmoji\";\n        private static final String REPLY_BROADCAST = \"replyBroadcast\";\n        private static final String USERNAME = \"username\";\n        private static final String TEXT = \"text\";\n        private static final String BLOCKS = \"blocks\";\n        private static final String CHANNEL_ID = \"channelId\";\n        private static final String TS = \"ts\";\n\n        public SendMessageParams(Variables variables) {\n            super(variables);\n        }\n\n        public String json() {\n            return variables.getString(JSON);\n        }\n\n        public boolean replyBroadcast() {\n            return variables.getBoolean(REPLY_BROADCAST, false);\n        }\n\n        public String iconEmoji() {\n            return variables.getString(ICON_EMOJI);\n        }\n\n        public String username() {\n            return variables.getString(USERNAME);\n        }\n\n        public Collection<Object> attachments() {\n            return variables.getCollection(ATTACHMENTS, Collections.emptyList());\n        }\n\n        public String text() {\n            return variables.getString(TEXT);\n        }\n\n        public Collection<Object> blocks() {\n            return variables.getCollection(BLOCKS, Collections.emptyList());\n        }\n\n        public String channelId() {\n            return variables.assertString(CHANNEL_ID);\n        }\n\n        public String ts() {\n            return variables.getString(TS);\n        }\n    }\n\n    public boolean ignoreErrors() {\n        return variables.getBoolean(IGNORE_ERRORS, false);\n    }\n\n    public enum Action {\n        SENDMESSAGE,\n        ADDREACTION,\n        UPDATEMESSAGE\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/slack/src/main/java/com/walmartlabs/concord/plugins/slack/Utils.java",
    "content": "package com.walmartlabs.concord.plugins.slack;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic final class Utils {\n\n    @SuppressWarnings(\"unchecked\")\n    public static String extractString(SlackClient.Response r, String... path) {\n        Map<String, Object> m = r.getParams();\n        if (m == null) {\n            return null;\n        }\n\n        int idx = 0;\n        while (true) {\n            String s = path[idx];\n\n            Object v = m.get(s);\n            if (v == null) {\n                return null;\n            }\n\n            if (idx + 1 >= path.length) {\n                if (v instanceof String) {\n                    return (String) v;\n                } else {\n                    throw new IllegalStateException(\"Expected a string value @ \" + Arrays.toString(path) + \", got: \" + v);\n                }\n            }\n\n            if (!(v instanceof Map)) {\n                throw new IllegalStateException(\"Expected a JSON object, got: \" + v);\n            }\n            m = (Map<String, Object>) v;\n\n            idx += 1;\n        }\n    }\n\n    public static Variables merge(Variables variables, Map<String, Object> defaults) {\n        Map<String, Object> variablesMap = new HashMap<>(defaults != null ? defaults : Collections.emptyMap());\n        variablesMap.putAll(variables.toMap());\n        return new MapBackedVariables(variablesMap);\n    }\n\n    private Utils() {\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/slack/src/main/java/com/walmartlabs/concord/plugins/slack/v2/SlackChannelTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.slack.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.plugins.slack.SlackChannelTaskCommon;\nimport com.walmartlabs.concord.plugins.slack.SlackChannelTaskParams;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.util.Map;\n\n@Named(\"slackChannel\")\n@SuppressWarnings(\"unused\")\npublic class SlackChannelTaskV2 implements Task {\n\n    private final Context context;\n\n    private final SlackChannelTaskCommon delegate = new SlackChannelTaskCommon();\n\n    @Inject\n    public SlackChannelTaskV2(Context context) {\n        this.context = context;\n    }\n\n    @Override\n    public TaskResult execute(Variables in) throws Exception {\n        Map<String, Object> result = delegate.execute(SlackChannelTaskParams.of(in, context.defaultVariables().toMap()));\n        return TaskResult.success()\n                .values(result);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/slack/src/main/java/com/walmartlabs/concord/plugins/slack/v2/SlackTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.slack.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.plugins.slack.SlackConfiguration;\nimport com.walmartlabs.concord.plugins.slack.SlackConfigurationParams;\nimport com.walmartlabs.concord.plugins.slack.SlackTaskCommon;\nimport com.walmartlabs.concord.plugins.slack.SlackTaskParams;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\nimport com.walmartlabs.concord.sdk.InjectVariable;\nimport com.walmartlabs.concord.sdk.MapUtils;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.util.Collections;\nimport java.util.Map;\n\n@Named(\"slack\")\n@SuppressWarnings(\"unused\")\npublic class SlackTaskV2 implements Task {\n\n    private final SlackTaskCommon delegate = new SlackTaskCommon();\n    private final Context context;\n\n    @Inject\n    public SlackTaskV2(Context context) {\n        this.context = context;\n    }\n\n    @Override\n    public TaskResult execute(Variables input) {\n        Map<String, Object> result = delegate.execute(SlackTaskParams.of(input, context.defaultVariables().toMap()));\n        return toResult(result);\n    }\n\n    public TaskResult send(String channelId, String text) {\n        Map<String, Object> result = delegate.sendMessage(cfg(), channelId, null, false, text, null, null, null, null, false);\n        return toResult(result);\n    }\n\n    public TaskResult sendJson(@InjectVariable(\"context\") com.walmartlabs.concord.sdk.Context ctx, SlackConfiguration slackCfg, String json, boolean ignoreErrors) {\n        Map<String, Object> result = delegate.sendJsonMessage(cfg(), json, ignoreErrors, false);\n        return toResult(result);\n    }\n\n    private SlackConfiguration cfg() {\n        return SlackConfiguration.from(SlackConfigurationParams.of(new MapBackedVariables(Collections.emptyMap()), context.defaultVariables().toMap()));\n    }\n\n    private static TaskResult toResult(Map<String, Object> result) {\n        return TaskResult.of(MapUtils.getBoolean(result, \"ok\", false), MapUtils.getString(result, \"error\"))\n                .values(result);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/slack/src/test/java/com/walmartlabs/concord/plugins/slack/SlackClientTest.java",
    "content": "package com.walmartlabs.concord.plugins.slack;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n *\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.junit.jupiter.api.Assumptions.assumeTrue;\n\n@Disabled\npublic class SlackClientTest {\n\n    @BeforeEach\n    public void setUp() {\n        assumeTrue(TestParams.TEST_API_TOKEN != null);\n    }\n\n    @Test\n    public void validateSlackClientPostingMessageWithJson() throws Exception {\n\n        Map<String, Object> cfgMap = new HashMap<>();\n        cfgMap.put(\"apiToken\", TestParams.TEST_API_TOKEN);\n        SlackClient client = new SlackClient(SlackConfiguration.from(new SlackConfigurationParams(new MapBackedVariables(cfgMap))));\n\n        String postJson = \"{\\n\" +\n                \"  \\\"channel\\\": \\\"@SLACK_CHANNEL@\\\",\\n\" +\n                \"  \\\"attachments\\\": [\\n\" +\n                \"    {\\n\" +\n                \"      \\\"mrkdwn_in\\\": [\\n\" +\n                \"        \\\"text\\\"\\n\" +\n                \"      ],\\n\" +\n                \"      \\\"color\\\": \\\"#36a64f\\\",\\n\" +\n                \"      \\\"author_name\\\": \\\"Jason van Zyl\\\",\\n\" +\n                \"      \\\"author_link\\\": \\\"https://github.com/jvanzyl\\\",\\n\" +\n                \"      \\\"author_icon\\\": \\\"https://github.com/jvanzyl.png\\\",\\n\" +\n                \"      \\\"title\\\": \\\"#1234 Add JSON support for Slack messages\\\",\\n\" +\n                \"      \\\"title_link\\\": \\\"https://github.com/jvanzyl/test/commit/44921371d769d85e7ad7665c36a802c2db47aee1\\\",\\n\" +\n                \"      \\\"text\\\": \\\"Modify all the dodos to make JSON slack messages work.\\\",\\n\" +\n                \"      \\\"fields\\\": [\\n\" +\n                \"        {\\n\" +\n                \"          \\\"title\\\": \\\":hourglass_flowing_sand: *Unit Tests*\\\"\\n\" +\n                \"        },\\n\" +\n                \"        {\\n\" +\n                \"          \\\"title\\\": \\\":hourglass_flowing_sand: *Integration Tests*\\\"\\n\" +\n                \"        }\\n\" +\n                \"      ],\\n\" +\n                \"      \\\"footer\\\": \\\"walmartlabs/concord\\\",\\n\" +\n                \"      \\\"footer_icon\\\": \\\"https://github.com/github.png\\\"\\n\" +\n                \"    }\\n\" +\n                \"  ]\\n\" +\n                \"}\";\n\n        postJson = postJson.replace(\"@SLACK_CHANNEL@\", TestParams.TEST_CHANNEL);\n        SlackClient.Response postResponse = client.postJsonMessage(postJson);\n        assertTrue(postResponse.isOk());\n\n        Thread.sleep(2000);\n        String ts = postResponse.getTs();\n        String updateJson = \"{\\n\" +\n                \"  \\\"channel\\\": \\\"@SLACK_CHANNEL@\\\",\\n\" +\n                \"  \\\"ts\\\": \\\"@TS@\\\",\\n\" +\n                \"  \\\"attachments\\\": [\\n\" +\n                \"    {\\n\" +\n                \"      \\\"mrkdwn_in\\\": [\\n\" +\n                \"        \\\"text\\\"\\n\" +\n                \"      ],\\n\" +\n                \"      \\\"color\\\": \\\"#36a64f\\\",\\n\" +\n                \"      \\\"author_name\\\": \\\"Jason van Zyl\\\",\\n\" +\n                \"      \\\"author_link\\\": \\\"https://github.com/jvanzyl\\\",\\n\" +\n                \"      \\\"author_icon\\\": \\\"https://github.com/jvanzyl.png\\\",\\n\" +\n                \"      \\\"title\\\": \\\"#1234 Add JSON support for Slack messages\\\",\\n\" +\n                \"      \\\"title_link\\\": \\\"https://github.com/jvanzyl/test/commit/44921371d769d85e7ad7665c36a802c2db47aee1\\\",\\n\" +\n                \"      \\\"text\\\": \\\"Modify all the dodos to make JSON slack messages work.\\\",\\n\" +\n                \"      \\\"fields\\\": [\\n\" +\n                \"        {\\n\" +\n                \"          \\\"title\\\": \\\":white_check_mark: *Unit Tests*\\\"\\n\" +\n                \"        },\\n\" +\n                \"        {\\n\" +\n                \"          \\\"title\\\": \\\":white_check_mark: *Integration Tests*\\\"\\n\" +\n                \"        }\\n\" +\n                \"      ],\\n\" +\n                \"      \\\"footer\\\": \\\"walmartlabs/concord\\\",\\n\" +\n                \"      \\\"footer_icon\\\": \\\"https://github.com/github.png\\\"\\n\" +\n                \"    }\\n\" +\n                \"  ]\\n\" +\n                \"}\";\n\n        updateJson = updateJson.replace(\"@TS@\", ts);\n        updateJson = updateJson.replace(\"@SLACK_CHANNEL@\", TestParams.TEST_CHANNEL);\n        System.out.println(updateJson);\n        SlackClient.Response updateResponse = client.updateJsonMessage(updateJson);\n        assertTrue(updateResponse.isOk());\n    }\n\n    @Test\n    public void validateSlackClientPostingMessageWithParameters() throws Exception {\n\n        Map<String, Object> cfgMap = new HashMap<>();\n        cfgMap.put(\"apiToken\", TestParams.TEST_API_TOKEN);\n        SlackClient client = new SlackClient(SlackConfiguration.from(new SlackConfigurationParams(new MapBackedVariables(cfgMap))));\n\n        String channelId = TestParams.TEST_CHANNEL;\n        SlackClient.Response response = client.message(channelId, null, false, \"Hello from Concord!\", null, null, null, null);\n        assertTrue(response.isOk());\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/slack/src/test/java/com/walmartlabs/concord/plugins/slack/SlackTaskTest.java",
    "content": "package com.walmartlabs.concord.plugins.slack;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.MockContext;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n@Disabled\nclass SlackTaskTest {\n\n    @Test\n    void testMessage() {\n        Map<String, Object> m = new HashMap<>(defaultInput());\n\n        m.put(\"channelId\", TestParams.TEST_CHANNEL);\n        m.put(\"text\", \"test\");\n        m.put(\"username\", \"My Bot\");\n        m.put(\"iconEmoji\", \":smile:\");\n\n        MockContext ctx = new MockContext(m);\n        SlackTask t = new SlackTask();\n        t.execute(ctx);\n\n        var result1 = assertInstanceOf(Map.class, ctx.getVariable(\"result\"));\n        assertTrue(assertInstanceOf(Boolean.class, result1.get(\"ok\")));\n\n\n        var ts = assertInstanceOf(String.class, result1.get(\"ts\"));\n        m.put(\"ts\", ts);\n        m.put(\"username\", \"My Other Bot\");\n        m.put(\"iconEmoji\", \":frowning:\");\n        m.put(\"text\", \"replying to message in thread, with broadcast\");\n\n        ctx = new MockContext(m);\n        t.execute(ctx);\n\n        var result2 = assertInstanceOf(Map.class, ctx.getVariable(\"result\"));\n        assertTrue(assertInstanceOf(Boolean.class, result2.get(\"ok\")));\n    }\n\n    @Test\n    void testJsonMessage() {\n        Map<String, Object> m = new HashMap<>();\n\n        Map<String, Object> slackCfg = new HashMap<>();\n        slackCfg.put(\"authToken\", TestParams.TEST_API_TOKEN);\n        slackCfg.put(\"proxyAddress\", TestParams.TEST_PROXY_ADDRESS);\n        slackCfg.put(\"proxyPort\", TestParams.TEST_PROXY_PORT);\n        m.put(\"slackCfg\", slackCfg);\n\n        String json = \"{\\n\" +\n                \"  \\\"channel\\\": \\\"@SLACK_CHANNEL@\\\",\\n\" +\n                \"  \\\"attachments\\\": [\\n\" +\n                \"    {\\n\" +\n                \"      \\\"mrkdwn_in\\\": [\\n\" +\n                \"        \\\"text\\\"\\n\" +\n                \"      ],\\n\" +\n                \"      \\\"color\\\": \\\"#36a64f\\\",\\n\" +\n                \"      \\\"author_name\\\": \\\"Jason van Zyl\\\",\\n\" +\n                \"      \\\"author_link\\\": \\\"https://github.com/jvanzyl\\\",\\n\" +\n                \"      \\\"author_icon\\\": \\\"https://github.com/jvanzyl.png\\\",\\n\" +\n                \"      \\\"title\\\": \\\"#1234 Add JSON support for Slack messages\\\",\\n\" +\n                \"      \\\"title_link\\\": \\\"https://github.com/jvanzyl/test/commit/44921371d769d85e7ad7665c36a802c2db47aee1\\\",\\n\" +\n                \"      \\\"text\\\": \\\"Modify all the dodos to make JSON slack messages work.\\\",\\n\" +\n                \"      \\\"fields\\\": [\\n\" +\n                \"        {\\n\" +\n                \"          \\\"title\\\": \\\":white_check_mark: *Unit Tests*\\\"\\n\" +\n                \"        },\\n\" +\n                \"        {\\n\" +\n                \"          \\\"title\\\": \\\":white_check_mark: *Integration Tests*\\\"\\n\" +\n                \"        }\\n\" +\n                \"      ],\\n\" +\n                \"      \\\"footer\\\": \\\"walmartlabs/concord\\\",\\n\" +\n                \"      \\\"footer_icon\\\": \\\"https://github.com/github.png\\\",\\n\" +\n                \"      \\\"ts\\\": \\\"now\\\"\\n\" +\n                \"    }\\n\" +\n                \"  ]\\n\" +\n                \"}\";\n\n        json = json.replace(\"@SLACK_CHANNEL@\", TestParams.TEST_CHANNEL);\n        m.put(\"json\", json);\n\n        MockContext ctx = new MockContext(m);\n        SlackTask t = new SlackTask();\n        t.execute(ctx);\n\n        var result = assertInstanceOf(Map.class, ctx.getVariable(\"result\"));\n        assertTrue(assertInstanceOf(Boolean.class, result.get(\"ok\")));\n    }\n\n    private Map<String, Object> defaultInput() {\n        Map<String, Object> m = new HashMap<>();\n\n        Map<String, Object> slackCfg = new HashMap<>();\n        slackCfg.put(\"authToken\", TestParams.TEST_API_TOKEN);\n        slackCfg.put(\"isLegacy\", false);\n        slackCfg.put(\"proxyAddress\", TestParams.TEST_PROXY_ADDRESS);\n        slackCfg.put(\"proxyPort\", TestParams.TEST_PROXY_PORT);\n        m.put(\"slackCfg\", slackCfg);\n        m.put(\"isLegacy\", false);\n\n        return m;\n    }\n\n    @Test\n    void testMessageInvalidProxyThrowErrors() {\n        Map<String, Object> m = new HashMap<>();\n\n        Map<String, Object> slackCfg = new HashMap<>();\n        slackCfg.put(\"authToken\", TestParams.TEST_API_TOKEN);\n        slackCfg.put(\"proxyAddress\", TestParams.TEST_INVALID_PROXY_ADDRESS);\n        slackCfg.put(\"proxyPort\", TestParams.TEST_PROXY_PORT);\n        m.put(\"slackCfg\", slackCfg);\n        m.put(\"channelId\", TestParams.TEST_CHANNEL);\n        m.put(\"text\", \"test\");\n\n        MockContext ctx = new MockContext(m);\n        SlackTask t = new SlackTask();\n\n        assertThrows(Exception.class, () -> t.execute(ctx));\n    }\n\n    @Test\n    void testMessageInvalidProxyIgnoreErrors() {\n        Map<String, Object> m = new HashMap<>();\n\n        Map<String, Object> slackCfg = new HashMap<>();\n        slackCfg.put(\"authToken\", TestParams.TEST_API_TOKEN);\n        slackCfg.put(\"proxyAddress\", TestParams.TEST_PROXY_ADDRESS);\n        slackCfg.put(\"proxyPort\", TestParams.TEST_PROXY_PORT);\n        m.put(\"slackCfg\", slackCfg);\n        m.put(\"channelId\", TestParams.TEST_CHANNEL);\n        m.put(\"text\", \"test\");\n        m.put(\"ignoreErrors\", true);\n\n        MockContext ctx = new MockContext(m);\n        SlackTask t = new SlackTask();\n        assertDoesNotThrow(() -> t.execute(ctx));\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/slack/src/test/java/com/walmartlabs/concord/plugins/slack/TestParams.java",
    "content": "package com.walmartlabs.concord.plugins.slack;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.InputStream;\nimport java.util.Optional;\nimport java.util.Properties;\n\npublic final class TestParams {\n\n    static File testPropertiesFile = new File(System.getProperty(\"user.home\"), \".concord/profile\");\n    static Properties testProperties;\n\n    static {\n        testProperties = new Properties();\n        if (testPropertiesFile.exists()) {\n            try (InputStream input = new FileInputStream(testPropertiesFile)) {\n                testProperties.load(input);\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n        }\n    }\n\n    private static String testParameter(String existingName, String profileName) {\n        String value = System.getenv(existingName);\n        return value != null ? value : testProperties.getProperty(profileName);\n    }\n\n    private static String assertTestParameter(String existingName, String profileName) {\n        return Optional.ofNullable(testParameter(existingName, profileName))\n                .orElseThrow(() -> new IllegalArgumentException(\"Missing test parameter: \" + existingName + \" (env var) or \" + profileName + \" (~/.concord/profile)\"));\n    }\n\n    public static final String TEST_API_TOKEN = assertTestParameter(\"SLACK_TEST_API_TOKEN\", \"SLACK_BOT_API_TOKEN\");\n    public static final String TEST_USER_API_TOKEN = testParameter(\"SLACK_TEST_API_TOKEN\", \"SLACK_USER_API_TOKEN\");\n    public static final String TEST_PROXY_ADDRESS = testParameter(\"SLACK_TEST_PROXY_ADDRESS\", \"SLACK_PROXY_ADDRESS\");\n    public static final String TEST_INVALID_PROXY_ADDRESS = testParameter(\"SLACK_TEST_INVALID_PROXY_ADDRESS\", \"SLACK_INVALID_PROXY_ADDRESS\");\n    public static final int TEST_PROXY_PORT = Integer.parseInt(Optional.ofNullable(testParameter(\"SLACK_TEST_PROXY_PORT\", \"SLACK_PROXY_PORT\")).orElse(\"-1\"));\n    public static final String TEST_CHANNEL = assertTestParameter(\"SLACK_TEST_CHANNEL\", \"SLACK_CHANNEL\");\n\n    private TestParams() {\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/sleep/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>sleep-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-client2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <configuration>\n                    <!-- no verify, as this plugin depends on runtime-v2 -->\n                    <skipVerify>true</skipVerify>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/tasks/sleep/src/main/java/com/walmartlabs/concord/plugins/sleep/Constants.java",
    "content": "package com.walmartlabs.concord.plugins.sleep;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class Constants {\n\n    public static final int RETRY_COUNT = 3;\n    public static final long RETRY_INTERVAL = 5000;\n\n    public static final String DATETIME_PATTERN = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\";\n    public static final String DURATION_KEY = \"duration\";\n    public static final String SUSPEND_KEY = \"suspend\";\n    public static final String UNTIL_KEY = \"until\";\n\n    public static final String[] ALL_IN_PARAMS = {\n            DURATION_KEY,\n            SUSPEND_KEY,\n            UNTIL_KEY\n    };\n\n    private Constants() {\n    }\n}"
  },
  {
    "path": "plugins/tasks/sleep/src/main/java/com/walmartlabs/concord/plugins/sleep/SleepTask.java",
    "content": "package com.walmartlabs.concord.plugins.sleep;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.client2.ApiClientConfiguration;\nimport com.walmartlabs.concord.client2.ApiClientFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.ContextUtils;\nimport com.walmartlabs.concord.sdk.Task;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.Supplier;\n\n@Named(\"sleep\")\npublic class SleepTask implements Task {\n\n    private final ApiClientFactory apiClientFactory;\n\n    @Inject\n    public SleepTask(ApiClientFactory apiClientFactory) {\n        this.apiClientFactory = apiClientFactory;\n    }\n\n    @SuppressWarnings(\"unused\")\n    public void ms(long t) {\n        SleepTaskCommon.sleep(t);\n    }\n\n    @Override\n    public void execute(Context ctx) throws Exception {\n        Supplier<Suspender> suspender = () -> {\n            ApiClient apiClient = apiClientFactory.create(ApiClientConfiguration.builder()\n                    .sessionToken(ContextUtils.getSessionToken(ctx))\n                    .build());\n\n            return new Suspender(apiClient, ContextUtils.getTxId(ctx));\n        };\n\n        Map<String, Object> cfg = createCfg(ctx);\n\n        TaskResult taskResult = new SleepTaskCommon(suspender)\n                .execute(new TaskParams(cfg));\n        if (taskResult instanceof TaskResult.SuspendResult) {\n            ctx.suspend(((TaskResult.SuspendResult) taskResult).eventName());\n        }\n    }\n\n    private static Map<String, Object> createCfg(Context ctx) {\n        Map<String, Object> m = new HashMap<>();\n        for (String k : Constants.ALL_IN_PARAMS) {\n            Object v = ctx.getVariable(k);\n            if (v != null) {\n                m.put(k, v);\n            }\n        }\n        return m;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/sleep/src/main/java/com/walmartlabs/concord/plugins/sleep/SleepTaskCommon.java",
    "content": "package com.walmartlabs.concord.plugins.sleep;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.function.Supplier;\n\npublic class SleepTaskCommon {\n\n    private static final Logger log = LoggerFactory.getLogger(SleepTaskCommon.class);\n\n    public static void sleep(long t) {\n        try {\n            Thread.sleep(t);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    private final Supplier<Suspender> suspender;\n\n    public SleepTaskCommon(Supplier<Suspender> suspender) {\n        this.suspender = suspender;\n    }\n\n    public TaskResult execute(TaskParams input) throws Exception {\n        Number duration = input.duration();\n        Instant until = input.until();\n\n        validateInputParams(duration, until);\n\n        if (input.suspend()) {\n            Instant sleepUntil = toSleepUntil(duration, until);\n            if (sleepUntil.isBefore(Instant.now())) {\n                log.warn(\"Skipping the sleep, the specified datetime is in the \" +\n                        \"past: {}\", sleepUntil);\n                return TaskResult.success();\n            }\n            log.info(\"Sleeping until {}...\", sleepUntil);\n            return suspender.get().suspend(sleepUntil);\n        } else {\n            long sleepTime = toSleepDuration(duration, until);\n            if (sleepTime <= 0) {\n                log.warn(\"Skipping the sleep, the specified datetime is either negative \" +\n                        \"or is in the past: {}\", sleepTime);\n                return TaskResult.success();\n            }\n            log.info(\"Sleeping for {}ms\", sleepTime);\n            sleep(sleepTime);\n            return TaskResult.success();\n        }\n    }\n\n    private static long toSleepDuration(Number duration, Instant until) {\n        if (duration != null) {\n            return duration.longValue() * 1000;\n        }\n\n        return Duration.between(Instant.now(), until).toMillis();\n    }\n\n    private static Instant toSleepUntil(Number duration, Instant until) {\n        if (until != null) {\n            return until;\n        }\n\n        return Instant.now().plusSeconds(duration.longValue());\n    }\n\n    public static void validateInputParams(Number duration, Instant until) {\n        if (duration == null && until == null) {\n            log.error(\"Invalid sleep task input parameters: 'duration' or 'until' must be specified\");\n            throw new IllegalArgumentException(\"Invalid arguments\");\n        }\n\n        if (duration != null && until != null) {\n            log.error(\"Invalid sleep task input parameters: 'duration' and 'until' are mutually exclusive\");\n            throw new IllegalArgumentException(\"Invalid arguments\");\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/sleep/src/main/java/com/walmartlabs/concord/plugins/sleep/Suspender.java",
    "content": "package com.walmartlabs.concord.plugins.sleep;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.client2.ApiException;\nimport com.walmartlabs.concord.client2.ClientUtils;\nimport com.walmartlabs.concord.client2.ProcessApi;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\n\nimport java.time.Instant;\nimport java.time.ZoneOffset;\nimport java.time.format.DateTimeFormatter;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic class Suspender {\n\n    private static final DateTimeFormatter dateFormatter = DateTimeFormatter.\n            ofPattern(Constants.DATETIME_PATTERN).withZone(ZoneOffset.UTC);\n\n    private final ProcessApi api;\n    private final UUID instanceId;\n\n    public Suspender(ApiClient api, UUID instanceId) {\n        this.api = new ProcessApi(api);\n        this.instanceId = instanceId;\n    }\n\n    public TaskResult suspend(Instant until) throws ApiException {\n        String eventName = UUID.randomUUID().toString();\n\n        ClientUtils.withRetry(Constants.RETRY_COUNT, Constants.RETRY_INTERVAL, () -> {\n            api.setWaitCondition(instanceId, createCondition(until, eventName));\n            return null;\n        });\n\n        return TaskResult.suspend(eventName);\n    }\n\n    private static Map<String, Object> createCondition(Instant until, String eventName) {\n        Map<String, Object> condition = new HashMap<>();\n        condition.put(\"type\", \"PROCESS_SLEEP\");\n        condition.put(\"until\", dateFormatter.format(until));\n        condition.put(\"reason\", \"Waiting till \" + until);\n        condition.put(\"resumeEvent\", eventName);\n        return condition;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/sleep/src/main/java/com/walmartlabs/concord/plugins/sleep/TaskParams.java",
    "content": "package com.walmartlabs.concord.plugins.sleep;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport java.time.Instant;\nimport java.time.ZonedDateTime;\nimport java.time.format.DateTimeFormatter;\nimport java.time.format.DateTimeParseException;\nimport java.util.Date;\nimport java.util.Map;\n\npublic class TaskParams {\n\n    private final Variables input;\n\n    public TaskParams(Map<String, Object> input) {\n        this(new MapBackedVariables(input));\n    }\n\n    public TaskParams(Variables input) {\n        this.input = input;\n    }\n\n    public Number duration() {\n        return input.getNumber(Constants.DURATION_KEY, null);\n    }\n\n    public Instant until() {\n        Object value = input.get(Constants.UNTIL_KEY);\n        if (value == null) {\n            return null;\n        }\n\n        if (value instanceof Date) {\n            return ((Date) value).toInstant();\n        }\n\n        if (value instanceof String) {\n            try {\n                return ZonedDateTime.parse((String) value, DateTimeFormatter.ISO_OFFSET_DATE_TIME).toInstant();\n            } catch (DateTimeParseException e) {\n                throw new IllegalArgumentException(\"Invalid datetime string. Expected \" + Constants.DATETIME_PATTERN +\n                        \" (e.g. \" + DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(ZonedDateTime.now()) + \") got: \" + value);\n            }\n        }\n\n        throw new IllegalArgumentException(\"Invalid variable '\" + value + \"' type, expected: date/string, got: \" + value.getClass());\n    }\n\n    public boolean suspend() {\n        return input.getBoolean(Constants.SUSPEND_KEY, false);\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/sleep/src/main/java/com/walmartlabs/concord/plugins/sleep/v2/SleepTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.sleep.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.plugins.sleep.SleepTaskCommon;\nimport com.walmartlabs.concord.plugins.sleep.Suspender;\nimport com.walmartlabs.concord.plugins.sleep.TaskParams;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\n\n@Named(\"sleep\")\n@DryRunReady\npublic class SleepTaskV2 implements Task {\n\n    private final SleepTaskCommon delegate;\n\n    @Inject\n    public SleepTaskV2(ApiClient apiClient, Context context) {\n        this.delegate = new SleepTaskCommon(() -> new Suspender(apiClient, context.processInstanceId()));\n    }\n\n    @SuppressWarnings(\"unused\")\n    public void ms(long t) {\n        SleepTaskCommon.sleep(t);\n    }\n\n    @Override\n    public TaskResult execute(Variables input) throws Exception {\n        return delegate.execute(new TaskParams(input));\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/smtp/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>smtp-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-email</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.sun.mail</groupId>\n            <artifactId>javax.mail</artifactId>\n            <exclusions>\n                <exclusion>\n                    <groupId>javax.activation</groupId>\n                    <artifactId>activation</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n        <dependency>\n            <groupId>com.sun.activation</groupId>\n            <artifactId>javax.activation</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>com.github.spullara.mustache.java</groupId>\n            <artifactId>compiler</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.icegreen</groupId>\n            <artifactId>greenmail</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.icegreen</groupId>\n            <artifactId>greenmail-junit5</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-core</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>ch.qos.logback</groupId>\n            <artifactId>logback-classic</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <dependencies>\n                    <dependency>\n                        <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n                        <artifactId>concord-runner-v2</artifactId>\n                        <version>${project.version}</version>\n                    </dependency>\n                </dependencies>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-shade-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>shade</goal>\n                        </goals>\n                        <configuration>\n                            <minimizeJar>false</minimizeJar>\n                            <createDependencyReducedPom>false</createDependencyReducedPom>\n                        </configuration>\n                    </execution>\n                </executions>\n                <configuration>\n                    <transformers>\n                        <transformer implementation=\"org.apache.maven.plugins.shade.resource.AppendingTransformer\">\n                            <resource>META-INF/sisu/javax.inject.Named</resource>\n                        </transformer>\n                    </transformers>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/tasks/smtp/src/main/java/com/walmartlabs/concord/plugins/smtp/Constants.java",
    "content": "package com.walmartlabs.concord.plugins.smtp;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class Constants {\n\n    public static final String HOST_KEY = \"host\";\n    public static final String PORT_KEY = \"port\";\n    public static final String FROM_KEY = \"from\";\n    public static final String TO_KEY = \"to\";\n    public static final String CC_KEY = \"cc\";\n    public static final String BCC_KEY = \"bcc\";\n    public static final String REPLYTO_KEY = \"replyTo\";\n    public static final String SUBJECT_KEY = \"subject\";\n    public static final String DEBUG_KEY = \"debug\";\n    public static final String SMTP_PARAMS_KEY = \"smtpParams\";\n    public static final String SMTP_KEY = \"smtp\";\n    public static final String MAIL_PARAMS_KEY = \"mailParams\";\n    public static final String MAIL_KEY = \"mail\";\n    public static final String MESSAGE_KEY = \"message\";\n    public static final String TEMPLATE_KEY = \"template\";\n    public static final String HTML_KEY = \".html\";\n    public static final String PARAMS_KEY = \"params\";\n    public static final String NAME_KEY = \"name\";\n    public static final String ATTACHMENTS_KEY = \"attachments\";\n    public static final String PATH_KEY = \"path\";\n    public static final String DESCRIPTION_KEY = \"description\";\n    public static final String DISPOSITION_KEY = \"disposition\";\n\n    private Constants() {\n    }\n}"
  },
  {
    "path": "plugins/tasks/smtp/src/main/java/com/walmartlabs/concord/plugins/smtp/SmtpTask.java",
    "content": "package com.walmartlabs.concord.plugins.smtp;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.ContextUtils;\nimport com.walmartlabs.concord.sdk.InjectVariable;\nimport com.walmartlabs.concord.sdk.Task;\n\nimport javax.inject.Named;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\n@Named(\"smtp\")\npublic class SmtpTask implements Task {\n\n    @Override\n    public void execute(Context ctx) throws Exception {\n        Map<String, Object> smtp = getCfg(ctx, Constants.SMTP_PARAMS_KEY, Constants.SMTP_KEY);\n        Map<String, Object> mail = getCfg(ctx, Constants.MAIL_PARAMS_KEY, Constants.MAIL_KEY);\n        send(ctx, smtp, mail);\n    }\n\n    @Deprecated\n    public void call(@InjectVariable(\"context\") Context ctx, Map<String, Object> smtpParams, Map<String, Object> mailParams) throws Exception {\n        send(ctx, smtpParams, mailParams);\n    }\n\n    @Deprecated\n    public void call(Map<String, Object> smtpParams, Map<String, Object> mailParams) throws Exception {\n        send(null, smtpParams, mailParams);\n    }\n\n    @Deprecated\n    public void send(String hostName, int port, String from, String to, String subject, String message, String bcc) throws Exception {\n        Map<String, Object> smtp = new HashMap<>();\n        smtp.put(Constants.HOST_KEY, hostName);\n        smtp.put(Constants.PORT_KEY, port);\n\n        Map<String, Object> mail = new HashMap<>();\n        mail.put(Constants.FROM_KEY, from);\n        mail.put(Constants.TO_KEY, to);\n        mail.put(Constants.SUBJECT_KEY, subject);\n        mail.put(Constants.MESSAGE_KEY, message);\n        mail.put(Constants.BCC_KEY, bcc);\n\n        send(null, smtp, mail);\n    }\n\n    private static void send(Context ctx, Map<String, Object> smtp, Map<String, Object> mail) throws Exception {\n        Path baseDir = getWorkDir(ctx);\n        Object scope = getScope(ctx, mail);\n        boolean debug = ContextUtils.getBoolean(ctx, Constants.DEBUG_KEY, false);\n\n        SmtpTaskUtils.send(smtp, mail, baseDir, scope, debug, false);\n    }\n\n    private static Path getWorkDir(Context ctx) {\n        if (ctx != null) {\n            return ContextUtils.getWorkDir(ctx);\n        }\n        return Paths.get(System.getProperty(\"user.dir\"));\n    }\n\n    private static Object getScope(Context ctx, Map<String, Object> mailParams) {\n        Map<String, Object> ctxParams = ctx != null ? ctx.toMap() : Collections.emptyMap();\n        return SmtpTaskUtils.getScope(mailParams, ctxParams);\n    }\n\n    private static Map<String, Object> getCfg(Context ctx, String a, String b) {\n        Map<String, Object> m = ContextUtils.getMap(ctx, a);\n        if (m == null) {\n            m = ContextUtils.getMap(ctx, b);\n        }\n        return m;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/smtp/src/main/java/com/walmartlabs/concord/plugins/smtp/SmtpTaskUtils.java",
    "content": "package com.walmartlabs.concord.plugins.smtp;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.github.mustachejava.DefaultMustacheFactory;\nimport com.github.mustachejava.Mustache;\nimport com.github.mustachejava.MustacheFactory;\nimport org.apache.commons.mail.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.FileReader;\nimport java.io.StringWriter;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.sdk.MapUtils.*;\n\npublic class SmtpTaskUtils {\n\n    private static final Logger log = LoggerFactory.getLogger(SmtpTask.class);\n\n    private static String assertPath(Path workDir, String path) {\n        Path result = workDir.resolve(path).normalize().toAbsolutePath();\n        if (!result.startsWith(workDir)) {\n            throw new IllegalArgumentException(\"invalid attachment path: \" + path);\n        }\n\n        if (!Files.exists(result)) {\n            throw new IllegalArgumentException(\"attachment not found: \" + result);\n        }\n\n        return result.toString();\n    }\n\n    private static String parseDisposition(String disposition) {\n        if (disposition == null) {\n            return EmailAttachment.ATTACHMENT;\n        }\n\n        if (EmailAttachment.ATTACHMENT.equals(disposition)) {\n            return EmailAttachment.ATTACHMENT;\n        } else if (EmailAttachment.INLINE.equals(disposition)) {\n            return EmailAttachment.INLINE;\n        }\n\n        throw new IllegalArgumentException(\"invalid 'attachment' disposition value: '\"\n                + disposition + \"', expected: \" + EmailAttachment.ATTACHMENT\n                + \" or \" + EmailAttachment.INLINE);\n    }\n\n    private static void processAttachments(MultiPartEmail email, List<EmailAttachment>\n            attachments) throws EmailException {\n        for (EmailAttachment a : attachments) {\n            email.attach(a);\n        }\n    }\n\n    private static boolean isHtml(Map<String, Object> mailParams) {\n        String template = getString(mailParams, Constants.TEMPLATE_KEY);\n        if (template == null) {\n            return false;\n        }\n        return template.trim().endsWith(Constants.HTML_KEY);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static Map<String, Object> getTemplateParams(Map<String, Object> mailParams) {\n        Object templateParam = mailParams.get(Constants.TEMPLATE_KEY);\n        if (templateParam instanceof Map) {\n            Map<String, Object> p = (Map<String, Object>) templateParam;\n            return getMap(p, Constants.PARAMS_KEY, Collections.emptyMap());\n        }\n        return Collections.emptyMap();\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static String getTemplateName(Map<String, Object> mailParams) {\n        Object templateParam = mailParams.get(Constants.TEMPLATE_KEY);\n        if (templateParam == null) {\n            return null;\n        }\n\n        if (templateParam instanceof String) {\n            return (String) templateParam;\n        } else if (templateParam instanceof Map) {\n            Map<String, Object> p = (Map<String, Object>) templateParam;\n            return getString(p, Constants.NAME_KEY);\n        }\n\n        throw new IllegalArgumentException(\"invalid template param type: \" + templateParam.getClass() + \". Expected String or Map\");\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Collection<String> zeroOrManyStrings(Map<String, Object> m, String k) {\n        Object v = m.get(k);\n\n        if (v instanceof String) {\n            String s = (String) v;\n            return Arrays.stream(s.split(\",\"))\n                    .map(String::trim)\n                    .collect(Collectors.toList());\n        }\n\n        if (v instanceof Collection) {\n            Collection<?> c = (Collection<?>) v;\n            c.forEach(i -> {\n                if (!(i instanceof String)) {\n                    throw new IllegalArgumentException(\"'\" + k + \"' - expected a list of string values, got: \" + v);\n                }\n            });\n            return (Collection<String>) c;\n        }\n\n        return Collections.emptyList();\n    }\n\n    private static Collection<String> oneOrManyStrings(Map<String, Object> m, String k) {\n        Collection<String> c = zeroOrManyStrings(m, k);\n        if (c.isEmpty()) {\n            throw new IllegalArgumentException(\"'\" + k + \"' - expected a single string value or a list of strings\");\n        }\n        return c;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static List<EmailAttachment> parseAttachments(Map<String, Object> mail, Path workDir) {\n        List<Object> attachments = getList(mail, Constants.ATTACHMENTS_KEY, Collections.emptyList());\n        if (attachments.isEmpty()) {\n            return Collections.emptyList();\n        }\n\n        List<EmailAttachment> result = new ArrayList<>();\n        for (Object o : attachments) {\n            EmailAttachment a = new EmailAttachment();\n            if (o == null) {\n                continue;\n            } else if (o instanceof String) {\n                a.setPath(assertPath(workDir, (String) o));\n            } else if (o instanceof Map) {\n                Map<String, Object> params = (Map<String, Object>) o;\n                a.setPath(assertPath(workDir, assertString(params, Constants.PATH_KEY)));\n                a.setName(getString(params, Constants.NAME_KEY));\n                a.setDescription(getString(params, Constants.DESCRIPTION_KEY));\n                a.setDisposition(parseDisposition(getString(params, Constants.DISPOSITION_KEY)));\n            } else {\n                throw new IllegalArgumentException(\"invalid 'attachments' item type - expected a string or map, got: \" + o.getClass());\n            }\n            result.add(a);\n        }\n        return result;\n    }\n\n    private static Email createEmail(Map<String, Object> mail, List<EmailAttachment> attachments) throws EmailException {\n        boolean isHtml = isHtml(mail);\n        String msg = assertString(mail, Constants.MESSAGE_KEY);\n\n        if (isHtml) {\n            HtmlEmail email = new HtmlEmail();\n            email.setHtmlMsg(msg);\n            processAttachments(email, attachments);\n            return email;\n        } else {\n            Email email;\n            if (!attachments.isEmpty()) {\n                MultiPartEmail mpe = new MultiPartEmail();\n                processAttachments(mpe, attachments);\n                email = mpe;\n            } else {\n                email = new SimpleEmail();\n            }\n            email.setMsg(msg);\n            return email;\n        }\n    }\n\n    private static Map<String, Object> applyTemplate(Map<String, Object> mailParams, Path baseDir, Object scope) throws Exception {\n        if (mailParams == null) {\n            return null;\n        }\n\n        String template = getTemplateName(mailParams);\n        if (template == null) {\n            return mailParams;\n        }\n\n        StringWriter out = new StringWriter();\n\n        try (FileReader in = new FileReader(baseDir.resolve(template).toFile())) {\n            MustacheFactory mf = new DefaultMustacheFactory();\n            Mustache mustache = mf.compile(in, template);\n\n            mustache.execute(out, scope);\n        }\n\n        Map<String, Object> m = new HashMap<>(mailParams);\n        m.put(Constants.MESSAGE_KEY, out.toString());\n        return m;\n    }\n\n    public static void send(Map<String, Object> smtp, Map<String, Object> mail,\n                            Path baseDir, Object scope, boolean debug,\n                            boolean dryRunMode) throws Exception {\n\n        if (mail == null) {\n            throw new IllegalArgumentException(\"'mail' param is required\");\n        }\n\n        String host = assertString(smtp, Constants.HOST_KEY);\n        int port = assertInt(smtp, Constants.PORT_KEY);\n\n        mail = applyTemplate(mail, baseDir, scope);\n\n        String from = assertString(mail, Constants.FROM_KEY);\n        Collection<String> to = oneOrManyStrings(mail, Constants.TO_KEY);\n        Collection<String> cc = zeroOrManyStrings(mail, Constants.CC_KEY);\n        Collection<String> bcc = zeroOrManyStrings(mail, Constants.BCC_KEY);\n        Collection<String> replyTo = zeroOrManyStrings(mail, Constants.REPLYTO_KEY);\n        String subject = getString(mail, Constants.SUBJECT_KEY);\n\n        try {\n            List<EmailAttachment> attachments = parseAttachments(mail, baseDir);\n            Email email = createEmail(mail, attachments);\n\n            email.setHostName(host);\n            email.setSmtpPort(port);\n\n            email.setFrom(from);\n\n            for (String s : to) {\n                email.addTo(s);\n            }\n\n            for (String s : cc) {\n                email.addCc(s);\n            }\n\n            for (String s : bcc) {\n                email.addBcc(s);\n            }\n\n            for (String s : replyTo) {\n                email.addReplyTo(s);\n            }\n\n            email.setSubject(subject);\n\n            if (dryRunMode) {\n                log.info(\"Running in dry-run mode: Skipping sending email\");\n                return;\n            }\n\n            String msgId = email.send();\n\n            if (debug) {\n                log.info(\"send [{}, {}] -> done, msgId: {}\", smtp, mail, msgId);\n            } else {\n                log.info(\"send -> done, msgId: {}\", msgId);\n            }\n        } catch (Exception e) {\n            if (debug) {\n                log.error(\"send [{}, {}] -> error\", smtp, mail, e);\n            } else {\n                log.error(\"send -> error\", e);\n            }\n            throw e;\n        }\n    }\n\n    public static Object getScope(Map<String, Object> mailParams, Map<String, Object> ctxParams) {\n        Map<String, Object> templateParams = getTemplateParams(mailParams);\n        Map<String, Object> result = new HashMap<>();\n        result.putAll(ctxParams);\n        result.putAll(templateParams);\n        return result;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/smtp/src/main/java/com/walmartlabs/concord/plugins/smtp/SmtpTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.smtp;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.Map;\n\n@Named(\"smtp\")\npublic class SmtpTaskV2 implements Task {\n\n    private final Context ctx;\n\n    @Inject\n    public SmtpTaskV2(Context ctx) {\n        this.ctx = ctx;\n    }\n\n    @Override\n    public TaskResult execute(Variables input) throws Exception {\n        Map<String, Object> smtp = getCfg(ctx, input, Constants.SMTP_PARAMS_KEY, Constants.SMTP_KEY);\n        Map<String, Object> mail = getCfg(ctx, input, Constants.MAIL_PARAMS_KEY, Constants.MAIL_KEY);\n        Path baseDir = ctx.workingDirectory();\n        Object scope = getScope(ctx, mail);\n        boolean debug = input.getBoolean(Constants.DEBUG_KEY, ctx.processConfiguration().debug());\n\n        SmtpTaskUtils.send(smtp, mail, baseDir, scope, debug, ctx.processConfiguration().dryRun());\n        return TaskResult.success();\n    }\n\n    private static Object getScope(Context ctx, Map<String, Object> mailParams) {\n        Map<String, Object> ctxParams = ctx != null ? ctx.variables().toMap() : Collections.emptyMap();\n        return SmtpTaskUtils.getScope(mailParams, ctxParams);\n    }\n\n    private static Map<String, Object> getCfg(Context ctx, Variables input, String a, String b) {\n        Map<String, Object> m = getMap(ctx, input, a);\n        if (m == null) {\n            m = getMap(ctx, input, b);\n        }\n\n        if (m == null) {\n            return ctx.defaultVariables().toMap();\n        }\n\n        return m;\n    }\n\n    private static Map<String, Object> getMap(Context ctx, Variables input, String s) {\n        Map<String, Object> m = input.getMap(s, null);\n        if (m == null) {\n            m = ctx.variables().getMap(s, null);\n        }\n        return m;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/smtp/src/test/java/com/walmartlabs/concord/plugins/smtp/SmtpTaskTest.java",
    "content": "package com.walmartlabs.concord.plugins.smtp;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.icegreen.greenmail.junit5.GreenMailExtension;\nimport com.icegreen.greenmail.smtp.SmtpServer;\nimport com.icegreen.greenmail.util.ServerSetupTest;\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.MockContext;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport javax.mail.internet.MimeMessage;\nimport javax.mail.internet.MimeMultipart;\nimport java.io.File;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\n@SuppressWarnings(\"deprecation\")\npublic class SmtpTaskTest {\n\n    @RegisterExtension\n    GreenMailExtension mail = new GreenMailExtension(ServerSetupTest.SMTP);\n\n    @Test\n    public void test() throws Exception {\n        SmtpServer server = mail.getSmtp();\n\n        SmtpTask t = new SmtpTask();\n        t.send(\"localhost\", server.getPort(), \"my@mail.com\", \"their@mail.com\", \"test\", \"Hello!\", \"another@mail.com\");\n\n        MimeMessage[] messages = mail.getReceivedMessages();\n        assertEquals(2, messages.length);\n        assertEquals(\"Hello!\\r\\n\", messages[0].getContent());\n\n        mail.reset();\n    }\n\n    @Test\n    public void testVariables() throws Exception {\n        SmtpServer server = mail.getSmtp();\n\n        Map<String, Object> smtpParams = new HashMap<>();\n        smtpParams.put(\"host\", \"localhost\");\n        smtpParams.put(\"port\", server.getPort());\n\n        Map<String, Object> mailParams = new HashMap<>();\n        mailParams.put(\"from\", \"my@mail.com\");\n        mailParams.put(\"to\", \"their@mail.com\");\n        mailParams.put(\"template\", new File(ClassLoader.getSystemResource(\"test.mustache\").toURI()).toString());\n\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"name\", \"Concord\");\n        m.put(\"workDir\", \"/\");\n        m.put(\"smtp\", smtpParams);\n        m.put(\"mail\", mailParams);\n\n        Context ctx = new MockContext(m);\n\n        SmtpTask t = new SmtpTask();\n        t.execute(ctx);\n\n        MimeMessage[] messages = mail.getReceivedMessages();\n        assertEquals(\"Hello, Concord!\\r\\n\", messages[0].getContent());\n\n        mail.reset();\n    }\n\n    @Test\n    public void testNoBcc() throws Exception {\n        SmtpServer server = mail.getSmtp();\n\n        SmtpTask t = new SmtpTask();\n        t.send(\"localhost\", server.getPort(), \"my@mail.com\", \"their@mail.com\", \"test\", \"Hello!\", null);\n\n        MimeMessage[] messages = mail.getReceivedMessages();\n        assertEquals(1, messages.length);\n        assertEquals(\"Hello!\\r\\n\", messages[0].getContent());\n        assertEquals(1, messages[0].getAllRecipients().length);\n\n        mail.reset();\n    }\n\n    @Test\n    public void testNoBccViaCall() throws Exception {\n        SmtpServer server = mail.getSmtp();\n\n        Map<String, Object> smtpParams = new HashMap<>();\n        Map<String, Object> mailParams = new HashMap<>();\n\n        smtpParams.put(\"host\", \"localhost\");\n        smtpParams.put(\"port\", server.getPort());\n\n        mailParams.put(\"from\", \"my@mail.com\");\n        mailParams.put(\"to\", \"their@mail.com\");\n        mailParams.put(\"subject\", \"test\");\n        mailParams.put(\"message\", \"Hello!\");\n\n        SmtpTask t = new SmtpTask();\n        t.call(smtpParams, mailParams);\n\n        MimeMessage[] messages = mail.getReceivedMessages();\n        assertEquals(1, messages.length);\n        assertEquals(\"Hello!\\r\\n\", messages[0].getContent());\n        assertEquals(1, messages[0].getAllRecipients().length);\n\n        mail.reset();\n    }\n\n    @Test\n    public void testMultipleTo() throws Exception {\n        SmtpServer server = mail.getSmtp();\n\n        Map<String, Object> smtpParams = new HashMap<>();\n        smtpParams.put(\"host\", \"localhost\");\n        smtpParams.put(\"port\", server.getPort());\n\n        Map<String, Object> mailParams = new HashMap<>();\n        mailParams.put(\"from\", \"my@mail.com\");\n        mailParams.put(\"to\", Arrays.asList(\"aaa@mail.com\", \"bbb@mail.com\"));\n        mailParams.put(\"subject\", \"test\");\n        mailParams.put(\"message\", \"Hello!\");\n\n        SmtpTask t = new SmtpTask();\n        t.call(smtpParams, mailParams);\n\n        MimeMessage[] messages = mail.getReceivedMessages();\n        assertEquals(2, messages.length);\n        assertEquals(\"Hello!\\r\\n\", messages[0].getContent());\n\n        mail.reset();\n    }\n\n    @Test\n    public void testMultipleToComma() throws Exception {\n        SmtpServer server = mail.getSmtp();\n\n        Map<String, Object> smtpParams = new HashMap<>();\n        smtpParams.put(\"host\", \"localhost\");\n        smtpParams.put(\"port\", server.getPort());\n\n        Map<String, Object> mailParams = new HashMap<>();\n        mailParams.put(\"from\", \"my@mail.com\");\n        mailParams.put(\"to\", \"aaa@mail.com, bbb@mail.com\");\n        mailParams.put(\"subject\", \"test\");\n        mailParams.put(\"message\", \"Hello!\");\n\n        SmtpTask t = new SmtpTask();\n        t.call(smtpParams, mailParams);\n\n        MimeMessage[] messages = mail.getReceivedMessages();\n        assertEquals(2, messages.length);\n        assertEquals(\"Hello!\\r\\n\", messages[0].getContent());\n\n        mail.reset();\n    }\n\n    @Test\n    public void testNoSubject() throws Exception {\n        SmtpServer server = mail.getSmtp();\n\n        Map<String, Object> smtpParams = new HashMap<>();\n        smtpParams.put(\"host\", \"localhost\");\n        smtpParams.put(\"port\", server.getPort());\n\n        Map<String, Object> mailParams = new HashMap<>();\n        mailParams.put(\"from\", \"my@mail.com\");\n        mailParams.put(\"to\", \"aaa@mail.com\");\n        mailParams.put(\"message\", \"Hello!\");\n\n        SmtpTask t = new SmtpTask();\n        t.call(smtpParams, mailParams);\n\n        MimeMessage[] messages = mail.getReceivedMessages();\n        assertEquals(1, messages.length);\n        assertEquals(\"Hello!\\r\\n\", messages[0].getContent());\n\n        mail.reset();\n    }\n\n    @Test\n    public void testNoMessage() throws Exception {\n        SmtpServer server = mail.getSmtp();\n\n        Map<String, Object> smtpParams = new HashMap<>();\n        smtpParams.put(\"host\", \"localhost\");\n        smtpParams.put(\"port\", server.getPort());\n\n        Map<String, Object> mailParams = new HashMap<>();\n        mailParams.put(\"from\", \"my@mail.com\");\n        mailParams.put(\"to\", \"aaa@mail.com\");\n        mailParams.put(\"subject\", \"test\");\n\n        try {\n            SmtpTask t = new SmtpTask();\n            t.call(smtpParams, mailParams);\n            fail(\"should fail\");\n        } catch (IllegalArgumentException e) {\n            // expected\n        }\n\n        mail.reset();\n    }\n\n    @Test\n    public void testAttachments() throws Exception {\n        SmtpServer server = mail.getSmtp();\n\n        Map<String, Object> smtpParams = new HashMap<>();\n        smtpParams.put(\"host\", \"localhost\");\n        smtpParams.put(\"port\", server.getPort());\n\n        Map<String, Object> mailParams = new HashMap<>();\n        mailParams.put(\"from\", \"my@mail.com\");\n        mailParams.put(\"to\", \"their@mail.com\");\n        mailParams.put(\"template\", new File(ClassLoader.getSystemResource(\"test.mustache\").toURI()).toString());\n        mailParams.put(\"attachments\", Collections.singletonList(new File(ClassLoader.getSystemResource(\"attahcment.txt\").toURI()).toString()));\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"name\", \"Concord\");\n        m.put(\"workDir\", \"/\");\n        m.put(\"smtp\", smtpParams);\n        m.put(\"mail\", mailParams);\n\n        Context ctx = new MockContext(m);\n\n        SmtpTask t = new SmtpTask();\n        t.execute(ctx);\n\n        MimeMessage[] messages = mail.getReceivedMessages();\n        assertEquals(1, messages.length);\n        MimeMessage msg = messages[0];\n        assertNotNull(msg.getContent());\n        assertTrue(msg.getContent() instanceof MimeMultipart);\n        MimeMultipart mp = (MimeMultipart) msg.getContent();\n        assertEquals(2, mp.getCount());\n        assertEquals(\"Hello, Concord!\", mp.getBodyPart(0).getContent());\n        assertEquals(\"test-attachment\", mp.getBodyPart(1).getContent());\n\n        mail.reset();\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/smtp/src/test/java/com/walmartlabs/concord/plugins/smtp/SmtpTaskV2Test.java",
    "content": "package com.walmartlabs.concord.plugins.smtp;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.icegreen.greenmail.junit5.GreenMailExtension;\nimport com.icegreen.greenmail.util.ServerSetupTest;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport javax.mail.internet.MimeMessage;\nimport java.nio.file.Paths;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\npublic class SmtpTaskV2Test {\n\n    @RegisterExtension\n    GreenMailExtension mailServer = new GreenMailExtension(ServerSetupTest.SMTP);\n\n    @AfterEach\n    public void cleanup() {\n        mailServer.reset();\n    }\n\n    @Test\n    public void testWithPolicyDefaults() throws Exception {\n        Map<String, Object> smtpParams = new HashMap<>();\n        smtpParams.put(\"host\", \"localhost\");\n        smtpParams.put(\"port\", mailServer.getSmtp().getPort());\n\n        Map<String, Object> mail = new HashMap<>();\n        mail.put(\"from\", \"me@localhost\");\n        mail.put(\"to\", \"you@localhost\");\n        mail.put(\"message\", \"Default vars from policy.\");\n\n        Context ctx = mock(Context.class);\n        when(ctx.processConfiguration()).thenReturn(ProcessConfiguration.builder().build());\n        when(ctx.workingDirectory()).thenReturn(Paths.get(System.getProperty(\"user.dir\")));\n        when(ctx.variables()).thenReturn(new MapBackedVariables(Collections.emptyMap()));\n        when(ctx.defaultVariables()).thenReturn(new MapBackedVariables(smtpParams));\n\n        SmtpTaskV2 t = new SmtpTaskV2(ctx);\n        t.execute(new MapBackedVariables(Collections.singletonMap(\"mail\", mail)));\n\n        MimeMessage[] messages = mailServer.getReceivedMessages();\n        assertEquals(1, messages.length);\n        assertEquals(\"Default vars from policy.\\r\\n\", messages[0].getContent());\n    }\n\n    @Test\n    public void testWithProcessDefaults() throws Exception {\n        Map<String, Object> smtpParams = new HashMap<>();\n        smtpParams.put(\"host\", \"localhost\");\n        smtpParams.put(\"port\", mailServer.getSmtp().getPort());\n\n        Map<String, Object> mail = new HashMap<>();\n        mail.put(\"from\", \"me@localhost\");\n        mail.put(\"to\", \"you@localhost\");\n        mail.put(\"message\", \"Default vars from process arguments.\");\n\n        Context ctx = mock(Context.class);\n        when(ctx.processConfiguration()).thenReturn(ProcessConfiguration.builder().build());\n        when(ctx.workingDirectory()).thenReturn(Paths.get(System.getProperty(\"user.dir\")));\n        when(ctx.variables()).thenReturn(new MapBackedVariables(Collections.singletonMap(\"smtpParams\", smtpParams)));\n        when(ctx.defaultVariables()).thenReturn(new MapBackedVariables(Collections.emptyMap()));\n\n        SmtpTaskV2 t = new SmtpTaskV2(ctx);\n        t.execute(new MapBackedVariables(Collections.singletonMap(\"mail\", mail)));\n\n        MimeMessage[] messages = mailServer.getReceivedMessages();\n        assertEquals(1, messages.length);\n        assertEquals(\"Default vars from process arguments.\\r\\n\", messages[0].getContent());\n    }\n\n    @Test\n    public void testWithBothDefaults() throws Exception {\n        Map<String, Object> policyDefaults = new HashMap<>();\n        policyDefaults.put(\"host\", \"badserver\");\n        policyDefaults.put(\"port\", -1);\n\n        // Process arg defaults override policy defaults\n        Map<String, Object> processArgsDefaults = new HashMap<>();\n        processArgsDefaults.put(\"host\", \"localhost\");\n        processArgsDefaults.put(\"port\", mailServer.getSmtp().getPort());\n\n        Map<String, Object> mail = new HashMap<>();\n        mail.put(\"from\", \"me@localhost\");\n        mail.put(\"to\", \"you@localhost\");\n        mail.put(\"message\", \"Default vars from process arguments.\");\n\n        Context ctx = mock(Context.class);\n        when(ctx.processConfiguration()).thenReturn(ProcessConfiguration.builder().build());\n        when(ctx.workingDirectory()).thenReturn(Paths.get(System.getProperty(\"user.dir\")));\n        when(ctx.variables()).thenReturn(new MapBackedVariables(Collections.singletonMap(\"smtpParams\", processArgsDefaults)));\n        when(ctx.defaultVariables()).thenReturn(new MapBackedVariables(policyDefaults));\n\n        SmtpTaskV2 t = new SmtpTaskV2(ctx);\n        t.execute(new MapBackedVariables(Collections.singletonMap(\"mail\", mail)));\n\n        MimeMessage[] messages = mailServer.getReceivedMessages();\n        assertEquals(1, messages.length);\n        assertEquals(\"Default vars from process arguments.\\r\\n\", messages[0].getContent());\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/smtp/src/test/resources/attahcment.txt",
    "content": "test-attachment"
  },
  {
    "path": "plugins/tasks/smtp/src/test/resources/logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss} [%-5level] %logger{12} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "plugins/tasks/smtp/src/test/resources/test.mustache",
    "content": "Hello, {{ name }}!"
  },
  {
    "path": "plugins/tasks/throw/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>throw-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <configuration>\n                    <!-- no verify, as this plugin depends on runtime-v2 -->\n                    <skipVerify>true</skipVerify>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/tasks/throw/src/main/java/com/walmartlabs/concord/plugins/throwex/ConcordException.java",
    "content": "package com.walmartlabs.concord.plugins.throwex;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\n\npublic class ConcordException extends Exception {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Serializable payload;\n\n    public ConcordException(String message) {\n        this(message, null);\n    }\n\n    public ConcordException(String message, Serializable payload) {\n        super(message);\n        this.payload = payload;\n    }\n\n    public Serializable getPayload() {\n        return payload;\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/throw/src/main/java/com/walmartlabs/concord/plugins/throwex/ThrowExceptionTask.java",
    "content": "package com.walmartlabs.concord.plugins.throwex;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Task;\nimport com.walmartlabs.concord.sdk.UserDefinedException;\n\nimport javax.inject.Named;\nimport java.io.Serializable;\n\n@Named(\"throw\")\npublic class ThrowExceptionTask implements Task {\n\n    public void call(Object o) throws Exception {\n        if (o instanceof Exception) {\n            throw (Exception) o;\n        } else if (o instanceof String) {\n            throw new UserDefinedException(o.toString());\n        } else if (o instanceof Serializable) {\n            throw new ConcordException(\"Process error\", (Serializable) o);\n        } else {\n            throw new UserDefinedException(o != null ? o.toString() : \"n/a\");\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/throw/src/main/java/com/walmartlabs/concord/plugins/throwex/ThrowExceptionTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.throwex;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\n\nimport javax.inject.Named;\nimport java.io.Serializable;\nimport java.util.Map;\n\n@Named(\"throw\")\n@DryRunReady\npublic class ThrowExceptionTaskV2 implements Task {\n\n    @Override\n    public TaskResult execute(Variables input) throws Exception {\n        var exception = input.get(\"exception\");\n\n        if (exception instanceof Exception) {\n            throw (Exception) exception;\n        } else if (exception instanceof String s) {\n            throw new UserDefinedException(s, input.getMap(\"payload\", Map.of()));\n        } else if (exception instanceof Serializable) {\n            throw new ConcordException(\"Process Error\", (Serializable) exception);\n        } else {\n            throw new UserDefinedException(exception != null ? exception.toString() : \"n/a\");\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/variables/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>variables-tasks</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.glassfish</groupId>\n            <artifactId>javax.el</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>dev.ybrig.concord</groupId>\n                <artifactId>concord-maven-plugin</artifactId>\n                <dependencies>\n                    <dependency>\n                        <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n                        <artifactId>concord-runner-v2</artifactId>\n                        <version>${project.version}</version>\n                    </dependency>\n                </dependencies>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/tasks/variables/src/main/java/com/walmartlabs/concord/plugins/variables/VariablesTask.java",
    "content": "package com.walmartlabs.concord.plugins.variables;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.InjectVariable;\nimport com.walmartlabs.concord.sdk.Task;\n\nimport javax.inject.Named;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.plugins.variables.VariablesTaskCommon.EvalVariable;\n\n@Named(\"vars\")\n@SuppressWarnings(\"unused\")\npublic class VariablesTask implements Task {\n\n    public Object get(@InjectVariable(\"context\") Context ctx, String key, Object defaultValue) {\n        return delegate(ctx).get(key, defaultValue);\n    }\n\n    public void set(@InjectVariable(\"context\") Context ctx, String targetKey, String sourceKey, String defaultKey) {\n        delegate(ctx).set(targetKey, sourceKey, defaultKey);\n    }\n\n    public void set(@InjectVariable(\"context\") Context ctx, Map<String, Object> vars) {\n        delegate(ctx).set(vars);\n    }\n\n    public Object eval(@InjectVariable(\"context\") Context ctx, Object v) {\n        return ctx.interpolate(v);\n    }\n\n    public List<Object> concat(Collection<Object> a, Collection<Object> b) {\n        return VariablesTaskCommon.concat(a, b);\n    }\n\n    private static VariablesTaskCommon delegate(Context ctx) {\n        return new VariablesTaskCommon(new VariablesAdapter(ctx), Collections.singletonList(new EvalVariable(Constants.Context.CONTEXT_KEY, ctx, Context.class)));\n    }\n\n    private static class VariablesAdapter implements VariablesTaskCommon.Variables {\n\n        private final Context context;\n\n        private VariablesAdapter(Context context) {\n            this.context = context;\n        }\n\n        @Override\n        public Object get(String name) {\n            return context.getVariable(name);\n        }\n\n        @Override\n        public void set(String name, Object value) {\n            context.setVariable(name, value);\n        }\n\n        @Override\n        public Object interpolate(Object v) {\n            return context.interpolate(v);\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/variables/src/main/java/com/walmartlabs/concord/plugins/variables/VariablesTaskCommon.java",
    "content": "package com.walmartlabs.concord.plugins.variables;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.el.*;\nimport java.beans.FeatureDescriptor;\nimport java.util.*;\n\npublic class VariablesTaskCommon {\n\n    public interface Variables {\n\n        Object get(String name);\n\n        void set(String name, Object value);\n\n        Object interpolate(Object v);\n    }\n\n    public static class EvalVariable {\n\n        private final String name;\n        private final Object value;\n        private final Class<?> clazz;\n\n        public EvalVariable(String name, Object value, Class<?> clazz) {\n            this.name = name;\n            this.value = value;\n            this.clazz = clazz;\n        }\n\n        public String name() {\n            return name;\n        }\n\n        public Object value() {\n            return value;\n        }\n\n        public Class<?> clazz() {\n            return clazz;\n        }\n    }\n\n    private final ExpressionFactory expressionFactory = ExpressionFactory.newInstance();\n\n    private final Variables variables;\n    private final List<EvalVariable> evalVariables;\n\n    public VariablesTaskCommon(Variables variables, List<EvalVariable> evalVariables) {\n        this.variables = variables;\n        this.evalVariables = evalVariables;\n    }\n\n    public Object get(String key, Object defaultValue) {\n        Object v;\n        if (isNestedVariable(key)) {\n            StandardELContext sc = createELContext();\n            ValueExpression x = expressionFactory.createValueExpression(sc, \"${\" + key + \"}\", Object.class);\n            try {\n                v = x.getValue(sc);\n            } catch (PropertyNotFoundException e) {\n                v = null;\n            }\n        } else {\n            v = variables.get(key);\n        }\n        return v != null ? v : defaultValue;\n    }\n\n    public void set(String targetKey, String sourceKey, String defaultKey) {\n        Object v = variables.get(sourceKey);\n        if (v == null) {\n            v = variables.get(defaultKey);\n        }\n        variables.set(targetKey, v);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public void set(Map<String, Object> vars) {\n        vars.forEach((k, value) -> {\n            Map<String, Object> vv = (Map<String, Object>) variables.interpolate(Collections.singletonMap(k, value));\n            Object v = vv.get(k);\n            if (isNestedVariable(k)) {\n                StandardELContext sc = createELContext();\n                ValueExpression x = expressionFactory.createValueExpression(sc, \"${\" + k + \"}\", Object.class);\n                x.setValue(sc, v);\n            } else {\n                variables.set(k, v);\n            }\n        });\n    }\n\n    public static List<Object> concat(Collection<Object> a, Collection<Object> b) {\n        if (a == null) {\n            a = Collections.emptyList();\n        }\n\n        if (b == null) {\n            b = Collections.emptyList();\n        }\n\n        List<Object> l = new ArrayList<>(a.size() + b.size());\n        l.addAll(a);\n        l.addAll(b);\n        return l;\n    }\n\n    private StandardELContext createELContext() {\n        ELResolver r = new VariablesResolver(variables);\n\n        StandardELContext sc = new StandardELContext(expressionFactory);\n        sc.putContext(ExpressionFactory.class, expressionFactory);\n        sc.addELResolver(r);\n\n        VariableMapper vm = sc.getVariableMapper();\n        for (EvalVariable v : evalVariables) {\n            vm.setVariable(v.name(), expressionFactory.createValueExpression(v.value(), v.clazz()));\n        }\n        return sc;\n    }\n\n    private static boolean isNestedVariable(String str) {\n        return str.contains(\".\");\n    }\n\n    private static class VariablesResolver extends ELResolver {\n\n        private final Variables variables;\n\n        public VariablesResolver(Variables variables) {\n            this.variables = variables;\n        }\n\n        @Override\n        public Class<?> getCommonPropertyType(ELContext context, Object base) {\n            return Object.class;\n        }\n\n        @Override\n        public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {\n            return null;\n        }\n\n        @Override\n        public Class<?> getType(ELContext context, Object base, Object property) {\n            return Object.class;\n        }\n\n        @Override\n        public Object getValue(ELContext context, Object base, Object property) {\n            if (base == null && property instanceof String) {\n                String k = (String) property;\n                Object v = variables.get(k);\n                if (v != null) {\n                    context.setPropertyResolved(true);\n                    return v;\n                }\n            }\n\n            return null;\n        }\n\n        @Override\n        public boolean isReadOnly(ELContext context, Object base, Object property) {\n            return true;\n        }\n\n        @Override\n        public void setValue(ELContext context, Object base, Object property, Object value) {\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/tasks/variables/src/main/java/com/walmartlabs/concord/plugins/variables/v2/VariablesTaskV2.java",
    "content": "package com.walmartlabs.concord.plugins.variables.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.plugins.variables.VariablesTaskCommon;\nimport com.walmartlabs.concord.plugins.variables.VariablesTaskCommon.Variables;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.DryRunReady;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport com.walmartlabs.concord.sdk.Constants;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n@Named(\"vars\")\n@DryRunReady\n@SuppressWarnings(\"unused\")\npublic class VariablesTaskV2 implements Task {\n\n    private final Context context;\n    private final VariablesTaskCommon delegate;\n\n    @Inject\n    public VariablesTaskV2(Context context) {\n        this.context = context;\n        this.delegate = new VariablesTaskCommon(new VariablesAdapter(context), Collections.singletonList(new VariablesTaskCommon.EvalVariable(Constants.Context.CONTEXT_KEY, context, Context.class)));\n    }\n\n    public Object get(String key, Object defaultValue) {\n        return delegate.get(key, defaultValue);\n    }\n\n    public void set(String targetKey, String sourceKey, String defaultKey) {\n       delegate.set(targetKey, sourceKey, defaultKey);\n    }\n\n    public void set(Map<String, Object> vars) {\n        delegate.set(vars);\n    }\n\n    public Object eval(Object v) {\n        return context.eval(v, Object.class);\n    }\n\n    public List<Object> concat(Collection<Object> a, Collection<Object> b) {\n        return VariablesTaskCommon.concat(a, b);\n    }\n\n    private static class VariablesAdapter implements Variables {\n\n        private final Context context;\n\n        private VariablesAdapter(Context context) {\n            this.context = context;\n        }\n\n        @Override\n        public Object get(String name) {\n            return context.variables().get(name);\n        }\n\n        @Override\n        public void set(String name, Object value) {\n            context.variables().set(name, value);\n        }\n\n        @Override\n        public Object interpolate(Object v) {\n            return context.eval(v, Object.class);\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/templates/ansible/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>ansible-template</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>ansible-tasks</artifactId>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <resources>\n            <resource>\n                <filtering>false</filtering>\n                <directory>${project.basedir}/src/main/resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n            <resource>\n                <filtering>true</filtering>\n                <directory>${project.basedir}/src/main/filtered-resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n                <excludes>\n                    <exclude>**/processes/*.yml</exclude>\n                    <exclude>**/*.py</exclude>\n                </excludes>\n            </resource>\n            <resource>\n                <filtering>false</filtering>\n                <directory>${project.basedir}/src/main/filtered-resources</directory>\n                <includes>\n                    <include>**/processes/*.yml</include>\n                    <include>**/*.py</include>\n                </includes>\n            </resource>\n        </resources>\n    </build>\n</project>\n"
  },
  {
    "path": "plugins/templates/ansible/src/main/filtered-resources/META-INF/concord/template.properties",
    "content": "name=ansible\n"
  },
  {
    "path": "plugins/templates/ansible/src/main/filtered-resources/_main.js",
    "content": "({\n    entryPoint: \"main\",\n    dependencies: [\n        \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:${project.version}\"\n    ],\n    arguments: {\n        ansibleParams: _input\n    }\n});\n"
  },
  {
    "path": "plugins/templates/ansible/src/main/filtered-resources/processes/main.yml",
    "content": "main:\n- expr: ${ansible.run(ansibleParams, workDir)}"
  },
  {
    "path": "policy-engine/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-policy-engine</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-dependency-manager</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.maven</groupId>\n            <artifactId>maven-artifact</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.maven.resolver</groupId>\n            <artifactId>maven-resolver-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.errorprone</groupId>\n            <artifactId>error_prone_annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n</project>\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/AttachmentsPolicy.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.nio.file.FileVisitResult;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.SimpleFileVisitor;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Objects;\n\npublic class AttachmentsPolicy {\n\n    private final AttachmentsRule rule;\n\n    public AttachmentsPolicy(AttachmentsRule rule) {\n        this.rule = rule;\n    }\n\n    public CheckResult<AttachmentsRule, Long> check(Path p) throws IOException {\n        if (rule == null || !Files.exists(p) || !Files.isDirectory(p)) {\n            return CheckResult.success();\n        }\n\n        List<CheckResult.Item<AttachmentsRule, Long>> deny = new ArrayList<>();\n\n        Long[] size = { 0L };\n\n        Files.walkFileTree(p, new SimpleFileVisitor<Path>() {\n\n            @Override\n            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {\n                size[0] += Files.size(file);\n\n                return FileVisitResult.CONTINUE;\n            }\n        });\n\n        if (size[0] > Objects.requireNonNull(rule.maxSizeInBytes())) {\n            deny.add(new CheckResult.Item<>(rule, size[0], null));\n        }\n\n        return new CheckResult<>(Collections.emptyList(), deny);\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/AttachmentsRule.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableAttachmentsRule.class)\n@JsonDeserialize(as = ImmutableAttachmentsRule.class)\npublic interface AttachmentsRule extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    @Value.Parameter\n    String msg();\n\n    @Value.Parameter\n    long maxSizeInBytes();\n\n    static AttachmentsRule of(String msg, Long maxSizeInBytes) {\n        return ImmutableAttachmentsRule.of(msg, maxSizeInBytes);\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/CheckResult.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\npublic class CheckResult<R, E> {\n\n    private static final CheckResult SUCCESS = new CheckResult<>();\n\n    @SuppressWarnings(\"unchecked\")\n    public static <R, E> CheckResult<R, E> success() {\n        return (CheckResult<R, E>)SUCCESS;\n    }\n\n    private final List<Item<R, E>> warn;\n    private final List<Item<R, E>> deny;\n\n    public CheckResult() {\n        this(Collections.emptyList(), Collections.emptyList());\n    }\n\n    public CheckResult(List<Item<R, E>> warn, List<Item<R, E>> deny) {\n        this.warn = warn;\n        this.deny = deny;\n    }\n\n    @SafeVarargs\n    public static <R, E> CheckResult<R, E> warn(Item<R, E> ... items) {\n        return new CheckResult<R, E>(Arrays.asList(items), Collections.emptyList());\n    }\n\n    @SafeVarargs\n    public static <R, E> CheckResult<R, E> error(Item<R, E> ... items) {\n        return new CheckResult<R, E>(Collections.emptyList(), Arrays.asList(items));\n    }\n\n    public List<Item<R, E>> getWarn() {\n        return warn;\n    }\n\n    public List<Item<R, E>> getDeny() {\n        return deny;\n    }\n\n    public static class Item<R, E> {\n\n        private final R rule;\n        private final E entity;\n        private final String msg;\n\n        public Item(R rule, E entity) {\n            this(rule, entity, null);\n        }\n\n        public Item(R rule, E entity, String msg) {\n            this.rule = rule;\n            this.entity = entity;\n            this.msg = msg;\n        }\n\n        public R getRule() {\n            return rule;\n        }\n\n        public E getEntity() {\n            return entity;\n        }\n\n        public String getMsg() {\n            return msg;\n        }\n\n        @Override\n        public String toString() {\n            return msg;\n        }\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/ConcurrentProcessPolicy.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.UUID;\nimport java.util.function.Supplier;\n\npublic class ConcurrentProcessPolicy {\n\n    private final ConcurrentProcessRule rule;\n\n    public ConcurrentProcessPolicy(ConcurrentProcessRule rule) {\n        this.rule = rule;\n    }\n\n    public CheckResult<ConcurrentProcessRule, List<UUID>> check(\n            Supplier<List<UUID>> processPerOrg,\n            Supplier<List<UUID>> processPerProject) {\n\n        if (rule == null || (rule.maxPerOrg() == null && rule.maxPerProject() == null)) {\n            return CheckResult.success();\n        }\n\n        int max;\n        List<UUID> processes;\n        if (rule.maxPerOrg() != null) {\n            processes = processPerOrg.get();\n            max = Objects.requireNonNull(rule.maxPerOrg());\n        } else {\n            processes = processPerProject.get();\n            max = Objects.requireNonNull(rule.maxPerProject());\n        }\n\n        if (processes.size() >= max) {\n            return CheckResult.error(new CheckResult.Item<>(rule, processes));\n        }\n        return CheckResult.success();\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/ConcurrentProcessRule.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonAlias;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableConcurrentProcessRule.class)\n@JsonDeserialize(as = ImmutableConcurrentProcessRule.class)\npublic interface ConcurrentProcessRule extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    String msg();\n\n    @Nullable\n    Integer maxPerOrg();\n\n    @Nullable\n    @JsonProperty(\"maxPerProject\") @JsonAlias(\"max\")\n    Integer maxPerProject();\n\n    static ImmutableConcurrentProcessRule.Builder builder() {\n        return ImmutableConcurrentProcessRule.builder();\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/ContainerPolicy.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\npublic class ContainerPolicy {\n\n    private final ContainerRule rule;\n\n    public ContainerPolicy(ContainerRule rule) {\n        this.rule = rule;\n    }\n\n    public CheckResult<ContainerRule, Object> check(Map<String, Object> containerOptions) {\n        if (rule == null) {\n            return CheckResult.success();\n        }\n\n        List<CheckResult.Item<ContainerRule, Object>> deny = new ArrayList<>();\n\n        if (rule.maxCpu() != null) {\n            Integer actualCpu = getInt(\"cpu\", containerOptions);\n            if (actualCpu != null && actualCpu > rule.maxCpu()) {\n                deny.add(new CheckResult.Item<>(rule, actualCpu, \"Max CPU exceeded: \" + actualCpu));\n            }\n        }\n\n        if (rule.maxRam() != null) {\n            String actualRamStr = getString(\"ram\", containerOptions);\n            Long maxRam = parseRam(rule.maxRam());\n            Long actualRam = parseRam(actualRamStr);\n            if (actualRam != null && actualRam > maxRam) {\n                deny.add(new CheckResult.Item<>(rule, actualRamStr, \"Max RAM exceeded: \" + actualRamStr));\n            }\n        }\n\n        return new CheckResult<>(Collections.emptyList(), deny);\n    }\n\n    private static Integer getInt(String name, Map<String, Object> params) {\n        Object o = params.get(name);\n        if (o == null) {\n            return null;\n        }\n\n        if (o instanceof Number) {\n            return ((Number)o).intValue();\n        }\n        throw new IllegalArgumentException(\"Expected an integer value '\" + name + \"', got: \" + o);\n    }\n\n    private static String getString(String name, Map<String, Object> params) {\n        Object o = params.get(name);\n        if (o == null) {\n            return null;\n        }\n\n        if (o instanceof String) {\n            return ((String)o).trim();\n        }\n        throw new IllegalArgumentException(\"Expected a string value '\" + name + \"', got: \" + o);\n    }\n\n    private static Long parseRam(String str) {\n        if (str == null) {\n            return null;\n        }\n\n        long c = 1L;\n        if (str.endsWith(\"k\")) {\n            c = 1024L;\n        } else if (str.endsWith(\"m\")) {\n            c = 1024 * 1024L;\n        } else if (str.endsWith(\"g\")) {\n            c = 1024 * 1024 * 1024L;\n        }\n\n        return Long.valueOf(str.substring(0, str.length() - 1).trim()) * c;\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/ContainerRule.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableContainerRule.class)\n@JsonDeserialize(as = ImmutableContainerRule.class)\npublic interface ContainerRule extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    String msg();\n\n    @Nullable\n    String maxRam();\n\n    @Nullable\n    Integer maxCpu();\n\n    static ContainerRule of(String msg, String maxRam, Integer maxCpu) {\n        return ImmutableContainerRule.builder()\n                .msg(msg)\n                .maxRam(maxRam)\n                .maxCpu(maxCpu)\n                .build();\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/CronTriggerPolicy.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.time.Duration;\nimport java.time.OffsetDateTime;\nimport java.util.concurrent.TimeUnit;\n\npublic class CronTriggerPolicy {\n\n    private final CronTriggerRule rule;\n\n    public CronTriggerPolicy(CronTriggerRule rule) {\n        this.rule = rule;\n    }\n\n    public CheckResult<CronTriggerRule, Duration> check(OffsetDateTime fireAt, OffsetDateTime nexFireAt) {\n        if (rule == null) {\n            return CheckResult.success();\n        }\n\n        Duration interval = Duration.between(fireAt.toLocalTime(), nexFireAt.toLocalTime());\n\n        if (TimeUnit.SECONDS.toMillis(rule.minInterval()) > interval.toMillis()) {\n            return CheckResult.error(new CheckResult.Item<>(rule, interval));\n        }\n\n        return CheckResult.success();\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/CronTriggerRule.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableCronTriggerRule.class)\n@JsonDeserialize(as = ImmutableCronTriggerRule.class)\npublic interface CronTriggerRule extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    @Value.Parameter\n    String msg();\n\n    @Value.Parameter\n    int minInterval();\n\n    static CronTriggerRule of(String msg, int minInterval) {\n        return ImmutableCronTriggerRule.of(msg, minInterval);\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/DependencyPolicy.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.dependencymanager.DependencyEntity;\nimport org.apache.maven.artifact.versioning.ComparableVersion;\n\nimport java.net.URI;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport static com.walmartlabs.concord.policyengine.Utils.matches;\n\npublic class DependencyPolicy {\n\n    private final PolicyRules<DependencyRule> rules;\n\n    public DependencyPolicy(PolicyRules<DependencyRule> rules) {\n        this.rules = rules;\n    }\n\n    public CheckResult<DependencyRule, DependencyEntity> check(Collection<DependencyEntity> dependencies) {\n        if (rules == null || rules.isEmpty()) {\n            return CheckResult.success();\n        }\n\n        List<CheckResult.Item<DependencyRule, DependencyEntity>> warn = new ArrayList<>();\n        List<CheckResult.Item<DependencyRule, DependencyEntity>> deny = new ArrayList<>();\n\n        for (DependencyEntity e : dependencies) {\n            check(e, warn, deny);\n        }\n\n        return new CheckResult<>(warn, deny);\n    }\n\n    private void check(DependencyEntity d,\n                       List<CheckResult.Item<DependencyRule, DependencyEntity>> warn,\n                       List<CheckResult.Item<DependencyRule, DependencyEntity>> deny) {\n\n        for (DependencyRule r : rules.getAllow()) {\n            if (matchRule(r, d)) {\n                return;\n            }\n        }\n\n        for (DependencyRule r : rules.getDeny()) {\n            if (matchRule(r, d)) {\n                deny.add(new CheckResult.Item<>(r, d));\n                return;\n            }\n        }\n\n        for (DependencyRule r : rules.getWarn()) {\n            if (matchRule(r, d)) {\n                warn.add(new CheckResult.Item<>(r, d));\n                return;\n            }\n        }\n    }\n\n    private static boolean matchRule(DependencyRule r, DependencyEntity d) {\n        if (d.getArtifact() != null) {\n            return matchRule(r, d.getArtifact());\n        } else {\n            return matchRule(r, d.getDirectLink());\n        }\n    }\n\n    private static boolean matchRule(DependencyRule r, DependencyEntity.Artifact a) {\n        if (r.scheme() != null && !matches(r.scheme(), \"mvn\")) {\n            return false;\n        }\n\n        if (r.groupId() != null && !matches(r.groupId(), a.getGroupId())) {\n            return false;\n        }\n\n        if (r.artifactId() != null && !matches(r.artifactId(), a.getArtifactId())) {\n            return false;\n        }\n\n        if (r.fromVersion() != null && compareVersions(r.fromVersion(), a.getVersion()) > 0) {\n            return false;\n        }\n\n        if (r.toVersion() != null && compareVersions(r.toVersion(), a.getVersion()) < 0) {\n            return false;\n        }\n\n        return true;\n    }\n\n    private static boolean matchRule(DependencyRule r, URI directLink) {\n        if (r.scheme() != null && matches(r.scheme(), directLink.getScheme())) {\n            return true;\n        }\n\n        return false;\n    }\n\n    private static int compareVersions(String a, String b) {\n        ComparableVersion v1 = new ComparableVersion(a);\n        ComparableVersion v2 = new ComparableVersion(b);\n        return v1.compareTo(v2);\n    }\n\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/DependencyRewritePolicy.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.apache.maven.artifact.versioning.ComparableVersion;\nimport org.eclipse.aether.artifact.Artifact;\nimport org.eclipse.aether.artifact.DefaultArtifact;\n\nimport java.net.URI;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport static com.walmartlabs.concord.dependencymanager.DependencyManager.MAVEN_SCHEME;\nimport static com.walmartlabs.concord.policyengine.Utils.matches;\n\npublic class DependencyRewritePolicy {\n\n    private final List<DependencyRewriteRule> rules;\n\n    public DependencyRewritePolicy(List<DependencyRewriteRule> rules) {\n        this.rules = rules;\n    }\n\n    public Collection<URI> rewrite(Collection<URI> dependencies, RewriteListener listener) {\n        if (rules == null || rules.isEmpty() || dependencies.isEmpty()) {\n            return dependencies;\n        }\n\n        List<URI> result = new ArrayList<>(dependencies.size());\n        for (URI u : dependencies) {\n            result.addAll(rewrite(u, listener));\n        }\n        return result;\n    }\n\n    private List<URI> rewrite(URI value, RewriteListener listener) {\n        String scheme = value.getScheme();\n        if (!MAVEN_SCHEME.equalsIgnoreCase(scheme)) {\n            return List.of(value);\n        }\n\n        List<URI> result = new ArrayList<>();\n        Artifact artifact = new DefaultArtifact(value.getAuthority());\n        for (DependencyRewriteRule rule : rules) {\n            if (match(rule, artifact)) {\n                if (rule.value() != null) {\n                    listener.onRewrite(rule.msg(), value, rule.value());\n                    result.add(rule.value());\n                }\n\n                for (URI u : rule.values()) {\n                    listener.onRewrite(rule.msg(), value, u);\n                }\n                result.addAll(rule.values());\n            }\n        }\n\n        return result.isEmpty() ? List.of(value) : result;\n    }\n\n    private static boolean match(DependencyRewriteRule r, Artifact a) {\n        if (r.groupId() != null && !matches(r.groupId(), a.getGroupId())) {\n            return false;\n        }\n\n        if (r.artifactId() != null && !matches(r.artifactId(), a.getArtifactId())) {\n            return false;\n        }\n\n        if (r.fromVersion() != null && compareVersions(r.fromVersion(), a.getVersion()) > 0) {\n            return false;\n        }\n\n        if (r.toVersion() != null && compareVersions(r.toVersion(), a.getVersion()) < 0) {\n            return false;\n        }\n\n        return true;\n    }\n\n    private static int compareVersions(String a, String b) {\n        ComparableVersion v1 = new ComparableVersion(a);\n        ComparableVersion v2 = new ComparableVersion(b);\n        return v1.compareTo(v2);\n    }\n\n    public interface RewriteListener {\n\n        void onRewrite(String msg, URI from, URI to);\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/DependencyRewriteRule.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.net.URI;\nimport java.util.List;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableDependencyRewriteRule.class)\n@JsonDeserialize(as = ImmutableDependencyRewriteRule.class)\npublic interface DependencyRewriteRule extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    String msg();\n\n    @Nullable\n    String groupId();\n\n    @Nullable\n    String artifactId();\n\n    @Nullable\n    String fromVersion();\n\n    @Nullable\n    String toVersion();\n\n    @Nullable\n    URI value();\n\n    @Value.Default\n    default List<URI> values() {\n        return List.of();\n    }\n\n    static ImmutableDependencyRewriteRule.Builder builder() {\n        return ImmutableDependencyRewriteRule.builder();\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/DependencyRule.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableDependencyRule.class)\n@JsonDeserialize(as = ImmutableDependencyRule.class)\npublic interface DependencyRule extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    String msg();\n\n    @Nullable\n    String scheme();\n\n    @Nullable\n    String groupId();\n\n    @Nullable\n    String artifactId();\n\n    @Nullable\n    String fromVersion();\n\n    @Nullable\n    String toVersion();\n\n    static ImmutableDependencyRule.Builder builder() {\n        return ImmutableDependencyRule.builder();\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/DependencyVersionsPolicy.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Objects;\n\npublic class DependencyVersionsPolicy {\n\n    private final List<Dependency> rules;\n\n    public DependencyVersionsPolicy(List<Dependency> rules) {\n        this.rules = rules != null ? rules : Collections.emptyList();\n    }\n\n    public List<Dependency> get() {\n        return rules;\n    }\n\n    public static class Dependency {\n\n        private final String artifact;\n        private final String version;\n\n        public Dependency(@JsonProperty(\"artifact\") String artifact,\n                          @JsonProperty(\"version\") String version) {\n\n            this.artifact = artifact;\n            this.version = version;\n        }\n\n        public String getArtifact() {\n            return artifact;\n        }\n\n        public String getVersion() {\n            return version;\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n            Dependency that = (Dependency) o;\n            return Objects.equals(artifact, that.artifact) &&\n                    Objects.equals(version, that.version);\n        }\n\n        @Override\n        public int hashCode() {\n            return Objects.hash(artifact, version);\n        }\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/EffectiveYamlPolicy.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class EffectiveYamlPolicy {\n\n    private final EffectiveYamlRule rule;\n\n    public EffectiveYamlPolicy(EffectiveYamlRule rule) {\n        this.rule = rule;\n    }\n\n    public boolean renderEffectiveYaml() {\n        // enabled by default\n        return rule == null || rule.renderEffectiveYaml();\n    }\n\n    public boolean isTooLarge(long sizeInBytes) {\n        // no enforcement by default\n        if (rule == null) {\n            return false;\n        }\n\n        var maxSizeInBytes = rule.maxSizeInBytes();\n\n        if (maxSizeInBytes == null) {\n            return false;\n        }\n\n        return sizeInBytes > maxSizeInBytes;\n    }\n\n    public boolean logWarning() {\n        // enabled by default\n        return rule == null || rule.logWarning();\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/EffectiveYamlRule.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableEffectiveYamlRule.class)\n@JsonDeserialize(as = ImmutableEffectiveYamlRule.class)\npublic interface EffectiveYamlRule {\n\n    @Value.Default\n    default boolean renderEffectiveYaml() {\n        return true;\n    }\n\n    @Nullable\n    Long maxSizeInBytes();\n\n    @Value.Default\n    default boolean logWarning() {\n        return true;\n    }\n\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/EntityPolicy.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Supplier;\n\nimport static com.walmartlabs.concord.policyengine.Utils.matches;\n\npublic class EntityPolicy {\n\n    private final PolicyRules<EntityRule> rules;\n\n    public EntityPolicy(PolicyRules<EntityRule> rules) {\n        this.rules = rules;\n    }\n\n    public CheckResult<EntityRule, Map<String, Object>> check(String entity, String action, Supplier<Map<String, Object>> attrs) {\n        if (rules == null || rules.isEmpty()) {\n            return CheckResult.success();\n        }\n\n        List<CheckResult.Item<EntityRule, Map<String, Object>>> warn = new ArrayList<>();\n        List<CheckResult.Item<EntityRule, Map<String, Object>>> deny = new ArrayList<>();\n\n        check(entity, action, attrs.get(), warn, deny);\n\n        return new CheckResult<>(warn, deny);\n    }\n\n    private void check(String entity, String action, Map<String, Object> attrs,\n                       List<CheckResult.Item<EntityRule, Map<String, Object>>> warn,\n                       List<CheckResult.Item<EntityRule, Map<String, Object>>> deny) {\n\n        for (EntityRule r : rules.getAllow()) {\n            if (matchRule(r, entity, action, attrs)) {\n                return;\n            }\n        }\n\n        for (EntityRule r : rules.getDeny()) {\n            if (matchRule(r, entity, action, attrs)) {\n                deny.add(new CheckResult.Item<>(r, attrs));\n                return;\n            }\n        }\n\n        for (EntityRule r : rules.getWarn()) {\n            if (matchRule(r, entity, action, attrs)) {\n                warn.add(new CheckResult.Item<>(r, attrs));\n                return;\n            }\n        }\n    }\n\n    private boolean matchRule(EntityRule r, String entity, String action, Map<String, Object> attrs) {\n        if (r.entity() != null && !matches(r.entity(), entity)) {\n            return false;\n        }\n\n        if (r.action() != null && !matches(r.action(), action)) {\n            return false;\n        }\n\n        if (r.conditions() != null && !matches(r.conditions(), attrs)) {\n            return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/EntityRule.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.Map;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableEntityRule.class)\n@JsonDeserialize(as = ImmutableEntityRule.class)\npublic interface EntityRule extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    String msg();\n\n    @Nullable\n    String entity();\n\n    @Nullable\n    String action();\n\n    @Nullable\n    Map<String, Object> conditions();\n\n    static ImmutableEntityRule.Builder builder() {\n        return ImmutableEntityRule.builder();\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/FilePolicy.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.nio.file.FileVisitResult;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.SimpleFileVisitor;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static com.walmartlabs.concord.policyengine.Utils.matchAny;\n\npublic class FilePolicy {\n\n    private final PolicyRules<FileRule> rules;\n\n    public FilePolicy(PolicyRules<FileRule> rules) {\n        this.rules = rules;\n    }\n\n    public CheckResult<FileRule, Path> check(Path p) throws IOException {\n        if (rules == null || rules.isEmpty()) {\n            return new CheckResult<>();\n        }\n\n        List<CheckResult.Item<FileRule, Path>> warn = new ArrayList<>();\n        List<CheckResult.Item<FileRule, Path>> deny = new ArrayList<>();\n\n        Files.walkFileTree(p, new SimpleFileVisitor<Path>() {\n\n            @Override\n            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {\n                for(FileRule r : rules.getAllow()) {\n                    if (matchRule(dir, r, FileRule.Type.DIR)) {\n                        return FileVisitResult.SKIP_SUBTREE;\n                    }\n                }\n\n                return FileVisitResult.CONTINUE;\n            }\n\n            @Override\n            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {\n                for(FileRule r : rules.getAllow()) {\n                    if (matchRule(file, r, FileRule.Type.FILE)) {\n                        return FileVisitResult.CONTINUE;\n                    }\n                }\n\n                for(FileRule r : rules.getDeny()) {\n                    if (matchRule(file, r, FileRule.Type.FILE)) {\n                        deny.add(new CheckResult.Item<>(r, file));\n                        return FileVisitResult.CONTINUE;\n                    }\n                }\n\n                for(FileRule r : rules.getWarn()) {\n                    if (matchRule(file, r, FileRule.Type.FILE)) {\n                        warn.add(new CheckResult.Item<>(r, file));\n                        return FileVisitResult.CONTINUE;\n                    }\n                }\n\n                return FileVisitResult.CONTINUE;\n            }\n        });\n\n        return new CheckResult<>(warn, deny);\n    }\n\n    private boolean matchRule(Path file, FileRule ri, FileRule.Type type) throws IOException {\n        if (ri.getType() != type) {\n            return false;\n        }\n\n        if (!ri.getNames().isEmpty() && !matchAny(ri.getNames(), file.getFileName().toString())) {\n            return false;\n        }\n\n        if (ri.getMaxSizeInBytes() != null && Files.size(file) < ri.getMaxSizeInBytes()) {\n            return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/FileRule.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Optional;\n\npublic class FileRule implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String msg;\n\n    private final Long maxSizeInBytes;\n    private final String maxSize;\n    private final Type type;\n    private final List<String> names;\n\n    @JsonCreator\n    public FileRule(\n            @JsonProperty(\"msg\") String msg,\n            @JsonProperty(\"maxSize\") String maxSize,\n            @JsonProperty(\"type\") String type,\n            @JsonProperty(\"names\") List<String> names) {\n\n        this.msg = msg;\n        this.maxSizeInBytes = Utils.parseFileSize(maxSize);\n        this.maxSize = maxSize;\n        this.type = Optional.ofNullable(type).map(v -> Type.valueOf(v.toUpperCase())).orElse(Type.FILE);\n        this.names = Optional.ofNullable(names).orElse(Collections.emptyList());\n    }\n\n    public String getMsg() {\n        return msg;\n    }\n\n    public String getMaxSize() {\n        return maxSize;\n    }\n\n    @JsonIgnore\n    public Long getMaxSizeInBytes() {\n        return maxSizeInBytes;\n    }\n\n    public Type getType() {\n        return type;\n    }\n\n    public List<String> getNames() {\n        return names;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        FileRule fileRule = (FileRule) o;\n        return Objects.equals(msg, fileRule.msg) &&\n                Objects.equals(maxSizeInBytes, fileRule.maxSizeInBytes) &&\n                Objects.equals(maxSize, fileRule.maxSize) &&\n                type == fileRule.type &&\n                Objects.equals(names, fileRule.names);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(msg, maxSizeInBytes, maxSize, type, names);\n    }\n\n    @Override\n    public String toString() {\n        return \"FileRule{\" +\n                \"msg='\" + msg + '\\'' +\n                \", maxSizeInBytes=\" + maxSizeInBytes +\n                \", maxSize='\" + maxSize + '\\'' +\n                \", type=\" + type +\n                \", names=\" + names +\n                '}';\n    }\n\n    public enum Type {\n        FILE, DIR\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/ForkDepthPolicy.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.concurrent.Callable;\n\npublic class ForkDepthPolicy {\n\n    private final ForkDepthRule rule;\n\n    public ForkDepthPolicy(ForkDepthRule rule) {\n        this.rule = rule;\n    }\n\n    public CheckResult<ForkDepthRule, Integer> check(Callable<Integer> c) throws Exception {\n        if (rule == null) {\n            return CheckResult.success();\n        }\n\n        int depth = c.call();\n        if (depth >= rule.max()) {\n            return CheckResult.error(new CheckResult.Item<>(rule, depth));\n        }\n        return CheckResult.success();\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/ForkDepthRule.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableForkDepthRule.class)\n@JsonDeserialize(as = ImmutableForkDepthRule.class)\npublic interface ForkDepthRule extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    @Value.Parameter\n    String msg();\n\n    @Value.Parameter\n    int max();\n\n    static ForkDepthRule of(String msg, int max) {\n        return ImmutableForkDepthRule.of(msg, max);\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/JsonStorePolicy.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Objects;\nimport java.util.concurrent.Callable;\n\npublic class JsonStorePolicy {\n\n    private final JsonStoreRule rule;\n\n    public JsonStorePolicy(JsonStoreRule rule) {\n        this.rule = rule;\n    }\n\n    public CheckResult<JsonStoreRule.StoreRule, Integer> checkStorage(Callable<Integer> storageCount) throws Exception {\n        if (rule == null || rule.store() == null) {\n            return CheckResult.success();\n        }\n\n        JsonStoreRule.StoreRule store = Objects.requireNonNull(rule.store());\n\n        int currentCount = storageCount.call();\n        if (currentCount >= store.maxNumberPerOrg()) {\n            return CheckResult.error(new CheckResult.Item<>(store, currentCount));\n        }\n        return CheckResult.success();\n    }\n\n    public CheckResult<JsonStoreRule.StoreDataRule, Long> checkStorageData(Callable<Long> currentStorageSize) throws Exception {\n        if (rule == null || rule.data() == null) {\n            return CheckResult.success();\n        }\n\n        JsonStoreRule.StoreDataRule data = Objects.requireNonNull(rule.data());\n\n        long current = currentStorageSize.call();\n        if (current >= data.maxSizeInBytes()) {\n            return CheckResult.error(new CheckResult.Item<>(data, current));\n        }\n        return CheckResult.success();\n    }\n\n    public Long getMaxSize() {\n        if (rule == null || rule.data() == null) {\n            return null;\n        }\n\n        return Objects.requireNonNull(rule.data()).maxSizeInBytes();\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/JsonStoreRule.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableJsonStoreRule.class)\n@JsonDeserialize(as = ImmutableJsonStoreRule.class)\npublic interface JsonStoreRule extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    @Value.Parameter\n    StoreRule store();\n\n    @Nullable\n    @Value.Parameter\n    StoreDataRule data();\n\n    static JsonStoreRule of(StoreRule store, StoreDataRule data) {\n        return ImmutableJsonStoreRule.of(store, data);\n    }\n\n    @Value.Immutable\n    @JsonInclude(JsonInclude.Include.NON_EMPTY)\n    @JsonSerialize(as = ImmutableStoreRule.class)\n    @JsonDeserialize(as = ImmutableStoreRule.class)\n    interface StoreRule extends Serializable {\n\n        long serialVersionUID = 1L;\n\n        @Value.Parameter\n        @Nullable\n        String msg();\n\n        @Value.Parameter\n        int maxNumberPerOrg();\n\n        static StoreRule of(String msg, int maxNumberPerOrg) {\n            return ImmutableStoreRule.of(msg, maxNumberPerOrg);\n        }\n    }\n\n    @Value.Immutable\n    @JsonInclude(JsonInclude.Include.NON_EMPTY)\n    @JsonSerialize(as = ImmutableStoreDataRule.class)\n    @JsonDeserialize(as = ImmutableStoreDataRule.class)\n    interface StoreDataRule extends Serializable {\n\n        long serialVersionUID = 1L;\n\n        @Nullable\n        @Value.Parameter\n        String msg();\n\n        @Value.Parameter\n        long maxSizeInBytes();\n\n        static StoreDataRule of(String msg, long maxSizeInBytes) {\n            return ImmutableStoreDataRule.of(msg, maxSizeInBytes);\n        }\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/KvPolicy.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.function.Supplier;\n\npublic class KvPolicy {\n\n    private final KvRule rule;\n\n    public KvPolicy(KvRule rule) {\n        this.rule = rule;\n    }\n\n    public CheckResult<KvRule, Integer> check(\n            Supplier<Integer> currentEntriesCount,\n            Supplier<Boolean> exists) {\n\n        if (rule == null) {\n            return CheckResult.success();\n        }\n\n        Integer count = currentEntriesCount.get();\n        if (count == null) {\n            count = 0;\n        }\n\n        if (rule.maxEntries() >= count + 1) {\n            return CheckResult.success();\n        }\n\n        if (exists.get() && rule.maxEntries() >= count) {\n            return CheckResult.success();\n        }\n\n        return CheckResult.error(new CheckResult.Item<>(rule, count));\n    }\n\n    public Integer getMaxEntries() {\n        if (rule == null) {\n            return null;\n        }\n\n        return rule.maxEntries();\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/KvRule.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableKvRule.class)\n@JsonDeserialize(as = ImmutableKvRule.class)\npublic interface KvRule extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    @Value.Parameter\n    String msg();\n\n    @Value.Parameter\n    int maxEntries();\n\n    static KvRule of(String msg, int maxEntries) {\n        return ImmutableKvRule.of(msg, maxEntries);\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/PolicyEngine.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Collections;\nimport java.util.List;\n\npublic class PolicyEngine {\n\n    private final List<String> ruleNames;\n    private final PolicyEngineRules rules;\n\n    private final DependencyPolicy dependencyPolicy;\n    private final DependencyRewritePolicy dependencyRewritePolicy;\n    private final FilePolicy filePolicy;\n    private final TaskPolicy taskPolicy;\n    private final WorkspacePolicy workspacePolicy;\n    private final AttachmentsPolicy attachmentsPolicy;\n    private final ContainerPolicy containerPolicy;\n    private final ConcurrentProcessPolicy concurrentProcessPolicy;\n    private final ForkDepthPolicy forkDepthPolicy;\n    private final ProcessTimeoutPolicy processTimeoutPolicy;\n    private final ProtectedTasksPolicy protectedTasksPolicy;\n    private final EntityPolicy entityPolicy;\n    private final ProcessCfgPolicy processCfgPolicy;\n    private final JsonStorePolicy jsonStoragePolicy;\n    private final ProcessCfgPolicy defaultProcessCfgPolicy;\n    private final DependencyVersionsPolicy defaultDependencyVersionsPolicy;\n    private final StatePolicy statePolicy;\n    private final RawPayloadPolicy rawPayloadPolicy;\n    private final RuntimePolicy runtimePolicy;\n    private final CronTriggerPolicy cronTriggerPolicy;\n    private final KvPolicy kvPolicy;\n    private final EffectiveYamlPolicy effectiveYamlPolicy;\n\n    public PolicyEngine(PolicyEngineRules rules) {\n        this(Collections.emptyList(), rules);\n    }\n\n    public PolicyEngine(String ruleName, PolicyEngineRules rules) {\n        this(Collections.singletonList(ruleName), rules);\n    }\n\n    public PolicyEngine(List<String> ruleNames, PolicyEngineRules rules) {\n        this.ruleNames = ruleNames;\n        this.rules = rules;\n        this.dependencyPolicy = new DependencyPolicy(rules.dependencyRules());\n        this.dependencyRewritePolicy = new DependencyRewritePolicy(rules.dependencyRewriteRules());\n        this.filePolicy = new FilePolicy(rules.fileRules());\n        this.taskPolicy = new TaskPolicy(rules.taskRules());\n        this.workspacePolicy = new WorkspacePolicy(rules.workspaceRule());\n        this.attachmentsPolicy = new AttachmentsPolicy(rules.attachmentsRule());\n        this.containerPolicy = new ContainerPolicy(rules.containerRules());\n        this.rawPayloadPolicy = new RawPayloadPolicy(rules.rawPayloadRule());\n\n        QueueRule qr = getQueueRule(rules);\n        this.concurrentProcessPolicy = new ConcurrentProcessPolicy(qr.concurrentRule());\n        this.forkDepthPolicy = new ForkDepthPolicy(qr.forkDepthRule());\n        this.processTimeoutPolicy = new ProcessTimeoutPolicy(qr.processTimeoutRule());\n\n        this.protectedTasksPolicy = new ProtectedTasksPolicy(rules.protectedTasksRules());\n        this.entityPolicy = new EntityPolicy(rules.entityRules());\n        this.processCfgPolicy = new ProcessCfgPolicy(rules.processCfg());\n        this.jsonStoragePolicy = new JsonStorePolicy(rules.jsonStoreRule());\n        this.defaultProcessCfgPolicy = new ProcessCfgPolicy(rules.defaultProcessCfg());\n        this.defaultDependencyVersionsPolicy = new DependencyVersionsPolicy(rules.dependencyVersions());\n        this.statePolicy = new StatePolicy(rules.stateRules());\n        this.effectiveYamlPolicy = new EffectiveYamlPolicy(rules.effectiveYamlRule());\n        this.runtimePolicy = new RuntimePolicy(rules.runtimeRule());\n        this.cronTriggerPolicy = new CronTriggerPolicy(rules.cronTriggerRule());\n        this.kvPolicy = new KvPolicy(rules.kvRule());\n    }\n\n    public List<String> policyNames() {\n        return ruleNames;\n    }\n\n    public PolicyEngineRules getRules() {\n        return rules;\n    }\n\n    public DependencyPolicy getDependencyPolicy() {\n        return dependencyPolicy;\n    }\n\n    public DependencyRewritePolicy getDependencyRewritePolicy() {\n        return dependencyRewritePolicy;\n    }\n\n    public FilePolicy getFilePolicy() {\n        return filePolicy;\n    }\n\n    public TaskPolicy getTaskPolicy() {\n        return taskPolicy;\n    }\n\n    public WorkspacePolicy getWorkspacePolicy() {\n        return workspacePolicy;\n    }\n\n    public AttachmentsPolicy getAttachmentsPolicy() {\n        return attachmentsPolicy;\n    }\n\n    public ContainerPolicy getContainerPolicy() {\n        return containerPolicy;\n    }\n\n    public ConcurrentProcessPolicy getConcurrentProcessPolicy() {\n        return concurrentProcessPolicy;\n    }\n\n    public ForkDepthPolicy getForkDepthPolicy() {\n        return forkDepthPolicy;\n    }\n\n    public ProcessTimeoutPolicy getProcessTimeoutPolicy() {\n        return processTimeoutPolicy;\n    }\n\n    public ProtectedTasksPolicy getProtectedTasksPolicy() {\n        return protectedTasksPolicy;\n    }\n\n    public EntityPolicy getEntityPolicy() {\n        return entityPolicy;\n    }\n\n    public ProcessCfgPolicy getProcessCfgPolicy() {\n        return processCfgPolicy;\n    }\n\n    public JsonStorePolicy getJsonStoragePolicy() {\n        return jsonStoragePolicy;\n    }\n\n    public ProcessCfgPolicy getDefaultProcessCfgPolicy() {\n        return defaultProcessCfgPolicy;\n    }\n\n    public DependencyVersionsPolicy getDefaultDependencyVersionsPolicy() {\n        return defaultDependencyVersionsPolicy;\n    }\n\n    public StatePolicy getStatePolicy() {\n        return statePolicy;\n    }\n\n    public RawPayloadPolicy getRawPayloadPolicy() {\n        return rawPayloadPolicy;\n    }\n    \n    public RuntimePolicy getRuntimePolicy() {\n        return runtimePolicy;\n    }\n\n    public CronTriggerPolicy getCronTriggerPolicy() {\n        return cronTriggerPolicy;\n    }\n\n    public KvPolicy getKvPolicy() {\n        return kvPolicy;\n    }\n\n    public EffectiveYamlPolicy getEffectiveYamlPolicy() {\n        return effectiveYamlPolicy;\n    }\n\n    @Override\n    public String toString() {\n        if (ruleNames == null) {\n            return \"no rules defined\";\n        }\n\n        return String.join(\", \", ruleNames);\n    }\n\n    private static QueueRule getQueueRule(PolicyEngineRules rules) {\n        if (rules.queueRules() == null) {\n            return QueueRule.empty();\n        }\n\n        return rules.queueRules();\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/PolicyEngineRules.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonAnyGetter;\nimport com.fasterxml.jackson.annotation.JsonAnySetter;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.List;\nimport java.util.Map;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutablePolicyEngineRules.class)\n@JsonDeserialize(as = ImmutablePolicyEngineRules.class)\npublic interface PolicyEngineRules extends Serializable {\n\n    @JsonProperty(\"dependency\")\n    @Nullable\n    PolicyRules<DependencyRule> dependencyRules();\n\n    @JsonProperty(\"dependencyRewrite\")\n    @Nullable\n    List<DependencyRewriteRule> dependencyRewriteRules();\n\n    @JsonProperty(\"file\")\n    @Nullable\n    PolicyRules<FileRule> fileRules();\n\n    @JsonProperty(\"task\")\n    @Nullable\n    PolicyRules<TaskRule> taskRules();\n\n    @JsonProperty(\"workspace\")\n    @Nullable\n    WorkspaceRule workspaceRule();\n\n    @JsonProperty(\"attachments\")\n    @Nullable\n    AttachmentsRule attachmentsRule();\n\n    @JsonProperty(\"container\")\n    @Nullable\n    ContainerRule containerRules();\n\n    @JsonProperty(\"queue\")\n    @Nullable\n    QueueRule queueRules();\n\n    @JsonProperty(\"protectedTask\")\n    @Nullable\n    ProtectedTasksRule protectedTasksRules();\n\n    @JsonProperty(\"entity\")\n    @Nullable\n    PolicyRules<EntityRule> entityRules();\n\n    @JsonProperty(\"processCfg\")\n    @Nullable\n    Map<String, Object> processCfg();\n\n    @JsonAnySetter\n    @JsonAnyGetter\n    @Nullable\n    Map<String, Object> customRule();\n\n    @JsonProperty(\"jsonStore\")\n    @Nullable\n    JsonStoreRule jsonStoreRule();\n\n    @JsonProperty(\"defaultProcessCfg\")\n    @Nullable\n    Map<String, Object> defaultProcessCfg();\n\n    @JsonProperty(\"dependencyVersions\")\n    @Nullable\n    List<DependencyVersionsPolicy.Dependency> dependencyVersions();\n\n    @JsonProperty(\"state\")\n    @Nullable\n    PolicyRules<StateRule> stateRules();\n\n    @JsonProperty(\"rawPayload\")\n    @Nullable\n    RawPayloadRule rawPayloadRule();\n\n    @JsonProperty(\"runtime\")\n    @Nullable\n    RuntimeRule runtimeRule();\n\n    @JsonProperty(\"cronTrigger\")\n    @Nullable\n    CronTriggerRule cronTriggerRule();\n\n    @JsonProperty(\"kv\")\n    @Nullable\n    KvRule kvRule();\n\n    @JsonProperty(\"effectiveYaml\")\n    @Nullable\n    EffectiveYamlRule effectiveYamlRule();\n\n    static ImmutablePolicyEngineRules.Builder builder() {\n        return ImmutablePolicyEngineRules.builder();\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/PolicyRules.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Optional;\n\npublic class PolicyRules<E extends Serializable> implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final List<E> allow;\n    private final List<E> warn;\n    private final List<E> deny;\n\n    @JsonCreator\n    public PolicyRules(\n            @JsonProperty(\"allow\") List<E> allow,\n            @JsonProperty(\"warn\") List<E> warn,\n            @JsonProperty(\"deny\") List<E> deny) {\n\n        this.allow = Optional.ofNullable(allow).orElse(Collections.emptyList());\n        this.warn = Optional.ofNullable(warn).orElse(Collections.emptyList());\n        this.deny = Optional.ofNullable(deny).orElse(Collections.emptyList());\n    }\n\n    public List<E> getAllow() {\n        return allow;\n    }\n\n    public List<E> getWarn() {\n        return warn;\n    }\n\n    public List<E> getDeny() {\n        return deny;\n    }\n\n    @JsonIgnore\n    public boolean isEmpty() {\n        return allow.isEmpty() && deny.isEmpty() && warn.isEmpty();\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        PolicyRules<?> that = (PolicyRules<?>) o;\n        return Objects.equals(allow, that.allow) &&\n                Objects.equals(warn, that.warn) &&\n                Objects.equals(deny, that.deny);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(allow, warn, deny);\n    }\n\n    @Override\n    public String toString() {\n        return \"{\" +\n                \"allow=\" + allow +\n                \", warn=\" + warn +\n                \", deny=\" + deny +\n                '}';\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/ProcessCfgPolicy.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Collections;\nimport java.util.Map;\n\npublic class ProcessCfgPolicy {\n\n    private final Map<String, Object> rules;\n\n    public ProcessCfgPolicy(Map<String, Object> rules) {\n        this.rules = rules != null ? rules : Collections.emptyMap();\n    }\n\n    public Map<String, Object> get() {\n        return rules;\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/ProcessTimeoutPolicy.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.time.Duration;\nimport java.time.temporal.ChronoUnit;\n\npublic class ProcessTimeoutPolicy {\n\n    private final ProcessTimeoutRule rule;\n\n    public ProcessTimeoutPolicy(ProcessTimeoutRule rule) {\n        this.rule = rule;\n    }\n\n    public CheckResult<ProcessTimeoutRule, Object> check(Object timeout) {\n        if (rule == null) {\n            return CheckResult.success();\n        }\n\n        Long processTimeout = parseTimeout(timeout);\n        if (processTimeout == null) {\n            return CheckResult.success();\n        }\n\n        if (processTimeout >= parseTimeout(rule.max())) {\n            return CheckResult.error(new CheckResult.Item<>(rule, timeout));\n        }\n        return CheckResult.success();\n    }\n\n    private static Long parseTimeout(Object timeout) {\n        if (timeout == null) {\n            return null;\n        }\n\n        if (timeout instanceof String) {\n            Duration duration = Duration.parse((CharSequence) timeout);\n            return duration.get(ChronoUnit.SECONDS);\n        }\n\n        throw new IllegalArgumentException(\"Invalid process timeout value type: expected an ISO-8601 value, got: \" + timeout);\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/ProcessTimeoutRule.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableProcessTimeoutRule.class)\n@JsonDeserialize(as = ImmutableProcessTimeoutRule.class)\npublic interface ProcessTimeoutRule extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    @Value.Parameter\n    String msg();\n\n    @Value.Parameter\n    String max();\n\n    static ProcessTimeoutRule of(String msg, String max) {\n        return ImmutableProcessTimeoutRule.of(msg, max);\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/ProtectedTasksPolicy.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class ProtectedTasksPolicy {\n\n    private final ProtectedTasksRule rule;\n\n    public ProtectedTasksPolicy(ProtectedTasksRule rule) {\n        this.rule = rule;\n    }\n\n    public boolean isProtected(String taskName) {\n        if (rule == null) {\n            return false;\n        }\n\n        return rule.names().contains(taskName);\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/ProtectedTasksRule.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\nimport java.util.Set;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableProtectedTasksRule.class)\n@JsonDeserialize(as = ImmutableProtectedTasksRule.class)\npublic interface ProtectedTasksRule extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Value.Parameter\n    Set<String> names();\n\n    static ProtectedTasksRule of(Set<String> names) {\n        return ImmutableProtectedTasksRule.of(names);\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/QueueRule.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableQueueRule.class)\n@JsonDeserialize(as = ImmutableQueueRule.class)\npublic interface QueueRule extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    static QueueRule empty() {\n        return QueueRule.of(null, null, null);\n    }\n\n    @Nullable\n    @JsonProperty(\"concurrent\")\n    ConcurrentProcessRule concurrentRule();\n\n    @Nullable\n    @JsonProperty(\"forkDepth\")\n    ForkDepthRule forkDepthRule();\n\n    @Nullable\n    @JsonProperty(\"processTimeout\")\n    ProcessTimeoutRule processTimeoutRule();\n\n    static QueueRule of(ConcurrentProcessRule concurrentRule, ForkDepthRule forkDepthRule, ProcessTimeoutRule processTimeoutRule) {\n        return ImmutableQueueRule.builder()\n                .concurrentRule(concurrentRule)\n                .forkDepthRule(forkDepthRule)\n                .processTimeoutRule(processTimeoutRule)\n                .build();\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/RawPayloadPolicy.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\npublic class RawPayloadPolicy {\n    \n    private final RawPayloadRule rule;\n\n    public RawPayloadPolicy(RawPayloadRule rule) {\n        this.rule = rule;\n    }\n\n    public CheckResult<RawPayloadRule, Long> check(Path p) throws IOException {\n        if (rule == null || !Files.exists(p) || !Files.isRegularFile(p)) {\n            return CheckResult.success();\n        }\n\n        List<CheckResult.Item<RawPayloadRule, Long>> deny = new ArrayList<>();\n\n        long size = Files.size(p);\n\n        if (size > rule.maxSizeInBytes()) {\n            deny.add(new CheckResult.Item<>(rule, size, null));\n        }\n\n        return new CheckResult<>(Collections.emptyList(), deny);\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/RawPayloadRule.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableRawPayloadRule.class)\n@JsonDeserialize(as = ImmutableRawPayloadRule.class)\npublic interface RawPayloadRule extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    @Value.Parameter\n    String msg();\n\n    @Value.Parameter\n    long maxSizeInBytes();\n\n    static RawPayloadRule of(String msg, long size) {\n        return ImmutableRawPayloadRule.of(msg, size);\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/RuntimePolicy.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.time.LocalTime;\nimport java.time.OffsetDateTime;\nimport java.time.ZoneOffset;\nimport java.util.function.Supplier;\n\npublic class RuntimePolicy {\n\n    private final RuntimeRule rule;\n\n    public RuntimePolicy(RuntimeRule rule) {\n        this.rule = rule;\n    }\n\n    public CheckResult<RuntimeRule, String> check(String processRuntime, Supplier<OffsetDateTime> projectCreatedAt) {\n        if (rule == null || processRuntime == null) {\n            return CheckResult.success();\n        }\n\n        if (rule.projectCreatedAfter() != null) {\n            OffsetDateTime createdAt = projectCreatedAt.get();\n            if (createdAt == null) {\n                return CheckResult.success();\n            }\n\n            if (createdAt.isBefore(OffsetDateTime.of(rule.projectCreatedAfter(), LocalTime.of(0, 0, 0), ZoneOffset.UTC))) {\n                return CheckResult.success();\n            }\n        }\n\n        if (!rule.allowedRuntimes().contains(processRuntime)) {\n            return CheckResult.error(new CheckResult.Item<>(rule, processRuntime));\n        }\n\n        return CheckResult.success();\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/RuntimeRule.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.time.LocalDate;\nimport java.util.Set;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableRuntimeRule.class)\n@JsonDeserialize(as = ImmutableRuntimeRule.class)\npublic interface RuntimeRule extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    @Value.Parameter\n    String msg();\n\n    @Value.Parameter\n    @JsonProperty(\"runtimes\")\n    Set<String> allowedRuntimes();\n\n    @Nullable\n    @Value.Parameter\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd\")\n    LocalDate projectCreatedAfter();\n\n    static RuntimeRule of(String msg, Set<String> runtimes, LocalDate projectCreatedAfter) {\n        return ImmutableRuntimeRule.of(msg, runtimes, projectCreatedAfter);\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/StatePolicy.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.nio.file.*;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.BiFunction;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\n\npublic class StatePolicy {\n\n    public static class StateStats {\n\n        private final long size;\n        private final int filesCount;\n\n        public StateStats(long size, int filesCount) {\n            this.size = size;\n            this.filesCount = filesCount;\n        }\n\n        public long getSize() {\n            return size;\n        }\n\n        public int getFilesCount() {\n            return filesCount;\n        }\n    }\n\n    private final PolicyRules<StateRule> rules;\n\n    public StatePolicy(PolicyRules<StateRule> rules) {\n        this.rules = rules;\n    }\n\n    public CheckResult<StateRule, StateStats> check(Supplier<StateStats> statsSupplier) {\n        if (rules == null || rules.isEmpty()) {\n            return CheckResult.success();\n        }\n\n        List<StateRule> warnRules = rules.getWarn().stream().filter(StatePolicy::hasStats).collect(Collectors.toList());\n        List<StateRule> denyRules = rules.getDeny().stream().filter(StatePolicy::hasStats).collect(Collectors.toList());\n        if (warnRules.isEmpty() && denyRules.isEmpty()) {\n            return CheckResult.success();\n        }\n\n        List<CheckResult.Item<StateRule, StateStats>> warn = new ArrayList<>();\n        List<CheckResult.Item<StateRule, StateStats>> deny = new ArrayList<>();\n\n        StateStats stats = statsSupplier.get();\n\n        checkStats(warnRules, stats, warn);\n        checkStats(denyRules, stats, deny);\n\n        return new CheckResult<>(warn, deny);\n    }\n\n    public CheckResult<StateRule, Path> check(Path src, BiFunction<Path, BasicFileAttributes, Boolean> filter) throws IOException {\n        if (rules == null || rules.isEmpty()) {\n            return CheckResult.success();\n        }\n\n        if (Files.notExists(src)) {\n            return CheckResult.success();\n        }\n\n        List<StateRule> warnRules = rules.getWarn().stream().filter(r -> !r.patterns().isEmpty()).collect(Collectors.toList());\n        List<StateRule> denyRules = rules.getDeny().stream().filter(r -> !r.patterns().isEmpty()).collect(Collectors.toList());\n        if (warnRules.isEmpty() && denyRules.isEmpty()) {\n            return CheckResult.success();\n        }\n\n        List<CheckResult.Item<StateRule, Path>> warn = new ArrayList<>();\n        List<CheckResult.Item<StateRule, Path>> deny = new ArrayList<>();\n\n        Files.walkFileTree(src, new SimpleFileVisitor<Path>() {\n            @Override\n            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {\n                if (!filter.apply(file, attrs)) {\n                    return FileVisitResult.CONTINUE;\n                }\n\n                if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) {\n                    return FileVisitResult.CONTINUE;\n                }\n\n                checkPatterns(warnRules, file, warn);\n                checkPatterns(denyRules, file, deny);\n\n                return FileVisitResult.CONTINUE;\n            }\n        });\n\n        return new CheckResult<>(warn, deny);\n    }\n\n    private static void checkStats(List<StateRule> rules, StateStats stats, List<CheckResult.Item<StateRule, StateStats>> result) {\n        for (StateRule r : rules) {\n            if (r.maxFilesCount() != null && stats.getFilesCount() > r.maxFilesCount()) {\n                result.add(new CheckResult.Item<>(r, stats, \"Max files count exceeded. Actual: \" + stats.getFilesCount() + \" limit: \" + r.maxFilesCount()));\n            }\n\n            if (r.maxSizeInBytes() != null && stats.getSize() > r.maxSizeInBytes()) {\n                result.add(new CheckResult.Item<>(r, stats, \"Max state size exceeded. Actual: \" + stats.getSize() + \" limit: \" + r.maxSizeInBytes()));\n            }\n        }\n    }\n\n    private static void checkPatterns(List<StateRule> rules, Path file, List<CheckResult.Item<StateRule, Path>> result) {\n        for (StateRule r : rules) {\n            if (matchPattern(r.patterns(), file.toString())) {\n                result.add(new CheckResult.Item<>(r, file));\n            }\n        }\n    }\n\n    private static boolean hasStats(StateRule rule) {\n        return rule.maxFilesCount() != null || rule.maxSizeInBytes() != null;\n    }\n\n    private static boolean matchPattern(List<String> patterns, String fileName) {\n        return Utils.matchAny(patterns, fileName);\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/StateRule.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableStateRule.class)\n@JsonDeserialize(as = ImmutableStateRule.class)\npublic interface StateRule extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    String msg();\n\n    @Nullable\n    Long maxSizeInBytes();\n\n    @Nullable\n    Integer maxFilesCount();\n\n    @Value.Default\n    default List<String> patterns() {\n        return Collections.emptyList();\n    }\n\n    static ImmutableStateRule.Builder builder() {\n        return ImmutableStateRule.builder();\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/TaskPolicy.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\nimport com.walmartlabs.concord.sdk.Context;\n\nimport java.io.Serializable;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.policyengine.Utils.matches;\n\npublic class TaskPolicy {\n\n    private final PolicyRules<TaskRule> rules;\n\n    public TaskPolicy(PolicyRules<TaskRule> rules) {\n        this.rules = rules;\n    }\n\n    public CheckResult<TaskRule, String> check(String taskName, String methodName, Object[] params, Map<String, List<Serializable>> taskResults) {\n        if (rules == null || rules.isEmpty()) {\n            return CheckResult.success();\n        }\n\n        for (TaskRule r : rules.getAllow()) {\n            if (matchRule(taskName, methodName, params, taskResults, r)) {\n                return CheckResult.success();\n            }\n        }\n\n        for (TaskRule r : rules.getDeny()) {\n            if (matchRule(taskName, methodName, params, taskResults, r)) {\n                return CheckResult.error(new CheckResult.Item<>(r, methodName));\n            }\n        }\n\n        for (TaskRule r : rules.getWarn()) {\n            if (matchRule(taskName, methodName, params, taskResults, r)) {\n                return CheckResult.warn(new CheckResult.Item<>(r, methodName));\n            }\n        }\n\n        return CheckResult.success();\n    }\n\n    public Set<String> getTaskResults() {\n        if (rules == null || rules.isEmpty()) {\n            return Collections.emptySet();\n        }\n\n        Set<String> result = new HashSet<>();\n        rules.getAllow().forEach(r -> collectTaskNames(r.taskResults(), result));\n        rules.getDeny().forEach(r -> collectTaskNames(r.taskResults(), result));\n        rules.getWarn().forEach(r -> collectTaskNames(r.taskResults(), result));\n        return result;\n    }\n\n    private static void collectTaskNames(List<TaskRule.TaskResult> taskResults, Set<String> result) {\n        for (TaskRule.TaskResult tr : taskResults) {\n            result.add(tr.task());\n        }\n    }\n\n    private boolean matchRule(String taskName, String methodName, Object[] params, Map<String, List<Serializable>> taskResults, TaskRule r) {\n        if (!matches(r.taskName(), taskName)) {\n            return false;\n        }\n\n        if (r.method() != null && !matches(r.method(), methodName)) {\n            return false;\n        }\n\n        if (paramsMatches(r.params(), params)) {\n            return true;\n        }\n\n        if (taskResultsMatches(r.taskResults(), taskResults)) {\n            return true;\n        }\n\n        return false;\n    }\n\n    private static boolean paramsMatches(List<TaskRule.Param> r, Object[] params) {\n        if (params == null) {\n            return r.isEmpty();\n        }\n\n        for (TaskRule.Param p : r) {\n            if (p.index() >= params.length) {\n                return false;\n            }\n\n            if (!paramMatches(\n                    Optional.ofNullable(p.name()).map(n -> n.split(\"\\\\.\")).orElse(null),\n                    0,\n                    p.values(), params[p.index()],\n                    p.protectedVariable())) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static boolean paramMatches(String[] names, int nameIndex, List<Object> values, Object param, boolean isProtected) {\n        if (param == null) {\n            return values.contains(null);\n        }\n\n        if (param instanceof Map) {\n            if (names == null) {\n                return false;\n            }\n            Map<String, Object> m = (Map<String, Object>) param;\n            String name = names[nameIndex];\n            nameIndex += 1;\n            return paramMatches(names, nameIndex, values, m.get(name), isProtected);\n        } else if (param instanceof Context) {\n            if (names == null) {\n                return false;\n            }\n            Context ctx = (Context) param;\n            String name = names[nameIndex];\n            nameIndex += 1;\n            Object v = isProtected ? ctx.getProtectedVariable(name) : ctx.getVariable(name);\n            return paramMatches(names, nameIndex, values, v, isProtected);\n        } else if (param instanceof Variables) {\n            Variables vars = (Variables) param;\n            String name = names[nameIndex];\n            nameIndex += 1;\n            Object v = vars.get(name);\n            return paramMatches(names, nameIndex, values, v, isProtected);\n        } else if (param instanceof com.walmartlabs.concord.runtime.v2.sdk.Context) {\n            com.walmartlabs.concord.runtime.v2.sdk.Context ctx = (com.walmartlabs.concord.runtime.v2.sdk.Context) param;\n            String name = names[nameIndex];\n            nameIndex += 1;\n            Object v = ctx.variables().get(name);\n            return paramMatches(names, nameIndex, values, v, isProtected);\n        } else if (param instanceof String) {\n            return Utils.matchAny(values.stream().map(Object::toString).collect(Collectors.toList()), param.toString());\n        } else {\n            for (Object v : values) {\n                if (v != null && v.equals(param)) {\n                    return true;\n                }\n            }\n        }\n\n        return false;\n    }\n\n    private static boolean taskResultsMatches(List<TaskRule.TaskResult> rule, Map<String, List<Serializable>> taskResults) {\n        if (rule.isEmpty() || taskResults == null) {\n            return false;\n        }\n\n        for (TaskRule.TaskResult tr : rule) {\n            String taskName = tr.task();\n            List<Serializable> results = taskResults.getOrDefault(taskName, Collections.emptyList());\n            String resultName = tr.result();\n\n            for (Object result : results) {\n                if (paramMatches(\n                        Optional.ofNullable(resultName).map(n -> n.split(\"\\\\.\")).orElse(null),\n                        0, tr.values(), result, false)) {\n                    return true;\n                }\n            }\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/TaskRule.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonAlias;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.common.AllowNulls;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableTaskRule.class)\n@JsonDeserialize(as = ImmutableTaskRule.class)\npublic interface TaskRule extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    String msg();\n\n    @Nullable\n    @JsonProperty(\"name\")\n    @JsonAlias(\"taskName\")\n    String taskName();\n\n    @Nullable\n    String method();\n\n    @Value.Default\n    default List<Param> params() {\n        return Collections.emptyList();\n    }\n\n    @Value.Default\n    default List<TaskResult> taskResults() {\n        return Collections.emptyList();\n    }\n\n    static ImmutableTaskRule.Builder builder() {\n        return ImmutableTaskRule.builder();\n    }\n\n    @Value.Immutable\n    @JsonInclude(JsonInclude.Include.NON_EMPTY)\n    @JsonSerialize(as = ImmutableParam.class)\n    @JsonDeserialize(as = ImmutableParam.class)\n    interface Param extends Serializable {\n\n        long serialVersionUID = 1L;\n\n        int index();\n\n        @Nullable\n        String name();\n\n        @JsonProperty(\"protected\")\n        boolean protectedVariable();\n\n        @Value.Default\n        @AllowNulls\n        default List<Object> values() {\n            return Collections.emptyList();\n        }\n\n        static ImmutableParam.Builder builder() {\n            return ImmutableParam.builder();\n        }\n    }\n\n    @Value.Immutable\n    @JsonInclude(JsonInclude.Include.NON_EMPTY)\n    @JsonSerialize(as = ImmutableTaskResult.class)\n    @JsonDeserialize(as = ImmutableTaskResult.class)\n    interface TaskResult extends Serializable {\n\n        long serialVersionUID = 1L;\n\n        String task();\n\n        @Nullable\n        String result();\n\n        @Value.Default\n        @AllowNulls\n        default List<Object> values() {\n            return Collections.emptyList();\n        }\n\n        static ImmutableTaskResult.Builder builder() {\n            return ImmutableTaskResult.builder();\n        }\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/Utils.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.Matcher;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Pattern;\n\npublic final class Utils {\n\n    private static final String FILE_SIZE_UNITS = \"KMGTPE\";\n\n    private static final Pattern FILE_SIZE_PATTERN = Pattern.compile(\"([\\\\d.]+)(.*)\");\n\n    public static boolean matchAny(List<String> patterns, String value) {\n        return Matcher.matchAny(patterns, value);\n    }\n\n    public static boolean matches(String pattern, String value) {\n        return Matcher.matches(value, pattern);\n    }\n\n    public static boolean matches(Map<String, Object> conditions, Map<String, Object> data) {\n        return Matcher.matches(data, conditions);\n    }\n\n    public static Long parseFileSize(String v) {\n        if (v == null) {\n            return null;\n        }\n\n        v = deleteAllWhitespace(v);\n\n        java.util.regex.Matcher m = FILE_SIZE_PATTERN.matcher(v.toUpperCase());\n        if (!m.matches()) {\n            throw new NumberFormatException(\"invalid file size format: '\" + v + \"'\");\n        }\n\n        long number = Long.parseLong(m.group(1));\n        String unit = m.group(2);\n\n        String identifier = unit.substring(0, 1);\n        int index = FILE_SIZE_UNITS.indexOf(identifier);\n\n        if( index != -1) {\n            for (int i = 0; i <= index; i++) {\n                number = number * 1024;\n            }\n        }\n        return number;\n    }\n\n    private static String deleteAllWhitespace(String str) {\n        if (str == null || str.trim().isEmpty()) {\n            return str;\n        }\n\n        char[] ch = new char[str.length()];\n        int count = 0;\n        for (int i = 0; i < str.length(); i++) {\n            if (!Character.isWhitespace(str.charAt(i))) {\n                ch[count++] = str.charAt(i);\n            }\n        }\n        if (count == str.length()) {\n            return str;\n        }\n        return new String(ch, 0, count);\n    }\n\n    private Utils() {\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/WorkspacePolicy.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.nio.file.FileVisitResult;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.SimpleFileVisitor;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Set;\n\npublic class WorkspacePolicy {\n\n    private final WorkspaceRule rule;\n\n    public WorkspacePolicy(WorkspaceRule rule) {\n        this.rule = rule;\n    }\n\n    public CheckResult<WorkspaceRule, Path> check(Path p) throws IOException {\n        if (rule == null) {\n            return CheckResult.success();\n        }\n\n        List<CheckResult.Item<WorkspaceRule, Path>> deny = new ArrayList<>();\n\n        if (!Files.exists(p)) {\n            deny.add(new CheckResult.Item<>(rule, p, \"File not found: \" + p));\n        } else if (!Files.isDirectory(p)) {\n            deny.add(new CheckResult.Item<>(rule, p, \"Not a directory: \" + p));\n        } else if (rule.maxSizeInBytes() != null) {\n            Long[] size = { 0L };\n\n            Files.walkFileTree(p, new SimpleFileVisitor<Path>() {\n\n                @Override\n                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {\n                    if (isIgnored(file, rule.ignoredFiles())) {\n                        return FileVisitResult.CONTINUE;\n                    }\n\n                    size[0] += Files.size(file);\n\n                    return FileVisitResult.CONTINUE;\n                }\n            });\n\n            if (size[0] > rule.maxSizeInBytes()) {\n                deny.add(new CheckResult.Item<>(rule, p, \"Workspace too big: \" + size[0] + \" byte(s)\"));\n            }\n        }\n\n        return new CheckResult<>(Collections.emptyList(), deny);\n    }\n\n    private static boolean isIgnored(Path p, Set<String> patterns) {\n        if (patterns == null) {\n            return false;\n        }\n\n        for (String s : patterns) {\n            if (p.toString().matches(s)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/WorkspaceRule.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.Set;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableWorkspaceRule.class)\n@JsonDeserialize(as = ImmutableWorkspaceRule.class)\npublic interface WorkspaceRule extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    String msg();\n\n    @Nullable\n    Long maxSizeInBytes();\n\n    @Value.Default\n    default Set<String> ignoredFiles() {\n        return Collections.emptySet();\n    }\n\n    static WorkspaceRule of(String msg, Long maxSizeInBytes, Set<String> ignoredFiles) {\n        return ImmutableWorkspaceRule.builder()\n                .msg(msg)\n                .maxSizeInBytes(maxSizeInBytes)\n                .ignoredFiles(ignoredFiles != null ? ignoredFiles : Collections.emptySet())\n                .build();\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/main/java/com/walmartlabs/concord/policyengine/package-info.java",
    "content": "@Value.Style(jdkOnly = true)\npackage com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n"
  },
  {
    "path": "policy-engine/src/test/java/com/walmartlabs/concord/policyengine/ContainerPolicyTest.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class ContainerPolicyTest {\n\n    @Test\n    public void testCpu() {\n        ContainerPolicy oneCpu = new ContainerPolicy(ContainerRule.of(\"1 CPU\", null, 1));\n        ContainerPolicy twoCpu = new ContainerPolicy(ContainerRule.of(\"1 CPU\", null, 2));\n\n        Map<String, Object> containerParams = new HashMap<>();\n        containerParams.put(\"cpu\", 2);\n\n        // ---\n\n        assertDeny(oneCpu, containerParams);\n        assertAllow(twoCpu, containerParams);\n    }\n\n    @Test\n    public void testRam() {\n        ContainerPolicy ram1 = new ContainerPolicy(ContainerRule.of(\"128 RAM\", \"128m\", null));\n        ContainerPolicy ram2 = new ContainerPolicy(ContainerRule.of(\"256 RAM\", \"256m\", null));\n\n        Map<String, Object> containerParams = new HashMap<>();\n        containerParams.put(\"ram\", \"256m\");\n\n        // ---\n\n        assertDeny(ram1, containerParams);\n        assertAllow(ram2, containerParams);\n    }\n\n    private static void assertAllow(ContainerPolicy policy, Map<String, Object> p) {\n        CheckResult<ContainerRule, Object> result = policy.check(p);\n        assertTrue(result.getDeny().isEmpty());\n    }\n\n    private static void assertDeny(ContainerPolicy policy, Map<String, Object> p) {\n        CheckResult<ContainerRule, Object> result = policy.check(p);\n        assertFalse(result.getDeny().isEmpty());\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/test/java/com/walmartlabs/concord/policyengine/DependencyPolicyTest.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.dependencymanager.DependencyEntity;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.file.Paths;\nimport java.util.Collections;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class DependencyPolicyTest {\n\n    @Test\n    public void testSingleDepDeny() {\n        DependencyRule di = DependencyRule.builder()\n                .groupId(\".*\")\n                .build();\n\n        PolicyRules<DependencyRule> rules = new PolicyRules<>(null, null, Collections.singletonList(di));\n\n        DependencyPolicy policy = new DependencyPolicy(rules);\n\n        DependencyEntity dependency = buildDependency(\"com.walmartlabs.concord.plugins.basic\",\n                \"ansible-tasks\",\n                \"0.57.1-SNAPSHOT\");\n\n        // ---\n        assertDeny(policy, dependency);\n    }\n\n    @Test\n    public void testSingleDepAllow() {\n        DependencyRule di = DependencyRule.builder()\n                .groupId(\"com.walmartlabs.concord.plugins.basic\")\n                .build();\n\n        PolicyRules<DependencyRule> rules = new PolicyRules<>(Collections.singletonList(di), null, null);\n\n        DependencyPolicy policy = new DependencyPolicy(rules);\n\n        DependencyEntity dependency = buildDependency(\"com.walmartlabs.concord.plugins.basic\",\n                \"ansible-tasks\",\n                \"0.57.1-SNAPSHOT\");\n\n        // ---\n\n        assertAllow(policy, dependency);\n    }\n\n    @Test\n    public void testSingleDepAllowVersion() {\n        DependencyRule allow = DependencyRule.builder()\n                .groupId(\"com.walmartlabs.concord.plugins.basic\")\n                .fromVersion(\"0.5.0\")\n                .toVersion(\"1.0.0\")\n                .build();\n        DependencyRule deny = DependencyRule.builder()\n                .groupId(\"com.walmartlabs.concord.plugins.basic\")\n                .build();\n\n        PolicyRules<DependencyRule> rules = new PolicyRules<>(Collections.singletonList(allow), null, Collections.singletonList(deny));\n\n        DependencyPolicy policy = new DependencyPolicy(rules);\n\n        // ---\n        DependencyEntity dep1 = buildDependency(\"com.walmartlabs.concord.plugins.basic\",\n                \"ansible-tasks\",\n                \"0.57.1-SNAPSHOT\");\n\n        assertAllow(policy, dep1);\n\n        // ---\n        DependencyEntity dep2 = buildDependency(\"com.walmartlabs.concord.plugins.basic\",\n                \"ansible-tasks\",\n                \"1.1.1-SNAPSHOT\");\n\n        assertDeny(policy, dep2);\n\n        // ---\n        DependencyEntity dep3 = buildDependency(\"com.walmartlabs.concord.plugins.basic\",\n                \"ansible-tasks\",\n                \"0.4.9\");\n\n        assertDeny(policy, dep3);\n\n        // ---\n        DependencyEntity dep4 = buildDependency(\"com.walmartlabs.concord.plugins.basic\",\n                \"ansible-tasks\",\n                \"1.0.1\");\n\n        assertDeny(policy, dep4);\n    }\n\n    private static void assertDeny(DependencyPolicy policy, DependencyEntity entity) {\n        CheckResult<DependencyRule, DependencyEntity> result = policy.check(Collections.singletonList(entity));\n        assertFalse(result.getDeny().isEmpty());\n    }\n\n    private static void assertAllow(DependencyPolicy policy, DependencyEntity entity) {\n        CheckResult<DependencyRule, DependencyEntity> result = policy.check(Collections.singletonList(entity));\n        assertTrue(result.getDeny().isEmpty());\n    }\n\n    private static DependencyEntity buildDependency(String groupId, String artifactId, String version) {\n        return new DependencyEntity(Paths.get(\"/whatever\"), groupId, artifactId, version);\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/test/java/com/walmartlabs/concord/policyengine/DependencyRewritePolicyTest.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport java.net.URI;\nimport java.util.Collection;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class DependencyRewritePolicyTest {\n\n    @Test\n    public void testRewriteWithMultiValues() throws Exception {\n        DependencyRewriteRule r1 = DependencyRewriteRule.builder()\n                .msg(\"msg\")\n                .groupId(\"com.walmartlabs.concord.plugins\")\n                .artifactId(\"git\")\n                .values(List.of(\n                        new URI(\"mvn://com.walmartlabs.concord.plugins:git:2.3.1\"),\n                        new URI(\"mvn://com.google.code.gson:gson:2.8.7\")))\n                .build();\n\n        DependencyRewritePolicy policy = new DependencyRewritePolicy(List.of(r1));\n\n        List<URI> dependencies = List.of(new URI(\"mvn://com.walmartlabs.concord.plugins:git:1.0.1\"));\n\n        List<URI> result = (List<URI>) policy.rewrite(dependencies, (msg, from, to) -> {});\n\n        assertEquals(2, result.size());\n        assertEquals(new URI(\"mvn://com.walmartlabs.concord.plugins:git:2.3.1\"), result.get(0));\n        assertEquals(new URI(\"mvn://com.google.code.gson:gson:2.8.7\"), result.get(1));\n    }\n\n    @Test\n    public void testRewriteNotMatch() throws Exception {\n        DependencyRewriteRule r1 = DependencyRewriteRule.builder()\n                .msg(\"msg\")\n                .groupId(\"com.walmartlabs.concord.plugins\")\n                .artifactId(\"git\")\n                .values(List.of(\n                        new URI(\"mvn://com.walmartlabs.concord.plugins:git:2.3.1\"),\n                        new URI(\"mvn://com.google.code.gson:gson:2.8.7\")))\n                .build();\n\n        DependencyRewritePolicy policy = new DependencyRewritePolicy(List.of(r1));\n\n        List<URI> dependencies = List.of(new URI(\"mvn://my.plugin:my:1.0.1\"));\n\n        List<URI> result = (List<URI>) policy.rewrite(dependencies, (msg, from, to) -> {});\n        assertEquals(1, result.size());\n        assertEquals(dependencies.get(0), result.get(0));\n    }\n\n}\n"
  },
  {
    "path": "policy-engine/src/test/java/com/walmartlabs/concord/policyengine/EffectiveYamlPolicyTest.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nclass EffectiveYamlPolicyTest {\n\n    @Test\n    void testDefault() {\n        var rule = ImmutableEffectiveYamlRule.builder().build();\n        var policy = new EffectiveYamlPolicy(rule);\n\n        assertTrue(policy.renderEffectiveYaml());\n        assertFalse(policy.isTooLarge(Long.MAX_VALUE));\n    }\n\n    @Test\n    void testDisabled() {\n        var rule = ImmutableEffectiveYamlRule.builder()\n                .renderEffectiveYaml(false)\n                .build();\n        var policy = new EffectiveYamlPolicy(rule);\n\n        assertFalse(policy.renderEffectiveYaml());\n        assertFalse(policy.isTooLarge(Long.MAX_VALUE));\n    }\n\n    @Test\n    void testTooLarge() {\n        var rule = ImmutableEffectiveYamlRule.builder()\n                .maxSizeInBytes(100L)\n                .build();\n        var policy = new EffectiveYamlPolicy(rule);\n\n        assertTrue(policy.renderEffectiveYaml());\n        assertTrue(policy.isTooLarge(Long.MAX_VALUE));\n    }\n\n    @Test\n    void testUnderLimit() {\n        var rule = ImmutableEffectiveYamlRule.builder()\n                .maxSizeInBytes(100L)\n                .build();\n        var policy = new EffectiveYamlPolicy(rule);\n\n        assertTrue(policy.renderEffectiveYaml());\n        assertFalse(policy.isTooLarge(99));\n        assertTrue(policy.isTooLarge(Long.MAX_VALUE));\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/test/java/com/walmartlabs/concord/policyengine/PolicyEngineRulesTest.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.InputStream;\nimport java.net.URI;\nimport java.util.*;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class PolicyEngineRulesTest {\n\n    private final ObjectMapper om = new ObjectMapper();\n\n    @Test\n    public void serializationTest() throws Exception {\n        DependencyRule r1 = DependencyRule.builder()\n                .msg(\"msg1\")\n                .scheme(\"schema1\")\n                .groupId(\"groupId1\")\n                .artifactId(\"artifactId1\")\n                .fromVersion(\"fromVersion1\")\n                .toVersion(\"toVersion1\")\n                .build();\n\n        DependencyRule r2 = DependencyRule.builder()\n                .msg(\"msg2\")\n                .scheme(\"schema2\")\n                .groupId(\"groupId2\")\n                .artifactId(\"artifactId2\")\n                .fromVersion(\"fromVersion2\")\n                .toVersion(\"toVersion2\")\n                .build();\n\n        DependencyRule r3 = DependencyRule.builder()\n                .msg(\"msg3\")\n                .scheme(\"schema3\")\n                .groupId(\"groupId3\")\n                .artifactId(\"artifactId3\")\n                .fromVersion(\"fromVersion3\")\n                .toVersion(\"toVersion3\")\n                .build();\n        PolicyRules<DependencyRule> dependencyRules = new PolicyRules<>(Collections.singletonList(r1), Collections.singletonList(r2), Collections.singletonList(r3));\n\n        FileRule f1 = new FileRule(\"msg1\", \"1K\", \"FILE\", Collections.singletonList(\"name1\"));\n        FileRule f2 = new FileRule(\"msg2\", \"2K\", \"DIR\", Collections.singletonList(\"name2\"));\n        FileRule f3 = new FileRule(\"msg3\", \"3K\", \"FILE\", Collections.singletonList(\"name3\"));\n        PolicyRules<FileRule> fileRules = new PolicyRules<>(Collections.singletonList(f1), Collections.singletonList(f2), Collections.singletonList(f3));\n\n        TaskRule t1 = TaskRule.builder()\n                .msg(\"msg1\")\n                .taskName(\"taskName1\")\n                .method(\"methodName1\")\n                .addParams()\n                .build()\n                .withParams(\n                        TaskRule.Param.builder()\n                                .index(1)\n                                .name(\"name1\")\n                                .protectedVariable(true)\n                                .values(Collections.singletonList(\"values\"))\n                                .build())\n                .withTaskResults(\n                        TaskRule.TaskResult.builder()\n                                .task(\"task1\")\n                                .result(\"result1\")\n                                .build());\n\n        TaskRule t2 = TaskRule.builder()\n                .msg(\"msg2\")\n                .taskName(\"taskName2\")\n                .method(\"methodName2\")\n                .addParams()\n                .build()\n                .withParams(\n                        TaskRule.Param.builder()\n                                .index(2)\n                                .name(\"name2\")\n                                .protectedVariable(true)\n                                .values(Collections.singletonList(\"values\"))\n                                .build());\n\n        TaskRule t3 = TaskRule.builder()\n                .msg(\"msg3\")\n                .taskName(\"taskName3\")\n                .method(\"methodName3\")\n                .addParams()\n                .build()\n                .withParams(\n                        TaskRule.Param.builder()\n                                .index(3)\n                                .name(\"name3\")\n                                .protectedVariable(true)\n                                .values(Collections.singletonList(\"values\"))\n                                .build());\n\n        PolicyRules<TaskRule> taskRules = new PolicyRules<>(Collections.singletonList(t1), Collections.singletonList(t2), Collections.singletonList(t3));\n\n        WorkspaceRule workspaceRule = WorkspaceRule.of(\"msg\", 12345L, Collections.singleton(\"a\"));\n\n        ContainerRule containerRules = ContainerRule.of(\"msg1\", \"maxRam\", 2);\n\n        ConcurrentProcessRule concurrent = ConcurrentProcessRule.builder()\n                .msg(\"nsg1\")\n                .maxPerProject(23)\n                .maxPerOrg(433)\n                .build();\n        ForkDepthRule forkDepthRule = ForkDepthRule.of(\"msg1\", 12);\n        ProcessTimeoutRule processTimeoutRule = ProcessTimeoutRule.of(\"msg1\", \"13\");\n        QueueRule queueRules = QueueRule.of(concurrent, forkDepthRule, processTimeoutRule);\n\n        ProtectedTasksRule protectedTasksRules = ProtectedTasksRule.of(Collections.singleton(\"task1\"));\n\n        EntityRule e1 = EntityRule.builder()\n                .msg(\"msg1\")\n                .entity(\"entity1\")\n                .action(\"action1\")\n                .conditions(Collections.singletonMap(\"k1\", \"v\"))\n                .build();\n        EntityRule e2 = EntityRule.builder()\n                .msg(\"msg2\")\n                .entity(\"entity2\")\n                .action(\"action2\")\n                .conditions(Collections.singletonMap(\"k2\", \"v\"))\n                .build();\n        EntityRule e3 = EntityRule.builder()\n                .msg(\"msg3\")\n                .entity(\"entity3\")\n                .action(\"action3\")\n                .conditions(Collections.singletonMap(\"k3\", \"v\"))\n                .build();\n        PolicyRules<EntityRule> entityRules = new PolicyRules<>(Collections.singletonList(e1), Collections.singletonList(e2), Collections.singletonList(e3));\n\n        Map<String, Object> processCfg = new HashMap<>();\n        processCfg.put(\"a\", \"b\");\n\n        JsonStoreRule.StoreRule storageRule = JsonStoreRule.StoreRule.of(\"storage-rule\", 123);\n        JsonStoreRule.StoreDataRule storageDataRule = JsonStoreRule.StoreDataRule.of(\"storage-data-rule\", 11L);\n\n        Map<String, Object> defaultProcessCfg = new HashMap<>();\n        defaultProcessCfg.put(\"x\", \"y\");\n\n        List<DependencyVersionsPolicy.Dependency> dependencies = new ArrayList<>();\n        dependencies.add(new DependencyVersionsPolicy.Dependency(\"foo\", \"v1\"));\n\n        AttachmentsRule attachmentsRule = AttachmentsRule.of(\"msg\", 12345L);\n\n        RawPayloadRule rawPayloadRule = RawPayloadRule.of(\"msg\", 12345L);\n        \n        StateRule s1 = StateRule.builder()\n                .msg(\"msg1\")\n                .maxSizeInBytes(123L)\n                .maxFilesCount(456)\n                .patterns(Collections.singletonList(\"a\"))\n                .build();\n        StateRule s2 = StateRule.builder()\n                .maxSizeInBytes(321L)\n                .build();\n        PolicyRules<StateRule> stateRules = new PolicyRules<>(null, Collections.singletonList(s1), Collections.singletonList(s2));\n\n        List<DependencyRewriteRule> depRewriteRules = Collections.singletonList(\n                DependencyRewriteRule.builder()\n                        .msg(\"msg\")\n                        .groupId(\"group\")\n                        .artifactId(\"artifact\")\n                        .fromVersion(\"fromVersion\")\n                        .toVersion(\"versionTo\")\n                        .value(new URI(\"value\"))\n                        .build());\n        RuntimeRule runtimeRule = RuntimeRule.of(null, Collections.singleton(\"concord-v2\"), null);\n\n        PolicyEngineRules r = PolicyEngineRules.builder()\n                .dependencyRules(dependencyRules)\n                .dependencyRewriteRules(depRewriteRules)\n                .fileRules(fileRules)\n                .taskRules(taskRules)\n                .workspaceRule(workspaceRule)\n                .containerRules(containerRules)\n                .queueRules(queueRules)\n                .protectedTasksRules(protectedTasksRules)\n                .entityRules(entityRules)\n                .processCfg(processCfg)\n                .jsonStoreRule(JsonStoreRule.of(storageRule, storageDataRule))\n                .defaultProcessCfg(defaultProcessCfg)\n                .dependencyVersions(dependencies)\n                .attachmentsRule(attachmentsRule)\n                .stateRules(stateRules)\n                .rawPayloadRule(rawPayloadRule)\n                .runtimeRule(runtimeRule)\n                .putCustomRule(\"ansible\", Collections.singletonMap(\"k\", \"v\"))\n                .cronTriggerRule(CronTriggerRule.of(\"msg\", 1000))\n                .kvRule(KvRule.of(\"msg\", 123))\n                .build();\n\n        String s = om.writeValueAsString(r);\n        PolicyEngineRules rr = om.readValue(s, PolicyEngineRules.class);\n        assertEquals(r, rr);\n    }\n\n    @Test\n    public void deserializeTest1() throws Exception {\n        PolicyEngineRules r = om.readValue(resource(\"policy1.json\"), PolicyEngineRules.class);\n\n        assertNotNull(r.queueRules());\n        assertEquals(5, r.queueRules().forkDepthRule().max());\n        assertEquals(Collections.singletonMap(\"__currentFlow\", \"n/a\"), r.processCfg().get(\"meta\"));\n        assertEquals(Collections.singleton(\"gatekeeper\"), r.protectedTasksRules().names());\n        assertEquals(3, r.dependencyRewriteRules().size());\n\n        DependencyRewriteRule rw = r.dependencyRewriteRules().get(0);\n        assertEquals(\"msg1\", rw.msg());\n        assertEquals(\"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:1.71.0\", rw.value().toString());\n        assertEquals(\"com.walmartlabs.concord.plugins.basic\", rw.groupId());\n        assertEquals(\"ansible-tasks\", rw.artifactId());\n        assertEquals(\"1.62.0\", rw.fromVersion());\n        assertEquals(\"1.70.3\", rw.toVersion());\n\n        rw = r.dependencyRewriteRules().get(1);\n        assertEquals(\"msg2\", rw.msg());\n        assertEquals(\"mvn://com.walmartlabs.concord.plugins:git:1.33.0\", rw.value().toString());\n        assertEquals(\"com.walmartlabs.concord.plugins\", rw.groupId());\n        assertEquals(\"git\", rw.artifactId());\n        assertEquals(\"1.25.0\", rw.fromVersion());\n        assertEquals(\"1.32.3\", rw.toVersion());\n\n        rw = r.dependencyRewriteRules().get(2);\n        assertEquals(\"msg3\", rw.msg());\n        assertEquals(2, rw.values().size());\n    }\n\n    @Test\n    public void deserializeTest2() throws Exception {\n        PolicyEngineRules r = om.readValue(resource(\"policy2.json\"), PolicyEngineRules.class);\n\n        assertEquals(\"PT2H\", r.queueRules().processTimeoutRule().max());\n        assertEquals(\"PT1H\", r.processCfg().get(\"processTimeout\"));\n        assertEquals(Collections.singletonMap(\"agent\", Collections.singletonMap(\"flavor\", \"xxx-tunr\")), r.processCfg().get(\"requirements\"));\n    }\n\n    @Test\n    public void deserializeTest3() throws Exception {\n        PolicyEngineRules r = om.readValue(resource(\"policy3.json\"), PolicyEngineRules.class);\n\n        assertEquals(20, r.queueRules().concurrentRule().maxPerProject());\n    }\n\n    @Test\n    @SuppressWarnings(\"unchecked\")\n    public void deserializeTest4() throws Exception {\n        PolicyEngineRules r = om.readValue(resource(\"policy4.json\"), PolicyEngineRules.class);\n\n        assertEquals(1, r.taskRules().getDeny().size());\n\n        TaskRule tr = r.taskRules().getDeny().get(0);\n        assertEquals(\"msg1\", tr.msg());\n        assertEquals(\"execute\", tr.method());\n        assertEquals(\"ansible.*\", tr.taskName());\n        assertTrue(tr.taskResults().isEmpty());\n        assertEquals(1, tr.params().size());\n\n        TaskRule.Param p = tr.params().get(0);\n        assertEquals(\"gatekeeperResult\", p.name());\n        assertEquals(0, p.index());\n        assertTrue(p.protectedVariable());\n        assertEquals(2, p.values().size());\n        assertEquals(Arrays.asList(false, null), p.values());\n\n        assertEquals(0, r.taskRules().getAllow().size());\n        assertEquals(0, r.taskRules().getWarn().size());\n\n        assertEquals(Collections.singleton(\"gatekeeper\"), r.protectedTasksRules().names());\n\n        assertNotNull(r.customRule().get(\"ansible\"));\n\n        Map<String, Object> ansible = (Map<String, Object>) r.customRule().get(\"ansible\");\n        assertEquals(3, ansible.size());\n    }\n\n    @Test\n    public void deserializeTest5() throws Exception {\n        PolicyEngineRules r = om.readValue(resource(\"policy5.json\"), PolicyEngineRules.class);\n\n        assertEquals(4, r.entityRules().getDeny().size());\n\n        EntityRule er = r.entityRules().getDeny().get(0);\n        assertEquals(\"New project creation is disabled in this organization by the environment's policy\", er.msg());\n        assertEquals(\"create\", er.action());\n        assertEquals(\"project\", er.entity());\n        assertEquals(1, er.conditions().size());\n        assertEquals(Collections.singletonMap(\"orgId\", \".*\"), er.conditions().get(\"entity\"));\n\n        er = r.entityRules().getDeny().get(1);\n        assertEquals(\"Subscribing to all GitHub repository notifications is not allowed\", er.msg());\n        assertEquals(\"create\", er.action());\n        assertEquals(\"trigger\", er.entity());\n        assertEquals(1, er.conditions().size());\n        Map<String, Object> params = new HashMap<>();\n        params.put(\"org\", \"\\\\.\\\\*\");\n        params.put(\"project\", \"\\\\.\\\\*\");\n        params.put(\"repository\", \"\\\\.\\\\*\");\n        Map<String, Object> conditions = new HashMap<>();\n        conditions.put(\"params\", params);\n        conditions.put(\"eventSource\", \"github\");\n        assertEquals(conditions, er.conditions().get(\"entity\"));\n    }\n\n    @Test\n    public void deserializeTest6() throws Exception {\n        PolicyEngineRules r = om.readValue(resource(\"policy6.json\"), PolicyEngineRules.class);\n\n        assertEquals(2, ((Map<?,?>)r.defaultProcessCfg().get(\"defaultTaskVariables\")).size());\n    }\n\n    private static InputStream resource(String name) {\n        InputStream r = PolicyEngineRulesTest.class.getResourceAsStream(name);\n        assertNotNull(r);\n        return r;\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/test/java/com/walmartlabs/concord/policyengine/QueueRuleTest.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.DeserializationFeature;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class QueueRuleTest {\n\n    private static final ObjectMapper objectMapper = createObjectMapper();\n\n    private static ObjectMapper createObjectMapper() {\n        ObjectMapper om = new ObjectMapper();\n        om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);\n        return om;\n    }\n\n    @Test\n    public void testDeserialize() {\n        Map<String, Object> process = new HashMap<>();\n        process.put(\"msg\", \"Process message\");\n        process.put(\"RUNNING\", 1);\n        process.put(\"FAILED\", 2);\n\n        Map<String, Object> processPerOrg = new HashMap<>();\n        processPerOrg.put(\"msg\", \"Process per org message\");\n        processPerOrg.put(\"RUNNING2\", 2);\n\n        Map<String, Object> processPerProject = new HashMap<>();\n        processPerProject.put(\"msg\", \"Process per project message\");\n        processPerProject.put(\"RUNNING3\", 3);\n\n        Map<String, Object> concurrent = new HashMap<>();\n        concurrent.put(\"msg\", \"Concurrent message\");\n        concurrent.put(\"maxPerOrg\", 4);\n        concurrent.put(\"maxPerProject\", 5);\n\n        Map<String, Object> rules = new HashMap<>();\n        rules.put(\"concurrent\", concurrent);\n        rules.put(\"process\", process);\n        rules.put(\"processPerOrg\", processPerOrg);\n        rules.put(\"processPerProject\", processPerProject);\n\n        QueueRule r = objectMapper.convertValue(rules, QueueRule.class);\n\n        assertNotNull(r.concurrentRule());\n        assertEquals(4, (int) r.concurrentRule().maxPerOrg());\n        assertEquals(5, (int) r.concurrentRule().maxPerProject());\n        assertEquals(\"Concurrent message\", r.concurrentRule().msg());\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/test/java/com/walmartlabs/concord/policyengine/RawPayloadPolicyTest.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class RawPayloadPolicyTest {\n\n    @Test\n    public void testMaxSize() throws Exception {\n        Path p = Files.createTempDirectory(\"test1\");\n        Files.write(p.resolve(\"test.bin\"), new byte[]{0, 1, 2, 3, 4, 5}, StandardOpenOption.CREATE_NEW);\n\n        RawPayloadPolicy fiveBytes = new RawPayloadPolicy(RawPayloadRule.of(\"5 bytes\", 5L));\n        RawPayloadPolicy tenBytes = new RawPayloadPolicy(RawPayloadRule.of(\"10 bytes\", 10L));\n\n        // ---\n\n        assertDeny(fiveBytes, p.resolve(\"test.bin\"));\n        assertAllow(tenBytes, p.resolve(\"test.bin\"));\n    }\n\n    private static void assertAllow(RawPayloadPolicy policy, Path p) throws IOException {\n        CheckResult<RawPayloadRule, Long> result = policy.check(p);\n        assertTrue(result.getDeny().isEmpty());\n    }\n\n    private static void assertDeny(RawPayloadPolicy policy, Path p) throws IOException {\n        CheckResult<RawPayloadRule, Long> result = policy.check(p);\n        assertFalse(result.getDeny().isEmpty());\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/test/java/com/walmartlabs/concord/policyengine/TaskPolicyTest.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport java.io.Serializable;\nimport java.util.*;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class TaskPolicyTest {\n\n    @Test\n    public void testDenyByTaskName() {\n        TaskRule r = TaskRule.builder()\n                .taskName(\"taskName-.*\")\n                .build();\n\n        PolicyRules<TaskRule> rules = new PolicyRules<>(null, null, Collections.singletonList(r));\n\n        TaskPolicy policy = new TaskPolicy(rules);\n\n        // ---\n        assertDeny(policy, \"taskName-1234\", \"foo\");\n    }\n\n    @Test\n    public void testAllowByTaskName() {\n        TaskRule allowRule = TaskRule.builder()\n                .taskName(\"taskName-1234\")\n                .build();\n\n        TaskRule denyRule = TaskRule.builder()\n                .taskName(\".*\")\n                .build();\n\n        PolicyRules<TaskRule> rules = new PolicyRules<>(Collections.singletonList(allowRule), null, Collections.singletonList(denyRule));\n\n        TaskPolicy policy = new TaskPolicy(rules);\n\n        // ---\n        assertAllow(policy, \"taskName-1234\", \"foo\");\n    }\n\n    @Test\n    public void testDenyByMethodName() {\n        TaskRule r = TaskRule.builder()\n                .taskName(\"taskName-.*\")\n                .method(\"foo\")\n                .build();\n\n        PolicyRules<TaskRule> rules = new PolicyRules<>(null, null, Collections.singletonList(r));\n\n        TaskPolicy policy = new TaskPolicy(rules);\n\n        // ---\n        assertDeny(policy, \"taskName-1234\", \"foo\");\n    }\n\n    @Test\n    public void testAllowByMethodName() {\n        TaskRule allowRule = TaskRule.builder()\n                .taskName(\"taskName-1234\")\n                .method(\"foo\")\n                .build();\n\n        TaskRule denyRule = TaskRule.builder()\n                .taskName(\".*\")\n                .method(\".*\")\n                .build();\n\n        PolicyRules<TaskRule> rules = new PolicyRules<>(Collections.singletonList(allowRule), null, Collections.singletonList(denyRule));\n\n        TaskPolicy policy = new TaskPolicy(rules);\n\n        // ---\n        assertAllow(policy, \"taskName-1234\", \"foo\");\n    }\n\n    @Test\n    public void testDenyByStringParams() {\n        TaskRule.Param p1 = TaskRule.Param.builder()\n                        .index(1)\n                        .protectedVariable(false)\n                        .addValues(\"value-1\")\n                        .build();\n\n        TaskRule.Param p2 = TaskRule.Param.builder()\n                .index(0)\n                .protectedVariable(false)\n                .addValues(\"value-2\")\n                .build();\n\n        TaskRule r = TaskRule.builder()\n                .taskName(\"taskName-.*\")\n                .method(\"foo\")\n                .addParams(p1, p2)\n                .build();\n\n        PolicyRules<TaskRule> rules = new PolicyRules<>(null, null, Collections.singletonList(r));\n\n        TaskPolicy policy = new TaskPolicy(rules);\n\n        // ---\n        assertDeny(policy, \"taskName-12\", \"foo\", \"value-2\", \"value-1\");\n    }\n\n    @Test\n    public void testDenyByMapStringParam() {\n        TaskRule.Param p1 = TaskRule.Param.builder()\n                .index(1)\n                .name(\"k\")\n                .protectedVariable(false)\n                .addValues(\"v\")\n                .build();\n\n        TaskRule r = TaskRule.builder()\n                .taskName(\"taskName-.*\")\n                .method(\"foo\")\n                .addParams(p1)\n                .build();\n\n        PolicyRules<TaskRule> rules = new PolicyRules<>(null, null, Collections.singletonList(r));\n\n        TaskPolicy policy = new TaskPolicy(rules);\n\n        // ---\n        assertDeny(policy, \"taskName-12\", \"foo\", \"xxx\", Collections.singletonMap(\"k\", \"v\"));\n    }\n\n    @Test\n    public void testDenyByMapMapStringParam() {\n        TaskRule.Param p1 = TaskRule.Param.builder()\n                .index(1)\n                .name(\"k.kk\")\n                .protectedVariable(false)\n                .addValues(\"v\")\n                .build();\n\n        TaskRule r = TaskRule.builder()\n                .taskName(\"taskName-.*\")\n                .method(\"foo\")\n                .addParams(p1)\n                .build();\n\n        PolicyRules<TaskRule> rules = new PolicyRules<>(null, null, Collections.singletonList(r));\n\n        TaskPolicy policy = new TaskPolicy(rules);\n\n        // ---\n        assertDeny(policy, \"taskName-12\", \"foo\", \"xxx\", Collections.singletonMap(\"k\", Collections.singletonMap(\"kk\", \"v\")));\n    }\n\n    @Test\n    public void testDenyByMapMapStringParamNull() {\n        TaskRule.Param p1 = TaskRule.Param.builder()\n                .index(1)\n                .name(\"k.kk\")\n                .protectedVariable(false)\n                .values(Collections.singletonList(null))\n                .build();\n\n        TaskRule r = TaskRule.builder()\n                .taskName(\"taskName-.*\")\n                .method(\"foo\")\n                .addParams(p1)\n                .build();\n\n        PolicyRules<TaskRule> rules = new PolicyRules<>(null, null, Collections.singletonList(r));\n\n        TaskPolicy policy = new TaskPolicy(rules);\n\n        // ---\n        assertDeny(policy, \"taskName-12\", \"foo\", \"xxx\", Collections.singletonMap(\"k1\", \"v\"));\n    }\n\n    @Test\n    public void testDenyByTaskResultsSimpleObject() {\n        TaskRule.TaskResult tr1 = TaskRule.TaskResult.builder()\n                .task(\"taskName-12\")\n                .addValues(\"result1\")\n                .build();\n\n        TaskRule r = TaskRule.builder()\n                .taskName(\"taskName-.*\")\n                .method(\"foo\")\n                .addTaskResults(tr1)\n                .build();\n\n        PolicyRules<TaskRule> rules = new PolicyRules<>(null, null, Collections.singletonList(r));\n\n        TaskPolicy policy = new TaskPolicy(rules);\n\n        // ---\n        assertDenyByTaskResults(policy, \"taskName-12\", \"foo\", Collections.singletonMap(\"taskName-12\", Collections.singletonList(\"result1\")));\n    }\n\n    @Test\n    public void testDenyByTaskResultsMap() {\n        TaskRule.TaskResult tr1 = TaskRule.TaskResult.builder()\n                .task(\"taskName-12\")\n                .result(\"k.k1\")\n                .addValues(\"result1\")\n                .build();\n\n        TaskRule r = TaskRule.builder()\n                .taskName(\"taskName-.*\")\n                .method(\"foo\")\n                .addTaskResults(tr1)\n                .build();\n\n        PolicyRules<TaskRule> rules = new PolicyRules<>(null, null, Collections.singletonList(r));\n\n        TaskPolicy policy = new TaskPolicy(rules);\n\n        // ---\n        List<Serializable> taskResults = new ArrayList<>();\n        taskResults.add(\"result12\");\n        taskResults.add(singletonMap(\"k\", singletonMap(\"k1\", \"result1\")));\n\n        assertDenyByTaskResults(policy, \"taskName-12\", \"foo\", Collections.singletonMap(\"taskName-12\", taskResults));\n    }\n\n    private static void assertDeny(TaskPolicy policy, String taskName, String methodName, Object... params) {\n        CheckResult<TaskRule, String> result = policy.check(taskName, methodName, params, null);\n        assertFalse(result.getDeny().isEmpty());\n    }\n\n    private static void assertAllow(TaskPolicy policy, String taskName, String methodName, Object... params) {\n        CheckResult<TaskRule, String> result = policy.check(taskName, methodName, params, null);\n        assertTrue(result.getDeny().isEmpty());\n    }\n\n    private static void assertDenyByTaskResults(TaskPolicy policy, String taskName, String methodName, Map<String, List<Serializable>> taskResults) {\n        CheckResult<TaskRule, String> result = policy.check(taskName, methodName, null, taskResults);\n        assertFalse(result.getDeny().isEmpty());\n    }\n\n    private static HashMap<String, Serializable> singletonMap(String k, Serializable v) {\n        HashMap<String, Serializable> result = new HashMap<>();\n        result.put(k, v);\n        return result;\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/test/java/com/walmartlabs/concord/policyengine/UtilsTest.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class UtilsTest {\n\n    @Test\n    public void testMatchAny() {\n        boolean result = Utils.matchAny(Collections.singletonList(\"\\\\.concord\"), \".concord\");\n        assertTrue(result);\n\n        result = Utils.matchAny(Collections.emptyList(), \".concord\");\n        assertFalse(result);\n    }\n\n    @Test\n    public void testSimple() {\n        String s = \"100KB\";\n\n        long result = Utils.parseFileSize(s);\n\n        assertEquals(100 * 1024, result);\n    }\n\n    @Test\n    public void testTrim() {\n        String s = \"   100  kb \";\n\n        long result = Utils.parseFileSize(s);\n\n        assertEquals(100 * 1024, result);\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/test/java/com/walmartlabs/concord/policyengine/WorkspacePolicyTest.java",
    "content": "package com.walmartlabs.concord.policyengine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class WorkspacePolicyTest {\n\n    @Test\n    public void testMaxSize() throws Exception {\n        Path p = Files.createTempDirectory(\"test1\");\n        Files.write(p.resolve(\"test.bin\"), new byte[]{0, 1, 2, 3, 4, 5}, StandardOpenOption.CREATE_NEW);\n\n        WorkspacePolicy fiveBytes = new WorkspacePolicy(WorkspaceRule.of(\"5 bytes\", 5L, null));\n        WorkspacePolicy tenBytes = new WorkspacePolicy(WorkspaceRule.of(\"10 bytes\", 10L, null));\n\n        // ---\n\n        assertDeny(fiveBytes, p);\n        assertAllow(tenBytes, p);\n    }\n\n    private static void assertAllow(WorkspacePolicy policy, Path p) throws IOException {\n        CheckResult<WorkspaceRule, Path> result = policy.check(p);\n        assertTrue(result.getDeny().isEmpty());\n    }\n\n    private static void assertDeny(WorkspacePolicy policy, Path p) throws IOException {\n        CheckResult<WorkspaceRule, Path> result = policy.check(p);\n        assertFalse(result.getDeny().isEmpty());\n    }\n}\n"
  },
  {
    "path": "policy-engine/src/test/resources/com/walmartlabs/concord/policyengine/policy1.json",
    "content": "{\n  \"queue\": {\n    \"forkDepth\": {\n      \"max\": 5\n    }\n  },\n  \"processCfg\": {\n    \"meta\": {\n      \"__currentFlow\": \"n/a\"\n    }\n  },\n  \"protectedTask\": {\n    \"names\": [\n      \"gatekeeper\"\n    ]\n  },\n  \"dependencyRewrite\": [\n    {\n      \"msg\": \"msg1\",\n      \"value\": \"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:1.71.0\",\n      \"groupId\": \"com.walmartlabs.concord.plugins.basic\",\n      \"toVersion\": \"1.70.3\",\n      \"artifactId\": \"ansible-tasks\",\n      \"fromVersion\": \"1.62.0\"\n    },\n    {\n      \"msg\": \"msg2\",\n      \"value\": \"mvn://com.walmartlabs.concord.plugins:git:1.33.0\",\n      \"groupId\": \"com.walmartlabs.concord.plugins\",\n      \"toVersion\": \"1.32.3\",\n      \"artifactId\": \"git\",\n      \"fromVersion\": \"1.25.0\"\n    },\n    {\n      \"msg\": \"msg3\",\n      \"values\": [\"mvn://com.walmartlabs.concord.plugins.basic:ansible-tasks:1.72.0\", \"mvn://com.google.code.gson:gson:2.8.7\"],\n      \"groupId\": \"com.walmartlabs.concord.plugins.basic\",\n      \"toVersion\": \"1.72.0\",\n      \"artifactId\": \"ansible-tasks\",\n      \"fromVersion\": \"1.71.0\"\n    }\n  ]\n}"
  },
  {
    "path": "policy-engine/src/test/resources/com/walmartlabs/concord/policyengine/policy2.json",
    "content": "{\n  \"queue\": {\n    \"processTimeout\": {\n      \"max\": \"PT2H\"\n    }\n  },\n  \"processCfg\": {\n    \"requirements\": {\n      \"agent\": {\n        \"flavor\": \"xxx-tunr\"\n      }\n    },\n    \"processTimeout\": \"PT1H\"\n  }\n}"
  },
  {
    "path": "policy-engine/src/test/resources/com/walmartlabs/concord/policyengine/policy3.json",
    "content": "{\"queue\": {\"concurrent\": {\"max\": 20}}}"
  },
  {
    "path": "policy-engine/src/test/resources/com/walmartlabs/concord/policyengine/policy4.json",
    "content": "{\n  \"task\": {\n    \"deny\": [\n      {\n        \"msg\": \"msg1\",\n        \"method\": \"execute\",\n        \"params\": [\n          {\n            \"name\": \"gatekeeperResult\",\n            \"index\": 0,\n            \"values\": [\n              false,\n              null\n            ],\n            \"protected\": true\n          }\n        ],\n        \"taskName\": \"ansible.*\"\n      }\n    ],\n    \"warn\": [],\n    \"allow\": []\n  },\n  \"ansible\": {\n    \"deny\": [\n      {\n        \"msg\": \"msg2\",\n        \"action\": \"uri\",\n        \"gatekeeperArtifacts\": \"${context.getProtectedVariable('gatekeeperArtifacts')}\"\n      },\n      {\n        \"msg\": \"msg3\",\n        \"action\": \"maven_artifact\",\n        \"gatekeeperArtifacts\": \"${context.getProtectedVariable('gatekeeperArtifacts')}\"\n      },\n      {\n        \"msg\": \"msg4\",\n        \"action\": \"docker_container\",\n        \"gatekeeperArtifacts\": \"${context.getProtectedVariable('gatekeeperArtifacts')}\"\n      }\n    ],\n    \"warn\": [],\n    \"allow\": [\n      {\n        \"action\": \"uri\",\n        \"params\": [\n          {\n            \"name\": \"url\",\n            \"values\": \"${context.getProtectedVariable('gatekeeperArtifacts')}\"\n          }\n        ]\n      },\n      {\n        \"action\": \"maven_artifact\",\n        \"params\": [\n          {\n            \"name\": \"artifact_url\",\n            \"values\": \"${context.getProtectedVariable('gatekeeperArtifacts')}\"\n          }\n        ]\n      },\n      {\n        \"action\": \"docker_container\",\n        \"params\": [\n          {\n            \"name\": \"image\",\n            \"values\": \"${context.getProtectedVariable('gatekeeperArtifacts')}\"\n          }\n        ]\n      }\n    ]\n  },\n  \"protectedTask\": {\n    \"names\": [\n      \"gatekeeper\"\n    ]\n  }\n}"
  },
  {
    "path": "policy-engine/src/test/resources/com/walmartlabs/concord/policyengine/policy5.json",
    "content": "{\n  \"entity\": {\n    \"deny\": [\n      {\n        \"msg\": \"New project creation is disabled in this organization by the environment's policy\",\n        \"action\": \"create\",\n        \"entity\": \"project\",\n        \"conditions\": {\n          \"entity\": {\n            \"orgId\": \".*\"\n          }\n        }\n      },\n      {\n        \"msg\": \"Subscribing to all GitHub repository notifications is not allowed\",\n        \"action\": \"create\",\n        \"entity\": \"trigger\",\n        \"conditions\": {\n          \"entity\": {\n            \"params\": {\n              \"org\": \"\\\\.\\\\*\",\n              \"project\": \"\\\\.\\\\*\",\n              \"repository\": \"\\\\.\\\\*\"\n            },\n            \"eventSource\": \"github\"\n          }\n        }\n      },\n      {\n        \"msg\": \"Subscribing to all GitHub repository notifications is not allowed\",\n        \"action\": \"create\",\n        \"entity\": \"trigger\",\n        \"conditions\": {\n          \"entity\": {\n            \"params\": {\n              \"githubOrg\": \"\\\\.\\\\*\",\n              \"githubRepo\": \"\\\\.\\\\*\"\n            },\n            \"eventSource\": \"github\"\n          }\n        }\n      },\n      {\n        \"msg\": \"Using Github triggers without passing condition parameters is not allowed. Please check your syntax\",\n        \"action\": \"create\",\n        \"entity\": \"trigger\",\n        \"conditions\": {\n          \"entity\": {\n            \"params\": {\n              \"type\": null,\n              \"payload\": {\n                \"type\": null\n              }\n            },\n            \"eventSource\": \"github\"\n          }\n        }\n      }\n    ]\n  },\n  \"jsonStore\": {\n    \"data\": {\n      \"maxSizeInBytes\": 16777216\n    },\n    \"store\": {\n      \"maxNumberPerOrg\": 128\n    }\n  },\n  \"attachments\": {\n    \"msg\": \"Attachments too big: current {0} bytes, limit {1} bytes\",\n    \"maxSizeInBytes\": 2097152\n  }\n}"
  },
  {
    "path": "policy-engine/src/test/resources/com/walmartlabs/concord/policyengine/policy6.json",
    "content": "{\n  \"defaultProcessCfg\": {\n    \"defaultTaskVariables\": {\n      \"jira\": {\n        \"apiUrl\": \"https://jira.walmart.com/rest/api/2/\",\n        \"readTimeout\": 30,\n        \"writeTimeout\": 30,\n        \"connectTimeout\": 30\n      },\n      \"slack\": {\n        \"proxyPort\": 9080,\n        \"proxyAddress\": \"proxy.com\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>com.walmartlabs.concord</groupId>\n    <artifactId>parent</artifactId>\n    <version>2.40.1-SNAPSHOT</version>\n    <packaging>pom</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n    <description>Concord: Orchestrate More. Live Better.</description>\n    <url>https://concord.walmartlabs.com/</url>\n\n    <licenses>\n        <license>\n            <name>Apache License, Version 2.0</name>\n            <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>\n            <distribution>repo</distribution>\n        </license>\n    </licenses>\n\n    <developers>\n        <developer>\n            <id>team</id>\n            <name>Concord Development Team</name>\n            <roles>\n                <role>Project Lead</role>\n            </roles>\n            <timezone>-5</timezone>\n        </developer>\n    </developers>\n\n    <modules>\n        <module>config</module>\n        <module>sdk</module>\n        <module>common</module>\n        <module>policy-engine</module>\n        <module>dependency-manager</module>\n        <module>github-app-installation</module>\n        <module>repository</module>\n        <module>imports</module>\n        <module>forms</module>\n        <module>server</module>\n        <module>client2</module>\n        <module>runtime/model</module>\n        <module>runtime/common</module>\n        <module>runtime/loader</module>\n        <module>runtime/v1</module>\n        <module>runtime/v2</module>\n        <module>plugins</module>\n        <module>agent</module>\n        <module>console2</module>\n        <module>docker-images</module>\n        <module>it</module>\n        <module>agent-operator</module>\n        <module>cli</module>\n        <module>targetplatform</module>\n    </modules>\n\n    <properties>\n        <maven.compiler.release>${jdk.version}</maven.compiler.release>\n        <maven.compiler.source>${maven.compiler.release}</maven.compiler.source>\n        <maven.compiler.target>${maven.compiler.release}</maven.compiler.target>\n\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n\n        <scm.connection>scm:git:https://github.com/walmartlabs/concord.git</scm.connection>\n\n        <docker.plugin.version>0.48.0</docker.plugin.version>\n        <jooq.version>3.14.0</jooq.version>\n        <junit.version>5.9.1</junit.version>\n        <liquibase.version>4.29.2</liquibase.version>\n        <node.version>24.14.0</node.version>\n        <frontend.plugin.version>1.15.1</frontend.plugin.version>\n\n        <!-- used for ITs and db module(s) schema generation -->\n        <db.image>library/postgres:14.21-alpine</db.image>\n\n        <jdk.version>17</jdk.version>\n        <failsafe.args>\n            --add-opens java.base/java.util=ALL-UNNAMED\n        </failsafe.args>\n    </properties>\n\n    <dependencyManagement>\n        <dependencies>\n            <dependency>\n                <groupId>com.walmartlabs.concord</groupId>\n                <artifactId>concord-targetplatform</artifactId>\n                <version>${project.version}</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n        </dependencies>\n    </dependencyManagement>\n\n    <build>\n        <extensions>\n            <extension>\n                <groupId>org.apache.maven.wagon</groupId>\n                <artifactId>wagon-webdav-jackrabbit</artifactId>\n                <version>3.1.0</version>\n            </extension>\n        </extensions>\n        <plugins>\n            <plugin>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>3.14.0</version>\n                <configuration>\n                    <compilerArgs>-proc:full</compilerArgs>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>license-maven-plugin</artifactId>\n                <version>2.5.0</version>\n                <executions>\n                    <execution>\n                        <id>first</id>\n                        <goals>\n                            <goal>update-file-header</goal>\n                        </goals>\n                        <phase>process-sources</phase>\n                    </execution>\n                </executions>\n                <configuration>\n                    <verbose>false</verbose>\n                    <licenseName>apache_v2</licenseName>\n                    <organizationName>Walmart Inc.</organizationName>\n                    <projectName>Concord</projectName>\n                    <inceptionYear>2017</inceptionYear>\n                    <processStartTag>*****</processStartTag>\n                    <sectionDelimiter>-----</sectionDelimiter>\n                    <processEndTag>=====</processEndTag>\n                    <trimHeaderLine>true</trimHeaderLine>\n                    <roots>\n                        <root>src/main/java</root>\n                        <root>src/test/java</root>\n                    </roots>\n                    <extraExtensions>\n                        <tsx>java</tsx>\n                    </extraExtensions>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-checkstyle-plugin</artifactId>\n                <version>3.6.0</version>\n                <configuration>\n                    <configLocation>checkstyle.xml</configLocation>\n                    <consoleOutput>true</consoleOutput>\n                    <failsOnError>true</failsOnError>\n                    <linkXRef>false</linkXRef>\n                </configuration>\n                <executions>\n                    <execution>\n                        <id>validate</id>\n                        <phase>validate</phase>\n                        <goals>\n                            <goal>check</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-enforcer-plugin</artifactId>\n                <version>3.5.0</version>\n                <executions>\n                    <execution>\n                        <phase>validate</phase>\n                        <goals>\n                            <goal>enforce</goal>\n                        </goals>\n                    </execution>\n                </executions>\n                <configuration>\n                    <rules>\n                        <banDuplicatePomDependencyVersions />\n                        <requireUpperBoundDeps />\n                        <requireMavenVersion>\n                            <version>3.6.3</version>\n                        </requireMavenVersion>\n                        <requireJavaVersion>\n                            <version>${jdk.version}</version>\n                        </requireJavaVersion>\n                    </rules>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-source-plugin</artifactId>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-gpg-plugin</artifactId>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-javadoc-plugin</artifactId>\n            </plugin>\n        </plugins>\n\n        <pluginManagement>\n            <plugins>\n                <plugin>\n                    <groupId>org.sonatype.central</groupId>\n                    <artifactId>central-publishing-maven-plugin</artifactId>\n                    <version>0.7.0</version>\n                    <extensions>true</extensions>\n                    <configuration>\n                        <publishingServerId>central</publishingServerId>\n                    </configuration>\n                </plugin>\n                <plugin>\n                    <groupId>com.github.eirslett</groupId>\n                    <artifactId>frontend-maven-plugin</artifactId>\n                    <version>${frontend.plugin.version}</version>\n                </plugin>\n                <plugin>\n                    <groupId>io.fabric8</groupId>\n                    <artifactId>docker-maven-plugin</artifactId>\n                    <version>${docker.plugin.version}</version>\n                    <configuration>\n                        <containerNamePattern>%e</containerNamePattern>\n                    </configuration>\n                </plugin>\n                <plugin>\n                    <groupId>org.jooq</groupId>\n                    <artifactId>jooq-codegen-maven</artifactId>\n                    <version>${jooq.version}</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-release-plugin</artifactId>\n                    <version>3.1.1</version>\n                    <configuration>\n                        <indentSize>4</indentSize>\n                        <tagNameFormat>@{project.version}</tagNameFormat>\n                        <localCheckout>true</localCheckout>\n                        <autoVersionSubmodules>true</autoVersionSubmodules>\n                        <pushChanges>false</pushChanges>\n                        <useReleaseProfile>false</useReleaseProfile>\n                        <goals>install org.sonatype.central:central-publishing-maven-plugin:publish</goals>\n                        <arguments>--batch-mode -DskipTests -Pconcord-release</arguments>\n                    </configuration>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-assembly-plugin</artifactId>\n                    <version>2.6</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-failsafe-plugin</artifactId>\n                    <version>3.0.0-M5</version>\n                    <dependencies>\n                        <dependency>\n                            <groupId>org.junit.jupiter</groupId>\n                            <artifactId>junit-jupiter-engine</artifactId>\n                            <version>${junit.version}</version>\n                        </dependency>\n                    </dependencies>\n                    <configuration>\n                        <includes>\n                            <include>**/IT*.java</include>\n                            <include>**/*IT.java</include>\n                            <include>**/*ITCase.java</include>\n                            <include>**/Test*.java</include>\n                        </includes>\n                        <argLine>${failsafe.args}</argLine>\n                    </configuration>\n                    <executions>\n                        <execution>\n                            <goals>\n                                <goal>integration-test</goal>\n                                <goal>verify</goal>\n                            </goals>\n                        </execution>\n                    </executions>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-surefire-plugin</artifactId>\n                    <version>3.0.0-M5</version>\n                    <dependencies>\n                        <dependency>\n                            <groupId>org.junit.jupiter</groupId>\n                            <artifactId>junit-jupiter-engine</artifactId>\n                            <version>${junit.version}</version>\n                        </dependency>\n                    </dependencies>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-source-plugin</artifactId>\n                    <version>3.3.0</version>\n                    <executions>\n                        <execution>\n                            <id>attach-sources</id>\n                            <goals>\n                                <goal>jar-no-fork</goal>\n                            </goals>\n                        </execution>\n                    </executions>\n                    <configuration>\n                        <skipSource>true</skipSource>\n                    </configuration>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-gpg-plugin</artifactId>\n                    <version>3.1.0</version>\n                    <executions>\n                        <execution>\n                            <id>sign-artifacts</id>\n                            <phase>verify</phase>\n                            <goals>\n                                <goal>sign</goal>\n                            </goals>\n                        </execution>\n                    </executions>\n                    <configuration>\n                        <skip>true</skip>\n                    </configuration>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-clean-plugin</artifactId>\n                    <version>3.0.0</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.eluder.coveralls</groupId>\n                    <artifactId>coveralls-maven-plugin</artifactId>\n                    <version>4.2.0</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.codehaus.mojo</groupId>\n                    <artifactId>cobertura-maven-plugin</artifactId>\n                    <version>2.7</version>\n                    <configuration>\n                        <format>xml</format>\n                        <maxmem>256m</maxmem>\n                        <aggregate>true</aggregate>\n                    </configuration>\n                </plugin>\n                <plugin>\n                    <groupId>io.swagger.core.v3</groupId>\n                    <artifactId>swagger-maven-plugin</artifactId>\n                    <version>2.2.16</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.eclipse.sisu</groupId>\n                    <artifactId>sisu-maven-plugin</artifactId>\n                    <version>0.9.0.M4</version>\n                    <executions>\n                        <execution>\n                            <id>index</id>\n                            <goals>\n                                <goal>main-index</goal>\n                            </goals>\n                        </execution>\n                    </executions>\n                </plugin>\n                <plugin>\n                    <groupId>dev.ybrig.concord</groupId>\n                    <artifactId>concord-maven-plugin</artifactId>\n                    <version>0.0.37</version>\n                    <configuration>\n                        <concordVersion>${project.version}</concordVersion>\n                    </configuration>\n                    <executions>\n                        <execution>\n                            <goals>\n                                <goal>sisu-index</goal>\n                                <goal>verify</goal>\n                            </goals>\n                        </execution>\n                    </executions>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-antrun-plugin</artifactId>\n                    <version>1.8</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.codehaus.mojo</groupId>\n                    <artifactId>build-helper-maven-plugin</artifactId>\n                    <version>3.6.0</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-shade-plugin</artifactId>\n                    <version>3.6.0</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-plugin-plugin</artifactId>\n                    <version>3.15.1</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.codehaus.mojo</groupId>\n                    <artifactId>exec-maven-plugin</artifactId>\n                    <version>3.5.0</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-resources-plugin</artifactId>\n                    <version>3.3.1</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.openapitools</groupId>\n                    <artifactId>openapi-generator-maven-plugin</artifactId>\n                    <version>7.12.0</version>\n                </plugin>\n                <plugin>\n                    <groupId>pl.project13.maven</groupId>\n                    <artifactId>git-commit-id-plugin</artifactId>\n                    <version>4.9.10</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.revapi</groupId>\n                    <artifactId>revapi-maven-plugin</artifactId>\n                    <version>0.15.0</version>\n                    <dependencies>\n                        <dependency>\n                            <groupId>org.revapi</groupId>\n                            <artifactId>revapi-java</artifactId>\n                            <version>0.28.1</version>\n                        </dependency>\n                    </dependencies>\n                    <executions>\n                        <execution>\n                            <goals>\n                                <goal>check</goal>\n                            </goals>\n                        </execution>\n                    </executions>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-dependency-plugin</artifactId>\n                    <version>3.8.1</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.apache.maven.plugins</groupId>\n                    <artifactId>maven-javadoc-plugin</artifactId>\n                    <version>3.11.2</version>\n                </plugin>\n                <plugin>\n                    <groupId>org.skife.maven</groupId>\n                    <artifactId>really-executable-jar-maven-plugin</artifactId>\n                    <version>2.1.1</version>\n                </plugin>\n            </plugins>\n        </pluginManagement>\n    </build>\n\n    <scm>\n        <url>${scm.connection}</url>\n        <developerConnection>${scm.connection}</developerConnection>\n        <tag>HEAD</tag>\n    </scm>\n\n    <distributionManagement>\n        <repository>\n            <id>sonatype.releases</id>\n            <name>Sonatype Releases</name>\n            <url>https://oss.sonatype.org/service/local/staging/deploy/maven2</url>\n        </repository>\n        <snapshotRepository>\n            <id>sonatype.snapshots</id>\n            <name>Sonatype Snapshots</name>\n            <url>https://oss.sonatype.org/content/repositories/snapshots</url>\n        </snapshotRepository>\n    </distributionManagement>\n\n    <profiles>\n        <profile>\n            <id>jdk21</id>\n            <properties>\n                <jdk.version>21</jdk.version>\n            </properties>\n        </profile>\n        <profile>\n            <id>jdk21-aarch64</id>\n            <properties>\n                <jdk.version>21</jdk.version>\n            </properties>\n        </profile>\n        <profile>\n            <id>concord-release</id>\n            <build>\n                <plugins>\n                    <plugin>\n                        <artifactId>maven-gpg-plugin</artifactId>\n                        <configuration>\n                            <skip>false</skip>\n                        </configuration>\n                    </plugin>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-javadoc-plugin</artifactId>\n                        <executions>\n                            <execution>\n                                <id>attach-javadocs</id>\n                                <goals>\n                                    <goal>jar</goal>\n                                </goals>\n                            </execution>\n                        </executions>\n                        <configuration>\n                            <quiet>true</quiet>\n                            <doclint>none</doclint>\n                        </configuration>\n                    </plugin>\n                    <plugin>\n                        <groupId>org.apache.maven.plugins</groupId>\n                        <artifactId>maven-source-plugin</artifactId>\n                        <executions>\n                            <execution>\n                                <id>attach-sources</id>\n                                <goals>\n                                    <goal>jar-no-fork</goal>\n                                </goals>\n                            </execution>\n                        </executions>\n                        <configuration>\n                            <skipSource>false</skipSource>\n                        </configuration>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n        <profile>\n            <id>looper</id>\n            <build>\n                <plugins>\n                    <plugin>\n                        <artifactId>maven-gpg-plugin</artifactId>\n                        <configuration>\n                            <skip>true</skip>\n                        </configuration>\n                    </plugin>\n                    <plugin>\n                        <artifactId>maven-release-plugin</artifactId>\n                        <configuration>\n                            <pushChanges>true</pushChanges>\n                        </configuration>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n        <profile>\n            <id>walmart</id>\n            <distributionManagement>\n                <repository>\n                    <id>${public-release.serverId}</id>\n                    <url>${public-release.url}</url>\n                </repository>\n\n                <snapshotRepository>\n                    <id>${public-snapshot.serverId}</id>\n                    <url>${public-snapshot.url}</url>\n                </snapshotRepository>\n\n                <site>\n                    <id>${site.id}</id>\n                    <url>${site.url}</url>\n                </site>\n            </distributionManagement>\n        </profile>\n        <profile>\n            <id>concord-external</id>\n            <distributionManagement>\n                <repository>\n                    <id>${public-release.serverId}</id>\n                    <url>${public-release.url}</url>\n                </repository>\n                <snapshotRepository>\n                    <id>${public-snapshot.serverId}</id>\n                    <url>${public-snapshot.url}</url>\n                </snapshotRepository>\n                <site>\n                    <id>${site.id}</id>\n                    <url>${site.url}</url>\n                </site>\n            </distributionManagement>\n        </profile>\n    </profiles>\n</project>\n"
  },
  {
    "path": "repository/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-repository</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.guava</groupId>\n            <artifactId>guava</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-dependency-manager</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.errorprone</groupId>\n            <artifactId>error_prone_annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>ch.qos.logback</groupId>\n            <artifactId>logback-classic</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>commons-io</groupId>\n            <artifactId>commons-io</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-compress</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jgit</groupId>\n            <artifactId>org.eclipse.jgit</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-core</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-junit-jupiter</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n</project>\n"
  },
  {
    "path": "repository/src/main/java/com/walmartlabs/concord/repository/FetchRequest.java",
    "content": "package com.walmartlabs.concord.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Secret;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.nio.file.Path;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface FetchRequest {\n\n    /**\n     * URL of GIT repository.\n     */\n    String url();\n\n    /**\n     * Target directory.\n     */\n    Path destination();\n\n    /**\n     * Concord secret for authentication. If not provided, an available\n     * {@link com.walmartlabs.concord.common.AuthTokenProvider} matching\n     * {@link #url()} will be used for {@code https://} URLs. Otherwise,\n     * anonymous auth is attempted.\n     */\n    @Nullable\n    Secret secret();\n\n    /**\n     * Version to checkout (branch, tag, commit ID).\n     */\n    Version version();\n\n    /**\n     * If {@code true} use shallow cloning.\n     */\n    @Value.Default\n    default boolean shallow() {\n        return true;\n    }\n\n    /**\n     * Fetch the repository's submodules if {@code true}.\n     */\n    @Value.Default\n    default boolean includeSubmodules() {\n        return true;\n    }\n\n    /**\n     * Get latest commit message and author if {@code true}.\n     */\n    @Value.Default\n    default boolean withCommitInfo() {\n        return false;\n    }\n\n    @Value.Default\n    default boolean checkAlreadyFetched() {\n        return false;\n    }\n\n    static ImmutableFetchRequest.Builder builder() {\n        return ImmutableFetchRequest.builder();\n    }\n\n    class Version {\n\n        /**\n         * Any usable \"version\" value - branch name, tag or a commit ID.\n         */\n        public static Version from(String value) {\n            return new Version(value, value);\n        }\n\n        /**\n         * Commit ID + branch or a tag.\n         */\n        public static Version commitWithBranch(String commitId, String branchOrTag) {\n            if (commitId == null) {\n                return from(branchOrTag);\n            }\n            return new Version(commitId, branchOrTag);\n        }\n\n        private final String value;\n\n        private final String ref;\n\n        private Version(String value, String ref) {\n            this.value = value;\n            this.ref = ref;\n        }\n\n        public String value() {\n            return value;\n        }\n\n        public String ref() {\n            return ref;\n        }\n\n        @Override\n        public String toString() {\n            return \"Version{\" +\n                    \"value='\" + value + '\\'' +\n                    \", ref='\" + ref + '\\'' +\n                    '}';\n        }\n    }\n}\n"
  },
  {
    "path": "repository/src/main/java/com/walmartlabs/concord/repository/FetchResult.java",
    "content": "package com.walmartlabs.concord.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface FetchResult {\n\n    /**\n     * Current HEAD.\n     */\n    String head();\n\n    /**\n     * Current branch or tag.\n     */\n    @Nullable\n    String branchOrTag();\n\n    /**\n     * Current commit message.\n     */\n    @Nullable\n    String message();\n\n    /**\n     * Current commit author.\n     */\n    @Nullable\n    String author();\n\n    static ImmutableFetchResult.Builder builder() {\n        return ImmutableFetchResult.builder();\n    }\n}\n"
  },
  {
    "path": "repository/src/main/java/com/walmartlabs/concord/repository/GitCliRepositoryProvider.java",
    "content": "package com.walmartlabs.concord.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AuthTokenProvider;\nimport com.walmartlabs.concord.common.PathUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class GitCliRepositoryProvider implements RepositoryProvider {\n\n    private static final Logger log = LoggerFactory.getLogger(GitCliRepositoryProvider.class);\n    private static final String GIT_FILES = \"^(\\\\.git|\\\\.gitmodules|\\\\.gitignore)$\";\n\n    private final GitClient client;\n\n    public GitCliRepositoryProvider(GitClientConfiguration cfg, AuthTokenProvider authProvider) {\n        this.client = new GitClient(cfg, authProvider);\n    }\n\n    @Override\n    public boolean canHandle(String url) {\n        return true;\n    }\n\n    @Override\n    public FetchResult fetch(FetchRequest request) {\n        RepositoryException lastException = null;\n\n        // try twice\n        for (int attemptNo = 0; attemptNo < 2; attemptNo++) {\n            if (attemptNo > 0) {\n                log.warn(\"fetch ['{}', '{}', '{}'] -> error: {}, retrying...\",\n                        request.url(), request.version(), request.destination(), lastException.getMessage());\n            }\n\n            try {\n                return client.fetch(request);\n            } catch (RepositoryException e) {\n                lastException = e;\n                try {\n                    PathUtils.deleteRecursively(request.destination());\n                } catch (IOException ee) {\n                    log.warn(\"fetch ['{}', '{}', '{}'] -> cleanup error: {}\",\n                            request.url(), request.version(), request.destination(), e.getMessage());\n                }\n            }\n        }\n\n        throw lastException;\n    }\n\n    @Override\n    public Snapshot export(Path src, Path dst, List<String> ignorePatterns) throws IOException {\n        LastModifiedSnapshot snapshot = new LastModifiedSnapshot();\n        List<String> allIgnorePatterns = new ArrayList<>();\n        allIgnorePatterns.add(GIT_FILES);\n        allIgnorePatterns.addAll(ignorePatterns);\n        PathUtils.copy(src, dst, allIgnorePatterns, snapshot, StandardCopyOption.REPLACE_EXISTING);\n        return snapshot;\n    }\n}\n"
  },
  {
    "path": "repository/src/main/java/com/walmartlabs/concord/repository/GitClient.java",
    "content": "package com.walmartlabs.concord.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AuthTokenProvider;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.secret.BinaryDataSecret;\nimport com.walmartlabs.concord.common.secret.KeyPair;\nimport com.walmartlabs.concord.common.secret.UsernamePassword;\nimport com.walmartlabs.concord.sdk.Secret;\nimport org.immutables.value.Value;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.annotation.Nullable;\nimport java.io.*;\nimport java.net.URI;\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Duration;\nimport java.util.*;\nimport java.util.concurrent.*;\n\nimport static com.walmartlabs.concord.common.LogUtils.withMdc;\nimport static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE;\nimport static java.nio.file.attribute.PosixFilePermission.OWNER_READ;\n\npublic class GitClient {\n\n    private static final Logger log = LoggerFactory.getLogger(GitClient.class);\n\n    private static final int OBJECT_ID_LENGTH = 20;\n    private static final int OBJECT_ID_STRING_LENGTH = OBJECT_ID_LENGTH * 2;\n\n    private static final int SUCCESS_EXIT_CODE = 0;\n\n    private final GitClientConfiguration cfg;\n    private final AuthTokenProvider authProvider;\n    private final Set<Obfuscation> sensitiveData;\n    private final ExecutorService executor;\n\n    public GitClient(GitClientConfiguration cfg, AuthTokenProvider authProvider) {\n        this.cfg = cfg;\n        this.authProvider = authProvider;\n        this.executor = Executors.newCachedThreadPool();\n        this.sensitiveData = new LinkedHashSet<>();\n\n        // urls with user info.\n        sensitiveData.add(new Obfuscation(\"https://([^@]*)@\", \"https://***@\"));\n\n        cfg.oauthToken().ifPresent(oauth ->\n                // definitely don't print a hard-code oauth token\n                sensitiveData.add(new Obfuscation(oauth, \"***\")));\n    }\n\n    public FetchResult fetch(FetchRequest req) {\n        assertSecret(req.url(), req.secret());\n\n        try {\n            boolean exists = Files.exists(req.destination().resolve(\".git\"));\n            if (!exists) {\n                Files.createDirectories(req.destination());\n\n                init(req.destination());\n            }\n\n            configure(req.destination());\n            configureRemote(req.destination(), updateUrl(req.url(), req.secret()));\n\n            Ref ref = getHeadRef(req.destination(), req.version().ref(), req.secret());\n            configureFetch(req.destination(), getRefSpec(ref));\n\n            NormalizedVersion version = NormalizedVersion.from(req.version(), ref);\n\n            // fetch\n            boolean alreadyFetched = exists && req.checkAlreadyFetched() && alreadyFetched(req.destination(), ref, version);\n            if (!alreadyFetched) {\n                boolean effectiveShallow = req.shallow() && version.commitId() == null;\n                fetch(req.destination(), effectiveShallow, req.secret());\n\n                checkout(req.destination(), req.version().value());\n                if (version.commitId() == null) {\n                    reset(req.destination(), ref.tag() ? \"origin/tags/\" + ref.name() : \"origin/\" + ref.name());\n                }\n\n                cleanup(req.destination());\n\n                if (req.includeSubmodules() && hasSubmodules(req.destination())) {\n                    updateSubmodules(req.destination(), req.secret());\n                    resetSubmodules(req.destination());\n                }\n            }\n\n            ImmutableFetchResult.Builder result = FetchResult.builder()\n                    .head(revParse(req.destination(), \"HEAD\"))\n                    .branchOrTag(ref != null ? ref.name() : null);\n\n            if (req.withCommitInfo()) {\n                CommitInfo ci = getCommitInfo(req.destination());\n                result.message(ci.message())\n                        .author(ci.author());\n            }\n\n            return result.build();\n        } catch (RepositoryException e) {\n            throw e;\n        } catch (Exception e) {\n            log.error(\"fetch ['{}'] -> error\", req, e);\n            throw new RepositoryException(\"Error while fetching a repository: \" + e.getMessage());\n        }\n    }\n\n    private boolean alreadyFetched(Path workDir, Ref ref, NormalizedVersion version) {\n        String head = revParse(workDir, \"HEAD\");\n\n        if (version.commitId() != null) {\n            return head.equalsIgnoreCase(version.commitId());\n        }\n\n        if (ref == null) {\n            return false;\n        }\n\n        return ref.commitId().equals(head);\n    }\n\n    private void init(Path workDir) {\n        exec(Command.builder()\n                .workDir(workDir)\n                .timeout(cfg.defaultOperationTimeout())\n                .addArgs(\"init\")\n                .build());\n    }\n\n    private void configure(Path workDir) {\n        exec(Command.builder()\n                .workDir(workDir)\n                .timeout(cfg.defaultOperationTimeout())\n                .addArgs(\"config\", \"advice.detachedHead\", \"false\")\n                .build());\n        exec(Command.builder()\n                .workDir(workDir)\n                .timeout(cfg.defaultOperationTimeout())\n                .addArgs(\"config\", \"maintenance.auto\", \"false\")\n                .build());\n        exec(Command.builder()\n                .workDir(workDir)\n                .timeout(cfg.defaultOperationTimeout())\n                .addArgs(\"config\", \"gc.auto\", \"0\")\n                .build());\n    }\n\n    private void configureRemote(Path workDir, String url) {\n        exec(Command.builder()\n                .workDir(workDir)\n                .timeout(cfg.defaultOperationTimeout())\n                .addArgs(\"config\", \"remote.origin.url\", url)\n                .build());\n    }\n\n    private void configureFetch(Path workDir, String refSpec) {\n        exec(Command.builder()\n                .workDir(workDir)\n                .timeout(cfg.defaultOperationTimeout())\n                .addArgs(\"config\", \"--replace-all\", \"remote.origin.fetch\", refSpec)\n                .build());\n    }\n\n    private void fetch(Path workDir, boolean shallow, Secret secret) {\n        List<String> args = new ArrayList<>();\n        args.add(\"fetch\");\n        if (shallow) {\n            args.add(\"--depth=1\");\n        } else if (isShallowRepo(workDir)){\n            args.add(\"--unshallow\");\n        }\n        args.add(\"--quiet\");\n        args.add(\"origin\");\n\n        execWithCredentials(Command.builder()\n                .workDir(workDir)\n                .timeout(cfg.fetchTimeout())\n                .addAllArgs(args)\n                .build(), secret);\n    }\n\n    private boolean isShallowRepo(Path workDir) {\n        String result = exec(Command.builder()\n                .workDir(workDir)\n                .timeout(cfg.defaultOperationTimeout())\n                .addArgs(\"rev-parse\", \"--is-shallow-repository\")\n                .build());\n\n        return Boolean.parseBoolean(result.trim());\n    }\n\n    private void checkout(Path workDir, String rev) {\n        exec(Command.builder()\n                .workDir(workDir)\n                .timeout(cfg.defaultOperationTimeout())\n                .addArgs(\"checkout\", \"-q\", \"-f\", rev)\n                .build());\n    }\n\n    private void reset(Path workDir, String rev) {\n        exec(Command.builder()\n                .workDir(workDir)\n                .timeout(cfg.defaultOperationTimeout())\n                .addArgs(\"reset\", \"--hard\", rev)\n                .build());\n    }\n\n    private void cleanup(Path workDir) {\n        exec(Command.builder()\n                .workDir(workDir)\n                .timeout(cfg.defaultOperationTimeout())\n                .addArgs(\"clean\", \"-fdx\")\n                .build());\n    }\n\n    private String revParse(Path workDir, String rev) {\n        String result = exec(Command.builder()\n                .workDir(workDir)\n                .timeout(cfg.defaultOperationTimeout())\n                .addArgs(\"rev-parse\", rev)\n                .build());\n        String line = result.trim();\n        if (line.isEmpty()) {\n            throw new RepositoryException(\"rev-parse no content returned for '\" + rev + \"'\");\n        }\n        return fromString(line);\n    }\n\n    private List<Ref> getRefs(Path workDir, String version, Secret secret) {\n        String result = execWithCredentials(Command.builder()\n                .workDir(workDir)\n                .timeout(cfg.defaultOperationTimeout())\n                .addArgs(\"ls-remote\", \"--symref\", \"origin\", version)\n                .build(), secret);\n\n        List<Ref> refs = new ArrayList<>();\n\n        try (BufferedReader reader = new BufferedReader(new StringReader(result))) {\n            while (true) {\n                String line = reader.readLine();\n                if (line == null) {\n                    break;\n                }\n\n                String[] commitRef = line.split(\"\\t\");\n                if (commitRef.length != 2) {\n                    throw new RepositoryException(\"invalid response: \" + result);\n                }\n\n                refs.add(Ref.builder()\n                        .commitId(commitRef[0].trim())\n                        .ref(commitRef[1].trim())\n                        .name(version)\n                        .build());\n            }\n        } catch (IOException e) {\n            throw new RepositoryException(\"Error parsing result: \" + result, e);\n        }\n\n        return refs;\n    }\n\n    private Ref getHeadRef(Path workDir, String version, Secret secret) {\n        if (version == null) {\n            return null;\n        }\n\n        String branchHeadRef = \"refs/heads/\" + version;\n        String tagRef = \"refs/tags/\" + version;\n        return getRefs(workDir, version, secret).stream()\n                .filter(r -> r.ref().equalsIgnoreCase(branchHeadRef) || r.ref().equalsIgnoreCase(tagRef))\n                .findFirst()\n                .orElse(null);\n    }\n\n    private static String getRefSpec(Ref ref) {\n        if (ref == null) {\n            return \"+refs/heads/*:refs/remotes/origin/*\";\n        }\n        if (ref.tag()) {\n            return String.format(\"+refs/tags/%s:refs/remotes/origin/tags/%s\", ref.name(), ref.name());\n        } else {\n            return String.format(\"+refs/heads/%s:refs/remotes/origin/%s\", ref.name(), ref.name());\n        }\n    }\n\n    String updateUrl(String url, Secret secret) {\n        if (!url.matches(\"^[A-Za-z][A-Za-z0-9+.-]+://.*\")) {\n            // no scheme. Assume ssh, e.g. from GitHub like 'git@github.com:owner/repo.git'\n            assertUriAllowed(\"ssh://\" + url);\n\n            return url; // return un-modified. git cli doesn't want the ssh scheme\n        }\n\n        URI uri = assertUriAllowed(url);\n\n        if (url.contains(\"@\") || !url.startsWith(\"https://\")) {\n            // provided url already has credentials OR it's a non-https url\n            return url;\n        }\n\n        if (secret instanceof UsernamePassword up) {\n            // Secret contains static auth (token or username). No lookup needed.\n            return \"https://\" +\n                    (up.getUsername() == null ? \"\" : up.getUsername() + \":\") +\n                    String.valueOf(up.getPassword()) +\n                    \"@\" +\n                    url.substring(\"https://\".length());\n        }\n\n        // This will either add auth from a matching provider, or none for anonymous access\n        return authProvider.addUserInfoToUri(uri, secret).toString();\n    }\n\n    private URI assertUriAllowed(String rawUri) {\n        // make sure it's in valid format\n        URI uri = URI.create(rawUri);\n        assertUriAllowed(uri);\n\n        return uri;\n    }\n\n    private void assertUriAllowed(URI uri) {\n        String providedScheme = uri.getScheme();\n        Set<String> allowedSchemes = cfg.allowedSchemes();\n        boolean hasScheme = providedScheme != null && (!providedScheme.isEmpty());\n\n        if (allowedSchemes.isEmpty()) {\n            return; // allow all\n        }\n\n        // the provided repo string is definitely an allowed protocol.\n        if (hasScheme && allowedSchemes.contains(providedScheme)) {\n            return;\n        }\n\n        // the provided repo string has no explicit scheme, should be understood to use an ssh connection\n        if (!hasScheme && uri.getUserInfo() != null) {\n            return;\n        }\n\n        String msg = String.format(\"Provided repository ('%s') contains an unsupported URI scheme: '%s'.\",\n                uri, uri.getScheme());\n        log.warn(msg);\n        throw new RepositoryException(msg);\n    }\n\n    private void updateSubmodules(Path workDir, Secret secret) {\n        exec(Command.builder()\n                .workDir(workDir)\n                .timeout(cfg.defaultOperationTimeout())\n                .addArgs(\"submodule\", \"init\")\n                .build());\n        exec(Command.builder()\n                .workDir(workDir)\n                .timeout(cfg.defaultOperationTimeout())\n                .addArgs(\"submodule\", \"sync\")\n                .build());\n\n        String[] modulePaths;\n\n        try {\n            modulePaths = exec(\n                    Command.builder()\n                            .workDir(workDir)\n                            .timeout(cfg.defaultOperationTimeout())\n                            .addArgs(\"config\", \"--file\", \".gitmodules\", \"--name-only\", \"--get-regexp\", \"path\")\n                    .build())\n                    .split(\"\\\\r?\\\\n\");\n        } catch (RepositoryException e) {\n            log.warn(\"updateSubmodules ['{}'] -> error while retrieving the list of submodules: {}\", workDir, e.getMessage());\n            return;\n        }\n\n        List<String> args = new ArrayList<>();\n        args.add(\"submodule\");\n        args.add(\"update\");\n\n        args.add(\"--init\");\n        args.add(\"--recursive\");\n\n        for (String mp : modulePaths) {\n            if (mp.trim().isEmpty()) {\n                continue;\n            }\n\n            String moduleName = mp.substring(\"submodule.\".length(), mp.length() - \".path\".length());\n\n            String url = getSubmoduleUrl(workDir, moduleName);\n            if (url == null) {\n                throw new RepositoryException(\"Empty repository for \" + moduleName);\n            }\n\n            String pUrl = updateUrl(url, secret);\n            if (!pUrl.equals(url)) {\n                exec(Command.builder()\n                        .workDir(workDir)\n                        .timeout(cfg.defaultOperationTimeout())\n                        .addArgs(\"config\", \"submodule.\" + moduleName + \".url\", pUrl)\n                        .build());\n            }\n\n            String sModulePath = getSubmodulePath(workDir, moduleName);\n\n            List<String> perModuleArgs = new ArrayList<>(args);\n            perModuleArgs.add(sModulePath);\n            execWithCredentials(Command.builder()\n                            .workDir(workDir)\n                            .timeout(cfg.fetchTimeout())\n                            .addAllArgs(perModuleArgs)\n                            .build(),\n                    secret);\n        }\n    }\n\n    private void resetSubmodules(Path workDir) {\n        exec(Command.builder()\n                .workDir(workDir)\n                .timeout(cfg.defaultOperationTimeout())\n                .addArgs(\"submodule\", \"foreach\", \"git\", \"reset\", \"--hard\")\n                .build());\n    }\n\n    private String getSubmoduleUrl(Path workDir, String name) {\n        String result = exec(Command.builder()\n                .workDir(workDir)\n                .timeout(cfg.defaultOperationTimeout())\n                .addArgs(\"config\", \"-f\", \".gitmodules\", \"--get\", \"submodule.\" + name + \".url\")\n                .build());\n        String s = firstLine(result);\n        return s != null ? s.trim() : null;\n    }\n\n    private String getSubmodulePath(Path workDir, String name) {\n        String result = exec(Command.builder()\n                .workDir(workDir)\n                .timeout(cfg.defaultOperationTimeout())\n                .addArgs(\"config\", \"-f\", \".gitmodules\", \"--get\", \"submodule.\" + name + \".path\")\n                .build());\n        String s = firstLine(result);\n        return s != null ? s.trim() : null;\n    }\n\n    private CommitInfo getCommitInfo(Path path) {\n        String result = exec(Command.builder()\n                .workDir(path)\n                .timeout(cfg.defaultOperationTimeout())\n                .addArgs(\"log\", \"-1\", \"--format=%an (%ae)%n%s%n%b\")\n                .build());\n        String[] info = result.split(\"\\n\");\n        if (info.length < 1) {\n            return CommitInfo.builder().build();\n        }\n        String author = info[0];\n        StringBuilder message = new StringBuilder();\n        for (int i = 1; i < info.length; i++) {\n            message.append(info[i]).append(\"\\n\");\n        }\n\n        return CommitInfo.builder()\n                .message(message.toString())\n                .author(author)\n                .build();\n    }\n\n    private String exec(Command command) {\n        List<String> cmd = new ArrayList<>(command.args().size() + 1);\n        cmd.add(\"git\");\n        cmd.addAll(command.args());\n\n        ProcessBuilder pb = new ProcessBuilder(cmd)\n                .directory(command.workDir().toFile());\n\n        Map<String, String> env = pb.environment();\n        env.putAll(command.env());\n\n        // Prevent interactive credential input.\n        if (!env.containsKey(\"GIT_ASKPASS\")) {\n            env.put(\"GIT_ASKPASS\", \"echo\");\n        }\n\n        log.info(\"> {}\", hideSensitiveData(String.join(\" \", cmd)));\n\n        try {\n            Process p = pb.start();\n\n            Future<StringBuilder> out = executor.submit(withMdc(() -> {\n                StringBuilder sb = new StringBuilder();\n                try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()))) {\n                    String line;\n                    long bytesRead = 0;\n\n                    while ((line = reader.readLine()) != null) {\n                        bytesRead += line.getBytes().length;\n\n                        log.info(\"GIT (stdout): {}\", hideSensitiveData(line));\n                        if (bytesRead <= cfg.maxGitCliOutputBytes()) {\n                            sb.append(line).append(\"\\n\");\n                        }\n                    }\n                }\n                return sb;\n            }));\n\n            Future<StringBuilder> error = executor.submit(withMdc(() -> {\n                StringBuilder sb = new StringBuilder();\n                try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getErrorStream()))) {\n                    String line;\n                    long bytesRead = 0;\n                    while ((line = reader.readLine()) != null) {\n                        bytesRead += line.getBytes().length;\n\n                        log.info(\"GIT (stderr): {}\", hideSensitiveData(line));\n                        if (bytesRead <= cfg.maxGitCliOutputBytes()) {\n                            sb.append(line).append(\"\\n\");\n                        }\n                    }\n                }\n                return sb;\n            }));\n\n            if (!p.waitFor(command.timeout().toMillis(), TimeUnit.MILLISECONDS)) {\n                p.destroy();\n                throw new RepositoryException(String.format(\"git operation timed out after %sms\", command.timeout()));\n            }\n\n            int code = p.exitValue();\n            if (code != SUCCESS_EXIT_CODE) {\n                String msg = String.format(\"code: %d, %s\", code, hideSensitiveData(error.get().toString()));\n                log.warn(\"exec ['{}'] -> finished with code {}, error: '{}'\",\n                        hideSensitiveData(String.join(\" \", cmd)), code, msg);\n                throw new RepositoryException(msg);\n            }\n\n            return out.get().toString();\n        } catch (ExecutionException | IOException | InterruptedException e) { // NOSONAR\n            log.error(\"exec ['{}'] -> error\", hideSensitiveData(String.join(\" \", cmd)), e);\n            throw new RepositoryException(\"git operation error: \" + e.getMessage());\n        }\n    }\n\n    private String execWithCredentials(Command cmd, Secret secret) {\n        Path key = null;\n        Path ssh = null;\n        Path askpass = null;\n\n        Map<String, String> env = new HashMap<>();\n        env.put(\"GIT_TERMINAL_PROMPT\", \"0\");\n\n        try {\n            if (secret instanceof KeyPair keyPair) {\n                key = createSshKeyFile(keyPair);\n                ssh = createUnixGitSSH(key);\n\n                env.put(\"GIT_SSH\", ssh.toAbsolutePath().toString());\n                env.put(\"GIT_SSH_COMMAND\", ssh.toAbsolutePath().toString());\n\n                // supply a dummy value for DISPLAY so ssh will invoke SSH_ASKPASS\n                if (!env.containsKey(\"DISPLAY\")) {\n                    env.put(\"DISPLAY\", \":\");\n                }\n\n                log.info(\"using GIT_SSH to set credentials\");\n            } else if (secret instanceof UsernamePassword userPass) {\n                askpass = createUnixStandardAskpass(userPass);\n\n                env.put(\"GIT_ASKPASS\", askpass.toAbsolutePath().toString());\n                env.put(\"SSH_ASKPASS\", askpass.toAbsolutePath().toString());\n\n                log.info(\"using GIT_ASKPASS to set credentials \");\n            }\n            // if secret is single-value, it was already applied to the URL in updateUrl()\n\n            env.put(\"GIT_HTTP_LOW_SPEED_LIMIT\", String.valueOf(cfg.httpLowSpeedLimit()));\n            env.put(\"GIT_HTTP_LOW_SPEED_TIME\", String.valueOf(cfg.httpLowSpeedTime().getSeconds()));\n\n            return exec(Command.builder().from(cmd)\n                    .putAllEnv(env)\n                    .build());\n        } catch (IOException e) {\n            throw new RepositoryException(\"Failed to setup credentials\", e);\n        } finally {\n            deleteTempFile(key);\n            deleteTempFile(ssh);\n            deleteTempFile(askpass);\n        }\n    }\n\n    private String hideSensitiveData(String s) {\n        if (s == null) {\n            return null;\n        }\n\n        for (Obfuscation o : sensitiveData) {\n            s = s.replaceAll(o.pattern(), o.replacement());\n        }\n        return s;\n    }\n\n    private Path createUnixGitSSH(Path key) throws IOException {\n        Path ssh = PathUtils.createTempFile(\"ssh\", \".sh\");\n\n        try (PrintWriter w = new PrintWriter(ssh.toFile(), Charset.defaultCharset().toString())) {\n            w.println(\"#!/bin/sh\");\n            // ${SSH_ASKPASS} might be ignored if ${DISPLAY} is not set\n            w.println(\"if [ -z \\\"${DISPLAY}\\\" ]; then\");\n            w.println(\"  DISPLAY=:123.456\");\n            w.println(\"  export DISPLAY\");\n            w.println(\"fi\");\n            w.println(\"ssh -i \\\"\" + key.toAbsolutePath().toString() + \"\\\" -o ServerAliveCountMax=\" + cfg.sshTimeoutRetryCount() +\n                    \" -o ServerAliveInterval=\" + cfg.sshTimeout().getSeconds() +\n                    \" -o StrictHostKeyChecking=no \\\"$@\\\"\");\n        }\n        Files.setPosixFilePermissions(ssh, Set.of(OWNER_READ, OWNER_EXECUTE));\n        return ssh;\n    }\n\n    private static void assertSecret(String url, Secret secret) {\n        if (secret instanceof BinaryDataSecret && !url.trim().startsWith(\"https://\")) {\n            throw new RepositoryException(\"Tokens can only be used for https:// Git URLs\");\n        }\n    }\n\n    private static Path createUnixStandardAskpass(UsernamePassword creds) throws IOException {\n        Path askpass = PathUtils.createTempFile(\"pass\", \".sh\");\n        try (PrintWriter w = new PrintWriter(askpass.toFile(), Charset.defaultCharset().toString())) {\n            w.println(\"#!/bin/sh\");\n            w.println(\"case \\\"$1\\\" in\");\n            w.println(\"Username*) echo '\" + quoteUnixCredentials(creds.getUsername()) + \"' ;;\");\n            w.println(\"Password*) echo '\" + quoteUnixCredentials(new String(creds.getPassword())) + \"' ;;\");\n            w.println(\"esac\");\n        }\n        Files.setPosixFilePermissions(askpass, Set.of(OWNER_READ, OWNER_EXECUTE));\n        return askpass;\n    }\n\n    private static Path createSshKeyFile(KeyPair keyPair) throws IOException {\n        Path keyFile = PathUtils.createTempFile(\"ssh\", \".key\");\n\n        Files.write(keyFile, keyPair.getPrivateKey());\n\n        return keyFile;\n    }\n\n    private static void deleteTempFile(Path tempFile) {\n        if (tempFile == null) {\n            return;\n        }\n\n        try {\n            Files.delete(tempFile);\n        } catch (IOException e) {\n            log.warn(\"can't delete tmp file: {}\", tempFile);\n        }\n    }\n\n    private static String quoteUnixCredentials(String str) {\n        // Assumes string will be used inside of single quotes, as it will\n        // only replace \"'\" substrings.\n        return str.replace(\"'\", \"'\\\\''\");\n    }\n\n    private static boolean hasSubmodules(Path workDir) {\n        return Files.exists(workDir.resolve(\".gitmodules\"));\n    }\n\n    private static String firstLine(String result) {\n        BufferedReader reader = new BufferedReader(new StringReader(result));\n        String line;\n        try {\n            line = reader.readLine();\n            if (line == null) {\n                return null;\n            }\n            if (reader.readLine() != null) { // NOSONAR\n                throw new RepositoryException(\"Unexpected multiple lines: \" + result);\n            }\n        } catch (IOException e) {\n            throw new RepositoryException(\"Error parsing result\", e);\n        }\n\n        return line;\n    }\n\n    private static String fromString(String str) {\n        if (str.length() != OBJECT_ID_STRING_LENGTH) {\n            throw new RuntimeException(\"Invalid object id:\" + str);\n        }\n        return str;\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface Command {\n\n        Path workDir();\n\n        @Value.Default\n        default List<String> args() {\n            return Collections.emptyList();\n        }\n\n        @Value.Default\n        default Map<String, String> env() {\n            return Collections.emptyMap();\n        }\n\n        Duration timeout();\n\n        static ImmutableCommand.Builder builder() {\n            return ImmutableCommand.builder();\n        }\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface Ref {\n\n        String commitId();\n\n        String ref();\n\n        String name();\n\n        default boolean tag() {\n            return ref().equals(\"refs/tags/\" + name());\n        }\n\n        static ImmutableRef.Builder builder() {\n            return ImmutableRef.builder();\n        }\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface NormalizedVersion {\n\n        static NormalizedVersion from(FetchRequest.Version version, Ref ref) {\n            boolean versionRefIsBranchOrTag = ref != null;\n            boolean versionValueIsBranchOrTag = versionRefIsBranchOrTag && version.value().equals(version.ref());\n            String commitId = versionValueIsBranchOrTag ? null : version.value();\n            return ImmutableNormalizedVersion.builder()\n                    .commitId(commitId)\n                    .branchOrTag(version.ref())\n                    .build();\n        }\n\n        @Nullable\n        String commitId();\n\n        @Nullable\n        String branchOrTag();\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface CommitInfo {\n\n        @Nullable\n        String message();\n\n        @Nullable\n        String author();\n\n        static ImmutableCommitInfo.Builder builder() {\n            return ImmutableCommitInfo.builder();\n        }\n    }\n\n    private record Obfuscation(String pattern,\n                               String replacement) {\n    }\n}\n"
  },
  {
    "path": "repository/src/main/java/com/walmartlabs/concord/repository/GitClientConfiguration.java",
    "content": "package com.walmartlabs.concord.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Set;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface GitClientConfiguration {\n\n    Optional<String> oauthToken();\n\n    Optional<String> oauthUsername();\n\n    Optional<String> oauthUrlPattern();\n\n    @Value.Default\n    default Set<String> allowedSchemes() {\n        return Set.of(\"https\", \"http\", \"ssh\", \"classpath\");\n    }\n\n    @Value.Default\n    default Duration defaultOperationTimeout() {\n        return Duration.ofMinutes(10L);\n    }\n\n    @Value.Default\n    default Duration fetchTimeout() {\n        return Duration.ofMinutes(10L);\n    }\n\n    @Value.Default\n    default int httpLowSpeedLimit() {\n        return 0;\n    }\n\n    @Value.Default\n    default Duration httpLowSpeedTime() {\n        return Duration.ofMinutes(0L);\n    }\n\n    @Value.Default\n    default Duration sshTimeout() {\n        return Duration.ofMinutes(10);\n    }\n\n    @Value.Default\n    default int sshTimeoutRetryCount() {\n        return 1;\n    }\n\n    @Value.Default\n    default long maxGitCliOutputBytes() {\n        return 512;\n    }\n\n    static ImmutableGitClientConfiguration.Builder builder() {\n        return ImmutableGitClientConfiguration.builder();\n    }\n}\n"
  },
  {
    "path": "repository/src/main/java/com/walmartlabs/concord/repository/LastModifiedSnapshot.java",
    "content": "package com.walmartlabs.concord.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.FileVisitor;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.nio.file.attribute.FileTime;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class LastModifiedSnapshot implements Snapshot, FileVisitor {\n\n    private final Map<Path, FileTime> files = new HashMap<>();\n\n    @Override\n    public void visit(Path src, Path dst) throws IOException {\n        files.put(dst.normalize(), Files.getLastModifiedTime(dst));\n    }\n\n    @Override\n    public boolean contains(Path path) {\n        return files.get(path.normalize()) != null;\n    }\n\n    @Override\n    public boolean isModified(Path path, BasicFileAttributes attrs) {\n        FileTime prev = files.get(path.normalize());\n        if (prev == null) {\n            return true;\n        }\n\n        return !prev.equals(attrs.lastModifiedTime());\n    }\n}\n"
  },
  {
    "path": "repository/src/main/java/com/walmartlabs/concord/repository/MavenRepositoryProvider.java",
    "content": "package com.walmartlabs.concord.repository;\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.ZipUtils;\nimport com.walmartlabs.concord.dependencymanager.DependencyManager;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * *****\n * Concord\n * -----\n * Copyright (C) 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\npublic class MavenRepositoryProvider implements RepositoryProvider {\n\n    private static final String URL_PREFIX = \"mvn://\";\n    private static final Logger log = LoggerFactory.getLogger(MavenRepositoryProvider.class);\n    private final DependencyManager dependencyManager;\n\n    public MavenRepositoryProvider(DependencyManager dependencyManager) {\n        this.dependencyManager = dependencyManager;\n    }\n\n    /**\n     * @param url maven repo url in format mvn://groupId:artifactId:extension\n     * @return boolean can handle or not\n     */\n    @Override\n    public boolean canHandle(String url) {\n        return url.startsWith(URL_PREFIX);\n    }\n\n    /**\n     * @param request fetchRequest\n     * @return fetchResult\n     */\n    @Override\n    public FetchResult fetch(FetchRequest request) {\n        Path dst = request.destination();\n        try {\n            URI uri = new URI(request.url().concat(\":\").concat(request.version().value()));\n            Path dependencyPath = dependencyManager.resolveSingle(uri).getPath();\n            ZipUtils.unzip(dependencyPath, dst, false, StandardCopyOption.REPLACE_EXISTING);\n            return null;\n        } catch (URISyntaxException | IOException e) {\n            try {\n                PathUtils.deleteRecursively(request.destination());\n            } catch (IOException ee) {\n                log.warn(\"fetch ['{}', '{}', '{}'] -> cleanup error: {}\",\n                        request.url(), request.version(), request.destination(), e.getMessage());\n            }\n            throw new RepositoryException(\"Error while fetching a repository\", e);\n        }\n    }\n\n    /**\n     * @param src source of the fetched repo\n     * @param dst destination to be copied to\n     * @param ignorePatterns ignore some files while copying\n     * @return  snapshot of copied files\n     * @throws IOException exception during IO operation\n     */\n    @Override\n    public Snapshot export(Path src, Path dst, List<String> ignorePatterns) throws IOException {\n        LastModifiedSnapshot snapshot = new LastModifiedSnapshot();\n        List<String> allIgnorePatterns = new ArrayList<>();\n        allIgnorePatterns.addAll(ignorePatterns);\n        PathUtils.copy(src, dst, allIgnorePatterns, snapshot, StandardCopyOption.REPLACE_EXISTING);\n        return snapshot;\n    }\n}\n"
  },
  {
    "path": "repository/src/main/java/com/walmartlabs/concord/repository/Repository.java",
    "content": "package com.walmartlabs.concord.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.annotation.Nullable;\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.List;\n\npublic class Repository {\n\n    private final Path repoPath;\n    private final FetchResult fetchResult;\n    private final RepositoryProvider provider;\n\n    public Repository(Path repoPath, FetchResult fetchResult, RepositoryProvider provider) {\n        this.repoPath = repoPath;\n        this.provider = provider;\n        this.fetchResult = fetchResult;\n    }\n\n    public Snapshot export(Path dst) throws IOException {\n        return provider.export(repoPath, dst, Collections.emptyList());\n    }\n\n    public Snapshot export(Path dst, List<String> ignorePatterns) throws IOException {\n        return provider.export(repoPath, dst, ignorePatterns);\n    }\n\n    public Path path() {\n        return repoPath;\n    }\n\n    @Nullable\n    public FetchResult fetchResult() {\n        return fetchResult;\n    }\n}"
  },
  {
    "path": "repository/src/main/java/com/walmartlabs/concord/repository/RepositoryAccessJournal.java",
    "content": "package com.walmartlabs.concord.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.io.UnsupportedEncodingException;\nimport java.net.URLEncoder;\nimport java.nio.file.FileVisitOption;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class RepositoryAccessJournal {\n\n    private static final Logger log = LoggerFactory.getLogger(RepositoryAccessJournal.class);\n\n    private final Path repoJournalPath;\n    private final ObjectMapper objectMapper;\n    private final Map<String, RepositoryJournalItem> journal;\n\n    public RepositoryAccessJournal(ObjectMapper objectMapper, Path repoJournalPath) throws IOException {\n        this.objectMapper = objectMapper;\n\n        if (Files.notExists(repoJournalPath)) {\n            Files.createDirectories(repoJournalPath);\n        }\n        this.repoJournalPath = repoJournalPath;\n        this.journal = load(repoJournalPath);\n    }\n\n    public void recordAccess(String repoUrl, Path repoLocalPath) throws IOException {\n        RepositoryJournalItem item = RepositoryJournalItem.builder()\n                .repoUrl(repoUrl)\n                .repoPath(repoLocalPath)\n                .lastAccess(System.currentTimeMillis())\n                .build();\n        journal.put(repoUrl, item);\n\n        objectMapper.writeValue(repoJournalPath(repoUrl).toFile(), item);\n    }\n\n    public void removeRecord(String repoUrl) throws IOException {\n        Files.deleteIfExists(repoJournalPath(repoUrl));\n        journal.remove(repoUrl);\n    }\n\n    public List<RepositoryJournalItem> listOld(long age) {\n        long now = System.currentTimeMillis();\n        return journal.values().stream()\n                .filter(j -> j.lastAccess() + age < now)\n                .collect(Collectors.toList());\n    }\n\n    private RepositoryJournalItem loadItem(Path p) {\n        try {\n            return objectMapper.readValue(p.toFile(), RepositoryJournalItem.class);\n        } catch (Exception e) {\n            log.warn(\"loadItem ['{}'] -> error\", p, e);\n            return null;\n        }\n    }\n\n    private Map<String, RepositoryJournalItem> load(Path repoJournalPath) throws IOException {\n        Map<String, RepositoryJournalItem> result = new ConcurrentHashMap<>();\n        try (Stream<Path> paths = Files.walk(repoJournalPath, 1, FileVisitOption.FOLLOW_LINKS)) {\n            paths\n                    .filter(Files::isRegularFile)\n                    .filter(p -> p.getFileName().toString().endsWith(\".info.json\"))\n                    .map(this::loadItem)\n                    .filter(Objects::nonNull)\n                    .forEach(i -> result.put(i.repoUrl(), i));\n        }\n        return result;\n    }\n\n    private Path repoJournalPath(String repoUrl) {\n        return repoJournalPath.resolve(encodeUrl(repoUrl) + \".info.json\");\n    }\n\n    private static String encodeUrl(String url) {\n        String encodedUrl;\n        try {\n            encodedUrl = URLEncoder.encode(url, \"UTF-8\");\n        } catch (UnsupportedEncodingException e) {\n            throw new RepositoryException(\"Url encoding error\", e);\n        }\n\n        return encodedUrl;\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    @JsonSerialize(as = ImmutableRepositoryJournalItem.class)\n    @JsonDeserialize(as = ImmutableRepositoryJournalItem.class)\n    interface RepositoryJournalItem {\n\n        String repoUrl();\n\n        Path repoPath();\n\n        long lastAccess();\n\n        static ImmutableRepositoryJournalItem.Builder builder() {\n            return ImmutableRepositoryJournalItem.builder();\n        }\n    }\n}\n"
  },
  {
    "path": "repository/src/main/java/com/walmartlabs/concord/repository/RepositoryCache.java",
    "content": "package com.walmartlabs.concord.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.google.common.util.concurrent.Striped;\nimport com.walmartlabs.concord.common.PathUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.io.UnsupportedEncodingException;\nimport java.net.URLEncoder;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.locks.Lock;\n\npublic class RepositoryCache {\n\n    private static final Logger log = LoggerFactory.getLogger(RepositoryCache.class);\n\n    private final Path cacheDir;\n    private final RepositoryAccessJournal accessJournal;\n    private final long lockTimeout;\n    private final long maxCacheAge;\n\n    private final Striped<Lock> locks;\n\n    public RepositoryCache(Path cacheDir,\n                           Path repoJournalPath,\n                           Duration lockTimeout,\n                           Duration maxCacheAge,\n                           int lockCount,\n                           ObjectMapper objectMapper) throws IOException {\n\n        this.cacheDir = cacheDir;\n        this.lockTimeout = lockTimeout.toMillis();\n        this.accessJournal = maxCacheAge.toMillis() > 0 ? new RepositoryAccessJournal(objectMapper, repoJournalPath) : null;\n        this.maxCacheAge = maxCacheAge.toMillis();\n        this.locks = Striped.lock(lockCount);\n    }\n\n    public Path getPath(String repositoryUrl) {\n        String encodedUrl = encodeUrl(repositoryUrl);\n        Path repoPath = cacheDir.resolve(encodedUrl);\n\n        if (accessJournal != null) {\n            try {\n                accessJournal.recordAccess(repositoryUrl, repoPath);\n            } catch (IOException e) {\n                throw new RepositoryException(\"Error while writing repository cache info\", e);\n            }\n        }\n        \n        return repoPath;\n    }\n\n    public <T> T withLock(String repoUrl, Callable<T> f) {\n        return withLock(lockTimeout, repoUrl, f);\n    }\n\n    private <T> T withLock(long lockTimeout, String repoUrl, Callable<T> f) {\n        Lock l = locks.get(repoUrl);\n        try {\n            if (l.tryLock(lockTimeout, TimeUnit.MILLISECONDS)) {\n                try {\n                    return f.call();\n                } catch (IllegalArgumentException e) {\n                    throw e;\n                } catch (Exception e) {\n                    throw new RuntimeException(e.getMessage(), e);\n                } finally {\n                    l.unlock();\n                }\n            }\n            throw new IllegalStateException(\"Timeout waiting for the repository lock. Repository url: \" + repoUrl);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n            throw new RuntimeException(e);\n        }\n    }\n\n    public void cleanup() {\n        if (maxCacheAge == 0) {\n            return;\n        }\n\n        List<RepositoryAccessJournal.RepositoryJournalItem> oldItems = accessJournal.listOld(maxCacheAge);\n        for (RepositoryAccessJournal.RepositoryJournalItem i : oldItems) {\n            Path repoPath = withLock(lockTimeout, i.repoUrl(), () -> {\n                try {\n                    Path tmpDir = null;\n\n                    if (Files.exists(i.repoPath())) {\n                        tmpDir = i.repoPath().getParent().resolve(i.repoPath().getFileName() + \".tmp\");\n                        Files.move(i.repoPath(), tmpDir);\n                    }\n                    \n                    accessJournal.removeRecord(i.repoUrl());\n                    return tmpDir;\n                } catch (IOException e) {\n                    log.warn(\"cleanup ['{}'] -> move error\", i.repoPath(), e);\n                }\n                return null;\n            });\n\n            if (repoPath != null) {\n                try {\n                    PathUtils.deleteRecursively(repoPath);\n                } catch (IOException e) {\n                    log.warn(\"cleanup ['{}'] -> delete error\", i.repoPath(), e);\n                }\n            }\n        }\n\n        log.info(\"cleanup -> {} repositories removed\", oldItems.size());\n    }\n\n    public long cleanupInterval() {\n        return maxCacheAge;\n    }\n\n    private static String encodeUrl(String url) {\n        String encodedUrl;\n        try {\n            encodedUrl = URLEncoder.encode(url, \"UTF-8\");\n        } catch (UnsupportedEncodingException e) {\n            throw new RepositoryException(\"Url encoding error\", e);\n        }\n\n        return encodedUrl;\n    }\n}\n"
  },
  {
    "path": "repository/src/main/java/com/walmartlabs/concord/repository/RepositoryException.java",
    "content": "package com.walmartlabs.concord.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport java.io.Serial;\n\npublic class RepositoryException extends RuntimeException {\n\n    @Serial\n    private static final long serialVersionUID = 1L;\n\n    public RepositoryException(String message) {\n        super(message);\n    }\n\n    public RepositoryException(String message, Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "repository/src/main/java/com/walmartlabs/concord/repository/RepositoryProvider.java",
    "content": "package com.walmartlabs.concord.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.util.List;\n\npublic interface RepositoryProvider {\n\n    boolean canHandle(String url);\n\n    FetchResult fetch(FetchRequest request);\n\n    Snapshot export(Path src, Path dst, List<String> ignorePatterns) throws IOException;\n}"
  },
  {
    "path": "repository/src/main/java/com/walmartlabs/concord/repository/RepositoryProviders.java",
    "content": "package com.walmartlabs.concord.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\n\npublic class RepositoryProviders {\n\n    private final List<RepositoryProvider> providers;\n\n    public RepositoryProviders(List<RepositoryProvider> providers) {\n        this.providers = providers;\n    }\n\n    public Repository fetch(FetchRequest request, String path) {\n        RepositoryProvider provider = getProvider(request.url());\n        FetchResult result = provider.fetch(request);\n\n        Path repoPath = repoPath(request.destination(), path);\n\n        return new Repository(repoPath, result, provider);\n    }\n\n    private RepositoryProvider getProvider(String url) {\n        return providers.stream()\n                .filter(p -> p.canHandle(url))\n                .findFirst()\n                .orElseThrow(() -> new RuntimeException(\"Can't find provider for '\" + url + \"'\"));\n    }\n\n    private static String normalizePath(String s) {\n        if (s == null) {\n            return null;\n        }\n\n        while (s.startsWith(\"/\")) {\n            s = s.substring(1);\n        }\n\n        while (s.endsWith(\"/\")) {\n            s = s.substring(0, s.length() - 1);\n        }\n\n        if (s.trim().isEmpty()) {\n            return null;\n        }\n\n        return s;\n    }\n\n    private static Path repoPath(Path baseDir, String p) {\n        String normalized = normalizePath(p);\n        if (normalized == null) {\n            return baseDir;\n        }\n\n        Path repoDir = baseDir.resolve(normalized);\n        if (!Files.exists(repoDir)) {\n            throw new RepositoryException(\"Invalid repository path: '\" + p + \"' doesn't exist\");\n        } else if (!repoDir.toFile().isDirectory()) {\n            throw new RepositoryException(\"Invalid repository path: '\" + p + \"' must be a valid directory\");\n        }\n\n        return repoDir;\n    }\n}\n"
  },
  {
    "path": "repository/src/main/java/com/walmartlabs/concord/repository/Snapshot.java",
    "content": "package com.walmartlabs.concord.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.nio.file.Path;\nimport java.nio.file.attribute.BasicFileAttributes;\n\n/**\n * Reflects the state of the process working directory while it is processed\n * by the Server. Allows the Server to know which files are updated during the\n * working directory preparation and which can be recreated using external\n * sources (i.e. SCM repositories).\n */\npublic interface Snapshot {\n\n    /**\n     * @param file absolute path\n     */\n    static Snapshot singleFile(Path file) {\n        return new Snapshot() {\n            @Override\n            public boolean isModified(Path path, BasicFileAttributes attrs) {\n                return true;\n            }\n\n            @Override\n            public boolean contains(Path path) {\n                return file.equals(path);\n            }\n        };\n    }\n\n    static Snapshot includeAll() {\n        return new Snapshot() {\n\n            @Override\n            public boolean isModified(Path path, BasicFileAttributes attrs) {\n                return true;\n            }\n\n            @Override\n            public boolean contains(Path path) {\n                return true;\n            }\n        };\n    }\n\n    boolean isModified(Path path, BasicFileAttributes attrs);\n\n    boolean contains(Path path);\n}\n"
  },
  {
    "path": "repository/src/test/java/com/walmartlabs/concord/repository/GitClientFetch2Test.java",
    "content": "package com.walmartlabs.concord.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AuthTokenProvider;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.TemporaryPath;\nimport org.eclipse.jgit.revwalk.RevCommit;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.Duration;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\n/**\n * test for checkout prev commitId on master branch + checkAlreadyFetched=true\n */\n@ExtendWith(MockitoExtension.class)\npublic class GitClientFetch2Test {\n\n    private GitClient client;\n\n    @Mock\n    AuthTokenProvider authProvider;\n\n    @BeforeEach\n    public void init() {\n        client = new GitClient(GitClientConfiguration.builder()\n                .sshTimeout(Duration.ofMinutes(10))\n                .sshTimeoutRetryCount(1)\n                .httpLowSpeedLimit(1)\n                .httpLowSpeedTime(Duration.ofMinutes(10))\n                .build(), authProvider);\n    }\n\n    @Test\n    public void testFetchByBranchAndPrevCommit() throws Exception {\n        Path repo = GitUtils.createBareRepository(resourceToPath(\"/master\"));\n        RevCommit commit0 = GitUtils.addContent(repo, resourceToPath(\"/test5/0_concord.yml\"));\n        GitUtils.addContent(repo, resourceToPath(\"/test5/1_concord.yml\"));\n\n        try (TemporaryPath repoPath = PathUtils.tempDir(\"git-client-test\")) {\n            // fetch master\n            fetch(repo.toString(), \"master\", null, repoPath.path());\n            assertContent(repoPath, \"master.txt\", \"master\");\n            assertContent(repoPath, \"0_concord.yml\", \"0-concord-content\");\n            assertContent(repoPath, \"1_concord.yml\", \"1-concord-content\");\n\n            // fetch master+prev commitId\n            fetch(repo.toString(), \"master\", commit0.name(), repoPath.path());\n            assertNoContent(repoPath, \"1_concord.yml\");\n        }\n    }\n\n    @Test\n    public void testFetchByPrevCommit() throws Exception {\n        Path repo = GitUtils.createBareRepository(resourceToPath(\"/master\"));\n        RevCommit commit0 = GitUtils.addContent(repo, resourceToPath(\"/test5/0_concord.yml\"));\n        GitUtils.addContent(repo, resourceToPath(\"/test5/1_concord.yml\"));\n\n        try (TemporaryPath repoPath = PathUtils.tempDir(\"git-client-test\")) {\n            // fetch master\n            fetch(repo.toString(), \"master\", null, repoPath.path());\n            assertContent(repoPath, \"master.txt\", \"master\");\n            assertContent(repoPath, \"0_concord.yml\", \"0-concord-content\");\n            assertContent(repoPath, \"1_concord.yml\", \"1-concord-content\");\n\n            System.out.println(\"fetching prev commit\");\n\n            // fetch prev commitId\n            fetch(repo.toString(), null, commit0.name(), repoPath.path());\n            assertNoContent(repoPath, \"1_concord.yml\");\n        }\n    }\n\n    @Test\n    public void testReFetch() throws Exception {\n        Path repo = GitUtils.createBareRepository(resourceToPath(\"/master\"));\n        RevCommit commit0 = GitUtils.addContent(repo, resourceToPath(\"/test5/0_concord.yml\"));\n        GitUtils.addContent(repo, resourceToPath(\"/test5/1_concord.yml\"));\n\n        try (TemporaryPath repoPath = PathUtils.tempDir(\"git-client-test\")) {\n            // fetch master\n            fetch(repo.toString(), \"master\", null, repoPath.path());\n            assertContent(repoPath, \"master.txt\", \"master\");\n            assertContent(repoPath, \"0_concord.yml\", \"0-concord-content\");\n            assertContent(repoPath, \"1_concord.yml\", \"1-concord-content\");\n\n            System.out.println(\"refetching\");\n\n            // refetch master\n            fetch(repo.toString(), \"master\", null, repoPath.path());\n            assertContent(repoPath, \"1_concord.yml\", \"1-concord-content\");\n        }\n    }\n\n    private String fetch(String repoUri, String branch, String commitId, Path dest) {\n        return client.fetch(FetchRequest.builder()\n                .url(repoUri)\n                .version(FetchRequest.Version.commitWithBranch(commitId, branch))\n                .destination(dest)\n                .shallow(true)\n                .checkAlreadyFetched(true)\n                .build()).head();\n    }\n\n    private static void assertNoContent(TemporaryPath repoPath, String path) {\n        assertTrue(Files.notExists(repoPath.path().resolve(path)));\n    }\n\n    private static void assertContent(TemporaryPath repoPath, String path, String expectedContent) throws IOException {\n        assertEquals(expectedContent, new String(Files.readAllBytes(repoPath.path().resolve(path))).trim());\n    }\n\n    private static Path resourceToPath(String resource) throws Exception {\n        return Paths.get(GitClientFetch2Test.class.getResource(resource).toURI());\n    }\n}\n"
  },
  {
    "path": "repository/src/test/java/com/walmartlabs/concord/repository/GitClientFetchTest.java",
    "content": "package com.walmartlabs.concord.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AuthTokenProvider;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.TemporaryPath;\nimport com.walmartlabs.concord.sdk.Secret;\nimport org.eclipse.jgit.api.Git;\nimport org.eclipse.jgit.api.errors.GitAPIException;\nimport org.eclipse.jgit.revwalk.RevCommit;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardCopyOption;\nimport java.time.Duration;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n@ExtendWith(MockitoExtension.class)\npublic class GitClientFetchTest {\n\n    private GitClient client;\n\n    @Mock\n    AuthTokenProvider authProvider;\n\n    @BeforeEach\n    public void init() {\n        client = new GitClient(GitClientConfiguration.builder()\n                .addAllowedSchemes(\"file\", \"ssh\")\n                .sshTimeout(Duration.ofMinutes(10))\n                .sshTimeoutRetryCount(1)\n                .httpLowSpeedLimit(1)\n                .httpLowSpeedTime(Duration.ofMinutes(10))\n                .build(), authProvider);\n    }\n\n    @Test\n    public void testFetch1() throws Exception {\n        Path tmpDir = PathUtils.createTempDir(\"test\");\n\n        PathUtils.copy(resourceToPath(\"/test4\"), tmpDir);\n\n        // init repo\n        Git repo = Git.init().setInitialBranch(\"master\").setDirectory(tmpDir.toFile()).call();\n        repo.add().addFilepattern(\".\").call();\n        RevCommit initialCommit = commit(repo, \"import\");\n\n        try (TemporaryPath repoPath = PathUtils.tempDir(\"git-client-test\")) {\n            // --- fetch master\n            String actualCommitId = fetch(tmpDir.toUri().toString(), \"master\", null, null, repoPath.path());\n            assertContent(repoPath, \"concord.yml\", \"concord-init\");\n            assertEquals(initialCommit.name(), actualCommitId);\n\n            // update file in repo\n            Files.copy(tmpDir.resolve(\"new_concord.yml\"), tmpDir.resolve(\"concord.yml\"), StandardCopyOption.REPLACE_EXISTING);\n            repo.add().addFilepattern(\".\").call();\n            RevCommit commitAfterUpdate = commit(repo, \"update\");\n\n            // --- fetch prev commit\n            String prevCommit = fetch(tmpDir.toUri().toString(), \"master\", initialCommit.name(), null, repoPath.path());\n            assertContent(repoPath, \"concord.yml\", \"concord-init\");\n            assertEquals(initialCommit.name(), prevCommit);\n\n            // --- fetch master again\n            actualCommitId = fetch(tmpDir.toUri().toString(), \"master\", null, null, repoPath.path());\n            assertContent(repoPath, \"concord.yml\", \"new-concord-content\");\n            assertEquals(commitAfterUpdate.name(), actualCommitId);\n        }\n    }\n\n    @Test\n    public void testFetch2() throws Exception {\n        Path repo = GitUtils.createBareRepository(resourceToPath(\"/master\"));\n        RevCommit commit0 = GitUtils.addContent(repo, resourceToPath(\"/test5/0_concord.yml\"));\n        RevCommit commit1 = GitUtils.addContent(repo, resourceToPath(\"/test5/1_concord.yml\"));\n        RevCommit commit2 = GitUtils.addContent(repo, resourceToPath(\"/test5/2_concord.yml\"));\n        List<RevCommit> commits = Arrays.asList(commit0, commit1, commit2);\n\n        // fetch by commit + branch with clean repo\n        for (int i = 0; i < 3; i++) {\n            String commitId = commits.get(i).name();\n            try (TemporaryPath repoPath = PathUtils.tempDir(\"git-client-test\")) {\n                String result = fetch(repo.toString(), \"master\", commitId, null, repoPath.path());\n                assertContent(repoPath, i + \"_concord.yml\", i + \"-concord-content\");\n                assertEquals(commitId, result);\n            }\n        }\n\n        // fetch by commit with clean repo\n        for (int i = 0; i < 3; i++) {\n            String commitId = commits.get(i).name();\n            try (TemporaryPath repoPath = PathUtils.tempDir(\"git-client-test\")) {\n                String result = fetch(repo.toString(), null, commitId, null, repoPath.path());\n                assertContent(repoPath, i + \"_concord.yml\", i + \"-concord-content\");\n                assertEquals(commitId, result);\n            }\n        }\n    }\n\n    @Test\n    public void testFetch3() throws Exception {\n        Path repo = GitUtils.createBareRepository(resourceToPath(\"/master\"));\n        GitUtils.createNewBranch(repo, \"branch-1\", resourceToPath(\"/branch-1\"));\n        GitUtils.createNewTag(repo, \"tag-1\", resourceToPath(\"/tag-1\"));\n\n        String commitId;\n        String tagCommitId;\n\n        try (TemporaryPath repoPath = PathUtils.tempDir(\"git-client-test\")) {\n            // fetch master\n            fetch(repo.toString(), \"master\", null, null, repoPath.path());\n            assertContent(repoPath, \"master.txt\", \"master\");\n\n            // fetch branch\n            commitId = fetch(repo.toString(), \"branch-1\", null, null, repoPath.path());\n            assertContent(repoPath, \"branch-1.txt\", \"branch-1\");\n\n            // fetch tag\n            tagCommitId = fetch(repo.toString(), \"tag-1\", null, null, repoPath.path());\n            assertContent(repoPath, \"tag-1.txt\", \"tag-1\");\n\n            // fetch by commit\n            fetch(repo.toString(), null, commitId, null, repoPath.path());\n            assertContent(repoPath, \"branch-1.txt\", \"branch-1\");\n\n            fetch(repo.toString(), null, tagCommitId, null, repoPath.path());\n            assertContent(repoPath, \"tag-1.txt\", \"tag-1\");\n        }\n\n        // fetch by commit with clean repo\n        try (TemporaryPath repoPath = PathUtils.tempDir(\"git-client-test\")) {\n            String result = fetch(repo.toString(), \"branch-1\", commitId, null, repoPath.path());\n            assertContent(repoPath, \"branch-1.txt\", \"branch-1\");\n            assertEquals(result, commitId);\n        }\n\n        // fetch by commit with clean repo\n        try (TemporaryPath repoPath = PathUtils.tempDir(\"git-client-test\")) {\n            String result = fetch(repo.toString(), \"tag-1\", tagCommitId, null, repoPath.path());\n            assertContent(repoPath, \"tag-1.txt\", \"tag-1\");\n            assertEquals(result, tagCommitId);\n        }\n\n        // fetch by commit with clean repo and without branch -> should  fetch all repo and checkout commit-id\n        try (TemporaryPath repoPath = PathUtils.tempDir(\"git-client-test\")) {\n            String result = fetch(repo.toString(), null, commitId, null, repoPath.path());\n            assertContent(repoPath, \"branch-1.txt\", \"branch-1\");\n            assertEquals(result, commitId);\n        }\n\n        // fetch same branch two times\n        try (TemporaryPath repoPath = PathUtils.tempDir(\"git-client-test\")) {\n            // fetch branch\n            fetch(repo.toString(), \"branch-1\", null, null, repoPath.path());\n            assertContent(repoPath, \"branch-1.txt\", \"branch-1\");\n\n            // fetch branch\n            fetch(repo.toString(), \"branch-1\", null, null, repoPath.path());\n            assertContent(repoPath, \"branch-1.txt\", \"branch-1\");\n        }\n    }\n\n    private String fetch(String repoUri, String branch, String commitId, Secret secret, Path dest) {\n        return client.fetch(FetchRequest.builder()\n                .url(repoUri)\n                .version(FetchRequest.Version.commitWithBranch(commitId, branch))\n                .secret(secret)\n                .destination(dest)\n                .shallow(true)\n                .build()).head();\n    }\n\n    private static RevCommit commit(Git repo, String message) throws GitAPIException {\n        return repo.commit()\n                .setSign(false)\n                .setMessage(message)\n                .call();\n    }\n\n    private static void assertContent(TemporaryPath repoPath, String path, String expectedContent) throws IOException {\n        assertEquals(expectedContent, new String(Files.readAllBytes(repoPath.path().resolve(path))).trim());\n    }\n\n    private static Path resourceToPath(String resource) throws Exception {\n        return Paths.get(GitClientFetchTest.class.getResource(resource).toURI());\n    }\n}\n"
  },
  {
    "path": "repository/src/test/java/com/walmartlabs/concord/repository/GitClientRealTest.java",
    "content": "package com.walmartlabs.concord.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AuthTokenProvider;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.TemporaryPath;\nimport com.walmartlabs.concord.common.secret.KeyPair;\nimport com.walmartlabs.concord.common.secret.UsernamePassword;\nimport com.walmartlabs.concord.sdk.Secret;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.Duration;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n@Disabled\n@ExtendWith(MockitoExtension.class)\npublic class GitClientRealTest {\n\n    private static final String HTTPS_REPO_URL = System.getenv(\"HTTPS_REPO_URL\");\n    private static final String SSH_REPO_URL = System.getenv(\"SSH_REPO_URL\");\n\n    private static final String HTTPS_SUBMODULE_REPO_URL = System.getenv(\"HTTPS_SUBMODULE_REPO_URL\");\n\n    private static final Secret USERNAME_PASSWORD = new UsernamePassword(System.getenv(\"GIT_TEST_USER\"), System.getenv(\"GIT_TEST_USER_PASSWD\").toCharArray());\n    private static final Secret KEYPAIR = createKeypair();\n\n    private static Secret createKeypair() {\n        try {\n            return new KeyPair(\n                    Files.readAllBytes(Paths.get(System.getenv(\"GIT_TEST_PUBLIC_KEY\"))),\n                    Files.readAllBytes(Paths.get(System.getenv(\"GIT_TEST_PRIVATE_KEY\")))\n            );\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private GitClient client;\n\n    @Mock\n    AuthTokenProvider authProvider;\n\n    @BeforeEach\n    public void init() {\n        client = new GitClient(GitClientConfiguration.builder()\n                .oauthToken(System.getenv(\"GIT_TEST_OAUTH_TOKEN\"))\n                .sshTimeout(Duration.ofMinutes(10))\n                .sshTimeoutRetryCount(1)\n                .httpLowSpeedLimit(1)\n                .httpLowSpeedTime(Duration.ofMinutes(10))\n                .build(), authProvider);\n    }\n\n    @Test\n    public void testFetchBranch() throws Exception {\n        String branch = \"master\";\n\n        assertFetchHttps(branch, null, \"master\");\n        assertFetchSsh(branch, null, \"master\");\n    }\n\n    @Test\n    public void testFetchTag() throws Exception {\n        String tag = \"test/tag-1\";\n\n        assertFetchHttps(tag, null, \"tag-1\");\n        assertFetchSsh(tag, null, \"tag-1\");\n    }\n\n    @Test\n    public void testFetchCommitId() throws Exception {\n        String commitId = System.getenv(\"GIT_TEST_COMMIT_ID\");\n\n        assertFetchHttps(null, commitId, \"commit-id\");\n        assertFetchSsh(null, commitId, \"commit-id\");\n    }\n\n    @Test\n    public void testFetchWithSubmodules() throws Exception {\n        String branch = \"master\";\n        String commitId = null;\n\n        // with default oauth token\n        Secret secret = null;\n        try (TemporaryPath repoPath = PathUtils.tempDir(\"git-client-test\")) {\n            fetch(HTTPS_SUBMODULE_REPO_URL, branch, commitId, secret, repoPath.path());\n\n            assertEquals(\"master\", new String(Files.readAllBytes(repoPath.path().resolve(\"test\"))).trim());\n            assertEquals(\"master\", new String(Files.readAllBytes(repoPath.path().resolve(\"concord_poc\").resolve(\"test\"))).trim());\n        }\n    }\n\n    private void assertFetchHttps(String branchOrTag, String commitId, String expected) throws IOException {\n        // with default oauth token\n        Secret secret = null;\n        assertFetch(HTTPS_REPO_URL, branchOrTag, commitId, secret, expected);\n\n        // with username/password\n        assertFetch(HTTPS_REPO_URL, branchOrTag, commitId, USERNAME_PASSWORD, expected);\n    }\n\n    private void assertFetchSsh(String branchOrTag, String commitId, String expected) throws IOException {\n        // with key pair\n        assertFetch(SSH_REPO_URL, branchOrTag, commitId, KEYPAIR, expected);\n    }\n\n    private void assertFetch(String url, String branch, String commitId, Secret secret, String expectedContent) throws IOException {\n        try (TemporaryPath repoPath = PathUtils.tempDir(\"git-client-test\")) {\n            fetch(url, branch, commitId, secret, repoPath.path());\n\n            assertEquals(expectedContent, new String(Files.readAllBytes(repoPath.path().resolve(\"test\"))).trim());\n        }\n    }\n\n    private String fetch(String repoUri, String branch, String commitId, Secret secret, Path dest) {\n        return client.fetch(FetchRequest.builder()\n                .url(repoUri)\n                .version(FetchRequest.Version.commitWithBranch(commitId, branch))\n                .secret(secret)\n                .destination(dest)\n                .shallow(true)\n                .build()).head();\n    }\n}\n"
  },
  {
    "path": "repository/src/test/java/com/walmartlabs/concord/repository/GitClientSpeedTest.java",
    "content": "package com.walmartlabs.concord.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AuthTokenProvider;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.TemporaryPath;\nimport com.walmartlabs.concord.sdk.Secret;\nimport org.apache.commons.io.FileUtils;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Duration;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\n/*\n OLD:          NEW:\n #1: 26 MB     #1: 14 MB\n #2: 26 MB     #2: 14 MB\n #3: 26 MB     #3: 15 MB\n               #4: 40 MB\n               #5: 40 MB\n #6: 36 MB     #6: 35 MB\n #7: 36 MB     #7: 35 MB\n #8: 36 MB     #8: 36 MB\n #9: 26 MB     #9: 14 MB\n #10: 26 MB    #10: 14 MB\n */\n\n/**\n * Require internet connection\n */\n@Disabled\n@ExtendWith(MockitoExtension.class)\npublic class GitClientSpeedTest {\n\n    private GitClient client;\n\n    @Mock\n    AuthTokenProvider authProvider;\n\n    @BeforeEach\n    public void init() {\n        client = new GitClient(GitClientConfiguration.builder()\n                .sshTimeout(Duration.ofMinutes(10))\n                .sshTimeoutRetryCount(1)\n                .httpLowSpeedLimit(1)\n                .httpLowSpeedTime(Duration.ofMinutes(10))\n                .build(), authProvider);\n    }\n\n    @Test\n    public void testFetch() throws Exception {\n        String url = \"https://github.com/walmartlabs/concord\";\n        try (TemporaryPath repoPath = PathUtils.tempDir(\"git-client-test\")) {\n            // fetch master\n            fetch(url, \"master\", null, null, repoPath.path());\n            assertContent(repoPath, \"pom.xml\", \"<version>1.73.1-SNAPSHOT</version>\");\n\n            long size = FileUtils.sizeOfDirectory(repoPath.path().toFile());\n            System.out.println(\"#1: \" + FileUtils.byteCountToDisplaySize(size));\n\n            // fetch branch\n            fetch(url, \"1.70.x\", null, null, repoPath.path());\n            assertContent(repoPath, \"pom.xml\", \"<version>1.70.2-SNAPSHOT</version>\");\n\n            size = FileUtils.sizeOfDirectory(repoPath.path().toFile());\n            System.out.println(\"#2: \" + FileUtils.byteCountToDisplaySize(size));\n\n            // fetch tag\n            String tagCommitId = fetch(url, \"1.73.0\", null, null, repoPath.path());\n            assertContent(repoPath, \"pom.xml\", \"<version>1.73.0</version>\");\n            assertEquals(\"9c080a5e3a34dea8ae7c3b19b66a7c26e78a9c62\", tagCommitId);\n\n            size = FileUtils.sizeOfDirectory(repoPath.path().toFile());\n            System.out.println(\"#3: \" + FileUtils.byteCountToDisplaySize(size));\n\n            // fetch by commit\n            fetch(url, null, \"071b966c36f30047aba6e94b49564a836c26bbd5\", null, repoPath.path());\n            assertContent(repoPath, \"pom.xml\", \"<version>1.72.1-SNAPSHOT</version>\");\n\n            size = FileUtils.sizeOfDirectory(repoPath.path().toFile());\n            System.out.println(\"#4: \" + FileUtils.byteCountToDisplaySize(size));\n\n            fetch(url, null, \"910dbf2830f67b04ccbfd6aa0f2fad4aa5b54834\", null, repoPath.path());\n            assertContent(repoPath, \"pom.xml\", \"<version>1.72.0</version>\");\n\n            size = FileUtils.sizeOfDirectory(repoPath.path().toFile());\n            System.out.println(\"#5: \" + FileUtils.byteCountToDisplaySize(size));\n        }\n\n        // fetch by commit with clean repo\n        try (TemporaryPath repoPath = PathUtils.tempDir(\"git-client-test\")) {\n            String result = fetch(url, \"1.63.x\", \"64feb9fe2d518e71a5497ba43132f4dafa1c471f\", null, repoPath.path());\n            assertContent(repoPath, \"pom.xml\", \"<version>1.63.1</version>\");\n            assertEquals(\"64feb9fe2d518e71a5497ba43132f4dafa1c471f\", result);\n\n            long size = FileUtils.sizeOfDirectory(repoPath.path().toFile());\n            System.out.println(\"#6: \" + FileUtils.byteCountToDisplaySize(size));\n        }\n\n        // fetch by commit with clean repo\n        try (TemporaryPath repoPath = PathUtils.tempDir(\"git-client-test\")) {\n            String result = fetch(url, \"1.66.0\", \"56e248cf2e6fa10d058e41aac005b5dee70526e4\", null, repoPath.path());\n            assertContent(repoPath, \"pom.xml\", \"<version>1.65.1-SNAPSHOT</version>\");\n            assertEquals(\"56e248cf2e6fa10d058e41aac005b5dee70526e4\", result);\n\n            long size = FileUtils.sizeOfDirectory(repoPath.path().toFile());\n            System.out.println(\"#7: \" + FileUtils.byteCountToDisplaySize(size));\n        }\n\n        // fetch by commit with clean repo and without branch -> should  fetch all repo and checkout commit-id\n        try (TemporaryPath repoPath = PathUtils.tempDir(\"git-client-test\")) {\n            String result = fetch(url, null, \"56e248cf2e6fa10d058e41aac005b5dee70526e4\", null, repoPath.path());\n            assertContent(repoPath, \"pom.xml\", \"<version>1.65.1-SNAPSHOT</version>\");\n            assertEquals(\"56e248cf2e6fa10d058e41aac005b5dee70526e4\", result);\n\n            long size = FileUtils.sizeOfDirectory(repoPath.path().toFile());\n            System.out.println(\"#8: \" + FileUtils.byteCountToDisplaySize(size));\n        }\n\n        // fetch same branch two times\n        try (TemporaryPath repoPath = PathUtils.tempDir(\"git-client-test\")) {\n            // fetch branch\n            fetch(url, \"1.70.x\", null, null, repoPath.path());\n            assertContent(repoPath, \"pom.xml\", \"<version>1.70.2-SNAPSHOT</version>\");\n\n            long size = FileUtils.sizeOfDirectory(repoPath.path().toFile());\n            System.out.println(\"#9: \" + FileUtils.byteCountToDisplaySize(size));\n\n            // fetch branch\n            fetch(url, \"1.70.x\", null, null, repoPath.path());\n            assertContent(repoPath, \"pom.xml\", \"<version>1.70.2-SNAPSHOT</version>\");\n\n            size = FileUtils.sizeOfDirectory(repoPath.path().toFile());\n            System.out.println(\"#10: \" + FileUtils.byteCountToDisplaySize(size));\n        }\n    }\n\n    private String fetch(String repoUri, String branch, String commitId, Secret secret, Path dest) {\n        return client.fetch(FetchRequest.builder()\n                .url(repoUri)\n                .version(FetchRequest.Version.commitWithBranch(commitId, branch))\n                .secret(secret)\n                .destination(dest)\n                .shallow(true)\n                .build()).head();\n    }\n\n    private static void assertContent(TemporaryPath repoPath, String path, String expectedContent) throws IOException {\n        String current = new String(Files.readAllBytes(repoPath.path().resolve(path)));\n        assertTrue(current.contains(expectedContent));\n    }\n}\n"
  },
  {
    "path": "repository/src/test/java/com/walmartlabs/concord/repository/GitUriTest.java",
    "content": "package com.walmartlabs.concord.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AuthTokenProvider;\nimport com.walmartlabs.concord.common.cfg.OauthTokenConfig;\nimport com.walmartlabs.concord.common.secret.BinaryDataSecret;\nimport com.walmartlabs.concord.sdk.Secret;\nimport org.immutables.value.Value;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nclass GitUriTest {\n    private static final AuthTokenProvider AUTH_PROVIDER = authProvider(null);\n    private static final AuthTokenProvider RESTRICTED_AUTH_PROVIDER = authProvider(\"gitserver.local\");\n\n    private static final GitClientConfiguration cfg = GitClientConfiguration.builder()\n            .oauthToken(\"mock-token\")\n            .build();\n    private static final GitClient client = new GitClient(cfg, AUTH_PROVIDER);\n    private static Secret secret = new BinaryDataSecret(\"secret-mock-token\".getBytes());\n\n    @Test\n    void testSsh() {\n        var sshWithUser = client.updateUrl(\"git@gitserver.local:my-org/my-repo.git\", null);\n        assertEquals(\"git@gitserver.local:my-org/my-repo.git\", sshWithUser);\n\n        var sshNoUser = client.updateUrl(\"gitserver.local:my-org/my-repo.git\", null);\n        assertEquals(\"gitserver.local:my-org/my-repo.git\",\n                sshNoUser);\n    }\n\n    @Test\n    void testHttps() {\n        var httpsDefault = client.updateUrl(\"https://gitserver.local/my-org/my-repo.git\", null);\n        assertEquals(\"https://mock-token@gitserver.local/my-org/my-repo.git\", httpsDefault);\n    }\n\n    @Test\n    void testHttpWithSecret() {\n        var httpsSecret = client.updateUrl(\"https://gitserver.local/my-org/my-repo.git\", secret);\n        assertEquals(\"https://secret-mock-token@gitserver.local/my-org/my-repo.git\", httpsSecret);\n    }\n\n    @Test\n    void testUnrestrictedHost() {\n        var anonAuth = client.updateUrl(\"https://elsewhere.local/my-org/my-repo.git\", null);\n        // backwards-compat, auth added\n        assertEquals(\"https://mock-token@elsewhere.local/my-org/my-repo.git\", anonAuth);\n\n        var url2 = client.updateUrl(\"https://gitserver.local/my-org/my-repo.git\", null);\n        // auth added\n        assertEquals(\"https://mock-token@gitserver.local/my-org/my-repo.git\", url2);\n\n    }\n\n    @Test\n    void testGitHostRestriction() {\n        var restrictedClient = new GitClient(GitClientConfiguration.builder()\n                .from(cfg)\n                .build(), RESTRICTED_AUTH_PROVIDER);\n\n        var anonAuth = restrictedClient.updateUrl(\"https://elsewhere.local/my-org/my-repo.git\", null);\n        // unchanged\n        assertEquals(\"https://elsewhere.local/my-org/my-repo.git\", anonAuth);\n\n        var url2 = restrictedClient.updateUrl(\"https://gitserver.local/my-org/my-repo.git\", null);\n        // auth added\n        assertEquals(\"https://mock-token@gitserver.local/my-org/my-repo.git\", url2);\n    }\n\n    private static AuthTokenProvider authProvider(String urlPattern) {\n        var builder = TestOauthTokenConfig.builder()\n                .oauthToken(\"mock-token\");\n\n        if (urlPattern != null) {\n            builder.oauthUrlPattern(urlPattern);\n        }\n\n        return new AuthTokenProvider.OauthTokenProvider(builder.build());\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface TestOauthTokenConfig extends OauthTokenConfig {\n        static ImmutableTestOauthTokenConfig.Builder builder() {\n            return ImmutableTestOauthTokenConfig.builder();\n        }\n    }\n}\n"
  },
  {
    "path": "repository/src/test/java/com/walmartlabs/concord/repository/GitUtils.java",
    "content": "package com.walmartlabs.concord.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.TemporaryPath;\nimport org.eclipse.jgit.api.Git;\nimport org.eclipse.jgit.revwalk.RevCommit;\nimport org.eclipse.jgit.transport.RefSpec;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\n\npublic final class GitUtils {\n\n    /**\n     * Creates a new bare Git repository using data from the provided\n     * path.\n     */\n    public static Path createBareRepository(Path data) throws Exception {\n        // init bare repository\n        Path tmp = Files.createTempDirectory(\"git-client-test\");\n        Path repo = tmp.resolve(\"test\");\n        Files.createDirectories(repo);\n\n        try (Git git = Git.init()\n                .setInitialBranch(\"master\")\n                .setDirectory(repo.toFile())\n                .call()) {\n\n            // copy our files into the repository\n            PathUtils.copy(data, repo);\n\n            // add and commit copied files\n            git.add().addFilepattern(\".\").call();\n            git.commit().setSign(false).setMessage(\"init from: \" + data).call();\n        }\n\n        return repo;\n    }\n\n    public static RevCommit addContent(Path bareRepo, Path file) throws Exception {\n        try (TemporaryPath tmp = PathUtils.tempDir(\"repo-tmp\");\n             Git git = Git.cloneRepository()\n                     .setDirectory(tmp.path().toFile())\n                     .setURI(bareRepo.toAbsolutePath().toString())\n                     .call()) {\n\n            Files.copy(file, tmp.path().resolve(file.getFileName()), StandardCopyOption.REPLACE_EXISTING);\n\n            git.add().addFilepattern(\".\").call();\n            RevCommit commit = git.commit().setSign(false).setMessage(\"update\").call();\n            git.push().call();\n            return commit;\n        }\n    }\n\n    /**\n     * Creates a new branch in the specified bare Git repository and\n     * adds all files from the {@code src} directory.\n     */\n    public static RevCommit createNewBranch(Path bareRepo, String branch, Path src) throws Exception {\n        Path dir = Files.createTempDirectory(\"repo-tmp\");\n\n        Git git = Git.cloneRepository()\n                .setDirectory(dir.toFile())\n                .setURI(bareRepo.toAbsolutePath().toString())\n                .call();\n\n        git.checkout()\n                .setCreateBranch(true)\n                .setName(branch)\n                .call();\n\n        PathUtils.copy(src, dir, StandardCopyOption.REPLACE_EXISTING);\n\n        git.add()\n                .addFilepattern(\".\")\n                .call();\n\n        RevCommit commit = git.commit()\n                .setSign(false)\n                .setMessage(\"adding files from \" + src.getFileName())\n                .call();\n\n        git.push()\n                .setRefSpecs(new RefSpec(branch + \":\" + branch))\n                .call();\n\n        return commit;\n    }\n\n    /**\n     * Adds all files from the {@code src} directory, commits then to\n     * a bare Git repository and creates a new tag.\n     */\n    public static RevCommit createNewTag(Path bareRepo, String tag, Path src) throws Exception {\n        Path dir = Files.createTempDirectory(\"repo-tmp\");\n\n        Git git = Git.cloneRepository()\n                .setDirectory(dir.toFile())\n                .setURI(bareRepo.toAbsolutePath().toString())\n                .call();\n\n        PathUtils.copy(src, dir, StandardCopyOption.REPLACE_EXISTING);\n\n        git.add()\n                .addFilepattern(\".\")\n                .call();\n\n        RevCommit commit = git.commit()\n                .setSign(false)\n                .setMessage(\"adding files from \" + src.getFileName())\n                .call();\n\n        git.tag().setMessage(\"tagging\").setName(tag).call();\n\n        git.push()\n                .setPushTags()\n                .call();\n\n        return commit;\n    }\n\n    private GitUtils() {\n    }\n}\n"
  },
  {
    "path": "repository/src/test/resources/branch-1/branch-1.txt",
    "content": "branch-1"
  },
  {
    "path": "repository/src/test/resources/logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] [%-5level] %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "repository/src/test/resources/master/master.txt",
    "content": "master"
  },
  {
    "path": "repository/src/test/resources/tag-1/tag-1.txt",
    "content": "tag-1"
  },
  {
    "path": "repository/src/test/resources/test4/concord.yml",
    "content": "concord-init"
  },
  {
    "path": "repository/src/test/resources/test4/new_concord.yml",
    "content": "new-concord-content"
  },
  {
    "path": "repository/src/test/resources/test5/0_concord.yml",
    "content": "0-concord-content"
  },
  {
    "path": "repository/src/test/resources/test5/1_concord.yml",
    "content": "1-concord-content"
  },
  {
    "path": "repository/src/test/resources/test5/2_concord.yml",
    "content": "2-concord-content"
  },
  {
    "path": "runtime/common/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <groupId>com.walmartlabs.concord.runtime</groupId>\n    <artifactId>concord-runtime-common</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-client2</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-forms</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.inject</groupId>\n            <artifactId>guice</artifactId>\n        </dependency>\n\n        <!-- Immutables -->\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.errorprone</groupId>\n            <artifactId>error_prone_annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-compress</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "runtime/common/src/main/java/com/walmartlabs/concord/runtime/common/FormService.java",
    "content": "package com.walmartlabs.concord.runtime.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.forms.Form;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport static java.nio.file.StandardCopyOption.REPLACE_EXISTING;\n\n// TODO make it an interface?\npublic class FormService {\n\n    private final Path dir;\n\n    public FormService(Path dir) {\n        this.dir = dir;\n\n        try {\n            if (Files.notExists(dir)) {\n                Files.createDirectories(dir);\n            }\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public void save(Form form) {\n        try {\n            Path p = PathUtils.assertInPath(dir, form.name());\n            Path tmp = PathUtils.createTempFile(form.name(), \"form\");\n            try (OutputStream out = Files.newOutputStream(tmp)) {\n                SerializationUtils.serialize(out, form);\n            }\n            Files.move(tmp, p, REPLACE_EXISTING);\n        } catch (IOException e) {\n            throw new RuntimeException(\"Error while saving a form ('\" + form.name() + \"') : \" + e.getMessage(), e);\n        }\n    }\n\n    public List<Form> list() {\n        try (Stream<Path> list = Files.list(dir)) {\n            return list.map(this::read)\n                    .collect(Collectors.toList());\n        } catch (IOException e) {\n            throw new RuntimeException(\"Error while getting a list of forms: \" + e.getMessage(), e);\n        }\n    }\n\n    private Form read(Path p) {\n        try (InputStream in = Files.newInputStream(p)) {\n            return SerializationUtils.deserialize(in, Form.class);\n        } catch (IOException e) {\n            throw new RuntimeException(\"Error while reading a form \" + p + \": \" + e.getMessage(), e);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/common/src/main/java/com/walmartlabs/concord/runtime/common/ObjectTruncater.java",
    "content": "package com.walmartlabs.concord.runtime.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.lang.reflect.Array;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic final class ObjectTruncater {\n\n    private static final String MAX_DEPTH_MSG = \"skipped: max depth reached\";\n\n    public static Map<String, Object> truncateMap(Map<String, Object> value, int maxStringLength, int maxArrayLength, int maxDepth) {\n        if (value == null || value.isEmpty()) {\n            return Collections.emptyMap();\n        }\n\n        return truncMap(value, 0, maxStringLength, maxArrayLength, maxDepth);\n    }\n\n    public static Object truncate(Object value, int maxStringLength, int maxArrayLength, int maxDepth) {\n        if (value == null) {\n            return null;\n        }\n\n        return trunc(value, 0, maxStringLength, maxArrayLength, maxDepth);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Object trunc(Object value, int currentDepth, int maxStringLength, int maxArrayLength, int maxDepth) {\n        if (value == null) {\n            return null;\n        }\n\n        if (value instanceof Map) {\n            Map<Object, Object> m = (Map<Object, Object>) value;\n            if (currentDepth > maxDepth) {\n                return Collections.singletonMap(\"_\", MAX_DEPTH_MSG);\n            }\n            return truncMap(m, currentDepth, maxStringLength, maxArrayLength, maxDepth);\n        } else if (value instanceof Collection) {\n            Collection<Object> l = (Collection<Object>) value;\n            if (l.isEmpty()) {\n                return l;\n            }\n\n            if (currentDepth > maxDepth) {\n                return Collections.singletonList(MAX_DEPTH_MSG);\n            }\n\n            return truncArray(l.size(), l::stream, currentDepth, maxStringLength, maxArrayLength, maxDepth);\n        } else if (value.getClass().isArray()) {\n            if (value.getClass().getComponentType().isPrimitive()) {\n                int size = Array.getLength(value);\n                if (size == 0) {\n                    return value;\n                }\n\n                if (currentDepth > maxDepth) {\n                    return new String[]{MAX_DEPTH_MSG};\n                }\n\n                return truncPrimitiveArray(size, value, maxArrayLength);\n            }\n\n            Object[] src = (Object[]) value;\n            if (src.length == 0) {\n                return src;\n            }\n\n            if (currentDepth > maxDepth) {\n                return new String[]{MAX_DEPTH_MSG};\n            }\n            return truncArray(src.length, () -> Stream.of(src), currentDepth, maxStringLength, maxArrayLength, maxDepth);\n        } else if (value instanceof String) {\n            return truncString((String) value, maxStringLength);\n        }\n\n        return value;\n    }\n\n    private static <K> Map<K, Object> truncMap(Map<K, Object> value, int currentDepth, int maxStringLength, int maxArrayLength, int maxDepth) {\n        if (value.isEmpty()) {\n            return value;\n        }\n\n        Map<K, Object> result = new HashMap<>();\n        for (Map.Entry<K, Object> e : value.entrySet()) {\n            result.put(e.getKey(), trunc(e.getValue(), currentDepth + 1, maxStringLength, maxArrayLength, maxDepth));\n        }\n        return result;\n    }\n\n    private static Collection<Object> truncArray(int size, Supplier<Stream<Object>> valueStreamSupplier,\n                                                 int currentDepth, int maxStringLength, int maxArrayLength, int maxDepth) {\n        if (size > maxArrayLength) {\n            int maxOverLimit = maxArrayLength / 10;\n            int overLimit = size - maxArrayLength;\n            if (overLimit > maxOverLimit) {\n                int halfMax = halfSize(maxArrayLength);\n                return Stream.concat(\n                        valueStreamSupplier.get().limit(halfMax)\n                                .map(v -> trunc(v, currentDepth + 1, maxStringLength, maxArrayLength, maxDepth)),\n                        Stream.concat(\n                                Stream.of(\"skipped \" + overLimit + \" lines\"),\n                                valueStreamSupplier.get().skip(halfMax + (long) overLimit)\n                                        .map(v -> trunc(v, currentDepth + 1, maxStringLength, maxArrayLength, maxDepth))))\n                        .collect(Collectors.toList());\n            }\n        }\n\n        return valueStreamSupplier.get()\n                .map(v -> trunc(v, currentDepth + 1, maxStringLength, maxArrayLength, maxDepth))\n                .collect(Collectors.toList());\n    }\n\n    private static Object truncPrimitiveArray(int size, Object array, int maxArrayLength) {\n\n        if (size > maxArrayLength) {\n            Object result = Array.newInstance(array.getClass().getComponentType(), maxArrayLength);\n            for (int i = 0; i < maxArrayLength; i++) {\n                Array.set(result, i, Array.get(array, i));\n            }\n            return result;\n        }\n\n        return array;\n    }\n\n    private static String truncString(String value, int maxStringLength) {\n        if (value.length() <= maxStringLength) {\n            return value;\n        }\n\n        int halfMax = halfSize(maxStringLength);\n        int overlimit = value.length() - maxStringLength;\n        return value.substring(0, halfMax) +\n                \"...[skipped \" + overlimit + \" chars]...\" +\n                value.substring(halfMax + overlimit);\n    }\n\n    private static int halfSize(int size) {\n        int result = size / 2;\n        if (result + result != size) {\n            result += 1;\n        }\n        return result;\n    }\n\n    private ObjectTruncater() {\n    }\n}\n"
  },
  {
    "path": "runtime/common/src/main/java/com/walmartlabs/concord/runtime/common/ProcessHeartbeat.java",
    "content": "package com.walmartlabs.concord.runtime.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.client2.ProcessHeartbeatApi;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Date;\nimport java.util.UUID;\n\npublic class ProcessHeartbeat {\n\n    private static final Logger log = LoggerFactory.getLogger(ProcessHeartbeat.class);\n\n    private static final long HEARTBEAT_INTERVAL = 10000;\n\n    private final ProcessHeartbeatApi processHeartbeatApi;\n    private final UUID instanceId;\n    private final long maxNoHeartbeatInterval;\n    private Thread worker;\n\n    public ProcessHeartbeat(ApiClient client, UUID instanceId, long maxNoHeartbeatInterval) {\n        this.processHeartbeatApi = new ProcessHeartbeatApi(client);\n        this.instanceId = instanceId;\n        this.maxNoHeartbeatInterval = maxNoHeartbeatInterval;\n    }\n\n    public synchronized void start() {\n        if (worker != null) {\n            throw new IllegalArgumentException(\"Heartbeat worker is already running\");\n        }\n\n        worker = new Thread(() -> {\n            log.debug(\"start ['{}'] -> running every {}ms, max interval: {}ms\", instanceId, HEARTBEAT_INTERVAL, maxNoHeartbeatInterval);\n\n            boolean prevPingFailed = false;\n            long lastSuccessPing = System.currentTimeMillis();\n            while (!Thread.currentThread().isInterrupted()) {\n                try {\n                    processHeartbeatApi.pingProcess(instanceId);\n                    lastSuccessPing = System.currentTimeMillis();\n                    if (prevPingFailed) {\n                        log.info(\"heartbeat: ok\");\n                    }\n                    prevPingFailed = false;\n                } catch (Exception e) {\n                    prevPingFailed = true;\n                    log.warn(\"heartbeat: error: {}, last successful at {}\", e.getMessage(), new Date(lastSuccessPing));\n\n                    // check if we hadn't had a successful heartbeat request in a while\n                    long pingInterval = System.currentTimeMillis() - lastSuccessPing;\n                    if (pingInterval > maxNoHeartbeatInterval) {\n                        log.error(\"No heartbeat for more than {}ms, terminating process...\", pingInterval);\n                        System.exit(1);\n                    }\n                }\n\n                try {\n                    Thread.sleep(HEARTBEAT_INTERVAL); // NOSONAR\n                } catch (InterruptedException e) {\n                    Thread.currentThread().interrupt();\n                }\n            }\n\n            log.debug(\"start ['{}'] -> stopped\", instanceId);\n        }, \"process-heartbeat\");\n\n        worker.start();\n    }\n\n    public synchronized void stop() {\n        if (worker == null) {\n            return;\n        }\n\n        worker.interrupt();\n        worker = null;\n    }\n}\n"
  },
  {
    "path": "runtime/common/src/main/java/com/walmartlabs/concord/runtime/common/SensitiveDataMasker.java",
    "content": "package com.walmartlabs.concord.runtime.common;\n\nimport java.util.*;\n\npublic class SensitiveDataMasker {\n\n    private static final String MASK = \"******\";\n\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    public static <T> T mask(T v, Set<String> sensitiveStrings) {\n        if (sensitiveStrings.isEmpty()) {\n            return v;\n        }\n\n        if (v instanceof String s) {\n            for (var sensitiveString : sensitiveStrings) {\n                s = s.replace(sensitiveString, MASK);\n            }\n            return (T) s;\n        } else if (v instanceof List<?> l) {\n            var result = new ArrayList<>(l.size());\n            for (var vv : l) {\n                vv = mask(vv, sensitiveStrings);\n                result.add(vv);\n            }\n            return (T) result;\n        } else if (v instanceof Map m) {\n            var result = new LinkedHashMap<>(m);\n            result.replaceAll((k, vv) -> mask(vv, sensitiveStrings));\n            return (T) result;\n        } else if (v instanceof Set<?> s) {\n            var result = new LinkedHashSet<>(s.size());\n            for (var vv : s) {\n                vv = mask(vv, sensitiveStrings);\n                result.add(vv);\n            }\n            return (T) result;\n        }\n\n        return v;\n    }\n}\n"
  },
  {
    "path": "runtime/common/src/main/java/com/walmartlabs/concord/runtime/common/SerializationUtils.java",
    "content": "package com.walmartlabs.concord.runtime.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.*;\n\npublic final class SerializationUtils {\n\n    private static final Logger log = LoggerFactory.getLogger(SerializationUtils.class);\n\n    public static void serialize(OutputStream out, Serializable o) throws IOException {\n        try (ObjectOutputStream oos = new ObjectOutputStream(out)) {\n            oos.writeObject(o);\n        } catch (NotSerializableException e) {\n            log.warn(\"Check if you're setting any not serializable values in your 'script', 'task' or 'form' steps.\"); // TODO include the original error\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <T> T deserialize(InputStream in, Class<T> expectedType) throws IOException {\n        try (ObjectInputStream ois = new ObjectInputStream(in)) {\n            return (T) ois.readObject();\n        } catch (ClassNotFoundException e) {\n            throw new IOException(\"Can't deserialize a value into \" + expectedType + \": \" + e.getMessage(), e);\n        }\n    }\n\n    private SerializationUtils() {\n    }\n}\n"
  },
  {
    "path": "runtime/common/src/main/java/com/walmartlabs/concord/runtime/common/StateManager.java",
    "content": "package com.walmartlabs.concord.runtime.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ObjectInputStreamWithClassLoader;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.TemporaryPath;\nimport com.walmartlabs.concord.sdk.Constants;\n\nimport java.io.*;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.util.HashSet;\nimport java.util.Set;\n\nimport static java.nio.file.StandardCopyOption.REPLACE_EXISTING;\n\n// TODO rename to PersistenceManager?\npublic final class StateManager {\n\n    private static final String RESUME_MARKER = Constants.Files.RESUME_MARKER_FILE_NAME;\n    private static final String SUSPEND_MARKER = Constants.Files.SUSPEND_MARKER_FILE_NAME;\n\n    public static void finalizeSuspendedState(Path baseDir, Serializable state, Set<String> eventNames) throws IOException {\n        Path stateDir = baseDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(Constants.Files.JOB_STATE_DIR_NAME);\n\n        Path resumeMarker = stateDir.resolve(RESUME_MARKER);\n        if (Files.exists(resumeMarker)) {\n            Files.delete(resumeMarker);\n        }\n\n        Path suspendMarker = stateDir.resolve(SUSPEND_MARKER);\n        if (Files.exists(suspendMarker)) {\n            Files.delete(suspendMarker);\n        }\n\n        if (Files.notExists(stateDir)) {\n            Files.createDirectories(stateDir);\n        }\n\n        Path marker = stateDir.resolve(SUSPEND_MARKER);\n        Files.write(marker, eventNames);\n\n        saveProcessState(baseDir, state);\n    }\n\n    public static void cleanupState(Path baseDir) throws IOException {\n        Path stateDir = baseDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(Constants.Files.JOB_STATE_DIR_NAME);\n\n        PathUtils.deleteRecursively(stateDir);\n\n        Path sessionFilesDir = baseDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(Constants.Files.JOB_SESSION_FILES_DIR_NAME);\n        PathUtils.deleteRecursively(sessionFilesDir);\n    }\n\n    public static void saveResumeEvent(Path baseDir, String eventName) {\n        Path dst = baseDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(Constants.Files.JOB_STATE_DIR_NAME)\n                .resolve(Constants.Files.RESUME_MARKER_FILE_NAME);\n\n        Path parent = dst.getParent();\n\n        try {\n            if (!Files.exists(parent)) {\n                Files.createDirectories(parent);\n            }\n\n            Files.write(dst, eventName.getBytes());\n        } catch (IOException e) {\n            throw new RuntimeException(\"Error while saving a resume event: \" + e.getMessage(), e);\n        }\n    }\n\n    public static Set<String> readResumeEvents(Path baseDir) {\n        Path p = baseDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(Constants.Files.JOB_STATE_DIR_NAME)\n                .resolve(Constants.Files.RESUME_MARKER_FILE_NAME);\n\n        if (!Files.exists(p)) {\n            return null;\n        }\n\n        try {\n            return new HashSet<>(Files.readAllLines(p));\n        } catch (IOException e) {\n            throw new RuntimeException(\"Error while reading a resume event: \" + e.getMessage(), e);\n        }\n    }\n\n    /**\n     * Reads a serialized process state object from\n     * the standard location inside the provided {@code baseDir}.\n     */\n    @SuppressWarnings(\"unchecked\")\n    public static <T extends Serializable> T readProcessState(Path baseDir, ClassLoader cl) {\n        Path stateDir = baseDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(Constants.Files.JOB_STATE_DIR_NAME);\n\n        Path p = stateDir.resolve(\"instance\");\n        if (Files.notExists(p)) {\n            return null;\n        }\n\n        try (ObjectInputStream in = new ObjectInputStreamWithClassLoader(Files.newInputStream(p), cl)) {\n            return (T) in.readObject();\n        } catch (ClassNotFoundException | IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    /**\n     * Serializes the specified process state object into a file\n     * in the standard location inside the provided {@code baseDir}.\n     */\n    public static void saveProcessState(Path baseDir, Serializable state) throws IOException {\n        Path stateDir = baseDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(Constants.Files.JOB_STATE_DIR_NAME);\n\n        if (Files.notExists(stateDir)) {\n            Files.createDirectories(stateDir);\n        }\n\n        Path dst = stateDir.resolve(\"instance\");\n\n        try (TemporaryPath tmp = PathUtils.tempFile(\"instance\", \"state\");\n             OutputStream out = Files.newOutputStream(tmp.path())) {\n\n            SerializationUtils.serialize(out, state);\n            Files.move(tmp.path(), dst, REPLACE_EXISTING);\n        }\n    }\n\n    public static void persist(Path baseDir, String storeName, Serializable object) throws IOException {\n        Path storageDir = baseDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(\"storage\"); // TODO: constants\n\n        if (!Files.exists(storageDir)) {\n            Files.createDirectories(storageDir);\n        }\n\n        Path storage = storageDir.resolve(storeName);\n        try (OutputStream out = Files.newOutputStream(storage, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {\n            SerializationUtils.serialize(out, object);\n        }\n    }\n\n    public static <T extends Serializable> T load(Path baseDir, String storageName, Class<T> expectedType) {\n        Path storage = baseDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(\"storage\")\n                .resolve(storageName); // TODO: constants\n\n        if (Files.notExists(storage)) {\n            return null;\n        }\n\n        try (InputStream in = Files.newInputStream(storage)) {\n            return SerializationUtils.deserialize(in, expectedType);\n        } catch (IOException e) {\n            throw new RuntimeException(\"Error while reading persisted storage \" + storageName + \": \" + e.getMessage(), e);\n        }\n    }\n\n    private StateManager() {\n    }\n}\n"
  },
  {
    "path": "runtime/common/src/main/java/com/walmartlabs/concord/runtime/common/cfg/ApiConfiguration.java",
    "content": "package com.walmartlabs.concord.runtime.common.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport java.util.concurrent.TimeUnit;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableApiConfiguration.class)\n@JsonDeserialize(as = ImmutableApiConfiguration.class)\npublic interface ApiConfiguration {\n\n    /**\n     * Base URL of the server API.\n     */\n    @Value.Default\n    default String baseUrl() {\n        return \"http://localhost:8001\";\n    }\n\n    /**\n     * Connection timeout (ms)\n     */\n    @Value.Default\n    default int connectTimeout() {\n        return 10000;\n    }\n\n    /**\n     * Socket read timeout (ms)\n     */\n    @Value.Default\n    default int readTimeout() {\n        return 60000;\n    }\n\n    /**\n     * Socket write timeout (ms)\n     */\n    @Value.Default\n    default int writeTimeout() {\n        return 30000;\n    }\n\n    /**\n     * Number of retries if an API call fails.\n     */\n    @Value.Default\n    default int retryCount() {\n        return 3;\n    }\n\n    /**\n     * Delay (in ms) between retries.\n     *\n     * @see #retryCount\n     */\n    @Value.Default\n    default int retryInterval() {\n        return 5000;\n    }\n\n    /**\n     * Max interval (in ms) without heartbeat before the process fails.\n     */\n    @Value.Default\n    default long maxNoHeartbeatInterval() {\n        return TimeUnit.MINUTES.toMillis(5);\n    }\n\n    static ImmutableApiConfiguration.Builder builder() {\n        return ImmutableApiConfiguration.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/common/src/main/java/com/walmartlabs/concord/runtime/common/cfg/CommonProcessConfiguration.java",
    "content": "package com.walmartlabs.concord.runtime.common.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n/**\n * Common process configuration parameters (for both runtime v1 and v2).\n * <p/>\n * Typically used by the server to create the process input data (aka _main.json).\n */\npublic interface CommonProcessConfiguration {\n\n\n}\n"
  },
  {
    "path": "runtime/common/src/main/java/com/walmartlabs/concord/runtime/common/cfg/DependencyManagerConfiguration.java",
    "content": "package com.walmartlabs.concord.runtime.common.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableDependencyManagerConfiguration.class)\n@JsonDeserialize(as = ImmutableDependencyManagerConfiguration.class)\npublic interface DependencyManagerConfiguration {\n\n    @Nullable\n    String cacheDir();\n\n    static ImmutableDependencyManagerConfiguration.Builder builder() {\n        return ImmutableDependencyManagerConfiguration.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/common/src/main/java/com/walmartlabs/concord/runtime/common/cfg/DockerConfiguration.java",
    "content": "package com.walmartlabs.concord.runtime.common.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport java.util.Collections;\nimport java.util.List;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableDockerConfiguration.class)\n@JsonDeserialize(as = ImmutableDockerConfiguration.class)\npublic interface DockerConfiguration {\n\n    /**\n     * List of additional volumes which will be mounted to the process' containers.\n     */\n    @Value.Default\n    default List<String> extraVolumes() {\n        return Collections.emptyList();\n    }\n\n    /**\n     * Expose docker daemon to containers created by DockerService\n     */\n    @Value.Default\n    default boolean exposeDockerDaemon() {\n        return true;\n    }\n\n    static ImmutableDockerConfiguration.Builder builder() {\n        return ImmutableDockerConfiguration.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/common/src/main/java/com/walmartlabs/concord/runtime/common/cfg/LoggingConfiguration.java",
    "content": "package com.walmartlabs.concord.runtime.common.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableLoggingConfiguration.class)\n@JsonDeserialize(as = ImmutableLoggingConfiguration.class)\npublic interface LoggingConfiguration {\n\n    @Value.Default\n    default boolean segmentedLogs() {\n        return true;\n    }\n\n    /**\n     * If {@code true} {@code System.out} and {@code System.err} will be\n     * redirected into SLF4J and, subsequently, into correct log segments.\n     * <p/>\n     * Default is {@code true}.\n     * <p/>\n     * Requires {@link #segmentedLogs()}.\n     *\n     * @apiNote only for the runtime v2. Not applicable for the CLI version\n     * of the runner.\n     */\n    @Value.Default\n    default boolean sendSystemOutAndErrToSLF4J() {\n        return true;\n    }\n\n    /**\n     * If {@code true}, any ${workDir} value will be replaced with literal\n     * \"$WORK_DIR\" string. Reduces noise in the logs.\n     */\n    @Value.Default\n    default boolean workDirMasking() {\n        return true;\n    }\n\n    static ImmutableLoggingConfiguration.Builder builder() {\n        return ImmutableLoggingConfiguration.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/common/src/main/java/com/walmartlabs/concord/runtime/common/cfg/RunnerConfiguration.java",
    "content": "package com.walmartlabs.concord.runtime.common.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.util.Collection;\nimport java.util.Collections;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableRunnerConfiguration.class)\n@JsonDeserialize(as = ImmutableRunnerConfiguration.class)\n@JsonIgnoreProperties(ignoreUnknown = true)\npublic interface RunnerConfiguration {\n\n    /**\n     * ID of the Agent which executes the process.\n     */\n    @Nullable\n    String agentId();\n\n    /**\n     * Print out additional debugging information.\n     */\n    @Value.Default\n    default boolean debug() {\n        return false;\n    }\n\n    /**\n     * Enable or disable the Sisu index for Guice bindings.\n     * Speeds up the startup, but requires the dependencies to use \"sisu-maven-plugin\".\n     *\n     * @apiNote for the runtime v2 this option is always {@code true}.\n     */\n    @Value.Default\n    default boolean enableSisuIndex() {\n        return false;\n    }\n\n    /**\n     * Default logging level.\n     *\n     * @deprecated used only in the runtime v1.\n     */\n    @Value.Default\n    @Deprecated\n    default String logLevel() {\n        return \"INFO\";\n    }\n\n    /**\n     * Logging parameters used in the runtime.\n     */\n    @Value.Default\n    default LoggingConfiguration logging() {\n        return LoggingConfiguration.builder().build();\n    }\n\n    /**\n     * Java Security Manager configuration.\n     */\n    @Value.Default\n    default SecurityManagerConfiguration securityManager() {\n        return SecurityManagerConfiguration.builder().build();\n    }\n\n    /**\n     * Concord API client configuration.\n     */\n    @Value.Default\n    default ApiConfiguration api() {\n        return ApiConfiguration.builder().build();\n    }\n\n    /**\n     * List of the process' dependencies (JAR file paths).\n     */\n    @Value.Default\n    default Collection<String> dependencies() {\n        return Collections.emptyList();\n    }\n\n    /**\n     * Dependency Manager configuration.\n     */\n    @Value.Default\n    default DependencyManagerConfiguration dependencyManager() {\n        return DependencyManagerConfiguration.builder().build();\n    }\n\n    /**\n     * Docker configuration for the process' containers.\n     */\n    @Value.Default\n    default DockerConfiguration docker() {\n        return DockerConfiguration.builder().build();\n    }\n\n    static ImmutableRunnerConfiguration.Builder builder() {\n        return ImmutableRunnerConfiguration.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/common/src/main/java/com/walmartlabs/concord/runtime/common/cfg/SecurityManagerConfiguration.java",
    "content": "package com.walmartlabs.concord.runtime.common.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableSecurityManagerConfiguration.class)\n@JsonDeserialize(as = ImmutableSecurityManagerConfiguration.class)\npublic interface SecurityManagerConfiguration {\n\n    /**\n     * Enable/disable Java Security Manager\n     */\n    @Value.Default\n    default boolean enabled() {\n        return false;\n    }\n\n    static ImmutableSecurityManagerConfiguration.Builder builder() {\n        return ImmutableSecurityManagerConfiguration.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/common/src/main/java/com/walmartlabs/concord/runtime/common/injector/InjectorUtils.java",
    "content": "package com.walmartlabs.concord.runtime.common.injector;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.TypeLiteral;\nimport com.google.inject.matcher.AbstractMatcher;\nimport com.google.inject.spi.TypeEncounter;\nimport com.google.inject.spi.TypeListener;\nimport com.walmartlabs.concord.common.ReflectionUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Named;\nimport javax.inject.Singleton;\n\npublic final class InjectorUtils {\n\n    private static final Logger log = LoggerFactory.getLogger(InjectorUtils.class);\n\n    public static <T> TaskClassesListener<T> taskClassesListener(TaskHolder<T> holder) {\n        return new TaskClassesListener<>(holder);\n    }\n\n    public static SubClassesOf subClassesOf(Class<?> baseClass) {\n        return new SubClassesOf(baseClass);\n    }\n\n    private static class TaskClassesListener<T> implements TypeListener {\n\n        private final TaskHolder<T> holder;\n\n        private TaskClassesListener(TaskHolder<T> holder) {\n            this.holder = holder;\n        }\n\n        @Override\n        @SuppressWarnings(\"unchecked\")\n        public <I> void hear(TypeLiteral<I> typeLiteral, TypeEncounter<I> typeEncounter) {\n            Class<T> klass = (Class<T>) typeLiteral.getRawType();\n\n            Named n = klass.getAnnotation(Named.class);\n            if (n == null) {\n                log.warn(\"Ignoring task class without @Named: {}\", klass);\n                return;\n            }\n\n            if (ReflectionUtils.findAnnotation(klass, Singleton.class) != null) {\n                log.warn(\"Ignoring task class with @Singleton: {}\", klass);\n                return;\n            }\n\n            String key = n.value();\n            if (\"\".equals(key)) {\n                log.warn(\"Task class with an empty @Named value: {}\", klass);\n                return;\n            }\n\n            holder.add(key, klass);\n        }\n    }\n\n    private static class SubClassesOf extends AbstractMatcher<TypeLiteral<?>> {\n\n        private final Class<?> baseClass;\n\n        private SubClassesOf(Class<?> baseClass) {\n            this.baseClass = baseClass;\n        }\n\n        @Override\n        public boolean matches(TypeLiteral<?> t) {\n            return baseClass.isAssignableFrom(t.getRawType());\n        }\n    }\n\n    private InjectorUtils() {\n    }\n}\n"
  },
  {
    "path": "runtime/common/src/main/java/com/walmartlabs/concord/runtime/common/injector/InstanceId.java",
    "content": "package com.walmartlabs.concord.runtime.common.injector;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\nimport java.util.UUID;\n\n/**\n * Contains ID of the current process.\n * Can be @Inject-ed into services.\n * @apiNote v2 only\n */\npublic class InstanceId implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final UUID value;\n\n    public InstanceId(UUID value) {\n        this.value = value;\n    }\n\n    public UUID getValue() {\n        return value;\n    }\n}\n"
  },
  {
    "path": "runtime/common/src/main/java/com/walmartlabs/concord/runtime/common/injector/TaskHolder.java",
    "content": "package com.walmartlabs.concord.runtime.common.injector;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\n\npublic class TaskHolder<T> {\n\n    private static final Logger log = LoggerFactory.getLogger(TaskHolder.class);\n\n    private final Map<String, Class<T>> classes = new HashMap<>();\n\n    public void add(String key, Class<T> value) {\n        log.debug(\"Registering {} as '{}'...\", value, key);\n        Class<T> old = classes.put(key, value);\n        if (old != null) {\n            throw new IllegalStateException(\"Non-unique task name: \" + key + \". \" +\n                    \"Another task with the same name: old: \" + old.getName() + \", new: \" + value.getName());\n        }\n    }\n\n    public Class<T> get(String key) {\n        return classes.get(key);\n    }\n\n    public Set<String> keys() {\n        return Collections.unmodifiableSet(classes.keySet());\n    }\n}\n"
  },
  {
    "path": "runtime/common/src/main/java/com/walmartlabs/concord/runtime/common/logger/LogSegmentDeserializer.java",
    "content": "package com.walmartlabs.concord.runtime.common.logger;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic final class LogSegmentDeserializer {\n\n    public static LogSegmentStatus deserializeStatus(String status) {\n        return LogSegmentStatus.fromId(Integer.parseInt(status));\n    }\n\n    private LogSegmentDeserializer() {\n    }\n}\n"
  },
  {
    "path": "runtime/common/src/main/java/com/walmartlabs/concord/runtime/common/logger/LogSegmentHeader.java",
    "content": "package com.walmartlabs.concord.runtime.common.logger;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface LogSegmentHeader {\n\n    int length();\n\n    long segmentId();\n\n    int warnCount();\n\n    int errorCount();\n\n    LogSegmentStatus status();\n\n    static ImmutableLogSegmentHeader.Builder builder() {\n        return ImmutableLogSegmentHeader.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/common/src/main/java/com/walmartlabs/concord/runtime/common/logger/LogSegmentSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.common.logger;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic final class LogSegmentSerializer {\n\n    private static final byte[] EMPTY_AB = new byte[0];\n\n    public static byte[] serialize(LogSegmentHeader header, String message) {\n        byte[] msgBytes = EMPTY_AB;\n        if (message != null) {\n            msgBytes = message.getBytes();\n        }\n        byte[] headerBytes = serializeHeader(header, msgBytes.length);\n        return concat(headerBytes, msgBytes);\n    }\n\n    public static byte[] serializeHeader(LogSegmentHeader header) {\n        return serializeHeader(header, header.length());\n    }\n\n    public static byte[] serializeHeader(LogSegmentHeader header, int messageLength) {\n        return String.format(\"|%d|%d|%d|%d|%d|\",\n                        messageLength,\n                        header.segmentId(),\n                        header.status().id(),\n                        header.warnCount(),\n                        header.errorCount())\n                .getBytes();\n    }\n\n    private static byte[] concat(byte[] a, byte[] b) {\n        byte[] c = new byte[a.length + b.length];\n        System.arraycopy(a, 0, c, 0, a.length);\n        System.arraycopy(b, 0, c, a.length, b.length);\n        return c;\n    }\n\n    private LogSegmentSerializer() {\n    }\n}\n"
  },
  {
    "path": "runtime/common/src/main/java/com/walmartlabs/concord/runtime/common/logger/LogSegmentStatus.java",
    "content": "package com.walmartlabs.concord.runtime.common.logger;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic enum LogSegmentStatus {\n\n    RUNNING (0),\n    OK (1),\n    SUSPENDED (2), // unused now...\n    ERROR (3);\n\n    private final int id;\n\n    LogSegmentStatus(int id) {\n        this.id = id;\n    }\n\n    public int id() {\n        return id;\n    }\n\n    public static LogSegmentStatus fromId(int status) {\n        for (LogSegmentStatus s : LogSegmentStatus.values()) {\n            if (s.id() == status) {\n                return s;\n            }\n        }\n        throw new IllegalArgumentException(\"Unknown log segment status: \" + status);\n    }\n}\n"
  },
  {
    "path": "runtime/common/src/test/java/com/walmartlabs/concord/runtime/common/ObjectTruncaterTest.java",
    "content": "package com.walmartlabs.concord.runtime.common;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport java.util.*;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ObjectTruncaterTest {\n\n    @Test\n    public void testTruncArray() {\n        List<Integer> value = Arrays.asList(1, 2, 3, 4, 5);\n        int maxStringLength = 3;\n        int maxArrayLength = 2;\n        int maxDepth = 2;\n\n        // ---\n        Object result = ObjectTruncater.truncate(value, maxStringLength, maxArrayLength, maxDepth);\n        assertEquals(Arrays.asList(1, \"skipped 3 lines\", 5), result);\n    }\n\n    @Test\n    public void testTruncArray1() {\n        int[] value = new int[]{1, 2, 3, 4, 5};\n        int maxStringLength = 3;\n        int maxArrayLength = 2;\n        int maxDepth = 2;\n\n        // ---\n        Object result = ObjectTruncater.truncate(value, maxStringLength, maxArrayLength, maxDepth);\n\n        assertTrue(result.getClass().getComponentType().isPrimitive());\n        assertArrayEquals(new int[]{1, 2}, (int[]) result);\n    }\n\n    @Test\n    public void testTruncArray2() {\n        List<Integer> value = Arrays.asList(1, 2, 3, 4, 5);\n        int maxStringLength = 3;\n        int maxArrayLength = 3;\n        int maxDepth = 2;\n\n        // ---\n        Object result = ObjectTruncater.truncate(value, maxStringLength, maxArrayLength, maxDepth);\n        assertEquals(Arrays.asList(1, 2, \"skipped 2 lines\", 5), result);\n    }\n\n    @Test\n    public void testTruncArray3() {\n        List<Integer> value = Arrays.asList(1, 2, 3, 4, 5);\n        int maxStringLength = 3;\n        int maxArrayLength = 4;\n        int maxDepth = 2;\n\n        // ---\n        Object result = ObjectTruncater.truncate(value, maxStringLength, maxArrayLength, maxDepth);\n        assertEquals(Arrays.asList(1, 2, \"skipped 1 lines\", 4, 5), result);\n    }\n\n    @Test\n    public void testTruncArray4() {\n        List<Integer> value = Arrays.asList(1, 2, 3);\n        int maxStringLength = 3;\n        int maxArrayLength = 4;\n        int maxDepth = 2;\n\n        // ---\n        Object result = ObjectTruncater.truncate(value, maxStringLength, maxArrayLength, maxDepth);\n        assertEquals(Arrays.asList(1, 2, 3), result);\n    }\n\n    @Test\n    public void testTruncArray5() {\n        List<Object> value = Arrays.asList(1, 2, Arrays.asList(11, 22, 33, 44, 55));\n        int maxStringLength = 3;\n        int maxArrayLength = 4;\n        int maxDepth = 2;\n\n        // ---\n        Object result = ObjectTruncater.truncate(value, maxStringLength, maxArrayLength, maxDepth);\n        assertEquals(Arrays.asList(1, 2, Arrays.asList(11, 22, \"skipped 1 lines\", 44, 55)), result);\n    }\n\n    @Test\n    public void testTruncArray6() {\n        List<Object> value = Arrays.asList(1, 2, Arrays.asList(11, Arrays.asList(111, 222)));\n        int maxStringLength = 3;\n        int maxArrayLength = 4;\n        int maxDepth = 1;\n\n        // ---\n        Object result = ObjectTruncater.truncate(value, maxStringLength, maxArrayLength, maxDepth);\n        assertEquals(Arrays.asList(1, 2, Arrays.asList(11, Collections.singletonList(\"skipped: max depth reached\"))), result);\n    }\n\n    @Test\n    public void testTruncString() {\n        Map<String, Object> value = new HashMap<>();\n        value.put(\"k\", \"123456\");\n        int maxStringLength = 3;\n        int maxArrayLength = 4;\n        int maxDepth = 2;\n\n        // ---\n        Object result = ObjectTruncater.truncate(value, maxStringLength, maxArrayLength, maxDepth);\n        assertEquals(Collections.<String, Object>singletonMap(\"k\", \"12...[skipped 3 chars]...6\"), result);\n    }\n\n    @Test\n    public void testTruncString2() {\n        Map<String, Object> value = new HashMap<>();\n        value.put(\"k\", \"123\");\n        int maxStringLength = 3;\n        int maxArrayLength = 4;\n        int maxDepth = 2;\n\n        // ---\n        Object result = ObjectTruncater.truncate(value, maxStringLength, maxArrayLength, maxDepth);\n        assertEquals(Collections.<String, Object>singletonMap(\"k\", \"123\"), result);\n    }\n\n    @Test\n    public void testTruncString3() {\n        Map<String, Object> value = new HashMap<>();\n        value.put(\"k\", \"1234567890\");\n        int maxStringLength = 2;\n        int maxArrayLength = 4;\n        int maxDepth = 2;\n\n        // ---\n        Object result = ObjectTruncater.truncate(value, maxStringLength, maxArrayLength, maxDepth);\n        assertEquals(Collections.<String, Object>singletonMap(\"k\", \"1...[skipped 8 chars]...0\"), result);\n    }\n}\n"
  },
  {
    "path": "runtime/common/src/test/java/com/walmartlabs/concord/runtime/common/SensitiveDataMaskerTest.java",
    "content": "package com.walmartlabs.concord.runtime.common;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Map;\nimport java.util.Set;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class SensitiveDataMaskerTest {\n\n    @Test\n    public void testSensitiveDataMasking() throws JsonProcessingException {\n        var sensitiveStrings = Set.of(\"foo\", \"bar\");\n\n        String in = \"{\" +\n                \"\\\"a\\\": \\\"foo\\\",\" +\n                \"\\\"b\\\": \\\"bar\\\",\" +\n                \"\\\"c\\\": \\\"baz\\\",\" +\n                \"\\\"d\\\": { \\\"e\\\": \\\"foo\\\" }\" +\n                \"}\";\n\n        Map<String, Object> result = SensitiveDataMasker.mask(vars(in), sensitiveStrings);\n        String expected = \"{\" +\n                \"   \\\"a\\\": \\\"******\\\",\" +\n                \"   \\\"b\\\": \\\"******\\\",\" +\n                \"   \\\"c\\\": \\\"baz\\\",\" +\n                \"   \\\"d\\\": { \\\"e\\\": \\\"******\\\" }\" +\n                \"}\";\n        assertEquals(vars(expected), result);\n    }\n\n    private static Map<String, Object> vars(String in) throws JsonProcessingException {\n        return new ObjectMapper().readValue(in, new TypeReference<>() {\n        });\n    }\n}\n"
  },
  {
    "path": "runtime/loader/README.md",
    "content": "# Process Loader\n\nA common `ProcessDefinition` loader for both runtime v1 and v2.\n\n"
  },
  {
    "path": "runtime/loader/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <groupId>com.walmartlabs.concord.runtime</groupId>\n    <artifactId>concord-runtime-loader</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime</groupId>\n            <artifactId>concord-runtime-model</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-imports</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-repository</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.dataformat</groupId>\n            <artifactId>jackson-dataformat-yaml</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jsr310</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jdk8</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>io.takari.bpm</groupId>\n            <artifactId>bpm-engine-api</artifactId>\n        </dependency>\n\n        <!-- Immutables -->\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>serial</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.errorprone</groupId>\n            <artifactId>error_prone_annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-core</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n    </dependencies>\n</project>\n"
  },
  {
    "path": "runtime/loader/src/main/java/com/walmartlabs/concord/process/loader/DelegatingProjectLoader.java",
    "content": "package com.walmartlabs.concord.process.loader;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.ImportsListener;\n\nimport javax.inject.Inject;\nimport java.nio.file.Path;\nimport java.util.Optional;\nimport java.util.Set;\n\nimport static com.walmartlabs.concord.process.loader.ProjectLoaderUtils.getRuntimeType;\nimport static com.walmartlabs.concord.process.loader.StandardRuntimeTypes.CONCORD_V1_RUNTIME_TYPE;\nimport static java.util.Objects.requireNonNull;\n\n/**\n * A project loader that tries to auto-detect the type of the runtime in ${workDir}\n * and use the appropriate runtime-specific loader.\n */\npublic class DelegatingProjectLoader implements ProjectLoader {\n\n    private final Set<ProjectLoader> delegates;\n\n    @Inject\n    public DelegatingProjectLoader(Set<ProjectLoader> delegates) {\n        this.delegates = requireNonNull(delegates);\n    }\n\n    @Override\n    public boolean supports(String runtime) {\n        for (var delegate : delegates) {\n            if (delegate.supports(runtime)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public Result loadProject(Path workDir, ImportsNormalizer importsNormalizer, ImportsListener listener) throws Exception {\n        var runtime = getRuntimeType(workDir).orElse(CONCORD_V1_RUNTIME_TYPE);\n        return loadProject(workDir, runtime, importsNormalizer, listener);\n    }\n\n    @Override\n    public Result loadProject(Path workDir, String runtime, ImportsNormalizer importsNormalizer, ImportsListener listener) throws Exception {\n        return getLoader(runtime).orElseThrow(() -> new UnsupportedRuntimeTypeException(\"Unsupported runtime type: \" + runtime))\n                .loadProject(workDir, runtime, importsNormalizer, listener);\n    }\n\n    private Optional<ProjectLoader> getLoader(String runtime) {\n        for (var delegate : delegates) {\n            if (delegate.supports(runtime)) {\n                return Optional.of(delegate);\n            }\n        }\n        return Optional.empty();\n    }\n}\n"
  },
  {
    "path": "runtime/loader/src/main/java/com/walmartlabs/concord/process/loader/ImportsNormalizer.java",
    "content": "package com.walmartlabs.concord.process.loader;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Imports;\n\npublic interface ImportsNormalizer {\n\n    Imports normalize(Imports imports);\n}\n"
  },
  {
    "path": "runtime/loader/src/main/java/com/walmartlabs/concord/process/loader/ProjectLoader.java",
    "content": "package com.walmartlabs.concord.process.loader;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.ImportsListener;\nimport com.walmartlabs.concord.runtime.model.ProcessDefinition;\nimport com.walmartlabs.concord.repository.Snapshot;\n\nimport java.nio.file.Path;\nimport java.util.List;\n\npublic interface ProjectLoader {\n\n    /**\n     * Returns true if the current loader can load a project of the specified runtime.\n     */\n    boolean supports(String runtime);\n\n    /**\n     * Loads the directory as a project of the specified runtime.\n     */\n    Result loadProject(Path workDir, String runtime, ImportsNormalizer importsNormalizer, ImportsListener listener) throws Exception;\n\n    interface Result {\n\n        List<Snapshot> snapshots();\n\n        ProcessDefinition projectDefinition();\n    }\n}\n"
  },
  {
    "path": "runtime/loader/src/main/java/com/walmartlabs/concord/process/loader/ProjectLoaderUtils.java",
    "content": "package com.walmartlabs.concord.process.loader;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.dataformat.yaml.YAMLMapper;\nimport com.walmartlabs.concord.sdk.Constants;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Optional;\n\nimport static com.walmartlabs.concord.process.loader.StandardRuntimeTypes.PROJECT_ROOT_FILE_NAMES;\n\npublic class ProjectLoaderUtils {\n\n    public static Optional<String> getRuntimeType(Path workDir) throws IOException {\n        for (var filename : PROJECT_ROOT_FILE_NAMES) {\n            var src = workDir.resolve(filename);\n            if (Files.exists(src)) {\n                var mapper = new YAMLMapper();\n                try (var in = Files.newInputStream(src)) {\n                    var n = mapper.readTree(in);\n\n                    n = n.get(Constants.Request.CONFIGURATION_KEY);\n                    if (n == null) {\n                        continue;\n                    }\n\n                    n = n.get(Constants.Request.RUNTIME_KEY);\n                    if (n == null) {\n                        continue;\n                    }\n\n                    var s = n.textValue();\n                    if (s != null) {\n                        return Optional.of(s);\n                    }\n                }\n            }\n        }\n\n        return Optional.empty();\n    }\n\n    public static boolean isConcordFileExists(Path repoPath) {\n        for (String projectFileName : PROJECT_ROOT_FILE_NAMES) {\n            Path projectFile = repoPath.resolve(projectFileName);\n            if (Files.exists(projectFile)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    private ProjectLoaderUtils() {\n    }\n}\n"
  },
  {
    "path": "runtime/loader/src/main/java/com/walmartlabs/concord/process/loader/StandardRuntimeTypes.java",
    "content": "package com.walmartlabs.concord.process.loader;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic final class StandardRuntimeTypes {\n\n    public static final String CONCORD_V1_RUNTIME_TYPE = \"concord-v1\";\n    public static final String CONCORD_V2_RUNTIME_TYPE = \"concord-v2\";\n\n    /**\n     * Files that the runtime considers \"root\" project files.\n     */\n    public static final String[] PROJECT_ROOT_FILE_NAMES = {\n            \".concord.yml\",\n            \"concord.yml\",\n            \".concord.yaml\",\n            \"concord.yaml\"\n    };\n\n\n    private StandardRuntimeTypes() {\n    }\n}\n"
  },
  {
    "path": "runtime/loader/src/main/java/com/walmartlabs/concord/process/loader/UnsupportedRuntimeTypeException.java",
    "content": "package com.walmartlabs.concord.process.loader;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class UnsupportedRuntimeTypeException extends Exception {\n\n    public UnsupportedRuntimeTypeException(String runtime) {\n        super(\"Unsupported runtime type: \" + runtime);\n    }\n}\n"
  },
  {
    "path": "runtime/model/README.md",
    "content": "# concord-runtime-model\n\nCommon interfaces for Concord runtimes. Declares top-level entities such as \"process configuration\", \"profiles\", etc.\nUsed in situations when support for multiple different but similarly structured runtimes is required (e.g.\n`concord-v1` and `concord-v2` situations).\n\nThe `concord-v1` and `concord-v2` runtimes implements concord-runtime-model interfaces in\n`com.walmartlabs.concord.runtime.*.wrapper` packages. Those implementations wrap the original model classes, mostly\ndue to some structural differences and us not wanting to change the base model classes in the runtimes too much.\n\nFuture runtimes can implement those interfaces directly in their respective model classes.\n"
  },
  {
    "path": "runtime/model/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <groupId>com.walmartlabs.concord.runtime</groupId>\n    <artifactId>concord-runtime-model</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-imports</artifactId>\n        </dependency>\n\n        <!-- Immutables -->\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>serial</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.errorprone</groupId>\n            <artifactId>error_prone_annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n</project>\n"
  },
  {
    "path": "runtime/model/src/main/java/com/walmartlabs/concord/runtime/model/AllowNulls.java",
    "content": "package com.walmartlabs.concord.runtime.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Target;\n\n@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.TYPE_USE})\npublic @interface AllowNulls {}"
  },
  {
    "path": "runtime/model/src/main/java/com/walmartlabs/concord/runtime/model/Configuration.java",
    "content": "package com.walmartlabs.concord.runtime.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.List;\nimport java.util.Map;\n\npublic interface Configuration {\n\n    Map<String, Object> asMap();\n\n    List<String> dependencies();\n\n    List<String> extraDependencies();\n}\n"
  },
  {
    "path": "runtime/model/src/main/java/com/walmartlabs/concord/runtime/model/EffectiveConfiguration.java",
    "content": "package com.walmartlabs.concord.runtime.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\n\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\npublic final class EffectiveConfiguration {\n\n    public static Map<String, Object> getEffectiveConfiguration(ProcessDefinition project, Collection<String> activeProfiles) {\n        Map<String, Object> cfg = asMap(project.configuration());\n        Map<String, ? extends Profile> profiles = project.profiles();\n\n        Map<String, Object> view = new LinkedHashMap<>(cfg != null ? cfg : Collections.emptyMap());\n        if (profiles == null || activeProfiles == null) {\n            return view;\n        }\n\n        for (String n : activeProfiles) {\n            Profile p1 = profiles.get(n);\n            if (p1 == null) {\n                continue;\n            }\n\n            Map<String, Object> overlays = asMap(p1.configuration());\n            if (overlays != null) {\n                view = ConfigurationUtils.deepMerge(view, overlays);\n            }\n        }\n\n        return view;\n    }\n\n    private static Map<String, Object> asMap(Configuration cfg) {\n        if (cfg == null) {\n            return null;\n        }\n\n        return cfg.asMap();\n    }\n\n    private EffectiveConfiguration() {\n    }\n}\n"
  },
  {
    "path": "runtime/model/src/main/java/com/walmartlabs/concord/runtime/model/EffectiveProcessDefinitionProvider.java",
    "content": "package com.walmartlabs.concord.runtime.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.OutputStream;\n\npublic interface EffectiveProcessDefinitionProvider {\n\n    void serialize(Options options, OutputStream out) throws Exception;\n}\n"
  },
  {
    "path": "runtime/model/src/main/java/com/walmartlabs/concord/runtime/model/ExpressionStep.java",
    "content": "package com.walmartlabs.concord.runtime.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.serial.Serial;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.Map;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@Serial.Version(1)\npublic interface ExpressionStep extends Step {\n\n    String expression();\n\n    @Value.Default\n    @AllowNulls\n    default Map<String, Serializable> input() {\n        return Collections.emptyMap();\n    }\n\n    static ImmutableExpressionStep.Builder builder() {\n        return ImmutableExpressionStep.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/model/src/main/java/com/walmartlabs/concord/runtime/model/FlowDefinition.java",
    "content": "package com.walmartlabs.concord.runtime.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\nimport java.util.List;\n\npublic interface FlowDefinition extends Serializable {\n\n    String name();\n\n    List<? extends Step> steps();\n}\n"
  },
  {
    "path": "runtime/model/src/main/java/com/walmartlabs/concord/runtime/model/Form.java",
    "content": "package com.walmartlabs.concord.runtime.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.util.Collections;\nimport java.util.List;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface Form {\n\n    String name();\n\n    @Value.Default\n    default List<FormField> fields() {\n        return Collections.emptyList();\n    }\n\n    @Nullable\n    SourceMap location();\n\n    static ImmutableForm.Builder builder() {\n        return ImmutableForm.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/model/src/main/java/com/walmartlabs/concord/runtime/model/FormField.java",
    "content": "package com.walmartlabs.concord.runtime.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.Map;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface FormField {\n\n    String name();\n\n    @Nullable\n    String label();\n\n    String type();\n\n    @Nullable\n    Serializable defaultValue();\n\n    @Nullable\n    Serializable allowedValue();\n\n    @Value.Default\n    @AllowNulls\n    default Map<String, Serializable> options() {\n        return Collections.emptyMap();\n    }\n\n    @Nullable\n    SourceMap location();\n\n    static ImmutableFormField.Builder builder() {\n        return ImmutableFormField.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/model/src/main/java/com/walmartlabs/concord/runtime/model/Location.java",
    "content": "package com.walmartlabs.concord.runtime.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface Location extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    static String toShortString(Location location) {\n        if (location == null || location.fileName() == null) {\n            return \"n/a\";\n        }\n\n        return \"line: \" + location.lineNum() + \", col: \" + location.column();\n    }\n\n    static String toErrorPrefix(Location location) {\n        if (location == null || location.fileName() == null) {\n            return \"(n/a): Error.\";\n        } else {\n            return \"(\" + location.fileName() + \"): Error @ \" + toShortString(location);\n        }\n    }\n\n    @Value.Default\n    default int lineNum() {\n        return -1;\n    }\n\n    @Value.Default\n    default int column() {\n        return -1;\n    }\n\n    @Nullable\n    String fileName();\n\n    static ImmutableLocation.Builder builder() {\n        return ImmutableLocation.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/model/src/main/java/com/walmartlabs/concord/runtime/model/Options.java",
    "content": "package com.walmartlabs.concord.runtime.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface Options {\n\n    UUID instanceId();\n\n    @Nullable\n    UUID parentInstanceId();\n\n    String entryPoint();\n\n    @Value.Default\n    @AllowNulls\n    default Map<String, Object> configuration() {\n        return Collections.emptyMap();\n    }\n\n    @Value.Default\n    default List<String> activeProfiles() {\n        return Collections.emptyList();\n    }\n\n    static ImmutableOptions.Builder builder() {\n        return ImmutableOptions.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/model/src/main/java/com/walmartlabs/concord/runtime/model/ProcessDefinition.java",
    "content": "package com.walmartlabs.concord.runtime.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Imports;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * Common interface for v1 and v2 process definitions.\n */\npublic interface ProcessDefinition extends EffectiveProcessDefinitionProvider {\n\n    String runtime();\n\n    Configuration configuration();\n\n    Map<String, ? extends FlowDefinition> flows();\n\n    Set<String> publicFlows();\n\n    Map<String, ? extends Profile> profiles();\n\n    List<Trigger> triggers();\n\n    Imports imports();\n\n    List<Form> forms();\n}\n"
  },
  {
    "path": "runtime/model/src/main/java/com/walmartlabs/concord/runtime/model/Profile.java",
    "content": "package com.walmartlabs.concord.runtime.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Map;\nimport java.util.Set;\n\npublic interface Profile {\n\n    Configuration configuration();\n\n    Set<String> publicFlows();\n\n    Map<String, ? extends FlowDefinition> flows();\n}\n"
  },
  {
    "path": "runtime/model/src/main/java/com/walmartlabs/concord/runtime/model/SourceMap.java",
    "content": "package com.walmartlabs.concord.runtime.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.serial.Serial;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\n\n@Value.Immutable\n@Serial.Version(1)\n@Value.Style(jdkOnly = true)\npublic interface SourceMap extends Serializable {\n\n    static SourceMap from(Location location) {\n        return SourceMap.builder()\n                .source(location.fileName() != null ? location.fileName() : \"n/a\")\n                .line(location.lineNum())\n                .column(location.column())\n                .build();\n    }\n\n    String source();\n\n    int line();\n\n    int column();\n\n    @Nullable\n    String description();\n\n    static ImmutableSourceMap.Builder builder() {\n        return ImmutableSourceMap.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/model/src/main/java/com/walmartlabs/concord/runtime/model/Step.java",
    "content": "package com.walmartlabs.concord.runtime.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\n\npublic interface Step extends Serializable {\n\n    SourceMap location();\n}\n"
  },
  {
    "path": "runtime/model/src/main/java/com/walmartlabs/concord/runtime/model/TaskCallStep.java",
    "content": "package com.walmartlabs.concord.runtime.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AllowNulls;\nimport org.immutables.serial.Serial;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.Map;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@Serial.Version(1)\npublic interface TaskCallStep extends Step {\n\n    String name();\n\n    @Value.Default\n    @AllowNulls\n    default Map<String, Serializable> input() {\n        return Collections.emptyMap();\n    }\n\n    static ImmutableTaskCallStep.Builder builder() {\n        return ImmutableTaskCallStep.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/model/src/main/java/com/walmartlabs/concord/runtime/model/Trigger.java",
    "content": "package com.walmartlabs.concord.runtime.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.util.List;\nimport java.util.Map;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface Trigger {\n\n    String name();\n\n    @Nullable\n    Map<String, Object> arguments();\n\n    @Nullable\n    Map<String, Object> conditions();\n\n    @Nullable\n    Map<String, Object> configuration();\n\n    @Nullable\n    List<String> activeProfiles();\n\n    @Nullable\n    SourceMap sourceMap();\n\n    static ImmutableTrigger.Builder builder() {\n        return ImmutableTrigger.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v1/README.md",
    "content": "# concord-runner\n\nThe default (v1) runtime for Concord processes.\n\nModules:\n- [project-model](./model) - Concord DSL parser/model;\n- [model](./model) - configuration classes;\n- [impl](./impl) - the runtime itself.\n"
  },
  {
    "path": "runtime/v1/impl/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.runtime.v1</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-runtime-impl-v1</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime</groupId>\n            <artifactId>concord-runtime-common</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v1</groupId>\n            <artifactId>concord-runtime-model-v1</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-policy-engine</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-client2</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-dependency-manager</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-imports</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jsr310</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>aopalliance</groupId>\n            <artifactId>aopalliance</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.jboss.spec.javax.ws.rs</groupId>\n            <artifactId>jboss-jaxrs-api_2.1_spec</artifactId>\n        </dependency>\n\n        <!-- TODO this should be pulled from transitive deps, shouldn't? -->\n        <dependency>\n            <groupId>org.glassfish</groupId>\n            <artifactId>javax.el</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.dataformat</groupId>\n            <artifactId>jackson-dataformat-yaml</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.inject</groupId>\n            <artifactId>guice</artifactId>\n            <exclusions>\n                <exclusion>\n                    <groupId>com.google.guava</groupId>\n                    <artifactId>guava</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n        <dependency>\n            <groupId>com.google.guava</groupId>\n            <artifactId>guava</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.takari.bpm</groupId>\n            <artifactId>bpm-engine-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.takari.bpm</groupId>\n            <artifactId>bpm-engine-impl</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.sisu</groupId>\n            <artifactId>org.eclipse.sisu.inject</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>ch.qos.logback</groupId>\n            <artifactId>logback-classic</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-compress</artifactId>\n        </dependency>\n\n        <!-- tasks -->\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>log-tasks</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>locale-tasks</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>kv-tasks</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>misc-tasks</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>resource-tasks</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>sleep-tasks</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>variables-tasks</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>crypto-tasks</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>docker-tasks</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>throw-tasks</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>lock-tasks</artifactId>\n        </dependency>\n\n        <!-- JDK9+ compatibility -->\n        <dependency>\n            <groupId>javax.xml.bind</groupId>\n            <artifactId>jaxb-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.sun.xml.bind</groupId>\n            <artifactId>jaxb-impl</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.sun.activation</groupId>\n            <artifactId>javax.activation</artifactId>\n        </dependency>\n\n        <!-- Immutables -->\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-guava</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jdk8</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-core</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>commons-beanutils</groupId>\n            <artifactId>commons-beanutils</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-shade-plugin</artifactId>\n                <version>3.2.4</version>\n                <executions>\n                    <execution>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>shade</goal>\n                        </goals>\n                        <configuration>\n                            <createDependencyReducedPom>false</createDependencyReducedPom>\n                            <shadedArtifactAttached>true</shadedArtifactAttached>\n                            <shadedClassifierName>jar-with-dependencies</shadedClassifierName>\n                            <transformers>\n                                <transformer implementation=\"org.apache.maven.plugins.shade.resource.AppendingTransformer\">\n                                    <resource>META-INF/sisu/javax.inject.Named</resource>\n                                </transformer>\n                                <transformer implementation=\"org.apache.maven.plugins.shade.resource.AppendingTransformer\">\n                                    <resource>META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider</resource>\n                                </transformer>\n                                <transformer implementation=\"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer\">\n                                    <mainClass>com.walmartlabs.concord.runner.Main</mainClass>\n                                </transformer>\n                            </transformers>\n                            <filters>\n                                <filter>\n                                    <artifact>*:*</artifact>\n                                    <excludes>\n                                        <exclude>META-INF/*.SF</exclude>\n                                        <exclude>META-INF/*.DSA</exclude>\n                                        <exclude>META-INF/*.RSA</exclude>\n                                        <exclude>META-INF/NOTICE.txt</exclude>\n                                        <exclude>META-INF/LICENSE</exclude>\n                                        <exclude>META-INF/LICENSE.txt</exclude>\n                                    </excludes>\n                                </filter>\n                            </filters>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/ApiClientFactoryImpl.java",
    "content": "package com.walmartlabs.concord.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.client2.ApiClientConfiguration;\nimport com.walmartlabs.concord.client2.ApiClientFactory;\nimport com.walmartlabs.concord.client2.DefaultApiClientFactory;\nimport com.walmartlabs.concord.sdk.ApiConfiguration;\n\nimport java.time.Duration;\nimport java.util.UUID;\n\npublic class ApiClientFactoryImpl implements ApiClientFactory {\n\n    private final ApiConfiguration cfg;\n    private final DefaultApiClientFactory clientFactory;\n    private UUID txId;\n\n    public ApiClientFactoryImpl(ApiConfiguration cfg) throws Exception {\n        this.cfg = cfg;\n        this.clientFactory = new DefaultApiClientFactory(cfg.getBaseUrl(), Duration.ofMillis(cfg.connectTimeout()));\n    }\n\n    @Override\n    public ApiClient create(ApiClientConfiguration overrides) {\n        ApiClient client = this.clientFactory.create(overrides)\n                .setReadTimeout(Duration.ofMillis(cfg.readTimeout()));\n\n        if (txId != null) {\n            client = client.setUserAgent(\"Concord-Runner: txId=\" + txId);\n        }\n\n        return client;\n    }\n\n    public void setTxId(UUID instanceId) {\n        this.txId = instanceId;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/ApiClientFactoryProvider.java",
    "content": "package com.walmartlabs.concord.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClientFactory;\nimport com.walmartlabs.concord.sdk.ApiConfiguration;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Provider;\nimport javax.inject.Singleton;\n\n@Named\n@Singleton\npublic class ApiClientFactoryProvider implements Provider<ApiClientFactory> {\n\n    private final ApiConfiguration cfg;\n\n    @Inject\n    public ApiClientFactoryProvider(ApiConfiguration cfg) {\n        this.cfg = cfg;\n    }\n\n    @Override\n    public ApiClientFactory get() {\n        try {\n            return new ApiClientFactoryImpl(cfg);\n        } catch (RuntimeException e) {\n            throw e;\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/CheckpointManager.java",
    "content": "package com.walmartlabs.concord.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.TemporaryPath;\nimport com.walmartlabs.concord.common.ZipUtils;\nimport com.walmartlabs.concord.project.InternalConstants;\nimport com.walmartlabs.concord.sdk.Constants;\nimport io.takari.bpm.api.ExecutionException;\nimport org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic class CheckpointManager {\n\n    private static final Logger log = LoggerFactory.getLogger(Main.class);\n\n    private final UUID instanceId;\n\n    private final ProcessApiClient processApiClient;\n\n    public CheckpointManager(UUID instanceId, ProcessApiClient processApiClient) {\n        this.instanceId = instanceId;\n        this.processApiClient = processApiClient;\n    }\n\n    public void process(UUID checkpointId, UUID correlationId, String checkpointName, Path baseDir) throws ExecutionException {\n        try {\n            Path checkpointDir = baseDir.resolve(Constants.Files.JOB_CHECKPOINTS_DIR_NAME);\n            if (!Files.exists(checkpointDir)) {\n                Files.createDirectories(checkpointDir);\n            }\n\n            Path checkpointMeta = baseDir.resolve(Constants.Files.CHECKPOINT_META_FILE_NAME);\n            Files.write(checkpointMeta, checkpointName.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);\n\n            try (TemporaryPath checkpointFile = new TemporaryPath(checkpointDir.resolve(checkpointId + \"_\" + checkpointName + \".zip\"))) {\n                try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(Files.newOutputStream(checkpointFile.path()))) {\n                    ZipUtils.zip(zip, InternalConstants.Files.JOB_ATTACHMENTS_DIR_NAME + \"/\", baseDir.resolve(InternalConstants.Files.JOB_ATTACHMENTS_DIR_NAME));\n                    ZipUtils.zip(zip, InternalConstants.Files.CONCORD_SYSTEM_DIR_NAME + \"/\", baseDir.resolve(InternalConstants.Files.CONCORD_SYSTEM_DIR_NAME));\n                    ZipUtils.zipFile(zip, checkpointMeta, Constants.Files.CHECKPOINT_META_FILE_NAME);\n                }\n\n                try (InputStream in = Files.newInputStream(checkpointFile.path())) {\n                    Map<String, Object> data = new HashMap<>();\n                    data.put(\"id\", checkpointId);\n                    data.put(\"correlationId\", correlationId);\n                    data.put(\"name\", checkpointName);\n                    data.put(\"data\", in);\n\n                    processApiClient.uploadCheckpoint(instanceId, data);\n                }\n            }\n\n            Files.delete(checkpointMeta);\n\n            // write resume event\n            Path resumeEventFile = baseDir.resolve(InternalConstants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                    .resolve(InternalConstants.Files.JOB_STATE_DIR_NAME)\n                    .resolve(InternalConstants.Files.RESUME_MARKER_FILE_NAME);\n\n            Files.write(resumeEventFile, checkpointName.getBytes());\n\n            log.info(\"checkpoint {} ('{}')\", checkpointName, checkpointId);\n        } catch (Exception e) {\n            throw new ExecutionException(\"Checkpoint process error\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/ConcordSecurityManager.java",
    "content": "package com.walmartlabs.concord.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.PrivilegedAction;\n\nimport java.io.FilePermission;\nimport java.security.Permission;\n\npublic class ConcordSecurityManager extends SecurityManager {\n\n    private static final String ALL_FILES_TOKEN = \"<<ALL FILES>>\"; // special java token\n\n    private static final String[] ALLOWED_DOMAINS = {\n            \"docker\",\n            \"task\"\n    };\n\n    private static final String[] ALLOWED_PATHS = {\n            System.getProperty(\"java.home\"),             // JAVA_HOME\n            System.getProperty(\"user.dir\"),              // process' working directory\n            PathUtils.TMP_DIR.toAbsolutePath().toString(), // concord's own TMP\n            \"/tmp\",                                      // system TMP\n            \"/dev/\", \"/proc/\", \"/sys/\",                  // unix stuff\n            \"/groovy/\"                                   // Groovy's pseudo-files\n    };\n\n    @Override\n    public void checkPermission(Permission perm) {\n        if (perm instanceof FilePermission) {\n            // allow our own \"domains\"\n            String domain = PrivilegedAction.getCurrentDomain();\n            if (domain != null) {\n                for (String d : ALLOWED_DOMAINS) {\n                    if (d.equals(domain)) {\n                        return;\n                    }\n                }\n            }\n\n            String path = perm.getName();\n\n            // allow all local paths\n            if (!path.startsWith(\"/\") && !path.equals(ALL_FILES_TOKEN)) {\n                return;\n            }\n\n            // (crude) allow dependencies\n            if (path.endsWith(\".jar\")) {\n                return;\n            }\n\n            for (String p : ALLOWED_PATHS) {\n                if (path.startsWith(p)) {\n                    return;\n                }\n            }\n\n            // disallow anything else\n            System.err.println(\"FORBIDDEN: \" + path);\n            throw new SecurityException(\"Forbidden: \" + path);\n        }\n    }\n\n    @Override\n    public void checkPermission(Permission perm, Object context) {\n        checkPermission(perm);\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/ContextUtils.java",
    "content": "package com.walmartlabs.concord.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.Context;\nimport io.takari.bpm.api.Variables;\n\nimport java.util.Map;\n\npublic final class ContextUtils {\n\n    @SuppressWarnings(\"unchecked\")\n    public static String getSessionToken(Context ctx) {\n        Map<String, Object> processInfo = (Map<String, Object>) ctx.getVariable(Constants.Request.PROCESS_INFO_KEY);\n        if (processInfo == null) {\n            throw new IllegalArgumentException(\"Can't find process info in the context: \" + ctx.getVariableNames());\n        }\n\n        return assertSessionToken(processInfo);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static String getSessionToken(Variables variables) {\n        Map<String, Object> processInfo = (Map<String, Object>) variables.getVariable(Constants.Request.PROCESS_INFO_KEY);\n        if (processInfo == null) {\n            throw new IllegalArgumentException(\"Can't find process info in the variables: \" + variables.getVariableNames());\n        }\n\n        return assertSessionToken(processInfo);\n    }\n\n    private static String assertSessionToken(Map<String, Object> processInfo) {\n        String result = (String) processInfo.get(\"sessionKey\"); // TODO rename to sessionToken?\n        if (result == null) {\n            throw new IllegalArgumentException(\"Can't determine the 'sessionToken' value.\");\n        }\n\n        return result;\n    }\n\n    private ContextUtils() {\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/DefaultVariablesConverter.java",
    "content": "package com.walmartlabs.concord.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.runtime.common.StateManager;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\n\n@Named\npublic class DefaultVariablesConverter {\n\n    private final TaskClasses tasks;\n\n    @Inject\n    public DefaultVariablesConverter(TaskClasses tasks) {\n        this.tasks = tasks;\n    }\n\n    public Map<String, Object> convert(Path baseDir, Map<String, Object> processCfg) {\n        Set<String> eventNames = StateManager.readResumeEvents(baseDir);\n        boolean isResume = eventNames != null && !eventNames.isEmpty();\n        if (isResume) {\n            // do nothing for resume.\n            return processCfg;\n        }\n\n        Map<String, Object> v2Defaults = MapUtils.getMap(processCfg, \"defaultTaskVariables\", Collections.emptyMap());\n        Map<String, Object> v1Defaults = new HashMap<>(v2Defaults);\n\n        // v1 version of plugins expects default vars as `tasknameParams`\n        // v2 version: only `taskname`\n        for (Map.Entry<String, Object> e : v2Defaults.entrySet()) {\n            if (!e.getKey().endsWith(\"Params\")) {\n                v1Defaults.put(e.getKey() + \"Params\", e.getValue());\n            }\n        }\n\n        // remove variables with the same name as task name\n        v1Defaults.entrySet().removeIf(e -> tasks.get(e.getKey()) != null);\n\n        Map<String, Object> arguments = MapUtils.getMap(processCfg, Constants.Request.ARGUMENTS_KEY, Collections.emptyMap());\n\n        Map<String, Object> result = new HashMap<>(processCfg);\n        result.put(Constants.Request.ARGUMENTS_KEY, ConfigurationUtils.deepMerge(v1Defaults, arguments));\n        return result;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/DependencyManagerConfigurationProvider.java",
    "content": "package com.walmartlabs.concord.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.dependencymanager.DependencyManagerConfiguration;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Provider;\nimport javax.inject.Singleton;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\n@Named\n@Singleton\npublic class DependencyManagerConfigurationProvider implements Provider<DependencyManagerConfiguration> {\n\n    private final Path cacheDir;\n\n    @Inject\n    public DependencyManagerConfigurationProvider(RunnerConfiguration cfg) {\n        this.cacheDir = getCacheDir(cfg);\n    }\n\n    @Override\n    public DependencyManagerConfiguration get() {\n        return DependencyManagerConfiguration.of(cacheDir);\n    }\n\n    private static Path getCacheDir(RunnerConfiguration cfg) {\n        try {\n            String s = cfg.dependencyManager().cacheDir();\n            if (s == null) {\n                return PathUtils.createTempDir(\"dependencyCache\");\n            }\n\n            Path p = Paths.get(s);\n            if (!Files.exists(p) || !Files.isDirectory(p)) {\n                throw new RuntimeException(\"The dependency cache directory doesn't exist or not a directory: \" + p);\n            }\n\n            return p;\n        } catch (IOException e) {\n            throw new RuntimeException(\"Error while creating the dependency cache directory\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/DependencyManagerImpl.java",
    "content": "package com.walmartlabs.concord.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.DependencyManager;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Path;\n\n@Named\n@Singleton\npublic class DependencyManagerImpl implements DependencyManager {\n\n    private final com.walmartlabs.concord.dependencymanager.DependencyManager delegate;\n\n    @Inject\n    public DependencyManagerImpl(com.walmartlabs.concord.dependencymanager.DependencyManager delegate) throws IOException {\n        this.delegate = delegate;\n    }\n\n    @Override\n    public Path resolve(URI uri) throws IOException {\n        return delegate.resolveSingle(uri).getPath();\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/ExecutorServiceProvider.java",
    "content": "package com.walmartlabs.concord.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.inject.Named;\nimport javax.inject.Provider;\nimport javax.inject.Singleton;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\n\n@Named\n@Singleton\npublic class ExecutorServiceProvider implements Provider<ExecutorService> {\n\n    private final ExecutorService executor = Executors.newCachedThreadPool();\n\n    @Override\n    public ExecutorService get() {\n        return executor;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/LockServiceImpl.java",
    "content": "package com.walmartlabs.concord.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.ContextUtils;\nimport com.walmartlabs.concord.sdk.LockService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.util.UUID;\nimport java.util.concurrent.Callable;\n\n@Named\npublic class LockServiceImpl implements LockService {\n\n    private static final Logger log = LoggerFactory.getLogger(LockServiceImpl.class);\n\n    private static final int RETRY_COUNT = 3;\n    private static final long RETRY_INTERVAL = 5000;\n    private static final long LOCK_RETRY_INTERVAL = 10000; // TODO custom intervals?\n\n    private final ApiClientFactory apiClientFactory;\n\n    private enum LockScope {\n        ORG, PROJECT\n    }\n\n    @Inject\n    public LockServiceImpl(ApiClientFactory apiClientFactory) {\n        this.apiClientFactory = apiClientFactory;\n    }\n\n    @Override\n    public void projectLock(Context ctx, String lockName) throws Exception {\n        ProcessLocksApi api = new ProcessLocksApi(apiClientFactory.create(ApiClientConfiguration.builder()\n                .sessionToken(ContextUtils.getSessionToken(ctx))\n                .build()));\n\n        // TODO: timeout\n        UUID instanceId = ContextUtils.getTxId(ctx);\n        while (!Thread.currentThread().isInterrupted()) {\n            LockResult lock = withRetry(() -> api.tryLock(instanceId, lockName, LockScope.PROJECT.name()));\n            if (lock.getAcquired()) {\n                log.info(\"successfully acquired lock '{}' in '{}' scope...\", lockName, LockScope.PROJECT);\n                return;\n            }\n\n            log.info(\"waiting for lock '{}' in '{}' scope...\", lockName, LockScope.PROJECT);\n            sleep(LOCK_RETRY_INTERVAL);\n        }\n    }\n\n    @Override\n    public void projectUnlock(Context ctx, String lockName) throws Exception {\n        ProcessLocksApi api = new ProcessLocksApi(apiClientFactory.create(ApiClientConfiguration.builder()\n                .sessionToken(ContextUtils.getSessionToken(ctx))\n                .build()));\n\n        UUID instanceId = ContextUtils.getTxId(ctx);\n        withRetry(() -> {\n            api.unlock(instanceId, lockName, LockScope.PROJECT.name());\n            return null;\n        });\n        log.info(\"unlocking '{}' with scope '{}' -> done\", lockName, LockScope.PROJECT);\n    }\n\n    private static void sleep(long t) {\n        try {\n            Thread.sleep(t);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    private static <T> T withRetry(Callable<T> c) throws ApiException {\n        return ClientUtils.withRetry(RETRY_COUNT, RETRY_INTERVAL, c);\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/Main.java",
    "content": "package com.walmartlabs.concord.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.guava.GuavaModule;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport com.google.inject.AbstractModule;\nimport com.google.inject.Guice;\nimport com.google.inject.Injector;\nimport com.google.inject.Module;\nimport com.google.inject.TypeLiteral;\nimport com.google.inject.matcher.AbstractMatcher;\nimport com.google.inject.spi.TypeEncounter;\nimport com.google.inject.spi.TypeListener;\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.client2.ApiClientConfiguration;\nimport com.walmartlabs.concord.client2.ApiClientFactory;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.imports.ImportsListener;\nimport com.walmartlabs.concord.imports.NoopImportManager;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.policyengine.PolicyEngineRules;\nimport com.walmartlabs.concord.project.NoopImportsNormalizer;\nimport com.walmartlabs.concord.project.ProjectLoader;\nimport com.walmartlabs.concord.project.model.ProjectDefinition;\nimport com.walmartlabs.concord.runner.engine.EngineFactory;\nimport com.walmartlabs.concord.runner.engine.EventConfiguration;\nimport com.walmartlabs.concord.runner.engine.ProcessErrorProcessor;\nimport com.walmartlabs.concord.runtime.common.ProcessHeartbeat;\nimport com.walmartlabs.concord.runtime.common.StateManager;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.runtime.v2.sdk.WorkingDirectory;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport com.walmartlabs.concord.sdk.Task;\nimport com.walmartlabs.concord.sdk.UserDefinedException;\nimport io.takari.bpm.api.*;\nimport org.eclipse.sisu.space.BeanScanning;\nimport org.eclipse.sisu.space.SpaceModule;\nimport org.eclipse.sisu.space.URLClassSpace;\nimport org.eclipse.sisu.wire.WireModule;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.ObjectInputStream;\nimport java.io.OutputStream;\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport java.net.URLClassLoader;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardOpenOption;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n@Named\n@Singleton\npublic class Main {\n\n    private static final Logger log = LoggerFactory.getLogger(Main.class);\n    private static final String RESUME_MARKER = Constants.Files.RESUME_MARKER_FILE_NAME;\n    private static final String SUSPEND_MARKER = Constants.Files.SUSPEND_MARKER_FILE_NAME;\n\n    private final EngineFactory engineFactory;\n    private final ApiClientFactory apiClientFactory;\n    private final DefaultVariablesConverter variablesConverter;\n    private final ObjectMapper objectMapper = new ObjectMapper();\n\n    @Inject\n    public Main(EngineFactory engineFactory, ApiClientFactory apiClientFactory, DefaultVariablesConverter variablesConverter) {\n        this.engineFactory = engineFactory;\n        this.apiClientFactory = apiClientFactory;\n        this.variablesConverter = variablesConverter;\n    }\n\n    public void run(RunnerConfiguration runnerCfg, Path baseDir) throws Exception {\n        log.debug(\"run -> working directory: {}\", baseDir.toAbsolutePath());\n\n        long t1 = System.currentTimeMillis();\n\n        Path idPath = baseDir.resolve(Constants.Files.INSTANCE_ID_FILE_NAME);\n        UUID instanceId = readInstanceId(idPath);\n        ((ApiClientFactoryImpl)apiClientFactory).setTxId(instanceId);\n\n        long t2 = System.currentTimeMillis();\n        if (runnerCfg.debug()) {\n            log.info(\"Spent {}ms waiting for the payload\", (t2 - t1));\n        }\n\n        Map<String, Object> policy = readPolicyRules(baseDir);\n        if (policy.isEmpty()) {\n            PolicyEngineHolder.INSTANCE.setEngine(null);\n        } else {\n            PolicyEngineHolder.INSTANCE.setEngine(new PolicyEngine(objectMapper.convertValue(policy, PolicyEngineRules.class)));\n        }\n\n        // read the process configuration\n        Map<String, Object> processCfg = readRequest(baseDir);\n        processCfg = variablesConverter.convert(baseDir, processCfg);\n\n        String sessionToken = getSessionToken(processCfg);\n        ApiClient apiClient = apiClientFactory.create(ApiClientConfiguration.builder()\n                .sessionToken(sessionToken)\n                .build());\n\n        ProcessHeartbeat heartbeat = new ProcessHeartbeat(apiClient, instanceId, runnerCfg.api().maxNoHeartbeatInterval());\n        heartbeat.start();\n\n        ProcessApiClient processApiClient = new ProcessApiClient(runnerCfg, apiClient);\n\n        processApiClient.updateStatus(instanceId, runnerCfg.agentId(), ProcessEntry.StatusEnum.RUNNING);\n\n        CheckpointManager checkpointManager = new CheckpointManager(instanceId, processApiClient);\n\n        long t3 = System.currentTimeMillis();\n        if (runnerCfg.debug()) {\n            log.info(\"Ready to start in {}ms\", (t3 - t2));\n        }\n\n        executeProcess(instanceId.toString(), checkpointManager, baseDir, processCfg);\n    }\n\n    private void executeProcess(String instanceId, CheckpointManager checkpointManager, Path baseDir, Map<String, Object> processCfg) throws ExecutionException {\n        // get active profiles from the request data\n        Collection<String> activeProfiles = getActiveProfiles(processCfg);\n\n        // load the project\n        ProjectDefinition project = loadProject(baseDir);\n\n        // read the list of metadata variables\n        Set<String> metaVariables = getMetaVariables(processCfg);\n\n        // event recording processCfg\n        EventConfiguration eventCfg = getEventCfg(processCfg);\n\n        boolean dryRunMode = MapUtils.getBoolean(processCfg, Constants.Request.DRY_RUN_MODE_KEY, false);\n        if (dryRunMode) {\n            throw new IllegalArgumentException(\"Dry-run mode is not supported in runtime-v1\");\n        }\n\n        Engine engine = engineFactory.create(project, baseDir, activeProfiles, metaVariables, eventCfg);\n\n        Map<String, Object> resumeCheckpointReq = null;\n        while (true) {\n            Collection<Event> resultEvents;\n\n            // check if we need to resume the process from a saved point\n            Set<String> eventNames = StateManager.readResumeEvents(baseDir);\n            if (eventNames == null || eventNames.isEmpty()) {\n                // running fresh\n                // let's check if there are some saved variables (e.g. from the parent process)\n                Variables vars = readSavedVariables(baseDir);\n                resultEvents = start(engine, vars, processCfg, instanceId, baseDir);\n            } else {\n                if (eventNames.size() > 1) {\n                    throw new IllegalStateException(\"Runtime v1 supports resuming for only one event at the time. Got: \" + eventNames);\n                }\n\n                String eventName = eventNames.iterator().next();\n                resultEvents = resume(engine, processCfg, instanceId, baseDir, eventName);\n            }\n\n            Event checkpointEvent = resultEvents.stream()\n                    .filter(e -> e.getPayload() instanceof Map)\n                    .filter(e -> getCheckpointId(e) != null)\n                    .findFirst()\n                    .orElse(null);\n\n            // found a checkpoint, resume the process immediately\n            if (checkpointEvent != null) {\n                checkpointManager.process(getCheckpointId(checkpointEvent), getCorrelationId(checkpointEvent), checkpointEvent.getName(), baseDir);\n                // clear arguments\n                if (resumeCheckpointReq == null) {\n                    resumeCheckpointReq = new HashMap<>(processCfg);\n                    resumeCheckpointReq.remove(Constants.Request.ARGUMENTS_KEY);\n                }\n                processCfg = resumeCheckpointReq;\n            } else {\n                // no checkpoints, stop the execution and wait for an external event\n                return;\n            }\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private Map<String, Object> readRequest(Path baseDir) throws ExecutionException {\n        Path p = baseDir.resolve(Constants.Files.CONFIGURATION_FILE_NAME);\n\n        try (InputStream in = Files.newInputStream(p)) {\n            return objectMapper.readValue(in, Map.class);\n        } catch (IOException e) {\n            throw new ExecutionException(\"Error while reading request data\", e);\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private Map<String, Object> readPolicyRules(Path ws) throws ExecutionException {\n        Path policyFile = ws.resolve(Constants.Files.CONCORD_SYSTEM_DIR_NAME).resolve(Constants.Files.POLICY_FILE_NAME);\n        if (!Files.exists(policyFile)) {\n            return Collections.emptyMap();\n        }\n\n        try {\n            return objectMapper.readValue(policyFile.toFile(), Map.class);\n        } catch (IOException e) {\n            throw new ExecutionException(\"Error while reading policy rules\");\n        }\n    }\n\n    @SuppressWarnings(\"BusyWait\")\n    private static UUID readInstanceId(Path src) {\n        long errorCount = 0;\n        while (true) {\n            byte[] id = readIfExists(src);\n            if (id != null && id.length > 0) {\n                String s = new String(id);\n                try {\n                    return UUID.fromString(s.trim());\n                } catch (IllegalArgumentException e) {\n                    errorCount++;\n                    if (errorCount % 10 == 0) {\n                        log.warn(\"waitForInstanceId ['{}'] -> value: '{}', error: {}\", src, s, e.getMessage());\n                    }\n                }\n            }\n\n            // we are not using WatchService as it has issues when running in Docker\n            try {\n                Thread.sleep(100);\n            } catch (InterruptedException e) {\n                Thread.currentThread().interrupt();\n            }\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static UUID getCheckpointId(Event e) {\n        String s = MapUtils.getString((Map<String, Object>) e.getPayload(), \"checkpointId\");\n        if (s == null) {\n            return null;\n        }\n        return UUID.fromString(s);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static UUID getCorrelationId(Event e) {\n        String s = MapUtils.getString((Map<String, Object>) e.getPayload(), \"correlationId\");\n        if (s == null) {\n            return null;\n        }\n        return UUID.fromString(s);\n    }\n\n    private static Collection<Event> start(Engine e, Variables vars, Map<String, Object> req, String instanceId, Path baseDir) throws ExecutionException {\n        // get the entry point\n        String entryPoint = (String) req.get(Constants.Request.ENTRY_POINT_KEY);\n        if (entryPoint == null) {\n            throw new ExecutionException(\"Entry point must be set\");\n        }\n\n        // prepare the process' arguments\n        Map<String, Object> args = createArgs(instanceId, baseDir, req);\n\n        Object initiator = req.get(Constants.Request.INITIATOR_KEY);\n        if (initiator != null) {\n            args.put(Constants.Request.INITIATOR_KEY, initiator);\n            args.put(Constants.Request.CURRENT_USER_KEY, initiator);\n        }\n\n        // start the process\n        log.debug(\"start ['{}', '{}'] -> entry point: {}, starting...\", instanceId, baseDir, entryPoint);\n\n        e.start(instanceId, entryPoint, vars, args);\n\n        // save the suspended state marker if needed\n        return finalizeState(e, instanceId, baseDir);\n    }\n\n    private static Collection<Event> resume(Engine e, Map<String, Object> req, String instanceId, Path baseDir, String eventName) throws ExecutionException {\n        Map<String, Object> args = createArgs(instanceId, baseDir, req);\n\n        Object currentUser = req.get(Constants.Request.CURRENT_USER_KEY);\n        if (currentUser != null) {\n            args.put(Constants.Request.CURRENT_USER_KEY, currentUser);\n        }\n\n        log.debug(\"resume ['{}', '{}', '{}'] -> resuming...\", instanceId, baseDir, eventName);\n\n        // start the process\n        e.resume(instanceId, eventName, args);\n\n        // save the suspended state marker if needed\n        return finalizeState(e, instanceId, baseDir);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Collection<String> getActiveProfiles(Map<String, Object> cfg) {\n        if (cfg == null) {\n            return Collections.emptyList();\n        }\n\n        Object v = cfg.get(Constants.Request.ACTIVE_PROFILES_KEY);\n        if (v == null) {\n            return Collections.emptyList();\n        }\n\n        return (Collection<String>) v;\n    }\n\n    private static ProjectDefinition loadProject(Path baseDir) throws ExecutionException {\n        try {\n            // assume all imports were processed by the agent\n            return new ProjectLoader(new NoopImportManager())\n                    .loadProject(baseDir, new NoopImportsNormalizer(), ImportsListener.NOP_LISTENER)\n                    .getProjectDefinition();\n        } catch (Exception e) {\n            throw new ExecutionException(\"Error while loading a project\", e);\n        }\n    }\n\n    private static Variables readSavedVariables(Path baseDir) {\n        Path stateDir = baseDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(Constants.Files.JOB_STATE_DIR_NAME);\n\n        Path varsFile = stateDir.resolve(Constants.Files.LAST_KNOWN_VARIABLES_FILE_NAME);\n        if (!Files.exists(varsFile)) {\n            return null;\n        }\n\n        Variables vars;\n        try (ObjectInputStream in = new ObjectInputStream(Files.newInputStream(varsFile))) {\n            vars = (Variables) in.readObject();\n        } catch (ClassNotFoundException | IOException e) {\n            log.error(\"Can't restore the saved variables: {}\", e.getMessage());\n            return null;\n        }\n\n        // if a process error was successfully handled by the engine, it will be saved as a `lastError` variable\n        // we should't overwrite it with the Agent's version\n        if (vars.hasVariable(Constants.Context.LAST_ERROR_KEY)) {\n            return vars;\n        }\n\n        Path lastErrorFile = stateDir.resolve(Constants.Files.LAST_ERROR_FILE_NAME);\n        if (!Files.exists(lastErrorFile)) {\n            return vars;\n        }\n\n        try (ObjectInputStream in = new ObjectInputStream(Files.newInputStream(lastErrorFile))) {\n            Throwable t = (Throwable) in.readObject();\n            vars = vars.setVariable(Constants.Context.LAST_ERROR_KEY, t);\n        } catch (ClassNotFoundException | IOException e) {\n            log.error(\"Can't restore the saved last error variable: {}\", e.getMessage());\n            return vars;\n        }\n\n        return vars;\n    }\n\n    @SuppressWarnings({\"unchecked\"})\n    private static Map<String, Object> createArgs(String instanceId, Path workDir, Map<String, Object> cfg) {\n        Map<String, Object> m = new LinkedHashMap<>();\n\n        // original arguments\n        Map<String, Object> args = (Map<String, Object>) cfg.get(Constants.Request.ARGUMENTS_KEY);\n        if (args != null) {\n            m.putAll(args);\n        }\n\n        // instance ID\n        m.put(Constants.Context.TX_ID_KEY, instanceId);\n\n        // workDir\n        String dir = workDir.toAbsolutePath().toString();\n        m.put(Constants.Context.WORK_DIR_KEY, dir);\n\n        // out variables\n        Object outExpr = cfg.get(Constants.Request.OUT_EXPRESSIONS_KEY);\n        if (outExpr != null) {\n            m.put(Constants.Context.OUT_EXPRESSIONS_KEY, outExpr);\n        }\n\n        // processInfo\n        Map<String, Object> processInfo = (Map<String, Object>) cfg.get(Constants.Request.PROCESS_INFO_KEY);\n        if (processInfo != null) {\n            m.put(Constants.Request.PROCESS_INFO_KEY, processInfo);\n        }\n\n        // projectInfo\n        Map<String, Object> projectInfo = (Map<String, Object>) cfg.get(Constants.Request.PROJECT_INFO_KEY);\n        if (projectInfo != null) {\n            m.put(Constants.Request.PROJECT_INFO_KEY, projectInfo);\n        }\n\n        return m;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private Set<String> getMetaVariables(Map<String, Object> cfg) {\n        Map<String, Object> meta = (Map<String, Object>) cfg.get(Constants.Request.META);\n        if (meta != null) {\n            return meta.keySet();\n        }\n        return Collections.emptySet();\n    }\n\n    private EventConfiguration getEventCfg(Map<String, Object> cfg) {\n        Map<String, Object> m = MapUtils.getMap(cfg, \"runner\", Collections.emptyMap());\n        m = MapUtils.getMap(m, \"events\", Collections.emptyMap());\n        return objectMapper.convertValue(m, EventConfiguration.class);\n    }\n\n    private static Collection<Event> finalizeState(Engine engine, String instanceId, Path baseDir) throws ExecutionException {\n        Path stateDir = baseDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(Constants.Files.JOB_STATE_DIR_NAME);\n\n        try {\n            Path resumeMarker = stateDir.resolve(RESUME_MARKER);\n            if (Files.exists(resumeMarker)) {\n                Files.delete(resumeMarker);\n            }\n\n            Path suspendMarker = stateDir.resolve(SUSPEND_MARKER);\n            if (Files.exists(suspendMarker)) {\n                Files.delete(suspendMarker);\n            }\n        } catch (IOException e) {\n            throw new ExecutionException(\"State cleanup error\", e);\n        }\n\n        EventService es = engine.getEventService();\n        Collection<Event> events = es.getEvents(instanceId);\n\n        try {\n            if (events.isEmpty()) {\n                PathUtils.deleteRecursively(stateDir);\n                log.debug(\"finalizeState ['{}'] -> removed the state\", instanceId);\n            } else {\n                if (!Files.exists(stateDir)) {\n                    Files.createDirectories(stateDir);\n                }\n\n                Set<String> eventNames = events.stream().map(Event::getName).collect(Collectors.toSet());\n\n                Path marker = stateDir.resolve(SUSPEND_MARKER);\n                Files.write(marker, eventNames);\n                log.debug(\"finalizeState ['{}'] -> created the suspended marker\", instanceId);\n            }\n            return events;\n        } catch (IOException e) {\n            throw new ExecutionException(\"State saving error\", e);\n        }\n    }\n\n    public static void main(String[] args) {\n        // determine current working directory, it should contain the payload\n        Path baseDir = Paths.get(System.getProperty(\"user.dir\"));\n\n        try {\n            long t1 = System.currentTimeMillis();\n\n            // load the config\n            RunnerConfiguration runnerCfg = validate(loadRunnerCfg(args));\n            // TODO enable security manager\n\n            // load dependencies\n            List<URL> deps = loadDependencyList(runnerCfg);\n            if (runnerCfg.debug()) {\n                log.info(\"Effective dependencies:\\n\\t{}\", deps.stream()\n                        .map(URL::toString)\n                        .collect(Collectors.joining(\"\\n\\t\")));\n            }\n\n            URLClassLoader depsClassLoader = new URLClassLoader(deps.toArray(new URL[0]), Main.class.getClassLoader()); // NOSONAR\n            Thread.currentThread().setContextClassLoader(depsClassLoader);\n\n            // create the injector to wire up and initialize all dependencies\n            Injector injector = createInjector(runnerCfg, depsClassLoader, baseDir);\n\n            Main main = injector.getInstance(Main.class);\n\n            long t2 = System.currentTimeMillis();\n            if (runnerCfg.debug()) {\n                log.info(\"Runtime loaded in {}ms\", (t2 - t1));\n            }\n\n            main.run(runnerCfg, baseDir);\n\n            // force exit (helps with runaway threads)\n            System.exit(0);\n        } catch (Throwable e) { // catch both errors and exceptions\n            // try to unroll nested exceptions to get a meaningful one\n            Throwable t = unroll(e);\n\n            if (t instanceof UserDefinedException) {\n                log.error(\"{}\", t.getMessage());\n            } else {\n                log.error(\"main -> unhandled exception\", t);\n            }\n            saveLastError(baseDir, t);\n            System.exit(1);\n        }\n    }\n\n    private static RunnerConfiguration loadRunnerCfg(String[] args) throws IOException {\n        if (args.length < 1) {\n            return RunnerConfiguration.builder().build();\n        }\n\n        Path cfgFile = Paths.get(args[0]);\n        try (InputStream in = Files.newInputStream(cfgFile)) {\n            return createObjectMapper().readValue(in, RunnerConfiguration.class);\n        }\n    }\n\n    // TODO bind the same ObjectMapper instance into the injector?\n    private static ObjectMapper createObjectMapper() {\n        ObjectMapper om = new ObjectMapper();\n        om.registerModule(new GuavaModule());\n        om.registerModule(new Jdk8Module());\n        return om;\n    }\n\n    private static RunnerConfiguration validate(RunnerConfiguration cfg) {\n        if (cfg.agentId() == null) {\n            throw new IllegalArgumentException(\"Configuration parameter 'agentId' is required\");\n        }\n\n        if (cfg.api() == null || cfg.api().baseUrl() == null) {\n            throw new IllegalArgumentException(\"Configuration parameter 'api.baseUrl' is required\");\n        }\n\n        return cfg;\n    }\n\n    private static List<URL> loadDependencyList(RunnerConfiguration cfg) throws Exception {\n        if (cfg.dependencies() == null) {\n            return Collections.emptyList();\n        }\n\n        return parseDeps(cfg.dependencies());\n    }\n\n    private static String getSessionToken(Map<String, Object> cfg) {\n        Map<String, Object> processInfo = MapUtils.getMap(cfg, Constants.Request.PROCESS_INFO_KEY, Collections.emptyMap());\n        String s = MapUtils.getString(processInfo, \"sessionKey\");\n        if (s == null) {\n            throw new IllegalStateException(\"Can't find 'processInfo.sessionKey' in the process configuration\");\n        }\n        return s;\n    }\n\n    private static Throwable unroll(Throwable e) {\n        if (e instanceof ExecutionException) {\n            if (e.getCause() != null) {\n                e = e.getCause();\n            }\n        }\n\n        if (e instanceof BpmnError) {\n            if (e.getCause() != null) {\n                e = e.getCause();\n            }\n        }\n\n        if (e instanceof javax.el.ELException) {\n            if (e.getCause() != null) {\n                e = e.getCause();\n            }\n        }\n\n        return e;\n    }\n\n    private static List<URL> parseDeps(Collection<String> deps) throws IOException {\n        List<URL> result = deps.stream()\n                .sorted()\n                .map(s -> {\n                    if (!Files.exists(Paths.get(s))) {\n                        throw new RuntimeException(\"Dependency file: \" + s + \" not found\");\n                    }\n\n                    try {\n                        return new URL(\"file://\" + s);\n                    } catch (MalformedURLException e) {\n                        throw new RuntimeException(\"Invalid dependency path \" + s + \":\" + e.getMessage());\n                    }\n                }).collect(Collectors.toList());\n\n        // payload's own libraries are stored in `./lib/` directory in the working directory\n        Path baseDir = Paths.get(System.getProperty(\"user.dir\"));\n        Path lib = baseDir.resolve(Constants.Files.LIBRARIES_DIR_NAME);\n        if (Files.exists(lib)) {\n            try (Stream<Path> s = Files.list(lib)) {\n                s.forEach(f -> {\n                    if (f.toString().endsWith(\".jar\")) {\n                        try {\n                            result.add(f.toUri().toURL());\n                        } catch (MalformedURLException e) {\n                            throw new RuntimeException(e);\n                        }\n                    }\n                });\n            }\n        }\n\n        return result;\n    }\n\n    private static Injector createInjector(RunnerConfiguration runnerCfg, ClassLoader depsClassLoader, Path workDir) {\n        ClassLoader cl = Main.class.getClassLoader();\n\n        Module cfg = new AbstractModule() {\n            @Override\n            protected void configure() {\n                bind(RunnerConfiguration.class).toInstance(runnerCfg);\n                bind(WorkingDirectory.class).toInstance(new WorkingDirectory(workDir));\n            }\n        };\n\n        Module tasks = new AbstractModule() {\n            @Override\n            protected void configure() {\n                TaskClasses taskClassesHolder = new TaskClasses();\n                bindListener(new SubClassesOf(Task.class), new TaskClassesListener(taskClassesHolder, false));\n                bind(TaskClasses.class).toInstance(taskClassesHolder);\n            }\n        };\n\n        Module taskCallModule = new AbstractModule() {\n            @Override\n            protected void configure() {\n                install(new AbstractModule() {\n                    @Override\n                    protected void configure() {\n                        bindInterceptor(\n                                TaskCallInterceptor.CLASS_MATCHER,\n                                TaskCallInterceptor.METHOD_MATCHER,\n                                new TaskCallInterceptor(PolicyEngineHolder.INSTANCE));\n                    }\n                });\n            }\n        };\n\n        Module m = new WireModule(\n                cfg,\n                tasks,\n                taskCallModule,\n                new SpaceModule(new URLClassSpace(cl), runnerCfg.enableSisuIndex() ? BeanScanning.GLOBAL_INDEX : BeanScanning.CACHE),\n                new SpaceModule(new URLClassSpace(depsClassLoader), runnerCfg.enableSisuIndex() ? BeanScanning.GLOBAL_INDEX : BeanScanning.CACHE));\n\n        return Guice.createInjector(m);\n    }\n\n    private static void saveLastError(Path baseDir, Throwable t) {\n        Path attachmentsDir = baseDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME);\n        Path stateDir = attachmentsDir.resolve(Constants.Files.JOB_STATE_DIR_NAME);\n        try {\n            if (Files.notExists(stateDir)) {\n                Files.createDirectories(stateDir);\n            }\n        } catch (Throwable e) {\n            log.error(\"Can't create attachments dir: {}\", e.getMessage());\n            return;\n        }\n\n        Path dst = stateDir.resolve(Constants.Files.LAST_ERROR_FILE_NAME);\n\n        try (OutputStream out = Files.newOutputStream(dst, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {\n            SerializationUtils.serialize(out, t);\n        } catch (Throwable e) {\n            log.error(\"Can't save the last unhandled error: {}\", e.getMessage());\n        }\n\n        try {\n            Map<String, Object> error = ProcessErrorProcessor.process(t);\n\n            Map<String, Object> outVars = OutVariablesParser.read(attachmentsDir);\n            Map<String, Object> result = new HashMap<>(outVars);\n            result.putAll(error);\n\n            OutVariablesParser.write(attachmentsDir, result);\n        } catch (Throwable e) {\n            log.error(\"Can't write out variables: {}\", e.getMessage());\n        }\n    }\n\n    private static byte[] readIfExists(Path p) {\n        try {\n            if (Files.notExists(p)) {\n                return null;\n            }\n            return Files.readAllBytes(p);\n        } catch (Exception e) {\n            log.warn(\"readIfExists ['{}'] -> error: {}\", p, e.getMessage());\n            return null;\n        }\n    }\n\n    private static class SubClassesOf extends AbstractMatcher<TypeLiteral<?>> {\n\n        private final Class<?> baseClass;\n\n        private SubClassesOf(Class<?> baseClass) {\n            this.baseClass = baseClass;\n        }\n\n        @Override\n        public boolean matches(TypeLiteral<?> t) {\n            return baseClass.isAssignableFrom(t.getRawType());\n        }\n    }\n\n    private static class TaskClassesListener implements TypeListener {\n\n        private final TaskClasses taskClasses;\n        private final boolean debug;\n\n        private TaskClassesListener(TaskClasses taskClasses, boolean debug) {\n            this.taskClasses = taskClasses;\n            this.debug = debug;\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        public <T> void hear(TypeLiteral<T> typeLiteral, TypeEncounter<T> typeEncounter) {\n            Class<Task> clazz = (Class<Task>) typeLiteral.getRawType();\n\n            Named n = clazz.getAnnotation(Named.class);\n            if (n == null) {\n                return;\n            }\n\n            if (debug) {\n                log.info(\"Registering {} as {}...\", clazz, n);\n            }\n\n            taskClasses.add(n.value(), clazz);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/ObjectStorageImpl.java",
    "content": "package com.walmartlabs.concord.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.sdk.ContextUtils;\nimport com.walmartlabs.concord.sdk.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.util.concurrent.Callable;\n\n/**\n * @deprecated replaced by the JSON storage API\n */\n@Named\n@Deprecated\npublic class ObjectStorageImpl implements ObjectStorage {\n\n    private static final Logger log = LoggerFactory.getLogger(LockServiceImpl.class);\n\n    private static final int RETRY_COUNT = 3;\n    private static final long RETRY_INTERVAL = 5000;\n\n    private final ApiConfiguration cfg;\n    private final ApiClientFactory apiClientFactory;\n\n    @Inject\n    public ObjectStorageImpl(ApiConfiguration cfg, ApiClientFactory apiClientFactory) {\n        this.cfg = cfg;\n        this.apiClientFactory = apiClientFactory;\n    }\n\n    @Override\n    public BucketInfo createBucket(Context ctx, String name) throws Exception {\n        log.warn(\"ObjectStorage interface is deprecated and replaced by the JSON Storage API. \" +\n                \"It will be removed at some point in the future. Plugins must be updated to use the JSON Storage API directly.\");\n\n        ProjectInfo projectInfo = ContextUtils.getProjectInfo(ctx);\n        if (projectInfo == null) {\n            throw new IllegalArgumentException(\"Can't create an Object Storage bucket, a Concord project is required\");\n        }\n\n        InventoryEntry entry = new InventoryEntry()\n                .name(name)\n                .orgId(projectInfo.orgId())\n                .visibility(InventoryEntry.VisibilityEnum.PRIVATE);\n\n        ApiClientConfiguration c = ApiClientConfiguration.builder()\n                .sessionToken(ContextUtils.getSessionToken(ctx))\n                .build();\n        InventoriesApi api = new InventoriesApi(apiClientFactory.create(c));\n        CreateInventoryResponse resp = withRetry(() -> api.createOrUpdateInventory(projectInfo.orgName(), entry));\n        log.info(\"createBucket ['{}', '{}'] -> done ({})\", projectInfo.orgName(), name, resp.getId());\n\n        String address = String.format(\"%s/api/v1/org/%s/inventory/%s/data/%s?singleItem=true\",\n                cfg.getBaseUrl(), projectInfo.orgName(), name, name);\n\n        return ImmutableBucketInfo.builder()\n                .address(address)\n                .build();\n    }\n\n    private static <T> T withRetry(Callable<T> c) throws ApiException {\n        return ClientUtils.withRetry(RETRY_COUNT, RETRY_INTERVAL, c);\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/OutVariablesParser.java",
    "content": "package com.walmartlabs.concord.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.project.InternalConstants;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.util.Collections;\nimport java.util.Map;\n\npublic class OutVariablesParser {\n\n    private static final ObjectMapper objectMapper = new ObjectMapper();\n\n    public static void write(Path storeDir, Map<String, Object> vars) {\n        try {\n            if (!Files.exists(storeDir)) {\n                Files.createDirectories(storeDir);\n            }\n\n            Path p = storeDir.resolve(InternalConstants.Files.OUT_VALUES_FILE_NAME);\n            try (OutputStream out = Files.newOutputStream(p, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {\n                objectMapper.writeValue(out, vars);\n            }\n        } catch (IOException e) {\n            throw new RuntimeException(\"Error while saving OUT variables\", e);\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static Map<String, Object> read(Path storeDir) {\n        Path p = storeDir.resolve(InternalConstants.Files.OUT_VALUES_FILE_NAME);\n        if (!Files.exists(p)) {\n            return Collections.emptyMap();\n        }\n\n        try {\n            return objectMapper.readValue(p.toFile(), Map.class);\n        } catch (Exception e) {\n            throw new RuntimeException(\"Error while reading OUT variables\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/PolicyEngineHolder.java",
    "content": "package com.walmartlabs.concord.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\n\npublic class PolicyEngineHolder {\n\n    public static final PolicyEngineHolder INSTANCE = new PolicyEngineHolder();\n\n    private PolicyEngine engine;\n\n    private PolicyEngineHolder() {\n    }\n\n    public PolicyEngine getEngine() {\n        return engine;\n    }\n\n    public void setEngine(PolicyEngine engine) {\n        this.engine = engine;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/ProcessApiClient.java",
    "content": "package com.walmartlabs.concord.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\n\nimport java.util.Map;\nimport java.util.UUID;\n\npublic class ProcessApiClient {\n\n    private final ProcessApi processApi;\n    private final CheckpointApi checkpointApi;\n\n    private final int retryCount;\n    private final long retryInterval;\n\n    public ProcessApiClient(RunnerConfiguration runnerCfg, ApiClient apiClient) {\n        this.processApi = new ProcessApi(apiClient);\n        this.checkpointApi = new CheckpointApi(apiClient);\n\n        this.retryCount = runnerCfg.api().retryCount();\n        this.retryInterval = runnerCfg.api().retryInterval();\n    }\n\n    public void uploadCheckpoint(UUID instanceId, Map<String, Object> data) throws ApiException {\n        ClientUtils.withRetry(retryCount, retryInterval, () -> {\n            checkpointApi.uploadCheckpoint(instanceId, data);\n            return null;\n        });\n    }\n\n    public void updateStatus(UUID instanceId, String agentId, ProcessEntry.StatusEnum status) throws ApiException {\n        ClientUtils.withRetry(retryCount, retryInterval, () -> {\n            processApi.updateStatus(instanceId, agentId, status.name());\n            return null;\n        });\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/SecretServiceImpl.java",
    "content": "package com.walmartlabs.concord.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.secret.BinaryDataSecret;\nimport com.walmartlabs.concord.common.secret.KeyPair;\nimport com.walmartlabs.concord.common.secret.UsernamePassword;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.Secret;\nimport com.walmartlabs.concord.sdk.SecretService;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.xml.bind.DatatypeConverter;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.UUID;\n\n@Named\npublic class SecretServiceImpl implements SecretService {\n\n    private final ApiClientFactory clientFactory;\n\n    @Inject\n    public SecretServiceImpl(ApiClientFactory clientFactory) {\n        this.clientFactory = clientFactory;\n    }\n\n    @Override\n    public String exportAsString(Context ctx, String instanceId, String name, String password) throws Exception {\n        return exportAsString(ctx, instanceId, null, name, password);\n    }\n\n    @Override\n    public String exportAsString(Context ctx, String instanceId, String orgName, String name, String password) throws Exception {\n        BinaryDataSecret s = get(ctx, orgName, name, password, SecretEntryV2.TypeEnum.DATA);\n        return new String(s.getData());\n    }\n\n    @Override\n    public Map<String, String> exportKeyAsFile(Context ctx, String instanceId, String workDir, String name, String password) throws Exception {\n        return exportKeyAsFile(ctx, instanceId, workDir, null, name, password);\n    }\n\n    @Override\n    public Map<String, String> exportKeyAsFile(Context ctx, String instanceId, String workDir, String orgName, String name, String password) throws Exception {\n        KeyPair kp = get(ctx, orgName, name, password, SecretEntryV2.TypeEnum.KEY_PAIR);\n\n        Path baseDir = Paths.get(workDir);\n        Path tmpDir = assertTempDir(baseDir);\n\n        Path privateKey = Files.createTempFile(tmpDir, \"private\", \".key\");\n        Files.write(privateKey, kp.getPrivateKey());\n\n        Path publicKey = Files.createTempFile(tmpDir, \"public\", \".key\");\n        Files.write(publicKey, kp.getPublicKey());\n\n        Map<String, String> m = new HashMap<>();\n        m.put(\"private\", baseDir.relativize(privateKey).toString());\n        m.put(\"public\", baseDir.relativize(publicKey).toString());\n\n        return m;\n    }\n\n    @Override\n    public Map<String, String> exportCredentials(Context ctx, String instanceId, String workDir, String name, String password) throws Exception {\n        return exportCredentials(ctx, instanceId, workDir, null, name, password);\n    }\n\n    @Override\n    public Map<String, String> exportCredentials(Context ctx, String instanceId, String workDir, String orgName, String name, String password) throws Exception {\n        UsernamePassword up = get(ctx, orgName, name, password, SecretEntryV2.TypeEnum.USERNAME_PASSWORD);\n\n        Map<String, String> m = new HashMap<>();\n        m.put(\"username\", up.getUsername());\n        m.put(\"password\", new String(up.getPassword()));\n        return m;\n    }\n\n    @Override\n    public String exportAsFile(Context ctx, String instanceId, String workDir, String name, String password) throws Exception {\n        return exportAsFile(ctx, instanceId, workDir, null, name, password);\n    }\n\n    @Override\n    public String exportAsFile(Context ctx, String instanceId, String workDir, String orgName, String name, String password) throws Exception {\n        BinaryDataSecret bds = get(ctx, orgName, name, password, SecretEntryV2.TypeEnum.DATA);\n\n        Path baseDir = Paths.get(workDir);\n        Path tmpDir = assertTempDir(baseDir);\n\n        Path p = Files.createTempFile(tmpDir, \"file\", \".bin\");\n        Files.write(p, bds.getData());\n\n        return baseDir.relativize(p).toString();\n    }\n\n    @Override\n    public String encryptString(Context ctx, String instanceId, String orgName, String projectName, String value) throws Exception {\n        return new SecretClient(apiClient(ctx)).encryptString(orgName, projectName, value);\n    }\n\n    @Override\n    public String decryptString(Context ctx, String instanceId, String s) throws Exception {\n        byte[] input;\n\n        try {\n            input = DatatypeConverter.parseBase64Binary(s);\n        } catch (Exception e) {\n            throw new IllegalArgumentException(\"Invalid encrypted string value, please verify that it was specified/copied correctly: \" + e.getMessage());\n        }\n\n        return new String(new SecretClient(apiClient(ctx)).decryptString(UUID.fromString(instanceId), input));\n    }\n\n    private <T extends Secret> T get(Context ctx, String orgName, String secretName, String password, SecretEntryV2.TypeEnum type) throws Exception {\n        try {\n            return new SecretClient(apiClient(ctx)).getData(assertOrgName(ctx, orgName), secretName, password, type);\n        } catch (com.walmartlabs.concord.client2.SecretNotFoundException e) {\n            throw new SecretNotFoundException(e.getOrgName(), e.getSecretName());\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private String assertOrgName(Context ctx, String orgName) {\n        if (orgName != null) {\n            return orgName;\n        }\n\n        Map<String, Object> pi = (Map<String, Object>) ctx.getVariable(Constants.Request.PROJECT_INFO_KEY);\n        return Optional.ofNullable(pi)\n                .map(p -> (String) p.get(\"orgName\"))\n                .orElseThrow(() -> new IllegalArgumentException(\"Organization name not specified\"));\n    }\n\n    private ApiClient apiClient(Context ctx) {\n        ApiClientConfiguration cfg = ApiClientConfiguration.builder()\n                .sessionToken(ContextUtils.getSessionToken(ctx))\n                .build();\n\n        return clientFactory.create(cfg);\n    }\n\n    private static Path assertTempDir(Path baseDir) throws IOException {\n        Path p = baseDir.resolve(\".tmp\");\n        if (!Files.exists(p)) {\n            Files.createDirectories(p);\n        }\n        return p;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/SerializationUtils.java",
    "content": "package com.walmartlabs.concord.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.takari.bpm.api.Variables;\nimport io.takari.bpm.state.ProcessInstance;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.*;\nimport java.util.Map;\n\npublic class SerializationUtils {\n\n    private static final Logger log = LoggerFactory.getLogger(SerializationUtils.class);\n\n    public static void serialize(OutputStream out, Object o) throws IOException {\n        try (ObjectOutputStream oos = new ObjectOutputStream(out)) {\n            oos.writeObject(o);\n        } catch (NotSerializableException e) {\n            log.warn(\"Check if you're setting any not serializable values in your 'script' or 'task' steps.\");\n            reportNotSerializableItems(e, o);\n        }\n    }\n\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    private static void reportNotSerializableItems(NotSerializableException err, Object o) throws IOException {\n        if (o instanceof Variables) {\n            Variables v = (Variables) o;\n            reportNotSerializableItems(err, v.asMap());\n            return;\n        } else if (o instanceof Map) {\n            Map<Object, Object> m = (Map<Object, Object>) o;\n            for (Map.Entry<Object, Object> e : m.entrySet()) {\n                if (!isSerializable(e.getKey())) {\n                    log.warn(\"Not serializable key: {}\", e.getKey());\n                }\n\n                Object v = e.getValue();\n                if (v == null) {\n                    continue;\n                }\n\n                if (!isSerializable(v)) {\n                    Class k = v.getClass();\n                    log.warn(\"Not serializable value: {} -> {} = {}\", e.getKey(), k, v);\n                }\n            }\n        } else if (o instanceof ProcessInstance) {\n            ProcessInstance i = (ProcessInstance) o;\n            reportNotSerializableItems(err, i.getVariables());\n            return;\n        }\n\n        throw err;\n    }\n\n    private static boolean isSerializable(Object o) {\n        try (ObjectOutputStream oos = new ObjectOutputStream(new ByteArrayOutputStream())) {\n            oos.writeObject(o);\n        } catch (IOException e) {\n            return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/TaskCallInterceptor.java",
    "content": "package com.walmartlabs.concord.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.matcher.AbstractMatcher;\nimport com.google.inject.matcher.Matcher;\nimport com.walmartlabs.concord.common.ReflectionUtils;\nimport com.walmartlabs.concord.policyengine.CheckResult;\nimport com.walmartlabs.concord.policyengine.TaskRule;\nimport com.walmartlabs.concord.sdk.Task;\nimport org.aopalliance.intercept.MethodInterceptor;\nimport org.aopalliance.intercept.MethodInvocation;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Named;\nimport java.lang.reflect.Method;\n\npublic class TaskCallInterceptor implements MethodInterceptor {\n\n    public static final Matcher<? super Class<?>> CLASS_MATCHER = new AbstractMatcher<Class<?>>() {\n        @Override\n        public boolean matches(Class<?> aClass) {\n            return Task.class.isAssignableFrom(aClass);\n        }\n    };\n\n    public static final AbstractMatcher<Method> METHOD_MATCHER = new AbstractMatcher<Method>() {\n        @Override\n        public boolean matches(Method method) {\n            return !method.isSynthetic();\n        }\n    };\n\n    private static final Logger log = LoggerFactory.getLogger(TaskCallInterceptor.class);\n\n    private final PolicyEngineHolder holder;\n\n    public TaskCallInterceptor(PolicyEngineHolder holder) {\n        this.holder = holder;\n    }\n\n    @Override\n    public Object invoke(MethodInvocation invocation) throws Throwable {\n        if (holder.getEngine() == null) {\n            return invocation.proceed();\n        }\n\n        Named n = ReflectionUtils.findAnnotation(invocation.getThis().getClass(), Named.class);\n        if (n == null) {\n            return invocation.proceed();\n        }\n\n        CheckResult<TaskRule, String> result = holder.getEngine().getTaskPolicy().check(\n                n.value(),\n                invocation.getMethod().getName(),\n                invocation.getArguments(), null);\n\n        result.getWarn().forEach(d -> {\n            log.warn(\"Potentially restricted task call '{}' (task policy {})\", n.value(), d.getRule().toString());\n        });\n        result.getDeny().forEach(d -> {\n            log.error(\"Task call '{}' is forbidden by the task policy {}\", n.value(), d.getRule().toString());\n        });\n\n        if (!result.getDeny().isEmpty()) {\n            throw new RuntimeException(\"Found forbidden tasks\");\n        }\n\n        return invocation.proceed();\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/TaskClasses.java",
    "content": "package com.walmartlabs.concord.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Task;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class TaskClasses {\n\n    private final Map<String, Class<? extends Task>> classes = new HashMap<>();\n\n    public void add(String taskName, Class<? extends Task> clazz) {\n        Class<? extends Task> old = classes.put(taskName, clazz);\n        if (old != null) {\n            throw new IllegalArgumentException(\"Non-unique task name: \" + taskName);\n        }\n    }\n\n    public Class<? extends Task> get(String taskName) {\n        return classes.get(taskName);\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/VariablesSnapshotListener.java",
    "content": "package com.walmartlabs.concord.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.InternalConstants;\nimport io.takari.bpm.EngineListener;\nimport io.takari.bpm.api.Variables;\nimport io.takari.bpm.state.BpmnErrorHelper;\nimport io.takari.bpm.state.ProcessInstance;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\n\n/**\n * Creates a snapshot of variables each time the process stops.\n * The snapshot can be used later to restore the variables in, for example, process forks,\n * onCancel handlers, etc.\n */\npublic class VariablesSnapshotListener implements EngineListener {\n\n    private static final Logger log = LoggerFactory.getLogger(VariablesSnapshotListener.class);\n\n    private final Path stateDir;\n\n    public VariablesSnapshotListener(Path stateDir) {\n        this.stateDir = stateDir;\n    }\n\n    @Override\n    public ProcessInstance onFinalize(ProcessInstance state) {\n        return processState(state);\n    }\n\n    @Override\n    public void onUnhandledException(ProcessInstance state) {\n        processState(state);\n    }\n\n    private ProcessInstance processState(ProcessInstance state) {\n        Variables vars = state.getVariables();\n\n        // remove some internal variables before saving\n        vars = BpmnErrorHelper.clear(vars);\n\n        try {\n            Path dst = stateDir.resolve(InternalConstants.Files.LAST_KNOWN_VARIABLES_FILE_NAME);\n            try (OutputStream out = Files.newOutputStream(dst, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {\n                SerializationUtils.serialize(out, vars);\n            }\n        } catch (IOException e) {\n            log.error(\"Can't save a snapshot of the process variables. Process forks (including onError, onCancel and \" +\n                    \"other handlers) may not receive the updated variables. Error: {}\", e.getMessage());\n        }\n\n        return state;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/ApiConfigurationImpl.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runner.ContextUtils;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.sdk.ApiConfiguration;\nimport com.walmartlabs.concord.sdk.Context;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\n\n@Named\npublic class ApiConfigurationImpl implements ApiConfiguration {\n\n    private final RunnerConfiguration runnerCfg;\n\n    @Inject\n    public ApiConfigurationImpl(RunnerConfiguration runnerCfg) {\n        this.runnerCfg = runnerCfg;\n    }\n\n    @Override\n    public String getBaseUrl() {\n        return runnerCfg.api().baseUrl();\n    }\n\n    @Override\n    public int connectTimeout() {\n        return runnerCfg.api().connectTimeout();\n    }\n\n    @Override\n    public int readTimeout() {\n        return runnerCfg.api().readTimeout();\n    }\n\n    public String getSessionToken(Context ctx) {\n        return ContextUtils.getSessionToken(ctx);\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/CheckpointTask.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.ContextUtils;\nimport com.walmartlabs.concord.sdk.Task;\n\nimport javax.inject.Named;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.regex.Pattern;\n\n@Named(\"checkpoint\")\npublic class CheckpointTask implements Task, LogTagMetadataProvider {\n\n    private static final Pattern PATTERN = Pattern.compile(\"^[0-9a-zA-Z][0-9a-zA-Z_@.\\\\-~ ]{1,128}$\");\n\n    @Override\n    public void execute(Context ctx) {\n        String checkpointName = ContextUtils.assertString(ctx, \"checkpointName\");\n        if (!PATTERN.matcher(checkpointName).matches()) {\n            throw new IllegalArgumentException(\"Invalid checkpoint name: \" + checkpointName + \". \" +\n                    \"If you're using an expression in the checkpoint's name please validate its correctness. \" +\n                    \"Checkpoint names must start with a digit or a latin letter, the length must be between 2 and 128 characters. \" +\n                    \"Can contain whitespace, minus (-), tilde (~), dot (.), underscore (_) and @ characters.\");\n        }\n\n        UUID checkpointId = UUID.randomUUID();\n        ctx.setVariable(\"checkpointId\", checkpointId.toString());\n\n        Map<String, Object> eventPayload = new HashMap<>();\n        eventPayload.put(\"checkpointId\", checkpointId.toString());\n        eventPayload.put(\"correlationId\", ctx.getEventCorrelationId().toString());\n        ctx.suspend(checkpointName, eventPayload, false);\n    }\n\n    @Override\n    public Map<String, Object> createLogTagMetadata(Context ctx) {\n        String checkpointName = (String) ctx.getVariable(\"checkpointName\");\n        return Collections.singletonMap(\"checkpointName\", checkpointName);\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/ConcordExecutionContextFactory.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.InternalConstants;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.Context;\nimport io.takari.bpm.api.ExecutionContext;\nimport io.takari.bpm.api.ExecutionContextFactory;\nimport io.takari.bpm.api.ExecutionException;\nimport io.takari.bpm.api.Variables;\nimport io.takari.bpm.context.DefaultExecutionContextFactory;\nimport io.takari.bpm.context.ExecutionContextImpl;\nimport io.takari.bpm.el.ExpressionManager;\nimport io.takari.bpm.form.FormService;\nimport io.takari.bpm.model.form.FormDefinition;\nimport io.takari.bpm.model.form.FormField;\n\nimport java.util.*;\n\npublic class ConcordExecutionContextFactory implements ExecutionContextFactory<ConcordExecutionContextFactory.ConcordExecutionContext> {\n\n    private final ExpressionManager expressionManager;\n    private final ProtectedVarContext protectedVarContext;\n    private final FormService formService;\n\n    public ConcordExecutionContextFactory(ExpressionManager expressionManager, ProtectedVarContext protectedVarContext, FormService formService) {\n        this.expressionManager = expressionManager;\n        this.protectedVarContext = protectedVarContext;\n        this.formService = formService;\n    }\n\n    @Override\n    public ConcordExecutionContext create(Variables source) {\n        return new ConcordExecutionContext(this, expressionManager, source, protectedVarContext, formService);\n    }\n\n    @Override\n    public ConcordExecutionContext create(Variables source, String processDefinitionId, String elementId) {\n        return new ConcordExecutionContext(this, expressionManager, source, processDefinitionId, elementId, protectedVarContext, formService);\n    }\n\n    @Override\n    public ExecutionContext withOverrides(ExecutionContext delegate, Map<Object, Object> overrides) {\n        return new MapBackedExecutionContext(this, expressionManager, delegate, overrides);\n    }\n\n    public static class ConcordExecutionContext extends ExecutionContextImpl implements Context {\n\n        private static final String PROTECTED_VAR_KEY = \"__protected_vars\";\n\n        private final ExecutionContextFactory<? extends ExecutionContext> ctxFactory;\n        private final ExpressionManager expressionManager;\n        private final ProtectedVarContext protectedVarContext;\n        private final FormService formService;\n\n        public ConcordExecutionContext(ExecutionContextFactory<? extends ExecutionContext> ctxFactory,\n                                       ExpressionManager expressionManager, Variables source,\n                                       ProtectedVarContext protectedVarContext,\n                                       FormService formService) {\n\n            this(ctxFactory, expressionManager, source, null, null, protectedVarContext, formService);\n        }\n\n        public ConcordExecutionContext(ExecutionContextFactory<? extends ExecutionContext> ctxFactory,\n                                       ExpressionManager expressionManager, Variables source,\n                                       String processDefinitionId, String elementId,\n                                       ProtectedVarContext protectedVarContext,\n                                       FormService formService) {\n\n            super(ctxFactory, expressionManager, source, processDefinitionId, elementId);\n            this.ctxFactory = ctxFactory;\n            this.expressionManager = expressionManager;\n            this.protectedVarContext = protectedVarContext;\n            this.formService = formService;\n        }\n\n        @Override\n        public Object interpolate(Object v, Map<String, Object> variables) {\n            return expressionManager.interpolate(ctxFactory, ctxFactory.create(new Variables(variables)), v);\n        }\n\n        @Override\n        public UUID getEventCorrelationId() {\n            return (UUID) getVariable(InternalConstants.Context.EVENT_CORRELATION_KEY);\n        }\n\n        @Override\n        public void setVariable(String key, Object value) {\n            if (PROTECTED_VAR_KEY.equals(key)) {\n                throw new RuntimeException(\"Can't set concord internal variables\");\n            }\n\n            Set<String> protectedVars = getProtectedVariableNames();\n            if (protectedVars.contains(key)) {\n                throw new RuntimeException(\"Can't rewrite protected variable with name '\" + key + \"'\");\n            }\n\n            super.setVariable(key, value);\n        }\n\n        @Override\n        public void setProtectedVariable(String key, Object value) {\n            assertProtectedVarAccess();\n\n            Set<String> protectedVars = new HashSet<>(getProtectedVariableNames());\n            protectedVars.add(key);\n            super.setVariable(PROTECTED_VAR_KEY, Collections.unmodifiableSet(protectedVars));\n            super.setVariable(key, value);\n        }\n\n        @Override\n        public Object getProtectedVariable(String key) {\n            Set<String> protectedVars = getProtectedVariableNames();\n            if (!protectedVars.contains(key)) {\n                return null;\n            }\n            return getVariable(key);\n        }\n\n        @Override\n        public void removeVariable(String key) {\n            if (PROTECTED_VAR_KEY.equals(key)) {\n                throw new RuntimeException(\"Can't remove concord internal variables\");\n            }\n\n            Set<String> protectedVars = getProtectedVariableNames();\n            if (protectedVars.contains(key)) {\n                assertProtectedVarAccess();\n\n                Set<String> newVars = new HashSet<>(protectedVars);\n                newVars.remove(key);\n                super.setVariable(PROTECTED_VAR_KEY, Collections.unmodifiableSet(newVars));\n            }\n\n            super.removeVariable(key);\n        }\n\n        @Override\n        @SuppressWarnings(\"unchecked\")\n        public void form(String formName, Map<String, Object> formOptions) {\n            Map<String, Object> env = getVariables();\n\n            Map<String, Object> interpolatedOptions = (Map<String, Object>) interpolate(formOptions);\n            FormDefinition fd = new FormDefinition(formName, toFormFields((List<Object>) interpolatedOptions.get(\"fields\")));\n\n            String txId = (String) getVariable(Constants.Context.TX_ID_KEY);\n            UUID fId = UUID.randomUUID();\n            String eventName = UUID.randomUUID().toString();\n            try {\n                formService.create(txId, fId, eventName, fd, interpolatedOptions, env);\n            } catch (ExecutionException e) {\n                throw new RuntimeException(\"create form error: \" + e.getMessage());\n            }\n\n            suspend(eventName);\n        }\n\n        @Override\n        public String getCurrentFlowName() {\n            return (String) getVariable(CURRENT_FLOW_NAME_KEY);\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        private List<FormField> toFormFields(List<Object> input) {\n            List<FormField> result = new ArrayList<>(input.size());\n\n            for (Object v : input) {\n                if (v instanceof FormField) {\n                    result.add((FormField) v);\n                } else if (v instanceof Map) {\n                    FormField f = formService.toFormField((Map<String, Object>) v);\n                    result.add(f);\n                } else {\n                    throw new IllegalArgumentException(\"Expected either a FormField instance or a map of values, got: \" + v);\n                }\n            }\n\n            return result;\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        private Set<String> getProtectedVariableNames() {\n            Set<String> result = (Set<String>) getVariable(PROTECTED_VAR_KEY);\n            if (result == null) {\n                return Collections.emptySet();\n            }\n            return result;\n        }\n\n        private void assertProtectedVarAccess() {\n            if (!protectedVarContext.hasToken()) {\n                throw new RuntimeException(\"Not allowed to set protected variable\");\n            }\n        }\n    }\n\n    public static class MapBackedExecutionContext extends DefaultExecutionContextFactory.MapBackedExecutionContext implements Context {\n\n        private final ExecutionContextFactory<? extends ExecutionContext> ctxFactory;\n        private final ExpressionManager expressionManager;\n        private final ExecutionContext delegate;\n\n        public MapBackedExecutionContext(ExecutionContextFactory<? extends ExecutionContext> executionContextFactory,\n                                         ExpressionManager exprManager,\n                                         ExecutionContext delegate,\n                                         Map<Object, Object> overrides) {\n\n            super(executionContextFactory, exprManager, delegate, overrides);\n\n            this.ctxFactory = executionContextFactory;\n            this.expressionManager = exprManager;\n            this.delegate = delegate;\n        }\n\n        @Override\n        public Object interpolate(Object v, Map<String, Object> variables) {\n            return expressionManager.interpolate(ctxFactory, ctxFactory.create(new Variables(variables)), v);\n        }\n\n        @Override\n        public void setProtectedVariable(String key, Object value) {\n            throw new IllegalStateException(\"Not supported\");\n        }\n\n        @Override\n        public Object getProtectedVariable(String key) {\n            return super.getVariable(key);\n        }\n\n        @Override\n        public void form(String formName, Map<String, Object> formOptions) {\n            ((Context) delegate).form(formName, formOptions);\n        }\n\n        @Override\n        public String getCurrentFlowName() {\n            return (String) getVariable(CURRENT_FLOW_NAME_KEY);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/ConcordFormService.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.form.ConcordFormValidator;\nimport com.walmartlabs.concord.project.yaml.YamlConverterException;\nimport com.walmartlabs.concord.project.yaml.YamlFormConverter;\nimport io.takari.bpm.api.ExecutionContextFactory;\nimport io.takari.bpm.form.DefaultFormService;\nimport io.takari.bpm.form.FormStorage;\nimport io.takari.bpm.model.form.FormField;\n\nimport java.util.Map;\n\npublic class ConcordFormService extends DefaultFormService {\n\n    public ConcordFormService(ExecutionContextFactory contextFactory, ResumeHandler resumeHandler, FormStorage formStorage) {\n        super(contextFactory, resumeHandler, formStorage, new ConcordFormValidator());\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public FormField toFormField(Map<String, Object> m) {\n        if (m.size() != 1) {\n            throw new IllegalArgumentException(\"Expected a form field definition, got: \" + m);\n        }\n\n        Map.Entry<String, Object> entry = m.entrySet().iterator().next();\n\n        String name = entry.getKey();\n\n        Object v = entry.getValue();\n        if (!(v instanceof Map)) {\n            throw new IllegalArgumentException(\"Expected a form field definition, got: \" + m);\n        }\n\n        Map<String, Object> opts = (Map<String, Object>) v;\n\n        try {\n            return YamlFormConverter.convert(name, opts, null);\n        } catch (YamlConverterException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/DefaultElementEventProcessor.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessEventRequest;\nimport io.takari.bpm.ProcessDefinitionProvider;\nimport io.takari.bpm.ProcessDefinitionUtils;\nimport io.takari.bpm.api.ExecutionException;\nimport io.takari.bpm.model.AbstractElement;\nimport io.takari.bpm.model.ProcessDefinition;\nimport io.takari.bpm.model.SourceAwareProcessDefinition;\nimport io.takari.bpm.model.SourceMap;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.time.Instant;\nimport java.time.ZoneOffset;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.function.Predicate;\n\npublic class DefaultElementEventProcessor implements ElementEventProcessor {\n\n    private static final Logger log = LoggerFactory.getLogger(DefaultElementEventProcessor.class);\n\n    private final ProcessDefinitionProvider processDefinitionProvider;\n    private final EventReportingService eventReportingService;\n\n    public DefaultElementEventProcessor(ProcessDefinitionProvider processDefinitionProvider,\n                                        EventReportingService eventReportingService) {\n        this.processDefinitionProvider = processDefinitionProvider;\n        this.eventReportingService = eventReportingService;\n    }\n\n    @Override\n    public void process(ElementEvent event, EventParamsBuilder builder) throws ExecutionException {\n        process(event, builder, null);\n    }\n\n    @Override\n    public void process(ElementEvent event, EventParamsBuilder builder, Predicate<AbstractElement> filter) throws ExecutionException {\n        ProcessDefinition pd = processDefinitionProvider.getById(event.getProcessDefinitionId());\n        if (pd == null) {\n            throw new RuntimeException(\"Process definition not found: \" + event.getProcessDefinitionId());\n        }\n\n        if (!(pd instanceof SourceAwareProcessDefinition)) {\n            return;\n        }\n\n        Map<String, SourceMap> sourceMaps = ((SourceAwareProcessDefinition) pd).getSourceMaps();\n\n        SourceMap source = sourceMaps.get(event.getElementId());\n        if (source == null) {\n            return;\n        }\n\n        AbstractElement element = ProcessDefinitionUtils.findElement(pd, event.getElementId());\n\n        if (filter != null && !filter.test(element)) {\n            return;\n        }\n\n        try {\n            Map<String, Object> e = new HashMap<>();\n            e.put(\"processDefinitionId\", event.getProcessDefinitionId());\n            e.put(\"elementId\", event.getElementId());\n            e.put(\"line\", source.getLine());\n            e.put(\"column\", source.getColumn());\n            e.put(\"description\", source.getDescription());\n            e.putAll(builder.build(element));\n\n            ProcessEventRequest req = new ProcessEventRequest();\n            req.setEventType(\"ELEMENT\"); // TODO should it be in the constants?\n            req.setData(e);\n            req.setEventDate(Instant.now().atOffset(ZoneOffset.UTC));\n\n            eventReportingService.report(req, UUID.fromString(event.getInstanceId()), event.getSessionToken());\n        } catch (Exception e) {\n            log.warn(\"process ['{}'] -> transfer error: {}\", event.getInstanceId(), e.getMessage());\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/DefaultEventReportingService.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClientConfiguration;\nimport com.walmartlabs.concord.client2.ApiClientFactory;\nimport com.walmartlabs.concord.client2.ApiException;\nimport com.walmartlabs.concord.client2.ProcessEventRequest;\nimport com.walmartlabs.concord.client2.ProcessEventsApi;\nimport io.takari.bpm.state.ProcessInstance;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.ArrayDeque;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Queue;\nimport java.util.TimerTask;\nimport java.util.UUID;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.ScheduledExecutorService;\nimport java.util.concurrent.TimeUnit;\n\npublic class DefaultEventReportingService implements EventReportingService {\n\n    private static final Logger log = LoggerFactory.getLogger(DefaultEventReportingService.class);\n\n    private final ApiClientFactory apiClientFactory;\n    private final int maxBatchSize;\n    private final Object batchLock = new Object();\n    private final ScheduledExecutorService flushScheduler;\n    private final HashMap<ReportingContext, Queue<ProcessEventRequest>> eventQueues;\n\n    public DefaultEventReportingService(ApiClientFactory apiClientFactory,\n                                        int batchSize,\n                                        int batchFlushInterval) {\n        this.apiClientFactory = apiClientFactory;\n        this.maxBatchSize = batchSize;\n        this.eventQueues = new HashMap<>(1);\n        this.flushScheduler = Executors.newSingleThreadScheduledExecutor();\n\n        flushScheduler.scheduleAtFixedRate(new FlushTimer(this), batchFlushInterval, batchFlushInterval, TimeUnit.SECONDS);\n    }\n\n    @Override\n    public ProcessInstance onFinalize(ProcessInstance state) {\n        flushScheduler.shutdown();\n        flush();\n        return state;\n    }\n\n    private class ReportingBatch {\n        private final ReportingContext reportingContext;\n        private final List<ProcessEventRequest> events;\n\n        public ReportingBatch(ReportingContext reportingContext, List<ProcessEventRequest> events) {\n            this.reportingContext = reportingContext;\n            this.events = events;\n        }\n\n        public void send() {\n            try {\n                getProcessEventsApi(reportingContext.sessionToken).batchEvent(reportingContext.instanceId, events);\n            } catch (ApiException e) {\n                log.warn(\"Error while sending batch of {} event{} to the server: {}\",\n                        events.size(), events.isEmpty() ? \"\" : \"s\", e.getMessage());\n            }\n        }\n    }\n\n    ProcessEventsApi getProcessEventsApi(String sessionToken) {\n        return new ProcessEventsApi(apiClientFactory.create(\n                ApiClientConfiguration.builder()\n                        .sessionToken(sessionToken)\n                        .build()));\n    }\n\n    /**\n     * Event source context. Basically a combo of instance ID and session token.\n     * Intended to be used as a key for a map of context -> queue of events\n     */\n    static class ReportingContext {\n        private final UUID instanceId;\n        private final String  sessionToken;\n\n        public ReportingContext(UUID instanceId, String sessionToken) {\n            this.instanceId = instanceId;\n            this.sessionToken = sessionToken;\n        }\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n\n            ReportingContext that = (ReportingContext) o;\n\n            if (!instanceId.equals(that.instanceId)) return false;\n            return sessionToken.equals(that.sessionToken);\n        }\n\n        @Override\n        public int hashCode() {\n            int result = instanceId.hashCode();\n            result = 31 * result + sessionToken.hashCode();\n            return result;\n        }\n    }\n\n    @Override\n    public void report(ProcessEventRequest req, UUID instanceId, String sessionToken) {\n        if (maxBatchSize > 1) {\n            batch(req, instanceId, sessionToken);\n        } else {\n            sendSingle(req, instanceId, sessionToken);\n        }\n    }\n\n    void batch(ProcessEventRequest req, UUID instanceId, String sessionToken) {\n        Queue<ProcessEventRequest> queue;\n\n        synchronized (batchLock) {\n            queue = eventQueues.computeIfAbsent(new ReportingContext(instanceId, sessionToken), ctx -> new ArrayDeque<>(maxBatchSize));\n            queue.add(req);\n        }\n\n        if (queue.size() >= maxBatchSize) {\n            flush();\n        }\n    }\n\n    void sendSingle(ProcessEventRequest req, UUID instanceId, String sessionToken) {\n        try {\n            ProcessEventsApi client = getProcessEventsApi(sessionToken);\n            client.event(instanceId, req);\n        } catch (ApiException e) {\n            log.warn(\"error while sending an event to the server: {}\", e.getMessage());\n        }\n    }\n\n    /**\n     * Flushes all queued events across all reporting contexts.\n     */\n    void flush() {\n        ReportingBatch eventBatch = takeBatch();\n\n        while (eventBatch != null && !eventBatch.events.isEmpty()) {\n            eventBatch.send();\n            eventBatch = takeBatch();\n        }\n    }\n\n    /**\n     * @return batch of up-to {@link #maxBatchSize} queued process events\n     */\n    private ReportingBatch takeBatch() {\n        ReportingContext ctx;\n        List<ProcessEventRequest> batch = new ArrayList<>(maxBatchSize);\n\n        synchronized (batchLock) {\n            if (eventQueues.isEmpty()) {\n                return null; // nothing to report\n            }\n\n            Optional<ReportingContext> optionalCtx = eventQueues.entrySet().stream()\n                    .findFirst()\n                    .map(Map.Entry::getKey);\n\n            if (optionalCtx.isEmpty()) {\n                // that's odd, should've been cleaned up already\n                eventQueues.clear();\n                return null;\n            }\n\n            ctx = optionalCtx.get();\n\n            Queue<ProcessEventRequest> eventQueue = eventQueues.get(ctx);\n\n            for (int i = 0; i < maxBatchSize; i++) {\n                if (eventQueue.isEmpty()) {\n                    eventQueues.remove(ctx);\n                    break;\n                }\n\n                batch.add(eventQueue.poll());\n            }\n        }\n\n        return new ReportingBatch(ctx, batch);\n    }\n\n    private static class FlushTimer extends TimerTask {\n        private final DefaultEventReportingService reportingService;\n\n        public FlushTimer(DefaultEventReportingService reportingService) {\n            this.reportingService = reportingService;\n        }\n\n        @Override\n        public void run() {\n            reportingService.flush();\n        }\n    }\n\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/DockerServiceImpl.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.DockerProcessBuilder;\nimport com.walmartlabs.concord.common.TruncBufferedReader;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.DockerContainerSpec;\nimport com.walmartlabs.concord.sdk.DockerService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.util.*;\nimport java.util.regex.Pattern;\n\n@Named\npublic class DockerServiceImpl implements DockerService {\n\n    private static final Logger log = LoggerFactory.getLogger(DockerServiceImpl.class);\n\n    private static final int SUCCESS_EXIT_CODE = 0;\n    private static final String WORKSPACE_TARGET_DIR = \"/workspace\";\n\n    private static final Pattern[] REGISTRY_ERROR_PATTERNS = {\n            Pattern.compile(\"Error response from daemon.*received unexpected HTTP status: 5.*\"),\n            Pattern.compile(\"Error response from daemon.*Get.*connection refused.*\"),\n            Pattern.compile(\"Error response from daemon.*Client.Timeout exceeded.*\")\n    };\n\n    private final List<String> extraVolumes;\n    private final boolean exposeDockerDaemon;\n\n    @Inject\n    public DockerServiceImpl(RunnerConfiguration runnerCfg) {\n        this.extraVolumes = runnerCfg.docker().extraVolumes();\n        this.exposeDockerDaemon = runnerCfg.docker().exposeDockerDaemon();\n    }\n\n    @Override\n    public Process start(Context ctx, DockerContainerSpec spec) throws IOException {\n        return build(ctx, spec).start();\n    }\n\n    @Override\n    public int start(Context ctx, DockerContainerSpec spec, LogCallback outCallback, LogCallback errCallback) throws IOException, InterruptedException {\n        int tryCount = 0;\n        int result;\n        int retryCount = Math.max(spec.pullRetryCount(), 0);\n        long retryInterval = spec.pullRetryInterval();\n\n        do {\n            try (DockerProcessBuilder.DockerProcess dp = build(ctx, spec)) {\n                Process p = dp.start();\n\n                LogCapture c = new LogCapture(outCallback);\n                streamToLog(p.getInputStream(), c);\n                if (errCallback != null) {\n                    streamToLog(p.getErrorStream(), errCallback);\n                }\n\n                result = p.waitFor();\n                if (result == SUCCESS_EXIT_CODE || retryCount == 0 || tryCount >= retryCount) {\n                    return result;\n                }\n\n                if (!needRetry(c.getLines())) {\n                    return result;\n                }\n\n                log.info(\"Error pulling the image. Retry after {} sec\", retryInterval / 1000);\n                sleep(retryInterval);\n                tryCount++;\n            }\n        } while (!Thread.currentThread().isInterrupted() && tryCount <= retryCount);\n\n        return result;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private DockerProcessBuilder.DockerProcess build(Context ctx, DockerContainerSpec spec) throws IOException {\n        DockerProcessBuilder b = DockerProcessBuilder.from(ctx, spec);\n\n        b.env(createEffectiveEnv(spec.env(), exposeDockerDaemon));\n\n        List<String> volumes = new ArrayList<>();\n        // add the default volume - mount the process' workDir as /workspace\n        volumes.add(\"${\" + Constants.Context.WORK_DIR_KEY + \"}:\" + WORKSPACE_TARGET_DIR);\n        // add extra volumes from the runner's arguments\n        volumes.addAll(extraVolumes);\n\n        b.volumes((Collection<String>) ctx.interpolate(volumes));\n\n        return b.build();\n    }\n\n    private static Map<String, String> createEffectiveEnv(Map<String, String> env, boolean exposeDockerDaemon) {\n        Map<String, String> m = new HashMap<>();\n\n        if (exposeDockerDaemon) {\n            String dockerHost = System.getenv(\"DOCKER_HOST\");\n            if (dockerHost == null) {\n                dockerHost = \"unix:///var/run/docker.sock\";\n            }\n            m.put(\"DOCKER_HOST\", dockerHost);\n        }\n\n        if (env != null) {\n            m.putAll(env);\n        }\n\n        return m;\n    }\n\n    private static void streamToLog(InputStream in, LogCallback callback) throws IOException {\n        BufferedReader reader = new TruncBufferedReader(new InputStreamReader(in));\n        String line;\n        while ((line = reader.readLine()) != null) {\n            callback.onLog(line);\n        }\n    }\n\n    private static boolean needRetry(List<String> lines) {\n        for (String l : lines) {\n            for (Pattern p : REGISTRY_ERROR_PATTERNS) {\n                if (p.matcher(l).matches()) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    private static void sleep(long t) {\n        try {\n            Thread.sleep(t);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    private static class LogCapture implements LogCallback {\n\n        private static final int MAX_CAPTURE_LINES = 5;\n\n        private final LogCallback delegate;\n        private final List<String> lines;\n\n        private LogCapture(LogCallback delegate) {\n            this.delegate = delegate;\n            this.lines = new ArrayList<>();\n        }\n\n        @Override\n        public void onLog(String line) {\n            if (delegate != null) {\n                delegate.onLog(line);\n            }\n\n            if (lines.size() <= MAX_CAPTURE_LINES) {\n                lines.add(line);\n            }\n        }\n\n        List<String> getLines() {\n            return lines;\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/ElementEventProcessor.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.takari.bpm.api.ExecutionException;\nimport io.takari.bpm.model.AbstractElement;\n\nimport java.util.Map;\nimport java.util.function.Predicate;\n\npublic interface ElementEventProcessor {\n\n    interface EventParamsBuilder {\n\n        Map<String, Object> build(AbstractElement element);\n    }\n\n    class ElementEvent {\n\n        private final String instanceId;\n        private final String processDefinitionId;\n        private final String elementId;\n        private final String sessionToken;\n\n        public ElementEvent(String instanceId, String processDefinitionId, String elementId, String sessionToken) {\n            this.instanceId = instanceId;\n            this.processDefinitionId = processDefinitionId;\n            this.elementId = elementId;\n            this.sessionToken = sessionToken;\n        }\n\n        public String getInstanceId() {\n            return instanceId;\n        }\n\n        public String getProcessDefinitionId() {\n            return processDefinitionId;\n        }\n\n        public String getElementId() {\n            return elementId;\n        }\n\n        public String getSessionToken() {\n            return sessionToken;\n        }\n    }\n\n    void process(ElementEvent event, EventParamsBuilder builder) throws ExecutionException;\n\n    void process(ElementEvent event, EventParamsBuilder builder, Predicate<AbstractElement> filter) throws ExecutionException;\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/EngineFactory.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClientFactory;\nimport com.walmartlabs.concord.common.ReflectionUtils;\nimport com.walmartlabs.concord.project.InternalConstants;\nimport com.walmartlabs.concord.project.model.ProjectDefinition;\nimport com.walmartlabs.concord.project.model.ProjectDefinitionUtils;\nimport com.walmartlabs.concord.runner.PolicyEngineHolder;\nimport com.walmartlabs.concord.runner.VariablesSnapshotListener;\nimport com.walmartlabs.concord.runner.engine.el.InjectVariableELResolver;\nimport com.walmartlabs.concord.runner.engine.el.TaskResolver;\nimport com.walmartlabs.concord.sdk.Context;\nimport com.walmartlabs.concord.sdk.Task;\nimport io.takari.bpm.Configuration;\nimport io.takari.bpm.EngineBuilder;\nimport io.takari.bpm.ProcessDefinitionProvider;\nimport io.takari.bpm.api.Engine;\nimport io.takari.bpm.api.ExecutionContext;\nimport io.takari.bpm.api.ExecutionException;\nimport io.takari.bpm.api.JavaDelegate;\nimport io.takari.bpm.context.DefaultExecutionContextFactory;\nimport io.takari.bpm.el.DefaultExpressionManager;\nimport io.takari.bpm.el.ExecutionContextVariableResolver;\nimport io.takari.bpm.el.ExpressionManager;\nimport io.takari.bpm.event.EventStorage;\nimport io.takari.bpm.form.DefaultFormService.NoopResumeHandler;\nimport io.takari.bpm.form.FormDefinitionProvider;\nimport io.takari.bpm.form.FormService;\nimport io.takari.bpm.form.FormStorage;\nimport io.takari.bpm.form.FormTaskHandler;\nimport io.takari.bpm.lock.NoopLockManager;\nimport io.takari.bpm.persistence.PersistenceManager;\nimport io.takari.bpm.task.JavaDelegateHandler;\nimport io.takari.bpm.task.ServiceTaskRegistry;\nimport io.takari.bpm.task.UserTaskHandler;\n\nimport javax.el.CompositeELResolver;\nimport javax.el.ELResolver;\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Set;\n\n@Named\npublic class EngineFactory {\n\n    private final ApiClientFactory apiClientFactory;\n    private final ServiceTaskRegistry taskRegistry;\n\n    @Inject\n    public EngineFactory(ApiClientFactory apiClientFactory,\n                         ServiceTaskRegistry taskRegistry) {\n\n        this.apiClientFactory = apiClientFactory;\n        this.taskRegistry = taskRegistry;\n    }\n\n    @SuppressWarnings(\"deprecation\")\n    public Engine create(ProjectDefinition project,\n                         Path baseDir,\n                         Collection<String> activeProfiles,\n                         Set<String> metaVariables,\n                         EventConfiguration eventCfg) {\n\n        Path attachmentsDir = baseDir.resolve(InternalConstants.Files.JOB_ATTACHMENTS_DIR_NAME);\n        Path stateDir = attachmentsDir.resolve(InternalConstants.Files.JOB_STATE_DIR_NAME);\n\n        Path eventsDir = stateDir.resolve(\"events\");\n        Path instancesDir = stateDir.resolve(\"instances\");\n        Path formsDir = stateDir.resolve(InternalConstants.Files.JOB_FORMS_DIR_NAME);\n\n        try {\n            Files.createDirectories(eventsDir);\n            Files.createDirectories(instancesDir);\n            Files.createDirectories(formsDir);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n\n        ExpressionManager expressionManager = new DefaultExpressionManager(\n                new String[]{InternalConstants.Context.CONTEXT_KEY, InternalConstants.Context.EXECUTION_CONTEXT_KEY},\n                new TaskResolver(taskRegistry),\n                new InjectVariableELResolver()) {\n\n            @Override\n            protected ELResolver createResolver(ExecutionContext ctx) {\n                CompositeELResolver cr = new CompositeELResolver();\n                cr.add(new ExecutionContextVariableResolver(ctx));\n                for (ELResolver r : resolvers) {\n                    cr.add(r);\n                }\n                return cr;\n            }\n        };\n\n        FormStorage formStorage = new FileFormStorage(formsDir);\n        // here we create a separate ContextFactory to avoid circular references\n        // between ConcordFormService and ConcordExecutionContextFactory\n        // TODO find a better way\n        FormService formService = new ConcordFormService(new DefaultExecutionContextFactory(expressionManager), new NoopResumeHandler(), formStorage);\n\n        ProtectedVarContext protectedVarContext = new ProtectedVarContext(PolicyEngineHolder.INSTANCE.getEngine());\n        ConcordExecutionContextFactory contextFactory = new ConcordExecutionContextFactory(expressionManager, protectedVarContext, formService);\n\n        EventStorage eventStorage = new FileEventStorage(eventsDir);\n        PersistenceManager persistenceManager = new FilePersistenceManager(instancesDir);\n\n        ProjectDefinitionAdapter adapter = new ProjectDefinitionAdapter(project, activeProfiles, baseDir);\n\n        UserTaskHandler uth = new FormTaskHandler(contextFactory, adapter.forms(), formService);\n\n        Configuration cfg = new Configuration();\n        cfg.setInterpolateInputVariables(true);\n        cfg.setWrapAllExceptionsAsBpmnErrors(true);\n        cfg.setCopyAllCallActivityOutVariables(true);\n\n        EventReportingService evs = new DefaultEventReportingService(apiClientFactory, eventCfg.getBatchSize(), eventCfg.getBatchFlushInterval());\n        ElementEventProcessor eventProcessor;\n        if (eventCfg.isRecordEvents()) {\n            eventProcessor = new DefaultElementEventProcessor(adapter.processes(), evs);\n        } else {\n            eventProcessor = new NopElementEventProcessor();\n        }\n        ProcessOutVariables outVariables = new ProcessOutVariables(contextFactory);\n\n        List<TaskInterceptor> taskInterceptors = new ArrayList<>();\n        taskInterceptors.add(protectedVarContext);\n        taskInterceptors.add(new TaskEventInterceptor(eventCfg, eventProcessor));\n        taskInterceptors.add(new PolicyPreprocessor(baseDir));\n\n        Engine engine = new EngineBuilder()\n                .withContextFactory(contextFactory)\n                .withLockManager(new NoopLockManager())\n                .withExpressionManager(expressionManager)\n                .withDefinitionProvider(adapter.processes())\n                .withTaskRegistry(taskRegistry)\n                .withJavaDelegateHandler(new JavaDelegateHandlerImpl(taskInterceptors))\n                .withEventStorage(eventStorage)\n                .withPersistenceManager(persistenceManager)\n                .withUserTaskHandler(uth)\n                .withConfiguration(cfg)\n                .withListener(new ProcessOutVariablesListener(attachmentsDir, outVariables))\n                .withListener(new VariablesSnapshotListener(stateDir))\n                .withListener(evs)\n                .withResourceResolver(new ResourceResolverImpl(baseDir))\n                .build();\n\n        ProcessMetadataProcessor metadataProcessor = new ProcessMetadataProcessor(apiClientFactory, metaVariables, eventCfg.isUpdateMetaOnAllEvents());\n        engine.addInterceptor(new ProcessElementInterceptor(eventProcessor, metadataProcessor));\n\n        return engine;\n    }\n\n    private static class ProjectDefinitionAdapter {\n\n        private final ProjectDefinition project;\n        private final Collection<String> activeProfiles;\n\n        private ProjectDefinitionAdapter(ProjectDefinition project, Collection<String> activeProfiles, Path baseDir) {\n            this.project = project;\n            this.activeProfiles = activeProfiles;\n        }\n\n        ProcessDefinitionProvider processes() {\n            return id -> ProjectDefinitionUtils.getFlow(project, activeProfiles, id);\n        }\n\n        FormDefinitionProvider forms() {\n            return id -> ProjectDefinitionUtils.getForm(project, activeProfiles, id);\n        }\n    }\n\n    private static final class JavaDelegateHandlerImpl implements JavaDelegateHandler {\n\n        private final List<TaskInterceptor> interceptors;\n\n        private JavaDelegateHandlerImpl(List<TaskInterceptor> interceptors) {\n            this.interceptors = interceptors;\n        }\n\n        @Override\n        public void execute(Object task, ExecutionContext ctx) throws Exception {\n            Named n = ReflectionUtils.findAnnotation(task.getClass(), Named.class);\n            String taskName = null;\n            if (n != null) {\n                taskName = n.value();\n            }\n\n            // check JavaDelegate first because tasks can implement both JavaDelegate and Task\n            if (task instanceof JavaDelegate) {\n                JavaDelegate d = (JavaDelegate) task;\n                preTask(taskName, task, (Context) ctx);\n                d.execute(ctx);\n                postTask(taskName, task, (Context) ctx);\n            } else if (task instanceof Task) {\n                Task t = (Task) task;\n                preTask(taskName, task, (Context) ctx);\n                t.execute((Context) ctx);\n                postTask(taskName, task, (Context) ctx);\n            } else {\n                throw new ExecutionException(\"Unsupported task type: \" + task + \": tasks must implement either \" +\n                        Task.class.getName() + \" or \" + JavaDelegate.class.getName() + \" interfaces\");\n            }\n        }\n\n        private void preTask(String taskName, Object instance, Context ctx) throws ExecutionException {\n            for (TaskInterceptor i : interceptors) {\n                i.preTask(taskName, instance, ctx);\n            }\n        }\n\n        private void postTask(String taskName, Object instance, Context ctx) throws ExecutionException {\n            for (TaskInterceptor i : interceptors) {\n                i.postTask(taskName, instance, ctx);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/EventConfiguration.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\n\npublic class EventConfiguration {\n\n    private final Collection<String> DEFAULT_IN_VARS_BLACKLIST = Arrays.asList(\n            \"apiKey\",\n            \"apiToken\",\n            \"password\",\n            \"privateKey\",\n            \"vaultPassword\");\n\n    private boolean recordEvents = true;\n    private int batchFlushInterval = 15;\n    private int batchSize = 1;\n    private boolean truncateInVars = true;\n    private boolean recordTaskInVars = false;\n    private boolean recordTaskOutVars = false;\n    private boolean truncateOutVars = true;\n    private boolean updateMetaOnAllEvents = true;\n    private Collection<String> inVarsBlacklist = DEFAULT_IN_VARS_BLACKLIST;\n    private Collection<String> outVarsBlacklist = Collections.emptyList();\n    private int truncateMaxStringLength = 1024;\n    private int truncateMaxArrayLength = 32;\n    private int truncateMaxDepth = 32;\n\n    public boolean isRecordEvents() {\n        return recordEvents;\n    }\n\n    public void setRecordEvents(boolean recordEvents) {\n        this.recordEvents = recordEvents;\n    }\n\n    public int getBatchFlushInterval() {\n        return batchFlushInterval;\n    }\n\n    public void setBatchFlushInterval(int batchFlushInterval) {\n        this.batchFlushInterval = batchFlushInterval;\n    }\n\n    public int getBatchSize() {\n        return batchSize;\n    }\n\n    public void setBatchSize(int batchSize) {\n        this.batchSize = batchSize;\n    }\n\n    public boolean isRecordTaskInVars() {\n        return recordTaskInVars;\n    }\n\n    public void setRecordTaskInVars(boolean recordTaskInVars) {\n        this.recordTaskInVars = recordTaskInVars;\n    }\n\n    public boolean isRecordTaskOutVars() {\n        return recordTaskOutVars;\n    }\n\n    public void setRecordTaskOutVars(boolean recordTaskOutVars) {\n        this.recordTaskOutVars = recordTaskOutVars;\n    }\n\n    public Collection<String> getInVarsBlacklist() {\n        return inVarsBlacklist;\n    }\n\n    public void setInVarsBlacklist(Collection<String> inVarsBlacklist) {\n        this.inVarsBlacklist = inVarsBlacklist;\n    }\n\n    public Collection<String> getOutVarsBlacklist() {\n        return outVarsBlacklist;\n    }\n\n    public void setOutVarsBlacklist(Collection<String> outVarsBlacklist) {\n        this.outVarsBlacklist = outVarsBlacklist;\n    }\n\n    public boolean isTruncateInVars() {\n        return truncateInVars;\n    }\n\n    public boolean isTruncateOutVars() {\n        return truncateOutVars;\n    }\n\n    public boolean isUpdateMetaOnAllEvents() {\n        return updateMetaOnAllEvents;\n    }\n\n    public void setUpdateMetaOnAllEvents(boolean updateMetaOnAllEvents) {\n        this.updateMetaOnAllEvents = updateMetaOnAllEvents;\n    }\n\n    public int getTruncateMaxStringLength() {\n        return truncateMaxStringLength;\n    }\n\n    public int getTruncateMaxArrayLength() {\n        return truncateMaxArrayLength;\n    }\n\n    public int getTruncateMaxDepth() {\n        return truncateMaxDepth;\n    }\n\n    public void setTruncateInVars(boolean truncateInVars) {\n        this.truncateInVars = truncateInVars;\n    }\n\n    public void setTruncateOutVars(boolean truncateOutVars) {\n        this.truncateOutVars = truncateOutVars;\n    }\n\n    public void setTruncateMaxStringLength(int truncateMaxStringLength) {\n        this.truncateMaxStringLength = truncateMaxStringLength;\n    }\n\n    public void setTruncateMaxArrayLength(int truncateMaxArrayLength) {\n        this.truncateMaxArrayLength = truncateMaxArrayLength;\n    }\n\n    public void setTruncateMaxDepth(int truncateMaxDepth) {\n        this.truncateMaxDepth = truncateMaxDepth;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/EventReportingService.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessEventRequest;\nimport io.takari.bpm.EngineListener;\n\nimport java.util.UUID;\n\npublic interface EventReportingService extends EngineListener {\n\n    /**\n     * Report a process event to the server.\n     */\n    void report(ProcessEventRequest req, UUID instanceId, String sessionToken);\n\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/FileEventStorage.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.runner.SerializationUtils;\nimport io.takari.bpm.event.Event;\nimport io.takari.bpm.event.EventStorage;\nimport io.takari.bpm.event.ExpiredEvent;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.io.ObjectInputStream;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport static java.nio.file.StandardCopyOption.REPLACE_EXISTING;\n\npublic class FileEventStorage implements EventStorage {\n\n    private static final Logger log = LoggerFactory.getLogger(FileEventStorage.class);\n\n    private final Path dir;\n\n    public FileEventStorage(Path dir) {\n        this.dir = dir;\n    }\n\n    @Override\n    public void add(Event event) {\n        Path p = dir.resolve(event.getId().toString());\n\n        try {\n            Path tmp = PathUtils.createTempFile(event.getId().toString(), \"event\");\n            try (OutputStream out = Files.newOutputStream(tmp)) {\n                SerializationUtils.serialize(out, event);\n            }\n            Files.move(tmp, p, REPLACE_EXISTING);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n\n        log.debug(\"add ['{}', '{}'] -> done, {}\", event.getProcessBusinessKey(), event.getName(), p);\n    }\n\n    @Override\n    public Event get(UUID id) {\n        Path p = dir.resolve(id.toString());\n        if (!Files.exists(p)) {\n            return null;\n        }\n\n        return get(p);\n    }\n\n    private Event get(Path p) {\n        try (ObjectInputStream in = new ObjectInputStream(Files.newInputStream(p))) {\n            return (Event) in.readObject();\n        } catch (ClassNotFoundException | IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public Event remove(UUID id) {\n        Event ev = get(id);\n        if (ev == null) {\n            return null;\n        }\n\n        Path p = dir.resolve(id.toString());\n        try {\n            Files.delete(p);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n\n        log.debug(\"remove ['{}', '{}'] -> done, {}\", ev.getProcessBusinessKey(), ev.getName(), p);\n        return ev;\n    }\n\n    @Override\n    public Collection<Event> find(String processBusinessKey) {\n        return find(processBusinessKey, null);\n    }\n\n    @Override\n    public Collection<Event> find(String processBusinessKey, String eventName) {\n        try (Stream<Path> s = Files.list(dir)) {\n            return s.map(this::get)\n                    .filter(ev -> processBusinessKey.equals(ev.getProcessBusinessKey()) &&\n                            (eventName == null || eventName.equals(ev.getName())))\n                    .collect(Collectors.toList());\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public List<ExpiredEvent> findNextExpiredEvent(int maxEvents) {\n        throw new RuntimeException(\"Not implemented\");\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/FileFormStorage.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.runner.SerializationUtils;\nimport io.takari.bpm.api.ExecutionException;\nimport io.takari.bpm.form.Form;\nimport io.takari.bpm.form.FormStorage;\n\nimport java.io.IOException;\nimport java.io.ObjectInputStream;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.UUID;\n\nimport static java.nio.file.StandardCopyOption.REPLACE_EXISTING;\n\npublic class FileFormStorage implements FormStorage {\n\n    private final Path dir;\n\n    public FileFormStorage(Path dir) {\n        this.dir = dir;\n    }\n\n    @Override\n    public void save(Form form) throws ExecutionException {\n        assertValidFormName(form);\n        UUID id = form.getFormInstanceId();\n        try {\n            Path p = PathUtils.assertInPath(dir, form.getFormDefinition().getName());\n            Path tmp = PathUtils.createTempFile(id.toString(), \"form\");\n            try (OutputStream out = Files.newOutputStream(tmp)) {\n                SerializationUtils.serialize(out, form);\n            }\n            Files.move(tmp, p, REPLACE_EXISTING);\n        } catch (IOException e) {\n            throw new ExecutionException(\"Error while saving a form\", e);\n        }\n    }\n\n    @Override\n    public void complete(UUID formInstanceId) {\n        throw new IllegalStateException(\"Shouldn't be called from the runner's side\");\n    }\n\n    @Override\n    public Form get(UUID formInstanceId) throws ExecutionException {\n        Path p = dir.resolve(formInstanceId.toString());\n        if (!Files.exists(p)) {\n            return null;\n        }\n\n        try (ObjectInputStream out = new ObjectInputStream(Files.newInputStream(p))) {\n            return (Form) out.readObject();\n        } catch (ClassNotFoundException | IOException e) {\n            throw new ExecutionException(\"Error while reading a form\", e);\n        }\n    }\n\n    private static void assertValidFormName(Form form) {\n        String name = form.getFormDefinition().getName();\n        if (!name.matches(\"^[A-Za-z0-9_ $]+$\")) {\n            throw new IllegalArgumentException(String.format(\"Invalid form name: '%s'\", name));\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/FilePersistenceManager.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ObjectInputStreamWithClassLoader;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.runner.SerializationUtils;\nimport io.takari.bpm.api.ExecutionException;\nimport io.takari.bpm.persistence.PersistenceManager;\nimport io.takari.bpm.state.ProcessInstance;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.io.ObjectInputStream;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.UUID;\n\nimport static java.nio.file.StandardCopyOption.REPLACE_EXISTING;\n\npublic class FilePersistenceManager implements PersistenceManager {\n\n    private static final Logger log = LoggerFactory.getLogger(FilePersistenceManager.class);\n\n    private final Path dir;\n\n    public FilePersistenceManager(Path dir) {\n        this.dir = dir;\n    }\n\n    @Override\n    public void save(ProcessInstance state) throws ExecutionException {\n        Path p = dir.resolve(state.getId().toString());\n\n        try {\n            Path tmp = PathUtils.createTempFile(state.getId().toString(), \"state\");\n            try (OutputStream out = Files.newOutputStream(tmp)) {\n                SerializationUtils.serialize(out, state);\n            }\n            Files.move(tmp, p, REPLACE_EXISTING);\n        } catch (IOException e) {\n            throw new ExecutionException(\"Error while saving state\", e);\n        }\n\n        log.debug(\"save ['{}', '{}'] -> done, {}\", state.getBusinessKey(), state.getId(), p);\n    }\n\n    @Override\n    public ProcessInstance get(UUID id) {\n        Path p = dir.resolve(id.toString());\n        if (!Files.exists(p)) {\n            return null;\n        }\n\n        ClassLoader cl = Thread.currentThread().getContextClassLoader();\n\n        try (ObjectInputStream in = new ObjectInputStreamWithClassLoader(Files.newInputStream(p), cl)) {\n            return (ProcessInstance) in.readObject();\n        } catch (ClassNotFoundException | IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public void remove(UUID id) {\n        Path p = dir.resolve(id.toString());\n        if (!Files.exists(p)) {\n            return;\n        }\n\n        try {\n            Files.delete(p);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n\n        log.debug(\"remove ['{}'] -> done, {}\", id, p);\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/FormUtilsTask.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.*;\n\nimport javax.inject.Named;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.sdk.Constants.Context.TX_ID_KEY;\n\n@Named(\"forms\")\npublic class FormUtilsTask implements Task {\n\n    private static final String FORM_KEY = \"form\";\n    private static final String WIZARD_KEY = \"wizard\";\n\n    @InjectVariable(\"uiLinks\")\n    private Map<String, Object> defaults;\n\n    @InjectVariable(Constants.Context.CONTEXT_KEY)\n    Context context;\n\n    public String getFormLink(String formName) {\n        String instanceId = ContextUtils.assertString(context, TX_ID_KEY);\n        return String.format(getTemplate(FORM_KEY), instanceId, formName);\n    }\n\n    public String getWizardLink() {\n        String instanceId = ContextUtils.assertString(context, TX_ID_KEY);\n        return String.format(getTemplate(WIZARD_KEY), instanceId);\n    }\n\n    private String getTemplate(String key) {\n        String t = (String) (defaults != null ? defaults.get(key) : null);\n        if (t == null) {\n            throw new IllegalArgumentException(\"'uiLinks.\" + key + \"' is undefined\");\n        }\n        return t;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/LogTagMetadataProvider.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Context;\n\nimport java.util.Map;\n\n/**\n * Provides additional metadata for the process log \"tags\".\n */\npublic interface LogTagMetadataProvider {\n\n    /**\n     * Collects the task's tag metadata using the provided context.\n     * Don't expose any sensitive variables in this method as the returned data\n     * will be saved into the task's \"log tag\" in the process' log as is, without\n     * any filtering.\n     */\n    Map<String, Object> createLogTagMetadata(Context ctx);\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/NopElementEventProcessor.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.takari.bpm.api.ExecutionException;\nimport io.takari.bpm.model.AbstractElement;\n\nimport java.util.function.Predicate;\n\npublic class NopElementEventProcessor implements ElementEventProcessor {\n\n    @Override\n    public void process(ElementEvent event, EventParamsBuilder builder) throws ExecutionException {\n        // do nothing\n    }\n\n    @Override\n    public void process(ElementEvent event, EventParamsBuilder builder, Predicate<AbstractElement> filter) throws ExecutionException {\n        // do nothing\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/PolicyPreprocessor.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.project.InternalConstants;\nimport com.walmartlabs.concord.sdk.Context;\nimport io.takari.bpm.api.ExecutionException;\n\nimport javax.el.ExpressionFactory;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.*;\n\npublic class PolicyPreprocessor implements TaskInterceptor {\n\n    private final ExpressionFactory expressionFactory = ExpressionFactory.newInstance();\n    private final ObjectMapper objectMapper = new ObjectMapper();\n\n    private final Path workDir;\n    private final Map<String, Object> policy;\n\n    public PolicyPreprocessor(Path workDir) {\n        this.workDir = workDir;\n        this.policy = readPolicy(workDir);\n    }\n\n    @Override\n    public void preTask(String taskName, Object instance, Context ctx) throws ExecutionException {\n        if (!needProcess(taskName)) {\n            return;\n        }\n\n        Map<String, Object> newPolicy = eval(ctx, policy);\n        writePolicy(newPolicy);\n    }\n\n    @Override\n    public void postTask(String taskName, Object instance, Context ctx) throws ExecutionException {\n        if (!needProcess(taskName)) {\n            return;\n        }\n\n        writePolicy(policy);\n    }\n\n    private boolean needProcess(String taskName) {\n        return taskName.startsWith(\"ansible\") && !policy.isEmpty();\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private Map<String, Object> readPolicy(Path workDir) {\n        Path policyFile = workDir.resolve(InternalConstants.Files.CONCORD_SYSTEM_DIR_NAME).resolve(InternalConstants.Files.POLICY_FILE_NAME);\n        if (!Files.exists(policyFile)) {\n            return Collections.emptyMap();\n        }\n        try {\n            return objectMapper.readValue(policyFile.toFile(), Map.class);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private void writePolicy(Map<String, Object> policy) throws ExecutionException {\n        try {\n            Path policyFile = workDir.resolve(InternalConstants.Files.CONCORD_SYSTEM_DIR_NAME).resolve(InternalConstants.Files.POLICY_FILE_NAME);\n            objectMapper.writeValue(policyFile.toFile(), policy);\n        } catch (IOException e) {\n            throw new ExecutionException(\"write policy error\", e);\n        }\n    }\n\n    private Map<String, Object> eval(Context ctx, Map<String, Object> params) {\n        Map<String, Object> result = new HashMap<>();\n        for (Map.Entry<String, Object> e : params.entrySet()) {\n            String k = e.getKey();\n            Object v = e.getValue();\n            result.put(k, process(ctx, v));\n        }\n        return result;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private Object process(Context ctx, Object v) {\n        if (v instanceof String) {\n            return eval(ctx, (String) v);\n        } else if (v instanceof Map) {\n            Map<String, Object> result = new HashMap<>();\n            for (Map.Entry<String, Object> e : ((Map<String, Object>)v).entrySet()) {\n                result.put(e.getKey(), process(ctx, e.getValue()));\n            }\n            return result;\n        } else if (v instanceof List) {\n            List<Object> result = new ArrayList<>();\n            for (Object o : (List)v) {\n                result.add(process(ctx, o));\n            }\n            return result;\n        } else {\n            return v;\n        }\n    }\n\n    private Object eval(Context ctx, String expression) {\n        if (!isExpression(expression)) {\n            return expression;\n        }\n\n        return ctx.eval(expression, Object.class);\n    }\n\n    private boolean isExpression(String str) {\n        if (str == null) {\n            return false;\n        }\n\n        String s = str.trim();\n        return s.startsWith(\"${\") && s.endsWith(\"}\");\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/ProcessElementInterceptor.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runner.ContextUtils;\nimport io.takari.bpm.api.ExecutionException;\nimport io.takari.bpm.api.interceptors.ExecutionInterceptorAdapter;\nimport io.takari.bpm.api.interceptors.InterceptorElementEvent;\nimport io.takari.bpm.model.ExpressionType;\nimport io.takari.bpm.model.ServiceTask;\n\nimport java.util.Collections;\nimport java.util.UUID;\n\npublic class ProcessElementInterceptor extends ExecutionInterceptorAdapter {\n\n    private final ElementEventProcessor eventProcessor;\n    private final ProcessMetadataProcessor processMetadataProcessor;\n\n    public ProcessElementInterceptor(ElementEventProcessor eventProcessor, ProcessMetadataProcessor processMetadataProcessor) {\n        this.eventProcessor = eventProcessor;\n        this.processMetadataProcessor = processMetadataProcessor;\n    }\n\n    @Override\n    public void onElement(InterceptorElementEvent ev) throws ExecutionException {\n        ElementEventProcessor.ElementEvent event = new ElementEventProcessor.ElementEvent(ev.getProcessBusinessKey(),\n                ev.getProcessDefinitionId(), ev.getElementId(), ContextUtils.getSessionToken(ev.getVariables()));\n\n        eventProcessor.process(event,\n                element -> Collections.emptyMap(),\n                element -> !(element instanceof ServiceTask) || ((ServiceTask) element).getType() != ExpressionType.DELEGATE);\n\n        processMetadataProcessor.process(UUID.fromString(ev.getProcessBusinessKey()), ev.getVariables());\n    }\n\n    @Override\n    public void onFinish(String processBusinessKey) {\n        processMetadataProcessor.flush();\n    }\n\n    @Override\n    public void onFailure(String processBusinessKey, String errorRef) {\n        processMetadataProcessor.flush();\n    }\n\n    @Override\n    public void onSuspend() {\n        processMetadataProcessor.flush();\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/ProcessErrorProcessor.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.SerializationFeature;\nimport com.walmartlabs.concord.sdk.Constants;\nimport io.takari.bpm.api.BpmnError;\nimport io.takari.bpm.api.ExecutionException;\n\nimport javax.el.ELException;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class ProcessErrorProcessor {\n\n    private static final String DEFAULT_ERROR_REF = \"__default_error_ref\";\n\n    private static final ObjectMapper mapper = createMapper();\n\n    public static Map<String, Object> process(Throwable t) {\n        Map<String, Object> m = new HashMap<>();\n        m.put(Constants.Context.LAST_ERROR_KEY, mapper.convertValue(getError(t), Map.class));\n        return m;\n    }\n\n    private static Throwable getError(Throwable t) {\n        t = unroll(t);\n\n        if (t instanceof BpmnError) {\n            BpmnError error = (BpmnError) t;\n            if (!DEFAULT_ERROR_REF.equals(error.getErrorRef())) {\n                return error;\n            }\n\n            Throwable cause = error.getCause();\n            if (cause == null) {\n                return error;\n            }\n        }\n\n        if (t instanceof ELException && t.getCause() != null) {\n            return t.getCause();\n        }\n\n        return t;\n    }\n\n    private static Throwable unroll(Throwable t) {\n        if (t instanceof ExecutionException) {\n            if (t.getCause() != null) {\n                return t.getCause();\n            }\n        }\n        return t;\n    }\n\n    private static ObjectMapper createMapper() {\n        ObjectMapper om = new ObjectMapper();\n        om.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);\n        om.addMixIn(Throwable.class, ExceptionMixIn.class);\n        om.addMixIn(BpmnError.class, BpmnErrorMixIn.class);\n        return om;\n    }\n\n    @JsonInclude(JsonInclude.Include.NON_NULL)\n    abstract class ExceptionMixIn {\n        @JsonIgnore\n        abstract StackTraceElement[] getStackTrace();\n\n        @JsonIgnore\n        abstract String getLocalizedMessage();\n\n        @JsonIgnore\n        abstract Throwable[] getSuppressed();\n\n        @JsonIgnore\n        abstract Throwable getCause();\n    }\n\n    abstract class BpmnErrorMixIn {\n        @JsonIgnore\n        abstract String getDefinitionId();\n\n        @JsonIgnore\n        abstract String getElementId();\n\n        @JsonProperty(\"message\")\n        abstract String getErrorRef();\n\n        @JsonIgnore\n        abstract String getMessage();\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/ProcessMetadataProcessor.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiException;\nimport com.walmartlabs.concord.client2.ApiClientConfiguration;\nimport com.walmartlabs.concord.client2.ApiClientFactory;\nimport com.walmartlabs.concord.client2.ClientUtils;\nimport com.walmartlabs.concord.client2.ProcessApi;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.runner.ContextUtils;\nimport io.takari.bpm.api.Variables;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.*;\n\npublic class ProcessMetadataProcessor {\n\n    private static final Logger log = LoggerFactory.getLogger(ProcessMetadataProcessor.class);\n\n    private static final int RETRY_COUNT = 3;\n    private static final long RETRY_INTERVAL = 5000;\n\n    @SuppressWarnings(\"rawtypes\")\n    private static final Set<Class> VARIABLE_TYPES = new HashSet<>(Arrays.asList(\n            String.class, Boolean.class, Character.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class));\n\n    private final ApiClientFactory apiClientFactory;\n    private final Set<String> processMetaVariables;\n    private final boolean updateMetaOnAllEvents;\n\n    private Map<String, Object> currentProcessMeta = new HashMap<>();\n    private String sessionToken;\n    private UUID instanceId;\n\n    public ProcessMetadataProcessor(ApiClientFactory apiClientFactory, Set<String> processMetaVariables) {\n        this(apiClientFactory, processMetaVariables, true);\n    }\n\n    public ProcessMetadataProcessor(ApiClientFactory apiClientFactory, Set<String> processMetaVariables, boolean updateMetaOnAllEvents) {\n        this.apiClientFactory = apiClientFactory;\n        this.processMetaVariables = processMetaVariables;\n        this.updateMetaOnAllEvents = updateMetaOnAllEvents;\n    }\n\n    public void process(UUID instanceId, Variables variables) {\n        this.sessionToken = ContextUtils.getSessionToken(variables);\n        this.instanceId = instanceId;\n\n        Map<String, Object> meta = filter(variables.asMap());\n        if (meta.isEmpty() || !changed(currentProcessMeta, meta)) {\n            return;\n        }\n        this.currentProcessMeta = meta;\n\n        if (updateMetaOnAllEvents) {\n            sendMeta(instanceId, sessionToken, meta);\n        }\n    }\n\n    public void flush() {\n        if (!updateMetaOnAllEvents) {\n            sendMeta(instanceId, sessionToken, currentProcessMeta);\n        }\n    }\n\n    private void sendMeta(UUID instanceId, String sessionToken, Map<String, Object> meta) {\n        ProcessApi client = new ProcessApi(apiClientFactory.create(\n                ApiClientConfiguration.builder()\n                        .sessionToken(sessionToken)\n                        .build()));\n        try {\n            ClientUtils.withRetry(RETRY_COUNT, RETRY_INTERVAL, () -> {\n                client.updateMetadata(instanceId, meta);\n                return null;\n            });\n        } catch (ApiException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private boolean changed(Map<String, Object> oldMeta, Map<String, Object> meta) {\n        return !oldMeta.equals(meta);\n    }\n\n    private Map<String, Object> filter(Map<String, Object> vars) {\n        if (vars.isEmpty()) {\n            return vars;\n        }\n\n        Map<String, Object> result = new HashMap<>();\n        for (String v : processMetaVariables) {\n            Object value = ConfigurationUtils.get(vars, v.split(\"\\\\.\"));\n            if (value == null) {\n                continue;\n            }\n\n            if (value.getClass().isPrimitive() || VARIABLE_TYPES.contains(value.getClass())) {\n                result.put(v, value);\n            } else {\n                log.debug(\"out variable {} -> ignored (unsupported type: {})\", v, value.getClass());\n            }\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/ProcessOutVariables.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.InternalConstants;\nimport io.takari.bpm.api.ExecutionContext;\nimport io.takari.bpm.api.ExecutionContextFactory;\nimport io.takari.bpm.api.Variables;\nimport io.takari.bpm.context.ExecutionContextImpl;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.el.PropertyNotFoundException;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class ProcessOutVariables {\n\n    private static final Logger log = LoggerFactory.getLogger(ProcessOutVariables.class);\n\n    private final ExecutionContextFactory<? extends ExecutionContextImpl> contextFactory;\n\n    public ProcessOutVariables(ExecutionContextFactory<? extends ExecutionContextImpl> contextFactory) {\n        this.contextFactory = contextFactory;\n    }\n\n    public Map<String, Object> eval(Variables vars) {\n        Collection<String> outExprs = getOutExpressions(vars);\n        if (outExprs == null || outExprs.isEmpty()) {\n            return Collections.emptyMap();\n        }\n\n        Map<String, Object> result = new HashMap<>();\n        for (String x : outExprs) {\n            ExecutionContext ctx = contextFactory.create(vars);\n\n            Object v;\n            try {\n                v = ctx.eval(\"${\" + x + \"}\", Object.class);\n            } catch (PropertyNotFoundException e) {\n                log.warn(\"OUT variable not found: {}\", x);\n                v = null;\n            }\n\n            if (v == null) {\n                continue;\n            }\n\n            result.put(x, v);\n        }\n        return result;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Collection<String> getOutExpressions(Variables vars) {\n        Object o = vars.getVariable(InternalConstants.Context.OUT_EXPRESSIONS_KEY);\n        if (o == null) {\n            return null;\n        }\n\n        if (!(o instanceof Collection)) {\n            throw new IllegalArgumentException(\"Invalid type of OUT value expression list: \" + o.getClass());\n        }\n\n        return (Collection<String>) o;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/ProcessOutVariablesListener.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runner.OutVariablesParser;\nimport io.takari.bpm.EngineListener;\nimport io.takari.bpm.state.ProcessInstance;\n\nimport java.nio.file.Path;\nimport java.util.Map;\n\npublic class ProcessOutVariablesListener implements EngineListener {\n\n    private final Path storeDir;\n    private final ProcessOutVariables outVariables;\n\n    public ProcessOutVariablesListener(Path storeDir, ProcessOutVariables outVariables) {\n        this.storeDir = storeDir;\n        this.outVariables = outVariables;\n    }\n\n    @Override\n    public ProcessInstance onFinalize(ProcessInstance state) {\n        Map<String, Object> vars = outVariables.eval(state.getVariables());\n\n        if (vars.isEmpty()) {\n            return state;\n        }\n\n        OutVariablesParser.write(storeDir, vars);\n\n        return state;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/ProtectedVarContext.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.sdk.Context;\n\nimport java.util.UUID;\n\npublic class ProtectedVarContext implements TaskInterceptor {\n\n    private static final ThreadLocal<UUID> threadLocalScope = new ThreadLocal<>();\n\n    private static final UUID token = UUID.randomUUID();\n\n    private final PolicyEngine policyEngine;\n\n    public ProtectedVarContext(PolicyEngine policyEngine) {\n        this.policyEngine = policyEngine;\n    }\n\n    @Override\n    public void preTask(String taskName, Object instance, Context ctx) {\n        if (policyEngine != null && policyEngine.getProtectedTasksPolicy().isProtected(taskName)) {\n            threadLocalScope.set(token);\n        }\n    }\n\n    @Override\n    public void postTask(String taskName, Object instance, Context ctx) {\n        threadLocalScope.remove();\n    }\n\n    public boolean hasToken() {\n        return threadLocalScope.get() != null;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/ResourceResolverImpl.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.takari.bpm.resource.ClassPathResourceResolver;\nimport io.takari.bpm.resource.ResourceResolver;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.HttpURLConnection;\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport java.net.URLConnection;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic class ResourceResolverImpl implements ResourceResolver {\n\n    private static final int MAX_CONTENT_LENGTH = 640 * 1024; // \"640kB ought to be enough for anybody\"\n\n    private final ResourceResolver classPathDelegate = new ClassPathResourceResolver();\n    private final Path baseDir;\n\n    public ResourceResolverImpl(Path baseDir) {\n        this.baseDir = baseDir;\n    }\n\n    @Override\n    public InputStream getResourceAsStream(String name) throws IOException {\n        try {\n            URL url = new URL(name);\n            return toStream(url);\n        } catch (MalformedURLException e) {\n            // not a URL, let's try a file first...\n            InputStream in = toStream(name);\n            if (in != null) {\n                return in;\n            }\n\n            //...or look in the classpath\n            return classPathDelegate.getResourceAsStream(name);\n        }\n    }\n\n    private InputStream toStream(URL url) throws IOException {\n        URLConnection conn = url.openConnection();\n        int length = -1;\n\n        if (conn instanceof HttpURLConnection) {\n            HttpURLConnection httpConn = (HttpURLConnection) conn;\n\n            String s = httpConn.getHeaderField(\"Content-Length\");\n            if (s != null) {\n                length = Integer.parseInt(s);\n            }\n        }\n\n        assertContentLength(length);\n\n        ByteArrayOutputStream out;\n        if (length > 0) {\n            out = new ByteArrayOutputStream(length);\n        } else {\n            out = new ByteArrayOutputStream();\n        }\n\n        byte[] buf = new byte[4096];\n        int read;\n\n        try (InputStream in = conn.getInputStream()) {\n            while ((read = in.read(buf)) > 0) {\n                assertContentLength(out.size());\n                out.write(buf, 0, read);\n            }\n        }\n\n        return new ByteArrayInputStream(out.toByteArray());\n    }\n\n    private InputStream toStream(String name) throws IOException {\n        Path p = baseDir.resolve(name);\n        if (!Files.exists(p)) {\n            return null;\n        }\n        return Files.newInputStream(p);\n    }\n\n    private static void assertContentLength(int current) throws IOException {\n        if (current > MAX_CONTENT_LENGTH) {\n            throw new IOException(\"Content too long: \" + current + \" (max: \" + MAX_CONTENT_LENGTH + \")\");\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/TaskEventInterceptor.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.runner.ContextUtils;\nimport com.walmartlabs.concord.runtime.common.ObjectTruncater;\nimport com.walmartlabs.concord.sdk.Context;\nimport io.takari.bpm.api.ExecutionContext;\nimport io.takari.bpm.api.ExecutionException;\nimport io.takari.bpm.model.AbstractElement;\nimport io.takari.bpm.model.ServiceTask;\n\nimport java.io.IOException;\nimport java.io.Serializable;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.project.InternalConstants.Context.EVENT_CORRELATION_KEY;\nimport static com.walmartlabs.concord.project.InternalConstants.Context.EVENT_CREATED_AT_KEY;\n\npublic class TaskEventInterceptor implements TaskInterceptor {\n\n    private static final ObjectMapper objectMapper = new ObjectMapper();\n\n    private final EventConfiguration cfg;\n    private final ElementEventProcessor eventProcessor;\n\n    public TaskEventInterceptor(EventConfiguration cfg, ElementEventProcessor eventProcessor) {\n        this.cfg = cfg;\n        this.eventProcessor = eventProcessor;\n    }\n\n    @Override\n    public void preTask(String taskName, Object instance, Context ctx) throws ExecutionException {\n        UUID correlationId = UUID.randomUUID();\n\n        Map<String, Object> logMetaData = getLogMetaData(instance, ctx);\n        TaskTag.pre(taskName, instance, correlationId, logMetaData).log();\n\n        eventProcessor.process(buildEvent(ctx), (element) -> {\n            Map<String, Object> params = new HashMap<>();\n            params.put(\"name\", taskName);\n            params.put(\"correlationId\", correlationId);\n            params.put(\"phase\", \"pre\");\n            List<VariableMapping> p = getInParams(ctx, element);\n            if (p != null) {\n                params.put(\"in\", p);\n            }\n            return params;\n        });\n\n        ctx.setVariable(EVENT_CORRELATION_KEY, correlationId);\n        ctx.setVariable(EVENT_CREATED_AT_KEY, System.currentTimeMillis());\n    }\n\n    @Override\n    public void postTask(String taskName, Object instance, Context ctx) throws ExecutionException {\n        UUID correlationId = (UUID) ctx.getVariable(EVENT_CORRELATION_KEY);\n        Long preEventTime = (Long) ctx.getVariable(EVENT_CREATED_AT_KEY);\n\n        Map<String, Object> logMetaData = getLogMetaData(instance, ctx);\n        TaskTag.post(taskName, instance, correlationId, logMetaData).log();\n\n        eventProcessor.process(buildEvent(ctx), (element) -> {\n            Map<String, Object> params = new HashMap<>();\n            params.put(\"name\", taskName);\n            params.put(\"correlationId\", correlationId);\n            params.put(\"phase\", \"post\");\n            List<VariableMapping> p = getOutParams(ctx, element);\n            if (p != null) {\n                params.put(\"out\", p);\n            }\n            if (preEventTime != null) {\n                params.put(\"duration\", System.currentTimeMillis() - preEventTime);\n            }\n            if (logMetaData != null) {\n                params.put(\"logMetaData\", logMetaData);\n            }\n            return params;\n        });\n\n        ctx.removeVariable(EVENT_CORRELATION_KEY);\n        ctx.removeVariable(EVENT_CREATED_AT_KEY);\n    }\n\n    private Map<String, Object> getLogMetaData(Object task, Context ctx) {\n        Class<?> clazz = task.getClass();\n\n        if (LogTagMetadataProvider.class.isAssignableFrom(clazz)) {\n            LogTagMetadataProvider p = (LogTagMetadataProvider) task;\n            return p.createLogTagMetadata(ctx);\n        }\n\n        return null;\n    }\n\n    private List<VariableMapping> getInParams(Context ctx, AbstractElement element) {\n        if (!cfg.isRecordTaskInVars()) {\n            return Collections.emptyList();\n        }\n\n        if (!(element instanceof ServiceTask)) {\n            return null;\n        }\n\n        ServiceTask t = (ServiceTask) element;\n        if (t.getIn() == null) {\n            return null;\n        }\n\n        return convertParams(ctx, t.getIn(), cfg.getInVarsBlacklist(), cfg.isTruncateInVars(), cfg.getTruncateMaxStringLength(), cfg.getTruncateMaxArrayLength(), cfg.getTruncateMaxDepth());\n    }\n\n    private List<VariableMapping> getOutParams(Context ctx, AbstractElement element) {\n        if (!cfg.isRecordTaskOutVars()) {\n            return Collections.emptyList();\n        }\n\n        if (!(element instanceof ServiceTask)) {\n            return null;\n        }\n\n        ServiceTask t = (ServiceTask) element;\n        if (t.getOut() == null) {\n            return null;\n        }\n\n        return convertParams(ctx, t.getOut(), cfg.getOutVarsBlacklist(), cfg.isTruncateOutVars(), cfg.getTruncateMaxStringLength(), cfg.getTruncateMaxArrayLength(), cfg.getTruncateMaxDepth());\n    }\n\n    private static ElementEventProcessor.ElementEvent buildEvent(Context ctx) {\n        String instanceId = (String) ctx.getVariable(ExecutionContext.PROCESS_BUSINESS_KEY);\n\n        return new ElementEventProcessor.ElementEvent(instanceId,\n                ctx.getProcessDefinitionId(), ctx.getElementId(), ContextUtils.getSessionToken(ctx));\n    }\n\n    private static List<VariableMapping> convertParams(Context ctx,\n                                                       Collection<io.takari.bpm.model.VariableMapping> m,\n                                                       Collection<String> blacklist,\n                                                       boolean truncate,\n                                                       int maxStringLength, int maxArrayLength, int maxDepth) {\n        if (m == null) {\n            return null;\n        }\n\n        return m.stream()\n                .map(v -> toMapping(ctx, v, blacklist.contains(v.getTarget()), truncate, maxStringLength, maxArrayLength, maxDepth))\n                .collect(Collectors.toList());\n    }\n\n    private static VariableMapping toMapping(Context ctx,\n                                             io.takari.bpm.model.VariableMapping v,\n                                             boolean blacklisted,\n                                             boolean truncate,\n                                             int maxStringLength, int maxArrayLength, int maxDepth) {\n        Serializable resolved = \"n/a\";\n\n        Object o = ctx.getVariable(v.getTarget());\n        if (!blacklisted && o instanceof Serializable) {\n            resolved = (Serializable) o;\n\n            if (truncate) {\n                resolved = (Serializable)ObjectTruncater.truncate(resolved, maxStringLength, maxArrayLength, maxDepth);\n            }\n        }\n\n        Object sourceValue = ObjectTruncater.truncate(v.getSourceValue(), maxStringLength, maxArrayLength, maxDepth);\n\n        return new VariableMapping(v.getSource(), v.getSourceExpression(), sourceValue, v.getTarget(), resolved);\n    }\n\n    @JsonInclude(JsonInclude.Include.NON_NULL)\n    public static class VariableMapping implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        private final String source;\n        private final String sourceExpression;\n        private final Object sourceValue;\n        private final String target;\n        private final Serializable resolved;\n\n        @JsonCreator\n        public VariableMapping(\n                @JsonProperty(\"source\") String source,\n                @JsonProperty(\"sourceExpression\") String sourceExpression,\n                @JsonProperty(\"sourceValue\") Object sourceValue,\n                @JsonProperty(\"target\") String target,\n                @JsonProperty(\"resolved\") Serializable resolved) {\n\n            this.source = source;\n            this.sourceExpression = sourceExpression;\n            this.sourceValue = sourceValue;\n            this.target = target;\n            this.resolved = resolved;\n        }\n\n        public String getSource() {\n            return source;\n        }\n\n        public String getSourceExpression() {\n            return sourceExpression;\n        }\n\n        public Object getSourceValue() {\n            return sourceValue;\n        }\n\n        public String getTarget() {\n            return target;\n        }\n\n        public Serializable getResolved() {\n            return resolved;\n        }\n    }\n\n    @JsonInclude(JsonInclude.Include.NON_EMPTY)\n    private static final class TaskTag implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        public static TaskTag pre(String taskName, Object task, UUID correlationId, Map<String, Object> meta) {\n            return new TaskTag(\"pre\", taskName, correlationId, meta);\n        }\n\n        public static TaskTag post(String taskName, Object task, UUID correlationId, Map<String, Object> meta) {\n            return new TaskTag(\"post\", taskName, correlationId, meta);\n        }\n\n        private final String phase;\n        private final String taskName;\n        private final UUID correlationId;\n        private final Map<String, Object> meta;\n\n        private TaskTag(String phase, String taskName, UUID correlationId, Map<String, Object> meta) {\n            this.phase = phase;\n            this.taskName = taskName;\n            this.correlationId = correlationId;\n            this.meta = meta;\n        }\n\n        public String getPhase() {\n            return phase;\n        }\n\n        public String getTaskName() {\n            return taskName;\n        }\n\n        public UUID getCorrelationId() {\n            return correlationId;\n        }\n\n        public Map<String, Object> getMeta() {\n            return meta;\n        }\n\n        private void log() throws ExecutionException {\n            try {\n                System.out.print(\"__logTag:\");\n                System.out.println(objectMapper.writeValueAsString(this));\n            } catch (IOException e) {\n                throw new ExecutionException(\"Error while writing the task's tag: (\" + phase + \", \" + taskName + \")\", e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/TaskInterceptor.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Context;\nimport io.takari.bpm.api.ExecutionException;\n\n/**\n * Activates before and after task call.\n */\npublic interface TaskInterceptor {\n\n    /**\n     * Activates before the task call.\n     *\n     * @param taskName name of the called task\n     * @param instance the task's instance that is being called\n     * @param ctx      the current process context\n     */\n    void preTask(String taskName, Object instance, Context ctx) throws ExecutionException;\n\n    /**\n     * Activates after the task call.\n     *\n     * @param taskName name of the called task\n     * @param instance the task's instance that was being called\n     * @param ctx      the current process context\n     */\n    void postTask(String taskName, Object instance, Context ctx) throws ExecutionException;\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/TaskRegistry.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Injector;\nimport com.walmartlabs.concord.common.DynamicTaskRegistry;\nimport com.walmartlabs.concord.runner.TaskClasses;\nimport com.walmartlabs.concord.sdk.Task;\nimport io.takari.bpm.task.ServiceTaskRegistry;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\n@Named\n@Singleton\npublic class TaskRegistry implements ServiceTaskRegistry, DynamicTaskRegistry {\n\n    private static final Logger log = LoggerFactory.getLogger(TaskRegistry.class);\n\n    private final Injector injector;\n    private final TaskClasses taskClasses;\n    private final Map<String, Class<? extends Task>> dynamicTasks = new ConcurrentHashMap<>();\n\n    @Inject\n    public TaskRegistry(Injector injector, TaskClasses taskClasses) {\n        this.injector = injector;\n        this.taskClasses = taskClasses;\n    }\n\n    @Override\n    public Task getByKey(String key) {\n        Class<? extends Task> taskClass = taskClasses.get(key);\n        if(taskClass == null) {\n            taskClass = dynamicTasks.get(key);\n            if (taskClass == null) {\n                return null;\n            }\n        }\n\n        return injector.getInstance(taskClass);\n    }\n\n    @Override\n    public void register(Class<? extends Task> taskClass) {\n        Named n = taskClass.getAnnotation(Named.class);\n        if (n == null) {\n            throw new IllegalArgumentException(\"Tasks must be annotated with @Named\");\n        }\n\n        dynamicTasks.put(n.value(), taskClass);\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/el/InjectVariableELResolver.java",
    "content": "package com.walmartlabs.concord.runner.engine.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.InjectVariable;\n\nimport javax.el.BeanELResolver;\nimport javax.el.ELContext;\nimport javax.el.ELResolver;\nimport javax.el.MethodNotFoundException;\nimport java.beans.FeatureDescriptor;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Parameter;\nimport java.util.Arrays;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.stream.Collectors;\n\npublic class InjectVariableELResolver extends ELResolver {\n\n    private final BeanELResolver delegate = new BeanELResolver();\n\n    @Override\n    public Object getValue(ELContext context, Object base, Object property) {\n        return null;\n    }\n\n    @Override\n    public Object invoke(ELContext context, Object base, Object method, Class<?>[] paramTypes, Object[] paramValues) {\n        if (base == null || method == null) {\n            return null;\n        }\n\n        List<Method> methods = findMethodWithInjections(base.getClass(), method.toString());\n\n        if (paramTypes == null) {\n            paramTypes = getTypesFromValues(paramValues);\n        }\n\n        for(Method m : methods) {\n            Class<?>[] newParamTypes = processParamTypes(m.getParameters(), paramTypes);\n            if(newParamTypes == null) {\n                continue;\n            }\n            Object[] newParams = processParams(context, m.getParameters(), paramValues);\n            if(newParams == null) {\n                continue;\n            }\n\n            try {\n                return delegate.invoke(context, base, method, newParamTypes, newParams);\n            } catch (MethodNotFoundException e) {\n                // ignore\n            }\n        }\n\n        return null;\n    }\n\n    @Override\n    public Class<?> getType(ELContext context, Object base, Object property) {\n        return Object.class;\n    }\n\n    @Override\n    public void setValue(ELContext context, Object base, Object property, Object value) {\n    }\n\n    @Override\n    public boolean isReadOnly(ELContext context, Object base, Object property) {\n        return true;\n    }\n\n    @Override\n    public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {\n        return null;\n    }\n\n    @Override\n    public Class<?> getCommonPropertyType(ELContext context, Object base) {\n        return Object.class;\n    }\n\n    private static Class<?>[] getTypesFromValues(Object[] values) {\n        if (values == null) {\n            return null;\n        }\n\n        Class<?> result[] = new Class<?>[values.length];\n        for (int i = 0; i < values.length; i++) {\n            if (values[i] == null) {\n                result[i] = null;\n            } else {\n                result[i] = values[i].getClass();\n            }\n        }\n        return result;\n    }\n\n    private static Class<?>[] processParamTypes(Parameter[] methodParams, Class<?>[] originalParamTypes) {\n        Class<?>[] result = new Class[methodParams.length];\n        int originalParamTypesLength = originalParamTypes != null ? originalParamTypes.length : 0;\n        int paramIndex = 0;\n        for(int i = 0; i < methodParams.length; i++) {\n            Parameter mp = methodParams[i];\n            if (getInjectedVariableName(mp) != null) {\n                result[i] = mp.getType();\n            } else {\n                if(paramIndex >= originalParamTypesLength) {\n                    return null;\n                }\n                result[i] = originalParamTypes[paramIndex];\n                paramIndex++;\n            }\n        }\n\n        if(paramIndex < originalParamTypesLength) {\n            return null;\n        }\n\n        return result;\n    }\n\n    private static Object[] processParams(ELContext context, Parameter[] methodParams, Object[] originalParams) {\n        Object[] result = new Object[methodParams.length];\n        int paramIndex = 0;\n        int originalParamsLength = originalParams != null ? originalParams.length : 0;\n        for(int i = 0; i < methodParams.length; i++) {\n            Parameter mp = methodParams[i];\n            String n = getInjectedVariableName(mp);\n            if (n != null) {\n                result[i] = ResolverUtils.getVariable(context, n);\n            } else {\n                if(paramIndex >= originalParamsLength) {\n                    return null;\n                }\n                result[i] = originalParams[paramIndex];\n                paramIndex++;\n            }\n        }\n\n        if(paramIndex < originalParamsLength) {\n            return null;\n        }\n\n        return result;\n    }\n\n    private static String getInjectedVariableName(Parameter p) {\n        InjectVariable iv = p.getAnnotation(InjectVariable.class);\n        if (iv != null) {\n            return iv.value();\n        }\n        return null;\n    }\n\n    private static List<Method> findMethodWithInjections(Class<?> type, String name) {\n        List<Method> candidates = Arrays.stream(type.getMethods())\n                .filter(m -> m.getName().equals(name))\n                .collect(Collectors.toList());\n\n        return candidates.stream()\n                .map(m -> findMethodWithInjections(type, m))\n                .filter(Objects::nonNull)\n                .sorted((o1, o2) -> {\n                        int result = -Integer.compare(getInjectedParamCount(o1), getInjectedParamCount(o2));\n                        if(result == 0) {\n                            return -Integer.compare(o1.getParameterCount(), o2.getParameterCount());\n                        }\n                        return result;\n                })\n                .collect(Collectors.toList());\n    }\n\n    private static Method findMethodWithInjections(Class<?> type, Method m) {\n        if(getInjectedParamCount(m) > 0) {\n            return m;\n        }\n\n        Class<?>[] inf = type.getInterfaces();\n        Method mp;\n        for (Class<?> anInf : inf) {\n            try {\n                mp = anInf.getMethod(m.getName(), m.getParameterTypes());\n                mp = findMethodWithInjections(mp.getDeclaringClass(), mp);\n                if (mp != null) {\n                    return mp;\n                }\n            } catch (NoSuchMethodException e) {\n                // Ignore\n            }\n        }\n        Class<?> sup = type.getSuperclass();\n        if (sup != null) {\n            try {\n                mp = sup.getMethod(m.getName(), m.getParameterTypes());\n                mp = findMethodWithInjections(mp.getDeclaringClass(), mp);\n                if (mp != null) {\n                    return mp;\n                }\n            } catch (NoSuchMethodException e) {\n                // Ignore\n            }\n        }\n        return null;\n    }\n\n    private static int getInjectedParamCount(Method m) {\n        Parameter[] params = m.getParameters();\n        if(params == null) {\n            return 0;\n        }\n\n        int count = 0;\n        for(Parameter p : params) {\n            if (getInjectedVariableName(p) != null) {\n                count++;\n            }\n        }\n        return count;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/el/ResolverUtils.java",
    "content": "package com.walmartlabs.concord.runner.engine.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.sun.el.lang.EvaluationContext;\nimport com.walmartlabs.concord.project.InternalConstants;\nimport io.takari.bpm.api.ExecutionContext;\n\nimport javax.el.ELContext;\nimport javax.el.ValueExpression;\nimport javax.el.VariableMapper;\n\npublic final class ResolverUtils {\n\n    public static Object getVariable(ELContext context, String name) {\n        VariableMapper varMapper = ((EvaluationContext) context).getELContext().getVariableMapper();\n        ValueExpression v = varMapper.resolveVariable(name);\n        if(v != null) {\n            return v.getValue(context);\n        }\n        v = varMapper.resolveVariable(InternalConstants.Context.CONTEXT_KEY);\n        if(v != null) {\n            ExecutionContext ctx = (ExecutionContext) v.getValue(context);\n            return ctx.getVariable(name);\n        }\n        return null;\n    }\n\n    private ResolverUtils() {\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/java/com/walmartlabs/concord/runner/engine/el/TaskResolver.java",
    "content": "package com.walmartlabs.concord.runner.engine.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.InjectVariable;\nimport io.takari.bpm.task.ServiceTaskRegistry;\nimport io.takari.bpm.task.ServiceTaskResolver;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.el.ELContext;\nimport javax.inject.Singleton;\nimport java.lang.reflect.Field;\n\npublic class TaskResolver extends ServiceTaskResolver {\n\n    private static final Logger log = LoggerFactory.getLogger(TaskResolver.class);\n\n    public TaskResolver(ServiceTaskRegistry registry) {\n        super(registry);\n    }\n\n    @Override\n    public Object getValue(ELContext context, Object base, Object property) {\n        Object o = super.getValue(context, base, property);\n        if (o != null && context.isPropertyResolved()) {\n            injectVariables(context, o, property);\n        }\n        return o;\n    }\n\n    private static void injectVariables(ELContext context, Object o, Object property) {\n        Class<?> clazz = o.getClass();\n        boolean isSingleton = clazz.isAnnotationPresent(Singleton.class);\n        while (clazz != null) {\n            for (Field f : clazz.getDeclaredFields()) {\n                String v = getAnnotationValue(f);\n                if (v == null) {\n                    continue;\n                }\n\n                inject(f, v, context, o, property, isSingleton);\n            }\n\n            clazz = clazz.getSuperclass();\n        }\n    }\n\n    private static String getAnnotationValue(Field f) {\n        InjectVariable iv = f.getAnnotation(InjectVariable.class);\n        if (iv != null) {\n            return iv.value();\n        }\n        return null;\n    }\n\n    private static void inject(Field f, String value, ELContext context, Object base, Object property, boolean isSingleton) {\n        if (isSingleton) {\n            log.warn(\"invoke ['{}', '{}'] -> @InjectVariable cannot be used in @Singleton tasks: '{}'\", base, property, f.getName());\n            return;\n        }\n\n        try {\n            Object variableValue = ResolverUtils.getVariable(context, value);\n\n            if (!f.isAccessible()) {\n                f.setAccessible(true);\n            }\n            f.set(base, variableValue);\n\n            log.debug(\"invoke ['{}', '{}'] -> set value '{}' for '{}'\", base, property, f.getName(), variableValue);\n        } catch (IllegalAccessException e) {\n            throw new RuntimeException(\"Error while setting property '\" + f.getName() + \"': \" + e.getMessage(), e);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/main/resources/logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <!-- the UI expects log timestamps in a specific format to be able to convert it to the local time -->\n            <pattern>%date{yyyy-MM-dd'T'HH:mm:ss.SSSZ, UTC} [%-5level] %msg%n%rEx{full, com.sun, sun}</pattern>\n        </encoder>\n    </appender>\n\n    <appender name=\"PROCESS_STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <!-- the UI expects log timestamps in a specific format to be able to convert it to the local time -->\n            <pattern>%date{yyyy-MM-dd'T'HH:mm:ss.SSSZ, UTC} %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <logger name=\"io.takari.bpm.state\" level=\"ERROR\"/>\n\n    <logger name=\"processLog\" level=\"INFO\" additivity=\"false\">\n        <appender-ref ref=\"PROCESS_STDOUT\"/>\n    </logger>\n\n    <logger name=\"com.walmartlabs.concord.plugins.log\" level=\"${logLevel:-INFO}\"/>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "runtime/v1/impl/src/test/java/com/walmartlabs/concord/runner/engine/ConcordExecutionContextTest.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.policyengine.ProtectedTasksPolicy;\nimport io.takari.bpm.api.ExecutionContext;\nimport io.takari.bpm.api.ExecutionContextFactory;\nimport io.takari.bpm.api.Variables;\nimport io.takari.bpm.el.ExpressionManager;\nimport io.takari.bpm.form.FormService;\nimport org.junit.jupiter.api.Test;\n\nimport static com.walmartlabs.concord.runner.engine.ConcordExecutionContextFactory.ConcordExecutionContext;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ConcordExecutionContextTest {\n\n    private final ProtectedVarContext varContext = new ProtectedVarContext(policyEngine());\n\n    @Test\n    public void testProtectedVariableWithoutContext() {\n        assertThrows(RuntimeException.class, () -> {\n            ConcordExecutionContext ctx = ctx();\n            ctx.setProtectedVariable(\"oops\", 1);\n        });\n    }\n\n    @Test\n    public void testProtectedVariable() {\n        ConcordExecutionContext ctx = ctx();\n\n        varContext.preTask(\"task\", null, null);\n        ctx.setProtectedVariable(\"s\", \"s-var\");\n        varContext.postTask(\"task\", null,null);\n\n        assertEquals(\"s-var\", ctx.getVariable(\"s\"));\n        assertEquals(\"s-var\", ctx.getProtectedVariable(\"s\"));\n\n        assertNull(ctx.getVariable(\"not-found\"));\n        assertNull(ctx.getProtectedVariable(\"not-found\"));\n\n        try {\n            ctx.removeVariable(\"s\");\n            fail(\"exception expected\");\n        } catch (RuntimeException e) {\n            // ignore\n        }\n    }\n\n    private ConcordExecutionContext ctx() {\n        ExecutionContextFactory<? extends ExecutionContext> ctxFactory = null;\n        ExpressionManager expressionManager = null;\n        FormService formService = null;\n        Variables source = new Variables();\n        return new ConcordExecutionContext(ctxFactory, expressionManager, source, varContext, formService);\n    }\n\n    private static PolicyEngine policyEngine() {\n        ProtectedTasksPolicy protectedTasksPolicy = mock(ProtectedTasksPolicy.class);\n        when(protectedTasksPolicy.isProtected(eq(\"task\"))).thenReturn(true);\n\n        PolicyEngine policyEngine = mock(PolicyEngine.class);\n        when(policyEngine.getProtectedTasksPolicy()).thenReturn(protectedTasksPolicy);\n        return policyEngine;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/test/java/com/walmartlabs/concord/runner/engine/EventReportingServiceTest.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClientFactory;\nimport com.walmartlabs.concord.client2.ProcessEventRequest;\nimport com.walmartlabs.concord.client2.ProcessEventsApi;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.UUID;\nimport java.util.concurrent.atomic.AtomicInteger;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.mock;\n\nclass EventReportingServiceTest {\n\n    private static final UUID instanceId1 = UUID.randomUUID();\n    private static final String sessionToken1 = UUID.randomUUID().toString();\n\n    @Test\n    void testSingle() {\n        var evs = getService(1);\n        evs.report(new ProcessEventRequest(), instanceId1, sessionToken1);\n        evs.report(new ProcessEventRequest(), instanceId1, sessionToken1);\n        evs.report(new ProcessEventRequest(), instanceId1, sessionToken1);\n        evs.report(new ProcessEventRequest(), instanceId1, sessionToken1);\n\n        assertEquals(0, evs.flushCounter.get());\n\n        evs.onFinalize(null);\n\n        assertEquals(1, evs.flushCounter.get());\n        assertEquals(4, evs.sendSingleCounter.get());\n    }\n\n    @Test\n    void testBatch() {\n        var evs = getService(2);\n        evs.report(new ProcessEventRequest(), instanceId1, sessionToken1);\n        evs.report(new ProcessEventRequest(), instanceId1, sessionToken1);\n        evs.report(new ProcessEventRequest(), instanceId1, sessionToken1);\n        evs.report(new ProcessEventRequest(), instanceId1, sessionToken1);\n\n        assertEquals(2, evs.flushCounter.get());\n\n        evs.onFinalize(null);\n\n        assertEquals(3, evs.flushCounter.get());\n        assertEquals(0, evs.sendSingleCounter.get());\n    }\n\n    private static MockedEventReportingService getService(int batchSize) {\n        return new MockedEventReportingService(batchSize, 30);\n    }\n\n    private static class MockedEventReportingService extends DefaultEventReportingService {\n        final ProcessEventsApi mockProcessEventsApi;\n        private final AtomicInteger flushCounter;\n        private final AtomicInteger sendSingleCounter;\n\n        public MockedEventReportingService(int batchSize, int duration) {\n            super(mock(ApiClientFactory.class), batchSize, duration);\n            this.mockProcessEventsApi = mock(ProcessEventsApi.class);\n            this.flushCounter = new AtomicInteger();\n            this.sendSingleCounter = new AtomicInteger();\n        }\n\n        @Override\n        void flush() {\n            super.flush();\n            flushCounter.incrementAndGet();\n        }\n\n        @Override\n        void sendSingle(ProcessEventRequest req, UUID instanceId, String sessionToken) {\n            super.sendSingle(req, instanceId, sessionToken);\n            sendSingleCounter.incrementAndGet();\n        }\n\n        @Override\n        ProcessEventsApi getProcessEventsApi(String sessionToken) {\n            return mockProcessEventsApi;\n        }\n    }\n\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/test/java/com/walmartlabs/concord/runner/engine/PolicyPreprocessorTest.java",
    "content": "package com.walmartlabs.concord.runner.engine;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.project.InternalConstants;\nimport com.walmartlabs.concord.runner.engine.el.InjectVariableELResolver;\nimport com.walmartlabs.concord.sdk.Context;\nimport io.takari.bpm.api.Variables;\nimport io.takari.bpm.el.DefaultExpressionManager;\nimport io.takari.bpm.el.ExpressionManager;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.util.Arrays;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class PolicyPreprocessorTest {\n\n    private static final String EXPECTED_PROCESSED_POLICY = \"{\\\"ansible\\\":{\\\"allow\\\":[],\\\"warn\\\":[],\\\"deny\\\":[{\\\"msg\\\":\\\"Can't download artifacts without Gatekeeper\\\",\\\"action\\\":\\\"uri\\\",\\\"params\\\":[{\\\"values\\\":[\\\"a\\\",\\\"b\\\",\\\"c\\\"],\\\"name\\\":\\\"url\\\"}]}]}}\";\n\n    @Test\n    public void testPreprocess() throws Exception {\n        ExpressionManager expressionManager = new DefaultExpressionManager(\n                new String[]{InternalConstants.Context.CONTEXT_KEY, InternalConstants.Context.EXECUTION_CONTEXT_KEY},\n                new InjectVariableELResolver());\n\n        Context ctx = new ConcordExecutionContextFactory.ConcordExecutionContext(null, expressionManager, new Variables(), null, null);\n        ctx.setVariable(\"gatekeeperArtifacts\", Arrays.asList(\"a\", \"b\", \"c\"));\n\n        Path workDir = Files.createTempDirectory(\"concord-test\");\n        Files.createDirectories(workDir.resolve(InternalConstants.Files.CONCORD_SYSTEM_DIR_NAME));\n\n        // -- read original policy\n        String originalPolicy;\n        try (InputStream is = PolicyPreprocessorTest.class.getResourceAsStream(\"/com/walmartlabs/concord/runner/policy.json\")) {\n            ObjectMapper om = new ObjectMapper();\n            originalPolicy = om.writeValueAsString(om.readValue(is, Map.class));\n        }\n\n        // -- write tmp policy\n        Path policyFile = workDir.resolve(InternalConstants.Files.CONCORD_SYSTEM_DIR_NAME).resolve(InternalConstants.Files.POLICY_FILE_NAME);\n        try (InputStream is = PolicyPreprocessorTest.class.getResourceAsStream(\"/com/walmartlabs/concord/runner/policy.json\")) {\n            Files.copy(is, policyFile, StandardCopyOption.REPLACE_EXISTING);\n        }\n\n        // ---\n        PolicyPreprocessor pp = new PolicyPreprocessor(workDir);\n        pp.preTask(\"ansible\", null, ctx);\n\n        String processedPolicy = new String(Files.readAllBytes(policyFile));\n        assertEquals(EXPECTED_PROCESSED_POLICY, processedPolicy);\n\n        // ---\n        pp.postTask(\"ansible\", null, ctx);\n        String restoredPolicy = new String(Files.readAllBytes(policyFile));\n        assertEquals(originalPolicy, restoredPolicy);\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/test/java/com/walmartlabs/concord/runner/engine/el/AbstractElResolverTest.java",
    "content": "package com.walmartlabs.concord.runner.engine.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.sun.el.lang.EvaluationContext;\nimport io.takari.bpm.api.ExecutionContext;\n\nimport javax.el.*;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\npublic abstract class AbstractElResolverTest {\n\n    private final ExecutionContext execution = mock(ExecutionContext.class);\n\n    protected void mockVariables(String varName, Object varValue) {\n        when(execution.getVariable(eq(varName))).thenReturn(varValue);\n    }\n\n    protected ELContext createContext() {\n        ValueExpression ve = mock(ValueExpression.class);\n        when(ve.getValue(any())).thenReturn(execution);\n\n        VariableMapper varMapper = mock(VariableMapper.class);\n        when(varMapper.resolveVariable(\"execution\")).thenReturn(ve);\n        when(varMapper.resolveVariable(\"context\")).thenReturn(ve);\n\n        ELContext ctx = new ELContext() {\n            @Override\n            public ELResolver getELResolver() {\n                return null;\n            }\n\n            @Override\n            public FunctionMapper getFunctionMapper() {\n                return null;\n            }\n\n            @Override\n            public VariableMapper getVariableMapper() {\n                return varMapper;\n            }\n        };\n        return new EvaluationContext(ctx, null, null);\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/test/java/com/walmartlabs/concord/runner/engine/el/InjectVariableELResolverTest.java",
    "content": "package com.walmartlabs.concord.runner.engine.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.InjectVariable;\nimport io.takari.bpm.api.ExecutionContext;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport javax.el.ELContext;\n\nimport static org.mockito.Mockito.*;\n\npublic class InjectVariableELResolverTest extends AbstractElResolverTest {\n\n    private ELContext elContext;\n    private InjectVariableELResolver resolver;\n\n    @BeforeEach\n    public void setUp() {\n        resolver = new InjectVariableELResolver();\n        elContext = createContext();\n    }\n\n    @Test\n    public void callWithoutInject() {\n        TestTask task = spy(new TestTask());\n        Object method = \"callWithInject\";\n        Class<?>[] paramTypes = null;\n        Object[] paramValues = null;\n\n        resolver.invoke(elContext, task, method, paramTypes, paramValues);\n\n        verify(task, times(0)).callWithInject();\n    }\n\n    @Test\n    public void callWithInjectOnlyInjectedVars() {\n        TestTask task = spy(new TestTask());\n        Object method = \"callWithInject1\";\n        Class<?>[] paramTypes = null;\n        Object[] paramValues = null;\n\n        resolver.invoke(elContext, task, method, paramTypes, paramValues);\n\n        verify(task, times(1)).callWithInject1(any(ExecutionContext.class));\n    }\n\n    @Test\n    public void callWithInject() {\n        TestTask task = spy(new TestTask());\n        Object method = \"callWithInject1\";\n        Class<?>[] paramTypes = null;\n        Object[] paramValues = {\"xxxx\"};\n\n        resolver.invoke(elContext, task, method, paramTypes, paramValues);\n\n        verify(task, times(1)).callWithInject1(any(ExecutionContext.class), eq(\"xxxx\"));\n    }\n\n    @Test\n    public void callWithInject2() {\n        TestTask task = spy(new TestTask());\n        Object method = \"callWithInject2\";\n        Class<?>[] paramTypes = null;\n        Object[] paramValues = {\"xxxx\"};\n\n        mockVariables(\"injectedVar\", \"another-var\");\n\n        resolver.invoke(elContext, task, method, paramTypes, paramValues);\n\n        verify(task, times(1)).callWithInject2(any(ExecutionContext.class), eq(\"another-var\"), eq(\"xxxx\"));\n    }\n\n    private static class TestTask {\n\n        public void callWithInject() {\n            System.out.println(\"callWithInject\");\n        }\n\n        public void callWithInject1(\n                @InjectVariable(\"execution\") ExecutionContext executionContext) {\n            System.out.println(\"callWithInject ['\" + executionContext + \"']\");\n        }\n\n        public void callWithInject1(\n                @InjectVariable(\"execution\") ExecutionContext executionContext,\n                String var1) {\n            System.out.println(\"callWithInject ['\" + executionContext + \"', '\" + var1 + \"]\");\n        }\n\n        public void callWithInject2(\n                @InjectVariable(\"execution\") ExecutionContext injectedVar1,\n                @InjectVariable(\"injectedVar\") String injectedVar2,\n                String var1) {\n            System.out.println(\"callWithInject ['\" + injectedVar1 + \"', '\" + injectedVar2 + \"', '\" + injectedVar2 + \"]\");\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/test/java/com/walmartlabs/concord/runner/engine/el/TaskResolverTest.java",
    "content": "package com.walmartlabs.concord.runner.engine.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.InjectVariable;\nimport io.takari.bpm.task.ServiceTaskRegistryImpl;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport javax.el.ELContext;\n\nimport static org.mockito.Mockito.spy;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class TaskResolverTest extends AbstractElResolverTest {\n\n    private ServiceTaskRegistryImpl registry;\n    private ELContext elContext;\n    private TaskResolver resolver;\n\n    @BeforeEach\n    public void setUp() {\n        registry = new ServiceTaskRegistryImpl();\n        resolver = new TaskResolver(registry);\n        elContext = createContext();\n    }\n\n    @Test\n    public void test() {\n        TestTask task = spy(new TestTask());\n        registry.register(\"test\", task);\n\n        mockVariables(\"var1\", \"var1-value\");\n\n        resolver.getValue(elContext, null, \"test\");\n\n        assertEquals(\"var1-value\", task.value);\n    }\n\n    @SuppressWarnings(\"unused\")\n    private static class TestTask {\n\n        @InjectVariable(\"var1\")\n        private String value;\n\n        public void setValue(String value) {\n            this.value = value;\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/impl/src/test/resources/com/walmartlabs/concord/runner/policy.json",
    "content": "{\n  \"ansible\": {\n    \"allow\": [\n    ],\n    \"warn\": [\n    ],\n    \"deny\": [\n      {\n        \"msg\": \"Can't download artifacts without Gatekeeper\",\n        \"action\": \"uri\",\n        \"params\": [\n          {\n            \"name\": \"url\",\n            \"values\": \"${gatekeeperArtifacts}\"\n          }\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "runtime/v1/model/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.runtime.v1</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-runtime-model-v1</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime</groupId>\n            <artifactId>concord-runtime-model</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime</groupId>\n            <artifactId>concord-runtime-loader</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-imports</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-repository</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>io.takari.bpm</groupId>\n            <artifactId>bpm-engine-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.dataformat</groupId>\n            <artifactId>jackson-dataformat-yaml</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.takari</groupId>\n            <artifactId>parc</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-guava</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jdk8</artifactId>\n        </dependency>\n\n        <!-- to process YAML parsing errors -->\n        <dependency>\n            <groupId>org.yaml</groupId>\n            <artifactId>snakeyaml</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-engine</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>io.takari.bpm</groupId>\n            <artifactId>bpm-engine-impl</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-core</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.groovy</groupId>\n            <artifactId>groovy-all</artifactId>\n            <type>pom</type>\n            <scope>test</scope>\n            <exclusions>\n                <exclusion>\n                    <groupId>org.apache.groovy</groupId>\n                    <artifactId>groovy-test</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>org.apache.groovy</groupId>\n                    <artifactId>groovy-test-junit5</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n        <dependency>\n            <groupId>ch.qos.logback</groupId>\n            <artifactId>logback-classic</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/ImportsNormalizer.java",
    "content": "package com.walmartlabs.concord.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Imports;\n\npublic interface ImportsNormalizer {\n\n    Imports normalize(Imports imports);\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/InternalConstants.java",
    "content": "package com.walmartlabs.concord.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\n\n/**\n * Implementation-specific constants.\n * @deprecated will be removed after the v2 runtime introduction.\n */\npublic final class InternalConstants extends Constants {\n\n    /**\n     * Left for backward-compatibility.\n     */\n    public static final class Context extends Constants.Context {\n    }\n\n    /**\n     * Left for backward-compatibility.\n     */\n    public static final class Request extends Constants.Request {\n    }\n\n    /**\n     * Left for backward-compatibility.\n     */\n    public static final class Files extends Constants.Files {\n    }\n\n    /**\n     * Left for backward-compatibility.\n     */\n    public static final class Flows extends Constants.Flows {\n    }\n\n    /**\n     * Left for backward-compatibility.\n     */\n    public static final class Agent extends Constants.Agent {\n    }\n\n    /**\n     * Left for backward-compatibility.\n     */\n    public static final class Forms extends Constants.Forms {\n    }\n\n    /**\n     * Left for backward-compatibility.\n     */\n    public static final class Headers extends Constants.Headers {\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/NoopImportsNormalizer.java",
    "content": "package com.walmartlabs.concord.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Imports;\n\npublic class NoopImportsNormalizer implements ImportsNormalizer {\n\n    @Override\n    public Imports normalize(Imports imports) {\n        return imports;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/ProjectLoader.java",
    "content": "package com.walmartlabs.concord.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.imports.ImportManager;\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.imports.ImportsListener;\nimport com.walmartlabs.concord.project.model.Profile;\nimport com.walmartlabs.concord.project.model.ProjectDefinition;\nimport com.walmartlabs.concord.project.model.Resources;\nimport com.walmartlabs.concord.project.model.Trigger;\nimport com.walmartlabs.concord.project.yaml.*;\nimport com.walmartlabs.concord.project.yaml.model.*;\nimport com.walmartlabs.concord.project.yaml.validator.Validator;\nimport com.walmartlabs.concord.project.yaml.validator.ValidatorContext;\nimport com.walmartlabs.concord.repository.Snapshot;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport io.takari.bpm.model.ProcessDefinition;\nimport io.takari.bpm.model.form.FormDefinition;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.FileVisitResult;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.SimpleFileVisitor;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.process.loader.StandardRuntimeTypes.PROJECT_ROOT_FILE_NAMES;\n\npublic class ProjectLoader {\n\n    private final ImportManager importManager;\n    private final YamlParser parser = new YamlParser();\n\n    public ProjectLoader(ImportManager importManager) {\n        this.importManager = importManager;\n    }\n\n    /**\n     * Loads the project definition using the supplied stream. The stream is expected\n     * to contain the root project file (concord.yml).\n     */\n    public Result loadProject(InputStream in) throws Exception {\n        ProjectDefinitionBuilder b = new ProjectDefinitionBuilder(parser);\n        b.loadDefinitions(in);\n        return new Result(Collections.emptyList(), b.build());\n    }\n\n    /**\n     * Loads the project definition using the supplied path. Processes the configured\n     * imports, templates and extra directories with project files.\n     *\n     * @param workDir       the directory containing the project files. If {@code imports} are\n     *                      configured, the directory will be used as a target for repository\n     *                      checkouts.\n     */\n    public Result loadProject(Path workDir, ImportsNormalizer importsNormalizer, ImportsListener listener) throws Exception {\n        workDir = workDir.normalize().toAbsolutePath();\n\n        ProjectDefinition initial = initialLoad(workDir);\n        Resources resources = initial.getResources();\n\n        Imports imports = importsNormalizer.normalize(initial.getImports());\n        List<Snapshot> snapshots = importManager.process(imports, workDir, listener);\n\n        ProjectDefinitionBuilder b = new ProjectDefinitionBuilder(parser);\n\n        List<String> projectPaths = resources.getProjectFilePaths();\n        if (projectPaths != null) {\n            for (String n : projectPaths) {\n                Path p = assertLocal(workDir, workDir.resolve(n));\n                if (Files.exists(p)) {\n                    b.addProjects(p);\n                }\n            }\n        }\n\n        for (String n : PROJECT_ROOT_FILE_NAMES) {\n            Path p = assertLocal(workDir, workDir.resolve(n));\n            if (Files.exists(p)) {\n                b.addProjectFile(workDir, p);\n                break;\n            }\n        }\n\n        List<String> definitionPaths = resources.getDefinitionPaths();\n        if (definitionPaths != null) {\n            for (String n : definitionPaths) {\n                Path p = assertLocal(workDir, workDir.resolve(n));\n                if (Files.exists(p)) {\n                    b.addDefinitions(p);\n                }\n            }\n        }\n\n        List<String> profilesPaths = resources.getProfilesPaths();\n        if (profilesPaths != null) {\n            for (String n : profilesPaths) {\n                Path p = assertLocal(workDir, workDir.resolve(n));\n                if (Files.exists(p)) {\n                    b.addProfiles(p);\n                }\n            }\n        }\n\n        ProjectDefinition pd = b.build();\n\n        // save the normalized imports, so the exact same workDir structure\n        // can be re-created later (e.g. by the Agent)\n        pd = new ProjectDefinition(pd, imports);\n\n        return new Result(snapshots, pd);\n    }\n\n    /**\n     * Performs the initial load of the project files. Loads only the root concord.yml\n     * (if available), doesn't process imports, templates, etc.\n     */\n    private ProjectDefinition initialLoad(Path baseDir) throws IOException {\n        ProjectDefinitionBuilder b = new ProjectDefinitionBuilder(parser);\n\n        for (String n : PROJECT_ROOT_FILE_NAMES) {\n            Path p = baseDir.resolve(n);\n            if (Files.exists(p)) {\n                b.addProjectFile(baseDir, p);\n                break;\n            }\n        }\n\n        return b.build();\n    }\n\n    private static Path assertLocal(Path baseDir, Path p) throws IOException {\n        if (!p.normalize().toAbsolutePath().startsWith(baseDir)) {\n            throw new IOException(\"Invalid resource path, points outside of the base directory: \" + p);\n        }\n\n        return p;\n    }\n\n    private static class ProjectDefinitionBuilder {\n        private ValidatorContext validatorContext = new ValidatorContext();\n\n        private final YamlParser parser;\n\n        private Map<String, ProcessDefinition> flows;\n        private Set<String> publicFlows;\n        private Map<String, FormDefinition> forms;\n        private Map<String, Profile> profiles;\n        private List<ProjectDefinition> projectDefinitions;\n\n        private ProjectDefinitionBuilder(YamlParser parser) {\n            this.parser = parser;\n        }\n\n        public void addProjectFile(Path baseDir, Path file) throws IOException {\n            YamlProject yml = parser.parseProject(baseDir, file);\n            if (yml == null) {\n                throw new IOException(\"Empty project definition: \" + file);\n            }\n\n            Validator.validate(validatorContext, yml);\n\n            ProjectDefinition pd = YamlProjectConverter.convert(yml);\n\n            if (projectDefinitions == null) {\n                projectDefinitions = new ArrayList<>();\n            }\n            projectDefinitions.add(pd);\n\n        }\n\n        public void addDefinitions(Path path) throws IOException {\n            Files.walkFileTree(path, new SimpleFileVisitor<Path>() {\n                @Override\n                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {\n                    if (!isYaml(file)) {\n                        return FileVisitResult.CONTINUE;\n                    }\n\n                    loadDefinitions(path, file);\n                    return FileVisitResult.CONTINUE;\n                }\n            });\n        }\n\n        public void addProfiles(Path path) throws IOException {\n            Files.walkFileTree(path, new SimpleFileVisitor<Path>() {\n                @Override\n                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {\n                    if (!isYaml(file)) {\n                        return FileVisitResult.CONTINUE;\n                    }\n\n                    loadProfiles(path, file);\n                    return FileVisitResult.CONTINUE;\n                }\n            });\n        }\n\n        public void addProjects(Path path) throws IOException {\n            List<Path> files = new ArrayList<>();\n\n            Files.walkFileTree(path, new SimpleFileVisitor<Path>() {\n                @Override\n                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {\n                    if (isYaml(file)) {\n                        files.add(file);\n                    }\n                    return FileVisitResult.CONTINUE;\n                }\n            });\n\n            Collections.sort(files);\n\n            for (Path f : files) {\n                addProjectFile(path, f);\n            }\n        }\n\n        private void loadDefinitions(Path baseDir, Path file) throws IOException {\n            YamlDefinitionFile df = parser.parseDefinitionFile(baseDir, file);\n            loadDefinitions(df);\n        }\n\n        private void loadDefinitions(InputStream in) throws IOException {\n            YamlDefinitionFile df = parser.parseDefinitionFile(in);\n            loadDefinitions(df);\n        }\n\n        private void loadDefinitions(YamlDefinitionFile df) throws YamlConverterException {\n            if (df == null) {\n                return;\n            }\n\n            Map<String, YamlDefinition> m = df.getDefinitions();\n            if (m != null) {\n                for (Map.Entry<String, YamlDefinition> e : m.entrySet()) {\n                    String k = e.getKey();\n                    YamlDefinition v = e.getValue();\n\n                    if (v instanceof YamlProcessDefinition) {\n                        if (flows == null) {\n                            flows = new HashMap<>();\n                        }\n\n                        Validator.validate(validatorContext, (YamlProcessDefinition) v);\n\n                        flows.put(k, YamlProcessConverter.convert((YamlProcessDefinition) v));\n                    } else if (v instanceof YamlFormDefinition) {\n                        if (forms == null) {\n                            forms = new HashMap<>();\n                        }\n                        forms.put(k, YamlFormConverter.convert((YamlFormDefinition) v));\n                    }\n                }\n            }\n        }\n\n        private void loadProfiles(Path baseDir, Path file) throws IOException {\n            if (!isYaml(file)) {\n                return;\n            }\n\n            YamlProfileFile pf = parser.parseProfileFile(baseDir, file);\n            loadProfiles(pf);\n        }\n\n        private void loadProfiles(YamlProfileFile pf) throws YamlConverterException {\n            if (pf == null) {\n                return;\n            }\n\n            Map<String, YamlProfile> m = pf.getProfiles();\n\n            for (Map.Entry<String, YamlProfile> e : m.entrySet()) {\n                String k = e.getKey();\n                YamlProfile v = e.getValue();\n\n                Profile p = YamlProjectConverter.convert(v);\n                if (profiles == null) {\n                    profiles = new HashMap<>();\n                }\n                profiles.put(k, p);\n            }\n        }\n\n        public ProjectDefinition build() {\n            if (flows == null) {\n                flows = new HashMap<>();\n            }\n\n            if (publicFlows == null) {\n                publicFlows = new HashSet<>();\n            }\n\n            if (forms == null) {\n                forms = new HashMap<>();\n            }\n\n            if (profiles == null) {\n                profiles = new HashMap<>();\n            }\n\n            Map<String, Object> configuration = new LinkedHashMap<>();\n            List<String> dependencies = new ArrayList<>();\n            List<Trigger> triggers = new ArrayList<>();\n            Imports imports = Imports.builder().build();\n\n            // default resource paths\n            Resources resources = Resources.DEFAULT;\n\n            if (projectDefinitions != null) {\n                for (ProjectDefinition pd : projectDefinitions) {\n                    if (pd.getFlows() != null) {\n                        flows.putAll(pd.getFlows());\n                    }\n\n                    if (pd.getPublicFlows() != null) {\n                        publicFlows.addAll(pd.getPublicFlows());\n                    }\n\n                    if (pd.getForms() != null) {\n                        forms.putAll(pd.getForms());\n                    }\n\n                    if (pd.getConfiguration() != null) {\n                        Map<String, Object> cfg = pd.getConfiguration();\n\n                        List<String> deps = MapUtils.getList(cfg, Constants.Request.DEPENDENCIES_KEY, Collections.emptyList());\n                        dependencies.addAll(deps);\n\n                        configuration = ConfigurationUtils.deepMerge(configuration, cfg);\n                    }\n\n                    if (pd.getProfiles() != null) {\n                        for (Map.Entry<String, Profile> p : pd.getProfiles().entrySet()) {\n                            merge(profiles, p.getKey(), p.getValue());\n                        }\n                    }\n\n                    if (pd.getTriggers() != null) {\n                        triggers.addAll(pd.getTriggers());\n                    }\n\n                    if (pd.getImports() != null) {\n                        imports = Imports.builder()\n                                .addAllItems(imports.items())\n                                .addAllItems(pd.getImports().items())\n                                .build();\n                    }\n\n                    if (pd.getResources() != null) {\n                        // TODO avoid multiple conflicting resources definitions\n                        resources = pd.getResources();\n                    }\n                }\n            }\n\n            configuration.put(Constants.Request.DEPENDENCIES_KEY, dependencies);\n\n            return new ProjectDefinition(flows, publicFlows, forms, configuration, profiles, triggers, imports, resources);\n        }\n\n        private static boolean isYaml(Path p) {\n            String n = p.getFileName().toString().toLowerCase();\n            return n.endsWith(\".yml\") || n.endsWith(\".yaml\");\n        }\n\n        private static void merge(Map<String, Profile> profiles, String k, Profile b) {\n            Profile a = profiles.get(k);\n            if (a == null) {\n                profiles.put(k, b);\n                return;\n            }\n\n            Map<String, ProcessDefinition> flows = new HashMap<>();\n            if (a.getFlows() != null) {\n                flows.putAll(a.getFlows());\n            }\n            if (b.getFlows() != null) {\n                flows.putAll(b.getFlows());\n            }\n\n            Set<String> publicFlows = new HashSet<>();\n            if (a.getPublicFlows() != null) {\n                publicFlows.addAll(a.getPublicFlows());\n            }\n            if (b.getPublicFlows() != null) {\n                publicFlows.addAll(b.getPublicFlows());\n            }\n\n            Map<String, FormDefinition> forms = new HashMap<>();\n            if (a.getForms() != null) {\n                forms.putAll(a.getForms());\n            }\n            if (b.getForms() != null) {\n                forms.putAll(b.getForms());\n            }\n\n            Map<String, Object> cfg = new HashMap<>();\n            if (a.getConfiguration() != null) {\n                cfg.putAll(a.getConfiguration());\n            }\n            if (b.getConfiguration() != null) {\n                cfg = ConfigurationUtils.deepMerge(cfg, b.getConfiguration());\n            }\n\n            profiles.put(k, new Profile(flows, publicFlows, forms, cfg));\n        }\n    }\n\n    public static class Result {\n\n        private final List<Snapshot> snapshots;\n        private final ProjectDefinition projectDefinition;\n\n        public Result(List<Snapshot> snapshots, ProjectDefinition projectDefinition) {\n            this.snapshots = snapshots;\n            this.projectDefinition = projectDefinition;\n        }\n\n        public List<Snapshot> getSnapshots() {\n            return snapshots;\n        }\n\n        public ProjectDefinition getProjectDefinition() {\n            return projectDefinition;\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/model/Profile.java",
    "content": "package com.walmartlabs.concord.project.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.takari.bpm.model.ProcessDefinition;\nimport io.takari.bpm.model.form.FormDefinition;\n\nimport java.io.Serializable;\nimport java.util.Map;\nimport java.util.Set;\n\npublic class Profile implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Map<String, ProcessDefinition> flows;\n    private final Set<String> publicFlows;\n    private final Map<String, FormDefinition> forms;\n    private final Map<String, Object> configuration;\n\n    public Profile(Map<String, ProcessDefinition> flows,\n                   Set<String> publicFlows,\n                   Map<String, FormDefinition> forms,\n                   Map<String, Object> configuration) {\n\n        this.flows = flows;\n        this.publicFlows = publicFlows;\n        this.forms = forms;\n        this.configuration = configuration;\n    }\n\n    public Map<String, ProcessDefinition> getFlows() {\n        return flows;\n    }\n\n    public Set<String> getPublicFlows() {\n        return publicFlows;\n    }\n\n    public Map<String, FormDefinition> getForms() {\n        return forms;\n    }\n\n    public Map<String, Object> getConfiguration() {\n        return configuration;\n    }\n\n    @Override\n    public String toString() {\n        return \"Profile{\" +\n                \"flows=\" + flows +\n                \", publicFlows=\" + publicFlows +\n                \", forms=\" + forms +\n                \", variables=\" + configuration +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/model/ProjectDefinition.java",
    "content": "package com.walmartlabs.concord.project.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Imports;\nimport io.takari.bpm.model.ProcessDefinition;\nimport io.takari.bpm.model.form.FormDefinition;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\npublic class ProjectDefinition extends Profile {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Map<String, Profile> profiles;\n    private final List<Trigger> triggers;\n    private final Imports imports;\n    private final Resources resources;\n\n    public ProjectDefinition(Map<String, ProcessDefinition> flows,\n                             Set<String> publicFlows,\n                             Map<String, FormDefinition> forms,\n                             Map<String, Object> configuration,\n                             Map<String, Profile> profiles,\n                             List<Trigger> triggers,\n                             Imports imports,\n                             Resources resources) {\n\n        super(flows, publicFlows, forms, configuration);\n\n        this.profiles = profiles;\n        this.triggers = triggers;\n        this.imports = imports;\n        this.resources = resources;\n    }\n\n    public ProjectDefinition(ProjectDefinition src, Imports imports) {\n        this(\n                src.getFlows(),\n                src.getPublicFlows(),\n                src.getForms(),\n                src.getConfiguration(),\n                src.getProfiles(),\n                src.getTriggers(),\n                imports,\n                src.getResources()\n        );\n    }\n\n    public Map<String, Profile> getProfiles() {\n        return profiles;\n    }\n\n    public List<Trigger> getTriggers() {\n        return triggers;\n    }\n\n    public Imports getImports() {\n        return imports;\n    }\n\n    public Resources getResources() {\n        return resources;\n    }\n\n    @Override\n    public String toString() {\n        return \"ProjectDefinition{\" +\n                \"profiles=\" + profiles +\n                \", triggers=\" + triggers +\n                \", imports=\" + imports +\n                \", resources=\" + resources +\n                \"} \" + super.toString();\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/model/ProjectDefinitionUtils.java",
    "content": "package com.walmartlabs.concord.project.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.takari.bpm.model.ProcessDefinition;\nimport io.takari.bpm.model.form.FormDefinition;\n\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic final class ProjectDefinitionUtils {\n\n    public static ProcessDefinition getFlow(ProjectDefinition project, Collection<String> activeProfiles, String flowName) {\n        Map<String, ProcessDefinition> flows = project.getFlows();\n        Map<String, Profile> profiles = project.getProfiles();\n\n        Map<String, ProcessDefinition> view = overlay(flows, profiles, activeProfiles, Profile::getFlows);\n        return view.get(flowName);\n    }\n\n    public static FormDefinition getForm(ProjectDefinition project, Collection<String> activeProfiles, String formName) {\n        Map<String, FormDefinition> forms = project.getForms();\n        Map<String, Profile> profiles = project.getProfiles();\n\n        Map<String, FormDefinition> view = overlay(forms, profiles, activeProfiles, Profile::getForms);\n        return view.get(formName);\n    }\n\n    private static <T> Map<String, T> overlay(Map<String, T> initial,\n                                              Map<String, Profile> profiles,\n                                              Collection<String> activeProfiles,\n                                              Function<Profile, Map<String, T>> selector) {\n        return overlay(initial, profiles, activeProfiles, selector, (a, b) -> {\n            a.putAll(b);\n            return a;\n        });\n    }\n\n    private static <T> Map<String, T> overlay(Map<String, T> initial,\n                                              Map<String, Profile> profiles,\n                                              Collection<String> activeProfiles,\n                                              Function<Profile, Map<String, T>> selector,\n                                              BiFunction<Map<String, T>, Map<String, T>, Map<String, T>> mergeFn) {\n\n        Map<String, T> view = new LinkedHashMap<>(initial != null ? initial : Collections.emptyMap());\n        if (profiles == null || activeProfiles == null) {\n            return view;\n        }\n\n        for (String n : activeProfiles) {\n            Profile p = profiles.get(n);\n            if (p == null) {\n                continue;\n            }\n\n            Map<String, T> overlays = selector.apply(p);\n            if (overlays != null) {\n                view = mergeFn.apply(view, overlays);\n            }\n        }\n\n        return view;\n    }\n\n    private ProjectDefinitionUtils() {\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/model/Resources.java",
    "content": "package com.walmartlabs.concord.project.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\nimport static com.walmartlabs.concord.sdk.Constants.Files.*;\n\npublic class Resources implements Serializable {\n\n    public static final Resources DEFAULT = new Resources(Collections.singletonList(PROFILES_DIR_NAME),\n            Collections.singletonList(PROJECT_FILES_DIR_NAME),\n            Arrays.asList(DEFINITIONS_DIR_NAMES),\n            null);\n\n    private static final long serialVersionUID = 1L;\n\n    private final List<String> profilesPaths;\n    private final List<String> projectFilePaths;\n    private final List<String> definitionPaths;\n    private final List<String> disabledDirs;\n\n    public Resources(List<String> profilesPaths, List<String> projectFilePaths, List<String> definitionPaths, List<String> disabledDirs) {\n        this.profilesPaths = profilesPaths;\n        this.projectFilePaths = projectFilePaths;\n        this.definitionPaths = definitionPaths;\n        this.disabledDirs = disabledDirs;\n    }\n\n    public List<String> getProfilesPaths() {\n        return profilesPaths;\n    }\n\n    public List<String> getProjectFilePaths() {\n        return projectFilePaths;\n    }\n\n    public List<String> getDefinitionPaths() {\n        return definitionPaths;\n    }\n\n    public List<String> getDisabledDirs() {\n        return disabledDirs;\n    }\n\n    @Override\n    public String toString() {\n        return \"Resources{\" +\n                \"profilesPaths=\" + profilesPaths +\n                \", projectFilePaths=\" + projectFilePaths +\n                \", definitionPaths=\" + definitionPaths +\n                \", disabledDirs=\" + disabledDirs +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/model/Trigger.java",
    "content": "package com.walmartlabs.concord.project.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.walmartlabs.concord.sdk.Constants;\nimport io.takari.bpm.model.SourceMap;\n\nimport java.io.Serializable;\nimport java.util.List;\nimport java.util.Map;\n\npublic class Trigger implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String name;\n    private final List<String> activeProfiles;\n    private final Map<String, Object> arguments;\n    private final Map<String, Object> conditions;\n    private final SourceMap sourceMap;\n    private final Map<String, Object> cfg;\n\n    public Trigger(String name, List<String> activeProfiles, Map<String, Object> arguments, Map<String, Object> conditions, Map<String, Object> cfg, SourceMap sourceMap) {\n        this.name = name;\n        this.activeProfiles = activeProfiles;\n        this.arguments = arguments;\n        this.conditions = conditions;\n        this.sourceMap = sourceMap;\n        this.cfg = cfg;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public Map<String, Object> getConditions() {\n        return conditions;\n    }\n\n    public Map<String, Object> getCfg() {\n        return cfg;\n    }\n\n    @JsonIgnore\n    public String getEntryPoint() {\n        if (cfg == null) {\n            return null;\n        }\n\n        return (String) cfg.get(Constants.Request.ENTRY_POINT_KEY);\n    }\n\n    public List<String> getActiveProfiles() {\n        return activeProfiles;\n    }\n\n    public Map<String, Object> getArguments() {\n        return arguments;\n    }\n\n    public SourceMap getSourceMap() {\n        return sourceMap;\n    }\n\n    @Override\n    public String toString() {\n        return \"Trigger{\" +\n                \"name='\" + name + '\\'' +\n                \", activeProfiles=\" + activeProfiles +\n                \", arguments=\" + arguments +\n                \", conditions=\" + conditions +\n                \", sourceMap=\" + sourceMap +\n                \", cfg=\" + cfg +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/Atom.java",
    "content": "package com.walmartlabs.concord.project.yaml;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.core.JsonToken;\n\nimport java.io.IOException;\nimport java.io.Serializable;\n\npublic class Atom implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    public static Atom current(JsonParser p) throws IOException {\n        if (p.currentToken() == null) {\n            return null;\n        }\n\n        Object value;\n        switch (p.currentToken()) {\n            case VALUE_STRING:\n                value = p.getValueAsString();\n                break;\n            case VALUE_NUMBER_INT:\n                value = p.getValueAsInt();\n                break;\n            case VALUE_TRUE:\n                value = true;\n                break;\n            case VALUE_FALSE:\n                value = false;\n                break;\n            default:\n                value = null;\n        }\n        return new Atom(p.getTokenLocation(), p.currentToken(), p.getCurrentName(), value);\n    }\n\n    public final JsonLocation location;\n    public final JsonToken token;\n    public final String name;\n    public final Object value;\n\n    public Atom(JsonLocation location, JsonToken token, String name, Object value) {\n        this.location = location;\n        this.token = token;\n        this.name = name;\n        this.value = value;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        Atom atom = (Atom) o;\n\n        if (!location.equals(atom.location)) return false;\n        if (token != atom.token) return false;\n        if (name != null ? !name.equals(atom.name) : atom.name != null) return false;\n        return value != null ? value.equals(atom.value) : atom.value == null;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = location.hashCode();\n        result = 31 * result + token.hashCode();\n        result = 31 * result + (name != null ? name.hashCode() : 0);\n        result = 31 * result + (value != null ? value.hashCode() : 0);\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"Atom{\" +\n                \"location=\" + location +\n                \", token=\" + token +\n                \", name='\" + name + '\\'' +\n                \", value=\" + value +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/Grammar.java",
    "content": "package com.walmartlabs.concord.project.yaml;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.walmartlabs.concord.project.yaml.model.*;\nimport io.takari.parc.Parser;\nimport io.takari.parc.Seq;\n\nimport java.util.Collections;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport static io.takari.parc.Combinators.*;\n\npublic class Grammar {\n\n    // Misc functions\n\n    private static Parser<Atom, Atom> satisfyToken(JsonToken t) {\n        return satisfy((Atom a) -> a.token == t);\n    }\n\n    private static Parser<Atom, Atom> satisfyField(String name) {\n        return label(\"Field '\" + name + \"'\", satisfy((Atom a) -> name.equals(a.name)));\n    }\n\n    private static <O> Parser<Atom, O> betweenTokens(JsonToken start, JsonToken end, Parser<Atom, O> p) {\n        return between(satisfyToken(start), satisfyToken(end), p);\n    }\n\n    private static Object toValue(KV<String, Object> kv) {\n        Object v = kv.getValue();\n        if (v == null && kv.getKey() != null) {\n            return null;\n        }\n\n        return v;\n    }\n\n    private static Map<String, Object> toMap(Seq<KV<String, Object>> values) {\n        if (values == null) {\n            return Collections.emptyMap();\n        }\n\n        Map<String, Object> m = new LinkedHashMap<>();\n        values.stream().forEach(kv -> m.put(kv.getKey(), Grammar.toValue(kv)));\n        return m;\n    }\n\n    // Grammar rules\n\n    // declare forward references for recursive types\n    private static final Parser.Ref<Atom, Object> value = Parser.ref();\n    private static final Parser.Ref<Atom, Seq<Object>> arrayOfValues = Parser.ref();\n    private static final Parser.Ref<Atom, Map<String, Object>> object = Parser.ref();\n    private static final Parser.Ref<Atom, Seq<YamlStep>> steps = Parser.ref();\n\n    // expression := VALUE_STRING ${.*}\n    private static final Parser<Atom, Atom> expression = label(\"Expression\", satisfy((Atom a) -> {\n        if (a.token != JsonToken.VALUE_STRING) {\n            return false;\n        }\n\n        String s = (String) a.value;\n        return s != null && s.startsWith(\"${\") && s.endsWith(\"}\");\n    }));\n\n    // value := VALUE_STRING | VALUE_NUMBER_INT | VALUE_NUMBER_FLOAT | VALUE_TRUE | VALUE_FALSE | VALUE_NULL | arrayOfValues | object\n    private static Parser<Atom, Object> _val(JsonToken t) {\n        return satisfyToken(t).map(a -> a.value);\n    }\n\n    static {\n        value.set(choice(\n                choice(\n                        _val(JsonToken.VALUE_STRING),\n                        _val(JsonToken.VALUE_NUMBER_INT),\n                        _val(JsonToken.VALUE_NUMBER_FLOAT),\n                        _val(JsonToken.VALUE_TRUE),\n                        _val(JsonToken.VALUE_FALSE),\n                        _val(JsonToken.VALUE_NULL)\n                ),\n                arrayOfValues,\n                object\n        ));\n    }\n\n    // arrayOfValues := START_ARRAY value* END_ARRAY\n    static {\n        arrayOfValues.set(label(\"Array of values\",\n                betweenTokens(JsonToken.START_ARRAY, JsonToken.END_ARRAY,\n                        many(value))));\n    }\n\n    // object := START_OBJECT (FIELD_NAME value)* END_OBJECT\n    static {\n        object.set(label(\"Object\",\n                betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                        many(satisfyToken(JsonToken.FIELD_NAME).bind(a ->\n                                value.map(v -> new KV<>(a.name, v)))))\n                        .map(Grammar::toMap)));\n    }\n\n    // identifier := VALUE_STRING\n    // TODO validate identifiers\n    private static final Parser<Atom, Atom> identifier = label(\"Identifier\",\n            satisfyToken(JsonToken.VALUE_STRING));\n\n    // formName := VALUE_STRING ^form \\((.*)\\)$\n    private static final Pattern FORM_NAME_PATTERN = Pattern.compile(\"^form\\\\s?\\\\((.*)\\\\)$\");\n    private static final Parser<Atom, String> formName = label(\"Form name\",\n            satisfy((Atom a) -> {\n                if (a.token != JsonToken.FIELD_NAME) {\n                    return false;\n                }\n\n                String s = a.name;\n                Matcher m = FORM_NAME_PATTERN.matcher(s);\n                return m.matches();\n            }).map(a -> {\n                String s = a.name;\n                Matcher m = FORM_NAME_PATTERN.matcher(s);\n                m.matches();\n                return m.group(1);\n            }));\n\n    // outField := FIELD_NAME \"out\" identifier\n    private static final Parser<Atom, KV<String, Object>> outField = label(\"Out variable\",\n            satisfyField(\"out\").then(identifier)\n                    .map(a -> new KV<>(\"out\", a.value)));\n\n    // kv := FIELD_NAME value\n    private static final Parser<Atom, KV<String, Object>> kv = label(\"Key-value pair\",\n            satisfyToken(JsonToken.FIELD_NAME).bind(a ->\n                    value.map(v -> new KV<>(a.name, v))));\n\n    // errorRetryBlock := FIELD_NAME \"retry\" START_OBJECT (kv)+ END_OBJECT\n    private static final Parser<Atom, KV<String, Object>> retryBlock = label(\"Retry handling block\",\n            satisfyField(\"retry\").then(\n                    betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                            many1(kv))).map(v -> new KV<>(\"retry\", v)));\n\n    // errorBlock := FIELD_NAME \"error\" steps\n    private static final Parser<Atom, KV<String, Object>> errorBlock = label(\"Error handling block\",\n            satisfyField(\"error\").then(steps)\n                    .map(v -> new KV<>(\"error\", v)));\n\n    // inVars := FIELD_NAME \"in\" START_OBJECT (kv)+ END_OBJECT\n    private static final Parser<Atom, KV<String, Object>> inVars = label(\"IN variables\",\n            satisfyField(\"in\").then(\n                    betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                            many1(kv))).map(v -> new KV<>(\"in\", v)));\n\n    // outVars := FIELD_NAME \"out\" START_OBJECT (kv)+ END_OBJECT\n    private static final Parser<Atom, KV<String, Object>> outVars = label(\"OUT variables\",\n            satisfyField(\"out\").then(\n                    betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                            many1(kv))).map(v -> new KV<>(\"out\", v)));\n\n    private static final Parser<Atom, KV<String, Object>> withItems = label(\"With Items\",\n            satisfyField(\"withItems\").then(\n                    betweenTokens(JsonToken.START_ARRAY, JsonToken.END_ARRAY,\n                            many1(value))).map(v -> new KV<>(\"withItems\", v)));\n\n    private static final Parser<Atom, KV<String, Object>> withItemsShort = label(\"With Items (short form)\",\n            satisfyField(\"withItems\").then(\n                    satisfyToken(JsonToken.VALUE_STRING)\n                            .map(v -> new KV<>(\"withItems\", v.value))));\n\n    // body := FIELD_NAME \"body\" VALUE_STRING\n    private static final Parser<Atom, KV<String, Object>> body = label(\"script body\",\n            satisfyField(\"body\").then(satisfyToken(JsonToken.VALUE_STRING))\n                    .map(v -> new KV<>(\"body\", v.value)));\n\n    // scriptOptions := (body | errorBlock)*\n    private static final Parser<Atom, Map<String, Object>> scriptOptions = label(\"Script options\",\n            many(choice(body, errorBlock)).map(Grammar::toMap));\n\n    // exprOptions := (outField | errorBlock)*\n    private static final Parser<Atom, Map<String, Object>> exprOptions = label(\"Expression options\",\n            many(choice(errorBlock, outField)).map(Grammar::toMap));\n\n    // taskOptions := (inVars | outVars | outField | errorBlock)*\n    private static final Parser<Atom, Map<String, Object>> taskOptions = label(\"Task options\",\n            many(choice(inVars, outVars, errorBlock, outField, retryBlock, withItems, withItemsShort)).map(Grammar::toMap));\n\n    // callOptions := (inVars | outVars | errorBlock)*\n    private static final Parser<Atom, Map<String, Object>> callOptions = label(\"Process call options\",\n            many(choice(inVars, outVars, errorBlock, withItems, withItemsShort, retryBlock)).map(Grammar::toMap));\n\n    // groupOptions := (errorBlock)*\n    private static final Parser<Atom, Map<String, Object>> groupOptions = label(\"Group options\",\n            many(errorBlock).map(Grammar::toMap));\n\n    // formCallOptions := (inVars | errorBlock)*\n    private static final Parser<Atom, Map<String, Object>> formCallOptions = label(\"Form call options\",\n            many(kv).map(Grammar::toMap));\n\n    // exprShort := expression\n    private static final Parser<Atom, YamlStep> exprShort = label(\"Expression (short form)\",\n            expression.map(a -> new YamlExpressionStep(a.location, (String) a.value)));\n\n    // exprFull := FIELD_NAME \"expr\" expression exprOptions\n    private static final Parser<Atom, YamlStep> exprFull = label(\"Expression (full form)\",\n            satisfyField(\"expr\").then(expression).bind(a ->\n                    exprOptions.map(options -> new YamlExpressionStep(a.location, (String) a.value, options))));\n\n    // taskFull := FIELD_NAME \"task\" VALUE_STRING taskOptions\n    private static final Parser<Atom, YamlStep> taskFull = label(\"Task (full form)\",\n            satisfyField(\"task\").then(satisfyToken(JsonToken.VALUE_STRING)).bind(a ->\n                    taskOptions.map(options -> new YamlTaskStep(a.location, (String) a.value, options))));\n\n    // taskShort := FIELD_NAME literal\n    private static final Parser<Atom, YamlStep> taskShort = label(\"Task (short form)\",\n            satisfyToken(JsonToken.FIELD_NAME).bind(a ->\n                    value.map(arg -> new YamlTaskShortStep(a.location, a.name, arg))));\n\n    // ifExpr := FIELD_NAME \"if\" expression FIELD_NAME \"then\" steps (FIELD_NAME \"else\" steps)?\n    private static final Parser<Atom, YamlStep> ifExpr = label(\"IF expression\",\n            satisfyField(\"if\").then(expression).bind(a ->\n                    satisfyField(\"then\").then(steps).bind(thenSteps ->\n                            option(satisfyField(\"else\").then(steps)\n                                            .map(elseSteps -> new YamlIfExpr(a.location, (String) a.value, thenSteps, elseSteps)),\n                                    new YamlIfExpr(a.location, (String) a.value, thenSteps, Seq.empty())))));\n\n    // switchExpr := FIELD_NAME \"switch\" expression (FIELD_NAME steps)* (FIELD_NAME \"default\" steps)?\n    private static final Parser<Atom, Seq<KV<String, Seq<YamlStep>>>> switchOptions = label(\"SWITCH options\",\n            many1(satisfyToken(JsonToken.FIELD_NAME).bind(a -> steps.map(v -> new KV<>(a.name, v)))));\n\n    private static final Parser<Atom, YamlStep> switchExpr = label(\"SWITCH expression\",\n            satisfyField(\"switch\").then(expression).bind(a ->\n                    switchOptions.map(options ->\n                            new YamlSwitchExpr(a.location, (String) a.value, options))));\n\n    // returnExpr := VALUE_STRING \"return\"\n    private static final Parser<Atom, YamlStep> returnExpr = label(\"Return keyword\",\n            satisfy((Atom a) -> {\n                if (a.token != JsonToken.VALUE_STRING) {\n                    return false;\n                }\n\n                String s = (String) a.value;\n                return \"return\".equals(s);\n            }).map(a -> new YamlReturn(a.location)));\n\n    // errorReturn := FIELD_NAME \"return\" VALUE_STRING\n    private static final Parser<Atom, YamlStep> errorReturn = label(\"Error end event\",\n            satisfyField(\"return\").then(satisfyToken(JsonToken.VALUE_STRING))\n                    .map(a -> new YamlReturn(a.location, (String) a.value)));\n\n    // group := FIELD_NAME \":\" steps groupOptions\n    private static final Parser<Atom, YamlStep> group = label(\"Group of steps\",\n            choice(satisfyField(\":\"), satisfyField(\"try\")).bind(a -> steps.bind(items ->\n                    groupOptions.map(options -> new YamlGroup(a.location, items, options)))));\n\n    // callFull := FIELD_NAME \"call\" VALUE_STRING callOptions\n    private static final Parser<Atom, YamlStep> callFull = label(\"Process call (full form)\",\n            satisfyField(\"call\").then(satisfyToken(JsonToken.VALUE_STRING)).bind(a ->\n                    callOptions.map(options -> new YamlCall(a.location, (String) a.value, options))));\n\n    // callProc := VALUE_STRING\n    private static final Parser<Atom, YamlStep> callProc = label(\"Process call\",\n            satisfyToken(JsonToken.VALUE_STRING).map(a -> new YamlCall(a.location, (String) a.value, null)));\n\n    // checkPoint := VALUE_STRING\n    private static final Parser<Atom, YamlStep> checkpoint = label(\"Checkpoint\",\n            satisfyField(\"checkpoint\").then(satisfyToken(JsonToken.VALUE_STRING))\n                    .map(a -> new YamlCheckpoint(a.location, (String) a.value)));\n\n    // exit := VALUE_STRING\n    private static final Parser<Atom, YamlStep> exit = label(\"Exit call\",\n            satisfy((Atom a) -> a.token == JsonToken.VALUE_STRING && \"exit\".equals(a.value))\n                .map(a -> new YamlExit(a.location)));\n\n    // event := FIELD_NAME \"event\" VALUE_STRING\n    private static final Parser<Atom, YamlStep> event = label(\"Event (debug only)\",\n            satisfyField(\"event\").then(satisfyToken(JsonToken.VALUE_STRING))\n                    .map(a -> new YamlEvent(a.location, (String) a.value)));\n\n    // script := FIELD_NAME \"script\" VALUE_STRING (FIELD_NAME \"body\" VALUE_STRING)?\n    private static final Parser<Atom, YamlStep> script = label(\"Script\",\n            satisfyField(\"script\").then(satisfyToken(JsonToken.VALUE_STRING))\n                    .bind(a -> scriptOptions.map(opts -> new YamlScript(a.location, (String) a.value, opts))));\n\n    // formCall := FIELD_NAME \"form\" VALUE_STRING formCallOptions\n    private static final Parser<Atom, YamlStep> formCall = label(\"Form call\",\n            satisfyField(\"form\").then(satisfyToken(JsonToken.VALUE_STRING)).bind(a ->\n                    formCallOptions.map(options -> new YamlFormCall(a.location, (String) a.value, options))));\n\n    // inVars := FIELD_NAME \"set\" START_OBJECT (kv)+ END_OBJECT\n    private static final Parser<Atom, YamlStep> vars = label(\"Variables\",\n            satisfyField(\"set\")\n                    .bind(a ->\n                            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT, many1(kv))\n                                    .map(Grammar::toMap)\n                                    .map(v -> new YamlSetVariablesStep(a.location, v))));\n\n    private static final Parser<Atom, Map<String, Object>> dockerOptions = label(\"Docker call options\",\n            many(kv).map(Grammar::toMap));\n\n    // docker := FIELD_NAME \"docker\" VALUE_STRING dockerOptions\n    @SuppressWarnings(\"unchecked\")\n    private static final Parser<Atom, YamlStep> docker = label(\"Docker call\",\n            satisfyField(\"docker\").then(satisfyToken(JsonToken.VALUE_STRING)).bind(a ->\n                    dockerOptions.map(options ->\n                            new YamlDockerStep(a.location, (String) a.value,\n                                    (String) options.get(\"cmd\"),\n                                    (boolean) options.getOrDefault(\"forcePull\", true),\n                                    (boolean) options.getOrDefault(\"debug\", false),\n                                    (Map<String, Object>) options.get(\"env\"),\n                                    (String) options.get(\"envFile\"),\n                                    toListOfStrings(a.location, options.get(\"hosts\")),\n                                    (String) options.get(\"stdout\"),\n                                    (String) options.get(\"stderr\")))));\n\n    // stepObject := START_OBJECT docker | group | ifExpr | exprFull | formCall | vars | taskFull | callFull | checkpoint | event | script | taskShort | vars END_OBJECT\n    private static final Parser<Atom, YamlStep> stepObject = label(\"Process definition step (complex)\",\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    choice(choice(docker, group, switchExpr, ifExpr, exprFull, formCall, vars), choice(taskFull, callFull, checkpoint, event, errorReturn, script), taskShort)));\n\n    // step := returnExpr | exprShort | callProc | stepObject\n    private static final Parser<Atom, YamlStep> step = choice(returnExpr, exprShort, exit, callProc, stepObject);\n\n    // steps := START_ARRAY step+ END_ARRAY\n    static {\n        steps.set(label(\"Process definition step(s)\",\n                betweenTokens(JsonToken.START_ARRAY, JsonToken.END_ARRAY,\n                        many1(step))));\n    }\n\n    // formField := START_OBJECT FIELD_NAME object END_OBJECT\n    private static final Parser<Atom, YamlFormField> formField = label(\"Form field\",\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    satisfyToken(JsonToken.FIELD_NAME).bind(a ->\n                            object.map(options -> new YamlFormField(a.location, a.name, options)))));\n\n    // formFields := START_ARRAY formField+ END_ARRAY\n    private static final Parser<Atom, Seq<YamlFormField>> formFields = label(\"Form fields\",\n            betweenTokens(JsonToken.START_ARRAY, JsonToken.END_ARRAY,\n                    many1(formField)));\n\n    // procDef := FIELD_NAME steps\n    private static final Parser<Atom, YamlProcessDefinition> procDef = label(\"Process definition\",\n            satisfyToken(JsonToken.FIELD_NAME).bind(a ->\n                    steps.map(items -> new YamlProcessDefinition(a.name, items))));\n\n    // formDef := formName formFields\n    private static final Parser<Atom, YamlFormDefinition> formDef = label(\"Form definition\",\n            formName.bind(name ->\n                    formFields.map(fields -> new YamlFormDefinition(name, fields))));\n\n    // triggerDef := START_OBJECT FIELD_NAME object END_OBJECT\n    private static final Parser<Atom, YamlTrigger> triggerDef = label(\"Trigger\",\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    satisfyToken(JsonToken.FIELD_NAME).bind(a ->\n                            object.map(options -> new YamlTrigger(a.location, a.name, options)))));\n\n    // defs := START_OBJECT (formDef | procDef)+ END_OBJECT\n    private static final Parser<Atom, Seq<YamlDefinition>> defs = label(\"Process and form definitions\",\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    many1(choice(formDef, procDef))));\n\n    // imports := START_OBJECT FIELD_NAME object END_OBJECT\n    private static final Parser<Atom, YamlImport> importField = label(\"Import\",\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    satisfyToken(JsonToken.FIELD_NAME).bind(a ->\n                            object.map(options -> new YamlImport(a.location, a.name, options)))));\n\n    @SuppressWarnings(\"unchecked\")\n    private static List<String> toListOfStrings(JsonLocation loc, Object v) {\n        if (v == null) {\n            return null;\n        }\n\n        if (v instanceof List) {\n            return (List<String>) v;\n        }\n\n        if (v instanceof Seq) {\n            Seq<String> s = (Seq<String>) v;\n            return s.toList();\n        }\n\n        throw new IllegalArgumentException(\"@ \" + loc + \": expected a list of strings, got \" + v);\n    }\n\n    public static Parser<Atom, YamlStep> getProcessStep() {\n        return step;\n    }\n\n    public static Parser<Atom, YamlFormField> getFormField() {\n        return formField;\n    }\n\n    public static Parser<Atom, Seq<YamlDefinition>> getDefinitions() {\n        return defs;\n    }\n\n    public static Parser<Atom, YamlTrigger> getTrigger() {\n        return triggerDef;\n    }\n\n    public static Parser<Atom, YamlImport> getImport() {\n        return importField;\n    }\n\n    private Grammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/KV.java",
    "content": "package com.walmartlabs.concord.project.yaml;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Map;\n\npublic class KV<K, V> implements Map.Entry<K, V> {\n\n    private final K k;\n    private final V v;\n\n    public KV(K k, V v) {\n        this.k = k;\n        this.v = v;\n    }\n\n    @Override\n    public K getKey() {\n        return k;\n    }\n\n    @Override\n    public V getValue() {\n        return v;\n    }\n\n    @Override\n    public V setValue(V value) {\n        throw new IllegalStateException(\"Not allowed\");\n    }\n\n    @Override\n    public String toString() {\n        return \"KV{\" +\n                \"k=\" + k +\n                \", v=\" + v +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/ListInput.java",
    "content": "package com.walmartlabs.concord.project.yaml;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.takari.parc.Input;\n\nimport java.util.List;\n\npublic class ListInput<T> implements Input<T> {\n\n    private final int pos;\n    private final List<T> items;\n\n    public ListInput(List<T> items) {\n        this(0, items);\n    }\n\n    private ListInput(int pos, List<T> items) {\n        this.pos = pos;\n        this.items = items;\n    }\n\n    @Override\n    public int position() {\n        return pos;\n    }\n\n    @Override\n    public T first() {\n        return items.get(pos);\n    }\n\n    @Override\n    public Input<T> rest() {\n        return new ListInput<>(pos + 1, items);\n    }\n\n    @Override\n    public boolean end() {\n        return pos >= items.size();\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/YamlConverterException.java",
    "content": "package com.walmartlabs.concord.project.yaml;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\n\npublic class YamlConverterException extends JsonProcessingException {\n\n    private static final long serialVersionUID = 1L;\n\n    public YamlConverterException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/YamlDeserializers.java",
    "content": "package com.walmartlabs.concord.project.yaml;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\nimport com.fasterxml.jackson.core.JsonParseException;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.JsonDeserializer;\nimport com.fasterxml.jackson.databind.deser.std.StdDeserializer;\nimport com.walmartlabs.concord.project.yaml.model.*;\nimport io.takari.parc.Input;\nimport io.takari.parc.Parser;\nimport io.takari.parc.Result;\nimport io.takari.parc.Result.Failure;\nimport io.takari.parc.Seq;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic final class YamlDeserializers {\n\n    private static final JsonDeserializer<YamlStep> yamlStepDeserializer = new YamlStepDeserializer();\n    private static final JsonDeserializer<YamlFormField> yamlFormFieldDeserializer = new YamlFormFieldDeserializer();\n    private static final JsonDeserializer<YamlDefinitionFile> yamlDefinitionFileDeserializer = new YamlDefinitionFileDeserializer();\n    private static final JsonDeserializer<YamlTrigger> yamlTriggerDeserializer = new YamlTriggerDeserializer();\n    private static final JsonDeserializer<YamlImport> yamlImportDeserializer = new YamlImportDeserializer();\n\n    public static JsonDeserializer<YamlStep> getYamlStepDeserializer() {\n        return yamlStepDeserializer;\n    }\n\n    public static JsonDeserializer<YamlFormField> getYamlFormFieldDeserializer() {\n        return yamlFormFieldDeserializer;\n    }\n\n    public static JsonDeserializer<YamlDefinitionFile> getYamlDefinitionFileDeserializer() {\n        return yamlDefinitionFileDeserializer;\n    }\n\n    public static JsonDeserializer<YamlTrigger> getYamlTriggerDeserializer() {\n        return yamlTriggerDeserializer;\n    }\n\n    public static JsonDeserializer<YamlImport> getYamlImportDeserializer() {\n        return yamlImportDeserializer;\n    }\n\n    private static final class YamlStepDeserializer extends StdDeserializer<YamlStep> {\n\n        private static final long serialVersionUID = 1L;\n\n        protected YamlStepDeserializer() {\n            super(YamlStep.class);\n        }\n\n        @Override\n        public YamlStep deserialize(JsonParser json, DeserializationContext ctx) throws IOException {\n            return parse(json, Grammar.getProcessStep());\n        }\n    }\n\n    private static final class YamlFormFieldDeserializer extends StdDeserializer<YamlFormField> {\n\n        private static final long serialVersionUID = 1L;\n\n        protected YamlFormFieldDeserializer() {\n            super(YamlFormField.class);\n        }\n\n        @Override\n        public YamlFormField deserialize(JsonParser json, DeserializationContext ctx) throws IOException {\n            return parse(json, Grammar.getFormField());\n        }\n    }\n\n    private static final class YamlDefinitionFileDeserializer extends StdDeserializer<YamlDefinitionFile> {\n\n        private static final long serialVersionUID = 1L;\n\n        protected YamlDefinitionFileDeserializer() {\n            super(YamlDefinitionFile.class);\n        }\n\n        @Override\n        public YamlDefinitionFile deserialize(JsonParser json, DeserializationContext ctx) throws IOException {\n            Seq<YamlDefinition> defs = parse(json, Grammar.getDefinitions());\n\n            List<YamlDefinition> l = defs.toList();\n            Map<String, YamlDefinition> m = new HashMap<>(l.size());\n            for (YamlDefinition d : l) {\n                m.put(d.getName(), d);\n            }\n\n            return new YamlDefinitionFile(m);\n        }\n    }\n\n    private static final class YamlTriggerDeserializer extends StdDeserializer<YamlTrigger> {\n\n        private static final long serialVersionUID = 1L;\n\n        public YamlTriggerDeserializer() {\n            super(YamlTrigger.class);\n        }\n\n        @Override\n        public YamlTrigger deserialize(JsonParser json, DeserializationContext ctx) throws IOException {\n            return parse(json, Grammar.getTrigger());\n        }\n    }\n\n    private static final class YamlImportDeserializer extends StdDeserializer<YamlImport> {\n\n        private static final long serialVersionUID = 1L;\n\n        public YamlImportDeserializer() {\n            super(YamlImport.class);\n        }\n\n        @Override\n        public YamlImport deserialize(JsonParser json, DeserializationContext ctx) throws IOException {\n            return parse(json, Grammar.getImport());\n        }\n    }\n\n    private static <T> T parse(JsonParser json, Parser<Atom, T> parser) throws IOException {\n        List<Atom> atoms = asSubtree(json);\n\n        Input<Atom> in = new ListInput<>(atoms);\n        Result<Atom, T> result = parser.parse(in);\n\n        if (result.isFailure()) {\n            Failure<Atom, ?> f = result.toFailure();\n            throw toException(f, json, atoms);\n        }\n\n        return result.toSuccess().getResult();\n    }\n\n    private static List<Atom> asSubtree(JsonParser p) throws IOException {\n        int level = 0;\n\n        List<Atom> l = new ArrayList<>();\n        while (p.currentToken() != null) {\n            l.add(Atom.current(p));\n\n            switch (p.currentToken()) {\n                case START_OBJECT:\n                case START_ARRAY:\n                    level += 1;\n                    break;\n                case END_OBJECT:\n                case END_ARRAY:\n                    level -= 1;\n                    break;\n            }\n\n            if (level <= 0) {\n                break;\n            }\n\n            p.nextToken();\n        }\n\n        return l;\n    }\n\n    private static JsonParseException toException(Failure<Atom, ?> f, JsonParser p, List<Atom> atoms) {\n        JsonLocation loc = null;\n\n        int pos = f.getPosition();\n        if (pos >= 0) {\n            Atom a = atoms.get(f.getPosition());\n            loc = a.location;\n        }\n\n        return new JsonParseException(p, \"Expected: \" + f.getMessage() + \". Got \" + atoms, loc);\n    }\n\n    private YamlDeserializers() {\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/YamlFormConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\nimport com.walmartlabs.concord.common.form.ConcordFormFields;\nimport com.walmartlabs.concord.project.yaml.model.YamlFormDefinition;\nimport com.walmartlabs.concord.project.yaml.model.YamlFormField;\nimport io.takari.bpm.model.form.DefaultFormFields.BooleanField;\nimport io.takari.bpm.model.form.DefaultFormFields.DecimalField;\nimport io.takari.bpm.model.form.DefaultFormFields.IntegerField;\nimport io.takari.bpm.model.form.DefaultFormFields.StringField;\nimport io.takari.bpm.model.form.FormDefinition;\nimport io.takari.bpm.model.form.FormField;\nimport io.takari.bpm.model.form.FormField.Cardinality;\nimport io.takari.bpm.model.form.FormField.Option;\nimport io.takari.parc.Seq;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.common.form.ConcordFormFields.FieldOptions.*;\n\npublic final class YamlFormConverter {\n\n    private static final String OPTIONS_FIELD_NAME = \"_options\";\n\n    private static final Map<String, Option<?>> PASSTHROUGH_OPTIONS = createPassthroughOptions();\n\n    public static FormDefinition convert(String name, List<YamlFormField> fields) throws YamlConverterException {\n        Map<String, Object> options = null;\n\n        List<FormField> l = new ArrayList<>();\n        for (YamlFormField f : fields) {\n            if (f == null || f.getName() == null) {\n                throw new YamlConverterException(\"Empty field definition in form '\" + name + \"'\");\n            }\n\n            if (OPTIONS_FIELD_NAME.equals(f.getName())) {\n                if (options != null) {\n                    throw new YamlConverterException(\"Duplicate options definition in form '\" + name + \"'\");\n                }\n\n                options = f.getOptions();\n                continue;\n            }\n\n            l.add(convert(f));\n        }\n        return new FormDefinition(name, l);\n    }\n\n    public static FormDefinition convert(YamlFormDefinition def) throws YamlConverterException {\n        return convert(def.getName(), def.getFields().toList());\n    }\n\n    private static FormField convert(YamlFormField f) throws YamlConverterException {\n        return convert(f.getName(), f.getOptions(), f.getLocation());\n    }\n\n    public static FormField convert(String name, Map<String, Object> opts, JsonLocation loc) throws YamlConverterException {\n        // common parameters\n        String label = (String) opts.remove(\"label\");\n        Object defaultValue = box(opts.remove(\"value\"));\n        Object allowedValue = box(opts.remove(\"allow\"));\n\n        // type-specific options\n        Map<Option<?>, Object> options = new HashMap<>();\n\n        TypeInfo tInfo = parseType(opts.remove(\"type\"), loc);\n        switch (tInfo.type) {\n            case StringField.TYPE: {\n                options.put(StringField.PATTERN, opts.remove(\"pattern\"));\n                options.put(INPUT_TYPE, opts.remove(\"inputType\"));\n                break;\n            }\n            case IntegerField.TYPE: {\n                options.put(IntegerField.MIN, coerceToLong(opts.remove(\"min\")));\n                options.put(IntegerField.MAX, coerceToLong(opts.remove(\"max\")));\n                break;\n            }\n            case DecimalField.TYPE: {\n                options.put(DecimalField.MIN, coerceToDouble(opts.remove(\"min\")));\n                options.put(DecimalField.MAX, coerceToDouble(opts.remove(\"max\")));\n                break;\n            }\n            case BooleanField.TYPE: {\n                break;\n            }\n            case ConcordFormFields.FileField.TYPE: {\n                break;\n            }\n            case ConcordFormFields.DateField.TYPE:\n            case ConcordFormFields.DateTimeField.TYPE: {\n                options.put(ConcordFormFields.DateFieldOptions.POPUP_POSITION, opts.remove(\"popupPosition\"));\n                break;\n            }\n            default:\n                throw new YamlConverterException(\"Unknown field type: \" + tInfo.type + (loc != null ? \"@ \" + loc : \"\"));\n        }\n\n        PASSTHROUGH_OPTIONS.forEach((k, option) -> {\n            Object v = opts.remove(k);\n            if (v != null) {\n                options.put(option, v);\n            }\n        });\n\n        if (!opts.isEmpty()) {\n            throw new YamlConverterException(\"Unknown field options: \" + opts.keySet() + (loc != null ? \"@ \" + loc : \"\"));\n        }\n\n        return new FormField.Builder(name, tInfo.type)\n                .label(label)\n                .defaultValue(defaultValue)\n                .allowedValue(allowedValue)\n                .cardinality(tInfo.cardinality)\n                .options(options)\n                .build();\n    }\n\n    private static TypeInfo parseType(Object t, JsonLocation loc) throws YamlConverterException {\n        if (!(t instanceof String)) {\n            throw new YamlConverterException(\"Expected a field type\" + (loc != null ? \"@ \" + loc : \"\"));\n        }\n        return TypeInfo.parse((String) t);\n    }\n\n    private static Object box(Object v) {\n        if (v instanceof Seq) {\n            return ((Seq) v).toList();\n        }\n        return v;\n    }\n\n    private static Long coerceToLong(Object v) {\n        if (v == null) {\n            return null;\n        }\n\n        if (v instanceof Long) {\n            return (Long) v;\n        }\n\n        if (v instanceof Integer) {\n            return ((Integer) v).longValue();\n        }\n\n        throw new IllegalArgumentException(\"Can't coerce '\" + v + \"' to long\");\n    }\n\n    private static Double coerceToDouble(Object v) {\n        if (v == null) {\n            return null;\n        }\n\n        if (v instanceof Double) {\n            return (Double) v;\n        }\n\n        if (v instanceof Float) {\n            return ((Float) v).doubleValue();\n        }\n\n        if (v instanceof Integer) {\n            return ((Integer) v).doubleValue();\n        }\n\n        if (v instanceof Long) {\n            return ((Long) v).doubleValue();\n        }\n\n        throw new IllegalArgumentException(\"Can't coerce '\" + v + \"' to double\");\n    }\n\n    private static Map<String, Option<?>> createPassthroughOptions() {\n        Map<String, Option<?>> result = new HashMap<>();\n        result.put(\"placeholder\", PLACEHOLDER);\n        result.put(\"readonly\", READ_ONLY);\n        result.put(\"search\", SEARCH);\n        return result;\n    }\n\n    private static class TypeInfo implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        public static TypeInfo parse(String s) {\n            String type = s;\n            Cardinality cardinality = Cardinality.ONE_AND_ONLY_ONE;\n\n            if (s.endsWith(\"?\")) {\n                type = type.substring(0, type.length() - 1);\n                cardinality = Cardinality.ONE_OR_NONE;\n            } else if (s.endsWith(\"+\")) {\n                type = type.substring(0, type.length() - 1);\n                cardinality = Cardinality.AT_LEAST_ONE;\n            } else if (s.endsWith(\"*\")) {\n                type = type.substring(0, type.length() - 1);\n                cardinality = Cardinality.ANY;\n            }\n\n            return new TypeInfo(type, cardinality);\n        }\n\n        private final String type;\n        private final Cardinality cardinality;\n\n        private TypeInfo(String type, Cardinality cardinality) {\n            this.type = type;\n            this.cardinality = cardinality;\n        }\n    }\n\n    private YamlFormConverter() {\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/YamlImportConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.guava.GuavaModule;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport com.walmartlabs.concord.imports.Import;\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.project.yaml.converter.StepConverter;\nimport com.walmartlabs.concord.project.yaml.model.YamlImport;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic final class YamlImportConverter {\n\n    private static final ObjectMapper objectMapper = createMapper();\n\n    @SuppressWarnings(\"unchecked\")\n    public static Imports convertImports(List<YamlImport> imports) throws YamlConverterException {\n        if (imports == null || imports.isEmpty()) {\n            return null;\n        }\n\n        List<Import> result = new ArrayList<>();\n        for (YamlImport i : imports) {\n            Map<String, Object> opts = (Map<String, Object>) StepConverter.deepConvert(i.getOptions());\n\n            Map<String, Object> typedOpts = new HashMap<>(opts);\n            typedOpts.put(\"type\", i.getType());\n\n            try {\n                result.add(objectMapper.convertValue(typedOpts, Import.class));\n            } catch (Exception e) {\n                error(\"Error parsing import definition: \" + e.getMessage(), i);\n            }\n        }\n\n        return Imports.of(result);\n    }\n\n    private static void error(String message, YamlImport yamlImport) throws YamlConverterException {\n        throw new YamlConverterException(message + \" @ \" + yamlImport.getLocation());\n    }\n\n    private static ObjectMapper createMapper() {\n        ObjectMapper om = new ObjectMapper();\n        om.registerModule(new GuavaModule());\n        om.registerModule(new Jdk8Module());\n        return om;\n    }\n\n    private YamlImportConverter() {\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/YamlParser.java",
    "content": "package com.walmartlabs.concord.project.yaml;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.exc.MismatchedInputException;\nimport com.fasterxml.jackson.databind.module.SimpleModule;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLFactory;\nimport com.fasterxml.jackson.datatype.guava.GuavaModule;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport com.walmartlabs.concord.project.yaml.model.*;\nimport org.yaml.snakeyaml.error.MarkedYAMLException;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Path;\n\npublic class YamlParser {\n\n    private final ObjectMapper objectMapper;\n\n    public YamlParser() {\n        ObjectMapper om = new ObjectMapper(new YAMLFactory()\n                .enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION));\n\n        SimpleModule module = new SimpleModule();\n        module.addDeserializer(YamlStep.class, YamlDeserializers.getYamlStepDeserializer());\n        module.addDeserializer(YamlFormField.class, YamlDeserializers.getYamlFormFieldDeserializer());\n        module.addDeserializer(YamlDefinitionFile.class, YamlDeserializers.getYamlDefinitionFileDeserializer());\n        module.addDeserializer(YamlTrigger.class, YamlDeserializers.getYamlTriggerDeserializer());\n        module.addDeserializer(YamlImport.class, YamlDeserializers.getYamlImportDeserializer());\n\n        om.registerModule(module);\n        om.registerModule(new GuavaModule());\n        om.registerModule(new Jdk8Module());\n        this.objectMapper = om;\n    }\n\n    public YamlProject parseProject(Path baseDir, Path file) throws YamlParserException {\n        try {\n            return objectMapper.readValue(file.toFile(), YamlProject.class);\n        } catch (IOException e) {\n            if (e instanceof JsonProcessingException) {\n                JsonProcessingException jpe = (JsonProcessingException) e;\n                throw toErr(\"(\" + baseDir.relativize(file) + \"): Error\", jpe);\n            }\n            throw new YamlParserException(\"Error while loading a project file: \" + baseDir.relativize(file), e);\n        }\n    }\n\n    public YamlDefinitionFile parseDefinitionFile(Path baseDir, Path path) throws YamlParserException {\n        try {\n            return objectMapper.readValue(path.toFile(), YamlDefinitionFile.class);\n        } catch (IOException e) {\n            if (e instanceof JsonProcessingException) {\n                JsonProcessingException jpe = (JsonProcessingException) e;\n                throw toErr(\"Error while loading flow definitions: \" + baseDir.relativize(path), jpe);\n            }\n            throw new YamlParserException(\"Error while loading flow definitions: \" + baseDir.relativize(path), e);\n        }\n    }\n\n    public YamlDefinitionFile parseDefinitionFile(InputStream in) throws YamlParserException {\n        try {\n            return objectMapper.readValue(in, YamlDefinitionFile.class);\n        } catch (IOException e) {\n            Throwable cause = e.getCause();\n            if (cause instanceof MarkedYAMLException) {\n                throw new YamlParserException(\"Error while loading a definition file: \" + e.getMessage());\n            }\n            throw new YamlParserException(\"Error while loading a definition file\", e);\n        }\n    }\n\n    public YamlProfileFile parseProfileFile(Path baseDir, Path path) throws YamlParserException {\n        try {\n            return objectMapper.readValue(path.toFile(), YamlProfileFile.class);\n        } catch (IOException e) {\n            if (e instanceof MismatchedInputException) {\n                throw new YamlParserException(\"Error while loading profiles: \" + baseDir.relativize(path) + \". \" + e.getMessage());\n            }\n            throw new YamlParserException(\"Error while loading profiles: \" + baseDir.relativize(path), e);\n        }\n    }\n\n    private static YamlParserException toErr(String msg, JsonProcessingException jpe) {\n        JsonLocation loc = jpe.getLocation();\n        String originalMsg = jpe.getOriginalMessage();\n        return new YamlParserException(msg + \" @ \" + loc + \". \" + originalMsg);\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/YamlParserException.java",
    "content": "package com.walmartlabs.concord.project.yaml;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\n\npublic class YamlParserException extends IOException {\n\n    private static final long serialVersionUID = 1L;\n\n    public YamlParserException(String message) {\n        super(message);\n    }\n\n    public YamlParserException(String message, Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/YamlProcessConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.yaml.converter.ConverterContext;\nimport com.walmartlabs.concord.project.yaml.model.YamlProcessDefinition;\nimport com.walmartlabs.concord.project.yaml.model.YamlStep;\nimport io.takari.bpm.model.ProcessDefinition;\n\nimport java.util.List;\n\npublic final class YamlProcessConverter {\n\n    public static ProcessDefinition convert(String name, List<YamlStep> steps) throws YamlConverterException {\n        return ConverterContext.convert(name, steps);\n    }\n\n    public static ProcessDefinition convert(YamlProcessDefinition def) throws YamlConverterException {\n        return ConverterContext.convert(def.getName(), def.getSteps());\n    }\n\n    private YamlProcessConverter() {\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/YamlProjectConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.project.model.Profile;\nimport com.walmartlabs.concord.project.model.ProjectDefinition;\nimport com.walmartlabs.concord.project.model.Resources;\nimport com.walmartlabs.concord.project.model.Trigger;\nimport com.walmartlabs.concord.project.yaml.model.YamlFormField;\nimport com.walmartlabs.concord.project.yaml.model.YamlProfile;\nimport com.walmartlabs.concord.project.yaml.model.YamlProject;\nimport com.walmartlabs.concord.project.yaml.model.YamlStep;\nimport io.takari.bpm.model.ProcessDefinition;\nimport io.takari.bpm.model.form.FormDefinition;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\nimport static com.walmartlabs.concord.project.yaml.YamlImportConverter.convertImports;\n\npublic final class YamlProjectConverter {\n\n    public static ProjectDefinition convert(YamlProject project) throws YamlConverterException {\n        Map<String, ProcessDefinition> flows = convertFlows(project.getFlows());\n        Set<String> publicFlows = project.getPublicFlows();\n        Map<String, FormDefinition> forms = convertForms(project.getForms());\n        Map<String, Object> cfg = project.getConfiguration();\n        Map<String, Profile> profiles = convertProfiles(project.getProfiles());\n        List<Trigger> triggers = YamlTriggersConverter.convert(project.getTriggers());\n        Imports imports = convertImports(project.getImports());\n        Resources resources = convertResources(project.getResources());\n        return new ProjectDefinition(flows, publicFlows, forms, cfg, profiles, triggers, imports, resources);\n    }\n\n    public static Profile convert(YamlProfile profile) throws YamlConverterException {\n        return convertProfile(profile);\n    }\n\n    private static Map<String, Profile> convertProfiles(Map<String, YamlProfile> profiles) throws YamlConverterException {\n        if (profiles == null) {\n            return null;\n        }\n\n        Map<String, Profile> m = new HashMap<>(profiles.size());\n        for (Map.Entry<String, YamlProfile> e : profiles.entrySet()) {\n            String k = e.getKey();\n            m.put(k, convertProfile(e.getValue()));\n        }\n\n        return m;\n    }\n\n    private static Profile convertProfile(YamlProfile profile) throws YamlConverterException {\n        Map<String, ProcessDefinition> flows = convertFlows(profile.getFlows());\n        Set<String> publicFlows = profile.getPublicFlows();\n        Map<String, FormDefinition> forms = convertForms(profile.getForms());\n        Map<String, Object> cfg = profile.getConfiguration();\n        return new Profile(flows, publicFlows, forms, cfg);\n    }\n\n    private static Map<String, ProcessDefinition> convertFlows(Map<String, List<YamlStep>> flows) throws YamlConverterException {\n        if (flows == null) {\n            return null;\n        }\n\n        Map<String, ProcessDefinition> m = new HashMap<>(flows.size());\n        for (Map.Entry<String, List<YamlStep>> e : flows.entrySet()) {\n            String k = e.getKey();\n            m.put(k, YamlProcessConverter.convert(k, e.getValue()));\n        }\n\n        return m;\n    }\n\n    private static Map<String, FormDefinition> convertForms(Map<String, List<YamlFormField>> forms) throws YamlConverterException {\n        if (forms == null) {\n            return null;\n        }\n\n        Map<String, FormDefinition> m = new HashMap<>(forms.size());\n        for (Map.Entry<String, List<YamlFormField>> e : forms.entrySet()) {\n            String k = e.getKey();\n            m.put(k, YamlFormConverter.convert(k, e.getValue()));\n        }\n\n        return m;\n    }\n\n    private static Resources convertResources(Map<String, Object> resources) {\n        if (resources == null) {\n            return null;\n        }\n\n        return YamlResourcesConverter.parse(resources);\n    }\n\n    private YamlProjectConverter() {\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/YamlResourcesConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.model.Resources;\nimport com.walmartlabs.concord.sdk.Constants;\n\nimport java.util.*;\n\nimport static com.walmartlabs.concord.sdk.Constants.Files.PROFILES_DIR_NAME;\nimport static com.walmartlabs.concord.sdk.Constants.Files.PROJECT_FILES_DIR_NAME;\n\npublic final class YamlResourcesConverter {\n\n    @SuppressWarnings(\"unchecked\")\n    public static Resources parse(Map<String, Object> m) {\n        List<String> disabledDirs = (List<String>) m.getOrDefault(\"disabled\", Collections.emptyList());\n\n        List<String> profilesPath = getPaths(m, disabledDirs, PROFILES_DIR_NAME);\n        List<String> projectFilesPath = getPaths(m, disabledDirs, PROJECT_FILES_DIR_NAME);\n        List<String> definitionPath = getPaths(m, disabledDirs, Constants.Files.DEFINITIONS_DIR_NAMES);\n\n        return new Resources(profilesPath, projectFilesPath, definitionPath, disabledDirs);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static List<String> getPaths(Map<String, Object> resources, Collection<String> disabledDirs, String ... definitionsDirNames) {\n        List<String> result = new ArrayList<>();\n\n        for (String dirName : definitionsDirNames) {\n            Object v = resources.get(dirName);\n            if (v == null && !disabledDirs.contains(dirName)) {\n                result.add(dirName);\n            } else if (v instanceof String) {\n                result.add((String) v);\n            } else if (v instanceof Collection) {\n                result.addAll((Collection<String>) v);\n            }\n        }\n\n        return result;\n    }\n\n    private YamlResourcesConverter() {\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/YamlTriggersConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\nimport com.walmartlabs.concord.project.model.Trigger;\nimport com.walmartlabs.concord.project.yaml.converter.StepConverter;\nimport com.walmartlabs.concord.project.yaml.model.YamlTrigger;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport io.takari.bpm.model.SourceMap;\n\nimport java.util.*;\n\npublic final class YamlTriggersConverter {\n\n    private static final String[] MANUAL_TRIGGER_CONFIG_KEYS = {\n            \"name\",\n            Constants.Request.ENTRY_POINT_KEY,\n            Constants.Request.EXCLUSIVE\n    };\n\n    protected static final String[] TRIGGER_CONFIG_KEYS = {\n            Constants.Trigger.USE_INITIATOR,\n            Constants.Trigger.USE_EVENT_COMMIT_ID,\n            Constants.Trigger.IGNORE_EMPTY_PUSH,\n            Constants.Request.ENTRY_POINT_KEY,\n            Constants.Request.EXCLUSIVE,\n    };\n\n    private static final Map<String, TriggerConverter> converters = createConverters();\n    private static final TriggerConverter defaultConverter = new DefaultTriggerConverter();\n\n    private static Map<String, TriggerConverter> createConverters() {\n        Map<String, TriggerConverter> converters = new HashMap<>();\n        converters.put(\"manual\", new TriggerV1Converter(MANUAL_TRIGGER_CONFIG_KEYS));\n        converters.put(\"github\", new TriggerV2Converter(TRIGGER_CONFIG_KEYS));\n        converters.put(\"oneops\", new DefaultTriggerConverter());\n        return converters;\n    }\n\n    public static List<Trigger> convert(List<YamlTrigger> triggers) {\n        if (triggers == null || triggers.isEmpty()) {\n            return null;\n        }\n\n        List<Trigger> result = new ArrayList<>();\n        for (YamlTrigger t : triggers) {\n            TriggerConverter converter = converters.getOrDefault(t.getEventSource(), defaultConverter);\n            result.add(converter.convert(t));\n        }\n\n        return result;\n    }\n\n    private static class DefaultTriggerConverter implements TriggerConverter {\n\n        private final TriggerConverter v1Converter;\n        private final TriggerConverter v2Converter;\n\n        public DefaultTriggerConverter() {\n            this.v1Converter = new TriggerV1Converter(TRIGGER_CONFIG_KEYS);\n            this.v2Converter = new TriggerV2Converter(TRIGGER_CONFIG_KEYS);\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        @Override\n        public Trigger convert(YamlTrigger trigger) {\n            Map<String, Object> opts = (Map<String, Object>) StepConverter.deepConvert(trigger.getOptions());\n            int version = MapUtils.getInt(opts, Constants.Trigger.VERSION, 1);\n            if (version == 1) {\n                return v1Converter.convert(trigger);\n            }\n            return v2Converter.convert(trigger);\n        }\n    }\n\n    private static class TriggerV2Converter implements TriggerConverter {\n\n        private final String[] cfgKeys;\n\n        public TriggerV2Converter(String[] cfgKeys) {\n            this.cfgKeys = cfgKeys;\n        }\n\n        @Override\n        @SuppressWarnings(\"unchecked\")\n        public Trigger convert(YamlTrigger trigger) {\n            Map<String, Object> opts = (Map<String, Object>) StepConverter.deepConvert(trigger.getOptions());\n            Map<String, Object> cfg = new HashMap<>();\n            for (String key : cfgKeys) {\n                Object v = opts.remove(key);\n                if (v != null) {\n                    cfg.put(key, v);\n                }\n            }\n\n            Map<String, Object> params = new HashMap<>();\n            params.put(\"version\", 2);\n            params.putAll(MapUtils.getMap(opts, Constants.Trigger.CONDITIONS, Collections.emptyMap()));\n            List<String> activeProfiles = MapUtils.getList(opts, Constants.Request.ACTIVE_PROFILES_KEY, null);\n            Map<String, Object> arguments = MapUtils.getMap(opts, Constants.Request.ARGUMENTS_KEY, null);\n\n            return new Trigger(trigger.getEventSource(), activeProfiles, arguments, params, cfg, convertSourceMap(trigger));\n        }\n    }\n\n    private static class TriggerV1Converter implements TriggerConverter {\n\n        private final String[] cfgKeys;\n\n        public TriggerV1Converter(String[] cfgKeys) {\n            this.cfgKeys = cfgKeys;\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        @Override\n        public Trigger convert(YamlTrigger trigger) {\n            String name = trigger.getEventSource();\n\n            Map<String, Object> opts = (Map<String, Object>) StepConverter.deepConvert(trigger.getOptions());\n\n            List<String> activeProfiles = (List<String>) opts.remove(Constants.Request.ACTIVE_PROFILES_KEY);\n            Map<String, Object> arguments = (Map<String, Object>) opts.remove(Constants.Request.ARGUMENTS_KEY);\n\n            Map<String, Object> cfg = new HashMap<>();\n            for (String key : cfgKeys) {\n                Object v = opts.remove(key);\n                if (v != null) {\n                    cfg.put(key, v);\n                }\n            }\n\n            return new Trigger(name, activeProfiles, arguments, opts, cfg, convertSourceMap(trigger));\n        }\n    }\n\n    private interface TriggerConverter {\n\n        Trigger convert(YamlTrigger trigger);\n    }\n\n    private static SourceMap convertSourceMap(YamlTrigger t) {\n        String name = t.getEventSource();\n        JsonLocation l = t.getLocation();\n        return new SourceMap(SourceMap.Significance.HIGH,\n                String.valueOf(l.getSourceRef()),\n                l.getLineNr(),\n                l.getColumnNr(),\n                \"Trigger: \" + name);\n    }\n\n    private YamlTriggersConverter() {\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/converter/Chunk.java",
    "content": "package com.walmartlabs.concord.project.yaml.converter;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.takari.bpm.model.AbstractElement;\nimport io.takari.bpm.model.SourceMap;\n\nimport java.io.Serializable;\nimport java.util.*;\n\npublic class Chunk implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final List<AbstractElement> elements = new ArrayList<>();\n    private final Set<String> outputs = new HashSet<>();\n    private final Map<String, SourceMap> sourceMap = new HashMap<>();\n\n    public void addFirstElement(AbstractElement e) {\n        elements.add(0, e);\n    }\n\n    public void addElement(AbstractElement e) {\n        elements.add(e);\n    }\n\n    public void addElements(List<AbstractElement> l) {\n        elements.addAll(l);\n    }\n\n    public void addOutput(String id) {\n        outputs.add(id);\n    }\n\n    public void removeOutput(String id) {\n        outputs.remove(id);\n    }\n\n    public void addOutputs(Set<String> s) {\n        outputs.addAll(s);\n    }\n\n    public void addSourceMap(String id, SourceMap s) {\n        sourceMap.put(id, s);\n    }\n\n    public void addSourceMaps(Map<String, SourceMap> m) {\n        sourceMap.putAll(m);\n    }\n\n    public AbstractElement firstElement() {\n        return elements.get(0);\n    }\n\n    public List<AbstractElement> getElements() {\n        return elements;\n    }\n\n    public Set<String> getOutputs() {\n        return outputs;\n    }\n\n    public Map<String, SourceMap> getSourceMap() {\n        return sourceMap;\n    }\n\n    public boolean isEmpty() {\n        return elements.isEmpty();\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/converter/ConverterContext.java",
    "content": "package com.walmartlabs.concord.project.yaml.converter;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.yaml.YamlConverterException;\nimport com.walmartlabs.concord.project.yaml.model.*;\nimport io.takari.bpm.model.*;\nimport io.takari.parc.Seq;\n\nimport java.util.*;\n\npublic class ConverterContext {\n\n    private static final Map<Class, StepConverter> converters = new HashMap<>();\n\n    static {\n        converters.put(YamlExpressionStep.class, new YamlExpressionStepConverter());\n        converters.put(YamlCall.class, new YamlCallConverter());\n        converters.put(YamlCheckpoint.class, new YamlCheckpointConverter());\n        converters.put(YamlReturn.class, new YamlReturnConverter());\n        converters.put(YamlGroup.class, new YamlGroupConverter());\n        converters.put(YamlTaskStep.class, new YamlTaskStepConverter());\n        converters.put(YamlTaskShortStep.class, new YamlTaskShortStepConverter());\n        converters.put(YamlIfExpr.class, new YamlIfExprConverter());\n        converters.put(YamlEvent.class, new YamlEventConverter());\n        converters.put(YamlScript.class, new YamlScriptConverter());\n        converters.put(YamlSetVariablesStep.class, new YamlSetVariablesStepConverter());\n        converters.put(YamlDockerStep.class, new YamlDockerStepConverter());\n        converters.put(YamlFormCall.class, new YamlFormCallConverter());\n        converters.put(YamlSwitchExpr.class, new YamlSwitchExprConverter());\n        converters.put(YamlExit.class, new YamlExitConverter());\n    }\n\n    public static SourceAwareProcessDefinition convert(String name, Seq<YamlStep> steps) throws YamlConverterException {\n        return convert(name, steps.toList());\n    }\n\n    public static SourceAwareProcessDefinition convert(String name, List<YamlStep> steps) throws YamlConverterException {\n        ConverterContext ctx = new ConverterContext();\n\n        Chunk c = ctx.convert(steps);\n        List<AbstractElement> l = ctx.wrapAsProcess(c);\n        return new SourceAwareProcessDefinition(name, l, Collections.emptyMap(), c.getSourceMap());\n    }\n\n    private int index = 0;\n\n    public String nextId() {\n        return \"e_\" + index++;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public Chunk convert(YamlStep s) throws YamlConverterException {\n        StepConverter c = converters.get(s.getClass());\n        if (c == null) {\n            throw new YamlConverterException(\"Unsupported step type: \" + s.getClass());\n        }\n        return c.convert(this, s);\n    }\n\n    public Chunk convert(Seq<YamlStep> steps) throws YamlConverterException {\n        return convert(steps.toList());\n    }\n\n    public Chunk convert(List<YamlStep> steps) throws YamlConverterException {\n        if (steps == null || steps.isEmpty()) {\n            return new Chunk();\n        }\n\n        if (steps.size() == 1) {\n            return convert(steps.get(0));\n        }\n\n        Chunk c = new Chunk();\n\n        Chunk prev = null;\n        for (YamlStep s : steps) {\n            Chunk curr = convert(s);\n\n            if (prev != null) {\n                connect(c, prev, curr);\n            }\n\n            c.addElements(curr.getElements());\n            c.addSourceMaps(curr.getSourceMap());\n            prev = curr;\n        }\n\n        if (prev != null) {\n            c.addOutputs(prev.getOutputs());\n        }\n\n        return c;\n    }\n\n    public List<AbstractElement> wrapAsProcess(Chunk c) {\n        List<AbstractElement> l = new ArrayList<>();\n\n        String startId = nextId();\n        l.add(new StartEvent(startId));\n\n        if (c.isEmpty()) {\n            String endId = nextId();\n            l.add(new SequenceFlow(nextId(), startId, endId));\n            l.add(new EndEvent(endId));\n            return l;\n        }\n\n        l.add(new SequenceFlow(nextId(), startId, c.firstElement().getId()));\n        l.addAll(c.getElements());\n\n        if (!c.getOutputs().isEmpty()) {\n            String endId = nextId();\n            for (String src : c.getOutputs()) {\n                l.add(new SequenceFlow(nextId(), src, endId));\n            }\n            l.add(new EndEvent(endId));\n        }\n\n        return l;\n    }\n\n    private void connect(Chunk c, Chunk a, Chunk b) {\n        String dst = b.firstElement().getId();\n        for (String src : a.getOutputs()) {\n            c.addElement(new SequenceFlow(nextId(), src, dst));\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/converter/DockerOptionsConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml.converter;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.DockerProcessBuilder;\nimport io.takari.parc.Seq;\n\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Keeping it for backward compatibility with older version of ansible-plugin.\n */\n@Deprecated\npublic class DockerOptionsConverter {\n\n    public static List<Map.Entry<String, String>> convert(Map<String, Object> options) {\n        if (options == null) {\n            return Collections.emptyList();\n        }\n\n        DockerProcessBuilder.DockerOptionsBuilder result = new DockerProcessBuilder.DockerOptionsBuilder();\n\n        getList(options, \"hosts\").forEach(result::etcHost);\n\n        return result.build();\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Collection<String> getList(Map<String, Object> options, String key) {\n        Object o = options.get(key);\n        if (o == null) {\n            return Collections.emptyList();\n        }\n\n        if (o instanceof Collection) {\n            return (Collection<String>) o;\n        }\n\n        if (o instanceof Seq) {\n            return ((Seq) o).toList();\n        }\n\n        throw new IllegalArgumentException(\"unexpected '\" + key + \"' type: \" + o);\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/converter/StepConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml.converter;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.project.InternalConstants;\nimport com.walmartlabs.concord.project.yaml.KV;\nimport com.walmartlabs.concord.project.yaml.YamlConverterException;\nimport com.walmartlabs.concord.project.yaml.model.YamlStep;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.Task;\nimport io.takari.bpm.api.BpmnError;\nimport io.takari.bpm.api.ExecutionContext;\nimport io.takari.bpm.model.*;\nimport io.takari.bpm.model.SourceMap.Significance;\nimport io.takari.parc.Seq;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Named;\nimport java.io.Serializable;\nimport java.util.*;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.BiFunction;\nimport java.util.stream.Collectors;\n\npublic interface StepConverter<T extends YamlStep> {\n\n    long DEFAULT_DELAY = 5;\n\n    Chunk convert(ConverterContext ctx, T s) throws YamlConverterException;\n\n    @SuppressWarnings(\"unchecked\")\n    default void applyErrorBlock(ConverterContext ctx, Chunk c, String attachedRef, Map<String, Object> opts) throws YamlConverterException {\n        if (opts == null) {\n            return;\n        }\n\n        Seq<YamlStep> errorSteps = (Seq<YamlStep>) opts.get(\"error\");\n        if (errorSteps == null) {\n            return;\n        }\n\n        String evId = ctx.nextId();\n        c.addElement(new BoundaryEvent(evId, attachedRef, null));\n\n        Chunk err = ctx.convert(errorSteps);\n\n        // connect the boundary event to the error block's steps\n        String dst = err.firstElement().getId();\n        c.addElement(new SequenceFlow(ctx.nextId(), evId, dst));\n        c.addElements(err.getElements());\n\n        c.addOutputs(err.getOutputs());\n\n        // keep the source map of the error block's steps\n        c.addSourceMaps(err.getSourceMap());\n    }\n\n    default Chunk applyWithItems(ConverterContext ctx, Chunk c, Map<String, Object> opts) throws YamlConverterException {\n        Object withItems = getWithItems(opts);\n        if (withItems == null) {\n            return c;\n        }\n\n        Chunk result = new Chunk();\n\n        VariableMapping taskVars = new VariableMapping(null, null, withItems, \"items\", true);\n        Set<String> outVars = Optional.ofNullable(getVarMap(opts, \"out\")).map(vars -> vars.stream().map(VariableMapping::getTarget).collect(Collectors.toSet())).orElse(Collections.emptySet());\n        VariableMapping outVarsMapping = new VariableMapping(null, null, outVars, \"__0\", true);\n\n        String startId = ctx.nextId();\n        String initId = ctx.nextId();\n        String nextItemTaskId = ctx.nextId();\n        String processOutVarsTask = ctx.nextId();\n        String hasNextGwId = ctx.nextId();\n        String cleanupTaskId = ctx.nextId();\n\n        result.addElement(new ServiceTask(initId, ExpressionType.SIMPLE, \"${__withItemsUtils.init(execution)}\", Collections.singleton(taskVars), null, true));\n        result.addElement(new ServiceTask(nextItemTaskId, ExpressionType.SIMPLE, \"${__withItemsUtils.nextItem(execution)}\", Collections.singleton(taskVars), null, true));\n        result.addElement(new ServiceTask(processOutVarsTask, ExpressionType.SIMPLE, \"${__withItemsUtils.processOutVars(execution, __0)}\", Collections.singleton(outVarsMapping), null, true));\n        result.addElement(new ExclusiveGateway(hasNextGwId));\n        result.addElement(new ServiceTask(cleanupTaskId, ExpressionType.SIMPLE, \"${__withItemsUtils.cleanup(execution, __0)}\", Collections.singleton(outVarsMapping), null, true));\n\n        /*  ::bpm flow::            ---processOutVarsTask <---- theTask ----Y\n                                    v                                       |\n         startId -----> init -> nextItemTaskId --------------------> hasNextGwId --N--> cleanupTaskId\n         */\n        result.addElement(new SequenceFlow(ctx.nextId(), startId, initId));\n        result.addElement(new SequenceFlow(ctx.nextId(), initId, nextItemTaskId));\n        result.addElement(new SequenceFlow(ctx.nextId(), nextItemTaskId, hasNextGwId));\n        result.addElement(new SequenceFlow(ctx.nextId(), processOutVarsTask, nextItemTaskId));\n        result.addElement(new SequenceFlow(ctx.nextId(), hasNextGwId, c.firstElement().getId(), \"${__withItemsUtils.hasNext(execution)}\"));\n        result.addElement(new SequenceFlow(ctx.nextId(), hasNextGwId, cleanupTaskId));\n        c.getOutputs().forEach(o -> {\n            result.addElement(new SequenceFlow(ctx.nextId(), o, processOutVarsTask));\n        });\n\n        result.addOutput(cleanupTaskId);\n\n        result.addElements(c.getElements());\n        result.addSourceMaps(c.getSourceMap());\n\n        return result;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    default void applyRetryBlock(ConverterContext ctx, Chunk c, String attachedRef, JsonLocation loc, Map<String, Object> opts,\n                                 BiFunction<String, Map<String, Object>, String> f) throws YamlConverterException {\n\n        if (opts == null) {\n            return;\n        }\n\n        Map<String, Object> retryParams = toMap((Seq<KV<String, Object>>) opts.get(\"retry\"));\n        if (retryParams.isEmpty()) {\n            return;\n        }\n\n        c.removeOutput(attachedRef);\n\n        VariableMapping retryCountVar = new VariableMapping(null, null, retryParams.get(\"times\"), \"__maxRetryCount\", true);\n        VariableMapping delayVar = new VariableMapping(null, null, getRetryDelay(retryParams, loc), \"__retryDelay\", true);\n\n        // retry init\n        String initId = ctx.nextId();\n        c.addFirstElement(new ServiceTask(initId, ExpressionType.SIMPLE, \"${__retryUtils.init(execution)}\", null, null, true));\n        c.addElement(new SequenceFlow(ctx.nextId(), initId, attachedRef));\n\n        // boundary event\n        String originalEvId = ctx.nextId();\n        c.addElement(new BoundaryEvent(originalEvId, attachedRef, null));\n\n        // inc retry count\n        String incCounterId = ctx.nextId();\n        c.addElement(new SequenceFlow(ctx.nextId(), originalEvId, incCounterId));\n        c.addElement(new ServiceTask(incCounterId, ExpressionType.SIMPLE, \"${__retryUtils.inc(execution)}\", Collections.singleton(retryCountVar), null, true));\n\n        // retry count GW\n        String retryCountGwId = ctx.nextId();\n        c.addElement(new SequenceFlow(ctx.nextId(), incCounterId, retryCountGwId));\n        c.addElement(new ExclusiveGateway(retryCountGwId));\n\n        // sleep\n        String retryDelayId = ctx.nextId();\n        c.addElement(new SequenceFlow(ctx.nextId(), retryCountGwId, retryDelayId, \"${__retryUtils.isRetryCountExceeded(execution)}\"));\n        c.addElement(new ServiceTask(retryDelayId, ExpressionType.SIMPLE, \"${__retryUtils.sleep(execution)}\", Collections.singleton(delayVar), null, true));\n\n        // retry step\n        String retryId = f.apply(retryDelayId, retryParams);\n\n        // cleanup\n        String cleanupTaskId = ctx.nextId();\n        c.addElement(new SequenceFlow(ctx.nextId(), retryId, cleanupTaskId));\n        c.addElement(new ServiceTask(cleanupTaskId, ExpressionType.SIMPLE, \"${__retryUtils.cleanup(execution)}\", null, null, true));\n        c.addOutput(cleanupTaskId);\n\n        String retryEventId = ctx.nextId();\n        c.addElement(new BoundaryEvent(retryEventId, retryId, null));\n        c.addElement(new SequenceFlow(ctx.nextId(), retryEventId, incCounterId));\n\n        // throw last error\n        String throwCallId = ctx.nextId();\n        String expr = \"${__retryUtils.throwLastError(execution)}\";\n        c.addElement(new ServiceTask(throwCallId, ExpressionType.DELEGATE, expr, null, null, true));\n        c.addElement(new SequenceFlow(ctx.nextId(), retryCountGwId, throwCallId));\n\n        String endId = ctx.nextId();\n        c.addElement(new EndEvent(endId));\n        c.addElement(new SequenceFlow(ctx.nextId(), throwCallId, endId));\n\n        // cleanup after success execution\n        c.addElement(new SequenceFlow(ctx.nextId(), attachedRef, cleanupTaskId));\n\n        c.addSourceMaps(c.getSourceMap());\n    }\n\n    static Object getWithItems(Map<String, Object> options) {\n        if (options == null) {\n            return null;\n        }\n\n        Object withItems = options.get(\"withItems\");\n        if (withItems == null) {\n            return null;\n        }\n\n        return deepConvert(withItems);\n    }\n\n    default SourceMap toSourceMap(YamlStep step, String description) {\n        JsonLocation l = step.getLocation();\n        return new SourceMap(Significance.HIGH, String.valueOf(l.getSourceRef()), l.getLineNr(), l.getColumnNr(), description);\n    }\n\n    default Map<String, Object> toMap(Seq<KV<String, Object>> values) {\n        if (values == null) {\n            return Collections.emptyMap();\n        }\n\n        return values.stream()\n                .collect(Collectors.toMap(Map.Entry::getKey, this::toValue));\n    }\n\n    default Object toValue(KV<String, Object> kv) {\n        Object v = kv.getValue();\n        if (v == null && kv.getKey() != null) {\n            return false;\n        }\n        return StepConverter.deepConvert(v);\n    }\n\n    default Set<VariableMapping> toVarMapping(Map<String, Object> params) {\n        Set<VariableMapping> result = new HashSet<>();\n        for (Map.Entry<String, Object> e : params.entrySet()) {\n            String target = e.getKey();\n\n            String sourceExpr = null;\n            Object sourceValue = null;\n\n            Object v = StepConverter.deepConvert(e.getValue());\n            if (StepConverter.isExpression(v)) {\n                sourceExpr = v.toString();\n            } else {\n                sourceValue = v;\n            }\n\n            result.add(new VariableMapping(null, sourceExpr, sourceValue, target, true));\n        }\n\n        return result;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    default Set<VariableMapping> getVarMap(Map<String, Object> options, String key) {\n        if (options == null) {\n            return null;\n        }\n\n        Seq<KV<String, Object>> s = (Seq<KV<String, Object>>) options.get(key);\n        if (s == null) {\n            return null;\n        }\n\n        Set<VariableMapping> result = new HashSet<>();\n        for (KV<String, Object> kv : s.toList()) {\n            String target = kv.getKey();\n\n            String sourceExpr = null;\n            Object sourceValue = null;\n\n            Object v = deepConvert(kv.getValue());\n            if (isExpression(v)) {\n                sourceExpr = v.toString();\n            } else {\n                sourceValue = v;\n            }\n\n            result.add(new VariableMapping(null, sourceExpr, sourceValue, target, true));\n        }\n\n        if (key.equals(\"in\") && getWithItems(options) != null) {\n            result = appendWithItemsVar(result);\n        }\n        return result;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    default Set<VariableMapping> getInVars(Map<String, Object> opts, Map<String, Object> retryParams) {\n        Map<String, Object> retryInVars = Optional.ofNullable((Map<String, Object>) retryParams.get(\"in\")).orElse(Collections.emptyMap());\n        Map<String, Object> originalInVars = toMap((Seq<KV<String, Object>>) opts.get(\"in\"));\n        return toVarMapping(ConfigurationUtils.deepMerge(originalInVars, retryInVars));\n    }\n\n    default Object getRetryDelay(Map<String, Object> params, JsonLocation loc) throws YamlConverterException {\n        Object v = params.get(\"delay\");\n        if (v == null) {\n            return DEFAULT_DELAY;\n        }\n\n        if (v instanceof Integer) {\n            return v;\n        }\n\n        if (isExpression(v)) {\n            return v;\n        }\n\n        throw new YamlConverterException(\"Invalid 'delay' value. Expected integer or expression, got: \" + v + \" @ \" + loc);\n    }\n\n    static Set<VariableMapping> appendWithItemsVar(Set<VariableMapping> inVars) {\n        Set<VariableMapping> vars = new HashSet<>();\n        if (inVars != null) {\n            vars.addAll(inVars);\n        }\n        vars.add(new VariableMapping(null, \"${item}\", null, \"item\", true));\n        return vars;\n    }\n\n    static boolean isExpression(Object o) {\n        if (o == null) {\n            return false;\n        }\n\n        if (!(o instanceof String)) {\n            return false;\n        }\n\n        String s = (String) o;\n        int i = s.indexOf(\"${\");\n        return i >= 0 && s.indexOf(\"}\", i) > i;\n    }\n\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    static Object deepConvert(Object o) {\n        if (o instanceof Seq) {\n            List<Object> src = ((Seq) o).toList();\n\n            List<Object> dst = new ArrayList<>(src.size());\n            for (Object s : src) {\n                dst.add(deepConvert(s));\n            }\n\n            return dst;\n        } else if (o instanceof Map) {\n            Map<Object, Object> src = (Map<Object, Object>) o;\n\n            Map<Object, Object> dst = new LinkedHashMap<>(src.size());\n            for (Map.Entry<Object, Object> e : src.entrySet()) {\n                dst.put(e.getKey(), deepConvert(e.getValue()));\n            }\n\n            return dst;\n        }\n\n        return o;\n    }\n\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    default ELCall createELCall(String task, Object args) {\n        StringBuilder b = new StringBuilder(\"${\");\n        b.append(task).append(\".call(\");\n\n        Set<VariableMapping> maps = new HashSet<>();\n\n        if (args != null) {\n            args = deepConvert(args);\n\n            if (args instanceof List) {\n                int idx = 0;\n                for (Iterator<Object> i = ((List) args).iterator(); i.hasNext(); ) {\n                    String k = \"__\" + idx++;\n                    Object v = i.next();\n                    maps.add(new VariableMapping(null, null, v, k, true));\n\n                    b.append(k);\n                    if (i.hasNext()) {\n                        b.append(\", \");\n                    }\n                }\n            } else {\n                String k = \"__0\";\n                if (isExpression(args)) {\n                    String s = args.toString().trim();\n                    maps.add(new VariableMapping(null, s, null, k));\n                } else {\n                    maps.add(new VariableMapping(null, null, args, k, true));\n                }\n                b.append(k);\n            }\n        }\n\n        b.append(\")}\");\n\n        return new ELCall(b.toString(), maps.isEmpty() ? null : maps);\n    }\n\n    class ELCall implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        private final String expression;\n        private final Set<VariableMapping> args;\n\n        private ELCall(String expression, Set<VariableMapping> args) {\n            this.expression = expression;\n            this.args = args;\n        }\n\n        public String getExpression() {\n            return expression;\n        }\n\n        public Set<VariableMapping> getArgs() {\n            return args;\n        }\n    }\n\n    @Named(\"__withItemsUtils\")\n    class WithItemsUtilsTask implements Task {\n        private static final String CURRENT_INDEX = \"__currentWithItemIndex\";\n        private static final String HAS_NEXT = \"__withItemsHasNext\";\n        private static final String ITEM = \"item\";\n        private static final String ITEM_HISTORY = \"__withItemsItem\";\n\n        public void init(ExecutionContext ctx) {\n            List<Integer> currentIndex = getList(ctx, CURRENT_INDEX);\n            currentIndex.add(0);\n            ctx.setVariable(CURRENT_INDEX, currentIndex);\n\n            List<Boolean> hasNext = getList(ctx, HAS_NEXT);\n            hasNext.add(false);\n            ctx.setVariable(HAS_NEXT, hasNext);\n\n            ctx.setVariable(\"__withItems_keysBeforeTask\", new HashSet<>(ctx.toMap().keySet()));\n\n            // store current item variable\n            List<Object> item = getList(ctx, ITEM_HISTORY);\n            item.add(ctx.getVariable(ITEM));\n            ctx.setVariable(ITEM_HISTORY, item);\n        }\n\n        /**\n         * Checks for an item to process next. Will set variables within the execution context with the results.\n         * <ul>\n         * <li><code>__withItemsHasNext</code> - boolean - true if there's an item to process</li>\n         * <li><code>item</code> - Object - item to be processed</li>\n         * </ul>\n         *\n         * @param ctx context containing execution variables\n         */\n        @SuppressWarnings(\"unused\")\n        public void nextItem(ExecutionContext ctx) {\n            int currentItemIndex = getLastVariable(ctx, CURRENT_INDEX);\n\n            List<Object> items = assertItems(ctx);\n\n            if (currentItemIndex < items.size()) {\n                ctx.setVariable(ITEM, items.get(currentItemIndex));\n            }\n\n            setLastVariable(ctx, HAS_NEXT, currentItemIndex < items.size());\n            currentItemIndex++;\n            setLastVariable(ctx, CURRENT_INDEX, currentItemIndex);\n        }\n\n        @SuppressWarnings(\"unused\")\n        public boolean hasNext(ExecutionContext ctx) {\n            return getLastVariable(ctx, HAS_NEXT);\n        }\n\n        @SuppressWarnings(\"unused\")\n        public void processOutVars(ExecutionContext ctx, Set<String> taskOutVars) {\n            // restore current item variable\n            ctx.setVariable(ITEM, getLastVariable(ctx, ITEM_HISTORY));\n\n            Set<String> taskVariables = taskOutVars;\n            if (taskVariables.isEmpty()) {\n                taskVariables = collectOutVars(ctx);\n            }\n\n            taskVariables.forEach(v -> {\n                String tmpVarName = \"__withItems_\" + v;\n                List<Object> results = getList(ctx, tmpVarName);\n                results.add(ctx.getVariable(v));\n                ctx.setVariable(tmpVarName, results);\n            });\n        }\n\n        @SuppressWarnings(\"unused\")\n        public void cleanup(ExecutionContext ctx, Set<String> taskOutVars) {\n            clearLastVariable(ctx, CURRENT_INDEX);\n            ctx.removeVariable(ITEM);\n            clearLastVariable(ctx, HAS_NEXT);\n\n            Set<String> taskVariables = taskOutVars;\n            if (taskVariables.isEmpty()) {\n                taskVariables = collectOutVars(ctx);\n            }\n            ctx.removeVariable(\"__withItems_keysBeforeTask\");\n\n            clearLastVariable(ctx, ITEM_HISTORY);\n\n            taskVariables.forEach(v -> {\n                String tmpVarName = \"__withItems_\" + v;\n                Object var = ctx.getVariable(tmpVarName);\n                ctx.removeVariable(tmpVarName);\n                ctx.setVariable(v, var);\n            });\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        private static Set<String> collectOutVars(ExecutionContext ctx) {\n            Set<String> before = (Set<String>) ctx.getVariable(\"__withItems_keysBeforeTask\");\n            return ctx.toMap().keySet().stream()\n                    .filter(v -> !v.startsWith(\"__\"))\n                    .filter(v -> !v.equals(ITEM))\n                    .filter(v -> !before.contains(v))\n                    .collect(Collectors.toSet());\n        }\n\n        @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n        private static List<Object> assertItems(ExecutionContext ctx) {\n            Object result = ctx.getVariable(\"items\");\n            if (result == null) {\n                return new ArrayList<>(0);\n            }\n\n            if (result instanceof List) {\n                return (List<Object>) result;\n            }\n\n            if (result.getClass().isArray()) {\n                return Arrays.asList((Object[]) result);\n            }\n\n            if (result instanceof Map) {\n                return new ArrayList<>(((Map) result).entrySet());\n            }\n\n            throw new IllegalArgumentException(\"'withItems' values should be a list or an array, got: \" + result.getClass());\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        private static <E> List<E> getList(ExecutionContext ctx, String name) {\n            Object v = ctx.getVariable(name);\n            if (v == null) {\n                return new ArrayList<>();\n            }\n\n            if (v instanceof List) {\n                return (List<E>) v;\n            }\n\n            if (v.getClass().isArray()) {\n                return Arrays.asList((E[]) v);\n            }\n\n            throw new IllegalArgumentException(\"expected list with name '\" + name + \"', but got: \" + v);\n        }\n\n        private static <E> E getLastVariable(ExecutionContext ctx, String name) {\n            List<E> v = getList(ctx, name);\n            return v.get(v.size() - 1);\n        }\n\n        private static void setLastVariable(ExecutionContext ctx, String name, Object value) {\n            List<Object> v = getList(ctx, name);\n            v.set(v.size() - 1, value);\n            ctx.setVariable(name, v);\n        }\n\n        private static void clearLastVariable(ExecutionContext ctx, String name) {\n            List<Object> v = getList(ctx, name);\n            v.remove(v.size() - 1);\n            if (v.isEmpty()) {\n                ctx.removeVariable(name);\n            } else {\n                ctx.setVariable(name, v);\n            }\n        }\n    }\n\n    @Named(\"__retryUtils\")\n    class RetryUtilsTask implements Task {\n\n        private static final Logger log = LoggerFactory.getLogger(RetryUtilsTask.class);\n\n        public void sleep(ExecutionContext ctx) {\n            int t = assertInt(ctx, \"__retryDelay\");\n\n            try {\n                log.info(\"retry delay {} sec\", t);\n                Thread.sleep(TimeUnit.SECONDS.toMillis(t));\n            } catch (InterruptedException e) {\n                Thread.currentThread().interrupt();\n            }\n        }\n\n        private int assertInt(ExecutionContext ctx, String name) {\n            Object v = ctx.getVariable(name);\n            if (v == null) {\n                throw new IllegalArgumentException(\"Variable '\" + name + \"' not found\");\n            }\n            if (v instanceof Number) {\n                return ((Number) v).intValue();\n            }\n            if (v instanceof String) {\n                return Integer.parseInt((String)v);\n            }\n            throw new IllegalArgumentException(\"Invalid variable '\" + name + \"' type, expected: integer, got: \" + v.getClass());\n        }\n\n        @SuppressWarnings(\"unused\")\n        public void throwLastError(ExecutionContext ctx) throws Throwable {\n            clearLastVariable(ctx, InternalConstants.Context.RETRY_COUNTER);\n\n            Object lastError = ctx.getVariable(Constants.Context.LAST_ERROR_KEY);\n            if (lastError instanceof BpmnError) {\n                BpmnError e = (BpmnError) lastError;\n                if (e.getCause() != null) {\n                    throw e.getCause();\n                } else {\n                    throw e;\n                }\n            }\n\n            throw new RuntimeException(\"Retry count exceeded\");\n        }\n\n        public void init(ExecutionContext ctx) {\n            List<Integer> retryCounter = getList(ctx, InternalConstants.Context.RETRY_COUNTER);\n            retryCounter.add(0);\n            ctx.setVariable(InternalConstants.Context.RETRY_COUNTER, retryCounter);\n\n            ctx.setVariable(InternalConstants.Context.CURRENT_RETRY_COUNTER, 0);\n        }\n\n        public void inc(ExecutionContext ctx) {\n            int currentValue = getLastVariable(ctx, InternalConstants.Context.RETRY_COUNTER, 0);\n            currentValue++;\n            setLastVariable(ctx, InternalConstants.Context.RETRY_COUNTER, currentValue);\n            ctx.setVariable(InternalConstants.Context.CURRENT_RETRY_COUNTER, currentValue);\n            int maxRetryCount = assertInt(ctx, \"__maxRetryCount\");\n            ctx.setVariable(\"__retryCountExceeded\", currentValue <= maxRetryCount);\n        }\n\n        @SuppressWarnings(\"unused\")\n        public boolean isRetryCountExceeded(ExecutionContext ctx) {\n            return (boolean) ctx.getVariable(\"__retryCountExceeded\");\n        }\n\n        @SuppressWarnings(\"unused\")\n        public void cleanup(ExecutionContext ctx) {\n            clearLastVariable(ctx, InternalConstants.Context.RETRY_COUNTER);\n            ctx.removeVariable(InternalConstants.Context.CURRENT_RETRY_COUNTER);\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        private static <E> List<E> getList(ExecutionContext ctx, String name) {\n            Object v = ctx.getVariable(name);\n            if (v == null) {\n                return new ArrayList<>();\n            }\n\n            if (v instanceof List) {\n                return (List<E>) v;\n            }\n\n            throw new IllegalArgumentException(\"Expected a list with name '\" + name + \"', got: \" + v);\n        }\n\n        private static <E> E getLastVariable(ExecutionContext ctx, String name, E defaultValue) {\n            List<E> v = getList(ctx, name);\n            if (v.isEmpty()) {\n                return defaultValue;\n            }\n            return v.get(v.size() - 1);\n        }\n\n        private static void setLastVariable(ExecutionContext ctx, String name, Object value) {\n            List<Object> v = getList(ctx, name);\n            if (v.isEmpty()) {\n                v.add(value);\n            } else {\n                v.set(v.size() - 1, value);\n            }\n            ctx.setVariable(name, v);\n        }\n\n        private static void clearLastVariable(ExecutionContext ctx, String name) {\n            List<Object> v = getList(ctx, name);\n            v.remove(v.size() - 1);\n            if (v.isEmpty()) {\n                ctx.removeVariable(name);\n            } else {\n                ctx.setVariable(name, v);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/converter/YamlCallConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml.converter;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.yaml.YamlConverterException;\nimport com.walmartlabs.concord.project.yaml.model.YamlCall;\nimport io.takari.bpm.model.CallActivity;\nimport io.takari.bpm.model.SequenceFlow;\nimport io.takari.bpm.model.VariableMapping;\n\nimport java.util.Map;\nimport java.util.Set;\n\npublic class YamlCallConverter implements StepConverter<YamlCall> {\n\n    /**\n     * with items:\n     *         /<-----------------------/\n     * -> nextItem -> callActivity -> gw ->\n     *\n     *\n     * @param ctx\n     * @param s\n     * @return\n     * @throws YamlConverterException\n     */\n    @Override\n    public Chunk convert(ConverterContext ctx, YamlCall s) throws YamlConverterException {\n        Set<VariableMapping> inVars = getVarMap(s.getOptions(), \"in\");\n        Set<VariableMapping> outVars = getVarMap(s.getOptions(), \"out\");\n\n        Chunk c = new Chunk();\n        String id = ctx.nextId();\n\n        String calledElement = null;\n        String calledElementExpression = null;\n        if (StepConverter.isExpression(s.getKey())) {\n            calledElementExpression = s.getKey();\n        } else {\n            calledElement = s.getKey();\n        }\n        c.addElement(new CallActivity(id, calledElement, calledElementExpression, inVars, outVars, true));\n        c.addSourceMap(id, toSourceMap(s, \"Flow call: \" + s.getKey()));\n        c.addOutput(id);\n\n        applyErrorBlock(ctx, c, id, s.getOptions());\n\n        applyRetryBlock(ctx, c, id, s.getLocation(), s.getOptions(), (retryDelayId, retryParams) -> retryCall(ctx, s, c, retryDelayId, retryParams));\n\n        return applyWithItems(ctx, c, s.getOptions());\n    }\n\n    private String retryCall(ConverterContext ctx, YamlCall s, Chunk c, String retryDelayId, Map<String, Object> retryParams) {\n        String element = null;\n        String elementExpression = null;\n        if (StepConverter.isExpression(s.getKey())) {\n            elementExpression = s.getKey();\n        } else {\n            element = s.getKey();\n        }\n\n        String retryCallId = ctx.nextId();\n        c.addElement(new SequenceFlow(ctx.nextId(), retryDelayId, retryCallId));\n        c.addElement(new CallActivity(retryCallId, element, elementExpression,\n                getInVars(s.getOptions(), retryParams), getVarMap(s.getOptions(), \"out\"), true));\n        c.addSourceMap(retryCallId, toSourceMap(s, \"Flow call: \" + s.getKey() + \" (retry)\"));\n\n        return retryCallId;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/converter/YamlCheckpointConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml.converter;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.yaml.YamlConverterException;\nimport com.walmartlabs.concord.project.yaml.model.YamlCheckpoint;\nimport io.takari.bpm.model.ExpressionType;\nimport io.takari.bpm.model.ServiceTask;\nimport io.takari.bpm.model.VariableMapping;\n\nimport java.util.HashSet;\nimport java.util.Set;\n\npublic class YamlCheckpointConverter implements StepConverter<YamlCheckpoint> {\n\n    @Override\n    public Chunk convert(ConverterContext ctx, YamlCheckpoint s) throws YamlConverterException {\n        Chunk c = new Chunk();\n        String id = ctx.nextId();\n\n        String checkpointName = s.getName();\n\n        Set<VariableMapping> inVars = new HashSet<>();\n        inVars.add(new VariableMapping(null, checkpointName, null, \"checkpointName\", true));\n\n        Set<VariableMapping> outVars = new HashSet<>();\n        outVars.add(new VariableMapping(\"checkpointId\", null, null, \"checkpointId\", true));\n        outVars.add(new VariableMapping(\"checkpointName\", null, null, \"checkpointName\", true));\n\n        c.addElement(new ServiceTask(id, ExpressionType.DELEGATE, \"${checkpoint}\", inVars, outVars, true));\n\n        c.addSourceMap(id, toSourceMap(s, \"Checkpoint: \" + checkpointName));\n        c.addOutput(id);\n\n        return c;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/converter/YamlDockerStepConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml.converter;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.yaml.YamlConverterException;\nimport com.walmartlabs.concord.project.yaml.model.YamlDockerStep;\nimport io.takari.bpm.model.ExpressionType;\nimport io.takari.bpm.model.ServiceTask;\nimport io.takari.bpm.model.VariableMapping;\n\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Set;\n\npublic class YamlDockerStepConverter implements StepConverter<YamlDockerStep> {\n\n    @Override\n    public Chunk convert(ConverterContext ctx, YamlDockerStep s) throws YamlConverterException {\n        Chunk c = new Chunk();\n\n        String id = ctx.nextId();\n\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"image\", s.getImage());\n        args.put(\"forcePull\", s.isForcePull());\n        args.put(\"debug\", s.isDebug());\n        args.put(\"cmd\", s.getCmd());\n        args.put(\"env\", s.getEnv());\n        args.put(\"envFile\", s.getEnvFile());\n        args.put(\"hosts\", s.getHosts());\n        args.put(\"stdout\", s.getStdout());\n        args.put(\"stderr\", s.getStderr());\n\n        Set<VariableMapping> inVars = createVarsMapping(args);\n\n        c.addElement(new ServiceTask(id, ExpressionType.DELEGATE, \"${docker}\", inVars, null, true));\n        c.addOutput(id);\n        c.addSourceMap(id, toSourceMap(s, \"Docker: \" + s.getImage()));\n\n        return c;\n    }\n\n    private Set<VariableMapping> createVarsMapping(Map<String, Object> args) {\n        Set<VariableMapping> result = new HashSet<>();\n        \n        args.forEach((key, value) -> {\n            String sourceExpr = null;\n            Object sourceValue = null;\n\n            Object v = StepConverter.deepConvert(value);\n            if (StepConverter.isExpression(v)) {\n                sourceExpr = v.toString();\n            } else {\n                sourceValue = v;\n            }\n\n            result.add(new VariableMapping(null, sourceExpr, sourceValue, key, true));\n        });\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/converter/YamlEventConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml.converter;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.yaml.YamlConverterException;\nimport com.walmartlabs.concord.project.yaml.model.YamlEvent;\nimport io.takari.bpm.model.IntermediateCatchEvent;\n\npublic class YamlEventConverter implements StepConverter<YamlEvent> {\n\n    @Override\n    public Chunk convert(ConverterContext ctx, YamlEvent s) throws YamlConverterException {\n        Chunk c = new Chunk();\n\n        String id = ctx.nextId();\n        c.addElement(new IntermediateCatchEvent(id, s.getName()));\n        c.addOutput(id);\n        c.addSourceMap(id, toSourceMap(s, \"Event: \" + s.getName()));\n\n        return c;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/converter/YamlExitConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml.converter;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.yaml.YamlConverterException;\nimport com.walmartlabs.concord.project.yaml.model.YamlExit;\nimport io.takari.bpm.model.TerminateEvent;\n\npublic class YamlExitConverter implements StepConverter<YamlExit> {\n\n    @Override\n    public Chunk convert(ConverterContext ctx, YamlExit s) throws YamlConverterException {\n        Chunk c = new Chunk();\n\n        String id = ctx.nextId();\n        c.addElement(new TerminateEvent(id));\n        c.addSourceMap(id, toSourceMap(s, \"Exit\"));\n\n        return c;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/converter/YamlExpressionStepConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml.converter;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.yaml.YamlConverterException;\nimport com.walmartlabs.concord.project.yaml.model.YamlExpressionStep;\nimport io.takari.bpm.model.ExpressionType;\nimport io.takari.bpm.model.ServiceTask;\nimport io.takari.bpm.model.VariableMapping;\n\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.Set;\n\npublic class YamlExpressionStepConverter implements StepConverter<YamlExpressionStep> {\n\n    @Override\n    public Chunk convert(ConverterContext ctx, YamlExpressionStep s) throws YamlConverterException {\n        Chunk c = new Chunk();\n\n        Set<VariableMapping> outVars = null;\n        Map<String, Object> opts = s.getOptions();\n        if (opts != null) {\n            String out = (String) opts.get(\"out\");\n            if (out != null) {\n                outVars = Collections.singleton(new VariableMapping(ServiceTask.EXPRESSION_RESULT_VAR, null, out));\n            }\n        }\n\n        String id = ctx.nextId();\n        c.addElement(new ServiceTask(id, ExpressionType.SIMPLE, s.getExpr(), null, outVars, true));\n        c.addOutput(id);\n        c.addSourceMap(id, toSourceMap(s, \"Expression: \" + s.getExpr()));\n\n        applyErrorBlock(ctx, c, id, s.getOptions());\n\n        return c;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/converter/YamlFormCallConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml.converter;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\nimport com.walmartlabs.concord.project.InternalConstants;\nimport com.walmartlabs.concord.project.yaml.YamlConverterException;\nimport com.walmartlabs.concord.project.yaml.model.YamlFormCall;\nimport io.takari.bpm.model.UserTask;\nimport io.takari.bpm.model.form.FormExtension;\n\nimport java.util.*;\n\npublic class YamlFormCallConverter implements StepConverter<YamlFormCall> {\n\n    private static final List<String> SUPPORTED_FORM_OPTIONS = Arrays.asList(InternalConstants.Forms.YIELD_KEY,\n            InternalConstants.Forms.RUN_AS_KEY,\n            InternalConstants.Forms.VALUES_KEY,\n            InternalConstants.Forms.FIELDS_KEY,\n            InternalConstants.Forms.SAVE_SUBMITTED_BY_KEY);\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public Chunk convert(ConverterContext ctx, YamlFormCall s) throws YamlConverterException {\n        Chunk c = new Chunk();\n        String id = ctx.nextId();\n\n        Map<String, Object> opts = (Map<String, Object>) StepConverter.deepConvert(s.getOptions());\n        if (opts != null && opts.isEmpty()) {\n            opts = null;\n        }\n\n        validate(opts, s.getLocation());\n\n        String formId = null;\n        String formIdExpression = null;\n        if (StepConverter.isExpression(s.getKey())) {\n            formIdExpression = s.getKey();\n        } else {\n            formId = s.getKey();\n        }\n\n        c.addElement(new UserTask(id, new FormExtension(formId, formIdExpression, opts)));\n        c.addOutput(id);\n        c.addSourceMap(id, toSourceMap(s, \"Form: \" + s.getKey()));\n\n        return c;\n    }\n\n    private static void validate(Map<String, Object> opts, JsonLocation loc) {\n        if (opts == null) {\n            return;\n        }\n\n        Set<String> keys = new HashSet<>(opts.keySet());\n        keys.removeAll(SUPPORTED_FORM_OPTIONS);\n\n        if (keys.isEmpty()) {\n            return;\n        }\n\n        throw new IllegalArgumentException(\"'\" + keys + \"' are not supported options for a form. Supported options: \"\n                + SUPPORTED_FORM_OPTIONS + \". Error in a form step @:\" + loc);\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/converter/YamlGroupConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml.converter;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.yaml.YamlConverterException;\nimport com.walmartlabs.concord.project.yaml.model.YamlGroup;\nimport io.takari.bpm.model.AbstractElement;\nimport io.takari.bpm.model.SubProcess;\n\nimport java.util.List;\n\npublic class YamlGroupConverter implements StepConverter<YamlGroup> {\n\n    @Override\n    public Chunk convert(ConverterContext ctx, YamlGroup s) throws YamlConverterException {\n        Chunk c = new Chunk();\n\n        // create a subprocess\n        Chunk sub = ctx.convert(s.getSteps());\n        List<AbstractElement> l = ctx.wrapAsProcess(sub);\n\n        // add the subprocess\n        String id = ctx.nextId();\n        c.addElement(new SubProcess(id, l));\n        c.addOutput(id);\n        c.addSourceMap(id, toSourceMap(s, \"Group of steps\"));\n\n        // keep the subprocess' source map\n        c.addSourceMaps(sub.getSourceMap());\n\n        applyErrorBlock(ctx, c, id, s.getOptions());\n\n        return c;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/converter/YamlIfExprConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml.converter;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.yaml.YamlConverterException;\nimport com.walmartlabs.concord.project.yaml.model.YamlIfExpr;\nimport com.walmartlabs.concord.project.yaml.model.YamlStep;\nimport io.takari.bpm.model.ExclusiveGateway;\nimport io.takari.bpm.model.SequenceFlow;\nimport io.takari.parc.Seq;\n\npublic class YamlIfExprConverter implements StepConverter<YamlIfExpr> {\n\n    @Override\n    public Chunk convert(ConverterContext ctx, YamlIfExpr s) throws YamlConverterException {\n        Chunk c = new Chunk();\n\n        String gwId = ctx.nextId();\n        c.addElement(new ExclusiveGateway(gwId));\n        c.addSourceMap(gwId, toSourceMap(s, \"Check: \" + s.getExpr()));\n\n        // \"then\" branch\n        Chunk thenChunk = ctx.convert(s.getThenSteps());\n\n        // connect \"then\" steps with the gateway\n        String thenDst = thenChunk.firstElement().getId();\n        c.addElement(new SequenceFlow(ctx.nextId(), gwId, thenDst, s.getExpr()));\n        c.addElements(thenChunk.getElements());\n        c.addOutputs(thenChunk.getOutputs());\n        c.addSourceMaps(thenChunk.getSourceMap());\n\n        // \"else\" branch\n        applyElseBlock(ctx, c, gwId, s.getElseSteps());\n\n        return c;\n    }\n\n    private static void applyElseBlock(ConverterContext ctx, Chunk c, String gwId, Seq<YamlStep> steps) throws YamlConverterException {\n        Chunk elseChunk = steps != null ? ctx.convert(steps) : null;\n        if (elseChunk == null || elseChunk.isEmpty()) {\n            c.addOutput(gwId);\n            return;\n        }\n\n        // connect \"else\" steps with the gateway\n        String elseDst = elseChunk.firstElement().getId();\n        c.addElement(new SequenceFlow(ctx.nextId(), gwId, elseDst));\n        c.addElements(elseChunk.getElements());\n\n        // output of the \"else\" branch\n        c.addOutputs(elseChunk.getOutputs());\n\n        c.addSourceMaps(elseChunk.getSourceMap());\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/converter/YamlReturnConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml.converter;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.yaml.YamlConverterException;\nimport com.walmartlabs.concord.project.yaml.model.YamlReturn;\nimport io.takari.bpm.model.EndEvent;\n\npublic class YamlReturnConverter implements StepConverter<YamlReturn> {\n\n    @Override\n    public Chunk convert(ConverterContext ctx, YamlReturn s) throws YamlConverterException {\n        Chunk c = new Chunk();\n\n        String id = ctx.nextId();\n        c.addElement(new EndEvent(id, s.getErrorCode()));\n        c.addSourceMap(id, toSourceMap(s, \"Return from a flow (code: \" + s.getErrorCode() + \")\"));\n\n        // skip adding an output, it should be the last element of a branch\n\n        return c;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/converter/YamlScriptConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml.converter;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.yaml.YamlConverterException;\nimport com.walmartlabs.concord.project.yaml.model.YamlScript;\nimport io.takari.bpm.model.ScriptTask;\n\nimport java.util.Map;\n\npublic class YamlScriptConverter implements StepConverter<YamlScript> {\n\n    @Override\n    public Chunk convert(ConverterContext ctx, YamlScript s) throws YamlConverterException {\n        Chunk c = new Chunk();\n\n        String id = ctx.nextId();\n        Map<String, Object> options = s.getOptions();\n        String body = (String) options.get(\"body\");\n\n        if(body != null) {\n            c.addElement(new ScriptTask(id, ScriptTask.Type.CONTENT, s.getName(), body, true));\n        } else {\n            c.addElement(new ScriptTask(id, ScriptTask.Type.REFERENCE, null, s.getName(), true));\n        }\n\n        c.addOutput(id);\n        c.addSourceMap(id, toSourceMap(s, \"Script\"));\n\n        applyErrorBlock(ctx, c, id, s.getOptions());\n\n        return c;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/converter/YamlSetVariablesStepConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml.converter;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.yaml.YamlConverterException;\nimport com.walmartlabs.concord.project.yaml.model.YamlSetVariablesStep;\nimport io.takari.bpm.model.ExpressionType;\nimport io.takari.bpm.model.ServiceTask;\nimport io.takari.bpm.model.VariableMapping;\n\nimport java.util.Collections;\nimport java.util.Set;\n\npublic class YamlSetVariablesStepConverter implements StepConverter<YamlSetVariablesStep> {\n\n    @Override\n    public Chunk convert(ConverterContext ctx, YamlSetVariablesStep s) throws YamlConverterException {\n        Chunk c = new Chunk();\n\n        String id = ctx.nextId();\n        String expression = \"${vars.set(execution, __0)}\";\n\n        Object vars = StepConverter.deepConvert(s.getVariables());\n        Set<VariableMapping> inVars = Collections.singleton(new VariableMapping(null, null, vars, \"__0\", false));\n\n        c.addElement(new ServiceTask(id, ExpressionType.SIMPLE, expression, inVars, null, true));\n        c.addOutput(id);\n        c.addSourceMap(id, toSourceMap(s, \"Set variables\"));\n\n        return c;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/converter/YamlSwitchExprConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml.converter;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.yaml.KV;\nimport com.walmartlabs.concord.project.yaml.YamlConverterException;\nimport com.walmartlabs.concord.project.yaml.model.YamlStep;\nimport com.walmartlabs.concord.project.yaml.model.YamlSwitchExpr;\nimport io.takari.bpm.model.*;\nimport io.takari.parc.Seq;\n\nimport java.util.Collections;\nimport java.util.Set;\n\npublic class YamlSwitchExprConverter implements StepConverter<YamlSwitchExpr> {\n\n    @Override\n    public Chunk convert(ConverterContext ctx, YamlSwitchExpr s) throws YamlConverterException {\n        Chunk c = new Chunk();\n\n        // task for switch expression\n        String id = ctx.nextId();\n        String switchExprResultVarName = \"__switch_expr_result_\" + id;\n        Set<VariableMapping> outVars = Collections.singleton(new VariableMapping(ServiceTask.EXPRESSION_RESULT_VAR, null, switchExprResultVarName));\n        c.addElement(new ServiceTask(id, ExpressionType.SIMPLE, s.getExpr(), null, outVars, true));\n        c.addSourceMap(id, toSourceMap(s, \"Switch expression: \" + s.getExpr()));\n\n        String gwId = ctx.nextId();\n        c.addElement(new ExclusiveGateway(gwId));\n        c.addElement(new SequenceFlow(ctx.nextId(), id, gwId));\n        c.addSourceMap(gwId, toSourceMap(s, \"Switch: \" + s.getExpr()));\n\n        Seq<YamlStep> defaultSteps = null;\n        for(KV<String, Seq<YamlStep>> v : s.getCaseSteps().toList()) {\n            String caseKey = v.getKey();\n            if(\"default\".equals(caseKey)) {\n                defaultSteps = v.getValue();\n                continue;\n            }\n            Chunk caseChunk = ctx.convert(v.getValue());\n\n            String dst = caseChunk.firstElement().getId();\n            c.addElement(new SequenceFlow(ctx.nextId(), gwId, dst, buildCaseExpression(caseKey, switchExprResultVarName)));\n            c.addElements(caseChunk.getElements());\n            c.addOutputs(caseChunk.getOutputs());\n            c.addSourceMaps(caseChunk.getSourceMap());\n        }\n\n        // \"default\" case\n        applyDefaultBlock(ctx, c, gwId, defaultSteps);\n\n        return c;\n    }\n\n    private static String buildCaseExpression(String caseKey, String switchExprResultVarName) {\n        String op;\n        if(StepConverter.isExpression(caseKey)) {\n            op = removeExpressionSymbols(caseKey);\n        } else {\n            op = \"'\" + caseKey + \"'\";\n        }\n\n        return \"${\" + op + \" == \" + switchExprResultVarName + \"}\";\n    }\n\n    private static String removeExpressionSymbols(String expr) {\n        int i = expr.indexOf(\"${\");\n        return expr.substring(i + \"${\".length(), expr.indexOf(\"}\", i));\n    }\n\n    private static void applyDefaultBlock(ConverterContext ctx, Chunk c, String gwId, Seq<YamlStep> steps) throws YamlConverterException {\n        Chunk defaultChunk = steps != null ? ctx.convert(steps) : null;\n        if (defaultChunk == null || defaultChunk.isEmpty()) {\n            c.addOutput(gwId);\n            return;\n        }\n\n        String dst = defaultChunk.firstElement().getId();\n        c.addElement(new SequenceFlow(ctx.nextId(), gwId, dst));\n        c.addElements(defaultChunk.getElements());\n\n        // output of the \"default\" case\n        c.addOutputs(defaultChunk.getOutputs());\n\n        c.addSourceMaps(defaultChunk.getSourceMap());\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/converter/YamlTaskShortStepConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml.converter;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.yaml.YamlConverterException;\nimport com.walmartlabs.concord.project.yaml.model.YamlTaskShortStep;\nimport io.takari.bpm.model.ExpressionType;\nimport io.takari.bpm.model.ServiceTask;\n\npublic class YamlTaskShortStepConverter implements StepConverter<YamlTaskShortStep> {\n\n    @Override\n    public Chunk convert(ConverterContext ctx, YamlTaskShortStep s) throws YamlConverterException {\n        Chunk c = new Chunk();\n\n        ELCall call = createELCall(s.getKey(), s.getArg());\n\n        String id = ctx.nextId();\n        c.addElement(new ServiceTask(id, ExpressionType.SIMPLE, call.getExpression(), call.getArgs(), null, true));\n        c.addOutput(id);\n        c.addSourceMap(id, toSourceMap(s, \"Task: \" + s.getKey()));\n\n        return c;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/converter/YamlTaskStepConverter.java",
    "content": "package com.walmartlabs.concord.project.yaml.converter;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.yaml.YamlConverterException;\nimport com.walmartlabs.concord.project.yaml.model.YamlTaskStep;\nimport io.takari.bpm.model.ExpressionType;\nimport io.takari.bpm.model.SequenceFlow;\nimport io.takari.bpm.model.ServiceTask;\nimport io.takari.bpm.model.VariableMapping;\n\nimport java.util.Map;\nimport java.util.Set;\n\npublic class YamlTaskStepConverter implements StepConverter<YamlTaskStep> {\n\n    @Override\n    public Chunk convert(ConverterContext ctx, YamlTaskStep s) throws YamlConverterException {\n        Chunk c = new Chunk();\n\n        Set<VariableMapping> inVars = getVarMap(s.getOptions(), \"in\");\n        Set<VariableMapping> outVars = getVarMap(s.getOptions(), \"out\");\n\n        String id = ctx.nextId();\n        String expr = \"${\" + s.getKey() + \"}\";\n        c.addElement(new ServiceTask(id, ExpressionType.DELEGATE, expr, inVars, outVars, true));\n        c.addOutput(id);\n        c.addSourceMap(id, toSourceMap(s, \"Task: \" + s.getKey()));\n\n        Map<String, Object> opts = s.getOptions();\n        if (opts != null && opts.get(\"error\") != null && opts.get(\"retry\") != null) {\n            throw new YamlConverterException(\"'error' and 'retry' options are mutually exclusive @ \" + s.getLocation());\n        }\n\n        applyErrorBlock(ctx, c, id, s.getOptions());\n\n        applyRetryBlock(ctx, c, id, s.getLocation(), s.getOptions(), (retryDelayId, retryParams) -> retryTask(ctx, s, c, opts, retryDelayId, retryParams));\n\n        return applyWithItems(ctx, c, s.getOptions());\n    }\n\n    private String retryTask(ConverterContext ctx, YamlTaskStep s, Chunk c, Map<String, Object> opts, String retryDelayId, Map<String, Object> retryParams) {\n        // retry task\n        String retryTaskId = ctx.nextId();\n        c.addElement(new SequenceFlow(ctx.nextId(), retryDelayId, retryTaskId));\n        c.addElement(new ServiceTask(retryTaskId, ExpressionType.DELEGATE,\n                \"${\" + s.getKey() + \"}\",\n                getInVars(opts, retryParams), getVarMap(opts, \"out\"), true));\n        c.addSourceMap(retryTaskId, toSourceMap(s, \"Task: \" + s.getKey() + \" (retry)\"));\n\n        return retryTaskId;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlCall.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\n\nimport java.util.Map;\n\npublic class YamlCall extends YamlStep {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String key;\n    private final Map<String, Object> options;\n\n    public YamlCall(JsonLocation location, String key, Map<String, Object> options) {\n        super(location);\n        this.key = key;\n        this.options = options;\n    }\n\n    public String getKey() {\n        return key;\n    }\n\n    public Map<String, Object> getOptions() {\n        return options;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlCall{\" +\n                \"key='\" + key + '\\'' +\n                \", options=\" + options +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlCheckpoint.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\n\npublic class YamlCheckpoint extends YamlStep {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String name;\n\n    public YamlCheckpoint(JsonLocation location, String name) {\n        super(location);\n        this.name = name;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlCheckpoint{\" +\n                \"name='\" + name + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlDefinition.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\n\npublic interface YamlDefinition extends Serializable {\n\n    String getName();\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlDefinitionFile.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonUnwrapped;\n\nimport java.io.Serializable;\nimport java.util.Map;\n\npublic class YamlDefinitionFile implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Map<String, YamlDefinition> definitions;\n\n    @JsonCreator\n    public YamlDefinitionFile(@JsonUnwrapped Map<String, YamlDefinition> definitions) {\n        this.definitions = definitions;\n    }\n\n    public Map<String, YamlDefinition> getDefinitions() {\n        return definitions;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlDefinitionFile{\" +\n                \"definitions=\" + definitions +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlDockerStep.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\n\nimport java.util.List;\nimport java.util.Map;\n\npublic class YamlDockerStep extends YamlStep {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String image;\n    private final String cmd;\n    private final boolean forcePull;\n    private final boolean debug;\n    private final Map<String, Object> env;\n    private final String envFile;\n    private final List<String> hosts;\n    private final String stdout;\n    private final String stderr;\n\n    public YamlDockerStep(JsonLocation location,\n                          String image,\n                          String cmd,\n                          boolean forcePull,\n                          boolean debug,\n                          Map<String, Object> env,\n                          String envFile,\n                          List<String> hosts,\n                          String stdout,\n                          String stderr) {\n\n        super(location);\n\n        this.image = image;\n        this.cmd = cmd;\n        this.forcePull = forcePull;\n        this.debug = debug;\n        this.env = env;\n        this.envFile = envFile;\n        this.hosts = hosts;\n        this.stdout = stdout;\n        this.stderr = stderr;\n    }\n\n    public String getImage() {\n        return image;\n    }\n\n    public String getCmd() {\n        return cmd;\n    }\n\n    public boolean isForcePull() {\n        return forcePull;\n    }\n\n    public boolean isDebug() {\n        return debug;\n    }\n\n    public Map<String, Object> getEnv() {\n        return env;\n    }\n\n    public String getEnvFile() {\n        return envFile;\n    }\n\n    public List<String> getHosts() {\n        return hosts;\n    }\n\n    public String getStdout() {\n        return stdout;\n    }\n\n    public String getStderr() {\n        return stderr;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlDockerStep{\" +\n                \"image='\" + image + '\\'' +\n                \", cmd='\" + cmd + '\\'' +\n                \", forcePull=\" + forcePull +\n                \", debug=\" + debug +\n                \", env=\" + env +\n                \", envFile='\" + envFile + '\\'' +\n                \", hosts=\" + hosts +\n                \", stdout='\" + stdout + '\\'' +\n                \", stderr='\" + stderr + '\\'' +\n                \"} \" + super.toString();\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlEvent.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\n\npublic class YamlEvent extends YamlStep {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String name;\n\n    public YamlEvent(JsonLocation location, String name) {\n        super(location);\n        this.name = name;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlEvent{\" +\n                \"name='\" + name + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlExit.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\n\npublic class YamlExit extends YamlStep {\n\n    private static final long serialVersionUID = 1L;\n\n    public YamlExit(JsonLocation location) {\n        super(location);\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlExit{}\";\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlExpressionStep.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\n\nimport java.util.Collections;\nimport java.util.Map;\n\npublic class YamlExpressionStep extends YamlStep {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String expr;\n    private final Map<String, Object> options;\n\n    public YamlExpressionStep(JsonLocation location, String expr) {\n        this(location, expr, Collections.emptyMap());\n    }\n\n    public YamlExpressionStep(JsonLocation location, String expr, Map<String, Object> options) {\n        super(location);\n        this.expr = expr;\n        this.options = options;\n    }\n\n    public String getExpr() {\n        return expr;\n    }\n\n    public Map<String, Object> getOptions() {\n        return options;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlExpressionStep{\" +\n                \"expr='\" + expr + '\\'' +\n                \", options=\" + options +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlFormCall.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\n\nimport java.util.Map;\n\npublic class YamlFormCall extends YamlStep {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String key;\n    private final Map<String, Object> options;\n\n    public YamlFormCall(JsonLocation location, String key, Map<String, Object> options) {\n        super(location);\n        this.key = key;\n        this.options = options;\n    }\n\n    public String getKey() {\n        return key;\n    }\n\n    public Map<String, Object> getOptions() {\n        return options;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlFormCall{\" +\n                \"key='\" + key + '\\'' +\n                \", options=\" + options +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlFormDefinition.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.takari.parc.Seq;\n\npublic class YamlFormDefinition implements YamlDefinition {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String name;\n    private final Seq<YamlFormField> fields;\n\n    public YamlFormDefinition(Seq<YamlFormField> fields) {\n        this(null, fields);\n    }\n\n    public YamlFormDefinition(String name, Seq<YamlFormField> fields) {\n        this.name = name;\n        this.fields = fields;\n    }\n\n    @Override\n    public String getName() {\n        return name;\n    }\n\n    public Seq<YamlFormField> getFields() {\n        return fields;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlFormDefinition{\" +\n                \"name='\" + name + '\\'' +\n                \", fields=\" + fields +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlFormField.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.core.JsonLocation;\n\nimport java.io.Serializable;\nimport java.util.Map;\n\npublic class YamlFormField implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final JsonLocation location;\n    private final String name;\n    private final Map<String, Object> options;\n\n    public YamlFormField(JsonLocation location, String name, Map<String, Object> options) {\n        this.location = location;\n        this.name = name;\n        this.options = options;\n    }\n\n    public JsonLocation getLocation() {\n        return location;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public Map<String, Object> getOptions() {\n        return options;\n    }\n\n    @JsonIgnore\n    public Object getOption(String k) {\n        if (options == null) {\n            return null;\n        }\n        return options.get(k);\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlFormField{\" +\n                \"name='\" + name + '\\'' +\n                \", options=\" + options +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlGroup.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\nimport io.takari.parc.Seq;\n\nimport java.util.Map;\n\npublic class YamlGroup extends YamlStep {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Seq<YamlStep> steps;\n    private final Map<String, Object> options;\n\n    public YamlGroup(JsonLocation location, Seq<YamlStep> steps, Map<String, Object> options) {\n        super(location);\n        this.steps = steps;\n        this.options = options;\n    }\n\n    public Seq<YamlStep> getSteps() {\n        return steps;\n    }\n\n    public Map<String, Object> getOptions() {\n        return options;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlGroup{\" +\n                \"steps=\" + steps +\n                \", options=\" + options +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlIfExpr.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\nimport io.takari.parc.Seq;\n\npublic class YamlIfExpr extends YamlStep {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String expr;\n    private final Seq<YamlStep> thenSteps;\n    private final Seq<YamlStep> elseSteps;\n\n    public YamlIfExpr(JsonLocation location, String expr, Seq<YamlStep> thenSteps, Seq<YamlStep> elseSteps) {\n        super(location);\n        this.expr = expr;\n        this.thenSteps = thenSteps;\n        this.elseSteps = elseSteps;\n    }\n\n    public String getExpr() {\n        return expr;\n    }\n\n    public Seq<YamlStep> getThenSteps() {\n        return thenSteps;\n    }\n\n    public Seq<YamlStep> getElseSteps() {\n        return elseSteps;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlIfExpr{\" +\n                \"expr='\" + expr + '\\'' +\n                \", thenSteps=\" + thenSteps +\n                \", elseSteps=\" + elseSteps +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlImport.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\n\nimport java.io.Serializable;\nimport java.util.Map;\n\npublic class YamlImport implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final JsonLocation location;\n\n    private final String type;\n\n    private final Map<String, Object> options;\n\n    public YamlImport(JsonLocation location, String type, Map<String, Object> options) {\n        this.location = location;\n        this.type = type;\n        this.options = options;\n    }\n\n    public JsonLocation getLocation() {\n        return location;\n    }\n\n    public String getType() {\n        return type;\n    }\n\n    public Map<String, Object> getOptions() {\n        return options;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlImport{\" +\n                \"type='\" + type + '\\'' +\n                \", options=\" + options +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlProcessDefinition.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.takari.parc.Seq;\n\npublic class YamlProcessDefinition implements YamlDefinition {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String name;\n    private final Seq<YamlStep> steps;\n\n    public YamlProcessDefinition(Seq<YamlStep> steps) {\n        this(null, steps);\n    }\n\n    public YamlProcessDefinition(String name, Seq<YamlStep> steps) {\n        this.name = name;\n        this.steps = steps;\n    }\n\n    @Override\n    public String getName() {\n        return name;\n    }\n\n    public Seq<YamlStep> getSteps() {\n        return steps;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlProcessDefinition{\" +\n                \"name='\" + name + '\\'' +\n                \", steps=\" + steps +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlProfile.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.io.Serializable;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic class YamlProfile implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Map<String, List<YamlStep>> flows;\n    private final Set<String> publicFlows;\n    private final Map<String, List<YamlFormField>> forms;\n    private final Map<String, Object> configuration;\n\n    @JsonCreator\n    public YamlProfile(@JsonProperty(\"flows\") Map<String, List<YamlStep>> flows,\n                       @JsonProperty(\"publicFlows\") Set<String> publicFlows,\n                       @JsonProperty(\"forms\") Map<String, List<YamlFormField>> forms,\n                       @JsonProperty(\"configuration\") Map<String, Object> configuration,\n                       @JsonProperty(\"variables\") Map<String, Object> variables) {\n\n        this.flows = flows;\n        this.publicFlows = publicFlows;\n        this.forms = forms;\n\n        // alias \"variables\" to \"configuration\"\n        if (configuration != null) {\n            this.configuration = configuration;\n        } else {\n            this.configuration = variables;\n        }\n    }\n\n    public Map<String, List<YamlStep>> getFlows() {\n        return flows;\n    }\n\n    public Set<String> getPublicFlows() {\n        return publicFlows;\n    }\n\n    public Map<String, List<YamlFormField>> getForms() {\n        return forms;\n    }\n\n    public Map<String, Object> getConfiguration() {\n        return configuration;\n    }\n\n    private static <K, V> Map<K, List<V>> removeNullElements(Map<K, List<V>> items) {\n        if (items == null) {\n            return null;\n        }\n\n        Map<K, List<V>> result = new HashMap<>();\n        items.forEach((k, v) -> {\n            if (v == null) {\n                return;\n            }\n\n            List<V> l = v.stream()\n                    .filter(Objects::nonNull)\n                    .collect(Collectors.toList());\n\n            result.put(k, l);\n        });\n\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlProfile{\" +\n                \"flows=\" + flows +\n                \", publicFlows=\" + publicFlows +\n                \", forms=\" + forms +\n                \", configuration=\" + configuration +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlProfileFile.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonUnwrapped;\n\nimport java.io.Serializable;\nimport java.util.Map;\n\npublic class YamlProfileFile implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Map<String, YamlProfile> profiles;\n\n    @JsonCreator\n    public YamlProfileFile(@JsonUnwrapped Map<String, YamlProfile> profiles) {\n        this.profiles = profiles;\n    }\n\n    public Map<String, YamlProfile> getProfiles() {\n        return profiles;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlProfileFile{\" +\n                \"profiles=\" + profiles +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlProject.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\npublic class YamlProject extends YamlProfile {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Map<String, YamlProfile> profiles;\n    private final List<YamlTrigger> triggers;\n    private final List<YamlImport> imports;\n    private final Map<String, Object> resources;\n\n    @JsonCreator\n    public YamlProject(@JsonProperty(\"flows\") Map<String, List<YamlStep>> flows,\n                       @JsonProperty(\"publicFlows\") Set<String> publicFlows,\n                       @JsonProperty(\"forms\") Map<String, List<YamlFormField>> forms,\n                       @JsonProperty(\"configuration\") Map<String, Object> configuration,\n                       @JsonProperty(\"variables\") Map<String, Object> variables,\n                       @JsonProperty(\"profiles\") Map<String, YamlProfile> profiles,\n                       @JsonProperty(\"triggers\") List<YamlTrigger> triggers,\n                       @JsonProperty(\"imports\") List<YamlImport> imports,\n                       @JsonProperty(\"resources\") Map<String, Object> resources) {\n\n        super(flows, publicFlows, forms, configuration, variables);\n        this.profiles = profiles;\n        this.triggers = triggers;\n        this.imports = imports;\n        this.resources = resources;\n    }\n\n    public Map<String, YamlProfile> getProfiles() {\n        return profiles;\n    }\n\n    public List<YamlTrigger> getTriggers() {\n        return triggers;\n    }\n\n    public List<YamlImport> getImports() {\n        return imports;\n    }\n\n    public Map<String, Object> getResources() {\n        return resources;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlProject{\" +\n                \"profiles=\" + profiles +\n                \", triggers=\" + triggers +\n                \", imports=\" + imports +\n                \", resources=\" + resources +\n                \"} \" + super.toString();\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlReturn.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\n\npublic class YamlReturn extends YamlStep {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String errorCode;\n\n    public YamlReturn(JsonLocation location) {\n        this(location, null);\n    }\n\n    public YamlReturn(JsonLocation location, String errorCode) {\n        super(location);\n        this.errorCode = errorCode;\n    }\n\n    public String getErrorCode() {\n        return errorCode;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlReturn{\" +\n                \"errorCode='\" + errorCode + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlScript.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\n\nimport java.util.Map;\n\npublic class YamlScript extends YamlStep {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String name;\n    private final Map<String, Object> options;\n\n    public YamlScript(JsonLocation location, String name, Map<String, Object> options) {\n        super(location);\n        this.name = name;\n        this.options = options;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public Map<String, Object> getOptions() {\n        return options;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlScript{\" +\n                \"name='\" + name + '\\'' +\n                \", options=\" + options +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlSetVariablesStep.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\n\nimport java.util.Map;\n\npublic class YamlSetVariablesStep extends YamlStep {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Map<String, Object> variables;\n\n    public YamlSetVariablesStep(JsonLocation location, Map<String, Object> variables) {\n        super(location);\n\n        this.variables = variables;\n    }\n\n    public Map<String, Object> getVariables() {\n        return variables;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlStep.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\n\nimport java.io.Serializable;\n\npublic abstract class YamlStep implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final JsonLocation location;\n\n    protected YamlStep(JsonLocation location) {\n        this.location = location;\n    }\n\n    public JsonLocation getLocation() {\n        return location;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlSwitchExpr.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\nimport com.walmartlabs.concord.project.yaml.KV;\nimport io.takari.parc.Seq;\n\npublic class YamlSwitchExpr extends YamlStep {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String expr;\n    private final Seq<KV<String, Seq<YamlStep>>> caseSteps;\n\n    public YamlSwitchExpr(JsonLocation location, String expr, Seq<KV<String, Seq<YamlStep>>> options) {\n        super(location);\n        this.expr = expr;\n        this.caseSteps = options;\n    }\n\n    public String getExpr() {\n        return expr;\n    }\n\n    public Seq<KV<String, Seq<YamlStep>>> getCaseSteps() {\n        return caseSteps;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlSwitchExpr{\" +\n                \"expr='\" + expr + '\\'' +\n                \", caseSteps=\" + caseSteps +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlTaskShortStep.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\n\npublic class YamlTaskShortStep extends YamlStep {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String key;\n    private final Object arg;\n\n    public YamlTaskShortStep(JsonLocation location, String key, Object arg) {\n        super(location);\n        this.key = key;\n        this.arg = arg;\n    }\n\n    public String getKey() {\n        return key;\n    }\n\n    public Object getArg() {\n        return arg;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlTaskShortStep{\" +\n                \"key='\" + key + '\\'' +\n                \", arg=\" + arg +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlTaskStep.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\n\nimport java.util.Map;\n\npublic class YamlTaskStep extends YamlStep {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String key;\n    private final Map<String, Object> options;\n\n    public YamlTaskStep(JsonLocation location, String key, Map<String, Object> options) {\n        super(location);\n        this.key = key;\n        this.options = options;\n    }\n\n    public String getKey() {\n        return key;\n    }\n\n    public Map<String, Object> getOptions() {\n        return options;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlTaskStep{\" +\n                \"key='\" + key + '\\'' +\n                \", options=\" + options +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/model/YamlTrigger.java",
    "content": "package com.walmartlabs.concord.project.yaml.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\n\nimport java.io.Serializable;\nimport java.util.Map;\n\npublic class YamlTrigger implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final JsonLocation location;\n    private final String eventSource;\n    private final Map<String, Object> options;\n\n    public YamlTrigger(JsonLocation location, String eventSource, Map<String, Object> options) {\n        this.location = location;\n        this.eventSource = eventSource;\n        this.options = options;\n    }\n\n    public String getEventSource() {\n        return eventSource;\n    }\n\n    public Map<String, Object> getOptions() {\n        return options;\n    }\n\n    public JsonLocation getLocation() {\n        return location;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlTrigger{\" +\n                \"location=\" + location +\n                \", eventSource='\" + eventSource + '\\'' +\n                \", options=\" + options +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/validator/StepValidator.java",
    "content": "package com.walmartlabs.concord.project.yaml.validator;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.yaml.model.YamlStep;\n\npublic interface StepValidator<T extends YamlStep> {\n\n    void validate(ValidatorContext ctx, T step);\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/validator/Validator.java",
    "content": "package com.walmartlabs.concord.project.yaml.validator;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.yaml.model.*;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic class Validator {\n\n    private static final Map<Class, StepValidator> validators = new HashMap<>();\n\n    static {\n        validators.put(YamlCheckpoint.class, new YamlCheckpointValidator());\n    }\n\n    public static void validate(ValidatorContext ctx, YamlProcessDefinition def) {\n        if (def.getSteps() == null) {\n            return;\n        }\n\n        validateFlowSteps(ctx, def.getName(), def.getSteps().toList());\n    }\n\n    public static void validate(ValidatorContext ctx, YamlProject prj) {\n        validateFlows(ctx, prj.getFlows());\n        validateForms(prj.getForms());\n\n    }\n\n    private static void validateFlows(ValidatorContext ctx, Map<String, List<YamlStep>> flows) {\n        if (flows == null) {\n            return;\n        }\n\n        flows.forEach((name, steps) -> validateFlowSteps(ctx, name, steps));\n    }\n\n    private static void validateFlowSteps(ValidatorContext ctx, String name, List<YamlStep> steps) {\n        if (steps == null) {\n            throw new IllegalArgumentException(\"Flow -> \" + name + \" does not have any step to execute\");\n        }\n\n        steps.forEach(s -> validate(ctx, s));\n    }\n\n    private static void validateForms(Map<String, List<YamlFormField>> forms) {\n        if (forms == null) {\n            return;\n        }\n        forms.forEach(Validator::validateFormFields);\n    }\n\n    private static void validateFormFields(String name, List<YamlFormField> fields) {\n        if (fields == null) {\n            throw new IllegalArgumentException(\"Form -> \" + name + \" does not contain any field\");\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static void validate(ValidatorContext ctx, YamlStep s) {\n        StepValidator v = validators.get(s.getClass());\n        if (v != null) {\n            v.validate(ctx, s);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/validator/ValidatorContext.java",
    "content": "package com.walmartlabs.concord.project.yaml.validator;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class ValidatorContext {\n    private final Map<String, Map<String, JsonLocation>> counters = new HashMap<>();\n\n    public void assertUnique(String counterName, String key, JsonLocation location) {\n        Map<String, JsonLocation> keys = counters.computeIfAbsent(counterName, k -> new HashMap<>());\n        JsonLocation old = keys.put(key, location);\n        if (old != null) {\n            throw new IllegalArgumentException(counterName + \" '\" + key + \"' @:\" + location + \" already defined at @:\" + old);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/project/yaml/validator/YamlCheckpointValidator.java",
    "content": "package com.walmartlabs.concord.project.yaml.validator;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.yaml.model.YamlCheckpoint;\n\npublic class YamlCheckpointValidator implements StepValidator<YamlCheckpoint> {\n\n    public void validate(ValidatorContext ctx, YamlCheckpoint s) {\n        ctx.assertUnique(\"checkpoint\", s.getName(), s.getLocation());\n    }\n}"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/runtime/v1/ProjectLoaderV1.java",
    "content": "package com.walmartlabs.concord.runtime.v1;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.ImportManager;\nimport com.walmartlabs.concord.imports.ImportsListener;\nimport com.walmartlabs.concord.process.loader.ImportsNormalizer;\nimport com.walmartlabs.concord.process.loader.ProjectLoader;\nimport com.walmartlabs.concord.repository.Snapshot;\nimport com.walmartlabs.concord.runtime.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.v1.wrapper.ProcessDefinitionV1;\n\nimport javax.inject.Inject;\nimport java.nio.file.Path;\nimport java.util.List;\n\nimport static com.walmartlabs.concord.process.loader.StandardRuntimeTypes.CONCORD_V1_RUNTIME_TYPE;\nimport static java.util.Objects.requireNonNull;\n\npublic class ProjectLoaderV1 implements ProjectLoader {\n\n    private final com.walmartlabs.concord.project.ProjectLoader v1;\n\n    @Inject\n    public ProjectLoaderV1(ImportManager importManager) {\n        this.v1 = new com.walmartlabs.concord.project.ProjectLoader(requireNonNull(importManager));\n    }\n\n    @Override\n    public boolean supports(String runtime) {\n        return CONCORD_V1_RUNTIME_TYPE.equals(runtime);\n    }\n\n    @Override\n    public Result loadProject(Path workDir, String runtime, ImportsNormalizer importsNormalizer, ImportsListener listener) throws Exception {\n        var v1Result = v1.loadProject(workDir, importsNormalizer::normalize, listener);\n        return toCommonResultType(v1Result);\n    }\n\n    private static Result toCommonResultType(com.walmartlabs.concord.project.ProjectLoader.Result r) {\n        var snapshots = r.getSnapshots();\n        var pd = new ProcessDefinitionV1(r.getProjectDefinition());\n\n        return new Result() {\n            @Override\n            public List<Snapshot> snapshots() {\n                return snapshots;\n            }\n\n            @Override\n            public ProcessDefinition projectDefinition() {\n                return pd;\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/runtime/v1/wrapper/ConfigurationV1.java",
    "content": "package com.walmartlabs.concord.runtime.v1.wrapper;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Configuration;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\n\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\npublic class ConfigurationV1 implements Configuration, Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Map<String, Object> values;\n\n    public ConfigurationV1(Map<String, Object> values) {\n        this.values = values;\n    }\n\n    @Override\n    public List<String> dependencies() {\n        return MapUtils.getList(values, Constants.Request.DEPENDENCIES_KEY, Collections.emptyList());\n    }\n\n    @Override\n    public List<String> extraDependencies() {\n        return Collections.emptyList();\n    }\n\n    @Override\n    public Map<String, Object> asMap() {\n        return values;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/runtime/v1/wrapper/EffectiveProcessDefinitionProviderV1.java",
    "content": "package com.walmartlabs.concord.runtime.v1.wrapper;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.EffectiveProcessDefinitionProvider;\nimport com.walmartlabs.concord.runtime.model.Options;\n\nimport java.io.OutputStream;\n\npublic class EffectiveProcessDefinitionProviderV1 implements EffectiveProcessDefinitionProvider {\n\n    private static final byte[] BANNER = \"# the effective Concord YAML feature is currently supported only for the 'concord-v2' runtime\".getBytes();\n\n    @Override\n    public void serialize(Options options, OutputStream out) throws Exception {\n        out.write(BANNER);\n        out.flush();\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/runtime/v1/wrapper/FlowDefinitionV1.java",
    "content": "package com.walmartlabs.concord.runtime.v1.wrapper;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.ExpressionStep;\nimport com.walmartlabs.concord.runtime.model.FlowDefinition;\nimport com.walmartlabs.concord.runtime.model.Step;\nimport com.walmartlabs.concord.runtime.model.TaskCallStep;\nimport io.takari.bpm.model.*;\n\nimport java.io.Serializable;\nimport java.util.*;\n\npublic class FlowDefinitionV1 implements FlowDefinition {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String name;\n    private final List<Step> steps;\n\n    public FlowDefinitionV1(ProcessDefinition pd) {\n        this.name = pd.getName();\n\n        this.steps = new ArrayList<>();\n        if (pd.getChildren() != null) {\n            pd.getChildren().forEach(c -> {\n                SourceMap sm = null;\n                if (pd instanceof SourceAwareProcessDefinition) {\n                    SourceAwareProcessDefinition sapd = (SourceAwareProcessDefinition) pd;\n                    sm = sapd.getSourceMaps().get(c.getId());\n                }\n\n                Step step = toStep(c, sm);\n                if (step != null) {\n                    this.steps.add(step);\n                }\n            });\n        }\n    }\n\n    @Override\n    public String name() {\n        return name;\n    }\n\n    @Override\n    public List<Step> steps() {\n        return steps;\n    }\n\n    private static Step toStep(AbstractElement element, SourceMap sm) {\n        if (element instanceof ServiceTask) {\n            ServiceTask task = (ServiceTask) element;\n            if (task.getType() == ExpressionType.DELEGATE) {\n                return TaskCallStep.builder()\n                        .name(task.getExpression())\n                        .input(toInput(task.getIn()))\n                        .location(new SourceMapV1(sm))\n                        .build();\n            } else if (task.getType() == ExpressionType.SIMPLE) {\n                return ExpressionStep.builder()\n                        .expression(task.getExpression())\n                        .input(toInput(task.getIn()))\n                        .location(new SourceMapV1(sm))\n                        .build();\n            }\n        }\n        return null;\n    }\n\n    private static Map<String, Serializable> toInput(Set<VariableMapping> in) {\n        if (in == null || in.isEmpty()) {\n            return Collections.emptyMap();\n        }\n\n        Map<String, Serializable> result = new LinkedHashMap<>();\n        for (VariableMapping m : in) {\n            Serializable value = null;\n            if (m.getSourceValue() != null) {\n                value = (Serializable)m.getSourceValue();\n            } else if (m.getSourceExpression() != null) {\n                value = m.getSourceExpression();\n            }\n            result.put(m.getTarget(), value);\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/runtime/v1/wrapper/ProcessDefinitionV1.java",
    "content": "package com.walmartlabs.concord.runtime.v1.wrapper;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.runtime.model.*;\nimport io.takari.bpm.model.form.FormDefinition;\n\nimport java.io.Serializable;\nimport java.util.*;\n\n/**\n * Adapter class for v1 ProjectDefinition.\n * {@see com.walmartlabs.concord.project.model.ProjectDefinition}\n */\npublic class ProcessDefinitionV1 extends EffectiveProcessDefinitionProviderV1 implements ProcessDefinition, Serializable {\n\n    private static final long serialVersionUID = 1;\n\n    private final Configuration cfg;\n    private final Map<String, FlowDefinition> flows;\n    private final Set<String> publicFlows;\n    private final Map<String, Profile> profiles;\n    private final List<Trigger> triggers;\n    private final Imports imports;\n    private final List<Form> forms;\n\n    public ProcessDefinitionV1(com.walmartlabs.concord.project.model.ProjectDefinition delegate) {\n        this.cfg = new ConfigurationV1(delegate.getConfiguration());\n\n        this.flows = new HashMap<>();\n        if (delegate.getFlows() != null) {\n            delegate.getFlows().forEach((k, v) -> flows.put(k, new FlowDefinitionV1(v)));\n        }\n\n        this.publicFlows = new HashSet<>();\n        if (delegate.getPublicFlows() != null) {\n            publicFlows.addAll(delegate.getPublicFlows());\n        }\n\n        this.profiles = new HashMap<>();\n        if (delegate.getProfiles() != null) {\n            delegate.getProfiles().forEach((k, v) -> profiles.put(k, new ProfileV1(v)));\n        }\n\n        this.triggers = new ArrayList<>();\n        if (delegate.getTriggers() != null) {\n            delegate.getTriggers().forEach(t -> triggers.add(new TriggerV1(t)));\n        }\n\n        this.imports = delegate.getImports();\n\n        this.forms = new ArrayList<>();\n        if (delegate.getForms() != null) {\n            delegate.getForms().forEach((formName, formDefinition) -> forms.add(Form.builder()\n                    .name(formName)\n                    .fields(toFields(formDefinition))\n                    .build()));\n        }\n    }\n\n    @Override\n    public String runtime() {\n        return \"concord-v1\"; // TODO constants\n    }\n\n    @Override\n    public Configuration configuration() {\n        return cfg;\n    }\n\n    @Override\n    public Map<String, FlowDefinition> flows() {\n        return flows;\n    }\n\n    @Override\n    public Set<String> publicFlows() {\n        return publicFlows;\n    }\n\n    @Override\n    public Map<String, Profile> profiles() {\n        return profiles;\n    }\n\n    @Override\n    public List<Trigger> triggers() {\n        return triggers;\n    }\n\n    @Override\n    public Imports imports() {\n        return imports;\n    }\n\n    @Override\n    public List<Form> forms() {\n        return forms;\n    }\n\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    private static List<FormField> toFields(FormDefinition form) {\n        if (form.getFields() == null) {\n            return Collections.emptyList();\n        }\n\n        List<FormField> result = new ArrayList<>();\n        form.getFields().forEach(f -> result.add(FormField.builder()\n                .name(f.getName())\n                .label(f.getLabel())\n                .type(f.getType())\n                .defaultValue((Serializable)f.getDefaultValue())\n                .allowedValue((Serializable)f.getAllowedValue())\n                .options(f.getOptions() != null ? (Map)f.getOptions() : Collections.emptyMap())\n                .build()));\n        return result;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/runtime/v1/wrapper/ProfileV1.java",
    "content": "package com.walmartlabs.concord.runtime.v1.wrapper;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Configuration;\nimport com.walmartlabs.concord.runtime.model.FlowDefinition;\nimport com.walmartlabs.concord.runtime.model.Profile;\n\nimport java.io.Serializable;\nimport java.util.*;\n\npublic class ProfileV1 implements Profile, Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Configuration cfg;\n    private final Set<String> publicFlows;\n    private final Map<String, FlowDefinition> flows;\n\n    public ProfileV1(com.walmartlabs.concord.project.model.Profile delegate) {\n        this.cfg = new ConfigurationV1(delegate.getConfiguration());\n\n        this.publicFlows = new HashSet<>(delegate.getPublicFlows() != null ? delegate.getPublicFlows() : Collections.emptySet());\n\n        this.flows = new HashMap<>();\n        if (delegate.getFlows() != null) {\n            delegate.getFlows().forEach((k, v) -> flows.put(k, new FlowDefinitionV1(v)));\n        }\n    }\n\n    @Override\n    public Configuration configuration() {\n        return cfg;\n    }\n\n    @Override\n    public Set<String> publicFlows() {\n        return publicFlows;\n    }\n\n    @Override\n    public Map<String, FlowDefinition> flows() {\n        return flows;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/runtime/v1/wrapper/SourceMapV1.java",
    "content": "package com.walmartlabs.concord.runtime.v1.wrapper;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.SourceMap;\n\nimport java.io.Serializable;\n\npublic class SourceMapV1 implements SourceMap, Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final io.takari.bpm.model.SourceMap delegate;\n\n    public SourceMapV1(io.takari.bpm.model.SourceMap delegate) {\n        this.delegate = delegate;\n    }\n\n    @Override\n    public String source() {\n        return delegate.getSource();\n    }\n\n    @Override\n    public int line() {\n        return delegate.getLine();\n    }\n\n    @Override\n    public int column() {\n        return delegate.getColumn();\n    }\n\n    @Override\n    public String description() {\n        return delegate.getDescription();\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/main/java/com/walmartlabs/concord/runtime/v1/wrapper/TriggerV1.java",
    "content": "package com.walmartlabs.concord.runtime.v1.wrapper;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.SourceMap;\nimport com.walmartlabs.concord.runtime.model.Trigger;\n\nimport java.io.Serializable;\nimport java.util.List;\nimport java.util.Map;\n\npublic class TriggerV1 implements Trigger, Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final com.walmartlabs.concord.project.model.Trigger delegate;\n    private final SourceMap sourceMap;\n\n    public TriggerV1(com.walmartlabs.concord.project.model.Trigger delegate) {\n        this.delegate = delegate;\n        this.sourceMap = new SourceMapV1(delegate.getSourceMap());\n    }\n\n    @Override\n    public String name() {\n        return delegate.getName();\n    }\n\n    @Override\n    public Map<String, Object> arguments() {\n        return delegate.getArguments();\n    }\n\n    @Override\n    public Map<String, Object> conditions() {\n        return delegate.getConditions();\n    }\n\n    @Override\n    public Map<String, Object> configuration() {\n        return delegate.getCfg();\n    }\n\n    @Override\n    public List<String> activeProfiles() {\n        return delegate.getActiveProfiles();\n    }\n\n    @Override\n    public SourceMap sourceMap() {\n        return sourceMap;\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/test/java/com/walmartlabs/concord/project/BrokenTest.java",
    "content": "package com.walmartlabs.concord.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.ImportManager;\nimport com.walmartlabs.concord.imports.ImportsListener;\nimport com.walmartlabs.concord.project.yaml.YamlParserException;\nimport org.junit.jupiter.api.Test;\n\nimport java.nio.file.Paths;\n\nimport static org.junit.jupiter.api.Assertions.assertThrows;\nimport static org.mockito.Mockito.mock;\n\npublic class BrokenTest {\n\n    @Test\n    public void testDir() {\n        assertThrows(YamlParserException.class, () -> loadProject(\"brokenMain\"));\n    }\n\n    @Test\n    public void testStream() {\n        assertThrows(YamlParserException.class, () -> {\n            ProjectLoader l = new ProjectLoader(mock(ImportManager.class));\n            l.loadProject(ClassLoader.getSystemResourceAsStream(\"brokenMain/concord.yml\"));\n        });\n    }\n\n    @Test\n    public void testProfiles() {\n        assertThrows(YamlParserException.class, () -> loadProject(\"brokenProfiles\"));\n    }\n\n    @Test\n    public void testFlows() {\n        assertThrows(YamlParserException.class, () -> loadProject(\"brokenFlows\"));\n    }\n\n    private static void loadProject(String name) throws Exception {\n        ProjectLoader l = new ProjectLoader(mock(ImportManager.class));\n        l.loadProject(Paths.get(ClassLoader.getSystemResource(name).toURI()), new NoopImportsNormalizer(), ImportsListener.NOP_LISTENER);\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/test/java/com/walmartlabs/concord/project/ProjectLoaderTest.java",
    "content": "package com.walmartlabs.concord.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.ImportManager;\nimport com.walmartlabs.concord.imports.ImportsListener;\nimport com.walmartlabs.concord.project.model.ProjectDefinition;\nimport com.walmartlabs.concord.project.yaml.YamlConverterException;\nimport com.walmartlabs.concord.project.yaml.YamlParserException;\nimport org.junit.jupiter.api.Test;\n\nimport java.net.URI;\nimport java.nio.file.Paths;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.Mockito.mock;\n\npublic class ProjectLoaderTest {\n\n    @Test\n    public void testSimple() throws Exception {\n        ProjectLoader loader = new ProjectLoader(mock(ImportManager.class));\n\n        URI uri = ClassLoader.getSystemResource(\"simple\").toURI();\n        ProjectDefinition pd = loader.loadProject(Paths.get(uri), new NoopImportsNormalizer(), ImportsListener.NOP_LISTENER)\n                .getProjectDefinition();\n\n        assertNotNull(pd);\n\n        assertNotNull(pd.getFlows().get(\"main\"));\n        assertNotNull(pd.getFlows().get(\"other\"));\n\n        assertNotNull(pd.getForms().get(\"myForm\"));\n    }\n\n    @Test\n    public void testEmptyField() {\n        assertThrows(YamlConverterException.class, () -> {\n            ProjectLoader loader = new ProjectLoader(mock(ImportManager.class));\n\n            URI uri = ClassLoader.getSystemResource(\"emptyField\").toURI();\n            loader.loadProject(Paths.get(uri), new NoopImportsNormalizer(), ImportsListener.NOP_LISTENER);\n        });\n    }\n\n    @Test\n    public void testDuplicateConfigurationSection() throws Exception {\n        ProjectLoader loader = new ProjectLoader(mock(ImportManager.class));\n\n        URI uri = ClassLoader.getSystemResource(\"duplicateConfiguration\").toURI();\n        try {\n            loader.loadProject(Paths.get(uri), new NoopImportsNormalizer(), ImportsListener.NOP_LISTENER);\n            fail(\"exception expected\");\n        } catch (YamlParserException e) {\n            assertTrue(e.getMessage().contains(\"Duplicate field 'configuration'\"));\n        }\n    }\n\n    @Test\n    public void testDuplicateConfigurationVariable() throws Exception {\n        ProjectLoader loader = new ProjectLoader(mock(ImportManager.class));\n\n        URI uri = ClassLoader.getSystemResource(\"duplicateConfigurationVariable\").toURI();\n        try {\n            loader.loadProject(Paths.get(uri), new NoopImportsNormalizer(), ImportsListener.NOP_LISTENER);\n            fail(\"exception expected\");\n        } catch (YamlParserException e) {\n            assertTrue(e.getMessage().contains(\"Duplicate field 'x'\"));\n        }\n    }\n\n    @Test\n    public void testComplex() throws Exception {\n        ProjectLoader loader = new ProjectLoader(mock(ImportManager.class));\n\n        URI uri = ClassLoader.getSystemResource(\"complex\").toURI();\n        ProjectDefinition pd = loader.loadProject(Paths.get(uri), new NoopImportsNormalizer(), ImportsListener.NOP_LISTENER).getProjectDefinition();\n        assertNotNull(pd);\n\n        //--- triggers\n        assertNotNull(pd.getTriggers());\n        assertEquals(4, pd.getTriggers().size());\n        assertEquals(\"manual\", pd.getTriggers().get(3).getName());\n        assertEquals(\"integration\", pd.getTriggers().get(3).getEntryPoint());\n        assertEquals(\"Run Integration Tests\", pd.getTriggers().get(3).getCfg().get(\"name\"));\n\n        //--- imports\n        assertNotNull(pd.getImports());\n        assertEquals(3, pd.getImports().items().size());\n        assertEquals(\"git\", pd.getImports().items().get(0).type());\n\n        assertNotNull(pd.getResources());\n        assertEquals(1, pd.getResources().getDefinitionPaths().size());\n        assertEquals(\"myFlows\", pd.getResources().getDefinitionPaths().get(0));\n        assertEquals(1, pd.getResources().getDisabledDirs().size());\n        assertEquals(\"processes\", pd.getResources().getDisabledDirs().get(0));\n        assertEquals(1, pd.getResources().getProfilesPaths().size());\n        assertEquals(\"myProfiles\", pd.getResources().getProfilesPaths().get(0));\n    }\n\n    @Test\n    @SuppressWarnings(\"unchecked\")\n    public void testMultiProjectFiles() throws Exception {\n        ProjectLoader loader = new ProjectLoader(mock(ImportManager.class));\n\n        URI uri = ClassLoader.getSystemResource(\"multiProjectFile\").toURI();\n        ProjectDefinition pd = loader.loadProject(Paths.get(uri), new NoopImportsNormalizer(), ImportsListener.NOP_LISTENER).getProjectDefinition();\n        assertNotNull(pd);\n\n        assertNotNull(pd.getFlows().get(\"default\"));\n        assertNotNull(pd.getForms().get(\"myForm\"));\n        assertFalse(pd.getTriggers().isEmpty());\n\n        Map<String, Object> cfg = pd.getConfiguration();\n        assertNotNull(cfg);\n        assertEquals(\"ttt\", ((Map<String, Object>) cfg.get(\"arguments\")).get(\"abc\"));\n        assertEquals(\"234\", ((Map<String, Object>) ((Map<String, Object>) cfg.get(\"arguments\")).get(\"nested\")).get(\"value\"));\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/test/java/com/walmartlabs/concord/project/yaml/AbstractYamlParserTest.java",
    "content": "package com.walmartlabs.concord.project.yaml;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.ImportManager;\nimport com.walmartlabs.concord.project.ProjectLoader;\nimport com.walmartlabs.concord.project.model.ProjectDefinition;\nimport io.takari.bpm.Configuration;\nimport io.takari.bpm.EngineBuilder;\nimport io.takari.bpm.ProcessDefinitionProvider;\nimport io.takari.bpm.api.Engine;\nimport io.takari.bpm.api.ExecutionException;\nimport io.takari.bpm.context.DefaultExecutionContextFactory;\nimport io.takari.bpm.el.DefaultExpressionManager;\nimport io.takari.bpm.el.ExpressionManager;\nimport io.takari.bpm.form.*;\nimport io.takari.bpm.model.ProcessDefinition;\nimport io.takari.bpm.model.form.FormDefinition;\nimport io.takari.bpm.model.form.FormField;\nimport io.takari.bpm.resource.ResourceResolver;\nimport io.takari.bpm.task.ServiceTaskRegistry;\nimport org.junit.jupiter.api.BeforeEach;\n\nimport java.io.InputStream;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static org.mockito.Mockito.mock;\n\npublic abstract class AbstractYamlParserTest {\n\n    private TestWorkflowProvider workflowProvider;\n    private TestServiceTaskRegistry taskRegistry;\n    private Engine engine;\n    private Map<UUID, Form> forms;\n    private FormService formService;\n\n    @BeforeEach\n    @SuppressWarnings(\"unchecked\")\n    public void setUp() {\n        workflowProvider = new TestWorkflowProvider();\n\n        taskRegistry = new TestServiceTaskRegistry();\n\n        forms = new HashMap<>();\n        FormStorage fs = new TestFormStorage(forms);\n\n        ExpressionManager expressionManager = new DefaultExpressionManager(taskRegistry);\n        DefaultExecutionContextFactory contextFactory = new DefaultExecutionContextFactory(expressionManager);\n\n        DefaultFormService.ResumeHandler rs = (form, args) -> getEngine().resume(form.getProcessBusinessKey(), form.getEventName(), args);\n        formService = new DefaultFormService(contextFactory, rs, fs) {\n            @Override\n            public FormField toFormField(Map<String, Object> m) {\n                Map.Entry<String, Object> entry = m.entrySet().iterator().next();\n                Map<String, Object> opts = (Map<String, Object>) entry.getValue();\n                return new FormField.Builder(entry.getKey(), (String) opts.get(\"type\"))\n                        .build();\n            }\n        };\n\n        ResourceResolver resourceResolver = ClassLoader::getSystemResourceAsStream;\n\n        Configuration cfg = new Configuration();\n        cfg.setInterpolateInputVariables(true);\n        cfg.setWrapAllExceptionsAsBpmnErrors(true);\n        cfg.setCopyAllCallActivityOutVariables(true);\n\n        engine = new EngineBuilder()\n                .withDefinitionProvider(workflowProvider.processes())\n                .withTaskRegistry(taskRegistry)\n                .withUserTaskHandler(new FormTaskHandler(contextFactory, workflowProvider.forms(), formService))\n                .withResourceResolver(resourceResolver)\n                .withConfiguration(cfg)\n                .build();\n    }\n\n    protected ProcessDefinition getDefinition(String id) throws ExecutionException {\n        return workflowProvider.processes().getById(id);\n    }\n\n    protected void deploy(String resource) {\n        workflowProvider.deploy(resource);\n    }\n\n    protected void register(String name, Object t) {\n        taskRegistry.register(name, t);\n    }\n\n    protected void start(String processBusinessKey, String processDefinitionId) throws ExecutionException {\n        start(processBusinessKey, processDefinitionId, null);\n    }\n\n    protected void start(String processBusinessKey, String processDefinitionId, Map<String, Object> variables) throws ExecutionException {\n        engine.start(processBusinessKey, processDefinitionId, variables);\n    }\n\n    protected void resume(String processBusinessKey, String eventName) throws ExecutionException {\n        engine.resume(processBusinessKey, eventName, null);\n    }\n\n    protected Form getForm(UUID formId) throws ExecutionException {\n        return formService.get(formId);\n    }\n\n    protected UUID getFirstFormId() {\n        if (forms == null || forms.isEmpty()) {\n            return null;\n        }\n        return forms.keySet().iterator().next();\n    }\n\n    protected FormSubmitResult submitForm(UUID formInstanceId, Map<String, Object> data) throws ExecutionException {\n        return formService.submit(formInstanceId, data);\n    }\n\n    private Engine getEngine() {\n        return engine;\n    }\n\n    private static class TestFormStorage implements FormStorage {\n\n        private final Map<UUID, Form> forms;\n\n        private TestFormStorage(Map<UUID, Form> forms) {\n            this.forms = forms;\n        }\n\n        @Override\n        public void save(Form form) {\n            forms.put(form.getFormInstanceId(), form);\n        }\n\n        @Override\n        public void complete(UUID formInstanceId) {\n            forms.remove(formInstanceId);\n        }\n\n        @Override\n        public Form get(UUID formId) {\n            return forms.get(formId);\n        }\n    }\n\n    private static class TestServiceTaskRegistry implements ServiceTaskRegistry {\n\n        private final Map<String, Object> entries = new HashMap<>();\n\n        public void register(String k, Object v) {\n            entries.put(k, v);\n        }\n\n        @Override\n        public Object getByKey(String k) {\n            return entries.get(k);\n        }\n    }\n\n    private static class TestWorkflowProvider {\n\n        private final ProjectLoader projectLoader = new ProjectLoader(mock(ImportManager.class));\n        private final Map<String, ProcessDefinition> processes = new HashMap<>();\n        private final Map<String, FormDefinition> forms = new HashMap<>();\n\n        public void deploy(String resource) {\n            ProjectDefinition wd = loadWorkflow(resource);\n            processes.putAll(wd.getFlows());\n            forms.putAll(wd.getForms());\n        }\n\n        public ProcessDefinitionProvider processes() {\n            return processes::get;\n        }\n\n        public FormDefinitionProvider forms() {\n            return forms::get;\n        }\n\n        private ProjectDefinition loadWorkflow(String resource) {\n            try (InputStream in = ClassLoader.getSystemResourceAsStream(resource)) {\n                return projectLoader.loadProject(in).getProjectDefinition();\n            } catch (Exception e) {\n                throw new RuntimeException(\"Error while loading a definition\", e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/test/java/com/walmartlabs/concord/project/yaml/DiagramPrint.java",
    "content": "package com.walmartlabs.concord.project.yaml;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.takari.bpm.model.*;\n\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic class DiagramPrint {\n\n    public static void process(ProcessDefinition pd) {\n        Collection<AbstractElement> elements = getElements(pd.getChildren());\n        elements.forEach(e -> System.out.println(getName(e)));\n\n        Map<String, List<AbstractElement>> a = new HashMap<>();\n        List<AbstractElement> roots = new ArrayList<>();\n\n        for(AbstractElement e : elements) {\n            if(e instanceof SequenceFlow) {\n                continue;\n            }\n\n            List<AbstractElement> parents = findParents(elements, e);\n            if(parents.isEmpty()) {\n                roots.add(e);\n            } else {\n                for(AbstractElement p : parents) {\n                    if (!a.containsKey(p.getId())) {\n                        a.put(p.getId(), new ArrayList<>());\n                    }\n                    a.get(p.getId()).add(e);\n                }\n            }\n        }\n\n        System.out.println(\"---------------------\");\n        print(a, roots);\n        System.out.println(\"---------------------\");\n    }\n\n    private static Collection<AbstractElement> getElements(Collection<AbstractElement> elements) {\n        List<AbstractElement> result = new ArrayList<>();\n        for(AbstractElement e : elements) {\n            result.add(e);\n            if(e instanceof ProcessDefinition) {\n                ProcessDefinition s = (ProcessDefinition) e;\n                result.addAll(getElements(s.getChildren()));\n            }\n        }\n\n        return result;\n    }\n\n    private static void print(Map<String, List<AbstractElement>> nodes, List<AbstractElement> roots) {\n        for(AbstractElement r : roots) {\n            System.out.println(getName(r));\n\n            List<AbstractElement> children = nodes.getOrDefault(r.getId(), new ArrayList<>());\n            for(AbstractElement c : children) {\n                print(c, nodes, \"\", true);\n            }\n            System.out.println(\"===\");\n        }\n    }\n\n    private static void print(AbstractElement node, Map<String, List<AbstractElement>> nodes, String prefix, boolean isTail) {\n        System.out.println(prefix + (isTail ? \"└── \" : \"├── \") + getName(node));\n\n        List<AbstractElement> children = nodes.getOrDefault(node.getId(), new ArrayList<>());\n        for (int i = 0; i < children.size() - 1; i++) {\n            AbstractElement c = children.get(i);\n            print(c, nodes, prefix + (isTail ? \"    \" : \"│   \"), false);\n        }\n        if (children.size() > 0) {\n            AbstractElement c = children.get(children.size() - 1);\n            print(c, nodes, prefix + (isTail ?\"    \" : \"│   \"), true);\n        }\n    }\n\n    private static String getName(AbstractElement node) {\n        String result = node.getId() + \": \";\n        if(node instanceof EndEvent) {\n            result += \"EndEvent (\" + ((EndEvent) node).getErrorRef() + \")\";\n        } else if(node instanceof ServiceTask) {\n            ServiceTask serviceTask = (ServiceTask) node;\n            String expr = serviceTask.getExpression();\n\n            result += \"ServiceTask (\" + expr + \")\";\n        } else if(node instanceof ExclusiveGateway) {\n            ExclusiveGateway gw = (ExclusiveGateway) node;\n            result += \"GW (\" + gw.getDefaultFlow() + \")\";\n        } else if(node instanceof SequenceFlow) {\n            SequenceFlow f = (SequenceFlow) node;\n            result += f.getFrom() + \" -> \" + f.getTo() + \" (\" + f.getExpression() + \")\";\n        } else {\n            result += node.getClass().getName();\n        }\n\n        return result;\n    }\n\n    private static List<AbstractElement> findParents(Collection<AbstractElement> elements, AbstractElement element) {\n        if(element instanceof BoundaryEvent) {\n            String pid = ((BoundaryEvent) element).getAttachedToRef();\n            return new ArrayList<>(Collections.singletonList(find(elements, pid)));\n        }\n        return elements.stream()\n            .filter(e -> e instanceof SequenceFlow)\n            .map(e -> (SequenceFlow)e)\n            .filter(e -> e.getTo().equals(element.getId()))\n            .map(SequenceFlow::getFrom)\n            .map(e -> find(elements, e))\n            .collect(Collectors.toList());\n    }\n\n    private static AbstractElement find(Collection<AbstractElement> elements, String id) {\n        return elements.stream()\n                .filter(e -> e.getId().equals(id))\n                .findFirst()\n                .orElse(null);\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/test/java/com/walmartlabs/concord/project/yaml/YamlParserTest.java",
    "content": "package com.walmartlabs.concord.project.yaml;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.yaml.converter.StepConverter;\nimport com.walmartlabs.concord.project.yaml.converter.YamlTaskStepConverter;\nimport com.walmartlabs.concord.sdk.Task;\nimport io.takari.bpm.api.BpmnError;\nimport io.takari.bpm.api.ExecutionContext;\nimport io.takari.bpm.api.ExecutionException;\nimport io.takari.bpm.api.JavaDelegate;\nimport io.takari.bpm.form.Form;\nimport io.takari.bpm.form.FormSubmitResult;\nimport io.takari.bpm.model.ProcessDefinition;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.parallel.Execution;\nimport org.junit.jupiter.api.parallel.ExecutionMode;\nimport org.mockito.ArgumentCaptor;\n\nimport java.io.Serializable;\nimport java.util.*;\nimport java.util.concurrent.ThreadLocalRandom;\nimport java.util.stream.Collectors;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.ArgumentMatchers.*;\nimport static org.mockito.Mockito.anyInt;\nimport static org.mockito.Mockito.anyString;\nimport static org.mockito.Mockito.isNull;\nimport static org.mockito.Mockito.*;\n\n@Execution(ExecutionMode.CONCURRENT)\npublic class YamlParserTest extends AbstractYamlParserTest {\n\n    // PROCESSES (000 - 099)\n\n    @Test\n    public void test000() {\n        assertThrows(RuntimeException.class, () -> {\n            deploy(\"000.yml\");\n\n            String key = UUID.randomUUID().toString();\n\n            start(key, \"main\", null);\n        });\n    }\n\n    @Test\n    public void test001() throws Exception {\n        deploy(\"001.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        // start -> task -> end\n        assertEquals(5, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\");\n\n        // ---\n\n        verify(testBean, times(1)).hello();\n    }\n\n    @Test\n    public void test002() throws Exception {\n        deploy(\"002.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        // start -> task -> task -> end\n        assertEquals(7, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\");\n\n        // ---\n\n        verify(testBean, times(2)).hello();\n    }\n\n    @Test\n    public void test003() throws Exception {\n        deploy(\"003.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        // start -> task -> task -> end\n        assertEquals(7, pd.getChildren().size());\n\n        String testValue = \"test#\" + System.currentTimeMillis();\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aStr\", testValue);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(testValue));\n        verify(testBean, times(1)).checkString(eq(testValue));\n    }\n\n    @Test\n    public void test004() throws Exception {\n        deploy(\"004.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        // start -> task -> end\n        assertEquals(5, pd.getChildren().size());\n\n        TestTask testTask = spy(new TestTask());\n        register(\"testTask\", testTask);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\");\n\n        // ---\n\n        verify(testTask, times(1)).execute(any(ExecutionContext.class));\n    }\n\n    @Test\n    public void test005() throws Exception {\n        deploy(\"005.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        // start -> task -> task -> task -> task -> end\n        assertEquals(11, pd.getChildren().size());\n\n        String testValue = \"test#\" + System.currentTimeMillis();\n\n        TestTask testTask = spy(new TestTask());\n        register(\"testTask\", testTask);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aStr\", testValue);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testTask, times(1)).call(eq(\"hello\"));\n        verify(testTask, times(1)).call(eq(testValue));\n        verify(testTask, times(1)).call(anyMap());\n        verify(testTask, times(1)).call(eq(1), eq(2));\n    }\n\n    @Test\n    public void test006() throws Exception {\n        deploy(\"006.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        // start -> task -> task -> end\n        assertEquals(7, pd.getChildren().size());\n\n        String testValue = \"test#\" + System.currentTimeMillis();\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        TestTask testTask = spy(new TestTask(\"bStr\", \"cStr\"));\n        register(\"testTask\", testTask);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aStr\", testValue);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testTask, times(1)).execute(any(ExecutionContext.class));\n        verify(testBean, times(1)).toString(eq(testValue));\n    }\n\n    @Test\n    public void test007() throws Exception {\n        deploy(\"007.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //            /-> task -\\   /-> task -\\\n        // start -> gw -> task -> gw -> task -> end\n        assertEquals(17, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aInt\", 100);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"a\"));\n        verify(testBean, times(1)).toString(eq(\"d\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test008() throws Exception {\n        deploy(\"008.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //            /-> task -> end\n        // start -> gw -> task -> task -> end\n        assertEquals(13, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aInt\", 100);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"a\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test009() throws Exception {\n        deploy(\"009.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        // start -> task -> subprocess -> task -> end\n        assertEquals(9, pd.getChildren().size());\n        // subprocess: start -> task -> task -> end\n        assertEquals(7, findSubprocess(pd).getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"a\"));\n        verify(testBean, times(1)).toString(eq(\"b\"));\n        verify(testBean, times(1)).toString(eq(\"c\"));\n        verify(testBean, times(1)).toString(eq(\"d\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test010() throws Exception {\n        deploy(\"010.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //                    /----------------------------->\\\n        // start -> task -> task + boundary-event -> task -> task -> end\n        assertEquals(13, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n\n        verify(testBean, times(1)).throwBpmnError(anyString());\n        verify(testBean, times(1)).toString(eq(\"a\"));\n        verify(testBean, times(1)).toString(eq(\"b\"));\n        verify(testBean, times(1)).toString(eq(\"c\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test011() throws Exception {\n        deploy(\"011.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //            /----------------------------->\\\n        // start -> task + boundary-event -> task -> task -> end\n        assertEquals(11, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        TestErrorTask testErrorTask = spy(new TestErrorTask());\n        register(\"testErrorTask\", testErrorTask);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n\n        verify(testErrorTask, times(1)).execute(any(ExecutionContext.class));\n        verify(testBean, times(1)).toString(eq(\"a\"));\n        verify(testBean, times(1)).toString(eq(\"b\"));\n\n        verifyNoMoreInteractions(testBean);\n        verifyNoMoreInteractions(testErrorTask);\n    }\n\n    @Test\n    public void test012() throws Exception {\n        deploy(\"012.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //                /------------------------------->\\\n        // start -> subprocess + boundary-event -> task -> task -> end\n        assertEquals(11, pd.getChildren().size());\n        // subprocess:\n        // start -> task -> task -> end\n        assertEquals(7, findSubprocess(pd).getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n\n        verify(testBean, times(1)).throwBpmnError(anyString());\n        verify(testBean, times(1)).toString(eq(\"b\"));\n        verify(testBean, times(1)).toString(eq(\"c\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test013() throws Exception {\n        deploy(\"013.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        // start -> task -> callactiviti -> end\n        assertEquals(7, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"a\"));\n        verify(testBean, times(1)).toString(eq(\"b\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test014() throws Exception {\n        deploy(\"014.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //           /------------------->\\\n        // start -> gw -> callactivity -> end\n        assertEquals(8, pd.getChildren().size());\n\n        int loops = 100;\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"cnt\", 0);\n        args.put(\"loops\", loops);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(loops)).inc(anyInt());\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test015() throws Exception {\n        deploy(\"015.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        // start -> task -> end\n        assertEquals(5, pd.getChildren().size());\n\n        String testValue = \"test#\" + System.currentTimeMillis();\n\n        TestTask testTask = spy(new TestTask());\n        register(\"testTask\", testTask);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"interpolation\", testValue);\n        start(key, \"main\", args);\n\n        // ---\n\n        String s = \"multiline test with\\nstring \" + testValue;\n        verify(testTask, times(1)).call(eq(s));\n    }\n\n    @Test\n    public void test016() throws Exception {\n        deploy(\"016.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        // start -> task -> task -> task -> end\n        assertEquals(9, pd.getChildren().size());\n\n        int inputNumber = ThreadLocalRandom.current().nextInt();\n        boolean inputBoolean = ThreadLocalRandom.current().nextBoolean();\n        String inputString = \"test#\" + System.currentTimeMillis();\n\n        TestInterface testTask1 = mock(TestInterface.class);\n        register(\"testTask1\", testTask1);\n\n        TestInterface testTask2 = spy(new TestInterface() {\n            @Override\n            @SuppressWarnings(\"unchecked\")\n            public void call(Object arg1) {\n                List<Object> l = (List<Object>) arg1;\n                assertEquals(inputNumber, l.get(0));\n            }\n        });\n        register(\"testTask2\", testTask2);\n\n        TestInterface testTask3 = spy(new TestInterface() {\n            @Override\n            @SuppressWarnings(\"unchecked\")\n            public void call(Object arg1) {\n                assertNotNull(arg1);\n\n                Map<String, Object> m = (Map<String, Object>) arg1;\n                assertEquals(123, m.get(\"a\"));\n                assertEquals(inputNumber, m.get(\"b\"));\n\n                List<Object> l = (List<Object>) m.get(\"c\");\n                assertEquals(inputBoolean, l.get(0));\n                assertEquals(inputString, l.get(1));\n            }\n        });\n        register(\"testTask3\", testTask3);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"inputNumber\", inputNumber);\n        args.put(\"inputBoolean\", inputBoolean);\n        args.put(\"inputString\", inputString);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testTask1, times(1)).call(eq(inputNumber));\n        verify(testTask2, times(1)).call(any());\n        verify(testTask3, times(1)).call(any());\n    }\n\n    @Test\n    public void test017() throws Exception {\n        deploy(\"017.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        // start -> task -> catchevent -> task -> end\n        assertEquals(9, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        verify(testBean, times(1)).toString(eq(\"a\"));\n        verifyNoMoreInteractions(testBean);\n        reset(testBean);\n\n        // ---\n\n        resume(key, \"ev1\");\n\n        verify(testBean, times(1)).toString(eq(\"b\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test018() throws Exception {\n        deploy(\"018.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        // start -> task -> task -> end\n        assertEquals(7, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"input\", 10);\n        start(key, \"main\", args);\n\n        ArgumentCaptor<Object> ac = ArgumentCaptor.forClass(Object.class);\n        verify(testBean, times(1)).toString(ac.capture());\n\n        Object v = ac.getValue();\n        assertTrue(v.equals(20) || v.equals(20.0)); // Oracle OpenJDK8 vs \"post-Oracle\" OpenJDKs quirks\n    }\n\n    @Test\n    public void test019() throws Exception {\n        deploy(\"019.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        // start -> task -> task -> end\n        assertEquals(7, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        verify(testBean, times(1)).toString(eq(12345));\n    }\n\n    @Test\n    public void test020() throws Exception {\n        deploy(\"020.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        // start -> task -> task -> end\n        assertEquals(7, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        verify(testBean, times(1)).toString(eq(12345));\n    }\n\n    @Test\n    public void test021() throws Exception {\n        deploy(\"021.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //           /--> task ---------> end\n        // start -> gw -> task -> task -> end\n        assertEquals(13, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aInt\", 100);\n\n        start(key, \"main\", args);\n\n        // ---\n        verify(testBean, times(1)).toString(eq(\"a\"));\n        verify(testBean, times(1)).toString(eq(\"c\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test021_2() throws Exception {\n        deploy(\"021_2.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //           /--> task ---------> end\n        // start -> gw -> task -> task -> end\n        assertEquals(13, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aInt\", -100);\n        try {\n            start(key, \"main\", args);\n            fail(\"exception expected\");\n        } catch (ExecutionException e) {\n            assertTrue(e.getMessage().contains(\"error-code-2\"));\n        }\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"b\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test022() throws Exception {\n        deploy(\"022.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //           /--> task ---------> end\n        // start -> gw -> task -> task -> end\n        assertEquals(13, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aInt\", -100);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"b\"));\n        verify(testBean, times(1)).toString(eq(\"c\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test022_2() throws Exception {\n        deploy(\"022_2.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //           /--> task ---------> end\n        // start -> gw -> task -> task -> end\n        assertEquals(13, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aInt\", 100);\n        try {\n            start(key, \"main\", args);\n            fail(\"exception expected\");\n        } catch (ExecutionException e) {\n            assertTrue(e.getMessage().contains(\"error-code\"));\n        }\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"a\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test023() throws Exception {\n        deploy(\"023.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //           /--> task ---------> end\n        // start -> gw -> task -> task -> end\n        assertEquals(13, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aInt\", 100);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"a\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test023_2() throws Exception {\n        deploy(\"023.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //           /--> task ---------> end\n        // start -> gw -> task -> task -> end\n        assertEquals(13, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aInt\", -100);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"b\"));\n        verify(testBean, times(1)).toString(eq(\"c\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test024() throws Exception {\n        deploy(\"024.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //               /-------------------------------->\\\n        // start -> subprocess + boundary-event -> task -> end\n        assertEquals(9, pd.getChildren().size());\n        // subprocess\n        // start -> task -> end\n        assertEquals(5, findSubprocess(pd).getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", Collections.emptyMap());\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"a\"));\n        verify(testBean, times(1)).toString(eq(\"e\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test025() throws Exception {\n        deploy(\"025.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //               /-------------------------------->\\\n        // start -> subprocess + boundary-event -> task -> end\n        assertEquals(9, pd.getChildren().size());\n        // subprocess\n        //                   /----------> end\n        // start -> task -> gw -> task -> end\n        assertEquals(11, findSubprocess(pd).getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aInt\", 1);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"a\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test025_2() throws Exception {\n        deploy(\"025.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //               /-------------------------------->\\\n        // start -> subprocess + boundary-event -> task -> end\n        assertEquals(9, pd.getChildren().size());\n        // subprocess\n        //                   /----------> end\n        // start -> task -> gw -> task -> end\n        assertEquals(11, findSubprocess(pd).getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aInt\", -1);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"a\"));\n        verify(testBean, times(1)).toString(eq(\"else\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test026() throws Exception {\n        deploy(\"026.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //               /-------------------------------->\\\n        // start -> subprocess + boundary-event -> task -> end\n        assertEquals(9, pd.getChildren().size());\n        // subprocess\n        //                   /--> task -> end-error\n        // start -> task -> gw -> task -> end\n        assertEquals(13, findSubprocess(pd).getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aInt\", 1);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"a\"));\n        verify(testBean, times(1)).toString(eq(\"b\"));\n        verify(testBean, times(1)).toString(eq(\"e\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test027() throws Exception {\n        deploy(\"027.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //             /--------------------------------->\\\n        // start -> subprocess + boundary-event -> task -> end\n        assertEquals(9, pd.getChildren().size());\n        // subprocess\n        //                   /--> end\n        // start -> task -> gw -> end\n        assertEquals(9, findSubprocess(pd).getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aInt\", 1);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"a\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test027_2() throws Exception {\n        deploy(\"027.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //             /--------------------------------->\\\n        // start -> subprocess + boundary-event -> task -> end\n        assertEquals(9, pd.getChildren().size());\n        // subprocess\n        //                   /--> end\n        // start -> task -> gw -> end\n        assertEquals(9, findSubprocess(pd).getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aInt\", -1);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"a\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test028() throws Exception {\n        deploy(\"028.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //                         /--> task(\"success\") -> end\n        //                   /--> gw -> task(\"success=2\") -> error-end\n        // start -> task -> gw -> task(\"success=1\") -> end\n        assertEquals(17, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aInt\", -1);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"a\"));\n        verify(testBean, times(1)).toString(eq(\"success\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test028_2() throws Exception {\n        deploy(\"028.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //                         /--> task(\"success\") -> end\n        //                   /--> gw -> task(\"success=2\") -> error-end\n        // start -> task -> gw -> task(\"success=1\") -> end\n        assertEquals(17, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aInt\", 1);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"a\"));\n        verify(testBean, times(1)).toString(eq(\"success=1\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test028_3() throws Exception {\n        deploy(\"028.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //                         /--> task(\"success\") -> end\n        //                   /--> gw -> task(\"success=2\") -> error-end\n        // start -> task -> gw -> task(\"success=1\") -> end\n        assertEquals(17, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aInt\", 2);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"a\"));\n        verify(testBean, times(1)).toString(eq(\"success=2\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test029() throws Exception {\n        deploy(\"029.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //                         /--> task(\"success\")   ->\\\n        //                   /--> gw -> task(\"success=2\") ->\\\n        // start -> task -> gw -> task(\"success=1\")       -> task(\"success=end\") -> end\n        assertEquals(19, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aInt\", 2);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"a\"));\n        verify(testBean, times(1)).toString(eq(\"success=2\"));\n        verify(testBean, times(1)).toString(eq(\"success=end\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test030() throws Exception {\n        deploy(\"030.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //           /--> task -> task ---------------->\\\n        //          /              /--> task -> task -->\\\n        // start -> gw -> task -> gw -> task ----------> end\n        assertEquals(21, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"log\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"name\", \"foo\");\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).info(eq(\"test\"), eq(\"Hello, foo\"));\n        verify(testBean, times(1)).info(eq(\"test -- 3\"), eq(\"Hello, foo\"));\n        verify(testBean, times(1)).info(eq(\"test -- 4\"), eq(\"Hello, foo\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test031() throws Exception {\n        deploy(\"031.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        //                          /--> task -->\\\n        // start -> subprocess + boundary-event -> end\n        assertEquals(9, pd.getChildren().size());\n\n        // subprocess\n        // start -> callactiviti -> end\n        assertEquals(5, findSubprocess(pd).getChildren().size());\n\n        pd = getDefinition(\"myOtherFlow\");\n\n        // callactiviti\n        // start -> task -> end\n        assertEquals(5, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aInt\", 2);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"in-call-activiti\"));\n        verify(testBean, times(1)).toString(eq(\"e\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test032() throws Exception {\n        deploy(\"032.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        register(\"vars\", testBean);\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"aInt\", 2);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"1234\"));\n        verify(testBean, times(1)).toString(eq(\"12341\"));\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    @Test\n    public void test033() throws Exception {\n        String txId = UUID.randomUUID().toString();\n\n        // ---\n\n        deploy(\"033.yml\");\n\n        DockerTask task = spy(new DockerTask());\n        register(\"docker\", task);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"workDir\", \"/tmp\");\n        args.put(\"txId\", txId);\n        start(key, \"main\", args);\n\n        // ---\n\n        ArgumentCaptor<ExecutionContext> captor = ArgumentCaptor.forClass(ExecutionContext.class);\n        verify(task, times(1)).execute(captor.capture());\n\n        ExecutionContext ctx = captor.getValue();\n\n        Map<String, Object> env = (Map<String, Object>) ctx.getVariable(\"env\");\n        assertNotNull(env);\n        assertEquals(2, env.size());\n        assertEquals(123, env.get(\"x\"));\n        assertEquals(txId, env.get(\"y\"));\n\n        List<String> opts = (List<String>) ctx.getVariable(\"hosts\");\n        assertNotNull(opts);\n        assertEquals(2, opts.size());\n        assertEquals(\"foo:10.0.0.3\", opts.get(0));\n    }\n\n    @Test\n    public void test034() throws Exception {\n        deploy(\"034.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"x\", new HashMap<>());\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(isNull());\n    }\n\n    @Test\n    public void test035() throws Exception {\n        deploy(\"035.yml\");\n\n        JavaDelegate task = spy(new JavaDelegate() {\n            @SuppressWarnings(\"rawtypes\")\n            @Override\n            public void execute(ExecutionContext ctx) {\n                Object o = ctx.getVariable(\"aList\");\n                assertTrue(o instanceof List);\n\n                List l = (List) o;\n                assertEquals(3, l.size());\n                assertEquals(132, l.get(2));\n            }\n        });\n        register(\"testTask\", task);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n\n        verify(task, times(1)).execute(any(ExecutionContext.class));\n    }\n\n    @Test\n    public void test036() throws Exception {\n        deploy(\"036.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"x\", 1L);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(101L));\n        verify(testBean, times(1)).toString(eq(102L));\n        verify(testBean, times(1)).toString(eq(103L));\n        verify(testBean, times(1)).toString(eq(104L));\n        verify(testBean, times(1)).toString(eq(\"handled!\"));\n    }\n\n    @Test\n    public void test037() throws Exception {\n        deploy(\"037.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"1\"));\n        verify(testBean, times(1)).toString(eq(\"3\"));\n    }\n\n    @Test\n    public void test040() throws Exception {\n        deploy(\"040.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n        //                   /--> taskA1 -> taskA2 -\\\n        // start -> task -> gw -> taskB ------------> end\n        //                   \\--> taskD ------------/\n        assertEquals(17, pd.getChildren().size());\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"switchValue\", \"a\");\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"do a\"));\n        verify(testBean, times(1)).toString(eq(\"do a2\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test040_2() throws Exception {\n        deploy(\"040.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"switchValue\", \"b\");\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"do b\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test040_3() throws Exception {\n        deploy(\"040.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"switchValue\", \"123\");\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"do default\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test041() throws Exception {\n        deploy(\"041.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"switchValue\", 1);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"do 1\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test042() throws Exception {\n        deploy(\"042.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"switchValue\", 42);\n        start(key, \"main\", args);\n\n        verify(testBean, times(1)).toString(eq(\"after switch/case\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test043() throws Exception {\n        deploy(\"043.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"switchValue\", 2);\n        args.put(\"caseValue1\", 10);\n        args.put(\"caseValue2\", 2);\n        start(key, \"main\", args);\n\n        verify(testBean, times(1)).toString(eq(\"do 2\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test044() throws Exception {\n        deploy(\"044.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"switchValue\", \"default\");\n        start(key, \"main\", args);\n\n        verify(testBean, times(1)).toString(eq(\"do default as string\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test045() throws Exception {\n        deploy(\"045.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"err\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test046() throws Exception {\n        deploy(\"046.yml\");\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n    }\n\n    @Test\n    public void test047() throws Exception {\n        deploy(\"047.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"myFlowA\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test048() throws Exception {\n        deploy(\"048.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        JavaDelegate task = mock(JavaDelegate.class);\n        doThrow(new BpmnError(\"first error\"))\n                .doNothing()\n                .when(task).execute(any());\n\n        register(\"testErrorTask\", task);\n\n        register(\"__retryUtils\", new YamlTaskStepConverter.RetryUtilsTask());\n\n        // ---\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n        ArgumentCaptor<ExecutionContext> ctxCaptor = ArgumentCaptor.forClass(ExecutionContext.class);\n        verify(task, times(2)).execute(ctxCaptor.capture());\n\n        List<ExecutionContext> retryCtx = ctxCaptor.getAllValues();\n        assertEquals(\"test\", retryCtx.get(0).getVariable(\"msg\"));\n        assertEquals(\"retry\", retryCtx.get(1).getVariable(\"msg\"));\n\n        Map<String, String> expectedVars0 = new HashMap<>();\n        expectedVars0.put(\"a\", \"a-value-original\");\n        expectedVars0.put(\"b\", \"b-value-original\");\n\n        Map<String, String> expectedVars1 = new HashMap<>();\n        expectedVars1.put(\"a\", \"a-value-original\");\n        expectedVars1.put(\"b\", \"b-value-retry\");\n\n        assertEquals(expectedVars0, retryCtx.get(0).getVariable(\"nested\"));\n        assertEquals(expectedVars1, retryCtx.get(1).getVariable(\"nested\"));\n\n        verify(testBean, times(1)).toString(eq(\"end\"));\n\n        verifyNoMoreInteractions(testBean);\n        verifyNoMoreInteractions(task);\n    }\n\n    @Test\n    public void test049() throws Exception {\n        deploy(\"049.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n        register(\"__withItemsUtils\", new StepConverter.WithItemsUtilsTask());\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"x\", 1L);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(101L));\n        verify(testBean, times(1)).toString(eq(\"2:item1\"));\n        verify(testBean, times(1)).toString(eq(\"2:2\"));\n        verify(testBean, times(1)).toString(eq(\"2:item3\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test050() throws Exception {\n        deploy(\"050.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n        register(\"__withItemsUtils\", new StepConverter.WithItemsUtilsTask());\n\n        // ---\n\n        List<String> arr = Arrays.asList(\"item1\", \"item2\", \"item3\");\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"myArray\", arr);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(arr));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test051() throws Exception {\n        deploy(\"051.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n        register(\"__withItemsUtils\", new StepConverter.WithItemsUtilsTask());\n\n        // ---\n\n        List<String> arr = Arrays.asList(\"item1\", \"item2\", \"item3\");\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"myArray\", arr);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"single1\"));\n        verify(testBean, times(1)).toString(eq(arr));\n        verify(testBean, times(1)).toString(eq(\"singleLast\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test052() throws Exception {\n        deploy(\"052.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n        register(\"__withItemsUtils\", new StepConverter.WithItemsUtilsTask());\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"myArray\", Arrays.asList(\"item1\", \"item2\", \"item3\"));\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"item1\"));\n        verify(testBean, times(1)).toString(eq(\"item2\"));\n        verify(testBean, times(1)).toString(eq(\"item3\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test053() throws Exception {\n        deploy(\"053.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n        register(\"__withItemsUtils\", new StepConverter.WithItemsUtilsTask());\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"myArray\", Arrays.asList(\"item1\", \"item2\", \"item3\"));\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(testBean, times(1)).toString(eq(\"testuser1:wheel\"));\n        verify(testBean, times(1)).toString(eq(\"testuser2:root\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test054() throws Exception {\n        deploy(\"054.yml\");\n\n        MyLogger task = spy(new MyLogger());\n        register(\"myLogger\", task);\n        register(\"__withItemsUtils\", new StepConverter.WithItemsUtilsTask());\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(task, times(3)).execute(any(ExecutionContext.class));\n        verify(task, times(1)).log(eq(\"hello red\"));\n        verify(task, times(1)).log(eq(\"hello green\"));\n        verify(task, times(1)).log(eq(\"hello blue\"));\n        verifyNoMoreInteractions(task);\n    }\n\n    @Test\n    public void test055() throws Exception {\n        deploy(\"055.yml\");\n\n        MyLogger task = spy(new MyLogger());\n        register(\"myLogger\", task);\n        register(\"__withItemsUtils\", new StepConverter.WithItemsUtilsTask());\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"x\", Arrays.asList(\"item1\", \"item2\"));\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(task, times(3)).execute(any(ExecutionContext.class));\n        verify(task, times(1)).log(eq(\"hello red\"));\n        verify(task, times(1)).log(eq(\"hello [item1, item2]\"));\n        verify(task, times(1)).log(eq(\"hello blue\"));\n        verifyNoMoreInteractions(task);\n    }\n\n    @Test\n    public void test056() throws Exception {\n        deploy(\"056.yml\");\n\n        MyLogger task = spy(new MyLogger());\n        register(\"myLogger\", task);\n        register(\"__withItemsUtils\", new StepConverter.WithItemsUtilsTask());\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"myArray\", Arrays.asList(\"item1\", \"item2\"));\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(task, times(1)).execute(any(ExecutionContext.class));\n        verify(task, times(1)).log(eq(\"hello [item1, item2]\"));\n        verifyNoMoreInteractions(task);\n    }\n\n    @Test\n    public void test057() throws Exception {\n        deploy(\"057.yml\");\n\n        MyLogger task = spy(new MyLogger());\n        register(\"myLogger\", task);\n        register(\"__withItemsUtils\", new StepConverter.WithItemsUtilsTask());\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"myArray\", Arrays.asList(\"item1\", \"item2\"));\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(task, times(2)).execute(any(ExecutionContext.class));\n        verify(task, times(1)).log(eq(\"hello item1\"));\n        verify(task, times(1)).log(eq(\"hello item2\"));\n        verifyNoMoreInteractions(task);\n    }\n\n    @Test\n    public void test058() throws Exception {\n        deploy(\"058.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        JavaDelegate task = mock(JavaDelegate.class);\n        doThrow(new BpmnError(\"first error\"))\n                .doNothing()\n                .when(task).execute(any());\n\n        register(\"testErrorTask\", task);\n\n        register(\"__retryUtils\", new YamlTaskStepConverter.RetryUtilsTask());\n        register(\"__withItemsUtils\", new StepConverter.WithItemsUtilsTask());\n\n        // ---\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n        ArgumentCaptor<ExecutionContext> ctxCaptor = ArgumentCaptor.forClass(ExecutionContext.class);\n        verify(task, times(3)).execute(ctxCaptor.capture());\n\n        List<ExecutionContext> retryCtx = ctxCaptor.getAllValues();\n        assertEquals(\"test: item1\", retryCtx.get(0).getVariable(\"msg\"));\n        assertEquals(\"retry: item1\", retryCtx.get(1).getVariable(\"msg\"));\n        assertEquals(\"test: item2\", retryCtx.get(2).getVariable(\"msg\"));\n\n        verify(testBean, times(1)).toString(eq(\"end\"));\n\n        verifyNoMoreInteractions(testBean);\n        verifyNoMoreInteractions(task);\n    }\n\n    @Test\n    public void test059() throws Exception {\n        deploy(\"059.yml\");\n\n        MyLogger task = spy(new MyLogger());\n        register(\"__withItemsUtils\", new StepConverter.WithItemsUtilsTask());\n        register(\"myLogger\", task);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"myArray\", Arrays.asList(\"a1\", \"a2\", \"a3\"));\n        start(key, \"main\", args);\n\n        // ---\n        verify(task, times(6)).execute(any(ExecutionContext.class));\n        verify(task, times(1)).log(eq(\"hello a1\"));\n        verify(task, times(1)).log(eq(\"hello a2\"));\n        verify(task, times(1)).log(eq(\"hello a3\"));\n\n        verify(task, times(1)).log(eq(\"hello from call item1\"));\n        verify(task, times(1)).log(eq(\"hello from call item2\"));\n        verify(task, times(1)).log(eq(\"hello from call item3\"));\n\n        verifyNoMoreInteractions(task);\n    }\n\n    @Test\n    public void test060() throws Exception {\n        deploy(\"060.yml\");\n\n        MyLogger task = spy(new MyLogger());\n        register(\"__withItemsUtils\", new StepConverter.WithItemsUtilsTask());\n        register(\"myLogger\", task);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"nullVariable\", null);\n\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(task, times(0)).execute(any(ExecutionContext.class));\n        verify(task, times(0)).log(eq(\"hello a1\"));\n        verifyNoMoreInteractions(task);\n    }\n\n    @Test\n    public void test061() {\n        deploy(\"061.yml\");\n\n        MyLogger task = spy(new MyLogger());\n        register(\"__withItemsUtils\", new StepConverter.WithItemsUtilsTask());\n        register(\"myLogger\", task);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"notArrayVariable\", \"invalid variable type\");\n\n        try {\n            start(key, \"main\", args);\n            fail(\"exception expected\");\n        } catch (ExecutionException e) {\n            assertTrue(e.getCause().getCause().getMessage().contains(\"should be a list\"));\n        }\n    }\n\n    @Test\n    public void test062() throws Exception {\n        deploy(\"062.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"calledFlow\", \"myFlow1\");\n\n        start(key, \"main\", args);\n\n        verify(testBean, times(1)).toString(eq(\"from MyFlow1\"));\n        verifyNoMoreInteractions(testBean);\n    }\n\n    @Test\n    public void test063() throws Exception {\n        deploy(\"063.yml\");\n\n        JavaDelegate checkpointTask = spy(new JavaDelegate() {\n            @Override\n            public void execute(ExecutionContext ctx) {\n                ctx.setVariable(\"checkpointId\", \"123\");\n            }\n        });\n        register(\"checkpoint\", checkpointTask);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        verify(checkpointTask, times(1)).execute(any());\n        verifyNoMoreInteractions(checkpointTask);\n    }\n\n    @Test\n    public void test064() throws Exception {\n        /*\n        Variables provided to withItems may be null. The Task should only execute in cases where the variable provided\n        to withItems is non-null\n         */\n        deploy(\"064.yml\");\n\n        MyLogger task = spy(new MyLogger());\n        register(\"__withItemsUtils\", new StepConverter.WithItemsUtilsTask());\n        register(\"myLogger\", task);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"nullVariable\", null); // should't execute the task\n        List<String> theList = new ArrayList<>(3);\n        theList.add(\"hello\");\n        theList.add(null);\n        theList.add(\"world\");\n        args.put(\"listWithNull\", theList);\n        theList = new ArrayList<>(2);\n        theList.add(\"Hello\");\n        theList.add(\"World\");\n        args.put(\"fineList\", theList);\n\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(task, times(5)).execute(any(ExecutionContext.class));\n        verify(task, times(1)).log(eq(\"None of these are null -> Hello\"));\n        verify(task, times(1)).log(eq(\"None of these are null -> World\"));\n\n        verify(task, times(1)).log(eq(\"It's okay to have a null item -> hello\"));\n        verify(task, times(1)).log(eq(\"It's okay to have a null item -> \"));\n        verify(task, times(1)).log(eq(\"It's okay to have a null item -> world\"));\n\n        verifyNoMoreInteractions(task);\n    }\n\n    @Test\n    public void test065() throws Exception {\n        deploy(\"065.yml\");\n\n        MyLogger task = spy(new MyLogger());\n        register(\"__withItemsUtils\", new StepConverter.WithItemsUtilsTask());\n        register(\"myLogger\", task);\n        // ---\n\n        String key = UUID.randomUUID().toString();\n\n        start(key, \"default\", Collections.emptyMap());\n\n        // ---\n\n        verify(task, times(6)).execute(any(ExecutionContext.class));\n\n        verify(task, times(1)).log(eq(\"A 0\"));\n        verify(task, times(1)).log(eq(\"A 1\"));\n\n        verify(task, times(1)).log(eq(\"B 0\"));\n        verify(task, times(1)).log(eq(\"B 1\"));\n\n        verify(task, times(1)).log(eq(\"C 0\"));\n        verify(task, times(1)).log(eq(\"C 1\"));\n\n        verifyNoMoreInteractions(task);\n    }\n\n    @Test\n    public void test066() throws Exception {\n        deploy(\"066.yml\");\n\n        MyLogger task = spy(new MyLogger());\n        register(\"__withItemsUtils\", new StepConverter.WithItemsUtilsTask());\n        register(\"myLogger\", task);\n        // ---\n\n        String key = UUID.randomUUID().toString();\n\n        start(key, \"default\", Collections.singletonMap(\"nestedWithItems\", Arrays.asList(\"0\", null, \"2\", \"3\")));\n\n        // ---\n\n        verify(task, times(12)).execute(any(ExecutionContext.class));\n\n        verify(task, times(1)).log(eq(\"A 0\"));\n        verify(task, times(1)).log(eq(\"A \"));\n        verify(task, times(1)).log(eq(\"A 2\"));\n        verify(task, times(1)).log(eq(\"A 3\"));\n\n        verify(task, times(1)).log(eq(\"B 0\"));\n        verify(task, times(1)).log(eq(\"B \"));\n        verify(task, times(1)).log(eq(\"B 2\"));\n        verify(task, times(1)).log(eq(\"B 3\"));\n\n        verify(task, times(1)).log(eq(\"C 0\"));\n        verify(task, times(1)).log(eq(\"C \"));\n        verify(task, times(1)).log(eq(\"C 2\"));\n        verify(task, times(1)).log(eq(\"C 3\"));\n\n        verifyNoMoreInteractions(task);\n    }\n\n    @Test\n    public void test067() throws Exception {\n        deploy(\"067.yml\");\n\n        MyLogger task = spy(new MyLogger());\n        register(\"__withItemsUtils\", new StepConverter.WithItemsUtilsTask());\n        register(\"myLogger\", task);\n        register(\"myTask\", new JavaDelegate() {\n            @Override\n            public void execute(ExecutionContext ctx) {\n                ctx.setVariable(\"var\", ctx.getVariable(\"message\"));\n            }\n        });\n        // ---\n\n        String key = UUID.randomUUID().toString();\n\n        start(key, \"default\", Collections.singletonMap(\"nestedWithItems\", Arrays.asList(\"0\", \"1\", \"2\")));\n\n        // ---\n\n        verify(task, times(1)).log(eq(\n                Arrays.asList(\n                        Arrays.asList(\"A 0\", \"A 1\", \"A 2\"),\n                        Arrays.asList(\"B 0\", \"B 1\", \"B 2\"),\n                        Arrays.asList(\"C 0\", \"C 1\", \"C 2\"))));\n    }\n\n    @Test\n    public void test068() throws Exception {\n        deploy(\"068.yml\");\n\n        MyLogger logBean = spy(new MyLogger());\n        register(\"log\", logBean);\n\n        JavaDelegate task = mock(JavaDelegate.class);\n        doThrow(new BpmnError(\"first error\"))\n                .doNothing()\n                .when(task).execute(any());\n\n        register(\"testErrorTask\", task);\n\n        JavaDelegate task2 = mock(JavaDelegate.class);\n        doThrow(new BpmnError(\"first error\"))\n                .doNothing()\n                .when(task2).execute(any());\n\n        register(\"testErrorTask2\", task2);\n\n        register(\"__retryUtils\", new YamlTaskStepConverter.RetryUtilsTask());\n\n        // ---\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n        ArgumentCaptor<ExecutionContext> ctxCaptor = ArgumentCaptor.forClass(ExecutionContext.class);\n        verify(task, times(2)).execute(ctxCaptor.capture());\n\n        List<ExecutionContext> retryCtx = ctxCaptor.getAllValues();\n        assertEquals(\"test\", retryCtx.get(0).getVariable(\"msg\"));\n        assertEquals(\"retry\", retryCtx.get(1).getVariable(\"msg\"));\n\n        verifyNoMoreInteractions(task);\n\n        // ---\n        ArgumentCaptor<ExecutionContext> ctxCaptor2 = ArgumentCaptor.forClass(ExecutionContext.class);\n        verify(task2, times(2)).execute(ctxCaptor2.capture());\n\n        List<ExecutionContext> retryCtx2 = ctxCaptor2.getAllValues();\n        assertEquals(\"test2\", retryCtx2.get(0).getVariable(\"msg\"));\n        assertEquals(\"retry2\", retryCtx2.get(1).getVariable(\"msg\"));\n\n        verifyNoMoreInteractions(task2);\n    }\n\n    @Test\n    public void test069() throws Exception {\n        deploy(\"069.yml\");\n\n        MyLogger task = spy(new MyLogger());\n        register(\"myLogger\", task);\n        register(\"__withItemsUtils\", new StepConverter.WithItemsUtilsTask());\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", new HashMap<>());\n\n        // ---\n\n        verify(task, times(3)).execute(any(ExecutionContext.class));\n        verify(task, times(1)).log(eq(\"hello a\"));\n        verify(task, times(1)).log(eq(\"hello b\"));\n        verify(task, times(1)).log(eq(\"hello c\"));\n        verifyNoMoreInteractions(task);\n    }\n\n    @Test\n    public void test070() throws Exception {\n        deploy(\"070.yml\");\n\n        MyLogger task = spy(new MyLogger());\n        register(\"__withItemsUtils\", new StepConverter.WithItemsUtilsTask());\n        register(\"log\", task);\n        // ---\n\n        String key = UUID.randomUUID().toString();\n\n        start(key, \"default\", null);\n\n        // ---\n\n        verify(task, times(12)).call(any());\n\n        verify(task, times(1)).log(eq(\"Something: [apple, orange, bannana], 0\"));\n        verify(task, times(1)).log(eq(\"Something else: apple\"));\n        verify(task, times(1)).log(eq(\"Something else: orange\"));\n        verify(task, times(1)).log(eq(\"Something else: bannana\"));\n\n        verify(task, times(1)).log(eq(\"Something: [dog, cat, mouse], 1\"));\n        verify(task, times(1)).log(eq(\"Something else: dog\"));\n        verify(task, times(1)).log(eq(\"Something else: cat\"));\n        verify(task, times(1)).log(eq(\"Something else: mouse\"));\n\n        verify(task, times(1)).log(eq(\"Something: [one, two, three], 2\"));\n        verify(task, times(1)).log(eq(\"Something else: one\"));\n        verify(task, times(1)).log(eq(\"Something else: two\"));\n        verify(task, times(1)).log(eq(\"Something else: three\"));\n\n        verifyNoMoreInteractions(task);\n    }\n\n    @Test\n    public void test071() throws Exception {\n        deploy(\"071.yml\");\n\n        MyLogger task = spy(new MyLogger());\n        register(\"myLogger\", task);\n        register(\"__withItemsUtils\", new StepConverter.WithItemsUtilsTask());\n\n        // ---\n        Map<String, String> items = new HashMap<>();\n        items.put(\"k1\", \"v1\");\n        items.put(\"k2\", \"v2\");\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"myMap\", items);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(task, times(2)).execute(any(ExecutionContext.class));\n        verify(task, times(1)).log(eq(\"hello k1 -> v1\"));\n        verify(task, times(1)).log(eq(\"hello k2 -> v2\"));\n        verifyNoMoreInteractions(task);\n    }\n\n    @Test\n    public void test072() throws Exception {\n        deploy(\"072.yml\");\n\n        MyLogger task = spy(new MyLogger());\n        register(\"log\", task);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.emptyMap();\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(task, times(3)).log(eq(\"flow: main\"));\n        verify(task, times(1)).log(eq(\"flow: myFlow\"));\n        verify(task, times(1)).log(eq(\"flow: myFlow2\"));\n        verify(task, times(1)).log(eq(\"flow: myFaultyFlow\"));\n        verify(task, times(1)).log(eq(\"error handler: flow: main\"));\n    }\n\n    @Test\n    public void test073() throws Exception {\n        deploy(\"073.yml\");\n\n        MyLogger task = spy(new MyLogger());\n        register(\"log\", task);\n\n        TestBean testBean = spy(new TestBean());\n        register(\"vars\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        Map<String, Object> args = Collections.singletonMap(\"nested\", 123);\n        start(key, \"main\", args);\n\n        // ---\n\n        verify(task, times(2)).log(eq(\"Hello, 123\"));\n    }\n\n    @Test\n    public void test074() throws Exception {\n        deploy(\"074.yml\");\n\n        register(\"__withItemsUtils\", new StepConverter.WithItemsUtilsTask());\n        register(\"__retryUtils\", new YamlTaskStepConverter.RetryUtilsTask());\n\n        MyLogger task = spy(new MyLogger());\n        register(\"log\", task);\n\n        TestErrorTask http = spy(new TestErrorTask());\n        register(\"http\", http);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n\n        start(key, \"default\");\n\n        // ---\n        ArgumentCaptor<ExecutionContext> captor = ArgumentCaptor.forClass(ExecutionContext.class);\n        verify(http, times(12)).execute(captor.capture());\n\n        List<String> urls = captor.getAllValues()\n                .stream()\n                .map(e -> (String) e.getVariable(\"url\"))\n                .collect(Collectors.toList());\n\n        assertContains(\"https://nonexistant.example.com/test/a\", urls, 4);\n        assertContains(\"https://nonexistant.example.com/test/b\", urls, 4);\n        assertContains(\"https://nonexistant.example.com/test/c\", urls, 4);\n\n        assertTrue(urls.isEmpty());\n\n        verifyNoMoreInteractions(http);\n    }\n\n    @Test\n    public void test075() throws Exception {\n        deploy(\"075.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        register(\"__retryUtils\", new YamlTaskStepConverter.RetryUtilsTask());\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        try {\n            start(key, \"default\");\n            fail(\"should fail\");\n        } catch (ExecutionException e) {\n            // do nothing\n        }\n\n        // ---\n\n        verify(testBean, times(3)).throwBpmnError(anyString());\n    }\n\n    @Test\n    public void test076() {\n        deploy(\"076.yml\");\n\n        MyLogger log = spy(new MyLogger());\n        register(\"log\", log);\n\n        register(\"throw\", new ThrowExceptionTask());\n\n        register(\"__retryUtils\", new YamlTaskStepConverter.RetryUtilsTask());\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        try {\n            start(key, \"default\");\n            fail(\"should fail\");\n        } catch (ExecutionException e) {\n            // do nothing\n        }\n\n        // ---\n\n        verify(log, times(2)).log(eq(\"Here's calledFlow\"));\n        verify(log, times(2)).log(eq(\"Here's nestedFlow called by calledFlow\"));\n        verify(log, times(2)).log(eq(\"Error NOW!\"));\n    }\n\n    @Test\n    public void test077() throws Exception {\n        deploy(\"077.yml\");\n\n        ProcessDefinition pd = getDefinition(\"main\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        JavaDelegate task = mock(JavaDelegate.class);\n        doThrow(new BpmnError(\"first error\"))\n                .doNothing()\n                .when(task).execute(any());\n\n        register(\"testErrorTask\", task);\n\n        register(\"__retryUtils\", new YamlTaskStepConverter.RetryUtilsTask());\n\n        // ---\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n        ArgumentCaptor<ExecutionContext> ctxCaptor = ArgumentCaptor.forClass(ExecutionContext.class);\n        verify(task, times(2)).execute(ctxCaptor.capture());\n\n        List<ExecutionContext> retryCtx = ctxCaptor.getAllValues();\n        assertEquals(\"test\", retryCtx.get(0).getVariable(\"msg\"));\n        assertEquals(\"retry\", retryCtx.get(1).getVariable(\"msg\"));\n\n        verify(testBean, times(1)).toString(eq(\"end\"));\n\n        verifyNoMoreInteractions(testBean);\n        verifyNoMoreInteractions(task);\n    }\n\n    // FORMS (100 - 199)\n\n    @Test\n    public void test100() throws Exception {\n        deploy(\"100.yml\");\n\n        String formValue = \"test#\" + System.currentTimeMillis();\n\n        // ---\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        verify(testBean, times(1)).toString(eq(\"aaa\"));\n\n        // ---\n\n        UUID formId = getFirstFormId();\n        Map<String, Object> data = Collections.singletonMap(\"name\", formValue);\n        submitForm(formId, data);\n\n        verify(testBean, times(1)).toString(eq(formValue));\n    }\n\n    @Test\n    public void test101() throws Exception {\n        deploy(\"101.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n\n        UUID formId = getFirstFormId();\n\n        FormSubmitResult result = submitForm(formId, Collections.singletonMap(\"age\", 256));\n        assertFalse(result.isValid());\n\n        Map<String, Object> data = new HashMap<>();\n        data.put(\"age\", 64);\n        data.put(\"percent\", -1.0);\n\n        result = submitForm(formId, data);\n        assertFalse(result.isValid());\n\n        result = submitForm(formId, Collections.singletonMap(\"age\", 64));\n        assertTrue(result.isValid());\n\n        verify(testBean, times(1)).toString(eq(64));\n    }\n\n    @Test\n    public void test102() throws Exception {\n        deploy(\"102.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n\n        UUID formId = getFirstFormId();\n\n        int[] numbers = new int[]{-1, 5, 98};\n\n        FormSubmitResult result = submitForm(formId, Collections.singletonMap(\"favouriteNumbers\", numbers));\n        assertFalse(result.isValid());\n\n        numbers = new int[]{0, 5, 98};\n\n        result = submitForm(formId, Collections.singletonMap(\"favouriteNumbers\", numbers));\n        assertTrue(result.isValid());\n\n        verify(testBean, times(1)).toString(eq(numbers));\n    }\n\n    @Test\n    @SuppressWarnings(\"unchecked\")\n    public void test103() throws Exception {\n        deploy(\"103.yml\");\n\n        String inputValue = \"test#\" + System.currentTimeMillis();\n\n        // ---\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", Collections.singletonMap(\"inputValue\", inputValue));\n\n        // ---\n\n        UUID formId = getFirstFormId();\n        Form f = getForm(formId);\n\n        Map<String, Object> data = (Map<String, Object>) f.getEnv().get(\"myForm\");\n        FormSubmitResult result = submitForm(formId, data);\n        assertTrue(result.isValid());\n\n        verify(testBean, times(1)).toString(eq(\"Hello, \" + inputValue));\n    }\n\n    @Test\n    public void test104() throws Exception {\n        deploy(\"104.yml\");\n\n        String valueA = \"a_\" + System.currentTimeMillis();\n        String valueB = \"b_\" + System.currentTimeMillis();\n        String valueC = \"c_\" + System.currentTimeMillis();\n\n        // ---\n\n        TestBean testBean = spy(new TestBean(new String[]{valueA, valueB, valueC}));\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n\n        UUID formId = getFirstFormId();\n\n        Map<String, Object> data = Collections.singletonMap(\"testValue\", valueB);\n        FormSubmitResult result = submitForm(formId, data);\n        assertTrue(result.isValid());\n\n        verify(testBean, times(1)).toString(eq(valueB));\n    }\n\n    @Test\n    public void test105() throws Exception {\n        deploy(\"105.yml\");\n\n        String valueA = \"a_\" + System.currentTimeMillis();\n        String valueB = \"b_\" + System.currentTimeMillis();\n        String valueC = \"c_\" + System.currentTimeMillis();\n\n        // ---\n\n        TestBean testBean = spy(new TestBean(new String[]{valueA, valueB, valueC}));\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n\n        UUID formId = getFirstFormId();\n\n        FormSubmitResult result = submitForm(formId, Collections.emptyMap());\n        assertFalse(result.isValid());\n\n        verify(testBean, never()).toString(any());\n    }\n\n    @Test\n    public void test106() throws Exception {\n        deploy(\"106.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n\n        UUID formId = getFirstFormId();\n\n        FormSubmitResult result = submitForm(formId, Collections.singletonMap(\"testValue\", \"d\"));\n        assertFalse(result.isValid());\n\n        result = submitForm(formId, Collections.singletonMap(\"testValue\", \"a\"));\n        assertTrue(result.isValid());\n\n        verify(testBean, times(1)).toString(eq(\"a\"));\n    }\n\n    @Test\n    public void test107() throws Exception {\n        deploy(\"107.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n\n        UUID formId = getFirstFormId();\n\n        FormSubmitResult result = submitForm(formId, Collections.singletonMap(\"testValue\", \"else\"));\n        assertTrue(result.isValid());\n\n        verify(testBean, times(1)).toString(eq(\"else\"));\n    }\n\n    @Test\n    public void test108() throws Exception {\n        deploy(\"108.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n\n        UUID formId = getFirstFormId();\n\n        FormSubmitResult result = submitForm(formId, Collections.singletonMap(\"testValue\", \"else\"));\n        assertTrue(result.isValid());\n\n        verify(testBean, times(1)).toString(eq(\"else\"));\n    }\n\n    @Test\n    public void test109() throws Exception {\n        deploy(\"109.yml\");\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n\n        UUID formId = getFirstFormId();\n\n        FormSubmitResult result = submitForm(formId, Collections.singletonMap(\"testValue\", \"else\"));\n        assertTrue(result.isValid());\n\n        verify(testBean, times(1)).toString(eq(\"else\"));\n    }\n\n    @Test\n    public void test110() throws Exception {\n        deploy(\"110.yml\");\n\n        String formValue = \"test#\" + System.currentTimeMillis();\n\n        // ---\n\n        TestBean testBean = spy(new TestBean());\n        register(\"testBean\", testBean);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\", null);\n\n        // ---\n\n        UUID formId = getFirstFormId();\n        Map<String, Object> data = Collections.singletonMap(\"myField\", formValue);\n        submitForm(formId, data);\n\n        verify(testBean, times(1)).toString(eq(formValue));\n    }\n\n    @Test\n    public void testJunk() {\n        deploy(\"junk.yml\");\n    }\n\n    // MISC\n\n    @Test\n    public void testOld() {\n        assertThrows(RuntimeException.class, () -> deploy(\"old.yml\"));\n    }\n\n\n    @Test\n    public void test113() throws Exception {\n        deploy(\"113.yml\");\n\n        MyLogger logger = spy(new MyLogger());\n        register(\"myLogger\", logger);\n\n        TestTaskWithResume testTask = spy(new TestTaskWithResume());\n        register(\"testTask\", testTask);\n\n        // ---\n\n        String key = UUID.randomUUID().toString();\n        start(key, \"main\");\n\n        resume(key, \"myEvent\");\n\n        resume(key, \"myEvent\");\n\n        // ---\n        verify(logger, times(1)).log(eq(\"RESUME1\"));\n        verify(logger, times(1)).log(eq(\"RESUME2\"));\n    }\n\n    private ProcessDefinition findSubprocess(ProcessDefinition pd) {\n        return pd.getChildren().stream()\n                .filter(e -> e instanceof ProcessDefinition)\n                .map(e -> (ProcessDefinition) e)\n                .findFirst()\n                .orElseThrow(() -> new RuntimeException(\"subprocess not found\"));\n    }\n\n    private static void assertContains(String str, List<String> items, int expectedCount) {\n        for (int i = 0; i < expectedCount; i++) {\n            boolean removed = items.remove(str);\n            assertTrue(removed);\n        }\n\n        assertFalse(items.contains(str));\n    }\n\n    private static class TestBean {\n\n        private final String[] items;\n\n        public TestBean() {\n            this(null);\n        }\n\n        public TestBean(String[] items) {\n            this.items = items;\n        }\n\n        public void hello() {\n        }\n\n        public String toString(Object s) {\n            if (s == null) {\n                return null;\n            }\n            return s.toString();\n        }\n\n        public void checkString(String s) {\n        }\n\n        public void throwBpmnError(String errorRef) {\n            throw new BpmnError(errorRef);\n        }\n\n        public int inc(int i) {\n            return i + 1;\n        }\n\n        public String[] getItems() {\n            return items;\n        }\n\n        public void info(String a, String b) {\n            // do nothing\n        }\n\n        public void set(ExecutionContext executionContext, Map<String, Object> vars) {\n            vars.forEach((k, v) -> {\n                Object vv = executionContext.interpolate(v);\n                executionContext.setVariable(k, vv);\n            });\n        }\n    }\n\n    private static class MyLogger implements JavaDelegate {\n\n        @Override\n        public void execute(ExecutionContext executionContext) {\n            log(executionContext.getVariable(\"message\"));\n        }\n\n        public void log(Object message) {\n            System.out.println(message);\n        }\n\n        public void call(Object message) {\n            log(message);\n        }\n    }\n\n    private static class TestTask implements JavaDelegate {\n\n        private final String srcVar;\n        private final String dstVar;\n\n        public TestTask() {\n            this(null, null);\n        }\n\n        private TestTask(String srcVar, String dstVar) {\n            this.srcVar = srcVar;\n            this.dstVar = dstVar;\n        }\n\n        @Override\n        public void execute(ExecutionContext ctx) {\n            if (srcVar != null && dstVar != null) {\n                ctx.setVariable(dstVar, ctx.getVariable(srcVar));\n            }\n        }\n\n        public void call(Object arg1) {\n        }\n\n        public void call(Object arg1, Object arg2) {\n        }\n    }\n\n    private static class TestTaskWithResume implements JavaDelegate {\n\n        public static final String SUSPEND_MARKER = \"TestTaskWithResumeSuspend\";\n\n        @Override\n        public void execute(ExecutionContext ctx) {\n            Object in = ctx.getVariable(\"num\");\n\n            if (ctx.getVariable(SUSPEND_MARKER) != null && (Boolean) ctx.getVariable(SUSPEND_MARKER)) {\n                ctx.setVariable(\"TestTaskWithResumeResult\", \"RESUME\" + in);\n            } else {\n                ctx.setVariable(SUSPEND_MARKER, true);\n                ctx.suspend(\"myEvent\", null, true);\n\n                ctx.setVariable(\"TestTaskWithResumeResult\", \"SUSPEND\" + in);\n            }\n        }\n    }\n\n    private static class DockerTask implements JavaDelegate {\n\n        @Override\n        public void execute(ExecutionContext ctx) throws Exception {\n        }\n    }\n\n    private interface TestInterface {\n\n        void call(Object arg1);\n    }\n\n    private static class TestErrorTask implements JavaDelegate {\n\n        @Override\n        public void execute(ExecutionContext ctx) {\n            throw new BpmnError(\"boom!\");\n        }\n    }\n\n    public class ThrowExceptionTask implements Task {\n\n        public void call(Object o) throws Exception {\n            if (o instanceof Exception) {\n                throw (Exception) o;\n            } else if (o instanceof String) {\n                throw new RuntimeException(o.toString());\n            } else if (o instanceof Serializable) {\n                throw new RuntimeException(\"Process error:\" + o);\n            } else {\n                throw new RuntimeException(o != null ? o.toString() : \"n/a\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/test/java/com/walmartlabs/concord/project/yaml/validator/YamlValidationTest.java",
    "content": "package com.walmartlabs.concord.project.yaml.validator;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.project.yaml.AbstractYamlParserTest;\nimport static org.junit.jupiter.api.Assertions.*;\nimport org.junit.jupiter.api.Test;\n\npublic class YamlValidationTest extends AbstractYamlParserTest {\n\n    @Test\n    public void test002() {\n        try {\n            deploy(\"validator/002.yml\");\n            fail(\"exception expected\");\n        } catch (Exception e) {\n            Throwable t = e.getCause();\n            assertTrue(t.getMessage().contains(\"checkpoint 'one'\"));\n            assertTrue(t.getMessage().contains(\"already defined at\"));\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/test/java/com/walmartlabs/concord/runtime/model/ImportTest.java",
    "content": "package com.walmartlabs.concord.runtime.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.SerializationFeature;\nimport com.fasterxml.jackson.datatype.guava.GuavaModule;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport com.walmartlabs.concord.imports.Import;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ImportTest {\n\n    @Test\n    public void testSerialization() throws Exception {\n        Import item = Import.GitDefinition.builder()\n                .url(\"http://\")\n                .dest(\"/tmp\")\n                .path(\"/path\")\n                .version(\"1.0.0\")\n                .secret(Import.SecretDefinition.builder()\n                        .org(\"default\")\n                        .name(\"secret-name\")\n                        .password(\"secret-password\")\n                        .build())\n                .build();\n\n        ObjectMapper om = new ObjectMapper();\n        om.enable(SerializationFeature.INDENT_OUTPUT);\n        om.registerModule(new GuavaModule());\n        om.registerModule(new Jdk8Module());\n\n        String r = om.writeValueAsString(item);\n\n        Import di = om.readValue(r, Import.class);\n        assertEquals(item, di);\n    }\n}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/000.yml",
    "content": "# invalid definition: empty body\nmain:"
  },
  {
    "path": "runtime/v1/model/src/test/resources/001.yml",
    "content": "main:\n  - ${testBean.hello()}"
  },
  {
    "path": "runtime/v1/model/src/test/resources/002.yml",
    "content": "main:\n  - ${testBean.hello()}\n  - ${testBean.hello()}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/003.yml",
    "content": "main:\n  - expr: ${testBean.toString(aStr)}\n    out: myVar\n  - ${testBean.checkString(myVar)}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/004.yml",
    "content": "main:\n  - task: testTask\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/005.yml",
    "content": "main:\n  - testTask: hello\n  - testTask: ${aStr}\n  - testTask: [1, 2]\n  - testTask: { a: 123 }\n\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/006.yml",
    "content": "main:\n  - task: testTask\n    in:\n      bStr: ${aStr}\n    out:\n      dStr: ${cStr}\n  - ${testBean.toString(dStr)}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/007.yml",
    "content": "main:\n  - if: ${aInt > 0}\n    then:\n      - ${testBean.toString(\"a\")}\n    else:\n      - ${testBean.toString(\"b\")}\n\n  - if: ${aInt < 0}\n    then:\n      - ${testBean.toString(\"c\")}\n    else:\n      - ${testBean.toString(\"d\")}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/008.yml",
    "content": "main:\n  - if: ${aInt > 0}\n    then:\n      - ${testBean.toString(\"a\")}\n      - return\n    else:\n      - ${testBean.toString(\"b\")}\n\n  - ${testBean.toString(\"c\")}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/009.yml",
    "content": "main:\n  - ${testBean.toString(\"a\")}\n\n  - ::\n    - ${testBean.toString(\"b\")}\n    - ${testBean.toString(\"c\")}\n\n  - ${testBean.toString(\"d\")}\n\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/010.yml",
    "content": "main:\n  - ${testBean.toString(\"a\")}\n\n  - expr: ${testBean.throwBpmnError(\"boom\")}\n    error:\n      - ${testBean.toString(\"b\")}\n\n  - ${testBean.toString(\"c\")}\n\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/011.yml",
    "content": "main:\n  - task: testErrorTask\n    error:\n      - ${testBean.toString(\"a\")}\n\n  - ${testBean.toString(\"b\")}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/012.yml",
    "content": "main:\n  - ::\n    - ${testBean.throwBpmnError(\"boom\")}\n    - ${testBean.toString(\"a\")}\n    error:\n      - ${testBean.toString(\"b\")}\n\n  - ${testBean.toString(\"c\")}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/013.yml",
    "content": "main:\n  - ${testBean.toString(\"a\")}\n  - myProc\n\nmyProc:\n  - ${testBean.toString(\"b\")}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/014.yml",
    "content": "# deep recursion\n# no tail call optimization for now :-)\nmain:\n  - if: ${cnt < loops}\n    then:\n      - myProc\n\nmyProc:\n  - expr: ${testBean.inc(cnt)}\n    out: cnt\n  - main\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/015.yml",
    "content": "main:\n  - testTask: |\n      multiline test with\n      string ${interpolation}"
  },
  {
    "path": "runtime/v1/model/src/test/resources/016.yml",
    "content": "main:\n  # EL expressions can be used as task arguments\n  - testTask1: ${inputNumber}\n\n  # in nested objects too\n  - testTask2: [[ \"${inputNumber}\" ]]\n  - testTask3: {\n      a: 123,\n      b: \"${inputNumber}\",\n      c: [ \"${inputBoolean}\", \"${inputString}\" ]\n    }"
  },
  {
    "path": "runtime/v1/model/src/test/resources/017.yml",
    "content": "main:\n  - ${testBean.toString(\"a\")}\n  - event: ev1\n  - ${testBean.toString(\"b\")}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/018.yml",
    "content": "main:\n  # inline script\n  - script: js\n    body: |\n      function doSomething(i) {\n        return i * 2;\n      }\n\n      var x = execution.getVariable(\"input\");\n      execution.setVariable(\"output\", doSomething(x));\n\n  - ${testBean.toString(output)}"
  },
  {
    "path": "runtime/v1/model/src/test/resources/019.yml",
    "content": "main:\n  # inline script\n  - script: groovy\n    body: |\n      // comment\n      def n = 12345;\n\n      // comment\n      execution.setVariable(\"output\", n);\n\n  - ${testBean.toString(output)}"
  },
  {
    "path": "runtime/v1/model/src/test/resources/020.yml",
    "content": "main:\n  - script: externalscript/test.js\n  - ${testBean.toString(x)}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/021.yml",
    "content": "main:\n  - if: ${aInt > 0}\n    then:\n      - ${testBean.toString(\"a\")}\n    else:\n      - ${testBean.toString(\"b\")}\n      - return: error-code-2\n\n  - ${testBean.toString(\"c\")}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/021_2.yml",
    "content": "main:\n  - if: ${aInt > 0}\n    then:\n      - ${testBean.toString(\"a\")}\n    else:\n      - ${testBean.toString(\"b\")}\n      - return: error-code-2\n\n  - ${testBean.toString(\"c\")}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/022.yml",
    "content": "main:\n  - if: ${aInt > 0}\n    then:\n      - ${testBean.toString(\"a\")}\n      - return: error-code\n    else:\n      - ${testBean.toString(\"b\")}\n\n  - ${testBean.toString(\"c\")}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/022_2.yml",
    "content": "main:\n  - if: ${aInt > 0}\n    then:\n      - ${testBean.toString(\"a\")}\n      - return: error-code\n    else:\n      - ${testBean.toString(\"b\")}\n\n  - ${testBean.toString(\"c\")}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/023.yml",
    "content": "main:\n  - if: ${aInt > 0}\n    then:\n      - ${testBean.toString(\"a\")}\n      - return\n    else:\n      - ${testBean.toString(\"b\")}\n\n  - ${testBean.toString(\"c\")}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/024.yml",
    "content": "main:\n  - ::\n    - ${testBean.toString(\"a\")}\n    - return: error\n    error:\n      - ${testBean.toString(\"e\")}"
  },
  {
    "path": "runtime/v1/model/src/test/resources/025.yml",
    "content": "main:\n  - ::\n    - ${testBean.toString(\"a\")}\n    - if: ${aInt > 0}\n      then:\n        - return\n      else:\n        - ${testBean.toString(\"else\")}\n    error:\n      - ${testBean.toString(\"e\")}"
  },
  {
    "path": "runtime/v1/model/src/test/resources/026.yml",
    "content": "main:\n  - ::\n    - ${testBean.toString(\"a\")}\n    - if: ${aInt > 0}\n      then:\n        - ${testBean.toString(\"b\")}\n        - return: err-code\n      else:\n        - ${testBean.toString(\"else\")}\n    error:\n      - ${testBean.toString(\"e\")}"
  },
  {
    "path": "runtime/v1/model/src/test/resources/027.yml",
    "content": "main:\n  - ::\n    - ${testBean.toString(\"a\")}\n    - if: ${aInt == 1}\n      then:\n        - return\n    error:\n      - ${testBean.toString(\"e\")}"
  },
  {
    "path": "runtime/v1/model/src/test/resources/028.yml",
    "content": "main:\n  - ${testBean.toString(\"a\")}\n  - if: ${aInt == 1}\n    then:\n      - ${testBean.toString(\"success=1\")}\n    else:\n      - if: ${aInt == 2}\n        then:\n          - ${testBean.toString(\"success=2\")}\n        else:\n          - ${testBean.toString(\"success\")}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/029.yml",
    "content": "main:\n  - ${testBean.toString(\"a\")}\n  - if: ${aInt == 1}\n    then:\n      - ${testBean.toString(\"success=1\")}\n    else:\n      - if: ${aInt == 2}\n        then:\n          - ${testBean.toString(\"success=2\")}\n        else:\n          - ${testBean.toString(\"success\")}\n\n  - ${testBean.toString(\"success=end\")}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/030.yml",
    "content": "main:\n- if: ${1 > 0}\n  then:\n     - expr: ${log.info(\"test\", \"Hello, \" += name)}\n     - if: ${2 > 4}\n       then:\n         - expr: ${log.info(\"test\", \"Hello, \" += name)}\n       else:\n         - expr: ${log.info(\"test -- 3\", \"Hello, \" += name)}\n         - expr: ${log.info(\"test -- 4\", \"Hello, \" += name)}\n\n  else:\n     - expr: ${log.info(\"test -- 1\", \"Hello, \" += name)}\n     - expr: ${log.info(\"test -- 2\", \"Hello, \" += name)}"
  },
  {
    "path": "runtime/v1/model/src/test/resources/031.yml",
    "content": "main:\n  - ::\n    - myOtherFlow\n    error:\n      - ${testBean.toString(\"e\")}\n\nmyOtherFlow:\n  - ${testBean.toString(\"in-call-activiti\")}\n  - return: someError"
  },
  {
    "path": "runtime/v1/model/src/test/resources/032.yml",
    "content": "main:\n  - set: {a: \"1234\", b: 2}\n  - ${testBean.toString(a)}\n  - set:\n      a: ${a += \"1\"}\n      b: 3\n  - ${testBean.toString(a)}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/033.yml",
    "content": "main:\n  - docker: test-image\n    cmd: test-cmd\n    env:\n      x: 123\n      y: ${txId}\n    hosts:\n      - foo:10.0.0.3\n      - bar:10.7.3.21"
  },
  {
    "path": "runtime/v1/model/src/test/resources/034.yml",
    "content": "main:\n  - ${testBean.toString(x.y.z)}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/035.yml",
    "content": "main:\n  - task: testTask\n    in:\n      aList:\n        - abc\n        - xyz\n        - 132\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/036.yml",
    "content": "main:\n  - ${testBean.toString(x + 100)}\n\n  - call: myFlow\n    in:\n      x: 2\n\n  - call: myFlow\n    in:\n      x: 3\n\n  - call: myFaultyFlow\n    in:\n      x: 4\n    error:\n      - ${testBean.toString(\"handled!\")}\n\nmyFlow:\n  - ${testBean.toString(x + 100)}\n\nmyFaultyFlow:\n  - ${testBean.toString(x + 100)}\n  - return: ohNoes"
  },
  {
    "path": "runtime/v1/model/src/test/resources/037.yml",
    "content": "main:\n- ${testBean.toString(\"1\")}\n- if: ${false}\n  then:\n  - ${testBean.toString(\"2\")}\n  else:\n  - ::\n    - ${testBean.throwBpmnError(\"kaboom!\")}\n    error:\n    - ${testBean.toString(\"3\")}\n\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/040.yml",
    "content": "main:\n- switch: ${switchValue}\n  a:\n    - ${testBean.toString(\"do a\")}\n    - ${testBean.toString(\"do a2\")}\n  b:\n    - ${testBean.toString(\"do b\")}\n  default:\n    - ${testBean.toString(\"do default\")}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/041.yml",
    "content": "main:\n- switch: ${switchValue}\n  1:\n    - ${testBean.toString(\"do 1\")}\n  2:\n    - ${testBean.toString(\"do 2\")}\n  default:\n    - ${testBean.toString(\"do default\")}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/042.yml",
    "content": "main:\n- switch: ${switchValue}\n  1:\n    - ${testBean.toString(\"do 1\")}\n  2:\n    - ${testBean.toString(\"do 2\")}\n- ${testBean.toString(\"after switch/case\")}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/043.yml",
    "content": "main:\n- switch: ${switchValue}\n  ${caseValue1}:\n    - ${testBean.toString(\"do 1\")}\n  ${caseValue2}:\n    - ${testBean.toString(\"do 2\")}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/044.yml",
    "content": "main:\n- switch: ${switchValue}\n  \"a\":\n    - ${testBean.toString(\"do a\")}\n  ${'default'}:\n    - ${testBean.toString(\"do default as string\")}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/045.yml",
    "content": "main:\n  - try:\n    - myOtherFlow\n    error:\n      - ${testBean.toString(\"err\")}\n\nmyOtherFlow:\n  - return: someError"
  },
  {
    "path": "runtime/v1/model/src/test/resources/046.yml",
    "content": "main:\n  - form: myForm\n\nform (myForm):\n  - favouriteNumbers: { type: \"int+\", min: 0, max: 100 }\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/047.yml",
    "content": "main:\n  - myFlowA\n  - myFlowB\n\nmyFlowA:\n  - ${testBean.toString(\"myFlowA\")}\n  - exit\n\nmyFlowB:\n  - ${testBean.toString(\"myFlowB\")}"
  },
  {
    "path": "runtime/v1/model/src/test/resources/048.yml",
    "content": "main:\n  - task: testErrorTask\n    in:\n      msg: \"test\"\n      nested:\n        a: \"a-value-original\"\n        b: \"b-value-original\"\n    retry:\n      in:                     # optional\n        msg: \"retry\"\n        nested:\n          b: \"b-value-retry\"\n      times: 3\n#      delay: \"1 min\"   # optional\n\n  - ${testBean.toString(\"end\")}"
  },
  {
    "path": "runtime/v1/model/src/test/resources/049.yml",
    "content": "main:\n  - ${testBean.toString(x + 100)}\n\n  - call: myFlow\n    in:\n      x: 2\n    withItems:\n      - item1\n      - ${x}\n      - item3\n\nmyFlow:\n  - ${testBean.toString(x += ':' += item)}"
  },
  {
    "path": "runtime/v1/model/src/test/resources/050.yml",
    "content": "main:\n  - call: myFlow\n    withItems:\n      - ${myArray}\n\nmyFlow:\n  - ${testBean.toString(item)}"
  },
  {
    "path": "runtime/v1/model/src/test/resources/051.yml",
    "content": "main:\n  - call: myFlow\n    withItems:\n      - single1\n      - ${myArray}\n      - singleLast\n\nmyFlow:\n  - ${testBean.toString(item)}"
  },
  {
    "path": "runtime/v1/model/src/test/resources/052.yml",
    "content": "main:\n  - call: myFlow\n    withItems: ${myArray}\n\nmyFlow:\n  - ${testBean.toString(item)}"
  },
  {
    "path": "runtime/v1/model/src/test/resources/053.yml",
    "content": "main:\n  - call: myFlow\n    withItems:\n      - { name: 'testuser1', groups: 'wheel' }\n      - { name: 'testuser2', groups: 'root' }\n\nmyFlow:\n  - ${testBean.toString(item.name += \":\" += item.groups)}"
  },
  {
    "path": "runtime/v1/model/src/test/resources/054.yml",
    "content": "main:\n  - task: myLogger\n    in:\n       message: \"hello ${item}\"\n    withItems:\n    - red\n    - green\n    - blue"
  },
  {
    "path": "runtime/v1/model/src/test/resources/055.yml",
    "content": "main:\n  - task: myLogger\n    in:\n       message: \"hello ${item}\"\n    withItems:\n    - red\n    - ${x}\n    - blue"
  },
  {
    "path": "runtime/v1/model/src/test/resources/056.yml",
    "content": "main:\n  - task: myLogger\n    in:\n       message: \"hello ${item}\"\n    withItems:\n    - ${myArray}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/057.yml",
    "content": "main:\n  - task: myLogger\n    in:\n       message: \"hello ${item}\"\n    withItems: ${myArray}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/058.yml",
    "content": "main:\n  - task: testErrorTask\n    in:\n      msg: \"test: ${item}\"\n    retry:\n      in:\n        msg: \"retry: ${item}\"\n      times: 3\n    withItems:\n      - item1\n      - item2\n\n  - ${testBean.toString(\"end\")}"
  },
  {
    "path": "runtime/v1/model/src/test/resources/059.yml",
    "content": "main:\n  - task: myLogger\n    in:\n       message: \"hello ${item}\"\n    withItems: ${myArray}\n\n  - call: myFlow\n    withItems:\n      - item1\n      - item2\n      - item3\n\nmyFlow:\n- task: myLogger\n  in:\n     message: \"hello from call ${item}\"\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/060.yml",
    "content": "main:\n  - task: myLogger\n    in:\n       message: \"hello ${item}\"\n    withItems: ${nullVariable}"
  },
  {
    "path": "runtime/v1/model/src/test/resources/061.yml",
    "content": "main:\n  - task: myLogger\n    in:\n       message: \"hello ${item}\"\n    withItems: ${notArrayVariable}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/062.yml",
    "content": "main:\n  - call: ${calledFlow}\n\nmyFlow1:\n  - ${testBean.toString('from MyFlow1')}"
  },
  {
    "path": "runtime/v1/model/src/test/resources/063.yml",
    "content": "main:\n  - checkpoint: \"one\"\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/064.yml",
    "content": "main:\n  - task: myLogger\n    in:\n      message: \"None of these are null -> ${item}\"\n    withItems: ${fineList}\n  - task: myLogger\n    in:\n      message: \"It's okay to have a null item -> ${item}\"\n    withItems: ${listWithNull}\n  - task: myLogger\n    in:\n      msg: \"Won't print because the list is null -> ${item}\"\n    withItems: ${nullVariable}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/065.yml",
    "content": "nested:\n  - ${execution.setVariable(\"outer\", item)}\n  - task: myLogger\n    in:\n      message: \"${outer} ${item}\"\n    withItems:\n    - \"0\"\n    - \"1\"\n\ndefault:\n  - call: nested\n    withItems:\n    - \"A\"\n    - \"B\"\n    - \"C\""
  },
  {
    "path": "runtime/v1/model/src/test/resources/066.yml",
    "content": "nested:\n  - ${execution.setVariable(\"outer\", item)}\n  - task: myLogger\n    in:\n      message: \"${outer} ${item}\"\n    withItems: ${nestedWithItems}\n\ndefault:\n  - call: nested\n    withItems:\n    - \"A\"\n    - \"B\"\n    - \"C\""
  },
  {
    "path": "runtime/v1/model/src/test/resources/067.yml",
    "content": "nested:\n  - ${execution.setVariable(\"outer\", item)}\n  - task: myTask\n    in:\n      message: \"${outer} ${item}\"\n    out:\n      myVar: ${var}\n    withItems: ${nestedWithItems}\n\ndefault:\n  - call: nested\n    out:\n      myVarOut: ${myVar}\n    withItems:\n    - \"A\"\n    - \"B\"\n    - \"C\"\n\n  - task: myLogger\n    in:\n      message: \"${myVarOut}\"\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/068.yml",
    "content": "main:\n  - task: testErrorTask\n    in:\n      msg: \"test\"\n    retry:\n      in:\n        msg: \"retry\"\n      times: 1\n\n  - task: testErrorTask2\n    in:\n      msg: \"test2\"\n    retry:\n      in:\n        msg: \"retry2\"\n      times: 1"
  },
  {
    "path": "runtime/v1/model/src/test/resources/069.yml",
    "content": "main:\n  - task: myLogger\n    in:\n       message: \"hello ${item}\"\n    withItems: ${\"a,b,c\".split(\",\")}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/070.yml",
    "content": "default:\n  - call: reproduceBug\n\nreproduceBug:\n  - call: doSomething\n    withItems:\n      - id: 0\n        words:\n          - \"apple\"\n          - \"orange\"\n          - \"bannana\"\n      - id: 1\n        words:\n          - \"dog\"\n          - \"cat\"\n          - \"mouse\"\n      - id: 2\n        words:\n          - \"one\"\n          - \"two\"\n          - \"three\"\n\ndoSomething:\n  - log: \"Something: ${item.words}, ${item.id}\"\n  - call: doSomethingElse\n    withItems: ${item.words}\n\ndoSomethingElse:\n  - log: \"Something else: ${item}\"\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/071.yml",
    "content": "main:\n  - task: myLogger\n    in:\n       message: \"hello ${item.key} -> ${item.value}\"\n    withItems: ${myMap}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/072.yml",
    "content": "main:\n  - log: \"flow: ${__currentFlow}\"\n  - call: myFlow\n    in:\n      x: 2\n  - log: \"flow: ${__currentFlow}\"\n\n  - call: myFaultyFlow\n    error:\n      - log: \"error handler: flow: ${__currentFlow}\"\n\n  - log: \"flow: ${__currentFlow}\"\n\nmyFlow:\n  - log: \"flow: ${__currentFlow}\"\n  - call: myFlow2\n\nmyFlow2:\n  - log: \"flow: ${__currentFlow}\"\n\nmyFaultyFlow:\n  - log: \"flow: ${__currentFlow}\"\n  - return: ohNoes"
  },
  {
    "path": "runtime/v1/model/src/test/resources/073.yml",
    "content": "main:\n  - log: \"Hello, ${nested}\"\n  - set:\n      nested:\n        z: ${nested}\n  - log: \"Hello, ${nested.z}\"\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/074.yml",
    "content": "myFlow:\n  - try:\n      - log: \"Fetching item: ${item}\"\n      - task: http\n        retry:\n          times: 3\n          delay: 1\n        in:\n          url: \"https://nonexistant.example.com/test/${item}\"\n          response: string\n          out: response\n\n    error:\n      - log: \"woowee: ${lastError}\"\n\ndefault:\n  - call: myFlow\n    withItems:\n    - \"a\"\n    - \"b\"\n    - \"c\""
  },
  {
    "path": "runtime/v1/model/src/test/resources/075.yml",
    "content": "myFlow:\n  - ${testBean.throwBpmnError('boom')}\n\ndefault:\n  - call: myFlow\n    retry:\n      times: 2\n      delay: 1\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/076.yml",
    "content": "default:\n  - call: calledFlow\n    retry:\n      times: 1\n      delay: 1\n\ncalledFlow:\n  - log: \"Here's calledFlow\"\n  - call: nestedFlow\n    retry:\n      times: 1\n      delay: 1\n\n  - log: \"Error NOW!\"\n  - throw: \"Pretend there's an error\"\n\nnestedFlow:\n  - log: \"Here's nestedFlow called by calledFlow\""
  },
  {
    "path": "runtime/v1/model/src/test/resources/077.yml",
    "content": "main:\n  - task: testErrorTask\n    in:\n      msg: \"test\"\n    retry:\n      in:\n        msg: \"retry\"\n      times: \"${1}\"\n      delay: \"${1}\"   # optional\n\n  - ${testBean.toString(\"end\")}"
  },
  {
    "path": "runtime/v1/model/src/test/resources/100.yml",
    "content": "main:\n  - ${testBean.toString(\"aaa\")}\n  - form: myForm\n  - ${testBean.toString(myForm.name)}\n\nform (myForm):\n  - name: { type: \"string\" }\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/101.yml",
    "content": "main:\n  - form: myForm\n  - ${testBean.toString(myForm.age)}\n\nform (myForm):\n  - age: { type: \"int\", min: 21, max: 200 }\n  - percent: { type: \"decimal?\", min: 0, max: 1 }"
  },
  {
    "path": "runtime/v1/model/src/test/resources/102.yml",
    "content": "main:\n  - form: myForm\n  - ${testBean.toString(myForm.favouriteNumbers)}\n\nform (myForm):\n  - favouriteNumbers: { type: \"int+\", min: 0, max: 100 }\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/103.yml",
    "content": "main:\n  - form: myForm\n  - ${testBean.toString(myForm.testValue)}\n\nform (myForm):\n  - testValue: { type: \"string\", value: \"${'Hello, '.concat(inputValue)}\" }\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/104.yml",
    "content": "main:\n  - form: myForm\n  - ${testBean.toString(myForm.testValue)}\n\nform (myForm):\n  - testValue: { type: \"string\", allow: \"${testBean.items}\" }\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/105.yml",
    "content": "main:\n  - form: myForm\n  - ${testBean.toString(myForm.testValue)}\n\nform (myForm):\n  - testValue: { type: \"string+\", allow: \"${testBean.items}\" }\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/106.yml",
    "content": "main:\n  - form: myForm\n  - ${testBean.toString(myForm.testValue)}\n\nform (myForm):\n  - testValue: { type: \"string\", allow: [\"a\", \"b\", \"c\"] }\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/107.yml",
    "content": "main:\n  - form: myForm\n    yield: true\n  - ${testBean.toString(myForm.testValue)}\n\nform (myForm):\n  - testValue: { type: \"string\", value: \"something\" }\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/108.yml",
    "content": "main:\n  - form: myForm\n    runAs:\n      ldap:\n      - group: \"CN=Strati-SDE-Concord-sdeconcord,.*\"\n      - group: \"CN=Open Source Developers-opensource_devs,.*\"\n\n  - ${testBean.toString(myForm.testValue)}\n\nform (myForm):\n  - testValue: { type: \"string\", value: \"something\" }\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/109.yml",
    "content": "main:\n  - form: myForm\n    runAs:\n      ldap:\n        group: \"CN=Strati-SDE-Concord-sdeconcord,.*\"\n\n  - ${testBean.toString(myForm.testValue)}\n\nform (myForm):\n  - testValue: { type: \"string\", value: \"something\" }\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/110.yml",
    "content": "main:\n  - form: myForm\n    fields:\n    - myField: {type: \"string\"}\n  - ${testBean.toString(myForm.myField)}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/111.yml",
    "content": "main:\n  - set:\n      commitEvent:\n        event: 'push'\n        branch: '${event.branch}'\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/112.yml",
    "content": "main:\n  - set:\n      obj.name: \"Concord\"\n      obj.msg: \"Hello, ${obj.name}\"\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/113.yml",
    "content": "main:\n  - task: testTask\n    in:\n      num: 1\n    out:\n      result: ${TestTaskWithResumeResult}\n\n  - task: myLogger\n    in:\n      message: \"${result}\"\n\n  - task: testTask\n    in:\n      num: 2\n    out:\n      result: ${TestTaskWithResumeResult}\n\n  - task: myLogger\n    in:\n      message: \"${result}\"\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/brokenFlows/concord.yml",
    "content": "flows:\n  default:\n  - log: \"This is fine\"\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/brokenFlows/flows/test.yml",
    "content": "test:\n  whop dee doo"
  },
  {
    "path": "runtime/v1/model/src/test/resources/brokenMain/concord.yml",
    "content": "flows:\n  default:\n    - {}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/brokenProfiles/profiles/test.yml",
    "content": "profileA:\n  variables:\n  - x: {\n  - y:\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/complex/.concord.yml",
    "content": "imports:\n  - git: {url: \"http://host/org/repo\", version: \"master\", path: \"dir\", dest: \"concord\"}\n  - git:\n      name: \"git-org/concord-template\"\n      version: \"dev\"\n      path: \"root\"\n      dest: \"tmp\"\n      secret:\n        org: \"Default\"\n        name: \"secret-name\"\n  - mvn:\n      url: \"mvn://maven/my-artifact.jar\"\n\nresources:\n  flows: myFlows\n  profiles: myProfiles\n  disabled:\n    - processes\n\nflows:\n  main:\n  - ${myTask.method(arg)}\n\n  - expr: ${myTask.method(arg)}\n\n  - expr: ${myTask.method(arg)}\n    out: myVar\n    error:\n    - ${handleError}\n\n  - myTask: [arg1, arg2]\n\n  - log: |\n      multiline\n      stuff\n\n  - task: myTask\n    in:\n      inVar: myVar\n      otherInVar: ${myTask.doSomething()}\n    out:\n      outVar: inVar\n    error:\n    - ${handleError}\n\n  - if: ${condition}\n    then:\n    - log: it's true\n    else:\n    - log: nope\n\n  - if: ${condition}\n    then:\n    - log: it's also true\n\n  - ::\n    - ${task.doSomethingDangerous()}\n    error:\n    - ${handleError}\n\n  - myOtherFlow\n\n  - script: js\n    body: |\n      function hello() {\n        console.log(\"Hello\");\n      }\n\n  myOtherFlow:\n  - log: hola!\n  - form: myForm\n\nforms:\n  myForm:\n  - fullName: { label: \"Name\", type: \"string\", pattern: \".* .*\" }\n  - age: { label: \"Age\", type: \"int\", min: 21, max: 100 }\n  - favouriteColour: { label: \"Favourite colour\", type: \"string\", allow: [\"gray\", \"grey\"] }\n  - languages: { label: \"Preferred languages\", type: \"string+\", allow: \"${locale.languages()}\" }\n\nvariables:\n  nested:\n    stuff: true\n\nprofiles:\n  myProfile:\n    flows:\n      myEmptyFlow:\n      - log: not so empty!\n    forms:\n      additionalForm:\n      - field1: {type: \"string\"}\n    variables:\n      more:\n        nested:\n          stuff: false\n\ntriggers:\n  # OneOps compute replacement event\n  - oneops:\n      org: \"myOrg\"\n      asm: \"myAsm\"\n      env: \"myEnv\"\n      platform: \"myPlatform\"\n      component: \"compute\"\n      type: \"replace\"\n      state: \"complete\"\n      entryPoint: doAfterReplacement\n      arguments:\n        message: \"We got some ${event.payload}\"\n  - github:\n      repository: myRepo\n      action: push\n      branch: \"*\"\n      entryPoint: doItAfterPush\n      arguments:\n        message: \"${event.action} ${event.commitId} ${event.author}\"\n  - manual:\n      name: Build\n      entryPoint: main\n  - manual:\n      name: Run Integration Tests\n      entryPoint: integration\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/duplicateConfiguration/.concord.yml",
    "content": "configuration:\n  arguments:\n    x1: 1\n    y2: 2\n\n\nflows:\n  main:\n  - ${example.hello()}\n\nconfiguration:\n  arguments:\n    x: 123\n    y: 234\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/duplicateConfigurationVariable/.concord.yml",
    "content": "flows:\n  main:\n  - ${example.hello()}\n\nconfiguration:\n  arguments:\n    x: 123\n    y: 234\n    x: 1235\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/emptyField/.concord.yml",
    "content": "flows:\n  main:\n  - ${example.hello()}\n  other:\n  - ${olala}\n\nforms:\n  myForm:\n  - testField: {type: \"string\"}\n  -\n\nvariables:\n  dependencies: [\"abc\"]\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/externalscript/test.js",
    "content": "execution.setVariable(\"x\", 12345);"
  },
  {
    "path": "runtime/v1/model/src/test/resources/junk.yml",
    "content": "main:\n  - form: myForm1\n  - form: myForm2\n  - ${log.info(\"test\", myForm1.firstName.concat(' ').concat(myForm2.lastName))}\n\nform (myForm1):\n  - firstName: { label: \"Fist Name\", type: \"string\" }\n\nform (myForm2):\n  - lastName: { label: \"Last Name\", type: \"string\" }\n  - age: { label: \"Age\", type: \"decimal?\", min: 21, max: 120 }\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/logback-test.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] [%-5level] %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/multiProjectFile/concord/0.yml",
    "content": "flows:\n  default:\n  - log: \"bye!\"\n\nforms:\n  myForm:\n  - myName: {type: \"string\"}\n\ntriggers:\n  - oneops:\n      stuff: \"yep\"\n      entryPoint: \"default\"\n\nconfiguration:\n  arguments:\n    abc: \"xyz\"\n    nested:\n      value: \"123\""
  },
  {
    "path": "runtime/v1/model/src/test/resources/multiProjectFile/concord/1.yml",
    "content": "configuration:\n  arguments:\n    abc: \"ttt\"\n    nested:\n      value: \"123\""
  },
  {
    "path": "runtime/v1/model/src/test/resources/multiProjectFile/concord.yml",
    "content": "flows:\n  default:\n  - log: \"hello!\"\n\nconfiguration:\n  arguments:\n    nested:\n      value: \"234\""
  },
  {
    "path": "runtime/v1/model/src/test/resources/old.yml",
    "content": "simple1:\n- expr: ${test.sayHello()}\n\nsimple2:\n- subprocess:\n  steps:\n  - expr: ${test.sayHello()}\n- expr: ${test.sayBye()}\n\nsimple3:\n- call: simple1\n- call: simple2\n\nsimple4:\n- end: kaboom\n\nsimple5:\n- subprocess:\n  errors:\n  - ref: myError\n    call: simple1\n  steps:\n  - expr: ${test.doThrow(\"myError\")}\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/simple/.concord.yml",
    "content": "flows:\n  main:\n  - ${example.hello()}\n  other:\n  - ${olala}\n\nforms:\n  myForm:\n  - testField: {type: \"string\"}\n\nvariables:\n  dependencies: [\"abc\"]\n"
  },
  {
    "path": "runtime/v1/model/src/test/resources/validator/002.yml",
    "content": "main:\n  - checkpoint: \"one\"\n  - log: \"trololo\"\n  - checkpoint: \"one\""
  },
  {
    "path": "runtime/v1/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <groupId>com.walmartlabs.concord.runtime.v1</groupId>\n    <artifactId>parent</artifactId>\n    <packaging>pom</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <modules>\n        <module>model</module>\n        <module>impl</module>\n    </modules>\n</project>\n"
  },
  {
    "path": "runtime/v2/model/README.md",
    "content": "# Concord Runtime V2 Model Classes\n\nA few notable differences to the v1 model classes:\n- `ProjectDefinition` is `ProcessDefinition` now. As before it contains flows, forms, profiles, etc.\n- TBD"
  },
  {
    "path": "runtime/v2/model/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-runtime-model-v2</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime</groupId>\n            <artifactId>concord-runtime-model</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime</groupId>\n            <artifactId>concord-runtime-loader</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-imports</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-forms</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-repository</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.dataformat</groupId>\n            <artifactId>jackson-dataformat-yaml</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-guava</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jdk8</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jsr310</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.takari</groupId>\n            <artifactId>parc</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>serial</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.errorprone</groupId>\n            <artifactId>error_prone_annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>com.kjetland</groupId>\n            <artifactId>mbknor-jackson-jsonschema_2.12</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-core</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.networknt</groupId>\n            <artifactId>json-schema-validator</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>exec-maven-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <phase>process-classes</phase>\n                        <goals>\n                            <goal>java</goal>\n                        </goals>\n                        <configuration>\n                            <mainClass>com.walmartlabs.concord.runtime.v2.ConcordJsonSchemaGenerator</mainClass>\n                            <arguments>\n                                <argument>${project.build.outputDirectory}/concord-schema.json</argument>\n                            </arguments>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>build-helper-maven-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>attach-json-schema</id>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>attach-artifact</goal>\n                        </goals>\n                        <configuration>\n                            <artifacts>\n                                <artifact>\n                                    <file>${project.build.outputDirectory}/concord-schema.json</file>\n                                    <type>json</type>\n                                    <classifier>schema</classifier>\n                                </artifact>\n                            </artifacts>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/ConcordJsonSchemaGenerator.java",
    "content": "package com.walmartlabs.concord.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.*;\nimport com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;\nimport com.fasterxml.jackson.databind.module.SimpleModule;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.kjetland.jackson.jsonSchema.*;\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.runtime.v2.model.*;\nimport com.walmartlabs.concord.runtime.v2.schema.*;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardOpenOption;\nimport java.time.Duration;\nimport java.util.*;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\n\npublic class ConcordJsonSchemaGenerator {\n\n    public static JsonNode generate() {\n        ObjectMapper objectMapper = new ObjectMapper();\n        objectMapper.registerModule(new JsonSchemaModule());\n\n        JsonSchemaGenerator jsonSchemaGenerator = new JsonSchemaGenerator(objectMapper, config().withJsonSchemaDraft(JsonSchemaDraft.DRAFT_07));\n        JsonNode jsonSchema = jsonSchemaGenerator.generateJsonSchema(ProcessDefinition.class);\n\n        // remove type attribute for entities with `@JsonTypeInfo`\n        clearAllProperty(jsonSchema, \"@type\");\n        clearProperty(path(jsonSchema, \"definitions/ImmutableMvnDefinition\"), \"type\");\n        clearProperty(path(jsonSchema, \"definitions/ImmutableGitDefinition\"), \"type\");\n        clearProperty(path(jsonSchema, \"definitions/ImmutableDirectoryDefinition\"), \"type\");\n\n        clearAllProperty(jsonSchema, \"removeMe\");\n\n        // SwitchStep\n        JsonNode switchDefault = path(jsonSchema, \"definitions/SwitchStep/properties/default\");\n        ObjectNode switchStep = (ObjectNode) path(jsonSchema, \"definitions/SwitchStep\");\n        switchStep.set(\"additionalProperties\", switchDefault);\n\n        // remove invalid required primitive attributes\n        removeRequired(path(jsonSchema, \"definitions/ProcessDefinitionConfiguration\"), \"debug\", \"parallelLoopParallelism\");\n        removeRequired(path(jsonSchema, \"definitions/EventConfiguration\"), \"recordEvents\", \"recordTaskInVars\", \"truncateInVars\", \"truncateMaxStringLength\", \"truncateMaxArrayLength\", \"truncateMaxDepth\", \"recordTaskOutVars\", \"truncateOutVars\", \"recordTaskMeta\", \"truncateMeta\");\n        removeRequired(path(jsonSchema, \"definitions/TaskCall\"), \"ignoreErrors\");\n\n        // remove invalid Object definition\n        /*\n            \"additionalProperties\" : {\n              \"$ref\" : \"#/definitions/Object\"\n            }\n         */\n        removeFieldIf(jsonSchema, \"additionalProperties\", n -> \"#/definitions/Object\".equals(n.path(\"$ref\").asText()));\n\n        return jsonSchema;\n    }\n\n    public static void main(String[] args) throws Exception {\n        JsonNode jsonSchema = generate();\n\n        try (OutputStream os = outputStream(args)) {\n            new ObjectMapper().writerWithDefaultPrettyPrinter()\n                    .writeValue(os, jsonSchema);\n        }\n    }\n\n    private static OutputStream outputStream(String[] args) throws IOException {\n        if (args.length == 1) {\n            Path schemaFile = Paths.get(args[0]);\n            if (Files.notExists(schemaFile.getParent())) {\n                Files.createDirectories(schemaFile.getParent());\n            }\n            return Files.newOutputStream(schemaFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);\n        } else {\n            return System.out;\n        }\n    }\n\n    private static JsonSchemaConfig config() {\n        boolean autoGenerateTitleForProperties = false;\n        Optional<String> defaultArrayFormat = Optional.empty();\n        boolean useOneOfForOption = false;\n        boolean useOneOfForNullables = false;\n        boolean usePropertyOrdering = false;\n        boolean hidePolymorphismTypeProperty = false;\n        boolean disableWarnings = false;\n        boolean useMinLengthForNotNull = false;\n        boolean useTypeIdForDefinitionName = false;\n        Map<String, String> customType2FormatMapping = Collections.emptyMap();\n        boolean useMultipleEditorSelectViaProperty = false;\n        Set<Class<?>> uniqueItemClasses = Collections.emptySet();\n        Map<Class<?>, Class<?>> classTypeReMapping = new HashMap<>();\n        classTypeReMapping.put(Imports.class, ImportsMixIn.class);\n        classTypeReMapping.put(Form.class, Object.class);\n        classTypeReMapping.put(Flow.class, FlowsMixIn.class);\n        Map<String, Supplier<JsonNode>> jsonSuppliers = Collections.emptyMap();\n        SubclassesResolver subclassesResolver = new SubclassesResolverImpl();\n        boolean failOnUnknownProperties = true;\n        List<Class<?>> javaxValidationGroups = null;\n\n        return JsonSchemaConfig.create(autoGenerateTitleForProperties, defaultArrayFormat, useOneOfForOption, useOneOfForNullables,\n                usePropertyOrdering, hidePolymorphismTypeProperty, disableWarnings, useMinLengthForNotNull, useTypeIdForDefinitionName,\n                customType2FormatMapping, useMultipleEditorSelectViaProperty, uniqueItemClasses, classTypeReMapping, jsonSuppliers,\n                subclassesResolver, failOnUnknownProperties, javaxValidationGroups);\n\n    }\n\n    private static JsonNode path(JsonNode root, String path) {\n        JsonNode n = root;\n        for (String p : path.split(\"/\")) {\n            n = n.path(p);\n        }\n        return n;\n    }\n\n    private static void removeProperty(JsonNode node, String propName) {\n        JsonNode propsNode = node.path(\"properties\");\n        if (propsNode instanceof ObjectNode) {\n            ((ObjectNode) propsNode).remove(propName);\n        }\n    }\n\n    private static void removeRequired(JsonNode node, String... fieldNames) {\n        JsonNode requiredNode = node.path(\"required\");\n        if (requiredNode.isMissingNode()) {\n            return;\n        }\n\n        for (Iterator<JsonNode> it = requiredNode.elements(); it.hasNext(); ) {\n            JsonNode n = it.next();\n            if (Arrays.stream(fieldNames).anyMatch(f -> f.equals(n.asText()))) {\n                it.remove();\n            }\n        }\n        if (!requiredNode.elements().hasNext()) {\n            ((ObjectNode) node).remove(\"required\");\n        }\n    }\n\n    private static void clearProperty(JsonNode node, String propName) {\n        removeProperty(node, propName);\n        removeRequired(node, propName);\n    }\n\n    private static void clearAllProperty(JsonNode node, String propName) {\n        for (Iterator<JsonNode> it = node.elements(); it.hasNext(); ) {\n            JsonNode n = it.next();\n            if (n instanceof ObjectNode) {\n                clearProperty(n, propName);\n                clearAllProperty(n, propName);\n            }\n        }\n    }\n\n    private static void removeFieldIf(JsonNode root, String fieldName, Predicate<JsonNode> p) {\n        for (Iterator<JsonNode> it = root.elements(); it.hasNext(); ) {\n            JsonNode n = it.next();\n            JsonNode ap = n.path(fieldName);\n            if (!ap.isMissingNode() && p.test(ap)) {\n                ((ObjectNode) n).remove(fieldName);\n            } else {\n                removeFieldIf(n, fieldName, p);\n            }\n        }\n    }\n\n    private static class JsonSchemaModule extends SimpleModule {\n\n        private static final long serialVersionUID = 1L;\n\n        public JsonSchemaModule() {\n            setMixInAnnotation(ProcessDefinition.class, ProcessDefinitionMixIn.class);\n            setMixInAnnotation(ProcessDefinitionConfiguration.class, ProcessDefinitionConfigurationMixIn.class);\n            setMixInAnnotation(Trigger.class, TriggerMixIn.class);\n            setMixInAnnotation(Step.class, StepMixIn.class);\n\n            addSerializer(Duration.class, new StdSerializer<>(Duration.class) {\n                private static final long serialVersionUID = 1L;\n\n                @Override\n                public void serialize(Duration value, JsonGenerator gen, SerializerProvider provider) {\n                    // do nothing\n                }\n\n                @Override\n                public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException {\n                    visitor.expectStringFormat(typeHint);\n                }\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/Constants.java",
    "content": "package com.walmartlabs.concord.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic final class Constants {\n\n    public static final String SEGMENT_NAME = \"segmentName\";\n    public static final String LOG_LEVEL = \"logLevel\";\n\n    private Constants() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/ImportsNormalizer.java",
    "content": "package com.walmartlabs.concord.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Imports;\n\n@FunctionalInterface\npublic interface ImportsNormalizer {\n\n    Imports normalize(Imports imports);\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/NoopImportsNormalizer.java",
    "content": "package com.walmartlabs.concord.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Imports;\n\npublic class NoopImportsNormalizer implements ImportsNormalizer {\n\n    @Override\n    public Imports normalize(Imports imports) {\n        return imports;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/ProcessDefinitionUtils.java",
    "content": "package com.walmartlabs.concord.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.*;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\npublic final class ProcessDefinitionUtils {\n\n    public static String getCurrentFlowName(ProcessDefinition processDefinition, Step currentStep) {\n        if (currentStep == null) {\n            return null;\n        }\n\n        for (Map.Entry<String, Flow> e : processDefinition.flows().entrySet()) {\n            if (containsStep(e.getValue().steps(), currentStep)) {\n                return e.getKey();\n            }\n        }\n\n        return null;\n    }\n\n    private static boolean containsStep(List<Step> steps, Step step) {\n        if (steps == null) {\n            return false;\n        }\n\n        for (Step s : steps) {\n            if (s.getLocation().equals(step.getLocation())) {\n                return true;\n            } else if (s instanceof IfStep) {\n                boolean contains = containsStep(((IfStep) s).getThenSteps(), step)\n                        || containsStep(((IfStep) s).getElseSteps(), step);\n                if (contains) {\n                    return true;\n                }\n            } else if (s instanceof SwitchStep) {\n                List<Step> caseSteps = null;\n                if (((SwitchStep) s).getCaseSteps() != null) {\n                    caseSteps = ((SwitchStep) s).getCaseSteps().stream().flatMap(o -> o.getValue().stream()).collect(Collectors.toList());\n                }\n                boolean contains = containsStep(((SwitchStep) s).getDefaultSteps(), step)\n                        || containsStep(caseSteps, step);\n                if (contains) {\n                    return true;\n                }\n            } else if (s instanceof GroupOfSteps) {\n                GroupOfStepsOptions options = ((GroupOfSteps)s).getOptions();\n                if (options != null && containsStep(options.errorSteps(),step)) {\n                    return true;\n                }\n                if (containsStep(((GroupOfSteps) s).getSteps(), step)) {\n                    return true;\n                }\n            } else if (s instanceof ParallelBlock) {\n                if (containsStep(((ParallelBlock) s).getSteps(), step)) {\n                    return true;\n                }\n            } else if (s instanceof TaskCall) {\n                TaskCallOptions options = ((TaskCall)s).getOptions();\n                if (options != null && containsStep(options.errorSteps(),step)) {\n                    return true;\n                }\n            } else if (s instanceof FlowCall) {\n                FlowCallOptions options = ((FlowCall)s).getOptions();\n                if (options != null && containsStep(options.errorSteps(),step)) {\n                    return true;\n                }\n            } else if (s instanceof ScriptCall) {\n                ScriptCallOptions options = ((ScriptCall)s).getOptions();\n                if (options != null && containsStep(options.errorSteps(),step)) {\n                    return true;\n                }\n            } else if (s instanceof Expression) {\n                ExpressionOptions options = ((Expression)s).getOptions();\n                if (options != null && containsStep(options.errorSteps(),step)) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    private ProcessDefinitionUtils() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/ProjectLoaderV2.java",
    "content": "package com.walmartlabs.concord.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.imports.Import;\nimport com.walmartlabs.concord.imports.ImportManager;\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.imports.ImportsListener;\nimport com.walmartlabs.concord.process.loader.ProjectLoader;\nimport com.walmartlabs.concord.repository.Snapshot;\nimport com.walmartlabs.concord.runtime.v2.model.*;\nimport com.walmartlabs.concord.runtime.v2.parser.YamlParserV2;\nimport com.walmartlabs.concord.runtime.v2.wrapper.ProcessDefinitionV2;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.*;\nimport java.util.*;\nimport java.util.stream.Stream;\n\nimport static com.walmartlabs.concord.process.loader.StandardRuntimeTypes.CONCORD_V2_RUNTIME_TYPE;\nimport static com.walmartlabs.concord.process.loader.StandardRuntimeTypes.PROJECT_ROOT_FILE_NAMES;\n\npublic class ProjectLoaderV2 implements ProjectLoader {\n\n    private final ImportManager importManager;\n\n    @Inject\n    public ProjectLoaderV2(ImportManager importManager) {\n        this.importManager = importManager;\n    }\n\n    @Override\n    public boolean supports(String runtime) {\n        return CONCORD_V2_RUNTIME_TYPE.equals(runtime);\n    }\n\n    @Override\n    public ProjectLoader.Result loadProject(Path workDir, String runtime, com.walmartlabs.concord.process.loader.ImportsNormalizer importsNormalizer, ImportsListener listener) throws Exception {\n        var v2Result = load(workDir, importsNormalizer::normalize, listener);\n        return toCommonResultType(v2Result);\n    }\n\n    public Result load(Path baseDir, ImportsNormalizer importsNormalizer, ImportsListener listener) throws Exception {\n        YamlParserV2 parser = new YamlParserV2();\n\n        // load the initial ProcessDefinition from the root concord.yml file\n        // it will be used to determine whether we need to load other resources (e.g. imports)\n        ProcessDefinition root = loadRoot(parser, baseDir);\n\n        List<Snapshot> snapshots = Collections.emptyList();\n        if (root != null) {\n            Imports imports = importsNormalizer.normalize(root.imports());\n            snapshots = importManager.process(imports, baseDir, listener);\n        }\n\n        List<Path> files = loadResources(baseDir, root != null ? root.resources() : Resources.builder().build());\n        Collections.sort(files);\n\n        List<ProcessDefinition> definitions = new ArrayList<>();\n        for (Path p : files) {\n            ProcessDefinition pd = parser.parse(baseDir, p);\n            definitions.add(pd);\n        }\n\n        if (root != null) {\n            definitions.add(root);\n        }\n\n        if (definitions.isEmpty()) {\n            throw new IllegalStateException(\"Can't find any Concord process definition files in '\" + baseDir + \"'\");\n        }\n\n        return new Result(snapshots, merge(definitions));\n    }\n\n    public void export(Path baseDir, Path destDir, ImportsNormalizer importsNormalizer, ImportsListener listener, CopyOption... options) throws Exception {\n        YamlParserV2 parser = new YamlParserV2();\n\n        ProcessDefinition root = loadRoot(parser, baseDir);\n\n        Resources resources = root != null ? root.resources() : Resources.builder().build();\n        boolean hasImports = root != null && root.imports() != null && !root.imports().isEmpty();\n        if (!hasImports) {\n            copyResources(baseDir, resources, destDir, options);\n            return;\n        }\n\n        Path tmpDir = null;\n        try {\n            tmpDir = PathUtils.createTempDir(\"concord-export\");\n            copyResources(baseDir, resources, tmpDir, options);\n\n            Imports imports = importsNormalizer.normalize(root.imports());\n            importManager.process(imports, tmpDir, listener);\n\n            copyResources(tmpDir, resources, destDir, options);\n        } finally {\n            if (tmpDir != null) {\n                PathUtils.deleteRecursively(tmpDir);\n            }\n        }\n    }\n\n    private ProcessDefinition loadRoot(YamlParserV2 parser, Path baseDir) throws IOException {\n        for (String fileName : PROJECT_ROOT_FILE_NAMES) {\n            Path p = baseDir.resolve(fileName);\n            if (Files.exists(p)) {\n                ProcessDefinition result = parser.parse(baseDir, p);\n                return result;\n            }\n        }\n        return null;\n    }\n\n    public Result loadFromFile(Path path) throws IOException {\n        YamlParserV2 parser = new YamlParserV2();\n\n        if (Files.notExists(path)) {\n            throw new IllegalStateException(\"Can't find Concord process definition file: \" + path);\n        }\n\n        return new Result(Collections.emptyList(), parser.parse(path.getParent(), path));\n    }\n\n    private static List<Path> loadResources(Path baseDir, Resources resources) throws IOException {\n        List<Path> result = new ArrayList<>();\n        for (String pattern : resources.concord()) {\n            PathMatcher pathMatcher = parsePattern(baseDir, pattern);\n            if (pathMatcher != null) {\n                try (Stream<Path> w = Files.walk(baseDir)) {\n                    w.filter(pathMatcher::matches).forEach(result::add);\n                }\n            } else {\n                Path path = Paths.get(concat(baseDir, pattern.trim()));\n                if (Files.exists(path)) {\n                    result.add(path);\n                }\n            }\n        }\n        return result;\n    }\n\n    private static PathMatcher parsePattern(Path baseDir, String pattern) {\n        String normalizedPattern = null;\n\n        pattern = pattern.trim();\n\n        if (pattern.startsWith(\"glob:\")) {\n            normalizedPattern = \"glob:\" + concat(baseDir, pattern.substring(\"glob:\".length()));\n        } else if (pattern.startsWith(\"regex:\")) {\n            normalizedPattern = \"regex:\" + concat(baseDir, pattern.substring(\"regex:\".length()));\n        }\n\n        if (normalizedPattern != null) {\n            return FileSystems.getDefault().getPathMatcher(normalizedPattern);\n        }\n\n        return null;\n    }\n\n    private static ProcessDefinition merge(List<ProcessDefinition> definitions) {\n        if (definitions.isEmpty()) {\n            throw new IllegalArgumentException(\"Definitions is empty\");\n        }\n\n        Map<String, Flow> flows = new LinkedHashMap<>();\n        Map<String, Profile> profiles = new LinkedHashMap<>();\n        List<Trigger> triggers = new ArrayList<>();\n        List<Import> imports = new ArrayList<>();\n        Map<String, Form> forms = new LinkedHashMap<>();\n        Set<String> resources = new HashSet<>();\n        Set<String> dependencies = new HashSet<>();\n        Set<String> extraDependencies = new HashSet<>();\n        Map<String, Object> arguments = new LinkedHashMap<>();\n\n        for (ProcessDefinition pd : definitions) {\n            flows.putAll(pd.flows());\n            profiles.putAll(pd.profiles());\n            triggers.addAll(pd.triggers());\n            imports.addAll(pd.imports().items());\n            forms.putAll(pd.forms());\n            resources.addAll(pd.resources().concord());\n            dependencies.addAll(pd.configuration().dependencies());\n            extraDependencies.addAll(pd.configuration().extraDependencies());\n            arguments = ConfigurationUtils.deepMerge(arguments, pd.configuration().arguments());\n        }\n\n        ProcessDefinition root = definitions.get(definitions.size() - 1);\n\n        return ProcessDefinition.builder().from(root)\n                .configuration(ProcessDefinitionConfiguration.builder().from(root.configuration())\n                        .dependencies(dependencies)\n                        .extraDependencies(extraDependencies)\n                        .arguments(arguments)\n                        .build())\n                .flows(flows)\n                .profiles(profiles)\n                .triggers(triggers)\n                .imports(Imports.of(imports))\n                .forms(forms)\n                .resources(Resources.builder().concord(resources).build())\n                .build();\n    }\n\n    private static String concat(Path path, String str) {\n        String separator = \"/\";\n        if (str.startsWith(\"/\")) {\n            separator = \"\";\n        }\n        return path.toAbsolutePath() + separator + str;\n    }\n\n    private static void copyResources(Path baseDir, Resources resources, Path destDir, CopyOption... options) throws IOException {\n        List<Path> files = loadResources(baseDir, resources);\n        for (String fileName : PROJECT_ROOT_FILE_NAMES) {\n            Path p = baseDir.resolve(fileName);\n            files.add(p);\n        }\n        copy(new HashSet<>(files), baseDir, destDir, options);\n    }\n\n    private static void copy(Set<Path> files, Path baseDir, Path dest, CopyOption... options) throws IOException {\n        for (Path f : files) {\n            if (Files.notExists(f)) {\n                continue;\n            }\n\n            Path src = baseDir.relativize(f);\n            Path dst = dest.resolve(src);\n            Path dstParent = dst.getParent();\n            if (dstParent != null && Files.notExists(dstParent)) {\n                Files.createDirectories(dstParent);\n            }\n            Files.copy(f, dst, options);\n        }\n    }\n\n    public static class Result {\n\n        private final List<Snapshot> snapshots;\n        private final ProcessDefinition projectDefinition;\n\n        public Result(List<Snapshot> snapshots, ProcessDefinition projectDefinition) {\n            this.snapshots = snapshots;\n            this.projectDefinition = projectDefinition;\n        }\n\n        public List<Snapshot> getSnapshots() {\n            return snapshots;\n        }\n\n        public ProcessDefinition getProjectDefinition() {\n            return projectDefinition;\n        }\n    }\n\n    private static ProjectLoader.Result toCommonResultType(Result r) {\n        List<Snapshot> snapshots = r.getSnapshots();\n        com.walmartlabs.concord.runtime.model.ProcessDefinition pd = new ProcessDefinitionV2(r.getProjectDefinition());\n\n        return new ProjectLoader.Result() {\n            @Override\n            public List<Snapshot> snapshots() {\n                return snapshots;\n            }\n\n            @Override\n            public com.walmartlabs.concord.runtime.model.ProcessDefinition projectDefinition() {\n                return pd;\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/ProjectSerializerV2.java",
    "content": "package com.walmartlabs.concord.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.module.SimpleModule;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLFactory;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;\nimport com.walmartlabs.concord.runtime.v2.model.*;\nimport com.walmartlabs.concord.runtime.v2.parser.SimpleOptions;\nimport com.walmartlabs.concord.runtime.v2.serializer.*;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.time.Duration;\n\npublic class ProjectSerializerV2 {\n\n    private final ObjectMapper objectMapper;\n\n    public ProjectSerializerV2() {\n        this.objectMapper = createObjectMapper();\n    }\n\n    public void write(ProcessDefinition processDefinition, OutputStream out) throws IOException {\n        this.objectMapper.writeValue(out, processDefinition);\n    }\n\n    public String toString(ProcessDefinition processDefinition) throws Exception {\n        return objectMapper.writeValueAsString(processDefinition);\n    }\n\n    public ObjectMapper getObjectMapper() {\n        return objectMapper;\n    }\n\n    private static ObjectMapper createObjectMapper() {\n        SimpleModule module = new SimpleModule()\n                .addSerializer(SimpleOptions.class, new SimpleOptionsSerializer())\n                .addSerializer(Retry.class, new RetryOptionsSerializer())\n                .addSerializer(Loop.class, new LoopOptionsSerializer())\n                .addSerializer(WithItems.class, new WithItemsSerializer())\n                .addSerializer(Checkpoint.class, new CheckpointStepSerializer())\n                .addSerializer(ExitStep.class, new ExitStepSerializer())\n                .addSerializer(Expression.class, new ExpressionStepSerializer())\n                .addSerializer(FlowCall.class, new FlowCallStepSerializer())\n                .addSerializer(FormCall.class, new FormCallStepSerializer())\n                .addSerializer(FormField.class, new FormFieldSerializer())\n                .addSerializer(GroupOfSteps.class, new GroupOfStepsSerializer())\n                .addSerializer(IfStep.class, new IfStepSerializer())\n                .addSerializer(ParallelBlock.class, new ParallelBlockSerializer())\n                .addSerializer(ReturnStep.class, new ReturnStepSerializer())\n                .addSerializer(ScriptCall.class, new ScriptCallStepSerializer())\n                .addSerializer(SetVariablesStep.class, new SetVariablesStepSerializer())\n                .addSerializer(SuspendStep.class, new SuspendStepSerializer())\n                .addSerializer(SwitchStep.class, new SwitchStepSerializer())\n                .addSerializer(TaskCall.class, new TaskCallStepSerializer())\n                .addSerializer(Form.class, new FormDefinitionSerializer())\n                .addSerializer(Trigger.class, new TriggerSerializer())\n                .addSerializer(ProcessDefinition.class, new ProcessDefinitionSerializer())\n                .addSerializer(Duration.class, new DurationSerializer())\n                .addSerializer(Flow.class, new FlowSerializer());\n\n        return new ObjectMapper(new YAMLFactory()\n                .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER))\n                .registerModule(module);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/exception/InvalidFieldDefinitionException.java",
    "content": "package com.walmartlabs.concord.runtime.v2.exception;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\n\npublic class InvalidFieldDefinitionException extends YamlProcessingException {\n\n    // for backward compatibility (java8 concord 1.92.0 version)\n    private static final long serialVersionUID = 7839061025127853584L;\n\n    private final String fieldName;\n    private final YamlProcessingException cause;\n\n    public InvalidFieldDefinitionException(String fieldName, Location stepLocation, YamlProcessingException cause) {\n        super(stepLocation);\n        this.fieldName = fieldName;\n        this.cause = cause;\n    }\n\n    @Override\n    public String getMessage() {\n        return \"Invalid '\" + fieldName + \"' definition\";\n    }\n\n    @Override\n    public synchronized YamlProcessingException getCause() {\n        return cause;\n    }\n\n    public String getFieldName() {\n        return fieldName;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/exception/InvalidValueException.java",
    "content": "package com.walmartlabs.concord.runtime.v2.exception;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\n\nimport java.io.Serializable;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.StringJoiner;\n\npublic class InvalidValueException extends YamlProcessingException {\n\n    // for backward compatibility (java8 concord 1.92.0 version)\n    private static final long serialVersionUID = -3923028761051516018L;\n\n    private final List<String> expected;\n    private final Serializable actual;\n\n    private InvalidValueException(List<String> expected, Serializable actual, Location location) {\n        super(location);\n        this.expected = expected;\n        this.actual = actual;\n    }\n\n    public List<String> getExpected() {\n        return expected;\n    }\n\n    public Serializable getActual() {\n        return actual;\n    }\n\n    @Override\n    public String getMessage() {\n        return buildMessage();\n    }\n\n    private String buildMessage() {\n        String msg = \"Invalid value: \" + actual;\n\n        StringJoiner expectedOptions = new StringJoiner(\", \", \"[\", \"]\");\n        expected.forEach(expectedOptions::add);\n        msg += \", expected: \" + expectedOptions.toString();\n\n        return msg;\n    }\n\n    public static Builder builder() {\n        return new Builder();\n    }\n\n    public static class Builder {\n        private List<String> expected;\n        private Serializable actual;\n        private Location location;\n\n        public Builder expected(String ... expected) {\n            return expected(Arrays.asList(expected));\n        }\n\n        public Builder expected(List<String> expected) {\n            this.expected = expected;\n            return this;\n        }\n\n        public Builder actual(Serializable actual) {\n            this.actual = actual;\n            return this;\n        }\n\n        public Builder location(Location location) {\n            this.location = location;\n            return this;\n        }\n\n        public InvalidValueException build() {\n            return new InvalidValueException(expected, actual, location);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/exception/InvalidValueTypeException.java",
    "content": "package com.walmartlabs.concord.runtime.v2.exception;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.parser.YamlValueType;\n\nimport java.util.Arrays;\nimport java.util.StringJoiner;\n\n@SuppressWarnings(\"rawtypes\")\npublic class InvalidValueTypeException extends YamlProcessingException {\n\n    // for backward compatibility (java8 concord 1.92.0 version)\n    private static final long serialVersionUID = -1409066590954412190L;\n\n    private final YamlValueType[] expectedType;\n    private final YamlValueType actualType;\n    private final String valueKey;\n    private final String message;\n\n    private InvalidValueTypeException(String valueKey, YamlValueType[] expectedType, YamlValueType actualType, String message, Location location) {\n        super(location);\n        this.expectedType = expectedType;\n        this.actualType = actualType;\n        this.valueKey = valueKey;\n        this.message = message;\n    }\n\n    public YamlValueType[] getExpectedType() {\n        return expectedType;\n    }\n\n    public YamlValueType getActualType() {\n        return actualType;\n    }\n\n    @Override\n    public String getMessage() {\n        return buildMessage();\n    }\n\n    private String buildMessage() {\n        String msg = buildPrefix() + \", expected: \" + typeToString(expectedType) + \", got: \" + actualType;\n        if (actualType == YamlValueType.NULL) {\n            msg += \". Remove attribute or complete the definition\";\n        }\n        if (message != null) {\n            msg += \". Error info: \" + message;\n        }\n        return msg;\n    }\n\n    private String buildPrefix() {\n        if (valueKey != null) {\n            return \"Invalid value type of '\" + valueKey + \"' parameter\";\n        } else {\n            return \"Invalid value type\";\n        }\n    }\n\n    private static String typeToString(YamlValueType[] expectedTypes) {\n        if (expectedTypes.length == 1) {\n            return expectedTypes[0].toString();\n        }\n\n        StringJoiner joiner = new StringJoiner(\", \", \"[\", \"]\");\n        Arrays.stream(expectedTypes).forEach(e -> joiner.add(e.toString()));\n        return joiner.toString();\n    }\n\n    public static Builder builder() {\n        return new Builder();\n    }\n\n    public static class Builder {\n        private String valueKey;\n        private YamlValueType[] expectedType;\n        private YamlValueType actualType;\n        private Location location;\n        private String message;\n\n        public Builder from(InvalidValueTypeException e) {\n            this.expectedType = e.getExpectedType();\n            this.actualType = e.getActualType();\n            this.location = e.getLocation();\n            this.valueKey = e.valueKey;\n            return this;\n        }\n\n        public Builder valueKey(String key) {\n            this.valueKey = key;\n            return this;\n        }\n\n        public Builder expected(YamlValueType... expected) {\n            this.expectedType = expected;\n            return this;\n        }\n\n        public Builder actual(YamlValueType actual) {\n            this.actualType = actual;\n            return this;\n        }\n\n        public Builder message(String message) {\n            this.message = message;\n            return this;\n        }\n\n        public Builder location(Location location) {\n            this.location = location;\n            return this;\n        }\n\n        public InvalidValueTypeException build() {\n            return new InvalidValueTypeException(valueKey, expectedType, actualType, message, location);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/exception/MandatoryFieldNotFoundException.java",
    "content": "package com.walmartlabs.concord.runtime.v2.exception;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Collections;\nimport java.util.List;\n\npublic class MandatoryFieldNotFoundException extends YamlProcessingException {\n\n    // for backward compatibility (java8 concord 1.92.0 version)\n    private static final long serialVersionUID = 2604568258403971784L;\n\n    private final List<String> fields;\n\n    public MandatoryFieldNotFoundException(String field) {\n        this(Collections.singletonList(field));\n    }\n\n    public MandatoryFieldNotFoundException(List<String> fields) {\n        super(null);\n        this.fields = fields;\n    }\n\n    @Override\n    public String getMessage() {\n        if (fields.size() == 1) {\n            return \"Mandatory parameter '\" + fields.get(0) + \"' not found\";\n        } else {\n            return \"Mandatory parameters '\" + String.join(\", \", fields) + \"' not found\";\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/exception/OneOfMandatoryFieldsNotFoundException.java",
    "content": "package com.walmartlabs.concord.runtime.v2.exception;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Arrays;\nimport java.util.List;\n\npublic class OneOfMandatoryFieldsNotFoundException extends YamlProcessingException {\n\n    private static final long serialVersionUID = 1L;\n\n    private final List<String> fields;\n\n    public OneOfMandatoryFieldsNotFoundException(String... fields) {\n        this(Arrays.asList(fields));\n    }\n\n    public OneOfMandatoryFieldsNotFoundException(List<String> fields) {\n        super(null);\n        this.fields = fields;\n    }\n\n    @Override\n    public String getMessage() {\n        return \"One of mandatory parameters '\" + String.join(\", \", fields) + \"' not found\";\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/exception/UnknownOptionException.java",
    "content": "package com.walmartlabs.concord.runtime.v2.exception;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.parser.UnknownOption;\n\nimport java.util.List;\nimport java.util.StringJoiner;\n\npublic class UnknownOptionException extends YamlProcessingException {\n\n    // for backward compatibility (java8 concord 1.92.0 version)\n    private static final long serialVersionUID = 8029483558704928232L;\n\n    private final List<String> expected;\n    private final List<UnknownOption> unknown;\n\n    private UnknownOptionException(List<String> expected, List<UnknownOption> unknown, Location location) {\n        super(location);\n        this.expected = expected;\n        this.unknown = unknown;\n    }\n\n    public List<String> getExpected() {\n        return expected;\n    }\n\n    public List<UnknownOption> getUnknown() {\n        return unknown;\n    }\n\n    @Override\n    public String getMessage() {\n        return buildMessage();\n    }\n\n    private String buildMessage() {\n        StringJoiner unknownOptions = new StringJoiner(\", \", \"[\", \"]\");\n        unknown.forEach(o -> unknownOptions.add(toString(o)));\n\n        String msg = \"Unknown options: \" + unknownOptions;\n        if (expected.isEmpty()) {\n            msg += \", no options expected\";\n        } else {\n            StringJoiner expectedOptions = new StringJoiner(\", \", \"[\", \"]\");\n            expected.forEach(expectedOptions::add);\n            msg += \", expected: \" + expectedOptions;\n        }\n\n        msg += \". Remove invalid options and/or fix indentation\";\n\n        return msg;\n    }\n\n    private String toString(UnknownOption o) {\n        String type = \"\";\n        if (o.type() != null) {\n            type = \" [\" + o.type() + \"]\";\n        }\n        return \"'\" + o.key() + \"'\" + type + \" @ \" + Location.toShortString(o.location());\n    }\n\n    public static Builder builder() {\n        return new Builder();\n    }\n\n    public static class Builder {\n        private List<String> expected;\n        private List<UnknownOption> unknown;\n        private Location location;\n\n        public Builder from(UnknownOptionException e) {\n            this.expected = e.getExpected();\n            this.unknown = e.getUnknown();\n            this.location = e.getLocation();\n            return this;\n        }\n\n        public Builder expected(List<String> expected) {\n            this.expected = expected;\n            return this;\n        }\n\n        public Builder unknown(List<UnknownOption> unknown) {\n            this.unknown = unknown;\n            return this;\n        }\n\n        public Builder location(Location location) {\n            this.location = location;\n            return this;\n        }\n\n        public UnknownOptionException build() {\n            return new UnknownOptionException(expected, unknown, location);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/exception/UnsupportedException.java",
    "content": "package com.walmartlabs.concord.runtime.v2.exception;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class UnsupportedException extends YamlProcessingException {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String message;\n\n    public UnsupportedException(String message) {\n        super(null);\n        this.message = message;\n    }\n\n    @Override\n    public String getMessage() {\n        return message;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/exception/YamlParserException.java",
    "content": "package com.walmartlabs.concord.runtime.v2.exception;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\n\npublic class YamlParserException extends IOException {\n\n    // for backward compatibility (java8 concord 1.92.0 version)\n    private static final long serialVersionUID = -1967047158567778135L;\n\n    public YamlParserException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/exception/YamlProcessingException.java",
    "content": "package com.walmartlabs.concord.runtime.v2.exception;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\n\npublic class YamlProcessingException extends RuntimeException {\n\n    // for backward compatibility (java8 concord 1.92.0 version)\n    private static final long serialVersionUID = 8416854398739494361L;\n\n    private final Location location;\n\n    public YamlProcessingException(Location location, String message) {\n        super(message);\n        this.location = location;\n    }\n\n    protected YamlProcessingException(Location location) {\n        this.location = location;\n    }\n\n    public Location getLocation() {\n        return location;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/AbstractStep.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.parser.StepOptions;\n\nimport javax.annotation.Nullable;\n\npublic abstract class AbstractStep<O extends StepOptions> implements Step {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Location location;\n    private final O options;\n\n    protected AbstractStep(Location location, O options) {\n        this.location = location;\n        this.options = options;\n    }\n\n    @Override\n    public Location getLocation() {\n        return location;\n    }\n\n    @Nullable\n    public O getOptions() {\n        return options;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/Checkpoint.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.parser.SimpleOptions;\n\npublic class Checkpoint extends AbstractStep<SimpleOptions> {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String name;\n\n    public Checkpoint(Location location, String name, SimpleOptions options) {\n        super(location, options);\n\n        this.name = name;\n    }\n\n    public String getName() {\n        return name;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/EventConfiguration.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonIgnoreProperties(ignoreUnknown = true)\n@JsonSerialize(as = ImmutableEventConfiguration.class)\n@JsonDeserialize(as = ImmutableEventConfiguration.class)\npublic interface EventConfiguration extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    /**\n     * Commonly used variable names that can potentially contain sensitive data.\n     */\n    Collection<String> DEFAULT_IN_VARS_BLACKLIST = Arrays.asList(\n            \"apiKey\",\n            \"apiToken\",\n            \"password\",\n            \"privateKey\",\n            \"vaultPassword\");\n\n    /**\n     * Enable/disable recording of process events.\n     */\n    @Value.Default\n    default boolean recordEvents() {\n        return true;\n    }\n\n    /**\n     * Maximum number of process events to report per batch.\n     */\n    @Value.Default\n    default int batchSize() {\n        return 1;\n    }\n\n    /**\n     * Interval, in seconds after which any queued process events will be reported.\n     * <p>\n     * Typically, batched events are reported on process termination or when\n     * the queued number equals {@link #batchSize()}. A long-running task call\n     * holds up event recording if a scheduled flush is not performed.\n     */\n    @Value.Default\n    default int batchFlushInterval() {\n        return 15;\n    }\n\n    /**\n     * Enable/disable recording of IN variables in task calls.\n     */\n    @Value.Default\n    default boolean recordTaskInVars() {\n        return false;\n    }\n\n    /**\n     * Enable/disable truncating of IN variables in task call for events.\n     */\n    @Value.Default\n    default boolean truncateInVars() {\n        return true;\n    }\n\n    /**\n     * Maximum allowed length of string values.\n     * The runtime truncates strings larger than {@link #truncateMaxStringLength()}.\n     *\n     * @return\n     */\n    @Value.Default\n    default int truncateMaxStringLength() {\n        return 1024;\n    }\n\n    /**\n     * Maximum allowed length of array (list) values.\n     * The runtime truncates arrays larger than {@link #truncateMaxArrayLength()}.\n     *\n     * @return\n     */\n    @Value.Default\n    default int truncateMaxArrayLength() {\n        return 32;\n    }\n\n    /**\n     * Maximum allowed depth of nested values.\n     * The runtime truncates references deeper than {@link #truncateMaxDepth()}.\n     *\n     * @return\n     */\n    @Value.Default\n    default int truncateMaxDepth() {\n        return 10;\n    }\n\n    /**\n     * Enable/disable recording of OUT variables in task calls.\n     */\n    @Value.Default\n    default boolean recordTaskOutVars() {\n        return false;\n    }\n\n    /**\n     * Enable/disable truncating of OUT variables in task call for events.\n     */\n    @Value.Default\n    default boolean truncateOutVars() {\n        return true;\n    }\n\n    @Value.Default\n    default boolean updateMetaOnAllEvents() {\n        return true;\n    }\n\n    /**\n     * IN variables in the blacklist won't be recorded.\n     */\n    @Value.Default\n    default Collection<String> inVarsBlacklist() {\n        return DEFAULT_IN_VARS_BLACKLIST;\n    }\n\n    /**\n     * OUT variables in the blacklist won't be recorded.\n     */\n    @Value.Default\n    default Collection<String> outVarsBlacklist() {\n        return Collections.emptyList();\n    }\n\n    /**\n     * Enable/disable recording of metadata in task calls.\n     */\n    @Value.Default\n    default boolean recordTaskMeta() {\n        return false;\n    }\n\n    /**\n     * Enable/disable truncating of metadata in task call for events.\n     */\n    @Value.Default\n    default boolean truncateMeta() {\n        return true;\n    }\n\n    /**\n     * metadata in the blacklist won't be recorded.\n     */\n    @Value.Default\n    default Collection<String> metaBlacklist() {\n        return Collections.emptyList();\n    }\n\n    static ImmutableEventConfiguration.Builder builder() {\n        return ImmutableEventConfiguration.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/ExclusiveMode.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonIgnoreProperties(ignoreUnknown = true)\n@JsonSerialize(as = ImmutableExclusiveMode.class)\n@JsonDeserialize(as = ImmutableExclusiveMode.class)\npublic interface ExclusiveMode extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Value.Parameter\n    @JsonProperty(value = \"group\", required = true)\n    String group();\n\n    @Value.Parameter\n    @Value.Default\n    default Mode mode() {\n        return Mode.cancel;\n    }\n\n    enum Mode {\n        /**\n         * Cancel the current process if there's already a running process with the same {@link #group()} value.\n         */\n        cancel,\n\n        /**\n         * Cancel all other processes (enqueued, waiting, running or suspended) with the same {@link #group()} value.\n         */\n        cancelOld,\n\n        /**\n         * Wait in the queue if there's already a running process with the same {@link #group()} value.\n         */\n        wait\n    }\n\n    static ExclusiveMode of(String group, Mode mode) {\n        return ImmutableExclusiveMode.of(group, mode);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/ExitStep.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\n\npublic class ExitStep implements Step {\n\n    // for backward compatibility (java8 concord 1.92.0 version)\n    private static final long serialVersionUID = -2225644171118983147L;\n\n    private final Location location;\n\n    public ExitStep(Location location) {\n        this.location = location;\n    }\n\n    @Override\n    public Location getLocation() {\n        return location;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/Expression.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\n\npublic class Expression extends AbstractStep<ExpressionOptions> {\n\n    public static Expression shortForm(Location location, String expr) {\n        return new Expression(location, expr, ExpressionOptions.builder().build());\n    }\n\n    private static final long serialVersionUID = 1L;\n\n    private final String expr;\n\n    public Expression(Location location, String expr, ExpressionOptions options) {\n        super(location, options);\n        this.expr = expr;\n    }\n\n    public String getExpr() {\n        return expr;\n    }\n\n    @Override\n    public String toString() {\n        return \"Expression{\" +\n                \"expr='\" + expr + '\\'' +\n                \"}\";\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/ExpressionOptions.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AllowNulls;\nimport com.walmartlabs.concord.runtime.v2.parser.StepOptions;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface ExpressionOptions extends StepOptions {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    String out();\n\n    @Value.Default\n    @AllowNulls\n    default Map<String, Serializable> outExpr() {\n        return Collections.emptyMap();\n    }\n\n    @Value.Default\n    default List<Step> errorSteps() {\n        return Collections.emptyList();\n    }\n\n    static ImmutableExpressionOptions.Builder builder() {\n        return ImmutableExpressionOptions.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/Flow.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport org.immutables.value.Value;\n\nimport java.io.Serial;\nimport java.io.Serializable;\nimport java.util.List;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface Flow extends Serializable {\n\n    @Serial\n    long serialVersionUID = 1L;\n\n    @Value.Parameter\n    Location location();\n\n    @Value.Default\n    @Value.Parameter\n    default List<Step> steps() {\n        return List.of();\n    }\n\n    static Flow of(Location location, List<Step> steps) {\n        return ImmutableFlow.of(location, steps);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/FlowCall.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\n\npublic class FlowCall extends AbstractStep<FlowCallOptions> {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String flowName;\n\n    public FlowCall(Location location, String flowName, FlowCallOptions options) {\n        super(location, options);\n        this.flowName = flowName;\n    }\n\n    public String getFlowName() {\n        return flowName;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/FlowCallOptions.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AllowNulls;\nimport com.walmartlabs.concord.runtime.v2.parser.StepOptions;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface FlowCallOptions extends StepOptions {\n\n    long serialVersionUID = 1L;\n\n    @Value.Default\n    @AllowNulls\n    default Map<String, Serializable> input() {\n        return Collections.emptyMap();\n    }\n\n    @Nullable\n    String inputExpression();\n\n    @Value.Default\n    default List<String> out() {\n        return Collections.emptyList();\n    }\n\n    @Nullable\n    String outExpression();\n\n    @Value.Default\n    @AllowNulls\n    default Map<String, Serializable> outMapping() {\n        return Collections.emptyMap();\n    }\n\n    @Deprecated(forRemoval = true, since = \"2.29.0\")\n    @Value.Default\n    @AllowNulls\n    default Map<String, Serializable> outExpr() {\n        return Collections.emptyMap();\n    }\n\n    @Nullable\n    WithItems withItems();\n\n    @Nullable\n    Loop loop();\n\n    @Nullable\n    Retry retry();\n\n    @Value.Default\n    default List<Step> errorSteps() {\n        return Collections.emptyList();\n    }\n\n    static ImmutableFlowCallOptions.Builder builder() {\n        return ImmutableFlowCallOptions.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/Form.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface Form extends Serializable  {\n\n    long serialVersionUID = 1L;\n\n    String name();\n\n    @Value.Default\n    default List<FormField> fields() {\n        return Collections.emptyList();\n    }\n\n    Location location();\n\n    static ImmutableForm.Builder builder() {\n        return ImmutableForm.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/FormCall.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\n\npublic class FormCall extends AbstractStep<FormCallOptions> {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String name;\n\n    public FormCall(Location location, String name, FormCallOptions options) {\n        super(location, options);\n\n        this.name = name;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    @Override\n    public String toString() {\n        return \"TaskCall{\" +\n                \"name='\" + name + '\\'' +\n                \"}\";\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/FormCallOptions.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AllowNulls;\nimport com.walmartlabs.concord.runtime.v2.parser.StepOptions;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface FormCallOptions extends StepOptions {\n\n    long serialVersionUID = 1L;\n\n    @Value.Default\n    default boolean isYield() {\n        return false;\n    }\n\n    @Value.Default\n    default boolean saveSubmittedBy() {\n        return false;\n    }\n\n    // TODO types\n    @Value.Default\n    @AllowNulls\n    default Map<String, Serializable> runAs() {\n        return Collections.emptyMap();\n    }\n\n    @Value.Default\n    default Map<String, Serializable> values() {\n        return Collections.emptyMap();\n    }\n\n    @Value.Default\n    default List<FormField> fields() {\n        return Collections.emptyList();\n    }\n\n    /**\n     * Support for expressions in {@code fields}.\n     */\n    @Nullable\n    String fieldsExpression();\n\n    /**\n     * Support for expressions in {@code values}.\n     */\n    @Nullable\n    String valuesExpression();\n\n    /**\n     * Support for expressions in {@code runAs}.\n     */\n    @Nullable\n    String runAsExpression();\n\n    static ImmutableFormCallOptions.Builder builder() {\n        return ImmutableFormCallOptions.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/FormField.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.forms.FormField.Cardinality;\nimport com.walmartlabs.concord.runtime.model.Location;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.Map;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface FormField extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    String name();\n\n    @Nullable\n    String label();\n\n    String type();\n\n    Cardinality cardinality();\n\n    @Nullable\n    Serializable defaultValue();\n\n    @Nullable\n    Serializable allowedValue();\n\n    @Value.Default\n    default Map<String, Serializable> options() {\n        return Collections.emptyMap();\n    }\n\n    Location location();\n\n    static ImmutableFormField.Builder builder() {\n        return ImmutableFormField.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/GithubTriggerExclusiveMode.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serial;\nimport java.io.Serializable;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableGithubTriggerExclusiveMode.class)\n@JsonDeserialize(as = ImmutableGithubTriggerExclusiveMode.class)\npublic interface GithubTriggerExclusiveMode extends Serializable {\n\n    @Serial\n    long serialVersionUID = 1L;\n\n    @Nullable\n    @Value.Parameter\n    @JsonProperty(value = \"group\")\n    String group();\n\n    // Unused, just for backward compatibility\n    @Nullable\n    @JsonIgnore\n    GroupBy groupBy();\n    enum GroupBy {\n        branch\n    }\n\n    @Value.Parameter\n    @JsonProperty(value = \"groupBy\")\n    @Nullable\n    String groupByProperty();\n\n    @Value.Parameter\n    @Value.Default\n    default ExclusiveMode.Mode mode() {\n        return ExclusiveMode.Mode.wait;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/GroupOfSteps.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\n\nimport java.util.List;\n\npublic class GroupOfSteps extends AbstractStep<GroupOfStepsOptions> {\n\n    private static final long serialVersionUID = 1L;\n\n    private final List<Step> steps;\n\n    public GroupOfSteps(Location location, List<Step> steps, GroupOfStepsOptions options) {\n        super(location, options);\n\n        this.steps = steps;\n    }\n\n    public List<Step> getSteps() {\n        return steps;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/GroupOfStepsOptions.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.parser.StepOptions;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.util.Collections;\nimport java.util.List;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface GroupOfStepsOptions extends StepOptions {\n\n    long serialVersionUID = 1L;\n\n    @Value.Default\n    default List<String> out() {\n        return Collections.emptyList();\n    }\n\n    @Value.Default\n    default List<Step> errorSteps() {\n        return Collections.emptyList();\n    }\n\n    @Nullable\n    WithItems withItems();\n\n    @Nullable\n    Loop loop();\n\n    static ImmutableGroupOfStepsOptions.Builder builder() {\n        return ImmutableGroupOfStepsOptions.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/IfStep.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.parser.SimpleOptions;\n\nimport java.util.List;\n\npublic class IfStep extends AbstractStep<SimpleOptions> {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String expression;\n    private final List<Step> thenSteps;\n    private final List<Step> elseSteps;\n\n    public IfStep(Location location, String expression, List<Step> thenSteps, List<Step> elseSteps, SimpleOptions options) {\n        super(location, options);\n\n        this.expression = expression;\n        this.thenSteps = thenSteps;\n        this.elseSteps = elseSteps;\n    }\n\n    public String getExpression() {\n        return expression;\n    }\n\n    public List<Step> getThenSteps() {\n        return thenSteps;\n    }\n\n    public List<Step> getElseSteps() {\n        return elseSteps;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/Location.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\n/**\n * For backward-compatibility with runtime-v2 state serialized before 2.27.0.\n */\n@Value.Immutable\npublic interface Location extends com.walmartlabs.concord.runtime.model.Location {\n\n    long serialVersionUID = 1L;\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/Loop.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.Map;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface Loop extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    static ImmutableLoop.Builder builder() {\n        return  ImmutableLoop.builder();\n    }\n\n    Serializable items();\n\n    @Value.Default\n    default Mode mode() {\n        return Mode.SERIAL;\n    }\n\n    @Value.Default\n    default Map<String, Object> options() {\n        return Collections.emptyMap();\n    }\n\n    enum Mode {\n        SERIAL,\n        PARALLEL\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/ParallelBlock.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\n\nimport java.util.List;\n\npublic class ParallelBlock extends AbstractStep<ParallelBlockOptions> {\n\n    private static final long serialVersionUID = 1L;\n\n    private final List<Step> steps;\n\n    public ParallelBlock(Location location, List<Step> steps, ParallelBlockOptions options) {\n        super(location, options);\n        this.steps = steps;\n    }\n\n    public List<Step> getSteps() {\n        return steps;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/ParallelBlockOptions.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AllowNulls;\nimport com.walmartlabs.concord.runtime.v2.parser.StepOptions;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface ParallelBlockOptions extends StepOptions {\n\n    @Value.Default\n    default List<String> out() {\n        return Collections.emptyList();\n    }\n\n    @Value.Default\n    @AllowNulls\n    default Map<String, Serializable> outExpr() {\n        return Collections.emptyMap();\n    }\n\n    static ImmutableParallelBlockOptions.Builder builder() {\n        return ImmutableParallelBlockOptions.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/ProcessDefinition.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Imports;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n@Value.Style(jdkOnly = true)\n@Value.Immutable\npublic interface ProcessDefinition extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Value.Default\n    default ProcessDefinitionConfiguration configuration() {\n        return ProcessDefinitionConfiguration.builder().build();\n    }\n\n    @Value.Default\n    default Map<String, Flow> flows() {\n        return Collections.emptyMap();\n    }\n\n    @Value.Default\n    default Set<String> publicFlows() {\n        return Collections.emptySet();\n    }\n\n    @Value.Default\n    default Map<String, Profile> profiles() {\n        return Collections.emptyMap();\n    }\n\n    @Value.Default\n    default List<Trigger> triggers() {\n        return Collections.emptyList();\n    }\n\n    @Value.Default\n    default Imports imports() {\n        return Imports.builder().build();\n    }\n\n    @Value.Default\n    default Map<String, Form> forms() {\n        return Collections.emptyMap();\n    }\n\n    @Value.Default\n    default Resources resources() {\n        return Resources.builder().build();\n    }\n\n    static ImmutableProcessDefinition.Builder builder() {\n        return ImmutableProcessDefinition.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/ProcessDefinitionConfiguration.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.common.AllowNulls;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.time.Duration;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonIgnoreProperties(ignoreUnknown = true)\n@JsonSerialize(as = ImmutableProcessDefinitionConfiguration.class)\n@JsonDeserialize(as = ImmutableProcessDefinitionConfiguration.class)\npublic interface ProcessDefinitionConfiguration extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    /**\n     * The runtime version to use.\n     */\n    @Value.Default\n    default String runtime() {\n        return \"concord-v2\";\n    }\n\n    /**\n     * Global debug mode.\n     */\n    @Value.Default\n    default boolean debug() {\n        return false;\n    }\n\n    /**\n     * List of active profiles.\n     */\n    @Value.Default\n    default List<String> activeProfiles() {\n        return Collections.emptyList();\n    }\n\n    /**\n     * Name of the flow to start.\n     */\n    @Value.Default\n    default String entryPoint() {\n        return Constants.Request.DEFAULT_ENTRY_POINT_NAME;\n    }\n\n    /**\n     * List of required dependencies.\n     * May be a list of Maven coordinates (mvn://), URLs (https://), or local file paths (file://).\n     */\n    @Value.Default\n    default List<String> dependencies() {\n        return Collections.emptyList();\n    }\n\n    /**\n     * List of extra dependencies. Extra dependencies are appended to the regular list of dependencies.\n     * In profiles, the regular \"dependencies\" must be overridden entirely, \"extraDependencies\" can be used to add additional dependencies.\n     */\n    @Value.Default\n    default List<String> extraDependencies() {\n        return Collections.emptyList();\n    }\n\n    /**\n     * Process arguments. Arbitrary key-value pairs.\n     */\n    @Value.Default\n    @AllowNulls\n    default Map<String, Object> arguments() {\n        return Collections.emptyMap();\n    }\n\n    /**\n     * Process metadata. Arbitrary key-value pairs.\n     */\n    @Value.Default\n    default Map<String, Object> meta() {\n        return Collections.emptyMap();\n    }\n\n    /**\n     * Event configuration. Used to control the behavior of the event recording system.\n     */\n    @Value.Default\n    default EventConfiguration events() {\n        return EventConfiguration.builder().build();\n    }\n\n    /**\n     * Process requirements. Used by the server to find a suitable agent.\n     */\n    @Value.Default\n    default Map<String, Object> requirements() {\n        return Collections.emptyMap();\n    }\n\n    /**\n     * Process timeout. Limits the execution time of a process instance.\n     */\n    @Nullable\n    Duration processTimeout();\n\n    /**\n     * Process suspend timeout. Limits the time a process instance can be suspended.\n     */\n    @Nullable\n    Duration suspendTimeout();\n\n    /**\n     * Exclusive mode configuration.\n     */\n    @Nullable\n    ExclusiveMode exclusive();\n\n    /**\n     * List of output variables.\n     */\n    @Value.Default\n    default List<String> out() {\n        return Collections.emptyList();\n    }\n\n    /**\n     * Process template to use.\n     */\n    @Nullable\n    String template();\n\n    /**\n     * Number of parallel threads to use in loops by default.\n     */\n    @Value.Default\n    default int parallelLoopParallelism() {\n        return Runtime.getRuntime().availableProcessors();\n    }\n\n    /**\n     * Validation configuration.\n     */\n    @Value.Default\n    default ValidationConfiguration validation() {\n        return new ValidationConfiguration();\n    }\n\n    static ImmutableProcessDefinitionConfiguration.Builder builder() {\n        return ImmutableProcessDefinitionConfiguration.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/Profile.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.Set;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableProfile.class)\n@JsonDeserialize(as = ImmutableProfile.class)\npublic interface Profile extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Value.Default\n    default ProcessDefinitionConfiguration configuration() {\n        return ProcessDefinitionConfiguration.builder().build();\n    }\n\n    @Value.Default\n    default Set<String> publicFlows() {\n        return Collections.emptySet();\n    }\n\n    @Value.Default\n    default Map<String, Flow> flows() {\n        return Collections.emptyMap();\n    }\n\n    @Value.Default\n    default Map<String, Form> forms() {\n        return Collections.emptyMap();\n    }\n\n    static ImmutableProfile.Builder builder() {\n        return ImmutableProfile.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/Resources.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonIgnoreProperties(ignoreUnknown = true)\n@JsonSerialize(as = ImmutableResources.class)\n@JsonDeserialize(as = ImmutableResources.class)\npublic interface Resources extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Value.Default\n    default List<String> concord() {\n        return Collections.singletonList(\"glob:concord/{**/,}{*.,}concord.{yml,yaml}\");\n    }\n\n    static ImmutableResources.Builder builder() {\n        return ImmutableResources.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/Retry.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AllowNulls;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.time.Duration;\nimport java.util.Map;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface Retry extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    int DEFAULT_RETRY_TIMES = 1;\n\n    Duration DEFAULT_RETRY_DELAY = Duration.ofSeconds(5);\n\n    @Value.Default\n    default int times() {\n        return DEFAULT_RETRY_TIMES;\n    }\n\n    @Value.Default\n    default Duration delay() {\n        return DEFAULT_RETRY_DELAY;\n    }\n\n    @Nullable\n    String timesExpression();\n\n    @Nullable\n    String delayExpression();\n\n    @Nullable\n    @AllowNulls\n    Map<String, Serializable> input();\n\n    static ImmutableRetry.Builder builder() {\n        return ImmutableRetry.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/ReturnStep.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\n\npublic class ReturnStep implements Step {\n\n    // for backward compatibility (java8 concord 1.92.0 version)\n    private static final long serialVersionUID = 2881891545035010979L;\n\n    private final Location location;\n\n    public ReturnStep(Location location) {\n        this.location = location;\n    }\n\n    @Override\n    public Location getLocation() {\n        return location;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/ScriptCall.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\n\npublic class ScriptCall extends AbstractStep<ScriptCallOptions> {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String languageOrRef;\n\n    public ScriptCall(Location location, String languageOrRef, ScriptCallOptions options) {\n        super(location, options);\n\n        this.languageOrRef = languageOrRef;\n    }\n\n    public String getLanguageOrRef() {\n        return languageOrRef;\n    }\n\n    @Override\n    public String toString() {\n        return \"ScriptCall{\" +\n                \"languageOrRef='\" + languageOrRef + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/ScriptCallOptions.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AllowNulls;\nimport com.walmartlabs.concord.runtime.v2.parser.StepOptions;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface ScriptCallOptions extends StepOptions {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    String body();\n\n    @Value.Default\n    @AllowNulls\n    default Map<String, Serializable> input() {\n        return Collections.emptyMap();\n    }\n\n    @Nullable\n    String out();\n\n    @Value.Default\n    @AllowNulls\n    default Map<String, Serializable> outExpr() {\n        return Collections.emptyMap();\n    }\n\n    @Nullable\n    String inputExpression();\n\n    @Nullable\n    WithItems withItems();\n\n    @Nullable\n    Loop loop();\n\n    @Nullable\n    Retry retry();\n\n    @Value.Default\n    default List<Step> errorSteps() {\n        return Collections.emptyList();\n    }\n\n    static ImmutableScriptCallOptions.Builder builder() {\n        return ImmutableScriptCallOptions.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/SetVariablesStep.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.parser.SimpleOptions;\n\nimport java.io.Serializable;\nimport java.util.Map;\n\npublic class SetVariablesStep extends AbstractStep<SimpleOptions> {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Map<String, Serializable> vars;\n\n    public SetVariablesStep(Location location, Map<String, Serializable> variables, SimpleOptions options) {\n        super(location, options);\n        this.vars = variables;\n    }\n\n    public Map<String, Serializable> getVars() {\n        return vars;\n    }\n\n    @Override\n    public String toString() {\n        return \"SetVariablesStep{\" +\n                \"vars=\" + vars +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/SourceMap.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableSourceMap.class)\n@JsonDeserialize(as = ImmutableSourceMap.class)\npublic interface SourceMap extends Serializable {\n\n    String source();\n\n    int line();\n\n    int column();\n\n    String description();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/Step.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\n\nimport java.io.Serializable;\n\npublic interface Step extends Serializable {\n\n    Location getLocation();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/SuspendStep.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.parser.SimpleOptions;\n\npublic class SuspendStep extends AbstractStep<SimpleOptions> {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String event;\n\n    public SuspendStep(Location location, String event, SimpleOptions options) {\n        super(location, options);\n\n        this.event = event;\n    }\n\n    public String getEvent() {\n        return event;\n    }\n\n    @Override\n    public String toString() {\n        return \"SuspendStep{\" +\n                \"event='\" + event + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/SwitchStep.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.parser.SimpleOptions;\n\nimport java.util.List;\nimport java.util.Map;\n\npublic class SwitchStep extends AbstractStep<SimpleOptions> {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String expression;\n    private final List<Map.Entry<String, List<Step>>> caseSteps;\n    private final List<Step> defaultSteps;\n\n    public SwitchStep(Location location, String expression, List<Map.Entry<String, List<Step>>> caseSteps, List<Step> defaultSteps, SimpleOptions options) {\n        super(location, options);\n\n        this.expression = expression;\n        this.caseSteps = caseSteps;\n        this.defaultSteps = defaultSteps;\n    }\n\n    public String getExpression() {\n        return expression;\n    }\n\n    public List<Map.Entry<String, List<Step>>> getCaseSteps() {\n        return caseSteps;\n    }\n\n    public List<Step> getDefaultSteps() {\n        return defaultSteps;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/TaskCall.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\n\npublic class TaskCall extends AbstractStep<TaskCallOptions> {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String name;\n\n    public TaskCall(Location location, String name, TaskCallOptions options) {\n        super(location, options);\n\n        this.name = name;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    @Override\n    public String toString() {\n        return \"TaskCall{\" +\n                \"name='\" + name + '\\'' +\n                \"}\";\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/TaskCallOptions.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AllowNulls;\nimport com.walmartlabs.concord.runtime.v2.parser.StepOptions;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface TaskCallOptions extends StepOptions {\n\n    long serialVersionUID = 1L;\n\n    @Value.Default\n    @AllowNulls\n    default Map<String, Serializable> input() {\n        return Collections.emptyMap();\n    }\n\n    @Nullable\n    String inputExpression();\n\n    @Nullable\n    String out();\n\n    @Value.Default\n    @AllowNulls\n    default Map<String, Serializable> outExpr() {\n        return Collections.emptyMap();\n    }\n\n    @Nullable\n    WithItems withItems();\n\n    @Nullable\n    Loop loop();\n\n    @Nullable\n    Retry retry();\n\n    @Value.Default\n    default List<Step> errorSteps() {\n        return Collections.emptyList();\n    }\n\n    @Value.Default\n    default boolean ignoreErrors() {\n        return false;\n    }\n\n    static ImmutableTaskCallOptions.Builder builder() {\n        return ImmutableTaskCallOptions.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/TaskCallValidation.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\n\nimport java.io.Serial;\nimport java.io.Serializable;\n\n/**\n * Configuration for task call validation.\n *\n * @param in  validation mode for task input parameters\n * @param out validation mode for task output parameters\n */\n@JsonIgnoreProperties(ignoreUnknown = true)\npublic record TaskCallValidation(\n        ValidationMode in,\n        ValidationMode out\n) implements Serializable {\n\n    @Serial\n    private static final long serialVersionUID = 1L;\n\n    public enum ValidationMode {\n        /**\n         * No validation (default)\n         */\n        DISABLED,\n        /**\n         * Log warnings but continue\n         */\n        WARN,\n        /**\n         * Fail on validation errors\n         */\n        FAIL\n    }\n\n    public TaskCallValidation {\n        if (in == null) in = ValidationMode.DISABLED;\n        if (out == null) out = ValidationMode.DISABLED;\n    }\n\n    public TaskCallValidation() {\n        this(ValidationMode.DISABLED, ValidationMode.DISABLED);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/Trigger.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface Trigger extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    String name();\n\n    @Value.Default\n    default Map<String, Object> arguments() {\n        return Collections.emptyMap();\n    }\n\n    @Value.Default\n    default Map<String, Object> conditions() {\n        return Collections.emptyMap();\n    }\n\n    @Value.Default\n    default Map<String, Object> configuration() {\n        return Collections.emptyMap();\n    }\n\n    @Value.Default\n    default List<String> activeProfiles() {\n        return Collections.emptyList();\n    }\n\n    @Value.Default\n    default Location location() {\n        return Location.builder().build();\n    }\n\n    static ImmutableTrigger.Builder builder() {\n        return ImmutableTrigger.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/ValidationConfiguration.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\n\nimport java.io.Serial;\nimport java.io.Serializable;\n\n/**\n * Configuration for process validation settings.\n *\n * @param taskCalls validation settings for task calls\n */\n@JsonIgnoreProperties(ignoreUnknown = true)\npublic record ValidationConfiguration(TaskCallValidation taskCalls) implements Serializable {\n\n    @Serial\n    private static final long serialVersionUID = 1L;\n\n    public ValidationConfiguration {\n        if (taskCalls == null) {\n            taskCalls = new TaskCallValidation();\n        }\n    }\n\n    public ValidationConfiguration() {\n        this(new TaskCallValidation());\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/model/WithItems.java",
    "content": "package com.walmartlabs.concord.runtime.v2.model;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\n\n/**\n * @deprecated use {@link com.walmartlabs.concord.runtime.v2.model.Loop}\n */\n@Deprecated\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface WithItems extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    static WithItems of(Serializable items, Mode mode) {\n        return ImmutableWithItems.of(items, mode);\n    }\n\n    @Value.Parameter\n    Serializable value();\n\n    @Value.Parameter\n    Mode mode();\n\n    enum Mode {\n        SERIAL,\n        PARALLEL\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/Atom.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.walmartlabs.concord.runtime.model.Location;\n\nimport java.io.IOException;\nimport java.io.Serializable;\n\npublic class Atom implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    public static Atom current(JsonParser p) throws IOException {\n        if (p.currentToken() == null) {\n            return null;\n        }\n\n        Object value;\n        switch (p.currentToken()) {\n            case VALUE_STRING:\n                value = p.getValueAsString();\n                break;\n            case VALUE_NUMBER_INT:\n                value = p.getValueAsInt();\n                break;\n            case VALUE_NUMBER_FLOAT:\n                value = p.getValueAsDouble();\n                break;\n            case VALUE_TRUE:\n                value = true;\n                break;\n            case VALUE_FALSE:\n                value = false;\n                break;\n            default:\n                value = null;\n        }\n        return new Atom(toLocation(p.currentTokenLocation()), p.currentToken(), p.getCurrentName(), value);\n    }\n\n    public final Location location;\n    public final JsonToken token;\n    public final String name;\n    public final Object value;\n\n    public Atom(Location location, JsonToken token, String name, Object value) {\n        this.location = location;\n        this.token = token;\n        this.name = name;\n        this.value = value;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        Atom atom = (Atom) o;\n\n        if (!location.equals(atom.location)) return false;\n        if (token != atom.token) return false;\n        if (name != null ? !name.equals(atom.name) : atom.name != null) return false;\n        return value != null ? value.equals(atom.value) : atom.value == null;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = location.hashCode();\n        result = 31 * result + token.hashCode();\n        result = 31 * result + (name != null ? name.hashCode() : 0);\n        result = 31 * result + (value != null ? value.hashCode() : 0);\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"Atom{\" +\n                \"location=\" + location +\n                \", token=\" + token +\n                \", name='\" + name + '\\'' +\n                \", value=\" + value +\n                '}';\n    }\n\n    private static Location toLocation(JsonLocation tokenLocation) {\n        return Location.builder()\n                .lineNum(tokenLocation.getLineNr())\n                .column(tokenLocation.getColumnNr())\n                .fileName(ThreadLocalFileName.get())\n                .build();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/CheckpointGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.Checkpoint;\nimport io.takari.parc.Parser;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.satisfyField;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.simpleOptions;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.stringVal;\n\npublic final class CheckpointGrammar {\n\n    public static final Parser<Atom, Checkpoint> checkpoint =\n            satisfyField(\"checkpoint\", YamlValueType.CHECKPOINT, a -> stringVal.bind(name ->\n                    simpleOptions.map(options -> new Checkpoint(a.location, name, options))));\n\n    private CheckpointGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/ConditionalExpressionsGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.exception.YamlProcessingException;\nimport com.walmartlabs.concord.runtime.v2.model.IfStep;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.model.SwitchStep;\nimport io.takari.parc.Parser;\n\nimport java.util.AbstractMap;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.ExpressionGrammar.expressionVal;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.satisfyField;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.with;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.mapVal;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.stepsVal;\n\npublic final class ConditionalExpressionsGrammar {\n\n    public static final Parser<Atom, SwitchStep> switchExpr =\n            satisfyField(\"switch\", YamlValueType.SWITCH, a -> expressionVal.bind(expr ->\n                    with(SwitchStepBuilder::builder,\n                            o -> options(\n                                    optional(\"default\", stepsVal.map(o::defaultSteps)),\n                                    any((atom, s) -> stepsVal.map(steps -> o.caseStep(s, steps)))))\n                            .map(o -> o.build(a.location, expr))\n            ));\n\n    public static final Parser<Atom, IfStep> ifExpr =\n            satisfyField(\"if\", YamlValueType.IF, a -> expressionVal.bind(expr ->\n                    with(IfStepBuilder::builder,\n                        o -> options(\n                                mandatory(\"then\", stepsVal.map(o::thenSteps)),\n                                optional(\"else\", stepsVal.map(o::elseSteps)),\n                                optional(\"meta\", mapVal.map(v -> o.options(SimpleOptions.of(v))))))\n                        .map(o -> o.build(a.location, expr))\n                    ));\n\n    static class IfStepBuilder {\n\n        private List<Step> thenSteps;\n        private List<Step> elseSteps;\n        private SimpleOptions options;\n\n        public IfStepBuilder thenSteps(List<Step> thenSteps) {\n            this.thenSteps = thenSteps;\n            return this;\n        }\n\n        public IfStepBuilder elseSteps(List<Step> elseSteps) {\n            this.elseSteps = elseSteps;\n            return this;\n        }\n\n        public IfStepBuilder options(SimpleOptions options) {\n            this.options = options;\n            return this;\n        }\n\n        public IfStep build(Location location, String expression) {\n            return new IfStep(location, expression, thenSteps, elseSteps, options);\n        }\n\n        static IfStepBuilder builder() {\n            return new IfStepBuilder();\n        }\n    }\n\n    static class SwitchStepBuilder {\n\n        private final List<Map.Entry<String, List<Step>>> caseSteps = new ArrayList<>();\n        private List<Step> defaultSteps;\n        private SimpleOptions options;\n\n        public SwitchStepBuilder caseStep(String caseLabel, List<Step> caseSteps) {\n            this.caseSteps.add(new AbstractMap.SimpleEntry<>(caseLabel, caseSteps));\n            return this;\n        }\n\n        public SwitchStepBuilder defaultSteps(List<Step> defaultSteps) {\n            this.defaultSteps = defaultSteps;\n            return this;\n        }\n\n        public SwitchStepBuilder options(SimpleOptions options) {\n            this.options = options;\n            return this;\n        }\n\n        public SwitchStep build(Location location, String expression) {\n            if (defaultSteps == null && caseSteps.isEmpty()) {\n                throw new YamlProcessingException(location, \"No branch labels defined\");\n            }\n            return new SwitchStep(location, expression, caseSteps, defaultSteps, options);\n        }\n\n        static SwitchStepBuilder builder() {\n            return new SwitchStepBuilder();\n        }\n    }\n\n    private ConditionalExpressionsGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/ConfigurationGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.walmartlabs.concord.runtime.v2.model.*;\nimport com.walmartlabs.concord.runtime.v2.model.TaskCallValidation.ValidationMode;\nimport io.takari.parc.Parser;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.*;\n\n/**\n * Grammar for the {@code configuration} section of Concord YAML files.\n */\npublic final class ConfigurationGrammar {\n\n    private static final Parser<Atom, ExclusiveMode> exclusive =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    with(ImmutableExclusiveMode::builder,\n                            o -> options(\n                                    mandatory(\"group\", stringNotEmptyVal.map(o::group)),\n                                    optional(\"mode\", enumVal(ExclusiveMode.Mode.class).map(o::mode))))\n                            .map(ImmutableExclusiveMode.Builder::build));\n\n    public static final Parser<Atom, ExclusiveMode> exclusiveVal =\n            orError(exclusive, YamlValueType.EXCLUSIVE_MODE);\n\n    private static final Parser<Atom, EventConfiguration> events =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    with(ImmutableEventConfiguration::builder,\n                            o -> options(\n                                    optional(\"batchFlushInterval\", intVal.map(o::batchFlushInterval)),\n                                    optional(\"batchSize\", intVal.map(o::batchSize)),\n                                    optional(\"recordEvents\", booleanVal.map(o::recordEvents)),\n                                    optional(\"recordTaskInVars\", booleanVal.map(o::recordTaskInVars)),\n                                    optional(\"truncateInVars\", booleanVal.map(o::truncateInVars)),\n                                    optional(\"truncateMaxStringLength\", intVal.map(o::truncateMaxStringLength)),\n                                    optional(\"truncateMaxArrayLength\", intVal.map(o::truncateMaxArrayLength)),\n                                    optional(\"truncateMaxDepth\", intVal.map(o::truncateMaxDepth)),\n                                    optional(\"recordTaskOutVars\", booleanVal.map(o::recordTaskOutVars)),\n                                    optional(\"truncateOutVars\", booleanVal.map(o::truncateOutVars)),\n                                    optional(\"updateMetaOnAllEvents\", booleanVal.map(o::updateMetaOnAllEvents)),\n                                    optional(\"inVarsBlacklist\", stringArrayVal.map(o::inVarsBlacklist)),\n                                    optional(\"outVarsBlacklist\", stringArrayVal.map(o::outVarsBlacklist)),\n                                    optional(\"recordTaskMeta\", booleanVal.map(o::recordTaskMeta)),\n                                    optional(\"truncateMeta\", booleanVal.map(o::truncateMeta)),\n                                    optional(\"metaBlacklist\", stringArrayVal.map(o::metaBlacklist))))\n                            .map(ImmutableEventConfiguration.Builder::build));\n\n    private static final Parser<Atom, EventConfiguration> eventsVal =\n            orError(events, YamlValueType.EVENTS_CFG);\n\n    private static final Parser<Atom, ValidationMode> validationModeVal =\n            orError(enumVal(ValidationMode.class, String::equalsIgnoreCase), YamlValueType.VALIDATION_MODE);\n\n    private static final class TaskCallValidationHolder {\n        ValidationMode in = ValidationMode.DISABLED;\n        ValidationMode out = ValidationMode.DISABLED;\n\n        TaskCallValidationHolder in(ValidationMode v) {\n            this.in = v;\n            return this;\n        }\n\n        TaskCallValidationHolder out(ValidationMode v) {\n            this.out = v;\n            return this;\n        }\n\n        TaskCallValidation build() {\n            return new TaskCallValidation(in, out);\n        }\n    }\n\n    private static final Parser<Atom, TaskCallValidation> taskCallValidation =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    with(TaskCallValidationHolder::new,\n                            o -> options(\n                                    optional(\"in\", validationModeVal.map(o::in)),\n                                    optional(\"out\", validationModeVal.map(o::out))))\n                            .map(TaskCallValidationHolder::build));\n\n    private static final Parser<Atom, TaskCallValidation> taskCallValidationVal =\n            orError(taskCallValidation, YamlValueType.TASK_CALL_VALIDATION);\n\n    private static final class ValidationConfigurationHolder {\n        TaskCallValidation taskCalls = new TaskCallValidation();\n\n        ValidationConfigurationHolder taskCalls(TaskCallValidation v) {\n            this.taskCalls = v;\n            return this;\n        }\n\n        ValidationConfiguration build() {\n            return new ValidationConfiguration(taskCalls);\n        }\n    }\n\n    private static final Parser<Atom, ValidationConfiguration> validation =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    with(ValidationConfigurationHolder::new,\n                            o -> options(\n                                    optional(\"taskCalls\", taskCallValidationVal.map(o::taskCalls))))\n                            .map(ValidationConfigurationHolder::build));\n\n    private static final Parser<Atom, ValidationConfiguration> validationVal =\n            orError(validation, YamlValueType.VALIDATION_CFG);\n\n    private static final Parser<Atom, ProcessDefinitionConfiguration> cfg =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    with(ImmutableProcessDefinitionConfiguration::builder,\n                            o -> options(\n                                    optional(\"runtime\", stringVal.map(o::runtime)),\n                                    optional(\"entryPoint\", stringVal.map(o::entryPoint)),\n                                    optional(\"dependencies\", stringArrayVal.map(o::dependencies)),\n                                    optional(\"extraDependencies\", stringArrayVal.map(o::extraDependencies)),\n                                    optional(\"meta\", mapVal.map(o::meta)),\n                                    optional(\"requirements\", mapVal.map(o::requirements)),\n                                    optional(\"processTimeout\", durationVal.map(o::processTimeout)),\n                                    optional(\"suspendTimeout\", durationVal.map(o::suspendTimeout)),\n                                    optional(\"activeProfiles\", stringArrayVal.map(o::activeProfiles)),\n                                    optional(\"exclusive\", exclusiveVal.map(o::exclusive)),\n                                    optional(\"events\", eventsVal.map(o::events)),\n                                    optional(\"out\", stringArrayVal.map(o::addAllOut)),\n                                    optional(\"arguments\", mapVal.map(o::arguments)),\n                                    optional(\"debug\", booleanVal.map(o::debug)),\n                                    optional(\"template\", stringVal.map(o::template)),\n                                    optional(\"parallelLoopParallelism\", intVal.map(o::parallelLoopParallelism)),\n                                    optional(\"validation\", validationVal.map(o::validation))))\n                            .map(ImmutableProcessDefinitionConfiguration.Builder::build));\n\n    public static final Parser<Atom, ProcessDefinitionConfiguration> processCfgVal =\n            orError(cfg, YamlValueType.PROCESS_CFG);\n\n    private ConfigurationGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/ExitGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.walmartlabs.concord.runtime.v2.model.ExitStep;\nimport io.takari.parc.Parser;\n\nimport static io.takari.parc.Combinators.satisfy;\n\npublic final class ExitGrammar {\n\n    public static final Parser<Atom, ExitStep> exit =\n            satisfy((Atom a) -> a.token == JsonToken.VALUE_STRING && \"exit\".equals(a.value))\n                    .map(a -> new ExitStep(a.location));\n\n    private ExitGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/ExpressionGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.walmartlabs.concord.runtime.v2.Constants;\nimport com.walmartlabs.concord.runtime.v2.model.Expression;\nimport com.walmartlabs.concord.runtime.v2.model.ExpressionOptions;\nimport com.walmartlabs.concord.runtime.v2.model.ImmutableExpressionOptions;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport io.takari.parc.Parser;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.optional;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.options;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.*;\nimport static io.takari.parc.Combinators.or;\nimport static io.takari.parc.Combinators.satisfy;\n\npublic final class ExpressionGrammar {\n\n    // expression := VALUE_STRING ${.*}\n    private static final Parser<Atom, Atom> expressionParser = satisfy((Atom a) -> {\n        if (a.token != JsonToken.VALUE_STRING) {\n            return false;\n        }\n\n        String s = (String) a.value;\n        return s != null && s.startsWith(\"${\") && s.endsWith(\"}\");\n    });\n\n    public static final Parser<Atom, String> maybeExpression = expressionParser.map(expr -> (String)expr.value);\n    public static final Parser<Atom, String> expressionVal = orError(expressionParser.map(expr -> (String)expr.value), YamlValueType.EXPRESSION_VAL);\n\n    private static Parser<Atom, ImmutableExpressionOptions.Builder> exprCallOutOption(ImmutableExpressionOptions.Builder o) {\n        return orError(or(maybeMap.map(o::outExpr), maybeString.map(o::out)), YamlValueType.EXPRESSION_CALL_OUT);\n    }\n\n    private static Parser<Atom, ExpressionOptions> expressionOptions(String stepName) {\n        return with(() -> optionsBuilder(stepName),\n                o -> options(\n                        optional(\"error\", stepsVal.map(o::errorSteps)),\n                        optional(\"out\", exprCallOutOption(o)),\n                        optional(\"meta\", mapVal.map(o::putAllMeta)),\n                        optional(\"name\", stringVal.map(v -> o.putMeta(Constants.SEGMENT_NAME, v)))\n                ))\n                .map(ImmutableExpressionOptions.Builder::build);\n    }\n\n    private static ImmutableExpressionOptions.Builder optionsBuilder(String stepName) {\n        ImmutableExpressionOptions.Builder result = ImmutableExpressionOptions.builder();\n        if (stepName != null) {\n            result.putMeta(Constants.SEGMENT_NAME, stepName);\n        }\n        return result;\n    }\n\n    public static final Parser<Atom, Expression> exprFull =\n            namedStep(\"expr\", YamlValueType.EXPRESSION, (stepName, a) ->\n                    maybeExpression.bind(expr ->\n                            expressionOptions(stepName).map(options -> new Expression(a.location, expr, options))));\n\n    // exprShort := expression\n    public static final Parser<Atom, Step> exprShort =\n            expressionParser.map(a -> Expression.shortForm(a.location, (String) a.value));\n\n    private ExpressionGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/FlowCallGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.Constants;\nimport com.walmartlabs.concord.runtime.v2.model.FlowCall;\nimport com.walmartlabs.concord.runtime.v2.model.FlowCallOptions;\nimport com.walmartlabs.concord.runtime.v2.model.ImmutableFlowCallOptions;\nimport com.walmartlabs.concord.runtime.v2.model.WithItems;\nimport io.takari.parc.Parser;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.ExpressionGrammar.maybeExpression;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.optional;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.options;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.LoopGrammar.loopVal;\nimport static com.walmartlabs.concord.runtime.v2.parser.RetryGrammar.retryVal;\nimport static io.takari.parc.Combinators.or;\n\npublic final class FlowCallGrammar {\n\n    private static Parser<Atom, ImmutableFlowCallOptions.Builder> flowCallInOption(ImmutableFlowCallOptions.Builder o) {\n        return orError(or(maybeMap.map(o::input), maybeExpression.map(o::inputExpression)), YamlValueType.FLOW_CALL_INPUT);\n    }\n\n    private static Parser<Atom, ImmutableFlowCallOptions.Builder> flowCallOutOption(ImmutableFlowCallOptions.Builder o) {\n        return orError(or(maybeMap.map(o::outMapping), or(maybeExpression.map(o::outExpression), or(maybeString.map(o::addOut), maybeStringArray.map(o::out)))), YamlValueType.FLOW_CALL_OUT);\n    }\n\n    private static Parser<Atom, FlowCallOptions> callOptions(String stepName) {\n        return with(() -> optionsBuilder(stepName),\n                o -> options(\n                        optional(\"in\", flowCallInOption(o)),\n                        optional(\"out\", flowCallOutOption(o)),\n                        optional(\"meta\", mapVal.map(o::putAllMeta)),\n                        optional(\"name\", stringVal.map(v -> o.putMeta(Constants.SEGMENT_NAME, v))),\n                        optional(\"withItems\", nonNullVal.map(v -> o.withItems(WithItems.of(v, WithItems.Mode.SERIAL)))),\n                        optional(\"parallelWithItems\", nonNullVal.map(v -> o.withItems(WithItems.of(v, WithItems.Mode.PARALLEL)))),\n                        optional(\"loop\", loopVal.map(o::loop)),\n                        optional(\"retry\", retryVal.map(o::retry)),\n                        optional(\"error\", stepsVal.map(o::errorSteps))\n                ))\n                .map(ImmutableFlowCallOptions.Builder::build);\n    }\n\n    private static ImmutableFlowCallOptions.Builder optionsBuilder(String stepName) {\n        ImmutableFlowCallOptions.Builder result = ImmutableFlowCallOptions.builder();\n        if (stepName != null) {\n            result.putMeta(Constants.SEGMENT_NAME, stepName);\n        }\n        return result;\n    }\n\n    public static final Parser<Atom, FlowCall> callFull =\n            namedStep(\"call\", YamlValueType.FLOW_CALL, (stepName, a) ->\n                    stringVal.bind(flowName ->\n                            callOptions(stepName).map(options -> new FlowCall(a.location, flowName, options))));\n\n    private FlowCallGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/FlowsGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.walmartlabs.concord.runtime.v2.model.Flow;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport io.takari.parc.Parser;\nimport io.takari.parc.Seq;\n\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.*;\nimport static io.takari.parc.Combinators.many;\nimport static io.takari.parc.Combinators.many1;\n\npublic final class FlowsGrammar {\n\n    private static final Parser<Atom, List<Step>> steps =\n            betweenTokens(JsonToken.START_ARRAY, JsonToken.END_ARRAY,\n                    many1(GrammarV2.getProcessStep()).map(Seq::toList));\n\n    private static final Parser<Atom, KV<String, Flow>> flow =\n            satisfyAnyField(YamlValueType.FLOW, f -> steps.map(s -> new KV<>(f.name, Flow.of(f.location, s))));\n\n    private static Map<String, Flow> toMap(Seq<KV<String, Flow>> values) {\n        Map<String, Flow> m = new LinkedHashMap<>();\n        values.stream().forEach(kv -> m.put(kv.getKey(), kv.getValue()));\n        return m;\n    }\n\n    private static final Parser<Atom, Map<String, Flow>> flows =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    many(flow).map(FlowsGrammar::toMap));\n\n    public static final Parser<Atom, Map<String, Flow>> flowsVal =\n            orError(flows, YamlValueType.FLOWS);\n\n    private FlowsGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/FormFieldParser.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.forms.FormField.Cardinality;\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.exception.*;\nimport com.walmartlabs.concord.runtime.v2.model.FormField;\n\nimport java.io.Serializable;\nimport java.util.*;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport static com.walmartlabs.concord.forms.FormField.Option;\nimport static com.walmartlabs.concord.forms.FormFields.*;\n\npublic final class FormFieldParser {\n\n    private static final String[] COMMON_ATTRS = new String[] {\"label\", \"value\", \"allow\", \"type\"};\n\n    public static List<FormField> parse(Location location, List<Map<String, Map<String, Object>>> rawFields) {\n        List<FormField> fields = new ArrayList<>();\n        for (Map<String, Map<String, Object>> rf : rawFields) {\n            if (rf.size() != 1) {\n                throw new RuntimeException(\"Invalid dynamic form fields definition @ \" + Location.toShortString(location) + \". Expected \" + YamlValueType.ARRAY_OF_FORM_FIELD);\n            }\n            Map.Entry<String, Map<String, Object>> entry = rf.entrySet().iterator().next();\n\n            fields.add(parse(entry.getKey(), location, YamlObjectConverter.from(entry.getValue(), location)));\n        }\n        return fields;\n    }\n\n    public static FormField parse(String name, Location location, YamlObject options) {\n        try {\n            return parseField(name, location, options);\n        } catch (YamlProcessingException e) {\n            throw new InvalidFieldDefinitionException(name, location, e);\n        }\n    }\n\n    private static FormField parseField(String name, Location location, YamlObject optionsObject) {\n        Map<String, YamlValue> values = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);\n        values.putAll(optionsObject.getValues());\n        YamlObject options = new YamlObject(values, optionsObject.getLocation());\n\n        String label = options.getValue(\"label\", YamlValueType.STRING);\n        options.remove(\"label\");\n\n        Serializable defaultValue = options.remove(\"value\");\n        Serializable allowedValue = options.remove(\"allow\");\n\n        YamlValue typeValue = options.getYamlValue(\"type\");\n        if (typeValue == null) {\n            throw new MandatoryFieldNotFoundException(\"type\");\n        }\n\n        String type = typeValue.getValue(YamlValueType.STRING);\n        options.remove(\"type\");\n\n        TypeInfo typeInfo = TypeInfo.parse(type);\n        Map<String, Serializable> opts = convertOptions(typeInfo.type, typeValue.getLocation(), options);\n\n        return FormField.builder()\n                .name(name)\n                .label(label)\n                .type(typeInfo.type)\n                .cardinality(typeInfo.cardinality)\n                .defaultValue(defaultValue)\n                .allowedValue(allowedValue)\n                .location (location)\n                .options(opts)\n                .build();\n    }\n\n    private static Map<String, Serializable> convertOptions(String type, Location typeLocation, YamlObject options) {\n        Map<String, Serializable> result = new LinkedHashMap<>();\n        switch (type) {\n            case StringField.TYPE: {\n                processOption(StringField.PATTERN, options, YamlValueType.STRING, result);\n                processOption(StringField.INPUT_TYPE, options, YamlValueType.STRING, result);\n                processOption(CommonFieldOptions.READ_ONLY, options, YamlValueType.BOOLEAN, result);\n                processOption(StringField.PLACEHOLDER, options, YamlValueType.STRING, result);\n                processOption(StringField.SEARCH, options, YamlValueType.BOOLEAN, result);\n\n                assertNoMoreOptions(options, StringField.PATTERN, StringField.INPUT_TYPE, CommonFieldOptions.READ_ONLY, StringField.PLACEHOLDER, StringField.SEARCH);\n\n                break;\n            }\n            case IntegerField.TYPE: {\n                processOption(IntegerField.MIN.name(), options, result, FormFieldParser::coerceToLong);\n                processOption(IntegerField.MAX.name(), options, result, FormFieldParser::coerceToLong);\n                processOption(CommonFieldOptions.READ_ONLY, options, YamlValueType.BOOLEAN, result);\n                processOption(IntegerField.PLACEHOLDER, options, YamlValueType.STRING, result);\n\n                assertNoMoreOptions(options, IntegerField.MIN, IntegerField.MAX, CommonFieldOptions.READ_ONLY, IntegerField.PLACEHOLDER);\n\n                break;\n            }\n            case DecimalField.TYPE: {\n                processOption(DecimalField.MIN.name(), options, result, FormFieldParser::coerceToDouble);\n                processOption(DecimalField.MAX.name(), options, result, FormFieldParser::coerceToDouble);\n                processOption(CommonFieldOptions.READ_ONLY, options, YamlValueType.BOOLEAN, result);\n                processOption(DecimalField.PLACEHOLDER, options, YamlValueType.STRING, result);\n\n                assertNoMoreOptions(options, DecimalField.MIN, DecimalField.MAX, CommonFieldOptions.READ_ONLY, DecimalField.PLACEHOLDER);\n\n                break;\n            }\n            case BooleanField.TYPE:\n                processOption(CommonFieldOptions.READ_ONLY, options, YamlValueType.BOOLEAN, result);\n\n                assertNoMoreOptions(options, CommonFieldOptions.READ_ONLY);\n\n                break;\n            case FileField.TYPE: {\n                assertNoMoreOptions(options);\n                break;\n            }\n            case DateField.TYPE:\n            case DateTimeField.TYPE: {\n                processOption(DateFieldOptions.POPUP_POSITION, options, YamlValueType.STRING, result);\n\n                assertNoMoreOptions(options, CommonFieldOptions.READ_ONLY, DateFieldOptions.POPUP_POSITION);\n\n                break;\n            }\n            default:\n                throw InvalidValueException.builder()\n                        .location(typeLocation)\n                        .actual(type)\n                        .expected(StringField.TYPE, IntegerField.TYPE, DecimalField.TYPE,\n                                BooleanField.TYPE, FileField.TYPE, DateField.TYPE, DateTimeField.TYPE)\n                        .build();\n\n        }\n\n        return result;\n    }\n\n    private static void assertNoMoreOptions(YamlObject options, Option<?> ... expected) {\n        if (options.isEmpty()) {\n            return;\n        }\n\n        throw UnknownOptionException.builder()\n                .location(options.getLocation())\n                .unknown(options.values.entrySet().stream()\n                        .map(kv -> UnknownOption.of(kv.getKey(), kv.getValue().getType(), kv.getValue().getLocation()))\n                        .collect(Collectors.toList()))\n                .expected(Stream.concat(Arrays.stream(COMMON_ATTRS), Arrays.stream(expected).map(Option::name)).collect(Collectors.toList()))\n                .build();\n    }\n\n    private static <T extends Serializable> void processOption(String name, YamlObject options, Map<String, Serializable> result, Function<YamlValue, T> converter) {\n        YamlValue v = options.getYamlValue(name);\n        if (v != null) {\n            result.put(name, converter.apply(v));\n        }\n        options.remove(name);\n    }\n\n    private static <T extends Serializable> void processOption(Option<?> option, YamlObject options, YamlValueType<T> type, Map<String, Serializable> result) {\n        String name = option.name();\n        T v = options.getValue(name, type);\n        if (v != null) {\n            result.put(name, v);\n        }\n        options.remove(name);\n    }\n\n    private static Long coerceToLong(YamlValue value) {\n        Object v = value.getValue();\n        if (v == null) {\n            return null;\n        }\n\n        if (v instanceof Long) {\n            return (Long) v;\n        }\n\n        if (v instanceof Integer) {\n            return ((Integer) v).longValue();\n        }\n\n        throw InvalidValueTypeException.builder()\n                .expected(YamlValueType.INT)\n                .actual(value.getType())\n                .location(value.getLocation())\n                .build();\n    }\n\n    private static Double coerceToDouble(YamlValue value) {\n        Object v = value.getValue();\n        if (v == null) {\n            return null;\n        }\n\n        if (v instanceof Double) {\n            return (Double) v;\n        }\n\n        if (v instanceof Float) {\n            return ((Float) v).doubleValue();\n        }\n\n        if (v instanceof Integer) {\n            return ((Integer) v).doubleValue();\n        }\n\n        if (v instanceof Long) {\n            return ((Long) v).doubleValue();\n        }\n\n        throw InvalidValueTypeException.builder()\n                .expected(YamlValueType.FLOAT)\n                .actual(value.getType())\n                .location(value.getLocation())\n                .build();\n    }\n\n    private static class TypeInfo implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        public static TypeInfo parse(String s) {\n            String type = s;\n            Cardinality cardinality = Cardinality.ONE_AND_ONLY_ONE;\n\n            if (s.endsWith(\"?\")) {\n                type = type.substring(0, type.length() - 1);\n                cardinality = Cardinality.ONE_OR_NONE;\n            } else if (s.endsWith(\"+\")) {\n                type = type.substring(0, type.length() - 1);\n                cardinality = Cardinality.AT_LEAST_ONE;\n            } else if (s.endsWith(\"*\")) {\n                type = type.substring(0, type.length() - 1);\n                cardinality = Cardinality.ANY;\n            }\n\n            return new TypeInfo(type, cardinality);\n        }\n\n        private final String type;\n        private final Cardinality cardinality;\n\n        private TypeInfo(String type, Cardinality cardinality) {\n            this.type = type;\n            this.cardinality = cardinality;\n        }\n    }\n\n    private FormFieldParser() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/FormsGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.walmartlabs.concord.runtime.v2.exception.InvalidValueException;\nimport com.walmartlabs.concord.runtime.v2.model.*;\nimport io.takari.parc.Parser;\nimport io.takari.parc.Seq;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.ExpressionGrammar.maybeExpression;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.optional;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.options;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.*;\nimport static io.takari.parc.Combinators.*;\n\npublic final class FormsGrammar {\n\n    private static final Parser<Atom, FormField> formField =\n            orError(betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    satisfyToken(JsonToken.FIELD_NAME).bind(a ->\n                            value.map(optionsValue -> {\n                                if (optionsValue.getType() != YamlValueType.OBJECT) {\n                                    // will throw exception\n                                    optionsValue.getValue(YamlValueType.FORM_FIELD);\n                                }\n                                YamlObject options = (YamlObject) optionsValue;\n                                return FormFieldParser.parse(a.name, a.location, options);\n                            }))),\n                    YamlValueType.FORM_FIELD);\n\n    private static final Parser<Atom, List<FormField>> formFieldsArray =\n            betweenTokens(JsonToken.START_ARRAY, JsonToken.END_ARRAY, many1(formField).map(Seq::toList));\n\n    private static final Parser<Atom, List<FormField>> formFields =\n            orError(formFieldsArray, YamlValueType.ARRAY_OF_FORM_FIELD);\n\n    private static final Parser<Atom, Form> form =\n            satisfyAnyField(YamlValueType.FORM, f ->\n                    formFields.map(fields -> Form.builder()\n                            .name(f.name)\n                            .fields(fields)\n                            .location(f.location)\n                            .build()));\n\n    private static final Parser<Atom, Map<String, Form>> forms =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    many(form).map(Seq::toList)\n                            .map(f -> f.stream().collect(Collectors.toMap(FormsGrammar::assertFormName, Function.identity()))));\n\n    private static String assertFormName(Form form) {\n        // Form names in project doc root 'forms' configs are not expressions. We can\n        // validate while linting before execution begins\n        if (!form.name().matches(\"^[A-Za-z0-9_ $]+$\")) {\n            throw InvalidValueException.builder()\n                    .location(form.location())\n                    .actual(form.name())\n                    .expected(\"String matching regex \\\"^[A-Za-z0-9_ $]+$\\\"\")\n                    .build();\n        }\n\n        return form.name();\n    }\n\n    private static Parser<Atom, ImmutableFormCallOptions.Builder> formCallFieldsOption(ImmutableFormCallOptions.Builder o) {\n        return orError(or(formFieldsArray.map(o::fields), maybeExpression.map(o::fieldsExpression)), YamlValueType.FORM_CALL_FIELDS);\n    }\n\n    private static Parser<Atom, ImmutableFormCallOptions.Builder> formCallValuesOption(ImmutableFormCallOptions.Builder o) {\n        return orError(or(maybeMap.map(o::values), maybeExpression.map(o::valuesExpression)), YamlValueType.FORM_CALL_VALUES);\n    }\n\n    private static Parser<Atom, ImmutableFormCallOptions.Builder> formCallRunAsOption(ImmutableFormCallOptions.Builder o) {\n        return orError(or(maybeMap.map(o::runAs), maybeExpression.map(o::runAsExpression)), YamlValueType.FORM_CALL_RUN_AS);\n    }\n\n    private static final Parser<Atom, FormCallOptions> formCallOptions =\n            with(FormCallOptions::builder,\n                    o -> options(\n                            optional(\"yield\", booleanVal.map(o::isYield)),\n                            optional(\"saveSubmittedBy\", booleanVal.map(o::saveSubmittedBy)),\n                            optional(\"runAs\", formCallRunAsOption(o)),\n                            optional(\"values\", formCallValuesOption(o)),\n                            optional(\"fields\", formCallFieldsOption(o))\n                    ))\n                    .map(ImmutableFormCallOptions.Builder::build);\n\n    public static final Parser<Atom, FormCall> callForm =\n            satisfyField(\"form\", YamlValueType.FORM_CALL, a ->\n                    stringVal.bind(formName ->\n                            formCallOptions.map(options -> new FormCall(a.location, formName, options))));\n\n    public static final Parser<Atom, Map<String, Form>> formsVal =\n            orError(forms, YamlValueType.FORMS);\n\n    private FormsGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/GrammarLookup.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.walmartlabs.concord.runtime.v2.exception.InvalidFieldDefinitionException;\nimport com.walmartlabs.concord.runtime.v2.exception.YamlProcessingException;\nimport io.takari.parc.Parser;\nimport io.takari.parc.Result;\nimport io.takari.parc.Seq;\n\nimport java.util.List;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.satisfyToken;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.testToken;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.value;\nimport static io.takari.parc.Combinators.many1;\n\npublic final class GrammarLookup {\n\n    public static <O, T> Parser<Atom, O> lookup(String fieldName, YamlValueType<T> valueType,\n                                                T value, Parser<Atom, O> valueMatchParser,\n                                                Parser<Atom, O> elseParser) {\n        return in -> {\n            Result<Atom, List<KV<String, YamlValue>>> lookupValues = many1(fieldValue).map(Seq::toList).apply(in);\n            if (lookupValues.isFailure()) {\n                return elseParser.apply(in);\n            }\n\n            List<KV<String, YamlValue>> fields = lookupValues.toSuccess().getResult();\n            YamlValue field = fields.stream().filter(f -> f.getKey().equals(fieldName)).findFirst().map(KV::getValue).orElse(null);\n            if (field == null) {\n                return elseParser.apply(in);\n            }\n\n            T actualValue;\n            try {\n                actualValue = field.getValue(valueType);\n            } catch (YamlProcessingException e) {\n                throw new InvalidFieldDefinitionException(fieldName, field.getLocation(), e);\n            }\n\n            if (value.equals(actualValue)) {\n                return valueMatchParser.apply(in);\n            }\n            return elseParser.apply(in);\n        };\n    }\n\n    private static final Parser<Atom, KV<String, YamlValue>> fieldValue =\n            testToken(JsonToken.FIELD_NAME).bind(a -> satisfyToken(JsonToken.FIELD_NAME).then(\n                    value.map(v -> new KV<>(a.name, v))));\n\n    private GrammarLookup() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/GrammarMisc.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.walmartlabs.concord.runtime.v2.exception.InvalidFieldDefinitionException;\nimport com.walmartlabs.concord.runtime.v2.exception.InvalidValueTypeException;\nimport com.walmartlabs.concord.runtime.v2.exception.YamlProcessingException;\nimport io.takari.parc.Input;\nimport io.takari.parc.Parser;\nimport io.takari.parc.Result;\n\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\nimport java.util.function.Predicate;\nimport java.util.function.Supplier;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.stringVal;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.value;\nimport static io.takari.parc.Combinators.*;\n\npublic final class GrammarMisc {\n\n    public static Parser<Atom, Atom> satisfyToken(JsonToken t) {\n        return satisfy((Atom a) -> a.token == t);\n    }\n\n    public static Parser<Atom, Atom> satisfyField(String name) {\n        return satisfy((Atom a) -> name.equals(a.name));\n    }\n\n    public static <O> Parser<Atom, O> betweenTokens(JsonToken start, JsonToken end, Parser<Atom, O> p) {\n        return between(satisfyToken(start), satisfyToken(end), p);\n    }\n\n    public static <Atom> Parser<Atom, Atom> test(Predicate<Atom> p) {\n        return in -> {\n            if (in.end()) {\n                return fail(in, \"EOF\");\n            }\n\n            Atom val = in.first();\n            if (p.test(val)) {\n                return ok(val, in);\n            }\n\n            return fail(in, null);\n        };\n    }\n\n    public static Parser<Atom, Atom> testToken(JsonToken t) {\n        return test(a -> a.token == t);\n    }\n\n    public static Parser<Atom, Atom> testField(String name) {\n        return test(a -> name.equals(a.name));\n    }\n\n    public static <O, X> Parser<Atom, O> with(Supplier<O> s, Function<O, Parser<Atom, X>> p) {\n        return in -> {\n            O o = s.get();\n            Result<Atom, X> rp = p.apply(o).apply(in);\n            if (rp.isSuccess()) {\n                return ok(o, rp.getRest());\n            }\n            return rp.cast();\n        };\n    }\n\n    public static <O> Parser<Atom, O> invalidValueTypeError(YamlValueType<O> type) {\n        return value.map(v -> {\n            throw InvalidValueTypeException.builder()\n                    .expected(type)\n                    .actual(v.getType())\n                    .location(v.getLocation())\n                    .build();\n        });\n    }\n\n    public static <O> Parser<Atom, O> orError(Parser<Atom, O> p, YamlValueType<O> type) {\n        return or(p, invalidValueTypeError(type));\n    }\n\n    public static <O> Parser<Atom, O> field(String name, Function<Atom, Parser<Atom, O>> f) {\n        return testField(name).bind(a -> in -> {\n            try {\n                return f.apply(a).apply(in);\n            } catch (YamlProcessingException e) {\n                throw new InvalidFieldDefinitionException(name, a.location, e);\n            }\n        });\n    }\n\n    public static <O> Parser<Atom, O> satisfyField(String name, Function<Atom, Parser<Atom, O>> f) {\n        return satisfyField(name).bind(a -> in -> {\n            try {\n                return f.apply(a).apply(in);\n            } catch (YamlProcessingException e) {\n                throw new InvalidFieldDefinitionException(name, a.location, e);\n            }\n        });\n    }\n\n    public static <O> Parser<Atom, O> satisfyField(String name, YamlValueType<O> valueType, Function<Atom, Parser<Atom, O>> f) {\n        return satisfyField(name).bind(a -> in -> {\n            try {\n                Result<Atom, O> rp = f.apply(a).apply(in);\n                if (rp.isSuccess()) {\n                    return rp.cast();\n                }\n                return invalidValueTypeError(valueType).apply(in).cast();\n            } catch (YamlProcessingException e) {\n                throw new InvalidFieldDefinitionException(name, a.location, e);\n            }\n        });\n    }\n\n    public static <O> Parser<Atom, O> satisfyAnyField(YamlValueType<O> valueType, Function<Atom, Parser<Atom, O>> f) {\n        return satisfyToken(JsonToken.FIELD_NAME).bind(a -> in -> {\n            try {\n                Result<Atom, O> rp = f.apply(a).apply(in);\n                if (rp.isSuccess()) {\n                    return rp.cast();\n                }\n                return invalidValueTypeError(valueType).apply(in).cast();\n            } catch (YamlProcessingException e) {\n                throw new InvalidFieldDefinitionException(a.name, a.location, e);\n            }\n        });\n    }\n\n    public static <O> Parser<Atom, O> namedStep(String name, YamlValueType<O> valueType, BiFunction<String, Atom, Parser<Atom, O>> f) {\n        return in -> {\n            if (in.end()) {\n                return fail(in, \"EOF\");\n            }\n\n            Input<Atom> rest = in;\n            String stepName = null;\n            Atom val = in.first();\n            if (\"name\".equals(val.name)) {\n                try {\n                    Result<Atom, String> stepNameResult = stringVal.apply(in.rest());\n                    if (stepNameResult.isFailure()) {\n                            return invalidValueTypeError(YamlValueType.STRING).apply(in.rest()).cast();\n                    }\n                    rest = stepNameResult.getRest();\n                    stepName = stepNameResult.toSuccess().getResult();\n                } catch (YamlProcessingException e) {\n                    throw new InvalidFieldDefinitionException(\"name\", val.location, e);\n                }\n            }\n\n            final String finalStepName = stepName;\n            return satisfyField(name, valueType, atom -> f.apply(finalStepName, atom)).apply(rest);\n        };\n    }\n\n    private GrammarMisc() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/GrammarOptions.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.walmartlabs.concord.runtime.v2.Constants;\nimport com.walmartlabs.concord.runtime.v2.exception.MandatoryFieldNotFoundException;\nimport com.walmartlabs.concord.runtime.v2.exception.UnknownOptionException;\nimport io.takari.parc.Input;\nimport io.takari.parc.Parser;\nimport io.takari.parc.Result;\nimport io.takari.parc.Seq;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.util.*;\nimport java.util.function.BiFunction;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.*;\nimport static io.takari.parc.Combinators.*;\n\npublic final class GrammarOptions {\n\n    public static final Parser<Atom, SimpleOptions> simpleOptions =\n            with(ImmutableSimpleOptions::builder,\n                    o -> options(\n                            optional(\"meta\", mapVal.map(o::meta))\n                    ))\n                    .map(ImmutableSimpleOptions.Builder::build);\n\n    public static final Parser<Atom, SimpleOptions> namedOptions =\n            with(ImmutableSimpleOptions::builder,\n                    o -> options(\n                            optional(\"meta\", mapVal.map(o::meta)),\n                            optional(\"name\", stringVal.map(v -> o.putMeta(Constants.SEGMENT_NAME, v)))\n                    ))\n                    .map(ImmutableSimpleOptions.Builder::build);\n\n    public static <O> Option<O> any(BiFunction<Atom, String, Parser<Atom, ? extends O>> f) {\n        return Option.of(f);\n    }\n\n    public static <O> Option<O> optional(String name, Parser<Atom, O> p) {\n        return Option.of(name, false, p);\n    }\n\n    public static <O> Option<O> mandatory(String name, Parser<Atom, O> p) {\n        return Option.of(name, true, p);\n    }\n\n    @SafeVarargs\n    public static <O> Parser<Atom, List<O>> options(Option<? extends O>... os) {\n        return options(Arrays.asList(os));\n    }\n\n    public static <O> Parser<Atom, List<O>> options(List<Option<? extends O>> options) {\n        return in -> {\n            // TODO: skip check if no mandatory options\n            // Check mandatory options\n            Result<Atom, Set<String>> yamlOptions = allOptionKeys.apply(in);\n            Set<String> opts = Collections.emptySet();\n            if (yamlOptions.isSuccess()) {\n                opts = yamlOptions.toSuccess().getResult();\n            }\n            assertMandatoryOptions(opts, options);\n\n            Result<Atom, List<O>> rp = many(_choice(options)).map(Seq::toList).apply(in);\n            if (rp.isFailure()) {\n                return fail(in, null);\n            }\n\n            // handle unparsed options\n            Input<Atom> rest = rp.getRest();\n            Result<Atom, List<KV<String, YamlValue>>> rp2 = unparsedOptionsVal.apply(rest);\n            if (rp2.isFailure()) { // no unparsed options\n                return rp.cast();\n            }\n\n            List<String> expectedOptions = options.stream().map(Option::name).filter(Objects::nonNull).sorted().collect(Collectors.toList());\n\n            List<KV<String, YamlValue>> unparsedOptions = rp2.toSuccess().getResult();\n\n            // all options after unparsed are marked as unknown, so cleanup\n            if (unparsedOptions.size() > 1) {\n                unparsedOptions = unparsedOptions.stream().filter(u -> !expectedOptions.contains(u.getKey())).collect(Collectors.toList());\n            }\n\n            throw UnknownOptionException.builder()\n                    .location(unparsedOptions.get(0).getValue().getLocation())\n                    .unknown(unparsedOptions.stream()\n                            .map(kv -> UnknownOption.of(kv.getKey(), kv.getValue().getType(), kv.getValue().getLocation()))\n                            .collect(Collectors.toList()))\n                    .expected(expectedOptions)\n                    .build();\n        };\n    }\n\n    private static <O> void assertMandatoryOptions(Set<String> yamlOptions, List<Option<? extends O>> options) {\n        List<String> notFoundMandatoryOptions = new ArrayList<>();\n        for (Option<? extends O> o : options) {\n            if (o.mandatory() && !yamlOptions.contains(o.name())) {\n                notFoundMandatoryOptions.add(o.name());\n            }\n        }\n\n        if (!notFoundMandatoryOptions.isEmpty()) {\n            throw new MandatoryFieldNotFoundException(notFoundMandatoryOptions);\n        }\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    public interface Option<O> {\n\n        @Nullable\n        String name();\n\n        @Value.Default\n        default boolean mandatory() {\n            return false;\n        }\n\n        @Nullable\n        Parser<Atom, ? extends O> parser();\n\n        @Nullable\n        BiFunction<Atom, String, ? extends Parser<Atom, ? extends O>> anyOptionFunction();\n\n        static <O> Option<O> of(String name, boolean mandatory, Parser<Atom, O> parser) {\n            return ImmutableOption.<O>builder()\n                    .name(name)\n                    .mandatory(mandatory)\n                    .parser(parser)\n                    .build();\n        }\n\n        static <O> Option<O> of(BiFunction<Atom, String, Parser<Atom, ? extends O>> anyOptionFunction) {\n            return ImmutableOption.<O>builder()\n                    .anyOptionFunction(anyOptionFunction)\n                    .build();\n        }\n    }\n\n    private static <O> Parser<Atom, O> _choice(List<Option<? extends O>> options) {\n        if (options.isEmpty()) {\n            throw new IllegalArgumentException(\"Empty options\");\n        }\n\n        return in -> {\n            for (Option<? extends O> o : options) {\n                if (o.parser() == null) {\n                    continue;\n                }\n                Result<Atom, ? extends O> rp = satisfyField(o.name(), atom -> o.parser()).apply(in);\n                if (rp.isSuccess()) {\n                    return rp.cast();\n                }\n            }\n\n            BiFunction<Atom, String, ? extends Parser<Atom, ? extends O>> anyFunction = options.stream()\n                    .map(Option::anyOptionFunction)\n                    .filter(Objects::nonNull)\n                    .findFirst()\n                    .orElse(null);\n            if (anyFunction != null) {\n                Result<Atom, ? extends O> rp = satisfyToken(JsonToken.FIELD_NAME).bind(a -> {\n                    Parser<Atom, ? extends O> result = anyFunction.apply(a, a.name);\n                    return result;\n                }).apply(in);\n\n                if (rp.isSuccess()) {\n                    return rp.cast();\n                }\n            }\n\n            return fail(in, null);\n        };\n    }\n\n    private static final Parser<Atom, KV<String, YamlValue>> fieldValue =\n            testToken(JsonToken.FIELD_NAME).bind(a -> satisfyToken(JsonToken.FIELD_NAME).then(\n                    value.map(v -> new KV<>(a.name, v))));\n\n    private static final Parser<Atom, List<KV<String, YamlValue>>> unparsedOptionsVal =\n            many1(fieldValue).map(Seq::toList);\n\n    private static final Parser<Atom, Set<String>> allOptionKeys =\n            many1(fieldValue).map(v -> v.stream().map(KV::getKey).collect(Collectors.toSet()));\n\n    private GrammarOptions() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/GrammarV2.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.walmartlabs.concord.runtime.v2.exception.InvalidValueException;\nimport com.walmartlabs.concord.runtime.v2.exception.InvalidValueTypeException;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport io.takari.parc.Parser;\nimport io.takari.parc.Seq;\n\nimport java.io.Serializable;\nimport java.time.Duration;\nimport java.time.format.DateTimeParseException;\nimport java.util.*;\nimport java.util.function.BiPredicate;\nimport java.util.regex.Pattern;\nimport java.util.regex.PatternSyntaxException;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.CheckpointGrammar.checkpoint;\nimport static com.walmartlabs.concord.runtime.v2.parser.ConditionalExpressionsGrammar.ifExpr;\nimport static com.walmartlabs.concord.runtime.v2.parser.ConditionalExpressionsGrammar.switchExpr;\nimport static com.walmartlabs.concord.runtime.v2.parser.ExitGrammar.exit;\nimport static com.walmartlabs.concord.runtime.v2.parser.ExpressionGrammar.exprFull;\nimport static com.walmartlabs.concord.runtime.v2.parser.ExpressionGrammar.exprShort;\nimport static com.walmartlabs.concord.runtime.v2.parser.FlowCallGrammar.callFull;\nimport static com.walmartlabs.concord.runtime.v2.parser.FormsGrammar.callForm;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.GroupOfStepsGrammar.group;\nimport static com.walmartlabs.concord.runtime.v2.parser.LogGrammar.logStep;\nimport static com.walmartlabs.concord.runtime.v2.parser.LogGrammar.logYamlStep;\nimport static com.walmartlabs.concord.runtime.v2.parser.ParallelGrammar.parallelBlock;\nimport static com.walmartlabs.concord.runtime.v2.parser.ReturnGrammar.returnStep;\nimport static com.walmartlabs.concord.runtime.v2.parser.ScriptGrammar.script;\nimport static com.walmartlabs.concord.runtime.v2.parser.SetVariablesGrammar.setVars;\nimport static com.walmartlabs.concord.runtime.v2.parser.SuspendGrammar.suspendStep;\nimport static com.walmartlabs.concord.runtime.v2.parser.TaskGrammar.taskFull;\nimport static com.walmartlabs.concord.runtime.v2.parser.ThrowGrammar.throwStep;\nimport static io.takari.parc.Combinators.*;\n\npublic final class GrammarV2 {\n\n    public static final Parser.Ref<Atom, YamlValue> value = Parser.ref();\n    public static final Parser.Ref<Atom, YamlList> arrayOfValues = Parser.ref();\n    public static final Parser.Ref<Atom, YamlObject> object = Parser.ref();\n\n    public static final Parser<Atom, Serializable> anyVal = value.map(YamlValue::getValue);\n    public static final Parser<Atom, Integer> intVal = value.map(v -> v.getValue(YamlValueType.INT));\n    public static final Parser<Atom, String> stringVal = value.map(v -> v.getValue(YamlValueType.STRING));\n    public static final Parser<Atom, Boolean> booleanVal = value.map(v -> v.getValue(YamlValueType.BOOLEAN));\n    public static final Parser<Atom, Map<String, Serializable>> mapVal = value.map(v -> v.getValue(YamlValueType.OBJECT));\n    public static final Parser<Atom, List<String>> regexpArrayVal = value.map(v -> asList(v, YamlValueType.ARRAY_OF_PATTERN).getListValue(GrammarV2::regexpConverter));\n    public static final Parser<Atom, String> regexpVal = value.map(GrammarV2::regexpConverter);\n    public static final Parser<Atom, List<String>> stringArrayVal = value.map(v -> asList(v, YamlValueType.ARRAY_OF_STRING).getListValue(YamlValueType.STRING));\n    public static final Parser<Atom, Set<String>> stringSetVal = stringArrayVal.map(HashSet::new);\n    public static final Parser<Atom, Serializable> nonNullVal = value.map(v -> {\n        assertNotNull(v);\n        return v.getValue();\n    });\n    public static final Parser<Atom, Integer> maybeInt = _val(JsonToken.VALUE_NUMBER_INT).map(v -> v.getValue(YamlValueType.INT));\n    public static final Parser<Atom, String> maybeString = _val(JsonToken.VALUE_STRING).map(v -> v.getValue(YamlValueType.STRING));\n    public static final Parser<Atom, List<String>> maybeStringArray = arrayOfValues.map(v -> v.getListValue(YamlValueType.STRING));\n    public static final Parser<Atom, Map<String, Serializable>> maybeMap = object.map(YamlObject::getValue);\n    public static final Parser<Atom, Object> regexpOrArrayVal = value.map(GrammarV2::regexpOrArrayConverter);\n    public static final Parser<Atom, Duration> durationVal = value.map(GrammarV2::durationConverter);\n    public static final Parser<Atom, String> timezoneVal = value.map(GrammarV2::timezoneConverter);\n    public static final Parser<Atom, List<String>> stringOrArrayVal = value.map(GrammarV2::stringOrArrayConverter);\n    public static final Parser<Atom, String> stringNotEmptyVal = value.map(v -> {\n        String vv = v.getValue(YamlValueType.STRING);\n        if (vv.trim().isEmpty()) {\n            throw new InvalidValueTypeException.Builder()\n                    .location(v.getLocation())\n                    .expected(YamlValueType.NON_EMPTY_STRING)\n                    .actual(v.getType())\n                    .message(\"Empty value\")\n                    .build();\n        }\n        return vv;\n    });\n\n    public static <E extends Enum<E>> Parser<Atom, E> enumVal(Class<E> enumData) {\n        return enumVal(enumData, String::equals);\n    }\n\n    public static <E extends Enum<E>> Parser<Atom, E> enumVal(Class<E> enumData,\n                                                              BiPredicate<String, String> cmp) {\n        return value.map(vv -> {\n            String v = vv.getValue(YamlValueType.STRING);\n\n            for (E enumVal : enumData.getEnumConstants()) {\n                if (cmp.test(enumVal.name(), v)) {\n                    return enumVal;\n                }\n            }\n\n            throw InvalidValueException.builder()\n                    .actual(v)\n                    .expected(Arrays.stream(enumData.getEnumConstants()).map(Enum::name).collect(Collectors.toList()))\n                    .location(vv.getLocation())\n                    .build();\n        });\n    }\n\n    public static final Parser.Ref<Atom, List<Step>> stepsVal = Parser.ref();\n\n    @SuppressWarnings(\"rawtypes\")\n    private static YamlValueType toType(JsonToken t) {\n        switch (t) {\n            case VALUE_STRING:\n                return YamlValueType.STRING;\n            case VALUE_NUMBER_INT:\n                return YamlValueType.INT;\n            case VALUE_NUMBER_FLOAT:\n                return YamlValueType.FLOAT;\n            case VALUE_FALSE:\n            case VALUE_TRUE:\n                return YamlValueType.BOOLEAN;\n            case VALUE_NULL:\n                return YamlValueType.NULL;\n            default:\n                throw new IllegalArgumentException(\"Unknown type: \" + t);\n        }\n    }\n\n    // value := VALUE_STRING | VALUE_NUMBER_INT | VALUE_NUMBER_FLOAT | VALUE_TRUE | VALUE_FALSE | VALUE_NULL | arrayOfValues | object\n    @SuppressWarnings(\"unchecked\")\n    private static Parser<Atom, YamlValue> _val(JsonToken t) {\n        return satisfyToken(t).map(a -> new YamlValue(a.value, toType(t), a.location));\n    }\n\n    // arrayOfValues := START_ARRAY value* END_ARRAY\n    static {\n        arrayOfValues.set(label(\"Array of values\",\n                testToken(JsonToken.START_ARRAY).bind(t ->\n                        betweenTokens(JsonToken.START_ARRAY, JsonToken.END_ARRAY,\n                                many(value).map(a -> new YamlList(a.toList(), t.location)))\n                )));\n    }\n\n    // object := START_OBJECT (FIELD_NAME value)* END_OBJECT\n    static {\n        object.set(\n                testToken(JsonToken.START_OBJECT).bind(t ->\n                        betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                                many(satisfyToken(JsonToken.FIELD_NAME).bind(a ->\n                                        value.map(v -> new KV<>(a.name, v)))))\n                                .map(a -> new YamlObject(valueToMap(a), t.location))));\n    }\n\n    static {\n        value.set(choice(choice(\n                _val(JsonToken.VALUE_STRING),\n                _val(JsonToken.VALUE_NUMBER_INT),\n                _val(JsonToken.VALUE_NUMBER_FLOAT),\n                _val(JsonToken.VALUE_TRUE),\n                _val(JsonToken.VALUE_FALSE),\n                _val(JsonToken.VALUE_NULL)),\n                arrayOfValues,\n                object\n        ));\n    }\n\n    private static final Parser<Atom, Step> stepObject =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    choice(choice(parallelBlock, group, exprFull), choice(taskFull, script, callFull, callForm),\n                            choice(checkpoint, ifExpr, switchExpr, setVars), logStep, logYamlStep, throwStep, suspendStep));\n\n    // step := exit | exprShort | parallelBlock | stepObject\n    private static final Parser<Atom, Step> step = orError(choice(exit, returnStep, exprShort, stepObject), YamlValueType.STEP);\n\n    // steps := START_ARRAY step+ END_ARRAY\n    static {\n        stepsVal.set(\n                orError(betweenTokens(JsonToken.START_ARRAY, JsonToken.END_ARRAY, many1(step).map(Seq::toList)),\n                        YamlValueType.ARRAY_OF_STEP));\n    }\n\n    public static Parser<Atom, Step> getProcessStep() {\n        return step;\n    }\n\n    public static <K, V> Map<K, V> toMap(Seq<KV<K, V>> values) {\n        Map<K, V> m = new LinkedHashMap<>();\n        values.stream().forEach(kv -> m.put(kv.getKey(), kv.getValue()));\n        return m;\n    }\n\n    public static YamlValue assertNotNull(YamlValue v) {\n        if (v.getType() != YamlValueType.NULL) {\n            return v;\n        }\n\n        throw new InvalidValueTypeException.Builder()\n                .location(v.getLocation())\n                .expected(YamlValueType.NON_NULL)\n                .actual(v.getType())\n                .build();\n    }\n\n    private static Map<String, YamlValue> valueToMap(Seq<KV<String, YamlValue>> values) {\n        if (values == null) {\n            return Collections.emptyMap();\n        }\n\n        Map<String, YamlValue> m = new LinkedHashMap<>();\n        values.stream().forEach(kv -> m.put(kv.getKey(), kv.getValue()));\n        return m;\n    }\n\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    private static YamlList asList(YamlValue value, YamlValueType listType) {\n        if (YamlValueType.ARRAY != value.getType()) {\n            // will throw exception\n            value.getValue(listType);\n        }\n        return (YamlList) value;\n    }\n\n    private static String regexpConverter(YamlValue v) {\n        if (v.getType() != YamlValueType.STRING) {\n            // will throw exception\n            v.getValue(YamlValueType.PATTERN);\n        }\n\n        String p = v.getValue(YamlValueType.STRING);\n        try {\n            Pattern.compile(p);\n            return p;\n        } catch (PatternSyntaxException e) {\n            throw new InvalidValueTypeException.Builder()\n                    .location(v.getLocation())\n                    .expected(YamlValueType.PATTERN)\n                    .actual(v.getType())\n                    .message(e.getMessage())\n                    .build();\n        }\n    }\n\n    private static Object regexpOrArrayConverter(YamlValue v) {\n        if (v.getType() == YamlValueType.STRING) {\n            return regexpConverter(v);\n        }\n\n        YamlList list = asList(v, YamlValueType.REGEXP_OR_ARRAY);\n        return list.getListValue(GrammarV2::regexpConverter);\n    }\n\n    private static Duration durationConverter(YamlValue v) {\n        if (v.getType() != YamlValueType.STRING) {\n            // will throw exception\n            v.getValue(YamlValueType.DURATION);\n        }\n\n        String maybePattern = v.getValue(YamlValueType.STRING);\n        try {\n            return Duration.parse(maybePattern);\n        } catch (DateTimeParseException e) {\n            throw new InvalidValueTypeException.Builder()\n                    .location(v.getLocation())\n                    .expected(YamlValueType.DURATION)\n                    .actual(v.getType())\n                    .message(e.getMessage())\n                    .build();\n        }\n    }\n\n    private static String timezoneConverter(YamlValue v) {\n        if (v.getType() != YamlValueType.STRING) {\n            // will throw exception\n            v.getValue(YamlValueType.TIMEZONE);\n        }\n\n        String timezone = v.getValue(YamlValueType.STRING);\n\n        boolean valid = Arrays.asList(TimeZone.getAvailableIDs()).contains(timezone);\n        if (valid) {\n            return timezone;\n        }\n\n        throw new InvalidValueTypeException.Builder()\n                .location(v.getLocation())\n                .expected(YamlValueType.TIMEZONE)\n                .actual(v.getType())\n                .message(\"Unknown timezone: '\" + timezone + \"'\")\n                .build();\n    }\n\n    private static List<String> stringOrArrayConverter(YamlValue v) {\n        if (v.getType() == YamlValueType.STRING) {\n            return Collections.singletonList(v.getValue());\n        }\n\n        YamlList list = asList(v, YamlValueType.STRING_OR_ARRAY);\n        return list.getListValue(YamlValueType.STRING);\n    }\n\n    private GrammarV2() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/GroupOfStepsGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.Constants;\nimport com.walmartlabs.concord.runtime.v2.model.*;\nimport io.takari.parc.Parser;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.namedStep;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.with;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.optional;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.options;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.LoopGrammar.loopVal;\nimport static io.takari.parc.Combinators.choice;\n\npublic final class GroupOfStepsGrammar {\n\n    private static ImmutableGroupOfStepsOptions.Builder optionsWithStepName(String stepName) {\n        ImmutableGroupOfStepsOptions.Builder result = GroupOfStepsOptions.builder();\n        if (stepName != null) {\n            result.putMeta(Constants.SEGMENT_NAME, stepName);\n        }\n        return result;\n    }\n\n    private static Parser<Atom, GroupOfStepsOptions> groupOptions(String stepName) {\n        return with(() -> optionsWithStepName(stepName),\n                o -> options(\n                        optional(\"out\", stringOrArrayVal.map(o::out)),\n                        optional(\"error\", stepsVal.map(o::errorSteps)),\n                        optional(\"withItems\", nonNullVal.map(v -> o.withItems(WithItems.of(v, WithItems.Mode.SERIAL)))),\n                        optional(\"parallelWithItems\", nonNullVal.map(v -> o.withItems(WithItems.of(v, WithItems.Mode.PARALLEL)))),\n                        optional(\"loop\", loopVal.map(o::loop)),\n                        optional(\"meta\", mapVal.map(o::putAllMeta)),\n                        optional(\"name\", stringVal.map(v -> o.putMeta(Constants.SEGMENT_NAME, v)))\n                ))\n                .map(ImmutableGroupOfStepsOptions.Builder::build);\n    }\n\n    private static Parser<Atom, GroupOfSteps> groupDef(String stepName, Atom a) {\n        return stepsVal.bind(steps -> groupOptions(stepName).map(options -> new GroupOfSteps(a.location, steps, options)));\n    }\n\n    public static final Parser<Atom, GroupOfSteps> groupAsTry =\n            namedStep(\"try\", YamlValueType.TRY, GroupOfStepsGrammar::groupDef);\n\n    public static final Parser<Atom, GroupOfSteps> groupAsBlock =\n            namedStep(\"block\", YamlValueType.BLOCK, GroupOfStepsGrammar::groupDef);\n\n    public static final Parser<Atom, Step> group = choice(groupAsTry, groupAsBlock);\n\n    private GroupOfStepsGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/ImportsGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.walmartlabs.concord.imports.*;\nimport com.walmartlabs.concord.runtime.v2.exception.UnknownOptionException;\nimport io.takari.parc.Parser;\nimport io.takari.parc.Seq;\n\nimport java.util.Arrays;\nimport java.util.Collections;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.regexpArrayVal;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.stringVal;\nimport static io.takari.parc.Combinators.choice;\nimport static io.takari.parc.Combinators.many1;\n\npublic final class ImportsGrammar {\n\n    private static final Parser<Atom, Import.SecretDefinition> secret =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    with(ImmutableSecretDefinition::builder,\n                            o -> options(\n                                    optional(\"org\", stringVal.map(o::org)),\n                                    mandatory(\"name\", stringVal.map(o::name)),\n                                    optional(\"password\", stringVal.map(o::password))))\n                            .map(ImmutableSecretDefinition.Builder::build));\n\n    private static final Parser<Atom, Import.SecretDefinition> secretVal =\n            orError(secret, YamlValueType.IMPORT_SECRET);\n\n    private static final Parser<Atom, Import.GitDefinition> gitImport =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    with(ImmutableGitDefinition::builder,\n                            o -> options(\n                                    optional(\"name\", stringVal.map(o::name)),\n                                    optional(\"url\", stringVal.map(o::url)),\n                                    optional(\"version\", stringVal.map(o::version)),\n                                    optional(\"path\", stringVal.map(o::path)),\n                                    optional(\"dest\", stringVal.map(o::dest)),\n                                    optional(\"exclude\", regexpArrayVal.map(o::exclude)),\n                                    optional(\"secret\", secretVal.map(o::secret))))\n                            .map(ImmutableGitDefinition.Builder::build));\n\n    private static final Parser<Atom, Import.MvnDefinition> mvnImport =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    with(ImmutableMvnDefinition::builder,\n                            o -> options(\n                                    mandatory(\"url\", stringVal.map(o::url)),\n                                    optional(\"dest\", stringVal.map(o::dest))))\n                            .map(ImmutableMvnDefinition.Builder::build));\n\n    private static final Parser<Atom, Import.DirectoryDefinition> dirImport =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    with(ImmutableDirectoryDefinition::builder,\n                            o -> options(\n                                    mandatory(\"src\", stringVal.map(o::src)),\n                                    optional(\"dest\", stringVal.map(o::dest))))\n                            .map(ImmutableDirectoryDefinition.Builder::build));\n\n    private static final Parser<Atom, Import.GitDefinition> gitImportVal =\n            orError(gitImport, YamlValueType.GIT_IMPORT);\n\n    private static final Parser<Atom, Import.MvnDefinition> mvnImportVal =\n            orError(mvnImport, YamlValueType.MVN_IMPORT);\n\n    private static final Parser<Atom, Import.DirectoryDefinition> dirImportVal =\n            orError(dirImport, YamlValueType.DIR_IMPORT);\n\n    private static final Parser<Atom, Import> importDef =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    choice(\n                            satisfyField(\"git\", atom -> gitImportVal),\n                            satisfyField(\"dir\", atom -> dirImportVal),\n                            choice(satisfyField(\"mvn\", atom -> mvnImportVal),\n                                    satisfyToken(JsonToken.FIELD_NAME).bind(a -> {\n                                        throw UnknownOptionException.builder()\n                                                .location(a.location)\n                                                .unknown(Collections.singletonList(UnknownOption.of(a.name, null, a.location)))\n                                                .expected(Arrays.asList(\"git\", \"mvn\"))\n                                                .build();\n                                    }))));\n\n    private static final Parser<Atom, Import> importVal =\n            orError(importDef, YamlValueType.IMPORT);\n\n    private static final Parser<Atom, Imports> imports =\n            betweenTokens(JsonToken.START_ARRAY, JsonToken.END_ARRAY,\n                    many1(importVal).map(Seq::toList))\n                    .map(Imports::of);\n\n    public static final Parser<Atom, Imports> importsVal =\n            orError(imports, YamlValueType.IMPORTS);\n\n    private ImportsGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/KV.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Map;\n\npublic class KV<K, V> implements Map.Entry<K, V> {\n\n    private final K k;\n    private final V v;\n\n    public KV(K k, V v) {\n        this.k = k;\n        this.v = v;\n    }\n\n    @Override\n    public K getKey() {\n        return k;\n    }\n\n    @Override\n    public V getValue() {\n        return v;\n    }\n\n    @Override\n    public V setValue(V value) {\n        throw new IllegalStateException(\"Not allowed\");\n    }\n\n    @Override\n    public String toString() {\n        return \"KV{\" +\n                \"k=\" + k +\n                \", v=\" + v +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/ListInput.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.takari.parc.Input;\n\nimport java.util.List;\n\npublic class ListInput<T> implements Input<T> {\n\n    private final int pos;\n    private final List<T> items;\n\n    public ListInput(List<T> items) {\n        this(0, items);\n    }\n\n    private ListInput(int pos, List<T> items) {\n        this.pos = pos;\n        this.items = items;\n    }\n\n    @Override\n    public int position() {\n        return pos;\n    }\n\n    @Override\n    public T first() {\n        return items.get(pos);\n    }\n\n    @Override\n    public Input<T> rest() {\n        return new ListInput<>(pos + 1, items);\n    }\n\n    @Override\n    public boolean end() {\n        return pos >= items.size();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/LogGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.TaskCall;\nimport io.takari.parc.Parser;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.namedStep;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.namedOptions;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.anyVal;\n\npublic final class LogGrammar {\n\n    public static final Parser<Atom, TaskCall> logStep =\n            namedStep(\"log\", YamlValueType.TASK, (stepName, a) ->\n                    anyVal.bind(msg ->\n                            namedOptions.map(options -> new TaskCall(a.location, \"log\", TaskGrammar.optionsWithStepName(stepName)\n                                    .putInput(\"msg\", msg)\n                                    .putAllMeta(options.meta())\n                                    .build()))));\n\n    public static final Parser<Atom, TaskCall> logYamlStep =\n            namedStep(\"logYaml\", YamlValueType.TASK, (stepName, a) ->\n                    anyVal.bind(msg ->\n                            namedOptions.map(options -> new TaskCall(a.location, \"log\", TaskGrammar.optionsWithStepName(stepName)\n                                    .putInput(\"msg\", msg)\n                                    .putInput(\"format\", \"yaml\")\n                                    .putAllMeta(options.meta())\n                                    .build()))));\n\n    private LogGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/LoopGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.walmartlabs.concord.runtime.v2.model.ImmutableLoop;\nimport com.walmartlabs.concord.runtime.v2.model.Loop;\nimport io.takari.parc.Parser;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.ExpressionGrammar.maybeExpression;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.*;\nimport static io.takari.parc.Combinators.or;\n\npublic final class LoopGrammar {\n\n    private static Parser<Atom, ImmutableLoop.Builder> parallelism(ImmutableLoop.Builder o) {\n        return orError(or(maybeInt.map(v -> o.putOptions(\"parallelism\", v)), maybeExpression.map(v -> o.putOptions(\"parallelism\", v))), YamlValueType.LOOP_PARALLELISM);\n    }\n\n    private static final Parser<Atom, Loop> loop =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    with(Loop::builder,\n                            o -> options(\n                                    mandatory(\"items\", nonNullVal.map(o::items)),\n                                    optional(\"mode\", enumVal(Loop.Mode.class, String::equalsIgnoreCase).map(o::mode)),\n                                    optional(\"parallelism\", parallelism(o))\n                            ))\n                            .map(ImmutableLoop.Builder::build));\n\n    public static final Parser<Atom, Loop> loopVal =\n            orError(loop, YamlValueType.LOOP);\n\n    private LoopGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/ParallelGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.ImmutableParallelBlockOptions;\nimport com.walmartlabs.concord.runtime.v2.model.ParallelBlock;\nimport com.walmartlabs.concord.runtime.v2.model.ParallelBlockOptions;\nimport io.takari.parc.Parser;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.optional;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.options;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.*;\nimport static io.takari.parc.Combinators.or;\n\npublic final class ParallelGrammar {\n\n    private static Parser<Atom, ImmutableParallelBlockOptions.Builder> parallelOutOption(ImmutableParallelBlockOptions.Builder o) {\n        return orError(or(maybeMap.map(o::outExpr), or(maybeString.map(o::addOut), maybeStringArray.map(o::out))), YamlValueType.PARALLEL_BLOCK_OUT);\n    }\n\n    private static final Parser<Atom, ParallelBlockOptions> parallelOptions =\n            with(ParallelBlockOptions::builder,\n                    o -> options(\n                            optional(\"out\", parallelOutOption(o)),\n                            optional(\"meta\", mapVal.map(o::meta))\n                    ))\n                    .map(ImmutableParallelBlockOptions.Builder::build);\n\n    public static final Parser<Atom, ParallelBlock> parallelBlock =\n            satisfyField(\"parallel\", YamlValueType.PARALLEL, a ->\n                    stepsVal.bind(steps -> parallelOptions.map(options -> new ParallelBlock(a.location, steps, options))));\n\n    private ParallelGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/ProcessDefinitionGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.walmartlabs.concord.runtime.v2.model.ImmutableProcessDefinition;\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\nimport io.takari.parc.Parser;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.ConfigurationGrammar.processCfgVal;\nimport static com.walmartlabs.concord.runtime.v2.parser.FlowsGrammar.flowsVal;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.betweenTokens;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.with;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.optional;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.options;\nimport static com.walmartlabs.concord.runtime.v2.parser.ProfilesGrammar.profilesVal;\nimport static com.walmartlabs.concord.runtime.v2.parser.PublicFlowsGrammar.publicFlowsVal;\nimport static com.walmartlabs.concord.runtime.v2.parser.TriggersGrammar.triggersVal;\n\npublic final class ProcessDefinitionGrammar {\n\n    public static final Parser<Atom, ProcessDefinition> processDefinition =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    with(ImmutableProcessDefinition::builder,\n                            o -> options(\n                                    optional(\"configuration\", processCfgVal.map(o::configuration)),\n                                    optional(\"flows\", flowsVal.map(o::flows)),\n                                    optional(\"publicFlows\", publicFlowsVal.map(o::publicFlows)),\n                                    optional(\"profiles\", profilesVal.map(o::profiles)),\n                                    optional(\"triggers\", triggersVal.map(o::triggers)),\n                                    optional(\"forms\", FormsGrammar.formsVal.map(o::forms)),\n                                    optional(\"imports\", ImportsGrammar.importsVal.map(o::imports)),\n                                    optional(\"resources\", ResourcesGrammar.resourcesVal.map(o::resources))))\n                            .map(ImmutableProcessDefinition.Builder::build));\n\n    private ProcessDefinitionGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/ProfilesGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.walmartlabs.concord.runtime.v2.model.ImmutableProfile;\nimport com.walmartlabs.concord.runtime.v2.model.Profile;\nimport io.takari.parc.Parser;\n\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.ConfigurationGrammar.processCfgVal;\nimport static com.walmartlabs.concord.runtime.v2.parser.FlowsGrammar.flowsVal;\nimport static com.walmartlabs.concord.runtime.v2.parser.FormsGrammar.formsVal;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.optional;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.options;\nimport static io.takari.parc.Combinators.many;\n\npublic final class ProfilesGrammar {\n\n    public static final Parser<Atom, Profile> profileDefinition =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    with(ImmutableProfile::builder,\n                            o -> options(\n                                    optional(\"configuration\", processCfgVal.map(o::configuration)),\n                                    optional(\"flows\", flowsVal.map(o::flows)),\n                                    optional(\"forms\", formsVal.map(o::forms))))\n                            .map(ImmutableProfile.Builder::build));\n\n    private static final Parser<Atom, KV<String, Profile>> profile =\n            satisfyAnyField(YamlValueType.PROFILE, f -> profileDefinition.map(s -> new KV<>(f.name, s)));\n\n    private static final Parser<Atom, Map<String, Profile>> profiles =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    many(profile).map(GrammarV2::toMap));\n\n    public static final Parser<Atom, Map<String, Profile>> profilesVal =\n            orError(profiles, YamlValueType.PROFILES);\n\n    private ProfilesGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/PublicFlowsGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.takari.parc.Parser;\n\nimport java.util.Set;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.orError;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.stringSetVal;\n\npublic class PublicFlowsGrammar {\n\n    public static final Parser<Atom, Set<String>> publicFlowsVal =\n        orError(stringSetVal, YamlValueType.PUBLIC_FLOWS);\n\n    private PublicFlowsGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/ResourcesGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.walmartlabs.concord.runtime.v2.model.ImmutableResources;\nimport com.walmartlabs.concord.runtime.v2.model.Resources;\nimport io.takari.parc.Parser;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.optional;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.options;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.stringArrayVal;\n\npublic final class ResourcesGrammar {\n\n    private static final Parser<Atom, Resources> resources =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    with(ImmutableResources::builder,\n                            o -> options(\n                                    optional(\"concord\", stringArrayVal.map(o::concord))))\n                            .map(ImmutableResources.Builder::build));\n\n    public static final Parser<Atom, Resources> resourcesVal =\n            orError(resources, YamlValueType.RESOURCES);\n\n    private ResourcesGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/RetryGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.walmartlabs.concord.runtime.v2.model.ImmutableRetry;\nimport com.walmartlabs.concord.runtime.v2.model.Retry;\nimport io.takari.parc.Parser;\n\nimport java.time.Duration;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.ExpressionGrammar.maybeExpression;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.optional;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.options;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.mapVal;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.maybeInt;\nimport static io.takari.parc.Combinators.or;\n\npublic final class RetryGrammar {\n\n    private static final Parser<Atom, Retry> retry =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    with(Retry::builder,\n                            o -> options(\n                                    optional(\"times\", orError(or(maybeInt.map(o::times), maybeExpression.map(o::timesExpression)), YamlValueType.RETRY_TIMES)),\n                                    optional(\"delay\", orError(or(maybeInt.map(i -> o.delay(Duration.ofSeconds(i))), maybeExpression.map(o::delayExpression)), YamlValueType.RETRY_DELAY)),\n                                    optional(\"in\", mapVal.map(o::input))\n                            ))\n                            .map(ImmutableRetry.Builder::build));\n\n    public static final Parser<Atom, Retry> retryVal =\n            orError(retry, YamlValueType.RETRY);\n\n    private RetryGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/ReturnGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.walmartlabs.concord.runtime.v2.model.ReturnStep;\nimport io.takari.parc.Parser;\n\nimport static io.takari.parc.Combinators.satisfy;\n\npublic final class ReturnGrammar {\n\n    public static final Parser<Atom, ReturnStep> returnStep =\n            satisfy((Atom a) -> a.token == JsonToken.VALUE_STRING && \"return\".equals(a.value))\n                    .map(a -> new ReturnStep(a.location));\n\n    private ReturnGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/ScriptGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.Constants;\nimport com.walmartlabs.concord.runtime.v2.model.ImmutableScriptCallOptions;\nimport com.walmartlabs.concord.runtime.v2.model.ScriptCall;\nimport com.walmartlabs.concord.runtime.v2.model.ScriptCallOptions;\nimport com.walmartlabs.concord.runtime.v2.model.WithItems;\nimport io.takari.parc.Parser;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.ExpressionGrammar.maybeExpression;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.optional;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.options;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.LoopGrammar.loopVal;\nimport static com.walmartlabs.concord.runtime.v2.parser.RetryGrammar.retryVal;\nimport static io.takari.parc.Combinators.or;\n\npublic final class ScriptGrammar {\n\n    private static Parser<Atom, ImmutableScriptCallOptions.Builder> scriptCallInOption(ImmutableScriptCallOptions.Builder o) {\n        return orError(or(maybeMap.map(o::input), maybeExpression.map(o::inputExpression)), YamlValueType.SCRIPT_CALL_IN);\n    }\n\n    private static Parser<Atom, ImmutableScriptCallOptions.Builder> scriptCallOutOption(ImmutableScriptCallOptions.Builder o) {\n        return orError(or(maybeMap.map(o::outExpr), maybeString.map(o::out)), YamlValueType.SCRIPT_CALL_OUT);\n    }\n\n    private static Parser<Atom, ScriptCallOptions> scriptOptions(String stepName) {\n        return with(() -> optionsBuilder(stepName),\n                o -> options(\n                        optional(\"body\", stringVal.map(o::body)),\n                        optional(\"in\", scriptCallInOption(o)),\n                        optional(\"out\", scriptCallOutOption(o)),\n                        optional(\"meta\", mapVal.map(o::putAllMeta)),\n                        optional(\"name\", stringVal.map(v -> o.putMeta(Constants.SEGMENT_NAME, v))),\n                        optional(\"withItems\", nonNullVal.map(v -> o.withItems(WithItems.of(v, WithItems.Mode.SERIAL)))),\n                        optional(\"parallelWithItems\", nonNullVal.map(v -> o.withItems(WithItems.of(v, WithItems.Mode.PARALLEL)))),\n                        optional(\"loop\", loopVal.map(o::loop)),\n                        optional(\"retry\", retryVal.map(o::retry)),\n                        optional(\"error\", stepsVal.map(o::errorSteps))\n                ))\n                .map(ImmutableScriptCallOptions.Builder::build);\n    }\n\n    private static ImmutableScriptCallOptions.Builder optionsBuilder(String stepName) {\n        ImmutableScriptCallOptions.Builder result = ImmutableScriptCallOptions.builder();\n        if (stepName != null) {\n            result.putMeta(Constants.SEGMENT_NAME, stepName);\n        }\n        return result;\n    }\n\n    public static final Parser<Atom, ScriptCall> script =\n            namedStep(\"script\", YamlValueType.SCRIPT, (stepName, a) ->\n                stringVal.bind(languageOrRef ->\n                        scriptOptions(stepName).map(options -> new ScriptCall(a.location, languageOrRef, options))));\n\n    private ScriptGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/SetVariablesGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.SetVariablesStep;\nimport io.takari.parc.Parser;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.satisfyField;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.simpleOptions;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.mapVal;\n\npublic final class SetVariablesGrammar {\n\n    public static final Parser<Atom, SetVariablesStep> setVars =\n            satisfyField(\"set\", YamlValueType.SET_VARS, a -> mapVal.bind(vars ->\n                    simpleOptions.map(options -> new SetVariablesStep(a.location, vars, options))));\n\n    private SetVariablesGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/SimpleOptions.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.serial.Serial;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\nimport java.util.Map;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@Serial.Version(-8269676324702677451L)\npublic interface SimpleOptions extends StepOptions {\n\n    long serialVersionUID = -8269676324702677451L;\n\n    static SimpleOptions of(Map<String, Serializable> meta) {\n        return ImmutableSimpleOptions.builder()\n                .meta(meta)\n                .build();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/StepOptions.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AllowNulls;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.Map;\n\npublic interface StepOptions extends Serializable {\n\n    @AllowNulls\n    @Value.Default\n    default Map<String, Serializable> meta() {\n        return Collections.emptyMap();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/SuspendGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.SuspendStep;\nimport io.takari.parc.Parser;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.satisfyField;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.simpleOptions;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.stringVal;\n\npublic final class SuspendGrammar {\n\n    public static final Parser<Atom, SuspendStep> suspendStep =\n            satisfyField(\"suspend\", YamlValueType.SUSPEND, a -> stringVal.bind(event ->\n                    simpleOptions.map(options -> new SuspendStep(a.location, event, options))));\n\n    private SuspendGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/TaskGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.Constants;\nimport com.walmartlabs.concord.runtime.v2.model.ImmutableTaskCallOptions;\nimport com.walmartlabs.concord.runtime.v2.model.TaskCall;\nimport com.walmartlabs.concord.runtime.v2.model.TaskCallOptions;\nimport com.walmartlabs.concord.runtime.v2.model.WithItems;\nimport io.takari.parc.Parser;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.ExpressionGrammar.maybeExpression;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.optional;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.options;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.LoopGrammar.loopVal;\nimport static com.walmartlabs.concord.runtime.v2.parser.RetryGrammar.retryVal;\nimport static io.takari.parc.Combinators.or;\n\npublic final class TaskGrammar {\n\n    public static ImmutableTaskCallOptions.Builder optionsWithStepName(String stepName) {\n        ImmutableTaskCallOptions.Builder result = ImmutableTaskCallOptions.builder();\n        if (stepName != null) {\n            result.putMeta(Constants.SEGMENT_NAME, stepName);\n        }\n        return result;\n    }\n\n    private static Parser<Atom, ImmutableTaskCallOptions.Builder> taskCallInOption(ImmutableTaskCallOptions.Builder o) {\n        return orError(or(maybeMap.map(o::input), maybeExpression.map(o::inputExpression)), YamlValueType.TASK_CALL_IN);\n    }\n\n    private static Parser<Atom, ImmutableTaskCallOptions.Builder> taskCallOutOption(ImmutableTaskCallOptions.Builder o) {\n        return orError(or(maybeMap.map(o::outExpr), maybeString.map(o::out)), YamlValueType.TASK_CALL_OUT);\n    }\n\n    private static Parser<Atom, TaskCallOptions> taskOptions(String stepName) {\n        return with(() -> optionsWithStepName(stepName),\n                o -> options(\n                        optional(\"in\", taskCallInOption(o)),\n                        optional(\"out\", taskCallOutOption(o)),\n                        optional(\"meta\", mapVal.map(o::putAllMeta)),\n                        optional(\"name\", stringVal.map(v -> o.putMeta(Constants.SEGMENT_NAME, v))),\n                        optional(\"withItems\", nonNullVal.map(v -> o.withItems(WithItems.of(v, WithItems.Mode.SERIAL)))),\n                        optional(\"parallelWithItems\", nonNullVal.map(v -> o.withItems(WithItems.of(v, WithItems.Mode.PARALLEL)))),\n                        optional(\"loop\", loopVal.map(o::loop)),\n                        optional(\"retry\", retryVal.map(o::retry)),\n                        optional(\"error\", stepsVal.map(o::errorSteps)),\n                        optional(\"ignoreErrors\", booleanVal.map(o::ignoreErrors))\n                ))\n                .map(ImmutableTaskCallOptions.Builder::build);\n    }\n\n    public static final Parser<Atom, TaskCall> taskFull =\n            namedStep(\"task\", YamlValueType.TASK, (stepName, a) ->\n                    stringVal.bind(taskName ->\n                            taskOptions(stepName).map(options -> new TaskCall(a.location, taskName, options))));\n\n    private TaskGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/ThreadLocalFileName.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.concurrent.Callable;\n\npublic final class ThreadLocalFileName {\n\n    public static <T> T withFileName(String fileName, Callable<T> callable) throws RuntimeException {\n        set(fileName);\n        try {\n            return callable.call();\n        } catch (RuntimeException e) {\n            throw e;\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        } finally {\n            clear();\n        }\n    }\n\n    private static final ThreadLocal<String> value = new ThreadLocal<>();\n\n    public static String get() {\n        return value.get();\n    }\n\n    private static void set(String ctx) {\n        value.set(ctx);\n    }\n\n    private static void clear() {\n        value.remove();\n    }\n\n    private ThreadLocalFileName() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/ThrowGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.TaskCall;\nimport io.takari.parc.Parser;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.namedStep;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.namedOptions;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.anyVal;\nimport static com.walmartlabs.concord.runtime.v2.parser.TaskGrammar.optionsWithStepName;\n\npublic final class ThrowGrammar {\n\n    public static final Parser<Atom, TaskCall> throwStep =\n            namedStep(\"throw\", YamlValueType.TASK, (stepName, a) -> anyVal.bind(e ->\n                    namedOptions.map(options ->\n                            new TaskCall(a.location, \"throw\", optionsWithStepName(stepName)\n                                    .putInput(\"exception\", e)\n                                    .build()))));\n\n    private ThrowGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/TriggersGrammar.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonToken;\nimport com.walmartlabs.concord.runtime.v2.exception.OneOfMandatoryFieldsNotFoundException;\nimport com.walmartlabs.concord.runtime.v2.exception.UnsupportedException;\nimport com.walmartlabs.concord.runtime.v2.model.*;\nimport io.takari.parc.Parser;\nimport io.takari.parc.Seq;\n\nimport java.util.*;\nimport java.util.function.Supplier;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.ConfigurationGrammar.exclusiveVal;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarLookup.lookup;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarMisc.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarOptions.*;\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.*;\nimport static io.takari.parc.Combinators.choice;\nimport static io.takari.parc.Combinators.many1;\n\npublic final class TriggersGrammar {\n\n    private static final Parser<Atom, Map<String, Object>> githubTriggerRepositoryInfoItem =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    with((Supplier<HashMap<String, Object>>) LinkedHashMap::new,\n                            o -> options(\n                                    optional(\"repositoryId\", regexpVal.map(v -> o.put(\"repositoryId\", v))),\n                                    optional(\"repository\", regexpVal.map(v -> o.put(\"repository\", v))),\n                                    optional(\"projectId\", regexpVal.map(v -> o.put(\"projectId\", v))),\n                                    optional(\"branch\", regexpVal.map(v -> o.put(\"branch\", v))),\n                                    optional(\"enabled\", booleanVal.map(v -> o.put(\"enabled\", v)))))\n                        .map(Collections::unmodifiableMap));\n\n    private static final Parser<Atom, Map<String, Object>> githubTriggerRepositoryInfoItemVal =\n            orError(githubTriggerRepositoryInfoItem, YamlValueType.GITHUB_REPOSITORY_INFO);\n\n    private static final Parser<Atom, List<Map<String, Object>>> githubTriggerRepositoryInfo =\n            betweenTokens(JsonToken.START_ARRAY, JsonToken.END_ARRAY, many1(githubTriggerRepositoryInfoItemVal).map(Seq::toList));\n\n    private static final Parser<Atom, List<Map<String, Object>>> githubTriggerRepositoryInfoVal =\n            orError(githubTriggerRepositoryInfo, YamlValueType.ARRAY_OF_GITHUB_REPOSITORY_INFO);\n\n    private static GithubTriggerExclusiveMode validateGithubExclusiveMode(GithubTriggerExclusiveMode e) {\n        if (e.groupByProperty() == null && e.group() == null) {\n            throw new OneOfMandatoryFieldsNotFoundException(\"group\", \"groupBy\");\n        }\n        return e;\n    }\n\n    private static final Parser<Atom, GithubTriggerExclusiveMode> githubTriggerExclusiveV2 =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    with(ImmutableGithubTriggerExclusiveMode::builder,\n                            o -> options(\n                                    optional(\"group\", stringVal.map(o::group)),\n                                    optional(\"groupBy\", stringVal.map(o::groupByProperty)),\n                                    optional(\"mode\", enumVal(ExclusiveMode.Mode.class).map(o::mode))))\n                            .map(ImmutableGithubTriggerExclusiveMode.Builder::build)\n                            .map(TriggersGrammar::validateGithubExclusiveMode));\n\n    private static final Parser<Atom, GithubTriggerExclusiveMode> githubTriggerExclusiveValV2 =\n            orError(githubTriggerExclusiveV2, YamlValueType.GITHUB_EXCLUSIVE_MODE);\n\n    private static final Parser<Atom, Map<String, Object>> githubTriggerFilesVal =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                with((Supplier<HashMap<String, Object>>) LinkedHashMap::new,\n                        o -> options(\n                            optional(\"added\", regexpOrArrayVal.map(v -> o.put(\"added\", v))),\n                            optional(\"removed\", regexpOrArrayVal.map(v -> o.put(\"removed\", v))),\n                            optional(\"modified\", regexpOrArrayVal.map(v -> o.put(\"modified\", v))),\n                            optional(\"any\", regexpOrArrayVal.map(v -> o.put(\"any\", v)))))\n                        .map(Collections::unmodifiableMap));\n\n    private static final Parser<Atom, Map<String, Object>> githubTriggerConditionsV2 =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    with((Supplier<HashMap<String, Object>>) LinkedHashMap::new,\n                            o -> options(\n                                mandatory(\"type\", stringVal.map(v -> o.put(\"type\", v))),\n                                optional(\"githubOrg\", regexpVal.map(v -> o.put(\"githubOrg\", v))),\n                                optional(\"githubRepo\", regexpVal.map(v -> o.put(\"githubRepo\", v))),\n                                optional(\"githubHost\", regexpVal.map(v -> o.put(\"githubHost\", v))),\n                                optional(\"branch\", regexpVal.map(v -> o.put(\"branch\", v))),\n                                optional(\"sender\", regexpVal.map(v -> o.put(\"sender\", v))),\n                                optional(\"status\", regexpVal.map(v -> o.put(\"status\", v))),\n                                optional(\"repositoryInfo\", githubTriggerRepositoryInfoVal.map(v -> o.put(\"repositoryInfo\", v))),\n                                optional(\"files\", githubTriggerFilesVal.map(v -> o.put(\"files\", v))),\n                                optional(\"payload\", mapVal.map(v -> o.put(\"payload\", v)))))\n                            .map(Collections::unmodifiableMap));\n\n    private static final Parser<Atom, Map<String, Object>> githubTriggerConditionsValV2 =\n            orError(githubTriggerConditionsV2, YamlValueType.GITHUB_TRIGGER_CONDITIONS);\n\n    private static final Parser<Atom, Trigger> githubTriggerV1 = in -> {\n        throw new UnsupportedException(\"Version 1 of GitHub triggers is not supported\");\n    };\n\n    private static final Parser<Atom, Trigger> githubTriggerV2 =\n            with(ImmutableTrigger::builder,\n                    o -> options(\n                            optional(\"useInitiator\", booleanVal.map(v -> o.putConfiguration(\"useInitiator\", v))),\n                            mandatory(\"entryPoint\", stringVal.map(v -> o.putConfiguration(\"entryPoint\", v))),\n                            optional(\"activeProfiles\", stringArrayVal.map(o::activeProfiles)),\n                            optional(\"useEventCommitId\", booleanVal.map(v -> o.putConfiguration(\"useEventCommitId\", v))),\n                            optional(\"ignoreEmptyPush\", booleanVal.map(v -> o.putConfiguration(\"ignoreEmptyPush\", v))),\n                            optional(\"arguments\", mapVal.map(o::arguments)),\n                            optional(\"exclusive\", githubTriggerExclusiveValV2.map(v -> o.putConfiguration(\"exclusive\", v))),\n                            mandatory(\"conditions\", githubTriggerConditionsValV2.map(o::putAllConditions)),\n                            mandatory(\"version\", intVal.map(v -> o.putConditions(\"version\", v)))))\n                    .map(t -> t.name(\"github\"))\n                    .map(ImmutableTrigger.Builder::build);\n\n    private static final Parser<Atom, Trigger> githubTrigger =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    lookup(\"version\", YamlValueType.INT, 2, githubTriggerV2, githubTriggerV1));\n\n    private static final Parser<Atom, Trigger> githubTriggerVal =\n            orError(githubTrigger, YamlValueType.GITHUB_TRIGGER);\n\n\n    private static final Parser<Atom, Map<String, Object>> runAs =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    with((Supplier<HashMap<String, Object>>) LinkedHashMap::new,\n                            o -> options(\n                                    mandatory(\"withSecret\", stringNotEmptyVal.map(v -> o.put(\"withSecret\", v)))))\n                            .map(Collections::unmodifiableMap));\n\n    private static final Parser<Atom, Map<String, Object>> runAsVal =\n            orError(runAs, YamlValueType.RUN_AS);\n\n    private static final Parser<Atom, Trigger> cronTrigger =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                with(ImmutableTrigger::builder,\n                        o -> options(\n                                mandatory(\"spec\", stringVal.map(v -> o.putConditions(\"spec\", v))),\n                                mandatory(\"entryPoint\", stringVal.map(v -> o.putConfiguration(\"entryPoint\", v))),\n                                optional(\"runAs\", runAsVal.map(v -> o.putConfiguration(\"runAs\", v))),\n                                optional(\"activeProfiles\", stringArrayVal.map(o::activeProfiles)),\n                                optional(\"timezone\", timezoneVal.map(v -> o.putConditions(\"timezone\", v))),\n                                optional(\"arguments\", mapVal.map(o::arguments)),\n                                optional(\"exclusive\", exclusiveVal.map(v -> o.putConfiguration(\"exclusive\", v))))))\n                        .map(t -> t.name(\"cron\"))\n                        .map(ImmutableTrigger.Builder::build);\n\n    private static final Parser<Atom, Trigger> cronTriggerVal =\n            orError(cronTrigger, YamlValueType.CRON_TRIGGER);\n\n    private static final Parser<Atom, Trigger> manualTrigger =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    with(ImmutableTrigger::builder,\n                            o -> options(\n                                    optional(\"name\", stringVal.map(v -> o.putConfiguration(\"name\", v))),\n                                    mandatory(\"entryPoint\", stringVal.map(v -> o.putConfiguration(\"entryPoint\", v))),\n                                    optional(\"activeProfiles\", stringArrayVal.map(o::activeProfiles)),\n                                    optional(\"arguments\", mapVal.map(o::arguments)),\n                                    optional(\"exclusive\", exclusiveVal.map(v -> o.putConfiguration(\"exclusive\", v))))))\n                    .map(t -> t.name(\"manual\"))\n                    .map(ImmutableTrigger.Builder::build);\n\n    private static final Parser<Atom, Trigger> manualTriggerVal =\n            orError(manualTrigger, YamlValueType.MANUAL_TRIGGER);\n\n    private static final Parser<Atom, Trigger> oneopsTriggerV1 = in -> {\n        throw new UnsupportedException(\"Version 1 of oneops trigger not supported\");\n    };\n\n    private static final Parser<Atom, Trigger> oneopsTriggerV2 =\n            with(ImmutableTrigger::builder,\n                    o -> options(\n                            optional(\"useInitiator\", booleanVal.map(v -> o.putConfiguration(\"useInitiator\", v))),\n                            mandatory(\"entryPoint\", stringVal.map(v -> o.putConfiguration(\"entryPoint\", v))),\n                            optional(\"activeProfiles\", stringArrayVal.map(o::activeProfiles)),\n                            optional(\"arguments\", mapVal.map(o::arguments)),\n                            optional(\"exclusive\", exclusiveVal.map(v -> o.putConfiguration(\"exclusive\", v))),\n                            mandatory(\"conditions\", mapVal.map(o::putAllConditions)),\n                            mandatory(\"version\", intVal.map(v -> o.putConditions(\"version\", v)))))\n                    .map(t -> t.name(\"oneops\"))\n                    .map(ImmutableTrigger.Builder::build);\n\n    private static final Parser<Atom, Trigger> oneopsTrigger =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    lookup(\"version\", YamlValueType.INT, 2, oneopsTriggerV2, oneopsTriggerV1));\n\n    private static final Parser<Atom, Trigger> oneopsTriggerVal =\n            orError(oneopsTrigger, YamlValueType.ONEOPS_TRIGGER);\n\n    private static final Parser<Atom, Trigger> genericTriggerV1 = in -> {\n        throw new UnsupportedException(\"Version 1 of generic trigger not supported\");\n    };\n\n    private static Parser<Atom, Trigger> genericTriggerV2(String triggerName) {\n        return with(ImmutableTrigger::builder,\n                o -> options(\n                        mandatory(\"entryPoint\", stringVal.map(v -> o.putConfiguration(\"entryPoint\", v))),\n                        optional(\"activeProfiles\", stringArrayVal.map(o::activeProfiles)),\n                        optional(\"arguments\", mapVal.map(o::arguments)),\n                        optional(\"exclusive\", exclusiveVal.map(v -> o.putConfiguration(\"exclusive\", v))),\n                        mandatory(\"conditions\", mapVal.map(o::conditions)),\n                        mandatory(\"version\", intVal.map(v -> o.putConfiguration(\"version\", v)))))\n                .map(t -> t.name(triggerName))\n                .map(ImmutableTrigger.Builder::build);\n    }\n\n    private static Parser<Atom, Trigger> genericTrigger(String triggerName)  {\n        return betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                lookup(\"version\", YamlValueType.INT, 2, genericTriggerV2(triggerName), genericTriggerV1));\n    }\n\n    private static Parser<Atom, Trigger> genericTriggerVal(String triggerName) {\n        return orError(genericTrigger(triggerName), YamlValueType.GENERIC_TRIGGER);\n    }\n\n    private static final Parser<Atom, Trigger> triggerDef =\n            betweenTokens(JsonToken.START_OBJECT, JsonToken.END_OBJECT,\n                    choice(\n                            satisfyField(\"github\", atom -> githubTriggerVal.map(t -> addLocation(t, atom))),\n                            satisfyField(\"cron\", atom -> cronTriggerVal.map(t -> addLocation(t, atom))),\n                            satisfyField(\"manual\", atom -> manualTriggerVal.map(t -> addLocation(t, atom))),\n                            satisfyField(\"oneops\", atom -> oneopsTriggerVal.map(t -> addLocation(t, atom))),\n                            satisfyAnyField(YamlValueType.GENERIC_TRIGGER, atom -> genericTriggerVal(atom.name).map(t -> addLocation(t, atom)))));\n\n    private static Trigger addLocation(Trigger t, Atom atom) {\n        return Trigger.builder().from(t)\n                .location(atom.location)\n                .build();\n    }\n\n    private static final Parser<Atom, Trigger> triggerVal =\n            orError(triggerDef, YamlValueType.TRIGGER);\n\n    private static final Parser<Atom, List<Trigger>> triggers =\n            betweenTokens(JsonToken.START_ARRAY, JsonToken.END_ARRAY,\n                    many1(triggerVal).map(Seq::toList));\n\n    public static final Parser<Atom, List<Trigger>> triggersVal =\n            orError(triggers, YamlValueType.TRIGGERS);\n\n    private TriggersGrammar() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/UnknownOption.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface UnknownOption {\n\n    @Value.Parameter\n    String key();\n\n    @Nullable\n    @Value.Parameter\n    YamlValueType type();\n\n    @Value.Parameter\n    Location location();\n\n    static UnknownOption of(String key, YamlValueType type, Location location) {\n        return ImmutableUnknownOption.of(key, type, location);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/YamlDeserializersV2.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.JsonDeserializer;\nimport com.fasterxml.jackson.databind.deser.std.StdDeserializer;\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.exception.YamlProcessingException;\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport io.takari.parc.Input;\nimport io.takari.parc.Parser;\nimport io.takari.parc.Result;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic final class YamlDeserializersV2 {\n\n    private static final JsonDeserializer<ProcessDefinition> processDefinitionDeserializer = new ProcessDefinitionDeserializer();\n\n    public static JsonDeserializer<ProcessDefinition> getProcessDefinitionDeserializer() {\n        return processDefinitionDeserializer;\n    }\n\n    private static final class ProcessDefinitionDeserializer extends StdDeserializer<ProcessDefinition> {\n\n        private static final long serialVersionUID = 1L;\n\n        protected ProcessDefinitionDeserializer() {\n            super(Step.class);\n        }\n\n        @Override\n        public ProcessDefinition deserialize(JsonParser json, DeserializationContext ctx) throws IOException {\n            return parse(json, ProcessDefinitionGrammar.processDefinition);\n        }\n    }\n\n    private static <T> T parse(JsonParser json, Parser<Atom, T> parser) throws IOException {\n        List<Atom> atoms = asSubtree(json);\n\n        Input<Atom> in = new ListInput<>(atoms);\n        Result<Atom, T> result = parser.parse(in);\n\n        if (result.isFailure()) {\n            Result.Failure<Atom, ?> f = result.toFailure();\n            throw toException(f, json, atoms);\n        }\n\n        return result.toSuccess().getResult();\n    }\n\n    private static List<Atom> asSubtree(JsonParser p) throws IOException {\n        int level = 0;\n\n        List<Atom> l = new ArrayList<>();\n        while (p.currentToken() != null) {\n            l.add(Atom.current(p));\n\n            switch (p.currentToken()) {\n                case START_OBJECT:\n                case START_ARRAY:\n                    level += 1;\n                    break;\n                case END_OBJECT:\n                case END_ARRAY:\n                    level -= 1;\n                    break;\n            }\n\n            if (level <= 0) {\n                break;\n            }\n\n            p.nextToken();\n        }\n\n        return l;\n    }\n\n    private static YamlProcessingException toException(Result.Failure<Atom, ?> f, JsonParser p, List<Atom> atoms) {\n        Location loc = null;\n        String got = \"n/a\";\n\n        int pos = f.getPosition();\n        if (pos >= 0) {\n            Atom a = atoms.get(f.getPosition());\n            loc = a.location;\n            got = a.name;\n        }\n\n        return new YamlProcessingException(loc, \"Expected: \" + f.getMessage() + \". Got '\" + got + \"'\");\n    }\n\n    private YamlDeserializersV2() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/YamlList.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.exception.InvalidValueTypeException;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\npublic class YamlList extends YamlValue {\n\n    private static final long serialVersionUID = 1L;\n\n    private final List<YamlValue> values;\n\n    public YamlList(List<YamlValue> values, Location location) {\n        super(null, YamlValueType.ARRAY, location);\n        this.values = values;\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public <T> T getValue() {\n        return (T) values.stream()\n                .map(YamlValue::getValue)\n                .collect(Collectors.toList());\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public <T> T getValue(YamlValueType<T> expectedType) throws InvalidValueTypeException {\n        assertType(expectedType);\n        return (T) values.stream()\n                .map(YamlValue::getValue)\n                .collect(Collectors.toList());\n    }\n\n    public <T> List<T> getListValue(YamlValueType<T> itemExpectedType) {\n        return values.stream()\n                .map(v -> v.getValue(itemExpectedType))\n                .collect(Collectors.toList());\n    }\n\n    public <T> List<T> getListValue(ValueConverter<T> converter) {\n        return values.stream()\n                .map(converter::convert)\n                .collect(Collectors.toList());\n    }\n\n    interface ValueConverter<T> {\n\n        T convert(YamlValue value);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/YamlObject.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\n\nimport java.io.Serializable;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.runtime.v2.parser.GrammarV2.assertNotNull;\n\npublic class YamlObject extends YamlValue {\n\n    private static final long serialVersionUID = 1L;\n\n    protected final Map<String, YamlValue> values;\n\n    public YamlObject(Map<String, YamlValue> values, Location location) {\n        super(null, YamlValueType.OBJECT, location);\n        this.values = values;\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public Map<String, Serializable> getValue() {\n        Map<String, Serializable> result = new LinkedHashMap<>();\n        for (Map.Entry<String, YamlValue> e : values.entrySet()) {\n            result.put(e.getKey(), assertNotNull(e.getValue()).getValue());\n        }\n        return result;\n    }\n\n    public Serializable remove(String name) {\n        YamlValue v = values.remove(name);\n        if (v == null) {\n            return null;\n        }\n        return v.getValue();\n    }\n\n    public YamlValue getYamlValue(String name) {\n        return values.get(name);\n    }\n\n    public <T> T getValue(String name, YamlValueType<T> type) {\n        YamlValue v = values.get(name);\n        if (v == null) {\n            return null;\n        }\n        return v.getValue(type);\n    }\n\n    public boolean isEmpty() {\n        return values.isEmpty();\n    }\n\n    public Map<String, YamlValue> getValues() {\n        return values;\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlObject{\" +\n                \"values=\" + values +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/YamlObjectConverter.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\n\nimport java.util.*;\n\npublic final class YamlObjectConverter {\n\n    public static YamlObject from(Map<String, Object> value, Location location) {\n        Map<String, YamlValue> values = new LinkedHashMap<>();\n\n        for (Map.Entry<String, Object> e : value.entrySet()) {\n            YamlValue v = fromObject(e.getValue(), location);\n            if (v != null) {\n                values.put(e.getKey(), v);\n            }\n        }\n\n        return new YamlObject(values, location);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static YamlValue fromObject(Object value, Location location) {\n        if (value == null) {\n            return null;\n        }\n\n        if (value instanceof String) {\n            return new YamlValue((String)value, YamlValueType.STRING, location);\n        } else if (value instanceof Integer) {\n            return new YamlValue((Integer) value, YamlValueType.INT, location);\n        } else if (value instanceof Float) {\n            return new YamlValue((Float)value, YamlValueType.FLOAT, location);\n        } else if (value instanceof Number) {\n            return new YamlValue(((Number) value).intValue(), YamlValueType.INT, location);\n        } else if (value instanceof Boolean) {\n            return new YamlValue((Boolean)value, YamlValueType.BOOLEAN, location);\n        } else if (value instanceof Collection) {\n            return fromCollection((Collection<Object>)value, location);\n        } else if (value instanceof Map) {\n            return from((Map<String, Object>)value, location);\n        } else if (value.getClass().isArray()) {\n            return fromCollection(new ArrayList<>(Arrays.asList((Object[]) value)), location);\n        }\n\n        return null;\n    }\n\n    private static YamlList fromCollection(Collection<Object> rawValues, Location location) {\n        List<YamlValue> values = new ArrayList<>();\n        for (Object v : rawValues) {\n            YamlValue value = fromObject(v, location);\n            if (value != null) {\n                values.add(value);\n            }\n        }\n        return new YamlList(values, location);\n    }\n\n    private YamlObjectConverter() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/YamlParserV2.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonLocation;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.MapperFeature;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.module.SimpleModule;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLFactory;\nimport com.fasterxml.jackson.datatype.guava.GuavaModule;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.exception.InvalidFieldDefinitionException;\nimport com.walmartlabs.concord.runtime.v2.exception.YamlParserException;\nimport com.walmartlabs.concord.runtime.v2.exception.YamlProcessingException;\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\npublic class YamlParserV2 {\n\n    private final ObjectMapper objectMapper;\n\n    public YamlParserV2() {\n        ObjectMapper om = new ObjectMapper(new YAMLFactory()\n                .enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION))\n                .disable(MapperFeature.USE_ANNOTATIONS);\n\n        SimpleModule module = new SimpleModule();\n        module.addDeserializer(ProcessDefinition.class, YamlDeserializersV2.getProcessDefinitionDeserializer());\n\n        om.registerModule(module);\n        om.registerModule(new GuavaModule());\n        om.registerModule(new Jdk8Module());\n\n        this.objectMapper = om;\n    }\n\n    public ProcessDefinition parse(Path baseDir, Path file) throws IOException {\n        String fileName = baseDir.relativize(file).toString();\n        try {\n            return ThreadLocalFileName.withFileName(fileName,\n                    () -> objectMapper.readValue(file.toFile(), ProcessDefinition.class));\n        } catch (YamlProcessingException e) {\n            throw new YamlParserException(buildErrorMessage(fileName, e));\n        } catch (Exception e) {\n            if (e.getCause() instanceof JsonProcessingException) {\n                JsonProcessingException jpe = (JsonProcessingException) e.getCause();\n                throw toErr(\"(\" + fileName + \"): Error\", jpe);\n            }\n            throw new YamlParserException(\"Error while loading a project file '\" + baseDir.relativize(file) +\"', \" + e.getMessage());\n        }\n    }\n\n    private static YamlParserException toErr(String msg, JsonProcessingException jpe) {\n        String loc = toShortString(jpe.getLocation());\n        String originalMsg = jpe.getOriginalMessage();\n        return new YamlParserException(msg + \" @ \" + loc + \". \" + originalMsg);\n    }\n\n    private static String buildErrorMessage(String fileName, YamlProcessingException e) {\n        String prefix = \"(\" + fileName + \"): Error\";\n\n        List<YamlProcessingException> errors = getYamlProcessingExceptionList(e);\n        Collections.reverse(errors);\n\n        String padding = \"\\t\";\n        StringBuilder result = new StringBuilder(toMessage(prefix, errors.remove(0)));\n\n        List<InvalidFieldDefinitionException> stepErrors = errors.stream().filter(err -> err instanceof InvalidFieldDefinitionException)\n                .map(err -> (InvalidFieldDefinitionException) err).collect(Collectors.toList());\n        if (stepErrors.size() > 0) {\n            result.append(\"\\n\\t\").append(\"while processing steps:\");\n        }\n        for (InvalidFieldDefinitionException err : stepErrors) {\n            result.append(\"\\n\").append(padding)\n                    .append(\"'\").append(err.getFieldName()).append(\"'\")\n                    .append(\" @ \").append(Location.toShortString(err.getLocation()));\n            padding += \"\\t\";\n        }\n        return result.toString();\n    }\n\n    private static List<YamlProcessingException> getYamlProcessingExceptionList(YamlProcessingException e) {\n        List<YamlProcessingException> list = new ArrayList<>();\n        while (e != null && !list.contains(e)) {\n            list.add(e);\n            Throwable cause = e.getCause();\n            if (cause instanceof YamlProcessingException) {\n                e = (YamlProcessingException) cause;\n            } else {\n                e = null;\n            }\n        }\n        return list;\n    }\n\n    private static String toMessage(String prefix, YamlProcessingException e) {\n        return prefix + \" @ \" + Location.toShortString(e.getLocation()) + \". \" + e.getMessage();\n    }\n\n    public static String toShortString(JsonLocation location) {\n        if (location == null) {\n            return \"n/a\";\n        }\n        return \"line: \" + location.getLineNr() + \", col: \" + location.getColumnNr();\n    }\n\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/YamlValue.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.exception.InvalidValueTypeException;\n\nimport java.io.Serializable;\n\npublic class YamlValue implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Object value;\n\n    private final YamlValueType<?> type;\n\n    private final Location location;\n\n    public <T> YamlValue(T value, YamlValueType<T> type, Location location) {\n        this.value = value;\n        this.type = type;\n        this.location = location;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public <T> T getValue() {\n        return (T) value;\n    }\n\n    public <T> T getValue(YamlValueType<T> expectedType) throws InvalidValueTypeException {\n        assertType(expectedType);\n        return getValue();\n    }\n\n    public YamlValueType<?> getType() {\n        return type;\n    }\n\n    public Location getLocation() {\n        return location;\n    }\n\n    protected <T> void assertType(YamlValueType<T> expectedType) throws InvalidValueTypeException {\n        if (expectedType != type) {\n            throw InvalidValueTypeException.builder()\n                    .expected(expectedType)\n                    .actual(type)\n                    .location(location)\n                    .build();\n        }\n    }\n\n    @Override\n    public String toString() {\n        return \"YamlValue{\" +\n                \"value=\" + value +\n                \", type=\" + type +\n                \", location=\" + location +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/parser/YamlValueType.java",
    "content": "package com.walmartlabs.concord.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Import;\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.runtime.v2.model.*;\nimport com.walmartlabs.concord.runtime.v2.model.TaskCallValidation.ValidationMode;\n\nimport java.io.Serializable;\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\npublic final class YamlValueType<T> {\n\n    public static final YamlValueType<Object> NON_NULL = type(\"NON_NULL\");\n\n    public static final YamlValueType<Integer> INT = type(\"INT\");\n    public static final YamlValueType<Float> FLOAT = type(\"FLOAT\");\n    public static final YamlValueType<Boolean> BOOLEAN = type(\"BOOLEAN\");\n    public static final YamlValueType<String> STRING = type(\"STRING\");\n    public static final YamlValueType<String> NON_EMPTY_STRING = type(\"NON_EMPTY_STRING\");\n    public static final YamlValueType<String> PATTERN = type(\"PATTERN\");\n    public static final YamlValueType<Object> NULL = type(\"NULL\");\n    public static final YamlValueType<List<Serializable>> ARRAY = array(\"ARRAY\", null);\n    public static final YamlValueType<List<String>> ARRAY_OF_PATTERN = array(\"ARRAY_OF_PATTERN\", PATTERN);\n    public static final YamlValueType<List<String>> ARRAY_OF_STRING = array(\"ARRAY_OF_STRING\", STRING);\n    public static final YamlValueType<Map<String, Serializable>> OBJECT = map(\"OBJECT\");\n    public static final YamlValueType<Object> REGEXP_OR_ARRAY = type(\"REGEXP_OR_ARRAY\");\n    public static final YamlValueType<Duration> DURATION = type(\"ISO 8601 DURATION\");\n    public static final YamlValueType<String> TIMEZONE = type(\"TIMEZONE\");\n    public static final YamlValueType<Object> STRING_OR_ARRAY = type(\"STRING_OR_ARRAY\");\n    public static final YamlValueType<TaskCall> TASK = type(\"TASK\");\n    public static final YamlValueType<SetVariablesStep> SET_VARS = type(\"SET_VARIABLES\");\n    public static final YamlValueType<SuspendStep> SUSPEND = type(\"SUSPEND\");\n    public static final YamlValueType<ScriptCall> SCRIPT = type(\"SCRIPT\");\n    public static final YamlValueType<ImmutableScriptCallOptions.Builder> SCRIPT_CALL_IN = type(\"OBJECT or EXPRESSION\");\n    public static final YamlValueType<ImmutableScriptCallOptions.Builder> SCRIPT_CALL_OUT = type(\"STRING or OBJECT\");\n    public static final YamlValueType<Expression> EXPRESSION = type(\"EXPRESSION\");\n    public static final YamlValueType<String> EXPRESSION_VAL = type(\"EXPRESSION\");\n    public static final YamlValueType<Retry> RETRY = type(\"RETRY\");\n    public static final YamlValueType<Checkpoint> CHECKPOINT = type(\"CHECKPOINT\");\n    public static final YamlValueType<Step> STEP = type(\"STEP\");\n    public static final YamlValueType<List<Step>> ARRAY_OF_STEP = array(\"ARRAY_OF_STEP\", STEP);\n    public static final YamlValueType<GroupOfSteps> TRY = type(\"TRY\");\n    public static final YamlValueType<GroupOfSteps> BLOCK = type(\"BLOCK\");\n    public static final YamlValueType<ParallelBlock> PARALLEL = type(\"PARALLEL\");\n    public static final YamlValueType<Map<String, Form>> FORMS = type(\"FORMS\");\n    public static final YamlValueType<Form> FORM = type(\"FORM\");\n    public static final YamlValueType<FormField> FORM_FIELD = type(\"FORM_FIELD\");\n    public static final YamlValueType<List<FormField>> ARRAY_OF_FORM_FIELD = array(\"ARRAY_OF_FORM_FIELD\", FORM_FIELD);\n    public static final YamlValueType<FormCall> FORM_CALL = type(\"FORM_CALL\");\n    public static final YamlValueType<ImmutableFormCallOptions.Builder> FORM_CALL_FIELDS = type(\"ARRAY_OF_FORM_FIELD or EXPRESSION\");\n    public static final YamlValueType<ImmutableFormCallOptions.Builder> FORM_CALL_RUN_AS = type(\"OBJECT or EXPRESSION\");\n    public static final YamlValueType<ImmutableFormCallOptions.Builder> FORM_CALL_VALUES = type(\"OBJECT or EXPRESSION\");\n    public static final YamlValueType<ImmutableRetry.Builder> RETRY_TIMES = type(\"INT or EXPRESSION\");\n    public static final YamlValueType<ImmutableRetry.Builder> RETRY_DELAY = type(\"INT or EXPRESSION\");\n    public static final YamlValueType<Imports> IMPORTS = type(\"IMPORTS\");\n    public static final YamlValueType<Import> IMPORT = type(\"IMPORT\");\n    public static final YamlValueType<Import.GitDefinition> GIT_IMPORT = type(\"GIT_IMPORT\");\n    public static final YamlValueType<Import.MvnDefinition> MVN_IMPORT = type(\"MVN_IMPORT\");\n    public static final YamlValueType<Import.DirectoryDefinition> DIR_IMPORT = type(\"DIR_IMPORT\");\n    public static final YamlValueType<Import.SecretDefinition> IMPORT_SECRET = type(\"IMPORT_SECRET\");\n    public static final YamlValueType<Map<String, Flow>> FLOWS = type(\"FLOWS\");\n    public static final YamlValueType<KV<String, Flow>> FLOW = type(\"FLOW\");\n    public static final YamlValueType<FlowCall> FLOW_CALL = type(\"FLOW_CALL\");\n    public static final YamlValueType<ImmutableFlowCallOptions.Builder> FLOW_CALL_INPUT = type(\"OBJECT or EXPRESSION\");\n    public static final YamlValueType<ImmutableFlowCallOptions.Builder> FLOW_CALL_OUT = type(\"STRING or ARRAY_OF_STRING or OBJECT\");\n    public static final YamlValueType<Set<String>> PUBLIC_FLOWS = type(\"PUBLIC_FLOWS\");\n    public static final YamlValueType<Map<String, Profile>> PROFILES = type(\"PROFILES\");\n    public static final YamlValueType<KV<String, Profile>> PROFILE = type(\"PROFILE\");\n    public static final YamlValueType<ProcessDefinitionConfiguration> PROCESS_CFG = type(\"CONFIGURATION\");\n    public static final YamlValueType<Trigger> TRIGGER = type(\"TRIGGER\");\n    public static final YamlValueType<List<Trigger>> TRIGGERS = array(\"TRIGGER\", TRIGGER);\n    public static final YamlValueType<Trigger> GITHUB_TRIGGER = type(\"GITHUB_TRIGGER\");\n    public static final YamlValueType<Map<String, Object>> GITHUB_TRIGGER_CONDITIONS = type(\"GITHUB_TRIGGER_CONDITIONS\");\n    public static final YamlValueType<Trigger> CRON_TRIGGER = type(\"CRON_TRIGGER\");\n    public static final YamlValueType<Trigger> MANUAL_TRIGGER = type(\"MANUAL_TRIGGER\");\n    public static final YamlValueType<Trigger> ONEOPS_TRIGGER = type(\"ONEOPS_TRIGGER\");\n    public static final YamlValueType<Trigger> GENERIC_TRIGGER = type(\"GENERIC_TRIGGER\");\n    public static final YamlValueType<IfStep> IF = type(\"IF\");\n    public static final YamlValueType<SwitchStep> SWITCH = type(\"SWITCH\");\n    public static final YamlValueType<Resources> RESOURCES = type(\"RESOURCES\");\n    public static final YamlValueType<Map<String, Object>> RUN_AS = type(\"RUN_AS\");\n    public static final YamlValueType<ExclusiveMode> EXCLUSIVE_MODE = type(\"EXCLUSIVE_MODE\");\n    public static final YamlValueType<EventConfiguration> EVENTS_CFG = type(\"EVENTS_CONFIGURATION\");\n    public static final YamlValueType<ImmutableTaskCallOptions.Builder> TASK_CALL_IN = type(\"OBJECT or EXPRESSION\");\n    public static final YamlValueType<ImmutableTaskCallOptions.Builder> TASK_CALL_OUT = type(\"STRING or OBJECT\");\n    public static final YamlValueType<ImmutableExpressionOptions.Builder> EXPRESSION_CALL_OUT = type(\"STRING or OBJECT\");\n    public static final YamlValueType<ImmutableParallelBlockOptions.Builder> PARALLEL_BLOCK_OUT = type(\"STRING or ARRAY_OF_STRING or OBJECT\");\n    public static final YamlValueType<GithubTriggerExclusiveMode> GITHUB_EXCLUSIVE_MODE = type(\"GITHUB_EXCLUSIVE_MODE\");\n    public static final YamlValueType<Map<String, Object>> GITHUB_REPOSITORY_INFO = type(\"GITHUB_REPOSITORY_INFO\");\n    public static final YamlValueType<List<Map<String, Object>>> ARRAY_OF_GITHUB_REPOSITORY_INFO = array(\"REPOSITORY_INFO\", GITHUB_REPOSITORY_INFO);\n    public static final YamlValueType<Loop> LOOP = type(\"LOOP\");\n    public static final YamlValueType<ImmutableLoop.Builder> LOOP_PARALLELISM = type(\"int or expression\");\n    public static final YamlValueType<TaskCallValidation.ValidationMode> VALIDATION_MODE = type(\"VALIDATION_MODE\");\n    public static final YamlValueType<TaskCallValidation> TASK_CALL_VALIDATION = type(\"TASK_CALL_VALIDATION\");\n    public static final YamlValueType<ValidationConfiguration> VALIDATION_CFG = type(\"VALIDATION_CONFIGURATION\");\n\n    private final String name;\n\n    private YamlValueType(String name) {\n        this.name = name;\n    }\n\n    @Override\n    public String toString() {\n        return name;\n    }\n\n    private static <T> YamlValueType<T> type(String name) {\n        return new YamlValueType<>(name);\n    }\n\n    private static <T> YamlValueType<List<T>> array(String name, YamlValueType<T> arrayValueType) {\n        return new YamlValueType<>(name);\n    }\n\n    private static <T> YamlValueType<Map<String, T>> map(String name) {\n        return new YamlValueType<>(name);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/BlockStepMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle;\n\nimport java.util.List;\n\n@JsonTypeName(\"BlockStep\")\npublic interface BlockStepMixIn extends GroupOfStepsMixIn {\n\n    @Override\n    @JsonProperty(value = \"block\", required = true)\n    @JsonSchemaTitle(\"Block step\")\n    List<StepMixIn> steps();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/CheckpointStepMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle;\n\nimport java.util.Map;\n\n@JsonTypeName(\"CheckpointStep\")\npublic interface CheckpointStepMixIn extends StepMixIn {\n\n    @JsonSchemaTitle(\"Checkpoint step\")\n    @JsonProperty(value = \"checkpoint\", required = true)\n    String checkpoint();\n\n    @JsonProperty(\"meta\")\n    Map<String, Object> meta();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/ExitStepMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\n\n@JsonTypeName(\"ExitStep\")\n@JsonSchemaInject(json = \"{\\\"type\\\": \\\"string\\\", \\\"enum\\\": [ \\\"exit\\\" ], \\\"default\\\": \\\"exit\\\", \\\"title\\\": \\\"Exit step\\\"}\", merge = false)\npublic interface ExitStepMixIn extends StepMixIn {\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/ExpressionFullMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\n\nimport java.util.List;\nimport java.util.Map;\n\n@JsonTypeName(\"ExpressionFullStep\")\npublic interface ExpressionFullMixIn extends NamedStep {\n\n    @JsonSchemaTitle(\"Expression step (full form)\")\n    @JsonProperty(value = \"expr\", required = true)\n    String expr();\n\n    @JsonProperty(\"error\")\n    List<Step> error();\n\n    @JsonProperty(\"meta\")\n    Map<String, Object> meta();\n\n    @JsonProperty(\"out\")\n    @JsonSchemaInject(json = \"{\\\"oneOf\\\": [ {\\\"type\\\": \\\"string\\\"}, {\\\"type\\\": \\\"object\\\"} ]}\", merge = false)\n    Object out();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/ExpressionShortMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\n\n@JsonTypeName(\"ExpressionShortStep\")\n@JsonSchemaInject(json = \"{\\\"type\\\": \\\"string\\\", \\\"pattern\\\": \\\"^\\\\\\\\$\\\\\\\\{.*}\\\", \\\"title\\\": \\\"Expression step (short form)\\\"}\", merge = false)\npublic interface ExpressionShortMixIn extends StepMixIn {\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/FlowCallStepMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\n\nimport java.util.List;\nimport java.util.Map;\n\n@JsonTypeName(\"FlowCallStep\")\npublic interface FlowCallStepMixIn extends NamedStep {\n\n    @JsonSchemaTitle(\"Flow Call step\")\n    @JsonProperty(value = \"call\", required = true)\n    String call();\n\n    @JsonProperty(\"in\")\n    @JsonSchemaInject(json = \"{\\\"oneOf\\\": [ {\\\"type\\\": \\\"string\\\"}, {\\\"type\\\": \\\"object\\\"} ]}\", merge = false)\n    Object input();\n\n    @JsonProperty(\"out\")\n    @JsonSchemaInject(json = \"{\\\"oneOf\\\": [ {\\\"type\\\": \\\"array\\\", \\\"items\\\" : {\\\"type\\\" : \\\"string\\\"}}, {\\\"type\\\": \\\"object\\\"}, {\\\"type\\\": \\\"string\\\"} ]}\", merge = false)\n    Object out();\n\n    @JsonProperty(\"retry\")\n    RetryMixIn retry();\n\n    @JsonProperty(\"error\")\n    List<Step> error();\n\n    @JsonProperty(\"loop\")\n    LoopMixIn loop();\n\n    @JsonProperty(\"meta\")\n    Map<String, Object> meta();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/FlowsMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.Step;\n\nimport java.util.List;\n\npublic interface FlowsMixIn extends List<Step> {\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/FormCallStepMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle;\n\nimport java.util.List;\nimport java.util.Map;\n\n@JsonTypeName(\"FormCallStep\")\npublic interface FormCallStepMixIn extends StepMixIn {\n\n    @JsonProperty(value = \"form\", required = true)\n    @JsonSchemaTitle(\"Form Call step\")\n    String form();\n\n    @JsonProperty(\"yield\")\n    @JsonSchemaTitle(\"yield\")\n    Boolean yield();\n\n    @JsonProperty(\"saveSubmittedBy\")\n    Boolean saveSubmittedBy();\n\n    @JsonProperty(\"runAs\")\n    Map<String, Object> runAs();\n\n    @JsonProperty(\"values\")\n    Map<String, Object> values();\n\n    // TODO: types\n    @JsonProperty(\"fields\")\n    List<Map<String, Object>> fields();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/FormFieldMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaString;\nimport com.walmartlabs.concord.runtime.v2.model.FormField;\n\nimport javax.annotation.Nullable;\n\n@JsonSchemaInject(strings = {\n        @JsonSchemaString(path = \"patternProperties/^[a-zA-Z0-9_]*$/$ref\", value = \"#/definitions/FormFieldParams\")\n})\n@JsonTypeName(\"FormField\")\npublic interface FormFieldMixIn {\n\n    @JsonProperty(\"removeMe\")\n    FormFieldParams params();\n\n    interface FormFieldParams extends FormField {\n\n        @Nullable\n        @Override\n        @JsonProperty(\"label\")\n        String label();\n\n        @Override\n        @JsonProperty(value = \"type\", required = true)\n        String type();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/FormFieldsMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.List;\n\n//@JsonSchemaInject(json = \"{\\\"type\\\" : \\\"object\\\", \\\"patternProperties\\\" : {\\\"^[a-zA-Z0-9_]*$\\\": {\\\"type\\\" : \\\"array\\\", \\\"items\\\" : {\\\"$ref\\\" : \\\"#/definitions/FormField\\\"}}}}\", merge = false)\npublic interface FormFieldsMixIn extends List<FormFieldMixIn> {\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/GroupOfStepsMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\n\nimport java.util.List;\nimport java.util.Map;\n\npublic interface GroupOfStepsMixIn extends NamedStep {\n\n    List<StepMixIn> steps();\n\n    @JsonProperty(\"out\")\n    @JsonSchemaInject(json = \"{\\\"oneOf\\\": [ {\\\"type\\\": \\\"array\\\", \\\"items\\\" : {\\\"type\\\" : \\\"string\\\"}}, {\\\"type\\\": \\\"string\\\"} ]}\", merge = false)\n    Object out();\n\n    @JsonProperty(\"loop\")\n    LoopMixIn loop();\n\n    @JsonProperty(\"error\")\n    List<StepMixIn> errorSteps();\n\n    @JsonProperty(\"meta\")\n    Map<String, Object> meta();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/IfStepMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle;\n\nimport java.util.List;\nimport java.util.Map;\n\n@JsonTypeName(\"IfStep\")\n@SuppressWarnings(\"unused\")\npublic interface IfStepMixIn extends StepMixIn {\n\n    @JsonProperty(value = \"if\", required = true)\n    @JsonSchemaTitle(\"IF step\")\n    String expression();\n\n    @JsonProperty(value = \"then\", required = true)\n    List<StepMixIn> thenSteps();\n\n    @JsonProperty(\"else\")\n    List<StepMixIn> elseSteps();\n\n    @JsonProperty(\"meta\")\n    Map<String, Object> meta();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/ImportMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonSubTypes;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.walmartlabs.concord.imports.Import;\n\n@JsonTypeInfo(\n        use = JsonTypeInfo.Id.NAME)\n@JsonSubTypes({\n        @JsonSubTypes.Type(value = ImportMixIn.MvnImportMixIn.class, name = \"MVN Import\"),\n        @JsonSubTypes.Type(value = ImportMixIn.DirImportMixIn.class, name = \"Dir Import\"),\n        @JsonSubTypes.Type(value = ImportMixIn.GitImportMixIn.class, name = \"GIT Import\")\n})\npublic interface ImportMixIn {\n\n    @JsonTypeName(\"MvnImport\")\n    interface MvnImportMixIn extends ImportMixIn {\n\n        @JsonProperty(\"mvn\")\n        Import.MvnDefinition params();\n    }\n\n    @JsonTypeName(\"DirImport\")\n    interface DirImportMixIn extends ImportMixIn {\n\n        @JsonProperty(\"dir\")\n        Import.DirectoryDefinition params();\n    }\n\n    @JsonTypeName(\"GitImport\")\n    interface GitImportMixIn extends ImportMixIn {\n\n        @JsonProperty(\"git\")\n        Import.GitDefinition params();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/ImportsMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.List;\n\npublic interface ImportsMixIn extends List<ImportMixIn> {\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/LogStepMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle;\n\nimport java.util.Map;\n\n@JsonTypeName(\"LogStep\")\npublic interface LogStepMixIn extends NamedStep {\n\n    @JsonProperty(value = \"log\", required = true)\n    @JsonSchemaTitle(\"Log step\")\n    String log();\n\n    @JsonProperty(\"meta\")\n    Map<String, Object> meta();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/LogYamlStepMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle;\n\nimport java.util.Map;\n\n@JsonTypeName(\"LogYamlStep\")\npublic interface LogYamlStepMixIn extends NamedStep {\n\n    @JsonProperty(value = \"logYaml\", required = true)\n    @JsonSchemaTitle(\"Log yaml step\")\n    String logYaml();\n\n    @JsonProperty(\"meta\")\n    Map<String, Object> meta();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/LoopMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\nimport com.walmartlabs.concord.runtime.v2.model.Loop;\n\n@JsonTypeName(\"Loop\")\npublic interface LoopMixIn {\n\n    @JsonProperty(\"items\")\n    @JsonSchemaInject(json = \"{\\\"oneOf\\\": [ {\\\"type\\\": \\\"string\\\"}, {\\\"type\\\": \\\"object\\\"}, {\\\"type\\\": \\\"array\\\"} ]}\", merge = false)\n    Object items();\n\n    @JsonProperty(\"mode\")\n    @JsonSchemaInject(json = \"{\\\"enum\\\" : [\\\"serial\\\", \\\"parallel\\\"]}\")\n    Loop.Mode mode();\n\n    @JsonProperty(\"parallelism\")\n    Integer parallelism();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/NamedStep.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\npublic interface NamedStep extends StepMixIn {\n\n    @JsonProperty(\"name\")\n    String name();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/ParallelStepMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle;\n\nimport java.util.List;\nimport java.util.Map;\n\n@JsonTypeName(\"ParallelStep\")\n@SuppressWarnings(\"unused\")\npublic interface ParallelStepMixIn extends StepMixIn {\n\n    @JsonProperty(value = \"parallel\", required = true)\n    @JsonSchemaTitle(\"Parallel step\")\n    List<StepMixIn> steps();\n\n    @JsonProperty(\"out\")\n    @JsonSchemaInject(json = \"{\\\"oneOf\\\": [ {\\\"type\\\": \\\"array\\\", \\\"items\\\" : {\\\"type\\\" : \\\"string\\\"}}, {\\\"type\\\": \\\"object\\\"} ]}\", merge = false)\n    Object out();\n\n    @JsonProperty(\"meta\")\n    Map<String, Object> meta();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/ProcessDefinitionConfigurationMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinitionConfiguration;\n\nimport java.util.List;\n\npublic interface ProcessDefinitionConfigurationMixIn extends ProcessDefinitionConfiguration {\n\n    @Override\n    @JsonProperty(\"runtime\")\n    @JsonSchemaInject(json = \"{\\\"enum\\\" : [\\\"concord-v2\\\"]}\")\n    String runtime();\n\n    @Override\n    @JsonIgnore\n    default List<String> activeProfiles() {\n        return ProcessDefinitionConfiguration.super.activeProfiles();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/ProcessDefinitionMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.runtime.v2.model.*;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\npublic interface ProcessDefinitionMixIn extends ProcessDefinition {\n\n    @Override\n    @JsonProperty(\"configuration\")\n    ProcessDefinitionConfiguration configuration();\n\n    @Override\n    @JsonProperty(\"flows\")\n    Map<String, Flow> flows();\n\n    @Override\n    @JsonProperty(\"publicFlows\")\n    Set<String> publicFlows();\n\n    @Override\n    @JsonProperty(\"profiles\")\n    Map<String, Profile> profiles();\n\n    @Override\n    @JsonProperty(\"triggers\")\n    List<Trigger> triggers();\n\n    @Override\n    @JsonProperty(\"imports\")\n    Imports imports();\n\n    @Override\n    @JsonProperty(\"forms\")\n    Map<String, Form> forms();\n\n    @Override\n    @JsonProperty(\"resources\")\n    Resources resources();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/RetryMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\n\nimport java.io.Serializable;\nimport java.util.Map;\n\n@JsonTypeName(\"Retry\")\npublic interface RetryMixIn {\n\n    @JsonProperty(\"times\")\n    @JsonSchemaInject(json = \"{\\\"oneOf\\\": [ {\\\"type\\\": \\\"string\\\"}, {\\\"type\\\": \\\"integer\\\"} ]}\", merge = false)\n    Object times();\n\n    @JsonProperty(\"delay\")\n    String delay();\n\n    @JsonProperty(\"in\")\n    Map<String, Serializable> input();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/ReturnStepMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\n\n@JsonSchemaInject(json = \"{\\\"type\\\": \\\"string\\\", \\\"enum\\\": [ \\\"return\\\" ], \\\"default\\\": \\\"return\\\", \\\"title\\\": \\\"Return step\\\"}\", merge = false)\n@JsonTypeName(\"ReturnStep\")\npublic interface ReturnStepMixIn extends StepMixIn {\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/ScriptCallMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle;\n\nimport java.util.List;\nimport java.util.Map;\n\n@JsonTypeName(\"ScriptCall\")\npublic interface ScriptCallMixIn extends NamedStep {\n\n    @JsonSchemaTitle(\"Script Call step\")\n    @JsonProperty(value = \"script\", required = true)\n    String script();\n\n    @JsonProperty(\"body\")\n    String body();\n\n    @JsonProperty(\"in\")\n    @JsonSchemaInject(json = \"{\\\"oneOf\\\": [ {\\\"type\\\": \\\"string\\\"}, {\\\"type\\\": \\\"object\\\"} ]}\", merge = false)\n    Object input();\n\n    @JsonProperty(\"out\")\n    @JsonSchemaInject(json = \"{\\\"oneOf\\\": [ {\\\"type\\\": \\\"string\\\"}, {\\\"type\\\": \\\"object\\\"} ]}\", merge = false)\n    Object out();\n\n    @JsonProperty(\"loop\")\n    LoopMixIn loop();\n\n    @JsonProperty(\"retry\")\n    RetryMixIn retry();\n\n    @JsonProperty(\"error\")\n    List<StepMixIn> errorSteps();\n\n    @JsonProperty(\"meta\")\n    Map<String, Object> meta();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/SetStepMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle;\n\nimport java.util.Map;\n\n@JsonTypeName(\"SetStep\")\npublic interface SetStepMixIn extends StepMixIn {\n\n    @JsonSchemaTitle(\"Set step\")\n    @JsonProperty(value = \"set\", required = true)\n    Map<String, Object> variables();\n\n    @JsonProperty(\"meta\")\n    Map<String, Object> meta();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/StepMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonSubTypes;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\n\n@JsonTypeInfo(\n        use = JsonTypeInfo.Id.NAME)\n@JsonSubTypes({\n        @JsonSubTypes.Type(value = TaskCallMixIn.class, name = \"Task call\"),\n        @JsonSubTypes.Type(value = ReturnStepMixIn.class, name = \"return\"),\n        @JsonSubTypes.Type(value = ExitStepMixIn.class, name = \"exit\"),\n        @JsonSubTypes.Type(value = CheckpointStepMixIn.class, name = \"Checkpoint\"),\n        @JsonSubTypes.Type(value = ExpressionShortMixIn.class, name = \"Expression (short form)\"),\n        @JsonSubTypes.Type(value = ExpressionFullMixIn.class, name = \"Expression (full form)\"),\n        @JsonSubTypes.Type(value = LogStepMixIn.class, name = \"Log\"),\n        @JsonSubTypes.Type(value = LogYamlStepMixIn.class, name = \"LogYaml\"),\n        @JsonSubTypes.Type(value = FlowCallStepMixIn.class, name = \"Flow call\"),\n        @JsonSubTypes.Type(value = FormCallStepMixIn.class, name = \"Form call\"),\n        @JsonSubTypes.Type(value = IfStepMixIn.class, name = \"IF step\"),\n        @JsonSubTypes.Type(value = ParallelStepMixIn.class, name = \"Parallel step\"),\n        @JsonSubTypes.Type(value = ScriptCallMixIn.class, name = \"Script call\"),\n        @JsonSubTypes.Type(value = SetStepMixIn.class, name = \"Set step\"),\n        @JsonSubTypes.Type(value = SuspendStepMixIn.class, name = \"Suspend step\"),\n        @JsonSubTypes.Type(value = SwitchStepMixIn.class, name = \"Suspend step\"),\n        @JsonSubTypes.Type(value = ThrowStepMixIn.class, name = \"Throw step\"),\n        @JsonSubTypes.Type(value = TryStepMixIn.class, name = \"Try step\"),\n        @JsonSubTypes.Type(value = BlockStepMixIn.class, name = \"Block step\")\n})\npublic interface StepMixIn extends Step {\n\n    @Override\n    @JsonIgnore\n    Location getLocation();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/SuspendStepMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle;\n\nimport java.util.Map;\n\n@JsonTypeName(\"SuspendStep\")\npublic interface SuspendStepMixIn extends StepMixIn {\n\n    @JsonSchemaTitle(\"Suspend step\")\n    @JsonProperty(value = \"suspend\", required = true)\n    String suspend();\n\n    @JsonProperty(\"meta\")\n    Map<String, Object> meta();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/SwitchStepMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle;\n\nimport java.util.List;\nimport java.util.Map;\n\n@JsonTypeName(\"SwitchStep\")\n@SuppressWarnings(\"unused\")\npublic interface SwitchStepMixIn extends StepMixIn {\n\n    @JsonSchemaTitle(\"Switch step\")\n    @JsonProperty(value = \"switch\", required = true)\n    String expression();\n\n    @JsonProperty(\"default\")\n    List<StepMixIn> defaultSteps();\n\n    @JsonProperty(\"meta\")\n    Map<String, Object> meta();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/TaskCallMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\n\nimport java.util.List;\nimport java.util.Map;\n\n@JsonTypeName(\"TaskCall\")\npublic interface TaskCallMixIn extends NamedStep {\n\n    @JsonProperty(value = \"task\", required = true)\n    String task();\n\n    @JsonProperty(\"in\")\n    @JsonSchemaInject(json = \"{\\\"oneOf\\\": [ {\\\"type\\\": \\\"string\\\"}, {\\\"type\\\": \\\"object\\\"} ]}\", merge = false)\n    Object input();\n\n    @JsonProperty(\"out\")\n    @JsonSchemaInject(json = \"{\\\"oneOf\\\": [ {\\\"type\\\": \\\"string\\\"}, {\\\"type\\\": \\\"object\\\"} ]}\", merge = false)\n    Object out();\n\n    @JsonProperty(\"loop\")\n    LoopMixIn loop();\n\n    @JsonProperty(\"retry\")\n    RetryMixIn retry();\n\n    @JsonProperty(\"error\")\n    List<StepMixIn> errorSteps();\n\n    @JsonProperty(\"meta\")\n    Map<String, Object> meta();\n\n    @JsonProperty(\"ignoreErrors\")\n    boolean ignoreErrors();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/ThrowStepMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\n\n@JsonTypeName(\"ThrowStep\")\n@SuppressWarnings(\"unused\")\npublic interface ThrowStepMixIn extends NamedStep {\n\n    @JsonProperty(value = \"throw\", required = true)\n    String throwValue();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/TriggerMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.*;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaInject;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaString;\nimport com.walmartlabs.concord.runtime.v2.model.ExclusiveMode;\nimport com.walmartlabs.concord.runtime.v2.model.GithubTriggerExclusiveMode;\nimport com.walmartlabs.concord.runtime.v2.model.Trigger;\n\nimport java.util.List;\nimport java.util.Map;\n\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonTypeInfo(\n        use = JsonTypeInfo.Id.NAME)\n@JsonSubTypes({\n        @JsonSubTypes.Type(value = TriggerMixIn.ManualTriggerMixIn.class, name = \"Manual Trigger\"),\n        @JsonSubTypes.Type(value = TriggerMixIn.CronTriggerMixIn.class, name = \"Cron Trigger\"),\n        @JsonSubTypes.Type(value = TriggerMixIn.GithubTriggerMixIn.class, name = \"Github Trigger\"),\n        @JsonSubTypes.Type(value = TriggerMixIn.OneOpsTriggerMixIn.class, name = \"OneOps Trigger\"),\n        @JsonSubTypes.Type(value = TriggerMixIn.GenericTriggerMixIn.class, name = \"Generic Trigger\")\n})\n@SuppressWarnings(\"unused\")\npublic interface TriggerMixIn extends Trigger {\n\n    @JsonTypeName(\"ManualTrigger\")\n    interface ManualTriggerMixIn extends TriggerMixIn {\n\n        @JsonProperty(\"manual\")\n        ManualTriggerParams params();\n\n        interface ManualTriggerParams extends DefaultTriggerParams {\n\n            @JsonProperty(\"name\")\n            String name();\n\n            @JsonProperty(\"exclusive\")\n            ExclusiveMode exclusive();\n        }\n    }\n\n    @JsonTypeName(\"CronTrigger\")\n    interface CronTriggerMixIn extends TriggerMixIn {\n\n        @JsonProperty(\"cron\")\n        CronTriggerMixIn.CronTriggerParams params();\n\n        interface CronTriggerParams extends DefaultTriggerParams {\n\n            @JsonProperty(\"exclusive\")\n            ExclusiveMode exclusive();\n\n            @JsonProperty(value = \"spec\", required = true)\n            String spec();\n\n            @JsonProperty(\"timezone\")\n            String timezone();\n\n            @JsonProperty(\"runAs\")\n            RunAs runAs();\n        }\n\n        interface RunAs {\n\n            @JsonProperty(value = \"withSecret\", required = true)\n            String withSecret();\n        }\n    }\n\n    @JsonTypeName(\"GithubTrigger\")\n    interface GithubTriggerMixIn extends TriggerMixIn {\n\n        @JsonProperty(\"github\")\n        GithubTriggerMixIn.GithubTriggerParams params();\n\n        interface GithubTriggerParams extends DefaultTriggerParams {\n\n            @JsonSchemaInject(json = \"{\\\"enum\\\" : [2]}\")\n            @JsonProperty(value = \"version\", required = true)\n            int version();\n\n            @JsonProperty(\"useInitiator\")\n            Boolean useInitiator();\n\n            @JsonProperty(\"useEventCommitId\")\n            Boolean useEventCommitId();\n\n            @JsonProperty(\"ignoreEmptyPush\")\n            Boolean ignoreEmptyPush();\n\n            @JsonProperty(\"conditions\")\n            GithubTriggerConditions conditions();\n\n            @JsonProperty(\"exclusive\")\n            GithubTriggerExclusiveMode exclusive();\n\n            interface GithubTriggerConditions {\n                @JsonProperty(value = \"type\", required = true)\n                String type();\n\n                @JsonProperty(\"githubOrg\")\n                String githubOrg();\n\n                @JsonProperty(\"githubRepo\")\n                String githubRepo();\n\n                @JsonProperty(\"githubHost\")\n                String githubHost();\n\n                @JsonProperty(\"branch\")\n                String branch();\n\n                @JsonProperty(\"sender\")\n                String sender();\n\n                @JsonProperty(\"status\")\n                String status();\n\n                @JsonProperty(\"repositoryInfo\")\n                List<Map<String, Object>> repositoryInfo();\n\n                @JsonProperty(\"payload\")\n                Map<String, Object> payload();\n\n                @JsonProperty(\"queryParams\")\n                Map<String, Object> queryParams();\n            }\n        }\n    }\n\n    @JsonTypeName(\"OneOpsTrigger\")\n    interface OneOpsTriggerMixIn extends TriggerMixIn {\n\n        @JsonProperty(\"oneops\")\n        Map<String, Object> params();\n    }\n\n    @JsonTypeName(\"GenericTrigger\")\n    @JsonSchemaInject(strings = {@JsonSchemaString(path = \"patternProperties/^(?!(manual|cron|github)$).*$/$ref\", value = \"#/definitions/GenericTriggerParams\")})\n    interface GenericTriggerMixIn extends TriggerMixIn {\n\n        @JsonProperty(\"removeMe\")\n        GenericTriggerParams params();\n\n        interface GenericTriggerParams extends DefaultTriggerParams {\n\n            @JsonSchemaInject(json = \"{\\\"enum\\\" : [2]}\")\n            @JsonProperty(value = \"version\", required = true)\n            int version();\n\n            @JsonProperty(\"conditions\")\n            Map<String, Object> conditions();\n\n            @JsonProperty(\"exclusive\")\n            ExclusiveMode exclusive();\n        }\n    }\n\n    interface DefaultTriggerParams {\n\n        @JsonProperty(value = \"entryPoint\", required = true)\n        String entryPoint();\n\n        @JsonProperty(\"activeProfiles\")\n        List<String> activeProfiles();\n\n        @JsonProperty(\"arguments\")\n        Map<String, Object> arguments();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/TryStepMixIn.java",
    "content": "package com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonTypeName;\nimport com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle;\n\nimport java.util.List;\n\n@JsonTypeName(\"TryStep\")\npublic interface TryStepMixIn extends GroupOfStepsMixIn {\n\n    @Override\n    @JsonProperty(value = \"try\", required = true)\n    @JsonSchemaTitle(\"Try step\")\n    List<StepMixIn> steps();\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/schema/package-info.java",
    "content": "/**\n * Contains mix-in classes used to generate the JSON schema for the runtime v2 YAML syntax.\n * @see com.walmartlabs.concord.runtime.v2.ConcordJsonSchemaGenerator\n */\npackage com.walmartlabs.concord.runtime.v2.schema;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/CheckpointStepSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.Checkpoint;\n\nimport java.io.IOException;\n\npublic class CheckpointStepSerializer extends StdSerializer<Checkpoint> {\n\n    private static final long serialVersionUID = 1L;\n\n    public CheckpointStepSerializer() {\n        this(null);\n    }\n\n    public CheckpointStepSerializer(Class<Checkpoint> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(Checkpoint value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeStartObject();\n\n        gen.writeObjectField(\"checkpoint\", value.getName());\n        gen.writeObject(value.getOptions());\n\n        gen.writeEndObject();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/DurationSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\n\nimport java.io.IOException;\nimport java.time.Duration;\n\npublic class DurationSerializer extends StdSerializer<Duration> {\n\n    private static final long serialVersionUID = 1L;\n\n    public DurationSerializer() {\n        this(null);\n    }\n\n    public DurationSerializer(Class<Duration> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(Duration value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        if (value != null) {\n            gen.writeString(value.toString());\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/ExitStepSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.ExitStep;\n\nimport java.io.IOException;\n\npublic class ExitStepSerializer extends StdSerializer<ExitStep> {\n\n    private static final long serialVersionUID = 1L;\n\n    public ExitStepSerializer() {\n        this(null);\n    }\n\n    public ExitStepSerializer(Class<ExitStep> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(ExitStep value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeString(\"exit\");\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/ExpressionStepSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.Expression;\nimport com.walmartlabs.concord.runtime.v2.model.ExpressionOptions;\n\nimport java.io.IOException;\n\nimport static com.walmartlabs.concord.runtime.v2.serializer.SerializerUtils.writeNotEmptyObjectField;\n\npublic class ExpressionStepSerializer extends StdSerializer<Expression> {\n\n    private static final long serialVersionUID = 1L;\n\n    public ExpressionStepSerializer() {\n        this(null);\n    }\n\n    public ExpressionStepSerializer(Class<Expression> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(Expression value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeStartObject();\n\n        gen.writeObjectField(\"expr\", value.getExpr());\n        serializeOptions(value.getOptions(), gen);\n\n        gen.writeEndObject();\n    }\n\n    private static void serializeOptions(ExpressionOptions options, JsonGenerator gen) throws IOException {\n        if (options == null) {\n            return;\n        }\n\n        if (options.out() != null) {\n            gen.writeObjectField(\"out\", options.out());\n        }\n\n        writeNotEmptyObjectField(\"out\", options.outExpr(), gen);\n\n        writeNotEmptyObjectField(\"meta\", options.meta(), gen);\n        writeNotEmptyObjectField(\"error\", options.errorSteps(), gen);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/FlowCallStepSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.FlowCall;\nimport com.walmartlabs.concord.runtime.v2.model.FlowCallOptions;\nimport com.walmartlabs.concord.runtime.v2.model.WithItems;\n\nimport java.io.IOException;\nimport java.util.Objects;\n\nimport static com.walmartlabs.concord.runtime.v2.serializer.SerializerUtils.writeLoop;\nimport static com.walmartlabs.concord.runtime.v2.serializer.SerializerUtils.writeNotEmptyObjectField;\n\npublic class FlowCallStepSerializer extends StdSerializer<FlowCall> {\n\n    private static final long serialVersionUID = 1L;\n\n    public FlowCallStepSerializer() {\n        this(null);\n    }\n\n    public FlowCallStepSerializer(Class<FlowCall> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(FlowCall value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeStartObject();\n\n        gen.writeObjectField(\"call\", value.getFlowName());\n        serializeOptions(value.getOptions(), gen);\n\n        gen.writeEndObject();\n    }\n\n    private static void serializeOptions(FlowCallOptions options, JsonGenerator gen) throws IOException {\n        if (options == null) {\n            return;\n        }\n\n        writeNotEmptyObjectField(\"in\", options.input(), gen);\n        writeNotEmptyObjectField(\"in\", options.inputExpression(), gen);\n\n        writeNotEmptyObjectField(\"out\", options.out(), gen);\n        writeNotEmptyObjectField(\"out\", options.outExpression(), gen);\n        writeNotEmptyObjectField(\"out\", options.outMapping(), gen);\n        writeNotEmptyObjectField(\"out\", options.outExpr(), gen);\n\n        if (options.withItems() != null) {\n            WithItems items = Objects.requireNonNull(options.withItems());\n            SerializerUtils.writeWithItems(items, gen);\n        }\n\n        writeLoop(options.loop(), gen);\n\n        if (options.retry() != null) {\n            gen.writeObjectField(\"retry\", options.retry());\n        }\n\n        writeNotEmptyObjectField(\"error\", options.errorSteps(), gen);\n        writeNotEmptyObjectField(\"meta\", options.meta(), gen);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/FlowSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.Flow;\n\nimport java.io.IOException;\nimport java.io.Serial;\n\npublic class FlowSerializer extends StdSerializer<Flow> {\n\n    @Serial\n    private static final long serialVersionUID = 1L;\n\n    public FlowSerializer() {\n        this(null);\n    }\n\n    public FlowSerializer(Class<Flow> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(Flow value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeObject(value.steps());\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/FormCallStepSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.FormCall;\nimport com.walmartlabs.concord.runtime.v2.model.FormCallOptions;\n\nimport java.io.IOException;\n\nimport static com.walmartlabs.concord.runtime.v2.serializer.SerializerUtils.writeNotEmptyObjectField;\n\npublic class FormCallStepSerializer extends StdSerializer<FormCall> {\n\n    private static final long serialVersionUID = 1L;\n\n    public FormCallStepSerializer() {\n        this(null);\n    }\n\n    public FormCallStepSerializer(Class<FormCall> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(FormCall value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeStartObject();\n        gen.writeObjectField(\"form\", value.getName());\n        serializeOptions(value.getOptions(), gen);\n        gen.writeEndObject();\n    }\n\n    private static void serializeOptions(FormCallOptions options, JsonGenerator gen) throws IOException {\n        if (options == null) {\n            return;\n        }\n\n        gen.writeObjectField(\"yield\", options.isYield());\n        gen.writeObjectField(\"saveSubmittedBy\", options.saveSubmittedBy());\n\n        if (!options.runAs().isEmpty()) {\n            writeNotEmptyObjectField(\"runAs\", options.runAs(), gen);\n        } else {\n            gen.writeObjectField(\"runAs\", options.runAsExpression());\n        }\n\n        if (!options.values().isEmpty()) {\n            writeNotEmptyObjectField(\"values\", options.values(), gen);\n        } else if (options.valuesExpression() != null) {\n            gen.writeObjectField(\"values\", options.valuesExpression());\n        }\n\n        if (!options.fields().isEmpty()) {\n            writeNotEmptyObjectField(\"fields\", options.fields(), gen);\n        } else if (options.fieldsExpression() != null) {\n            gen.writeObjectField(\"fields\", options.fieldsExpression());\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/FormDefinitionSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.Form;\n\nimport java.io.IOException;\n\npublic class FormDefinitionSerializer extends StdSerializer<Form> {\n\n    private static final long serialVersionUID = 1L;\n\n    public FormDefinitionSerializer() {\n        this(null);\n    }\n\n    public FormDefinitionSerializer(Class<Form> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(Form value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeObject(value.fields());\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/FormFieldSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.FormField;\n\nimport java.io.IOException;\nimport java.io.Serializable;\nimport java.util.Map;\n\npublic class FormFieldSerializer extends StdSerializer<FormField> {\n\n    private static final long serialVersionUID = 1L;\n\n    public FormFieldSerializer() {\n        this(null);\n    }\n\n    public FormFieldSerializer(Class<FormField> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(FormField value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeStartObject();\n        gen.writeFieldName(value.name());\n\n        gen.writeStartObject();\n        if (value.label() != null) {\n            gen.writeObjectField(\"label\", value.label());\n        }\n\n        gen.writeObjectField(\"type\", toType(value.type(), value.cardinality()));\n\n        if (value.defaultValue() != null) {\n            gen.writeObjectField(\"value\", value.defaultValue());\n        }\n\n        if (value.allowedValue() != null) {\n            gen.writeObjectField(\"allow\", value.allowedValue());\n        }\n\n        if (!value.options().isEmpty()) {\n            for (Map.Entry<String, Serializable> e : value.options().entrySet()) {\n                gen.writeObjectField(e.getKey(), e.getValue());\n            }\n        }\n        gen.writeEndObject();\n\n        gen.writeEndObject();\n    }\n\n    private String toType(String type, com.walmartlabs.concord.forms.FormField.Cardinality cardinality) {\n        switch (cardinality) {\n            case ONE_AND_ONLY_ONE:\n                return type;\n            case ONE_OR_NONE:\n                return type + \"?\";\n            case AT_LEAST_ONE:\n                return type + \"+\";\n            case ANY:\n                return type + \"*\";\n            default:\n                throw new IllegalArgumentException(\"Unknown cardinality: '\" + cardinality + \"'\");\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/GroupOfStepsSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.GroupOfSteps;\nimport com.walmartlabs.concord.runtime.v2.model.GroupOfStepsOptions;\n\nimport java.io.IOException;\n\nimport static com.walmartlabs.concord.runtime.v2.serializer.SerializerUtils.writeNotEmptyObjectField;\n\npublic class GroupOfStepsSerializer extends StdSerializer<GroupOfSteps> {\n\n    private static final long serialVersionUID = 1L;\n\n    public GroupOfStepsSerializer() {\n        this(null);\n    }\n\n    public GroupOfStepsSerializer(Class<GroupOfSteps> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(GroupOfSteps value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeStartObject();\n\n        gen.writeFieldName(\"block\");\n        gen.writeObject(value.getSteps());\n        serializeOptions(value.getOptions(), gen);\n\n        gen.writeEndObject();\n    }\n\n    private static void serializeOptions(GroupOfStepsOptions options, JsonGenerator gen) throws IOException {\n        if (options == null) {\n            return;\n        }\n\n        writeNotEmptyObjectField(\"out\", options.out(), gen);\n        writeNotEmptyObjectField(\"error\", options.errorSteps(), gen);\n        writeNotEmptyObjectField(\"meta\", options.meta(), gen);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/IfStepSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.IfStep;\n\nimport java.io.IOException;\n\nimport static com.walmartlabs.concord.runtime.v2.serializer.SerializerUtils.writeNotEmptyObjectField;\n\npublic class IfStepSerializer extends StdSerializer<IfStep> {\n\n    private static final long serialVersionUID = 1L;\n\n    public IfStepSerializer() {\n        this(null);\n    }\n\n    public IfStepSerializer(Class<IfStep> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(IfStep value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeStartObject();\n\n        gen.writeObjectField(\"if\", value.getExpression());\n        gen.writeObjectField(\"then\", value.getThenSteps());\n        writeNotEmptyObjectField(\"else\", value.getElseSteps(), gen);\n\n        if (value.getOptions() != null) {\n            gen.writeObject(value.getOptions());\n        }\n\n        gen.writeEndObject();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/LoopOptionsSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.Loop;\n\nimport java.io.IOException;\nimport java.util.Map;\n\npublic class LoopOptionsSerializer extends StdSerializer<Loop> {\n\n    private static final long serialVersionUID = 1L;\n\n    public LoopOptionsSerializer() {\n        this(null);\n    }\n\n    public LoopOptionsSerializer(Class<Loop> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(Loop value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeStartObject();\n\n        gen.writeObjectField(\"mode\", value.mode().name().toLowerCase());\n        gen.writeObjectField(\"items\", value.items());\n\n        for (Map.Entry<String, Object> e : value.options().entrySet()) {\n            gen.writeObjectField(e.getKey(), e.getValue());\n        }\n\n        gen.writeEndObject();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/ParallelBlockSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.ParallelBlock;\nimport com.walmartlabs.concord.runtime.v2.model.ParallelBlockOptions;\n\nimport java.io.IOException;\n\nimport static com.walmartlabs.concord.runtime.v2.serializer.SerializerUtils.writeNotEmptyObjectField;\n\npublic class ParallelBlockSerializer extends StdSerializer<ParallelBlock> {\n\n    private static final long serialVersionUID = 1L;\n\n    public ParallelBlockSerializer() {\n        this(null);\n    }\n\n    public ParallelBlockSerializer(Class<ParallelBlock> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(ParallelBlock value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeStartObject();\n\n        gen.writeObjectField(\"parallel\", value.getSteps());\n        serializeOptions(value.getOptions(), gen);\n\n        gen.writeEndObject();\n    }\n\n    private static void serializeOptions(ParallelBlockOptions options, JsonGenerator gen) throws IOException {\n        if (options == null) {\n            return;\n        }\n\n        writeNotEmptyObjectField(\"out\", options.out(), gen);\n        writeNotEmptyObjectField(\"out\", options.outExpr(), gen);\n        writeNotEmptyObjectField(\"meta\", options.meta(), gen);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/ProcessDefinitionSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\n\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.runtime.v2.serializer.SerializerUtils.writeNotEmptyObjectField;\n\npublic class ProcessDefinitionSerializer extends StdSerializer<ProcessDefinition> {\n\n    private static final long serialVersionUID = 1L;\n\n    public ProcessDefinitionSerializer() {\n        this(null);\n    }\n\n    public ProcessDefinitionSerializer(Class<ProcessDefinition> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(ProcessDefinition value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeStartObject();\n\n        gen.writeObjectField(\"configuration\", value.configuration());\n        writeNotEmptyObjectField(\"flows\", value.flows(), gen);\n        writeNotEmptyObjectField(\"publicFlows\", value.publicFlows(), gen);\n        writeNotEmptyObjectField(\"profiles\", value.profiles(), gen);\n        writeNotEmptyObjectField(\"triggers\", value.triggers(), gen);\n\n        if (!value.imports().isEmpty()) {\n            gen.writeObjectField(\"imports\", value.imports().items().stream().map(o -> Collections.singletonMap(o.type(), o)).collect(Collectors.toList()));\n        }\n\n        if (!value.forms().isEmpty()) {\n            gen.writeObjectField(\"forms\", value.forms());\n        }\n\n        gen.writeObjectField(\"resources\", value.resources());\n\n        gen.writeEndObject();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/RetryOptionsSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.Retry;\n\nimport java.io.IOException;\n\nimport static com.walmartlabs.concord.runtime.v2.serializer.SerializerUtils.writeNotEmptyObjectField;\n\npublic class RetryOptionsSerializer extends StdSerializer<Retry> {\n\n    private static final long serialVersionUID = 1L;\n\n    public RetryOptionsSerializer() {\n        this(null);\n    }\n\n    public RetryOptionsSerializer(Class<Retry> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(Retry value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeStartObject();\n\n        if (value.timesExpression() != null) {\n            gen.writeObjectField(\"times\", value.timesExpression());\n        } else {\n            gen.writeObjectField(\"times\", value.times());\n        }\n\n        if (value.delayExpression() != null) {\n            gen.writeObjectField(\"delay\", value.delayExpression());\n        } else {\n            gen.writeObjectField(\"delay\", value.delay());\n        }\n\n        writeNotEmptyObjectField(\"in\", value.input(), gen);\n\n        gen.writeEndObject();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/ReturnStepSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.ReturnStep;\n\nimport java.io.IOException;\n\npublic class ReturnStepSerializer extends StdSerializer<ReturnStep> {\n\n    private static final long serialVersionUID = 1L;\n\n    public ReturnStepSerializer() {\n        this(null);\n    }\n\n    public ReturnStepSerializer(Class<ReturnStep> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(ReturnStep value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeString(\"return\");\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/ScriptCallStepSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.ScriptCall;\nimport com.walmartlabs.concord.runtime.v2.model.ScriptCallOptions;\nimport com.walmartlabs.concord.runtime.v2.model.WithItems;\n\nimport java.io.IOException;\nimport java.util.Objects;\n\nimport static com.walmartlabs.concord.runtime.v2.serializer.SerializerUtils.*;\n\npublic class ScriptCallStepSerializer extends StdSerializer<ScriptCall> {\n\n    private static final long serialVersionUID = 1L;\n\n    public ScriptCallStepSerializer() {\n        this(null);\n    }\n\n    public ScriptCallStepSerializer(Class<ScriptCall> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(ScriptCall value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeStartObject();\n\n        gen.writeObjectField(\"script\", value.getLanguageOrRef());\n        serializeOptions(value.getOptions(), gen);\n\n        gen.writeEndObject();\n    }\n\n    private static void serializeOptions(ScriptCallOptions options, JsonGenerator gen) throws IOException {\n        if (options == null) {\n            return;\n        }\n\n        if (options.body() != null) {\n            gen.writeObjectField(\"body\", options.body());\n        }\n\n        writeNotEmptyObjectField(\"in\", options.input(), gen);\n        writeNotEmptyObjectField(\"in\", options.inputExpression(), gen);\n\n        writeNotEmptyObjectField(\"out\", options.out(), gen);\n        writeNotEmptyObjectField(\"out\", options.outExpr(), gen);\n\n        if (options.withItems() != null) {\n            WithItems items = Objects.requireNonNull(options.withItems());\n            writeWithItems(items, gen);\n        }\n\n        writeLoop(options.loop(), gen);\n\n        if (options.retry() != null) {\n            gen.writeObjectField(\"retry\", options.retry());\n        }\n\n        writeNotEmptyObjectField(\"error\", options.errorSteps(), gen);\n        writeNotEmptyObjectField(\"meta\", options.meta(), gen);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/SerializerUtils.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.walmartlabs.concord.runtime.v2.model.Loop;\nimport com.walmartlabs.concord.runtime.v2.model.WithItems;\n\nimport java.io.IOException;\nimport java.util.Collection;\nimport java.util.Map;\n\npublic final class SerializerUtils {\n\n    public static void writeNotEmptyObjectField(String fieldName, String value, JsonGenerator gen) throws IOException {\n        if (value == null || value.trim().isEmpty()) {\n            return;\n        }\n\n        gen.writeObjectField(fieldName, value);\n    }\n\n    public static <K, V> void writeNotEmptyObjectField(String fieldName, Map<K, V> value, JsonGenerator gen) throws IOException {\n        if (value == null || value.isEmpty()) {\n            return;\n        }\n\n        gen.writeObjectField(fieldName, value);\n    }\n\n    public static <T> void writeNotEmptyObjectField(String fieldName, Collection<T> value, JsonGenerator gen) throws IOException {\n        if (value == null || value.isEmpty()) {\n            return;\n        }\n\n        gen.writeObjectField(fieldName, value);\n    }\n\n    @Deprecated\n    public static void writeWithItems(WithItems items, JsonGenerator gen) throws IOException {\n        switch (items.mode()) {\n            case PARALLEL: {\n                gen.writeObjectField(\"parallelWithItems\", items);\n                break;\n            }\n            case SERIAL: {\n                gen.writeObjectField(\"withItems\", items);\n                break;\n            }\n            default:\n                throw new IllegalArgumentException(\"Unsupported withItems mode: \" + items.mode());\n        }\n    }\n\n    public static void writeLoop(Loop loop, JsonGenerator gen) throws IOException {\n        if (loop == null) {\n            return;\n        }\n\n        gen.writeObjectField(\"loop\", loop);\n    }\n\n    private SerializerUtils() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/SetVariablesStepSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.SetVariablesStep;\n\nimport java.io.IOException;\n\npublic class SetVariablesStepSerializer extends StdSerializer<SetVariablesStep> {\n\n    private static final long serialVersionUID = 1L;\n\n    public SetVariablesStepSerializer() {\n        this(null);\n    }\n\n    public SetVariablesStepSerializer(Class<SetVariablesStep> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(SetVariablesStep value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeStartObject();\n\n        gen.writeObjectField(\"set\", value.getVars());\n        gen.writeObject(value.getOptions());\n\n        gen.writeEndObject();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/SimpleOptionsSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.parser.SimpleOptions;\n\nimport java.io.IOException;\n\npublic class SimpleOptionsSerializer extends StdSerializer<SimpleOptions> {\n\n    private static final long serialVersionUID = 1L;\n\n    public SimpleOptionsSerializer() {\n        this(null);\n    }\n\n    public SimpleOptionsSerializer(Class<SimpleOptions> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(SimpleOptions value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        if (!value.meta().isEmpty()) {\n            gen.writeObjectField(\"meta\", value.meta());\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/SuspendStepSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.SuspendStep;\n\nimport java.io.IOException;\n\npublic class SuspendStepSerializer extends StdSerializer<SuspendStep> {\n\n    private static final long serialVersionUID = 1L;\n\n    public SuspendStepSerializer() {\n        this(null);\n    }\n\n    public SuspendStepSerializer(Class<SuspendStep> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(SuspendStep value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeStartObject();\n\n        gen.writeObjectField(\"suspend\", value.getEvent());\n        gen.writeObject(value.getOptions());\n\n        gen.writeEndObject();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/SwitchStepSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.model.SwitchStep;\n\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.runtime.v2.serializer.SerializerUtils.writeNotEmptyObjectField;\n\npublic class SwitchStepSerializer extends StdSerializer<SwitchStep> {\n\n    private static final long serialVersionUID = 1L;\n\n    public SwitchStepSerializer() {\n        this(null);\n    }\n\n    public SwitchStepSerializer(Class<SwitchStep> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(SwitchStep value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeStartObject();\n\n        gen.writeObjectField(\"switch\", value.getExpression());\n\n        if (value.getCaseSteps() != null && !value.getCaseSteps().isEmpty()) {\n            for (Map.Entry<String, List<Step>> e : value.getCaseSteps()) {\n                gen.writeObjectField(e.getKey(), e.getValue());\n            }\n        }\n        writeNotEmptyObjectField(\"default\", value.getDefaultSteps(), gen);\n\n        if (value.getOptions() != null) {\n            gen.writeObject(value.getOptions());\n        }\n\n        gen.writeEndObject();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/TaskCallStepSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.TaskCall;\nimport com.walmartlabs.concord.runtime.v2.model.TaskCallOptions;\nimport com.walmartlabs.concord.runtime.v2.model.WithItems;\n\nimport java.io.IOException;\nimport java.util.Objects;\n\nimport static com.walmartlabs.concord.runtime.v2.serializer.SerializerUtils.*;\n\npublic class TaskCallStepSerializer extends StdSerializer<TaskCall> {\n\n    private static final long serialVersionUID = 1L;\n\n    public TaskCallStepSerializer() {\n        this(null);\n    }\n\n    public TaskCallStepSerializer(Class<TaskCall> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(TaskCall value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n\n        gen.writeStartObject();\n\n        TaskCallOptions o = Objects.requireNonNull(value.getOptions());\n\n        if (\"log\".equals(value.getName())) {\n            gen.writeObjectField(\"log\", o.input().get(\"msg\"));\n        } else {\n            gen.writeObjectField(\"task\", value.getName());\n\n            writeNotEmptyObjectField(\"in\", o.input(), gen);\n            writeNotEmptyObjectField(\"in\", o.inputExpression(), gen);\n        }\n\n        writeNotEmptyObjectField(\"out\", o.out(), gen);\n        writeNotEmptyObjectField(\"out\", o.outExpr(), gen);\n\n        if (o.withItems() != null) {\n            WithItems items = Objects.requireNonNull(o.withItems());\n            writeWithItems(items, gen);\n        }\n\n        writeLoop(o.loop(), gen);\n\n        if (o.retry() != null) {\n            gen.writeObjectField(\"retry\", o.retry());\n        }\n\n        writeNotEmptyObjectField(\"error\", o.errorSteps(), gen);\n        writeNotEmptyObjectField(\"meta\", o.meta(), gen);\n\n        if (o.ignoreErrors()) {\n            gen.writeObjectField(\"ignoreErrors\", o.ignoreErrors());\n        }\n\n        gen.writeEndObject();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/TriggerSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.Trigger;\n\nimport java.io.IOException;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.runtime.v2.serializer.SerializerUtils.writeNotEmptyObjectField;\n\npublic class TriggerSerializer extends StdSerializer<Trigger> {\n\n    private static final long serialVersionUID = 1L;\n\n    public TriggerSerializer() {\n        this(null);\n    }\n\n    public TriggerSerializer(Class<Trigger> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(Trigger value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeStartObject();\n\n        gen.writeFieldName(value.name());\n\n        gen.writeStartObject();\n        writeNotEmptyObjectField(\"activeProfiles\", value.activeProfiles(), gen);\n        if (!value.configuration().isEmpty()) {\n            for (Map.Entry<String, Object> e : value.configuration().entrySet()) {\n                gen.writeObjectField(e.getKey(), e.getValue());\n            }\n        }\n        writeNotEmptyObjectField(\"arguments\", value.arguments(), gen);\n\n        if (\"cron\".equalsIgnoreCase(value.name())) {\n            for (Map.Entry<String, Object> e : value.conditions().entrySet()) {\n                gen.writeObjectField(e.getKey(), e.getValue());\n            }\n        } else {\n            writeNotEmptyObjectField(\"conditions\", value.conditions(), gen);\n        }\n\n        gen.writeEndObject();\n\n        gen.writeEndObject();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/serializer/WithItemsSerializer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.serializer;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer;\nimport com.walmartlabs.concord.runtime.v2.model.WithItems;\n\nimport java.io.IOException;\n\npublic class WithItemsSerializer extends StdSerializer<WithItems> {\n\n    private static final long serialVersionUID = 1L;\n\n    public WithItemsSerializer() {\n        this(null);\n    }\n\n    public WithItemsSerializer(Class<WithItems> t) {\n        super(t);\n    }\n\n    @Override\n    public void serialize(WithItems value, JsonGenerator gen, SerializerProvider provider) throws IOException {\n        gen.writeObject(value.value());\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/wrapper/ConfigurationV2.java",
    "content": "package com.walmartlabs.concord.runtime.v2.wrapper;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\nimport com.walmartlabs.concord.runtime.model.Configuration;\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinitionConfiguration;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\n\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\npublic class ConfigurationV2 implements Configuration, Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Map<String, Object> values;\n\n    @SuppressWarnings(\"unchecked\")\n    public ConfigurationV2(ProcessDefinitionConfiguration cfg) {\n        ObjectMapper om = new ObjectMapper();\n        om.registerModule(new Jdk8Module());\n        om.registerModule(new JavaTimeModule());\n        this.values = om.convertValue(cfg, Map.class);\n    }\n\n    @Override\n    public List<String> dependencies() {\n        return MapUtils.getList(values, Constants.Request.DEPENDENCIES_KEY, Collections.emptyList());\n    }\n\n    @Override\n    public List<String> extraDependencies() {\n        return MapUtils.getList(values, Constants.Request.EXTRA_DEPENDENCIES_KEY, Collections.emptyList());\n    }\n\n    @Override\n    public Map<String, Object> asMap() {\n        return values;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/wrapper/EffectiveProcessDefinitionProviderV2.java",
    "content": "package com.walmartlabs.concord.runtime.v2.wrapper;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.runtime.model.EffectiveProcessDefinitionProvider;\nimport com.walmartlabs.concord.runtime.model.Options;\nimport com.walmartlabs.concord.runtime.v2.ProjectSerializerV2;\nimport com.walmartlabs.concord.runtime.v2.model.*;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\n\nimport java.io.OutputStream;\nimport java.util.*;\n\npublic class EffectiveProcessDefinitionProviderV2 implements EffectiveProcessDefinitionProvider {\n\n    private final ProjectSerializerV2 serializer = new ProjectSerializerV2();\n    private final ProcessDefinition delegate;\n\n    public EffectiveProcessDefinitionProviderV2(ProcessDefinition delegate) {\n        this.delegate = delegate;\n    }\n\n    @Override\n    public void serialize(Options options, OutputStream out) throws Exception {\n        Map<String, Flow> flows = new HashMap<>(delegate.flows());\n        for (String ap : options.activeProfiles()) {\n            Profile p = delegate.profiles().get(ap);\n            if (p != null) {\n                flows.putAll(p.flows());\n            }\n        }\n\n        Map<String, Object> arguments = new LinkedHashMap<>(MapUtils.getMap(options.configuration(), \"arguments\", Collections.emptyMap()));\n        arguments.put(Constants.Context.TX_ID_KEY, options.instanceId());\n        if (options.parentInstanceId() != null) {\n            arguments.put(Constants.Request.PARENT_INSTANCE_ID_KEY, options.parentInstanceId());\n        }\n        arguments.put(Constants.Request.INITIATOR_KEY, options.configuration().get(Constants.Request.INITIATOR_KEY));\n        arguments.put(Constants.Request.PROJECT_INFO_KEY, MapUtils.getMap(options.configuration(), Constants.Request.PROJECT_INFO_KEY, Collections.emptyMap()));\n        arguments.put(Constants.Request.PROCESS_INFO_KEY, MapUtils.getMap(options.configuration(), Constants.Request.PROCESS_INFO_KEY, Collections.emptyMap()));\n\n        ProcessDefinition pd = ProcessDefinition.builder().from(delegate)\n                .configuration(ProcessDefinitionConfiguration.builder().from(delegate.configuration())\n                        .entryPoint(options.entryPoint())\n                        .arguments(arguments)\n                        .build())\n                .flows(flows)\n                .imports(Imports.builder().build())\n                .profiles(Collections.emptyMap())\n                .build();\n\n        serializer.write(pd, out);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/wrapper/FlowDefinitionV2.java",
    "content": "package com.walmartlabs.concord.runtime.v2.wrapper;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.*;\nimport com.walmartlabs.concord.runtime.v2.model.Expression;\nimport com.walmartlabs.concord.runtime.v2.model.TaskCall;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\n\npublic class FlowDefinitionV2 implements FlowDefinition {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String name;\n    private final List<Step> steps;\n\n    public FlowDefinitionV2(String name, List<com.walmartlabs.concord.runtime.v2.model.Step> steps) {\n        this.name = name;\n        this.steps = new ArrayList<>();\n        steps.stream()\n                .map(this::toStep)\n                .filter(Objects::nonNull)\n                .forEach(this.steps::add);\n    }\n\n    private Step toStep(com.walmartlabs.concord.runtime.v2.model.Step s) {\n        if (s instanceof TaskCall) {\n            TaskCall task = (TaskCall)s;\n            return TaskCallStep.builder()\n                    .name(task.getName())\n                    .input(task.getOptions().input())\n                    .location(SourceMap.from(task.getLocation()))\n                    .build();\n        } else if (s instanceof Expression) {\n            Expression expression = (Expression)s;\n            return ExpressionStep.builder()\n                    .expression(expression.getExpr())\n                    .location(SourceMap.from(expression.getLocation()))\n                    .build();\n        }\n        return null;\n    }\n\n    @Override\n    public String name() {\n        return name;\n    }\n\n    @Override\n    public List<Step> steps() {\n        return steps;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/wrapper/ProcessDefinitionV2.java",
    "content": "package com.walmartlabs.concord.runtime.v2.wrapper;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.runtime.model.*;\n\nimport java.io.Serializable;\nimport java.util.*;\n\npublic class ProcessDefinitionV2 extends EffectiveProcessDefinitionProviderV2 implements ProcessDefinition, Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Configuration cfg;\n    private final Map<String, FlowDefinition> flows;\n    private final Set<String> publicFlows;\n    private final Map<String, Profile> profiles;\n    private final List<Trigger> triggers;\n    private final Imports imports;\n    private final List<Form> forms;\n\n    public ProcessDefinitionV2(com.walmartlabs.concord.runtime.v2.model.ProcessDefinition delegate) {\n        super(delegate);\n\n        this.cfg = new ConfigurationV2(delegate.configuration());\n\n        this.flows = new HashMap<>();\n        if (delegate.flows() != null) {\n            delegate.flows().forEach((k, v) -> flows.put(k, new FlowDefinitionV2(k, v.steps())));\n        }\n\n        this.publicFlows = new HashSet<>();\n        if (delegate.publicFlows() != null) {\n            this.publicFlows.addAll(delegate.publicFlows());\n        }\n\n        this.profiles = new HashMap<>();\n        if (delegate.profiles() != null) {\n            delegate.profiles().forEach((k, v) -> profiles.put(k, new ProfileV2(v)));\n        }\n\n        this.triggers = new ArrayList<>();\n        if (delegate.triggers() != null) {\n            delegate.triggers().forEach(t -> triggers.add(new TriggerV2(t)));\n        }\n\n        this.imports = delegate.imports();\n        this.forms = new ArrayList<>();\n        if (delegate.forms() != null) {\n            delegate.forms().values().forEach(f -> forms.add(Form.builder()\n                    .name(f.name())\n                    .fields(toFields(f))\n                    .location(SourceMap.from(f.location()))\n                    .build()));\n        }\n    }\n\n    @Override\n    public String runtime() {\n        return \"concord-v2\";\n    }\n\n    @Override\n    public Configuration configuration() {\n        return cfg;\n    }\n\n    @Override\n    public Map<String, FlowDefinition> flows() {\n        return flows;\n    }\n\n    @Override\n    public Set<String> publicFlows() {\n        return publicFlows;\n    }\n\n    @Override\n    public Map<String, Profile> profiles() {\n        return profiles;\n    }\n\n    @Override\n    public List<Trigger> triggers() {\n        return triggers;\n    }\n\n    @Override\n    public Imports imports() {\n        return imports;\n    }\n\n    @Override\n    public List<Form> forms() {\n        return forms;\n    }\n\n    private static List<FormField> toFields(com.walmartlabs.concord.runtime.v2.model.Form form) {\n        List<FormField> fields = new ArrayList<>();\n        form.fields().forEach(f -> fields.add(FormField.builder()\n                .name(f.name())\n                .label(f.label())\n                .type(f.type())\n                .defaultValue(f.defaultValue())\n                .allowedValue(f.allowedValue())\n                .options(f.options())\n                .location(SourceMap.from(f.location()))\n                .build()));\n        return fields;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/wrapper/ProfileV2.java",
    "content": "package com.walmartlabs.concord.runtime.v2.wrapper;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Configuration;\nimport com.walmartlabs.concord.runtime.model.FlowDefinition;\nimport com.walmartlabs.concord.runtime.model.Profile;\n\nimport java.io.Serializable;\nimport java.util.*;\n\npublic class ProfileV2 implements Profile, Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Configuration cfg;\n    private final Set<String> publicFlows;\n    private final Map<String, FlowDefinition> flows;\n\n    public ProfileV2(com.walmartlabs.concord.runtime.v2.model.Profile delegate) {\n        this.cfg = new ConfigurationV2(delegate.configuration());\n\n        this.publicFlows = delegate.publicFlows() != null ? new HashSet<>(delegate.publicFlows()) : Collections.emptySet();\n\n        this.flows = new HashMap<>();\n        if (delegate.flows() != null) {\n            delegate.flows().forEach((k, v) -> flows.put(k, new FlowDefinitionV2(k, v.steps())));\n        }\n    }\n\n    @Override\n    public Configuration configuration() {\n        return cfg;\n    }\n\n    @Override\n    public Set<String> publicFlows() {\n        return publicFlows;\n    }\n\n    @Override\n    public Map<String, FlowDefinition> flows() {\n        return flows;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/main/java/com/walmartlabs/concord/runtime/v2/wrapper/TriggerV2.java",
    "content": "package com.walmartlabs.concord.runtime.v2.wrapper;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport com.walmartlabs.concord.runtime.model.SourceMap;\nimport com.walmartlabs.concord.runtime.model.Trigger;\n\nimport java.io.Serializable;\nimport java.util.List;\nimport java.util.Map;\n\npublic class TriggerV2 implements Trigger, Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final com.walmartlabs.concord.runtime.v2.model.Trigger delegate;\n    private final SourceMap sourceMap;\n\n    public TriggerV2(com.walmartlabs.concord.runtime.v2.model.Trigger delegate) {\n        this.delegate = delegate;\n        this.sourceMap = SourceMap.from(delegate.location());\n    }\n\n    @Override\n    public String name() {\n        return delegate.name();\n    }\n\n    @Override\n    public Map<String, Object> arguments() {\n        return delegate.arguments();\n    }\n\n    @Override\n    public Map<String, Object> conditions() {\n        return delegate.conditions();\n    }\n\n    @Override\n    public Map<String, Object> configuration() {\n        return delegate.configuration();\n    }\n\n    @Override\n    public List<String> activeProfiles() {\n        return delegate.activeProfiles();\n    }\n\n    @Override\n    public SourceMap sourceMap() {\n        return sourceMap;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/test/java/com/walmartlabs/concord/project/runtime/v2/ConcordJsonSchemaGeneratorTest.java",
    "content": "package com.walmartlabs.concord.project.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.dataformat.yaml.YAMLFactory;\nimport com.networknt.schema.JsonSchema;\nimport com.networknt.schema.JsonSchemaFactory;\nimport com.networknt.schema.SpecVersion;\nimport com.networknt.schema.ValidationMessage;\nimport com.walmartlabs.concord.runtime.v2.ConcordJsonSchemaGenerator;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Set;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\n@Disabled\npublic class ConcordJsonSchemaGeneratorTest {\n\n    @Test\n    public void validateOk() throws Exception {\n        JsonNode concordYml = new ObjectMapper(new YAMLFactory())\n                .readTree(ConcordJsonSchemaGeneratorTest.class.getResourceAsStream(\"/schema/concord.yml\"));\n\n        JsonSchemaFactory schemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);\n        JsonSchema schema = schemaFactory.getSchema(ConcordJsonSchemaGenerator.generate());\n\n        Set<ValidationMessage> validationResult = schema.validate(concordYml);\n\n        System.out.println(validationResult);\n        assertTrue(validationResult.isEmpty());\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/test/java/com/walmartlabs/concord/project/runtime/v2/ProjectLoaderV2Test.java",
    "content": "package com.walmartlabs.concord.project.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Import;\nimport com.walmartlabs.concord.imports.ImportManager;\nimport com.walmartlabs.concord.imports.ImportsListener;\nimport com.walmartlabs.concord.runtime.v2.NoopImportsNormalizer;\nimport com.walmartlabs.concord.runtime.v2.ProjectLoaderV2;\nimport com.walmartlabs.concord.runtime.v2.model.Checkpoint;\nimport com.walmartlabs.concord.runtime.v2.model.EventConfiguration;\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinitionConfiguration;\nimport org.junit.jupiter.api.Test;\n\nimport java.net.URI;\nimport java.nio.file.Paths;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.Mockito.mock;\n\npublic class ProjectLoaderV2Test {\n\n    @Test\n    @SuppressWarnings(\"unchecked\")\n    public void testMultiProjectFiles() throws Exception {\n        ProjectLoaderV2 loader = new ProjectLoaderV2(mock(ImportManager.class));\n\n        URI uri = ClassLoader.getSystemResource(\"multiProjectFile\").toURI();\n        ProjectLoaderV2.Result result = loader.load(Paths.get(uri), new NoopImportsNormalizer(), ImportsListener.NOP_LISTENER);\n        assertNotNull(result);\n        assertNotNull(result.getProjectDefinition());\n\n        ProcessDefinition pd = result.getProjectDefinition();\n\n        // configuration:\n        ProcessDefinitionConfiguration cfg = pd.configuration();\n        assertNotNull(cfg);\n\n        // configuration.debug: should be collected from ROOT concord.yml\n        assertTrue(cfg.debug());\n\n        // configuration.activeProfiles: should be collected from ROOT concord.yml\n        assertEquals(Collections.singletonList(\"concord.yml\"), cfg.activeProfiles());\n\n        // configuration.entryPoint: should be collected from ROOT concord.yml\n        assertEquals(\"root\", cfg.entryPoint());\n\n        // configuration.dependencies: should be collected from ALL *.concord.yml\n        assertEquals(Arrays.asList(\"2.concord.yml\", \"concord.yml\"), cfg.dependencies());\n\n        // configuration.arguments: should be collected from ALL *.concord.yml and mereged\n        assertEquals(\"ttt\", cfg.arguments().get(\"abc\"));\n        assertEquals(\"234\", ((Map<String, Object>) cfg.arguments().get(\"nested\")).get(\"value\"));\n\n        // configuration.meta: should be collected from ROOT concord.yml\n        assertEquals(Collections.singletonMap(\"k\", \"concord.yml\"), cfg.meta());\n\n        // configuration.events: should be collected from ROOT concord.yml\n        assertEquals(EventConfiguration.builder()\n                .recordTaskInVars(true)\n                .recordTaskOutVars(true)\n                .truncateInVars(true)\n                .truncateOutVars(true)\n                .truncateMaxStringLength(1)\n                .truncateMaxArrayLength(2)\n                .truncateMaxDepth(3)\n                .inVarsBlacklist(Arrays.asList(\"apiKey\", \"apiRootToken\"))\n                .recordTaskMeta(true)\n                .truncateMeta(true)\n                .build(), cfg.events());\n\n        // configuration.requirements: should be collected from ROOT concord.yml\n        assertEquals(Collections.singletonMap(\"req\", \"concord.yml\"), cfg.requirements());\n\n        // configuration.processTimeout: should be collected from ROOT concord.yml\n        assertEquals(\"PT1H\", cfg.processTimeout().toString());\n\n        // configuration.suspendTimeout: should be collected from ROOT concord.yml\n        assertEquals(\"PT26H\", cfg.suspendTimeout().toString());\n\n        // configuration.out: should be collected from ROOT concord.yml\n        assertEquals(Collections.singletonList(\"from-root\"), cfg.out());\n\n        // configuration.template: should be collected from ROOT concord.yml\n        assertNotNull(cfg.template());\n        assertEquals(\"mytemplate\", cfg.template());\n\n        // flows: should be collected from ALL *.concord.yml\n        // if flow has same name then most recent used\n        assertEquals(new HashSet<>(Arrays.asList(\"default\", \"flowN3\")), pd.flows().keySet());\n        assertEquals(1, pd.flows().get(\"default\").steps().size());\n        assertInstanceOf(Checkpoint.class, pd.flows().get(\"default\").steps().get(0));\n        assertEquals(\"root\", ((Checkpoint) pd.flows().get(\"default\").steps().get(0)).getName());\n\n        // publicFlows: should be collected from ROOT concord.yml\n        assertEquals(Collections.singleton(\"root\"), pd.publicFlows());\n\n        // profiles: should be collected from ALL *.concord.yml\n        // if profile has same name then most recent used\n        assertEquals(Collections.emptyMap(), pd.profiles());\n\n        // triggers: should be collected from ALL *.concord.yml\n        assertEquals(3, pd.triggers().size());\n        assertEquals(Arrays.asList(\"1.concord.yml\", \"2.concord.yml\", \"concord.yml\"), pd.triggers().stream().map(t -> t.configuration().get(\"entryPoint\")).collect(Collectors.toList()));\n\n        // imports: should be collected from ALL *.concord.yml\n        assertEquals(2, pd.imports().items().size());\n        assertEquals(Arrays.asList(\"2.concord.yml\", \"concord.yml\"), pd.imports().items().stream().map(i -> ((Import.GitDefinition) i).url()).collect(Collectors.toList()));\n\n        // forms: should be collected from ALL *.concord.yml\n        // if form has same name then most recent used\n        assertEquals(1, pd.forms().size());\n        assertNotNull(pd.forms().get(\"myForm\"));\n        assertEquals(1, pd.forms().get(\"myForm\").fields().size());\n        assertEquals(\"myName3\", pd.forms().get(\"myForm\").fields().get(0).name());\n\n        // resources: should be collected from ALL *.concord.yml\n        assertEquals(Arrays.asList(\"glob:concord/{**/,}{*.,}concord.{yml,yaml}\", \"glob:tmp/1.yml\"), pd.resources().concord());\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/test/java/com/walmartlabs/concord/project/runtime/v2/ProjectSerializerV2Test.java",
    "content": "package com.walmartlabs.concord.project.runtime.v2;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Import;\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.project.runtime.v2.parser.AbstractParserTest;\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.ProjectSerializerV2;\nimport com.walmartlabs.concord.runtime.v2.model.*;\nimport com.walmartlabs.concord.runtime.v2.parser.SimpleOptions;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.Serializable;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\nimport java.time.Duration;\nimport java.util.*;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class ProjectSerializerV2Test extends AbstractParserTest {\n\n    private final ProjectSerializerV2 serializer = new ProjectSerializerV2();\n\n    @Test\n    public void testConfiguration() throws Exception {\n        ProcessDefinitionConfiguration cfg = ProcessDefinitionConfiguration.builder()\n                .processTimeout(Duration.parse(\"PT1H\"))\n                .build();\n\n        String result = toYaml(cfg);\n        assertTrue(result.contains(\"processTimeout: \\\"PT1H\\\"\"));\n    }\n\n    @Test\n    public void testCheckpoint() throws Exception {\n        String result = toYaml(new Checkpoint(location(), \"checkpoint-1\", simpleOptions()));\n        assertEquals(\"checkpoint: \\\"checkpoint-1\\\"\\n\" +\n                \"meta:\\n\" +\n                \"  meta-1: \\\"v1\\\"\\n\" +\n                \"  meta-2: \\\"v2\\\"\\n\", result);\n    }\n\n    @Test\n    public void testExit() throws Exception {\n        String result = toYaml(new ExitStep(location()));\n        assertEquals(\"\\\"exit\\\"\\n\", result);\n    }\n\n    @Test\n    public void testExpression() throws Exception {\n        ExpressionOptions opts = ExpressionOptions.builder()\n                .out(\"out-result\")\n                .meta(meta())\n                .errorSteps(steps())\n                .build();\n\n        String result = toYaml(new Expression(location(), \"${a}\", opts));\n        assertResult(\"serializer/expressionStep.yml\", result);\n    }\n\n    @Test\n    public void testFlowCall() throws Exception {\n        FlowCallOptions opts = FlowCallOptions.builder()\n                .putInput(\"in-1\", \"v1\")\n                .addOut(\"o1\")\n                .withItems(withItems())\n                .loop(serialLoop())\n                .retry(retry())\n                .errorSteps(steps())\n                .build();\n\n        String result = toYaml(new FlowCall(location(), \"flow\", opts));\n        assertResult(\"serializer/flowCallStep.yml\", result);\n    }\n\n    @Test\n    public void testFlowCallOutMapping() throws Exception {\n        FlowCallOptions opts = FlowCallOptions.builder()\n                .putInput(\"in-1\", \"v1\")\n                .putOutMapping(\"o1\", \"${o.one}\")\n                .withItems(withItems())\n                .retry(retry())\n                .errorSteps(steps())\n                .build();\n\n        String result = toYaml(new FlowCall(location(), \"flow\", opts));\n        assertResult(\"serializer/flowCallStepOutMapping.yml\", result);\n    }\n\n    @Test\n    public void testFlowCallOutExpression() throws Exception {\n        FlowCallOptions opts = FlowCallOptions.builder()\n                .putInput(\"in-1\", \"v1\")\n                .outExpression(\"${myOutExpr}\")\n                .withItems(withItems())\n                .retry(retry())\n                .errorSteps(steps())\n                .build();\n\n        String result = toYaml(new FlowCall(location(), \"flow\", opts));\n        assertResult(\"serializer/flowCallStepOutExpr.yml\", result);\n    }\n\n    @Test\n    public void testFormCall() throws Exception {\n        FormField f = FormField.builder()\n                .name(\"field-name\")\n                .label(\"field-label\")\n                .type(\"field-type\")\n                .cardinality(com.walmartlabs.concord.forms.FormField.Cardinality.ANY)\n                .defaultValue(\"default-value\")\n                .allowedValue(\"allowed-value\")\n                .putOptions(\"o1\", \"v1\")\n                .location(location())\n                .build();\n\n        FormCallOptions opts = FormCallOptions.builder()\n                .isYield(true)\n                .saveSubmittedBy(true)\n                .putRunAs(\"user\", \"u1\")\n                .putValues(\"k\", \"v\")\n                .addFields(f)\n                .fieldsExpression(\"${fieldExpression}\")\n                .meta(meta())\n                .build();\n\n        String result = toYaml(new FormCall(location(), \"form\", opts));\n        assertResult(\"serializer/formCallStep.yml\", result);\n    }\n\n    @Test\n    public void testGroupOfSteps() throws Exception {\n        GroupOfStepsOptions opts = GroupOfStepsOptions.builder()\n                .addOut(\"out\")\n                .errorSteps(steps())\n                .withItems(withItems())\n                .meta(meta())\n                .build();\n\n        String result = toYaml(new GroupOfSteps(location(), steps(), opts));\n        assertResult(\"serializer/groupOfSteps.yml\", result);\n    }\n\n    @Test\n    public void testIfStep() throws Exception {\n        List<Step> thenSteps = Collections.singletonList(new Expression(location(), \"${exprForThen}\", ExpressionOptions.builder().build()));\n        List<Step> elseSteps = steps();\n        String result = toYaml(new IfStep(location(), \"${ifExpression}\", thenSteps, elseSteps, simpleOptions()));\n        assertResult(\"serializer/ifStep.yml\", result);\n    }\n\n    @Test\n    public void testParallelBlock() throws Exception {\n        ParallelBlockOptions opts = ParallelBlockOptions.builder()\n                .addOut(\"out\")\n                .meta(meta())\n                .build();\n\n        List<Step> steps = new ArrayList<>();\n        steps.add(new Checkpoint(location(), \"ch1\", simpleOptions()));\n        steps.add(new Checkpoint(location(), \"ch2\", simpleOptions()));\n\n        String result = toYaml(new ParallelBlock(location(), steps, opts));\n        assertResult(\"serializer/parallelBlock.yml\", result);\n    }\n\n    @Test\n    public void testParallelBlockOutExpr() throws Exception {\n        ParallelBlockOptions opts = ParallelBlockOptions.builder()\n                .putOutExpr(\"out\", \"${expr}\")\n                .meta(meta())\n                .build();\n\n        List<Step> steps = new ArrayList<>();\n        steps.add(new Checkpoint(location(), \"ch1\", simpleOptions()));\n        steps.add(new Checkpoint(location(), \"ch2\", simpleOptions()));\n\n        String result = toYaml(new ParallelBlock(location(), steps, opts));\n        assertResult(\"serializer/parallelBlockOutExpr.yml\", result);\n    }\n\n    @Test\n    public void testReturnStep() throws Exception {\n        String result = toYaml(new ReturnStep(location()));\n        assertResult(\"serializer/returnStep.yml\", result);\n    }\n\n    @Test\n    public void testScriptCall() throws Exception {\n        ScriptCallOptions opts = ScriptCallOptions.builder()\n                .body(\"print(\\\"Hello, \\\", myVar)\")\n                .putInput(\"in\", \"v\")\n                .withItems(withItems())\n                .loop(serialLoop())\n                .retry(retry())\n                .errorSteps(steps())\n                .meta(meta())\n                .build();\n\n        String result = toYaml(new ScriptCall(location(), \"js\", opts));\n        assertResult(\"serializer/scriptStep.yml\", result);\n    }\n\n    @Test\n    public void testSetVariablesStep() throws Exception {\n        String result = toYaml(new SetVariablesStep(location(), Collections.singletonMap(\"k1\", \"v1\"), simpleOptions()));\n        assertResult(\"serializer/setVariablesStep.yml\", result);\n    }\n\n    @Test\n    public void testSuspendStep() throws Exception {\n        String result = toYaml(new SuspendStep(location(), \"event\", simpleOptions()));\n        assertResult(\"serializer/suspendStep.yml\", result);\n    }\n\n    @Test\n    public void testSwitchStep() throws Exception {\n        List<Map.Entry<String, List<Step>>> caseSteps = Collections.singletonList(new AbstractMap.SimpleEntry<>(\"red\", steps()));\n        List<Step> defaultSteps = Collections.singletonList(new Expression(location(), \"defaultStep\", ExpressionOptions.builder().build()));\n\n        String result = toYaml(new SwitchStep(location(), \"${switch}\", caseSteps, defaultSteps, simpleOptions()));\n        assertResult(\"serializer/switchStep.yml\", result);\n    }\n\n    @Test\n    public void testTaskCall() throws Exception {\n        TaskCallOptions opts = TaskCallOptions.builder()\n                .putInput(\"msg\", \"BOO\")\n                .out(\"out\")\n                .withItems(withItems())\n                .loop(serialLoop())\n                .retry(retry())\n                .errorSteps(steps())\n                .meta(meta())\n                .build();\n\n        String result = toYaml(new TaskCall(location(), \"log\", opts));\n        assertResult(\"serializer/taskCallStep.yml\", result);\n    }\n\n    @Test\n    public void testTaskCallOutExpr() throws Exception {\n        TaskCallOptions opts = TaskCallOptions.builder()\n                .putInput(\"msg\", \"BOO\")\n                .putOutExpr(\"result\", \"${result}\")\n                .putOutExpr(\"v1\", \"${result.v1}\")\n                .withItems(withItems())\n                .retry(retry())\n                .errorSteps(steps())\n                .meta(meta())\n                .build();\n\n        String result = toYaml(new TaskCall(location(), \"log\", opts));\n        assertResult(\"serializer/taskCallStepOutExpr.yml\", result);\n    }\n\n    @Test\n    public void testTaskCallParallelWithItems() throws Exception {\n        TaskCallOptions opts = TaskCallOptions.builder()\n                .putInput(\"msg\", \"BOO\")\n                .out(\"out\")\n                .withItems(parallelWithItems())\n                .retry(retry())\n                .errorSteps(steps())\n                .meta(meta())\n                .build();\n\n        String result = toYaml(new TaskCall(location(), \"log\", opts));\n        assertResult(\"serializer/taskCallStepParallel.yml\", result);\n    }\n\n    @Test\n    public void testExprCallOutExpr() throws Exception {\n        ExpressionOptions opts = ExpressionOptions.builder()\n                .putOutExpr(\"out-result\", \"${result.first}\")\n                .meta(meta())\n                .errorSteps(steps())\n                .build();\n\n        String result = toYaml(new Expression(location(), \"${a}\", opts));\n        assertResult(\"serializer/expressionStepOutExpr.yml\", result);\n    }\n\n    @Test\n    public void testProcessDefinition() throws Exception {\n        Map<String, Form> forms = Collections.singletonMap(\"form1\", Form.builder()\n                .name(\"form1\")\n                .addFields(FormField.builder()\n                        .name(\"field1\")\n                        .type(\"string\")\n                        .cardinality(com.walmartlabs.concord.forms.FormField.Cardinality.ANY)\n                        .location(location())\n                        .build())\n                .location(location())\n                .build());\n\n        Trigger trigger = Trigger.builder()\n                .name(\"github\")\n                .location(location())\n                .putConfiguration(\"entryPoint\", \"www\")\n                .putConfiguration(\"useInitiator\", true)\n                .putConditions(\"type\", \"push\")\n                .putConditions(\"status\", Arrays.asList(\"opened\", \"reopened\"))\n                .putArguments(\"arg\", \"arg-value\")\n                .addActiveProfiles(\"p1\")\n                .build();\n\n        Imports imports = Imports.of(Collections.singletonList(Import.MvnDefinition.builder()\n                .url(\"http://url\")\n                .dest(\"dest\")\n                .build()));\n\n        ProcessDefinitionConfiguration cfg = ProcessDefinitionConfiguration.builder()\n                .parallelLoopParallelism(123)\n                .build();\n\n        ProcessDefinition pd = ProcessDefinition.builder()\n                .configuration(cfg)\n                .forms(forms)\n                .putFlows(\"flow1\", Flow.of(Location.builder().build(), steps()))\n                .addPublicFlows(\"flow1\")\n                .addTriggers(trigger)\n                .imports(imports)\n                .build();\n\n        String result = toYaml(pd);\n        assertResult(\"serializer/processDefinition.yml\", result);\n    }\n\n    private void assertResult(String resource, String result) throws Exception {\n        URI uri = ClassLoader.getSystemResource(resource).toURI();\n        String expected = new String(Files.readAllBytes(Paths.get(uri)));\n        assertEquals(expected, result);\n    }\n\n    private static Location location() {\n        return Location.builder()\n                .fileName(\"test.concord.yml\")\n                .build();\n    }\n\n    private static SimpleOptions simpleOptions() {\n        return SimpleOptions.of(meta());\n    }\n\n    private static Map<String, Serializable> meta() {\n        Map<String, Serializable> options = new LinkedHashMap<>();\n        options.put(\"meta-1\", \"v1\");\n        options.put(\"meta-2\", \"v2\");\n        return options;\n    }\n\n    private static WithItems withItems() {\n        ArrayList<String> items = new ArrayList<>();\n        items.add(\"item1\");\n        items.add(\"item2\");\n        return WithItems.of(items, WithItems.Mode.SERIAL);\n    }\n\n    private static WithItems parallelWithItems() {\n        ArrayList<String> items = new ArrayList<>();\n        items.add(\"item1\");\n        items.add(\"item2\");\n        return WithItems.of(items, WithItems.Mode.PARALLEL);\n    }\n\n    private static Loop serialLoop() {\n        ArrayList<String> items = new ArrayList<>();\n        items.add(\"item1\");\n        items.add(\"item2\");\n        return Loop.builder()\n                .items(items)\n                .mode(Loop.Mode.SERIAL)\n                .build();\n    }\n\n    private static Retry retry() {\n        return Retry.builder()\n                .times(1)\n                .delay(Duration.ofDays(1))\n                .timesExpression(\"${times}\")\n                .delayExpression(\"${delay}\")\n                .input(Collections.singletonMap(\"retry-input\", \"v1\"))\n                .build();\n    }\n\n    private static List<Step> steps() {\n        return Collections.singletonList(new Checkpoint(location(), \"chp1\", simpleOptions()));\n    }\n\n    private String toYaml(Object o) throws Exception {\n        return serializer.getObjectMapper().writeValueAsString(o);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/test/java/com/walmartlabs/concord/project/runtime/v2/parser/AbstractParserTest.java",
    "content": "package com.walmartlabs.concord.project.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.ImportManager;\nimport com.walmartlabs.concord.runtime.v2.ProjectLoaderV2;\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\n\nimport java.net.URI;\nimport java.nio.file.Paths;\n\nimport static org.mockito.Mockito.mock;\n\npublic abstract class AbstractParserTest {\n\n    protected static ProcessDefinition load(String resource) throws Exception {\n        URI uri = ClassLoader.getSystemResource(resource).toURI();\n\n        ProjectLoaderV2 loader = new ProjectLoaderV2(mock(ImportManager.class));\n        return loader.loadFromFile(Paths.get(uri)).getProjectDefinition();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/test/java/com/walmartlabs/concord/project/runtime/v2/parser/YamlErrorParserTest.java",
    "content": "package com.walmartlabs.concord.project.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.exception.YamlParserException;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.fail;\n\npublic class YamlErrorParserTest extends AbstractParserTest {\n\n    @Test\n    public void test001() throws Exception {\n        String msg = \"(001.yml): Error @ line: 1, col: 10. Invalid value type, expected: IMPORTS, got: STRING\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'imports' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/imports/001.yml\", msg);\n    }\n\n    @Test\n    public void test002() throws Exception {\n        String msg = \"(002.yml): Error @ line: 2, col: 3. Invalid value type, expected: IMPORTS, got: OBJECT\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'imports' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/imports/002.yml\", msg);\n    }\n\n    @Test\n    public void test003() throws Exception {\n        String msg = \"(003.yml): Error @ line: 2, col: 9. Invalid value type, expected: GIT_IMPORT, got: NULL. Remove attribute or complete the definition\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'git' @ line: 2, col: 5\\n\" +\n                \"\\t\\t'imports' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/imports/003.yml\", msg);\n    }\n\n    @Test\n    public void test004() throws Exception {\n        String msg = \"(004.yml): Error @ line: 2, col: 10. Invalid value type, expected: GIT_IMPORT, got: STRING\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'git' @ line: 2, col: 5\\n\" +\n                \"\\t\\t'imports' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/imports/004.yml\", msg);\n    }\n\n    @Test\n    public void test005() throws Exception {\n        String msg = \"(005.yml): Error @ line: 2, col: 9. Invalid value type, expected: GIT_IMPORT, got: NULL. Remove attribute or complete the definition\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'git' @ line: 2, col: 5\\n\" +\n                \"\\t\\t'imports' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/imports/005.yml\", msg);\n    }\n\n    @Test\n    public void test006() throws Exception {\n        String msg = \"(006.yml): Error @ line: 3, col: 13. Invalid value type, expected: STRING, got: INT\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'name' @ line: 3, col: 7\\n\" +\n                \"\\t\\t'git' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'imports' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/imports/006.yml\", msg);\n    }\n\n    @Test\n    public void test007() throws Exception {\n        String msg = \"(007.yml): Error @ line: 4, col: 16. Invalid value type, expected: STRING, got: INT\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'version' @ line: 4, col: 7\\n\" +\n                \"\\t\\t'git' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'imports' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/imports/007.yml\", msg);\n    }\n\n    @Test\n    public void test008() throws Exception {\n        String msg = \"(008.yml): Error @ line: 5, col: 12. Invalid value type, expected: STRING, got: NULL. Remove attribute or complete the definition\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'path' @ line: 5, col: 7\\n\" +\n                \"\\t\\t'git' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'imports' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/imports/008.yml\", msg);\n    }\n\n    @Test\n    public void test009() throws Exception {\n        String msg = \"(009.yml): Error @ line: 8, col: 14. Invalid value type, expected: STRING, got: INT\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'org' @ line: 8, col: 9\\n\" +\n                \"\\t\\t'secret' @ line: 7, col: 7\\n\" +\n                \"\\t\\t\\t'git' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t\\t'imports' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/imports/009.yml\", msg);\n    }\n\n    @Test\n    public void test010() throws Exception {\n        String msg = \"(010.yml): Error @ n/a. Mandatory parameter 'name' not found\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'secret' @ line: 7, col: 7\\n\" +\n                \"\\t\\t'git' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'imports' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/imports/010.yml\", msg);\n    }\n\n    @Test\n    public void test011() throws Exception {\n        String msg = \"(011.yml): Error @ line: 2, col: 5. Unknown options: ['git-trash' @ line: 2, col: 5], expected: [git, mvn]. Remove invalid options and/or fix indentation\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'imports' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/imports/011.yml\", msg);\n    }\n\n    @Test\n    public void test012() throws Exception {\n        String msg = \"(012.yml): Error @ line: 4, col: 16. Invalid value type, expected: ARRAY_OF_PATTERN, got: STRING\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'exclude' @ line: 4, col: 7\\n\" +\n                \"\\t\\t'git' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'imports' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/imports/012.yml\", msg);\n    }\n\n    @Test\n    public void test013() throws Exception {\n        String msg = \"(013.yml): Error @ line: 7, col: 14. Unknown options: ['trash' [STRING] @ line: 7, col: 14], expected: [dest, exclude, name, path, secret, url, version]. Remove invalid options and/or fix indentation\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'git' @ line: 2, col: 5\\n\" +\n                \"\\t\\t'imports' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/imports/013.yml\", msg);\n    }\n\n    @Test\n    public void test014() throws Exception {\n        String msg = \"(014.yml): Error @ line: 2, col: 10. Invalid value type, expected: MVN_IMPORT, got: STRING\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'mvn' @ line: 2, col: 5\\n\" +\n                \"\\t\\t'imports' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/imports/014.yml\", msg);\n    }\n\n    @Test\n    public void test015() throws Exception {\n        String msg = \"(015.yml): Error @ line: 6, col: 11. Invalid value type, expected: PATTERN, got: INT\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'exclude' @ line: 4, col: 7\\n\" +\n                \"\\t\\t'git' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'imports' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/imports/015.yml\", msg);\n    }\n\n    @Test\n    public void test016() throws Exception {\n        String msg = \"(016.yml): Error @ line: 6, col: 11. Invalid value type, expected: PATTERN, got: STRING. Error info: Unclosed character class near index 1\\n\" +\n                \"[.\\n\" +\n                \" ^\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'exclude' @ line: 4, col: 7\\n\" +\n                \"\\t\\t'git' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'imports' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/imports/016.yml\", msg);\n    }\n\n    @Test\n    public void test101() throws Exception {\n        String msg = \"(001.yml): Error @ line: 1, col: 11. Invalid value type, expected: TRIGGER, got: STRING\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/001.yml\", msg);\n    }\n\n    @Test\n    public void test102() throws Exception {\n        String msg = \"(002.yml): Error @ line: 2, col: 12. Invalid value type, expected: GITHUB_TRIGGER, got: NULL. Remove attribute or complete the definition\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'github' @ line: 2, col: 5\\n\" +\n                \"\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/002.yml\", msg);\n    }\n\n    @Test\n    public void test103() throws Exception {\n        String msg = \"(003.yml): Error @ line: 3, col: 16. Invalid value type, expected: INT, got: STRING\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'version' @ line: 3, col: 16\\n\" +\n                \"\\t\\t'github' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/003.yml\", msg);\n    }\n\n    @Test\n    public void test104() throws Exception {\n        String msg = \"(004.yml): Error @ n/a. Mandatory parameters 'entryPoint, conditions' not found\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'github' @ line: 2, col: 5\\n\" +\n                \"\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/004.yml\", msg);\n    }\n\n    @Test\n    public void test105() throws Exception {\n        String msg = \"(005.yml): Error @ line: 4, col: 19. Invalid value type, expected: STRING, got: INT\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'entryPoint' @ line: 4, col: 7\\n\" +\n                \"\\t\\t'github' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/005.yml\", msg);\n    }\n\n    @Test\n    public void test106() throws Exception {\n        String msg = \"(006.yml): Error @ n/a. Mandatory parameter 'conditions' not found\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'github' @ line: 2, col: 5\\n\" +\n                \"\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/006.yml\", msg);\n    }\n\n    @Test\n    public void test107() throws Exception {\n        String msg = \"(007.yml): Error @ line: 5, col: 19. Invalid value type, expected: GITHUB_TRIGGER_CONDITIONS, got: INT\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'conditions' @ line: 5, col: 7\\n\" +\n                \"\\t\\t'github' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/007.yml\", msg);\n    }\n\n    @Test\n    public void test108() throws Exception {\n        String msg = \"(008.yml): Error @ n/a. Mandatory parameter 'type' not found\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'conditions' @ line: 5, col: 7\\n\" +\n                \"\\t\\t'github' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/008.yml\", msg);\n    }\n\n    @Test\n    public void test109() throws Exception {\n        String msg = \"(009.yml): Error @ line: 7, col: 23. Invalid value type, expected: ARRAY_OF_STRING, got: INT\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'activeProfiles' @ line: 7, col: 7\\n\" +\n                \"\\t\\t'github' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/009.yml\", msg);\n    }\n\n    @Test\n    public void test110() throws Exception {\n        String msg = \"(010.yml): Error @ line: 9, col: 21. Invalid value type, expected: BOOLEAN, got: STRING\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'useInitiator' @ line: 9, col: 7\\n\" +\n                \"\\t\\t'github' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/010.yml\", msg);\n    }\n\n    @Test\n    public void test111() throws Exception {\n        String msg = \"(011.yml): Error @ line: 10, col: 25. Invalid value type, expected: BOOLEAN, got: STRING\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'useEventCommitId' @ line: 10, col: 7\\n\" +\n                \"\\t\\t'github' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/011.yml\", msg);\n    }\n\n    @Test\n    public void test112() throws Exception {\n        String msg = \"(012.yml): Error @ line: 11, col: 18. Invalid value type, expected: GITHUB_EXCLUSIVE_MODE, got: INT\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'exclusive' @ line: 11, col: 7\\n\" +\n                \"\\t\\t'github' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/012.yml\", msg);\n    }\n\n    @Test\n    public void test113() throws Exception {\n        String msg = \"(013.yml): Error @ line: 13, col: 18. Invalid value type, expected: OBJECT, got: INT\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'arguments' @ line: 13, col: 7\\n\" +\n                \"\\t\\t'github' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/013.yml\", msg);\n    }\n\n    @Test\n    public void test114() throws Exception {\n        String msg = \"(014.yml): Error @ line: 15, col: 20. Invalid value type, expected: PATTERN, got: STRING. Error info: Dangling meta character '*' near index 0\\n\" +\n                \"*\\n\" +\n                \"^\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'githubOrg' @ line: 15, col: 9\\n\" +\n                \"\\t\\t'conditions' @ line: 13, col: 7\\n\" +\n                \"\\t\\t\\t'github' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/014.yml\", msg);\n    }\n\n    @Test\n    public void test115() throws Exception {\n        String msg = \"(015.yml): Error @ n/a. Version 1 of GitHub triggers is not supported\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'github' @ line: 2, col: 5\\n\" +\n                \"\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/015.yml\", msg);\n    }\n\n    @Test\n    public void test116() throws Exception {\n        String msg = \"(016.yml): Error @ line: 2, col: 10. Invalid value type, expected: CRON_TRIGGER, got: NULL. Remove attribute or complete the definition\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'cron' @ line: 2, col: 5\\n\" +\n                \"\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/016.yml\", msg);\n    }\n\n    @Test\n    public void test117() throws Exception {\n        String msg = \"(017.yml): Error @ n/a. Mandatory parameter 'entryPoint' not found\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'cron' @ line: 2, col: 5\\n\" +\n                \"\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/017.yml\", msg);\n    }\n\n    @Test\n    public void test118() throws Exception {\n        String msg = \"(018.yml): Error @ line: 3, col: 13. Invalid value type, expected: STRING, got: INT\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'spec' @ line: 3, col: 7\\n\" +\n                \"\\t\\t'cron' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/018.yml\", msg);\n    }\n\n    @Test\n    public void test119() throws Exception {\n        String msg = \"(019.yml): Error @ line: 4, col: 19. Invalid value type, expected: STRING, got: INT\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'entryPoint' @ line: 4, col: 7\\n\" +\n                \"\\t\\t'cron' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/019.yml\", msg);\n    }\n\n    @Test\n    public void test120() throws Exception {\n        String msg = \"(020.yml): Error @ line: 5, col: 23. Invalid value type, expected: ARRAY_OF_STRING, got: INT\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'activeProfiles' @ line: 5, col: 7\\n\" +\n                \"\\t\\t'cron' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/020.yml\", msg);\n    }\n\n    @Test\n    public void test121() throws Exception {\n        String msg = \"(021.yml): Error @ line: 7, col: 18. Invalid value type, expected: OBJECT, got: INT\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'arguments' @ line: 7, col: 7\\n\" +\n                \"\\t\\t'cron' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/021.yml\", msg);\n    }\n\n    @Test\n    public void test122() throws Exception {\n        String msg = \"(022.yml): Error @ line: 2, col: 12. Invalid value type, expected: MANUAL_TRIGGER, got: NULL. Remove attribute or complete the definition\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'manual' @ line: 2, col: 5\\n\" +\n                \"\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/022.yml\", msg);\n    }\n\n    @Test\n    public void test123() throws Exception {\n        String msg = \"(023.yml): Error @ n/a. Mandatory parameter 'entryPoint' not found\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'manual' @ line: 2, col: 5\\n\" +\n                \"\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/023.yml\", msg);\n    }\n\n    @Test\n    public void test124() throws Exception {\n        String msg = \"(024.yml): Error @ line: 5, col: 23. Invalid value type, expected: ARRAY_OF_STRING, got: INT\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'activeProfiles' @ line: 5, col: 7\\n\" +\n                \"\\t\\t'manual' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/024.yml\", msg);\n    }\n\n    @Test\n    public void test125() throws Exception {\n        String msg = \"(025.yml): Error @ n/a. Version 1 of oneops trigger not supported\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'oneops' @ line: 2, col: 5\\n\" +\n                \"\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/025.yml\", msg);\n    }\n\n    @Test\n    public void test126() throws Exception {\n        String msg = \"(026.yml): Error @ n/a. Mandatory parameter 'conditions' not found\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'oneops' @ line: 2, col: 5\\n\" +\n                \"\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/026.yml\", msg);\n    }\n\n    @Test\n    public void test127() throws Exception {\n        String msg = \"(027.yml): Error @ line: 7, col: 14. Unknown options: ['trash' [STRING] @ line: 7, col: 14], expected: [activeProfiles, arguments, conditions, entryPoint, exclusive, useInitiator, version]. Remove invalid options and/or fix indentation\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'oneops' @ line: 2, col: 5\\n\" +\n                \"\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/027.yml\", msg);\n    }\n\n    @Test\n    public void test128() throws Exception {\n        String msg = \"(028.yml): Error @ line: 5, col: 19. Invalid value type, expected: OBJECT, got: INT\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'conditions' @ line: 5, col: 7\\n\" +\n                \"\\t\\t'example' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/028.yml\", msg);\n    }\n\n    @Test\n    public void test129() throws Exception {\n        String msg = \"(029.yml): Error @ line: 8, col: 11. Invalid value type, expected: PATTERN, got: ARRAY\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'githubOrg' @ line: 7, col: 9\\n\" +\n                \"\\t\\t'conditions' @ line: 5, col: 7\\n\" +\n                \"\\t\\t\\t'github' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/029.yml\", msg);\n    }\n\n    @Test\n    public void test130() throws Exception {\n        String msg = \"(030.yml): Error @ line: 5, col: 17. Invalid value type, expected: TIMEZONE, got: STRING. Error info: Unknown timezone: 'test'\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'timezone' @ line: 5, col: 7\\n\" +\n                \"\\t\\t'cron' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/030.yml\", msg);\n    }\n\n    @Test\n    public void test131() throws Exception {\n        String msg = \"(031.yml): Error @ line: 6, col: 14. Unknown options: ['trash' [STRING] @ line: 6, col: 14], expected: [activeProfiles, arguments, entryPoint, exclusive, runAs, spec, timezone]. Remove invalid options and/or fix indentation\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'cron' @ line: 2, col: 5\\n\" +\n                \"\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/031.yml\", msg);\n    }\n\n    @Test\n    public void test131_1() throws Exception {\n        String msg = \"(031_1.yml): Error @ n/a. Mandatory parameter 'withSecret' not found\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'runAs' @ line: 5, col: 7\\n\" +\n                \"\\t\\t'cron' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/031_1.yml\", msg);\n    }\n\n    @Test\n    public void test132() throws Exception {\n        String msg = \"(032.yml): Error @ line: 8, col: 13. Invalid value type, expected: GITHUB_REPOSITORY_INFO, got: INT\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'repositoryInfo' @ line: 7, col: 9\\n\" +\n                \"\\t\\t'conditions' @ line: 5, col: 7\\n\" +\n                \"\\t\\t\\t'github' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/032.yml\", msg);\n    }\n\n    @Test\n    public void test133() throws Exception {\n        String msg = \"(033.yml): Error @ line: 7, col: 24. Invalid value type, expected: REPOSITORY_INFO, got: NULL. Remove attribute or complete the definition\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'repositoryInfo' @ line: 7, col: 9\\n\" +\n                \"\\t\\t'conditions' @ line: 5, col: 7\\n\" +\n                \"\\t\\t\\t'github' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/033.yml\", msg);\n    }\n\n    @Test\n    public void test134() throws Exception {\n        String msg = \"(034.yml): Error @ line: 8, col: 20. Unknown options: ['trash' [STRING] @ line: 8, col: 20], expected: [branch, enabled, projectId, repository, repositoryId]. Remove invalid options and/or fix indentation\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'repositoryInfo' @ line: 7, col: 9\\n\" +\n                \"\\t\\t'conditions' @ line: 5, col: 7\\n\" +\n                \"\\t\\t\\t'github' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/034.yml\", msg);\n    }\n\n    @Test\n    public void test135() throws Exception {\n        String msg = \"(035.yml): Error @ line: 8, col: 22. Invalid value type, expected: BOOLEAN, got: STRING\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'enabled' @ line: 8, col: 13\\n\" +\n                \"\\t\\t'repositoryInfo' @ line: 7, col: 9\\n\" +\n                \"\\t\\t\\t'conditions' @ line: 5, col: 7\\n\" +\n                \"\\t\\t\\t\\t'github' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/035.yml\", msg);\n    }\n\n    @Test\n    public void test137() throws Exception {\n        String msg = \"(037.yml): Error @ n/a. One of mandatory parameters 'group, groupBy' not found\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'exclusive' @ line: 5, col: 7\\n\" +\n                \"\\t\\t'github' @ line: 2, col: 5\\n\" +\n                \"\\t\\t\\t'triggers' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/triggers/037.yml\", msg);\n    }\n\n    @Test\n    public void test200() throws Exception {\n        String msg =\n                \"(000.yml): Error @ line: 3, col: 12. Invalid value type, expected: STRING, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'task' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/tasks/000.yml\", msg);\n    }\n\n    @Test\n    public void test201() throws Exception {\n        String msg =\n                \"(001.yml): Error @ line: 3, col: 13. Invalid value type, expected: STRING, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'task' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/tasks/001.yml\", msg);\n    }\n\n    @Test\n    public void test202() throws Exception {\n        String msg =\n                \"(002.yml): Error @ line: 4, col: 11. Invalid value type, expected: STRING or OBJECT, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'out' @ line: 4, col: 7\\n\" +\n                        \"\\t\\t'task' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/tasks/002.yml\", msg);\n    }\n\n    @Test\n    public void test203() throws Exception {\n        String msg =\n                \"(003.yml): Error @ line: 4, col: 12. Invalid value type, expected: STRING or OBJECT, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'out' @ line: 4, col: 7\\n\" +\n                        \"\\t\\t'task' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/tasks/003.yml\", msg);\n    }\n\n    @Test\n    public void test205() throws Exception {\n        String msg =\n                \"(005.yml): Error @ line: 5, col: 10. Invalid value type, expected: OBJECT or EXPRESSION, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'in' @ line: 5, col: 7\\n\" +\n                        \"\\t\\t'task' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/tasks/005.yml\", msg);\n    }\n\n    @Test\n    public void test206() throws Exception {\n        String msg =\n                \"(006.yml): Error @ line: 5, col: 11. Invalid value type, expected: OBJECT or EXPRESSION, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'in' @ line: 5, col: 7\\n\" +\n                        \"\\t\\t'task' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/tasks/006.yml\", msg);\n    }\n\n    @Test\n    @Disabled(\"we allow nulls in kv now\")\n    public void test207() throws Exception {\n        String msg =\n                \"(007.yml): Error @ line: 8, col: 12. Invalid value type of 'k3' parameter, expected: NON_NULL, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'task' @ line: 3, col: 7\";\n\n        assertErrorMessage(\"errors/tasks/007.yml\", msg);\n    }\n\n    @Test\n    public void test208() throws Exception {\n        String msg =\n                \"(008.yml): Error @ line: 9, col: 17. Invalid value type, expected: NON_NULL, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'withItems' @ line: 9, col: 7\\n\" +\n                        \"\\t\\t'task' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/tasks/008.yml\", msg);\n    }\n\n    @Test\n    public void test209() throws Exception {\n        String msg =\n                \"(009.yml): Error @ line: 10, col: 13. Invalid value type, expected: RETRY, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'retry' @ line: 10, col: 7\\n\" +\n                        \"\\t\\t'task' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/tasks/009.yml\", msg);\n    }\n\n    @Test\n    public void test210() throws Exception {\n        String msg =\n                \"(010.yml): Error @ line: 10, col: 14. Invalid value type, expected: RETRY, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'retry' @ line: 10, col: 7\\n\" +\n                        \"\\t\\t'task' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/tasks/010.yml\", msg);\n    }\n\n    @Test\n    public void test211() throws Exception {\n        String msg =\n                \"(011.yml): Error @ line: 11, col: 15. Invalid value type, expected: INT or EXPRESSION, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'times' @ line: 11, col: 9\\n\" +\n                        \"\\t\\t'retry' @ line: 10, col: 7\\n\" +\n                        \"\\t\\t\\t'task' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/tasks/011.yml\", msg);\n    }\n\n    @Test\n    public void test212() throws Exception {\n        String msg =\n                \"(012.yml): Error @ line: 12, col: 15. Invalid value type, expected: INT or EXPRESSION, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'delay' @ line: 12, col: 9\\n\" +\n                        \"\\t\\t'retry' @ line: 10, col: 7\\n\" +\n                        \"\\t\\t\\t'task' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/tasks/012.yml\", msg);\n    }\n\n    @Test\n    public void test213() throws Exception {\n        String msg =\n                \"(013.yml): Error @ line: 13, col: 12. Invalid value type, expected: OBJECT, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'in' @ line: 13, col: 9\\n\" +\n                        \"\\t\\t'retry' @ line: 10, col: 7\\n\" +\n                        \"\\t\\t\\t'task' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/tasks/013.yml\", msg);\n    }\n\n    @Test\n    public void test214() throws Exception {\n        String msg =\n                \"(014.yml): Error @ line: 13, col: 13. Invalid value type, expected: OBJECT, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'in' @ line: 13, col: 9\\n\" +\n                        \"\\t\\t'retry' @ line: 10, col: 7\\n\" +\n                        \"\\t\\t\\t'task' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/tasks/014.yml\", msg);\n    }\n\n    @Test\n    public void test215() throws Exception {\n        String msg =\n                \"(015.yml): Error @ line: 15, col: 14. Unknown options: ['trash' [STRING] @ line: 15, col: 14], expected: [error, ignoreErrors, in, loop, meta, name, out, parallelWithItems, retry, withItems]. Remove invalid options and/or fix indentation\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'task' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/tasks/015.yml\", msg);\n    }\n\n    @Test\n    public void test216() throws Exception {\n        String msg =\n                \"(016.yml): Error @ line: 15, col: 12. Invalid value type, expected: OBJECT, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'meta' @ line: 15, col: 7\\n\" +\n                        \"\\t\\t'task' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/tasks/016.yml\", msg);\n    }\n\n    @Test\n    public void test217() throws Exception {\n        String msg =\n                \"(017.yml): Error @ line: 15, col: 13. Invalid value type, expected: OBJECT, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'meta' @ line: 15, col: 7\\n\" +\n                        \"\\t\\t'task' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/tasks/017.yml\", msg);\n    }\n\n    @Test\n    public void test218() throws Exception {\n        String msg =\n                \"(018.yml): Error @ line: 11, col: 16. Invalid value type, expected: INT or EXPRESSION, got: STRING\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'times' @ line: 11, col: 9\\n\" +\n                        \"\\t\\t'retry' @ line: 10, col: 7\\n\" +\n                        \"\\t\\t\\t'task' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/tasks/018.yml\", msg);\n    }\n\n    @Test\n    public void test219() throws Exception {\n        String msg =\n                \"(019.yml): Error @ line: 3, col: 13. Invalid value type, expected: STRING, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'name' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/tasks/019.yml\", msg);\n    }\n\n    @Test\n    public void test220() throws Exception {\n        String msg =\n                \"(020.yml): Error @ line: 7, col: 15. Invalid value: trash, expected: [SERIAL, PARALLEL]\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'mode' @ line: 7, col: 9\\n\" +\n                        \"\\t\\t'loop' @ line: 5, col: 7\\n\" +\n                        \"\\t\\t\\t'task' @ line: 4, col: 7\\n\" +\n                        \"\\t\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/tasks/020.yml\", msg);\n    }\n\n    @Test\n    public void test221() throws Exception {\n        String msg =\n                \"(021.yml): Error @ line: 7, col: 22. Invalid value type, expected: int or expression, got: STRING\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'parallelism' @ line: 7, col: 9\\n\" +\n                        \"\\t\\t'loop' @ line: 5, col: 7\\n\" +\n                        \"\\t\\t\\t'task' @ line: 4, col: 7\\n\" +\n                        \"\\t\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/tasks/021.yml\", msg);\n    }\n\n    @Test\n    public void test300() throws Exception {\n        String msg =\n                \"(000.yml): Error @ line: 3, col: 12. Invalid value type, expected: STRING, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'call' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/flowCall/000.yml\", msg);\n    }\n\n    @Test\n    public void test301() throws Exception {\n        String msg =\n                \"(001.yml): Error @ line: 3, col: 13. Invalid value type, expected: STRING, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'call' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/flowCall/001.yml\", msg);\n    }\n\n    @Test\n    public void test302() throws Exception {\n        String msg =\n                \"(002.yml): Error @ line: 4, col: 11. Invalid value type, expected: STRING or ARRAY_OF_STRING or OBJECT, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'out' @ line: 4, col: 7\\n\" +\n                        \"\\t\\t'call' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/flowCall/002.yml\", msg);\n    }\n\n    @Test\n    public void test303() throws Exception {\n        String msg =\n                \"(003.yml): Error @ line: 4, col: 12. Invalid value type, expected: STRING or ARRAY_OF_STRING or OBJECT, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'out' @ line: 4, col: 7\\n\" +\n                        \"\\t\\t'call' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/flowCall/003.yml\", msg);\n    }\n\n    @Test\n    public void test305() throws Exception {\n        String msg =\n                \"(005.yml): Error @ line: 5, col: 10. Invalid value type, expected: OBJECT or EXPRESSION, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'in' @ line: 5, col: 7\\n\" +\n                        \"\\t\\t'call' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/flowCall/005.yml\", msg);\n    }\n\n    @Test\n    public void test306() throws Exception {\n        String msg =\n                \"(006.yml): Error @ line: 5, col: 11. Invalid value type, expected: OBJECT or EXPRESSION, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'in' @ line: 5, col: 7\\n\" +\n                        \"\\t\\t'call' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/flowCall/006.yml\", msg);\n    }\n\n    @Test\n    @Disabled(\"we allow nulls in kv now\")\n    public void test307() throws Exception {\n        String msg =\n                \"(003.yml): Error @ line: 4, col: 12. Invalid value type, expected: STRING, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'out' @ line: 4, col: 7\\n\" +\n                        \"\\t\\t'call' @ line: 3, col: 7\";\n\n        assertErrorMessage(\"errors/flowCall/007.yml\", msg);\n    }\n\n    @Test\n    public void test308() throws Exception {\n        String msg =\n                \"(008.yml): Error @ line: 9, col: 17. Invalid value type, expected: NON_NULL, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'withItems' @ line: 9, col: 7\\n\" +\n                        \"\\t\\t'call' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/flowCall/008.yml\", msg);\n    }\n\n    @Test\n    public void test309() throws Exception {\n        String msg =\n                \"(009.yml): Error @ line: 10, col: 13. Invalid value type, expected: RETRY, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'retry' @ line: 10, col: 7\\n\" +\n                        \"\\t\\t'call' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/flowCall/009.yml\", msg);\n    }\n\n    @Test\n    public void test310() throws Exception {\n        String msg =\n                \"(010.yml): Error @ line: 10, col: 14. Invalid value type, expected: RETRY, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'retry' @ line: 10, col: 7\\n\" +\n                        \"\\t\\t'call' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/flowCall/010.yml\", msg);\n    }\n\n    @Test\n    public void test311() throws Exception {\n        String msg =\n                \"(011.yml): Error @ line: 11, col: 15. Invalid value type, expected: INT or EXPRESSION, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'times' @ line: 11, col: 9\\n\" +\n                        \"\\t\\t'retry' @ line: 10, col: 7\\n\" +\n                        \"\\t\\t\\t'call' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/flowCall/011.yml\", msg);\n    }\n\n    @Test\n    public void test312() throws Exception {\n        String msg =\n                \"(012.yml): Error @ line: 12, col: 15. Invalid value type, expected: INT or EXPRESSION, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'delay' @ line: 12, col: 9\\n\" +\n                        \"\\t\\t'retry' @ line: 10, col: 7\\n\" +\n                        \"\\t\\t\\t'call' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/flowCall/012.yml\", msg);\n    }\n\n    @Test\n    public void test313() throws Exception {\n        String msg =\n                \"(013.yml): Error @ line: 13, col: 12. Invalid value type, expected: OBJECT, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'in' @ line: 13, col: 9\\n\" +\n                        \"\\t\\t'retry' @ line: 10, col: 7\\n\" +\n                        \"\\t\\t\\t'call' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/flowCall/013.yml\", msg);\n    }\n\n    @Test\n    public void test314() throws Exception {\n        String msg =\n                \"(014.yml): Error @ line: 13, col: 13. Invalid value type, expected: OBJECT, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'in' @ line: 13, col: 9\\n\" +\n                        \"\\t\\t'retry' @ line: 10, col: 7\\n\" +\n                        \"\\t\\t\\t'call' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/flowCall/014.yml\", msg);\n    }\n\n    @Test\n    public void test315() throws Exception {\n        String msg =\n                \"(015.yml): Error @ line: 15, col: 14. Unknown options: ['trash' [STRING] @ line: 15, col: 14], expected: [error, in, loop, meta, name, out, parallelWithItems, retry, withItems]. Remove invalid options and/or fix indentation\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'call' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/flowCall/015.yml\", msg);\n    }\n\n    @Test\n    public void test316() throws Exception {\n        String msg =\n                \"(016.yml): Error @ line: 15, col: 12. Invalid value type, expected: OBJECT, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'meta' @ line: 15, col: 7\\n\" +\n                        \"\\t\\t'call' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/flowCall/016.yml\", msg);\n    }\n\n    @Test\n    public void test317() throws Exception {\n        String msg =\n                \"(017.yml): Error @ line: 15, col: 13. Invalid value type, expected: OBJECT, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'meta' @ line: 15, col: 7\\n\" +\n                        \"\\t\\t'call' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/flowCall/017.yml\", msg);\n    }\n\n    @Test\n    public void test318() throws Exception {\n        String msg =\n                \"(018.yml): Error @ line: 5, col: 11. Invalid value type, expected: STRING, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'out' @ line: 4, col: 7\\n\" +\n                        \"\\t\\t'call' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/flowCall/018.yml\", msg);\n    }\n\n    @Test\n    public void test400() throws Exception {\n        String msg =\n                \"(000.yml): Error @ line: 3, col: 18. Invalid value type, expected: STRING, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'checkpoint' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/checkpoint/000.yml\", msg);\n    }\n\n    @Test\n    public void test401() throws Exception {\n        String msg =\n                \"(001.yml): Error @ line: 3, col: 19. Invalid value type, expected: STRING, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'checkpoint' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/checkpoint/001.yml\", msg);\n    }\n\n    @Test\n    public void test402() throws Exception {\n        String msg =\n                \"(002.yml): Error @ line: 4, col: 14. Unknown options: ['trash' [STRING] @ line: 4, col: 14], expected: [meta]. Remove invalid options and/or fix indentation\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'checkpoint' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/checkpoint/002.yml\", msg);\n    }\n\n    @Test\n    public void test403() throws Exception {\n        String msg =\n                \"(003.yml): Error @ line: 4, col: 13. Invalid value type, expected: OBJECT, got: STRING\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'meta' @ line: 4, col: 7\\n\" +\n                        \"\\t\\t'checkpoint' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/checkpoint/003.yml\", msg);\n    }\n\n    @Test\n    public void test404() throws Exception {\n        String msg =\n                \"(004.yml): Error @ line: 4, col: 12. Invalid value type, expected: OBJECT, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'meta' @ line: 4, col: 7\\n\" +\n                        \"\\t\\t'checkpoint' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/checkpoint/004.yml\", msg);\n    }\n\n    @Test\n    public void test405() throws Exception {\n        String msg =\n                \"(005.yml): Error @ line: 6, col: 14. Unknown options: ['trash' [INT] @ line: 6, col: 14], expected: [meta]. Remove invalid options and/or fix indentation\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'checkpoint' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/checkpoint/005.yml\", msg);\n    }\n\n    @Test\n    public void test600() throws Exception {\n        String msg =\n                \"(000.yml): Error @ line: 3, col: 12. Invalid value type, expected: EXPRESSION, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'expr' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/expression/000.yml\", msg);\n    }\n\n    @Test\n    public void test601() throws Exception {\n        String msg =\n                \"(001.yml): Error @ line: 3, col: 13. Invalid value type, expected: EXPRESSION, got: STRING\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'expr' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/expression/001.yml\", msg);\n    }\n\n    @Test\n    public void test602() throws Exception {\n        String msg =\n                \"(002.yml): Error @ line: 4, col: 11. Invalid value type, expected: STRING or OBJECT, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'out' @ line: 4, col: 7\\n\" +\n                        \"\\t\\t'expr' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/expression/002.yml\", msg);\n    }\n\n    @Test\n    public void test603() throws Exception {\n        String msg =\n                \"(003.yml): Error @ line: 4, col: 12. Invalid value type, expected: STRING or OBJECT, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'out' @ line: 4, col: 7\\n\" +\n                        \"\\t\\t'expr' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/expression/003.yml\", msg);\n    }\n\n    @Test\n    public void test605() throws Exception {\n        String msg =\n                \"(005.yml): Error @ line: 5, col: 12. Invalid value type, expected: OBJECT, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'meta' @ line: 5, col: 7\\n\" +\n                        \"\\t\\t'expr' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/expression/005.yml\", msg);\n    }\n\n    @Test\n    public void test606() throws Exception {\n        String msg =\n                \"(006.yml): Error @ line: 5, col: 13. Invalid value type, expected: OBJECT, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'meta' @ line: 5, col: 7\\n\" +\n                        \"\\t\\t'expr' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/expression/006.yml\", msg);\n    }\n\n    @Test\n    public void test607() throws Exception {\n        String msg =\n                \"(007.yml): Error @ line: 7, col: 13. Invalid value type, expected: ARRAY_OF_STEP, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'error' @ line: 7, col: 7\\n\" +\n                        \"\\t\\t'expr' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/expression/007.yml\", msg);\n    }\n\n    @Test\n    public void test608() throws Exception {\n        String msg =\n                \"(008.yml): Error @ line: 7, col: 14. Invalid value type, expected: ARRAY_OF_STEP, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'error' @ line: 7, col: 7\\n\" +\n                        \"\\t\\t'expr' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/expression/008.yml\", msg);\n    }\n\n    @Test\n    public void test609() throws Exception {\n        String msg =\n                \"(009.yml): Error @ line: 8, col: 10. Invalid value type, expected: STEP, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'error' @ line: 7, col: 7\\n\" +\n                        \"\\t\\t'expr' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/expression/009.yml\", msg);\n    }\n\n    @Test\n    public void test610() throws Exception {\n        String msg =\n                \"(010.yml): Error @ line: 8, col: 11. Invalid value type, expected: STEP, got: STRING\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'error' @ line: 7, col: 7\\n\" +\n                        \"\\t\\t'expr' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/expression/010.yml\", msg);\n    }\n\n    @Test\n    public void test700() throws Exception {\n        String msg =\n                \"(000.yml): Error @ line: 3, col: 11. Invalid value type, expected: ARRAY_OF_STEP, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'try' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/group/000.yml\", msg);\n    }\n\n    @Test\n    public void test701() throws Exception {\n        String msg =\n                \"(001.yml): Error @ line: 3, col: 12. Invalid value type, expected: ARRAY_OF_STEP, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'try' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/group/001.yml\", msg);\n    }\n\n    @Test\n    public void test702() throws Exception {\n        String msg =\n                \"(002.yml): Error @ line: 4, col: 10. Invalid value type, expected: STEP, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'try' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/group/002.yml\", msg);\n    }\n\n    @Test\n    public void test703() throws Exception {\n        String msg =\n                \"(003.yml): Error @ line: 5, col: 13. Unknown options: ['trash' [NULL] @ line: 5, col: 13], expected: [error, loop, meta, name, out, parallelWithItems, withItems]. Remove invalid options and/or fix indentation\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'try' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/group/003.yml\", msg);\n    }\n\n    @Test\n    public void test704() throws Exception {\n        String msg =\n                \"(004.yml): Error @ line: 5, col: 13. Invalid value type, expected: ARRAY_OF_STEP, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'error' @ line: 5, col: 7\\n\" +\n                        \"\\t\\t'try' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/group/004.yml\", msg);\n    }\n\n    @Test\n    public void test705() throws Exception {\n        String msg =\n                \"(005.yml): Error @ line: 7, col: 12. Invalid value type, expected: OBJECT, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'meta' @ line: 7, col: 7\\n\" +\n                        \"\\t\\t'try' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/group/005.yml\", msg);\n    }\n\n    @Test\n    public void test706() throws Exception {\n        String msg =\n                \"(006.yml): Error @ line: 9, col: 17. Invalid value type, expected: NON_NULL, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'withItems' @ line: 9, col: 7\\n\" +\n                        \"\\t\\t'try' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/group/006.yml\", msg);\n    }\n\n    @Test\n    public void test707() throws Exception {\n        String msg =\n                \"(007.yml): Error @ line: 11, col: 13. Unknown options: ['trash' [NULL] @ line: 11, col: 13], expected: [error, loop, meta, name, out, parallelWithItems, withItems]. Remove invalid options and/or fix indentation\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'try' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/group/007.yml\", msg);\n    }\n\n    @Test\n    public void test708() throws Exception {\n        String msg =\n                \"(008.yml): Error @ line: 3, col: 13. Invalid value type, expected: ARRAY_OF_STEP, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'block' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/group/008.yml\", msg);\n    }\n\n    @Test\n    public void test800() throws Exception {\n        String msg =\n                \"(000.yml): Error @ line: 3, col: 16. Invalid value type, expected: ARRAY_OF_STEP, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'parallel' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/parallel/000.yml\", msg);\n    }\n\n    @Test\n    public void test801() throws Exception {\n        String msg =\n                \"(001.yml): Error @ line: 3, col: 17. Invalid value type, expected: ARRAY_OF_STEP, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'parallel' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/parallel/001.yml\", msg);\n    }\n\n    @Test\n    public void test802() throws Exception {\n        String msg =\n                \"(002.yml): Error @ line: 4, col: 11. Invalid value type, expected: STEP, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'parallel' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/parallel/002.yml\", msg);\n    }\n\n    @Test\n    public void test803() throws Exception {\n        String msg =\n                \"(003.yml): Error @ line: 5, col: 13. Unknown options: ['trash' [NULL] @ line: 5, col: 13], expected: [meta, out]. Remove invalid options and/or fix indentation\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'parallel' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/parallel/003.yml\", msg);\n    }\n\n    @Test\n    public void test804() throws Exception {\n        String msg =\n                \"(004.yml): Error @ line: 5, col: 12. Invalid value type, expected: OBJECT, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'meta' @ line: 5, col: 7\\n\" +\n                        \"\\t\\t'parallel' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/parallel/004.yml\", msg);\n    }\n\n    @Test\n    public void test805() throws Exception {\n        String msg =\n                \"(005.yml): Error @ line: 7, col: 13. Unknown options: ['trash' [NULL] @ line: 7, col: 13], expected: [meta, out]. Remove invalid options and/or fix indentation\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'parallel' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/parallel/005.yml\", msg);\n    }\n\n    @Test\n    public void test900() throws Exception {\n        String msg =\n                \"(000.yml): Error @ line: 1, col: 7. Invalid value type, expected: FORMS, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'forms' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/forms/000.yml\", msg);\n    }\n\n    @Test\n    public void test901() throws Exception {\n        String msg =\n                \"(001.yml): Error @ line: 2, col: 6. Invalid value type, expected: ARRAY_OF_FORM_FIELD, got: STRING\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'k' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t'forms' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/forms/001.yml\", msg);\n    }\n\n    @Test\n    public void test902() throws Exception {\n        String msg =\n                \"(002.yml): Error @ line: 2, col: 10. Invalid value type, expected: ARRAY_OF_FORM_FIELD, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'myForm' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t'forms' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/forms/002.yml\", msg);\n    }\n\n    @Test\n    public void test903() throws Exception {\n        String msg =\n                \"(003.yml): Error @ line: 3, col: 7. Invalid value type, expected: FORM_FIELD, got: STRING\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'myForm' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t'forms' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/forms/003.yml\", msg);\n    }\n\n    @Test\n    public void test904() throws Exception {\n        String msg =\n                \"(004.yml): Error @ line: 3, col: 16. Invalid value type, expected: FORM_FIELD, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'myForm' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t'forms' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/forms/004.yml\", msg);\n    }\n\n    @Test\n    public void test905() throws Exception {\n        String msg =\n                \"(005.yml): Error @ line: 3, col: 17. Unknown options: ['error' [INT] @ line: 3, col: 123], expected: [label, value, allow, type, pattern, inputType, readOnly, placeholder, search]. Remove invalid options and/or fix indentation\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'fullName' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'myForm' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'forms' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/forms/005.yml\", msg);\n    }\n\n    @Test\n    public void test906() throws Exception {\n        String msg =\n                \"(006.yml): Error @ line: 3, col: 26. Invalid value type, expected: STRING, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'fullName' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'myForm' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'forms' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/forms/006.yml\", msg);\n    }\n\n    @Test\n    public void test907() throws Exception {\n        String msg =\n                \"(007.yml): Error @ line: 3, col: 25. Invalid value type, expected: STRING, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'fullName' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'myForm' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'forms' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/forms/007.yml\", msg);\n    }\n\n    @Test\n    public void test908() throws Exception {\n        String msg =\n                \"(008.yml): Error @ n/a. Mandatory parameter 'type' not found\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'fullName' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'myForm' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'forms' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/forms/008.yml\", msg);\n    }\n\n    @Test\n    public void test909() throws Exception {\n        String msg =\n                \"(009.yml): Error @ line: 3, col: 25. Invalid value: 123, expected: [string, int, decimal, boolean, file, date, dateTime]\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'fullName' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'myForm' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'forms' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/forms/009.yml\", msg);\n    }\n\n    @Test\n    public void test910() throws Exception {\n        String msg =\n                \"(010.yml): Error @ line: 3, col: 64. Invalid value type, expected: BOOLEAN, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'fullName' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'myForm' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'forms' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/forms/010.yml\", msg);\n    }\n\n    @Test\n    public void test911() throws Exception {\n        String msg =\n                \"(011.yml): Error @ line: 1, col: 8. Invalid value type, expected: FORMS, got: STRING\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'forms' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/forms/011.yml\", msg);\n    }\n\n    @Test\n    public void test913() throws Exception {\n        String msg =\n                \"(013.yml): Error @ line: 2, col: 3. Invalid value: invalid.name, expected: [String matching regex \\\"^[A-Za-z0-9_ $]+$\\\"]\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'forms' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/forms/013.yml\", msg);\n    }\n\n    @Test\n    public void test1000() throws Exception {\n        String msg =\n                \"(000.yml): Error @ line: 3, col: 12. Invalid value type, expected: STRING, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'form' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/formCall/000.yml\", msg);\n    }\n\n    @Test\n    public void test1001() throws Exception {\n        String msg =\n                \"(001.yml): Error @ line: 3, col: 13. Invalid value type, expected: STRING, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'form' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/formCall/001.yml\", msg);\n    }\n\n    @Test\n    public void test1002() throws Exception {\n        String msg =\n                \"(002.yml): Error @ line: 4, col: 9. Unknown options: ['a' [NULL] @ line: 4, col: 9, 'b' [NULL] @ line: 6, col: 9], expected: [fields, runAs, saveSubmittedBy, values, yield]. Remove invalid options and/or fix indentation\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'form' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/formCall/002.yml\", msg);\n    }\n\n    @Test\n    public void test1003() throws Exception {\n        String msg =\n                \"(003.yml): Error @ line: 4, col: 14. Invalid value type, expected: BOOLEAN, got: STRING\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'yield' @ line: 4, col: 7\\n\" +\n                        \"\\t\\t'form' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/formCall/003.yml\", msg);\n    }\n\n    @Test\n    public void test1004() throws Exception {\n        String msg =\n                \"(004.yml): Error @ line: 5, col: 23. Invalid value type, expected: BOOLEAN, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'saveSubmittedBy' @ line: 5, col: 7\\n\" +\n                        \"\\t\\t'form' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/formCall/004.yml\", msg);\n    }\n\n    @Test\n    public void test1005() throws Exception {\n        String msg =\n                \"(005.yml): Error @ line: 6, col: 14. Invalid value type, expected: OBJECT or EXPRESSION, got: ARRAY\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'runAs' @ line: 6, col: 7\\n\" +\n                        \"\\t\\t'form' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/formCall/005.yml\", msg);\n    }\n\n    @Test\n    public void test1006() throws Exception {\n        String msg =\n                \"(006.yml): Error @ line: 10, col: 15. Invalid value type, expected: OBJECT or EXPRESSION, got: ARRAY\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'values' @ line: 10, col: 7\\n\" +\n                        \"\\t\\t'form' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/formCall/006.yml\", msg);\n    }\n\n    @Test\n    public void test1007() throws Exception {\n        String msg =\n                \"(007.yml): Error @ line: 12, col: 15. Invalid value type, expected: ARRAY_OF_FORM_FIELD or EXPRESSION, got: STRING\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'fields' @ line: 12, col: 7\\n\" +\n                        \"\\t\\t'form' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/formCall/007.yml\", msg);\n    }\n\n    @Test\n    public void test1008() throws Exception {\n        String msg =\n                \"(008.yml): Error @ line: 4, col: 15. Invalid value type, expected: ARRAY_OF_FORM_FIELD or EXPRESSION, got: STRING\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'fields' @ line: 4, col: 7\\n\" +\n                        \"\\t\\t'form' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/formCall/008.yml\", msg);\n    }\n\n    @Test\n    public void test1100() throws Exception {\n        String msg =\n                \"(000.yml): Error @ line: 1, col: 7. Invalid value type, expected: FLOWS, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/flows/000.yml\", msg);\n    }\n\n    @Test\n    public void test1101() throws Exception {\n        String msg =\n                \"(001.yml): Error @ line: 1, col: 8. Invalid value type, expected: FLOWS, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/flows/001.yml\", msg);\n    }\n\n    @Test\n    public void test1102() throws Exception {\n        String msg =\n                \"(002.yml): Error @ line: 2, col: 8. Invalid value type, expected: FLOW, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/flows/002.yml\", msg);\n    }\n\n    @Test\n    public void test1200() throws Exception {\n        String msg =\n                \"(000.yml): Error @ line: 1, col: 10. Invalid value type, expected: PROFILES, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'profiles' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/profiles/000.yml\", msg);\n    }\n\n    @Test\n    public void test1201() throws Exception {\n        String msg =\n                \"(001.yml): Error @ line: 2, col: 14. Invalid value type, expected: PROFILE, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'myProfile' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t'profiles' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/profiles/001.yml\", msg);\n    }\n\n    @Test\n    public void test1202() throws Exception {\n        String msg =\n                \"(002.yml): Error @ line: 3, col: 12. Invalid value type, expected: FLOWS, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'flows' @ line: 3, col: 5\\n\" +\n                        \"\\t\\t'myProfile' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'profiles' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/profiles/002.yml\", msg);\n    }\n\n    @Test\n    public void test1203() throws Exception {\n        String msg =\n                \"(003.yml): Error @ line: 3, col: 12. Unknown options: ['trash' [INT] @ line: 3, col: 12], expected: [configuration, flows, forms]. Remove invalid options and/or fix indentation\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'myProfile' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t'profiles' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/profiles/003.yml\", msg);\n    }\n\n    @Test\n    public void test1300() throws Exception {\n        String msg =\n                \"(000.yml): Error @ line: 1, col: 15. Invalid value type, expected: CONFIGURATION, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/000.yml\", msg);\n    }\n\n    @Test\n    public void test1301() throws Exception {\n        String msg =\n                \"(001.yml): Error @ line: 2, col: 14. Invalid value type, expected: STRING, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'entryPoint' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/001.yml\", msg);\n    }\n\n    @Test\n    public void test1302() throws Exception {\n        String msg =\n                \"(002.yml): Error @ line: 2, col: 15. Invalid value type, expected: STRING, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'entryPoint' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/002.yml\", msg);\n    }\n\n    @Test\n    public void test1303() throws Exception {\n        String msg =\n                \"(003.yml): Error @ line: 3, col: 16. Invalid value type, expected: ARRAY_OF_STRING, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'dependencies' @ line: 3, col: 3\\n\" +\n                        \"\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/003.yml\", msg);\n    }\n\n    @Test\n    public void test1304() throws Exception {\n        String msg =\n                \"(004.yml): Error @ line: 4, col: 7. Invalid value type, expected: STRING, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'dependencies' @ line: 3, col: 3\\n\" +\n                        \"\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/004.yml\", msg);\n    }\n\n    @Test\n    public void test1305() throws Exception {\n        String msg =\n                \"(005.yml): Error @ line: 6, col: 13. Invalid value type, expected: OBJECT, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'arguments' @ line: 6, col: 3\\n\" +\n                        \"\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/005.yml\", msg);\n    }\n\n    @Test\n    public void test1305_1() throws Exception {\n        String msg =\n                \"(005_1.yml): Error @ line: 7, col: 7. Invalid value type, expected: NON_NULL, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'arguments' @ line: 6, col: 3\\n\" +\n                        \"\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/005_1.yml\", msg);\n    }\n\n    @Test\n    public void test1306() throws Exception {\n        String msg =\n                \"(006.yml): Error @ line: 8, col: 9. Unknown options: ['trash' [NULL] @ line: 8, col: 9], expected: [activeProfiles, arguments, debug, dependencies, entryPoint, events, exclusive, extraDependencies, meta, out, parallelLoopParallelism, processTimeout, requirements, runtime, suspendTimeout, template, validation]. Remove invalid options and/or fix indentation\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/006.yml\", msg);\n    }\n\n    @Test\n    public void test1307() throws Exception {\n        String msg =\n                \"(007.yml): Error @ line: 8, col: 9. Invalid value type, expected: OBJECT, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'meta' @ line: 8, col: 3\\n\" +\n                        \"\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/007.yml\", msg);\n    }\n\n    @Test\n    public void test1308() throws Exception {\n        String msg =\n                \"(008.yml): Error @ line: 8, col: 17. Invalid value type, expected: OBJECT, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'requirements' @ line: 8, col: 3\\n\" +\n                        \"\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/008.yml\", msg);\n    }\n\n    @Test\n    public void test1309() throws Exception {\n        String msg =\n                \"(009.yml): Error @ line: 10, col: 19. Invalid value type, expected: ISO 8601 DURATION, got: STRING. Error info: Text cannot be parsed to a Duration\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'processTimeout' @ line: 10, col: 3\\n\" +\n                        \"\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/009.yml\", msg);\n    }\n\n    @Test\n    public void test1310() throws Exception {\n        String msg =\n                \"(010.yml): Error @ line: 11, col: 14. Invalid value type, expected: EXCLUSIVE_MODE, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'exclusive' @ line: 11, col: 3\\n\" +\n                        \"\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/010.yml\", msg);\n    }\n\n    @Test\n    public void test1311() throws Exception {\n        String msg =\n                \"(011.yml): Error @ n/a. Mandatory parameter 'group' not found\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'exclusive' @ line: 11, col: 3\\n\" +\n                        \"\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/011.yml\", msg);\n    }\n\n    @Test\n    public void test1311_1() throws Exception {\n        String msg =\n                \"(011_1.yml): Error @ line: 4, col: 12. Invalid value type, expected: NON_EMPTY_STRING, got: STRING. Error info: Empty value\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'group' @ line: 4, col: 5\\n\" +\n                        \"\\t\\t'exclusive' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/011_1.yml\", msg);\n    }\n\n    @Test\n    public void test1312() throws Exception {\n        String msg =\n                \"(012.yml): Error @ line: 13, col: 12. Unknown options: ['mode1' [STRING] @ line: 13, col: 12], expected: [group, mode]. Remove invalid options and/or fix indentation\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'exclusive' @ line: 11, col: 3\\n\" +\n                        \"\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/012.yml\", msg);\n    }\n\n    @Test\n    public void test1313() throws Exception {\n        String msg =\n                \"(013.yml): Error @ line: 13, col: 11. Invalid value: canceL, expected: [cancel, cancelOld, wait]\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'mode' @ line: 13, col: 5\\n\" +\n                        \"\\t\\t'exclusive' @ line: 11, col: 3\\n\" +\n                        \"\\t\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/013.yml\", msg);\n    }\n\n    @Test\n    public void test1314() throws Exception {\n        String msg =\n                \"(014.yml): Error @ line: 14, col: 11. Invalid value type, expected: EVENTS_CONFIGURATION, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'events' @ line: 14, col: 3\\n\" +\n                        \"\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/014.yml\", msg);\n    }\n\n    @Test\n    public void test1315() throws Exception {\n        String msg =\n                \"(015.yml): Error @ line: 16, col: 22. Invalid value type, expected: ARRAY_OF_STRING, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'inVarsBlacklist' @ line: 16, col: 5\\n\" +\n                        \"\\t\\t'events' @ line: 14, col: 3\\n\" +\n                        \"\\t\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/015.yml\", msg);\n    }\n\n    @Test\n    public void test1316() throws Exception {\n        String msg =\n                \"(016.yml): Error @ line: 20, col: 24. Invalid value type, expected: BOOLEAN, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'recordTaskOutVars' @ line: 20, col: 5\\n\" +\n                        \"\\t\\t'events' @ line: 14, col: 3\\n\" +\n                        \"\\t\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/016.yml\", msg);\n    }\n\n    @Test\n    public void test1317() throws Exception {\n        String msg =\n                \"(017.yml): Error @ line: 21, col: 23. Invalid value type, expected: ARRAY_OF_STRING, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'outVarsBlacklist' @ line: 21, col: 5\\n\" +\n                        \"\\t\\t'events' @ line: 14, col: 3\\n\" +\n                        \"\\t\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/017.yml\", msg);\n    }\n\n    @Test\n    public void test1318() throws Exception {\n        String msg =\n                \"(018.yml): Error @ line: 26, col: 11. Unknown options: ['trash' [NULL] @ line: 26, col: 11], expected: [batchFlushInterval, batchSize, inVarsBlacklist, metaBlacklist, outVarsBlacklist, recordEvents, recordTaskInVars, recordTaskMeta, recordTaskOutVars, truncateInVars, truncateMaxArrayLength, truncateMaxDepth, truncateMaxStringLength, truncateMeta, truncateOutVars, updateMetaOnAllEvents]. Remove invalid options and/or fix indentation\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'events' @ line: 14, col: 3\\n\" +\n                        \"\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/018.yml\", msg);\n    }\n\n    @Test\n    public void test1319() throws Exception {\n        String msg =\n                \"(019.yml): Error @ line: 23, col: 8. Invalid value type, expected: ARRAY_OF_STRING, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'out' @ line: 23, col: 3\\n\" +\n                        \"\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/019.yml\", msg);\n    }\n\n    @Test\n    public void test1320() throws Exception {\n        String msg =\n                \"(020.yml): Error @ line: 4, col: 5. Invalid value type, expected: STRING, got: ARRAY\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'template' @ line: 3, col: 3\\n\" +\n                        \"\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/020.yml\", msg);\n    }\n\n    @Test\n    public void test1400() throws Exception {\n        String msg =\n                \"(000.yml): Error @ line: 3, col: 10. Invalid value type, expected: EXPRESSION, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'if' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/if/000.yml\", msg);\n    }\n\n    @Test\n    public void test1401() throws Exception {\n        String msg =\n                \"(001.yml): Error @ line: 3, col: 11. Invalid value type, expected: EXPRESSION, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'if' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/if/001.yml\", msg);\n    }\n\n    @Test\n    public void test1402() throws Exception {\n        String msg =\n                \"(002.yml): Error @ n/a. Mandatory parameter 'then' not found\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'if' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/if/002.yml\", msg);\n    }\n\n    @Test\n    public void test1403() throws Exception {\n        String msg =\n                \"(003.yml): Error @ line: 4, col: 12. Invalid value type, expected: ARRAY_OF_STEP, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'then' @ line: 4, col: 7\\n\" +\n                        \"\\t\\t'if' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/if/003.yml\", msg);\n    }\n\n    @Test\n    public void test1404() throws Exception {\n        String msg =\n                \"(004.yml): Error @ line: 6, col: 10. Unknown options: ['el' [NULL] @ line: 6, col: 10], expected: [else, meta, then]. Remove invalid options and/or fix indentation\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'if' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/if/004.yml\", msg);\n    }\n\n    @Test\n    public void test1405() throws Exception {\n        String msg =\n                \"(005.yml): Error @ line: 6, col: 12. Invalid value type, expected: ARRAY_OF_STEP, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'else' @ line: 6, col: 7\\n\" +\n                        \"\\t\\t'if' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/if/005.yml\", msg);\n    }\n\n    @Test\n    public void test1406() throws Exception {\n        String msg =\n                \"(006.yml): Error @ line: 8, col: 13. Unknown options: ['trash' [NULL] @ line: 8, col: 13], expected: [else, meta, then]. Remove invalid options and/or fix indentation\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'if' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/if/006.yml\", msg);\n    }\n\n    @Test\n    public void test1407() throws Exception {\n        String msg =\n                \"(007.yml): Error @ line: 8, col: 12. Invalid value type, expected: OBJECT, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'meta' @ line: 8, col: 7\\n\" +\n                        \"\\t\\t'if' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/if/007.yml\", msg);\n    }\n\n    @Test\n    public void test1500() throws Exception {\n        String msg =\n                \"(000.yml): Error @ line: 3, col: 14. Invalid value type, expected: EXPRESSION, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'switch' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/switch/000.yml\", msg);\n    }\n\n    @Test\n    public void test1501() throws Exception {\n        String msg =\n                \"(001.yml): Error @ line: 3, col: 7. No branch labels defined\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'switch' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/switch/001.yml\", msg);\n    }\n\n    @Test\n    public void test1502() throws Exception {\n        String msg =\n                \"(002.yml): Error @ line: 4, col: 11. Invalid value type, expected: ARRAY_OF_STEP, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'switch' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/switch/002.yml\", msg);\n    }\n\n    @Test\n    public void test1503() throws Exception {\n        String msg =\n                \"(003.yml): Error @ line: 6, col: 15. Invalid value type, expected: ARRAY_OF_STEP, got: NULL. Remove attribute or complete the definition\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'default' @ line: 6, col: 7\\n\" +\n                        \"\\t\\t'switch' @ line: 3, col: 7\\n\" +\n                        \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/switch/003.yml\", msg);\n    }\n\n    @Test\n    public void test1600() throws Exception {\n        String msg = \"(000.yml): Error @ line: 1, col: 13. Invalid value type, expected: ARRAY_OF_STRING, got: NULL. Remove attribute or complete the definition\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'publicFlows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/publicFlows/000.yml\", msg);\n    }\n\n    @Test\n    public void test1601() throws Exception {\n        String msg = \"(001.yml): Error @ line: 1, col: 14. Invalid value type, expected: ARRAY_OF_STRING, got: STRING\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'publicFlows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/publicFlows/001.yml\", msg);\n    }\n\n    @Test\n    public void test1602() throws Exception {\n        String msg = \"(002.yml): Error @ line: 2, col: 5. Invalid value type, expected: STRING, got: INT\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'publicFlows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/publicFlows/002.yml\", msg);\n    }\n\n    @Test\n    public void test1700() throws Exception {\n        String msg = \"(000.yml): Error @ line: 3, col: 14. Invalid value type, expected: STRING, got: NULL. Remove attribute or complete the definition\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'script' @ line: 3, col: 7\\n\" +\n                \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/scripts/000.yml\", msg);\n    }\n\n    @Test\n    public void test1701() throws Exception {\n        String msg = \"(001.yml): Error @ line: 3, col: 15. Invalid value type, expected: STRING, got: INT\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'script' @ line: 3, col: 7\\n\" +\n                \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/scripts/001.yml\", msg);\n    }\n\n    @Test\n    public void test1702() throws Exception {\n        String msg = \"(002.yml): Error @ line: 4, col: 14. Unknown options: ['body1' [STRING] @ line: 4, col: 14], expected: [body, error, in, loop, meta, name, out, parallelWithItems, retry, withItems]. Remove invalid options and/or fix indentation\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'script' @ line: 3, col: 7\\n\" +\n                \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/scripts/002.yml\", msg);\n    }\n\n    @Test\n    public void test1703() throws Exception {\n        String msg = \"(003.yml): Error @ line: 7, col: 11. Invalid value type, expected: STEP, got: STRING\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'error' @ line: 6, col: 7\\n\" +\n                \"\\t\\t'script' @ line: 3, col: 7\\n\" +\n                \"\\t\\t\\t'main' @ line: 2, col: 3\\n\" +\n                \"\\t\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/scripts/003.yml\", msg);\n    }\n\n    @Test\n    public void test1800() throws Exception {\n        String msg = \"(000.yml): Error @ line: 1, col: 11. Invalid value type, expected: RESOURCES, got: NULL. Remove attribute or complete the definition\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'resources' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/resources/000.yml\", msg);\n    }\n\n    @Test\n    public void test1801() throws Exception {\n        String msg = \"(001.yml): Error @ line: 3, col: 5. Unknown options: ['trash' [ARRAY] @ line: 3, col: 5], expected: [concord]. Remove invalid options and/or fix indentation\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'resources' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/resources/001.yml\", msg);\n    }\n\n    @Test\n    public void test1802() throws Exception {\n        String msg = \"(002.yml): Error @ line: 2, col: 12. Invalid value type, expected: ARRAY_OF_STRING, got: STRING\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'concord' @ line: 2, col: 3\\n\" +\n                \"\\t\\t'resources' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/resources/002.yml\", msg);\n    }\n\n    @Test\n    public void test1900() throws Exception {\n        String msg = \"(000.yml): Error @ line: 3, col: 11. Invalid value type, expected: OBJECT, got: NULL. Remove attribute or complete the definition\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'set' @ line: 3, col: 7\\n\" +\n                \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/setVariables/000.yml\", msg);\n    }\n\n    @Test\n    public void test1901() throws Exception {\n        String msg = \"(001.yml): Error @ line: 7, col: 9. Unknown options: ['meta1' [OBJECT] @ line: 7, col: 9], expected: [meta]. Remove invalid options and/or fix indentation\\n\" +\n                \"\\twhile processing steps:\\n\" +\n                \"\\t'set' @ line: 3, col: 7\\n\" +\n                \"\\t\\t'main' @ line: 2, col: 3\\n\" +\n                \"\\t\\t\\t'flows' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/setVariables/001.yml\", msg);\n    }\n\n    @Test\n    public void test1902() throws Exception {\n        String msg =\n                \"(021.yml): Error @ line: 23, col: 21. Invalid value type, expected: BOOLEAN, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'recordTaskMeta' @ line: 23, col: 5\\n\" +\n                        \"\\t\\t'events' @ line: 14, col: 3\\n\" +\n                        \"\\t\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/021.yml\", msg);\n    }\n\n    @Test\n    public void test1903() throws Exception {\n        String msg =\n                \"(022.yml): Error @ line: 24, col: 20. Invalid value type, expected: ARRAY_OF_STRING, got: INT\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'metaBlacklist' @ line: 24, col: 5\\n\" +\n                        \"\\t\\t'events' @ line: 14, col: 3\\n\" +\n                        \"\\t\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/022.yml\", msg);\n    }\n\n    @Test\n    public void test1904() throws Exception {\n        String msg =\n                \"(023.yml): Error @ line: 11, col: 19. Invalid value type, expected: ISO 8601 DURATION, got: STRING. Error info: Text cannot be parsed to a Duration\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'suspendTimeout' @ line: 11, col: 3\\n\" +\n                        \"\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/023.yml\", msg);\n    }\n\n    @Test\n    public void test1905() throws Exception {\n        String msg =\n                \"(024.yml): Error @ line: 4, col: 11. Invalid value: fatal, expected: [DISABLED, WARN, FAIL]\\n\" +\n                        \"\\twhile processing steps:\\n\" +\n                        \"\\t'in' @ line: 4, col: 7\\n\" +\n                        \"\\t\\t'taskCalls' @ line: 3, col: 5\\n\" +\n                        \"\\t\\t\\t'validation' @ line: 2, col: 3\\n\" +\n                        \"\\t\\t\\t\\t'configuration' @ line: 1, col: 1\";\n\n        assertErrorMessage(\"errors/configuration/024.yml\", msg);\n    }\n\n    private void assertErrorMessage(String resource, String expectedError) throws Exception {\n        try {\n            load(resource);\n            fail(\"exception expected\");\n        } catch (YamlParserException e) {\n            assertEquals(expectedError, e.getMessage());\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/test/java/com/walmartlabs/concord/project/runtime/v2/parser/YamlOkParserTest.java",
    "content": "package com.walmartlabs.concord.project.runtime.v2.parser;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.forms.FormField.Cardinality;\nimport com.walmartlabs.concord.imports.Import;\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.runtime.v2.Constants;\nimport com.walmartlabs.concord.runtime.v2.model.*;\nimport com.walmartlabs.concord.runtime.v2.parser.StepOptions;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.Serializable;\nimport java.time.Duration;\nimport java.util.*;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class YamlOkParserTest extends AbstractParserTest {\n\n    // Full Task Definition Test\n    @Test\n    public void test000() throws Exception {\n        ProcessDefinition pd = load(\"000.yml\");\n\n        Flow main = pd.flows().get(\"main\");\n\n        assertEquals(1, main.steps().size());\n\n        assertTrue(main.steps().get(0) instanceof TaskCall);\n        TaskCall t = (TaskCall) main.steps().get(0);\n        assertEquals(\"boo\", t.getName());\n\n        // options\n        assertNotNull(t.getOptions());\n        assertEquals(\"result\", t.getOptions().out());\n\n        // withItems\n        assertEquals(1, t.getOptions().withItems().value());\n\n        // loop\n        assertEquals(1, t.getOptions().loop().items());\n        assertEquals(Loop.Mode.SERIAL, t.getOptions().loop().mode());\n\n        // input\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"k\", \"v\");\n        input.put(\"k2\", 2);\n        input.put(\"k3\", false);\n        assertEquals(input, t.getOptions().input());\n\n        // retry\n        assertNotNull(t.getOptions().retry());\n        assertEquals(1, t.getOptions().retry().times());\n        assertEquals(Duration.ofSeconds(2), t.getOptions().retry().delay());\n        Map<String, Object> retryInput = new HashMap<>();\n        retryInput.put(\"k\", \"retry-1\");\n        retryInput.put(\"k2\", \"retry-2\");\n        retryInput.put(\"k3\", Collections.singletonMap(\"kk\", \"vv\"));\n        assertEquals(retryInput, t.getOptions().retry().input());\n\n        // meta\n        assertMeta(\"Boo\", t.getOptions());\n    }\n\n    @Test\n    public void test000_1() throws Exception {\n        ProcessDefinition pd = load(\"000.1.yml\");\n\n        Flow main = pd.flows().get(\"main\");\n\n        assertEquals(1, main.steps().size());\n\n        assertTrue(main.steps().get(0) instanceof TaskCall);\n        TaskCall t = (TaskCall) main.steps().get(0);\n        assertEquals(\"boo\", t.getName());\n\n        // options\n        assertNotNull(t.getOptions());\n\n        // input\n        assertEquals(0, t.getOptions().input().size());\n        assertEquals(\"${inExpr}\", t.getOptions().inputExpression());\n    }\n\n    @Test\n    public void test000_2() throws Exception {\n        ProcessDefinition pd = load(\"000.2.yml\");\n\n        List<Step> main = pd.flows().get(\"main\").steps();\n\n        assertEquals(1, main.size());\n\n        assertTrue(main.get(0) instanceof TaskCall);\n        TaskCall t = (TaskCall) main.get(0);\n        assertEquals(\"project\", t.getName());\n\n        // options\n        assertNotNull(t.getOptions());\n\n        // name\n        assertEquals(\"Test name\", t.getOptions().meta().get(Constants.SEGMENT_NAME));\n\n        // input\n        assertNotNull(t.getOptions().input());\n\n        assertEquals(\"ProjectName\", t.getOptions().input().get(\"name\"));\n        assertEquals(\"Default\", t.getOptions().input().get(\"org\"));\n        assertEquals(\"create\", t.getOptions().input().get(\"action\"));\n    }\n\n    // Full Call Flow Definition Test\n    @Test\n    public void test002() throws Exception {\n        ProcessDefinition pd = load(\"002.yml\");\n\n        List<Step> main = pd.flows().get(\"main\").steps();\n\n        assertEquals(1, main.size());\n\n        assertTrue(main.get(0) instanceof FlowCall);\n        FlowCall t = (FlowCall) main.get(0);\n        assertEquals(\"boo\", t.getFlowName());\n\n        // options\n        assertNotNull(t.getOptions());\n        assertEquals(Collections.singletonList(\"result\"), t.getOptions().out());\n\n        // withItems\n        assertEquals(1, t.getOptions().withItems().value());\n\n        // input\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"k\", \"v\");\n        input.put(\"k2\", 2);\n        input.put(\"k3\", false);\n        assertEquals(input, t.getOptions().input());\n\n        // retry\n        assertNotNull(t.getOptions().retry());\n        assertEquals(1, t.getOptions().retry().times());\n        assertEquals(Duration.ofSeconds(2), t.getOptions().retry().delay());\n        Map<String, Object> retryInput = new HashMap<>();\n        retryInput.put(\"k\", \"retry-1\");\n        retryInput.put(\"k2\", \"retry-2\");\n        retryInput.put(\"k3\", Collections.singletonMap(\"kk\", \"vv\"));\n        assertEquals(retryInput, t.getOptions().retry().input());\n\n        // meta\n        assertMeta(\"boo-call\", t.getOptions());\n    }\n\n    @Test\n    public void test002_1() throws Exception {\n        ProcessDefinition pd = load(\"002.1.yml\");\n\n        List<Step> main = pd.flows().get(\"main\").steps();\n\n        assertEquals(1, main.size());\n\n        assertTrue(main.get(0) instanceof FlowCall);\n        FlowCall t = (FlowCall) main.get(0);\n        assertEquals(\"boo\", t.getFlowName());\n\n        // options\n        assertNotNull(t.getOptions());\n        // input\n        assertEquals(0, t.getOptions().input().size());\n        assertEquals(\"${inExpr}\", t.getOptions().inputExpression());\n    }\n\n    // Snapshot Definition Test\n    @Test\n    public void test003() throws Exception {\n        ProcessDefinition pd = load(\"003.yml\");\n\n        List<Step> main = pd.flows().get(\"main\").steps();\n\n        assertEquals(1, main.size());\n\n        assertTrue(main.get(0) instanceof Checkpoint);\n        Checkpoint t = (Checkpoint) main.get(0);\n        assertEquals(\"ZZZ\", t.getName());\n\n        // meta\n        assertMeta(t.getOptions());\n    }\n\n    // Full Expression Definition Test\n    @Test\n    public void test004() throws Exception {\n        ProcessDefinition pd = load(\"004.yml\");\n\n        List<Step> main = pd.flows().get(\"main\").steps();\n\n        assertEquals(1, main.size());\n\n        assertTrue(main.get(0) instanceof Expression);\n        Expression t = (Expression) main.get(0);\n        assertEquals(\"${boo}\", t.getExpr());\n\n        // options\n        assertNotNull(t.getOptions());\n        assertEquals(\"result\", t.getOptions().out());\n\n        // error\n        assertNotNull(t.getOptions().errorSteps());\n        assertEquals(1, t.getOptions().errorSteps().size());\n        assertTrue(t.getOptions().errorSteps().get(0) instanceof Expression);\n        Expression errorStep = (Expression) t.getOptions().errorSteps().get(0);\n        assertEquals(\"${booError}\", errorStep.getExpr());\n        assertNotNull(errorStep.getOptions());\n\n        // meta\n        assertMeta(\"expression-call\", t.getOptions());\n    }\n\n    // Group of Steps Definition Test\n    @Test\n    @SuppressWarnings(\"unchecked\")\n    public void test005() throws Exception {\n        ProcessDefinition pd = load(\"005.yml\");\n\n        List<Step> main = pd.flows().get(\"main\").steps();\n\n        assertEquals(1, main.size());\n\n        assertTrue(main.get(0) instanceof GroupOfSteps);\n        GroupOfSteps t = (GroupOfSteps) main.get(0);\n\n        // error\n        assertNotNull(t.getOptions().errorSteps());\n        assertEquals(1, t.getOptions().errorSteps().size());\n        assertTrue(t.getOptions().errorSteps().get(0) instanceof Expression);\n        Expression errorStep = (Expression) t.getOptions().errorSteps().get(0);\n        assertEquals(\"${exp}\", errorStep.getExpr());\n        assertNotNull(errorStep.getOptions());\n\n        // withItems\n        assertEquals(\"a\", ((List<String>) t.getOptions().withItems().value()).get(0));\n\n        // meta\n        assertMeta(\"Test try\", t.getOptions());\n    }\n\n    // Forms Definition Test\n    @Test\n    public void test006() throws Exception {\n        ProcessDefinition pd = load(\"006.yml\");\n\n        List<Step> main = pd.flows().get(\"main\").steps();\n\n        assertEquals(2, main.size());\n\n        assertTrue(main.get(0) instanceof FormCall);\n        FormCall t = (FormCall) main.get(0);\n\n        assertNotNull(t.getName());\n        assertNotNull(t.getLocation());\n        assertEquals(\"myForm\", t.getName());\n        assertNotNull(t.getOptions());\n        assertTrue(t.getOptions().isYield());\n        assertTrue(t.getOptions().saveSubmittedBy());\n        assertEquals(Collections.singletonMap(\"username\", Arrays.asList(\"userA\", \"userB\")), t.getOptions().runAs());\n        assertEquals(Collections.singletonMap(\"myField\", \"a different value\"), t.getOptions().values());\n        assertEquals(2, t.getOptions().fields().size());\n        FormField field = t.getOptions().fields().get(0);\n        assertEquals(\"firstName\", field.name());\n        assertEquals(\"string\", field.type());\n        assertEquals(Cardinality.ONE_AND_ONLY_ONE, field.cardinality());\n        assertEquals(0, field.options().size());\n        assertNotNull(field.location());\n\n\n        assertTrue(main.get(1) instanceof FormCall);\n        FormCall t2 = (FormCall) main.get(1);\n\n        assertNotNull(t2.getOptions());\n        assertNotNull(t2.getOptions().valuesExpression());\n        assertFalse(t2.getOptions().isYield());\n        assertFalse(t2.getOptions().saveSubmittedBy());\n        assertEquals(\"${{ 'fieldA': 'valueA' }}\", t2.getOptions().valuesExpression());\n        assertEquals(\"${{ 'username': [ 'userA', 'userB' ] }}\", t2.getOptions().runAsExpression());\n\n\n        assertNotNull(pd.forms());\n        assertEquals(1, pd.forms().size());\n        Form fd = pd.forms().get(\"myForm\");\n        assertNotNull(fd);\n        assertEquals(\"myForm\", fd.name());\n        assertNotNull(fd.location());\n        assertNotNull(fd.fields());\n        assertEquals(2, fd.fields().size());\n    }\n\n    // Imports Definition Test\n    @Test\n    public void test007() throws Exception {\n        ProcessDefinition pd = load(\"007.yml\");\n\n        Imports imports = pd.imports();\n        assertNotNull(imports);\n\n        assertEquals(4, imports.items().size());\n\n        Import i = imports.items().get(0);\n        assertEquals(\"git\", i.type());\n        Import.GitDefinition g = (Import.GitDefinition) i;\n        assertEquals(\"https://github.com/me/my_private_repo.git\", g.url());\n        assertEquals(\"test\", g.name());\n        assertEquals(\"1.2.3\", g.version());\n        assertEquals(\"/\", g.path());\n        assertEquals(\"/dest\", g.dest());\n        assertEquals(Arrays.asList(\"a\", \"b\"), g.exclude());\n        assertEquals(Import.SecretDefinition.builder().name(\"my_secret_key\").build(), g.secret());\n\n        assertEquals(\"git\", imports.items().get(1).type());\n        assertEquals(\"mvn\", imports.items().get(2).type());\n        assertEquals(\"dir\", imports.items().get(3).type());\n    }\n\n    // Configuration Definition Test\n    @Test\n    public void test008() throws Exception {\n        ProcessDefinition pd = load(\"008.yml\");\n\n        ProcessDefinitionConfiguration cfg = pd.configuration();\n        assertNotNull(cfg);\n\n        assertTrue(cfg.debug());\n        assertEquals(\"main-test\", cfg.entryPoint());\n        assertEquals(Arrays.asList(\"d1\", \"d2\"), cfg.dependencies());\n        assertEquals(Arrays.asList(\"d3\", \"d4\"), cfg.extraDependencies());\n        assertEquals(Collections.singletonMap(\"k\", \"v\"), cfg.arguments());\n        assertEquals(Collections.singletonMap(\"k\", \"v1\"), cfg.requirements());\n        assertEquals(Duration.parse(\"PT1H\"), cfg.processTimeout());\n        assertEquals(ExclusiveMode.of(\"X\", ExclusiveMode.Mode.cancel), cfg.exclusive());\n        assertEquals(EventConfiguration.builder()\n                .recordTaskInVars(true)\n                .inVarsBlacklist(Collections.singletonList(\"pass\"))\n                .recordTaskOutVars(true)\n                .outVarsBlacklist(Collections.singletonList(\"bass\"))\n                .recordTaskMeta(true)\n                .metaBlacklist(Collections.singletonList(\"bass\"))\n                .build(), cfg.events());\n    }\n\n    /**\n     * Triggers\n     */\n    @Test\n    public void test009() throws Exception {\n        ProcessDefinition pd = load(\"009.yml\");\n\n        List<Trigger> triggers = pd.triggers();\n        assertNotNull(triggers);\n\n        assertEquals(6, triggers.size());\n\n        Trigger t = triggers.get(0);\n        assertEquals(\"github\", t.name());\n        assertFalse((Boolean) t.configuration().get(\"ignoreEmptyPush\"));\n\n        t = triggers.get(1);\n        assertEquals(\"github\", t.name());\n\n        t = triggers.get(2);\n        assertEquals(\"cron\", t.name());\n        assertEquals(Collections.singletonMap(\"withSecret\", \"secret-name\"), t.configuration().get(\"runAs\"));\n\n        t = triggers.get(3);\n        assertEquals(\"manual\", t.name());\n\n        t = triggers.get(4);\n        assertEquals(\"example\", t.name());\n\n        t = triggers.get(5);\n        assertEquals(\"oneops\", t.name());\n    }\n\n    // Full form of IF definition\n    @Test\n    public void test010() throws Exception {\n        ProcessDefinition pd = load(\"010.yml\");\n\n        List<Step> main = pd.flows().get(\"default\").steps();\n        assertEquals(1, main.size());\n\n        IfStep ifStep = (IfStep) main.get(0);\n        assertEquals(\"${myVar > 0}\", ifStep.getExpression());\n        assertEquals(2, ifStep.getThenSteps().size());\n        assertEquals(1, ifStep.getElseSteps().size());\n    }\n\n    // Switch full definition\n    @Test\n    public void test011() throws Exception {\n        ProcessDefinition pd = load(\"012.yml\");\n\n        List<Step> main = pd.flows().get(\"default\").steps();\n        assertEquals(1, main.size());\n\n        SwitchStep switchStep = (SwitchStep) main.get(0);\n        assertEquals(\"${myVar}\", switchStep.getExpression());\n        assertEquals(2, switchStep.getCaseSteps().size());\n        assertEquals(\"red\", switchStep.getCaseSteps().get(0).getKey());\n        assertEquals(\"green\", switchStep.getCaseSteps().get(1).getKey());\n        assertEquals(1, switchStep.getDefaultSteps().size());\n    }\n\n    // publicFlows definition\n    @Test\n    public void test013() throws Exception {\n        ProcessDefinition pd = load(\"013.yml\");\n\n        Set<String> publicFlows = pd.publicFlows();\n        Set<String> flowNames = pd.flows().keySet();\n\n        assertEquals(1, publicFlows.size());\n        assertTrue(publicFlows.contains(\"publicFlow\"));\n        assertTrue(flowNames.contains(publicFlows.iterator().next()));\n    }\n\n    // script definition\n    @Test\n    public void test014() throws Exception {\n        ProcessDefinition pd = load(\"014.yml\");\n\n        List<Step> main = pd.flows().get(\"default\").steps();\n        assertEquals(1, main.size());\n\n        assertTrue(main.get(0) instanceof ScriptCall);\n        ScriptCall t = (ScriptCall) main.get(0);\n        assertEquals(\"groovy\", t.getLanguageOrRef());\n\n        // withItems\n        assertEquals(1, t.getOptions().withItems().value());\n\n        // input\n        Map<String, Object> input = new HashMap<>();\n        input.put(\"k\", \"v1\");\n        assertEquals(input, t.getOptions().input());\n\n        // out\n        assertEquals(\"result\", t.getOptions().out());\n\n        // retry\n        assertNotNull(t.getOptions().retry());\n        assertEquals(1, t.getOptions().retry().times());\n        assertEquals(Duration.ofSeconds(2), t.getOptions().retry().delay());\n        Map<String, Object> retryInput = new HashMap<>();\n        retryInput.put(\"k\", \"v\");\n        assertEquals(retryInput, t.getOptions().retry().input());\n\n        // meta\n        assertMeta(\"Test script\", t.getOptions());\n    }\n\n\n    // script definition\n    @Test\n    public void test014_1() throws Exception {\n        ProcessDefinition pd = load(\"014.1.yml\");\n\n        List<Step> main = pd.flows().get(\"default\").steps();\n        assertEquals(1, main.size());\n\n        assertTrue(main.get(0) instanceof ScriptCall);\n        ScriptCall t = (ScriptCall) main.get(0);\n        assertEquals(\"groovy\", t.getLanguageOrRef());\n\n        // input\n        assertEquals(0, t.getOptions().input().size());\n        assertEquals(\"${inExpr}\", t.getOptions().inputExpression());\n    }\n\n    // resources definition\n    @Test\n    public void test015() throws Exception {\n        ProcessDefinition pd = load(\"015.yml\");\n\n        Resources resources = pd.resources();\n        assertEquals(3, resources.concord().size());\n\n        assertEquals(\"glob:abc\", resources.concord().get(0));\n        assertEquals(\"regex:boo\", resources.concord().get(1));\n        assertEquals(\"concord/myfile.yml\", resources.concord().get(2));\n    }\n\n    // set variables definition\n    @Test\n    public void test016() throws Exception {\n        ProcessDefinition pd = load(\"016.yml\");\n\n        List<Step> main = pd.flows().get(\"default\").steps();\n        assertEquals(1, main.size());\n\n        assertTrue(main.get(0) instanceof SetVariablesStep);\n        SetVariablesStep t = (SetVariablesStep) main.get(0);\n        assertMeta(t.getOptions());\n\n        Map<String, Serializable> m = new HashMap<>();\n        m.put(\"k\", \"v\");\n        m.put(\"x\", 2);\n        assertEquals(m, t.getVars());\n    }\n\n    // exit step\n    @Test\n    public void test017() throws Exception {\n        ProcessDefinition pd = load(\"017.yml\");\n\n        List<Step> main = pd.flows().get(\"default\").steps();\n        assertEquals(1, main.size());\n\n        assertTrue(main.get(0) instanceof ExitStep);\n        ExitStep t = (ExitStep) main.get(0);\n        assertNotNull(t.getLocation());\n    }\n\n    // return step\n    @Test\n    public void test018() throws Exception {\n        ProcessDefinition pd = load(\"018.yml\");\n\n        List<Step> main = pd.flows().get(\"default\").steps();\n        assertEquals(1, main.size());\n\n        assertTrue(main.get(0) instanceof ReturnStep);\n        ReturnStep t = (ReturnStep) main.get(0);\n        assertNotNull(t.getLocation());\n    }\n\n    // Profiles\n    @Test\n    public void test019() throws Exception {\n        ProcessDefinition pd = load(\"019.yml\");\n\n        assertTrue(pd.flows().isEmpty());\n        assertEquals(1, pd.profiles().size());\n\n        Profile p1 = pd.profiles().get(\"p1\");\n        assertNotNull(p1);\n\n        // flows\n        assertEquals(1, p1.flows().size());\n        assertTrue(p1.flows().get(\"default\").steps().get(0) instanceof ReturnStep);\n\n        assertEquals(1, p1.forms().size());\n        assertNotNull(p1.forms().get(\"myForm\"));\n        assertEquals(2, p1.forms().get(\"myForm\").fields().size());\n\n        assertTrue(p1.configuration().debug());\n    }\n\n    @Test // GitHub trigger exclusive grouping\n    void test020() throws Exception {\n        ProcessDefinition pd = load(\"020.yml\");\n\n        List<Trigger> triggers = pd.triggers();\n        assertNotNull(triggers);\n\n        assertEquals(2, triggers.size());\n\n        Trigger t = triggers.get(0);\n        assertEquals(\"github\", t.name());\n        var exclusive = assertInstanceOf(GithubTriggerExclusiveMode.class, t.configuration().get(\"exclusive\"));\n        assertEquals(\"branch\", exclusive.groupByProperty());\n\n        t = triggers.get(1);\n        assertEquals(\"github\", t.name());\n        exclusive = assertInstanceOf(GithubTriggerExclusiveMode.class, t.configuration().get(\"exclusive\"));\n        assertEquals(\"event.pull_request.html_url\", exclusive.groupByProperty());\n    }\n\n    @Test\n    void test021() throws Exception {\n        ProcessDefinition pd = load(\"021.yml\");\n\n        List<Trigger> triggers = pd.triggers();\n        assertNotNull(triggers);\n\n        assertEquals(1, triggers.size());\n\n        Trigger t = triggers.get(0);\n        assertEquals(\"manual\", t.name());\n        var exclusive = assertInstanceOf(ExclusiveMode.class, t.configuration().get(\"exclusive\"));\n        assertEquals(\"manual-group\", exclusive.group());\n        assertEquals(ExclusiveMode.Mode.cancel, exclusive.mode());\n    }\n\n    @Test\n    public void testArgsOrder() throws Exception {\n        ProcessDefinition pd = load(\"args-order.concord.yml\");\n        Map.Entry<String, Object> e = pd.configuration().arguments().entrySet().iterator().next();\n        assertEquals(\"a\", e.getKey());\n    }\n\n    @Test\n    public void testValidationConfiguration() throws Exception {\n        ProcessDefinition pd = load(\"validationConfig.yml\");\n\n        ValidationConfiguration validation = pd.configuration().validation();\n        assertNotNull(validation);\n\n        TaskCallValidation taskCalls = validation.taskCalls();\n        assertNotNull(taskCalls);\n        assertEquals(TaskCallValidation.ValidationMode.FAIL, taskCalls.in());\n        assertEquals(TaskCallValidation.ValidationMode.WARN, taskCalls.out());\n    }\n\n    private static void assertMeta(StepOptions o) {\n        assertMeta(null, o);\n    }\n\n    private static void assertMeta(String stepName, StepOptions o) {\n        assertNotNull(o.meta());\n        Map<String, Serializable> expected = new HashMap<>();\n        if (stepName != null) {\n            expected.put(\"segmentName\", stepName);\n        }\n        expected.put(\"m1\", \"v1\");\n        assertEquals(expected, o.meta());\n    }\n}\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/000.1.yml",
    "content": "flows:\n  main:\n    - name: \"Boo\"\n      task: \"boo\"\n      in: ${inExpr}\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/000.2.yml",
    "content": "flows:\n  main:\n    - task: project\n      in:\n        action: create\n        org: Default\n        name: ProjectName\n      name: \"Test name\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/000.yml",
    "content": "flows:\n  main:\n    - task: \"boo\"\n      name: \"Boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      loop:\n        items: 1\n      retry:\n        times: 1\n        delay: 2\n        in:\n          k: \"retry-1\"\n          k2: \"retry-2\"\n          k3:\n            kk: \"vv\"\n      meta:\n        m1: \"v1\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/001.yml",
    "content": "flows:\n  main:\n    - myShortTask: \"boo\"\n      meta:\n        m1: \"v1\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/002.1.yml",
    "content": "flows:\n  main:\n    - name: \"boo-call\"\n      call: \"boo\"\n      in: ${inExpr}\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/002.yml",
    "content": "flows:\n  main:\n    - name: \"boo-call\"\n      call: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      retry:\n        times: 1\n        delay: 2\n        in:\n          k: \"retry-1\"\n          k2: \"retry-2\"\n          k3:\n            kk: \"vv\"\n      meta:\n        m1: \"v1\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/003.yml",
    "content": "flows:\n  main:\n    - checkpoint: \"ZZZ\"\n      meta:\n        m1: \"v1\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/004.yml",
    "content": "flows:\n  main:\n    - name: \"expression-call\"\n      expr: \"${boo}\"\n      out: \"result\"\n      meta:\n        m1: \"v1\"\n      error:\n        - \"${booError}\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/005.yml",
    "content": "flows:\n  main:\n    - name: \"Test try\"\n      try:\n        - \"${exp}\"\n      error:\n        - \"${exp}\"\n      meta:\n        m1: \"v1\"\n      withItems:\n        - \"a\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/006.yml",
    "content": "flows:\n  main:\n    - form: myForm\n      yield: true\n      saveSubmittedBy: true\n      fields:\n        - firstName: {type: \"string\"}\n        - lastName: {type: \"string\"}\n      runAs:\n        username:\n          - \"userA\"\n          - \"userB\"\n      values:\n        myField: \"a different value\"\n\n    - form: myForm\n      fields:\n        - firstName: {type: \"string\"}\n        - lastName: {type: \"string\"}\n      runAs: \"${{ 'username': [ 'userA', 'userB' ] }}\"\n      values: \"${{ 'fieldA': 'valueA' }}\"\n\nforms:\n  myForm:\n    - fullName: { label: \"Name\", type: \"string\", pattern: \".* .*\", readonly: true, placeholder: \"Place name here\" }\n    - age: { label: \"Age\", type: \"int\", min: 21, max: 100 }\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/007.yml",
    "content": "imports:\n  - git:\n      url: \"https://github.com/me/my_private_repo.git\"\n      name: \"test\"\n      version: \"1.2.3\"\n      path: \"/\"\n      dest: \"/dest\"\n      exclude:\n        - \"a\"\n        - \"b\"\n      secret:\n        name: \"my_secret_key\"\n  - git:\n      url: \"https://github.com/walmartlabs/concord.git\"\n      path: \"examples/hello_world\"\n  - mvn:\n      url: \"mvn://groupId:artifactId:version\"\n      dest: \"/dest\"\n  - dir:\n      src: \"/some/path\"\n      dest: \"/dest\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/008.yml",
    "content": "configuration:\n  debug: true\n  entryPoint: \"main-test\"\n  dependencies:\n    - \"d1\"\n    - \"d2\"\n  extraDependencies:\n    - \"d3\"\n    - \"d4\"\n  arguments:\n    k: \"v\"\n  requirements:\n    k: \"v1\"\n  processTimeout: \"PT1H\"\n  exclusive:\n    group: \"X\"\n    mode: \"cancel\"\n  events:\n    recordTaskInVars: true\n    inVarsBlacklist:\n      - \"pass\"\n    recordTaskOutVars: true\n    outVarsBlacklist:\n      - \"bass\"\n    recordTaskMeta: true\n    metaBlacklist:\n      - \"bass\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/009.yml",
    "content": "triggers:\n  - github:\n      version: 2\n      useInitiator: true\n      entryPoint: onPush\n      ignoreEmptyPush: false\n      conditions:\n        type: \"push\"\n        githubOrg: \".*\"\n        githubRepo: \".*\"\n        branch: \".*\"\n        repositoryInfo:\n          - repository: \".*\"\n            enabled: true\n\n  - github:\n      version: 2\n      useInitiator: true\n      entryPoint: onPush2\n      conditions:\n        type: push2\n\n  - cron:\n      spec: \"* 12 * * *\"\n      entryPoint: eventOutput\n      runAs:\n        withSecret: \"secret-name\"\n      activeProfiles:\n        - myProfile\n      arguments:\n        name: \"Concord\"\n\n  - manual:\n      name: Deploy Dev and Test\n      entryPoint: deployDev\n      activeProfiles:\n        - devProfile\n      arguments:\n        runTests: true\n\n  - example:\n      version: 2\n      entryPoint: exampleFLow\n      conditions:\n        aField: \"aValue\"\n\n  - oneops:\n      version: 2\n      conditions:\n        org: \"myOrganization\"\n        asm: \"myAssembly\"\n        env: \"myEnvironment\"\n        platform: \"myPlatform\"\n        type: \"deployment\"\n        deploymentState: \"complete\"\n      useInitiator: true\n      entryPoint: onDeployment"
  },
  {
    "path": "runtime/v2/model/src/test/resources/010.yml",
    "content": "flows:\n  default:\n    - if: \"${myVar > 0}\"\n      then:\n        - log: \"it's clearly non-zero\"\n        - log: \"yep\"\n      else:\n        - log: \"zero or less\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/011.yml",
    "content": "flows:\n  default:\n    - if: \"${myVar > 0}\"\n      then:\n        - log: \"it's clearly non-zero\"\n        - log: \"yep\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/012.yml",
    "content": "flows:\n  default:\n    - switch: \"${myVar}\"\n      red:\n        - log: \"It's red!\"\n      green:\n        - log: \"It's definitely green\"\n      default:\n        - log: \"I don't know what it is\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/013.yml",
    "content": "publicFlows:\n  - publicFlow\n\nflows:\n  publicFlow:\n    - log: \"This is the public flow\"\n\n  privateFlow:\n    - log: \"This is the private flow\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/014.1.yml",
    "content": "flows:\n  default:\n    - script: groovy\n      body: |\n        throw new RuntimeException(\"kaboom!\")\n      in: ${inExpr}\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/014.yml",
    "content": "flows:\n  default:\n    - name: \"Test script\"\n      script: groovy\n      body: |\n        throw new RuntimeException(\"kaboom!\")\n      in:\n        k: \"v1\"\n      out: result\n      error:\n        - log: \"Caught an error\"\n      withItems: 1\n      retry:\n        times: 1\n        delay: 2\n        in:\n          k: \"v\"\n      meta:\n        m1: \"v1\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/015.yml",
    "content": "resources:\n  concord:\n    - \"glob:abc\"\n    - \"regex:boo\"\n    - \"concord/myfile.yml\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/016.yml",
    "content": "flows:\n  default:\n    - set:\n        k: \"v\"\n        x: 2\n      meta:\n        m1: \"v1\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/017.yml",
    "content": "flows:\n  default:\n    - exit"
  },
  {
    "path": "runtime/v2/model/src/test/resources/018.yml",
    "content": "flows:\n  default:\n    - return"
  },
  {
    "path": "runtime/v2/model/src/test/resources/019.yml",
    "content": "profiles:\n  p1:\n    configuration:\n      debug: true\n\n    flows:\n      default:\n        - return\n\n    forms:\n      myForm:\n        - fullName: { label: \"Name\", type: \"string\", pattern: \".* .*\", readonly: true, placeholder: \"Place name here\" }\n        - age: { label: \"Age\", type: \"int\", min: 21, max: 100 }\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/020.yml",
    "content": "\ntriggers:\n  - github:\n      version: 2\n      useInitiator: true\n      entryPoint: onPush\n      conditions:\n        type: \"push\"\n      exclusive:\n        groupBy: branch\n\n  - github:\n      version: 2\n      useInitiator: true\n      entryPoint: onPush2\n      conditions:\n        type: push2\n      exclusive:\n        groupBy: \"event.pull_request.html_url\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/021.yml",
    "content": "flows:\n  main:\n    - log: \"hello: ${v}\"\n\ntriggers:\n  - manual:\n      name: \"test\"\n      entryPoint: \"main\"\n      arguments:\n        k: \"v\"\n      activeProfiles:\n        - \"profile-1\"\n      exclusive:\n        mode: \"cancel\"\n        group: \"manual-group\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/args-order.concord.yml",
    "content": "flows:\n  default:\n    - log: \"DONE\"\n\nconfiguration:\n  arguments:\n    a: \"${log.info('a-main')}\"\n    bb: \"${log.info('bb-main')}\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/checkpoint/000.yml",
    "content": "flows:\n  main:\n    - checkpoint:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/checkpoint/001.yml",
    "content": "flows:\n  main:\n    - checkpoint: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/checkpoint/002.yml",
    "content": "flows:\n  main:\n    - checkpoint: \"test\"\n      trash: \"boo\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/checkpoint/003.yml",
    "content": "flows:\n  main:\n    - checkpoint: \"test\"\n      meta: \"boo\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/checkpoint/004.yml",
    "content": "flows:\n  main:\n    - checkpoint: \"test\"\n      meta:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/checkpoint/005.yml",
    "content": "flows:\n  main:\n    - checkpoint: \"test\"\n      meta:\n        k: \"v\"\n      trash: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/000.yml",
    "content": "configuration:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/001.yml",
    "content": "configuration:\n  entryPoint:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/002.yml",
    "content": "configuration:\n  entryPoint: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/003.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  dependencies:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/004.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  dependencies:\n    - 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/005.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  dependencies:\n    - \"d1\"\n    - \"d2\"\n  arguments:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/005_1.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  dependencies:\n    - \"d1\"\n    - \"d2\"\n  arguments:\n    k:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/006.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  dependencies:\n    - \"d1\"\n    - \"d2\"\n  arguments:\n    k: \"v\"\n  trash:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/007.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  dependencies:\n    - \"d1\"\n    - \"d2\"\n  arguments:\n    k: \"v\"\n  meta: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/008.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  dependencies:\n    - \"d1\"\n    - \"d2\"\n  arguments:\n    k: \"v\"\n  requirements: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/009.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  dependencies:\n    - \"d1\"\n    - \"d2\"\n  arguments:\n    k: \"v\"\n  requirements:\n    k: \"v\"\n  processTimeout: \"PT\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/010.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  dependencies:\n    - \"d1\"\n    - \"d2\"\n  arguments:\n    k: \"v\"\n  requirements:\n    k: \"v\"\n  processTimeout: \"PT1H\"\n  exclusive: 1\n#    group: \"myGroup\"\n#    mode: \"cancel\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/011.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  dependencies:\n    - \"d1\"\n    - \"d2\"\n  arguments:\n    k: \"v\"\n  requirements:\n    k: \"v\"\n  processTimeout: \"PT1H\"\n  exclusive:\n    mode: \"cancel\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/011_1.yml",
    "content": "configuration:\n  exclusive:\n    mode: \"cancel\"\n    group: \"\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/012.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  dependencies:\n    - \"d1\"\n    - \"d2\"\n  arguments:\n    k: \"v\"\n  requirements:\n    k: \"v\"\n  processTimeout: \"PT1H\"\n  exclusive:\n    group: \"X\"\n    mode1: \"cancel\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/013.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  dependencies:\n    - \"d1\"\n    - \"d2\"\n  arguments:\n    k: \"v\"\n  requirements:\n    k: \"v\"\n  processTimeout: \"PT1H\"\n  exclusive:\n    group: \"X\"\n    mode: \"canceL\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/014.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  dependencies:\n    - \"d1\"\n    - \"d2\"\n  arguments:\n    k: \"v\"\n  requirements:\n    k: \"v\"\n  processTimeout: \"PT1H\"\n  exclusive:\n    group: \"X\"\n    mode: \"cancel\"\n  events: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/015.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  dependencies:\n    - \"d1\"\n    - \"d2\"\n  arguments:\n    k: \"v\"\n  requirements:\n    k: \"v\"\n  processTimeout: \"PT1H\"\n  exclusive:\n    group: \"X\"\n    mode: \"cancel\"\n  events:\n    recordTaskInVars: true\n    inVarsBlacklist: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/016.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  dependencies:\n    - \"d1\"\n    - \"d2\"\n  arguments:\n    k: \"v\"\n  requirements:\n    k: \"v\"\n  processTimeout: \"PT1H\"\n  exclusive:\n    group: \"X\"\n    mode: \"cancel\"\n  events:\n    recordTaskInVars: true\n    inVarsBlacklist:\n      - \"password\"\n      - \"apiToken\"\n      - \"apiKey\"\n    recordTaskOutVars: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/017.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  dependencies:\n    - \"d1\"\n    - \"d2\"\n  arguments:\n    k: \"v\"\n  requirements:\n    k: \"v\"\n  processTimeout: \"PT1H\"\n  exclusive:\n    group: \"X\"\n    mode: \"cancel\"\n  events:\n    recordTaskInVars: true\n    inVarsBlacklist:\n      - \"password\"\n      - \"apiToken\"\n      - \"apiKey\"\n    recordTaskOutVars: true\n    outVarsBlacklist: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/018.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  dependencies:\n    - \"d1\"\n    - \"d2\"\n  arguments:\n    k: \"v\"\n  requirements:\n    k: \"v\"\n  processTimeout: \"PT1H\"\n  exclusive:\n    group: \"X\"\n    mode: \"cancel\"\n  events:\n    recordTaskInVars: true\n    inVarsBlacklist:\n      - \"password\"\n      - \"apiToken\"\n      - \"apiKey\"\n    recordTaskOutVars: true\n    outVarsBlacklist:\n      - \"FOO\"\n    truncateMaxStringLength: 128\n    truncateMaxArrayLength: 8\n    truncateMaxDepth: 5\n    trash:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/019.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  dependencies:\n    - \"d1\"\n    - \"d2\"\n  arguments:\n    k: \"v\"\n  requirements:\n    k: \"v\"\n  processTimeout: \"PT1H\"\n  exclusive:\n    group: \"X\"\n    mode: \"cancel\"\n  events:\n    recordTaskInVars: true\n    inVarsBlacklist:\n      - \"password\"\n      - \"apiToken\"\n      - \"apiKey\"\n    recordTaskOutVars: true\n    outVarsBlacklist:\n      - \"FOO\"\n  out: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/020.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  template:\n    - \"t1\"\n    - \"t2\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/021.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  dependencies:\n    - \"d1\"\n    - \"d2\"\n  arguments:\n    k: \"v\"\n  requirements:\n    k: \"v\"\n  processTimeout: \"PT1H\"\n  exclusive:\n    group: \"X\"\n    mode: \"cancel\"\n  events:\n    recordTaskInVars: true\n    inVarsBlacklist:\n      - \"password\"\n      - \"apiToken\"\n      - \"apiKey\"\n    recordTaskOutVars: true\n    outVarsBlacklist:\n      - \"FOO\"\n    recordTaskMeta: 1\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/022.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  dependencies:\n    - \"d1\"\n    - \"d2\"\n  arguments:\n    k: \"v\"\n  requirements:\n    k: \"v\"\n  processTimeout: \"PT1H\"\n  exclusive:\n    group: \"X\"\n    mode: \"cancel\"\n  events:\n    recordTaskInVars: true\n    inVarsBlacklist:\n      - \"password\"\n      - \"apiToken\"\n      - \"apiKey\"\n    recordTaskOutVars: true\n    outVarsBlacklist:\n      - \"FOO\"\n    recordTaskMeta: true\n    metaBlacklist: 1\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/023.yml",
    "content": "configuration:\n  entryPoint: \"main-test\"\n  dependencies:\n    - \"d1\"\n    - \"d2\"\n  arguments:\n    k: \"v\"\n  requirements:\n    k: \"v\"\n  processTimeout: \"PT1H\"\n  suspendTimeout: \"PT\"\n  exclusive:\n    group: \"X\"\n    mode: \"cancel\"\n  events:\n    recordTaskInVars: true\n    inVarsBlacklist:\n      - \"password\"\n      - \"apiToken\"\n      - \"apiKey\"\n    recordTaskOutVars: true\n    outVarsBlacklist:\n      - \"FOO\"\n    recordTaskMeta: true\n    metaBlacklist:\n      - \"apiKey\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/configuration/024.yml",
    "content": "configuration:\n  validation:\n    taskCalls:\n      in: fatal\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/expression/000.yml",
    "content": "flows:\n  main:\n    - expr:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/expression/001.yml",
    "content": "flows:\n  main:\n    - expr: \"123\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/expression/002.yml",
    "content": "flows:\n  main:\n    - expr: \"${boo}\"\n      out:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/expression/003.yml",
    "content": "flows:\n  main:\n    - expr: \"${boo}\"\n      out: 123"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/expression/005.yml",
    "content": "flows:\n  main:\n    - expr: \"${boo}\"\n      out: \"result\"\n      meta:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/expression/006.yml",
    "content": "flows:\n  main:\n    - expr: \"${boo}\"\n      out: \"result\"\n      meta: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/expression/007.yml",
    "content": "flows:\n  main:\n    - expr: \"${boo}\"\n      out: \"result\"\n      meta:\n        k: \"v\"\n      error:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/expression/008.yml",
    "content": "flows:\n  main:\n    - expr: \"${boo}\"\n      out: \"result\"\n      meta:\n        k: \"v\"\n      error: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/expression/009.yml",
    "content": "flows:\n  main:\n    - expr: \"${boo}\"\n      out: \"result\"\n      meta:\n        k: \"v\"\n      error:\n        -"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/expression/010.yml",
    "content": "flows:\n  main:\n    - expr: \"${boo}\"\n      out: \"result\"\n      meta:\n        k: \"v\"\n      error:\n        - \"a\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/flowCall/000.yml",
    "content": "flows:\n  main:\n    - call:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/flowCall/001.yml",
    "content": "flows:\n  main:\n    - call: 123"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/flowCall/002.yml",
    "content": "flows:\n  main:\n    - call: \"boo\"\n      out:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/flowCall/003.yml",
    "content": "flows:\n  main:\n    - call: \"boo\"\n      out: 123"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/flowCall/005.yml",
    "content": "flows:\n  main:\n    - call: \"boo\"\n      out: \"result\"\n      in:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/flowCall/006.yml",
    "content": "flows:\n  main:\n    - call: \"boo\"\n      out: \"result\"\n      in: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/flowCall/007.yml",
    "content": "flows:\n  main:\n    - call: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/flowCall/008.yml",
    "content": "flows:\n  main:\n    - call: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/flowCall/009.yml",
    "content": "flows:\n  main:\n    - call: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      retry:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/flowCall/010.yml",
    "content": "flows:\n  main:\n    - call: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      retry: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/flowCall/011.yml",
    "content": "flows:\n  main:\n    - call: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      retry:\n        times:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/flowCall/012.yml",
    "content": "flows:\n  main:\n    - call: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      retry:\n        times: 1\n        delay:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/flowCall/013.yml",
    "content": "flows:\n  main:\n    - call: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      retry:\n        times: 1\n        delay: 1\n        in:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/flowCall/014.yml",
    "content": "flows:\n  main:\n    - call: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      retry:\n        times: 1\n        delay: 1\n        in: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/flowCall/015.yml",
    "content": "flows:\n  main:\n    - call: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      retry:\n        times: 1\n        delay: 1\n        in:\n          k: \"v\"\n      trash: value"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/flowCall/016.yml",
    "content": "flows:\n  main:\n    - call: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      retry:\n        times: 1\n        delay: 1\n        in:\n          k: \"v\"\n      meta:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/flowCall/017.yml",
    "content": "flows:\n  main:\n    - call: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      retry:\n        times: 1\n        delay: 1\n        in:\n          k: \"v\"\n      meta: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/flowCall/018.yml",
    "content": "flows:\n  main:\n    - call: \"boo\"\n      out:\n        - 1\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/flows/000.yml",
    "content": "flows:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/flows/001.yml",
    "content": "flows: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/flows/002.yml",
    "content": "flows:\n  main:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/formCall/000.yml",
    "content": "flows:\n  main:\n    - form:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/formCall/001.yml",
    "content": "flows:\n  main:\n    - form: 123"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/formCall/002.yml",
    "content": "flows:\n  main:\n    - form: \"boo\"\n      a:\n      yield: true\n      b:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/formCall/003.yml",
    "content": "flows:\n  main:\n    - form: \"boo\"\n      yield: \"true\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/formCall/004.yml",
    "content": "flows:\n  main:\n    - form: \"boo\"\n      yield: true\n      saveSubmittedBy:\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/formCall/005.yml",
    "content": "flows:\n  main:\n    - form: \"boo\"\n      yield: true\n      saveSubmittedBy: true\n      runAs: [ \"one\" ]\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/formCall/006.yml",
    "content": "flows:\n  main:\n    - form: \"boo\"\n      yield: true\n      saveSubmittedBy: true\n      runAs:\n        username:\n          - \"userA\"\n          - \"userB\"\n      values: [ \"one\" ]"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/formCall/007.yml",
    "content": "flows:\n  main:\n    - form: \"boo\"\n      yield: true\n      saveSubmittedBy: true\n      runAs:\n        username:\n          - \"userA\"\n          - \"userB\"\n      values:\n          a: \"b\"\n      fields: \"er\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/formCall/008.yml",
    "content": "flows:\n  main:\n    - form: \"boo\"\n      fields: \"er\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/forms/000.yml",
    "content": "forms:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/forms/001.yml",
    "content": "forms:\n  k: v"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/forms/002.yml",
    "content": "forms:\n  myForm:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/forms/003.yml",
    "content": "forms:\n  myForm:\n    - k"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/forms/004.yml",
    "content": "forms:\n  myForm:\n    - fullName:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/forms/005.yml",
    "content": "forms:\n  myForm:\n    - fullName: { label: \"Name\", type: \"string\", pattern: \".* .*\", readonly: true, placeholder: \"Place name here\", error: 1 }"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/forms/006.yml",
    "content": "forms:\n  myForm:\n    - fullName: { label: 123, type: \"string\", pattern: \".* .*\", readonly: true, placeholder: \"Place name here\" }"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/forms/007.yml",
    "content": "forms:\n  myForm:\n    - fullName: { type: 123, pattern: \".* .*\", readonly: true, placeholder: \"Place name here\" }"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/forms/008.yml",
    "content": "forms:\n  myForm:\n    - fullName: { pattern: \".* .*\", readonly: true, placeholder: \"Place name here\" }"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/forms/009.yml",
    "content": "forms:\n  myForm:\n    - fullName: { type: \"123\", pattern: \".* .*\", readonly: true, placeholder: \"Place name here\" }"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/forms/010.yml",
    "content": "forms:\n  myForm:\n    - fullName: { type: \"string+\", pattern: \".* .*\", readOnly: 1, placeholder: \"Place name here\" }"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/forms/011.yml",
    "content": "forms: \"string\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/forms/012.yml",
    "content": "forms:\n  myForm:\n    - fullName: { type: \"string+\", pattern: \".* .*\", readOnly: 1, placeholder: \"Place name here\" }"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/forms/013.yml",
    "content": "forms:\n  invalid.name:\n    - fullName: { type: \"string+\" }\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/group/000.yml",
    "content": "flows:\n  main:\n    - try:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/group/001.yml",
    "content": "flows:\n  main:\n    - try: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/group/002.yml",
    "content": "flows:\n  main:\n    - try:\n        -"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/group/003.yml",
    "content": "flows:\n  main:\n    - try:\n        - \"${exp}\"\n      trash:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/group/004.yml",
    "content": "flows:\n  main:\n    - try:\n        - \"${exp}\"\n      error:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/group/005.yml",
    "content": "flows:\n  main:\n    - try:\n        - \"${exp}\"\n      error:\n        - \"${exp}\"\n      meta:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/group/006.yml",
    "content": "flows:\n  main:\n    - try:\n        - \"${exp}\"\n      error:\n        - \"${exp}\"\n      meta:\n        m1: \"v1\"\n      withItems:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/group/007.yml",
    "content": "flows:\n  main:\n    - try:\n        - \"${exp}\"\n      error:\n        - \"${exp}\"\n      meta:\n        m1: \"v1\"\n      withItems:\n        - \"a\"\n      trash:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/group/008.yml",
    "content": "flows:\n  main:\n    - block:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/if/000.yml",
    "content": "flows:\n  main:\n    - if:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/if/001.yml",
    "content": "flows:\n  main:\n    - if: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/if/002.yml",
    "content": "flows:\n  main:\n    - if: \"${myVar > 0}\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/if/003.yml",
    "content": "flows:\n  main:\n    - if: \"${myVar > 0}\"\n      then:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/if/004.yml",
    "content": "flows:\n  main:\n    - if: \"${myVar > 0}\"\n      then:\n        - log: \"it's clearly non-zero\"\n      el:\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/if/005.yml",
    "content": "flows:\n  main:\n    - if: \"${myVar > 0}\"\n      then:\n        - log: \"it's clearly non-zero\"\n      else:\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/if/006.yml",
    "content": "flows:\n  main:\n    - if: \"${myVar > 0}\"\n      then:\n        - log: \"it's clearly non-zero\"\n      else:\n        - log: \"zero or less\"\n      trash:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/if/007.yml",
    "content": "flows:\n  main:\n    - if: \"${myVar > 0}\"\n      then:\n        - log: \"it's clearly non-zero\"\n      else:\n        - log: \"zero or less\"\n      meta:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/imports/001.yml",
    "content": "imports: trash\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/imports/002.yml",
    "content": "imports:\n  k: v\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/imports/003.yml",
    "content": "imports:\n  - git:\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/imports/004.yml",
    "content": "imports:\n  - git: trash"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/imports/005.yml",
    "content": "imports:\n  - git:\n    trash: value"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/imports/006.yml",
    "content": "imports:\n  - git:\n      name: 1\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/imports/007.yml",
    "content": "imports:\n  - git:\n      name: \"org/repo\"\n      version: 123\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/imports/008.yml",
    "content": "imports:\n  - git:\n      name: \"org/repo\"\n      version: \"1.0\"\n      path:\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/imports/009.yml",
    "content": "imports:\n  - git:\n      name: \"org/repo\"\n      version: \"dev\"\n      path: \"root\"\n      dest: \"tmp\"\n      secret:\n        org: 1\n        name: \"secret-name\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/imports/010.yml",
    "content": "imports:\n  - git:\n      name: \"org/repo\"\n      version: \"dev\"\n      path: \"root\"\n      dest: \"tmp\"\n      secret:\n        org: \"default\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/imports/011.yml",
    "content": "imports:\n  - git-trash:\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/imports/012.yml",
    "content": "imports:\n  - git:\n      name: \"org/repo\"\n      exclude: \"dev\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/imports/013.yml",
    "content": "imports:\n  - git:\n      name: \"org/repo\"\n      version: \"dev\"\n      path: \"root\"\n      dest: \"tmp\"\n      trash: \"XXX\"\n      secret:\n        org: \"default\"\n        name: \"test\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/imports/014.yml",
    "content": "imports:\n  - mvn: \"one\"\n  - git: \"two\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/imports/015.yml",
    "content": "imports:\n  - git:\n      name: \"org/repo\"\n      exclude:\n        - \"dev\"\n        - 1\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/imports/016.yml",
    "content": "imports:\n  - git:\n      name: \"org/repo\"\n      exclude:\n        - \"test\"\n        - \"[.\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/parallel/000.yml",
    "content": "flows:\n  main:\n    - parallel:\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/parallel/001.yml",
    "content": "flows:\n  main:\n    - parallel: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/parallel/002.yml",
    "content": "flows:\n  main:\n    - parallel:\n        - null"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/parallel/003.yml",
    "content": "flows:\n  main:\n    - parallel:\n        - \"${exp}\"\n      trash:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/parallel/004.yml",
    "content": "flows:\n  main:\n    - parallel:\n        - \"${exp}\"\n      meta:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/parallel/005.yml",
    "content": "flows:\n  main:\n    - parallel:\n        - \"${exp}\"\n      meta:\n        m1: \"v1\"\n      trash:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/profiles/000.yml",
    "content": "profiles:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/profiles/001.yml",
    "content": "profiles:\n  myProfile: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/profiles/002.yml",
    "content": "profiles:\n  myProfile:\n    flows: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/profiles/003.yml",
    "content": "profiles:\n  myProfile:\n    trash: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/publicFlows/000.yml",
    "content": "publicFlows:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/publicFlows/001.yml",
    "content": "publicFlows: myflow"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/publicFlows/002.yml",
    "content": "publicFlows:\n  - 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/resources/000.yml",
    "content": "resources:\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/resources/001.yml",
    "content": "resources:\n  trash:\n    - \"glob:abc\"\n    - \"regex:boo\"\n    - \"concord/myfile.yml\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/resources/002.yml",
    "content": "resources:\n  concord: \"/test\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/scripts/000.yml",
    "content": "flows:\n  main:\n    - script:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/scripts/001.yml",
    "content": "flows:\n  main:\n    - script: 1\n      body: |\n        throw new RuntimeException(\"kaboom!\")"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/scripts/002.yml",
    "content": "flows:\n  main:\n    - script: \"groovy\"\n      body1: |\n        throw new RuntimeException(\"kaboom!\")"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/scripts/003.yml",
    "content": "flows:\n  main:\n    - script: \"groovy\"\n      body: |\n        throw new RuntimeException(\"kaboom!\")\n      error:\n        - \"\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/setVariables/000.yml",
    "content": "flows:\n  main:\n    - set:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/setVariables/001.yml",
    "content": "flows:\n  main:\n    - set:\n        k: \"v\"\n        x: \"y\"\n      meta1:\n        boo: moo"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/switch/000.yml",
    "content": "flows:\n  main:\n    - switch:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/switch/001.yml",
    "content": "flows:\n  main:\n    - switch: \"${myVar}\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/switch/002.yml",
    "content": "flows:\n  main:\n    - switch: \"${myVar}\"\n      red:\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/switch/003.yml",
    "content": "flows:\n  main:\n    - switch: \"${myVar}\"\n      red:\n        - log: \"It's red!\"\n      default:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/tasks/000.yml",
    "content": "flows:\n  main:\n    - task:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/tasks/001.yml",
    "content": "flows:\n  main:\n    - task: 123"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/tasks/002.yml",
    "content": "flows:\n  main:\n    - task: \"boo\"\n      out:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/tasks/003.yml",
    "content": "flows:\n  main:\n    - task: \"boo\"\n      out: 123"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/tasks/005.yml",
    "content": "flows:\n  main:\n    - task: \"boo\"\n      out: \"result\"\n      in:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/tasks/006.yml",
    "content": "flows:\n  main:\n    - task: \"boo\"\n      out: \"result\"\n      in: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/tasks/007.yml",
    "content": "flows:\n  main:\n    - task: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/tasks/008.yml",
    "content": "flows:\n  main:\n    - task: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/tasks/009.yml",
    "content": "flows:\n  main:\n    - task: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      retry:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/tasks/010.yml",
    "content": "flows:\n  main:\n    - task: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      retry: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/tasks/011.yml",
    "content": "flows:\n  main:\n    - task: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      retry:\n        times:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/tasks/012.yml",
    "content": "flows:\n  main:\n    - task: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      retry:\n        times: 1\n        delay:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/tasks/013.yml",
    "content": "flows:\n  main:\n    - task: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      retry:\n        times: 1\n        delay: 1\n        in:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/tasks/014.yml",
    "content": "flows:\n  main:\n    - task: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      retry:\n        times: 1\n        delay: 1\n        in: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/tasks/015.yml",
    "content": "flows:\n  main:\n    - task: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      retry:\n        times: 1\n        delay: 1\n        in:\n          k: \"v\"\n      trash: value"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/tasks/016.yml",
    "content": "flows:\n  main:\n    - task: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      retry:\n        times: 1\n        delay: 1\n        in:\n          k: \"v\"\n      meta:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/tasks/017.yml",
    "content": "flows:\n  main:\n    - task: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      retry:\n        times: 1\n        delay: 1\n        in:\n          k: \"v\"\n      meta: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/tasks/018.yml",
    "content": "flows:\n  main:\n    - task: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      retry:\n        times: \"eeee\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/tasks/019.yml",
    "content": "flows:\n  main:\n    - name: 123123\n      task: \"boo\"\n      out: \"result\"\n      in:\n        k: \"v\"\n        k2: 2\n        k3: false\n      withItems: 1\n      retry:\n        times: \"eeee\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/tasks/020.yml",
    "content": "flows:\n  main:\n    - name: \"123123\"\n      task: \"boo\"\n      loop:\n        items: 1\n        mode: trash"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/tasks/021.yml",
    "content": "flows:\n  main:\n    - name: \"123123\"\n      task: \"boo\"\n      loop:\n        items: 1\n        parallelism: one"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/001.yml",
    "content": "triggers: trash\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/002.yml",
    "content": "triggers:\n  - github:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/003.yml",
    "content": "triggers:\n  - github:\n      version: \"x\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/004.yml",
    "content": "triggers:\n  - github:\n      version: 2"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/005.yml",
    "content": "triggers:\n  - github:\n      version: 2\n      entryPoint: 123\n      conditions:\n        type: \"push\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/006.yml",
    "content": "triggers:\n  - github:\n      version: 2\n      entryPoint: \"onGithub\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/007.yml",
    "content": "triggers:\n  - github:\n      version: 2\n      entryPoint: \"onGithub\"\n      conditions: 1\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/008.yml",
    "content": "triggers:\n  - github:\n      version: 2\n      entryPoint: \"onGithub\"\n      conditions:\n        test: \"value\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/009.yml",
    "content": "triggers:\n  - github:\n      version: 2\n      entryPoint: \"onGithub\"\n      conditions:\n        type: \"push\"\n      activeProfiles: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/010.yml",
    "content": "triggers:\n  - github:\n      version: 2\n      entryPoint: \"onGithub\"\n      conditions:\n        type: \"push\"\n      activeProfiles:\n        - \"one\"\n      useInitiator: \"che\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/011.yml",
    "content": "triggers:\n  - github:\n      version: 2\n      entryPoint: \"onGithub\"\n      conditions:\n        type: \"push\"\n      activeProfiles:\n        - \"one\"\n      useInitiator: true\n      useEventCommitId: \"che?\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/012.yml",
    "content": "triggers:\n  - github:\n      version: 2\n      entryPoint: \"onGithub\"\n      conditions:\n        type: \"push\"\n      activeProfiles:\n        - \"one\"\n      useInitiator: true\n      useEventCommitId: true\n      exclusive: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/013.yml",
    "content": "triggers:\n  - github:\n      version: 2\n      entryPoint: \"onGithub\"\n      conditions:\n        type: \"push\"\n      activeProfiles:\n        - \"one\"\n      useInitiator: true\n      useEventCommitId: true\n      exclusive:\n        group: \"red\"\n      arguments: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/014.yml",
    "content": "triggers:\n  - github:\n      version: 2\n      entryPoint: \"onGithub\"\n      activeProfiles:\n          - \"one\"\n      useInitiator: true\n      useEventCommitId: true\n      exclusive:\n        group: \"red\"\n      arguments:\n        k: \"v\"\n      conditions:\n        type: \"push\"\n        githubOrg: \"*\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/015.yml",
    "content": "triggers:\n  - github:\n      version: 1\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/016.yml",
    "content": "triggers:\n  - cron:\n\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/017.yml",
    "content": "triggers:\n  - cron:\n      spec: 1\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/018.yml",
    "content": "triggers:\n  - cron:\n      spec: 1\n      entryPoint: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/019.yml",
    "content": "triggers:\n  - cron:\n      spec: \"*\"\n      entryPoint: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/020.yml",
    "content": "triggers:\n  - cron:\n      spec: \"*\"\n      entryPoint: \"main\"\n      activeProfiles: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/021.yml",
    "content": "triggers:\n  - cron:\n      spec: \"*\"\n      entryPoint: \"main\"\n      activeProfiles:\n        - \"main\"\n      arguments: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/022.yml",
    "content": "triggers:\n  - manual:\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/023.yml",
    "content": "triggers:\n  - manual:\n      name: Deploy Dev and Test\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/024.yml",
    "content": "triggers:\n  - manual:\n      name: Deploy Dev and Test\n      entryPoint: \"main\"\n      activeProfiles: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/025.yml",
    "content": "triggers:\n  - oneops:\n      entryPoint: \"main\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/026.yml",
    "content": "triggers:\n  - oneops:\n      version: 2\n      entryPoint: \"main\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/027.yml",
    "content": "triggers:\n  - oneops:\n      version: 2\n      entryPoint: \"main\"\n      conditions:\n        k: \"v\"\n      trash: \"boom\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/028.yml",
    "content": "triggers:\n  - example:\n      version: 2\n      entryPoint: exampleFLow\n      conditions: 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/029.yml",
    "content": "triggers:\n  - github:\n      version: 2\n      entryPoint: \"onGithub\"\n      conditions:\n        type: \"push\"\n        githubOrg:\n          - 1\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/030.yml",
    "content": "triggers:\n  - cron:\n      spec: \"*\"\n      entryPoint: \"foo\"\n      timezone: \"test\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/031.yml",
    "content": "triggers:\n  - cron:\n      spec: \"*\"\n      entryPoint: \"foo\"\n      timezone: \"Europe/Moscow\"\n      trash: \"unknown-attr\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/031_1.yml",
    "content": "triggers:\n  - cron:\n      spec: \"*\"\n      entryPoint: \"foo\"\n      runAs:\n        unknown: 123\n      timezone: \"Europe/Moscow\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/032.yml",
    "content": "triggers:\n  - github:\n      version: 2\n      entryPoint: \"onGithub\"\n      conditions:\n        type: \"push\"\n        repositoryInfo:\n          - 1"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/033.yml",
    "content": "triggers:\n  - github:\n      version: 2\n      entryPoint: \"onGithub\"\n      conditions:\n        type: \"push\"\n        repositoryInfo:"
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/034.yml",
    "content": "triggers:\n  - github:\n      version: 2\n      entryPoint: \"onGithub\"\n      conditions:\n        type: \"push\"\n        repositoryInfo:\n          - trash: \"1\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/035.yml",
    "content": "triggers:\n  - github:\n      version: 2\n      entryPoint: \"onGithub\"\n      conditions:\n        type: \"push\"\n        repositoryInfo:\n          - enabled: \"1\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/errors/triggers/037.yml",
    "content": "triggers:\n  - github:\n      version: 2\n      entryPoint: \"onGithub\"\n      exclusive:\n        mode: cancel\n      conditions:\n        type: \"push\""
  },
  {
    "path": "runtime/v2/model/src/test/resources/multiProjectFile/concord/1.concord.yml",
    "content": "flows:\n  default:\n  - log: \"hello from 1.concor.yml!\"\n\nforms:\n  myForm:\n  - myName: {type: \"string\"}\n\ntriggers:\n  - github:\n      entryPoint: \"1.concord.yml\"\n      version: 2\n      conditions:\n        type: \"push\"\n        sender: \".*some.*dude.*\"\n\npublicFlows:\n  - test1\n\nconfiguration:\n  arguments:\n    abc: \"xyz\"\n    nested:\n      value: \"123\"\n  processTimeout: \"PT2H\"\n  suspendTimeout: \"PT26H\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/multiProjectFile/concord/2.concord.yml",
    "content": "imports:\n  - git:\n      url: \"2.concord.yml\"\n\nconfiguration:\n  debug: false\n  activeProfiles:\n    - \"2.concord.yml\"\n  arguments:\n    abc: \"ttt\"\n    nested:\n      value: \"1232\"\n  dependencies:\n    - \"2.concord.yml\"\n  meta:\n    k: \"concord.yml\"\n  events:\n    recordTaskInVars: false\n    recordTaskOutVars: false\n    truncateInVars: false\n    truncateOutVars: false\n    truncateMaxStringLength: 10\n    truncateMaxArrayLength: 20\n    truncateMaxDepth: 30\n    inVarsBlacklist:\n      - \"2.concord.yml\"\n    recordTaskMeta: false\n    truncateMeta: false\n  requirements:\n    k: \"2.concord.yml\"\n  out:\n    - \"from-2.concord.yml\"\n\ntriggers:\n  - github:\n      entryPoint: \"2.concord.yml\"\n      version: 2\n      conditions:\n        sender: \".*some.*dude.*\"\n        type: \"push\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/multiProjectFile/concord/3.concord.yml",
    "content": "flows:\n  flowN3:\n    - log: \"bye!\"\n\nforms:\n  myForm:\n    - myName3: {type: \"string\"}\n\nresources:\n  concord:\n    - \"glob:tmp/1.yml\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/multiProjectFile/concord.yml",
    "content": "imports:\n  - git:\n      url: \"concord.yml\"\n\nflows:\n  default:\n  - checkpoint: \"root\"\n\nconfiguration:\n  activeProfiles:\n    - \"concord.yml\"\n  template: \"mytemplate\"\n  arguments:\n    nested:\n      value: \"234\"\n  dependencies:\n    - \"concord.yml\"\n  meta:\n    k: \"concord.yml\"\n  debug: true\n  entryPoint: \"root\"\n  events:\n    recordTaskInVars: true\n    recordTaskOutVars: true\n    truncateInVars: true\n    truncateOutVars: true\n    truncateMaxStringLength: 1\n    truncateMaxArrayLength: 2\n    truncateMaxDepth: 3\n    inVarsBlacklist:\n      - \"apiKey\"\n      - \"apiRootToken\"\n    recordTaskMeta: true\n    truncateMeta: true\n  requirements:\n    req: \"concord.yml\"\n  processTimeout: \"PT1H\"\n  suspendTimeout: \"PT26H\"\n  out:\n    - \"from-root\"\n\npublicFlows:\n  - root\n\nresources:\n  concord:\n    - \"glob:concord/{**/,}{*.,}concord.{yml,yaml}\"\n\ntriggers:\n  - github:\n      entryPoint: \"concord.yml\"\n      version: 2\n      conditions:\n        type: \"push\"\n        sender: \".*some.*dude.*\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/schema/concord.yml",
    "content": "configuration:\n  runtime: concord-v2\n  arguments:\n    k: \"v\"\n  entryPoint: \"test\"\n  dependencies:\n    - \"mvn://\"\n  activeProfiles:\n    - \"profile-2\"\n  processTimeout: \"PT1H\"\n  requirements:\n    key: \"value\"\n  out:\n    - \"test\"\n  meta:\n    k: \"v\"\n  events:\n    truncateInVars: false\n\nresources:\n  concord:\n    - \"/\"\n\npublicFlows:\n  - \"f\"\n\nprofiles:\n  p1:\n    configuration:\n      arguments:\n        k: \"v\"\n\nflows:\n  default:\n    - return\n    - exit\n    - checkpoint: test\n    - ${oneops}\n    - expr: ${qweqweqwe}\n      meta:\n        k: \"v\"\n      error:\n        - log: \"log\"\n      out:\n        k: \"v\"\n    - call: \"default1\"\n    - form: \"formName\"\n    - throw: ${error}\n    - name: call step name\n      call: \"default2\"\n#      in:\n#        k: \"v\"\n      out:\n        k: \"one\"\n      meta:\n        k: \"v\"\n      retry:\n        times: 1\n        delay: \"${delayExpr}\"\n#    - try:\n#        - log: \"ME\"\n#    - if: \"${test}\"\n#      then:\n#        - log: \"test\"\n#      else:\n#        - log: \"test\"\n    - parallel:\n        - log: \"test\"\n      meta:\n        k: \"v\"\n      out:\n        - o\n    - script: groovy\n      body: \"test\"\n      withItems: \"${test}\"\n    - set:\n        k: \"v\"\n    - suspend: \"event\"\n#    - switch: \"test\"\n#      one:\n#        - \"return\"\n#        - return\n#      default:\n#        - suspend: \"now\"\n    - name: \"Test task\"\n      task: \"test\"\n      in:\n        k: v\n      out: \"test\"\n      withItems:\n        - \"one\"\n        - \"two\"\n      ignoreErrors: true\n\nforms:\n  myForm:\n    - firstName: { label: \"First name\", type: \"string\", placeholder: \"Place first name here\" }\n    - lastName: { label: \"Last name\", type: \"string\" }\n    - password:  { label: \"Password\", type: \"string\", inputType: 'password' }\n    - age: { label: \"Age\", type: \"int?\" }\n    - height: { label: \"Height\", type: \"decimal?\" }\n    - rememberMe: { label: \"Remember me\", type: \"boolean\", value: true }\n    - file: { label: \"File\", type: \"file\"}\n    - skills: { label: \"Skills\", type: \"string*\", allow: [\"css\", \"design\", \"angular\"], search: true }\n    - email: { label: \"Email\", type: \"string\", inputType: \"email\" }\n    - readOnlyField: { label: \"Read only field\", type: \"string\", value: \"constant\", readonly: true}\n\nimports:\n  - git:\n      url: \"https://github.com/walmartlabs/concord.git\"\n      path: \"examples/hello_world\"\n      version: \"master\"\n      name: \"test\"\n      dest: \"/concord\"\n      exclude:\n        - \"/one\"\n      secret:\n        name: \"my-secret\"\n        org: \"secret-org\"\n        password: \"test\"\n  - mvn:\n      url: \"mvn://groupId:artifactId:version\"\n      dest: \"/\"\n\ntriggers:\n  - manual:\n      name: \"test\"\n      entryPoint: \"main\"\n      arguments:\n        k: \"v\"\n      activeProfiles:\n        - \"profile-1\"\n      exclusive:\n        mode: \"cancel\"\n        group: \"manual-group\"\n  - cron:\n      spec: \"* 12 * * *\"\n      entryPoint: eventOutput\n      activeProfiles:\n        - myProfile\n      arguments:\n        name: \"Concord\"\n  - github:\n      version: 2\n      entryPoint: \"onPr\"\n      conditions:\n        type: \"pull_request\"\n        status: \"closed\"\n        branch: \".*\"\n        payload:\n          pull_request:\n            merged: true\n  - example:\n      version: 2\n      entryPoint: exampleFLow\n      conditions:\n        aField: \"aValue\"\n  - oneops:\n      version: 2\n      conditions:\n        org: \"myOrganization\"\n        asm: \"myAssembly\"\n        env: \"myEnvironment\"\n        platform: \"myPlatform\"\n        type: \"deployment\"\n        deploymentState: \"complete\"\n      useInitiator: true\n      entryPoint: onDeployment"
  },
  {
    "path": "runtime/v2/model/src/test/resources/serializer/expressionStep.yml",
    "content": "expr: \"${a}\"\nout: \"out-result\"\nmeta:\n  meta-1: \"v1\"\n  meta-2: \"v2\"\nerror:\n- checkpoint: \"chp1\"\n  meta:\n    meta-1: \"v1\"\n    meta-2: \"v2\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/serializer/expressionStepOutExpr.yml",
    "content": "expr: \"${a}\"\nout:\n  out-result: \"${result.first}\"\nmeta:\n  meta-1: \"v1\"\n  meta-2: \"v2\"\nerror:\n- checkpoint: \"chp1\"\n  meta:\n    meta-1: \"v1\"\n    meta-2: \"v2\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/serializer/flowCallStep.yml",
    "content": "call: \"flow\"\nin:\n  in-1: \"v1\"\nout:\n- \"o1\"\nwithItems:\n- \"item1\"\n- \"item2\"\nloop:\n  mode: \"serial\"\n  items:\n  - \"item1\"\n  - \"item2\"\nretry:\n  times: \"${times}\"\n  delay: \"${delay}\"\n  in:\n    retry-input: \"v1\"\nerror:\n- checkpoint: \"chp1\"\n  meta:\n    meta-1: \"v1\"\n    meta-2: \"v2\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/serializer/flowCallStepOutExpr.yml",
    "content": "call: \"flow\"\nin:\n  in-1: \"v1\"\nout: \"${myOutExpr}\"\nwithItems:\n- \"item1\"\n- \"item2\"\nretry:\n  times: \"${times}\"\n  delay: \"${delay}\"\n  in:\n    retry-input: \"v1\"\nerror:\n- checkpoint: \"chp1\"\n  meta:\n    meta-1: \"v1\"\n    meta-2: \"v2\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/serializer/flowCallStepOutMapping.yml",
    "content": "call: \"flow\"\nin:\n  in-1: \"v1\"\nout:\n  o1: \"${o.one}\"\nwithItems:\n- \"item1\"\n- \"item2\"\nretry:\n  times: \"${times}\"\n  delay: \"${delay}\"\n  in:\n    retry-input: \"v1\"\nerror:\n- checkpoint: \"chp1\"\n  meta:\n    meta-1: \"v1\"\n    meta-2: \"v2\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/serializer/formCallStep.yml",
    "content": "form: \"form\"\nyield: true\nsaveSubmittedBy: true\nrunAs:\n  user: \"u1\"\nvalues:\n  k: \"v\"\nfields:\n- field-name:\n    label: \"field-label\"\n    type: \"field-type*\"\n    value: \"default-value\"\n    allow: \"allowed-value\"\n    o1: \"v1\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/serializer/groupOfSteps.yml",
    "content": "block:\n- checkpoint: \"chp1\"\n  meta:\n    meta-1: \"v1\"\n    meta-2: \"v2\"\nout:\n- \"out\"\nerror:\n- checkpoint: \"chp1\"\n  meta:\n    meta-1: \"v1\"\n    meta-2: \"v2\"\nmeta:\n  meta-1: \"v1\"\n  meta-2: \"v2\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/serializer/ifStep.yml",
    "content": "if: \"${ifExpression}\"\nthen:\n- expr: \"${exprForThen}\"\nelse:\n- checkpoint: \"chp1\"\n  meta:\n    meta-1: \"v1\"\n    meta-2: \"v2\"\nmeta:\n  meta-1: \"v1\"\n  meta-2: \"v2\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/serializer/parallelBlock.yml",
    "content": "parallel:\n- checkpoint: \"ch1\"\n  meta:\n    meta-1: \"v1\"\n    meta-2: \"v2\"\n- checkpoint: \"ch2\"\n  meta:\n    meta-1: \"v1\"\n    meta-2: \"v2\"\nout:\n- \"out\"\nmeta:\n  meta-1: \"v1\"\n  meta-2: \"v2\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/serializer/parallelBlockOutExpr.yml",
    "content": "parallel:\n- checkpoint: \"ch1\"\n  meta:\n    meta-1: \"v1\"\n    meta-2: \"v2\"\n- checkpoint: \"ch2\"\n  meta:\n    meta-1: \"v1\"\n    meta-2: \"v2\"\nout:\n  out: \"${expr}\"\nmeta:\n  meta-1: \"v1\"\n  meta-2: \"v2\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/serializer/processDefinition.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  debug: false\n  entryPoint: \"default\"\n  events:\n    recordEvents: true\n    batchSize: 1\n    batchFlushInterval: 15\n    recordTaskInVars: false\n    truncateInVars: true\n    truncateMaxStringLength: 1024\n    truncateMaxArrayLength: 32\n    truncateMaxDepth: 10\n    recordTaskOutVars: false\n    truncateOutVars: true\n    updateMetaOnAllEvents: true\n    inVarsBlacklist:\n    - \"apiKey\"\n    - \"apiToken\"\n    - \"password\"\n    - \"privateKey\"\n    - \"vaultPassword\"\n    recordTaskMeta: false\n    truncateMeta: true\n  parallelLoopParallelism: 123\n  validation:\n    taskCalls:\n      in: \"DISABLED\"\n      out: \"DISABLED\"\nflows:\n  flow1:\n  - checkpoint: \"chp1\"\n    meta:\n      meta-1: \"v1\"\n      meta-2: \"v2\"\npublicFlows:\n- \"flow1\"\ntriggers:\n- github:\n    activeProfiles:\n    - \"p1\"\n    entryPoint: \"www\"\n    useInitiator: true\n    arguments:\n      arg: \"arg-value\"\n    conditions:\n      type: \"push\"\n      status:\n      - \"opened\"\n      - \"reopened\"\nimports:\n- mvn:\n    url: \"http://url\"\n    dest: \"dest\"\nforms:\n  form1:\n  - field1:\n      type: \"string*\"\nresources:\n  concord:\n  - \"glob:concord/{**/,}{*.,}concord.{yml,yaml}\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/serializer/returnStep.yml",
    "content": "\"return\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/serializer/scriptStep.yml",
    "content": "script: \"js\"\nbody: \"print(\\\"Hello, \\\", myVar)\"\nin:\n  in: \"v\"\nwithItems:\n- \"item1\"\n- \"item2\"\nloop:\n  mode: \"serial\"\n  items:\n  - \"item1\"\n  - \"item2\"\nretry:\n  times: \"${times}\"\n  delay: \"${delay}\"\n  in:\n    retry-input: \"v1\"\nerror:\n- checkpoint: \"chp1\"\n  meta:\n    meta-1: \"v1\"\n    meta-2: \"v2\"\nmeta:\n  meta-1: \"v1\"\n  meta-2: \"v2\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/serializer/setVariablesStep.yml",
    "content": "set:\n  k1: \"v1\"\nmeta:\n  meta-1: \"v1\"\n  meta-2: \"v2\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/serializer/suspendStep.yml",
    "content": "suspend: \"event\"\nmeta:\n  meta-1: \"v1\"\n  meta-2: \"v2\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/serializer/switchStep.yml",
    "content": "switch: \"${switch}\"\nred:\n- checkpoint: \"chp1\"\n  meta:\n    meta-1: \"v1\"\n    meta-2: \"v2\"\ndefault:\n- expr: \"defaultStep\"\nmeta:\n  meta-1: \"v1\"\n  meta-2: \"v2\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/serializer/taskCallStep.yml",
    "content": "log: \"BOO\"\nout: \"out\"\nwithItems:\n- \"item1\"\n- \"item2\"\nloop:\n  mode: \"serial\"\n  items:\n  - \"item1\"\n  - \"item2\"\nretry:\n  times: \"${times}\"\n  delay: \"${delay}\"\n  in:\n    retry-input: \"v1\"\nerror:\n- checkpoint: \"chp1\"\n  meta:\n    meta-1: \"v1\"\n    meta-2: \"v2\"\nmeta:\n  meta-1: \"v1\"\n  meta-2: \"v2\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/serializer/taskCallStepOutExpr.yml",
    "content": "log: \"BOO\"\nout:\n  result: \"${result}\"\n  v1: \"${result.v1}\"\nwithItems:\n- \"item1\"\n- \"item2\"\nretry:\n  times: \"${times}\"\n  delay: \"${delay}\"\n  in:\n    retry-input: \"v1\"\nerror:\n- checkpoint: \"chp1\"\n  meta:\n    meta-1: \"v1\"\n    meta-2: \"v2\"\nmeta:\n  meta-1: \"v1\"\n  meta-2: \"v2\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/serializer/taskCallStepParallel.yml",
    "content": "log: \"BOO\"\nout: \"out\"\nparallelWithItems:\n- \"item1\"\n- \"item2\"\nretry:\n  times: \"${times}\"\n  delay: \"${delay}\"\n  in:\n    retry-input: \"v1\"\nerror:\n- checkpoint: \"chp1\"\n  meta:\n    meta-1: \"v1\"\n    meta-2: \"v2\"\nmeta:\n  meta-1: \"v1\"\n  meta-2: \"v2\"\n"
  },
  {
    "path": "runtime/v2/model/src/test/resources/validationConfig.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n  validation:\n    taskCalls:\n      in: fail\n      out: warn\n\nflows:\n  default:\n    - log: \"Hello\"\n"
  },
  {
    "path": "runtime/v2/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../../pom.xml</relativePath>\n    </parent>\n\n    <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n    <artifactId>parent</artifactId>\n    <packaging>pom</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <modules>\n        <module>vm</module>\n        <module>sdk</module>\n        <module>model</module>\n        <module>runner</module>\n        <module>runner-test</module>\n    </modules>\n</project>\n"
  },
  {
    "path": "runtime/v2/runner/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-runner-v2</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-forms</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime</groupId>\n            <artifactId>concord-runtime-common</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-sdk-v2</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-model-v2</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-vm-v2</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-client2</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-imports</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-policy-engine</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-dependency-manager</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.sisu</groupId>\n            <artifactId>org.eclipse.sisu.inject</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.inject</groupId>\n            <artifactId>guice</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>aopalliance</groupId>\n            <artifactId>aopalliance</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jsr310</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jdk8</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>ch.qos.logback</groupId>\n            <artifactId>logback-classic</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>ch.qos.logback</groupId>\n            <artifactId>logback-core</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-compress</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-text</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.graalvm.js</groupId>\n            <artifactId>js</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.graalvm.js</groupId>\n            <artifactId>js-scriptengine</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.graalvm.sdk</groupId>\n            <artifactId>graal-sdk</artifactId>\n        </dependency>\n\n        <!-- bundled in tasks -->\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>crypto-tasks</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>sleep-tasks</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>misc-tasks</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>throw-tasks</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>resource-tasks</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>lock-tasks</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>kv-tasks</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>locale-tasks</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>docker-tasks</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n            <artifactId>log-tasks</artifactId>\n        </dependency>\n\n        <!-- Java EL support -->\n        <dependency>\n            <groupId>org.glassfish</groupId>\n            <artifactId>javax.el</artifactId>\n        </dependency>\n\n        <!-- Immutables -->\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.errorprone</groupId>\n            <artifactId>error_prone_annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <!-- JDK9 compatibility -->\n        <dependency>\n            <groupId>javax.xml.bind</groupId>\n            <artifactId>jaxb-api</artifactId>\n            <exclusions>\n                <exclusion>\n                    <groupId>javax.activation</groupId>\n                    <artifactId>javax.activation-api</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n        <dependency>\n            <groupId>com.sun.xml.bind</groupId>\n            <artifactId>jaxb-impl</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.sun.activation</groupId>\n            <artifactId>javax.activation</artifactId>\n        </dependency>\n        \n        <dependency>\n            <groupId>uk.org.lidalia</groupId>\n            <artifactId>sysout-over-slf4j</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.networknt</groupId>\n            <artifactId>json-schema-validator</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.hamcrest</groupId>\n            <artifactId>hamcrest</artifactId>\n            <version>2.2</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-core</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.reflections</groupId>\n            <artifactId>reflections</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <!-- to test the scripting feature -->\n        <dependency>\n            <groupId>org.apache.groovy</groupId>\n            <artifactId>groovy-all</artifactId>\n            <type>pom</type>\n            <scope>test</scope>\n            <exclusions>\n                <exclusion>\n                    <groupId>org.apache.groovy</groupId>\n                    <artifactId>groovy-test</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>org.apache.groovy</groupId>\n                    <artifactId>groovy-test-junit5</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-shade-plugin</artifactId>\n                <version>3.2.4</version>\n                <executions>\n                    <execution>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>shade</goal>\n                        </goals>\n                        <configuration>\n                            <createDependencyReducedPom>false</createDependencyReducedPom>\n                            <shadedArtifactAttached>true</shadedArtifactAttached>\n                            <shadedClassifierName>jar-with-dependencies</shadedClassifierName>\n                            <transformers>\n                                <transformer implementation=\"org.apache.maven.plugins.shade.resource.AppendingTransformer\">\n                                    <resource>META-INF/sisu/javax.inject.Named</resource>\n                                </transformer>\n                                <transformer implementation=\"org.apache.maven.plugins.shade.resource.AppendingTransformer\">\n                                    <resource>META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider</resource>\n                                </transformer>\n                                <transformer implementation=\"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer\">\n                                    <mainClass>com.walmartlabs.concord.runtime.v2.runner.Main</mainClass>\n                                </transformer>\n                            </transformers>\n                            <filters>\n                                <filter>\n                                    <artifact>*:*</artifact>\n                                    <excludes>\n                                        <exclude>META-INF/*.SF</exclude>\n                                        <exclude>META-INF/*.DSA</exclude>\n                                        <exclude>META-INF/*.RSA</exclude>\n                                        <exclude>META-INF/NOTICE.txt</exclude>\n                                        <exclude>META-INF/LICENSE</exclude>\n                                        <exclude>META-INF/LICENSE.txt</exclude>\n                                    </excludes>\n                                </filter>\n                            </filters>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-surefire-plugin</artifactId>\n                <configuration>\n                    <environmentVariables>\n                        <CONCORD_TMP_DIR>${java.io.tmpdir}</CONCORD_TMP_DIR>\n                    </environmentVariables>\n                    <parallel>methods</parallel>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/DefaultDependencyManager.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.DependencyManager;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Path;\n\npublic class DefaultDependencyManager implements DependencyManager {\n\n    private final com.walmartlabs.concord.dependencymanager.DependencyManager delegate;\n\n    @Inject\n    public DefaultDependencyManager(com.walmartlabs.concord.dependencymanager.DependencyManager delegate) {\n        this.delegate = delegate;\n    }\n\n    @Override\n    public Path resolve(URI uri) throws IOException {\n        return delegate.resolveSingle(uri).getPath();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/DefaultDockerService.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.TruncBufferedReader;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.runtime.common.injector.InstanceId;\nimport com.walmartlabs.concord.runtime.v2.sdk.DockerContainerSpec;\nimport com.walmartlabs.concord.runtime.v2.sdk.DockerService;\nimport com.walmartlabs.concord.runtime.v2.sdk.WorkingDirectory;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Pattern;\n\nimport static com.walmartlabs.concord.runtime.v2.runner.DockerProcessBuilder.DockerProcess;\n\npublic class DefaultDockerService implements DockerService {\n\n    private static final Logger log = LoggerFactory.getLogger(DefaultDockerService.class);\n\n    private static final int SUCCESS_EXIT_CODE = 0;\n    private static final String WORKSPACE_TARGET_DIR = \"/workspace\";\n\n    private static final Pattern[] REGISTRY_ERROR_PATTERNS = {\n            Pattern.compile(\"Error response from daemon.*received unexpected HTTP status: 5.*\"),\n            Pattern.compile(\"Error response from daemon.*Get.*connection refused.*\"),\n            Pattern.compile(\"Error response from daemon.*Client.Timeout exceeded.*\")\n    };\n\n    private final WorkingDirectory workingDirectory;\n    private final InstanceId instanceId;\n    private final List<String> extraVolumes;\n    private final boolean exposeDockerDaemon;\n\n    @Inject\n    public DefaultDockerService(WorkingDirectory workingDirectory, InstanceId instanceId, RunnerConfiguration runnerCfg) {\n        this.workingDirectory = workingDirectory;\n        this.instanceId = instanceId;\n        this.extraVolumes = runnerCfg.docker().extraVolumes();\n        this.exposeDockerDaemon = runnerCfg.docker().exposeDockerDaemon();\n    }\n\n    @Override\n    public int start(DockerContainerSpec spec, LogCallback outCallback, LogCallback errCallback) throws IOException, InterruptedException {\n        int tryCount = 0;\n        int result;\n        int retryCount = Math.max(spec.pullRetryCount(), 0);\n        long retryInterval = spec.pullRetryInterval();\n\n        do {\n            try (DockerProcess dp = build(spec)) {\n                Process p = dp.start();\n\n                LogCapture c = new LogCapture(outCallback);\n                streamToLog(p.getInputStream(), c);\n                if (errCallback != null) {\n                    streamToLog(p.getErrorStream(), errCallback);\n                }\n\n                result = p.waitFor();\n                if (result == SUCCESS_EXIT_CODE || retryCount == 0 || tryCount >= retryCount) {\n                    return result;\n                }\n\n                if (!needRetry(c.getLines())) {\n                    return result;\n                }\n\n                log.info(\"Error pulling the image. Retry after {} sec\", retryInterval / 1000);\n                sleep(retryInterval);\n                tryCount++;\n            }\n        } while (!Thread.currentThread().isInterrupted() && tryCount <= retryCount);\n\n        return result;\n    }\n\n    private DockerProcess build(DockerContainerSpec spec) throws IOException {\n        DockerProcessBuilder b = DockerProcessBuilder.from(instanceId.getValue(), spec);\n\n        b.env(createEffectiveEnv(spec.env(), exposeDockerDaemon));\n        // add the default volume - mount the process' workDir as /workspace\n        b.volume(workingDirectory.getValue().toString(), WORKSPACE_TARGET_DIR);\n        // add extra volumes from the runner's arguments\n        extraVolumes.forEach(b::volume);\n\n        return b.build();\n    }\n\n    private static boolean needRetry(List<String> lines) {\n        for (String l : lines) {\n            for (Pattern p : REGISTRY_ERROR_PATTERNS) {\n                if (p.matcher(l).matches()) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    private static void sleep(long t) {\n        try {\n            Thread.sleep(t);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    private static void streamToLog(InputStream in, LogCallback callback) throws IOException {\n        BufferedReader reader = new TruncBufferedReader(new InputStreamReader(in));\n        String line;\n        while ((line = reader.readLine()) != null) {\n            callback.onLog(line);\n        }\n    }\n\n    private static Map<String, String> createEffectiveEnv(Map<String, String> env, boolean exposeDockerDaemon) {\n        Map<String, String> m = new HashMap<>();\n\n        if (exposeDockerDaemon) {\n            String dockerHost = System.getenv(\"DOCKER_HOST\");\n            if (dockerHost == null) {\n                dockerHost = \"unix:///var/run/docker.sock\";\n            }\n            m.put(\"DOCKER_HOST\", dockerHost);\n        }\n\n        if (env != null) {\n            m.putAll(env);\n        }\n\n        return m;\n    }\n\n    private static class LogCapture implements LogCallback {\n\n        private static final int MAX_CAPTURE_LINES = 5;\n\n        private final LogCallback delegate;\n        private final List<String> lines;\n\n        private LogCapture(LogCallback delegate) {\n            this.delegate = delegate;\n            this.lines = new ArrayList<>();\n        }\n\n        @Override\n        public void onLog(String line) {\n            if (delegate != null) {\n                delegate.onLog(line);\n            }\n\n            if (lines.size() <= MAX_CAPTURE_LINES) {\n                lines.add(line);\n            }\n        }\n\n        List<String> getLines() {\n            return lines;\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/DefaultEventReportingService.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.client2.ApiException;\nimport com.walmartlabs.concord.client2.ProcessEventRequest;\nimport com.walmartlabs.concord.client2.ProcessEventsApi;\nimport com.walmartlabs.concord.runtime.common.injector.InstanceId;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration;\nimport com.walmartlabs.concord.svm.ExecutionListener;\nimport com.walmartlabs.concord.svm.Frame;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.State;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.TimerTask;\nimport java.util.UUID;\nimport java.util.concurrent.BlockingQueue;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.ScheduledExecutorService;\nimport java.util.concurrent.TimeUnit;\n\npublic class DefaultEventReportingService implements EventReportingService, ExecutionListener {\n\n    private static final Logger log = LoggerFactory.getLogger(DefaultEventReportingService.class);\n\n    private final InstanceId instanceId;\n    private final ProcessEventsApi processEventsApi;\n    private final BlockingQueue<ProcessEventRequest> eventQueue;\n    private final int maxBatchSize;\n    private final Object batchLock = new Object();\n    private final ScheduledExecutorService flushScheduler;\n    private final PersistenceService persistenceService;\n\n    @Inject\n    public DefaultEventReportingService(InstanceId instanceId,\n                                        ProcessConfiguration processConfiguration,\n                                        ApiClient apiClient,\n                                        PersistenceService persistenceService) {\n        this.instanceId = instanceId;\n        this.processEventsApi = new ProcessEventsApi(apiClient);\n        this.maxBatchSize = processConfiguration.events().batchSize();\n        this.persistenceService = persistenceService;\n        this.eventQueue = initializeQueue(maxBatchSize);\n        this.flushScheduler = Executors.newSingleThreadScheduledExecutor();\n\n        int period = processConfiguration.events().batchFlushInterval();\n        flushScheduler.scheduleAtFixedRate(new FlushTimer(this), period, period, TimeUnit.SECONDS);\n    }\n\n    private static BlockingQueue<ProcessEventRequest> initializeQueue(int maxBatchSize) {\n        if (maxBatchSize <= 0) {\n            throw new IllegalArgumentException(\"Invalid event batch size '\" + maxBatchSize + \"'. Must be greater than zero.\");\n        }\n\n        return new LinkedBlockingQueue<>();\n    }\n\n    @Override\n    public synchronized void report(ProcessEventRequest req) {\n        if (req == null) {\n            return;\n        }\n\n        // avoid modification while a flush may be occurring from another thread\n        synchronized (batchLock) {\n            eventQueue.add(req);\n        }\n\n        // Don't allow batch to grow larger than max batch size\n        // This is why the method is synchronized. If not, another thread may add\n        // one-too-many items to the batch before this flush finishes.\n        if (eventQueue.size() >= maxBatchSize) {\n            flush();\n        }\n    }\n\n    @Override\n    public void afterProcessEnds(Runtime runtime, State state, Frame lastFrame) {\n        flushScheduler.shutdown();\n        flush();\n    }\n\n    @Override\n    public void onProcessError(Runtime runtime, State state, Exception e) {\n        flushScheduler.shutdown();\n        flush();\n    }\n\n    ProcessEventsApi getProcessEventsApi() {\n        return processEventsApi;\n    }\n\n    synchronized void flush() {\n        List<ProcessEventRequest> eventBatch = takeBatch();\n\n        while (!eventBatch.isEmpty()) {\n            send(eventBatch);\n            eventBatch = takeBatch();\n        }\n    }\n\n    private void send(List<ProcessEventRequest> eventBatch) {\n        if (eventBatch.size() == 1) {\n            sendSingle(eventBatch.get(0));\n        } else {\n            sendBatch(eventBatch);\n        }\n    }\n\n    private void sendBatch(List<ProcessEventRequest> eventBatch) {\n        try {\n            getProcessEventsApi().batchEvent(instanceId.getValue(), eventBatch);\n        } catch (ApiException e) {\n            for (var event: eventBatch) {\n                saveEvent(event);\n            }\n            log.warn(\"Error while sending batch of {} event{} to the server: {}\",\n                    eventBatch.size(), eventBatch.isEmpty() ? \"\" : \"s\", e.getMessage());\n        }\n    }\n\n    private void sendSingle(ProcessEventRequest req) {\n        try {\n            getProcessEventsApi().event(instanceId.getValue(), req);\n        } catch (ApiException e) {\n            saveEvent(req);\n            log.warn(\"error while sending an event to the server: {}\", e.getMessage());\n        }\n    }\n\n    /**\n     * @return batch of up-to {@link #maxBatchSize} queued process events\n     */\n    private List<ProcessEventRequest> takeBatch() {\n        List<ProcessEventRequest> batch = new ArrayList<>(maxBatchSize);\n\n        synchronized (batchLock) { // avoid draining while an element may be added\n            eventQueue.drainTo(batch, maxBatchSize);\n        }\n\n        return batch;\n    }\n\n    private static class FlushTimer extends TimerTask {\n        private final DefaultEventReportingService reportingService;\n\n        public FlushTimer(DefaultEventReportingService reportingService) {\n            this.reportingService = reportingService;\n        }\n\n        @Override\n        public void run() {\n            reportingService.flush();\n        }\n    }\n\n    private void saveEvent(ProcessEventRequest event) {\n        try {\n            persistenceService.persistFile(\"invalid_event_\" + UUID.randomUUID() + \".json\", out -> processEventsApi.getApiClient().getObjectMapper().writeValue(out, event));\n        } catch (Exception e) {\n            log.warn(\"can't save event\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/DefaultFileService.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.FileService;\nimport com.walmartlabs.concord.runtime.v2.sdk.WorkingDirectory;\nimport com.walmartlabs.concord.sdk.Constants;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic class DefaultFileService implements FileService {\n\n    private final WorkingDirectory workingDirectory;\n\n    @Inject\n    public DefaultFileService(WorkingDirectory workingDirectory) {\n        this.workingDirectory = workingDirectory;\n    }\n\n    @Override\n    public Path createTempFile(String prefix, String suffix) throws IOException {\n        return Files.createTempFile(baseTmpDir(), prefix, suffix);\n    }\n\n    @Override\n    public Path createTempDirectory(String prefix) throws IOException {\n        return Files.createTempDirectory(baseTmpDir(), prefix);\n    }\n\n    private Path baseTmpDir() throws IOException {\n        Path p = workingDirectory.getValue().resolve(Constants.Files.CONCORD_TMP_DIR_NAME);\n        Files.createDirectories(p);\n        return p;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/DefaultLockService.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.runtime.common.injector.InstanceId;\nimport com.walmartlabs.concord.runtime.v2.sdk.LockService;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.util.concurrent.Callable;\n\npublic class DefaultLockService implements LockService {\n\n    private static final Logger log = LoggerFactory.getLogger(DefaultLockService.class);\n\n    private static final int RETRY_COUNT = 3;\n    private static final long RETRY_INTERVAL = 5000;\n    private static final long LOCK_RETRY_INTERVAL = 10000;\n\n    private final InstanceId instanceId;\n    private final ApiClient apiClient;\n\n    @Inject\n    public DefaultLockService(InstanceId instanceId, ApiClient apiClient) {\n        this.instanceId = instanceId;\n        this.apiClient = apiClient;\n    }\n\n    @Override\n    public void projectLock(String lockName) throws Exception {\n        ProcessLocksApi api = new ProcessLocksApi(apiClient);\n\n        // TODO: timeout\n        while (!Thread.currentThread().isInterrupted()) {\n            LockResult lock = withRetry(() -> api.tryLock(instanceId.getValue(), lockName, LockScope.PROJECT.name()));\n            if (lock.getAcquired()) {\n                log.info(\"successfully acquired lock '{}' in '{}' scope...\", lockName, LockScope.PROJECT);\n                return;\n            }\n\n            log.info(\"waiting for lock '{}' in '{}' scope...\", lockName, LockScope.PROJECT);\n            sleep(LOCK_RETRY_INTERVAL);\n        }\n    }\n\n    @Override\n    public void projectUnlock(String lockName) throws Exception {\n        ProcessLocksApi api = new ProcessLocksApi(apiClient);\n        withRetry(() -> {\n            api.unlock(instanceId.getValue(), lockName, LockScope.PROJECT.name());\n            return null;\n        });\n        log.info(\"unlocking '{}' with scope '{}' -> done\", lockName, LockScope.PROJECT);\n    }\n\n    private static void sleep(long t) {\n        try {\n            Thread.sleep(t);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    private enum LockScope {\n        ORG, PROJECT\n    }\n\n    private static <T> T withRetry(Callable<T> c) throws ApiException {\n        return ClientUtils.withRetry(RETRY_COUNT, RETRY_INTERVAL, c);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/DefaultPersistenceService.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.runtime.common.StateManager;\nimport com.walmartlabs.concord.runtime.v2.sdk.WorkingDirectory;\nimport com.walmartlabs.concord.sdk.Constants;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.Serializable;\nimport java.nio.file.Files;\nimport java.nio.file.OpenOption;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\n\npublic class DefaultPersistenceService implements PersistenceService {\n\n    private final WorkingDirectory workingDirectory;\n\n    @Inject\n    public DefaultPersistenceService(WorkingDirectory workingDirectory) {\n        this.workingDirectory = workingDirectory;\n    }\n\n    @Override\n    public <T extends Serializable> T load(String storageName, Class<T> expectedType) {\n        return StateManager.load(workingDirectory.getValue(), storageName, expectedType);\n    }\n\n    @Override\n    public void save(String storageName, Serializable object) throws IOException {\n        StateManager.persist(workingDirectory.getValue(), storageName, object);\n    }\n\n    @Override\n    public void persistFile(String name, Writer writer) {\n        persistFile(name, writer, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);\n    }\n\n    @Override\n    public void persistFile(String name, Writer writer, OpenOption... options) {\n        Path storeDir = workingDirectory.getValue()\n                .resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME);\n\n        persistFile(storeDir, name, writer, options);\n    }\n\n    @Override\n    public void persistSessionFile(String name, Writer writer) {\n        Path storeDir = workingDirectory.getValue()\n                .resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(Constants.Files.JOB_SESSION_FILES_DIR_NAME);\n\n        persistFile(storeDir, name, writer, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);\n    }\n\n    @Override\n    public void deletePersistedFile(String name) throws IOException {\n        PathUtils.deleteRecursively(workingDirectory.getValue()\n                .resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(name));\n    }\n\n    private void persistFile(Path storeDir, String name, Writer writer, OpenOption... options) {\n        try {\n            Path p = storeDir.resolve(name);\n            if (!Files.exists(p.getParent())) {\n                Files.createDirectories(p.getParent());\n            }\n            try (OutputStream out = Files.newOutputStream(p, options)) {\n                writer.write(out);\n            }\n        } catch (IOException e) {\n            throw new RuntimeException(\"Error persisting file '\" + name + \"': \" + e.getMessage());\n        }\n    }\n\n    @Override\n    public <T> T loadPersistedFile(String name, Converter<InputStream, T> converter) {\n        Path file = workingDirectory.getValue()\n                .resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(name);\n\n        return loadPersistedFile(file, name, converter);\n    }\n\n    @Override\n    public <T> T loadPersistedSessionFile(String name, Converter<InputStream, T> converter) {\n        Path file = workingDirectory.getValue()\n                .resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(Constants.Files.JOB_SESSION_FILES_DIR_NAME)\n                .resolve(name);\n\n        return loadPersistedFile(file, name, converter);\n    }\n\n    private <T> T loadPersistedFile(Path file, String name, Converter<InputStream, T> converter) {\n        try {\n            if (!Files.exists(file)) {\n                return null;\n            }\n\n            try (InputStream in = Files.newInputStream(file)) {\n                return converter.apply(in);\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(\"Error loading persisted file '\" + name + \"': \" + e.getMessage());\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/DefaultProcessConfigurationProvider.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.runtime.v2.runner.guice.ObjectMapperProvider;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Provider;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.UUID;\n\n/**\n * Waits for the process state files to appear in the  working directory and tries to\n * load the process' configuration from it.\n */\npublic class DefaultProcessConfigurationProvider implements Provider<ProcessConfiguration> {\n\n    private static final Logger log = LoggerFactory.getLogger(DefaultProcessConfigurationProvider.class);\n\n    private final Path workDir;\n    private final ObjectMapper objectMapper;\n\n    public DefaultProcessConfigurationProvider(Path workDir) {\n        this.workDir = workDir;\n        this.objectMapper = new ObjectMapperProvider().get();\n    }\n\n    @Override\n    public ProcessConfiguration get() {\n        try {\n            UUID instanceId = waitForInstanceId(workDir);\n            // TODO pass instanceId directly in _main.json?\n            return readProcessConfiguration(instanceId, workDir);\n        } catch (RuntimeException e) {\n            throw e;\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    /**\n     * Waits until an instanceId file appears in the specified directory\n     * then reads it and parses as UUID.\n     */\n    @SuppressWarnings(\"BusyWait\")\n    private static UUID waitForInstanceId(Path workDir) {\n        Path p = workDir.resolve(Constants.Files.INSTANCE_ID_FILE_NAME);\n        while (true) {\n            byte[] id = readIfExists(p);\n            if (id != null && id.length > 0) {\n                String s = new String(id);\n                try {\n                    return UUID.fromString(s.trim());\n                } catch (IllegalArgumentException e) {\n                    log.warn(\"waitForInstanceId ['{}'] -> value: '{}', error: {}\", workDir, s, e.getMessage());\n                }\n            }\n\n            // we are not using WatchService as it has issues when running in Docker\n            try {\n                Thread.sleep(1000);\n            } catch (InterruptedException e) {\n                Thread.currentThread().interrupt();\n            }\n        }\n    }\n\n    private ProcessConfiguration readProcessConfiguration(UUID instanceId, Path workDir) throws IOException {\n        Path p = workDir.resolve(Constants.Files.CONFIGURATION_FILE_NAME);\n        if (!Files.exists(p)) {\n            return ProcessConfiguration.builder()\n                    .instanceId(instanceId)\n                    .build();\n        }\n\n        try (InputStream in = Files.newInputStream(p)) {\n            return ProcessConfiguration.builder()\n                    .from(objectMapper.readValue(in, ProcessConfiguration.class))\n                    .instanceId(instanceId)\n                    .build();\n        }\n    }\n\n    private static byte[] readIfExists(Path p) {\n        try {\n            if (Files.notExists(p)) {\n                return null;\n            }\n            return Files.readAllBytes(p);\n        } catch (Exception e) {\n            log.warn(\"readIfExists ['{}'] -> error: {}\", p, e.getMessage());\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/DefaultResourceResolver.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.WorkingDirectory;\n\nimport javax.inject.Inject;\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.HttpURLConnection;\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport java.net.URLConnection;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic class DefaultResourceResolver implements ResourceResolver {\n\n    private static final int MAX_CONTENT_LENGTH = 640 * 1024;\n\n    private final WorkingDirectory workingDirectory;\n\n    @Inject\n    public DefaultResourceResolver(WorkingDirectory workingDirectory) {\n        this.workingDirectory = workingDirectory;\n    }\n\n    @Override\n    public InputStream resolve(String name) throws IOException {\n        try {\n            URL url = new URL(name);\n            return toStream(url);\n        } catch (MalformedURLException e) {\n            // not a URL, let's try a file first...\n            InputStream in = toStream(name);\n            if (in != null) {\n                return in;\n            }\n\n            //...or look in the classpath\n            return ClassLoader.getSystemResourceAsStream(name);\n        }\n    }\n\n    private InputStream toStream(URL url) throws IOException {\n        URLConnection conn = url.openConnection();\n        int length = -1;\n\n        if (conn instanceof HttpURLConnection) {\n            HttpURLConnection httpConn = (HttpURLConnection) conn;\n\n            String s = httpConn.getHeaderField(\"Content-Length\");\n            if (s != null) {\n                length = Integer.parseInt(s);\n            }\n        }\n\n        assertContentLength(length);\n\n        ByteArrayOutputStream out;\n        if (length > 0) {\n            out = new ByteArrayOutputStream(length);\n        } else {\n            out = new ByteArrayOutputStream();\n        }\n\n        byte[] buf = new byte[4096];\n        int read;\n\n        try (InputStream in = conn.getInputStream()) {\n            while ((read = in.read(buf)) > 0) {\n                assertContentLength(out.size());\n                out.write(buf, 0, read);\n            }\n        }\n\n        return new ByteArrayInputStream(out.toByteArray());\n    }\n\n    private InputStream toStream(String name) throws IOException {\n        Path p = workingDirectory.getValue().resolve(name);\n        if (!Files.exists(p)) {\n            return null;\n        }\n        return Files.newInputStream(p);\n    }\n\n    private static void assertContentLength(int current) throws IOException {\n        if (current > MAX_CONTENT_LENGTH) {\n            throw new IOException(\"Content too long: \" + current + \" (max: \" + MAX_CONTENT_LENGTH + \")\");\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/DefaultRuntime.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Injector;\nimport com.walmartlabs.concord.svm.*;\nimport com.walmartlabs.concord.svm.Runtime;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\n\npublic class DefaultRuntime implements Runtime {\n\n    private static final Logger log = LoggerFactory.getLogger(DefaultRuntime.class);\n\n    private final VM vm;\n    private final Injector injector;\n    private final ExecutorService executor;\n\n    public DefaultRuntime(VM vm, Injector injector) {\n        this.vm = vm;\n        this.injector = injector;\n\n        this.executor = Executors.newCachedThreadPool();\n    }\n\n    @Override\n    public void spawn(State state, ThreadId threadId) {\n        executor.submit(() -> eval(state, threadId));\n    }\n\n    @Override\n    public EvalResult eval(State state, ThreadId threadId) throws Exception {\n        return vm.eval(this, state, threadId);\n    }\n\n    @Override\n    public <T> T getService(Class<T> klass) {\n        return injector.getInstance(klass);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/DefaultSecretService.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.secret.BinaryDataSecret;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.runtime.common.injector.InstanceId;\nimport com.walmartlabs.concord.runtime.v2.sdk.FileService;\nimport com.walmartlabs.concord.runtime.v2.sdk.SecretNotFoundException;\nimport com.walmartlabs.concord.runtime.v2.sdk.SecretService;\nimport com.walmartlabs.concord.sdk.Secret;\n\nimport javax.inject.Inject;\nimport javax.xml.bind.DatatypeConverter;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Objects;\n\npublic class DefaultSecretService implements SecretService {\n\n    private final SecretClient secretClient;\n    private final FileService fileService;\n    private final InstanceId instanceId;\n\n    @Inject\n    public DefaultSecretService(RunnerConfiguration cfg, ApiClient apiClient, FileService fileService, InstanceId instanceId) {\n        this.secretClient = new SecretClient(apiClient, cfg.api().retryCount(), cfg.api().retryInterval());\n        this.fileService = fileService;\n        this.instanceId = instanceId;\n    }\n\n    @Override\n    public String exportAsString(String orgName, String secretName, String password) throws Exception {\n        BinaryDataSecret s = get(orgName, secretName, password, SecretEntryV2.TypeEnum.DATA);\n        return new String(s.getData());\n    }\n\n    @Override\n    public KeyPair exportKeyAsFile(String orgName, String secretName, String password) throws Exception {\n        com.walmartlabs.concord.common.secret.KeyPair kp = get(orgName, secretName, password, SecretEntryV2.TypeEnum.KEY_PAIR);\n\n        Path tmpDir = fileService.createTempDirectory(\"secret-service\");\n\n        Path privateKey = Files.createTempFile(tmpDir, \"private\", \".key\");\n        Files.write(privateKey, kp.getPrivateKey());\n\n        Path publicKey = Files.createTempFile(tmpDir, \"public\", \".key\");\n        Files.write(publicKey, kp.getPublicKey());\n\n        return KeyPair.builder()\n                .publicKey(publicKey)\n                .privateKey(privateKey)\n                .build();\n    }\n\n    @Override\n    public UsernamePassword exportCredentials(String orgName, String secretName, String password) throws Exception {\n        com.walmartlabs.concord.common.secret.UsernamePassword up = get(orgName, secretName, password, SecretEntryV2.TypeEnum.USERNAME_PASSWORD);\n        return UsernamePassword.of(up.getUsername(), new String(up.getPassword()));\n    }\n\n    @Override\n    public Path exportAsFile(String orgName, String secretName, String password) throws Exception {\n        BinaryDataSecret bds = get(orgName, secretName, password, SecretEntryV2.TypeEnum.DATA);\n\n        Path p = fileService.createTempFile(\"secret-service-file\", \".bin\");\n        Files.write(p, bds.getData());\n\n        return p;\n    }\n\n    @Override\n    public String decryptString(String encryptedValue) throws Exception {\n        byte[] input;\n\n        try {\n            input = DatatypeConverter.parseBase64Binary(encryptedValue);\n        } catch (Exception e) {\n            throw new IllegalArgumentException(\"Invalid encrypted string value, please verify that it was specified/copied correctly: \" + e.getMessage());\n        }\n\n        return new String(secretClient.decryptString(instanceId.getValue(), input));\n    }\n\n    @Override\n    public String encryptString(String orgName, String projectName, String value) throws Exception {\n        return secretClient.encryptString(orgName, projectName, value);\n    }\n\n    @Override\n    public SecretCreationResult createKeyPair(SecretParams secret, KeyPair keyPair) throws Exception {\n        return toResult(secretClient.createSecret(secretRequest(secret)\n                .keyPair(CreateSecretRequest.KeyPair.builder()\n                        .publicKey(keyPair.publicKey())\n                        .privateKey(keyPair.privateKey())\n                        .build())\n                .build()));\n    }\n\n    @Override\n    public SecretCreationResult createUsernamePassword(SecretParams secret, UsernamePassword usernamePassword) throws Exception {\n        return toResult(secretClient.createSecret(secretRequest(secret)\n                .usernamePassword(CreateSecretRequest.UsernamePassword.of(usernamePassword.username(), usernamePassword.password()))\n                .build()));\n    }\n\n    @Override\n    public SecretCreationResult createData(SecretParams secret, byte[] data) throws Exception {\n        return toResult(secretClient.createSecret(secretRequest(secret)\n                .data(data)\n                .build()));\n    }\n\n    private static SecretCreationResult toResult(SecretOperationResponse response) {\n        return SecretCreationResult.builder()\n                .id(response.getId())\n                .password(response.getPassword())\n                .build();\n    }\n\n    private <T extends Secret> T get(String orgName, String secretName, String password, SecretEntryV2.TypeEnum type) throws Exception {\n        try {\n            return secretClient.getData(orgName, secretName, password, type);\n        } catch (com.walmartlabs.concord.client2.SecretNotFoundException e) {\n            throw new SecretNotFoundException(e.getOrgName(), e.getSecretName());\n        }\n    }\n\n    private ImmutableCreateSecretRequest.Builder secretRequest(SecretParams secret) {\n        SecretParams.Visibility visibility = secret.visibility();\n        ImmutableCreateSecretRequest .Builder result = CreateSecretRequest.builder()\n                .org(secret.orgName())\n                .name(secret.secretName())\n                .generatePassword(secret.generatePassword())\n                .storePassword(secret.storePassword())\n                .visibility(visibility != null ? SecretEntryV2.VisibilityEnum.fromValue(visibility.name()) : null);\n\n        if (secret.project() != null) {\n            result.addProjectNames(Objects.requireNonNull(secret.project()));\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/DefaultSynchronizationService.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.inject.Singleton;\nimport java.util.ArrayList;\nimport java.util.List;\n\n@Singleton\npublic class DefaultSynchronizationService implements SynchronizationService {\n\n    private final List<Runnable> callbacks = new ArrayList<>();\n\n    private boolean stop;\n\n    @Override\n    public boolean hasPoint() {\n        synchronized (this) {\n            return !callbacks.isEmpty();\n        }\n    }\n\n    @Override\n    public void maintain() {\n        for (Runnable callback : callbacks) {\n            callback.run();\n        }\n\n        callbacks.clear();\n        stop = false;\n    }\n\n    @Override\n    public void point(Runnable callback) {\n        synchronized (this) {\n            callbacks.add(callback);\n        }\n    }\n\n    @Override\n    public void stop() {\n        this.stop = true;\n    }\n\n    @Override\n    public boolean hasStop() {\n        return stop;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/DefaultTaskVariablesProvider.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration;\n\nimport javax.inject.Inject;\nimport javax.inject.Provider;\nimport javax.inject.Singleton;\n\n@Singleton\npublic class DefaultTaskVariablesProvider implements Provider<DefaultTaskVariablesService> {\n\n    private final DefaultTaskVariablesService service;\n\n    @Inject\n    public DefaultTaskVariablesProvider(ProcessConfiguration cfg) {\n        this.service = new MapBackedDefaultTaskVariablesService(cfg.defaultTaskVariables());\n    }\n\n    @Override\n    public DefaultTaskVariablesService get() {\n        return service;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/DefaultTaskVariablesService.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Collections;\nimport java.util.Map;\n\npublic interface DefaultTaskVariablesService {\n\n    default Map<String, Object> get(String taskName) {\n        return Collections.emptyMap();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/DependencyManagerConfigurationProvider.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.dependencymanager.DependencyManagerConfiguration;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\n\nimport javax.inject.Inject;\nimport javax.inject.Provider;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\npublic class DependencyManagerConfigurationProvider implements Provider<DependencyManagerConfiguration> {\n\n    private final Path cacheDir;\n\n    @Inject\n    public DependencyManagerConfigurationProvider(RunnerConfiguration cfg) {\n        this.cacheDir = getCacheDir(cfg);\n    }\n\n    @Override\n    public DependencyManagerConfiguration get() {\n        return DependencyManagerConfiguration.of(cacheDir);\n    }\n\n    private static Path getCacheDir(RunnerConfiguration cfg) {\n        try {\n            String s = cfg.dependencyManager().cacheDir();\n            if (s == null) {\n                return PathUtils.createTempDir(\"dependencyCache\");\n            }\n\n            Path p = Paths.get(s);\n            if (!Files.exists(p) || !Files.isDirectory(p)) {\n                throw new RuntimeException(\"The dependency cache directory doesn't exist or not a directory: \" + p);\n            }\n\n            return p;\n        } catch (IOException e) {\n            throw new RuntimeException(\"Error while creating the dependency cache directory\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/DockerProcessBuilder.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.PrivilegedAction;\nimport com.walmartlabs.concord.runtime.v2.sdk.DockerContainerSpec;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.*;\n\npublic class DockerProcessBuilder {\n\n    public static DockerProcessBuilder from(UUID txId, DockerContainerSpec spec) {\n        DockerProcessBuilder b = new DockerProcessBuilder(spec.image())\n                .name(spec.name())\n                .user(Optional.ofNullable(spec.user()).orElse(DEFAULT_USER))\n                .workdir(spec.workdir())\n                .entryPoint(spec.entryPoint())\n                .cpu(spec.cpu())\n                .memory(spec.memory())\n                .args(spec.args())\n                .env(spec.env())\n                .envFile(spec.envFile())\n                .labels(spec.labels())\n                .debug(spec.debug())\n                .forcePull(spec.forcePull())\n                .stdOutFilePath(spec.stdOutFilePath())\n                .redirectErrorStream(spec.redirectErrorStream());\n\n        DockerContainerSpec.Options options = spec.options();\n        if (options != null) {\n            DockerOptionsBuilder ob = new DockerOptionsBuilder();\n\n            List<String> hosts = options.hosts();\n            if (hosts != null) {\n                hosts.forEach(ob::etcHost);\n            }\n\n            b.options(ob.build());\n        }\n\n        // system stuff\n        b.addLabel(CONCORD_TX_ID_LABEL, txId.toString());\n\n        return b;\n    }\n\n    private static final Logger log = LoggerFactory.getLogger(DockerProcessBuilder.class);\n\n    public static final String CONCORD_DOCKER_LOCAL_MODE_KEY = \"CONCORD_DOCKER_LOCAL_MODE\";\n    public static final String CONCORD_DOCKER_DEFAULT_USER_KEY = \"CONCORD_DOCKER_DEFAULT_USER\";\n    public static final String CONCORD_DOCKER_USE_CONTAINER_USER_KEY = \"CONCORD_DOCKER_USE_CONTAINER_USER\";\n\n    public static final String CONCORD_TX_ID_LABEL = \"concordTxId\";\n\n    private static final String DEFAULT_USER;\n\n    static {\n        String s = System.getenv(CONCORD_DOCKER_DEFAULT_USER_KEY);\n        if (s != null) {\n            DEFAULT_USER = s;\n        } else {\n            DEFAULT_USER = \"456\"; // as in dockerPasswd\n        }\n    }\n\n    private final String image;\n\n    private String name;\n    private String user = DEFAULT_USER;\n    private String workdir;\n    private String entryPoint;\n    private String cpu;\n    private String memory;\n    private String stdOutFilePath;\n\n    private final List<String> args = new ArrayList<>();\n    private Map<String, String> env;\n    private String envFile;\n    private Map<String, String> labels;\n    private Collection<String> volumes = new ArrayList<>();\n    private List<Map.Entry<String, String>> options = new ArrayList<>();\n\n    private boolean cleanup = true;\n    private boolean debug = false;\n    private boolean forcePull = true;\n    private boolean generateUsers = false;\n    private boolean exposeHostUsers = false;\n    private boolean useHostUser = false;\n    private boolean useHostNetwork = true;\n    private boolean useContainerUser;\n\n    private boolean redirectErrorStream = true;\n\n    private final List<Path> tmpPaths = new ArrayList<>();\n\n    public DockerProcessBuilder(String image) {\n        this.image = image;\n\n        if (Boolean.parseBoolean(env(CONCORD_DOCKER_LOCAL_MODE_KEY, \"true\"))) {\n            // in the \"local docker mode\" we run all Docker processes using the current OS user's UID/GID\n            // in order to do that, we need to mount the local /etc/passwd inside of the container\n            log.warn(\"Running in the local Docker mode. Consider setting {}=false in the production environment.\", CONCORD_DOCKER_LOCAL_MODE_KEY);\n\n            this.exposeHostUsers = true;\n            this.useHostUser = true;\n        } else {\n            this.generateUsers = true;\n        }\n\n        this.useContainerUser = Boolean.parseBoolean(env(CONCORD_DOCKER_USE_CONTAINER_USER_KEY, \"false\"));\n    }\n\n    public DockerProcess build() throws IOException {\n        String[] cmd = buildCmd();\n\n        if (debug) {\n            log.info(\"CMD: {}\", (Object) cmd);\n        }\n\n        return new DockerProcess(cmd, redirectErrorStream, tmpPaths);\n    }\n\n    private String[] buildCmd() throws IOException {\n        if (forcePull) {\n            return new String[]{\"/bin/bash\", \"-c\", \"docker pull \" + q(image) + \" && \" + buildDockerCmd()};\n        } else {\n            return new String[]{\"/bin/bash\", \"-c\", buildDockerCmd()};\n        }\n    }\n\n    private String buildDockerCmd() throws IOException {\n        List<String> c = new ArrayList<>();\n        c.add(\"docker\");\n        c.add(\"run\");\n        if (name != null) {\n            c.add(\"--name\");\n            c.add(q(name));\n        }\n        if (user != null && !useHostUser && !useContainerUser) {\n            c.add(\"-u\");\n            c.add(user);\n        }\n        if (cleanup) {\n            c.add(\"--rm\");\n        }\n        c.add(\"-i\");\n        if (volumes != null) {\n            volumes.forEach(v -> {\n                c.add(\"-v\");\n                c.add(q(v));\n            });\n        }\n        if (env != null) {\n            env.forEach((k, v) -> {\n                c.add(\"-e\");\n                c.add(q(k + \"=\" + v));\n            });\n        }\n        if (envFile != null) {\n            c.add(\"--env-file\");\n            c.add(q(envFile));\n        }\n        if (workdir != null) {\n            c.add(\"-w\");\n            c.add(q(workdir));\n        }\n        if (labels != null) {\n            for (Map.Entry<String, String> l : labels.entrySet()) {\n                String k = l.getKey();\n                String v = l.getValue();\n\n                c.add(\"--label\");\n                c.add(q(k + (v != null ? \"=\" + v : \"\")));\n            }\n        }\n        if (entryPoint != null) {\n            c.add(\"--entrypoint\");\n            c.add(entryPoint);\n        }\n        if (generateUsers) {\n            Path tmp = PathUtils.createTempFile(\"passwd\", \".docker\"); // NOSONAR\n            tmpPaths.add(tmp);\n            try (InputStream src = Objects.requireNonNull(DockerProcessBuilder.class.getResourceAsStream(\"dockerPasswd\"));\n                 OutputStream dst = Files.newOutputStream(tmp)) {\n                src.transferTo(dst);\n            }\n            c.add(\"-v\");\n            c.add(tmp.toAbsolutePath() + \":/etc/passwd:ro\");\n        }\n        if (exposeHostUsers) {\n            c.add(\"-v\");\n            c.add(\"/etc/passwd:/etc/passwd:ro\");\n        }\n        if (useHostUser && !useContainerUser) {\n            c.add(\"-u\");\n            c.add(\"`id -u`:`id -g`\");\n\n            c.add(\"-e\");\n            c.add(\"HOME=/tmp\");\n        }\n        if (useHostNetwork) {\n            c.add(\"--net=host\");\n        }\n        if (cpu != null) {\n            c.add(\"--cpus\");\n            c.add(cpu);\n        }\n        if (memory != null) {\n            c.add(\"-m\");\n            c.add(memory);\n        }\n        options.forEach(o -> {\n            c.add(o.getKey());\n            if (o.getValue() != null) {\n                c.add(o.getValue());\n            }\n        });\n        c.add(q(image));\n\n        args.forEach(a -> c.add(q(a)));\n\n        if (stdOutFilePath != null) {\n            c.add(0, \"set -o pipefail && \");\n            c.add(\"| tee \");\n            c.add(stdOutFilePath);\n        }\n        return String.join(\" \", c);\n    }\n\n    public DockerProcessBuilder cpu(String cpu) {\n        this.cpu = cpu;\n        return this;\n    }\n\n    public DockerProcessBuilder memory(String memory) {\n        this.memory = memory;\n        return this;\n    }\n\n    public DockerProcessBuilder stdOutFilePath(String stdOutFilePath) {\n        this.stdOutFilePath = stdOutFilePath;\n        return this;\n    }\n\n    public DockerProcessBuilder name(String name) {\n        this.name = name;\n        return this;\n    }\n\n    public DockerProcessBuilder user(String user) {\n        this.user = user;\n        return this;\n    }\n\n    public DockerProcessBuilder labels(Map<String, String> labels) {\n        this.labels = labels;\n        return this;\n    }\n\n    public DockerProcessBuilder addLabel(String k, String v) {\n        if (labels == null) {\n            labels = new HashMap<>();\n        }\n        labels.put(k, v);\n        return this;\n    }\n\n    public DockerProcessBuilder debug(boolean debug) {\n        this.debug = debug;\n        return this;\n    }\n\n    public DockerProcessBuilder workdir(String workdir) {\n        this.workdir = workdir;\n        return this;\n    }\n\n    public DockerProcessBuilder volumes(Collection<String> volumes) {\n        this.volumes = volumes;\n        return this;\n    }\n\n    public DockerProcessBuilder volume(String spec) {\n        volumes.add(spec);\n        return this;\n    }\n\n    public DockerProcessBuilder volume(String hostSrc, String containerDest) {\n        volumes.add(hostSrc + \":\" + containerDest);\n        return this;\n    }\n\n    public DockerProcessBuilder volume(String hostSrc, String containerDest, boolean readOnly) {\n        volumes.add(hostSrc + \":\" + containerDest + (readOnly ? \":ro\" : \":rw\"));\n        return this;\n    }\n\n    public DockerProcessBuilder cleanup(boolean cleanup) {\n        this.cleanup = cleanup;\n        return this;\n    }\n\n    public DockerProcessBuilder args(List<String> args) {\n        if (args == null) {\n            return this;\n        }\n\n        this.args.addAll(args);\n        return this;\n    }\n\n    public DockerProcessBuilder arg(String v) {\n        this.args.add(v);\n        return this;\n    }\n\n    public DockerProcessBuilder arg(String k, String v) {\n        this.args.add(k);\n        this.args.add(v);\n        return this;\n    }\n\n    public DockerProcessBuilder env(Map<String, String> env) {\n        this.env = env;\n        return this;\n    }\n\n    public DockerProcessBuilder envFile(String envFile) {\n        this.envFile = envFile;\n        return this;\n    }\n\n    public DockerProcessBuilder entryPoint(String entryPoint) {\n        this.entryPoint = entryPoint;\n        return this;\n    }\n\n    public DockerProcessBuilder forcePull(boolean forcePull) {\n        this.forcePull = forcePull;\n        return this;\n    }\n\n    public DockerProcessBuilder useHostNetwork(boolean useHostNetwork) {\n        this.useHostNetwork = useHostNetwork;\n        return this;\n    }\n\n    public DockerProcessBuilder options(List<Map.Entry<String, String>> options) {\n        this.options = options;\n        return this;\n    }\n\n    public DockerProcessBuilder option(String k, String v) {\n        this.options.add(new AbstractMap.SimpleEntry<>(k, v));\n        return this;\n    }\n\n    public DockerProcessBuilder redirectErrorStream(boolean redirectErrorStream) {\n        this.redirectErrorStream = redirectErrorStream;\n        return this;\n    }\n\n    public DockerProcessBuilder useContainerUser(boolean useContainerUser) {\n        this.useContainerUser = useContainerUser;\n        return this;\n    }\n\n    private static String q(String s) {\n        if (s == null) {\n            return null;\n        }\n\n        return \"'\" + s + \"'\";\n    }\n\n    private static String env(String k, String defaultValue) {\n        String s = System.getenv(k);\n        return s != null ? s : defaultValue;\n    }\n\n    public static class DockerOptionsBuilder {\n\n        private final List<Map.Entry<String, String>> options = new ArrayList<>();\n\n        public DockerOptionsBuilder etcHost(String host) {\n            this.options.add(new AbstractMap.SimpleEntry<>(\"--add-host\", host));\n            return this;\n        }\n\n        public List<Map.Entry<String, String>> build() {\n            return options;\n        }\n    }\n\n    public static class DockerProcess implements AutoCloseable {\n\n        private final String[] cmd;\n        private final boolean redirectErrorStream;\n        private final List<Path> tmpPaths;\n\n        public DockerProcess(String[] cmd, boolean redirectErrorStream, List<Path> tmpPaths) {\n            this.cmd = cmd;\n            this.redirectErrorStream = redirectErrorStream;\n            this.tmpPaths = tmpPaths;\n        }\n\n        public Process start() throws IOException {\n            return PrivilegedAction.perform(\"docker\", () -> new ProcessBuilder(cmd)\n                    .redirectErrorStream(redirectErrorStream)\n                    .start());\n        }\n\n        public String[] cmd() {\n            return cmd;\n        }\n\n        @Override\n        public void close() {\n            for (Path p : tmpPaths) {\n                try {\n                    PathUtils.deleteRecursively(p);\n                } catch (IOException e) {\n                    log.warn(\"delete '{}' -> error: {}\", p, e.getMessage());\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/EventReportingService.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessEventRequest;\n\npublic interface EventReportingService {\n\n    /**\n     * Report a process event to the server.\n     */\n    void report(ProcessEventRequest req);\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/FormServiceProvider.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.common.FormService;\nimport com.walmartlabs.concord.runtime.v2.sdk.WorkingDirectory;\nimport com.walmartlabs.concord.sdk.Constants;\n\nimport javax.inject.Inject;\nimport javax.inject.Provider;\nimport java.nio.file.Path;\n\npublic class FormServiceProvider implements Provider<FormService> {\n\n    private final WorkingDirectory workingDirectory;\n\n    @Inject\n    public FormServiceProvider(WorkingDirectory workingDirectory) {\n        this.workingDirectory = workingDirectory;\n    }\n\n    @Override\n    public FormService get() {\n        Path attachmentsDir = workingDirectory.getValue().resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME);\n        Path stateDir = attachmentsDir.resolve(Constants.Files.JOB_STATE_DIR_NAME);\n        Path formsDir = stateDir.resolve(Constants.Files.JOB_FORMS_V2_DIR_NAME);\n        return new FormService(formsDir);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/InjectorFactory.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.AbstractModule;\nimport com.google.inject.Guice;\nimport com.google.inject.Injector;\nimport com.google.inject.Module;\nimport com.google.inject.TypeLiteral;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.runtime.common.injector.InjectorUtils;\nimport com.walmartlabs.concord.runtime.common.injector.InstanceId;\nimport com.walmartlabs.concord.runtime.common.injector.TaskHolder;\nimport com.walmartlabs.concord.runtime.v2.runner.guice.CurrentClasspathModule;\nimport com.walmartlabs.concord.runtime.v2.runner.guice.DefaultRunnerModule;\nimport com.walmartlabs.concord.runtime.v2.runner.guice.ProcessDependenciesModule;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.CustomLayout;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.V2;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\nimport org.eclipse.sisu.wire.WireModule;\n\nimport javax.inject.Inject;\nimport javax.inject.Provider;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.UUID;\n\n// TODO poor ergonomics\npublic class InjectorFactory {\n\n    public static Injector createDefault(RunnerConfiguration runnerCfg) {\n        Path src = Paths.get(System.getProperty(\"user.dir\"));\n\n        Provider<ProcessConfiguration> processCfgProvider = new DefaultProcessConfigurationProvider(src);\n        WorkingDirectory workDir = new WorkingDirectory(src);\n\n        return new InjectorFactory(workDir,\n                runnerCfg,\n                processCfgProvider,\n                new DefaultRunnerModule(), // bind default services\n                new ProcessDependenciesModule(workDir.getValue(), runnerCfg.dependencies(), runnerCfg.debug())) // grab process dependencies\n                .create();\n    }\n\n    private final WorkingDirectory workDir;\n    private final RunnerConfiguration runnerCfg;\n    private final com.google.inject.Module[] modules;\n    private final Provider<ProcessConfiguration> processConfigurationProvider;\n\n    public InjectorFactory(WorkingDirectory workDir,\n                           RunnerConfiguration runnerCfg,\n                           Provider<ProcessConfiguration> processConfigurationProvider,\n                           com.google.inject.Module... modules) {\n\n        this.workDir = workDir;\n        this.runnerCfg = runnerCfg;\n        this.modules = modules;\n        this.processConfigurationProvider = processConfigurationProvider;\n    }\n\n    public Injector create() {\n        List<Module> l = new ArrayList<>();\n        l.add(new ConfigurationModule(workDir, runnerCfg, processConfigurationProvider));\n        l.add(new CurrentClasspathModule());\n\n        com.google.inject.Module tasks = new AbstractModule() {\n            @Override\n            protected void configure() {\n                TaskHolder<Task> holder = new TaskHolder<>();\n                bindListener(InjectorUtils.subClassesOf(Task.class), InjectorUtils.taskClassesListener(holder));\n\n                bind(new TypeLiteral<TaskHolder<Task>>() {\n                }).annotatedWith(V2.class).toInstance(holder);\n            }\n        };\n        l.add(tasks);\n\n        if (modules != null) {\n            l.addAll(Arrays.asList(modules));\n        }\n\n        com.google.inject.Module m = new WireModule(l);\n        return Guice.createInjector(m);\n    }\n\n    private static class ConfigurationModule extends AbstractModule {\n\n        private final WorkingDirectory workDir;\n        private final RunnerConfiguration runnerCfg;\n        private final Provider<ProcessConfiguration> processCfgProvider;\n\n        private ConfigurationModule(WorkingDirectory workDir,\n                                    RunnerConfiguration runnerCfg,\n                                    Provider<ProcessConfiguration> processCfgProvider) {\n\n            this.workDir = workDir;\n            this.runnerCfg = runnerCfg;\n            this.processCfgProvider = processCfgProvider;\n        }\n\n        @Override\n        protected void configure() {\n            bind(WorkingDirectory.class).toInstance(workDir);\n            if (runnerCfg.logging().workDirMasking()) {\n                CustomLayout.enableWorkingDirectoryMasking(workDir);\n            }\n\n            var holder = com.walmartlabs.concord.runtime.v2.runner.SensitiveDataHolder.getInstance();\n            bind(com.walmartlabs.concord.runtime.v2.sdk.SensitiveDataHolder.class).toInstance(holder);\n            CustomLayout.setSensitiveDataHolder(holder);\n\n            bind(RunnerConfiguration.class).toInstance(runnerCfg);\n            bind(ProcessConfiguration.class).toProvider(processCfgProvider);\n            bind(InstanceId.class).toProvider(InstanceIdProvider.class);\n        }\n    }\n\n    private static class InstanceIdProvider implements Provider<InstanceId> {\n\n        private final UUID value;\n\n        @Inject\n        private InstanceIdProvider(ProcessConfiguration cfg) {\n            this.value = cfg.instanceId();\n        }\n\n        @Override\n        public InstanceId get() {\n            return new InstanceId(value);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/Main.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.google.inject.Inject;\nimport com.google.inject.Injector;\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.imports.ImportsListener;\nimport com.walmartlabs.concord.imports.NoopImportManager;\nimport com.walmartlabs.concord.runtime.common.ProcessHeartbeat;\nimport com.walmartlabs.concord.runtime.common.StateManager;\nimport com.walmartlabs.concord.runtime.common.cfg.ApiConfiguration;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.runtime.v2.NoopImportsNormalizer;\nimport com.walmartlabs.concord.runtime.v2.ProjectLoaderV2;\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.v2.runner.guice.ObjectMapperProvider;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.LoggingConfigurator;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskProviders;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration;\nimport com.walmartlabs.concord.runtime.v2.sdk.UserDefinedException;\nimport com.walmartlabs.concord.runtime.v2.sdk.WorkingDirectory;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.svm.Frame;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.ParallelExecutionException;\nimport com.walmartlabs.concord.svm.State;\nimport com.walmartlabs.concord.svm.ThreadStatus;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Named;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.Serializable;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic class Main {\n\n    private static final Logger log = LoggerFactory.getLogger(Main.class);\n\n    private final Runner runner;\n    private final RunnerConfiguration runnerCfg;\n    private final ProcessConfiguration processCfg;\n    private final WorkingDirectory workDir;\n    private final TaskProviders taskProviders;\n    private final ObjectMapper objectMapper;\n    private final ClassLoader classLoader;\n\n    @Inject\n    public Main(Runner runner,\n                RunnerConfiguration runnerCfg,\n                ProcessConfiguration processCfg,\n                WorkingDirectory workDir,\n                TaskProviders taskProviders,\n                ObjectMapper objectMapper,\n                @Named(\"runtime\") ClassLoader classLoader) {\n\n        this.runner = runner;\n        this.runnerCfg = runnerCfg;\n        this.processCfg = processCfg;\n        this.workDir = workDir;\n        this.taskProviders = taskProviders;\n        this.objectMapper = objectMapper;\n        this.classLoader = classLoader;\n    }\n\n    public static void main(String[] args) throws Exception {\n        RunnerConfiguration runnerCfg = loadRunnerConfiguration(args);\n\n        // create the injector with all dependencies and services available before\n        // the actual process' working directory is ready. It allows us to load\n        // all dependencies and have them available in \"pre-fork\" situations\n        Injector injector = InjectorFactory.createDefault(runnerCfg);\n\n        try {\n            ProcessConfiguration processCfg = injector.getInstance(ProcessConfiguration.class);\n            ApiClient apiClient = injector.getInstance(ApiClient.class);\n            ProcessHeartbeat heartbeat = new ProcessHeartbeat(apiClient, processCfg.instanceId(), runnerCfg.api().maxNoHeartbeatInterval());\n            heartbeat.start();\n\n            Main main = injector.getInstance(Main.class);\n            main.execute();\n\n            System.exit(0);\n        } catch (UserDefinedException | ParallelExecutionException e) {\n            System.exit(1);\n        } catch (Throwable t) {\n            log.error(\"\", t);\n            System.exit(1);\n        }\n    }\n\n    private static RunnerConfiguration loadRunnerConfiguration(String[] args) throws IOException {\n        RunnerConfiguration result = RunnerConfiguration.builder().build();\n\n        // load file first, then overlay system env vars\n\n        if (args.length > 0) {\n            Path src = Paths.get(args[0]);\n            try (InputStream in = Files.newInputStream(src)) {\n                ObjectMapper om = new ObjectMapperProvider().get();\n                result = om.readValue(in, RunnerConfiguration.class);\n            }\n        }\n\n        String agentId = System.getenv(\"RUNNER_AGENT_ID\");\n        if (agentId != null) {\n            result = RunnerConfiguration.builder().from(result)\n                    .agentId(agentId)\n                    .build();\n        }\n\n        String apiBaseUrl = System.getenv(\"RUNNER_API_BASE_URL\");\n        if (apiBaseUrl != null) {\n            result = RunnerConfiguration.builder().from(result)\n                    .api(ApiConfiguration.builder().from(result.api())\n                            .baseUrl(apiBaseUrl)\n                            .build())\n                    .build();\n        }\n\n        if (result.agentId() == null || result.api() == null || result.api().baseUrl() == null) {\n            throw new IllegalArgumentException(\"Specify the path to the runner configuration file or \" +\n                                               \"provide RUNNER_AGENT_ID and RUNNER_API_BASE_URL environment variables.\");\n        }\n\n        return result;\n    }\n\n    public void execute() throws Exception {\n        validate(processCfg);\n\n        LoggingConfigurator.configure(runnerCfg.logging().segmentedLogs());\n\n        if (processCfg.debug()) {\n            log.info(\"Available tasks: {}\", taskProviders.names().stream().sorted().collect(Collectors.toList()));\n        }\n\n        Path workDir = this.workDir.getValue();\n\n        // three modes:\n        //  - regular start \"from scratch\" (or running a \"handler\" process)\n        //  - continuing from a checkpoint\n        //  - resuming after suspend\n\n        ProcessSnapshot snapshot = StateManager.readProcessState(workDir, classLoader);\n        snapshot = StateBackwardCompatibility.apply(snapshot);\n        Set<String> events = StateManager.readResumeEvents(workDir); // TODO make it an interface?\n\n        Action action = currentAction(events);\n        switch (action) {\n            case START: {\n                Map<String, Object> processArgs = new LinkedHashMap<>();\n                if (snapshot != null) {\n                    // grab top-level variables from the snapshot and use them as process arguments\n                    processArgs.putAll(getTopLevelVariables(snapshot));\n                }\n                processArgs.putAll(prepareProcessArgs(processCfg));\n\n                snapshot = start(runner, processCfg, workDir, processArgs);\n                break;\n            }\n            case RESUME: {\n                Map<String, Object> processArgs = prepareProcessArgs(processCfg);\n\n                snapshot = resume(runner, processCfg, snapshot, processArgs, events);\n                break;\n            }\n            default: {\n                throw new IllegalStateException(\"Unsupported action: \" + action);\n            }\n        }\n\n        if (isSuspended(snapshot)) {\n            StateManager.finalizeSuspendedState(workDir, snapshot, getEvents(snapshot)); // TODO make it an interface?\n        } else {\n            StateManager.cleanupState(workDir); // TODO make it an interface\n        }\n    }\n\n    private Map<String, Object> prepareProcessArgs(ProcessConfiguration cfg) {\n        // use LinkedHashMap to preserve the order of the keys\n        Map<String, Object> m = new LinkedHashMap<>(cfg.arguments());\n\n        // save the current process ID as an argument, flows and plugins expect it to be a string value\n        m.put(Constants.Context.TX_ID_KEY, Objects.requireNonNull(cfg.instanceId()).toString());\n\n        m.put(Constants.Context.WORK_DIR_KEY, workDir.getValue().toAbsolutePath().toString());\n\n        // save processInfo and projectInfo variables\n        m.put(Constants.Request.PROCESS_INFO_KEY, objectMapper.convertValue(cfg.processInfo(), Map.class));\n        m.put(Constants.Request.PROJECT_INFO_KEY, objectMapper.convertValue(cfg.projectInfo(), Map.class));\n\n        return m;\n    }\n\n    private static Map<String, Serializable> getTopLevelVariables(ProcessSnapshot snapshot) {\n        State state = snapshot.vmState();\n        List<Frame> frames = state.getFrames(state.getRootThreadId());\n        Frame rootFrame = frames.get(frames.size() - 1);\n        return rootFrame.getLocals();\n    }\n\n    private static void validate(ProcessConfiguration cfg) {\n        if (cfg.instanceId() == null) {\n            throw new IllegalStateException(\"ProcessConfiguration -> instanceId cannot be null\");\n        }\n    }\n\n    private static ProcessSnapshot start(Runner runner, ProcessConfiguration cfg, Path workDir, Map<String, Object> args) throws Exception {\n        // assume all imports were processed by the agent\n        ProjectLoaderV2 loader = new ProjectLoaderV2(new NoopImportManager());\n        ProcessDefinition processDefinition = loader.load(workDir, new NoopImportsNormalizer(), ImportsListener.NOP_LISTENER).getProjectDefinition();\n\n        Map<String, Object> initiator = cfg.initiator();\n        if (initiator != null) {\n            // when the process starts the process' initiator and the current user are the same\n            args.put(Constants.Request.INITIATOR_KEY, initiator);\n            args.put(Constants.Request.CURRENT_USER_KEY, initiator);\n        }\n\n        return runner.start(cfg, processDefinition, args);\n    }\n\n    private static ProcessSnapshot resume(Runner runner, ProcessConfiguration cfg,\n                                          ProcessSnapshot snapshot, Map<String, Object> args,\n                                          Set<String> events) throws Exception {\n\n        Map<String, Object> initiator = cfg.initiator();\n        if (initiator != null) {\n            args.put(Constants.Request.INITIATOR_KEY, initiator);\n        }\n\n        Map<String, Object> currentUser = cfg.currentUser();\n        if (currentUser != null) {\n            args.put(Constants.Request.CURRENT_USER_KEY, currentUser);\n        }\n\n        return runner.resume(snapshot, events, args);\n    }\n\n    private static boolean isSuspended(ProcessSnapshot snapshot) {\n        return snapshot.vmState().threadStatus().entrySet().stream()\n                .anyMatch(e -> e.getValue() == ThreadStatus.SUSPENDED);\n    }\n\n    private static Set<String> getEvents(ProcessSnapshot snapshot) {\n        Collection<String> eventRefs = snapshot.vmState().getEventRefs().values();\n\n        Set<String> events = new HashSet<>(eventRefs);\n        if (events.size() != eventRefs.size()) {\n            throw new IllegalStateException(\"Non-unique event refs: \" + eventRefs + \". This is most likely a bug.\");\n        }\n\n        return events;\n    }\n\n    private static Action currentAction(Set<String> events) {\n        if (events != null && !events.isEmpty()) {\n            return Action.RESUME;\n        }\n\n        return Action.START;\n    }\n\n    private enum Action {\n        /**\n         * Regular start. If there's a process snapshot (e.g. a handler process like \"onCancel\"),\n         * its variables from the root frame are passed as process arguments.\n         */\n        START,\n\n        /**\n         * Resuming with an event (e.g. a form event).\n         * Previous state + an event ref.\n         */\n        RESUME,\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/MapBackedDefaultTaskVariablesService.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Collections;\nimport java.util.Map;\n\npublic class MapBackedDefaultTaskVariablesService implements DefaultTaskVariablesService {\n\n    private final Map<String, Map<String, Object>> variables;\n\n    public MapBackedDefaultTaskVariablesService(Map<String, Map<String, Object>> variables) {\n        this.variables = Collections.unmodifiableMap(variables);\n    }\n\n    @Override\n    public Map<String, Object> get(String taskName) {\n        return variables.getOrDefault(taskName, Collections.emptyMap());\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/MetadataProcessor.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.runtime.common.injector.InstanceId;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.StepCommand;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.VMUtils;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.util.*;\nimport java.util.concurrent.atomic.AtomicReference;\n\n/**\n * Records updates to the process' metadata.\n *\n * @see ProcessConfiguration#meta()\n */\npublic class MetadataProcessor implements ExecutionListener {\n\n    private static final Logger log = LoggerFactory.getLogger(MetadataProcessor.class);\n\n    private static final Set<Class<?>> VARIABLE_TYPES = new HashSet<>(Arrays.asList(\n            Boolean.class,\n            Byte.class,\n            Character.class,\n            Double.class,\n            Float.class,\n            Integer.class,\n            Long.class,\n            Short.class,\n            String.class));\n\n    private final InstanceId processInstanceId;\n    private final Set<String> metaVariables;\n    private final ProcessApi processApi;\n    private final int retryCount;\n    private final int retryInterval;\n    private final boolean updateMetaOnAllEvents;\n\n    private final AtomicReference<Map<String, Object>> currentMeta = new AtomicReference<>(new HashMap<>());\n\n    @Inject\n    public MetadataProcessor(InstanceId processInstanceId,\n                             ProcessConfiguration processConfiguration,\n                             RunnerConfiguration runnerConfiguration,\n                             ApiClient apiClient) {\n\n        this.processInstanceId = processInstanceId;\n        this.metaVariables = processConfiguration.meta().keySet();\n        this.processApi = new ProcessApi(apiClient);\n        this.retryCount = runnerConfiguration.api().retryCount();\n        this.retryInterval = runnerConfiguration.api().retryInterval();\n        this.updateMetaOnAllEvents = processConfiguration.events().updateMetaOnAllEvents();\n    }\n\n    @Override\n    public Result afterCommand(Runtime runtime, VM vm, State state, ThreadId threadId, Command cmd) {\n        if (!(cmd instanceof StepCommand)) {\n            return Result.CONTINUE;\n        }\n\n        if (metaVariables.isEmpty()) {\n            return Result.CONTINUE;\n        }\n\n        Map<String, Object> vars = VMUtils.getCombinedLocals(state, threadId);\n\n        Map<String, Object> meta = filter(vars, metaVariables);\n        if (meta.isEmpty() || !changed(currentMeta.get(), meta)) {\n            return Result.CONTINUE;\n        }\n\n        currentMeta.set(meta);\n\n        if (updateMetaOnAllEvents) {\n            sendMeta(meta);\n        }\n\n        return Result.CONTINUE;\n    }\n\n    @Override\n    public void onProcessError(Runtime runtime, State state, Exception e) {\n        flush();\n    }\n\n    @Override\n    public void afterProcessEnds(Runtime runtime, State state, Frame lastFrame) {\n        flush();\n    }\n\n    private void flush() {\n        if (!updateMetaOnAllEvents) {\n            sendMeta(currentMeta.get());\n        }\n    }\n\n    private void sendMeta(Map<String, Object> meta) {\n        try {\n            log.debug(\"sending process meta\");\n            ClientUtils.withRetry(retryCount, retryInterval, () -> {\n                processApi.updateMetadata(processInstanceId.getValue(), meta);\n                return null;\n            });\n        } catch (ApiException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static Map<String, Object> filter(Map<String, Object> vars, Set<String> metaVariables) {\n        if (vars.isEmpty()) {\n            return vars;\n        }\n\n        Map<String, Object> result = new HashMap<>();\n        for (String v : metaVariables) {\n            Object value = ConfigurationUtils.get(vars, v.split(\"\\\\.\"));\n            if (value == null) {\n                continue;\n            }\n\n            if (value.getClass().isPrimitive() || VARIABLE_TYPES.contains(value.getClass())) {\n                result.put(v, value);\n            } else {\n                log.debug(\"meta variable '{}' -> ignored (unsupported type: {})\", v, value.getClass());\n            }\n        }\n\n        return result;\n    }\n\n    private static boolean changed(Map<String, Object> oldMeta, Map<String, Object> newMeta) {\n        return !oldMeta.equals(newMeta);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/OutVariablesProcessor.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContext;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContextFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.ExpressionEvaluator;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.svm.ExecutionListener;\nimport com.walmartlabs.concord.svm.Frame;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.State;\n\nimport javax.inject.Inject;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Saves the process' {@code out} variables into a persistent file.\n * Normally, the server picks up the file and saves the data into the process' metadata.\n */\npublic class OutVariablesProcessor implements ExecutionListener {\n\n    private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {\n    };\n\n    private final ObjectMapper objectMapper;\n    private final PersistenceService persistenceService;\n    private final List<String> outVariables;\n\n    @Inject\n    public OutVariablesProcessor(ObjectMapper objectMapper, PersistenceService persistenceService, ProcessConfiguration processConfiguration) {\n        this.objectMapper = objectMapper;\n        this.persistenceService = persistenceService;\n        this.outVariables = processConfiguration.out();\n    }\n\n    @Override\n    public void afterProcessEnds(Runtime runtime, State state, Frame lastFrame) {\n        if (outVariables.isEmpty() || lastFrame == null) {\n            return;\n        }\n\n        Map<String, Object> vars = Collections.unmodifiableMap(lastFrame.getLocals());\n\n        EvalContextFactory ecf = runtime.getService(EvalContextFactory.class);\n        ExpressionEvaluator ee = runtime.getService(ExpressionEvaluator.class);\n        EvalContext evalContext = EvalContext.builder()\n                .from(ecf.strict(vars))\n                .undefinedVariableAsNull(true)\n                .build();\n\n        Map<String, Object> outValues = new HashMap<>();\n        for (String out : outVariables) {\n            Object v = ee.eval(evalContext, \"${\" + out + \"}\", Object.class); // TODO sanitize\n\n            if (v == null) {\n                continue;\n            }\n\n            outValues.put(out, v);\n        }\n\n        if (outValues.isEmpty()) {\n            return;\n        }\n\n        Map<String, Object> currentOut = persistenceService.loadPersistedFile(Constants.Files.OUT_VALUES_FILE_NAME,\n                in -> objectMapper.readValue(in, MAP_TYPE));\n\n        Map<String, Object> result = new HashMap<>(currentOut != null ? currentOut : Collections.emptyMap());\n        result.putAll(outValues);\n\n        persistenceService.persistFile(Constants.Files.OUT_VALUES_FILE_NAME,\n                out -> objectMapper.writeValue(out, result));\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/PersistenceService.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.Serializable;\nimport java.nio.file.OpenOption;\n\npublic interface PersistenceService {\n\n    <T extends Serializable> T load(String storageName, Class<T> expectedType);\n\n    void save(String storageName, Serializable object) throws IOException;\n\n    void persistFile(String name, Writer writer);\n\n    void persistFile(String name, Writer writer, OpenOption... options);\n\n    void persistSessionFile(String name, Writer writer);\n\n    <T> T loadPersistedFile(String name, Converter<InputStream, T> converter);\n\n    <T> T loadPersistedSessionFile(String name, Converter<InputStream, T> converter);\n\n    void deletePersistedFile(String name) throws IOException;\n\n    interface Writer {\n\n        void write(OutputStream out) throws IOException;\n    }\n\n    interface Converter<T, R> {\n\n        R apply(T t) throws Exception;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/PolicyEngineProvider.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.policyengine.PolicyEngineRules;\nimport com.walmartlabs.concord.runtime.v2.sdk.WorkingDirectory;\nimport com.walmartlabs.concord.sdk.Constants;\n\nimport javax.inject.Inject;\nimport javax.inject.Provider;\nimport javax.inject.Singleton;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\n@Singleton\npublic class PolicyEngineProvider implements Provider<PolicyEngine> {\n\n    private final ObjectMapper objectMapper;\n    private final WorkingDirectory workDir;\n\n    @Inject\n    public PolicyEngineProvider(ObjectMapper objectMapper, WorkingDirectory workDir) {\n        this.objectMapper = objectMapper;\n        this.workDir = workDir;\n    }\n\n    @Override\n    public PolicyEngine get() {\n        try {\n            PolicyEngineRules rules = readPolicyRules(workDir.getValue());\n            return new PolicyEngine(rules);\n        } catch (RuntimeException e) {\n            throw e;\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private PolicyEngineRules readPolicyRules(Path ws) {\n        Path policyFile = ws.resolve(Constants.Files.CONCORD_SYSTEM_DIR_NAME).resolve(Constants.Files.POLICY_FILE_NAME);\n        if (!Files.exists(policyFile)) {\n            return PolicyEngineRules.builder().build();\n        }\n\n        try {\n            return objectMapper.readValue(policyFile.toFile(), PolicyEngineRules.class);\n        } catch (IOException e) {\n            throw new RuntimeException(\"Error while reading policy rules: \" + e.getMessage());\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/ProcessSnapshot.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\nimport com.walmartlabs.concord.svm.State;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface ProcessSnapshot extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    State vmState();\n\n    ProcessDefinition processDefinition();\n\n    static ImmutableProcessSnapshot.Builder builder() {\n        return ImmutableProcessSnapshot.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/ProcessStatusCallback.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.UUID;\n\npublic interface ProcessStatusCallback {\n\n    void onRunning(UUID instanceId);\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/ResourceResolver.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.io.InputStream;\n\npublic interface ResourceResolver {\n\n    InputStream resolve(String name) throws IOException;\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/Runner.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.AbstractModule;\nimport com.google.inject.Inject;\nimport com.google.inject.Injector;\nimport com.walmartlabs.concord.runtime.common.injector.InstanceId;\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.v2.runner.compiler.CompilerUtils;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.*;\nimport com.walmartlabs.concord.runtime.v2.sdk.Compiler;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration;\nimport com.walmartlabs.concord.svm.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\npublic class Runner {\n\n    private static final Logger log = LoggerFactory.getLogger(Runner.class);\n\n    private final Injector injector;\n    private final InstanceId instanceId;\n    private final Compiler compiler;\n    private final SynchronizationService synchronizationService;\n    private final Set<ExecutionListener> listeners;\n    private final ProcessStatusCallback statusCallback;\n\n    @Inject\n    public Runner(Injector injector,\n                  InstanceId instanceId,\n                  Compiler compiler,\n                  SynchronizationService synchronizationService,\n                  Set<ExecutionListener> listeners,\n                  ProcessStatusCallback statusCallback) {\n\n        this.injector = injector;\n        this.instanceId = instanceId;\n        this.compiler = compiler;\n        this.synchronizationService = synchronizationService;\n        this.listeners = listeners;\n        this.statusCallback = statusCallback;\n    }\n\n    public ProcessSnapshot start(ProcessConfiguration processConfiguration, ProcessDefinition processDefinition, Map<String, Object> input) throws Exception {\n        statusCallback.onRunning(instanceId.getValue());\n        log.debug(\"start ['{}'] -> running...\", processConfiguration.entryPoint());\n\n        Command cmd = CompilerUtils.compile(compiler, processConfiguration, processDefinition, processConfiguration.entryPoint());\n        State state = new InMemoryState(cmd);\n        // install the exception handler into the root frame\n        // takes care of all unhandled errors bubbling up\n        VMUtils.assertNearestRoot(state, state.getRootThreadId())\n                .setExceptionHandler(new BlockCommand(new SaveOutVariablesOnErrorCommand(), new SaveLastErrorCommand()));\n\n        VM vm = createVM(processDefinition);\n        // update the global variables using the input map by running a special command\n        try {\n            vm.run(state, new UpdateLocalsCommand(input)); // TODO merge with the cfg's arguments\n        } catch (RuntimeException e) {\n            log.error(\"Error while evaluating process arguments: {}\", e.getMessage(), e);\n            throw e;\n        }\n        // start the normal execution\n        vm.start(state);\n\n        log.debug(\"start ['{}'] -> done\", processConfiguration.entryPoint());\n\n        return ProcessSnapshot.builder()\n                .vmState(state)\n                .processDefinition(processDefinition)\n                .build();\n    }\n\n    public ProcessSnapshot resume(ProcessSnapshot snapshot, Set<String> eventRefs, Map<String, Object> input) throws Exception {\n        statusCallback.onRunning(instanceId.getValue());\n        log.debug(\"resume ['{}'] -> running...\", eventRefs);\n\n        State state = snapshot.vmState();\n\n        VM vm = createVM(snapshot.processDefinition());\n\n        // update the global variables using the input map by running a special command\n        // only the threads with the specified eventRef will receive the input\n        Collection<ThreadId> resumingThreads = state.getEventRefs().entrySet().stream()\n                .filter(kv -> eventRefs.contains(kv.getValue()))\n                .map(Map.Entry::getKey)\n                .collect(Collectors.toList());\n        try {\n            vm.run(state, new UpdateLocalsCommand(input, resumingThreads));\n        } catch (RuntimeException e) {\n            log.error(\"Error while evaluating resume arguments: {}\", e.getMessage(), e);\n            throw e;\n        }\n\n        // resume normally\n        vm.resume(state, eventRefs);\n\n        log.debug(\"resume ['{}'] -> done\", eventRefs);\n\n        return ProcessSnapshot.builder()\n                .from(snapshot)\n                .vmState(state)\n                .build();\n    }\n\n    public ProcessSnapshot resume(ProcessSnapshot snapshot, Map<String, Object> input) throws Exception {\n        statusCallback.onRunning(instanceId.getValue());\n        log.debug(\"resume -> running...\");\n\n        State state = snapshot.vmState();\n\n        VM vm = createVM(snapshot.processDefinition());\n        // update the global variables using the input map by running a special command\n        try {\n            vm.run(state, new UpdateLocalsCommand(input));\n        } catch (RuntimeException e) {\n            log.error(\"Error while evaluating resume arguments: {}\", e.getMessage(), e);\n            throw e;\n        }\n\n        // continue as usual\n        vm.start(state);\n\n        log.debug(\"resume -> done\");\n\n        return ProcessSnapshot.builder()\n                .from(snapshot)\n                .vmState(state)\n                .build();\n    }\n\n    private VM createVM(ProcessDefinition processDefinition) {\n        Collection<ExecutionListener> listeners = new ArrayList<>();\n        listeners.add(new SynchronizationServiceListener(synchronizationService));\n        listeners.addAll(this.listeners);\n\n        RuntimeFactory runtimeFactory = vm -> new DefaultRuntime(vm, injectorWithProcessDefinition(injector, processDefinition));\n\n        return new VM(runtimeFactory, listeners);\n    }\n\n    private static Injector injectorWithProcessDefinition(Injector injector, ProcessDefinition processDefinition) {\n        return injector.createChildInjector(new AbstractModule() {\n            @Override\n            protected void configure() {\n                bind(ProcessDefinition.class).toInstance(processDefinition);\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/SensitiveDataHolder.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Collection;\nimport java.util.Set;\nimport java.util.concurrent.CopyOnWriteArraySet;\n\npublic class SensitiveDataHolder implements com.walmartlabs.concord.runtime.v2.sdk.SensitiveDataHolder {\n\n    private static final SensitiveDataHolder INSTANCE = new SensitiveDataHolder();\n\n    @Deprecated\n    public static SensitiveDataHolder getInstance() {\n        return INSTANCE;\n    }\n\n    private final Set<String> sensitiveData = new CopyOnWriteArraySet<>();\n\n    @Override\n    public Set<String> get() {\n        return sensitiveData;\n    }\n\n    @Override\n    public void add(String sensitiveData) {\n        if (sensitiveData == null || sensitiveData.isBlank()) {\n            return;\n        }\n\n        this.sensitiveData.add(sensitiveData);\n    }\n\n    @Override\n    public void addAll(Collection<String> sensitiveData) {\n        this.sensitiveData.addAll(sensitiveData);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/SensitiveDataPersistenceService.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.runtime.v2.sdk.SensitiveDataHolder;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.svm.ExecutionListener;\nimport com.walmartlabs.concord.svm.Frame;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.State;\n\nimport javax.inject.Inject;\nimport java.util.Set;\n\npublic class SensitiveDataPersistenceService implements ExecutionListener {\n\n    private final ObjectMapper objectMapper;\n    private final PersistenceService persistenceService;\n    private final SensitiveDataHolder sensitiveDataHolder;\n\n    @Inject\n    public SensitiveDataPersistenceService(ObjectMapper objectMapper, PersistenceService persistenceService, SensitiveDataHolder sensitiveDataHolder) {\n        this.objectMapper = objectMapper;\n        this.persistenceService = persistenceService;\n        this.sensitiveDataHolder = sensitiveDataHolder;\n    }\n\n    @Override\n    public void afterProcessEnds(Runtime runtime, State state, Frame lastFrame) {\n        var data = sensitiveDataHolder.get();\n        if (data.isEmpty()) {\n            return;\n        }\n\n        persistenceService.persistSessionFile(Constants.Files.SENSITIVE_DATA_FILE_NAME,\n                out -> objectMapper.writeValue(out, data));\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public void beforeProcessResume(Runtime runtime, State state) {\n        Set<String> sensitiveData = persistenceService.loadPersistedSessionFile(Constants.Files.SENSITIVE_DATA_FILE_NAME, is -> objectMapper.readValue(is, Set.class));\n        if (sensitiveData != null) {\n            sensitiveDataHolder.addAll(sensitiveData);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/StackTraceCollector.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.model.FlowCall;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.FlowCallCommand;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\npublic class StackTraceCollector implements ExecutionListener {\n\n    @Override\n    public Result afterCommand(Runtime runtime, VM vm, State state, ThreadId threadId, Command cmd) {\n        if (cmd instanceof FlowCallCommand) {\n            FlowCallCommand fcc = (FlowCallCommand) cmd;\n            FlowCall step = fcc.getStep();\n            String flowName = FlowCallCommand.getFlowName(state, threadId);\n            if (flowName == null) {\n                flowName = step.getFlowName();\n            }\n            Location location = step.getLocation();\n            FrameId frameId = state.peekFrame(threadId).id();\n            state.pushStackTraceItem(threadId, new StackTraceItem(frameId, threadId, location.fileName(), flowName, location.lineNum(), location.column()));\n            return ExecutionListener.super.afterCommand(runtime, vm, state, threadId, cmd);\n        }\n\n        Frame frame = state.peekFrame(threadId);\n        if (frame == null) {\n            state.clearStackTrace(threadId);\n            return ExecutionListener.super.afterCommand(runtime, vm, state, threadId, cmd);\n        }\n\n        return ExecutionListener.super.afterCommand(runtime, vm, state, threadId, cmd);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/StateBackwardCompatibility.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.model.*;\n\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Backward compatibility for reading old {@link ProcessSnapshot} instances.\n */\npublic final class StateBackwardCompatibility {\n\n    public static ProcessSnapshot apply(ProcessSnapshot processSnapshot) {\n        if (processSnapshot == null) {\n            return null;\n        }\n\n        // ProcessDefinition.flows and ProcessDefinition.profiles.flows has changed\n        // from Map<String, List<Step>> to Map<String, Flow>.\n        // Here we convert the old format to the new one.\n\n        ProcessDefinition processDefinition = processSnapshot.processDefinition();\n        if (isOldFlowsDefinition(processDefinition.flows())) {\n            return ProcessSnapshot.builder().from(processSnapshot)\n                    .processDefinition(ProcessDefinition.builder()\n                            .configuration(processDefinition.configuration())\n                            .flows(fixFlows(processDefinition.flows()))\n                            .publicFlows(processDefinition.publicFlows())\n                            .profiles(fixProfiles(processDefinition.profiles()))\n                            .triggers(processDefinition.triggers())\n                            .imports(processDefinition.imports())\n                            .forms(processDefinition.forms())\n                            .resources(processDefinition.resources())\n                            .build())\n                    .build();\n        }\n        return processSnapshot;\n    }\n\n    private static Map<String, Profile> fixProfiles(Map<String, Profile> profiles) {\n        Map<String, Profile> result = new LinkedHashMap<>(profiles.size());\n        for (var e : profiles.entrySet()) {\n            Profile p = e.getValue();\n            result.put(e.getKey(), Profile.builder()\n                    .configuration(p.configuration())\n                    .publicFlows(p.publicFlows())\n                    .flows(fixFlows(p.flows()))\n                    .forms(p.forms())\n                    .build());\n        }\n        return result;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Map<String, Flow> fixFlows(Map<String, Flow> flows) {\n        Map<String, Flow> result = new LinkedHashMap<>(flows.size());\n        for (var e : flows.entrySet()) {\n            result.put(e.getKey(), Flow.of(Location.builder().build(), (List<Step>) e.getValue()));\n        }\n        return result;\n    }\n\n    private static boolean isOldFlowsDefinition(Map<String, Flow> flows) {\n        Object flowOrStepsArray = flows.values().stream().findFirst().orElse(null);\n        return (flowOrStepsArray instanceof List<?>);\n    }\n\n    private StateBackwardCompatibility() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/SynchronizationService.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface SynchronizationService {\n\n    boolean hasPoint();\n\n    void maintain();\n\n    void point(Runnable callback);\n\n    void stop();\n\n    boolean hasStop();\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/SynchronizationServiceListener.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\nimport java.util.Map;\n\npublic class SynchronizationServiceListener implements ExecutionListener {\n\n    private final SynchronizationService delegate;\n\n    public SynchronizationServiceListener(SynchronizationService delegate) {\n        this.delegate = delegate;\n    }\n\n    @Override\n    public Result afterCommand(Runtime runtime, VM vm, State state, ThreadId threadId, Command cmd) {\n        if (delegate.hasPoint() || delegate.hasStop()) {\n            state.setStatus(threadId, ThreadStatus.SUSPENDED);\n            return Result.BREAK;\n        }\n\n        return Result.CONTINUE;\n    }\n\n    @Override\n    public Result afterEval(Runtime runtime, VM vm, State state) {\n        if (delegate.hasStop()) {\n            for (Map.Entry<ThreadId, ThreadStatus> e : state.threadStatus().entrySet()) {\n                state.setStatus(e.getKey(), ThreadStatus.DONE);\n            }\n            return Result.BREAK;\n        }\n\n        if (!delegate.hasPoint()) {\n            return Result.BREAK;\n        }\n\n        return Result.CONTINUE;\n    }\n\n    @Override\n    public Result afterWakeUp(Runtime runtime, VM vm, State state) {\n        delegate.maintain();\n        return Result.CONTINUE;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/TaskResultService.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\nimport javax.inject.Inject;\nimport javax.inject.Singleton;\nimport java.io.Serializable;\nimport java.util.*;\n\n@Singleton\npublic class TaskResultService implements ExecutionListener {\n\n    private HashMap<String, List<Serializable>> taskResults = new HashMap<>();\n    private final PersistenceService persistenceService;\n\n    private final Object mutex = new Object();\n\n    @Inject\n    public TaskResultService(PersistenceService persistenceService) {\n        this.persistenceService = persistenceService;\n    }\n\n    public void store(String taskName, Serializable result) {\n        synchronized (mutex) {\n            List<Serializable> results = taskResults.computeIfAbsent(taskName, s -> new ArrayList<>());\n            results.add(result);\n        }\n    }\n\n    public Map<String, List<Serializable>> getResults() {\n        synchronized (mutex) {\n            return Collections.unmodifiableMap(taskResults);\n        }\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public void beforeProcessStart(Runtime runtime, State state) {\n        taskResults = persistenceService.load(\"taskResults\", HashMap.class);\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public void beforeProcessResume(Runtime runtime, State state) {\n        taskResults = persistenceService.load(\"taskResults\", HashMap.class);\n    }\n\n    @Override\n    public void afterProcessEnds(Runtime runtime, State state, Frame lastFrame) {\n        boolean isSuspended = state.threadStatus().entrySet().stream()\n                .anyMatch(e -> e.getValue() == ThreadStatus.SUSPENDED);\n        if (!isSuspended) {\n            return;\n        }\n\n        try {\n            persistenceService.save(\"taskResults\", taskResults);\n        } catch (Exception e) {\n            throw new RuntimeException(\"Task results save error\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/checkpoints/CheckpointService.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.checkpoints;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.runner.ProcessSnapshot;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.ThreadId;\n\nimport java.util.UUID;\n\npublic interface CheckpointService {\n\n    void create(ThreadId threadId, UUID correlationId, String name, Runtime runtime, ProcessSnapshot snapshot);\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/checkpoints/CheckpointUploader.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.checkpoints;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.nio.file.Path;\nimport java.util.UUID;\n\npublic interface CheckpointUploader {\n\n    void upload(UUID checkpointId, UUID correlationId, String name, Path archivePath) throws Exception;\n}"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/checkpoints/DefaultCheckpointService.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.checkpoints;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.annotations.VisibleForTesting;\nimport com.walmartlabs.concord.common.ObjectInputStreamWithClassLoader;\nimport com.walmartlabs.concord.common.TemporaryPath;\nimport com.walmartlabs.concord.runtime.v2.runner.ProcessSnapshot;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.VMUtils;\nimport com.walmartlabs.concord.runtime.v2.sdk.WorkingDirectory;\nimport com.walmartlabs.concord.svm.*;\nimport com.walmartlabs.concord.svm.Runtime;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.io.*;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.sdk.Constants.Request.RESUME_EVENTS_KEY;\n\npublic class DefaultCheckpointService implements CheckpointService {\n\n    private static final Logger log = LoggerFactory.getLogger(DefaultCheckpointService.class);\n\n    private final WorkingDirectory workingDirectory;\n    private final CheckpointUploader checkpointUploader;\n    private final ClassLoader classLoader;\n\n    @Inject\n    public DefaultCheckpointService(WorkingDirectory workingDirectory,\n                                    CheckpointUploader checkpointUploader,\n                                    @Named(\"runtime\") ClassLoader classLoader) {\n\n        this.workingDirectory = workingDirectory;\n        this.checkpointUploader = checkpointUploader;\n        this.classLoader = classLoader;\n    }\n\n    @Override\n    public void create(ThreadId threadId, UUID correlationId, String name, Runtime runtime, ProcessSnapshot snapshot) {\n        validate(threadId, snapshot);\n\n        UUID checkpointId = UUID.randomUUID();\n\n        try (StateArchive archive = new StateArchive()) {\n            // the goal here is to create a process state snapshot with\n            // a \"synthetic\" event that can be used to continue the process\n            // after the checkpoint step\n\n            String resumeEventRef = checkpointId.toString();\n\n            State state = clone(snapshot.vmState(), classLoader);\n            state.setEventRef(threadId, resumeEventRef);\n            state.setStatus(threadId, ThreadStatus.SUSPENDED);\n\n            List<Frame> frames = state.getFrames(state.getRootThreadId());\n            Frame rootFrame = frames.get(frames.size() - 1);\n            VMUtils.putLocal(rootFrame, RESUME_EVENTS_KEY, Collections.singletonList(name));\n\n            archive.withResumeEvent(resumeEventRef)\n                    .withProcessState(ProcessSnapshot.builder()\n                            .from(snapshot)\n                            .vmState(state)\n                            .build())\n                    .withSystemDirectory(workingDirectory.getValue());\n\n            try (TemporaryPath zip = archive.zip()) {\n                checkpointUploader.upload(checkpointId, correlationId, name, zip.path());\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(\"Checkpoint upload error\", e);\n        }\n\n        log.info(\"Checkpoint '{}' created\", name);\n    }\n\n    private static void validate(ThreadId threadId, ProcessSnapshot snapshot) {\n        State state = snapshot.vmState();\n\n        String eventRef = state.getEventRefs().get(threadId);\n        if (eventRef != null) {\n            throw new IllegalStateException(\"Can't create a checkpoint, the current thread has an unprocessed eventRef: \" + eventRef);\n        }\n    }\n\n    @VisibleForTesting\n    @SuppressWarnings(\"unchecked\")\n    static <T> T clone(T object, ClassLoader cl) throws Exception {\n        byte[] ab;\n\n        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();\n             ObjectOutputStream oos = new ObjectOutputStream(baos)) {\n            oos.writeObject(object);\n            ab = baos.toByteArray();\n        }\n\n        try (ByteArrayInputStream bais = new ByteArrayInputStream(ab);\n             ObjectInputStream ois = new ObjectInputStreamWithClassLoader(bais, cl)) {\n            return (T) ois.readObject();\n        } catch (OptionalDataException odx) {\n            log.error(\"Error while cloning an instance of {} (eof={}, length={}) using the provided class loader (name={}, class={})\", object.getClass(), odx.eof, odx.length, cl.getName(), cl.getClass());\n            throw odx;\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/checkpoints/DefaultCheckpointUploader.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.checkpoints;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.*;\nimport com.walmartlabs.concord.runtime.common.cfg.ApiConfiguration;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.runtime.common.injector.InstanceId;\n\nimport javax.inject.Inject;\nimport java.nio.file.Path;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic class DefaultCheckpointUploader implements CheckpointUploader {\n\n    private final InstanceId instanceId;\n    private final ApiClient apiClient;\n    private final ApiConfiguration apiConfiguration;\n\n    @Inject\n    public DefaultCheckpointUploader(InstanceId instanceId,\n                                     RunnerConfiguration configuration,\n                                     ApiClient apiClient) {\n        this.instanceId = instanceId;\n        this.apiClient = apiClient;\n        this.apiConfiguration = configuration.api();\n    }\n\n    @Override\n    public void upload(UUID checkpointId, UUID correlationId, String name, Path path) throws Exception {\n        Map<String, Object> data = new HashMap<>();\n        data.put(\"id\", checkpointId);\n        data.put(\"correlationId\", correlationId);\n        data.put(\"name\", name);\n        data.put(\"data\", path);\n\n        ClientUtils.withRetry(apiConfiguration.retryCount(), apiConfiguration.retryInterval(), () -> {\n            new CheckpointApi(apiClient).uploadCheckpoint(instanceId.getValue(), data);\n            return null;\n        });\n    }\n}"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/checkpoints/StateArchive.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.checkpoints;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.TemporaryPath;\nimport com.walmartlabs.concord.common.ZipUtils;\nimport com.walmartlabs.concord.runtime.common.StateManager;\nimport com.walmartlabs.concord.runtime.v2.runner.ProcessSnapshot;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;\n\nimport java.io.BufferedOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic class StateArchive implements AutoCloseable {\n\n    private final TemporaryPath dir;\n\n    public StateArchive() throws IOException {\n        this.dir = PathUtils.tempDir(\"state-archive\");\n    }\n\n    @Override\n    public void close() {\n        this.dir.close();\n    }\n\n    public StateArchive withProcessState(ProcessSnapshot snapshot) {\n        try {\n            StateManager.saveProcessState(dir.path(), snapshot);\n        } catch (IOException e) {\n            throw new RuntimeException(\"Error while saving process state: \" + e.getMessage(), e);\n        }\n\n        return this;\n    }\n\n    public StateArchive withResumeEvent(String name) {\n        StateManager.saveResumeEvent(dir.path(), name);\n        return this;\n    }\n\n    public StateArchive withSystemDirectory(Path workDir) {\n        try {\n            Path src = workDir.resolve(Constants.Files.CONCORD_SYSTEM_DIR_NAME);\n            if (Files.notExists(src)) {\n                return this;\n            }\n\n            Path dst = dir.path().resolve(Constants.Files.CONCORD_SYSTEM_DIR_NAME);\n            if (!Files.exists(dst)) {\n                Files.createDirectories(dst);\n            }\n\n            PathUtils.copy(src, dst);\n        } catch (IOException e) {\n            throw new RuntimeException(\"Error while copying the process' system directory: \" + e.getMessage(), e);\n        }\n\n        return this;\n    }\n\n    public TemporaryPath zip() throws IOException {\n        Path dst = PathUtils.createTempFile(\"state\", \".zip\");\n        try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(dst));\n             ZipArchiveOutputStream zip = new ZipArchiveOutputStream(out)) {\n            ZipUtils.zip(zip, dir.path());\n        }\n        return new TemporaryPath(dst);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/compiler/CheckpointCompiler.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.compiler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.Checkpoint;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.CheckpointCommand;\nimport com.walmartlabs.concord.svm.Command;\n\nimport javax.inject.Named;\n\n@Named\npublic class CheckpointCompiler implements StepCompiler<Checkpoint> {\n\n    @Override\n    public boolean accepts(Step step) {\n        return step instanceof Checkpoint;\n    }\n\n    @Override\n    public Command compile(CompilerContext context, Checkpoint step) {\n        return new CheckpointCommand(step);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/compiler/CompilerContext.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.compiler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.v2.sdk.Compiler;\n\npublic interface CompilerContext {\n\n    Compiler compiler();\n\n    ProcessDefinition processDefinition();\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/compiler/CompilerUtils.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.compiler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.Flow;\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.v2.model.Profile;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.BlockCommand;\nimport com.walmartlabs.concord.runtime.v2.sdk.Compiler;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration;\nimport com.walmartlabs.concord.svm.Command;\n\nimport java.util.List;\nimport java.util.stream.Collectors;\n\npublic final class CompilerUtils {\n\n    public static Command compile(Compiler compiler, ProcessConfiguration processConfiguration, ProcessDefinition pd, String flowName) {\n        Flow flow = pd.flows().get(flowName);\n        for (String activeProfile : processConfiguration.processInfo().activeProfiles()) {\n            Flow maybeFlow = pd.profiles().getOrDefault(activeProfile, Profile.builder().build()).flows().get(flowName);\n            if (maybeFlow != null) {\n                flow = maybeFlow;\n            }\n        }\n        if (flow == null) {\n            throw new IllegalArgumentException(\"Flow not found: \" + flowName);\n        }\n\n        List<Command> commands = flow.steps().stream().map(s -> compiler.compile(pd, s)).collect(Collectors.toList());\n        return new BlockCommand(commands);\n    }\n\n    public static Command compile(CompilerContext context, List<Step> steps) {\n        return new BlockCommand(steps.stream()\n                .map(s -> context.compiler().compile(context.processDefinition(), s))\n                .collect(Collectors.toList()));\n    }\n\n    private CompilerUtils() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/compiler/DefaultCompiler.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.compiler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.sdk.Compiler;\nimport com.walmartlabs.concord.svm.Command;\n\nimport javax.inject.Inject;\nimport java.util.Collection;\n\npublic class DefaultCompiler implements Compiler {\n\n    private final Collection<StepCompiler<?>> compilers;\n\n    @Inject\n    public DefaultCompiler(Collection<StepCompiler<?>> compilers) {\n        this.compilers = compilers;\n    }\n\n    @Override\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    public Command compile(ProcessDefinition processDefinition, Step step) {\n        StepCompiler sc = compilers.stream().filter(c -> c.accepts(step))\n                .findFirst()\n                .orElseThrow(() -> new IllegalStateException(\"Can't find a compiler for \" + step.getClass()));\n\n        CompilerContext ctx = new DefaultCompilerContext(this, processDefinition);\n        return sc.compile(ctx, step);\n    }\n\n    public static class DefaultCompilerContext implements CompilerContext {\n\n        private final Compiler compiler;\n        private final ProcessDefinition processDefinition;\n\n        private DefaultCompilerContext(Compiler compiler, ProcessDefinition processDefinition) {\n            this.compiler = compiler;\n            this.processDefinition = processDefinition;\n        }\n\n        @Override\n        public Compiler compiler() {\n            return compiler;\n        }\n\n        @Override\n        public ProcessDefinition processDefinition() {\n            return processDefinition;\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/compiler/ExitCompiler.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.compiler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.ExitStep;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.ExitCommand;\nimport com.walmartlabs.concord.svm.Command;\n\nimport javax.inject.Named;\n\n@Named\npublic final class ExitCompiler implements StepCompiler<ExitStep> {\n\n    @Override\n    public boolean accepts(Step step) {\n        return step instanceof ExitStep;\n    }\n\n    @Override\n    public Command compile(CompilerContext context, ExitStep step) {\n        return new ExitCommand(step);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/compiler/ExpressionCompiler.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.compiler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.Expression;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.ExpressionCommand;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.LogSegmentScopeCommand;\nimport com.walmartlabs.concord.svm.Command;\n\nimport javax.inject.Named;\nimport java.util.UUID;\n\n@Named\npublic class ExpressionCompiler implements StepCompiler<Expression> {\n\n    @Override\n    public boolean accepts(Step step) {\n        return step instanceof Expression;\n    }\n\n    @Override\n    public Command compile(CompilerContext context, Expression step) {\n        UUID correlationId = UUID.randomUUID();\n\n        return new LogSegmentScopeCommand<>(correlationId, new ExpressionCommand(correlationId, step), step);\n\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/compiler/FlowCallCompiler.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.compiler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.*;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.*;\nimport com.walmartlabs.concord.svm.Command;\n\nimport javax.inject.Named;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.UUID;\n\n@Named\npublic class FlowCallCompiler implements StepCompiler<FlowCall> {\n\n    @Override\n    public boolean accepts(Step step) {\n        return step instanceof FlowCall;\n    }\n\n    @Override\n    public Command compile(CompilerContext context, FlowCall step) {\n        UUID correlationId = UUID.randomUUID();\n\n        Command cmd = new LogSegmentScopeCommand<>(correlationId, new FlowCallCommand(correlationId, step), step);\n\n        FlowCallOptions options = Objects.requireNonNull(step.getOptions());\n\n        Retry retry = options.retry();\n        if (retry != null) {\n            cmd = new RetryWrapper(cmd, retry, step);\n        }\n\n        WithItems withItems = options.withItems();\n        if (withItems != null) {\n            cmd = WithItemsWrapper.of(cmd, withItems, options.out(), options.outExpr() != null && !options.outExpr().isEmpty() ? options.outExpr() : options.outMapping(), step);\n        }\n\n        Loop loop = options.loop();\n        if (loop != null) {\n            cmd = LoopWrapper.of(context, cmd, loop, options.out(), options.outExpr() != null && !options.outExpr().isEmpty() ? options.outExpr() : options.outMapping(), options.outExpression(), step);\n        }\n\n        List<Step> errorSteps = options.errorSteps();\n        if (!errorSteps.isEmpty()) {\n            cmd = new ErrorWrapper(cmd, CompilerUtils.compile(context, errorSteps));\n        }\n\n        return cmd;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/compiler/FormCallCompiler.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.compiler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.FormCall;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.FormCallCommand;\nimport com.walmartlabs.concord.svm.Command;\n\nimport javax.inject.Named;\n\n@Named\npublic class FormCallCompiler implements StepCompiler<FormCall> {\n\n    @Override\n    public boolean accepts(Step step) {\n        return step instanceof FormCall;\n    }\n\n    @Override\n    public Command compile(CompilerContext context, FormCall step) {\n        return new FormCallCommand(step);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/compiler/GroupOfStepsCompiler.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.compiler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.*;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.BlockCommand;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.ErrorWrapper;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.LoopWrapper;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.WithItemsWrapper;\nimport com.walmartlabs.concord.svm.Command;\n\nimport javax.inject.Named;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.stream.Collectors;\n\n@Named\npublic final class GroupOfStepsCompiler implements StepCompiler<GroupOfSteps> {\n\n    @Override\n    public boolean accepts(Step step) {\n        return step instanceof GroupOfSteps;\n    }\n\n    @Override\n    public Command compile(CompilerContext context, GroupOfSteps step) {\n        Command cmd = compile(context, step.getSteps());\n\n        GroupOfStepsOptions options = Objects.requireNonNull(step.getOptions());\n        WithItems withItems = options.withItems();\n        if (withItems != null) {\n            return WithItemsWrapper.of(cmd, withItems, options.out(), Collections.emptyMap(), step);\n        }\n\n        Loop loop = options.loop();\n        if (loop != null) {\n            cmd = LoopWrapper.of(context, cmd, loop, options.out(), Collections.emptyMap(), null, step);\n        }\n\n        List<Step> errorSteps = options.errorSteps();\n        if (!options.errorSteps().isEmpty()) {\n            cmd = new ErrorWrapper(cmd, compile(context, errorSteps));\n        }\n\n        return cmd;\n    }\n\n    private static BlockCommand compile(CompilerContext context, List<Step> steps) {\n        if (steps == null) {\n            return null;\n        }\n\n        return new BlockCommand(steps.stream()\n                .map(s -> context.compiler().compile(context.processDefinition(), s))\n                .collect(Collectors.toList()));\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/compiler/IfCompiler.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.compiler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.IfStep;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.BlockCommand;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.IfCommand;\nimport com.walmartlabs.concord.svm.Command;\n\nimport javax.inject.Named;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n@Named\npublic class IfCompiler implements StepCompiler<IfStep> {\n\n    @Override\n    public boolean accepts(Step step) {\n        return step instanceof IfStep;\n    }\n\n    @Override\n    public Command compile(CompilerContext context, IfStep step) {\n        Command thenCommand = compile(context, step.getThenSteps());\n        Command elseCommand = compile(context, step.getElseSteps());\n        return new IfCommand(step, thenCommand, elseCommand);\n    }\n\n    private static Command compile(CompilerContext context, List<Step> steps) {\n        if (steps == null) {\n            return null;\n        }\n\n        return new BlockCommand(steps.stream()\n                .map(s -> context.compiler().compile(context.processDefinition(), s))\n                .collect(Collectors.toList()));\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/compiler/ParallelStepCompiler.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.compiler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.ParallelBlock;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.ParallelCommand;\nimport com.walmartlabs.concord.svm.Command;\n\nimport javax.inject.Named;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n@Named\npublic class ParallelStepCompiler implements StepCompiler<ParallelBlock> {\n\n    @Override\n    public boolean accepts(Step step) {\n        return step instanceof ParallelBlock;\n    }\n\n    @Override\n    public Command compile(CompilerContext context, ParallelBlock step) {\n        List<Command> steps = step.getSteps().stream()\n                .map(s -> context.compiler().compile(context.processDefinition(), s))\n                .collect(Collectors.toList());\n\n        return new ParallelCommand(step, steps);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/compiler/ReturnCompiler.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.compiler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.ReturnStep;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.ReturnCommand;\nimport com.walmartlabs.concord.svm.Command;\n\nimport javax.inject.Named;\n\n@Named\npublic final class ReturnCompiler implements StepCompiler<ReturnStep> {\n\n    @Override\n    public boolean accepts(Step step) {\n        return step instanceof ReturnStep;\n    }\n\n    @Override\n    public Command compile(CompilerContext context, ReturnStep step) {\n        return new ReturnCommand(step);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/compiler/ScriptCallCompiler.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.compiler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.*;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.*;\nimport com.walmartlabs.concord.svm.Command;\n\nimport javax.inject.Named;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.UUID;\n\n@Named\npublic final class ScriptCallCompiler implements StepCompiler<ScriptCall> {\n\n    @Override\n    public boolean accepts(Step step) {\n        return step instanceof ScriptCall;\n    }\n\n    @Override\n    public Command compile(CompilerContext context, ScriptCall step) {\n        UUID correlationId = UUID.randomUUID();\n        Command cmd = new LogSegmentScopeCommand<>(correlationId, new ScriptCallCommand(correlationId, step), step);\n\n        ScriptCallOptions options = Objects.requireNonNull(step.getOptions());\n\n        Retry retry = options.retry();\n        if (retry != null) {\n            cmd = new RetryWrapper(cmd, retry, step);\n        }\n\n        WithItems withItems = options.withItems();\n        if (withItems != null) {\n            cmd = WithItemsWrapper.of(cmd, withItems, Collections.emptyList(), Collections.emptyMap(), step);\n        }\n\n        Loop loop = options.loop();\n        if (loop != null) {\n            cmd = LoopWrapper.of(context, cmd, loop, Collections.emptyList(), Collections.emptyMap(), null, step);\n        }\n\n        List<Step> errorSteps = options.errorSteps();\n        if (!errorSteps.isEmpty()) {\n            cmd = new ErrorWrapper(cmd, CompilerUtils.compile(context, errorSteps));\n        }\n\n        return cmd;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/compiler/SetVariablesCompiler.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.compiler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.SetVariablesStep;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.SetVariablesCommand;\nimport com.walmartlabs.concord.svm.Command;\n\nimport javax.inject.Named;\n\n@Named\npublic class SetVariablesCompiler implements StepCompiler<SetVariablesStep> {\n\n    @Override\n    public boolean accepts(Step step) {\n        return step instanceof SetVariablesStep;\n    }\n\n    @Override\n    public Command compile(CompilerContext context, SetVariablesStep step) {\n        return new SetVariablesCommand(step);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/compiler/StepCompiler.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.compiler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.svm.Command;\n\n/**\n * Compiles an individual flow step into a single VM {@link Command}.\n */\npublic interface StepCompiler<T extends Step> {\n\n    boolean accepts(Step step);\n\n    Command compile(CompilerContext context, T step);\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/compiler/SuspendCompiler.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.compiler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.model.SuspendStep;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.SuspendStepCommand;\nimport com.walmartlabs.concord.svm.Command;\n\nimport javax.inject.Named;\n\n@Named\npublic class SuspendCompiler implements StepCompiler<SuspendStep> {\n\n    @Override\n    public boolean accepts(Step step) {\n        return step instanceof SuspendStep;\n    }\n\n    @Override\n    public Command compile(CompilerContext context, SuspendStep step) {\n        return new SuspendStepCommand(step);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/compiler/SwitchCompiler.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.compiler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.model.SwitchStep;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.BlockCommand;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.SwitchCommand;\nimport com.walmartlabs.concord.svm.Command;\n\nimport javax.inject.Named;\nimport java.util.AbstractMap;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\n@Named\npublic class SwitchCompiler implements StepCompiler<SwitchStep> {\n\n    @Override\n    public boolean accepts(Step step) {\n        return step instanceof SwitchStep;\n    }\n\n    @Override\n    public Command compile(CompilerContext context, SwitchStep step) {\n        List<Map.Entry<String, Command>> caseCommands = new ArrayList<>();\n        for (Map.Entry<String, List<Step>> kv : step.getCaseSteps()) {\n            caseCommands.add(new AbstractMap.SimpleEntry<>(kv.getKey(), compile(context, kv.getValue())));\n        }\n        Command defaultCommand = compile(context, step.getDefaultSteps());\n        return new SwitchCommand(step, caseCommands, defaultCommand);\n    }\n\n    private static Command compile(CompilerContext context, List<Step> steps) {\n        if (steps == null) {\n            return null;\n        }\n\n        return new BlockCommand(steps.stream()\n                .map(s -> context.compiler().compile(context.processDefinition(), s))\n                .collect(Collectors.toList()));\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/compiler/TaskCallCompiler.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.compiler;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.*;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.*;\nimport com.walmartlabs.concord.svm.Command;\n\nimport javax.inject.Named;\nimport java.util.*;\n\n@Named\npublic final class TaskCallCompiler implements StepCompiler<TaskCall> {\n\n    @Override\n    public boolean accepts(Step step) {\n        return step instanceof TaskCall;\n    }\n\n    @Override\n    public Command compile(CompilerContext context, TaskCall step) {\n        UUID correlationId = UUID.randomUUID();\n        Command cmd = new LogSegmentScopeCommand<>(correlationId, new TaskCallCommand(correlationId, step), step);\n\n        TaskCallOptions options = Objects.requireNonNull(step.getOptions());\n\n        Retry retry = options.retry();\n        if (retry != null) {\n            cmd = new RetryWrapper(cmd, retry, step);\n        }\n\n        WithItems withItems = options.withItems();\n        if (withItems != null) {\n            Collection<String> out = Collections.emptyList();\n            if (options.out() != null) {\n                out = Collections.singletonList(options.out());\n            }\n            cmd = WithItemsWrapper.of(cmd, withItems, out, options.outExpr(), step);\n        }\n\n        Loop loop = options.loop();\n        if (loop != null) {\n            Collection<String> out = Collections.emptyList();\n            if (options.out() != null) {\n                out = Collections.singletonList(options.out());\n            }\n            cmd = LoopWrapper.of(context, cmd, loop, out, options.outExpr(), null, step);\n        }\n\n        List<Step> errorSteps = options.errorSteps();\n        if (!errorSteps.isEmpty()) {\n            cmd = new ErrorWrapper(cmd, CompilerUtils.compile(context, errorSteps));\n        }\n\n        return cmd;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/context/ContextFactory.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.context;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.State;\nimport com.walmartlabs.concord.svm.ThreadId;\n\nimport java.util.UUID;\n\npublic interface ContextFactory {\n\n    /**\n     * Creates new {@link Context} instance with frame-local overrides for global variables.\n     */\n    Context create(Runtime runtime, State state, ThreadId currentThreadId, Step currentStep);\n\n    Context create(Runtime runtime, State state, ThreadId currentThreadId, Step currentStep, UUID correlationId);\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/context/ContextImpl.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.context;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.ProcessDefinitionUtils;\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.model.TaskCall;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.SuspendCommand;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.TaskSuspendCommand;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\nimport com.walmartlabs.concord.runtime.v2.sdk.Compiler;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.State;\nimport com.walmartlabs.concord.svm.ThreadId;\n\nimport java.io.Serializable;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic class ContextImpl implements Context {\n\n    private final Compiler compiler;\n    private final ExpressionEvaluator expressionEvaluator;\n    private final ThreadId currentThreadId;\n    private final Runtime runtime;\n    private final State state;\n    private final ProcessDefinition processDefinition;\n    private final Step currentStep;\n    private final UUID correlationId;\n    private final Path workingDir;\n    private final UUID processInstanceId;\n    private final Variables variables;\n    private final FileService fileService;\n    private final DockerService dockerService;\n    private final SecretService secretService;\n    private final LockService lockService;\n    private final ApiConfiguration apiConfiguration;\n    private final ProcessConfiguration processConfiguration;\n\n    public ContextImpl(Compiler compiler,\n                       ExpressionEvaluator expressionEvaluator,\n                       ThreadId currentThreadId,\n                       Runtime runtime,\n                       State state,\n                       ProcessDefinition processDefinition,\n                       Step currentStep,\n                       UUID correlationId,\n                       Path workingDir,\n                       UUID processInstanceId,\n                       FileService fileService,\n                       DockerService dockerService,\n                       SecretService secretService,\n                       LockService lockService,\n                       ApiConfiguration apiConfiguration,\n                       ProcessConfiguration processConfiguration) {\n\n        this.compiler = compiler;\n        this.expressionEvaluator = expressionEvaluator;\n        this.currentThreadId = currentThreadId;\n        this.runtime = runtime;\n        this.state = state;\n        this.processDefinition = processDefinition;\n        this.currentStep = currentStep;\n        this.correlationId = correlationId;\n        this.workingDir = workingDir;\n        this.processInstanceId = processInstanceId;\n        this.variables = new ContextVariables(this);\n        this.fileService = fileService;\n        this.dockerService = dockerService;\n        this.secretService = secretService;\n        this.lockService = lockService;\n        this.apiConfiguration = apiConfiguration;\n        this.processConfiguration = processConfiguration;\n    }\n\n    @Override\n    public Path workingDirectory() {\n        return workingDir;\n    }\n\n    @Override\n    public UUID processInstanceId() {\n        return processInstanceId;\n    }\n\n    @Override\n    public Variables variables() {\n        return variables;\n    }\n\n    @Override\n    public Variables defaultVariables() {\n        return new MapBackedVariables(Collections.emptyMap());\n    }\n\n    @Override\n    public FileService fileService() {\n        return fileService;\n    }\n\n    @Override\n    public DockerService dockerService() {\n        return dockerService;\n    }\n\n    @Override\n    public SecretService secretService() {\n        return secretService;\n    }\n\n    @Override\n    public LockService lockService() {\n        return lockService;\n    }\n\n    @Override\n    public ApiConfiguration apiConfiguration() {\n        return apiConfiguration;\n    }\n\n    @Override\n    public ProcessConfiguration processConfiguration() {\n        return processConfiguration;\n    }\n\n    @Override\n    public Execution execution() {\n        return new Execution() {\n            @Override\n            public ThreadId currentThreadId() {\n                return currentThreadId;\n            }\n\n            @Override\n            public Runtime runtime() {\n                return runtime;\n            }\n\n            @Override\n            public State state() {\n                return state;\n            }\n\n            @Override\n            public ProcessDefinition processDefinition() {\n                return processDefinition;\n            }\n\n            @Override\n            public Step currentStep() {\n                return currentStep;\n            }\n\n            @Override\n            public String currentFlowName() {\n                return ProcessDefinitionUtils.getCurrentFlowName(processDefinition, currentStep);\n            }\n\n            @Override\n            public UUID correlationId() {\n                return correlationId;\n            }\n        };\n    }\n\n    @Override\n    public Compiler compiler() {\n        return compiler;\n    }\n\n    @Override\n    public <T> T eval(Object v, Class<T> type) {\n        return expressionEvaluator.eval(runtime.getService(EvalContextFactory.class).global(this), v, type);\n    }\n\n    @Override\n    public <T> T eval(Object v, Map<String, Object> additionalVariables, Class<T> type) {\n        return expressionEvaluator.eval(runtime.getService(EvalContextFactory.class).global(this, additionalVariables), v, type);\n    }\n\n    @Override\n    public void suspend(String eventName) {\n        state.peekFrame(currentThreadId).push(new SuspendCommand(eventName));\n    }\n\n    @Override\n    public void reentrantSuspend(String eventName, Map<String, Serializable> taskState) {\n        Step step = execution().currentStep();\n        if (!(step instanceof TaskCall)) {\n            throw new IllegalStateException(\"Calling 'suspendResume' is allowed only in task calls. Current step: \" + (step != null ? step.getClass() : \"n/a\"));\n        }\n\n        state.peekFrame(currentThreadId)\n                .push(new TaskSuspendCommand(correlationId, eventName, (TaskCall) step, taskState));\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/context/ContextVariables.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.context;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskProviders;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.VMUtils;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\nimport com.walmartlabs.concord.svm.State;\nimport com.walmartlabs.concord.svm.ThreadId;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Map;\n\npublic class ContextVariables implements Variables {\n\n    private static final Logger log = LoggerFactory.getLogger(ContextVariables.class);\n\n    private final Context ctx;\n\n    public ContextVariables(Context ctx) {\n        this.ctx = ctx;\n    }\n\n    @Override\n    public Object get(String key) {\n        State state = ctx.execution().state();\n        ThreadId threadId = ctx.execution().currentThreadId();\n        return VMUtils.getCombinedLocal(state, threadId, key);\n    }\n\n    @Override\n    public boolean has(String key) {\n        State state = ctx.execution().state();\n        ThreadId threadId = ctx.execution().currentThreadId();\n        return VMUtils.hasCombinedLocal(state, threadId, key);\n    }\n\n    @Override\n    public void set(String key, Object value) {\n        TaskProviders providers = ctx.execution().runtime().getService(TaskProviders.class);\n        if (providers.hasTask(key)) {\n            log.warn(\"Local variable '{}' shadows a task. This may cause issues calling '{}' task in expressions. \" +\n                    \"Avoid using same names for tasks and variables.\", key, key);\n        }\n\n        ThreadId threadId = ctx.execution().currentThreadId();\n        State state = ctx.execution().state();\n        VMUtils.putLocal(state, threadId, key, value);\n    }\n\n    @Override\n    public Map<String, Object> toMap() {\n        return VMUtils.getCombinedLocals(ctx);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/context/ContextVariablesWithOverrides.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.context;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.runner.context.ContextVariables;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class ContextVariablesWithOverrides implements Variables {\n\n    private final Variables contextVariables;\n    private final Variables overrides;\n\n    public ContextVariablesWithOverrides(Context context, Map<String, Object> overrides) {\n        this.contextVariables = new ContextVariables(context);\n        this.overrides = new MapBackedVariables(overrides);\n    }\n\n    @Override\n    public Object get(String key) {\n        if (overrides.has(key)) {\n            return overrides.get(key);\n        }\n        return contextVariables.get(key);\n    }\n\n    @Override\n    public void set(String key, Object value) {\n        throw new IllegalStateException(\"Not supported\");\n    }\n\n    @Override\n    public boolean has(String key) {\n        return overrides.has(key) || contextVariables.has(key);\n    }\n\n    @Override\n    public Map<String, Object> toMap() {\n        Map<String, Object> a = contextVariables.toMap();\n        Map<String, Object> b = overrides.toMap();\n        Map<String, Object> result = new HashMap<>(a);\n        result.putAll(b);\n        return result;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/context/DefaultContextFactory.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.context;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.common.injector.InstanceId;\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.sdk.Compiler;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.State;\nimport com.walmartlabs.concord.svm.ThreadId;\n\nimport javax.inject.Inject;\nimport java.util.UUID;\n\npublic class DefaultContextFactory implements ContextFactory {\n\n    private final WorkingDirectory workingDirectory;\n    private final InstanceId processInstanceId;\n    private final ProcessConfiguration processConfiguration;\n    private final FileService fileService;\n    private final DockerService dockerService;\n    private final SecretService secretService;\n    private final LockService lockService;\n    private final ApiConfiguration apiConfiguration;\n\n    @Inject\n    public DefaultContextFactory(WorkingDirectory workingDirectory,\n                                 InstanceId processInstanceId,\n                                 ProcessConfiguration processConfiguration,\n                                 FileService fileService,\n                                 DockerService dockerService,\n                                 SecretService secretService,\n                                 LockService lockService,\n                                 ApiConfiguration apiConfiguration) {\n\n        this.workingDirectory = workingDirectory;\n        this.processInstanceId = processInstanceId;\n        this.processConfiguration = processConfiguration;\n        this.fileService = fileService;\n        this.dockerService = dockerService;\n        this.secretService = secretService;\n        this.lockService = lockService;\n        this.apiConfiguration = apiConfiguration;\n    }\n\n    @Override\n    public Context create(Runtime runtime, State state, ThreadId currentThreadId, Step currentStep) {\n        return create(runtime, state, currentThreadId, currentStep, null);\n    }\n\n    @Override\n    public Context create(Runtime runtime, State state, ThreadId currentThreadId, Step currentStep, UUID correlationId) {\n        ProcessDefinition pd = runtime.getService(ProcessDefinition.class);\n\n        Compiler compiler = runtime.getService(Compiler.class);\n        ExpressionEvaluator ee = runtime.getService(ExpressionEvaluator.class);\n\n        return new ContextImpl(compiler, ee, currentThreadId, runtime, state, pd, currentStep, correlationId, workingDirectory.getValue(),\n                processInstanceId.getValue(), fileService, dockerService, secretService, lockService,\n                apiConfiguration, processConfiguration);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/context/ResumeEventImpl.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.context;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.annotation.JsonPropertyOrder;\nimport com.walmartlabs.concord.runtime.v2.sdk.ResumeEvent;\n\nimport java.io.Serializable;\nimport java.util.Map;\n\n@JsonPropertyOrder({\"eventName\", \"state\"})\npublic class ResumeEventImpl implements ResumeEvent {\n\n    // for backward compatibility (java8 concord 1.92.0 version)\n    private static final long serialVersionUID = -8446483662158789554L;\n\n    private final String eventName;\n\n    private final Map<String, Serializable> state;\n\n    public ResumeEventImpl(String eventName, Map<String, Serializable> state) {\n        this.eventName = eventName;\n        this.state = state;\n    }\n\n    @Override\n    @JsonProperty(\"eventName\")\n    public String eventName() {\n        return eventName;\n    }\n\n    @Override\n    @JsonProperty(\"state\")\n    public Map<String, Serializable> state() {\n        return state;\n    }\n\n    @Override\n    public String toString() {\n        return \"ResumeEventImpl{\" +\n                \"eventName='\" + eventName + '\\'' +\n                \", state=\" + state +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/context/TaskContext.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.context;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Compiler;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\n\nimport java.io.Serializable;\nimport java.nio.file.Path;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic class TaskContext implements Context {\n\n    private final Context delegate;\n    private final Variables defaultVariables;\n\n    public TaskContext(Context delegate, Variables defaultVariables) {\n        this.delegate = delegate;\n        this.defaultVariables = defaultVariables;\n    }\n\n    @Override\n    public Path workingDirectory() {\n        return delegate.workingDirectory();\n    }\n\n    @Override\n    public UUID processInstanceId() {\n        return delegate.processInstanceId();\n    }\n\n    @Override\n    public Variables variables() {\n        return delegate.variables();\n    }\n\n    @Override\n    public Variables defaultVariables() {\n        return defaultVariables;\n    }\n\n    @Override\n    public FileService fileService() {\n        return delegate.fileService();\n    }\n\n    @Override\n    public DockerService dockerService() {\n        return delegate.dockerService();\n    }\n\n    @Override\n    public SecretService secretService() {\n        return delegate.secretService();\n    }\n\n    @Override\n    public LockService lockService() {\n        return delegate.lockService();\n    }\n\n    @Override\n    public ApiConfiguration apiConfiguration() {\n        return delegate.apiConfiguration();\n    }\n\n    @Override\n    public ProcessConfiguration processConfiguration() {\n        return delegate.processConfiguration();\n    }\n\n    @Override\n    public Execution execution() {\n        return delegate.execution();\n    }\n\n    @Override\n    public Compiler compiler() {\n        return delegate.compiler();\n    }\n\n    @Override\n    public <T> T eval(Object v, Class<T> type) {\n        return delegate.eval(v, type);\n    }\n\n    @Override\n    public <T> T eval(Object v, Map<String, Object> additionalVariables, Class<T> type) {\n        return delegate.eval(v, additionalVariables, type);\n    }\n\n    @Override\n    public void suspend(String eventName) {\n        delegate.suspend(eventName);\n    }\n\n    @Override\n    public void reentrantSuspend(String eventName, Map<String, Serializable> payload) {\n        delegate.reentrantSuspend(eventName, payload);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/DefaultExpressionEvaluator.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.runner.el.resolvers.SensitiveDataProcessor;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskProviders;\nimport com.walmartlabs.concord.runtime.v2.sdk.CustomBeanMethodResolver;\nimport com.walmartlabs.concord.runtime.v2.sdk.CustomTaskMethodResolver;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContext;\nimport com.walmartlabs.concord.runtime.v2.sdk.ExpressionEvaluator;\n\nimport javax.inject.Inject;\nimport java.util.*;\n\npublic class DefaultExpressionEvaluator implements ExpressionEvaluator {\n\n    private final LazyExpressionEvaluator delegate;\n\n    @Inject\n    public DefaultExpressionEvaluator(TaskProviders taskProviders,\n                                      FunctionHolder functionHolder,\n                                      List<CustomTaskMethodResolver> taskMethodResolvers,\n                                      List<CustomBeanMethodResolver> beanMethodResolvers,\n                                      SensitiveDataProcessor sensitiveDataProcessor) {\n        this.delegate = new LazyExpressionEvaluator(taskProviders, functionHolder, taskMethodResolvers, beanMethodResolvers, sensitiveDataProcessor);\n    }\n\n    @Override\n    public <T> T eval(EvalContext ctx, Object value, Class<T> expectedType) {\n        Object result = delegate.eval(ctx, value, Object.class);\n        return initializeAll(result, expectedType);\n    }\n\n    private static <T> T initializeAll(Object value, Class<T> expectedType) {\n        if (value instanceof Map) {\n            Map<?, ?> m = (Map<?, ?>) value;\n            return expectedType.cast(initializeMap(m));\n        } else if (value instanceof Set) {\n            Set<?> set = (Set<?>) value;\n            if (set.isEmpty()) {\n                return expectedType.cast(new LinkedHashSet<>());\n            }\n            return expectedType.cast(initializeSet(set));\n        } else if (value instanceof Collection) {\n            Collection<?> collection = (Collection<?>) value;\n            if (collection.isEmpty()) {\n                return expectedType.cast(new ArrayList<>());\n            }\n            return expectedType.cast(initializeList(collection));\n        } else if (value != null && value.getClass().isArray()) {\n            Object[] arr = (Object[])value;\n            if (arr.length == 0) {\n                return expectedType.cast(arr);\n            }\n            return expectedType.cast(initializeArray(arr));\n        }\n        return expectedType.cast(value);\n    }\n\n    private static Object[] initializeArray(Object[] arr) {\n        for (int i = 0; i < arr.length; i++) {\n            arr[i] = initializeAll(arr[i], Object.class);\n        }\n        return arr;\n    }\n\n    private static Map<Object, Object> initializeMap(Map<?, ?> value) {\n        Map<Object, Object> result = new LinkedHashMap<>(value.size());\n        for (Map.Entry<?, ?> e : value.entrySet()) {\n            Object kk = e.getKey();\n            kk = initializeAll(kk, Object.class);\n\n            Object vv = e.getValue();\n            vv = initializeAll(vv, Object.class);\n\n            result.put(kk, vv);\n        }\n        return result;\n    }\n\n    private static List<Object> initializeList(Collection<?> value) {\n        List<Object> result = new ArrayList<>(value.size());\n        for (Object o : value) {\n            result.add(initializeAll(o, Object.class));\n        }\n        return result;\n    }\n\n    private static Set<Object> initializeSet(Collection<?> value) {\n        Set<Object> result = new LinkedHashSet<>(value.size());\n        for (Object o : value) {\n            result.add(initializeAll(o, Object.class));\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/EvalContextFactoryImpl.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.runner.context.ContextVariables;\nimport com.walmartlabs.concord.runtime.v2.runner.context.ContextVariablesWithOverrides;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContext;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContextFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables;\n\nimport java.util.Map;\n\npublic class EvalContextFactoryImpl implements EvalContextFactory {\n\n    @Override\n    public EvalContext scope(Context ctx) {\n        return EvalContext.builder()\n                .context(ctx)\n                .variables(new ContextVariables(ctx))\n                .useIntermediateResults(true)\n                .build();\n    }\n\n    @Override\n    public EvalContext global(Context ctx) {\n        return EvalContext.builder()\n                .context(ctx)\n                .variables(new ContextVariables(ctx))\n                .build();\n    }\n\n    @Override\n    public EvalContext global(Context ctx, Map<String, Object> additionalVariables) {\n        return EvalContext.builder()\n                .context(ctx)\n                .variables(new ContextVariablesWithOverrides(ctx, additionalVariables))\n                .build();\n    }\n\n    @Override\n    public EvalContext strict(Context ctx, Map<String, Object> variables) {\n        return EvalContext.builder()\n                .context(ctx)\n                .variables(new MapBackedVariables(variables))\n                .build();\n    }\n\n    public EvalContext strict(Map<String, Object> variables) {\n        return EvalContext.builder()\n                .variables(new MapBackedVariables(variables))\n                .build();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/FunctionHolder.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.lang.reflect.Method;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentHashMap;\n\npublic class FunctionHolder {\n\n    private final Map<String, Method> functions = new ConcurrentHashMap<>();\n\n    public FunctionHolder register(String name, Method method) {\n        functions.put(name, method);\n        return this;\n    }\n\n    public Method resolve(String name) {\n        return functions.get(name);\n    }\n\n    public Set<String> names() {\n        return Set.copyOf(functions.keySet());\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/LazyEvalContext.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContext;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface LazyEvalContext extends EvalContext {\n\n    @Nullable\n    LazyEvalMap scope();\n\n    static LazyEvalContext of(EvalContext ctx, LazyEvalMap scope) {\n        return ImmutableLazyEvalContext.builder().from(ctx)\n                .scope(scope)\n                .build();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/LazyEvalList.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.*;\n\npublic class LazyEvalList extends AbstractList<Object> {\n\n    private final LazyExpressionEvaluator evaluator;\n    private final LazyEvalContext context;\n    private final Set<Integer> inflightKeys = new HashSet<>();\n    private final List<?> originalValues;\n    private final Map<Integer, Object> evaluatedValues = new HashMap<>();\n\n    public LazyEvalList(LazyExpressionEvaluator evaluator, LazyEvalContext context, List<?> src) {\n        this.evaluator = evaluator;\n        this.context = context;\n        this.originalValues = src;\n    }\n\n    @Override\n    public Object get(int index) {\n        return evalValue(index);\n    }\n\n    @Override\n    public int size() {\n        return originalValues.size();\n    }\n\n    private Object evalValue(int index) {\n        if (evaluatedValues.containsKey(index)) {\n            return evaluatedValues.get(index);\n        }\n\n        try {\n            boolean newKey = inflightKeys.add(index);\n            if (!newKey) {\n                throw new RuntimeException(\"Element with index='\" + index + \"' already in evaluation\");\n            }\n\n            Object originalValue = originalValues.get(index);\n\n            Object result = evaluator.evalValue(context, originalValue, Object.class);\n\n            evaluatedValues.put(index, result);\n\n            return result;\n        } finally {\n            inflightKeys.remove(index);\n        }\n    }\n\n    @Override\n    public String toString() {\n        return \"LazyEvalList{\" +\n                \"inflightKeys=\" + inflightKeys +\n                \", originalValues=\" + originalValues +\n                \", evaluatedValues=\" + evaluatedValues +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/LazyEvalMap.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContext;\n\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic class LazyEvalMap implements Map<String, Object> {\n\n    private final LazyExpressionEvaluator evaluator;\n    private final LazyEvalContext evalContext;\n\n    private final LinkedHashSet<String> orderedKeys;\n    private final Set<Object> inflightKeys = new HashSet<>();\n    private final Map<String, Object> originalValues;\n    private final Map<String, Object> evaluatedValues = new LinkedHashMap<>();\n\n    public LazyEvalMap(LazyExpressionEvaluator evaluator,\n                       LazyEvalContext evalContext,\n                       Map<String, Object> nonEvaluatedItems) {\n        this.evaluator = evaluator;\n        this.evalContext = evalContext;\n        this.orderedKeys = new LinkedHashSet<>(nonEvaluatedItems.keySet());\n        this.originalValues = nonEvaluatedItems;\n    }\n\n    public LazyEvalMap(LazyExpressionEvaluator evaluator,\n                       Map<String, Object> nonEvaluatedItems,\n                       EvalContext context) {\n        this.evaluator = evaluator;\n        this.evalContext = LazyEvalContext.of(context, this);\n        this.orderedKeys = new LinkedHashSet<>(nonEvaluatedItems.keySet());\n        this.originalValues = nonEvaluatedItems;\n    }\n\n    @Override\n    public int size() {\n        return orderedKeys.size();\n    }\n\n    @Override\n    public boolean isEmpty() {\n        return orderedKeys.isEmpty();\n    }\n\n    @Override\n    public boolean containsKey(Object key) {\n        if (inflightKeys.contains(key)) {\n            return false;\n        }\n        return orderedKeys.contains(key);\n    }\n\n    @Override\n    public boolean containsValue(Object value) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public Object get(Object key) {\n        if (!(key instanceof String)) {\n            return null;\n        }\n        return evalValue((String) key);\n    }\n\n    @Override\n    public Object put(String key, Object value) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public Object remove(Object key) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public void putAll(Map<? extends String, ?> m) {\n        for (Map.Entry<? extends String, ?> e : m.entrySet()) {\n            put(e.getKey(), e.getValue());\n        }\n    }\n\n    @Override\n    public void clear() {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public Set<String> keySet() {\n        return orderedKeys;\n    }\n\n    @Override\n    public Collection<Object> values() {\n        return entrySet().stream().map(Entry::getValue).collect(Collectors.toList());\n    }\n\n    @Override\n    public Set<Entry<String, Object>> entrySet() {\n        Set<Entry<String, Object>> result = new LinkedHashSet<>();\n        orderedKeys.forEach(k -> result.add(new LazyEntry(k)));\n        return result;\n    }\n\n    private Object evalValue(String key) {\n        if (evaluatedValues.containsKey(key)) {\n            return evaluatedValues.get(key);\n        }\n\n        if (!originalValues.containsKey(key)) {\n            return null;\n        }\n\n        try {\n            boolean newKey = inflightKeys.add(key);\n            if (!newKey) {\n                throw new RuntimeException(\"Key '\" + key + \"' already in evaluation\");\n            }\n\n            Object originalValue = originalValues.get(key);\n            Object evaluatedValue = evaluator.evalValue(evalContext, originalValue, Object.class);\n\n            evaluatedValues.put(key, evaluatedValue);\n\n            return evaluatedValue;\n        } finally {\n            inflightKeys.remove(key);\n        }\n    }\n\n    class LazyEntry implements Entry<String, Object> {\n\n        private final String key;\n\n        LazyEntry(String key) {\n            this.key = key;\n        }\n\n        @Override\n        public String getKey() {\n            return key;\n        }\n\n        @Override\n        public Object getValue() {\n            return get(key);\n        }\n\n        @Override\n        public Object setValue(Object value) {\n            throw new UnsupportedOperationException();\n        }\n    }\n\n    @Override\n    public String toString() {\n        return \"LazyEvalMap{\" +\n                \"orderedKeys=\" + orderedKeys +\n                \", inflightKeys=\" + inflightKeys +\n                \", originalValues=\" + originalValues +\n                \", evaluatedValues=\" + evaluatedValues +\n                '}';\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/LazyExpressionEvaluator.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.common.ExceptionUtils;\nimport com.walmartlabs.concord.runtime.v2.runner.el.resolvers.*;\nimport com.walmartlabs.concord.runtime.v2.runner.el.resolvers.MapELResolver;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskProviders;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.WrappedException;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\n\nimport javax.el.*;\nimport java.lang.reflect.Method;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.common.ConfigurationUtils.*;\nimport static com.walmartlabs.concord.runtime.v2.runner.el.ThreadLocalEvalContext.withEvalContext;\n\n/**\n * Evaluates values. Allows partial evaluation of nested data.\n */\npublic class LazyExpressionEvaluator implements ExpressionEvaluator {\n\n    private final ExpressionFactory expressionFactory = ExpressionFactory.newInstance();\n    private final TaskProviders taskProviders;\n    private final FunctionMapper functionMapper;\n    private final List<CustomTaskMethodResolver> taskMethodResolvers;\n    private final List<CustomBeanMethodResolver> beanMethodResolvers;\n    private final SensitiveDataProcessor sensitiveDataProcessor;\n\n    public LazyExpressionEvaluator(TaskProviders taskProviders,\n                                   FunctionHolder functionHolder,\n                                   List<CustomTaskMethodResolver> taskMethodResolvers,\n                                   List<CustomBeanMethodResolver> beanMethodResolvers,\n                                   SensitiveDataProcessor sensitiveDataProcessor) {\n        this.taskProviders = taskProviders;\n        this.sensitiveDataProcessor = sensitiveDataProcessor;\n        this.functionMapper = new DelegatingFunctionMapper(functionHolder);\n        this.taskMethodResolvers = taskMethodResolvers;\n        this.beanMethodResolvers = beanMethodResolvers;\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public <T> T eval(EvalContext ctx, Object value, Class<T> expectedType) {\n        if (value == null) {\n            return null;\n        }\n\n        if (value instanceof Map) {\n            var m = nestedToMap((Map<String, Object>) value);\n            value = mergeWithVariables(ctx, m, ((Map<String, Object>) value).keySet().stream().filter(ConfigurationUtils::isNestedKey).collect(Collectors.toSet()));\n        }\n\n        if (ctx.useIntermediateResults() && value instanceof Map) {\n            var m = (Map<String, Object>) value;\n            if (m.isEmpty()) {\n                return expectedType.cast(m);\n            }\n\n            return expectedType.cast(new LazyEvalMap(this, m, ctx));\n        }\n\n        return evalValue(LazyEvalContext.of(ctx, null), value, expectedType);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    <T> T evalValue(LazyEvalContext ctx, Object value, Class<T> expectedType) {\n        if (value == null) {\n            return null;\n        }\n\n        if (value instanceof Map) {\n            var m = (Map<String, Object>) value;\n            return expectedType.cast(new LazyEvalMap(this, ctx, m));\n        } else if (value instanceof List<?> src) {\n            return expectedType.cast(new LazyEvalList(this, ctx, src));\n        } else if (value instanceof Set<?> src) {\n            // use LinkedHashSet to preserve the order of keys\n            var dst = new LinkedHashSet<>(src.size());\n            for (var vv : src) {\n                dst.add(evalValue(ctx, vv, Object.class));\n            }\n\n            return expectedType.cast(dst);\n        } else if (value.getClass().isArray()) {\n            var src = (Object[]) value;\n            if (src.length == 0) {\n                return expectedType.cast(src);\n            }\n\n            for (int i = 0; i < src.length; i++) {\n                src[i] = evalValue(ctx, src[i], Object.class);\n            }\n\n            return expectedType.cast(src);\n        } else if (value instanceof String s) {\n            if (hasExpression(s)) {\n                return evalExpr(ctx, s, expectedType);\n            }\n        }\n\n        return expectedType.cast(value);\n    }\n\n    private <T> T evalExpr(LazyEvalContext ctx, String expr, Class<T> type) {\n        var resolver = createResolver(ctx, expressionFactory);\n\n        var sc = new StandardELContext(expressionFactory) {\n            @Override\n            public ELResolver getELResolver() {\n                return resolver;\n            }\n\n            @Override\n            public FunctionMapper getFunctionMapper() {\n                return functionMapper;\n            }\n        };\n        sc.putContext(ExpressionFactory.class, expressionFactory);\n\n        try {\n            var x = expressionFactory.createValueExpression(sc, expr, type);\n            var v = withEvalContext(ctx, () -> x.getValue(sc));\n            return type.cast(v);\n        } catch (PropertyNotFoundException e) {\n            if (ctx.undefinedVariableAsNull()) {\n                return null;\n            }\n\n            var errorMessage = propertyNameFromException(e)\n                    .map(propName -> String.format(\"Can't find a variable %s. \" +\n                            \"Check if it is defined in the current scope. Details: %s\", propName, e.getMessage()))\n                    .orElse(String.format(\"Can't find the specified variable. \" +\n                            \"Check if it is defined in the current scope. Details: %s\", e.getMessage()));\n\n            throw new UserDefinedException(exceptionPrefix(expr) + errorMessage);\n        } catch (MethodNotFoundException e) {\n            throw new UserDefinedException(exceptionPrefix(expr) + e.getMessage());\n        } catch (UserDefinedException e) {\n            throw e;\n        } catch (javax.el.ELException e) {\n            var lastElException = ExceptionUtils.findLastException(e, javax.el.ELException.class);\n            if (lastElException.getCause() instanceof UserDefinedException ue) {\n                throw ue;\n            } else if (e.getCause() instanceof com.sun.el.parser.ParseException pe) {\n                throw new UserDefinedException(\"while parsing expression '\" + expr + \"': \" + pe.getMessage());\n            } else if (lastElException.getCause() instanceof Exception ee) {\n                throw new WrappedException(exceptionPrefix(expr), ee);\n            }\n            throw lastElException;\n        } catch (Exception e) {\n            throw new WrappedException(exceptionPrefix(expr), e);\n        }\n    }\n\n    /**\n     * Based on the original code from {@link StandardELContext#getELResolver()}.\n     * Creates a {@link ELResolver} instance with \"sub-resolvers\" in the original order.\n     */\n    private ELResolver createResolver(LazyEvalContext evalContext,\n                                      ExpressionFactory expressionFactory) {\n\n        var r = new CompositeELResolver();\n        if (evalContext.scope() != null) {\n            r.add(new VariableResolver(evalContext.scope()));\n        }\n        r.add(new VariableResolver(evalContext.variables()));\n        if (evalContext.context() != null) {\n            r.add(new TaskResolver(evalContext.context(), taskProviders));\n        }\n        r.add(expressionFactory.getStreamELResolver());\n        r.add(new StaticFieldELResolver());\n        r.add(new MapELResolver(sensitiveDataProcessor));\n        r.add(new MethodAccessorResolver(sensitiveDataProcessor));\n        r.add(new ResourceBundleELResolver());\n        r.add(new ListELResolver());\n        r.add(new ArrayELResolver());\n        if (evalContext.context() != null) {\n            r.add(new TaskMethodResolver(taskMethodResolvers, evalContext.context(), sensitiveDataProcessor));\n        }\n        r.add(new CompositeBeanELResolver(taskMethodResolvers, beanMethodResolvers, sensitiveDataProcessor));\n        return r;\n    }\n\n    private static boolean hasExpression(String s) {\n        return s.contains(\"${\");\n    }\n\n    private static Map<String, Object> nestedToMap(Map<String, Object> value) {\n        Map<String, Object> result = new LinkedHashMap<String, Object>();\n        for (var e : value.entrySet()) {\n            if (isNestedKey(e.getKey())) {\n                var m = toNested(e.getKey(), e.getValue());\n                result = deepMerge(result, m);\n            } else {\n                result.put(e.getKey(), e.getValue());\n            }\n        }\n        return result;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Map<String, Object> mergeWithVariables(EvalContext ctx, Map<String, Object> m, Set<String> nestedKeys) {\n        var result = new LinkedHashMap<String, Object>();\n        for (var e : m.entrySet()) {\n            var key = e.getKey();\n            var value = e.getValue();\n            var isNested = nestedKeys.stream().anyMatch(s -> s.startsWith(key + \".\"));\n            if (isNested && ctx.variables().has(key)) {\n                var o = ctx.variables().get(key);\n                if (o instanceof Map && e.getValue() instanceof Map) {\n                    var valuesFromVars = (Map<String, Object>) o;\n                    value = deepMerge(valuesFromVars, (Map<String, Object>) value);\n                }\n            }\n            result.put(key, value);\n        }\n        return result;\n    }\n\n    private static String exceptionPrefix(String expr) {\n        return \"while evaluating expression '\" + expr + \"': \";\n    }\n\n    private static final String PROP_NOT_FOUND_EL_MESSAGE = \"ELResolver cannot handle a null base Object with identifier \";\n\n    private static Optional<String> propertyNameFromException(PropertyNotFoundException e) {\n        if (e.getMessage() == null) {\n            return Optional.empty();\n        }\n\n        if (e.getMessage().startsWith(PROP_NOT_FOUND_EL_MESSAGE)) {\n            return Optional.of(e.getMessage().substring(PROP_NOT_FOUND_EL_MESSAGE.length()));\n        }\n\n        return Optional.empty();\n    }\n\n    private static class DelegatingFunctionMapper extends FunctionMapper {\n        final FunctionHolder functionHolder;\n\n        DelegatingFunctionMapper(FunctionHolder functionHolder) {\n            this.functionHolder = functionHolder;\n        }\n\n        @Override\n        public Method resolveFunction(String prefix, String localName) {\n            if (prefix != null && !prefix.isBlank()) {\n                return functionHolder.resolve(prefix + \":\" + localName);\n            }\n            return functionHolder.resolve(localName);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/MethodNotFoundException.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.apache.commons.text.similarity.LevenshteinDistance;\n\nimport javax.el.ELException;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Modifier;\nimport java.util.Arrays;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\npublic class MethodNotFoundException extends ELException {\n\n    private static final long serialVersionUID = 1L;\n\n    private static final int MAX_HINTS = 3;\n\n    public MethodNotFoundException(Class<?> clazz, Object method, Class<?>[] paramTypes) {\n        super(formatMessage(clazz, method, paramTypes));\n    }\n\n    private static String formatMessage(Class<?> clazz, Object method, Class<?>[] paramTypes) {\n        Class<?> baseClass = getBaseClass(clazz);\n\n        String baseName = baseClass != null ? baseClass.getName() : \"n/a\";\n        String methodName = method != null ? method.toString() : \"n/a\";\n        String params = paramTypes != null ? Arrays.stream(paramTypes)\n                .map(Class::getName)\n                .collect(Collectors.joining(\", \")) : \"\";\n\n        StringBuilder b = new StringBuilder();\n        b.append(\"Can't find '\").append(methodName).append(\"(\").append(params).append(\")\")\n                .append(\"' method in \").append(baseName).append(\".\\n\")\n                .append(\"Check the task's or type's available methods and their signatures.\");\n\n        String hint = getMethodHint(baseClass, methodName);\n        if (hint != null) {\n            b.append(\"\\n\").append(hint);\n        }\n\n        return b.toString();\n    }\n\n    /**\n     * Returns the base's class.\n     * Tries to unwrap Guice proxy classes to get the original class.\n     */\n    private static Class<?> getBaseClass(Class<?> klass) {\n        if (klass == null) {\n            return null;\n        }\n\n        String name = klass.getName();\n        if (name.contains(\"EnhancerByGuice\")) {\n            return klass.getSuperclass();\n        }\n\n        return klass;\n    }\n\n    /**\n     * Creates a type hint, showing methods of the specified base class\n     * in order of their Levenshtein distance from the method name speficied by the user.\n     */\n    private static String getMethodHint(Class<?> baseClass, Object method) {\n        if (baseClass == null || method == null) {\n            return null;\n        }\n\n        String methodName = method.toString();\n\n        LevenshteinDistance distance = LevenshteinDistance.getDefaultInstance();\n        List<String> candidates = Arrays.stream(baseClass.getDeclaredMethods())\n                .filter(m -> Modifier.isPublic(m.getModifiers()))\n                .map(MethodNotFoundException::formatMethod)\n                .sorted(Comparator.comparingInt(m -> distance.apply(methodName, m)))\n                .limit(MAX_HINTS)\n                .distinct()\n                .collect(Collectors.toList());\n\n        if (candidates.isEmpty()) {\n            return null;\n        }\n\n        return \"Did you mean: \" + String.join(\", \", candidates) + \"?\";\n    }\n\n    private static String formatMethod(Method m) {\n        List<String> params = Arrays.stream(m.getParameterTypes())\n                .map(Class::getName)\n                .collect(Collectors.toList());\n\n        return String.format(\"%s(%s)\", m.getName(), String.join(\", \", params));\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/ThreadLocalEvalContext.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContext;\n\nimport java.util.concurrent.Callable;\n\npublic class ThreadLocalEvalContext {\n\n    public static <T> T withEvalContext(EvalContext ctx, Callable<T> callable) throws RuntimeException {\n        set(ctx);\n        try {\n            return callable.call();\n        } catch (RuntimeException e) {\n            throw e;\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        } finally {\n            clear();\n        }\n    }\n\n    private static final ThreadLocal<EvalContext> value = new ThreadLocal<>();\n\n    public static EvalContext get() {\n        return value.get();\n    }\n\n    private static void set(EvalContext ctx) {\n        value.set(ctx);\n    }\n\n    private static void clear() {\n        value.remove();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/AllVariablesFunction.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el.functions;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.ELFunction;\nimport com.walmartlabs.concord.runtime.v2.runner.el.ThreadLocalEvalContext;\n\nimport java.util.Map;\n\npublic final class AllVariablesFunction {\n\n    @ELFunction\n    public static Map<String, Object> allVariables() {\n        return ThreadLocalEvalContext.get()\n                .variables()\n                .toMap();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/CurrentFlowNameFunction.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el.functions;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.ELFunction;\nimport com.walmartlabs.concord.runtime.v2.runner.el.ThreadLocalEvalContext;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\n\npublic final class CurrentFlowNameFunction {\n\n    @ELFunction\n    public static String currentFlowName() {\n        Context ctx = ThreadLocalEvalContext.get().context();\n        if (ctx == null) {\n            return null;\n        }\n\n        return ctx.execution().currentFlowName();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/EvalAsMapFunction.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el.functions;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.ELFunction;\nimport com.walmartlabs.concord.runtime.v2.runner.el.ThreadLocalEvalContext;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContextFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.ExpressionEvaluator;\nimport com.walmartlabs.concord.svm.Runtime;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.common.ConfigurationUtils.deepMerge;\n\npublic final class EvalAsMapFunction {\n\n    @ELFunction\n    @SuppressWarnings(\"unchecked\")\n    public static Map<String, Object> evalAsMap(Object value) {\n        Context ctx = ThreadLocalEvalContext.get().context();\n        if (ctx == null) {\n            return null;\n        }\n\n        Runtime runtime = ctx.execution().runtime();\n        EvalContextFactory ecf = runtime.getService(EvalContextFactory.class);\n        ExpressionEvaluator ee = runtime.getService(ExpressionEvaluator.class);\n        Map<String, Object> evalValue = ee.evalAsMap(ecf.scope(ctx), value);\n\n        Map<String, Object> result = new HashMap<>();\n        evalValue.forEach((k, v) -> {\n            Object o = ctx.variables().get(k);\n            if (o instanceof Map && v instanceof Map) {\n                v = deepMerge((Map<String, Object>)o, (Map<String, Object>)v);\n            }\n            if (v != null) {\n                result.put(k, v);\n            }\n        });\n        return result;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/HasFlowFunction.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el.functions;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.v2.model.Profile;\nimport com.walmartlabs.concord.runtime.v2.sdk.ELFunction;\nimport com.walmartlabs.concord.runtime.v2.runner.el.ThreadLocalEvalContext;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\n\npublic final class HasFlowFunction {\n\n    @ELFunction\n    public static boolean hasFlow(String name) {\n        if (name == null || name.trim().isEmpty()) {\n            return false;\n        }\n\n        Context ctx = ThreadLocalEvalContext.get().context();\n        if (ctx == null) {\n            return false;\n        }\n\n        ProcessDefinition pd = ctx.execution().processDefinition();\n        if (pd.flows().containsKey(name)) {\n            return true;\n        }\n\n        for (String activeProfile : ctx.processConfiguration().processInfo().activeProfiles()) {\n            boolean fromProfile = pd.profiles().getOrDefault(activeProfile, Profile.builder().build()).flows().containsKey(name);\n            if (fromProfile) {\n                return true;\n            }\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/HasNonNullVariableFunction.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el.functions;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.runtime.v2.sdk.ELFunction;\nimport com.walmartlabs.concord.runtime.v2.runner.el.ThreadLocalEvalContext;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport java.util.Arrays;\nimport java.util.Map;\n\npublic final class HasNonNullVariableFunction {\n\n    @ELFunction\n    @SuppressWarnings(\"unchecked\")\n    public static boolean hasNonNullVariable(String name) {\n        if (name == null || name.trim().isEmpty()) {\n            return false;\n        }\n\n        Variables variables = ThreadLocalEvalContext.get().variables();\n\n        Object value = null;\n        String[] path = name.split(\"\\\\.\");\n        if (path.length == 1) {\n            value = variables.get(name);\n        } else {\n            Object maybeMap = variables.get(path[0]);\n            if (!(maybeMap instanceof Map)) {\n                return false;\n            }\n            String[] p = Arrays.copyOfRange(path, 1, path.length);\n            if (ConfigurationUtils.has((Map<String, Object>)maybeMap, p)) {\n                value = ConfigurationUtils.get((Map<String, Object>)maybeMap, p);\n            }\n        }\n        return value != null;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/HasVariableFunction.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el.functions;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.runtime.v2.sdk.ELFunction;\nimport com.walmartlabs.concord.runtime.v2.runner.el.ThreadLocalEvalContext;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport java.util.Arrays;\nimport java.util.Map;\n\npublic final class HasVariableFunction {\n\n    @ELFunction\n    @SuppressWarnings(\"unchecked\")\n    public static boolean hasVariable(String name) {\n        if (name == null || name.trim().isEmpty()) {\n            return false;\n        }\n\n        Variables variables = ThreadLocalEvalContext.get().variables();\n\n        String[] path = name.split(\"\\\\.\");\n        if (path.length == 1) {\n            return variables.has(name);\n        } else {\n            Object maybeMap = variables.get(path[0]);\n            if (!(maybeMap instanceof Map)) {\n                return false;\n            }\n            return ConfigurationUtils.has((Map<String, Object>)maybeMap, Arrays.copyOfRange(path, 1, path.length));\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/IsDebugFunction.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el.functions;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.ELFunction;\nimport com.walmartlabs.concord.runtime.v2.runner.el.ThreadLocalEvalContext;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\n\npublic final class IsDebugFunction {\n\n    @ELFunction\n    public static boolean isDebug() {\n        Context ctx = ThreadLocalEvalContext.get().context();\n        if (ctx == null) {\n            return false;\n        }\n\n        return ctx.processConfiguration().debug();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/IsDryRunFunction.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el.functions;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.ELFunction;\nimport com.walmartlabs.concord.runtime.v2.runner.el.ThreadLocalEvalContext;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\n\npublic class IsDryRunFunction {\n\n    @ELFunction\n    public static boolean isDryRun() {\n        Context ctx = ThreadLocalEvalContext.get().context();\n        if (ctx == null) {\n            return false;\n        }\n\n        return ctx.processConfiguration().dryRun();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/MarkAsSensitiveFunction.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el.functions;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.ELFunction;\nimport com.walmartlabs.concord.runtime.v2.sdk.SensitiveDataHolder;\n\nimport javax.inject.Inject;\n\nimport static java.util.Objects.requireNonNull;\n\npublic class MarkAsSensitiveFunction {\n\n    private static SensitiveDataHolder sensitiveData;\n\n    @Inject\n    public MarkAsSensitiveFunction(SensitiveDataHolder sensitiveData) {\n        MarkAsSensitiveFunction.sensitiveData = requireNonNull(sensitiveData);\n    }\n\n    @ELFunction\n    public static String sensitive(Object v) {\n        if (MarkAsSensitiveFunction.sensitiveData == null) {\n            throw new IllegalStateException(\"MaskFunction must be initialized first\");\n        }\n\n        if (v == null) {\n            return null;\n        }\n\n        if (v instanceof String s) {\n            if (s.isBlank()) {\n                return s;\n            }\n            sensitiveData.add(s);\n            return s;\n        }\n\n        throw new IllegalArgumentException(\"Only string values can be masked. Got a \" + v.getClass() + \" instead\");\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/OrDefaultFunction.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el.functions;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.runtime.v2.sdk.ELFunction;\nimport com.walmartlabs.concord.runtime.v2.runner.el.ThreadLocalEvalContext;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport java.util.Arrays;\nimport java.util.Map;\n\npublic final class OrDefaultFunction {\n\n    @ELFunction\n    @SuppressWarnings(\"unchecked\")\n    public static <T> T orDefault(String variableName, T defaultValue) {\n        boolean has = HasVariableFunction.hasVariable(variableName);\n        if (!has) {\n            return defaultValue;\n        }\n\n        return (T) getValue(variableName);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Object getValue(String variableName) {\n        Variables variables = ThreadLocalEvalContext.get().variables();\n\n        String[] path = variableName.split(\"\\\\.\");\n        if (path.length == 1) {\n            return variables.get(variableName);\n        } else {\n            Object maybeMap = variables.get(path[0]);\n            if (!(maybeMap instanceof Map)) {\n                throw new IllegalStateException(\"Expected a map. This is most likely a bug\");\n            }\n            return ConfigurationUtils.get((Map<String, Object>) maybeMap, Arrays.copyOfRange(path, 1, path.length));\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/ThrowFunction.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el.functions;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.ELFunction;\nimport com.walmartlabs.concord.runtime.v2.sdk.UserDefinedException;\n\npublic final class ThrowFunction {\n\n    @ELFunction(\"throw\")\n    public static Object throwError(String message) {\n        throw new UserDefinedException(message);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/functions/UuidFunction.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el.functions;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.ELFunction;\n\nimport java.util.UUID;\n\npublic final class UuidFunction {\n\n    @ELFunction\n    public static String uuid() {\n        return UUID.randomUUID().toString();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/resolvers/BeanELResolver.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el.resolvers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.sun.el.util.ReflectionUtil;\nimport com.walmartlabs.concord.runtime.v2.runner.el.MethodNotFoundException;\n\nimport javax.el.ELContext;\nimport java.lang.reflect.Method;\n\n/**\n * Same as {@link javax.el.BeanELResolver}, but throws more detailed \"method is not found\" exceptions.\n */\npublic class BeanELResolver extends javax.el.BeanELResolver {\n\n    private final SensitiveDataProcessor sensitiveDataProcessor;\n\n    public BeanELResolver(SensitiveDataProcessor sensitiveDataProcessor) {\n        this.sensitiveDataProcessor = sensitiveDataProcessor;\n    }\n\n    @Override\n    public Object invoke(ELContext context, Object base, Object method, Class<?>[] paramTypes, Object[] params) {\n        if (base == null || method == null) {\n            return null; // same as original javax.el.BeanELResolver\n        }\n\n        try {\n            // NPE in super.invoke if method not found :(\n            if (ReflectionUtil.findMethod(base.getClass(), method.toString(), paramTypes, params) == null) {\n                throw new MethodNotFoundException(base.getClass(), method, paramTypes);\n            }\n\n            var result = super.invoke(context, base, method, paramTypes, params);\n\n            if (context.isPropertyResolved()) {\n                Method m = ReflectionUtil.findMethod(base.getClass(), method.toString(), paramTypes, params);\n                sensitiveDataProcessor.process(result, m);\n            }\n\n            return result;\n        } catch (javax.el.MethodNotFoundException e) {\n            throw new MethodNotFoundException(base.getClass(), method, paramTypes);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/resolvers/CompositeBeanELResolver.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el.resolvers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.CustomBeanMethodResolver;\nimport com.walmartlabs.concord.runtime.v2.sdk.CustomTaskMethodResolver;\nimport com.walmartlabs.concord.runtime.v2.sdk.Invocation;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\n\nimport javax.el.ELContext;\nimport java.util.List;\nimport java.util.Objects;\n\npublic class CompositeBeanELResolver extends javax.el.BeanELResolver {\n\n    private final List<CustomTaskMethodResolver> customTaskMethodResolvers;\n    private final List<CustomBeanMethodResolver> customBeanMethodResolvers;\n    private final BeanELResolver defaultResolver;\n    private final SensitiveDataProcessor sensitiveDataProcessor;\n\n    public CompositeBeanELResolver(\n            List<CustomTaskMethodResolver> customTaskMethodResolvers,\n            List<CustomBeanMethodResolver> customBeanMethodResolvers,\n            SensitiveDataProcessor sensitiveDataProcessor) {\n        this.customTaskMethodResolvers = customTaskMethodResolvers;\n        this.customBeanMethodResolvers = customBeanMethodResolvers;\n        this.defaultResolver = new BeanELResolver(sensitiveDataProcessor);\n        this.sensitiveDataProcessor = sensitiveDataProcessor;\n    }\n\n    @Override\n    public Object invoke(ELContext elContext, Object base, Object method, Class<?>[] paramTypes, Object[] params) {\n        if (base == null || method == null) {\n            return null;\n        }\n\n        var invocation = findInvocation(base, method, paramTypes, params);\n        if (invocation != null) {\n            elContext.setPropertyResolved(base, method);\n            return invocation.invoke(new DefaultInvocationContext(elContext, sensitiveDataProcessor));\n        }\n\n        return defaultResolver.invoke(elContext, base, method, paramTypes, params);\n    }\n\n    private Invocation findInvocation(Object base, Object method, Class<?>[] paramTypes, Object[] params) {\n        if (base instanceof Task task) {\n            var invocation = customTaskMethodResolvers.stream()\n                    .map(resolver -> resolver.resolve(task, method.toString(), paramTypes, params))\n                    .filter(Objects::nonNull)\n                    .findFirst()\n                    .orElse(null);\n            if (invocation != null) {\n                return invocation;\n            }\n        }\n\n        return customBeanMethodResolvers.stream()\n                .map(resolver -> resolver.resolve(base, method.toString(), paramTypes, params))\n                .filter(Objects::nonNull)\n                .findFirst()\n                .orElse(null);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/resolvers/DefaultInvocationContext.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el.resolvers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.InvocationContext;\nimport com.walmartlabs.concord.runtime.v2.sdk.MethodInvoker;\n\nimport javax.el.ELContext;\n\npublic class DefaultInvocationContext implements InvocationContext {\n\n    private final ELContext elContext;\n    private final javax.el.BeanELResolver beanELResolver;\n\n    public DefaultInvocationContext(ELContext elContext, SensitiveDataProcessor sensitiveDataProcessor) {\n        this.elContext = elContext;\n        this.beanELResolver = new BeanELResolver(sensitiveDataProcessor);\n    }\n\n    @Override\n    public MethodInvoker invoker() {\n        return (base, method, paramTypes, params) -> beanELResolver.invoke(elContext, base, method, paramTypes, params);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/resolvers/MapELResolver.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el.resolvers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.sun.el.util.ReflectionUtil;\n\nimport javax.el.ELContext;\n\npublic class MapELResolver extends javax.el.MapELResolver {\n\n    private final SensitiveDataProcessor sensitiveDataProcessor;\n\n    public MapELResolver(SensitiveDataProcessor sensitiveDataProcessor) {\n        this.sensitiveDataProcessor = sensitiveDataProcessor;\n    }\n\n    @Override\n    public Object getValue(ELContext context, Object base, Object property) {\n        var result = super.getValue(context, base, property);\n        if (context.isPropertyResolved()) {\n            var m = ReflectionUtil.findMethod(base.getClass(), \"get\", new Class[] {Object.class}, new Object[] {property});\n            sensitiveDataProcessor.process(result, m);\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/resolvers/MethodAccessorResolver.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el.resolvers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.el.ELContext;\nimport javax.el.ELResolver;\nimport java.beans.FeatureDescriptor;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\nimport java.util.Iterator;\n\n/**\n * Looks for a method with the specified name, calls the method and returns the result.\n * Useful for using Immutables with Java EL.\n */\npublic class MethodAccessorResolver extends ELResolver {\n\n    private final SensitiveDataProcessor sensitiveDataProcessor;\n\n    public MethodAccessorResolver(SensitiveDataProcessor sensitiveDataProcessor) {\n        this.sensitiveDataProcessor = sensitiveDataProcessor;\n    }\n\n    @Override\n    public Object getValue(ELContext context, Object base, Object property) {\n        if (base != null && property instanceof String) {\n            String k = (String) property;\n\n            try {\n                Method m = base.getClass().getDeclaredMethod(k);\n                Object value = m.invoke(base);\n                sensitiveDataProcessor.process(value, m);\n                context.setPropertyResolved(true);\n                return value;\n            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {\n                return null;\n            }\n        }\n\n        return null;\n    }\n\n    @Override\n    public Class<?> getCommonPropertyType(ELContext context, Object base) {\n        return Object.class;\n    }\n\n    @Override\n    public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {\n        return null;\n    }\n\n    @Override\n    public Class<?> getType(ELContext context, Object base, Object property) {\n        return Object.class;\n    }\n\n    @Override\n    public boolean isReadOnly(ELContext context, Object base, Object property) {\n        return true;\n    }\n\n    @Override\n    public void setValue(ELContext context, Object base, Object property, Object value) {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/resolvers/SensitiveDataProcessor.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el.resolvers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Inject;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.runtime.v2.sdk.SensitiveData;\nimport com.walmartlabs.concord.runtime.v2.sdk.SensitiveDataHolder;\n\nimport java.lang.reflect.Array;\nimport java.lang.reflect.Method;\nimport java.util.*;\n\npublic class SensitiveDataProcessor {\n\n    private static final int MAX_DEPTH = 10;\n\n    private final SensitiveDataHolder sensitiveDataHolder;\n\n    @Inject\n    public SensitiveDataProcessor(SensitiveDataHolder sensitiveDataHolder) {\n        this.sensitiveDataHolder = sensitiveDataHolder;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public void process(Object value, Method method) {\n        if (value == null || method == null) {\n            return;\n        }\n\n        var a = findSensitiveDataAnnotation(method);\n        if (a == null) {\n            return;\n        }\n\n        if (value instanceof String) {\n            sensitiveDataHolder.add((String) value);\n        }\n\n        if (value instanceof Map<?, ?> m) {\n            collectFromMap((Map<String, Object>) m, a);\n        }\n    }\n\n    private void collectFromMap(Map<String, Object> m, SensitiveData a) {\n        if (a.keys() != null && a.keys().length > 0) {\n            for (var k : a.keys()) {\n                var path = k.split(\"\\\\.\");\n                if (ConfigurationUtils.has(m, path)) {\n                    var v = ConfigurationUtils.get(m, path);\n                    collectValue(v, a);\n                }\n            }\n        } else {\n            for (var key : m.keySet()) {\n                collectValue(m.get(key), a);\n            }\n        }\n    }\n\n    private void collectValue(Object v, SensitiveData a) {\n        if (v instanceof String s) {\n            sensitiveDataHolder.add(s);\n            return;\n        }\n\n        if (a.includeNestedValues() && v instanceof Map<?, ?> nested) {\n            processNestedValues(nested, 0);\n        }\n    }\n\n    private void processNestedValues(Object obj, int depth) {\n        if (depth > MAX_DEPTH) {\n            return;\n        }\n\n        if (obj instanceof Map<?, ?> map) {\n            for (var entry : map.entrySet()) {\n                processNestedValues(entry.getValue(), depth + 1);\n            }\n        } else if (obj instanceof List<?> list) {\n            for (var o : list) {\n                processNestedValues(o, depth + 1);\n            }\n        } else if (obj instanceof Set<?> set) {\n            for (var item : set) {\n                processNestedValues(item, depth + 1);\n            }\n        } else if (obj != null && obj.getClass().isArray()) {\n            var len = Array.getLength(obj);\n            for (var i = 0; i < len; i++) {\n                var item = Array.get(obj, i);\n                processNestedValues(item, depth + 1);\n            }\n        } else if (obj instanceof String str) {\n            sensitiveDataHolder.add(str);\n        }\n    }\n\n    private SensitiveData findSensitiveDataAnnotation(Method method) {\n        if (method.isBridge()) {\n            return findAnnotationFromBridgedMethod(method);\n        }\n        return method.getAnnotation(SensitiveData.class);\n    }\n\n    /**\n     * This is a simple bridge resolution mechanism suitable for sensitive data processing.\n     */\n    private static SensitiveData findAnnotationFromBridgedMethod(Method bridgeMethod) {\n        var declaringClass = bridgeMethod.getDeclaringClass();\n        for (var candidate : declaringClass.getDeclaredMethods()) {\n            if (isBridgedCandidateFor(bridgeMethod, candidate)) {\n                var result = candidate.getAnnotation(SensitiveData.class);\n                if (result != null) {\n                    return result;\n                }\n            }\n        }\n        return bridgeMethod.getAnnotation(SensitiveData.class);\n    }\n\n    private static boolean isBridgedCandidateFor(Method bridge, Method candidate) {\n        return !candidate.isBridge() &&\n                !candidate.equals(bridge) &&\n                candidate.getName().equals(bridge.getName()) &&\n                candidate.getParameterCount() == bridge.getParameterCount();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/resolvers/TaskMethodResolver.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el.resolvers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ReflectionUtils;\nimport com.walmartlabs.concord.runtime.v2.model.Expression;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.runner.el.MethodNotFoundException;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallInterceptor;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskException;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.CustomTaskMethodResolver;\nimport com.walmartlabs.concord.runtime.v2.sdk.InvocationContext;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\n\nimport javax.el.ELContext;\nimport javax.el.ELResolver;\nimport javax.inject.Named;\nimport java.beans.FeatureDescriptor;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallInterceptor.CallContext;\nimport static com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallInterceptor.Method;\n\npublic class TaskMethodResolver extends ELResolver {\n\n    private final List<CustomTaskMethodResolver> resolvers = new ArrayList<>();\n    private final Context context;\n    private final SensitiveDataProcessor sensitiveDataProcessor;\n\n    public TaskMethodResolver(List<CustomTaskMethodResolver> customResolvers,\n                              Context context,\n                              SensitiveDataProcessor sensitiveDataProcessor) {\n        this.sensitiveDataProcessor = sensitiveDataProcessor;\n        this.resolvers.addAll(customResolvers);\n        this.resolvers.add(new DefaultTaskMethodResolver());\n        this.context = context;\n    }\n\n    @Override\n    public Object invoke(ELContext elContext, Object base, Object method, Class<?>[] paramTypes, Object[] params) {\n        Step step = context.execution().currentStep();\n        if (!(step instanceof Expression)\n                || !(base instanceof Task task)\n                || !(method instanceof String)) {\n            return null;\n        }\n\n        var invocation = resolvers.stream()\n                .map(resolver -> resolver.resolve(task, method.toString(), paramTypes, params))\n                .filter(Objects::nonNull)\n                .findFirst()\n                .orElse(null);\n        if (invocation == null) {\n            return null;\n        }\n\n        CallContext callContext = TaskCallInterceptor.CallContext.builder()\n                .threadId(context.execution().currentThreadId())\n                .taskName(invocation.taskName())\n                .correlationId(context.execution().correlationId())\n                .currentStep(step)\n                .processDefinition(context.execution().processDefinition())\n                .build();\n\n        TaskCallInterceptor interceptor = context.execution().runtime().getService(TaskCallInterceptor.class);\n        try {\n            return interceptor.invoke(callContext, Method.of(invocation.taskClass(), method.toString(), Arrays.asList(params)),\n                    () -> {\n                        var result = invocation.invoke(new DefaultInvocationContext(elContext, sensitiveDataProcessor));\n                        elContext.setPropertyResolved(true);\n                        return result;\n                    });\n        } catch (javax.el.MethodNotFoundException e) {\n            throw new MethodNotFoundException(invocation.taskClass(), method, paramTypes);\n        } catch (RuntimeException e) {\n            throw e;\n        } catch (TaskException e) {\n            if (e.getCause() instanceof RuntimeException re) {\n                throw re;\n            }\n            throw new RuntimeException(e.getCause());\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public Class<?> getType(ELContext context, Object base, Object property) {\n        return null;\n    }\n\n    @Override\n    public void setValue(ELContext context, Object base, Object property, Object value) {\n\n    }\n\n    @Override\n    public boolean isReadOnly(ELContext context, Object base, Object property) {\n        return false;\n    }\n\n    @Override\n    public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {\n        return null;\n    }\n\n    @Override\n    public Class<?> getCommonPropertyType(ELContext context, Object base) {\n        return null;\n    }\n\n    @Override\n    public Object getValue(ELContext context, Object base, Object property) {\n        return null;\n    }\n\n    private static class DefaultTaskMethodResolver implements CustomTaskMethodResolver {\n\n        @Override\n        public TaskInvocation resolve(Task base, String method, Class<?>[] paramTypes, Object[] params) {\n            String taskName = getName(base);\n            if (taskName == null) {\n                return null;\n            }\n            return new DefaultInvocation(taskName, base, method, paramTypes, params);\n        }\n\n        private static String getName(Object task) {\n            Named n = ReflectionUtils.findAnnotation(task.getClass(), Named.class);\n            if (n != null) {\n                return n.value();\n            }\n\n            return null;\n        }\n    }\n\n    private record DefaultInvocation(String taskName, Task base, String method,\n                                     Class<?>[] paramTypes, Object[] params) implements CustomTaskMethodResolver.TaskInvocation {\n\n        @Override\n        public Class<? extends Task> taskClass() {\n            return base.getClass();\n        }\n\n        @Override\n        public Object invoke(InvocationContext context) {\n            return context.invoker().invoke(base, method, paramTypes, params);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/resolvers/TaskResolver.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el.resolvers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskProviders;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\n\nimport javax.el.ELContext;\nimport javax.el.ELResolver;\nimport java.beans.FeatureDescriptor;\nimport java.util.Iterator;\n\npublic class TaskResolver extends ELResolver {\n\n    private final Context context;\n    private final TaskProviders taskProviders;\n\n    public TaskResolver(Context context, TaskProviders taskProviders) {\n        this.context = context;\n        this.taskProviders = taskProviders;\n    }\n\n    @Override\n    public Class<?> getCommonPropertyType(ELContext context, Object base) {\n        return Object.class;\n    }\n\n    @Override\n    public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {\n        return null;\n    }\n\n    @Override\n    public Class<?> getType(ELContext context, Object base, Object property) {\n        return Object.class;\n    }\n\n    @Override\n    public Object getValue(ELContext context, Object base, Object property) {\n        if (base == null) {\n            String key = (String) property;\n\n            Object o = taskProviders.createTask(this.context, key);\n            if (o != null) {\n                context.setPropertyResolved(true);\n            }\n            return o;\n        }\n\n        return null;\n    }\n\n    @Override\n    public boolean isReadOnly(ELContext context, Object base, Object property) {\n        return true;\n    }\n\n    @Override\n    public void setValue(ELContext context, Object base, Object property, Object value) {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/el/resolvers/VariableResolver.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el.resolvers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport javax.el.ELContext;\nimport javax.el.ELResolver;\nimport java.beans.FeatureDescriptor;\nimport java.util.Iterator;\nimport java.util.Map;\n\npublic class VariableResolver extends ELResolver {\n\n    private final Variables variables;\n\n    public VariableResolver(Variables variables) {\n        this.variables = variables;\n    }\n\n    public VariableResolver(Map<String, Object> variables) {\n        this.variables = new MapBackedVariables(variables);\n    }\n\n    @Override\n    public Class<?> getCommonPropertyType(ELContext context, Object base) {\n        return Object.class;\n    }\n\n    @Override\n    public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {\n        return null;\n    }\n\n    @Override\n    public Class<?> getType(ELContext context, Object base, Object property) {\n        return Object.class;\n    }\n\n    @Override\n    public Object getValue(ELContext context, Object base, Object property) {\n        if (base == null && property instanceof String) {\n            String k = (String) property;\n\n            if (variables.has(k)) {\n                context.setPropertyResolved(true);\n                return variables.get(k);\n            }\n        }\n\n        return null;\n    }\n\n    @Override\n    public boolean isReadOnly(ELContext context, Object base, Object property) {\n        return true;\n    }\n\n    @Override\n    public void setValue(ELContext context, Object base, Object property, Object value) {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/guice/BaseRunnerModule.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.guice;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.google.inject.AbstractModule;\nimport com.google.inject.multibindings.Multibinder;\nimport com.walmartlabs.concord.dependencymanager.DependencyManagerConfiguration;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.runtime.common.FormService;\nimport com.walmartlabs.concord.runtime.v2.runner.*;\nimport com.walmartlabs.concord.runtime.v2.runner.compiler.DefaultCompiler;\nimport com.walmartlabs.concord.runtime.v2.runner.context.ContextFactory;\nimport com.walmartlabs.concord.runtime.v2.runner.context.DefaultContextFactory;\nimport com.walmartlabs.concord.runtime.v2.runner.el.EvalContextFactoryImpl;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\nimport com.walmartlabs.concord.runtime.v2.runner.script.DefaultScriptEvaluator;\nimport com.walmartlabs.concord.runtime.v2.runner.script.ScriptEvaluator;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.*;\nimport com.walmartlabs.concord.runtime.v2.sdk.Compiler;\n\n/**\n * Contains basic services that can work in anyenvironment (unit tests, actual runtime, CLI, etc).\n */\npublic class BaseRunnerModule extends AbstractModule {\n\n    @Override\n    protected void configure() {\n        install(new ExpressionSupportModule());\n\n        bind(ContextFactory.class).to(DefaultContextFactory.class);\n        bind(FileService.class).to(DefaultFileService.class);\n        bind(Compiler.class).to(DefaultCompiler.class);\n        bind(PolicyEngine.class).toProvider(PolicyEngineProvider.class);\n        bind(SynchronizationService.class).to(DefaultSynchronizationService.class);\n        bind(EvalContextFactory.class).to(EvalContextFactoryImpl.class);\n        bind(ScriptEvaluator.class).to(DefaultScriptEvaluator.class);\n        bind(ResourceResolver.class).to(DefaultResourceResolver.class);\n        bind(TaskResultService.class);\n        bind(FormService.class).toProvider(FormServiceProvider.class);\n        bind(Context.class).toProvider(ContextProvider.class);\n        bind(ObjectMapper.class).toProvider(ObjectMapperProvider.class);\n\n        bind(DependencyManagerConfiguration.class).toProvider(DependencyManagerConfigurationProvider.class);\n\n        Multibinder<TaskProvider> taskProviders = Multibinder.newSetBinder(binder(), TaskProvider.class);\n        taskProviders.addBinding().to(TaskV2Provider.class);\n\n        Multibinder<TaskCallListener> taskCallListeners = Multibinder.newSetBinder(binder(), TaskCallListener.class);\n        taskCallListeners.addBinding().to(TaskCallPolicyChecker.class);\n        taskCallListeners.addBinding().to(TaskResultListener.class);\n\n        bind(TaskSchemaRegistry.class);\n        bind(TaskSchemaValidator.class);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/guice/CurrentClasspathModule.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.guice;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.AbstractModule;\nimport org.eclipse.sisu.space.BeanScanning;\nimport org.eclipse.sisu.space.SpaceModule;\nimport org.eclipse.sisu.space.URLClassSpace;\n\npublic class CurrentClasspathModule extends AbstractModule {\n\n    @Override\n    protected void configure() {\n        ClassLoader cl = this.getClass().getClassLoader();\n        // use sisu impl when this one released: https://github.com/eclipse/sisu.inject/commit/4790d3e28987ee4c2472d576e544c07028a85f42\n        install(new SpaceModule(new URLClassSpace(cl), BeanScanning.GLOBAL_INDEX));\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/guice/DefaultRunnerModule.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.guice;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.AbstractModule;\nimport com.google.inject.multibindings.Multibinder;\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.runtime.v2.runner.*;\nimport com.walmartlabs.concord.runtime.v2.runner.checkpoints.CheckpointService;\nimport com.walmartlabs.concord.runtime.v2.runner.checkpoints.CheckpointUploader;\nimport com.walmartlabs.concord.runtime.v2.runner.checkpoints.DefaultCheckpointService;\nimport com.walmartlabs.concord.runtime.v2.runner.checkpoints.DefaultCheckpointUploader;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.DefaultLoggingClient;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.LoggerProvider;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.LoggingClient;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.RunnerLogger;\nimport com.walmartlabs.concord.runtime.v2.runner.remote.ApiClientProvider;\nimport com.walmartlabs.concord.runtime.v2.runner.remote.DefaultProcessStatusCallback;\nimport com.walmartlabs.concord.runtime.v2.runner.remote.EventRecordingExecutionListener;\nimport com.walmartlabs.concord.runtime.v2.runner.remote.TaskCallEventRecordingListener;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallListener;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\nimport com.walmartlabs.concord.svm.ExecutionListener;\n\nimport javax.inject.Singleton;\n\n/**\n * Default set of services.\n */\npublic class DefaultRunnerModule extends AbstractModule {\n\n    @Override\n    protected void configure() {\n        install(new BaseRunnerModule());\n\n        // singletons\n        bind(CheckpointUploader.class).to(DefaultCheckpointUploader.class).in(Singleton.class);\n        bind(CheckpointService.class).to(DefaultCheckpointService.class).in(Singleton.class);\n        bind(DependencyManager.class).to(DefaultDependencyManager.class).in(Singleton.class);\n        bind(DockerService.class).to(DefaultDockerService.class).in(Singleton.class);\n        bind(FileService.class).to(DefaultFileService.class).in(Singleton.class);\n        bind(EventReportingService.class).to(DefaultEventReportingService.class).in(Singleton.class);\n        bind(LockService.class).to(DefaultLockService.class).in(Singleton.class);\n        bind(PersistenceService.class).to(DefaultPersistenceService.class).in(Singleton.class);\n        bind(ProcessStatusCallback.class).to(DefaultProcessStatusCallback.class).in(Singleton.class);\n        bind(SecretService.class).to(DefaultSecretService.class).in(Singleton.class);\n        bind(RunnerLogger.class).toProvider(LoggerProvider.class);\n        bind(LoggingClient.class).to(DefaultLoggingClient.class);\n\n        bind(ApiClient.class).toProvider(ApiClientProvider.class);\n        bind(DefaultTaskVariablesService.class).toProvider(DefaultTaskVariablesProvider.class);\n\n        Multibinder<TaskCallListener> taskCallListeners = Multibinder.newSetBinder(binder(), TaskCallListener.class);\n        taskCallListeners.addBinding().to(TaskCallEventRecordingListener.class);\n\n        Multibinder<ExecutionListener> executionListeners = Multibinder.newSetBinder(binder(), ExecutionListener.class);\n        executionListeners.addBinding().to(EventRecordingExecutionListener.class);\n        executionListeners.addBinding().to(DefaultEventReportingService.class);\n        executionListeners.addBinding().to(MetadataProcessor.class);\n        executionListeners.addBinding().to(OutVariablesProcessor.class);\n        executionListeners.addBinding().to(SensitiveDataPersistenceService.class);\n        executionListeners.addBinding().to(StackTraceCollector.class);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/guice/ExpressionSupportModule.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.guice;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.google.inject.TypeLiteral;\nimport com.google.inject.matcher.Matchers;\nimport com.google.inject.spi.TypeEncounter;\nimport com.google.inject.spi.TypeListener;\nimport com.walmartlabs.concord.runtime.v2.runner.el.DefaultExpressionEvaluator;\nimport com.walmartlabs.concord.runtime.v2.runner.el.FunctionHolder;\nimport com.walmartlabs.concord.runtime.v2.runner.el.functions.*;\nimport com.walmartlabs.concord.runtime.v2.sdk.ELFunction;\nimport com.walmartlabs.concord.runtime.v2.sdk.ExpressionEvaluator;\n\nimport java.lang.reflect.Modifier;\n\npublic class ExpressionSupportModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        binder.bind(ExpressionEvaluator.class).to(DefaultExpressionEvaluator.class);\n\n        // support for @ELFunction\n        var functionHolder = new FunctionHolder();\n        binder.bind(FunctionHolder.class).toInstance(functionHolder);\n        binder.bindListener(Matchers.any(), new TypeListener() {\n            @Override\n            public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {\n                var clazz = type.getRawType();\n\n                for (var method : clazz.getDeclaredMethods()) {\n                    var annotation = method.getAnnotation(ELFunction.class);\n                    if (annotation == null) {\n                        continue;\n                    }\n\n                    if (!Modifier.isStatic(method.getModifiers())) {\n                        var msg = String.format(\"@ELFunction method must be static: %s.%s\", clazz.getName(), method.getName());\n                        encounter.addError(msg);\n                        continue;\n                    }\n\n                    if (!Modifier.isPublic(method.getModifiers())) {\n                        var msg = String.format(\"@ELFunction method must be public: %s.%s\", clazz.getName(), method.getName());\n                        encounter.addError(msg);\n                        continue;\n                    }\n\n                    var name = annotation.value();\n                    if (name == null || name.isBlank()) {\n                        name = method.getName();\n                    }\n                    functionHolder.register(name, method);\n                }\n            }\n        });\n\n        // built-in functions\n        binder.bind(AllVariablesFunction.class).asEagerSingleton();\n        binder.bind(CurrentFlowNameFunction.class).asEagerSingleton();\n        binder.bind(EvalAsMapFunction.class).asEagerSingleton();\n        binder.bind(HasFlowFunction.class).asEagerSingleton();\n        binder.bind(HasNonNullVariableFunction.class).asEagerSingleton();\n        binder.bind(HasVariableFunction.class).asEagerSingleton();\n        binder.bind(IsDebugFunction.class).asEagerSingleton();\n        binder.bind(IsDryRunFunction.class).asEagerSingleton();\n        binder.bind(MarkAsSensitiveFunction.class).asEagerSingleton();\n        binder.bind(OrDefaultFunction.class).asEagerSingleton();\n        binder.bind(ThrowFunction.class).asEagerSingleton();\n        binder.bind(UuidFunction.class).asEagerSingleton();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/guice/ObjectMapperProvider.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.guice;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\n\nimport javax.inject.Named;\nimport javax.inject.Provider;\nimport javax.inject.Singleton;\n\n@Named\npublic class ObjectMapperProvider implements Provider<ObjectMapper> {\n\n    private static final ObjectMapper instance;\n\n    static {\n        ObjectMapper om = new ObjectMapper();\n        om.registerModule(new Jdk8Module());\n        om.registerModule(new JavaTimeModule());\n\n        instance = om;\n    }\n\n    @Override\n    public ObjectMapper get() {\n        return instance.copy();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/guice/ProcessDependenciesModule.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.guice;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.AbstractModule;\nimport com.google.inject.name.Names;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.eclipse.sisu.space.BeanScanning;\nimport org.eclipse.sisu.space.SpaceModule;\nimport org.eclipse.sisu.space.URLClassSpace;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport java.net.URLClassLoader;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n/**\n * Loads the specified dependencies as Guice beans.\n */\npublic class ProcessDependenciesModule extends AbstractModule {\n\n    private static final Logger log = LoggerFactory.getLogger(ProcessDependenciesModule.class);\n\n    private final Path workDir;\n    private final Collection<String> dependencies;\n    private final boolean debug;\n\n    public ProcessDependenciesModule(Path workDir, Collection<String> dependencies, boolean debug) {\n        this.workDir = workDir;\n        this.dependencies = dependencies;\n        this.debug = debug;\n    }\n\n    @Override\n    protected void configure() {\n        try {\n            ClassLoader cl = loadDependencies(workDir, dependencies, debug);\n\n            // required to support ScriptEngines from external dependencies\n            Thread.currentThread().setContextClassLoader(cl);\n\n            install(new SpaceModule(new URLClassSpace(cl), BeanScanning.GLOBAL_INDEX));\n            bind(ClassLoader.class).annotatedWith(Names.named(\"runtime\")).toInstance(cl);\n        } catch (IOException e) {\n            addError(e);\n        }\n    }\n\n    private static URLClassLoader loadDependencies(Path workDir, Collection<String> dependencies, boolean debug) throws IOException {\n        List<URL> urls = toURLs(workDir, dependencies);\n        if (debug) {\n            log.info(\"Effective dependencies:\\n\\t{}\", urls.stream()\n                    .map(URL::toString)\n                    .collect(Collectors.joining(\"\\n\\t\")));\n        }\n        return new URLClassLoader(urls.toArray(new URL[0]), ProcessDependenciesModule.class.getClassLoader());\n    }\n\n    private static List<URL> toURLs(Path workDir, Collection<String> dependencies) throws IOException {\n        List<URL> urls = dependencies.stream()\n                .sorted()\n                .map(s -> {\n                    try {\n                        // assume all dependencies are resolved into file paths at this point\n                        return new URL(\"file://\" + s);\n                    } catch (MalformedURLException e) {\n                        throw new RuntimeException(\"Invalid dependency \" + s + \": \" + e.getMessage());\n                    }\n                }).collect(Collectors.toList());\n\n        // collect all JARs from the `${workDir}/lib/` directory\n\n        Path lib = workDir.resolve(Constants.Files.LIBRARIES_DIR_NAME);\n        if (Files.exists(lib)) {\n            try (Stream<Path> s = Files.list(lib).sorted()) {\n                s.forEach(f -> {\n                    if (f.toString().endsWith(\".jar\")) {\n                        try {\n                            urls.add(f.toUri().toURL());\n                        } catch (MalformedURLException e) {\n                            throw new RuntimeException(e);\n                        }\n                    }\n                });\n            }\n        }\n\n        return urls;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/logging/ConcordLogEncoder.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ch.qos.logback.classic.Level;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.encoder.LayoutWrappingEncoder;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentHeader;\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentSerializer;\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentStatus;\n\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\npublic class ConcordLogEncoder extends LayoutWrappingEncoder<ILoggingEvent> {\n\n    public static boolean SEGMENTED = false;\n\n    private static final Stats EMPTY_STATS = new Stats();\n    private static final Map<Long, Stats> statsHolder = new ConcurrentHashMap<>();\n\n    @Override\n    public byte[] encode(ILoggingEvent event) {\n        if (!SEGMENTED) {\n            return super.encode(event);\n        }\n\n        SegmentStatusMarker marker = segmentMarker(event);\n        boolean isStatusEvent = marker != null;\n        String msg = null;\n        if (!isStatusEvent) {\n            msg = layout.doLayout(event);\n        }\n        return LogSegmentSerializer.serialize(header(event, marker), msg);\n    }\n\n    public static SegmentStatusMarker segmentMarker(ILoggingEvent event) {\n        if (event.getMarkerList() == null) {\n            return null;\n        }\n\n        return (SegmentStatusMarker) event.getMarkerList().stream()\n                .filter(m -> m instanceof SegmentStatusMarker)\n                .findFirst()\n                .orElse(null);\n    }\n\n    private LogSegmentHeader header(ILoggingEvent event, SegmentStatusMarker marker) {\n        Long segmentId;\n        if (marker != null) {\n            segmentId = marker.getSegmentId();\n        } else {\n            segmentId = LogUtils.getSegmentId();\n        }\n\n        if (segmentId == null) {\n            segmentId = 0L;\n        }\n\n        Stats stats = processStats(segmentId, event, isFinalStatus(marker));\n\n        return LogSegmentHeader.builder()\n                .length(0)\n                .segmentId(segmentId)\n                .warnCount(stats.warnings())\n                .errorCount(stats.errors())\n                .status(status(marker))\n                .build();\n    }\n\n    private static boolean isFinalStatus(SegmentStatusMarker marker) {\n        return marker != null && marker.getStatus() != LogSegmentStatus.RUNNING;\n    }\n\n    private static Stats processStats(long segmentId, ILoggingEvent event, boolean finalStatus) {\n        Stats stats = EMPTY_STATS;\n        if (event.getLevel() == Level.ERROR) {\n            stats = statsHolder.computeIfAbsent(segmentId, s -> new Stats())\n                    .incError();\n        } else if (event.getLevel() == Level.WARN) {\n            stats = statsHolder.computeIfAbsent(segmentId, s -> new Stats())\n                    .incWarn();\n        }\n\n        if (finalStatus) {\n            stats = statsHolder.remove(segmentId);\n            if (stats == null) {\n                stats = EMPTY_STATS;\n            }\n        }\n\n        return stats;\n    }\n\n    private static LogSegmentStatus status(SegmentStatusMarker marker) {\n        if (marker == null || marker.getStatus() == null) {\n            return LogSegmentStatus.RUNNING;\n        }\n\n        return marker.getStatus();\n    }\n\n    private static class Stats {\n\n        private int errors = 0;\n        private int warnings = 0;\n\n        public int errors() {\n            return errors;\n        }\n\n        public int warnings() {\n            return warnings;\n        }\n\n        @JsonIgnore\n        public Stats incError() {\n            synchronized (this) {\n                errors++;\n                return this;\n            }\n        }\n\n        @JsonIgnore\n        public Stats incWarn() {\n            synchronized (this) {\n                warnings++;\n                return this;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/logging/CustomLayout.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ch.qos.logback.classic.PatternLayout;\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport com.walmartlabs.concord.runtime.common.SensitiveDataMasker;\nimport com.walmartlabs.concord.runtime.v2.sdk.SensitiveDataHolder;\nimport com.walmartlabs.concord.runtime.v2.sdk.WorkingDirectory;\n\nimport static java.util.Objects.requireNonNull;\n\npublic class CustomLayout extends PatternLayout {\n\n    private static volatile String workDirToReplace;\n    private static volatile SensitiveDataHolder sensitiveDataHolder;\n\n    /**\n     * Enables masking of ${workDir} values in logs. Such values often add noise to logs.\n     */\n    public static void enableWorkingDirectoryMasking(WorkingDirectory workDir) {\n        requireNonNull(workDir);\n        CustomLayout.workDirToReplace = workDir.getValue().toString();\n    }\n\n    public static void setSensitiveDataHolder(SensitiveDataHolder sensitiveDataHolder) {\n        requireNonNull(sensitiveDataHolder);\n        CustomLayout.sensitiveDataHolder = sensitiveDataHolder;\n    }\n\n    @Override\n    public String doLayout(ILoggingEvent event) {\n        var msg = super.doLayout(event);\n        msg = SensitiveDataMasker.mask(msg, sensitiveDataHolder.get());\n        if (CustomLayout.workDirToReplace != null) {\n            msg = msg.replace(workDirToReplace, \"$WORK_DIR\");\n        }\n        return msg;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/logging/DefaultLoggingClient.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.client2.ApiException;\nimport com.walmartlabs.concord.client2.ClientUtils;\nimport com.walmartlabs.concord.client2.LogSegmentOperationResponse;\nimport com.walmartlabs.concord.client2.LogSegmentRequest;\nimport com.walmartlabs.concord.client2.ProcessLogV2Api;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration;\n\nimport javax.inject.Inject;\nimport javax.inject.Singleton;\nimport java.time.OffsetDateTime;\nimport java.time.ZoneId;\nimport java.util.UUID;\n\n@Singleton\npublic class DefaultLoggingClient implements LoggingClient {\n\n    private final ProcessLogV2Api api;\n    private final UUID instanceId;\n    private final RunnerConfiguration cfg;\n\n    @Inject\n    public DefaultLoggingClient(ApiClient apiClient, ProcessConfiguration processCfg, RunnerConfiguration cfg) {\n        this.api = new ProcessLogV2Api(apiClient);\n        this.instanceId = processCfg.instanceId();\n        this.cfg = cfg;\n    }\n\n    public long createSegment(UUID correlationId, String name) {\n        LogSegmentRequest request = new LogSegmentRequest()\n                .correlationId(correlationId)\n                .createdAt(OffsetDateTime.now(ZoneId.of(\"UTC\")))\n                .name(name);\n\n        try {\n            LogSegmentOperationResponse result = ClientUtils.withRetry(cfg.api().retryCount(), cfg.api().retryInterval(), () -> api.createProcessLogSegment(instanceId, request));\n            return result.getId();\n        } catch (ApiException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/logging/LogContext.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ch.qos.logback.classic.Level;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface LogContext extends Serializable {\n\n   long serialVersionUID = 1L;\n\n    @Nullable\n    Long segmentId();\n\n    String segmentName();\n\n    UUID correlationId();\n\n    boolean redirectSystemOutAndErr();\n\n    @Value.Default\n    default Level logLevel() {\n        return Level.INFO;\n    }\n\n    static ImmutableLogContext.Builder builder() {\n        return ImmutableLogContext.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/logging/LogContextThreadGroup.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class LogContextThreadGroup extends ThreadGroup {\n\n    private final LogContext context;\n\n    public LogContextThreadGroup(LogContext context) {\n        super(context.segmentName());\n        this.context = context;\n    }\n\n    public LogContext getContext() {\n        return context;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/logging/LogLevelFilter.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ch.qos.logback.classic.spi.ILoggingEvent;\nimport ch.qos.logback.core.filter.Filter;\nimport ch.qos.logback.core.spi.FilterReply;\n\npublic class LogLevelFilter extends Filter<ILoggingEvent> {\n\n    @Override\n    public FilterReply decide(ILoggingEvent event) {\n        if (ConcordLogEncoder.segmentMarker(event) != null) {\n            return FilterReply.NEUTRAL;\n        }\n\n        LogContext logContext = LogUtils.getContext();\n\n        if (logContext != null && !event.getLevel().isGreaterOrEqual(logContext.logLevel())) {\n            return FilterReply.DENY;\n        }\n\n        return FilterReply.NEUTRAL;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/logging/LogUtils.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic final class LogUtils {\n\n    /**\n     * The segment ID lookup's maximum depth (limits nesting of {@link ThreadGroup}s).\n     */\n    private static final int MAX_DEPTH = 100;\n\n    public static Long getSegmentId() {\n        LogContext ctx = getContext();\n        if (ctx == null) {\n            return null;\n        }\n        return ctx.segmentId();\n    }\n\n    public static LogContext getContext() {\n        int depth = 0;\n\n        ThreadGroup g = Thread.currentThread().getThreadGroup();\n        while (true) {\n            if (g instanceof LogContextThreadGroup) {\n                LogContextThreadGroup ttg = (LogContextThreadGroup) g;\n                return ttg.getContext();\n            }\n\n            if (g.getParent() == null) {\n                break;\n            }\n\n            g = g.getParent();\n            depth++;\n\n            if (depth >= MAX_DEPTH) {\n                throw new IllegalStateException(\"Maximum ThreadGroup nesting limit is reached. \" +\n                        \"This is most likely a bug in the runtime and/or a plugin.\");\n            }\n        }\n\n        return null;\n    }\n\n    private LogUtils() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/logging/LoggerProvider.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\n\nimport javax.inject.Inject;\nimport javax.inject.Provider;\n\npublic class LoggerProvider implements Provider<RunnerLogger> {\n\n    private final RunnerLogger logger;\n\n    @Inject\n    public LoggerProvider(RunnerConfiguration runnerCfg,\n                          LoggingClient loggingClient) {\n        this.logger = createLogger(runnerCfg, loggingClient);\n    }\n\n    @Override\n    public RunnerLogger get() {\n        return logger;\n    }\n\n    private static RunnerLogger createLogger(RunnerConfiguration runnerCfg, LoggingClient loggingClient) {\n        if (runnerCfg.logging().segmentedLogs()) {\n            return new SegmentedLogger(loggingClient);\n        } else {\n            return new SimpleLogger();\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/logging/LoggingClient.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.UUID;\n\npublic interface LoggingClient {\n\n    long createSegment(UUID correlationId, String name);\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/logging/LoggingConfigurator.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class LoggingConfigurator {\n\n    public static void configure(boolean segmented) {\n        ConcordLogEncoder.SEGMENTED = segmented;\n    }\n\n    public static void reset() {\n        ConcordLogEncoder.SEGMENTED = false;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/logging/RunnerLogger.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentStatus;\n\nimport javax.annotation.Nullable;\nimport java.util.UUID;\n\npublic interface RunnerLogger {\n\n    void withContext(LogContext context, Runnable runnable);\n\n    @Nullable\n    Long createSegment(String segmentName, UUID correlationId);\n\n    void setSegmentStatus(long segmentId, LogSegmentStatus segmentStatus);\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/logging/SegmentStatusMarker.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentStatus;\nimport org.slf4j.Marker;\n\nimport java.util.Iterator;\n\npublic class SegmentStatusMarker implements Marker {\n\n    private final long segmentId;\n    private final LogSegmentStatus status;\n\n    public SegmentStatusMarker(long segmentId, LogSegmentStatus status) {\n        this.segmentId = segmentId;\n        this.status = status;\n    }\n\n    public long getSegmentId() {\n        return segmentId;\n    }\n\n    public LogSegmentStatus getStatus() {\n        return status;\n    }\n\n    @Override\n    public String getName() {\n        return \"close-segment-marker\";\n    }\n\n    @Override\n    public void add(Marker reference) {\n    }\n\n    @Override\n    public boolean remove(Marker reference) {\n        return false;\n    }\n\n    @Override\n    public boolean hasChildren() {\n        return false;\n    }\n\n    @Override\n    public boolean hasReferences() {\n        return false;\n    }\n\n    @Override\n    public Iterator<Marker> iterator() {\n        return null;\n    }\n\n    @Override\n    public boolean contains(Marker other) {\n        return false;\n    }\n\n    @Override\n    public boolean contains(String name) {\n        return false;\n    }\n}"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/logging/SegmentedLogger.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ch.qos.logback.classic.Level;\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentStatus;\nimport com.walmartlabs.concord.runtime.v2.Constants;\nimport com.walmartlabs.concord.runtime.v2.model.AbstractStep;\nimport com.walmartlabs.concord.runtime.v2.parser.StepOptions;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport uk.org.lidalia.sysoutslf4j.context.LogLevel;\nimport uk.org.lidalia.sysoutslf4j.context.SysOutOverSLF4J;\n\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.*;\n\npublic class SegmentedLogger implements RunnerLogger {\n\n    private static final long SYSTEM_SEGMENT_ID = 0;\n    public static final String SYSTEM_SEGMENT_NAME = \"system\";\n\n    private static final Logger log = LoggerFactory.getLogger(SegmentedLogger.class);\n\n    private final LoggingClient loggingClient;\n\n    public SegmentedLogger(LoggingClient loggingClient) {\n        this.loggingClient = loggingClient;\n    }\n\n    @Override\n    public Long createSegment(String segmentName, UUID correlationId) {\n        if (SYSTEM_SEGMENT_NAME.equals(segmentName)) {\n            return SYSTEM_SEGMENT_ID;\n        }\n        return loggingClient.createSegment(correlationId, segmentName);\n    }\n\n    @Override\n    public void setSegmentStatus(long segmentId, LogSegmentStatus segmentStatus) {\n        // now the system segment never closes...\n        if (SYSTEM_SEGMENT_ID == segmentId) {\n            return;\n        }\n\n        log.info(new SegmentStatusMarker(segmentId, segmentStatus), segmentStatus.name());\n    }\n\n    @Override\n    public void withContext(LogContext context, Runnable runnable) {\n        ThreadGroup threadGroup = new LogContextThreadGroup(context);\n        executeInThreadGroup(threadGroup, \"thread-\" + context.segmentName(), () -> {\n            // make sure the redirection is enabled in the current thread\n            if (context.redirectSystemOutAndErr() && !SysOutOverSLF4J.systemOutputsAreSLF4JPrintStreams()) {\n                SysOutOverSLF4J.sendSystemOutAndErrToSLF4J(LogLevel.INFO, LogLevel.ERROR);\n            }\n\n            runnable.run();\n        });\n    }\n\n    public static String getSegmentName(AbstractStep<?> step) {\n        Map<String, Serializable> meta = meta(step);\n        return (String) meta.get(Constants.SEGMENT_NAME);\n    }\n\n    public static Level getLogLevel(AbstractStep<?> step) {\n        Map<String, Serializable> meta = meta(step);\n        String logLevel = (String) meta.get(Constants.LOG_LEVEL);\n        return Level.toLevel(logLevel, Level.INFO);\n    }\n\n    private static Map<String, Serializable> meta(AbstractStep<?> step) {\n        StepOptions opts = step.getOptions();\n        if (opts == null) {\n            return Collections.emptyMap();\n        }\n\n        return opts.meta();\n    }\n\n    /**\n     * Executes the {@link Callable} in the specified {@link ThreadGroup}.\n     * A bit expensive as it is creates a new thread.\n     */\n    private static void executeInThreadGroup(ThreadGroup group, String threadName, Runnable runnable) {\n        ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadGroupAwareThreadFactory(group, threadName));\n        Future<?> result = executor.submit(runnable);\n        try {\n            result.get();\n        } catch (InterruptedException e) { // NOSONAR\n            throw new RuntimeException(e);\n        } catch (ExecutionException e) {\n            Throwable cause = e.getCause();\n            if (cause != null) {\n                if (cause instanceof RuntimeException) {\n                    throw (RuntimeException) cause;\n                } else {\n                    throw new RuntimeException(cause);\n                }\n            }\n\n            throw new RuntimeException(e);\n        }\n    }\n\n    private record ThreadGroupAwareThreadFactory(ThreadGroup group, String threadName) implements ThreadFactory {\n\n        @Override\n        public Thread newThread(Runnable r) {\n            return new Thread(group, r, threadName);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/logging/SimpleLogger.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.logging;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentStatus;\n\nimport java.util.UUID;\n\npublic class SimpleLogger implements RunnerLogger {\n\n    @Override\n    public Long createSegment(String segmentName, UUID correlationId) {\n        return null;\n    }\n\n    @Override\n    public void setSegmentStatus(long segmentId, LogSegmentStatus segmentStatus) {\n        // do nothing\n    }\n\n    @Override\n    public void withContext(LogContext context, Runnable runnable) {\n        try {\n            runnable.run();\n        } catch (RuntimeException e) {\n            throw e;\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/remote/ApiClientProvider.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.remote;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.client2.ApiClientConfiguration;\nimport com.walmartlabs.concord.client2.ApiClientFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration;\n\nimport javax.inject.Inject;\nimport javax.inject.Provider;\n\npublic class ApiClientProvider implements Provider<ApiClient> {\n\n    private final ApiClientFactory clientFactory;\n    private final ProcessConfiguration processCfg;\n\n    @Inject\n    public ApiClientProvider(ApiClientFactory clientFactory, ProcessConfiguration processCfg) {\n        this.clientFactory = clientFactory;\n        this.processCfg = processCfg;\n    }\n\n    @Override\n    public ApiClient get() {\n        String s = processCfg.processInfo().sessionToken();\n        if (s == null) {\n            throw new IllegalStateException(\"Can't initialize the API client: 'processInfo.sessionToken' is not defined.\");\n        }\n\n        return clientFactory.create(ApiClientConfiguration.builder()\n                .sessionToken(s)\n                .build());\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/remote/DefaultProcessStatusCallback.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.remote;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.client2.ApiException;\nimport com.walmartlabs.concord.client2.ClientUtils;\nimport com.walmartlabs.concord.client2.ProcessApi;\nimport com.walmartlabs.concord.client2.ProcessEntry;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.runtime.v2.runner.ProcessStatusCallback;\n\nimport javax.inject.Inject;\nimport java.util.UUID;\n\npublic class DefaultProcessStatusCallback implements ProcessStatusCallback {\n\n    private final RunnerConfiguration cfg;\n    private final ProcessApi processApi;\n\n    @Inject\n    public DefaultProcessStatusCallback(RunnerConfiguration cfg, ApiClient apiClient) {\n        this.cfg = cfg;\n        this.processApi = new ProcessApi(apiClient);\n    }\n\n    @Override\n    public void onRunning(UUID instanceId) {\n        try {\n            ClientUtils.withRetry(cfg.api().retryCount(), cfg.api().retryInterval(), () -> {\n                processApi.updateStatus(instanceId, cfg.agentId(), ProcessEntry.StatusEnum.RUNNING.toString());\n                return null;\n            });\n        } catch (ApiException e) {\n            throw new RuntimeException(\"Error while updating the process status: \" + e.getMessage(), e);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/remote/EventRecordingExecutionListener.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.remote;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessEventRequest;\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.ProcessDefinitionUtils;\nimport com.walmartlabs.concord.runtime.v2.model.*;\nimport com.walmartlabs.concord.runtime.v2.runner.EventReportingService;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.ElementEventProducer;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\nimport javax.inject.Inject;\nimport java.time.Instant;\nimport java.time.ZoneOffset;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class EventRecordingExecutionListener implements ExecutionListener {\n\n    private final EventConfiguration eventConfiguration;\n    private final EventReportingService eventReportingService;\n\n    @Inject\n    public EventRecordingExecutionListener(ProcessConfiguration processConfiguration,\n                                           EventReportingService eventReportingService) {\n        this.eventConfiguration = processConfiguration.events();\n        this.eventReportingService = eventReportingService;\n    }\n\n    @Override\n    public Result afterCommand(Runtime runtime, VM vm, State state, ThreadId threadId, Command cmd) {\n        if (!eventConfiguration.recordEvents()) {\n            return Result.CONTINUE;\n        }\n\n        // TODO consider using marker interfaces to determine which step/command should produce ELEMENT events\n\n        if (!(cmd instanceof ElementEventProducer s)) {\n            return Result.CONTINUE;\n        }\n\n        ProcessDefinition pd = runtime.getService(ProcessDefinition.class);\n        Location loc = s.getStep().getLocation();\n\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"processDefinitionId\", ProcessDefinitionUtils.getCurrentFlowName(pd, s.getStep()));\n        m.put(\"fileName\", loc.fileName());\n        m.put(\"line\", loc.lineNum());\n        m.put(\"column\", loc.column());\n        m.put(\"description\", s.getDescription(state, threadId));\n        m.put(\"correlationId\", s.getCorrelationId());\n        if (threadId.id() != 0) {\n            m.put(\"threadId\", threadId.id());\n        }\n\n        ProcessEventRequest req = new ProcessEventRequest();\n        req.setEventType(\"ELEMENT\"); // TODO constants\n        req.setData(m);\n        req.setEventDate(Instant.now().atOffset(ZoneOffset.UTC));\n\n        eventReportingService.report(req);\n\n        return Result.CONTINUE;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/remote/TaskCallEventRecordingListener.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.remote;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ProcessEventRequest;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.runtime.common.ObjectTruncater;\nimport com.walmartlabs.concord.runtime.common.SensitiveDataMasker;\nimport com.walmartlabs.concord.runtime.v2.ProcessDefinitionUtils;\nimport com.walmartlabs.concord.runtime.v2.model.EventConfiguration;\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.runner.EventReportingService;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallEvent;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallListener;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\n\nimport javax.inject.Inject;\nimport java.lang.annotation.Annotation;\nimport java.time.Instant;\nimport java.time.ZoneOffset;\nimport java.util.*;\n\npublic class TaskCallEventRecordingListener implements TaskCallListener {\n\n    private static final String MASK = \"***\";\n\n    private final EventConfiguration eventConfiguration;\n    private final EventReportingService eventReportingService;\n    private final SensitiveDataHolder sensitiveDataHolder;\n\n    @Inject\n    public TaskCallEventRecordingListener(ProcessConfiguration processConfiguration,\n                                          EventReportingService eventReportingService,\n                                          SensitiveDataHolder sensitiveDataHolder) {\n        this.eventConfiguration = processConfiguration.events();\n        this.eventReportingService = eventReportingService;\n        this.sensitiveDataHolder = sensitiveDataHolder;\n    }\n\n    @Override\n    public void onEvent(TaskCallEvent event) {\n        if (!eventConfiguration.recordEvents()) {\n            return;\n        }\n\n        Map<String, Object> m = event(event);\n\n        m.put(\"phase\", event.phase().name().toLowerCase());\n\n        List<Object> inVars = event.input();\n        if (inVars != null && eventConfiguration.recordTaskInVars()) {\n            Map<String, Object> input = convertInput(processSensitiveDataAnnotations(inVars, event.inputAnnotations()));\n            input = processSensitiveData(input);\n            Map<String, Object> vars = maskVars(input, eventConfiguration.inVarsBlacklist());\n            if (eventConfiguration.truncateInVars()) {\n                vars = ObjectTruncater.truncateMap(vars, eventConfiguration.truncateMaxStringLength(), eventConfiguration.truncateMaxArrayLength(), eventConfiguration.truncateMaxDepth());\n            }\n            if (vars != null && !vars.isEmpty()) {\n                m.put(\"in\", vars);\n            }\n        }\n\n        Object outVars = event.result();\n        if (outVars != null && eventConfiguration.recordTaskOutVars()) {\n            Map<String, Object> output = asMapOrNull(outVars);\n            output = processSensitiveData(output);\n            Map<String, Object> vars = maskVars(output, eventConfiguration.outVarsBlacklist());\n            if (eventConfiguration.truncateOutVars()) {\n                vars = ObjectTruncater.truncateMap(vars, eventConfiguration.truncateMaxStringLength(), eventConfiguration.truncateMaxArrayLength(), eventConfiguration.truncateMaxDepth());\n            }\n            if (vars != null && !vars.isEmpty()) {\n                m.put(\"out\", vars);\n            }\n        }\n\n        Object metaVars = event.meta();\n        if (metaVars != null && eventConfiguration.recordTaskMeta()) {\n            Map<String, Object> rawMeta = asMapOrNull(metaVars);\n            Map<String, Object> meta = processSensitiveData(rawMeta);\n            meta = maskVars(meta, eventConfiguration.metaBlacklist());\n            if (eventConfiguration.truncateMeta()) {\n                meta = ObjectTruncater.truncateMap(meta, eventConfiguration.truncateMaxStringLength(), eventConfiguration.truncateMaxArrayLength(), eventConfiguration.truncateMaxDepth());\n            }\n            if (meta != null && !meta.isEmpty()) {\n                m.put(\"meta\", meta);\n            }\n        }\n\n        if (event.duration() != null) {\n            m.put(\"duration\", event.duration());\n        }\n        send(m);\n    }\n\n    private Map<String, Object> processSensitiveData(Map<String, Object> input) {\n        return SensitiveDataMasker.mask(input, sensitiveDataHolder.get());\n    }\n\n    private static Map<String, Object> event(TaskCallEvent event) {\n        Map<String, Object> m = new HashMap<>();\n\n        Step currentStep = event.currentStep();\n        m.put(\"processDefinitionId\", ProcessDefinitionUtils.getCurrentFlowName(event.processDefinition(), currentStep));\n        Location loc = currentStep != null ? currentStep.getLocation() : null;\n        if (loc != null) {\n            m.put(\"fileName\", currentStep.getLocation().fileName());\n            m.put(\"line\", currentStep.getLocation().lineNum());\n            m.put(\"column\", currentStep.getLocation().column());\n        }\n\n        String taskName = event.taskName();\n        m.put(\"description\", \"Task: \" + taskName);\n        m.put(\"name\", taskName);\n\n        if (event.methodName() != null && !\"execute\".equals(event.methodName())) {\n            m.put(\"method\", event.methodName());\n        }\n\n        if (event.threadId().id() != 0) {\n            m.put(\"threadId\", event.threadId().id());\n        }\n\n        m.put(\"correlationId\", event.correlationId());\n        if (event.error() != null) {\n            m.put(\"error\", event.error());\n        }\n        return m;\n    }\n\n    private void send(Map<String, Object> event) {\n        ProcessEventRequest req = new ProcessEventRequest();\n        req.setEventType(\"ELEMENT\"); // TODO should it be in the constants?\n        req.setData(event);\n        req.setEventDate(Instant.now().atOffset(ZoneOffset.UTC));\n\n        eventReportingService.report(req);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private Map<String, Object> asMapOrNull(Object v) {\n        if (v instanceof TaskResult.SimpleResult simpleResult) {\n            return simpleResult.toMap();\n        }\n\n        if (v instanceof Map) {\n            return (Map<String, Object>) v;\n        }\n\n        return null;\n    }\n\n    static Map<String, Object> maskVars(Map<String, Object> vars, Collection<String> blackList) {\n        if (blackList.isEmpty()) {\n            return vars;\n        }\n\n        Map<String, Object> result = new HashMap<>(vars);\n        for (String b : blackList) {\n            String[] path = b.split(\"\\\\.\");\n            if (ConfigurationUtils.has(result, path)) {\n                Map<String, Object> m = ensureModifiable(result, path.length - 1, path);\n                m.put(path[path.length - 1], MASK);\n            }\n        }\n        return result;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Map<String, Object> ensureModifiable(Map<String, Object> m, int depth, String[] path) {\n        if (depth == 0) {\n            return m;\n        }\n\n        for (int i = 0; i < depth; i++) {\n            Object v = m.get(path[i]);\n            if (v == null) {\n                throw new IllegalStateException(\"Can't find variable at \" + path[i]);\n            }\n\n            if (!(v instanceof Map)) {\n                throw new IllegalStateException(\"Not a map variable at \" + path[i]);\n            }\n\n            Map<String, Object> modifiable = new HashMap<>((Map<String, Object>) v);\n            m.put(path[i], modifiable);\n            m = modifiable;\n        }\n\n        return m;\n    }\n\n    private static Map<String, Object> convertInput(List<Object> input) {\n        if (input.isEmpty()) {\n            return Collections.emptyMap();\n        }\n\n        if (input.size() == 1) {\n            if (input.get(0) instanceof Variables) {\n                return ((Variables) input.get(0)).toMap();\n            }\n        }\n\n        Map<String, Object> result = new HashMap<>();\n        for (int i = 0; i < input.size(); i++) {\n            Object arg = input.get(i);\n            if (arg instanceof Context) {\n                arg = \"context\";\n            }\n            result.put(String.valueOf(i), arg);\n        }\n\n        return result;\n    }\n\n    private static List<Object> processSensitiveDataAnnotations(List<Object> input, List<List<Annotation>> annotations) {\n        if (annotations.isEmpty()) {\n            return input;\n        }\n\n        List<Object> result = new ArrayList<>(input);\n        for (int i = 0; i < result.size(); i++) {\n            List<Annotation> a = annotations.get(i);\n            boolean hasSensitiveData = a.stream().anyMatch(v -> v.annotationType() == SensitiveData.class);\n            if (hasSensitiveData) {\n                result.set(i, MASK);\n            }\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/script/DefaultScriptEvaluator.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.script;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.oracle.truffle.js.scriptengine.GraalJSEngineFactory;\nimport com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskProviders;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.UserDefinedException;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.graalvm.polyglot.Engine;\nimport org.graalvm.polyglot.HostAccess;\nimport org.graalvm.polyglot.Value;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.script.*;\nimport java.io.BufferedWriter;\nimport java.io.Reader;\nimport java.io.Writer;\nimport java.util.*;\n\npublic class DefaultScriptEvaluator implements ScriptEvaluator {\n\n    // https://www.graalvm.org/latest/reference-manual/js/JavaScriptCompatibility/#ecmascript-language-compliance\n    static Integer[] GRAAL_ES_VERSIONS = new Integer[]{\n            3,\n            5,\n            6,\n            7,\n            2015,\n            2016,\n            2017,\n            2018,\n            2019,\n            2020,\n            2021,\n            2022,\n    };\n\n\n    private static final Logger log = LoggerFactory.getLogger(DefaultScriptEvaluator.class);\n\n    // TODO: deprecate \"execution\"? what about scripts - can't use \"context\" there?\n    private static final String[] CONTEXT_VARIABLE_NAMES = {Constants.Context.CONTEXT_KEY, \"execution\"};\n\n    private final TaskProviders taskProviders;\n    private final ScriptEngineManager scriptEngineManager;\n\n    @Inject\n    public DefaultScriptEvaluator(TaskProviders taskProviders) {\n        this.taskProviders = taskProviders;\n        this.scriptEngineManager = new ScriptEngineManager();\n    }\n\n    @Override\n    public ScriptResult eval(Context context, String language, Reader input, Map<String, Object> variables) {\n        ScriptEngine engine = getEngine(language, variables);\n\n        if (engine == null) {\n            throw new RuntimeException(\"Script engine not found: \" + language);\n        }\n\n        Bindings b = ScriptEngineBindings.create(engine, language);\n\n        ScriptResult scriptResult = new ScriptResult();\n        ScriptContext ctx = new ScriptContext(context);\n        for (String ctxVar : CONTEXT_VARIABLE_NAMES) {\n            b.put(ctxVar, ctx);\n        }\n        b.put(\"tasks\", new TaskAccessor(taskProviders, ctx));\n        b.put(\"log\", log);\n        b.put(\"isDryRun\", ctx.processConfiguration().dryRun());\n        b.putAll(context.variables().toMap());\n        b.putAll(variables);\n        b.put(\"result\", scriptResult);\n\n        try {\n            engine.eval(input, b);\n            return scriptResult;\n        } catch (ScriptException e) {\n            if (e.getCause() != null) {\n                throw new UserDefinedException(e.getCause().getMessage());\n            }\n            throw new UserDefinedException(e.getMessage());\n        }\n    }\n\n    @Override\n    public String getLanguage(String languageOrExtension) {\n        for (ScriptEngineFactory factory : scriptEngineManager.getEngineFactories()) {\n            try {\n                if (listOrEmpty(factory.getNames()).contains(languageOrExtension)) {\n                    return factory.getLanguageName();\n                }\n            } catch (Exception exp) {\n                // ignore\n            }\n\n            try {\n                if (listOrEmpty(factory.getExtensions()).contains(languageOrExtension)) {\n                    return factory.getLanguageName();\n                }\n            } catch (Exception exp) {\n                // ignore\n            }\n        }\n        return null;\n    }\n\n    @SuppressWarnings({\"rawtypes\", \"unchecked\"})\n    private org.graalvm.polyglot.Context.Builder getGraalEngineContextBuilder(Map<String, Object> variables) {\n        HostAccess access = HostAccess.newBuilder(HostAccess.ALL)\n                .targetTypeMapping(Value.class, Object.class, Value::hasArrayElements, v -> new LinkedList<>(v.as(List.class))).build();\n        org.graalvm.polyglot.Context.Builder ctx = org.graalvm.polyglot.Context.newBuilder(\"js\")\n                .allowHostAccess(access);\n        for (Map.Entry<String, Object> entry : variables.entrySet()) {\n            String key = entry.getKey();\n            Object value = entry.getValue();\n            if (\"esVersion\".equals(key)) {\n                Optional<Integer> esVersion = Arrays.stream(GRAAL_ES_VERSIONS)\n                        .filter(it -> it.equals(value))\n                        .findFirst();\n                if (esVersion.isEmpty()) {\n                    throw new UserDefinedException(\"unsupported esVersion: \" + value.toString());\n                }\n                ctx.option(\"js.ecmascript-version\", esVersion.get().toString());\n            }\n        }\n        return ctx;\n    }\n\n    private ScriptEngine getEngine(String language, Map<String, Object> variables) {\n        ScriptEngine engine;\n        if (new GraalJSEngineFactory().getNames().contains(language)) {\n            // Javascript array is converted in Java to an empty map #214 (https://github.com/oracle/graaljs/issues/214)\n            engine = GraalJSScriptEngine.create(Engine.newBuilder()\n                            .allowExperimentalOptions(true)\n                            .option(\"engine.WarnInterpreterOnly\", \"false\")\n                            .option(\"js.nashorn-compat\", \"true\")\n                            .build(),\n                    getGraalEngineContextBuilder(variables));\n        } else {\n            ScriptEngineProperties.applyFor(language);\n\n            engine = scriptEngineManager.getEngineByName(language);\n        }\n\n        if (engine != null) {\n            engine.getContext().setWriter(new BufferedWriter(new LogWriter()));\n        }\n\n        return engine;\n    }\n\n    private static List<String> listOrEmpty(List<String> items) {\n        if (items == null) {\n            return Collections.emptyList();\n        }\n        return items;\n    }\n\n    public static class TaskAccessor {\n\n        private final TaskProviders tasks;\n        private final Context context;\n\n        public TaskAccessor(TaskProviders tasks, Context context) {\n            this.tasks = tasks;\n            this.context = context;\n        }\n\n        public Object get(String key) {\n            return tasks.createTask(context, key);\n        }\n    }\n\n    private static class LogWriter extends Writer {\n\n        @Override\n        public void write(char[] cbuf, int off, int len) {\n            if (len == 0) {\n                return;\n            }\n\n            int l = cbuf[len - 1] == '\\n' ? len - 1 : len;\n            log.info(\"{}\", new String(cbuf, off, l));\n        }\n\n        @Override\n        public void flush() {\n            //do nothing\n        }\n\n        @Override\n        public void close() {\n            // do nothing\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/script/SanitizedMap.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.script;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Collection;\nimport java.util.Map;\nimport java.util.Set;\n\npublic class SanitizedMap<K, V> implements Map<K, V> {\n\n    private final Map<K, V> delegate;\n\n    public SanitizedMap(Map<K, V> delegate) {\n        this.delegate = delegate;\n    }\n\n    @Override\n    public int size() {\n        return delegate.size();\n    }\n\n    @Override\n    public boolean isEmpty() {\n        return delegate.isEmpty();\n    }\n\n    @Override\n    public boolean containsKey(Object key) {\n        return delegate.containsKey(key);\n    }\n\n    @Override\n    public boolean containsValue(Object value) {\n        return delegate.containsValue(value);\n    }\n\n    @Override\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    public V get(Object key) {\n        V result = delegate.get(key);\n        if (result instanceof Map) {\n            return (V) new SanitizedMap((Map)result);\n        }\n        return result;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    @Override\n    public V put(K key, V value) {\n        V sanitized = (V) VariablesSanitizer.sanitize(value);\n        return delegate.put(key, sanitized);\n    }\n\n    @Override\n    public V remove(Object key) {\n        return delegate.remove(key);\n    }\n\n    @Override\n    public void putAll(Map<? extends K, ? extends V> m) {\n        m.forEach(this::put);\n    }\n\n    @Override\n    public void clear() {\n        delegate.clear();\n    }\n\n    @Override\n    public Set<K> keySet() {\n        return delegate.keySet();\n    }\n\n    @Override\n    public Collection<V> values() {\n        return delegate.values();\n    }\n\n    @Override\n    public Set<Entry<K, V>> entrySet() {\n        return delegate.entrySet();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/script/ScriptContext.java",
    "content": "\npackage com.walmartlabs.concord.runtime.v2.runner.script;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Compiler;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\n\nimport java.io.Serializable;\nimport java.nio.file.Path;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic class ScriptContext implements Context {\n\n    private final Context delegate;\n    private final ScriptVariables variables;\n\n    public ScriptContext(Context delegate) {\n        this.delegate = delegate;\n        this.variables = new ScriptVariables(delegate.variables());\n    }\n\n    @Override\n    public Path workingDirectory() {\n        return delegate.workingDirectory();\n    }\n\n    @Override\n    public UUID processInstanceId() {\n        return delegate.processInstanceId();\n    }\n\n    @Override\n    public Variables variables() {\n        return variables;\n    }\n\n    @Override\n    public Variables defaultVariables() {\n        return delegate.defaultVariables();\n    }\n\n    @Override\n    public FileService fileService() {\n        return delegate.fileService();\n    }\n\n    @Override\n    public DockerService dockerService() {\n        return delegate.dockerService();\n    }\n\n    @Override\n    public SecretService secretService() {\n        return delegate.secretService();\n    }\n\n    @Override\n    public LockService lockService() {\n        return delegate.lockService();\n    }\n\n    @Override\n    public ApiConfiguration apiConfiguration() {\n        return delegate.apiConfiguration();\n    }\n\n    @Override\n    public ProcessConfiguration processConfiguration() {\n        return delegate.processConfiguration();\n    }\n\n    @Override\n    public Execution execution() {\n        return delegate.execution();\n    }\n\n    @Override\n    public Compiler compiler() {\n        return delegate.compiler();\n    }\n\n    @Override\n    public <T> T eval(Object v, Class<T> type) {\n        return delegate.eval(v, type);\n    }\n\n    @Override\n    public <T> T eval(Object v, Map<String, Object> additionalVariables, Class<T> type) {\n        return delegate.eval(v, additionalVariables, type);\n    }\n\n    @Override\n    public void suspend(String eventName) {\n        delegate.suspend(eventName);\n    }\n\n    @Override\n    public void reentrantSuspend(String eventName, Map<String, Serializable> state) {\n        delegate.reentrantSuspend(eventName, state);\n    }\n}"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/script/ScriptEngineBindings.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.script;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.oracle.truffle.js.scriptengine.GraalJSEngineFactory;\n\nimport javax.script.Bindings;\nimport javax.script.ScriptEngine;\n\npublic final class ScriptEngineBindings {\n\n    public static Bindings create(ScriptEngine engine, String language) {\n        Bindings b = engine.createBindings();\n\n        if (new GraalJSEngineFactory().getNames().contains(language)) {\n            b.put(\"polyglot.js.allowAllAccess\", true);\n        }\n        \n        return b;\n    }\n\n    private ScriptEngineBindings() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/script/ScriptEngineProperties.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.script;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic final class ScriptEngineProperties {\n\n    public static void applyFor(String languageName) {\n        if (\"ruby\".equalsIgnoreCase(languageName)) {\n            System.setProperty(\"org.jruby.embed.localcontext.scope\", \"threadsafe\");\n        }\n    }\n\n    private ScriptEngineProperties() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/script/ScriptEvaluator.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.script;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\n\nimport java.io.Reader;\nimport java.util.Map;\n\npublic interface ScriptEvaluator {\n\n    ScriptResult eval(Context context, String language, Reader input, Map<String, Object> variables);\n\n    String getLanguage(String languageOrExtension);\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/script/ScriptResult.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.script;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\npublic class ScriptResult {\n\n    private final Map<String, Object> items = new ConcurrentHashMap<>();\n\n    public ScriptResult set(String key, Object value) {\n        Object sanitized = VariablesSanitizer.sanitize(value);\n        items.put(key, sanitized);\n        return this;\n    }\n\n    public Map<String, Object> items() {\n        return items;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/script/ScriptVariables.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.script;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport java.util.Map;\n\npublic class ScriptVariables implements Variables {\n\n    private final Variables delegate;\n\n    public ScriptVariables(Variables delegate) {\n        this.delegate = delegate;\n    }\n\n    @Override\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    public Object get(String key) {\n        Object result = delegate.get(key);\n        if (result instanceof Map) {\n            return new SanitizedMap((Map) result);\n        }\n\n        return result;\n    }\n\n    @Override\n    public void set(String key, Object value) {\n        Object sanitized = VariablesSanitizer.sanitize(value);\n        delegate.set(key, sanitized);\n    }\n\n    @Override\n    public boolean has(String key) {\n        return delegate.has(key);\n    }\n\n    @Override\n    public Map<String, Object> toMap() {\n        return delegate.toMap();\n    }\n}"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/script/VariablesSanitizer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.script;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic final class VariablesSanitizer {\n\n    @SuppressWarnings(\"unchecked\")\n    public static Object sanitize(Object obj) {\n        if (obj instanceof Set) {\n            Set<Object> c = (Set<Object>) obj;\n            return c.stream()\n                    .map(VariablesSanitizer::sanitize)\n                    .collect(Collectors.toSet());\n        } else if (obj instanceof Collection) {\n            Collection<Object> c = (Collection<Object>) obj;\n            return c.stream()\n                    .map(VariablesSanitizer::sanitize)\n                    .collect(Collectors.toList());\n        } else if (obj instanceof Map) {\n            Map<Object, Object> m = (Map<Object, Object>) obj;\n            Map<Object, Object> result = new LinkedHashMap<>();\n            m.forEach((key, value) -> result.put(sanitize(key), sanitize(value)));\n            return result;\n        } else if (obj instanceof Map.Entry && !(obj instanceof Serializable)) {\n            Map.Entry<?, ?> e = (Map.Entry<?, ?>) obj;\n            return new AbstractMap.SimpleEntry<>(e.getKey(), e.getValue());\n        } else {\n            return obj;\n        }\n    }\n\n    private VariablesSanitizer() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/sdk/ApiClientFactoryImpl.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.client2.ApiClientConfiguration;\nimport com.walmartlabs.concord.client2.ApiClientFactory;\nimport com.walmartlabs.concord.client2.DefaultApiClientFactory;\nimport com.walmartlabs.concord.runtime.common.injector.InstanceId;\nimport com.walmartlabs.concord.runtime.v2.sdk.ApiConfiguration;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport java.time.Duration;\nimport java.util.UUID;\n\n@Named\n@Singleton\npublic class ApiClientFactoryImpl implements ApiClientFactory {\n\n    private final ApiConfiguration cfg;\n    private final InstanceId instanceId;\n\n    private final DefaultApiClientFactory clientFactory;\n\n    @Inject\n    public ApiClientFactoryImpl(ApiConfiguration cfg, InstanceId instanceId) {\n        this.cfg = cfg;\n        this.instanceId = instanceId;\n        this.clientFactory = new DefaultApiClientFactory(cfg.baseUrl(), Duration.ofMillis(cfg.connectTimeout()));\n    }\n\n    @Override\n    public ApiClient create(ApiClientConfiguration overrides) {\n        ApiClient client = this.clientFactory.create(overrides)\n                .setReadTimeout(Duration.ofMillis(cfg.readTimeout()));\n\n        UUID txId = instanceId.getValue();\n        if (txId != null) {\n            client = client.setUserAgent(\"Concord-Runner-v2: txId=\" + txId);\n        }\n\n        return client;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/sdk/ApiConfigurationImpl.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.runtime.v2.sdk.ApiConfiguration;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\n\n@Named\npublic class ApiConfigurationImpl implements ApiConfiguration {\n\n    private final RunnerConfiguration runnerCfg;\n\n    @Inject\n    public ApiConfigurationImpl(RunnerConfiguration runnerCfg) {\n        this.runnerCfg = runnerCfg;\n    }\n\n    @Override\n    public String baseUrl() {\n        return runnerCfg.api().baseUrl();\n    }\n\n    @Override\n    public int connectTimeout() {\n        return runnerCfg.api().connectTimeout();\n    }\n\n    @Override\n    public int readTimeout() {\n        return runnerCfg.api().readTimeout();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/sdk/ApiConfigurationV1Impl.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.sdk.ApiConfiguration;\nimport com.walmartlabs.concord.sdk.Context;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\n\n@Named\npublic class ApiConfigurationV1Impl implements ApiConfiguration {\n\n    private final RunnerConfiguration runnerCfg;\n\n    @Inject\n    public ApiConfigurationV1Impl(RunnerConfiguration runnerCfg) {\n        this.runnerCfg = runnerCfg;\n    }\n\n    @Override\n    public String getBaseUrl() {\n        return runnerCfg.api().baseUrl();\n    }\n\n    @Override\n    public int connectTimeout() {\n        return runnerCfg.api().connectTimeout();\n    }\n\n    @Override\n    public int readTimeout() {\n        return runnerCfg.api().readTimeout();\n    }\n\n    @Override\n    public String getSessionToken(Context ctx) {\n        return null;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/tasks/ContextProvider.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.tasks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ThreadLocalStack;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\n\nimport javax.inject.Provider;\nimport java.util.concurrent.Callable;\n\npublic class ContextProvider implements Provider<Context> {\n\n    private static final ThreadLocalStack<Context> value = new ThreadLocalStack<>();\n\n    public static <T, C extends Context> T withContext(C ctx, Callable<T> callable) {\n        set(ctx);\n        try {\n            return callable.call();\n        } catch (RuntimeException e) {\n            throw e;\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        } finally {\n            clear();\n        }\n    }\n\n    public static <C extends Context> void withContext(C ctx, Runnable runnable) {\n        set(ctx);\n        try {\n            runnable.run();\n        } finally {\n            clear();\n        }\n    }\n\n    @Override\n    public Context get() {\n        Context ctx = value.peek();\n        if (ctx == null) {\n            throw new IllegalStateException(\"No context available. This is most likely a bug.\");\n        }\n\n        return ctx;\n    }\n\n    private static void set(Context ctx) {\n        value.push(ctx);\n    }\n\n    private static void clear() {\n        value.pop();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/tasks/TaskCallEvent.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.tasks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AllowNulls;\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.svm.ThreadId;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.lang.annotation.Annotation;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface TaskCallEvent {\n\n    Phase phase();\n\n    ProcessDefinition processDefinition();\n\n    Step currentStep();\n\n    String taskName();\n\n    String methodName();\n\n    ThreadId threadId();\n\n    @AllowNulls\n    @Value.Default\n    default List<Object> input() {\n        return Collections.emptyList();\n    }\n\n    @AllowNulls\n    @Value.Default\n    default List<List<Annotation>> inputAnnotations() {\n        return Collections.emptyList();\n    }\n\n    UUID correlationId();\n\n    @Nullable\n    Long duration();\n\n    @Nullable\n    Serializable result();\n\n    @Nullable\n    String error();\n\n    @Value.Default\n    @AllowNulls\n    default Map<String, Serializable> meta() {\n        return Collections.emptyMap();\n    }\n\n    static ImmutableTaskCallEvent.Builder builder() {\n        return ImmutableTaskCallEvent.builder();\n    }\n\n    enum Phase {\n\n        PRE,\n        POST\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/tasks/TaskCallInterceptor.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.tasks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.sun.el.util.ReflectionUtil;\nimport com.walmartlabs.concord.common.AllowNulls;\nimport com.walmartlabs.concord.runtime.v2.model.AbstractStep;\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallEvent.Phase;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport com.walmartlabs.concord.svm.ThreadId;\nimport org.immutables.value.Value;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\nimport java.lang.annotation.Annotation;\nimport java.util.*;\nimport java.util.concurrent.Callable;\nimport java.util.stream.Collectors;\n\n/**\n * Intercepts task calls and notifies {@link TaskCallListener}s.\n */\npublic class TaskCallInterceptor {\n\n    private final Set<TaskCallListener> listeners;\n\n    @Inject\n    public TaskCallInterceptor(Set<TaskCallListener> listeners) {\n        this.listeners = listeners;\n    }\n\n    public <T> T invoke(CallContext ctx, Method method, Callable<T> callable) throws TaskException {\n        // record the PRE event\n        var preEvent = eventBuilder(Phase.PRE, method, ctx).build();\n        listeners.forEach(l -> l.onEvent(preEvent));\n\n        // call the callable and measure the duration\n        T result = null;\n        Exception error = null;\n        var startedAt = System.currentTimeMillis();\n        try {\n            result = callable.call();\n        } catch (Exception e) {\n            error = e;\n        }\n        var duration = System.currentTimeMillis() - startedAt;\n\n        // record the POST event\n        var postEvent = eventBuilder(Phase.POST, method, ctx)\n                .error(errorMessage(error))\n                .duration(duration)\n                .result(result instanceof Serializable ? (Serializable) result : null)\n                .build();\n        listeners.forEach(l -> l.onEvent(postEvent));\n\n        if (error != null) {\n            throw new TaskException(error);\n        }\n\n        return result;\n    }\n\n    private static String errorMessage(Exception e) {\n        if (e == null) {\n            return null;\n        }\n\n        if (e.getMessage() != null) {\n            return e.getMessage();\n        }\n\n        return \"Error type: \" + e.getClass().getName();\n    }\n\n    private static ImmutableTaskCallEvent.Builder eventBuilder(Phase phase, Method method, CallContext ctx) {\n        return TaskCallEvent.builder()\n                .phase(phase)\n                .threadId(ctx.threadId())\n                .correlationId(ctx.correlationId())\n                .currentStep(ctx.currentStep())\n                .input(method.arguments())\n                .inputAnnotations(method.annotations())\n                .methodName(method.name())\n                .processDefinition(ctx.processDefinition())\n                .taskName(ctx.taskName())\n                .meta(((AbstractStep)ctx.currentStep()).getOptions().meta());\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    public interface Method {\n\n        String name();\n\n        @AllowNulls\n        @Value.Default\n        default List<Object> arguments() {\n            return List.of();\n        }\n\n        @AllowNulls\n        @Value.Default\n        default List<List<Annotation>> annotations() {\n            return List.of();\n        }\n\n        static Method of(Class<? extends Task> taskClass, String methodName, List<Object> params) throws javax.el.MethodNotFoundException {\n            List<List<Annotation>> annotations = List.of();\n            var m = ReflectionUtil.findMethod(taskClass, methodName, null, params.toArray());\n            if (m != null && !m.isVarArgs()) {\n                annotations = Arrays.stream(m.getParameterAnnotations())\n                        .map(Arrays::asList)\n                        .collect(Collectors.toList());\n            }\n            return ImmutableMethod.builder()\n                    .name(methodName)\n                    .arguments(params)\n                    .annotations(annotations)\n                    .build();\n        }\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    public interface CallContext {\n\n        String taskName();\n\n        UUID correlationId();\n\n        Step currentStep();\n\n        ProcessDefinition processDefinition();\n\n        ThreadId threadId();\n\n        static ImmutableCallContext.Builder builder() {\n            return ImmutableCallContext.builder();\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/tasks/TaskCallListener.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.tasks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface TaskCallListener {\n\n    void onEvent(TaskCallEvent event);\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/tasks/TaskCallPolicyChecker.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.tasks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.policyengine.CheckResult;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.policyengine.TaskRule;\nimport com.walmartlabs.concord.runtime.v2.runner.TaskResultService;\nimport com.walmartlabs.concord.runtime.v2.sdk.UserDefinedException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\n\npublic class TaskCallPolicyChecker implements TaskCallListener {\n\n    private static final Logger log = LoggerFactory.getLogger(TaskCallPolicyChecker.class);\n\n    private final PolicyEngine policyEngine;\n    private final TaskResultService taskResultService;\n\n    @Inject\n    public TaskCallPolicyChecker(PolicyEngine policyEngine, TaskResultService taskResultService) {\n        this.policyEngine = policyEngine;\n        this.taskResultService = taskResultService;\n    }\n\n    @Override\n    public void onEvent(TaskCallEvent event) {\n        if (event.phase() == TaskCallEvent.Phase.POST) {\n            return;\n        }\n\n        CheckResult<TaskRule, String> result = policyEngine.getTaskPolicy().check(\n                event.taskName(),\n                event.methodName(),\n                event.input().toArray(),\n                taskResultService.getResults());\n\n        result.getWarn().forEach(d -> log.warn(\"Potentially restricted task call '{}' (task policy {})\", event.taskName(), d.getRule()));\n        result.getDeny().forEach(d -> log.error(\"Task call '{}' is forbidden by the task policy {}\", event.taskName(), d.getRule()));\n\n        if (!result.getDeny().isEmpty()) {\n            throw new UserDefinedException(\"Found forbidden tasks\");\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/tasks/TaskException.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.tasks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class TaskException extends Exception {\n\n    // for backward compatibility (java8 concord 1.92.0 version)\n    private static final long serialVersionUID = -5809134340125047859L;\n\n    public TaskException(Exception cause) {\n        super(cause);\n    }\n\n    @Override\n    public synchronized Exception getCause() {\n        return (Exception) super.getCause();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/tasks/TaskProviders.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.tasks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskProvider;\nimport org.eclipse.sisu.Priority;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport java.util.*;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport java.util.stream.Collectors;\n\n/**\n * Container for all registered {@link TaskProvider} instances.\n */\n@Named\n@Singleton\npublic class TaskProviders {\n\n    private final List<TaskProvider> taskProviders;\n\n    public TaskProviders() {\n        this(Collections.emptySet());\n    }\n\n    @Inject\n    public TaskProviders(Set<TaskProvider> taskProviders) {\n        this.taskProviders = taskProviders.stream()\n                .sorted(Comparator.comparingInt(TaskProviders::getPriority))\n                .collect(Collectors.toCollection(CopyOnWriteArrayList::new));\n    }\n\n    public Task createTask(Context ctx, String key) {\n        for (TaskProvider p : taskProviders) {\n            Task t = p.createTask(ctx, key);\n            if (t != null) {\n                return t;\n            }\n        }\n\n        return null;\n    }\n\n    public Class<? extends Task> getTaskClass(Context ctx, String key) {\n        for (TaskProvider p : taskProviders) {\n            Class<? extends Task> taskClass = p.getTaskClass(ctx, key);\n            if (taskClass != null) {\n                return taskClass;\n            }\n        }\n\n        return null;\n    }\n\n    public boolean hasTask(String key) {\n        for (TaskProvider p : taskProviders) {\n            if (p.hasTask(key)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public Set<String> names() {\n        Set<String> result = new HashSet<>();\n        for (TaskProvider p : taskProviders) {\n            result.addAll(p.names());\n        }\n        return result;\n    }\n\n    private static int getPriority(TaskProvider p) {\n        Class<? extends TaskProvider> klass = p.getClass();\n        Priority priority = klass.getDeclaredAnnotation(Priority.class);\n        if (priority == null) {\n            return 0;\n        }\n        return priority.value();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/tasks/TaskResultListener.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.tasks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.runtime.v2.runner.TaskResultService;\n\nimport javax.inject.Inject;\nimport java.util.Set;\n\npublic class TaskResultListener implements TaskCallListener {\n\n    private final Set<String> tasksForCollect;\n    private final TaskResultService taskResults;\n\n    @Inject\n    public TaskResultListener(PolicyEngine policyEngine, TaskResultService taskResults) {\n        this.tasksForCollect = policyEngine.getTaskPolicy().getTaskResults();\n        this.taskResults = taskResults;\n    }\n\n    @Override\n    public void onEvent(TaskCallEvent event) {\n        if (event.phase() == TaskCallEvent.Phase.PRE || event.result() == null) {\n            return;\n        }\n\n        if (!tasksForCollect.contains(event.taskName())) {\n            return;\n        }\n\n        taskResults.store(event.taskName(), event.result());\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/tasks/TaskSchemaLookupResult.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.tasks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.networknt.schema.JsonSchema;\n\nimport java.util.List;\n\n/**\n * Result of looking up and compiling one section of a task schema resource.\n *\n * @param status       lookup status\n * @param schema       compiled JSON schema when {@link Status#FOUND}\n * @param rawSchema    raw task schema resource, when available\n * @param resourceName schema resource name\n * @param errors       lookup or compile errors when {@link Status#INVALID}\n */\npublic record TaskSchemaLookupResult(\n        Status status,\n        JsonSchema schema,\n        JsonNode rawSchema,\n        String resourceName,\n        List<String> errors\n) {\n\n    public enum Status {\n        /**\n         * No task schema resource exists.\n         */\n        ABSENT,\n        /**\n         * Schema resource exists, but the requested top-level section is not declared.\n         */\n        NO_SECTION,\n        /**\n         * Schema resource or requested section exists but cannot be parsed or compiled.\n         */\n        INVALID,\n        /**\n         * Requested section was found and compiled.\n         */\n        FOUND\n    }\n\n    public TaskSchemaLookupResult {\n        errors = errors != null ? List.copyOf(errors) : List.of();\n    }\n\n    public boolean hasErrors() {\n        return status == Status.INVALID && !errors.isEmpty();\n    }\n\n    public static TaskSchemaLookupResult absent(String resourceName) {\n        return new TaskSchemaLookupResult(Status.ABSENT, null, null, resourceName, List.of());\n    }\n\n    public static TaskSchemaLookupResult noSection(JsonNode rawSchema, String resourceName) {\n        return new TaskSchemaLookupResult(Status.NO_SECTION, null, rawSchema, resourceName, List.of());\n    }\n\n    public static TaskSchemaLookupResult invalid(JsonNode rawSchema, String resourceName, List<String> errors) {\n        return new TaskSchemaLookupResult(Status.INVALID, null, rawSchema, resourceName, errors);\n    }\n\n    public static TaskSchemaLookupResult found(JsonSchema schema, JsonNode rawSchema, String resourceName) {\n        return new TaskSchemaLookupResult(Status.FOUND, schema, rawSchema, resourceName, List.of());\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/tasks/TaskSchemaRegistry.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.tasks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.networknt.schema.JsonSchema;\nimport com.networknt.schema.JsonSchemaFactory;\nimport com.networknt.schema.SpecVersion;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ConcurrentMap;\n\n/**\n * Registry for task JSON schemas.\n * Schemas are loaded from resource files next to task classes.\n * Schema authoring convention:\n * <ul>\n *     <li>resource name: {@code <task-name>.schema.json}</li>\n *     <li>resource location: same package as the task class</li>\n *     <li>JSON Schema draft: draft-07</li>\n *     <li>top-level validation sections: {@code in} and {@code out}</li>\n *     <li>shared definitions: {@code definitions} and {@code $defs}</li>\n * </ul>\n * <p>\n * Only JSON Schema draft-07 is supported.\n */\n@Named\n@Singleton\npublic class TaskSchemaRegistry {\n\n    private static final Logger log = LoggerFactory.getLogger(TaskSchemaRegistry.class);\n\n    /**\n     * Cache entry holding both raw schema and compiled in/out schemas.\n     */\n    private record CachedSchema(\n            JsonNode rawSchema,\n            SectionSchema inSchema,\n            SectionSchema outSchema,\n            String resourceName\n    ) {\n\n        private TaskSchemaLookupResult section(String section) {\n            return switch (section) {\n                case \"in\" -> inSchema.toLookupResult(rawSchema, resourceName);\n                case \"out\" -> outSchema.toLookupResult(rawSchema, resourceName);\n                default -> throw new IllegalArgumentException(\"Unknown schema section: \" + section);\n            };\n        }\n\n        private static CachedSchema absent(String resourceName) {\n            SectionSchema absent = SectionSchema.absent();\n            return new CachedSchema(null, absent, absent, resourceName);\n        }\n\n        private static CachedSchema invalid(String resourceName, List<String> errors) {\n            SectionSchema invalid = SectionSchema.invalid(errors);\n            return new CachedSchema(null, invalid, invalid, resourceName);\n        }\n    }\n\n    private record SectionSchema(\n            TaskSchemaLookupResult.Status status,\n            JsonSchema schema,\n            List<String> errors\n    ) {\n\n        private SectionSchema {\n            errors = errors != null ? List.copyOf(errors) : List.of();\n        }\n\n        private TaskSchemaLookupResult toLookupResult(JsonNode rawSchema, String resourceName) {\n            return switch (status) {\n                case ABSENT -> TaskSchemaLookupResult.absent(resourceName);\n                case NO_SECTION -> TaskSchemaLookupResult.noSection(rawSchema, resourceName);\n                case INVALID -> TaskSchemaLookupResult.invalid(rawSchema, resourceName, errors);\n                case FOUND -> TaskSchemaLookupResult.found(schema, rawSchema, resourceName);\n            };\n        }\n\n        private static SectionSchema absent() {\n            return new SectionSchema(TaskSchemaLookupResult.Status.ABSENT, null, List.of());\n        }\n\n        private static SectionSchema noSection() {\n            return new SectionSchema(TaskSchemaLookupResult.Status.NO_SECTION, null, List.of());\n        }\n\n        private static SectionSchema invalid(List<String> errors) {\n            return new SectionSchema(TaskSchemaLookupResult.Status.INVALID, null, errors);\n        }\n\n        private static SectionSchema found(JsonSchema schema) {\n            return new SectionSchema(TaskSchemaLookupResult.Status.FOUND, schema, List.of());\n        }\n    }\n\n    private record SchemaKey(String taskName, Class<? extends Task> taskClass) {}\n\n    private final ConcurrentMap<SchemaKey, CachedSchema> cache = new ConcurrentHashMap<>();\n    private final ObjectMapper objectMapper;\n    private final JsonSchemaFactory schemaFactory;\n\n    @Inject\n    public TaskSchemaRegistry(ObjectMapper objectMapper) {\n        this.objectMapper = Objects.requireNonNull(objectMapper);\n        this.schemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);\n    }\n\n    /**\n     * Get the compiled input schema for a task.\n     *\n     * @param taskName  the task name\n     * @param taskClass the resolved task class\n     * @return the input schema lookup result\n     */\n    public TaskSchemaLookupResult getInputSchema(String taskName, Class<? extends Task> taskClass) {\n        return getSection(taskName, taskClass, \"in\");\n    }\n\n    /**\n     * Get the compiled output schema for a task.\n     *\n     * @param taskName  the task name\n     * @param taskClass the resolved task class\n     * @return the output schema lookup result\n     */\n    public TaskSchemaLookupResult getOutputSchema(String taskName, Class<? extends Task> taskClass) {\n        return getSection(taskName, taskClass, \"out\");\n    }\n\n    /**\n     * Get the raw task schema resource for future docs/UI use.\n     *\n     * @param taskName  the task name\n     * @param taskClass the resolved task class\n     * @return raw task schema resource, if present and parseable\n     */\n    public Optional<JsonNode> getRawSchema(String taskName, Class<? extends Task> taskClass) {\n        if (taskClass == null) {\n            return Optional.empty();\n        }\n\n        return Optional.ofNullable(cache.computeIfAbsent(new SchemaKey(taskName, taskClass), this::loadSchema)\n                .rawSchema());\n    }\n\n    /**\n     * Get the ObjectMapper used by this registry.\n     * Shared with TaskSchemaValidator to avoid duplicate instances.\n     *\n     * @return the ObjectMapper\n     */\n    public ObjectMapper getObjectMapper() {\n        return objectMapper;\n    }\n\n    private TaskSchemaLookupResult getSection(String taskName, Class<? extends Task> taskClass, String section) {\n        if (taskClass == null) {\n            log.debug(\"Task class not found for '{}', no schema available\", taskName);\n            return TaskSchemaLookupResult.absent(resourceName(taskName));\n        }\n\n        return cache.computeIfAbsent(new SchemaKey(taskName, taskClass), this::loadSchema)\n                .section(section);\n    }\n\n    private CachedSchema loadSchema(SchemaKey key) {\n        String taskName = key.taskName();\n        Class<? extends Task> taskClass = key.taskClass();\n        String resourceName = taskName + \".schema.json\";\n        try (InputStream is = taskClass.getResourceAsStream(resourceName)) {\n            if (is == null) {\n                log.debug(\"No schema found for task '{}' (resource: {})\", taskName, resourceName);\n                return CachedSchema.absent(resourceName);\n            }\n\n            JsonNode rawSchema = objectMapper.readTree(is);\n            if (!rawSchema.isObject()) {\n                String msg = \"Schema resource '\" + resourceName + \"' for task '\" + taskName + \"' must be a JSON object\";\n                log.warn(msg);\n                return CachedSchema.invalid(resourceName, List.of(msg));\n            }\n\n            log.debug(\"Loaded schema for task '{}' from {}\", taskName, resourceName);\n\n            SectionSchema inSchema = compileSection(rawSchema, \"in\", taskName, resourceName);\n            SectionSchema outSchema = compileSection(rawSchema, \"out\", taskName, resourceName);\n\n            return new CachedSchema(rawSchema, inSchema, outSchema, resourceName);\n        } catch (IOException e) {\n            String msg = \"Failed to load schema resource '\" + resourceName + \"' for task '\" + taskName + \"': \" + e.getMessage();\n            log.warn(msg);\n            return CachedSchema.invalid(resourceName, List.of(msg));\n        }\n    }\n\n    private SectionSchema compileSection(JsonNode rawSchema, String section, String taskName, String resourceName) {\n        JsonNode sectionSchema = rawSchema.get(section);\n        if (sectionSchema == null || sectionSchema.isNull()) {\n            log.debug(\"No '{}' section in schema for task '{}'\", section, taskName);\n            return SectionSchema.noSection();\n        }\n\n        if (!sectionSchema.isObject()) {\n            String msg = \"Schema resource '\" + resourceName + \"' section '\" + section + \"' for task '\" + taskName + \"' must be a JSON object\";\n            log.warn(msg);\n            return SectionSchema.invalid(List.of(msg));\n        }\n\n        try {\n            // Build a new schema that includes root definitions for $ref resolution\n            ObjectNode newSchema = objectMapper.createObjectNode();\n\n            copyIfPresent(rawSchema, newSchema, \"$schema\");\n            copyIfPresent(rawSchema, newSchema, \"$id\");\n            copyIfPresent(rawSchema, newSchema, \"definitions\");\n            copyIfPresent(rawSchema, newSchema, \"$defs\");\n\n            sectionSchema.fields().forEachRemaining(entry ->\n                newSchema.set(entry.getKey(), entry.getValue()));\n\n            return SectionSchema.found(schemaFactory.getSchema(newSchema));\n        } catch (Exception e) {\n            String msg = \"Failed to compile schema resource '\" + resourceName + \"' section '\" + section + \"' for task '\" + taskName + \"': \" + e.getMessage();\n            log.warn(msg);\n            return SectionSchema.invalid(List.of(msg));\n        }\n    }\n\n    private void copyIfPresent(JsonNode from, ObjectNode to, String field) {\n        JsonNode value = from.get(field);\n        if (value != null && !value.isNull()) {\n            to.set(field, value);\n        }\n    }\n\n    private static String resourceName(String taskName) {\n        return taskName + \".schema.json\";\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/tasks/TaskSchemaValidationException.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.tasks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.UserDefinedException;\n\nimport java.io.Serial;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Exception thrown when task schema validation fails in FAIL mode.\n */\npublic class TaskSchemaValidationException extends UserDefinedException {\n\n    @Serial\n    private static final long serialVersionUID = 1L;\n\n    private final String taskName;\n    private final String section;\n    private final String schemaResource;\n    private final List<String> validationErrors;\n\n    public TaskSchemaValidationException(String taskName, String section, List<String> validationErrors) {\n        this(taskName, section, null, validationErrors);\n    }\n\n    public TaskSchemaValidationException(String taskName, String section, String schemaResource, List<String> validationErrors) {\n        super(formatMessage(taskName, section, schemaResource, validationErrors), details(taskName, section, schemaResource, validationErrors));\n        this.taskName = taskName;\n        this.section = section;\n        this.schemaResource = schemaResource;\n        this.validationErrors = List.copyOf(validationErrors);\n    }\n\n    public String getTaskName() {\n        return taskName;\n    }\n\n    public String getSection() {\n        return section;\n    }\n\n    public String getSchemaResource() {\n        return schemaResource;\n    }\n\n    public List<String> getValidationErrors() {\n        return validationErrors;\n    }\n\n    private static Map<String, Object> details(String taskName, String section, String schemaResource, List<String> errors) {\n        Map<String, Object> result = new HashMap<>();\n        result.put(\"taskName\", taskName);\n        result.put(\"section\", section);\n        result.put(\"errors\", errors);\n        if (schemaResource != null) {\n            result.put(\"schemaResource\", schemaResource);\n        }\n        return result;\n    }\n\n    private static String formatMessage(String taskName, String section, String schemaResource, List<String> errors) {\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"Task '\").append(taskName).append(\"' \").append(section).append(\" validation failed\");\n        if (schemaResource != null) {\n            sb.append(\" (schema: \").append(schemaResource).append(\")\");\n        }\n        sb.append(\":\");\n        for (String error : errors) {\n            sb.append(\"\\n  - \").append(error);\n        }\n        return sb.toString();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/tasks/TaskSchemaValidationResult.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.tasks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.List;\n\n/**\n * Result of task schema validation.\n *\n * @param status         the validation status\n * @param schemaResource task schema resource used for validation, if any\n * @param errors         list of validation error messages\n */\npublic record TaskSchemaValidationResult(\n        Status status,\n        String schemaResource,\n        List<String> errors\n) {\n\n    public enum Status {\n        /**\n         * No schema found for task\n         **/\n        NO_SCHEMA,\n        /**\n         * Schema found but section (in/out) not defined\n         **/\n        SKIPPED,\n        /**\n         * Validation passed\n         **/\n        VALID,\n        /**\n         * Validation failed with errors\n         **/\n        INVALID\n    }\n\n    public TaskSchemaValidationResult {\n        errors = errors != null ? List.copyOf(errors) : List.of();\n    }\n\n    public boolean hasErrors() {\n        return status == Status.INVALID && !errors.isEmpty();\n    }\n\n    public static TaskSchemaValidationResult noSchema() {\n        return new TaskSchemaValidationResult(Status.NO_SCHEMA, null, List.of());\n    }\n\n    public static TaskSchemaValidationResult skipped() {\n        return new TaskSchemaValidationResult(Status.SKIPPED, null, List.of());\n    }\n\n    public static TaskSchemaValidationResult skipped(String schemaResource) {\n        return new TaskSchemaValidationResult(Status.SKIPPED, schemaResource, List.of());\n    }\n\n    public static TaskSchemaValidationResult valid(String schemaResource) {\n        return new TaskSchemaValidationResult(Status.VALID, schemaResource, List.of());\n    }\n\n    public static TaskSchemaValidationResult invalid(List<String> errors) {\n        return invalid(null, errors);\n    }\n\n    public static TaskSchemaValidationResult invalid(String schemaResource, List<String> errors) {\n        return new TaskSchemaValidationResult(Status.INVALID, schemaResource, errors);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/tasks/TaskSchemaValidator.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.tasks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.networknt.schema.JsonSchema;\nimport com.networknt.schema.ValidationMessage;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * Validates task input and output parameters against JSON schemas.\n * <p>\n * Only JSON Schema draft-07 is supported.\n */\n@Named\n@Singleton\npublic class TaskSchemaValidator {\n\n    private static final Logger log = LoggerFactory.getLogger(TaskSchemaValidator.class);\n\n    private final TaskSchemaRegistry registry;\n    private final ObjectMapper objectMapper;\n\n    @Inject\n    public TaskSchemaValidator(TaskSchemaRegistry registry) {\n        this.registry = registry;\n        this.objectMapper = registry.getObjectMapper();\n    }\n\n    /**\n     * Validate task input parameters.\n     *\n     * @param taskName  the task name\n     * @param taskClass the resolved task class\n     * @param input     the input parameters\n     * @return validation result\n     */\n    public TaskSchemaValidationResult validateInput(String taskName, Class<? extends Task> taskClass, Map<String, Object> input) {\n        TaskSchemaLookupResult schema = registry.getInputSchema(taskName, taskClass);\n        return validate(taskName, \"in\", schema, input);\n    }\n\n    /**\n     * Validate task output parameters.\n     *\n     * @param taskName  the task name\n     * @param taskClass the resolved task class\n     * @param output    the output parameters\n     * @return validation result\n     */\n    public TaskSchemaValidationResult validateOutput(String taskName, Class<? extends Task> taskClass, Map<String, Object> output) {\n        TaskSchemaLookupResult schema = registry.getOutputSchema(taskName, taskClass);\n        return validate(taskName, \"out\", schema, output);\n    }\n\n    private TaskSchemaValidationResult validate(String taskName, String section, TaskSchemaLookupResult lookupResult, Map<String, Object> data) {\n        if (lookupResult.status() == TaskSchemaLookupResult.Status.ABSENT) {\n            return TaskSchemaValidationResult.noSchema();\n        }\n        if (lookupResult.status() == TaskSchemaLookupResult.Status.NO_SECTION) {\n            return TaskSchemaValidationResult.skipped(lookupResult.resourceName());\n        }\n        if (lookupResult.status() == TaskSchemaLookupResult.Status.INVALID) {\n            return TaskSchemaValidationResult.invalid(lookupResult.resourceName(), lookupResult.errors());\n        }\n\n        JsonSchema schema = lookupResult.schema();\n        try {\n            JsonNode dataNode = objectMapper.valueToTree(data);\n\n            Set<ValidationMessage> errors = schema.validate(dataNode);\n            if (errors.isEmpty()) {\n                return TaskSchemaValidationResult.valid(lookupResult.resourceName());\n            }\n\n            List<String> errorMessages = errors.stream()\n                    .map(ValidationMessage::getMessage)\n                    .toList();\n\n            log.debug(\"Validation errors for task '{}' {}: {}\", taskName, section, errorMessages);\n            return TaskSchemaValidationResult.invalid(lookupResult.resourceName(), errorMessages);\n        } catch (Exception e) {\n            log.warn(\"Failed to validate task '{}' {}: {}\", taskName, section, e.getMessage());\n            return TaskSchemaValidationResult.invalid(lookupResult.resourceName(), List.of(\"Schema validation error: \" + e.getMessage()));\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/tasks/TaskV2Provider.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.tasks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Injector;\nimport com.walmartlabs.concord.runtime.common.injector.TaskHolder;\nimport com.walmartlabs.concord.runtime.v2.model.AbstractStep;\nimport com.walmartlabs.concord.runtime.v2.runner.DefaultTaskVariablesService;\nimport com.walmartlabs.concord.runtime.v2.runner.context.TaskContext;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.StepOptionsUtils;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\n\nimport javax.inject.Inject;\nimport java.util.Map;\nimport java.util.Set;\n\npublic class TaskV2Provider implements TaskProvider {\n\n    private final Injector injector;\n    private final TaskHolder<Task> holder;\n    private final DefaultTaskVariablesService defaultTaskVariables;\n\n    @Inject\n    public TaskV2Provider(Injector injector,\n                          TaskHolder<Task> holder,\n                          DefaultTaskVariablesService defaultTaskVariables) {\n\n        this.injector = injector;\n        this.holder = holder;\n        this.defaultTaskVariables = defaultTaskVariables;\n    }\n\n    @Override\n    public Task createTask(Context ctx, String key) {\n        Class<? extends Task> klass = holder.get(key);\n        if (klass == null) {\n            return null;\n        }\n\n        boolean dryRun = ctx.processConfiguration().dryRun();\n        if (dryRun && !(isStepDryRunReady(ctx) || klass.getAnnotation(DryRunReady.class) != null)) {\n            throw new UserDefinedException(\"Dry-run mode is not supported for '\" + key + \"' task (yet)\");\n        }\n\n        Map<String, Object> defaultVariables = defaultTaskVariables.get(key);\n        TaskContext taskContext = new TaskContext(ctx, new MapBackedVariables(defaultVariables));\n        return ContextProvider.withContext(taskContext, () -> injector.getInstance(klass));\n    }\n\n    @Override\n    public Class<? extends Task> getTaskClass(Context ctx, String key) {\n        return holder.get(key);\n    }\n\n    @Override\n    public boolean hasTask(String key) {\n        return holder.get(key) != null;\n    }\n\n    @Override\n    public Set<String> names() {\n        return holder.keys();\n    }\n\n    private static boolean isStepDryRunReady(Context ctx) {\n        var step = ctx.execution().currentStep();\n        if (step == null) {\n            return false;\n        }\n\n        if (!(step instanceof AbstractStep)) {\n            return false;\n        }\n\n        return StepOptionsUtils.isDryRunReady(ctx, ((AbstractStep<?>) step).getOptions());\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/tasks/V2.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.tasks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.BindingAnnotation;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n// TODO we might not need it if we instantiate TaskProvider directly\n@Retention(RetentionPolicy.RUNTIME)\n@BindingAnnotation\npublic @interface V2 {\n}\n\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/BlockCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Adds specified {@link #commands} to the stack.\n */\npublic class BlockCommand implements Command {\n\n    private static final long serialVersionUID = 1L;\n\n    private final List<Command> commands;\n\n    public BlockCommand(Command... commands) {\n        this(Arrays.asList(commands));\n    }\n\n    public BlockCommand(List<Command> commands) {\n        this.commands = commands;\n    }\n\n    @Override\n    public void eval(Runtime runtime, State state, ThreadId threadId) {\n        Frame frame = state.peekFrame(threadId);\n        frame.pop();\n\n        // sequential execution is very simple: we just need to add\n        // each command of the block onto the stack\n\n        List<Command> l = new ArrayList<>(commands);\n\n        // to preserve the original order the commands must be added onto\n        // the stack in the reversed order\n        Collections.reverse(l);\n        l.forEach(frame::push);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/CheckpointCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.Checkpoint;\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.v2.runner.ProcessSnapshot;\nimport com.walmartlabs.concord.runtime.v2.runner.SynchronizationService;\nimport com.walmartlabs.concord.runtime.v2.runner.checkpoints.CheckpointService;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContextFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.ExpressionEvaluator;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.State;\nimport com.walmartlabs.concord.svm.ThreadId;\n\npublic class CheckpointCommand extends StepCommand<Checkpoint> implements ElementEventProducer {\n\n    private static final long serialVersionUID = 1L;\n\n    public CheckpointCommand(Checkpoint step) {\n        super(step);\n    }\n\n    @Override\n    public String getDescription(State state, ThreadId threadId) {\n        return \"Checkpoint: \" + getStep().getName();\n    }\n\n    @Override\n    protected void execute(Runtime runtime, State state, ThreadId threadId) {\n        state.peekFrame(threadId).pop();\n\n        // eval the name in case it contains an expression\n        Context ctx = runtime.getService(Context.class);\n        EvalContextFactory ecf = runtime.getService(EvalContextFactory.class);\n        ExpressionEvaluator ee = runtime.getService(ExpressionEvaluator.class);\n        String name = ee.eval(ecf.global(ctx), getStep().getName(), String.class);\n\n        runtime.getService(SynchronizationService.class).point(() -> {\n            CheckpointService checkpointService = runtime.getService(CheckpointService.class);\n            ProcessDefinition processDefinition = runtime.getService(ProcessDefinition.class);\n\n            // cleanup the internal state to reduce the serialized data size\n            state.gc();\n\n            // TODO validate checkpoint name\n\n            checkpointService.create(threadId, getCorrelationId(), name, runtime, ProcessSnapshot.builder()\n                    .vmState(state)\n                    .processDefinition(processDefinition)\n                    .build());\n        });\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/CloseLogSegmentCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentStatus;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.LogContext;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.RunnerLogger;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\nimport java.io.Serial;\nimport java.util.UUID;\n\npublic class CloseLogSegmentCommand implements Command {\n\n    @Serial\n    private static final long serialVersionUID = 1L;\n\n    private final UUID correlationId;\n\n    public CloseLogSegmentCommand(UUID correlationId) {\n        this.correlationId = correlationId;\n    }\n\n    @Override\n    public void eval(Runtime runtime, State state, ThreadId threadId) {\n        state.peekFrame(threadId).pop();\n\n        LogContext logContext = LogSegmentUtils.popLogContext(threadId, state);\n        if (logContext == null) {\n            return;\n        }\n        Long segmentId = logContext.segmentId();\n        if (segmentId == null) {\n            return;\n        }\n        assert correlationId == logContext.correlationId();\n\n        runtime.getService(RunnerLogger.class)\n                .setSegmentStatus(segmentId, getStatus(state, threadId));\n    }\n\n    private static LogSegmentStatus getStatus(State state, ThreadId threadId) {\n        if (state.getThreadError(threadId) != null) {\n            return LogSegmentStatus.ERROR;\n        } else {\n            return LogSegmentStatus.OK;\n        }\n    }\n}"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/CopyVariablesCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\nimport java.io.Serializable;\nimport java.util.List;\nimport java.util.function.BiFunction;\n\n/**\n * Copies the specified list of variables from the source frame to the target frame.\n */\npublic class CopyVariablesCommand implements Command {\n\n    public interface FrameProducer extends BiFunction<State, ThreadId, Frame>, Serializable {}\n\n    private static final long serialVersionUID = 1L;\n\n    private final List<String> variables;\n    private final FrameProducer sourceFrameProducer;\n    private final FrameProducer targetFrameProducer;\n\n    public CopyVariablesCommand(List<String> variables, Frame sourceFrame, FrameProducer targetFrameProducer) {\n        this(variables, (state, threadId) -> sourceFrame, targetFrameProducer);\n    }\n\n    public CopyVariablesCommand(List<String> variables, FrameProducer sourceFrameProducer, Frame targetFrame) {\n        this(variables, sourceFrameProducer, (state, threadId) -> targetFrame);\n    }\n\n    public CopyVariablesCommand(List<String> variables, FrameProducer sourceFrameProducer, FrameProducer targetFrameProducer) {\n        this.variables = variables;\n        this.sourceFrameProducer = sourceFrameProducer;\n        this.targetFrameProducer = targetFrameProducer;\n    }\n\n    @Override\n    public void eval(Runtime runtime, State state, ThreadId threadId) {\n        Frame frame = state.peekFrame(threadId);\n        frame.pop();\n\n        if (variables.isEmpty()) {\n            return;\n        }\n\n        Frame effectiveSourceFrame = sourceFrameProducer.apply(state, threadId);\n        Frame effectiveTargetFrame = targetFrameProducer.apply(state, threadId);\n\n        for (String variable : variables) {\n            if (effectiveSourceFrame.hasLocal(variable)) {\n                Serializable value = effectiveSourceFrame.getLocal(variable);\n                VMUtils.putLocal(effectiveTargetFrame, variable, value);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/ElementEventProducer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.svm.State;\nimport com.walmartlabs.concord.svm.ThreadId;\n\nimport java.util.UUID;\n\npublic interface ElementEventProducer {\n\n    UUID getCorrelationId();\n\n    Step getStep();\n\n    String getDescription(State state, ThreadId threadId);\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/ErrorWrapper.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\n/**\n * Wraps the specified command into a new frame with an exception handler\n * consisting of a {@link ExposeLastErrorCommand} and the provided {@link #errorSteps}.\n */\npublic class ErrorWrapper implements Command {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Command cmd;\n    private final Command errorSteps;\n\n    public ErrorWrapper(Command cmd, Command errorSteps) {\n        this.cmd = cmd;\n        this.errorSteps = errorSteps;\n    }\n\n    @Override\n    public void eval(Runtime runtime, State state, ThreadId threadId) {\n        Frame frame = state.peekFrame(threadId);\n        frame.pop();\n\n        // create a block of commands that looks like this:\n        //  block:\n        //    - ExposeLastErrorCommand\n        //    - ...compiled steps from the \"error\" block...\n        BlockCommand exceptionHandler = new BlockCommand(new ExposeLastErrorCommand(), errorSteps);\n\n        Frame inner = Frame.builder()\n                .nonRoot()\n                .exceptionHandler(exceptionHandler)\n                .commands(cmd)\n                .build();\n\n        state.pushFrame(threadId, inner);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/ExitCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.ExitStep;\nimport com.walmartlabs.concord.runtime.v2.runner.SynchronizationService;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.State;\nimport com.walmartlabs.concord.svm.ThreadId;\n\npublic class ExitCommand extends StepCommand<ExitStep> implements ElementEventProducer {\n\n    private static final long serialVersionUID = 1L;\n\n    public ExitCommand(ExitStep step) {\n        super(step);\n    }\n\n    @Override\n    public String getDescription(State state, ThreadId threadId) {\n        return \"Exit\";\n    }\n\n    @Override\n    protected void execute(Runtime runtime, State state, ThreadId threadId) {\n        runtime.getService(SynchronizationService.class).stop();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/ExposeLastErrorCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\n/**\n * Provides {@code ${lastError}} variable for {@code error} blocks.\n */\npublic class ExposeLastErrorCommand implements Command {\n\n    private static final long serialVersionUID = 1L;\n\n    @Override\n    public void eval(Runtime runtime, State state, ThreadId threadId) {\n        Frame frame = state.peekFrame(threadId);\n        frame.pop();\n\n        Exception cause = (Exception) frame.getLocal(Frame.LAST_EXCEPTION_KEY);\n        frame.setLocal(Constants.Context.LAST_ERROR_KEY, cause);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/ExpressionCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.Expression;\nimport com.walmartlabs.concord.runtime.v2.model.ExpressionOptions;\nimport com.walmartlabs.concord.runtime.v2.runner.script.VariablesSanitizer;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContextFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.ExpressionEvaluator;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.State;\nimport com.walmartlabs.concord.svm.ThreadId;\n\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.UUID;\n\n/**\n * Evaluates the specified {@link Expression} step and (optionally) saves\n * the result as a global variable.\n */\npublic class ExpressionCommand extends StepCommand<Expression> {\n\n    private static final long serialVersionUID = 1L;\n\n    public ExpressionCommand(UUID correlationId, Expression step) {\n        super(correlationId, step);\n    }\n\n    @Override\n    protected void execute(Runtime runtime, State state, ThreadId threadId) {\n        state.peekFrame(threadId).pop();\n\n        Context ctx = runtime.getService(Context.class);\n\n        Expression step = getStep();\n        String expr = step.getExpr();\n\n        EvalContextFactory ecf = runtime.getService(EvalContextFactory.class);\n        ExpressionEvaluator ee = runtime.getService(ExpressionEvaluator.class);\n        Object result = ee.eval(ecf.global(ctx), expr, Object.class);\n        result = VariablesSanitizer.sanitize(result);\n\n        ExpressionOptions opts = Objects.requireNonNull(step.getOptions());\n        if (!opts.outExpr().isEmpty()) {\n            ExpressionEvaluator expressionEvaluator = runtime.getService(ExpressionEvaluator.class);\n            Map<String, Object> vars = Collections.singletonMap(\"result\", result);\n            Map<String, Serializable> out = expressionEvaluator.evalAsMap(ecf.global(ctx, vars), opts.outExpr());\n            out.forEach((k, v) -> ctx.variables().set(k, v));\n        } else if (opts.out() != null) {\n            ctx.variables().set(opts.out(), result);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/FlowCallCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.*;\nimport com.walmartlabs.concord.runtime.v2.runner.compiler.CompilerUtils;\nimport com.walmartlabs.concord.runtime.v2.sdk.Compiler;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.Serializable;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic class FlowCallCommand extends StepCommand<FlowCall> implements ElementEventProducer {\n\n    private static final String FLOW_NAME_VARIABLE = \"__flowName__b6bc6c58-c2bc-434c-9a6b-b0092237720b\";\n\n    private static final long serialVersionUID = 1L;\n\n    private static final Logger log = LoggerFactory.getLogger(FlowCallCommand.class);\n\n    public FlowCallCommand(UUID correlationId, FlowCall step) {\n        super(correlationId, step);\n    }\n\n    @Override\n    protected void execute(Runtime runtime, State state, ThreadId threadId) {\n        state.peekFrame(threadId).pop();\n\n        Context ctx = runtime.getService(Context.class);\n\n        EvalContextFactory ecf = runtime.getService(EvalContextFactory.class);\n        ExpressionEvaluator ee = runtime.getService(ExpressionEvaluator.class);\n        EvalContext evalCtx = ecf.global(ctx);\n\n        FlowCall call = getStep();\n\n        // the called flow's name\n        String flowName = ee.eval(evalCtx, call.getFlowName(), String.class);\n\n        // the called flow's steps\n        Compiler compiler = runtime.getService(Compiler.class);\n        ProcessDefinition pd = runtime.getService(ProcessDefinition.class);\n        ProcessConfiguration pc = runtime.getService(ProcessConfiguration.class);\n\n        Command steps = CompilerUtils.compile(compiler, pc, pd, flowName);\n\n        FlowCallOptions opts = Objects.requireNonNull(call.getOptions());\n        Map<String, Object> input = VMUtils.prepareInput(ecf, ee, ctx, opts.input(), opts.inputExpression());\n\n        // the call's frame should be a \"root\" frame\n        // all local variables will have this frame as their base\n        Frame innerFrame = Frame.builder()\n                .root()\n                .commands(steps)\n                .locals(input)\n                .build();\n\n        Command processOutVars = outCommandOrNull(opts, ee, evalCtx, innerFrame);\n\n        // push the out handler first so it executes after the called flow's frame is done\n        if (processOutVars != null) {\n            state.peekFrame(threadId).push(processOutVars);\n        }\n        state.pushFrame(threadId, innerFrame);\n        VMUtils.putLocal(innerFrame, FLOW_NAME_VARIABLE, flowName);\n    }\n\n    public static String getFlowName(State state, ThreadId threadId) {\n        return VMUtils.getLocal(state, threadId, FLOW_NAME_VARIABLE);\n    }\n\n    @Override\n    public String getDescription(State state, ThreadId threadId) {\n        return \"Flow call: \" + getFlowName(state, threadId);\n    }\n\n    private Command outCommandOrNull(FlowCallOptions opts, ExpressionEvaluator ee, EvalContext evalCtx, Frame innerFrame) {\n        if (opts.outExpression() != null) {\n            Object outExpr = ee.eval(evalCtx, opts.outExpression(), Object.class);\n            if (outExpr instanceof List<?> l) {\n                return new CopyVariablesCommand(l.stream().filter(Objects::nonNull).map(Object::toString).toList(), innerFrame, VMUtils::assertNearestRoot);\n            } else if (outExpr instanceof Map<?,?> m) {\n                Map<String, Serializable> mm = m.entrySet().stream()\n                        .filter(e -> e.getKey() != null)\n                        .filter(e -> e.getValue() instanceof Serializable)\n                        .collect(Collectors.toMap(\n                                e -> e.getKey().toString(),\n                                e -> (Serializable)e.getValue(),\n                                (v1, v2) -> v1));\n                return new EvalVariablesCommand(getStep(), mm, innerFrame);\n            } else if (outExpr != null){\n                log.warn(\"Unexpected out expr type: {}, expected list or map\", outExpr.getClass());\n            }\n            return null;\n        } else if (opts.outMapping() != null && !opts.outMapping().isEmpty()) {\n            return new EvalVariablesCommand(getStep(), opts.outMapping(), innerFrame);\n        } else if (opts.outExpr() != null && !opts.outExpr().isEmpty()) {\n            return new EvalVariablesCommand(getStep(), opts.outExpr(), innerFrame);\n        } else {\n            return new CopyVariablesCommand(opts.out(), innerFrame, VMUtils::assertNearestRoot);\n        }\n    }\n\n    private static class EvalVariablesCommand extends StepCommand<FlowCall> {\n\n        // for backward compatibility (java8 concord 1.92.0 version)\n        private static final long serialVersionUID = -7294220776008029488L;\n\n        // TODO: only for backward compatibility\n        private final FlowCall step;\n\n        private final Map<String, Serializable> variables;\n        private final Frame variablesFrame;\n\n        private EvalVariablesCommand(FlowCall step, Map<String, Serializable> variables, Frame variablesFrame) {\n            super(step);\n            this.step = Objects.requireNonNull(step);\n            this.variables = Objects.requireNonNull(variables);\n            this.variablesFrame = Objects.requireNonNull(variablesFrame);\n        }\n\n        @Override\n        @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n        protected void execute(Runtime runtime, State state, ThreadId threadId) {\n            Frame frame = state.peekFrame(threadId);\n            frame.pop();\n\n            Context ctx = runtime.getService(Context.class);\n\n            EvalContextFactory ecf = runtime.getService(EvalContextFactory.class);\n            ExpressionEvaluator expressionEvaluator = runtime.getService(ExpressionEvaluator.class);\n            Map<String, Object> vars = (Map) variablesFrame.getLocals();\n            Map<String, Serializable> out = expressionEvaluator.evalAsMap(ecf.global(ctx, vars), variables);\n            out.forEach((k, v) -> ctx.variables().set(k, v));\n        }\n\n        // TODO: only for backward compatibility\n        @Override\n        public FlowCall getStep() {\n            return step;\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/ForkCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\nimport java.util.Map;\n\npublic class ForkCommand implements Command {\n\n    private static final long serialVersionUID = 1L;\n\n    private final ThreadId childThreadId;\n    private final Command[] cmds;\n\n    public ForkCommand(ThreadId childThreadId, Command... cmds) {\n        this.childThreadId = childThreadId;\n        this.cmds = cmds;\n    }\n\n    @Override\n    public void eval(Runtime runtime, State state, ThreadId threadId) {\n        Frame frame = state.peekFrame(threadId);\n        frame.pop();\n\n        // create a new root frame\n        state.fork(threadId, childThreadId, cmds);\n\n        // copy all \"in\" variables\n        Frame targetFrame = state.peekFrame(childThreadId);\n        Map<String, Object> locals = VMUtils.getCombinedLocals(state, threadId);\n        VMUtils.putLocals(targetFrame, locals);\n\n        // run the new thread\n        runtime.spawn(state, childThreadId);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/FormCallCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.forms.Form;\nimport com.walmartlabs.concord.forms.FormOptions;\nimport com.walmartlabs.concord.runtime.common.FormService;\nimport com.walmartlabs.concord.runtime.v2.exception.InvalidValueException;\nimport com.walmartlabs.concord.runtime.v2.model.*;\nimport com.walmartlabs.concord.runtime.v2.parser.FormFieldParser;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\nimport java.io.Serializable;\nimport java.util.*;\n\npublic class FormCallCommand extends StepCommand<FormCall> implements ElementEventProducer {\n\n    private static final long serialVersionUID = 1L;\n\n    public FormCallCommand(FormCall formCall) {\n        super(formCall);\n    }\n\n    @Override\n    public String getDescription(State state, ThreadId threadId) {\n        return \"Form call: \" + getStep().getName();\n    }\n\n    @Override\n    protected void execute(Runtime runtime, State state, ThreadId threadId) {\n        String eventRef = UUID.randomUUID().toString();\n\n        Context ctx = runtime.getService(Context.class);\n\n        EvalContextFactory ecf = runtime.getService(EvalContextFactory.class);\n        EvalContext evalContext = ecf.global(ctx);\n\n        FormCall call = getStep();\n        ExpressionEvaluator expressionEvaluator = runtime.getService(ExpressionEvaluator.class);\n        String formName = expressionEvaluator.eval(evalContext, call.getName(), String.class);\n\n        if (!formName.matches(\"^[A-Za-z0-9_ $]+$\")) {\n            throw InvalidValueException.builder()\n                    .location(call.getLocation())\n                    .actual(formName)\n                    .expected(\"name matching regex \\\"^[A-Za-z0-9_ $]+$\\\"\")\n                    .build();\n        }\n\n        ProcessDefinition processDefinition = runtime.getService(ProcessDefinition.class);\n        ProcessConfiguration processConfiguration = runtime.getService(ProcessConfiguration.class);\n\n        List<FormField> fields = assertFormFields(expressionEvaluator, evalContext, processConfiguration, processDefinition, formName, call);\n        Map<String, Serializable> values = getFormValues(expressionEvaluator, evalContext, call);\n        Form form = Form.builder()\n                .name(formName)\n                .eventName(eventRef)\n                .options(buildFormOptions(expressionEvaluator, evalContext, call, values))\n                .fields(buildFormFields(expressionEvaluator, evalContext, fields, Objects.requireNonNull(values)))\n                .build();\n\n        FormService formService = runtime.getService(FormService.class);\n        formService.save(form);\n\n        state.peekFrame(threadId).pop();\n        state.setEventRef(threadId, eventRef);\n        state.setStatus(threadId, ThreadStatus.SUSPENDED);\n    }\n\n    private static FormOptions buildFormOptions(ExpressionEvaluator expressionEvaluator, EvalContext ctx, FormCall formCall, Map<String, Serializable> values) {\n        FormCallOptions options = Objects.requireNonNull(formCall.getOptions());\n\n        Map<String, Serializable> runAs = getFormRunAs(expressionEvaluator, ctx, formCall);\n\n        return FormOptions.builder()\n                .isYield(options.isYield())\n                .saveSubmittedBy(options.saveSubmittedBy())\n                .runAs(runAs)\n                .extraValues(values)\n                .build();\n    }\n\n    private static List<com.walmartlabs.concord.forms.FormField> buildFormFields(ExpressionEvaluator expressionEvaluator,\n                                                                                 EvalContext ctx,\n                                                                                 List<com.walmartlabs.concord.runtime.v2.model.FormField> fields,\n                                                                                 Map<String, Serializable> values) {\n        List<com.walmartlabs.concord.forms.FormField> result = new ArrayList<>();\n\n        fields.forEach(f -> {\n            Serializable defaultValue = null;\n            Serializable value = values.get(f.name());\n            if (value != null) {\n                defaultValue = expressionEvaluator.eval(ctx, value, Serializable.class);\n            } else {\n                if (f.defaultValue() != null) {\n                    defaultValue = expressionEvaluator.eval(ctx, f.defaultValue(), Serializable.class);\n                }\n            }\n\n            Serializable allowedValue = null;\n            if (f.allowedValue() != null) {\n                allowedValue = expressionEvaluator.eval(ctx, f.allowedValue(), Serializable.class);\n            }\n\n            String label = expressionEvaluator.eval(ctx, f.label(), String.class);\n\n            result.add(com.walmartlabs.concord.forms.FormField.builder()\n                    .name(f.name())\n                    .label(label)\n                    .type(f.type())\n                    .cardinality(f.cardinality())\n                    .defaultValue(defaultValue)\n                    .allowedValue(allowedValue)\n                    .options(f.options())\n                    .build());\n        });\n\n        return result;\n    }\n\n    private static List<FormField> assertFormFields(ExpressionEvaluator expressionEvaluator,\n                                                    EvalContext ctx,\n                                                    ProcessConfiguration processConfiguration,\n                                                    ProcessDefinition pd,\n                                                    String formName,\n                                                    FormCall formCall) {\n        FormCallOptions options = Objects.requireNonNull(formCall.getOptions());\n        if (!options.fields().isEmpty()) {\n            return options.fields();\n        }\n\n        List<Map<String, Map<String, Object>>> rawFields = expressionEvaluator.evalAsList(ctx, options.fieldsExpression());\n        if (rawFields != null) {\n            return FormFieldParser.parse(formCall.getLocation(), rawFields);\n        }\n\n        com.walmartlabs.concord.runtime.v2.model.Form fd = pd.forms().get(formName);\n        for (String activeProfile : processConfiguration.processInfo().activeProfiles()) {\n            com.walmartlabs.concord.runtime.v2.model.Form maybeForm = pd.profiles().getOrDefault(activeProfile, Profile.builder().build()).forms().get(formName);\n            if (maybeForm != null) {\n                fd = maybeForm;\n            }\n        }\n\n        if (fd != null) {\n            return fd.fields();\n        }\n\n        throw new IllegalStateException(\"Form not found: \" + formName);\n    }\n\n    private static Map<String, Serializable> getFormValues(ExpressionEvaluator expressionEvaluator,\n                                                           EvalContext ctx,\n                                                           FormCall formCall) {\n        FormCallOptions options = Objects.requireNonNull(formCall.getOptions());\n        if (!options.values().isEmpty()) {\n            return expressionEvaluator.evalAsMap(ctx, options.values());\n        }\n\n        Map<String, Serializable> rawValues = expressionEvaluator.evalAsMap(ctx, options.valuesExpression());\n        if (rawValues != null) {\n            return rawValues;\n        }\n\n        return Collections.emptyMap();\n    }\n\n    private static Map<String, Serializable> getFormRunAs(ExpressionEvaluator expressionEvaluator,\n                                                          EvalContext ctx,\n                                                          FormCall formCall) {\n        FormCallOptions options = Objects.requireNonNull(formCall.getOptions());\n        if (!options.runAs().isEmpty()) {\n            return expressionEvaluator.evalAsMap(ctx, options.runAs());\n        }\n\n        Map<String, Serializable> rawRunAs = expressionEvaluator.evalAsMap(ctx, options.runAsExpression());\n        if (rawRunAs != null) {\n            return rawRunAs;\n        }\n\n        return Collections.emptyMap();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/IfCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.IfStep;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContext;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContextFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.ExpressionEvaluator;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\npublic class IfCommand extends StepCommand<IfStep> implements ElementEventProducer {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Command thenCommand;\n    private final Command elseCommand;\n\n    public IfCommand(IfStep step, Command thenCommand, Command elseCommand) {\n        super(step);\n        this.thenCommand = thenCommand;\n        this.elseCommand = elseCommand;\n    }\n\n\n    @Override\n    public String getDescription(State state, ThreadId threadId) {\n        return \"Check: \" + getStep().getExpression();\n    }\n\n    @Override\n    protected void execute(Runtime runtime, State state, ThreadId threadId) {\n        Frame frame = state.peekFrame(threadId);\n        frame.pop();\n\n        IfStep step = getStep();\n        String expr = step.getExpression();\n\n        EvalContextFactory ecf = runtime.getService(EvalContextFactory.class);\n        Context ctx = runtime.getService(Context.class);\n        EvalContext evalContext = ecf.global(ctx);\n\n        ExpressionEvaluator ee = runtime.getService(ExpressionEvaluator.class);\n        Object ifResult = ee.eval(evalContext, expr, Object.class);\n        if (isTrue(ifResult)) {\n            frame.push(thenCommand);\n        } else if (elseCommand != null) {\n            frame.push(elseCommand);\n        }\n    }\n\n    private static boolean isTrue(Object value) {\n        if (value == null) {\n            return false;\n        }\n\n        if (value instanceof Boolean b) {\n            return b;\n        } else if (value instanceof String s) {\n            if (\"true\".equalsIgnoreCase(s)) {\n                return true;\n            } else if (\"false\".equalsIgnoreCase(s)) {\n                return false;\n            }\n        }\n\n        throw new RuntimeException(String.format(\"Expected boolean value or string 'true'/'false', got: '%s', type: %s\", value, value.getClass()));\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/JoinCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.Serial;\nimport java.util.*;\n\npublic class JoinCommand<T extends Step> extends StepCommand<T> {\n\n    @Serial\n    private static final long serialVersionUID = 1L;\n\n    private static final Logger log = LoggerFactory.getLogger(JoinCommand.class);\n    private static final long BUSY_WAIT_SLEEP = 100; // how often poll status of threads, milliseconds\n\n    private final Collection<ThreadId> ids;\n\n    public JoinCommand(Collection<ThreadId> ids, T step) {\n        super(step);\n\n        this.ids = ids;\n    }\n\n    @Override\n    protected void execute(Runtime runtime, State state, ThreadId threadId) {\n        // Here's a very dumb but working solution to the problem\n        // of monitoring child thread state - just a loop\n        // waiting on a monitor . On each iteration it decides whether\n        // the join command can be removed from the stack (and thus\n        // continuing the execution) or not.\n        // We could've used futures instead, but it's way more\n        // complicated - especially when suspend/resume are involved.\n\n        var finalStatuses = waitForChildren(state);\n        var failed = finalStatuses.entrySet().stream()\n                .filter(e -> e.getValue() == ThreadStatus.FAILED)\n                .map(Map.Entry::getKey)\n                .toList();\n        if (!failed.isEmpty()) {\n            throw new ParallelExecutionException(failed.stream()\n                    .map(state::clearThreadError)\n                    .filter(Objects::nonNull)\n                    .toList());\n        }\n\n        if (finalStatuses.containsValue(ThreadStatus.SUSPENDED)) {\n            log.trace(\"eval [{}] -> children suspended, suspending parent\", threadId);\n            state.setStatus(threadId, ThreadStatus.SUSPENDED);\n            return;\n        }\n\n        state.peekFrame(threadId).pop();\n    }\n\n    private Map<ThreadId, ThreadStatus> waitForChildren(State state) {\n        while (true) {\n            var allStatuses = state.threadStatus();\n\n            var targets = new HashMap<ThreadId, ThreadStatus>();\n            var active = false;\n            for (var id : ids) {\n                // null means the thread completed and was removed by gc()\n                var s = allStatuses.getOrDefault(id, ThreadStatus.DONE);\n                targets.put(id, s);\n                if (s == ThreadStatus.READY || s == ThreadStatus.UNWINDING) {\n                    active = true;\n                }\n            }\n\n            if (!active) {\n                return targets;\n            }\n\n            try {\n                Thread.sleep(BUSY_WAIT_SLEEP);\n            } catch (InterruptedException e) {\n                Thread.currentThread().interrupt();\n                throw new RuntimeException(\"JoinCommand interrupted\", e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/LogSegmentScopeCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport ch.qos.logback.classic.Level;\nimport com.walmartlabs.concord.runtime.common.SensitiveDataMasker;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.runtime.v2.model.*;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.LogContext;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.RunnerLogger;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.SegmentedLogger;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\nimport com.walmartlabs.concord.svm.*;\nimport com.walmartlabs.concord.svm.Runtime;\n\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.runtime.v2.runner.logging.SegmentedLogger.SYSTEM_SEGMENT_NAME;\n\npublic class LogSegmentScopeCommand<T extends Step> extends StepCommand<T> {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Command cmd;\n\n    public LogSegmentScopeCommand(UUID correlationId, Command cmd, T step) {\n        super(correlationId, step);\n\n        this.cmd = cmd;\n    }\n\n    @Override\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    protected void execute(Runtime runtime, State state, ThreadId threadId) {\n        Frame frame = state.peekFrame(threadId);\n        frame.pop();\n\n        Context ctx = runtime.getService(Context.class);\n        LogContext logContext = getLogContext(runtime, ctx, getCorrelationId());\n        if (logContext == null || logContext.segmentId() == null) {\n            frame.push(cmd);\n            return;\n        }\n\n        LogSegmentUtils.pushLogContext(threadId, state, logContext);\n\n        Frame scopeFrame = Frame.builder()\n                .nonRoot()\n                .commands(cmd)\n                .finallyHandler(new CloseLogSegmentCommand(getCorrelationId()))\n                .locals((Map)frame.getLocals())\n                .build();\n\n        state.pushFrame(threadId, scopeFrame);\n    }\n\n    private LogContext getLogContext(Runtime runtime, Context ctx, UUID correlationId) {\n        String segmentName = getSegmentName(ctx, (AbstractStep<?>) getStep());\n        if (segmentName == null) {\n            return null;\n        }\n\n        segmentName = SensitiveDataMasker.mask(segmentName, runtime.getService(SensitiveDataHolder.class).get());\n\n        return buildLogContext(runtime, segmentName, correlationId);\n    }\n\n    private LogContext buildLogContext(Runtime runtime, String segmentName, UUID correlationId) {\n        RunnerConfiguration runnerCfg = runtime.getService(RunnerConfiguration.class);\n        boolean redirectSystemOutAndErr = runnerCfg.logging().sendSystemOutAndErrToSLF4J();\n\n        return LogContext.builder()\n                .segmentName(segmentName)\n                .correlationId(correlationId)\n                .redirectSystemOutAndErr(redirectSystemOutAndErr)\n                .logLevel(getLogLevel((AbstractStep<?>) getStep()))\n                .segmentId(runtime.getService(RunnerLogger.class).createSegment(segmentName, correlationId))\n                .build();\n    }\n\n    private static String getSegmentName(Context ctx, AbstractStep<?> step) {\n        String rawSegmentName = SegmentedLogger.getSegmentName(step);\n        String segmentName = ctx.eval(rawSegmentName, String.class);\n        if (segmentName != null) {\n            return segmentName;\n        }\n\n        // default segment names...\n        if (step instanceof TaskCall taskCall) {\n            return \"task: \" + taskCall.getName();\n        } else if (step instanceof ScriptCall scriptCall) {\n            return \"script: \" + scriptCall.getLanguageOrRef();\n        } else if (step instanceof Expression) {\n            return SYSTEM_SEGMENT_NAME;\n        }\n\n        return null;\n    }\n\n    private static Level getLogLevel(AbstractStep<?> step) {\n        return SegmentedLogger.getLogLevel(step);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/LogSegmentUtils.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.runner.logging.LogContext;\nimport com.walmartlabs.concord.svm.State;\nimport com.walmartlabs.concord.svm.ThreadId;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic final class LogSegmentUtils {\n\n    private static final String KEY = \"logContext\";\n\n    public static LogContext popLogContext(ThreadId threadId, State state) {\n        List<LogContext> items = state.getThreadLocal(threadId, KEY);\n        if (items == null) {\n            return null;\n        }\n\n        if (items.isEmpty()) {\n            throw new RuntimeException(\"Can't pop log context: empty log context\");\n        }\n\n        LogContext result = items.remove(items.size() - 1);\n        if (items.isEmpty()) {\n            state.removeThreadLocal(threadId, KEY);\n        }\n        return result;\n    }\n\n    public static LogContext getLogContext(ThreadId threadId, State state) {\n        List<LogContext> items = state.getThreadLocal(threadId, KEY);\n        if (items == null) {\n            return null;\n        }\n\n        if (items.isEmpty()) {\n            throw new RuntimeException(\"Can't get log context: empty log context\");\n        }\n\n        return items.get(items.size() - 1);\n    }\n\n    public static List<LogContext> getLogContexts(ThreadId threadId, State state) {\n        List<LogContext> items = state.getThreadLocal(threadId, KEY);\n        if (items == null) {\n            return List.of();\n        }\n\n        return items;\n    }\n\n    public static void pushLogContext(ThreadId threadId, State state, LogContext logContext) {\n        ArrayList<LogContext> items = state.getThreadLocal(threadId, KEY);\n        if (items == null) {\n            items = new ArrayList<>();\n        }\n        items.add(logContext);\n        state.setThreadLocal(threadId, KEY, items);\n    }\n\n    private LogSegmentUtils() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/LoopItemSanitizer.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.ObjectOutputStream;\nimport java.io.Serializable;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic final class LoopItemSanitizer {\n\n    public static ArrayList<Serializable> sanitize(Object items) {\n        if (items == null) {\n            return null;\n        }\n\n        ArrayList<Serializable> result = toArray(items);\n        if (result == null) {\n            return null;\n        }\n\n        result.forEach(LoopItemSanitizer::assertItem);\n\n        return result;\n    }\n\n    public static ArrayList<Serializable> toArray(Object items) {\n        if (items instanceof Collection) {\n            Collection<?> collection = (Collection<?>) items;\n            if (collection.isEmpty()) {\n                return null;\n            }\n            return collection.stream()\n                    .map(LoopItemSanitizer::sanitizeItem)\n                    .collect(Collectors.toCollection(ArrayList::new));\n        } else if (items instanceof Map) {\n            Map<?, ?> m = (Map<?, ?>) items;\n            return m.entrySet().stream()\n                    .map(e -> new AbstractMap.SimpleImmutableEntry<>(e.getKey(), e.getValue()))\n                    .collect(Collectors.toCollection(ArrayList::new));\n        } else if (items.getClass().isArray()) {\n            return Arrays.stream((Object[])items)\n                    .map(LoopItemSanitizer::sanitizeItem)\n                    .collect(Collectors.toCollection(ArrayList::new));\n        }\n\n        throw new IllegalArgumentException(\"'loop' accepts only Lists of items, Java Maps or arrays of values. Got: \" + items.getClass());\n    }\n\n    private static Serializable sanitizeItem(Object item) {\n        if (item == null) {\n            return null;\n        }\n\n        if (item instanceof Serializable) {\n            return (Serializable) item;\n        }\n\n        if (item instanceof Map.Entry) {\n            Map.Entry<?, ?> e = (Map.Entry<?, ?>) item;\n            return new AbstractMap.SimpleImmutableEntry<>(e.getKey(), e.getValue());\n        } else if (item instanceof Map) {\n            return new HashMap<>((Map<?, ?>)item);\n        } else if (item instanceof Collection) {\n            return new ArrayList<>((Collection<?>) item);\n        }\n\n        throw new IllegalArgumentException(\"Can't use non-serializable values in 'loop': \" + item + \" (\" + item.getClass() + \")\");\n    }\n\n    static void assertItem(Object item) {\n        if (item == null) {\n            return;\n        }\n\n        try (ObjectOutputStream oos = new ObjectOutputStream(new ByteArrayOutputStream())) {\n            oos.writeObject(item);\n        } catch (IOException e) {\n            throw new IllegalArgumentException(\"Can't use non-serializable values in 'loop': \" + item + \" (\" + item.getClass() + \")\");\n        }\n    }\n\n    private LoopItemSanitizer() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/LoopWrapper.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.Loop;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.runner.compiler.CompilerContext;\nimport com.walmartlabs.concord.runtime.v2.runner.context.ContextFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContext;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContextFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.ExpressionEvaluator;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\nimport java.io.Serializable;\nimport java.util.*;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.stream.Collectors;\n\npublic abstract class LoopWrapper implements Command {\n\n    public static LoopWrapper of(CompilerContext ctx, Command cmd, Loop withItems,\n                                 Collection<String> outVariables,\n                                 Map<String, Serializable> outMapping,\n                                 String outExpression,\n                                 Step step) {\n        Collection<String> out = Collections.emptyList();\n        if (outMapping != null && !outMapping.isEmpty()) {\n            // serializable\n            out = new HashSet<>(outMapping.keySet());\n        } else if (!outVariables.isEmpty()) {\n            out = outVariables;\n        }\n\n        Loop.Mode mode = withItems.mode();\n        switch (mode) {\n            case SERIAL:\n                return new SerialWithItems(cmd, withItems, out, outExpression, step);\n            case PARALLEL:\n                return new ParallelWithItems(ctx, cmd, withItems, out, outExpression, step);\n            default:\n                throw new IllegalArgumentException(\"Unknown withItems mode: \" + mode);\n        }\n    }\n\n    private static final long serialVersionUID = 1L;\n\n    // TODO move into the actual Constants\n    public static final String CURRENT_ITEMS = \"items\";\n    public static final String CURRENT_INDEX = \"itemIndex\";\n    public static final String CURRENT_ITEM = \"item\";\n\n    protected final Command cmd;\n    protected final Serializable items;\n    private final Collection<String> outVariables;\n    protected final String outExpression;\n    private final Step step;\n\n    protected LoopWrapper(Command cmd, Serializable items, Collection<String> outVariables, String outExpression, Step step) {\n        this.cmd = cmd;\n        this.items = items;\n        this.outVariables = outVariables;\n        this.outExpression = outExpression;\n        this.step = step;\n    }\n\n    public Step getStep() {\n        return step;\n    }\n\n    @Override\n    public void eval(Runtime runtime, State state, ThreadId threadId) {\n        try {\n            execute(runtime, state, threadId);\n        } catch (Exception e) {\n            StepCommand.logException(step, state, threadId, e);\n            throw e;\n        }\n    }\n\n    private void execute(Runtime runtime, State state, ThreadId threadId) {\n        Frame frame = state.peekFrame(threadId);\n        frame.pop();\n\n        Serializable value = items;\n        if (value == null) {\n            // value is null, not going to run the wrapped command at all\n            return;\n        }\n\n        // create the context explicitly\n        ContextFactory contextFactory = runtime.getService(ContextFactory.class);\n        Context ctx = contextFactory.create(runtime, state, threadId, getCurrentStep());\n\n        EvalContextFactory ecf = runtime.getService(EvalContextFactory.class);\n        ExpressionEvaluator ee = runtime.getService(ExpressionEvaluator.class);\n        value = ee.eval(ecf.global(ctx), value, Serializable.class);\n\n        // prepare items\n        // store items in an ArrayList because it is Serializable\n        ArrayList<Serializable> items = LoopItemSanitizer.sanitize(value);\n        if (items == null || items.isEmpty()) {\n            return;\n        }\n\n        Collection<String> effectiveOUtVariables = outVariables;\n        if (outExpression != null) {\n            Object outExpr = ee.eval(ecf.global(ctx), outExpression, Object.class);\n            if (outExpr instanceof List<?> l) {\n                effectiveOUtVariables = l.stream().filter(Objects::nonNull).map(Object::toString).toList();\n            } else if (outExpr instanceof Map<?,?> m) {\n                effectiveOUtVariables = m.keySet().stream().filter(Objects::nonNull).map(Object::toString).toList();\n            }\n        }\n\n        eval(runtime, state, threadId, ctx, items, effectiveOUtVariables);\n    }\n\n    protected abstract void eval(Runtime runtime, State state, ThreadId threadId, Context ctx, ArrayList<Serializable> items, Collection<String> outVariables);\n\n    private Step getCurrentStep() {\n        if (cmd instanceof StepCommand) {\n            return ((StepCommand<?>) cmd).getStep();\n        }\n        return null;\n    }\n\n    static class ParallelWithItems extends LoopWrapper {\n\n        private static final long serialVersionUID = 1L;\n\n        private final Parallelism parallelism;\n\n        protected ParallelWithItems(CompilerContext ctx, Command cmd, Loop loop, Collection<String> outVariables, String outExpression, Step step) {\n            super(cmd, loop.items(), outVariables, outExpression, step);\n\n            this.parallelism = processParallelism(ctx, loop);\n        }\n\n        private static Parallelism processParallelism(CompilerContext ctx, Loop loop) {\n            int defaultParallelism = ctx.processDefinition().configuration().parallelLoopParallelism();\n\n            Object parallelism = loop.options().get(\"parallelism\");\n            if (parallelism == null) {\n                return new Parallelism(null, defaultParallelism);\n            } else if (parallelism instanceof String) {\n                return new Parallelism((String) parallelism, defaultParallelism);\n            } else if (parallelism instanceof Number) {\n                return new Parallelism(null, ((Number) parallelism).intValue());\n            }\n\n            throw new RuntimeException(\"Unknown loop parallelism value type: \" + parallelism.getClass());\n        }\n\n        @Override\n        protected void eval(Runtime runtime, State state, ThreadId threadId, Context ctx, ArrayList<Serializable> items, Collection<String> outVariables) {\n            // target frame for out variables\n            Frame targetFrame = VMUtils.assertNearestRoot(state, threadId);\n\n            Map<String, List<Serializable>> outVarsAccumulator = new ConcurrentHashMap<>();\n            state.pushFrame(threadId, Frame.builder()\n                    .commands(new SetVariablesCommand(outVarsAccumulator, targetFrame))\n                    .nonRoot()\n                    .build());\n\n            int batchSize = toBatchSize(runtime, ctx, parallelism);\n\n            List<ArrayList<Serializable>> batches = batches(items, batchSize);\n            int itemIndexStart = 0;\n            for (ArrayList<Serializable> batch : batches) {\n                evalBatch(itemIndexStart, state, threadId, batch, outVarsAccumulator, outVariables);\n                itemIndexStart += batch.size();\n            }\n        }\n\n        private void evalBatch(int itemIndexStart, State state, ThreadId threadId, ArrayList<Serializable> items, Map<String, List<Serializable>> outVarsAccumulator, Collection<String> outVariables) {\n            Frame frame = state.peekFrame(threadId);\n\n            List<Map.Entry<ThreadId, Serializable>> forks = items.stream()\n                    .map(e -> new AbstractMap.SimpleEntry<>(state.nextThreadId(), e))\n                    .collect(Collectors.toList());\n\n            for (int i = 0; i < forks.size(); i++) {\n                Map.Entry<ThreadId, Serializable> f = forks.get(i);\n                Frame cmdFrame = Frame.builder()\n                        .nonRoot()\n                        .build();\n\n                cmdFrame.setLocal(CURRENT_ITEMS, items);\n                cmdFrame.setLocal(CURRENT_INDEX, itemIndexStart + i);\n                cmdFrame.setLocal(CURRENT_ITEM, f.getValue());\n\n                // fork will create rootFrame for forked commands\n                Command itemCmd = new ForkCommand(f.getKey(),\n                        new CollectVariablesCommand(outVariables, null, outVarsAccumulator),\n                        cmd);\n                cmdFrame.push(itemCmd);\n\n                state.pushFrame(threadId, cmdFrame);\n            }\n\n            frame.push(new JoinCommand<>(forks.stream().map(Map.Entry::getKey).collect(Collectors.toSet()), getStep()));\n        }\n\n        private static List<ArrayList<Serializable>> batches(ArrayList<Serializable> items, int batchSize) {\n            List<ArrayList<Serializable>> result = new ArrayList<>();\n            for (int i = 0; i < items.size(); i += batchSize) {\n                result.add(new ArrayList<>(items.subList(i, Math.min(items.size(), i + batchSize))));\n            }\n            return result;\n        }\n\n        private static int toBatchSize(Runtime runtime, Context ctx, Parallelism parallelism) {\n            if (parallelism.getExpression() == null) {\n                return parallelism.getValue();\n            }\n\n            EvalContextFactory ecf = runtime.getService(EvalContextFactory.class);\n            EvalContext evalContext = ecf.global(ctx);\n\n            ExpressionEvaluator ee = runtime.getService(ExpressionEvaluator.class);\n            Number result = ee.eval(evalContext, parallelism.getExpression(), Number.class);\n            if (result == null) {\n                return parallelism.getValue();\n            }\n            return result.intValue();\n        }\n    }\n\n    /**\n     * Wraps a command into a loop specified by {@code withItems} option.\n     * Creates a new call frame and keeps the item list, the current item\n     * and the index as frame-local variables.\n     */\n    static class SerialWithItems extends LoopWrapper {\n\n        private static final long serialVersionUID = 1L;\n\n        protected SerialWithItems(Command cmd, Loop loop, Collection<String> outVariables, String outVariablesExpression, Step step) {\n            super(cmd, loop.items(), outVariables, outVariablesExpression, step);\n        }\n\n        @Override\n        protected void eval(Runtime runtime, State state, ThreadId threadId, Context ctx, ArrayList<Serializable> items, Collection<String> outVariables) {\n            Frame loop = Frame.builder()\n                    .nonRoot()\n                    .build();\n\n            loop.setLocal(CURRENT_ITEMS, items);\n            loop.setLocal(CURRENT_INDEX, 0);\n            loop.setLocal(CURRENT_ITEM, items.get(0));\n\n            Map<String, List<Serializable>> variablesAccumulator = new ConcurrentHashMap<>();\n\n            Frame targetFrame = VMUtils.assertNearestRoot(state, threadId);\n            loop.push(new SetVariablesCommand(variablesAccumulator, targetFrame));\n\n            loop.push(new WithItemsNext(outVariables, variablesAccumulator, cmd)); // next iteration\n\n            Frame cmdFrame = Frame.builder()\n                    .commands(cmd)\n                    .root()\n                    .build();\n\n            loop.push(new CollectVariablesCommand(outVariables, cmdFrame, variablesAccumulator));\n\n            state.pushFrame(threadId, loop);\n            state.pushFrame(threadId, cmdFrame);\n        }\n    }\n\n    static class WithItemsNext implements Command {\n\n        private static final long serialVersionUID = 1L;\n\n        private final Collection<String> outVariables;\n        private final Map<String, List<Serializable>> variablesAccumulator;\n        private final Command cmd;\n\n        public WithItemsNext(Collection<String> outVariables, Map<String, List<Serializable>> variablesAccumulator, Command cmd) {\n            this.outVariables = outVariables;\n            this.variablesAccumulator = variablesAccumulator;\n            this.cmd = cmd;\n        }\n\n        @Override\n        public void eval(Runtime runtime, State state, ThreadId threadId) {\n            Frame loop = state.peekFrame(threadId);\n            loop.pop();\n\n            List<Serializable> items = VMUtils.assertLocal(state, threadId, CURRENT_ITEMS);\n\n            int index = VMUtils.assertLocal(state, threadId, CURRENT_INDEX);\n            if (index + 1 >= items.size()) {\n                // end of the line, do nothing\n                return;\n            }\n\n            int newIndex = index + 1;\n            loop.setLocal(CURRENT_INDEX, newIndex);\n            loop.setLocal(CURRENT_ITEM, items.get(newIndex));\n\n            loop.push(new WithItemsNext(outVariables, variablesAccumulator, cmd)); // next iteration\n\n            // frame wrapped command\n            Frame cmdFrame = Frame.builder()\n                    .commands(cmd)\n                    .root()\n                    .build();\n\n            loop.push(new CollectVariablesCommand(outVariables, cmdFrame, variablesAccumulator));\n\n            state.pushFrame(threadId, cmdFrame);\n        }\n    }\n\n    static class SetVariablesCommand implements Command {\n\n        private static final long serialVersionUID = 1L;\n\n        private final Map<String, List<Serializable>> variables;\n        private final Frame targetFrame;\n\n        private SetVariablesCommand(Map<String, List<Serializable>> variables, Frame targetFrame) {\n            this.variables = variables;\n            this.targetFrame = targetFrame;\n        }\n\n        @Override\n        public void eval(Runtime runtime, State state, ThreadId threadId) throws Exception {\n            Frame frame = state.peekFrame(threadId);\n            frame.pop();\n\n            if (variables.isEmpty()) {\n                return;\n            }\n\n            for (Map.Entry<String, List<Serializable>> e : variables.entrySet()) {\n                VMUtils.putLocal(targetFrame, e.getKey(), e.getValue());\n            }\n        }\n    }\n\n    /**\n     * Collect values of the specified variables from the source frame or nearest root frame into\n     * internal accumulator.\n     */\n    // TODO: BRIG: step command, with handle error?\n    static class CollectVariablesCommand implements Command {\n\n        private static final long serialVersionUID = 1L;\n\n        private final Collection<String> variables;\n        private final Map<String, List<Serializable>> variablesAccumulator;\n\n        private final Frame sourceFrame;\n\n        public CollectVariablesCommand(Collection<String> variables, Frame sourceFrame, Map<String, List<Serializable>> variablesAccumulator) {\n            this.variables = variables;\n            this.sourceFrame = sourceFrame;\n            this.variablesAccumulator = variablesAccumulator;\n        }\n\n        @Override\n        public void eval(Runtime runtime, State state, ThreadId threadId) throws Exception {\n            Frame frame = state.peekFrame(threadId);\n            frame.pop();\n\n            if (variables.isEmpty()) {\n                return;\n            }\n\n            Frame effectiveSourceFrame = sourceFrame != null ? sourceFrame : VMUtils.assertNearestRoot(state, threadId);\n\n            for (String var : variables) {\n                Serializable result = effectiveSourceFrame.hasLocal(var) ? effectiveSourceFrame.getLocal(var) : null;\n\n                variablesAccumulator.compute(var, (k, v) -> {\n                    List<Serializable> values = v;\n                    if (values == null) {\n                        values = new ArrayList<>();\n                    }\n                    values.add(result);\n                    return values;\n                });\n            }\n        }\n    }\n\n    private static class Parallelism implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        private final String expression;\n        private final int value;\n\n        public Parallelism(String expression, int value) {\n            this.expression = expression;\n            this.value = value;\n        }\n\n        public String getExpression() {\n            return expression;\n        }\n\n        public int getValue() {\n            return value;\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/OutputUtils.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContextFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.ExpressionEvaluator;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.svm.Runtime;\n\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.Map;\n\npublic final class OutputUtils {\n\n    public static void process(Runtime runtime, Context ctx, Map<String, Object> result, String out, Map<String, Serializable> outExpr) {\n        if (out != null) {\n            ctx.variables().set(out, result);\n        } else if (!outExpr.isEmpty()) {\n            EvalContextFactory ecf = runtime.getService(EvalContextFactory.class);\n            ExpressionEvaluator expressionEvaluator = runtime.getService(ExpressionEvaluator.class);\n            Map<String, Object> vars = Collections.singletonMap(\"result\", result);\n            Map<String, Serializable> evalOut = expressionEvaluator.evalAsMap(ecf.global(ctx, vars), outExpr);\n            evalOut.forEach((k, v) -> ctx.variables().set(k, v));\n        }\n    }\n\n    private OutputUtils() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/ParallelCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.ParallelBlock;\nimport com.walmartlabs.concord.runtime.v2.model.ParallelBlockOptions;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContextFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.ExpressionEvaluator;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\nimport java.io.Serializable;\nimport java.util.*;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.stream.Collectors;\n\npublic class ParallelCommand extends StepCommand<ParallelBlock> implements ElementEventProducer {\n\n    private static final long serialVersionUID = 1L;\n\n    private final List<Command> commands;\n\n    public ParallelCommand(ParallelBlock step, List<Command> commands) {\n        super(step);\n        this.commands = commands;\n    }\n\n    @Override\n    public String getDescription(State state, ThreadId threadId) {\n        return \"Parallel block\";\n    }\n\n    @Override\n    protected void execute(Runtime runtime, State state, ThreadId threadId) {\n        Frame frame = state.peekFrame(threadId);\n        frame.pop();\n\n        // parallel execution consist of \"forks\" for each command running in separate threads\n        // and a combined \"join\" executing in the parent (current) thread\n\n        List<Map.Entry<ThreadId, Command>> forks = commands.stream()\n                .map(e -> new AbstractMap.SimpleEntry<>(state.nextThreadId(), e))\n                .collect(Collectors.toList());\n\n        ParallelBlockOptions opts = Objects.requireNonNull(getStep().getOptions());\n\n        Command outVarsCommand;\n        if (!opts.outExpr().isEmpty()) {\n            Map<String, Object> accumulator = new ConcurrentHashMap<>();\n            outVarsCommand = new CollectVariablesCommand(accumulator);\n\n            frame.push(new EvalVariablesCommand(getStep(), accumulator, opts.outExpr(), frame));\n        } else {\n            outVarsCommand = new CopyVariablesCommand(opts.out(), State::peekFrame, frame);\n        }\n\n        Collection<ThreadId> forkIds = forks.stream().map(Map.Entry::getKey).collect(Collectors.toSet());\n        frame.push(new JoinCommand<>(forkIds, getStep()));\n\n        Collections.reverse(forks);\n        forks.forEach(f -> {\n            Command cmd = new ForkCommand(f.getKey(), outVarsCommand, f.getValue());\n            frame.push(cmd);\n        });\n    }\n\n    static class CollectVariablesCommand implements Command {\n\n        // for backward compatibility (java8 concord 1.92.0 version)\n        private static final long serialVersionUID = 457427720732724912L;\n\n        private final Map<String, Object> accumulator;\n\n        public CollectVariablesCommand(Map<String, Object> accumulator) {\n            this.accumulator = accumulator;\n        }\n\n        @Override\n        public void eval(Runtime runtime, State state, ThreadId threadId) {\n            Frame frame = state.peekFrame(threadId);\n            frame.pop();\n\n            accumulator.putAll(frame.getLocals());\n        }\n    }\n\n    static class EvalVariablesCommand extends StepCommand<ParallelBlock> {\n\n        // for backward compatibility (java8 concord 1.92.0 version)\n        private static final long serialVersionUID = 1370076263447141826L;\n\n        private final Map<String, Object> allVars;\n        private final Map<String, Serializable> variables;\n        private final Frame target;\n\n        public EvalVariablesCommand(ParallelBlock step, Map<String, Object> allVars, Map<String, Serializable> variables, Frame target) {\n            super(step);\n\n            this.allVars = allVars;\n            this.variables = variables;\n            this.target = target;\n        }\n\n        @Override\n        protected void execute(Runtime runtime, State state, ThreadId threadId) {\n            Frame frame = state.peekFrame(threadId);\n            frame.pop();\n\n            Context ctx = runtime.getService(Context.class);\n            EvalContextFactory ecf = runtime.getService(EvalContextFactory.class);\n            ExpressionEvaluator expressionEvaluator = runtime.getService(ExpressionEvaluator.class);\n            Map<String, Serializable> out = expressionEvaluator.evalAsMap(ecf.global(ctx, allVars), variables);\n            out.forEach((k, v) -> VMUtils.putLocal(target, k, v));\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/ParallelExecutionException.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.svm.ThreadError;\n\nimport java.io.PrintStream;\nimport java.io.PrintWriter;\nimport java.io.Serial;\nimport java.io.StringWriter;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * An exception that is thrown when multiple exceptions are thrown\n * in {@code parallel} blocks.\n */\npublic class ParallelExecutionException extends RuntimeException {\n\n    @Serial\n    private static final long serialVersionUID = 1L;\n\n    private static final int MAX_STACK_TRACE_ELEMENTS = 3;\n    private final List<ThreadError> exceptions;\n\n    public ParallelExecutionException(Collection<ThreadError> causes) {\n        super(\"Parallel execution errors: \\n\" + toMessage(causes));\n        this.exceptions = new ArrayList<>(causes);\n    }\n\n    public List<Exception> getExceptions() {\n        return exceptions.stream().map(ThreadError::exception).toList();\n    }\n\n    private static String toMessage(Collection<ThreadError> causes) {\n        return causes.stream()\n                .map(ParallelExecutionException::stacktraceToString)\n                .collect(Collectors.joining(\"\\n\"));\n    }\n\n    @Override\n    public void printStackTrace(PrintStream s) {\n        s.println(getMessage());\n    }\n\n    @Override\n    public void printStackTrace(PrintWriter s) {\n        s.println(getMessage());\n    }\n\n    private static String stacktraceToString(ThreadError e) {\n        StringWriter sw = new StringWriter();\n        sw.append(toString(e));\n\n        StackTraceElement[] elements = e.getStackTrace();\n        if (elements.length > 0) {\n            sw.append(\"\\n\");\n        }\n\n        int maxElements = Math.min(elements.length, MAX_STACK_TRACE_ELEMENTS);\n        for (int i = 0; i < maxElements; i++) {\n            StackTraceElement element = elements[i];\n            sw.append(\"\\tat \").append(element.toString()).append(\"\\n\");\n        }\n        if (maxElements != elements.length) {\n            sw.append(\"\\t...\").append(String.valueOf(elements.length - maxElements)).append(\" more\\n\");\n        }\n        return sw.toString();\n    }\n\n    @Override\n    public String toString() {\n        return getMessage();\n    }\n\n    @Override\n    public StackTraceElement[] getStackTrace() {\n        return new StackTraceElement[0];\n    }\n\n    private static String toString(ThreadError threadError) {\n        String prefix = \"\";\n        if (threadError.cmd() instanceof StepCommand<?> sc) {\n            prefix = Location.toErrorPrefix(sc.getStep().getLocation()) + \", \";\n        }\n        return prefix + \"thread: \" + threadError.threadId().id() + \": \" + threadError.exception();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/ResumeLogSegmentsCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentStatus;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.RunnerLogger;\nimport com.walmartlabs.concord.svm.Command;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.State;\nimport com.walmartlabs.concord.svm.ThreadId;\n\nimport java.io.Serial;\nimport java.util.Objects;\n\npublic class ResumeLogSegmentsCommand implements Command {\n\n    @Serial\n    private static final long serialVersionUID = 1L;\n\n    @Override\n    public void eval(Runtime runtime, State state, ThreadId threadId) {\n        state.peekFrame(threadId).pop();\n\n        RunnerLogger logger = runtime.getService(RunnerLogger.class);\n        LogSegmentUtils.getLogContexts(threadId, state).stream()\n                .filter(Objects::nonNull)\n                .filter(c -> c.segmentId() != null)\n                .forEach(c -> logger.setSegmentStatus(c.segmentId(), LogSegmentStatus.RUNNING));\n    }\n}"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/RetryWrapper.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.Retry;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.runner.context.ContextFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.Constants;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContextFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.ExpressionEvaluator;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.time.Duration;\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.Objects;\n\n/**\n * Wraps a command into a \"retry\" loop specified by {@code retry} option.\n * Creates a new call frame, installs it's own exception handler and re-runs\n * the command on error.\n */\npublic class RetryWrapper implements Command {\n\n    private static final long serialVersionUID = 1L;\n\n    private static final Logger log = LoggerFactory.getLogger(RetryWrapper.class);\n\n    private static final String RETRY_CFG = \"__retry_cfg\";\n\n    private final Command cmd;\n    private final Retry retry;\n    private final Step step;\n\n    public RetryWrapper(Command cmd, Retry retry, Step step) {\n        this.cmd = cmd;\n        this.retry = retry;\n        this.step = step;\n    }\n\n    @Override\n    public void eval(Runtime runtime, State state, ThreadId threadId) {\n        try {\n            execute(runtime, state, threadId);\n        } catch (Exception e) {\n            StepCommand.logException(step, state, threadId, e);\n            throw e;\n        }\n    }\n\n    private void execute(Runtime runtime, State state, ThreadId threadId) {\n        Frame frame = state.peekFrame(threadId);\n        frame.pop();\n\n        // wrap the command into a frame with an exception handler\n        Frame inner = Frame.builder()\n                .nonRoot()\n                .exceptionHandler(new NextRetry(cmd))\n                .commands(cmd)\n                .build();\n\n        // create the context explicitly\n        ContextFactory contextFactory = runtime.getService(ContextFactory.class);\n        Context ctx = contextFactory.create(runtime, state, threadId, getCurrentStep());\n\n        EvalContextFactory ecf = runtime.getService(EvalContextFactory.class);\n        ExpressionEvaluator ee = runtime.getService(ExpressionEvaluator.class);\n\n        int times = retry.times();\n        if (retry.timesExpression() != null) {\n            Number n = ee.eval(ecf.global(ctx), retry.timesExpression(), Number.class);\n            if (n != null) {\n                times = n.intValue();\n            }\n        }\n\n        Duration delay = retry.delay();\n        if (retry.delayExpression() != null) {\n            Number n = ee.eval(ecf.global(ctx), retry.delayExpression(), Number.class);\n            if (n != null) {\n                delay = Duration.ofSeconds(n.longValue());\n            }\n        }\n\n        inner.setLocal(Constants.Runtime.RETRY_ATTEMPT_NUMBER, 0);\n        inner.setLocal(RETRY_CFG, Retry.builder().from(retry)\n                .times(times)\n                .delay(delay)\n                .build());\n\n        state.pushFrame(threadId, inner);\n    }\n\n    private Step getCurrentStep() {\n        if (cmd instanceof StepCommand) {\n            return ((StepCommand<?>) cmd).getStep();\n        }\n        return null;\n    }\n\n    public static class NextRetry implements Command {\n\n        private static final long serialVersionUID = 1L;\n\n        private final Command cmd;\n\n        public NextRetry(Command cmd) {\n            this.cmd = cmd;\n        }\n\n        @Override\n        public void eval(Runtime runtime, State state, ThreadId threadId) {\n            Frame frame = state.peekFrame(threadId);\n            frame.pop();\n\n            Retry retry = (Retry) frame.getLocal(RETRY_CFG);\n            int attemptNo = (int) frame.getLocal(Constants.Runtime.RETRY_ATTEMPT_NUMBER);\n            Throwable lastError = (Throwable) frame.getLocal(Frame.LAST_EXCEPTION_KEY);\n\n            if (attemptNo >= retry.times()) {\n                // no more attempts left, rethrow the last exception\n                if (lastError instanceof RuntimeException) {\n                    throw (RuntimeException) lastError;\n                } else {\n                    throw new RuntimeException(lastError);\n                }\n            }\n\n            Duration delay = retry.delay();\n            log.warn(\"Last error: {}. Waiting for {}ms before retry (attempt #{})\", lastError, delay.toMillis(), attemptNo);\n\n            try {\n                Thread.sleep(delay.toMillis());\n            } catch (InterruptedException e) {\n                Thread.currentThread().interrupt();\n            }\n\n            // set the same exception handler for the next attempt\n            frame.setExceptionHandler(this);\n            // update the attempt number\n            frame.setLocal(Constants.Runtime.RETRY_ATTEMPT_NUMBER, attemptNo + 1);\n\n            // override the task's \"in\" if needed\n            if (retry.input() != null) {\n                Map<String, Object> m = Collections.unmodifiableMap(Objects.requireNonNull(retry.input()));\n                VMUtils.setInputOverrides(frame, m);\n            }\n\n            frame.push(cmd);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/ReturnCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.ReturnStep;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\npublic class ReturnCommand extends StepCommand<ReturnStep> implements ElementEventProducer {\n\n    private static final long serialVersionUID = 1L;\n\n    public ReturnCommand(ReturnStep step) {\n        super(step);\n    }\n\n    @Override\n    public String getDescription(State state, ThreadId threadId) {\n        return \"Return\";\n    }\n\n    @Override\n    protected void execute(Runtime runtime, State state, ThreadId threadId) {\n        Frame frame = state.peekFrame(threadId);\n        frame.pop();\n        frame.push(new PopFrameCommand());\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/SaveLastErrorCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.JsonSerializer;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.SerializationFeature;\nimport com.fasterxml.jackson.databind.SerializerProvider;\nimport com.fasterxml.jackson.databind.module.SimpleModule;\nimport com.walmartlabs.concord.runtime.v2.runner.PersistenceService;\nimport com.walmartlabs.concord.runtime.v2.sdk.UserDefinedException;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * Saves the current unhandled exception as a process metadata variable.\n */\npublic class SaveLastErrorCommand implements Command {\n\n    // for backward compatibility (java8 concord 1.92.0 version)\n    private static final long serialVersionUID = 5759484819869224819L;\n\n    private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {\n    };\n\n    private static final AtomicInteger idGenerator = new AtomicInteger(1);\n\n    @Override\n    public void eval(Runtime runtime, State state, ThreadId threadId) throws Exception {\n        Frame frame = state.peekFrame(threadId);\n        frame.pop();\n\n        Exception e = VMUtils.assertLocal(state, threadId, Frame.LAST_EXCEPTION_KEY);\n\n        ObjectMapper om = runtime.getService(ObjectMapper.class);\n        PersistenceService persistenceService = runtime.getService(PersistenceService.class);\n        Map<String, Object> currentOut = persistenceService.loadPersistedFile(Constants.Files.OUT_VALUES_FILE_NAME, in -> om.readValue(in, MAP_TYPE));\n\n        Map<String, Object> outValues = new HashMap<>(currentOut != null ? currentOut : Collections.emptyMap());\n        outValues.put(Constants.Context.LAST_ERROR_KEY, serialize(e));\n        persistenceService.persistFile(Constants.Files.OUT_VALUES_FILE_NAME,\n                out -> om.writeValue(out, outValues));\n\n        throw e;\n    }\n\n    private static Map<String, Object> serialize(Exception e) {\n        try {\n            return createMapper().convertValue(e, MAP_TYPE);\n        } catch (Exception ex) {\n            // ignore ex\n            return Collections.singletonMap(\"message\", e.getMessage());\n        }\n    }\n\n    private static ObjectMapper createMapper() {\n        var module = new SimpleModule();\n        module.addSerializer(ParallelExecutionException.class, new ParallelExceptionSerializer());\n        module.addSerializer(WrappedException.class, new WrappedExceptionSerializer());\n        module.addSerializer(UserDefinedException.class, new UserDefinedExceptionSerializer());\n        module.addSerializer(Exception.class, new ExceptionSerializer());\n\n        var om = new ObjectMapper();\n        om.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);\n        om.registerModule(module);\n        return om;\n    }\n\n    private static class ParallelExceptionSerializer extends JsonSerializer<ParallelExecutionException> {\n\n        @Override\n        public void serialize(ParallelExecutionException exception, JsonGenerator gen, SerializerProvider serializers) throws IOException {\n            gen.writeStartObject();\n            gen.writeNumberField(\"@id\", idGenerator.getAndIncrement());\n            gen.writeStringField(\"message\", exception.getMessage());\n\n            gen.writeArrayFieldStart(\"exceptions\");\n            for (var e : exception.getExceptions()) {\n                gen.writeObject(e);\n            }\n            gen.writeEndArray();\n\n            gen.writeEndObject();\n        }\n    }\n\n    private static class WrappedExceptionSerializer extends JsonSerializer<WrappedException> {\n\n        @Override\n        public void serialize(WrappedException exception, JsonGenerator gen, SerializerProvider serializers) throws IOException {\n            gen.writeObject(exception.getCause());\n        }\n    }\n\n    private static class UserDefinedExceptionSerializer extends JsonSerializer<UserDefinedException> {\n\n        @Override\n        public void serialize(UserDefinedException exception, JsonGenerator gen, SerializerProvider serializers) throws IOException {\n            gen.writeStartObject();\n            gen.writeNumberField(\"@id\", idGenerator.getAndIncrement());\n            gen.writeStringField(\"message\", exception.getMessage());\n            if (exception.getPayload() != null && !exception.getPayload().isEmpty()) {\n                gen.writeObjectField(\"payload\", exception.getPayload());\n            }\n            gen.writeEndObject();\n        }\n    }\n\n    private static class ExceptionSerializer extends JsonSerializer<Exception> {\n\n        @Override\n        public void serialize(Exception exception, JsonGenerator gen, SerializerProvider serializers) throws IOException {\n            gen.writeStartObject();\n            gen.writeNumberField(\"@id\", idGenerator.getAndIncrement());\n            gen.writeStringField(\"message\", exception.getMessage());\n            gen.writeStringField(\"type\", exception.getClass().getName());\n            gen.writeEndObject();\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/SaveOutVariablesOnErrorCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.runner.OutVariablesProcessor;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\nimport java.io.Serial;\n\npublic class SaveOutVariablesOnErrorCommand implements Command {\n\n    @Serial\n    private static final long serialVersionUID = 1L;\n\n    @Override\n    public void eval(Runtime runtime, State state, ThreadId threadId) throws Exception {\n        Frame frame = state.peekFrame(threadId);\n        frame.pop();\n\n        runtime.getService(OutVariablesProcessor.class).afterProcessEnds(runtime, state, frame);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/ScriptCallCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.ScriptCall;\nimport com.walmartlabs.concord.runtime.v2.model.ScriptCallOptions;\nimport com.walmartlabs.concord.runtime.v2.runner.ResourceResolver;\nimport com.walmartlabs.concord.runtime.v2.runner.script.ScriptEvaluator;\nimport com.walmartlabs.concord.runtime.v2.runner.script.ScriptResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContextFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.ExpressionEvaluator;\nimport com.walmartlabs.concord.runtime.v2.sdk.UserDefinedException;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.State;\nimport com.walmartlabs.concord.svm.ThreadId;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.*;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.UUID;\n\n/**\n * Calls the specified script task. Responsible for preparing the script's input.\n */\npublic class ScriptCallCommand extends StepCommand<ScriptCall> implements ElementEventProducer {\n\n    private static final Logger log = LoggerFactory.getLogger(ScriptCallCommand.class);\n\n    private static final long serialVersionUID = 1L;\n\n    public ScriptCallCommand(UUID correlationId, ScriptCall step) {\n        super(correlationId, step);\n    }\n\n    @Override\n    public String getDescription(State state, ThreadId threadId) {\n        return \"Script: \" + getStep().getLanguageOrRef();\n    }\n\n    @Override\n    protected void execute(Runtime runtime, State state, ThreadId threadId) {\n        state.peekFrame(threadId).pop();\n\n        Context ctx = runtime.getService(Context.class);\n\n        EvalContextFactory ecf = runtime.getService(EvalContextFactory.class);\n        ExpressionEvaluator expressionEvaluator = runtime.getService(ExpressionEvaluator.class);\n        ScriptEvaluator scriptEvaluator = runtime.getService(ScriptEvaluator.class);\n        ResourceResolver resourceResolver = runtime.getService(ResourceResolver.class);\n\n        ScriptCall call = getStep();\n        ScriptCallOptions opts = Objects.requireNonNull(call.getOptions());\n\n        assertScriptDryRunReady(ctx, opts);\n\n        Map<String, Object> input = VMUtils.prepareInput(ecf, expressionEvaluator, ctx, opts.input(), opts.inputExpression());\n\n        String language = getLanguage(ecf, expressionEvaluator, scriptEvaluator, ctx, call);\n        Reader content = getContent(ecf, expressionEvaluator, resourceResolver, ctx, call);\n\n        ScriptResult scriptResult;\n        try {\n            scriptResult = scriptEvaluator.eval(ctx, language, content, input);\n        } finally {\n            try {\n                content.close();\n            } catch (IOException e) {\n                // we don't have to do anything about it, but we're going to log the error just in case\n                log.warn(\"Error while closing the script's reader: {}\", e.getMessage() + \". This is most likely a bug.\");\n            }\n        }\n\n        OutputUtils.process(runtime, ctx, scriptResult.items(), opts.out(), opts.outExpr());\n    }\n\n    private static String getLanguage(EvalContextFactory ecf, ExpressionEvaluator expressionEvaluator, ScriptEvaluator scriptEvaluator, Context ctx, ScriptCall call) {\n        String languageOrRef = expressionEvaluator.eval(ecf.global(ctx), call.getLanguageOrRef(), String.class);\n\n        // if we have body then languageOrRef is language\n        if (Objects.requireNonNull(call.getOptions()).body() != null) {\n            return assertLanguage(scriptEvaluator, languageOrRef);\n        }\n\n        String maybeLanguage = getExtension(languageOrRef);\n        if (maybeLanguage != null) {\n            return assertLanguage(scriptEvaluator, maybeLanguage);\n        }\n\n        if (scriptEvaluator.getLanguage(languageOrRef) != null) {\n            throw new RuntimeException(\"Invalid step definition: 'body' parameter not found.\");\n        }\n\n        throw new RuntimeException(\"Can't determine the script language: \" + call.getLanguageOrRef() + \" (\" + languageOrRef + \"). \" +\n                \"Check if the script's name is correct and the required dependencies are declared.\");\n    }\n\n    private static String assertLanguage(ScriptEvaluator scriptEvaluator, String language) {\n        String normalizedLanguage = scriptEvaluator.getLanguage(language);\n\n        if (normalizedLanguage != null) {\n            return normalizedLanguage;\n        }\n\n        throw new UserDefinedException(\"Unknown language '\" + language + \"'. Check process dependencies.\");\n    }\n\n    private static String getExtension(String s) {\n        int i = s.lastIndexOf(\".\");\n        if (i < 0 || i + 1 >= s.length()) {\n            return null;\n        }\n\n        return s.substring(i + 1);\n    }\n\n    private static Reader getContent(EvalContextFactory ecf, ExpressionEvaluator expressionEvaluator, ResourceResolver resourceResolver, Context ctx, ScriptCall call) {\n        if (call.getOptions().body() != null) {\n            return new StringReader(Objects.requireNonNull(call.getOptions().body()));\n        }\n\n        String ref = expressionEvaluator.eval(ecf.global(ctx), call.getLanguageOrRef(), String.class);\n        try {\n            InputStream in = resourceResolver.resolve(ref);\n            if (in == null) {\n                throw new RuntimeException(\"Resource not found: \" + ref);\n            }\n            return new InputStreamReader(in);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static void assertScriptDryRunReady(Context ctx, ScriptCallOptions opts) {\n        if (!ctx.processConfiguration().dryRun()) {\n            return;\n        }\n\n        if (StepOptionsUtils.isDryRunReady(ctx, opts)) {\n            return;\n        }\n\n        throw new UserDefinedException(\"Dry-run mode is not supported for this 'script' step\");\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/SetVariablesCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.SetVariablesStep;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContextFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.ExpressionEvaluator;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.State;\nimport com.walmartlabs.concord.svm.ThreadId;\n\nimport java.util.Map;\n\npublic class SetVariablesCommand extends StepCommand<SetVariablesStep> implements ElementEventProducer {\n\n    private static final long serialVersionUID = 1L;\n\n    public SetVariablesCommand(SetVariablesStep step) {\n        super(step);\n    }\n\n    @Override\n    public String getDescription(State state, ThreadId threadId) {\n        return \"Set variables\";\n    }\n\n    @Override\n    protected void execute(Runtime runtime, State state, ThreadId threadId) {\n        state.peekFrame(threadId).pop();\n\n        SetVariablesStep step = getStep();\n\n        Context ctx = runtime.getService(Context.class);\n\n        // eval the input\n        EvalContextFactory ecf = runtime.getService(EvalContextFactory.class);\n        ExpressionEvaluator ee = runtime.getService(ExpressionEvaluator.class);\n        Map<String, Object> vars = ee.evalAsMap(ecf.scope(ctx), step.getVars());\n\n        vars.forEach((k, v) -> {\n            ctx.variables().set(k, v);\n        });\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/StepCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Location;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.runner.context.ContextFactory;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.LogContext;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.RunnerLogger;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.ContextProvider;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\n\n/**\n * Base class for commands that were created from a flow {@link Step}.\n * <p/>\n * Subclasses must implement the {@link #execute(Runtime, State, ThreadId)} method.\n * <p/>\n * Subclasses can optionally implement {@link #getSegmentName(Context, Step)} to\n * enable \"segmented logging\" for the duration of their execution.\n */\npublic abstract class StepCommand<T extends Step> implements Command {\n\n    private static final Logger log = LoggerFactory.getLogger(StepCommand.class);\n\n    private static final long serialVersionUID = 1L;\n\n    private final T step;\n\n    private final UUID correlationId;\n\n    protected StepCommand(T step) {\n        this(UUID.randomUUID(), step);\n    }\n\n    protected StepCommand(UUID correlationId, T step) {\n        this.step = step;\n        this.correlationId = correlationId;\n    }\n\n    public T getStep() {\n        return step;\n    }\n\n    @Override\n    public void eval(Runtime runtime, State state, ThreadId threadId) {\n        ContextFactory contextFactory = runtime.getService(ContextFactory.class);\n\n        T step = getStep();\n        UUID correlationId = getCorrelationId();\n        Context ctx = contextFactory.create(runtime, state, threadId, step, correlationId);\n\n        LogContext logContext = LogSegmentUtils.getLogContext(threadId, state);\n        if (logContext == null) {\n            executeWithContext(ctx, runtime, state, threadId);\n        } else {\n            runtime.getService(RunnerLogger.class).withContext(logContext,\n                    () -> executeWithContext(ctx, runtime, state, threadId));\n        }\n    }\n\n    public UUID getCorrelationId() {\n        return correlationId;\n    }\n\n    private void executeWithContext(Context ctx, Runtime runtime, State state, ThreadId threadId) {\n        ContextProvider.withContext(ctx, () -> {\n            try {\n                execute(runtime, state, threadId);\n            } catch (Exception e) {\n                logException(step, state, threadId, e);\n                throw e;\n            }\n        });\n    }\n\n    public static void logException(Step step, State state, ThreadId threadId, Exception e) {\n        // for backward compatibility...\n        if (step != null) {\n            log.error(\"{}. {}\", Location.toErrorPrefix(step.getLocation()), e.getMessage());\n        } else {\n            log.error(\"{}\", e.getMessage());\n        }\n\n        List<StackTraceItem> stackTrace = state.getStackTrace(threadId);\n        if (!stackTrace.isEmpty()) {\n            log.error(\"Call stack:\\n{}\", stackTrace.stream().map(StackTraceItem::toString).collect(Collectors.joining(\"\\n\")));\n        }\n    }\n\n    protected abstract void execute(Runtime runtime, State state, ThreadId threadId);\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/StepOptionsUtils.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.parser.StepOptions;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\n\npublic final class StepOptionsUtils {\n\n    public static boolean isDryRunReady(Context ctx, StepOptions options) {\n        if (options == null) {\n            return false;\n        }\n\n        Object result = options.meta().get(\"dryRunReady\");\n        if (result == null) {\n            return false;\n        }\n        if (result instanceof Boolean) {\n            return (Boolean) result;\n        }\n\n        Object evalResult = ctx.eval(result.toString(), Object.class);\n        if (evalResult == null) {\n            return false;\n        }\n        if (evalResult instanceof Boolean) {\n            return (Boolean) evalResult;\n        }\n\n        return Boolean.parseBoolean(evalResult.toString());\n    }\n\n    private StepOptionsUtils() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/SuspendCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentStatus;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.RunnerLogger;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\nimport java.util.Objects;\n\npublic class SuspendCommand implements Command {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String eventRef;\n\n    public SuspendCommand(String eventRef) {\n        this.eventRef = eventRef;\n    }\n\n    @Override\n    public void eval(Runtime runtime, State state, ThreadId threadId) {\n        state.peekFrame(threadId).pop();\n        state.setEventRef(threadId, eventRef);\n        state.setStatus(threadId, ThreadStatus.SUSPENDED);\n\n        // SUSPEND log segments\n        RunnerLogger logger = runtime.getService(RunnerLogger.class);\n        LogSegmentUtils.getLogContexts(threadId, state).stream()\n                .filter(Objects::nonNull)\n                .filter(c -> c.segmentId() != null)\n                .forEach(c -> logger.setSegmentStatus(c.segmentId(), LogSegmentStatus.SUSPENDED));\n\n        state.peekFrame(threadId).push(new ResumeLogSegmentsCommand());\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/SuspendStepCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.SuspendStep;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\npublic class SuspendStepCommand extends StepCommand<SuspendStep> {\n\n    private static final long serialVersionUID = 1L;\n\n    public SuspendStepCommand(SuspendStep step) {\n        super(step);\n    }\n\n    @Override\n    protected void execute(Runtime runtime, State state, ThreadId threadId) {\n        Frame frame = state.peekFrame(threadId);\n        frame.pop();\n\n        frame.push(new SuspendCommand(getStep().getEvent()));\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/SwitchCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.SwitchStep;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContext;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContextFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.ExpressionEvaluator;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\nimport javax.annotation.Nullable;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\n\npublic class SwitchCommand extends StepCommand<SwitchStep> implements ElementEventProducer {\n\n    private static final long serialVersionUID = 1L;\n\n    private final List<Map.Entry<String, Command>> caseCommands;\n    private final Command defaultCommand;\n\n    public SwitchCommand(SwitchStep step, List<Map.Entry<String, Command>> caseCommands, @Nullable Command defaultCommand) {\n        super(step);\n        this.caseCommands = caseCommands;\n        this.defaultCommand = defaultCommand;\n    }\n\n    @Override\n    public String getDescription(State state, ThreadId threadId) {\n        return \"Switch: \" + getStep().getExpression();\n    }\n\n    @Override\n    protected void execute(Runtime runtime, State state, ThreadId threadId) {\n        Frame frame = state.peekFrame(threadId);\n        frame.pop();\n\n        SwitchStep step = getStep();\n        String expr = step.getExpression();\n\n        Context ctx = runtime.getService(Context.class);\n        EvalContextFactory ecf = runtime.getService(EvalContextFactory.class);\n        EvalContext evalContext = ecf.global(ctx);\n\n        ExpressionEvaluator ee = runtime.getService(ExpressionEvaluator.class);\n        String switchResult = ee.eval(evalContext, expr, String.class);\n        boolean caseFound = false;\n        for (Map.Entry<String, Command> kv : caseCommands) {\n            String caseLabel = ee.eval(evalContext, kv.getKey(), String.class);\n            if (Objects.equals(switchResult, caseLabel)) {\n                frame.push(kv.getValue());\n                caseFound = true;\n                break;\n            }\n        }\n\n        if (!caseFound && defaultCommand != null) {\n            frame.push(defaultCommand);\n        }\n\n        // TODO: log case not found?\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/TaskCallCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.sun.el.util.ReflectionUtil;\nimport com.walmartlabs.concord.runtime.v2.model.TaskCall;\nimport com.walmartlabs.concord.runtime.v2.model.TaskCallOptions;\nimport com.walmartlabs.concord.runtime.v2.model.TaskCallValidation;\nimport com.walmartlabs.concord.runtime.v2.runner.el.resolvers.SensitiveDataProcessor;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.*;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\nimport java.util.Collections;\nimport java.util.Objects;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallInterceptor.CallContext;\nimport static com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallInterceptor.Method;\n\n/**\n * Calls the specified task. Responsible for preparing the task's input\n * and processing the output.\n */\npublic class TaskCallCommand extends StepCommand<TaskCall> {\n\n    private static final long serialVersionUID = 1L;\n\n    public TaskCallCommand(UUID correlationId, TaskCall step) {\n        super(correlationId, step);\n    }\n\n    @Override\n    protected void execute(Runtime runtime, State state, ThreadId threadId) {\n        Frame frame = state.peekFrame(threadId);\n        frame.pop();\n\n        Context ctx = runtime.getService(Context.class);\n\n        TaskProviders taskProviders = runtime.getService(TaskProviders.class);\n        EvalContextFactory ecf = runtime.getService(EvalContextFactory.class);\n        ExpressionEvaluator expressionEvaluator = runtime.getService(ExpressionEvaluator.class);\n\n        TaskCall call = getStep();\n        String taskName = call.getName();\n        Task t = taskProviders.createTask(ctx, taskName);\n        if (t == null) {\n            throw new UserDefinedException(\"Task not found: '\" + taskName + \"'\");\n        }\n\n        TaskCallInterceptor interceptor = runtime.getService(TaskCallInterceptor.class);\n\n        CallContext callContext = CallContext.builder()\n                .threadId(threadId)\n                .taskName(taskName)\n                .correlationId(ctx.execution().correlationId())\n                .currentStep(getStep())\n                .processDefinition(ctx.execution().processDefinition())\n                .build();\n\n        TaskCallOptions opts = Objects.requireNonNull(call.getOptions());\n        Variables input = new MapBackedVariables(VMUtils.prepareInput(ecf, expressionEvaluator, ctx, opts.input(), opts.inputExpression()));\n\n        // Input/output validation (null-safe for backward compatibility with old serialized state)\n        TaskCallValidation validation = TaskSchemaValidation.getTaskCallValidation(ctx);\n        Class<? extends Task> taskClass = TaskSchemaValidation.getTaskClass(taskProviders, ctx, t, taskName);\n        TaskSchemaValidation.validateInput(runtime, taskName, taskClass, input, validation);\n\n        TaskResult result;\n        try {\n            result = interceptor.invoke(callContext, Method.of(t.getClass(), \"execute\", Collections.singletonList(input)),\n                    () -> t.execute(input));\n\n            if (result instanceof TaskResult.SimpleResult simpleResult) {\n                var m = ReflectionUtil.findMethod(t.getClass(), \"execute\", new Class[]{Variables.class}, new Variables[]{input});\n                runtime.getService(SensitiveDataProcessor.class).process(simpleResult.values(), m);\n            }\n        } catch (TaskException e) {\n            result = TaskResult.fail(e.getCause());\n        } catch (RuntimeException e) {\n            throw e;\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n\n        TaskSchemaValidation.validateOutput(runtime, taskName, taskClass, result, validation);\n        TaskCallUtils.processTaskResult(runtime, ctx, taskName, opts, result);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/TaskCallUtils.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.TaskCallOptions;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.UserDefinedException;\nimport com.walmartlabs.concord.svm.Runtime;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Map;\n\npublic final class TaskCallUtils {\n\n    private static final Logger log = LoggerFactory.getLogger(TaskCallUtils.class);\n\n    public static void processTaskResult(Runtime runtime, Context ctx, String taskName, TaskCallOptions opts, TaskResult result) {\n        assertTaskResult(taskName, result);\n\n        if (result instanceof TaskResult.SuspendResult r) {\n            ctx.suspend(r.eventName());\n        } else if (result instanceof TaskResult.ReentrantSuspendResult r) {\n            ctx.reentrantSuspend(r.eventName(), r.payload());\n        } else if (result instanceof TaskResult.SimpleResult r) {\n\n            OutputUtils.process(runtime, ctx, toMap(ctx, r), opts.out(), opts.outExpr());\n\n            if (r.ok()) {\n                return;\n            }\n\n            if (result instanceof TaskResult.SimpleFailResult rr) {\n                RuntimeException exception = toException(rr);\n\n                if (opts.ignoreErrors()) {\n                   log.error(\"Error ignored:\", exception);\n                } else {\n                    throw exception;\n                }\n            }\n        } else {\n            throw new IllegalArgumentException(\"Unknown result: '\" + result.getClass() + \"'\");\n        }\n    }\n\n    private static RuntimeException toException(TaskResult.SimpleFailResult rr) {\n        var cause = rr.cause();\n        if (cause == null) {\n            return new UserDefinedException(rr.error(), rr.values());\n        }\n\n        if (cause instanceof RuntimeException re) {\n            return re;\n        }\n\n        return new WrappedException(rr.cause());\n    }\n\n    private static Map<String, Object> toMap(Context ctx, TaskResult.SimpleResult result) {\n        Map<String, Object> resultAsMap = result.toMap();\n        resultAsMap.put(\"threadId\", ctx.execution().currentThreadId().id());\n        return resultAsMap;\n    }\n\n    private static void assertTaskResult(String taskName, TaskResult taskResult) {\n        if (taskResult == null) {\n            throw new RuntimeException(\"Task '\" + taskName + \"' return NULL. This is most likely a bug.\");\n        }\n    }\n\n    private TaskCallUtils() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/TaskResumeCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.TaskCall;\nimport com.walmartlabs.concord.runtime.v2.model.TaskCallValidation;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallInterceptor;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallInterceptor.Method;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskException;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskProviders;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\nimport java.util.Collections;\nimport java.util.UUID;\n\npublic class TaskResumeCommand extends StepCommand<TaskCall> {\n\n    private static final long serialVersionUID = 1L;\n\n    private final ResumeEvent event;\n\n    protected TaskResumeCommand(UUID correlationId, TaskCall step, ResumeEvent event) {\n        super(correlationId, step);\n\n        this.event = event;\n    }\n\n    @Override\n    protected void execute(Runtime runtime, State state, ThreadId threadId) {\n        Frame frame = state.peekFrame(threadId);\n        frame.pop();\n\n        Context ctx = runtime.getService(Context.class);\n\n        String taskName = getStep().getName();\n\n        TaskProviders taskProviders = runtime.getService(TaskProviders.class);\n        Task task = taskProviders.createTask(ctx, getStep().getName());\n        if (task == null) {\n            throw new IllegalStateException(\"Task not found: \" + taskName);\n        }\n\n        if (!(task instanceof ReentrantTask rt)) {\n            throw new IllegalStateException(\"The task doesn't implement the \" + ReentrantTask.class.getSimpleName() +\n                    \" interface and cannot be used as a \\\"reentrant\\\" task: \" + taskName);\n        }\n        Class<? extends Task> taskClass = TaskSchemaValidation.getTaskClass(taskProviders, ctx, task, taskName);\n        TaskCallValidation validation = TaskSchemaValidation.getTaskCallValidation(ctx);\n\n        TaskCallInterceptor.CallContext callContext = TaskCallInterceptor.CallContext.builder()\n                .threadId(threadId)\n                .taskName(taskName)\n                .correlationId(ctx.execution().correlationId())\n                .currentStep(getStep())\n                .processDefinition(ctx.execution().processDefinition())\n                .build();\n\n        TaskCallInterceptor interceptor = runtime.getService(TaskCallInterceptor.class);\n\n        TaskResult result;\n        try {\n            result = interceptor.invoke(callContext, Method.of(rt.getClass(), \"resume\", Collections.singletonList(event)),\n                    () -> rt.resume(event));\n        } catch (TaskException e) {\n            result = TaskResult.fail(e.getCause());\n        } catch (RuntimeException e) {\n            throw e;\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n\n        TaskSchemaValidation.validateOutput(runtime, taskName, taskClass, result, validation);\n        TaskCallUtils.processTaskResult(runtime, ctx, taskName, getStep().getOptions(), result);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/TaskSchemaValidation.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.TaskCallValidation;\nimport com.walmartlabs.concord.runtime.v2.model.TaskCallValidation.ValidationMode;\nimport com.walmartlabs.concord.runtime.v2.model.ValidationConfiguration;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskProviders;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskSchemaValidationException;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskSchemaValidationResult;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskSchemaValidator;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\nimport com.walmartlabs.concord.svm.Runtime;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.List;\n\nfinal class TaskSchemaValidation {\n\n    private static final Logger log = LoggerFactory.getLogger(TaskSchemaValidation.class);\n\n    static Class<? extends Task> getTaskClass(TaskProviders taskProviders, Context ctx, Task task, String taskName) {\n        Class<? extends Task> taskClass = taskProviders.getTaskClass(ctx, taskName);\n        if (taskClass != null) {\n            return taskClass;\n        }\n        return task.getClass();\n    }\n\n    static void validateInput(Runtime runtime,\n                              String taskName,\n                              Class<? extends Task> taskClass,\n                              Variables input,\n                              TaskCallValidation validation) {\n        if (validation.in() == ValidationMode.DISABLED) {\n            return;\n        }\n\n        TaskSchemaValidator validator = runtime.getService(TaskSchemaValidator.class);\n        TaskSchemaValidationResult result = validator.validateInput(taskName, taskClass, input.toMap());\n        handleValidationResult(taskName, \"in\", result, validation.in());\n    }\n\n    static void validateOutput(Runtime runtime,\n                               String taskName,\n                               Class<? extends Task> taskClass,\n                               TaskResult result,\n                               TaskCallValidation validation) {\n        if (result == null) {\n            return;\n        }\n\n        if (validation.out() == ValidationMode.DISABLED) {\n            return;\n        }\n\n        if (!(result instanceof TaskResult.SimpleResult simpleResult)) {\n            String resultType = result.getClass().getSimpleName();\n            if (validation.out() == ValidationMode.WARN) {\n                log.warn(\"Task '{}' output validation enabled but result type '{}' does not expose output values\", taskName, resultType);\n            } else {\n                log.debug(\"Task '{}' output validation skipped for result type '{}'\", taskName, resultType);\n            }\n            return;\n        }\n\n        // Failed results are skipped so schema errors do not mask the task's original failure.\n        if (!simpleResult.ok()) {\n            log.debug(\"Task '{}' output validation skipped for failed result\", taskName);\n            return;\n        }\n\n        TaskSchemaValidator validator = runtime.getService(TaskSchemaValidator.class);\n        TaskSchemaValidationResult validationResult = validator.validateOutput(taskName, taskClass, simpleResult.toMap());\n        handleValidationResult(taskName, \"out\", validationResult, validation.out());\n    }\n\n    /**\n     * Get the task call validation configuration, handling null for backward compatibility\n     * with old serialized process definitions that don't have the validation field.\n     */\n    static TaskCallValidation getTaskCallValidation(Context ctx) {\n        ValidationConfiguration cfg = ctx.execution().processDefinition().configuration().validation();\n        if (cfg == null) {\n            return new TaskCallValidation();\n        }\n\n        TaskCallValidation validation = cfg.taskCalls();\n        if (validation == null) {\n            return new TaskCallValidation();\n        }\n\n        return validation;\n    }\n\n    private static void handleValidationResult(String taskName,\n                                               String section,\n                                               TaskSchemaValidationResult result,\n                                               ValidationMode mode) {\n        if (!result.hasErrors()) {\n            return;\n        }\n\n        if (mode == ValidationMode.WARN) {\n            log.warn(\"Task '{}' {} validation errors{}:{}\", taskName, section, schemaResourceSuffix(result), formatErrors(result.errors()));\n        } else if (mode == ValidationMode.FAIL) {\n            throw new TaskSchemaValidationException(taskName, section, result.schemaResource(), result.errors());\n        }\n    }\n\n    private static String schemaResourceSuffix(TaskSchemaValidationResult result) {\n        if (result.schemaResource() == null) {\n            return \"\";\n        }\n        return \" (\" + result.schemaResource() + \")\";\n    }\n\n    private static String formatErrors(List<String> errors) {\n        StringBuilder sb = new StringBuilder();\n        for (String error : errors) {\n            sb.append(\"\\n  - \").append(error);\n        }\n        return sb.toString();\n    }\n\n    private TaskSchemaValidation() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/TaskSuspendCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.TaskCall;\nimport com.walmartlabs.concord.runtime.v2.runner.context.ResumeEventImpl;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\nimport java.io.Serializable;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic class TaskSuspendCommand implements Command {\n\n    private static final long serialVersionUID = 1L;\n\n    private final UUID correlationId;\n    private final String eventName;\n    private final TaskCall step;\n    private final Map<String, Serializable> taskState;\n\n    public TaskSuspendCommand(UUID correlationId, String eventName, TaskCall step, Map<String, Serializable> taskState) {\n        this.correlationId = correlationId;\n        this.eventName = eventName;\n        this.step = step;\n        this.taskState = taskState;\n    }\n\n    @Override\n    public void eval(Runtime runtime, State state, ThreadId threadId) {\n        Frame frame = state.peekFrame(threadId);\n        frame.pop();\n\n        frame.push(new TaskResumeCommand(correlationId, step, new ResumeEventImpl(eventName, taskState)));\n        frame.push(new SuspendCommand(eventName));\n    }\n}"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/UpdateLocalsCommand.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.runner.context.ContextFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContextFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.ExpressionEvaluator;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Takes the input, interpolates its values and sets the result\n * to all root frame's local variables.\n * <p/>\n * Optionally takes a list of threads which root frames should be\n * updated with provided variables.\n */\npublic class UpdateLocalsCommand implements Command {\n\n    private static final long serialVersionUID = 1L;\n\n    private final Map<String, Object> input;\n    private final Collection<ThreadId> threadIds;\n\n    public UpdateLocalsCommand(Map<String, Object> input) {\n        this(input, Collections.emptyList());\n    }\n\n    public UpdateLocalsCommand(Map<String, Object> input, Collection<ThreadId> threadIds) {\n        this.input = input;\n        this.threadIds = threadIds;\n    }\n\n    @Override\n    public void eval(Runtime runtime, State state, ThreadId threadId) {\n        // don't \"pop\" the stack, this command is a special case and evaluated separately\n\n        // create the context explicitly as this command is evaluated outside or the regular\n        // loop and doesn't inherit StepCommand\n        ContextFactory contextFactory = runtime.getService(ContextFactory.class);\n        Context ctx = contextFactory.create(runtime, state, threadId, null);\n\n        Collection<ThreadId> threads = threadIds;\n        if (threads.isEmpty()) {\n            threads = Collections.singletonList(threadId);\n        }\n\n        // allow access to arguments from arguments:\n        /* e.g.\n           configuration:\n             arguments:\n               args:\n                 k1: v1\n                 k2: ${context.variables().get('args.k1')}\n         */\n        for (ThreadId tid : threads) {\n            Frame root = VMUtils.assertNearestRoot(state, tid);\n            VMUtils.putLocals(root, input);\n        }\n\n        EvalContextFactory ecf = runtime.getService(EvalContextFactory.class);\n        ExpressionEvaluator ee = runtime.getService(ExpressionEvaluator.class);\n        Map<String, Object> m = ee.evalAsMap(ecf.scope(ctx), input);\n\n        for (ThreadId tid : threads) {\n            List<Frame> frames = state.getFrames(tid);\n\n            for (Frame f : frames) {\n                if (f.getType() == FrameType.ROOT) {\n                    VMUtils.putLocals(f, m);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/VMUtils.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContextFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.ExpressionEvaluator;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.svm.Frame;\nimport com.walmartlabs.concord.svm.FrameType;\nimport com.walmartlabs.concord.svm.State;\nimport com.walmartlabs.concord.svm.ThreadId;\n\nimport java.io.Serializable;\nimport java.util.*;\n\npublic final class VMUtils {\n\n    private static final String FRAME_INPUT_OVERRIDES_KEY = \"__frame_input_overrides\";\n\n    /**\n     * Evaluates a step's {@code in} section using all currently available variables.\n     */\n    public static Map<String, Object> prepareInput(EvalContextFactory ecf,\n                                                   ExpressionEvaluator ee,\n                                                   Context ctx,\n                                                   Map<String, Serializable> input,\n                                                   String inputExpression) {\n\n        Object value = null;\n        if (inputExpression != null) {\n            value = inputExpression;\n        } else if (input != null && !input.isEmpty()) {\n            value = input;\n        }\n\n        Map<String, Object> stepInput = ee.evalAsMap(ecf.global(ctx), value);\n        if (stepInput == null) {\n            stepInput = Collections.emptyMap();\n        }\n\n        Map<String, Object> overrides = getLocal(ctx.execution().state(), ctx.execution().currentThreadId(), FRAME_INPUT_OVERRIDES_KEY);\n        overrides = ee.evalAsMap(ecf.global(ctx), overrides);\n        if (overrides == null) {\n            overrides = Collections.emptyMap();\n        }\n\n        return Collections.unmodifiableMap(ConfigurationUtils.deepMerge(stepInput, overrides));\n    }\n\n    public static void setInputOverrides(Frame frame, Map<String, Object> overrides) {\n        putLocal(frame, FRAME_INPUT_OVERRIDES_KEY, new HashMap<>(overrides));\n    }\n\n    /**\n     * Returns a local variable {@code key} or throws an {@link IllegalStateException}\n     * if the variable doesn't exist or {@code null}.\n     * <p/>\n     * Only the current frame is considered.\n     */\n    public static <T> T assertLocal(State state, ThreadId threadId, String key) {\n        T v = getLocal(state, threadId, key);\n        if (v == null) {\n            throw new IllegalStateException(\"Variable doesn't exist or has a null value: \" + key);\n        }\n        return v;\n    }\n\n    /**\n     * Returns a local variable {@code key} or {@code null} if the variable\n     * doesn't exist or {@code null}.\n     * <p/>\n     * Only the current frame is considered.\n     */\n    @SuppressWarnings(\"unchecked\")\n    public static <T> T getLocal(State state, ThreadId threadId, String key) {\n        Serializable v = state.peekFrame(threadId).getLocal(key);\n        if (v == null) {\n            return null;\n        }\n\n        return (T) v;\n    }\n\n    /**\n     * Returns a local variable {@code key} or {@code null} if the variable\n     * doesn't exist or {@code null}.\n     * <p/>\n     * The method scans all frames starting from the most recent one and returns\n     * the first found element.\n     */\n    @SuppressWarnings(\"unchecked\")\n    public static <T> T getCombinedLocal(State state, ThreadId threadId, String key) {\n        List<Frame> frames = state.getFrames(threadId);\n\n        for (Frame f : frames) {\n            if (f.hasLocal(key)) {\n                return (T) f.getLocal(key);\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Returns {@code} true if a local variable {@code key} exists.\n     * <p/>\n     * The method scans all frames starting from the most recent one.\n     */\n    public static boolean hasCombinedLocal(State state, ThreadId threadId, String key) {\n        List<Frame> frames = state.getFrames(threadId);\n\n        for (Frame f : frames) {\n            if (f.hasLocal(key)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * @see #getCombinedLocals(State, ThreadId)\n     */\n    public static Map<String, Object> getCombinedLocals(Context ctx) {\n        ThreadId threadId = ctx.execution().currentThreadId();\n        State state = ctx.execution().state();\n        return getCombinedLocals(state, threadId);\n    }\n\n    /**\n     * Returns a map of all variables combined, starting from the bottom of the stack.\n     */\n    public static Map<String, Object> getCombinedLocals(State state, ThreadId threadId) {\n        Map<String, Object> result = new LinkedHashMap<>();\n\n        List<Frame> frames = state.getFrames(threadId);\n        for (int i = frames.size() - 1; i >= 0; i--) {\n            Frame f = frames.get(i);\n            result.putAll(f.getLocals());\n        }\n\n        return Collections.unmodifiableMap(result);\n    }\n\n    /**\n     * Puts a local variable into the nearest root frame of the specified thread.\n     * Only {@link Serializable} values are allowed.\n     */\n    public static void putLocal(State state, ThreadId threadId, String key, Object value) {\n        Frame root = assertNearestRoot(state, threadId);\n        putLocal(root, key, value);\n    }\n\n    /**\n     * Puts a local variable into the specified frame.\n     * Only {@link Serializable} values are allowed.\n     */\n    public static void putLocal(Frame frame, String key, Object value) {\n        if (value instanceof Serializable || value == null) {\n            frame.setLocal(key, (Serializable) value);\n            return;\n        }\n\n        String msg = \"Can't set a non-serializable local variable: %s -> %s\";\n        throw new IllegalStateException(String.format(msg, key, value.getClass()));\n    }\n\n    /**\n     * Puts all key-value pairs into the specified frame as locals.\n     * Only {@link Serializable} values are allowed.\n     *\n     * @see #putLocal(Frame, String, Object)\n     */\n    public static void putLocals(Frame frame, Map<String, Object> locals) {\n        if (locals == null || locals.isEmpty()) {\n            return;\n        }\n\n        locals.forEach((k, v) -> putLocal(frame, k, v));\n    }\n\n    public static Frame assertNearestRoot(State state, ThreadId threadId) {\n        List<Frame> frames = state.getFrames(threadId);\n\n        for (Frame f : frames) {\n            if (f.getType() == FrameType.ROOT) {\n                return f;\n            }\n        }\n\n        throw new IllegalStateException(\"Can't find a nearest ROOT frame. This is most likely a bug.\");\n    }\n\n    private VMUtils() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/WithItemsWrapper.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.model.WithItems;\nimport com.walmartlabs.concord.runtime.v2.runner.context.ContextFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContextFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.ExpressionEvaluator;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.ObjectOutputStream;\nimport java.io.Serializable;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\n/**\n * @deprecated use {@link com.walmartlabs.concord.runtime.v2.runner.vm.LoopWrapper}\n */\n@Deprecated\npublic abstract class WithItemsWrapper implements Command {\n\n    public static WithItemsWrapper of(Command cmd, WithItems withItems,\n                                      Collection<String> outVariables,\n                                      Map<String, Serializable> outMapping,\n                                      Step step) {\n        Collection<String> out = Collections.emptyList();\n        if (!outMapping.isEmpty()) {\n            out = new HashSet<>(outMapping.keySet());\n        } else if (!outVariables.isEmpty()) {\n            out = outVariables;\n        }\n\n        WithItems.Mode mode = withItems.mode();\n        switch (mode) {\n            case SERIAL:\n                return new SerialWithItems(cmd, withItems, out);\n            case PARALLEL:\n                return new ParallelWithItems(cmd, withItems, out, step);\n            default:\n                throw new IllegalArgumentException(\"Unknown withItems mode: \" + mode);\n        }\n    }\n\n    private static final long serialVersionUID = 1L;\n\n    // TODO move into the actual Constants\n    public static final String CURRENT_ITEMS = \"items\";\n    public static final String CURRENT_INDEX = \"itemIndex\";\n    public static final String CURRENT_ITEM = \"item\";\n\n    protected final Command cmd;\n    protected final WithItems withItems;\n    protected final Collection<String> outVariables;\n\n    protected WithItemsWrapper(Command cmd, WithItems withItems, Collection<String> outVariables) {\n        this.cmd = cmd;\n        this.withItems = withItems;\n        this.outVariables = outVariables;\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public void eval(Runtime runtime, State state, ThreadId threadId) {\n        Frame frame = state.peekFrame(threadId);\n        frame.pop();\n\n        Serializable value = withItems.value();\n        if (value == null) {\n            // value is null, not going to run the wrapped command at all\n            return;\n        }\n\n        Step currentStep = null;\n        if (cmd instanceof StepCommand) {\n            currentStep = ((StepCommand<?>) cmd).getStep();\n        }\n\n        // create the context explicitly\n        ContextFactory contextFactory = runtime.getService(ContextFactory.class);\n        Context ctx = contextFactory.create(runtime, state, threadId, currentStep);\n\n        EvalContextFactory ecf = runtime.getService(EvalContextFactory.class);\n        ExpressionEvaluator ee = runtime.getService(ExpressionEvaluator.class);\n        value = ee.eval(ecf.global(ctx), value, Serializable.class);\n\n        // prepare items\n        // store items in an ArrayList because it is Serializable\n        ArrayList<Serializable> items;\n        if (value == null) {\n            // value is null, not going to run the wrapped command at all\n            return;\n        } else if (value instanceof Collection) {\n            Collection<Serializable> v = (Collection<Serializable>) value;\n            if (v.isEmpty()) {\n                // no items, nothing to do\n                return;\n            }\n\n            items = new ArrayList<>(v);\n        } else if (value instanceof Map) {\n            Map<Serializable, Serializable> m = (Map<Serializable, Serializable>) value;\n            items = m.entrySet().stream()\n                    .map(e -> new AbstractMap.SimpleImmutableEntry<>(e.getKey(), e.getValue()))\n                    .collect(Collectors.toCollection(ArrayList::new));\n        } else if (value.getClass().isArray()) {\n            items = new ArrayList<>(Arrays.asList((Serializable[]) value));\n        } else {\n            throw new IllegalArgumentException(\"'withItems' accepts only Lists of items, Java Maps or arrays of values. Got: \" + value.getClass());\n        }\n\n        items.forEach(WithItemsWrapper::assertItem);\n\n        if (items.isEmpty()) {\n            return;\n        }\n\n        eval(state, threadId, items);\n    }\n\n    protected abstract void eval(State state, ThreadId threadId, ArrayList<Serializable> items);\n\n    static void assertItem(Object item) {\n        if (item == null) {\n            return;\n        }\n\n        try (ObjectOutputStream oos = new ObjectOutputStream(new ByteArrayOutputStream())) {\n            oos.writeObject(item);\n        } catch (IOException e) {\n            throw new IllegalArgumentException(\"Can't use non-serializable values in 'withItems': \" + item + \" (\" + item.getClass() + \")\");\n        }\n    }\n\n    static class ParallelWithItems extends WithItemsWrapper {\n\n        private static final long serialVersionUID = 1L;\n        private final Step step;\n\n        protected ParallelWithItems(Command cmd, WithItems withItems, Collection<String> outVariables, Step step) {\n            super(cmd, withItems, outVariables);\n            this.step = step;\n        }\n\n        @Override\n        protected void eval(State state, ThreadId threadId, ArrayList<Serializable> items) {\n            Frame frame = state.peekFrame(threadId);\n\n            // target frame for out variables\n            Frame targetFrame = VMUtils.assertNearestRoot(state, threadId);\n\n            List<Map.Entry<ThreadId, Serializable>> forks = items.stream()\n                    .map(e -> new AbstractMap.SimpleEntry<>(state.nextThreadId(), e))\n                    .collect(Collectors.toList());\n\n            for (int i = 0; i < forks.size(); i++) {\n                Map.Entry<ThreadId, Serializable> f = forks.get(i);\n\n                Frame cmdFrame = Frame.builder()\n                        .nonRoot()\n                        .commands()\n                        .build();\n\n                cmdFrame.setLocal(CURRENT_ITEMS, items);\n                cmdFrame.setLocal(CURRENT_INDEX, i);\n                cmdFrame.setLocal(CURRENT_ITEM, f.getValue());\n\n                // fork will create rootFrame for forked commands\n                Command itemCmd = new ForkCommand(f.getKey(),\n                        new AppendVariablesCommand(outVariables, null, targetFrame),\n                        cmd);\n                cmdFrame.push(itemCmd);\n\n                state.pushFrame(threadId, cmdFrame);\n            }\n\n            state.pushFrame(threadId, Frame.builder()\n                    .commands(new PrepareOutVariables(outVariables, targetFrame))\n                    .nonRoot()\n                    .build());\n\n            frame.push(new JoinCommand<>(forks.stream().map(Map.Entry::getKey).collect(Collectors.toSet()), step));\n        }\n    }\n\n    /**\n     * Wraps a command into a loop specified by {@code withItems} option.\n     * Creates a new call frame and keeps the item list, the current item\n     * and the index as frame-local variables.\n     */\n    static class SerialWithItems extends WithItemsWrapper {\n\n        private static final long serialVersionUID = 1L;\n\n        protected SerialWithItems(Command cmd, WithItems withItems, Collection<String> outVariables) {\n            super(cmd, withItems, outVariables);\n        }\n\n        @Override\n        protected void eval(State state, ThreadId threadId, ArrayList<Serializable> items) {\n            Frame loop = Frame.builder()\n                    .nonRoot()\n                    .build();\n\n            loop.setLocal(CURRENT_ITEMS, items);\n            loop.setLocal(CURRENT_INDEX, 0);\n            loop.setLocal(CURRENT_ITEM, items.get(0));\n\n            loop.push(new WithItemsNext(outVariables, cmd)); // next iteration\n\n            Frame cmdFrame = Frame.builder()\n                    .commands(cmd)\n                    .root()\n                    .build();\n\n            Frame targetFrame = VMUtils.assertNearestRoot(state, threadId);\n            loop.push(new AppendVariablesCommand(outVariables, cmdFrame, targetFrame));\n            loop.push(new PrepareOutVariables(outVariables, targetFrame));\n\n            state.pushFrame(threadId, loop);\n            state.pushFrame(threadId, cmdFrame);\n        }\n    }\n\n    static class WithItemsNext implements Command {\n\n        private static final long serialVersionUID = 1L;\n\n        private final Collection<String> outVariables;\n        private final Command cmd;\n\n        public WithItemsNext(Collection<String> outVariables, Command cmd) {\n            this.outVariables = outVariables;\n            this.cmd = cmd;\n        }\n\n        @Override\n        public void eval(Runtime runtime, State state, ThreadId threadId) {\n            Frame loop = state.peekFrame(threadId);\n            loop.pop();\n\n            List<Serializable> items = VMUtils.assertLocal(state, threadId, CURRENT_ITEMS);\n\n            int index = VMUtils.assertLocal(state, threadId, CURRENT_INDEX);\n            if (index + 1 >= items.size()) {\n                // end of the line, do nothing\n                return;\n            }\n\n            int newIndex = index + 1;\n            loop.setLocal(CURRENT_INDEX, newIndex);\n            loop.setLocal(CURRENT_ITEM, items.get(newIndex));\n\n            loop.push(new WithItemsNext(outVariables, cmd)); // next iteration\n\n            // frame wrapped command\n            Frame cmdFrame = Frame.builder()\n                    .commands(cmd)\n                    .root()\n                    .build();\n\n            Frame targetFrame = VMUtils.assertNearestRoot(state, threadId);\n            loop.push(new AppendVariablesCommand(outVariables, cmdFrame, targetFrame));\n\n            state.pushFrame(threadId, cmdFrame);\n        }\n    }\n\n    static class PrepareOutVariables implements Command {\n\n        private static final long serialVersionUID = 1L;\n\n        private final Collection<String> outVars;\n        private final Frame targetFrame;\n\n        private PrepareOutVariables(Collection<String> outVars, Frame targetFrame) {\n            this.outVars = outVars;\n            this.targetFrame = targetFrame;\n        }\n\n        @Override\n        public void eval(Runtime runtime, State state, ThreadId threadId) {\n            Frame frame = state.peekFrame(threadId);\n            frame.pop();\n\n            if (outVars.isEmpty()) {\n                return;\n            }\n\n            for (String outVar : outVars) {\n                VMUtils.putLocal(targetFrame, outVar, new ArrayList<>());\n            }\n        }\n    }\n\n    /**\n     * Appends values of the specified variables from the source frame into\n     * list variables in the target frame.\n     */\n    static class AppendVariablesCommand implements Command {\n\n        private static final long serialVersionUID = 1L;\n\n        private final Collection<String> variables;\n        private final Frame sourceFrame;\n        private final Frame targetFrame;\n\n        public AppendVariablesCommand(Collection<String> variables, Frame sourceFrame, Frame targetFrame) {\n            this.variables = variables;\n            this.sourceFrame = sourceFrame;\n            this.targetFrame = targetFrame;\n        }\n\n        @Override\n        @SuppressWarnings(\"unchecked\")\n        public void eval(Runtime runtime, State state, ThreadId threadId) {\n            Frame frame = state.peekFrame(threadId);\n            frame.pop();\n\n            if (variables.isEmpty()) {\n                return;\n            }\n\n            Frame effectiveSourceFrame = sourceFrame != null ? sourceFrame : VMUtils.assertNearestRoot(state, threadId);\n\n            for (String v : variables) {\n                // make sure we're not modifying the same list concurrently\n                synchronized (targetFrame) {\n                    ArrayList<Serializable> results = (ArrayList<Serializable>) targetFrame.getLocal(v);\n                    Serializable result = null;\n                    if (effectiveSourceFrame.hasLocal(v)) {\n                        result = effectiveSourceFrame.getLocal(v);\n                    }\n                    results.add(result);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/java/com/walmartlabs/concord/runtime/v2/runner/vm/WrappedException.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.PrintStream;\nimport java.io.PrintWriter;\nimport java.util.Objects;\n\npublic class WrappedException extends RuntimeException {\n\n    private final String prefix;\n\n    public WrappedException(Exception cause) {\n        this(null, Objects.requireNonNull(cause));\n    }\n\n    public WrappedException(String prefix, Exception cause) {\n        super(Objects.requireNonNull(cause));\n        this.prefix = prefix;\n    }\n\n    @Override\n    public Exception getCause() {\n        return (Exception) super.getCause();\n    }\n\n    @Override\n    public StackTraceElement[] getStackTrace() {\n        return getCause().getStackTrace();\n    }\n\n    @Override\n    public void printStackTrace(PrintWriter s) {\n        getCause().printStackTrace(s);\n    }\n\n    @Override\n    public void printStackTrace(PrintStream s) {\n        getCause().printStackTrace(s);\n    }\n\n    @Override\n    public String getLocalizedMessage() {\n        return messagePrefix() + getCause().getLocalizedMessage();\n    }\n\n    @Override\n    public String getMessage() {\n        return messagePrefix() + getCause().getMessage();\n    }\n\n    @Override\n    public String toString() {\n        return messagePrefix() + getCause().toString();\n    }\n\n    private String messagePrefix() {\n        if (prefix != null) {\n            return prefix;\n        }\n        return \"\";\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/main/resources/com/walmartlabs/concord/runtime/v2/runner/dockerPasswd",
    "content": "root:x:0:0:root:/root:/bin/bash\nconcord:x:456:456::/tmp:/sbin/nologin\n"
  },
  {
    "path": "runtime/v2/runner/src/main/resources/logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <filter class=\"com.walmartlabs.concord.runtime.v2.runner.logging.LogLevelFilter\" />\n\n        <encoder class=\"com.walmartlabs.concord.runtime.v2.runner.logging.ConcordLogEncoder\">\n            <layout class=\"com.walmartlabs.concord.runtime.v2.runner.logging.CustomLayout\">\n                <!-- the UI expects log timestamps in a specific format to be able to convert it to the local time -->\n                <pattern>%date{yyyy-MM-dd'T'HH:mm:ss.SSSZ, UTC} [%-5level] %msg%n%rEx{full, com.sun, sun}</pattern>\n            </layout>\n        </encoder>\n    </appender>\n\n    <appender name=\"PROCESS_STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <filter class=\"com.walmartlabs.concord.runtime.v2.runner.logging.LogLevelFilter\" />\n\n        <encoder class=\"com.walmartlabs.concord.runtime.v2.runner.logging.ConcordLogEncoder\">\n            <layout class=\"com.walmartlabs.concord.runtime.v2.runner.logging.CustomLayout\">\n                <!-- the UI expects log timestamps in a specific format to be able to convert it to the local time -->\n                <pattern>%date{yyyy-MM-dd'T'HH:mm:ss.SSSZ, UTC} %msg%n</pattern>\n            </layout>\n        </encoder>\n    </appender>\n\n    <logger name=\"processLog\" level=\"INFO\" additivity=\"false\">\n        <appender-ref ref=\"PROCESS_STDOUT\"/>\n    </logger>\n\n    <logger name=\"com.walmartlabs.concord.plugins.log\" level=\"${logLevel:-INFO}\"/>\n    <logger name=\"uk.org.lidalia.sysoutslf4j.context.SysOutOverSLF4J\" level=\"WARN\"/>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/EventReportingServiceTest.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.client2.ProcessEventRequest;\nimport com.walmartlabs.concord.client2.ProcessEventsApi;\nimport com.walmartlabs.concord.runtime.common.injector.InstanceId;\nimport com.walmartlabs.concord.runtime.v2.model.EventConfiguration;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.UUID;\nimport java.util.concurrent.atomic.AtomicInteger;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.mock;\n\nclass EventReportingServiceTest {\n\n    @Test\n    void testSingle() {\n        var evs = getService(1);\n        evs.report(new ProcessEventRequest());\n        evs.report(new ProcessEventRequest());\n        evs.report(new ProcessEventRequest());\n        evs.report(new ProcessEventRequest());\n\n        assertEquals(4, evs.flushCounter.get());\n\n        evs.afterProcessEnds(null, null, null);\n\n        assertEquals(5, evs.flushCounter.get());\n    }\n\n    @Test\n    void testBatch() {\n        var evs = getService(2);\n        evs.report(new ProcessEventRequest());\n        evs.report(new ProcessEventRequest());\n        evs.report(new ProcessEventRequest());\n        evs.report(new ProcessEventRequest());\n\n        assertEquals(2, evs.flushCounter.get());\n\n        evs.afterProcessEnds(null, null, null);\n\n        assertEquals(3, evs.flushCounter.get());\n    }\n\n    private static MockedEventReportingService getService(int batchSize) {\n        ProcessConfiguration processCfg = ProcessConfiguration.builder()\n                .events(EventConfiguration.builder()\n                        .batchSize(batchSize)\n                        .build())\n                .build();\n\n        return new MockedEventReportingService(processCfg);\n    }\n\n    private static class MockedEventReportingService extends DefaultEventReportingService {\n        final ProcessEventsApi mockProcessEventsApi;\n        private final AtomicInteger flushCounter;\n\n        public MockedEventReportingService(ProcessConfiguration processConfiguration) {\n            super(new InstanceId(UUID.randomUUID()), processConfiguration, mock(ApiClient.class), mock(PersistenceService.class));\n            this.mockProcessEventsApi = mock(ProcessEventsApi.class);\n            this.flushCounter = new AtomicInteger();\n        }\n\n        @Override\n        void flush() {\n            super.flush();\n            flushCounter.incrementAndGet();\n        }\n\n        @Override\n        ProcessEventsApi getProcessEventsApi() {\n            return mockProcessEventsApi;\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/checkpoints/DefaultCheckpointServiceTest.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.checkpoints;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport java.io.Serializable;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class DefaultCheckpointServiceTest {\n\n    @Test\n    public void simpleClone() throws Exception {\n        record Foo(String value) implements Serializable {\n        }\n        var input = new Foo(\"foo\");\n        var output = DefaultCheckpointService.clone(input, input.getClass().getClassLoader());\n        assertEquals(input, output);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/context/ResumeEventImplTest.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.context;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.Serializable;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class ResumeEventImplTest {\n\n    @Test\n    public void jsonSerializationTest() throws Exception {\n        Map<String, Serializable> payload = Map.of(\"key\", \"key-value\");\n\n        ResumeEventImpl event = new ResumeEventImpl(\"test\", payload);\n        String json = new ObjectMapper().writeValueAsString(event);\n        assertEquals(\"{\\\"eventName\\\":\\\"test\\\",\\\"state\\\":{\\\"key\\\":\\\"key-value\\\"}}\", json);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/el/DummyContext.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Compiler;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\n\nimport java.io.Serializable;\nimport java.nio.file.Path;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic class DummyContext implements Context {\n\n    @Override\n    public Path workingDirectory() {\n        throw new IllegalStateException(\"Not implemented\");\n    }\n\n    @Override\n    public UUID processInstanceId() {\n        throw new IllegalStateException(\"Not implemented\");\n    }\n\n    @Override\n    public Variables variables() {\n        throw new IllegalStateException(\"Not implemented\");\n    }\n\n    @Override\n    public Variables defaultVariables() {\n        throw new IllegalStateException(\"Not implemented\");\n    }\n\n    @Override\n    public FileService fileService() {\n        throw new IllegalStateException(\"Not implemented\");\n    }\n\n    @Override\n    public DockerService dockerService() {\n        throw new IllegalStateException(\"Not implemented\");\n    }\n\n    @Override\n    public SecretService secretService() {\n        throw new IllegalStateException(\"Not implemented\");\n    }\n\n    @Override\n    public LockService lockService() {\n        throw new IllegalStateException(\"Not implemented\");\n    }\n\n    @Override\n    public ApiConfiguration apiConfiguration() {\n        throw new IllegalStateException(\"Not implemented\");\n    }\n\n    @Override\n    public ProcessConfiguration processConfiguration() {\n        throw new IllegalStateException(\"Not implemented\");\n    }\n\n    @Override\n    public Execution execution() {\n        throw new IllegalStateException(\"Not implemented\");\n    }\n\n    @Override\n    public Compiler compiler() {\n        throw new IllegalStateException(\"Not implemented\");\n    }\n\n    @Override\n    public <T> T eval(Object v, Class<T> type) {\n        throw new IllegalStateException(\"Not implemented\");\n    }\n\n    @Override\n    public <T> T eval(Object v, Map<String, Object> additionalVariables, Class<T> type) {\n        throw new IllegalStateException(\"Not implemented\");\n    }\n\n    @Override\n    public void suspend(String eventName) {\n        throw new IllegalStateException(\"Not implemented\");\n    }\n\n    @Override\n    public void reentrantSuspend(String eventName, Map<String, Serializable> payload) {\n        throw new IllegalStateException(\"Not implemented\");\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/el/ExpressionEvaluatorTest.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.runner.el.functions.AllVariablesFunction;\nimport com.walmartlabs.concord.runtime.v2.runner.el.functions.HasVariableFunction;\nimport com.walmartlabs.concord.runtime.v2.runner.el.resolvers.SensitiveDataProcessor;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskProviders;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.Serializable;\nimport java.util.*;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.containsString;\nimport static org.hamcrest.Matchers.is;\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.*;\n\npublic class ExpressionEvaluatorTest {\n\n    @Test\n    public void testEvaGlobal() {\n        ExpressionEvaluator ee = expressionEvaluator(new TaskProviders());\n        Map<String, Object> vars = Collections.singletonMap(\"name\", \"${Concord}\");\n\n        // ---\n        String str = ee.eval(global(vars), \"Hello ${name}\", String.class);\n        assertEquals(\"Hello ${Concord}\", str);\n    }\n\n    @Test\n    public void testStrict() {\n        ExpressionEvaluator ee = expressionEvaluator(new TaskProviders());\n        Map<String, Object> vars = Collections.singletonMap(\"name\", \"Concord\");\n        Map<String, Object> strict = Collections.singletonMap(\"name\", \"Concord!!!\");\n\n        EvalContext ctx = new EvalContextFactoryImpl().strict(new SingleFrameContext(vars), strict);\n\n        // ---\n        String str = ee.eval(ctx, \"Hello ${name}\", String.class);\n        assertEquals(\"Hello Concord!!!\", str);\n    }\n\n    @Test\n    public void testStrictUndef() {\n        ExpressionEvaluator ee = expressionEvaluator(new TaskProviders());\n        Map<String, Object> vars = Collections.singletonMap(\"name\", \"Concord\");\n        Map<String, Object> strict = Collections.emptyMap();\n\n        EvalContext ctx = new EvalContextFactoryImpl().strict(new SingleFrameContext(vars), strict);\n\n        // ---\n        try {\n            ee.eval(ctx, \"Hello ${name}\", String.class);\n            fail(\"exception expected\");\n        } catch (RuntimeException e) {\n            assertThat(e.getMessage(), containsString(\"while evaluating expression 'Hello ${name}': Can't find a variable 'name'\"));\n        }\n\n        // undef as null\n        // ---\n        String str = ee.eval(undefAsNull(ctx), \"Hello ${name}\", String.class);\n        assertNull(str);\n    }\n\n    @Test\n    public void testEvalScope() {\n        ExpressionEvaluator ee = expressionEvaluator(new TaskProviders());\n\n        Map<String, Object> vars = Collections.singletonMap(\"name\", \"${Concord}\");\n\n        Map<String, Object> input = new LinkedHashMap<>();\n        input.put(\"msg\", \"Hello, ${name}\");\n        input.put(\"text\", \"${msg}\");\n\n        Map<String, Object> expected = new LinkedHashMap<>();\n        expected.put(\"msg\", \"Hello, ${Concord}\");\n        expected.put(\"text\", \"Hello, ${Concord}\");\n\n        Map<String, Object> output = ee.evalAsMap(scope(vars), input);\n        assertThat(output, is(expected));\n    }\n\n    @Test\n    public void testEvalListGlobal() {\n        ExpressionEvaluator ee = expressionEvaluator(new TaskProviders());\n\n        Map<String, Object> vars = Collections.singletonMap(\"name\", \"${Concord}\");\n\n        List<String> input = new ArrayList<>();\n        input.add(\"Hello, ${name}\");\n\n        List<String> expected = new ArrayList<>();\n        expected.add(\"Hello, ${Concord}\");\n\n        List<String> output = ee.evalAsList(global(vars), input);\n        assertThat(output, is(expected));\n    }\n\n    @Test\n    public void testEval1() {\n        /*\n         * configuration:\n         *   arguments:\n         *     x: ${y}\n         *     z: ${y.y1}\n         *     y:\n         *     \ty1: ${task(..)}\n         *     \ty2: \"asdasd\"\n         *     \ty3: ${z}\n         */\n\n        Map<Object, Object> input = map(\n                \"x\", \"${y}\",\n                \"z\", \"${y.y1}\",\n                \"y\", map(\n                        \"y1\", \"${in}\",\n                        \"y2\", \"abc\",\n                        \"y3\", \"${z}\"));\n\n        ExpressionEvaluator ee = expressionEvaluator(new TaskProviders());\n        Map<String, Object> vars = Collections.singletonMap(\"in\", \"task\");\n\n        // scope -> ok\n        // ---\n        Map<Object, Object> output = ee.evalAsMap(scope(vars), input);\n\n        Map<Object, Object> y = map(\"y1\", \"task\", \"y2\", \"abc\", \"y3\", \"task\");\n        assertThat(output, is(map(\"x\", y,\n                \"z\", \"task\",\n                \"y\", y)));\n\n        // global -> error (y undefined)\n        // ---\n        try {\n            ee.evalAsMap(global(vars), input);\n            fail(\"exception expected\");\n        } catch (RuntimeException e) {\n            assertThat(e.getMessage(), containsString(\"while evaluating expression '${y}': Can't find a variable 'y'\"));\n        }\n\n        // undef -> x = null, z = null, y ...y3 = null\n        // ---\n        output = ee.evalAsMap(undefAsNull(global(vars)), input);\n\n        y.put(\"y3\", null);\n        assertThat(output, is(map(\"x\", null,\n                \"z\", null,\n                \"y\", y)));\n    }\n\n    @Test\n    public void testEval2() {\n        Map<Object, Object> input = map(\n                \"x\", \"${y}\",\n                \"y\", \"${x}\");\n\n        ExpressionEvaluator ee = expressionEvaluator(new TaskProviders());\n        Map<String, Object> vars = Collections.emptyMap();\n\n        // global: x -> undefined\n        // ---\n        try {\n            ee.evalAsMap(global(vars), input);\n            fail(\"exception expected\");\n        } catch (RuntimeException e) {\n            assertThat(e.getMessage(), containsString(\"while evaluating expression '${y}': Can't find a variable 'y'\"));\n        }\n\n        // scope\n        // ---\n        try {\n            ee.evalAsMap(scope(vars), input);\n            fail(\"exception expected\");\n        } catch (RuntimeException e) {\n            assertThat(e.getMessage(), containsString(\"while evaluating expression '${x}': Can't find a variable 'x'\"));\n        }\n    }\n\n    @Test\n    public void testEval3() {\n        /*\n         * configuration:\n         *   arguments:\n         *     x: ${y}\n         *     z: ${y.y1}\n         *     y:\n         *     \ty1: ${task(y.y2)}\n         *     \ty2: \"asdasd\"\n         *     \ty3: ${z}\n         */\n        Map<Object, Object> input = map(\n                \"x\", \"${y}\",\n                \"z\", \"${y.y1}\",\n                \"y\", map(\n                        \"y1\", \"${task.foo(y.y2)}\",\n                        \"y2\", \"abc\",\n                        \"y3\", \"${z}\"));\n\n        TaskProviders providers = mock(TaskProviders.class);\n        TestTask task = spy(new TestTask());\n        when(providers.createTask(any(), eq(\"task\"))).thenReturn(task);\n\n        ExpressionEvaluator ee = expressionEvaluator(providers);\n        Map<String, Object> vars = Collections.emptyMap();\n\n        // global: y -> undefined\n        // ---\n        try {\n            ee.evalAsMap(global(vars), input);\n            fail(\"exception expected\");\n        } catch (Exception e) {\n            assertThat(e.getMessage(), containsString(\"while evaluating expression '${y}': Can't find a variable 'y'\"));\n        }\n\n        verify(task, times(0)).foo(anyString());\n\n        // scope:\n        // ---\n        Map<Object, Object> output = ee.evalAsMap(scope(vars), input);\n\n        Map<Object, Object> y = map(\"y1\", \"from-task: abc\", \"y2\", \"abc\", \"y3\", \"from-task: abc\");\n        assertThat(output, is(map(\"x\", y,\n                \"z\", \"from-task: abc\",\n                \"y\", y)));\n\n        verify(task, times(1)).foo(anyString());\n    }\n\n    @Test\n    public void testEval4() {\n        /*\n         * configuration:\n         *   arguments:\n         *     x: ${y}\n         *     z: ${y.y1}\n         *     y:\n         *     \ty1: ${task(y.y2)}\n         *     \ty2: \"asdasd\"\n         *     \ty3: ${z}\n         */\n        Map<Object, Object> input = map(\n                \"x\", \"${y}\",\n                \"z\", \"${y.y1}\",\n                \"y\", map(\n                        \"y1\", \"${task.foo(y.y2)}\",\n                        \"y2\", \"abc\",\n                        \"y3\", \"${z}\"));\n\n        TaskProviders providers = mock(TaskProviders.class);\n        TestTask2 task = spy(new TestTask2());\n        when(providers.createTask(any(), eq(\"task\"))).thenReturn(task);\n\n        ExpressionEvaluator ee = expressionEvaluator(providers);\n        Map<String, Object> vars = Collections.emptyMap();\n\n        // scope:\n        // ---\n        Map<Object, Object> output = ee.evalAsMap(scope(vars), input);\n\n        Map<Object, Object> y = map(\"y1\", \"${abc}\", \"y2\", \"abc\", \"y3\", \"${abc}\");\n        assertThat(output, is(map(\"x\", y,\n                \"z\", \"${abc}\",\n                \"y\", y)));\n\n        verify(task, times(1)).foo(anyString());\n    }\n\n    @Test\n    public void testEval5() {\n        Map<Object, Object> input = map(\n                \"y\", map(\n                        \"y1\", \"y1-value\",\n                        \"y2\", \"${y1}\"));\n\n        ExpressionEvaluator ee = expressionEvaluator(new TaskProviders());\n        Map<String, Object> vars = Collections.emptyMap();\n\n        try {\n            ee.evalAsMap(scope(vars), input);\n        } catch (RuntimeException e) {\n            assertThat(e.getMessage(), containsString(\"while evaluating expression '${y1}': Can't find a variable 'y1'\"));\n        }\n    }\n\n    @Test\n    public void testEval6() {\n        Map<Object, Object> input = map(\n                \"x\", Collections.singletonList(\"${y}\"),\n                \"y\", \"abc\");\n\n        ExpressionEvaluator ee = expressionEvaluator(new TaskProviders());\n        Map<String, Object> vars = Collections.emptyMap();\n\n        // scope:\n        // ---\n        Map<Object, Object> output = ee.evalAsMap(scope(vars), input);\n\n        assertThat(output, is(map(\"x\", Collections.singletonList(\"abc\"),\n                \"y\", \"abc\")));\n    }\n\n    @Test\n    public void testEval8() {\n        /*\n         * x:\n         *   - ${y}\n         * y: \"abc\"\n         */\n        Map<Object, Object> input = map(\"x\", Collections.singletonList(\"${y}\"), \"y\", \"abc\");\n\n        ExpressionEvaluator ee = expressionEvaluator(new TaskProviders());\n        Map<String, Object> vars = Collections.emptyMap();\n\n        // scope:\n        // ---\n        Map<Object, Object> output = ee.evalAsMap(scope(vars), input);\n\n        assertThat(output, is(map(\"x\", Collections.singletonList(\"abc\"), \"y\", \"abc\")));\n    }\n\n    @Test\n    public void testEvalHasVariable() {\n        String str = \"${hasVariable('x')}\";\n\n        ExpressionEvaluator ee = expressionEvaluator(new TaskProviders());\n\n        // ---\n\n        boolean result = ee.eval(global(Collections.emptyMap()), str, Boolean.class);\n        assertFalse(result);\n\n        // ---\n\n        Map<String, Object> vars = Collections.singletonMap(\"x\", \"x-value\");\n        result = ee.eval(global(vars), str, Boolean.class);\n        assertTrue(result);\n    }\n\n    @Test\n    public void testAllVariables() {\n        String str = \"${allVariables()}\";\n\n        ExpressionEvaluator ee = expressionEvaluator(new TaskProviders());\n\n        // ---\n        Map<String, Object> vars = new HashMap<>();\n        vars.put(\"a\", Collections.singletonList(\"b\"));\n        vars.put(\"b\", \"bb\");\n\n        Map<String, Object> result = ee.evalAsMap(global(vars), str);\n        assertEquals(vars, result);\n    }\n\n    private static EvalContext global(Map<String, Object> vars) {\n        return new EvalContextFactoryImpl().global(new SingleFrameContext(vars));\n    }\n\n    private static EvalContext scope(Map<String, Object> vars) {\n        return new EvalContextFactoryImpl().scope(new SingleFrameContext(vars));\n    }\n\n    private static EvalContext undefAsNull(EvalContext ctx) {\n        return EvalContext.builder().from(ctx)\n                .undefinedVariableAsNull(true)\n                .build();\n    }\n\n    private static Map<Object, Object> map(Object... values) {\n        Map<Object, Object> result = new LinkedHashMap<>();\n        for (int i = 0; i < values.length; i += 2) {\n            Object k = values[i];\n            Object v = values[i + 1];\n            result.put(k, v);\n        }\n        return result;\n    }\n\n    private static ExpressionEvaluator expressionEvaluator(TaskProviders taskProviders) {\n        try {\n            var functions = new FunctionHolder()\n                    .register(\"allVariables\", AllVariablesFunction.class.getMethod(\"allVariables\"))\n                    .register(\"hasVariable\", HasVariableFunction.class.getMethod(\"hasVariable\", String.class));\n            return new DefaultExpressionEvaluator(taskProviders, functions, List.of(), List.of(), mock(SensitiveDataProcessor.class));\n        } catch (NoSuchMethodException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static class TestTask implements Task {\n\n        @SuppressWarnings(\"UnusedReturnValue\")\n        public Serializable foo(String value) {\n            return \"from-task: \" + value;\n        }\n    }\n\n    public static class TestTask2 implements Task {\n\n        @SuppressWarnings(\"UnusedReturnValue\")\n        public Serializable foo(String value) {\n            return \"${\" + value + \"}\";\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/el/HasNoNullVariableFunctionTest.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.runtime.v2.runner.el.functions.HasNonNullVariableFunction;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContext;\nimport com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class HasNoNullVariableFunctionTest {\n\n    @Test\n    public void test() {\n        withVariables(Collections.singletonMap(\"test\", \"123\"), () -> {\n            assertTrue(HasNonNullVariableFunction.hasNonNullVariable(\"test\"));\n            assertFalse(HasNonNullVariableFunction.hasNonNullVariable(\"test.nested.deep\"));\n            assertFalse(HasNonNullVariableFunction.hasNonNullVariable(\"boom\"));\n        });\n\n        withVariables(Collections.singletonMap(\"testNull\", null), () -> {\n            assertFalse(HasNonNullVariableFunction.hasNonNullVariable(\"testNull\"));\n            assertFalse(HasNonNullVariableFunction.hasNonNullVariable(\"testNull.k2\"));\n        });\n\n        withVariables(Collections.singletonMap(\"a\", ConfigurationUtils.toNested(\"b.c\", \"123\")), () -> {\n            assertTrue(HasNonNullVariableFunction.hasNonNullVariable(\"a\"));\n            assertTrue(HasNonNullVariableFunction.hasNonNullVariable(\"a.b\"));\n            assertTrue(HasNonNullVariableFunction.hasNonNullVariable(\"a.b.c\"));\n            assertFalse(HasNonNullVariableFunction.hasNonNullVariable(\"a.b.c.d\"));\n            assertFalse(HasNonNullVariableFunction.hasNonNullVariable(\"\"));\n            assertFalse(HasNonNullVariableFunction.hasNonNullVariable(null));\n        });\n    }\n\n    public static void withVariables(Map<String, Object> variables, Runnable runnable) throws RuntimeException {\n        ThreadLocalEvalContext.withEvalContext(new EvalContext() {\n            @Override\n            public Context context() {\n                return null;\n            }\n\n            @Override\n            public Variables variables() {\n                return new MapBackedVariables(variables);\n            }\n        }, () -> {\n            runnable.run();\n            return null;\n        });\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/el/HasVariableFunctionTest.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.runtime.v2.runner.el.functions.HasVariableFunction;\nimport com.walmartlabs.concord.runtime.v2.sdk.Context;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContext;\nimport com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class HasVariableFunctionTest {\n\n    @Test\n    public void test() {\n        withVariables(Collections.singletonMap(\"test\", \"123\"), () -> {\n            assertTrue(HasVariableFunction.hasVariable(\"test\"));\n            assertFalse(HasVariableFunction.hasVariable(\"test.nested.deep\"));\n            assertFalse(HasVariableFunction.hasVariable(\"boom\"));\n        });\n\n        withVariables(Collections.singletonMap(\"a\", ConfigurationUtils.toNested(\"b.c\", \"123\")), () -> {\n            assertTrue(HasVariableFunction.hasVariable(\"a\"));\n            assertTrue(HasVariableFunction.hasVariable(\"a.b\"));\n            assertTrue(HasVariableFunction.hasVariable(\"a.b.c\"));\n            assertFalse(HasVariableFunction.hasVariable(\"a.b.c.d\"));\n            assertFalse(HasVariableFunction.hasVariable(\"\"));\n            assertFalse(HasVariableFunction.hasVariable(null));\n        });\n    }\n\n    public static void withVariables(Map<String, Object> variables, Runnable runnable) throws RuntimeException {\n        ThreadLocalEvalContext.withEvalContext(new EvalContext() {\n            @Override\n            public Context context() {\n                return null;\n            }\n\n            @Override\n            public Variables variables() {\n                return new MapBackedVariables(variables);\n            }\n        }, () -> {\n            runnable.run();\n            return null;\n        });\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/el/ImmutablesTest.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.runner.el.resolvers.SensitiveDataProcessor;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskProviders;\nimport com.walmartlabs.concord.runtime.v2.sdk.EvalContextFactory;\nimport com.walmartlabs.concord.runtime.v2.sdk.ExpressionEvaluator;\nimport org.immutables.value.Value;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.mock;\n\npublic class ImmutablesTest {\n\n    @Test\n    public void test() throws Exception {\n        TestBean testBean = ImmutableTestBean.builder()\n                .foo(\"foo\")\n                .build();\n\n        EvalContextFactory ecf = new EvalContextFactoryImpl();\n        ExpressionEvaluator ee = new DefaultExpressionEvaluator(new TaskProviders(), new FunctionHolder(), List.of(), List.of(), mock(SensitiveDataProcessor.class));\n        Map<String, Object> vars = Collections.singletonMap(\"testBean\", testBean);\n\n        // ---\n        String str = ee.eval(ecf.global(new SingleFrameContext(vars)), \"Hello ${testBean.foo}\", String.class);\n        assertEquals(\"Hello foo\", str);\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    public interface TestBean extends Serializable {\n\n        String foo();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/el/MethodNotFoundExceptionTest.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class MethodNotFoundExceptionTest {\n\n    @Test\n    public void test() {\n        TestBean bean = new TestBean();\n        MethodNotFoundException e = new MethodNotFoundException(bean.getClass(), \"test\", new Class[]{double.class, String.class, List.class});\n\n        String expected = \"Can't find 'test(double, java.lang.String, java.util.List)' method in com.walmartlabs.concord.runtime.v2.runner.el.MethodNotFoundExceptionTest$TestBean.\\n\" +\n                \"Check the task's or type's available methods and their signatures.\\n\" +\n                \"Did you mean: test(long)?\";\n\n        assertEquals(expected, e.getMessage());\n    }\n\n    public static class TestBean {\n\n        public void test(long i) {\n\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/el/OrDefaultFunctionTest.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.runtime.v2.runner.el.functions.OrDefaultFunction;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\n\nimport static com.walmartlabs.concord.runtime.v2.runner.el.HasVariableFunctionTest.withVariables;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\n\npublic class OrDefaultFunctionTest {\n\n    @Test\n    public void test() {\n        withVariables(Collections.singletonMap(\"test\", \"123\"), () -> {\n            assertEquals(\"123\", OrDefaultFunction.orDefault(\"test\", \"11\"));\n            assertEquals(\"11\", OrDefaultFunction.orDefault(\"test.nested.deep\", \"11\"));\n            assertEquals(\"11\", OrDefaultFunction.orDefault(\"boom\", \"11\"));\n        });\n\n        withVariables(Collections.singletonMap(\"a\", ConfigurationUtils.toNested(\"b.c\", \"123\")), () -> {\n            assertEquals(ConfigurationUtils.toNested(\"b.c\", \"123\"), OrDefaultFunction.orDefault(\"a\", Collections.emptyMap()));\n            assertEquals(Collections.singletonMap(\"c\", \"123\"), OrDefaultFunction.orDefault(\"a.b\", Collections.emptyMap()));\n            assertEquals(\"123\", OrDefaultFunction.orDefault(\"a.b.c\", \"x\"));\n            assertEquals(\"x\", OrDefaultFunction.orDefault(\"a.b.c.d\", \"x\"));\n            assertNull(OrDefaultFunction.orDefault(\"a.b.c.d\", null));\n        });\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/el/SingleFrameContext.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.el;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.runtime.v2.runner.context.ContextVariables;\nimport com.walmartlabs.concord.runtime.v2.sdk.Execution;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic class SingleFrameContext extends DummyContext {\n\n    private final Map<String, Object> variables;\n\n    public SingleFrameContext(Map<String, Object> variables) {\n        this.variables = variables;\n    }\n\n    @Override\n    public Execution execution() {\n        return new Execution() {\n            @Override\n            public ThreadId currentThreadId() {\n                return null;\n            }\n\n            @Override\n            public Runtime runtime() {\n                throw new IllegalStateException(\"Not implemented\");\n            }\n\n            @Override\n            public State state() {\n                return new State() {\n\n                    private static final long serialVersionUID = 1L;\n\n                    private final List<Frame> frames = Collections.singletonList(Frame.builder()\n                            .root()\n                            .locals(variables)\n                            .build());\n\n                    @Override\n                    public void pushFrame(ThreadId threadId, Frame frame) {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public Frame peekFrame(ThreadId threadId) {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public void popFrame(ThreadId threadId) {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public List<Frame> getFrames(ThreadId threadId) {\n                        return frames;\n                    }\n\n                    @Override\n                    public void dropAllFrames() {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public void setStatus(ThreadId threadId, ThreadStatus status) {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public ThreadStatus getStatus(ThreadId threadId) {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public ThreadId getRootThreadId() {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public void fork(ThreadId parentThreadId, ThreadId threadId, Command... cmds) {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public Map<ThreadId, ThreadStatus> threadStatus() {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public ThreadId nextThreadId() {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public void setEventRef(ThreadId threadId, String eventRef) {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public ThreadId removeEventRef(String eventRef) {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public Map<ThreadId, String> getEventRefs() {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public ThreadError getThreadError(ThreadId threadId) {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public void setThreadError(ThreadId threadId, Exception error) {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public void setThreadError(ThreadId threadId, Command cmd, Exception error) {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public ThreadError clearThreadError(ThreadId threadId) {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public List<StackTraceItem> getStackTrace(ThreadId threadId) {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public void pushStackTraceItem(ThreadId threadId, StackTraceItem item) {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public void clearStackTrace(ThreadId threadId) {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public void setThreadLocal(ThreadId threadId, String key, Serializable value) {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public <T extends Serializable> T getThreadLocal(ThreadId threadId, String key) {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public void removeThreadLocal(ThreadId threadId, String key) {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n\n                    @Override\n                    public void gc() {\n                        throw new IllegalStateException(\"Not implemented\");\n                    }\n                };\n            }\n\n            @Override\n            public ProcessDefinition processDefinition() {\n                throw new IllegalStateException(\"Not implemented\");\n            }\n\n            @Nullable\n            @Override\n            public Step currentStep() {\n                return null;\n            }\n\n            @Override\n            public String currentFlowName() {\n                return null;\n            }\n\n            @Override\n            public UUID correlationId() {\n                throw new IllegalStateException(\"Not implemented\");\n            }\n        };\n    }\n\n    @Override\n    public Variables variables() {\n        return new ContextVariables(this);\n    }\n\n    @Override\n    public Variables defaultVariables() {\n        throw new IllegalStateException(\"Not implemented\");\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/guice/ModuleTest.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.guice;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Inject;\nimport com.google.inject.Injector;\nimport com.walmartlabs.concord.runtime.common.cfg.ApiConfiguration;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.runtime.v2.runner.EventReportingService;\nimport com.walmartlabs.concord.runtime.v2.runner.InjectorFactory;\nimport com.walmartlabs.concord.runtime.v2.runner.PersistenceService;\nimport com.walmartlabs.concord.runtime.v2.runner.ProcessStatusCallback;\nimport com.walmartlabs.concord.runtime.v2.runner.checkpoints.CheckpointService;\nimport com.walmartlabs.concord.runtime.v2.runner.checkpoints.CheckpointUploader;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.LoggingClient;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.RunnerLogger;\nimport com.walmartlabs.concord.runtime.v2.sdk.DependencyManager;\nimport com.walmartlabs.concord.runtime.v2.sdk.DockerService;\nimport com.walmartlabs.concord.runtime.v2.sdk.FileService;\nimport com.walmartlabs.concord.runtime.v2.sdk.LockService;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessInfo;\nimport com.walmartlabs.concord.runtime.v2.sdk.SecretService;\nimport com.walmartlabs.concord.runtime.v2.sdk.WorkingDirectory;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.io.TempDir;\n\nimport java.nio.file.Path;\nimport java.util.UUID;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\nclass ModuleTest {\n\n    @TempDir\n    private Path tempDir;\n\n    private Validator injectValidator() {\n        RunnerConfiguration runnerCfg = RunnerConfiguration.builder()\n                .api(ApiConfiguration.builder().build())\n                .build();\n\n        ProcessConfiguration processConfiguration = ProcessConfiguration.builder()\n                .processInfo(ProcessInfo.builder()\n                        .sessionToken(UUID.randomUUID().toString())\n                        .build())\n                .build();\n\n        Injector injector = new InjectorFactory(new WorkingDirectory(tempDir),\n                runnerCfg,\n                () -> processConfiguration,\n                new DefaultRunnerModule(), // bind default services\n                new ProcessDependenciesModule(tempDir, runnerCfg.dependencies(), runnerCfg.debug())) // grab process dependencies\n                .create();\n\n        return injector.getInstance(Validator.class);\n    }\n\n    @BeforeEach\n    public void setup() {\n    }\n\n    @AfterEach\n    public void tearDown() {\n    }\n\n    @Test\n    void testDefaultProviderResults() {\n        Validator validator = injectValidator();\n\n        assertNotNull(validator);\n        validator.validate();\n    }\n\n    private static class Validator {\n\n        private final CheckpointUploader checkpointUploader;\n        private final CheckpointService checkpointService;\n        private final DependencyManager dependencyManager;\n        private final DockerService DockerService;\n        private final FileService fileService;\n        private final EventReportingService eventReportingService;\n        private final LockService lockService;\n        private final PersistenceService persistenceService;\n        private final ProcessStatusCallback processStatusCallback;\n        private final SecretService secretService;\n        private final RunnerLogger runnerLogger;\n        private final LoggingClient loggingClient;\n\n        @Inject\n        public Validator(CheckpointUploader checkpointUploader,\n                         CheckpointService checkpointService,\n                         DependencyManager dependencyManager,\n                         DockerService DockerService,\n                         FileService fileService,\n                         EventReportingService eventReportingService,\n                         LockService lockService,\n                         PersistenceService persistenceService,\n                         ProcessStatusCallback processStatusCallback,\n                         SecretService secretService,\n                         RunnerLogger runnerLogger,\n                         LoggingClient loggingClient) {\n            this.checkpointUploader = checkpointUploader;\n            this.checkpointService = checkpointService;\n            this.dependencyManager = dependencyManager;\n            this.DockerService = DockerService;\n            this.fileService = fileService;\n            this.eventReportingService = eventReportingService;\n            this.lockService = lockService;\n            this.persistenceService = persistenceService;\n            this.processStatusCallback = processStatusCallback;\n            this.secretService = secretService;\n            this.runnerLogger = runnerLogger;\n            this.loggingClient = loggingClient;\n        }\n\n        public void validate() {\n            assertNotNull(checkpointUploader);\n            assertNotNull(checkpointService);\n            assertNotNull(dependencyManager);\n            assertNotNull(DockerService);\n            assertNotNull(fileService);\n            assertNotNull(eventReportingService);\n            assertNotNull(lockService);\n            assertNotNull(persistenceService);\n            assertNotNull(processStatusCallback);\n            assertNotNull(secretService);\n            assertNotNull(runnerLogger);\n            assertNotNull(loggingClient);\n        }\n    }\n\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/remote/TaskCallEventRecordingListenerTest.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.remote;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class TaskCallEventRecordingListenerTest {\n\n    private static final ObjectMapper om = new ObjectMapper();\n\n    @Test\n    public void testMaskVars() throws Exception {\n        String in = \"{\" +\n                    \"   \\\"a\\\":1,\" +\n                    \"   \\\"b\\\":2,\" +\n                    \"   \\\"c\\\":{\" +\n                    \"      \\\"c1\\\":3,\" +\n                    \"      \\\"c2\\\":4,\" +\n                    \"      \\\"c3\\\":{\" +\n                    \"         \\\"c31\\\":5,\" +\n                    \"         \\\"c32\\\":6\" +\n                    \"      }\" +\n                    \"   }\" +\n                    \"}\";\n\n        List<String> blackList = Arrays.asList(\"b\", \"c.c1\", \"c.c3.c31\");\n        Map<String, Object> result = TaskCallEventRecordingListener.maskVars(vars(in), blackList);\n\n        String expected = \"{\" +\n                          \"   \\\"a\\\":1,\" +\n                          \"   \\\"b\\\":\\\"***\\\",\" +\n                          \"   \\\"c\\\":{\" +\n                          \"      \\\"c1\\\":\\\"***\\\",\" +\n                          \"      \\\"c2\\\":4,\" +\n                          \"      \\\"c3\\\":{\" +\n                          \"         \\\"c31\\\":\\\"***\\\",\" +\n                          \"         \\\"c32\\\":6\" +\n                          \"      }\" +\n                          \"   }\" +\n                          \"}\";\n        assertEquals(vars(expected), result);\n    }\n\n    @Test\n    public void testMaskVarsUnmodifiable() {\n        Map<String, Object> vars =\n                Collections.singletonMap(\"x\",\n                        Collections.singletonMap(\"y\",\n                                Collections.singletonMap(\"z\", 123)));\n\n        List<String> blackList = Collections.singletonList(\"x.y.z\");\n        Map<String, Object> result = TaskCallEventRecordingListener.maskVars(vars, blackList);\n        assertEquals(\"{x={y={z=***}}}\", result.toString());\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Map<String, Object> vars(String in) throws JsonProcessingException {\n        return om.readValue(in, Map.class);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/tasks/TaskCallInterceptorTest.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.tasks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallInterceptor.Method;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Arrays;\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class TaskCallInterceptorTest {\n\n    @Test\n    public void methodAnnotationsTest() {\n        Base base = new Base();\n        String method = \"varargs\";\n        List<Object> params = Arrays.asList(\"one\", \"two\");\n\n        Method m = Method.of(base.getClass(), method, params);\n\n        assertEquals(0, m.annotations().size());\n    }\n\n    public static class Base implements Task {\n\n        public void varargs(Object ... args) {\n\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/tasks/TaskSchemaRegistryTest.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.tasks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.other.OtherSchemaTask;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport static com.walmartlabs.concord.runtime.v2.runner.tasks.TaskSchemaLookupResult.Status.*;\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class TaskSchemaRegistryTest {\n\n    private ObjectMapper objectMapper;\n    private TaskSchemaRegistry registry;\n\n    @BeforeEach\n    void setUp() {\n        objectMapper = new ObjectMapper();\n        registry = new TaskSchemaRegistry(objectMapper);\n    }\n\n    @Test\n    void testResourceExistsWithInputAndOutputSections() throws Exception {\n        TaskSchemaLookupResult in = registry.getInputSchema(\"full\", RegistryTask.class);\n        assertEquals(FOUND, in.status());\n        assertEquals(\"full.schema.json\", in.resourceName());\n        assertTrue(registry.getRawSchema(\"full\", RegistryTask.class).isPresent());\n        assertTrue(in.schema().validate(objectMapper.readTree(\"\"\"\n                { \"message\": \"hello\" }\n                \"\"\")).isEmpty());\n        assertFalse(in.schema().validate(objectMapper.readTree(\"\"\"\n                { \"message\": 123 }\n                \"\"\")).isEmpty());\n\n        TaskSchemaLookupResult out = registry.getOutputSchema(\"full\", RegistryTask.class);\n        assertEquals(FOUND, out.status());\n        assertTrue(out.schema().validate(objectMapper.readTree(\"\"\"\n                { \"count\": 5 }\n                \"\"\")).isEmpty());\n        assertFalse(out.schema().validate(objectMapper.readTree(\"\"\"\n                { \"count\": \"bad\" }\n                \"\"\")).isEmpty());\n    }\n\n    @Test\n    void testResourceMissing() {\n        TaskSchemaLookupResult result = registry.getInputSchema(\"missing\", RegistryTask.class);\n        assertEquals(ABSENT, result.status());\n        assertFalse(result.hasErrors());\n        assertTrue(registry.getRawSchema(\"missing\", RegistryTask.class).isEmpty());\n    }\n\n    @Test\n    void testRequestedSectionMissing() {\n        TaskSchemaLookupResult in = registry.getInputSchema(\"noSection\", RegistryTask.class);\n        assertEquals(FOUND, in.status());\n\n        TaskSchemaLookupResult out = registry.getOutputSchema(\"noSection\", RegistryTask.class);\n        assertEquals(NO_SECTION, out.status());\n        assertFalse(out.hasErrors());\n        assertEquals(\"noSection.schema.json\", out.resourceName());\n    }\n\n    @Test\n    void testMalformedJsonResource() {\n        TaskSchemaLookupResult result = registry.getInputSchema(\"invalidJson\", RegistryTask.class);\n        assertEquals(INVALID, result.status());\n        assertTrue(result.hasErrors());\n        assertTrue(result.errors().get(0).contains(\"Failed to load schema resource\"));\n        assertEquals(\"invalidJson.schema.json\", result.resourceName());\n    }\n\n    @Test\n    void testNonObjectRoot() {\n        TaskSchemaLookupResult result = registry.getInputSchema(\"rootNotObject\", RegistryTask.class);\n        assertEquals(INVALID, result.status());\n        assertTrue(result.hasErrors());\n        assertTrue(result.errors().get(0).contains(\"must be a JSON object\"));\n    }\n\n    @Test\n    void testNonObjectSection() {\n        TaskSchemaLookupResult in = registry.getInputSchema(\"invalidSection\", RegistryTask.class);\n        assertEquals(INVALID, in.status());\n        assertTrue(in.hasErrors());\n        assertTrue(in.errors().get(0).contains(\"section 'in'\"));\n\n        TaskSchemaLookupResult out = registry.getOutputSchema(\"invalidSection\", RegistryTask.class);\n        assertEquals(FOUND, out.status());\n    }\n\n    @Test\n    void testCacheKeyIncludesTaskClass() throws Exception {\n        TaskSchemaLookupResult local = registry.getInputSchema(\"shared\", RegistryTask.class);\n        TaskSchemaLookupResult other = registry.getInputSchema(\"shared\", OtherSchemaTask.class);\n\n        assertEquals(FOUND, local.status());\n        assertEquals(FOUND, other.status());\n        assertTrue(local.schema().validate(objectMapper.readTree(\"\"\"\n                { \"local\": \"a\" }\n                \"\"\")).isEmpty());\n        assertFalse(local.schema().validate(objectMapper.readTree(\"\"\"\n                { \"other\": \"b\" }\n                \"\"\")).isEmpty());\n        assertTrue(other.schema().validate(objectMapper.readTree(\"\"\"\n                { \"other\": \"b\" }\n                \"\"\")).isEmpty());\n        assertFalse(other.schema().validate(objectMapper.readTree(\"\"\"\n                { \"local\": \"a\" }\n                \"\"\")).isEmpty());\n    }\n\n    public static class RegistryTask implements Task {\n\n        @Override\n        public TaskResult execute(Variables input) {\n            return TaskResult.success();\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/tasks/TaskSchemaValidatorTest.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.tasks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.networknt.schema.JsonSchema;\nimport com.networknt.schema.JsonSchemaFactory;\nimport com.networknt.schema.SpecVersion;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.Mockito.*;\n\npublic class TaskSchemaValidatorTest {\n\n    private static final Class<? extends Task> TASK_CLASS = Task.class;\n\n    private TaskSchemaRegistry registry;\n    private TaskSchemaValidator validator;\n    private ObjectMapper objectMapper;\n    private JsonSchemaFactory schemaFactory;\n\n    @BeforeEach\n    void setUp() {\n        registry = mock(TaskSchemaRegistry.class);\n        objectMapper = new ObjectMapper();\n        when(registry.getObjectMapper()).thenReturn(objectMapper);\n        validator = new TaskSchemaValidator(registry);\n        schemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);\n    }\n\n    @Test\n    void testNoSchema() {\n        when(registry.getInputSchema(\"testTask\", TASK_CLASS)).thenReturn(TaskSchemaLookupResult.absent(\"testTask.schema.json\"));\n\n        TaskSchemaValidationResult result = validator.validateInput(\"testTask\", TASK_CLASS, Map.of(\"key\", \"value\"));\n\n        assertEquals(TaskSchemaValidationResult.Status.NO_SCHEMA, result.status());\n        assertFalse(result.hasErrors());\n    }\n\n    @Test\n    void testNoSection() {\n        when(registry.getInputSchema(\"testTask\", TASK_CLASS)).thenReturn(TaskSchemaLookupResult.noSection(objectMapper.createObjectNode(), \"testTask.schema.json\"));\n\n        TaskSchemaValidationResult result = validator.validateInput(\"testTask\", TASK_CLASS, Map.of(\"key\", \"value\"));\n\n        assertEquals(TaskSchemaValidationResult.Status.SKIPPED, result.status());\n        assertEquals(\"testTask.schema.json\", result.schemaResource());\n        assertFalse(result.hasErrors());\n    }\n\n    @Test\n    void testInvalidSchemaLookup() {\n        when(registry.getInputSchema(\"testTask\", TASK_CLASS)).thenReturn(TaskSchemaLookupResult.invalid(objectMapper.createObjectNode(), \"testTask.schema.json\", List.of(\"bad schema\")));\n\n        TaskSchemaValidationResult result = validator.validateInput(\"testTask\", TASK_CLASS, Map.of(\"key\", \"value\"));\n\n        assertEquals(TaskSchemaValidationResult.Status.INVALID, result.status());\n        assertEquals(\"testTask.schema.json\", result.schemaResource());\n        assertTrue(result.hasErrors());\n        assertEquals(List.of(\"bad schema\"), result.errors());\n    }\n\n    @Test\n    void testValidInput() throws Exception {\n        JsonNode schemaNode = objectMapper.readTree(\"\"\"\n            {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"message\": { \"type\": \"string\" }\n                },\n                \"required\": [\"message\"]\n            }\n            \"\"\");\n\n        JsonSchema schema = schemaFactory.getSchema(schemaNode);\n        when(registry.getInputSchema(\"testTask\", TASK_CLASS)).thenReturn(TaskSchemaLookupResult.found(schema, schemaNode, \"testTask.schema.json\"));\n\n        TaskSchemaValidationResult result = validator.validateInput(\"testTask\", TASK_CLASS, Map.of(\"message\", \"hello\"));\n\n        assertEquals(TaskSchemaValidationResult.Status.VALID, result.status());\n        assertFalse(result.hasErrors());\n    }\n\n    @Test\n    void testInvalidInput() throws Exception {\n        JsonNode schemaNode = objectMapper.readTree(\"\"\"\n            {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"message\": { \"type\": \"string\" }\n                },\n                \"required\": [\"message\"]\n            }\n            \"\"\");\n\n        JsonSchema schema = schemaFactory.getSchema(schemaNode);\n        when(registry.getInputSchema(\"testTask\", TASK_CLASS)).thenReturn(TaskSchemaLookupResult.found(schema, schemaNode, \"testTask.schema.json\"));\n\n        // Missing required 'message'\n        TaskSchemaValidationResult result = validator.validateInput(\"testTask\", TASK_CLASS, Map.of(\"other\", \"value\"));\n\n        assertEquals(TaskSchemaValidationResult.Status.INVALID, result.status());\n        assertEquals(\"testTask.schema.json\", result.schemaResource());\n        assertTrue(result.hasErrors());\n        assertFalse(result.errors().isEmpty());\n    }\n\n    @Test\n    void testMultipleErrors() throws Exception {\n        JsonNode schemaNode = objectMapper.readTree(\"\"\"\n            {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"message\": { \"type\": \"string\" },\n                    \"count\": { \"type\": \"integer\", \"minimum\": 0 }\n                },\n                \"required\": [\"message\", \"count\"]\n            }\n            \"\"\");\n\n        JsonSchema schema = schemaFactory.getSchema(schemaNode);\n        when(registry.getInputSchema(\"testTask\", TASK_CLASS)).thenReturn(TaskSchemaLookupResult.found(schema, schemaNode, \"testTask.schema.json\"));\n\n        // Missing 'message' and 'count'\n        TaskSchemaValidationResult result = validator.validateInput(\"testTask\", TASK_CLASS, Map.of());\n\n        assertEquals(TaskSchemaValidationResult.Status.INVALID, result.status());\n        assertTrue(result.hasErrors());\n        // Should have at least 2 errors (missing message and count)\n        assertTrue(result.errors().size() >= 2);\n    }\n\n    @Test\n    void testValidOutput() throws Exception {\n        JsonNode schemaNode = objectMapper.readTree(\"\"\"\n            {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"ok\": { \"type\": \"boolean\" },\n                    \"result\": { \"type\": \"string\" }\n                },\n                \"required\": [\"ok\"]\n            }\n            \"\"\");\n\n        JsonSchema schema = schemaFactory.getSchema(schemaNode);\n        when(registry.getOutputSchema(\"testTask\", TASK_CLASS)).thenReturn(TaskSchemaLookupResult.found(schema, schemaNode, \"testTask.schema.json\"));\n\n        TaskSchemaValidationResult result = validator.validateOutput(\"testTask\", TASK_CLASS,\n                Map.of(\"ok\", true, \"result\", \"success\"));\n\n        assertEquals(TaskSchemaValidationResult.Status.VALID, result.status());\n        assertFalse(result.hasErrors());\n    }\n\n    @Test\n    void testTypeValidation() throws Exception {\n        JsonNode schemaNode = objectMapper.readTree(\"\"\"\n            {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"count\": { \"type\": \"integer\" }\n                }\n            }\n            \"\"\");\n\n        JsonSchema schema = schemaFactory.getSchema(schemaNode);\n        when(registry.getInputSchema(\"testTask\", TASK_CLASS)).thenReturn(TaskSchemaLookupResult.found(schema, schemaNode, \"testTask.schema.json\"));\n\n        // String instead of integer\n        TaskSchemaValidationResult result = validator.validateInput(\"testTask\", TASK_CLASS, Map.of(\"count\", \"not-a-number\"));\n\n        assertEquals(TaskSchemaValidationResult.Status.INVALID, result.status());\n        assertTrue(result.hasErrors());\n    }\n\n    @Test\n    void testNoOutputSchema() {\n        when(registry.getOutputSchema(\"testTask\", TASK_CLASS)).thenReturn(TaskSchemaLookupResult.absent(\"testTask.schema.json\"));\n\n        TaskSchemaValidationResult result = validator.validateOutput(\"testTask\", TASK_CLASS, Map.of(\"key\", \"value\"));\n\n        assertEquals(TaskSchemaValidationResult.Status.NO_SCHEMA, result.status());\n        assertFalse(result.hasErrors());\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/tasks/other/OtherSchemaTask.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.tasks.other;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\npublic class OtherSchemaTask implements Task {\n\n    @Override\n    public TaskResult execute(Variables input) {\n        return TaskResult.success();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/vm/JoinCommandTest.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.svm.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class JoinCommandTest {\n\n    @Test\n    public void testAllChildrenDoneShouldContinueExecution() {\n        Frame rootFrame = Frame.builder().root().build();\n        InMemoryState state = new InMemoryState(rootFrame);\n\n        ThreadId parentThread = state.getRootThreadId();\n        ThreadId child1 = state.nextThreadId();\n        ThreadId child2 = state.nextThreadId();\n\n        state.setStatus(child1, ThreadStatus.DONE);\n        state.setStatus(child2, ThreadStatus.DONE);\n\n        JoinCommand<Step> joinCommand = new JoinCommand<>(List.of(child1, child2), null);\n        state.peekFrame(parentThread).push(joinCommand);\n\n        // Should not throw — all children completed successfully\n        assertDoesNotThrow(() -> joinCommand.execute(null, state, parentThread));\n\n        // Parent thread should NOT be suspended\n        assertNotEquals(ThreadStatus.SUSPENDED, state.getStatus(parentThread));\n    }\n\n    @Test\n    public void testSuspendedChildShouldSuspendParent() {\n        Frame rootFrame = Frame.builder().root().build();\n        InMemoryState state = new InMemoryState(rootFrame);\n\n        ThreadId parentThread = state.getRootThreadId();\n        ThreadId child1 = state.nextThreadId();\n        ThreadId child2 = state.nextThreadId();\n\n        state.setStatus(child1, ThreadStatus.DONE);\n        state.setStatus(child2, ThreadStatus.SUSPENDED);\n\n        JoinCommand<Step> joinCommand = new JoinCommand<>(List.of(child1, child2), null);\n\n        joinCommand.execute(null, state, parentThread);\n\n        assertEquals(ThreadStatus.SUSPENDED, state.getStatus(parentThread));\n    }\n\n    @Test\n    public void testMultipleFailedThreadsShouldCollectAllErrors() {\n        Frame rootFrame = Frame.builder().root().build();\n        InMemoryState state = new InMemoryState(rootFrame);\n\n        ThreadId parentThread = state.getRootThreadId();\n        ThreadId child1 = state.nextThreadId();\n        ThreadId child2 = state.nextThreadId();\n        ThreadId child3 = state.nextThreadId();\n\n        state.setStatus(child1, ThreadStatus.DONE);\n        state.setStatus(child2, ThreadStatus.FAILED);\n        state.setStatus(child3, ThreadStatus.FAILED);\n\n        Exception error2 = new RuntimeException(\"error from child2\");\n        Exception error3 = new RuntimeException(\"error from child3\");\n        state.setThreadError(child2, error2);\n        state.setThreadError(child3, error3);\n\n        JoinCommand<Step> joinCommand = new JoinCommand<>(List.of(child1, child2, child3), null);\n\n        ParallelExecutionException exception = assertThrows(\n                ParallelExecutionException.class,\n                () -> joinCommand.execute(null, state, parentThread));\n\n        assertEquals(2, exception.getExceptions().size());\n        assertTrue(exception.getExceptions().containsAll(List.of(error2, error3)));\n    }\n\n    @Test\n    public void testFailedThreadWithNoErrorShouldBeFilteredOut() {\n        Frame rootFrame = Frame.builder().root().build();\n        InMemoryState state = new InMemoryState(rootFrame);\n\n        ThreadId parentThread = state.getRootThreadId();\n        ThreadId child1 = state.nextThreadId();\n        ThreadId child2 = state.nextThreadId();\n\n        state.setStatus(child1, ThreadStatus.FAILED);\n        state.setStatus(child2, ThreadStatus.FAILED);\n\n        // Only set error for child1; child2 has no error — clearThreadError returns null\n        Exception error1 = new RuntimeException(\"error 1\");\n        state.setThreadError(child1, error1);\n\n        JoinCommand<Step> joinCommand = new JoinCommand<>(List.of(child1, child2), null);\n\n        ParallelExecutionException exception = assertThrows(\n                ParallelExecutionException.class,\n                () -> joinCommand.execute(null, state, parentThread));\n\n        // Only non-null errors should be included\n        assertEquals(1, exception.getExceptions().size());\n        assertSame(error1, exception.getExceptions().get(0));\n    }\n\n    @Test\n    public void testFailedTakePriorityOverSuspended() {\n        Frame rootFrame = Frame.builder().root().build();\n        InMemoryState state = new InMemoryState(rootFrame);\n\n        ThreadId parentThread = state.getRootThreadId();\n        ThreadId child1 = state.nextThreadId();\n        ThreadId child2 = state.nextThreadId();\n\n        state.setStatus(child1, ThreadStatus.FAILED);\n        state.setStatus(child2, ThreadStatus.SUSPENDED);\n\n        Exception error = new RuntimeException(\"failure\");\n        state.setThreadError(child1, error);\n\n        JoinCommand<Step> joinCommand = new JoinCommand<>(List.of(child1, child2), null);\n\n        // Should throw even though child2 is suspended — failures are checked first\n        ParallelExecutionException exception = assertThrows(\n                ParallelExecutionException.class,\n                () -> joinCommand.execute(null, state, parentThread));\n\n        assertEquals(1, exception.getExceptions().size());\n        assertSame(error, exception.getExceptions().get(0));\n    }\n\n    @Test\n    public void testErrorsClearedFromStateAfterFailure() {\n        Frame rootFrame = Frame.builder().root().build();\n        InMemoryState state = new InMemoryState(rootFrame);\n\n        ThreadId parentThread = state.getRootThreadId();\n        ThreadId child = state.nextThreadId();\n\n        state.setStatus(child, ThreadStatus.FAILED);\n        state.setThreadError(child, new RuntimeException(\"err\"));\n\n        JoinCommand<Step> joinCommand = new JoinCommand<>(List.of(child), null);\n\n        assertThrows(ParallelExecutionException.class,\n                () -> joinCommand.execute(null, state, parentThread));\n\n        // clearThreadError should have removed the error from state\n        assertNull(state.getThreadError(child));\n    }\n\n    @Test\n    public void testFailedThreadsShouldBeFilteredByOwnedIds() {\n        // Setup state with a root frame for the parent thread\n        Frame rootFrame = Frame.builder().root().build();\n        InMemoryState state = new InMemoryState(rootFrame);\n\n        ThreadId parentThread = state.getRootThreadId();   // thread 0\n        ThreadId ownedThread1 = state.nextThreadId();      // thread 1\n        ThreadId ownedThread2 = state.nextThreadId();      // thread 2\n        ThreadId foreignThread = state.nextThreadId();     // thread 3\n\n        // ownedThread1 completed successfully\n        state.setStatus(ownedThread1, ThreadStatus.DONE);\n        // ownedThread2 failed (owned by this JoinCommand)\n        state.setStatus(ownedThread2, ThreadStatus.FAILED);\n        // foreignThread failed (NOT owned by this JoinCommand — belongs to another parallel block)\n        state.setStatus(foreignThread, ThreadStatus.FAILED);\n\n        Exception ownedError = new RuntimeException(\"owned thread error\");\n        Exception foreignError = new RuntimeException(\"foreign thread error\");\n        state.setThreadError(ownedThread2, ownedError);\n        state.setThreadError(foreignThread, foreignError);\n\n        // JoinCommand only owns threads 1 and 2\n        List<ThreadId> ids = List.of(ownedThread1, ownedThread2);\n        JoinCommand<Step> joinCommand = new JoinCommand<>(ids, null);\n\n        // Execute — should throw ParallelExecutionException for the owned failed thread only\n        ParallelExecutionException exception = assertThrows(\n                ParallelExecutionException.class,\n                () -> joinCommand.execute(null, state, parentThread));\n\n        // The exception should contain ONLY the owned thread's error\n        assertEquals(1, exception.getExceptions().size(),\n                \"Should contain only the owned thread's error, not the foreign thread's error\");\n        assertSame(ownedError, exception.getExceptions().get(0));\n\n        // The foreign thread's error should NOT have been cleared\n        assertNotNull(state.getThreadError(foreignThread),\n                \"Foreign thread's error should remain in state (not cleared by this JoinCommand)\");\n        assertSame(foreignError, state.getThreadError(foreignThread).exception(),\n                \"Foreign thread's error should be untouched\");\n    }\n}"
  },
  {
    "path": "runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/vm/SaveLastErrorCommandTest.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.runtime.v2.runner.PersistenceService;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.ArgumentCaptor;\n\nimport java.io.ByteArrayOutputStream;\nimport java.util.Collections;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.containsString;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.*;\n\npublic class SaveLastErrorCommandTest {\n\n    @Test\n    public void test() throws Exception {\n        ObjectMapper om = new ObjectMapper();\n\n        PersistenceService persistenceService = mock(PersistenceService.class);\n\n        Runtime runtime = mock(Runtime.class);\n        when(runtime.getService(eq(ObjectMapper.class))).thenReturn(om);\n        when(runtime.getService(eq(PersistenceService.class))).thenReturn(persistenceService);\n\n        MyException error = new MyException(\"BOOM1\");\n        error.setSomeCyclicField(error);\n\n        Frame rootFrame = Frame.builder()\n                .root()\n                .locals(Collections.singletonMap(Frame.LAST_EXCEPTION_KEY, error))\n                .build();\n        State state = new InMemoryState(rootFrame);\n        ThreadId threadId = state.getRootThreadId();\n\n        // ---\n        SaveLastErrorCommand cmd = new SaveLastErrorCommand();\n        rootFrame.push(new SaveLastErrorCommand());\n\n        try {\n            cmd.eval(runtime, state, threadId);\n        } catch (MyException e) {\n            assertEquals(error, e);\n        }\n\n        ArgumentCaptor<PersistenceService.Writer> writerCaptor = ArgumentCaptor.forClass(PersistenceService.Writer.class);\n        verify(persistenceService, times(1))\n                .persistFile(eq(Constants.Files.OUT_VALUES_FILE_NAME), writerCaptor.capture());\n\n        ByteArrayOutputStream bos = new ByteArrayOutputStream();\n        writerCaptor.getValue().write(bos);\n\n        assertThat(bos.toString(), containsString(\"\\\"message\\\":\\\"BOOM1\\\"\"));\n        assertThat(bos.toString(), containsString(\"\\\"@id\\\":1\"));\n    }\n\n    @SuppressWarnings(\"unused\")\n    static class MyException extends Exception {\n\n        private static final long serialVersionUID = 1L;\n\n        private Exception someCyclicField;\n\n        public MyException(String message) {\n            super(message);\n        }\n\n        public Exception getSomeCyclicField() {\n            return someCyclicField;\n        }\n\n        public void setSomeCyclicField(Exception someCyclicField) {\n            this.someCyclicField = someCyclicField;\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/vm/TaskSchemaValidationTest.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.TaskCallValidation;\nimport com.walmartlabs.concord.runtime.v2.model.TaskCallValidation.ValidationMode;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskSchemaValidationException;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskSchemaValidationResult;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskSchemaValidator;\nimport com.walmartlabs.concord.runtime.v2.sdk.Task;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.svm.Runtime;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.Serializable;\nimport java.util.List;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.ArgumentMatchers.*;\nimport static org.mockito.Mockito.*;\n\npublic class TaskSchemaValidationTest {\n\n    @Test\n    void testValidateOutputThrowsInFailMode() {\n        Runtime runtime = mock(Runtime.class);\n        TaskSchemaValidator validator = mock(TaskSchemaValidator.class);\n        when(runtime.getService(TaskSchemaValidator.class)).thenReturn(validator);\n        when(validator.validateOutput(eq(\"testTask\"), eq(Task.class), anyMap()))\n                .thenReturn(TaskSchemaValidationResult.invalid(\"testTask.schema.json\", List.of(\"bad output\")));\n\n        TaskSchemaValidationException e = assertThrows(TaskSchemaValidationException.class,\n                () -> TaskSchemaValidation.validateOutput(runtime,\n                        \"testTask\",\n                        Task.class,\n                        TaskResult.success(),\n                        new TaskCallValidation(ValidationMode.DISABLED, ValidationMode.FAIL)));\n\n        assertEquals(\"testTask\", e.getTaskName());\n        assertEquals(\"out\", e.getSection());\n        assertEquals(\"testTask.schema.json\", e.getSchemaResource());\n        assertEquals(List.of(\"bad output\"), e.getValidationErrors());\n    }\n\n    @Test\n    void testValidateOutputSkipsSuspendResults() {\n        Runtime runtime = mock(Runtime.class);\n\n        TaskSchemaValidation.validateOutput(runtime,\n                \"testTask\",\n                Task.class,\n                TaskResult.reentrantSuspend(\"event\", Map.of(\"k\", (Serializable) \"v\")),\n                new TaskCallValidation(ValidationMode.DISABLED, ValidationMode.WARN));\n\n        verifyNoInteractions(runtime);\n    }\n\n    @Test\n    void testValidateOutputSkipsFailedSimpleResults() {\n        Runtime runtime = mock(Runtime.class);\n\n        TaskSchemaValidation.validateOutput(runtime,\n                \"testTask\",\n                Task.class,\n                TaskResult.fail(\"boom\"),\n                new TaskCallValidation(ValidationMode.DISABLED, ValidationMode.FAIL));\n\n        verifyNoInteractions(runtime);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/java/com/walmartlabs/concord/runtime/v2/runner/vm/VMUtilsTest.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.vm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.svm.Frame;\nimport com.walmartlabs.concord.svm.InMemoryState;\nimport com.walmartlabs.concord.svm.State;\nimport com.walmartlabs.concord.svm.ThreadId;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class VMUtilsTest {\n\n    @Test\n    public void testLocals() {\n        Frame rootFrame = Frame.builder()\n                .root()\n                .locals(Collections.singletonMap(\"x\", 123))\n                .build();\n\n        State state = new InMemoryState(rootFrame);\n\n        ThreadId threadId = state.getRootThreadId();\n\n        Frame levelOneFrame = Frame.builder().nonRoot()\n                .locals(Collections.singletonMap(\"y\", 234))\n                .build();\n        state.pushFrame(threadId, levelOneFrame);\n\n        Frame levelTwoFrame = Frame.builder().nonRoot()\n                .locals(Collections.singletonMap(\"x\", 345))\n                .build();\n        state.pushFrame(threadId, levelTwoFrame);\n\n        Map<String, Object> locals = VMUtils.getCombinedLocals(state, threadId);\n        assertEquals(2, locals.size());\n        assertEquals(345, locals.get(\"x\"));\n        assertEquals(234, locals.get(\"y\"));\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/tasks/full.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"Full Test Schema\",\n  \"definitions\": {\n    \"message\": {\n      \"type\": \"string\"\n    }\n  },\n  \"$defs\": {\n    \"count\": {\n      \"type\": \"integer\"\n    }\n  },\n  \"in\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"message\": {\n        \"$ref\": \"#/definitions/message\"\n      }\n    },\n    \"required\": [\"message\"]\n  },\n  \"out\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"count\": {\n        \"$ref\": \"#/$defs/count\"\n      }\n    },\n    \"required\": [\"count\"]\n  }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/tasks/invalidJson.schema.json",
    "content": "{ \"in\":\n"
  },
  {
    "path": "runtime/v2/runner/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/tasks/invalidSection.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"in\": true,\n  \"out\": {\n    \"type\": \"object\"\n  }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/tasks/noSection.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"in\": {\n    \"type\": \"object\"\n  }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/tasks/other/shared.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"in\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"other\": {\n        \"type\": \"string\"\n      }\n    },\n    \"required\": [\"other\"]\n  }\n}\n"
  },
  {
    "path": "runtime/v2/runner/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/tasks/rootNotObject.schema.json",
    "content": "[\n  true\n]\n"
  },
  {
    "path": "runtime/v2/runner/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/tasks/shared.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"in\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"local\": {\n        \"type\": \"string\"\n      }\n    },\n    \"required\": [\"local\"]\n  }\n}\n"
  },
  {
    "path": "runtime/v2/runner-test/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-runner-v2-test</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runner-v2</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>ch.qos.logback</groupId>\n            <artifactId>logback-classic</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.hamcrest</groupId>\n            <artifactId>hamcrest</artifactId>\n            <version>2.2</version>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.reflections</groupId>\n            <artifactId>reflections</artifactId>\n        </dependency>\n\n        <!-- Immutables -->\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.errorprone</groupId>\n            <artifactId>error_prone_annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <!-- to test the scripting feature -->\n        <dependency>\n            <groupId>org.apache.groovy</groupId>\n            <artifactId>groovy-all</artifactId>\n            <type>pom</type>\n            <scope>test</scope>\n            <exclusions>\n                <exclusion>\n                    <groupId>org.apache.groovy</groupId>\n                    <artifactId>groovy-test</artifactId>\n                </exclusion>\n                <exclusion>\n                    <groupId>org.apache.groovy</groupId>\n                    <artifactId>groovy-test-junit5</artifactId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "runtime/v2/runner-test/src/main/java/com/walmartlabs/concord/runtime/v2/runner/TestCheckpointUploader.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.ZipUtils;\nimport com.walmartlabs.concord.runtime.v2.runner.checkpoints.CheckpointUploader;\nimport com.walmartlabs.concord.sdk.Constants;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class TestCheckpointUploader implements CheckpointUploader {\n\n    private final Map<String, Path> checkpoints = new ConcurrentHashMap<>();\n\n    @Override\n    public void upload(UUID checkpointId, UUID correlationId, String name, Path archivePath) throws Exception {\n        Path tmpFile = PathUtils.createTempDir(\"unittests\").resolve(archivePath.getFileName());\n        Files.move(archivePath, tmpFile);\n        checkpoints.put(name, tmpFile);\n\n        System.out.println(checkpoints);\n    }\n\n    public void put(String name, Path archivePath) {\n        checkpoints.put(name, archivePath);\n    }\n\n    public void restore(String name, Path workDir) throws Exception {\n        Path archive = checkpoints.get(name);\n        assertNotNull(archive);\n\n        PathUtils.deleteRecursively(workDir.resolve(Constants.Files.CONCORD_SYSTEM_DIR_NAME));\n        PathUtils.deleteRecursively(workDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME));\n\n        ZipUtils.unzip(archive, workDir);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner-test/src/main/java/com/walmartlabs/concord/runtime/v2/runner/TestRuntimeV2.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.AbstractModule;\nimport com.google.inject.Injector;\nimport com.google.inject.Module;\nimport com.google.inject.multibindings.Multibinder;\nimport com.google.inject.name.Names;\nimport com.walmartlabs.concord.client2.ApiClient;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.runtime.common.FormService;\nimport com.walmartlabs.concord.runtime.common.StateManager;\nimport com.walmartlabs.concord.runtime.common.cfg.ApiConfiguration;\nimport com.walmartlabs.concord.runtime.common.cfg.ImmutableRunnerConfiguration;\nimport com.walmartlabs.concord.runtime.common.cfg.LoggingConfiguration;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.runtime.v2.ProjectLoaderV2;\nimport com.walmartlabs.concord.runtime.v2.runner.checkpoints.CheckpointService;\nimport com.walmartlabs.concord.runtime.v2.runner.checkpoints.CheckpointUploader;\nimport com.walmartlabs.concord.runtime.v2.runner.checkpoints.DefaultCheckpointService;\nimport com.walmartlabs.concord.runtime.v2.runner.guice.BaseRunnerModule;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.LoggerProvider;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.LoggingClient;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.LoggingConfigurator;\nimport com.walmartlabs.concord.runtime.v2.runner.logging.RunnerLogger;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallListener;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallPolicyChecker;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskResultListener;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskV2Provider;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.*;\nimport com.walmartlabs.concord.runtime.v2.runner.vm.ParallelExecutionException;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\nimport com.walmartlabs.concord.runtime.v2.sdk.SensitiveDataHolder;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.*;\nimport org.junit.jupiter.api.extension.AfterEachCallback;\nimport org.junit.jupiter.api.extension.BeforeEachCallback;\nimport org.junit.jupiter.api.extension.ExtensionContext;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.*;\nimport java.net.URISyntaxException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport static com.walmartlabs.concord.process.loader.StandardRuntimeTypes.PROJECT_ROOT_FILE_NAMES;\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.spy;\n\npublic class TestRuntimeV2 implements BeforeEachCallback, AfterEachCallback {\n\n    private static final Logger log = LoggerFactory.getLogger(TestRuntimeV2.class);\n\n    protected Path workDir;\n    protected UUID instanceId;\n    protected FormService formService;\n    protected ProcessConfiguration processConfiguration;\n\n    protected ProcessStatusCallback processStatusCallback;\n    protected Module testServices;\n\n    protected TestCheckpointUploader checkpointService;\n    protected TestLoggingClient testLoggingClient;\n\n    protected boolean skipSerializationAssert = false;\n\n    protected byte[] lastLog;\n    protected byte[] allLogs;\n\n    private Class<?> testClass;\n\n    private Class<? extends PersistenceService> persistenceServiceClass;\n\n    public TestRuntimeV2 withPersistenceService(Class<? extends PersistenceService> persistenceServiceClass) {\n        this.persistenceServiceClass = persistenceServiceClass;\n        return this;\n    }\n\n    @Override\n    public void beforeEach(ExtensionContext context) throws Exception {\n        setUp();\n\n        this.testClass = context.getTestClass().orElseThrow(() -> new IllegalStateException(\"No test class found\"));\n    }\n\n    @Override\n    public void afterEach(ExtensionContext context) throws Exception {\n        tearDown();\n    }\n\n    public Path workDir() {\n        return workDir;\n    }\n\n    public FormService formService() {\n        return formService;\n    }\n\n    public UUID instanceId() {\n        return instanceId;\n    }\n\n    public byte[] lastLog() {\n        return lastLog;\n    }\n\n    public byte[] allLogs() {\n        return allLogs;\n    }\n\n    public TestCheckpointUploader checkpointService() {\n        return checkpointService;\n    }\n\n    public TestLoggingClient testLoggingClient() {\n        return testLoggingClient;\n    }\n\n    public ProcessStatusCallback processStatusCallback() {\n        return processStatusCallback;\n    }\n\n    public void setWorkDir(Path newWorkDir) {\n        this.workDir = newWorkDir;\n    }\n\n    public void deploy(String resource) throws URISyntaxException, IOException {\n        var res = testClass.getResource(resource);\n        assertNotNull(res, \"Resource not found: \" + resource);\n\n        Path src = Paths.get(res.toURI());\n        PathUtils.copy(src, workDir);\n    }\n\n    public ImmutableProcessConfiguration.Builder cfgFromDeployment() throws IOException {\n        for (String fileName : PROJECT_ROOT_FILE_NAMES) {\n            Path p = workDir.resolve(fileName);\n            if (Files.exists(p)) {\n                var result = new ProjectLoaderV2((imports, dest, listener) -> List.of()).loadFromFile(p);\n                return ProcessConfiguration.builder()\n                        .arguments(result.getProjectDefinition().configuration().arguments());\n            }\n        }\n\n        return ImmutableProcessConfiguration.builder();\n    }\n\n    public void save(ProcessConfiguration cfg) {\n        ImmutableProcessConfiguration.Builder b = ProcessConfiguration.builder().from(cfg)\n                .instanceId(instanceId);\n\n        if (cfg.entryPoint() == null) {\n            b.entryPoint(Constants.Request.DEFAULT_ENTRY_POINT_NAME);\n        }\n\n        this.processConfiguration = b.build();\n    }\n\n    public byte[] resume(String eventName, ProcessConfiguration cfg) throws Exception {\n        StateManager.saveResumeEvent(workDir, eventName); // TODO use interface\n        if (cfg != null) {\n            save(cfg);\n        }\n        return run();\n    }\n\n    public byte[] run() throws Exception {\n        return run(null);\n    }\n\n    public byte[] run(RunnerConfiguration baseCfg) throws Exception {\n        if (processConfiguration == null) {\n            save(cfgFromDeployment().build());\n        }\n\n        ImmutableRunnerConfiguration.Builder runnerCfg = RunnerConfiguration.builder()\n                .logging(LoggingConfiguration.builder()\n                        .segmentedLogs(false)\n                        .workDirMasking(false)\n                        .build());\n\n        if (baseCfg != null) {\n            runnerCfg.from(baseCfg);\n        }\n\n        runnerCfg.agentId(UUID.randomUUID().toString())\n                .api(ApiConfiguration.builder()\n                        .baseUrl(\"http://localhost:8001\") // TODO make optional?\n                        .build());\n\n        PrintStream oldOut = System.out;\n\n        ByteArrayOutputStream logStream = new ByteArrayOutputStream();\n        PrintStream out = new PrintStream(logStream);\n        System.setOut(out);\n\n        AbstractModule runtimeModule = new AbstractModule() {\n            @Override\n            protected void configure() {\n                bind(DefaultTaskVariablesService.class).toProvider(new DefaultTaskVariablesProvider(processConfiguration));\n                bind(RunnerLogger.class).toProvider(LoggerProvider.class);\n                bind(LoggingClient.class).toInstance(testLoggingClient);\n            }\n        };\n\n        byte[] log;\n        try {\n            Injector injector = new InjectorFactory(new WorkingDirectory(workDir),\n                    runnerCfg.build(),\n                    () -> processConfiguration,\n                    testServices,\n                    runtimeModule)\n                    .create();\n            injector.getInstance(Main.class).execute();\n        } catch (UserDefinedException | ParallelExecutionException e) { // see {@link com.walmartlabs.concord.runtime.v2.runner.Main#main}\n            throw e;\n        } catch (Throwable t) {\n            t.printStackTrace(out);\n            throw t;\n        } finally {\n            out.flush();\n            System.setOut(oldOut);\n\n            log = logStream.toByteArray();\n            System.out.write(log, 0, log.length);\n\n            lastLog = log;\n        }\n\n        if (allLogs == null) {\n            allLogs = log;\n        } else {\n            // append the current log to allLogs\n            ByteArrayOutputStream baos = new ByteArrayOutputStream(allLogs.length + log.length);\n            baos.write(allLogs);\n            baos.write(log);\n            allLogs = baos.toByteArray();\n        }\n\n        return log;\n    }\n\n    public static void assertLogAtLeast(byte[] ab, int n, String pattern) throws IOException {\n        if (ab == null) {\n            fail(\"Log is empty\");\n        }\n\n        if (grep(ab, pattern) < n) {\n            fail(\"Expected at least \" + n + \" log line(s): \" + pattern + \", got: \\n\" + new String(ab));\n        }\n    }\n\n    public static void assertLogExactMatch(byte[] ab, int n, String pattern) throws IOException {\n        if (ab == null) {\n            fail(\"Log is empty\");\n        }\n\n        int count = grep(ab, pattern);\n        if (count != n) {\n            fail(\"Expected exactly \" + n + \" log line(s): \" + pattern + \", but found: \" + count + \"\\nLog content:\\n\" + new String(ab));\n        }\n    }\n\n    public static void assertLog(byte[] ab, String pattern) throws IOException {\n        if (ab == null) {\n            fail(\"Log is empty\");\n        }\n\n        int cnt = grep(ab, pattern);\n        if (cnt != 1) {\n            fail(\"Expected a single log line: \" + pattern + \", got (\" + cnt + \"): \\n\" + new String(ab));\n        }\n    }\n\n    public static void assertMultiLineLog(byte[] ab, String pattern) throws IOException {\n        if (ab == null) {\n            fail(\"Log is empty\");\n        }\n\n        int cnt = grepMultiLine(ab, pattern);\n        if (cnt != 1) {\n            fail(\"Expected a single log line: \" + pattern + \", got (\" + cnt + \"): \\n\" + new String(ab));\n        }\n    }\n\n    public static void assertNoLog(byte[] ab, String pattern) throws IOException {\n        if (grep(ab, pattern) > 0) {\n            fail(\"Expected no log lines like this: \" + pattern + \", got: \\n\" + new String(ab));\n        }\n    }\n\n    public static int grep(byte[] ab, String pattern) throws IOException {\n        int cnt = 0;\n\n        try (ByteArrayInputStream bais = new ByteArrayInputStream(ab);\n             BufferedReader reader = new BufferedReader(new InputStreamReader(bais))) {\n\n            String line;\n            while ((line = reader.readLine()) != null) {\n                if (line.matches(pattern)) {\n                    cnt++;\n                }\n            }\n        }\n\n        return cnt;\n    }\n\n    public static int grepMultiLine(byte[] ab, String pattern) throws IOException {\n        int cnt = 0;\n\n        Pattern compiledPattern = Pattern.compile(pattern, Pattern.DOTALL);\n\n        try (ByteArrayInputStream bais = new ByteArrayInputStream(ab);\n             BufferedReader reader = new BufferedReader(new InputStreamReader(bais))) {\n\n            StringBuilder content = new StringBuilder();\n            String line;\n            while ((line = reader.readLine()) != null) {\n                content.append(line).append(\"\\n\");\n            }\n\n            Matcher matcher = compiledPattern.matcher(content.toString());\n            while (matcher.find()) {\n                cnt++;\n            }\n        }\n\n        return cnt;\n    }\n\n    private void setUp() throws IOException {\n        workDir = Files.createTempDirectory(\"test\");\n\n        instanceId = UUID.randomUUID();\n\n        Path formsDir = workDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(Constants.Files.JOB_STATE_DIR_NAME)\n                .resolve(Constants.Files.JOB_FORMS_V2_DIR_NAME);\n\n        formService = new FormService(formsDir);\n\n        processStatusCallback = mock(ProcessStatusCallback.class);\n\n        checkpointService = spy(new TestCheckpointUploader());\n        testLoggingClient = spy(new TestLoggingClient());\n\n        testServices = new AbstractModule() {\n            @Override\n            protected void configure() {\n                install(new BaseRunnerModule());\n\n                bind(ClassLoader.class).annotatedWith(Names.named(\"runtime\")).toInstance(testClass.getClassLoader());\n\n                bind(CheckpointUploader.class).toInstance(checkpointService);\n                bind(CheckpointService.class).to(DefaultCheckpointService.class);\n                bind(DependencyManager.class).to(DefaultDependencyManager.class);\n                bind(DockerService.class).to(DefaultDockerService.class);\n                bind(FileService.class).to(DefaultFileService.class);\n                bind(LockService.class).to(DefaultLockService.class);\n                if (persistenceServiceClass != null) {\n                    bind(PersistenceService.class).to(persistenceServiceClass);\n                } else {\n                    bind(PersistenceService.class).toInstance(mock(PersistenceService.class));\n                }\n                bind(ProcessStatusCallback.class).toInstance(processStatusCallback);\n                bind(SecretService.class).to(DefaultSecretService.class);\n                bind(ApiClient.class).toInstance(mock(ApiClient.class));\n\n                Multibinder<TaskProvider> taskProviders = Multibinder.newSetBinder(binder(), TaskProvider.class);\n                taskProviders.addBinding().to(TaskV2Provider.class);\n\n                Multibinder<TaskCallListener> taskCallListeners = Multibinder.newSetBinder(binder(), TaskCallListener.class);\n                taskCallListeners.addBinding().to(TaskCallPolicyChecker.class);\n                taskCallListeners.addBinding().to(TaskResultListener.class);\n\n                Multibinder<ExecutionListener> executionListeners = Multibinder.newSetBinder(binder(), ExecutionListener.class);\n                executionListeners.addBinding().toInstance(new ExecutionListener() {\n                    @Override\n                    public void beforeProcessStart(Runtime runtime, State state) {\n                        runtime.getService(SensitiveDataHolder.class).get().clear();\n                    }\n                });\n                executionListeners.addBinding().to(StackTraceCollector.class);\n\n                executionListeners.addBinding().toInstance(new ExecutionListener() {\n                    @Override\n                    public Result beforeCommand(Runtime runtime, VM vm, State state, ThreadId threadId, Command cmd) {\n                        if (cmd instanceof ForkCommand) {\n                            skipSerializationAssert = true;\n                        }\n                        return ExecutionListener.super.beforeCommand(runtime, vm, state, threadId, cmd);\n                    }\n\n                    @Override\n                    public Result afterCommand(Runtime runtime, VM vm, State state, ThreadId threadId, Command cmd) {\n                        if (cmd instanceof BlockCommand\n                            || cmd instanceof ParallelCommand\n                            || skipSerializationAssert) {\n                            return ExecutionListener.super.afterCommand(runtime, vm, state, threadId, cmd);\n                        }\n\n                        assertTrue(isSerializable(state), \"Non serializable state after: \" + cmd);\n                        return ExecutionListener.super.afterCommand(runtime, vm, state, threadId, cmd);\n                    }\n                });\n            }\n        };\n\n        allLogs = null;\n    }\n\n    private void tearDown() throws IOException {\n        if (workDir != null) {\n            PathUtils.deleteRecursively(workDir);\n        }\n\n        processConfiguration = null;\n        testLoggingClient = null;\n\n        LoggingConfigurator.reset();\n    }\n\n    private static boolean isSerializable(Object o) {\n        try (ObjectOutputStream oos = new ObjectOutputStream(new ByteArrayOutputStream())) {\n            oos.writeObject(o);\n        } catch (IOException e) {\n            log.warn(\"Serialization error: {}\", e.getMessage(), e);\n            return false;\n        }\n\n        return true;\n    }\n\n    public static class TestLoggingClient implements LoggingClient {\n\n        private final AtomicLong id = new AtomicLong(1L);\n        private final Map<Long, String> segmentNames = new ConcurrentHashMap<>();\n\n        @Override\n        public long createSegment(UUID correlationId, String name) {\n            long segmentId = id.getAndIncrement();\n            segmentNames.put(segmentId, name);\n            return segmentId;\n        }\n\n        public String getSegmentName(long segmentId) {\n            return segmentNames.get(segmentId);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/java/com/walmartlabs/concord/runtime/v2/runner/LogExceptionsTest.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport static com.walmartlabs.concord.runtime.v2.runner.TestRuntimeV2.*;\nimport static java.util.regex.Pattern.quote;\nimport static org.junit.jupiter.api.Assertions.fail;\n\npublic class LogExceptionsTest {\n\n    @RegisterExtension\n    private static final TestRuntimeV2 runtime = new TestRuntimeV2();\n\n    @Test\n    public void shouldLogExceptionStackTraceWhenTaskThrowsException() throws Exception {\n        runtime.deploy(\"logExceptionTests/fromTask\");\n\n        runtime.save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runtime.run();\n            fail(\"Exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        // error\n        assertLog(runtime.lastLog(), \".*\" + quote(\"(concord.yaml): Error @ line: 3, col: 7. boom!\") + \".*\");\n        // stacktrace\n        assertLog(runtime.lastLog(), \".*\" + quote(\"at com.walmartlabs.concord.runtime.v2.runner.tasks.Tasks$FaultyTask2.execute\") + \".*\");\n    }\n\n    @Test\n    public void shouldLogExceptionStackTraceWhenExpressionThrowsException() throws Exception {\n        runtime.deploy(\"logExceptionTests/fromExpression\");\n\n        runtime.save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runtime.run();\n            fail(\"Exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        // error\n        // TODO: \"javax.el.ELException: java.lang.Exception: ...\" remove javax.el.ELException?\n        assertLog(runtime.lastLog(), \".*\" + quote(\"Error @ line: 3, col: 7. while evaluating expression '${faultyTask.exception('BOOM')}': BOOM\") + \".*\");\n        // stacktrace\n        assertLog(runtime.lastLog(), \".*\" + quote(\"at com.walmartlabs.concord.runtime.v2.runner.tasks.Tasks$FaultyTask\") + \".*\");\n    }\n\n    @Test\n    public void shouldLogExceptionStackTraceWhenTaskThrowsExceptionFromParallel() throws Exception {\n        runtime.deploy(\"logExceptionTests/fromParallel\");\n\n        runtime.save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runtime.run();\n            fail(\"Exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        // error\n        assertLog(runtime.lastLog(), \".*\" + quote(\"(concord.yaml): Error @ line: 4, col: 11. boom!\") + \".*\");\n\n        // stacktrace\n        assertLog(runtime.lastLog(), \".*\" + quote(\"at com.walmartlabs.concord.runtime.v2.runner.tasks.Tasks$FaultyTask3.execute\") + \".*\");\n    }\n\n    @Test\n    public void noStacktraceForUserDefinedExceptionFromTask() throws Exception {\n        runtime.deploy(\"logExceptionTests/userDefinedExceptionFromTask\");\n\n        runtime.save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runtime.run();\n            fail(\"Exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        // error\n        assertLog(runtime.lastLog(), \".*\" + quote(\"(concord.yaml): Error @ line: 3, col: 9. boom!\") + \".*\");\n\n        // no single exception message\n        assertNoLog(runtime.lastLog(), quote(\"boom!\"));\n\n        // no stacktrace\n        assertNoLog(runtime.lastLog(), \".*noStacktraceForUserDefinedExceptionFromTask.*\");\n    }\n\n    @Test\n    public void noStacktraceForUserDefinedExceptionFromExpression() throws Exception {\n        runtime.deploy(\"logExceptionTests/userDefinedExceptionFromExpression\");\n\n        runtime.save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runtime.run();\n            fail(\"Exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        // error\n        assertLog(runtime.lastLog(), \".*\" + quote(\"(concord.yaml): Error @ line: 3, col: 7. BOOM\") + \".*\");\n\n        // no single exception message\n        assertNoLog(runtime.lastLog(), quote(\"BOOM\"));\n\n        // no stacktrace\n        assertNoLog(runtime.lastLog(), \".*noStacktraceForUserDefinedExceptionFromExpression.*\");\n    }\n\n    @Test\n    public void noStacktraceForUserDefinedExceptionFromTaskParallel() throws Exception {\n        runtime.deploy(\"logExceptionTests/userDefinedExceptionFromTaskParallel\");\n\n        runtime.save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runtime.run();\n            fail(\"Exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        // error\n        assertLog(runtime.lastLog(), \".*\" + quote(\"(concord.yaml): Error @ line: 4, col: 11. boom!\") + \".*\");\n        assertMultiLineLog(runtime.lastLog(), \".*\" + quote(\"(concord.yaml): Error @ line: 3, col: 7. Parallel execution errors: \") + \"\\n\" + quote(\"(concord.yaml): Error @ line: 4, col: 11, thread: 1: boom!\"));\n\n        assertLogExactMatch(runtime.lastLog(), 1, \".*\" + quote(\"Parallel execution errors\") + \".*\");\n\n        // no stacktrace\n        assertNoLog(runtime.lastLog(), \".*\" + quote(\"com.walmartlabs.concord.runtime.v2.runner.tasks.Tasks$UserDefinedExceptionTask.execute\") + \".*\");\n    }\n\n    @Test\n    public void noStacktraceForTaskFailReturn() throws Exception {\n        runtime.deploy(\"logExceptionTests/failResultFromTask\");\n\n        runtime.save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runtime.run();\n            fail(\"Exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        // error\n        assertLog(runtime.lastLog(), \".*\" + quote(\"(concord.yaml): Error @ line: 3, col: 9. boom!\") + \".*\");\n\n        assertLogExactMatch(runtime.lastLog(), 1, \".*\" + quote(\"boom!\") + \".*\");\n\n        // no stacktrace\n        assertNoLog(runtime.lastLog(), \".*noStacktraceForTaskFailReturn.*\");\n        assertNoLog(runtime.lastLog(), \".*\" + quote(\"at com.walmartlabs.concord.runtime.v2.runner.tasks.Tasks$FaultyTask.execute\") + \".*\");\n    }\n\n    @Test\n    public void noStackTraceForMethodNotFound() throws Exception {\n        runtime.deploy(\"logExceptionTests/methodNotFound\");\n\n        runtime.save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runtime.run();\n            fail(\"Exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        // error\n        assertLog(runtime.lastLog(), \".*\" + quote(\"(concord.yaml): Error @ line: 3, col: 7. while evaluating expression '${faultyTask.unknownMethod('BOOM')}': Can't find 'unknownMethod()' method in com.walmartlabs.concord.runtime.v2.runner.tasks.Tasks$FaultyTask\") + \".*\");\n\n        // no stacktrace\n        assertNoLog(runtime.lastLog(), \".*\" + quote(\"at com.walmartlabs.concord.runtime.v2.runner.el.LazyExpressionEvaluator.evalExpr\") + \".*\");\n    }\n\n    @Test\n    public void noStackTraceForVariableNotFound() throws Exception {\n        runtime.deploy(\"logExceptionTests/variableNotFound\");\n\n        runtime.save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runtime.run();\n            fail(\"Exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        // error\n        assertLog(runtime.lastLog(), \".*\" + quote(\"(concord.yaml): Error @ line: 3, col: 7. while evaluating expression '${unknown}': Can't find a variable 'unknown'. Check if it is defined in the current scope. Details: ELResolver cannot handle a null base Object with identifier 'unknown'\") + \".*\");\n\n        // no stacktrace\n        assertNoLog(runtime.lastLog(), \".*\" + quote(\"at com.walmartlabs.concord.runtime.v2.runner.el.LazyExpressionEvaluator.evalExpr\") + \".*\");\n    }\n\n    @Test\n    public void noStacktraceForScriptException() throws Exception {\n        runtime.deploy(\"logExceptionTests/fromScript\");\n\n        runtime.save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runtime.run();\n            fail(\"Exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        // error\n        assertLog(runtime.lastLog(), \".*\" + quote(\"(concord.yaml): Error @ line: 3, col: 7. Something went wrong\") + \".*\");\n\n        // no stacktrace\n        assertNoLog(runtime.lastLog(), \".*DefaultScriptEvaluator.*\");\n    }\n\n    @Test\n    public void noStacktraceForUserDefinedExceptionFromTaskParallelParallel() throws Exception {\n        runtime.deploy(\"logExceptionTests/fromParallelParallel\");\n\n        runtime.save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runtime.run();\n            fail(\"Exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        // error\n        assertLogExactMatch(runtime.lastLog(), 2, \".*\" + quote(\"(concord.yaml): Error @ line: 11, col: 11. boom!\") + \".*\");\n\n        // no stacktrace\n        assertNoLog(runtime.lastLog(), \".*\" + quote(\"com.walmartlabs.concord.svm.ParallelExecutionException\") + \".*\");\n        assertNoLog(runtime.lastLog(), \".*\" + quote(\"at com.walmartlabs.concord.runtime.v2.runner.vm.JoinCommand.execute\") + \".*\");\n    }\n\n\n    @Test\n    public void noStackTraceForArgsEvalError() throws Exception {\n        runtime.deploy(\"logExceptionTests/invalidArgs\");\n\n        runtime.save(ProcessConfiguration.builder()\n                .putArguments(\"name\", \"${undefinedVariableName}\")\n                .build());\n\n        try {\n            runtime.run();\n            fail(\"Exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        var errorMessage = \"[ERROR] Error while evaluating process arguments: while evaluating expression \" +\n                \"'${undefinedVariableName}': Can't find a variable 'undefinedVariableName'. \" +\n                \"Check if it is defined in the current scope. \" +\n                \"Details: ELResolver cannot handle a null base Object with identifier 'undefinedVariableName'\";\n\n        // error\n        assertLog(runtime.lastLog(), \".*\" + quote(errorMessage) + \".*\");\n\n        // no stacktrace\n        assertNoLog(runtime.lastLog(), \".*\" + quote(\"at com.walmartlabs.concord.runtime.v2\") + \".*\");\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/java/com/walmartlabs/concord/runtime/v2/runner/LogSegmentsTest.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.common.StateManager;\nimport com.walmartlabs.concord.runtime.common.cfg.LoggingConfiguration;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.runtime.common.logger.LogSegmentStatus;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.ReentrantTaskExample;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport java.io.IOException;\nimport java.net.URISyntaxException;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Arrays;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport static com.walmartlabs.concord.runtime.v2.runner.TestRuntimeV2.assertLog;\nimport static com.walmartlabs.concord.runtime.v2.runner.TestRuntimeV2.assertMultiLineLog;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.fail;\n\npublic class LogSegmentsTest {\n\n    @RegisterExtension\n    private static final TestRuntimeV2 runtime = new TestRuntimeV2();\n\n    private static final ThreadLocal<byte[]> logWithoutSegmentsHolder = new ThreadLocal<>();\n\n    @Test\n    public void testSystemOutRedirectInScripts() throws Exception {\n        deploy(\"systemOutRedirect\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = runWithSegments();\n\n        assertSegmentLog(log, 1, \"[INFO ] System.out in a script\");\n        assertSegmentStatusOk(log, 1);\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void throwStepShouldContainErrorDescription() throws Exception {\n        deploy(\"logSegments/throw\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runWithSegments();\n            fail(\"exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        assertSegmentLog(runtime.lastLog(), 1, \"[ERROR] (concord.yaml): Error @ line: 3, col: 7. BOOM\");\n        assertSegmentStatusError(runtime.lastLog(), 1);\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void loopStepShouldLogErrorInProperLogSegment() throws Exception {\n        deploy(\"logSegments/taskErrorWithLoop\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runWithSegments();\n            fail(\"exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        assertSegmentLog(runtime.lastLog(), 1, \"[ERROR] (concord.yaml): Error @ line: 3, col: 7. boom!\");\n        assertSegmentStatusError(runtime.lastLog(), 1);\n\n        assertSegmentLog(runtime.lastLog(), 2, \"[ERROR] (concord.yaml): Error @ line: 3, col: 7. boom!\");\n        assertSegmentStatusError(runtime.lastLog(), 2);\n\n        assertSegmentLog(runtime.lastLog(), 3, \"[ERROR] (concord.yaml): Error @ line: 3, col: 7. boom!\");\n        assertSegmentStatusError(runtime.lastLog(), 3);\n\n        assertSegmentLog(runtime.lastLog(), 4, \"[ERROR] (concord.yaml): Error @ line: 3, col: 7. boom!\");\n        assertSegmentStatusError(runtime.lastLog(), 4);\n\n        assertSegmentLog(runtime.lastLog(), 5, \"[ERROR] (concord.yaml): Error @ line: 3, col: 7. boom!\");\n        assertSegmentStatusError(runtime.lastLog(), 5);\n\n        assertSegmentLog(runtime.lastLog(), 6, \"[ERROR] (concord.yaml): Error @ line: 3, col: 7. boom!\");\n        assertSegmentStatusError(runtime.lastLog(), 6);\n    }\n\n    @Test\n    public void taskTest() throws Exception {\n        deploy(\"logSegments/task\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = runWithSegments();\n\n        assertSegmentLog(log, 1, \"[INFO ] Message\");\n        assertSegmentStatusOk(log, 1);\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void taskRetryTest() throws Exception {\n        deploy(\"logSegments/taskWithRetry\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = runWithSegments();\n\n        // first try\n        assertSegmentLog(log, 1, \"[INFO ] ConditionallyFailTask: fail\");\n        assertSegmentLog(log, 1, \"[ERROR] (concord.yaml): Error @ line: 3, col: 7. boom!\");\n        assertSegmentStatusError(log, 1);\n\n        // second\n        assertSegmentLog(log, 2, \"[INFO ] ConditionallyFailTask: ok\");\n        assertSegmentStatusOk(log, 2);\n\n        // TODO: maybe into task segment?\n        assertSystemSegment(log, \"[WARN ] Last error: boom!. Waiting for 1000ms before retry (attempt #0)\");\n\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void taskErrorWithReturn() throws Exception {\n        deploy(\"logSegments/taskErrorWithReturn\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = runWithSegments();\n\n        // first try\n        assertSegmentLog(log, 1, \"[INFO ] will fail with error\");\n        assertSegmentLog(log, 1, \"[ERROR] (concord.yaml): Error @ line: 3, col: 7. boom!\");\n        assertSegmentStatusError(log, 1);\n\n        // error block\n        assertSegmentLog(log, 2, \"[INFO ] in error block\");\n        assertSegmentStatusOk(log, 2);\n\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void taskLoopSerialTest() throws Exception {\n        deploy(\"logSegments/taskLoopSerial\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = runWithSegments();\n\n        assertSegmentLog(log, 1, \"[INFO ] test 1\");\n        assertSegmentStatusOk(log, 1);\n\n        assertSegmentLog(log, 2, \"[INFO ] test 2\");\n        assertSegmentStatusOk(log, 2);\n\n        assertSegmentLog(log, 3, \"[INFO ] test 3\");\n        assertSegmentStatusOk(log, 3);\n\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void taskLoopParallelTest() throws Exception {\n        deploy(\"logSegments/taskLoopParallel\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = runWithSegments();\n\n        assertSegmentLog(log, 1, \"[INFO ] test\");\n        assertSegmentStatusOk(log, 1);\n\n        assertSegmentLog(log, 2, \"[INFO ] test\");\n        assertSegmentStatusOk(log, 2);\n\n        assertSegmentLog(log, 3, \"[INFO ] test\");\n        assertSegmentStatusOk(log, 3);\n\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void taskLoopWithErrorTest() throws Exception {\n        deploy(\"logSegments/taskLoopWithError\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runWithSegments();\n            fail(\"exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        assertSegmentLog(runtime.lastLog(), 1, \"[INFO ] ConditionallyFailTask: ok\");\n        assertSegmentStatusOk(runtime.lastLog(), 1);\n\n        assertSegmentLog(runtime.lastLog(), 2, \"[INFO ] ConditionallyFailTask: fail\");\n        assertSegmentLog(runtime.lastLog(), 2, \"[ERROR] (concord.yaml): Error @ line: 3, col: 7. boom!\");\n        assertSegmentStatusError(runtime.lastLog(), 2);\n\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void taskOutInvalidTest() throws Exception {\n        deploy(\"logSegments/taskOutInvalid\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runWithSegments();\n            fail(\"exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        // invalid task out definition -> log into task segment\n        assertSegmentLog(runtime.lastLog(), 1, \"[INFO ] test\");\n        assertSegmentLog(runtime.lastLog(), 1, \"[ERROR] (concord.yaml): Error @ line: 3, col: 7. while evaluating expression '${undefined}': Can't find a variable 'undefined'. Check if it is defined in the current scope. Details: ELResolver cannot handle a null base Object with identifier 'undefined'\");\n        assertSegmentStatusError(runtime.lastLog(), 1);\n\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void taskLoopInvalidTest() throws Exception {\n        deploy(\"logSegments/taskLoopInvalid\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runWithSegments();\n            fail(\"exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        assertSystemSegment(runtime.lastLog(), \"[ERROR] (concord.yaml): Error @ line: 3, col: 7. while evaluating expression '${undefined}': Can't find a variable 'undefined'. Check if it is defined in the current scope. Details: ELResolver cannot handle a null base Object with identifier 'undefined'\");\n\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void taskSuspendTest() throws Exception {\n        deploy(\"logSegments/taskWithSuspend\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = runWithSegments();\n\n        assertSegmentLog(log, 1, \"[INFO ] will suspend with event: 'sleepingBeauty'\");\n        assertSegmentStatusSuspended(log, 1);\n\n        assertNoMoreSegments();\n\n        log = resumeWithSegments(\"sleepingBeauty\");\n\n        // running\n        assertSegmentStatusRunning(log, 1);\n\n        // ok\n        assertSegmentStatusOk(log, 1);\n\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void taskReentrantTest() throws Exception {\n        deploy(\"logSegments/taskWithReentrantSuspend\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = runWithSegments();\n\n        assertSegmentLog(log, 1, \"[INFO ] execute {action=boo}\");\n        assertSegmentStatusSuspended(log, 1);\n\n        assertNoMoreSegments();\n\n        log = resumeWithSegments(ReentrantTaskExample.EVENT_NAME);\n\n        assertSegmentStatusRunning(log, 1);\n        assertSegmentLogPattern(log, 1, 157, \"\\\\[INFO \\\\] RESUME: .*\");\n\n        assertSegmentStatusOk(log, 1);\n\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void taskErrorTest() throws Exception {\n        // task throws error\n        deploy(\"logSegments/taskError\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runWithSegments();\n            fail(\"exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        // task error segment\n        assertSegmentLog(runtime.lastLog(), 1, \"[INFO ] will fail with error\");\n        assertSegmentLog(runtime.lastLog(), 1, \"[ERROR] (concord.yaml): Error @ line: 3, col: 7. boom!\");\n        assertSegmentStatusError(runtime.lastLog(), 1);\n\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void taskErrorWithErrorTest() throws Exception {\n        // task with error handler throws error\n        deploy(\"logSegments/taskErrorWithError\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = runWithSegments();\n\n        // task error into task segment\n        assertSegmentLog(log, 1, \"[INFO ] will fail with error\");\n        assertSegmentLog(log, 1, \"[ERROR] (concord.yaml): Error @ line: 3, col: 7. boom!\");\n        assertSegmentStatusError(log, 1);\n\n        // error handler logs\n        assertSegmentLog(log, 2, \"[INFO ] in error block\");\n        assertSegmentStatusOk(log, 2);\n\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void taskWithInvalidName() throws Exception {\n        deploy(\"logSegments/taskWithInvalidName\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runWithSegments();\n            fail(\"exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        assertSystemSegment(runtime.lastLog(), \"[ERROR] (concord.yaml): Error @ line: 4, col: 7. while evaluating expression '${undefined}': Can't find a variable 'undefined'. Check if it is defined in the current scope. Details: ELResolver cannot handle a null base Object with identifier 'undefined'\");\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void taskUndefined() throws Exception {\n        // undefined task call\n        deploy(\"logSegments/taskUndefined\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runWithSegments();\n            fail(\"exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        // logs to the system segment\n        assertSegmentLog(runtime.lastLog(), 1, \"[ERROR] (concord.yaml): Error @ line: 3, col: 7. Task not found: 'undefinedTask'\");\n        assertSegmentStatusError(runtime.lastLog(), 1);\n\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void taskWithRetryInvalid() throws Exception {\n        // task step with invalid retry expressions\n        deploy(\"logSegments/taskWithRetryInvalid\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runWithSegments();\n            fail(\"exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        // logs to the system segment\n        assertSystemSegment(runtime.lastLog(), \"[ERROR] (concord.yaml): Error @ line: 3, col: 7. while evaluating expression '${undefined}': Can't find a variable 'undefined'. Check if it is defined in the current scope. Details: ELResolver cannot handle a null base Object with identifier 'undefined'\");\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void checkpointInvalidTest() throws Exception {\n        // checkpoint with invalid expression for checkpoint name\n        deploy(\"logSegments/checkpointInvalid\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runWithSegments();\n            fail(\"exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        // error into system segment\n        assertSystemSegment(runtime.lastLog(), \"[ERROR] (concord.yaml): Error @ line: 3, col: 7. while evaluating expression '${undefined}': Can't find a variable 'undefined'. Check if it is defined in the current scope. Details: ELResolver cannot handle a null base Object with identifier 'undefined'\");\n    }\n\n    @Test\n    public void exprWithoutNameTest() throws Exception {\n        deploy(\"logSegments/expr\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = runWithSegments();\n\n        // expr without name -> system segment\n        assertSystemSegment(log, \"[INFO ] simple\");\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void exprWithNameTest() throws Exception {\n        deploy(\"logSegments/exprWithName\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = runWithSegments();\n\n        // expr without name -> system segment\n        assertSegmentLog(log, 1, \"[INFO ] simple\");\n        assertSegmentStatusOk(log, 1);\n\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void exprInvalidTest() throws Exception {\n        deploy(\"logSegments/exprInvalid\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runWithSegments();\n            fail(\"exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        // error into system segment\n        assertSystemSegment(runtime.lastLog(), \"[ERROR] (concord.yaml): Error @ line: 3, col: 7. while evaluating expression '${undefined}': Can't find a variable 'undefined'. Check if it is defined in the current scope. Details: ELResolver cannot handle a null base Object with identifier 'undefined'\");\n    }\n\n    @Test\n    public void scriptTest() throws Exception {\n        deploy(\"logSegments/script\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = runWithSegments();\n\n        assertSegmentLog(log, 1, \"[INFO ] log message\");\n        assertSegmentStatusOk(log, 1);\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void scriptInvalidTest() throws Exception {\n        deploy(\"logSegments/scriptInvalid\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runWithSegments();\n            fail(\"exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        assertSegmentLog(runtime.lastLog(), 1, \"[ERROR] (concord.yml): Error @ line: 3, col: 7. Unknown language 'js234'. Check process dependencies.\");\n        assertSegmentStatusError(runtime.lastLog(), 1);\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void scriptInvalidBodyTest() throws Exception {\n        deploy(\"logSegments/scriptInvalidBody\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runWithSegments();\n            fail(\"exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        assertSegmentLogPattern(runtime.lastLog(), 1, null, Pattern.quote(\"[ERROR] (concord.yml): Error @ line: 3, col: 7. TypeError: invokeMember (unknownMethod) on\") + \".*\");\n        assertSegmentStatusError(runtime.lastLog(), 1);\n    }\n\n    @Test\n    public void callTest() throws Exception {\n        deploy(\"logSegments/call\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = runWithSegments();\n\n        assertSegmentLog(log, 1, \"[INFO ] in inner flow\");\n        assertSegmentStatusOk(log, 1);\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void callWithNameTest() throws Exception {\n        deploy(\"logSegments/callWithName\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = runWithSegments();\n\n        assertSegmentLog(log, 2, \"[INFO ] in inner flow\");\n        assertSegmentStatusOk(log, 2);\n\n        // segment 1 without any logs (named call step)\n        assertSegmentStatusOk(log, 1);\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void callWithNameLoopTest() throws Exception {\n        deploy(\"logSegments/callWithNameLoop\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = runWithSegments();\n\n        assertSegmentLog(log, 2, \"[INFO ] in inner flow, item: 1\");\n        assertSegmentStatusOk(log, 2);\n        // (named call step)\n        assertSegmentStatusOk(log, 1);\n\n        assertSegmentLog(log, 4, \"[INFO ] in inner flow, item: 2\");\n        assertSegmentStatusOk(log, 4);\n        // (named call step)\n        assertSegmentStatusOk(log, 3);\n\n        assertSegmentLog(log, 6, \"[INFO ] in inner flow, item: 3\");\n        assertSegmentStatusOk(log, 6);\n        // (named call step)\n        assertSegmentStatusOk(log, 5);\n\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void callWithRetryTest() throws Exception {\n        deploy(\"logSegments/callWithRetry\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = runWithSegments();\n\n        assertSegmentLog(log, 1, \"[ERROR] (concord.yaml): Error @ line: 15, col: 11. FAIL\");\n        assertSegmentMultilineLog(log, 1, \"[ERROR] Call stack:\\n\" +\n                \"(concord.yaml) @ line: 3, col: 7, thread: 0, flow: inner\");\n        assertSegmentStatusError(log, 1);\n\n        assertSystemSegment(log, \"[WARN ] Last error: FAIL. Waiting for 1000ms before retry (attempt #0)\");\n\n        assertSegmentLog(log, 2, \"[INFO ] in inner flow\");\n        assertSegmentStatusOk(log, 2);\n\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void callWithErrorTest() throws Exception {\n        deploy(\"logSegments/callWithError\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = runWithSegments();\n\n        assertSegmentLog(log, 1, \"[ERROR] (concord.yaml): Error @ line: 13, col: 11. FAIL\");\n        assertSegmentMultilineLog(log, 1, \"[ERROR] Call stack:\\n\" +\n                \"(concord.yaml) @ line: 3, col: 7, thread: 0, flow: inner\");\n        assertSegmentStatusError(log, 1);\n\n        assertSegmentLog(log, 2, \"[INFO ] in error block\");\n        assertSegmentStatusOk(log, 2);\n\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void callWithErrorThrowErrorTest() throws Exception {\n        deploy(\"logSegments/callWithErrorThrowError\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            runWithSegments();\n            fail(\"exception expected\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        assertSegmentLog(runtime.lastLog(), 1, \"[ERROR] (concord.yaml): Error @ line: 13, col: 11. FAIL\");\n        assertSegmentMultilineLog(runtime.lastLog(), 1, \"[ERROR] Call stack:\\n\" +\n                \"(concord.yaml) @ line: 3, col: 7, thread: 0, flow: inner\");\n        assertSegmentStatusError(runtime.lastLog(), 1);\n\n        assertSegmentLog(runtime.lastLog(), 2, \"[INFO ] in error block\");\n        assertSegmentStatusOk(runtime.lastLog(), 2);\n\n        assertSegmentLog(runtime.lastLog(), 3, \"[ERROR] (concord.yaml): Error @ line: 8, col: 11. FAIL\");\n        assertSegmentStatusError(runtime.lastLog(), 3);\n\n        assertNoMoreSegments();\n    }\n\n    @Test\n    public void taskSensitiveDataInName() throws Exception {\n        deploy(\"logSegments/taskWithSensitiveDataInName\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = runWithSegments();\n\n        assertSegmentName(1, \"My masked password: ******\");\n        assertSegmentLog(log, 1, \"[INFO ] Still masked: ******\");\n        assertSegmentStatusOk(log, 1);\n        assertNoMoreSegments();\n    }\n\n    private void assertSegmentName(int segmentId, String expectedName) {\n        assertEquals(expectedName, runtime.testLoggingClient().getSegmentName(segmentId));\n    }\n\n    private static void assertSystemSegment(byte[] log, String message) throws IOException {\n        assertSegmentLog(log, 0, message);\n    }\n\n    private static void assertSegmentLogPattern(byte[] log, int segmentId, Integer len, String messagePattern) throws IOException {\n        // RUNNING\n        String segmentLog;\n        if (len != null) {\n            segmentLog = Pattern.quote(\"|%d|%d|%d|\".formatted(len, segmentId, LogSegmentStatus.RUNNING.id())) + \"\\\\d+\\\\|\\\\d+\\\\|\" + \".*\" + messagePattern;\n        } else {\n            segmentLog = \"\\\\|.*\\\\|%d\\\\|%d\\\\|\".formatted(segmentId, LogSegmentStatus.RUNNING.id()) + \"\\\\d+\\\\|\\\\d+\\\\|\" + \".*\" + messagePattern;\n        }\n        assertLog(log, \".*\" + segmentLog);\n\n        byte[] logWithoutSegments = removePattern(logWithoutSegmentsHolder.get(), segmentLog + \"\\n\");\n        logWithoutSegmentsHolder.set(logWithoutSegments);\n    }\n\n    private static void assertSegmentMultilineLog(byte[] log, int segmentId, String message) throws IOException {\n        assertSegmentLog(log, segmentId, message, true);\n    }\n\n    private static void assertSegmentLog(byte[] log, int segmentId, String message) throws IOException {\n        assertSegmentLog(log, segmentId, message, false);\n    }\n\n    private static void assertSegmentLog(byte[] log, int segmentId, String message, boolean multiLine) throws IOException {\n        String messagePattern = Pattern.quote(message);\n        int len = \"2024-06-04T12:11:36.281+0000 \".length() + message.length() + \"\\n\".length();\n\n        // RUNNING\n        String segmentLog = Pattern.quote(\"|%d|%d|%d|\".formatted(len, segmentId, LogSegmentStatus.RUNNING.id())) + \"\\\\d+\\\\|\\\\d+\\\\|\" + \".*\" + messagePattern;\n        if (multiLine) {\n            assertMultiLineLog(log, \".*\" + segmentLog);\n        } else {\n            assertLog(log, \".*\" + segmentLog);\n        }\n\n        byte[] logWithoutSegments = removePattern(logWithoutSegmentsHolder.get(), segmentLog + \"\\n\");\n        logWithoutSegmentsHolder.set(logWithoutSegments);\n    }\n\n    private static void assertSegmentStatusOk(byte[] log, int segmentId) throws IOException {\n        assertSegmentStatus(log, segmentId, LogSegmentStatus.OK);\n    }\n\n    private static void assertSegmentStatusError(byte[] log, int segmentId) throws IOException {\n        assertSegmentStatus(log, segmentId, LogSegmentStatus.ERROR);\n    }\n\n    private static void assertSegmentStatusSuspended(byte[] log, int segmentId) throws IOException {\n        assertSegmentStatus(log, segmentId, LogSegmentStatus.SUSPENDED);\n    }\n\n    private static void assertSegmentStatusRunning(byte[] log, int segmentId) throws IOException {\n        assertSegmentStatus(log, segmentId, LogSegmentStatus.RUNNING);\n    }\n\n    private static void assertSegmentStatus(byte[] log, int segmentId, LogSegmentStatus status) throws IOException {\n        String statusLog = Pattern.quote(\"|%d|%d|%d|\".formatted(0, segmentId, status.id())) + \"\\\\d+\\\\|\\\\d+\\\\|\";\n        assertLog(log, \".*\" + statusLog + \".*\");\n\n        byte[] logWithoutSegments = removePattern(logWithoutSegmentsHolder.get(), statusLog);\n        logWithoutSegmentsHolder.set(logWithoutSegments);\n    }\n\n    private static void assertNoMoreSegments() {\n        byte[] logWithoutSegment = logWithoutSegmentsHolder.get();\n        assertEquals(0, logWithoutSegment.length, () -> \"Unexpected segments:\\n\" + new String(logWithoutSegment));\n    }\n\n    private byte[] runWithSegments() throws Exception {\n        RunnerConfiguration runnerCfg = RunnerConfiguration.builder()\n                .logging(LoggingConfiguration.builder()\n                        .segmentedLogs(true)\n                        .build())\n                .build();\n\n        try {\n            byte[] log = run(runnerCfg);\n            logWithoutSegmentsHolder.set(Arrays.copyOf(log, log.length));\n            return log;\n        } catch (Exception e){\n            logWithoutSegmentsHolder.set(Arrays.copyOf(runtime.lastLog(), runtime.lastLog().length));\n            throw e;\n        }\n    }\n\n    private byte[] resumeWithSegments(String eventName) throws Exception {\n        StateManager.saveResumeEvent(runtime.workDir(), eventName);\n\n        RunnerConfiguration runnerCfg = RunnerConfiguration.builder()\n                .logging(LoggingConfiguration.builder()\n                        .segmentedLogs(true)\n                        .build())\n                .build();\n\n        try {\n            byte[] log = run(runnerCfg);\n            logWithoutSegmentsHolder.set(Arrays.copyOf(log, log.length));\n            return log;\n        } catch (Exception e){\n            logWithoutSegmentsHolder.set(Arrays.copyOf(runtime.lastLog(), runtime.lastLog().length));\n            throw e;\n        }\n    }\n\n    private static byte[] removePattern(byte[] inputBytes, String str) {\n        String inputString = new String(inputBytes, StandardCharsets.UTF_8);\n\n        Matcher matcher = Pattern.compile(str).matcher(inputString);\n\n        String resultString = matcher.replaceFirst(\"\");\n\n        return resultString.getBytes(StandardCharsets.UTF_8);\n    }\n\n\n    private void deploy(String name) throws URISyntaxException, IOException {\n        runtime.deploy(name);\n    }\n\n    private void save(ProcessConfiguration cfg) {\n        runtime.save(cfg);\n    }\n\n    private byte[] run() throws Exception {\n        return runtime.run();\n    }\n\n    private byte[] run(RunnerConfiguration baseCfg) throws Exception {\n        return runtime.run(baseCfg);\n    }\n\n    private byte[] resume(String eventName, ProcessConfiguration cfg) throws Exception {\n        return runtime.resume(eventName, cfg);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/java/com/walmartlabs/concord/runtime/v2/runner/MainTest.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.forms.Form;\nimport com.walmartlabs.concord.runtime.common.cfg.LoggingConfiguration;\nimport com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.ReentrantTaskExample;\nimport com.walmartlabs.concord.runtime.v2.runner.tasks.TaskSchemaValidationException;\nimport com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.RegisterExtension;\n\nimport java.io.IOException;\nimport java.net.URISyntaxException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.*;\nimport java.util.regex.Pattern;\n\nimport static com.walmartlabs.concord.runtime.v2.runner.TestRuntimeV2.*;\nimport static java.util.regex.Pattern.quote;\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.Mockito.*;\n\npublic class MainTest  {\n\n    @RegisterExtension\n    private static final TestRuntimeV2 runtime = new TestRuntimeV2();\n\n    @Test\n    public void testVariablesAfterResume() throws Exception {\n        deploy(\"variablesAfterResume\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*workDir1: \" + runtime.workDir().toAbsolutePath() + \".*\");\n        assertLog(log, \".*workDir3: \" + runtime.workDir().toAbsolutePath() + \".*\");\n\n        List<Form> forms = runtime.formService().list();\n        assertEquals(1, forms.size());\n\n        Form myForm = forms.get(0);\n        assertEquals(\"myForm\", myForm.name());\n\n        // resume the process using the saved form\n\n        Map<String, Object> data = new HashMap<>();\n        data.put(\"fullName\", \"John Smith\");\n\n        Path newWorkDir = Files.createTempDirectory(\"test-new\");\n        PathUtils.copy(runtime.workDir(), newWorkDir);\n        runtime.setWorkDir(newWorkDir);\n\n        log = resume(myForm.eventName(), ProcessConfiguration.builder().arguments(Collections.singletonMap(\"myForm\", data)).build());\n        assertLog(log, \".*workDir4: \" + runtime.workDir().toAbsolutePath() + \".*\");\n        assertLog(log, \".*workDir2: \" + runtime.workDir().toAbsolutePath() + \".*\");\n    }\n\n    @Test\n    public void test() throws Exception {\n        deploy(\"hello\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"name\", \"Concord\")\n                .putDefaultTaskVariables(\"testDefaults\", Collections.singletonMap(\"a\", \"a-value\"))\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*Hello, Concord!.*\");\n        assertLog(log, \".*\" + Pattern.quote(\"defaultsMap:{a=a-value}\") + \".*\");\n        assertLog(log, \".*k: \\\"value\\\".*\");\n\n        verify(runtime.processStatusCallback(), times(1)).onRunning(runtime.instanceId());\n    }\n\n    @Test\n    public void testFlowNameVariable() throws Exception {\n        deploy(\"doNotTouchFlowNameVariable\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*flowName in inner flow: 'This is MY variable'.*\");\n\n        verify(runtime.processStatusCallback(), times(1)).onRunning(runtime.instanceId());\n    }\n\n    @Test\n    public void testStackTrace() throws Exception {\n        deploy(\"stackTrace\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            run();\n            fail(\"must fail\");\n        } catch (Exception e) {\n            // ignore\n        }\n        assertLog(runtime.lastLog(), \".*\" + Pattern.quote(\"(concord.yml) @ line: 9, col: 7, thread: 0, flow: flowB\") + \".*\");\n        assertLog(runtime.lastLog(), \".*\" + Pattern.quote(\"(concord.yml) @ line: 3, col: 7, thread: 0, flow: flowA\") + \".*\");\n    }\n\n    @Test\n    public void testStackTrace2() throws Exception {\n        deploy(\"stackTrace2\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            run();\n            fail(\"must fail\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        assertLog(runtime.lastLog(), \".*\" + Pattern.quote(\"(concord.yml) @ line: 10, col: 7, thread: 1, flow: flowB\") + \".*\");\n        assertLog(runtime.lastLog(), \".*\" + Pattern.quote(\"(concord.yml) @ line: 4, col: 11, thread: 1, flow: flowA\") + \".*\");\n        assertLog(runtime.lastLog(), \".*\" + Pattern.quote(\"(concord.yml) @ line: 5, col: 11, thread: 2, flow: flowB\") + \".*\");\n    }\n\n    @Test\n    public void testStackTrace3() throws Exception {\n        deploy(\"stackTrace3\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            run();\n            fail(\"must fail\");\n        } catch (Exception e) {\n            // ignore\n        }\n        assertLog(runtime.lastLog(), \".*\" + Pattern.quote(\"in flowA\") + \".*\");\n\n        String expected = \"Call stack:\\n\" +\n                \"(concord.yml) @ line: 13, col: 7, thread: 2, flow: flowB\\n\" +\n                \"(concord.yml) @ line: 3, col: 7, thread: 2, flow: flowA\";\n\n        String logString = new String(runtime.lastLog());\n        assertTrue(logString.contains(expected), \"expected log contains: \" + expected + \", actual: \" + logString);\n    }\n\n    @Test\n    public void testStackTrace4() throws Exception {\n        deploy(\"stackTrace4\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            run();\n            fail(\"must fail\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        String expected = \"Call stack:\\n\" +\n                \"(concord.yml) @ line: 8, col: 7, thread: 0, flow: flowB\\n\" +\n                \"(concord.yml) @ line: 3, col: 7, thread: 0, flow: flowA\";\n\n        String expected1 = \"Call stack:\\n\" +\n                \"(concord.yml) @ line: 16, col: 11, thread: 0, flow: flowThrow\\n\" +\n                \"(concord.yml) @ line: 8, col: 7, thread: 0, flow: flowB\\n\" +\n                \"(concord.yml) @ line: 3, col: 7, thread: 0, flow: flowA\";\n\n        String logString = new String(runtime.lastLog());\n        assertTrue(logString.contains(expected), \"expected log contains: \" + expected + \", actual: \" + logString);\n        assertTrue(logString.contains(expected1), \"expected log contains: \" + expected1 + \", actual: \" + logString);\n    }\n\n    @Test\n    public void testStackTrace5() throws Exception {\n        deploy(\"stackTrace5\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            run();\n            fail(\"must fail\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        String expected = \".*Call stack:\\n\" +\n                \"\\\\(concord.yml\\\\) @ line: 21, col: 7, thread: .*, flow: flowC\\n\" +\n                \"\\\\(concord.yml\\\\) @ line: 11, col: 11, thread: 2, flow: flowB\\n\" +\n                \"\\\\(concord.yml\\\\) @ line: 6, col: 7, thread: 0, flow: flowA\\n\" +\n                \"\\\\(concord.yml\\\\) @ line: 3, col: 7, thread: 0, flow: flow0.*\";\n        Pattern expectedPattern = Pattern.compile(expected, Pattern.MULTILINE|Pattern.DOTALL|Pattern.UNIX_LINES);\n\n        String logString = new String(runtime.lastLog());\n        assertTrue(expectedPattern.matcher(logString).matches(), \"expected log contains: \" + expected + \", actual: \" + logString);\n    }\n\n    @Test\n    public void testStackTrace6() throws Exception {\n        deploy(\"stackTrace6\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            run();\n            fail(\"must fail\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        assertNoLog(runtime.lastLog(), \".*\" + Pattern.quote(\"[ERROR] Call stack:\") + \".*\");\n    }\n\n    @Test\n    public void testStackTrace7() throws Exception {\n        deploy(\"stackTrace7\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            run();\n            fail(\"must fail\");\n        } catch (Exception e) {\n            // ignore\n        }\n        assertNoLog(runtime.lastLog(), \".*\" + Pattern.quote(\"[ERROR] Call stack:\") + \".*\");\n    }\n\n    @Test\n    public void testForm() throws Exception {\n        deploy(\"form\");\n\n        save(ProcessConfiguration.builder().build());\n\n        byte[] log = run();\n        assertLog(log, \".*Before.*\");\n\n        List<Form> forms = runtime.formService().list();\n        assertEquals(1, forms.size());\n\n        Form myForm = forms.get(0);\n        assertEquals(\"my Form\", myForm.name());\n\n        // resume the process using the saved form\n\n        Map<String, Object> data = new HashMap<>();\n        data.put(\"fullName\", \"John Smith\");\n        data.put(\"age\", 33);\n\n        log = resume(myForm.eventName(), ProcessConfiguration.builder().arguments(Collections.singletonMap(\"my Form\", data)).build());\n        assertLog(log, \".*After.*John Smith.*\");\n    }\n\n    @Test\n    public void testUnknownTask() throws Exception {\n        deploy(\"unknownTask\");\n\n        save(ProcessConfiguration.builder().build());\n\n        try {\n            run();\n            fail(\"must fail\");\n        } catch (Exception e) {\n            assertTrue(e.getMessage().contains(\"not found: 'unknown'\"));\n        }\n    }\n\n    @Test\n    public void testTaskErrorBlock() throws Exception {\n        deploy(\"faultyTask\");\n\n        save(ProcessConfiguration.builder().build());\n\n        byte[] log = run();\n        assertLog(log, \".*error occurred:.*boom!.*\");\n    }\n\n    @Test\n    public void testTaskErrorOut() throws Exception {\n        deploy(\"faultyTaskOut\");\n\n        save(ProcessConfiguration.builder().build());\n\n        byte[] log = run();\n        assertLog(log, \".*result.key: value.*\");\n    }\n\n    @Test\n    public void testTaskIgnoreErrors() throws Exception {\n        deploy(\"taskIgnoreErrors\");\n\n        save(ProcessConfiguration.builder().build());\n\n        byte[] log = run();\n        assertLog(log, \".*ok: false.*\");\n        assertLog(log, \".*error:.*boom!.*\");\n    }\n\n    @Test\n    public void testTaskIgnoreErrors2() throws Exception {\n        deploy(\"taskIgnoreErrors2\");\n\n        save(ProcessConfiguration.builder().build());\n\n        byte[] log = run();\n        assertLog(log, \".*ok: false.*\");\n        assertLog(log, \".*error:.*boom!.*\");\n    }\n\n    @Test\n    public void testFlowErrorBlock() throws Exception {\n        deploy(\"faultyFlow\");\n\n        save(ProcessConfiguration.builder().build());\n\n        byte[] log = run();\n        assertLog(log, \".*error occurred:.*BOO.*\");\n    }\n\n    @Test\n    public void testTryErrorBlock() throws Exception {\n        deploy(\"tryError\");\n\n        save(ProcessConfiguration.builder().build());\n\n        try {\n            run();\n            fail(\"should fail\");\n        } catch (Exception e) {\n            assertTrue(e.getMessage().contains(\"boom!\"));\n        }\n\n        assertLog(runtime.lastLog(), \".*error occurred:.*boom!.*\");\n    }\n\n    @Test\n    public void testCheckpoints() throws Exception {\n        deploy(\"checkpoints\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"name\", \"Concord\")\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*Hello, Concord!.*\");\n\n        verify(runtime.processStatusCallback(), times(1)).onRunning(eq(runtime.instanceId()));\n        verify(runtime.checkpointService(), times(1)).upload(any(), any(), eq(\"A\"), any());\n    }\n\n    @Test\n    public void testTaskResultPolicy() throws Exception {\n        deploy(\"taskResultPolicy\");\n\n        save(ProcessConfiguration.builder().build());\n\n        try {\n            run();\n            fail(\"exception expected\");\n        } catch (Exception e) {\n            assertEquals(\"Found forbidden tasks\", e.getMessage());\n        }\n\n        assertLog(runtime.lastLog(), \".*forbidden by the task policy.*\");\n    }\n\n    @Test\n    public void testTaskInputInterpolate() throws Exception {\n        deploy(\"taskInputInterpolate\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*Hello, \" + Pattern.quote(\"${myFavoriteExpression}\") + \"!.*\");\n    }\n\n    @Test\n    public void testIfExpression() throws Exception {\n        deploy(\"ifExpression\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"myVar\", \"1\")\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*it's clearly non-zero.*\");\n\n        verify(runtime.processStatusCallback(), times(1)).onRunning(eq(runtime.instanceId()));\n    }\n\n    @Test\n    public void testSwitchExpressionCaseFound() throws Exception {\n        deploy(\"switchExpressionFull\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"myVar\", \"red\")\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*It's red.*\");\n    }\n\n    @Test\n    public void testSwitchExpressionCaseNotFound() throws Exception {\n        deploy(\"switchExpressionFull\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"myVar\", \"red1\")\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*I don't know what it is.*\");\n    }\n\n    @Test\n    public void testSwitchExpressionDefault() throws Exception {\n        deploy(\"switchExpressionDefault\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"myVar\", \"red1\")\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*I don't know what it is.*\");\n    }\n\n    @Test\n    public void testSwitchExpressionCaseExpression() throws Exception {\n        deploy(\"switchExpressionCaseExpression\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"myVar\", \"red\")\n                .putArguments(\"aKnownValue\", \"red\")\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*Yes, I recognize this red.*\");\n    }\n\n    @Test\n    public void testSwitchExpressionCaseExpressionDefault() throws Exception {\n        deploy(\"switchExpressionCaseExpression\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"myVar\", \"boo\")\n                .putArguments(\"aKnownValue\", \"red\")\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*Nope.*\");\n    }\n\n    @Test\n    public void testScriptInline() throws Exception {\n        deploy(\"scriptInline\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*x: 1.*\");\n    }\n\n    @Test\n    public void testScriptAttached() throws Exception {\n        deploy(\"scriptAttached\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*x: 1.*\");\n    }\n\n    @Test\n    public void testScriptErrorBlock() throws Exception {\n        deploy(\"scriptError\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*\" + quote(\"(concord.yml): Error @ line: 3, col: 7. Error: this is an error\") + \".*\");\n    }\n\n    @Test\n    public void testScriptVersion() throws Exception {\n        deploy(\"scriptEsVersion\");\n\n        save(ProcessConfiguration.builder().build());\n\n        byte[] log = run();\n        assertLog(log, \".*\\\"charCountProduct\\\":9.*\");\n    }\n\n    @Test\n    public void testScriptEsVersionInvalid() throws Exception {\n        deploy(\"scriptEsVersionInvalid\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"kv\", Collections.singletonMap(\"k\", \"v\"))\n                .build());\n\n        try {\n            run();\n        } catch (Exception e) {\n            assertLog(e.toString().getBytes(), \".*unsupported.*\");\n            return;\n        }\n        throw new Exception(\"invalid esVersion should have thrown\");\n    }\n\n\n    @Test\n    public void testScriptUnboundedInputMapOk() throws Exception {\n        deploy(\"scriptUnboundedInputMapOk\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*ok.*\");\n    }\n\n    @Test\n    public void testScriptVariablesSanitize() throws Exception {\n        deploy(\"scriptVariablesSanitize\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"kv\", Collections.singletonMap(\"k\", \"v\"))\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*boom: \\\\{\\\\}.*\");\n    }\n\n    @Test\n    public void testScriptOut() throws Exception {\n        deploy(\"scriptOut\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*result.boom: \\\\{\\\\}.*\");\n        assertLog(log, \".*result.k1: value1\");\n        assertLog(log, \".*result.k2: value2\");\n    }\n\n    @Test\n    public void testScriptOutExpr() throws Exception {\n        deploy(\"scriptOutExpr\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*k1: value1\");\n    }\n\n    @Test\n    public void testNonSerializableLocal() throws Exception {\n        deploy(\"nonSerializableLocal\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*error occurred:.*Can't set a non-serializable local variable.*\");\n    }\n\n    @Test\n    public void testInitiator() throws Exception {\n        deploy(\"initiator\");\n\n        Map<String, Object> initiator = new HashMap<>();\n        initiator.put(\"username\", \"test\");\n        initiator.put(\"displayName\", \"Test User\");\n\n        save(ProcessConfiguration.builder()\n                .initiator(initiator)\n                .putArguments(\"name\", \"${initiator.displayName}\")\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*Test User.*\");\n    }\n\n    @Test\n    public void testSegmentedLogging() throws Exception {\n        deploy(\"logging\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        RunnerConfiguration runnerCfg = RunnerConfiguration.builder()\n                .logging(LoggingConfiguration.builder()\n                        .sendSystemOutAndErrToSLF4J(false)\n                        .workDirMasking(false)\n                        .build())\n                .build();\n\n        byte[] log = run(runnerCfg);\n        assertLog(log, \"^This goes directly into the stdout$\");\n        assertLog(log, \".*This is a processLog entry.*\");\n    }\n\n    @Test\n    public void testMultipleWithItems() throws Exception {\n        deploy(\"multipleWithItems\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        for (int i = 1; i < 7; i++) {\n            assertLog(log, \".*item: \" + i + \".*\");\n        }\n    }\n\n    @Test\n    public void testParallelWithItemsTask() throws Exception {\n        deploy(\"parallelWithItemsTask\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*result: \\\\[10, 20, 30\\\\].*\");\n        assertLog(log, \".*threadIds: \\\\[1, 2, 3].*\");\n    }\n\n    @Test\n    public void testParallelLoopTask() throws Exception {\n        deploy(\"parallelLoopTask\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*result: \\\\[10, 20, 30\\\\].*\");\n        assertLog(log, \".*threadIds: \\\\[1, 2, 3].*\");\n    }\n\n    @Test\n    public void testParallelWithError() throws Exception {\n        deploy(\"parallelWithError\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        Exception exception = assertThrows(Exception.class, this::run);\n        assertTrue(exception.getMessage().matches(\"(?s)Parallel execution errors:.*boom.*\\n.*boom.*\"));\n    }\n\n    @Test\n    public void testWithItemsBlock() throws Exception {\n        deploy(\"withItemsBlock\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*result: \\\\[10, 20, 30\\\\].*\");\n    }\n\n    @Test\n    public void testLoopBlock() throws Exception {\n        deploy(\"loopBlock\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*result: \\\\[10, 20, 30\\\\].*\");\n    }\n\n    @Test\n    public void testWithItemsSet() throws Exception {\n        deploy(\"withItemsSet\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLogAtLeast(log, 3, \".*empty: \\\\[\\\\].*\");\n        assertLog(log, \".*after add: \\\\[1\\\\].*\");\n        assertLog(log, \".*after add: \\\\[2\\\\].*\");\n        assertLog(log, \".*after add: \\\\[3\\\\].*\");\n    }\n\n    @Test\n    public void testLoopSet() throws Exception {\n        deploy(\"loopSet\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLogAtLeast(log, 3, \".*empty: \\\\[\\\\].*\");\n        assertLog(log, \".*after add: \\\\[1\\\\].*\");\n        assertLog(log, \".*after add: \\\\[2\\\\].*\");\n        assertLog(log, \".*after add: \\\\[3\\\\].*\");\n    }\n\n    @Test\n    public void testUnknownMethod() throws Exception {\n        deploy(\"unknownMethod\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            run();\n            fail(\"should fail\");\n        } catch (Exception e) {\n            String msg = e.getMessage();\n            assertTrue(msg.contains(\"Can't find 'sayGoodbye()' method\"), msg);\n            assertTrue(msg.contains(\"Did you mean: sayHello()\"));\n        }\n    }\n\n    @Test\n    public void testSuspend() throws Exception {\n        deploy(\"suspend\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"testValue\", \"XYZ\")\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*aaa.*\");\n\n        log = resume(\"ev1\", ProcessConfiguration.builder().build());\n        assertLog(log, \".*XYZ.*\");\n    }\n\n    @Test\n    public void testDefaultProcessVariables() throws Exception {\n        deploy(\"defaultVariables\");\n\n        save(ProcessConfiguration.builder().build());\n\n        byte[] log = run();\n        assertLog(log, \".*workDir: '.*'.*\");\n        assertLog(log, \".*processInfo: '\\\\{.*\\\\}'.*\");\n        assertLog(log, \".*projectInfo: '\\\\{\\\\}'.*\");\n\n        log = resume(\"ev1\", ProcessConfiguration.builder()\n                .putArguments(\"workDir\", \"1\")\n                .putArguments(\"processInfo\", \"2\")\n                .putArguments(\"projectInfo\", \"3\")\n                .build());\n        assertLog(log, \".*workDir: '.*'.*\");\n        assertLog(log, \".*processInfo: '\\\\{.*\\\\}'.*\");\n        assertLog(log, \".*projectInfo: '\\\\{\\\\}'.*\");\n    }\n\n    @Test\n    public void testSetVariables() throws Exception {\n        deploy(\"setVariables\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"k1\", \"XYZ\")\n                .putArguments(\"k2\", \"init\")\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*k1-value, init, k3-value.*\");\n    }\n\n    @Test\n    public void testReturn() throws Exception {\n        deploy(\"return\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*before return.*\");\n        assertNoLog(log, \".*after return.*\");\n    }\n\n    @Test\n    public void testExit() throws Exception {\n        deploy(\"exit\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*before exit.*\");\n        assertNoLog(log, \".*after exit.*\");\n    }\n\n    @Test\n    public void testCallFlow() throws Exception {\n        deploy(\"call\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"flowName\", \"myFlow\")\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*\" + Pattern.quote(\"Hello, default flow!\") + \".*\");\n        assertLog(log, \".*\" + Pattern.quote(\"Hello, myFlow flow!\") + \".*\");\n    }\n\n    @Test\n    public void testCallFlowWithErrorBlock() throws Exception {\n        deploy(\"callWithErrorBlock\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*variable has been planted.*\");\n        assertLog(log, \".*myVar should exist: world.*\");\n    }\n\n    @Test\n    public void testVariableVisibilityAfterErrorBlocks() throws Exception {\n        deploy(\"errorBlockScoping\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*Hello, world!.*\");\n    }\n\n    @Test\n    public void testCallFlowOut() throws Exception {\n        deploy(\"callOut\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*\" + quote(\"single out a=a-value\") + \".*\");\n        assertLog(log, \".*\" + quote(\"array out a=a-value, b=b-value\") + \".*\");\n        assertLog(log, \".*\" + quote(\"expression out a=a-value, xx=123, zz=10000\") + \".*\");\n        assertLog(log, \".*\" + quote(\"out after suspend: a=aaa-value\") + \".*\");\n\n        verify(runtime.checkpointService(), times(1)).upload(any(), any(), eq(\"A\"), any());\n    }\n\n    @Test\n    public void testCallOutWithItems() throws Exception {\n        deploy(\"callOutWithItems\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*\" + quote(\"single out x=[10, 20, 30]\") + \".*\");\n        assertLog(log, \".*\" + quote(\"array out: x=[10, 20, 30]\") + \".*\");\n        assertLog(log, \".*\" + quote(\"expression out: x=[10, 20, 30]\") + \".*\");\n        assertLog(log, \".*\" + quote(\"expression out: xx=[100, 200, 300]\") + \".*\");\n    }\n\n    @Test\n    public void testVarScoping() throws Exception {\n        deploy(\"varScoping\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"x\", 123)\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*1: 123.*\");\n        assertLog(log, \".*a: 123.*\");\n        assertLog(log, \".*2: false.*\");\n        assertLog(log, \".*3: 345.*\");\n        assertLog(log, \".*4: 456.*\");\n        assertLog(log, \".*c: 456.*\");\n        assertLog(log, \".*5: 567.*\");\n    }\n\n    @Test\n    public void testParallelIn() throws Exception {\n        deploy(\"parallelIn\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"x\", 123)\n                .putArguments(\"y\", 234)\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*thread A, x: 123.*\");\n        assertLog(log, \".*thread B, y: 234.*\");\n        assertLog(log, \".*thread C, x: 999.*\");\n        assertLog(log, \".*main, x: 123.*\");\n        assertLog(log, \".*main, y: 234.*\");\n    }\n\n    @Test\n    public void testParallelOut() throws Exception {\n        deploy(\"parallelOut\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*x: 123.*\");\n        assertLog(log, \".*y: 234.*\");\n    }\n\n    @Test\n    public void testSerialLoopEmptyCall() throws Exception {\n        deploy(\"serialEmptyCall\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*\" + Pattern.quote(\"outVar: [null, null]\") + \".*\");\n    }\n\n    @Test\n    public void testParallelLoopEmptyCall() throws Exception {\n        deploy(\"parallelEmptyCall\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*\" + Pattern.quote(\"outVar: [null, null]\") + \".*\");\n    }\n\n    @Test\n    public void testParallelOutExpr() throws Exception {\n        deploy(\"parallelOutExpr\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*x: 123.*\");\n        assertLog(log, \".*y: \\\\{inner=234\\\\}.*\");\n    }\n\n    @Test\n    public void testReentrant() throws Exception {\n        deploy(\"reentrantTask\");\n        save(ProcessConfiguration.builder()\n                .putArguments(\"actionName\", \"boo\")\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*execute .*action=boo.*\");\n\n        log = resume(ReentrantTaskExample.EVENT_NAME, ProcessConfiguration.builder().build());\n        assertLog(log, \".*result.ok: true.*\");\n        assertLog(log, \".*result.action: boo.*\");\n        assertLog(log, \".*result.k: v.*\");\n        assertLog(log, \".*resultAction: boo.*\");\n    }\n\n    @Test\n    public void testReentrantWithError() throws Exception {\n        deploy(\"reentrantTaskWithError\");\n        save(ProcessConfiguration.builder()\n                .putArguments(\"actionName\", \"boo\")\n                .putArguments(\"errorOnResume\", true)\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*execute .*errorOnResume=true.*\");\n\n        log = resume(ReentrantTaskExample.EVENT_NAME, ProcessConfiguration.builder().build());\n        assertLog(log, \".*error handled: java.lang.RuntimeException: Error on resume.*\");\n        assertLog(log, \".*process finished.*\");\n    }\n\n    @Test\n    public void testReentrantOutputValidation() throws Exception {\n        deploy(\"reentrantTaskSchemaValidation\");\n        save(ProcessConfiguration.builder()\n                .putArguments(\"actionName\", \"boo\")\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*execute .*action=boo.*\");\n\n        TaskSchemaValidationException e = assertThrows(TaskSchemaValidationException.class,\n                () -> resume(ReentrantTaskExample.EVENT_NAME, ProcessConfiguration.builder().build()));\n        assertEquals(\"reentrantTask\", e.getTaskName());\n        assertEquals(\"out\", e.getSection());\n        assertEquals(\"reentrantTask.schema.json\", e.getSchemaResource());\n        assertLog(runtime.lastLog(), \".*Task 'reentrantTask' out validation failed.*\");\n    }\n\n    @Test\n    public void testNestedSet() throws Exception {\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"deep\", 3);\n        args.put(\"z\", 234);\n\n        deploy(\"nestedSet\");\n        save(ProcessConfiguration.builder()\n                .putArguments(\"x\", args)\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*x: .*y=123.*\");\n        assertLog(log, \".*x: .*z=234.*\");\n        assertLog(log, \".*x: .*taskOut=42.*\");\n        assertLog(log, \".*x: .*taskOut2=165.*\");\n        assertLog(log, \".*x: .*fromArgs=234.*\");\n        assertLog(log, \".*x: .*deep=\\\\{beep=1\\\\}.*\");\n        assertLog(log, \".*a: 42.*\");\n        assertLog(log, \".*a2: 1.*\");\n    }\n\n    @Test\n    public void testSetMapVariableOverride() throws Exception {\n        deploy(\"setVariableOverride\");\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*myMap1: .*\\\\{y=2\\\\}.*\");\n        assertLog(log, \".*myMap2: .*\\\\{y=2, z=3\\\\}.*\");\n        assertLog(log, \".*myMap3: .*\\\\{z=4\\\\}.*\");\n        assertLog(log, \".*myMap4: .*\\\\{k=v\\\\}.*\");\n    }\n\n    @Test\n    public void testRetry() throws Exception {\n        deploy(\"retry\");\n\n        save(ProcessConfiguration.builder().build());\n\n        byte[] log = run();\n        assertLog(log, \".*faultyOnceTask: fail.*\");\n        assertLog(log, \".*Waiting for 1000ms.*\");\n        assertLog(log, \".*faultyOnceTask: ok.*\");\n        assertLog(log, \".*neverFailTask: ok.*\");\n    }\n\n    @Test\n    public void testRetryInput() throws Exception {\n        deploy(\"retryInput\");\n\n        save(ProcessConfiguration.builder().build());\n\n        byte[] log = run();\n        assertLogAtLeast(log, 1, \".*ConditionallyFailTask: fail.*\");\n        assertLog(log, \".*Waiting for 1000ms.*\");\n        assertLog(log, \".*ConditionallyFailTask: ok.*\");\n    }\n\n    @Test\n    public void testCheckpointExpr() throws Exception {\n        deploy(\"checkpointExpr\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"x\", 123)\n                .build());\n\n        run();\n        verify(runtime.checkpointService(), times(1)).upload(any(), any(), eq(\"test_123\"), any());\n    }\n\n    @Test\n    public void testCheckpointRestore() throws Exception {\n        deploy(\"checkpointRestore\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"aVar\", Collections.singletonMap(\"x\", 123))\n                .build());\n\n        run();\n\n        verify(runtime.checkpointService(), times(1)).upload(any(), any(), eq(\"first\"), any());\n        verify(runtime.checkpointService(), times(1)).upload(any(), any(), eq(\"second\"), any());\n\n        runtime.checkpointService().restore(\"first\", runtime.workDir());\n\n        run();\n\n        assertLogAtLeast(runtime.allLogs(), 2, \".*#3.*x=124.*\");\n        assertLogAtLeast(runtime.allLogs(), 2, \".*#3.*y=345.*\");\n\n        assertLog(runtime.allLogs(), \".*Event Name: first.*\");\n    }\n\n    @Test\n    public void testCheckpoint1_93_0Restore() throws Exception {\n        deploy(\"checkpointRestore2\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        runtime.checkpointService().put(\"first\", Paths.get(MainTest.class.getResource(\"checkpointRestore2/first_1.103.1.zip\").toURI()));\n        runtime.checkpointService().restore(\"first\", runtime.workDir());\n\n        run();\n\n        assertLog(runtime.allLogs(), \".*item: one.*\");\n        assertLog(runtime.allLogs(), \".*item: two.*\");\n    }\n\n    @Test\n    public void testNullIfExpression() throws Exception {\n        deploy(\"ifExpressionAsNull\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"myVar\", Collections.singletonMap(\"x\", 123))\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*it's null.*\");\n    }\n\n    @Test\n    public void testTaskOut() throws Exception {\n        deploy(\"taskOut\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*\" + quote(\"single out x.ok=true\") + \".*\");\n        assertLog(log, \".*\" + quote(\"single out x.k=some-value\") + \".*\");\n        assertLog(log, \".*\" + quote(\"expression out x=some-value\") + \".*\");\n    }\n\n    @Test\n    public void testTaskOutWithItems() throws Exception {\n        deploy(\"taskOutWithItems\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*\" + quote(\"single out x=[10, 20, 30]\") + \".*\");\n    }\n\n    @Test\n    public void testExprOutExpression() throws Exception {\n        deploy(\"exprOutExpr\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*result: v.*\");\n    }\n\n    @Test\n    public void testFormsParallel() throws Exception {\n        deploy(\"parallelForm\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*Before parallel.*\");\n\n        List<Form> forms = runtime.formService().list();\n        assertEquals(2, forms.size());\n\n        Form form1 = forms.stream()\n                .filter(f -> \"form1\".equals(f.name())).findFirst()\n                .orElseThrow(() -> new RuntimeException(\"form not found\"));\n\n        // resume the process using the saved form\n\n        log = resume(form1.eventName(), ProcessConfiguration.builder()\n                .arguments(Collections.singletonMap(\"form1\", Collections.singletonMap(\"firstName\", \"Vasia\")))\n                .build());\n        assertLog(log, \".*form1 in block: Vasia.*\");\n\n        // resume the process using the saved form\n\n        Form form2 = runtime.formService().list().stream()\n                .filter(f -> \"form2\".equals(f.name())).findFirst()\n                .orElseThrow(() -> new RuntimeException(\"form not found\"));\n        log = resume(form2.eventName(), ProcessConfiguration.builder()\n                .arguments(Collections.singletonMap(\"form2\", Collections.singletonMap(\"firstName\", \"Pupkin\")))\n                .build());\n        assertLog(log, \".*form2: Pupkin.*\");\n        assertLog(log, \".*form1: Vasia.*\");\n    }\n\n    @Test\n    public void testContextInjector() throws Exception {\n        deploy(\"injectorTest\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*done!.*\");\n    }\n\n    @Test\n    public void testContextInjectorWithSegmentedLogger() throws Exception {\n        deploy(\"injectorTest\");\n\n        RunnerConfiguration runnerCfg = RunnerConfiguration.builder()\n                .logging(LoggingConfiguration.builder()\n                        .build())\n                .build();\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run(runnerCfg);\n\n        assertLog(log, Pattern.quote(\"|0|1|1|0|0||70|2|0|0|0|\") + \".*done!.*\");\n        assertLog(log, Pattern.quote(\"|0|2|1|0|0|\"));\n    }\n\n    @Test\n    public void testCurrentFlowName() throws Exception {\n        deploy(\"currentFlowName\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*default: default.*\");\n        assertLog(log, \".*myFlow: myFlow.*\");\n\n        runtime.checkpointService().restore(\"first\", runtime.workDir());\n\n        run();\n\n        assertLogAtLeast(runtime.allLogs(), 2, \".*after checkpoint: default.*\");\n        assertLogAtLeast(runtime.allLogs(), 2, \".*current flow name in error block: default.*\");\n    }\n\n    @Test\n    public void testEvalAsMap() throws Exception {\n        deploy(\"evalAsMap\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*x:.*a.out3=\\\\$\\\\{a.out1}.*\");\n        assertLog(log, \".*x:.*a.out1=1.*\");\n        assertLog(log, \".*x:.*a.out2=2.*\");\n\n        assertLog(log, \".*eval: \\\\{a=\\\\{.*\");\n        assertLog(log, \".*1: 1.*\");\n        assertLog(log, \".*2: 2.*\");\n        assertLog(log, \".*3: 1.*\");\n    }\n\n    @Test\n    public void testOrDefaultFunction() throws Exception {\n        deploy(\"orDefault\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*x:.*defaultValue.*\");\n\n        // ---\n        save(ProcessConfiguration.builder()\n                .putArguments(\"x\", \"x-value\")\n                .build());\n\n        log = run();\n        assertLog(log, \".*x:.*x-value.*\");\n    }\n\n    @Test\n    public void testIsDebug() throws Exception {\n        deploy(\"isDebug\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*isDebug: false.*\");\n\n        save(ProcessConfiguration.builder()\n                .debug(true)\n                .build());\n\n        log = run();\n        assertLog(log, \".*isDebug: true.*\");\n    }\n\n    @Test\n    public void argsFromArgs() throws Exception {\n        deploy(\"argsFromArgs\");\n\n        Map<String, Object> args = new LinkedHashMap<>();\n        args.put(\"k1\", \"v1\");\n        args.put(\"k2\", \"${resultTask.get('args.k1')}\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"args\", args)\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*Hello, \" + Pattern.quote(\"{k1=v1, k2=v1}\") + \".*\");\n    }\n\n    @Test\n    public void loopItemSerialization() throws Exception {\n        deploy(\"loopSerializationError\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*name=one.*\");\n    }\n\n    @Test\n    public void testThrowExpression() throws Exception {\n        deploy(\"throwExpression\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            run();\n            fail(\"exception expected\");\n        } catch (Exception e) {\n            assertEquals(\"42 not found\", e.getMessage());\n        }\n    }\n\n    @Test\n    public void testSensitiveData() throws Exception {\n        deploy(\"sensitiveData\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*\" + Pattern.quote(\"sensitive: ******\") + \".*\");\n        assertLog(log, \".*\" + Pattern.quote(\"log value: ******\") + \".*\");\n        assertLog(log, \".*\" + Pattern.quote(\"hack: M A S K _ M E \") + \".*\");\n\n        assertLog(log, \".*\" + Pattern.quote(\"map: {nonSecretButMasked=******, secret=******}\") + \".*\");\n        assertLog(log, \".*\" + Pattern.quote(\"map: {nonSecret=non secret value, secret=******}\") + \".*\");\n        assertLog(log, \".*\" + Pattern.quote(\"map.nested: {nonSecret=non secret value, secret={top-secret=******}}\") + \".*\");\n        assertLog(log, \".*\" + Pattern.quote(\"map.path: {nonSecret=non secret value, key={top-secret=******, inner=non secret value}}\") + \".*\");\n\n        assertLog(log, \".*\" + Pattern.quote(\"plain: plain\") + \".*\");\n\n        assertLog(log, \".*\" + Pattern.quote(\"secret from map: ******\") + \".*\");\n\n        assertLog(log, \".*secret from task execute: .*\" + Pattern.quote(\"keyWithSecretValue=******\") + \".*\");\n\n        log = resume(\"ev1\", ProcessConfiguration.builder().build());\n        assertLog(log, \".*\" + Pattern.quote(\"mySecret after suspend: ******\") + \".*\");\n    }\n\n    @Test\n    public void testBse64SensitiveData() throws Exception {\n        deploy(\"base64Sensitive\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*\" + Pattern.quote(\"1. sensitive: ******\") + \".*\");\n        assertLog(log, \".*\" + Pattern.quote(\"1. also sensitive: ******\") + \".*\");\n        assertLog(log, \".*\" + Pattern.quote(\"1. non sensitive: NOT_SECRET\") + \".*\");\n\n        assertLog(log, \".*\" + Pattern.quote(\"2. base64 encode sensitive: ******\") + \".*\");\n        assertLog(log, \".*\" + Pattern.quote(\"2. base64 encode non sensitive: Tk9UX1NFQ1JFVA==\") + \".*\");\n\n        assertLog(log, \".*\" + Pattern.quote(\"3. base64 decode sensitive: ******\") + \".*\");\n        assertLog(log, \".*\" + Pattern.quote(\"3. base64 decode base64 sensitive: ******\") + \".*\");\n        assertLog(log, \".*\" + Pattern.quote(\"3. base64 decode non sensitive: NOT_SECRET\") + \".*\");\n    }\n\n    @Test\n    public void testIncVariable() throws Exception {\n        deploy(\"incVariable\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"counter\", 0)\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*counter: 1.*\");\n    }\n\n    @Test\n    public void testHasNonNullVariable() throws Exception {\n        deploy(\"hasNonNullVariable\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*true == true.*\");\n        assertLog(log, \".*false == false.*\");\n    }\n\n    @Test\n    public void testInvalidExpressionError() throws Exception {\n        deploy(\"invalidExpression\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            run();\n            fail(\"must fail\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        String logString = new String(runtime.lastLog());\n        String expected = \"[ERROR] (concord.yml): Error @ line: 9, col: 7. while parsing expression '${str.split('\\\\n')}': Encountered \\\"\\\\'\\\\\\\\n\\\" at line 1, column 13.\\n\" +\n                \"Was expecting one of:\\n\" +\n                \"    \\\"{\\\" ...\\n\" +\n                \"    <INTEGER_LITERAL> ...\\n\" +\n                \"    <FLOATING_POINT_LITERAL> ...\\n\" +\n                \"    <STRING_LITERAL> ...\\n\" +\n                \"    \\\"true\\\" ...\\n\" +\n                \"    \\\"false\\\" ...\\n\" +\n                \"    \\\"null\\\" ...\\n\" +\n                \"    \\\"(\\\" ...\\n\" +\n                \"    \\\")\\\" ...\\n\" +\n                \"    \\\"[\\\" ...\\n\" +\n                \"    \\\"!\\\" ...\\n\" +\n                \"    \\\"not\\\" ...\\n\" +\n                \"    \\\"empty\\\" ...\\n\" +\n                \"    \\\"-\\\" ...\\n\" +\n                \"    <IDENTIFIER> ...\";\n\n        assertTrue(logString.contains(expected), \"expected log contains: \" + expected + \", actual: \" + logString);\n    }\n\n    @Test\n    public void testNPEInExpression() throws Exception {\n        deploy(\"npeInExpression\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            run();\n            fail(\"must fail\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        String logString = new String(runtime.lastLog());\n        String expected = \"[ERROR] (concord.yml): Error @ line: 7, col: 7. while evaluating expression '${'a' += m.n += 'b'}': \";\n\n        assertTrue(logString.contains(expected), \"expected log contains: \" + expected + \", actual: \" + logString);\n    }\n\n    @Test\n    public void testExpressionThrowUserDefinedError() throws Exception {\n        deploy(\"faultyExpression\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            run();\n            fail(\"must fail\");\n        } catch (Exception e) {\n            // ignore\n        }\n        assertLog(runtime.lastLog(), \".*\" + Pattern.quote(\"[ERROR] (concord.yml): Error @ line: 3, col: 7. BOOM\"));\n    }\n\n    @Test\n    public void testExpressionThrowException() throws Exception {\n        deploy(\"exceptionFromExpression\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            run();\n            fail(\"must fail\");\n        } catch (Exception e) {\n            // ignore\n        }\n\n        String expected = \"[ERROR] (concord.yml): Error @ line: 3, col: 7. while evaluating expression '${faultyTask.exception('BOOM')}': BOOM\";\n        assertLog(runtime.lastLog(), \".*\" + Pattern.quote(expected));\n    }\n\n\n    @Test\n    public void testTaskThrowException() throws Exception {\n        deploy(\"faultyTask4\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            run();\n            fail(\"must fail\");\n        } catch (Exception e) {\n            // ignore\n        }\n        assertLog(runtime.lastLog(), \".*\" + Pattern.quote(\"[ERROR] (concord.yml): Error @ line: 3, col: 7. boom!\"));\n    }\n\n    @Test\n    public void testUnresolvedVarInStepName() throws Exception {\n        deploy(\"unresolvedVarInStepName\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            run();\n            fail(\"exception expected\");\n        } catch (Exception e) {\n        }\n\n        assertLog(runtime.lastLog(), \".*\" + quote(\"(concord.yml): Error @ line: 4, col: 7. while evaluating expression '${undefined}': Can't find a variable 'undefined'. Check if it is defined in the current scope. Details: ELResolver cannot handle a null base Object with identifier 'undefined'\") + \".*\");\n    }\n\n    @Test\n    public void testUnresolvedVarInLoop() throws Exception {\n        deploy(\"unresolvedVarInLoop\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            run();\n            fail(\"exception expected\");\n        } catch (Exception e) {\n        }\n\n        assertLog(runtime.lastLog(), \".*\" + quote(\"(concord.yml): Error @ line: 6, col: 7. while evaluating expression '${undefined}': Can't find a variable 'undefined'. Check if it is defined in the current scope. Details: ELResolver cannot handle a null base Object with identifier 'undefined'\") + \".*\");\n    }\n\n    @Test\n    public void testUnresolvedVarInRetry() throws Exception {\n        deploy(\"unresolvedVarInRetry\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        try {\n            run();\n            fail(\"exception expected\");\n        } catch (Exception e) {\n        }\n\n        assertLog(runtime.lastLog(), \".*\" + quote(\"(concord.yml): Error @ line: 6, col: 7. while evaluating expression '${undefined}': Can't find a variable 'undefined'. Check if it is defined in the current scope. Details: ELResolver cannot handle a null base Object with identifier 'undefined'\") + \".*\");\n    }\n\n    @Test\n    public void testArrayEvalSerialize() throws Exception {\n        deploy(\"lazyEvalMapInArgs\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*\" + Pattern.quote(\"{dev=dev-cloud1}, {prod=prod-cloud1}, {test=test-cloud1}, {perf=perf-cloud2}, {ci=perf-ci}\") + \".*\");\n    }\n\n    @Test\n    public void testEntrySetSerialization() throws Exception {\n        deploy(\"entrySetSerialization\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*myList: \\\\[k=v\\\\].*\");\n    }\n\n    @Test\n    public void testHasFlow() throws Exception {\n        deploy(\"hasFlow\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*123: false.*\");\n        assertLog(log, \".*myFlow: true.*\");\n    }\n\n    @Test\n    public void testUuidFunc() throws Exception {\n        deploy(\"uuid\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*uuid: [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}.*\");\n    }\n\n    @Test\n    public void testExitFromParallelLoop() throws Exception {\n        deploy(\"parallelLoopExit\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n\n        assertNoLog(log, \".*should not reach here.*\");\n        verify(runtime.checkpointService(), never()).upload(any(), any(), any(), any());\n    }\n\n    @Test\n    public void testExitFromSerialLoop() throws Exception {\n        deploy(\"serialLoopExit\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n\n        assertNoLog(log, \".*should not reach here.*\");\n\n        assertLog(log, \".*inner start: one.*\");\n        assertLog(log, \".*inner end: one.*\");\n        assertLog(log, \".*inner start: two.*\");\n\n        assertNoLog(log, \".*inner end: two.*\");\n        assertNoLog(log, \".*inner start: three.*\");\n        assertNoLog(log, \".*inner start: four.*\");\n    }\n\n    @Test\n    public void testStringIfExpression() throws Exception {\n        deploy(\"ifExpressionAsString\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"myVar\", Collections.singletonMap(\"str\", \"true\"))\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*it's true.*\");\n    }\n\n    @Test\n    public void testParallelLoopItemIndex() throws Exception {\n        deploy(\"parallelLoopItemIndex\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*serial: five==5.*\");\n        assertLog(log, \".*serial: four==4.*\");\n        assertLog(log, \".*serial: three==3.*\");\n        assertLog(log, \".*serial: two==2.*\");\n        assertLog(log, \".*serial: one==1.*\");\n\n        assertLog(log, \".*parallel: five==5.*\");\n        assertLog(log, \".*parallel: four==4.*\");\n        assertLog(log, \".*parallel: three==3.*\");\n        assertLog(log, \".*parallel: two==2.*\");\n        assertLog(log, \".*parallel: one==1.*\");\n    }\n\n    @Test\n    public void testDoubleValues() throws Exception {\n        deploy(\"doubleValues\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*PI1=3.14159264.*\");\n        assertLog(log, \".*PI2=3.14159264.*\");\n    }\n\n    @Test\n    public void testThreadLocals() throws Exception {\n        deploy(\"threadLocals\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*value: myValue1.*\");\n        assertLog(log, \".*value: myValue2.*\");\n        assertLog(log, \".*value: myValue3.*\");\n    }\n\n    @Test\n    public void dryRunReadyAsExpression() throws Exception {\n        deploy(\"dryRunReadyAsExpression\");\n\n        save(ProcessConfiguration.builder()\n                .dryRun(true)\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*myValue: 42.*\");\n    }\n\n    @Test\n    public void flowCallOutExpression() throws Exception {\n        deploy(\"flowCallOutExpression\");\n\n        save(ProcessConfiguration.builder()\n                .dryRun(true)\n                .build());\n\n        byte[] log = run();\n        assertLog(log, \".*\" + Pattern.quote(\"out as expression (array): abc\") + \".*\");\n        assertLog(log, \".*\" + Pattern.quote(\"out as expression (map): abc\") + \".*\");\n        assertLog(log, \".*\" + Pattern.quote(\"out as expression (array) with loop: [abc, abc]\") + \".*\");\n        assertLog(log, \".*\" + Pattern.quote(\"out as expression (map) with loop: [abc_0, abc_1]\") + \".*\");\n    }\n\n    @Test\n    public void flowCallOutExpressionCompat() throws Exception {\n        deploy(\"flowCallOutCompat\");\n\n        save(ProcessConfiguration.builder()\n                .build());\n\n        // checkpoint with state before changes\n        runtime.checkpointService().put(\"first\", Paths.get(MainTest.class.getResource(\"flowCallOutCompat/first.zip\").toURI()));\n        runtime.checkpointService().restore(\"first\", runtime.workDir());\n\n        run();\n\n        assertLog(runtime.allLogs(), \".*\" + Pattern.quote(\"out as array: abc\") + \".*\");\n        assertLog(runtime.allLogs(), \".*\" + Pattern.quote(\"out as map: abc\") + \".*\");\n        assertLog(runtime.allLogs(), \".*\" + Pattern.quote(\"out as array with loop: [abc, abc]\") + \".*\");\n        assertLog(runtime.allLogs(), \".*\" + Pattern.quote(\"out as map with loop: [abc_0, abc_1]\") + \".*\");\n    }\n\n    @Test\n    public void prefixedFunctionsInExpressions() throws Exception {\n        deploy(\"prefixedFunctions\");\n\n        save(ProcessConfiguration.builder()\n                .putArguments(\"name\", \"world\")\n                .build());\n\n        run();\n\n        assertLog(runtime.allLogs(), \".*Hi, world!.*\");\n        assertLog(runtime.allLogs(), \".*Hello, world!.*\");\n    }\n\n    @Test\n    public void sensitiveFunction() throws Exception {\n        deploy(\"sensitiveFunction\");\n\n        save(ProcessConfiguration.builder().build());\n\n        run();\n\n        assertLog(runtime.allLogs(), \".*\" + Pattern.quote(\"The '******' is masked now\") + \".*\");\n    }\n\n    private void deploy(String name) throws URISyntaxException, IOException {\n        runtime.deploy(name);\n    }\n\n    private void save(ProcessConfiguration cfg) {\n        runtime.save(cfg);\n    }\n\n    private byte[] run() throws Exception {\n        return runtime.run();\n    }\n\n    private byte[] run(RunnerConfiguration baseCfg) throws Exception {\n        return runtime.run(baseCfg);\n    }\n\n    private byte[] resume(String eventName, ProcessConfiguration cfg) throws Exception {\n        return runtime.resume(eventName, cfg);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/java/com/walmartlabs/concord/runtime/v2/runner/functions/TestFunction.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.functions;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.ELFunction;\n\nimport javax.inject.Named;\n\n@Named\npublic class TestFunction {\n\n    @ELFunction(\"testGreet\")\n    public static String regularGreet(String name) {\n        return \"Hi, \" + name + \"!\";\n    }\n\n    @ELFunction(\"testFunction:greet\")\n    public static String prefixedGreet(String name) {\n        return \"Hello, \" + name + \"!\";\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/java/com/walmartlabs/concord/runtime/v2/runner/tasks/ReentrantTaskExample.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.tasks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport com.walmartlabs.concord.runtime.v2.sdk.ReentrantTask;\nimport com.walmartlabs.concord.runtime.v2.sdk.ResumeEvent;\nimport com.walmartlabs.concord.runtime.v2.sdk.TaskResult;\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Named;\nimport java.io.Serializable;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\n\n@Named(\"reentrantTask\")\n@SuppressWarnings(\"unused\")\npublic class ReentrantTaskExample implements ReentrantTask {\n\n    private static final Logger log = LoggerFactory.getLogger(ReentrantTaskExample.class);\n\n    public static String EVENT_NAME = UUID.randomUUID().toString();\n\n    @Override\n    public TaskResult execute(Variables input) {\n        log.info(\"execute {}\", input.toMap());\n\n        HashMap<String, Serializable> payload = new HashMap<>();\n        payload.put(\"k\", \"v\");\n        payload.put(\"action\", input.assertString(\"action\"));\n        payload.put(\"errorOnResume\", input.getBoolean(\"errorOnResume\", false));\n\n        return TaskResult.reentrantSuspend(EVENT_NAME, payload);\n    }\n\n    @Override\n    public TaskResult resume(ResumeEvent event) {\n        log.info(\"RESUME: {}\", event);\n        if ((boolean) event.state().get(\"errorOnResume\")) {\n            throw new RuntimeException(\"Error on resume!\");\n        }\n\n        return TaskResult.success()\n                .values((Map) event.state());\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/java/com/walmartlabs/concord/runtime/v2/runner/tasks/Tasks.java",
    "content": "package com.walmartlabs.concord.runtime.v2.runner.tasks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.runtime.v2.runner.TestRuntimeV2;\nimport com.walmartlabs.concord.runtime.v2.sdk.*;\nimport org.immutables.value.Value;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.io.Serializable;\nimport java.util.*;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\npublic final class Tasks {\n\n    private static final Logger log = LoggerFactory.getLogger(Tasks.class);\n\n    @Named(\"testDefaults\")\n    static class TestDefaults implements Task {\n\n        private final Variables defaults;\n\n        @Inject\n        public TestDefaults(Context ctx) {\n            this.defaults = ctx.defaultVariables();\n        }\n\n        @Override\n        public TaskResult execute(Variables input) {\n            System.out.println(\"defaultsMap:\" + defaults.toMap());\n            return TaskResult.success();\n        }\n\n        @Value.Immutable\n        @Value.Style(jdkOnly = true)\n        @JsonSerialize(as = ImmutableDefaults.class)\n        @JsonDeserialize(as = ImmutableDefaults.class)\n        public interface Defaults {\n\n            String a();\n\n            @Nullable\n            String b();\n        }\n    }\n\n    @Named(\"wrapExpression\")\n    @SuppressWarnings(\"unused\")\n    static class WrapExpressionTask implements Task {\n\n        @Override\n        public TaskResult execute(Variables input) {\n            return TaskResult.success()\n                    .value(\"expression\", \"${\" + input.get(\"expression\") + \"}\");\n        }\n    }\n\n    @Named(\"testTask\")\n    @SuppressWarnings(\"unused\")\n    static class TestTask implements Task {\n\n        @Override\n        public TaskResult execute(Variables input) {\n            return TaskResult.success()\n                    .values(input.toMap());\n        }\n    }\n\n    @Named(\"resultTask\")\n    @SuppressWarnings(\"unused\")\n    public static class ResultTask implements Task {\n\n        private final Context context;\n\n        @Inject\n        public ResultTask(Context context) {\n            this.context = context;\n        }\n\n        @Override\n        public TaskResult execute(Variables input) {\n            return TaskResult.success()\n                    .value(\"result\", input.get(\"result\"));\n        }\n\n        public Object get(String path) {\n            String[] p = path.split(\"\\\\.\");\n            Map<String, Object> m = context.variables().getMap(p[0], Collections.emptyMap());\n            p = Arrays.copyOfRange(p, 1, p.length);\n            return ConfigurationUtils.get(m, p);\n        }\n    }\n\n    @Named(\"loggingExample\")\n    @SuppressWarnings(\"unused\")\n    public static class LoggingExampleTask implements Task {\n\n        private static final Logger log = LoggerFactory.getLogger(LoggingExampleTask.class);\n        private static final Logger processLog = LoggerFactory.getLogger(\"processLog\");\n\n        public void logString(String msg) {\n            System.out.println(msg);\n        }\n\n        @Override\n        public TaskResult execute(Variables input) throws Exception {\n            log.info(\"This goes into a regular log\");\n            processLog.info(\"This is a processLog entry\");\n            System.out.println(\"This goes directly into the stdout\");\n\n            ExecutorService executor = Executors.newCachedThreadPool();\n\n            for (int i = 0; i < 5; i++) {\n                final int n = i;\n                executor.submit(() -> {\n                    Logger log = LoggerFactory.getLogger(\"taskThread\" + n);\n                    log.info(\"Hey, I'm a task thread #\" + n);\n                });\n            }\n\n            executor.shutdown();\n            executor.awaitTermination(100, TimeUnit.SECONDS);\n\n            return TaskResult.success();\n        }\n    }\n\n    @Named(\"unknownMethod\")\n    @SuppressWarnings(\"unused\")\n    static class UnknownMethodTask implements Task {\n\n        public String sayHello() {\n            return \"Hello!\";\n        }\n    }\n\n    @Named(\"faultyTask\")\n    @SuppressWarnings(\"unused\")\n    public static class FaultyTask implements Task {\n\n        @Override\n        public TaskResult execute(Variables input) {\n            log.info(\"will fail with error\");\n            return TaskResult.fail(\"boom!\")\n                    .value(\"key\", \"value\");\n        }\n\n        public void fail(String msg) {\n            throw new UserDefinedException(msg);\n        }\n\n        public void exception(String msg) throws Exception {\n            throw new Exception(msg);\n        }\n    }\n\n    @Named(\"faultyTask2\")\n    @SuppressWarnings(\"unused\")\n    static class FaultyTask2 implements Task {\n\n        @Override\n        public TaskResult execute(Variables input) {\n            throw new RuntimeException(\"boom!\");\n        }\n    }\n\n    @Named(\"faultyTask3\")\n    @SuppressWarnings(\"unused\")\n    static class FaultyTask3 implements Task {\n\n        @Override\n        public TaskResult execute(Variables input) throws Exception {\n            throw new Exception(\"boom!\");\n        }\n    }\n\n    @Named(\"userDefinedExceptionTask\")\n    @SuppressWarnings(\"unused\")\n    public static class UserDefinedExceptionTask implements Task {\n\n        @Override\n        public TaskResult execute(Variables input) {\n            throw new UserDefinedException(\"boom!\");\n        }\n\n        public void exception(String msg) {\n            throw new UserDefinedException(msg);\n        }\n    }\n\n    @Named(\"faultyOnceTask\")\n    @SuppressWarnings(\"unused\")\n    static class FaultyOnceTask implements Task {\n\n        private static final Logger log = LoggerFactory.getLogger(FaultyOnceTask.class);\n\n        private static final AtomicBoolean toggle = new AtomicBoolean(false);\n\n        @Override\n        public TaskResult execute(Variables input) {\n            if (!toggle.getAndSet(true)) {\n                log.info(\"faultyOnceTask: fail\");\n                throw new RuntimeException(\"boom!\");\n            }\n\n            log.info(\"faultyOnceTask: ok\");\n            return TaskResult.success();\n        }\n    }\n\n    @Named(\"neverFailTask\")\n    @SuppressWarnings(\"unused\")\n    static class NeverFailTask implements Task {\n\n        private static final Logger log = LoggerFactory.getLogger(NeverFailTask.class);\n\n        @Override\n        public TaskResult execute(Variables input) {\n            log.info(\"neverFailTask: ok\");\n            return TaskResult.success();\n        }\n    }\n\n    @Named(\"conditionallyFailTask\")\n    @SuppressWarnings(\"unused\")\n    static class ConditionallyFailTask implements Task {\n\n        private static final Logger log = LoggerFactory.getLogger(NeverFailTask.class);\n\n        @Override\n        public TaskResult execute(Variables input) {\n            if (input.getBoolean(\"fail\", false)) {\n                log.info(\"ConditionallyFailTask: fail\");\n                return TaskResult.fail(\"boom!\");\n            }\n\n            log.info(\"ConditionallyFailTask: ok\");\n\n            return TaskResult.success();\n        }\n    }\n\n    @Named(\"simpleMethodTask\")\n    @SuppressWarnings(\"unused\")\n    public static class SimpleMethodTask implements Task {\n\n        public int getValue() {\n            return 42;\n        }\n\n        public int getDerivedValue(int value) {\n            return value + 42;\n        }\n    }\n\n    @Named(\"sensitiveTask\")\n    public static class TaskWithSensitiveData extends AbstractMap<String, String> implements Task {\n\n        @SensitiveData\n        public String getSensitive(String str) {\n            return str;\n        }\n\n        @SensitiveData\n        public Map<String, String> getSensitiveMap(String str) {\n            Map<String, String> result = new LinkedHashMap<>();\n            result.put(\"nonSecretButMasked\", \"some value\");\n            result.put(\"secret\", str);\n            return result;\n        }\n\n        @SensitiveData(keys = {\"secret\"})\n        public Map<String, String> getSensitiveMapStrict(String str) {\n            Map<String, String> result = new LinkedHashMap<>();\n            result.put(\"nonSecret\", \"non secret value\");\n            result.put(\"secret\", str);\n            return result;\n        }\n\n        @SensitiveData(keys = {\"key.top-secret\"})\n        public Map<String, Object> getSensitiveMapWithPath(String str) {\n            Map<String, Object> inner = new LinkedHashMap<>();\n            inner.put(\"top-secret\", str);\n            inner.put(\"inner\", \"non secret value\");\n\n            Map<String, Object> result = new LinkedHashMap<>();\n            result.put(\"nonSecret\", \"non secret value\");\n            result.put(\"key\", inner);\n            return result;\n        }\n\n        @SensitiveData(keys = {\"secret\"}, includeNestedValues = true)\n        public Map<String, Object> getSensitiveMapWithNested(String str) {\n            Map<String, Object> result = new LinkedHashMap<>();\n            result.put(\"nonSecret\", \"non secret value\");\n            result.put(\"secret\", Map.of(\"top-secret\", str));\n            return result;\n        }\n\n        public String getPlain(String str) {\n            return str;\n        }\n\n        @Override\n        @SensitiveData\n        public String get(Object key) {\n            return key + \"-value\";\n        }\n\n        @Override\n        public Set<Entry<String, String>> entrySet() {\n            return null;\n        }\n\n        @SensitiveData(keys = \"keyWithSecretValue\")\n        @Override\n        public TaskResult execute(Variables input) {\n            return TaskResult.success()\n                    .value(\"keyWithSecretValue\", \"topSecret!!!!\");\n        }\n    }\n\n    @Named(\"injectorTestBean\")\n    static class InjectorTestBean {\n\n        private final Context ctx;\n\n        @Inject\n        public InjectorTestBean(Context ctx) {\n            this.ctx = ctx;\n        }\n    }\n\n    @Named(\"injectorTestTask\")\n    static class InjectorTestTask implements Task {\n\n        private final Map<String, InjectorTestBean> testBeans;\n\n        @Inject\n        public InjectorTestTask(Map<String, InjectorTestBean> testBeans) {\n            this.testBeans = testBeans;\n        }\n\n        @Override\n        public TaskResult execute(Variables input) {\n            testBeans.forEach((k, v) -> v.ctx.workingDirectory());\n            return TaskResult.success()\n                    .value(\"x\", testBeans.size());\n        }\n    }\n\n    @Named(\"threadLocals\")\n    public static class ThreadLocalsTask implements Task {\n\n        private final Context ctx;\n\n        @Inject\n        public ThreadLocalsTask(Context ctx) {\n            this.ctx = ctx;\n        }\n\n        public void put(String key, Object value) {\n            ctx.execution().state().setThreadLocal(ctx.execution().currentThreadId(), key, (Serializable) value);\n        }\n\n        public Serializable get(String key) {\n            return ctx.execution().state().getThreadLocal(ctx.execution().currentThreadId(), key);\n        }\n\n        public void remove(String key) {\n            ctx.execution().state().removeThreadLocal(ctx.execution().currentThreadId(), key);\n        }\n    }\n\n    @Named(\"suspendTask\")\n    @SuppressWarnings(\"unused\")\n    static class SuspendTask implements Task {\n        @Override\n        public TaskResult execute(Variables input) {\n            log.info(\"will suspend with event: '{}'\", input.assertString(\"eventName\"));\n\n            return TaskResult.suspend(input.assertString(\"eventName\"));\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/argsFromArgs/concord.yml",
    "content": "flows:\n  default:\n    - log: \"Hello, ${args}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/base64Sensitive/concord.yaml",
    "content": "flows:\n  default:\n    - set:\n        plain: \"NOT_SECRET\"\n\n    - expr: \"${sensitiveTask.getSensitive('MASK_ME')}\" # MASK_ME is sensitive data now\n      out: mySecret\n\n    - expr: \"${sensitiveTask.getSensitive('QUxTT19TRU5TSVRJVkU=')}\"\n      out: myBase64Secret\n\n    - log: \"1. sensitive: ${mySecret}\"\n    - log: \"1. also sensitive: ${myBase64Secret}\"\n    - log: \"1. non sensitive: ${plain}\"\n\n    - log: \"2. base64 encode sensitive: ${base64.encode(mySecret)}\"\n    - log: \"2. base64 encode non sensitive: ${base64.encode(plain)}\"\n\n    - log: \"3. base64 decode sensitive: ${base64.decode(base64.encode(mySecret))}\"\n    - log: \"3. base64 decode base64 sensitive: ${base64.decode(myBase64Secret)}\"\n    - log: \"3. base64 decode non sensitive: ${base64.decode(base64.encode(plain))}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/call/concord.yml",
    "content": "flows:\n  default:\n    - log: \"Hello, default flow!\"\n    - call: ${flowName}\n\n  myFlow:\n    - log: \"Hello, myFlow flow!\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/callOut/concord.yml",
    "content": "flows:\n  default:\n    # single out\n    - call: myFlow\n      out: a\n\n    - log: \"single out a=${a}\"\n\n    # array out\n    - call: myFlow\n      out:\n        - a\n        - b\n        - c\n\n    - log: \"array out a=${a}, b=${b}\"\n\n    # expression out\n    - call: myFlow\n      out:\n        a: ${a}\n        xx: ${x.y}\n        zz: ${z * 100}\n\n    - log: \"expression out a=${a}, xx=${xx}, zz=${zz}\"\n\n    # expression on with inner call\n    - call: flowWithSuspend\n      out:\n        a: ${a}\n\n    - log: \"out after suspend: a=${a}\"\n\n  myFlow:\n    - set:\n        a: \"a-value\"\n        b: \"b-value\"\n        x:\n          y: 123\n        z: 100\n\n  flowWithSuspend:\n    - set:\n        a: \"aaa-value\"\n\n    - checkpoint: \"A\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/callOutWithItems/concord.yml",
    "content": "flows:\n  default:\n\n    # single out\n    - call: myFlow\n      out: x\n      withItems:\n        - 1\n        - 2\n        - 3\n\n    - log: \"single out x=${x}\"\n\n    # array out\n    - call: myFlow\n      in:\n        item: \"${item}\"\n      out:\n        - x\n      withItems:\n        - 1\n        - 2\n        - 3\n\n    - log: \"array out: x=${x}\"\n\n    # expression out\n    - call: myFlow\n      in:\n        item: ${item}\n      out:\n        x: ${x}\n        xx: ${x * 10}\n      withItems:\n        - 1\n        - 2\n        - 3\n\n    - log: \"expression out: x=${x}\"\n    - log: \"expression out: xx=${xx}\"\n\n  myFlow:\n    - log: \"item: ${item}\"\n    - set:\n        x: \"${item * 10}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/callWithErrorBlock/concord.yml",
    "content": "flows:\n  default:\n    - call: createMyVar\n      out:\n        - myVar\n      error:\n        - log: \"caught an error\"\n    - log: \"myVar should exist: ${myVar}\"\n\n  createMyVar:\n    - set:\n        myVar: 'world'\n    - log: \"variable has been planted\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/checkpointExpr/concord.yml",
    "content": "flows:\n  default:\n    - checkpoint: \"test_${x}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/checkpointRestore/concord.yml",
    "content": "flows:\n  default:\n    - log: \"#1 ${aVar}\" # {x=123}\n\n    - checkpoint: \"first\"\n\n    - set:\n        aVar.y: 234\n\n    - log: \"#2 ${aVar}\" # {x=123, y=234}\n\n    - checkpoint: \"second\"\n\n    - set:\n        aVar.y: 345\n\n    - expr: \"${aVar.x = aVar.x + 1}\"\n\n    - log: \"#3 ${aVar}\" # {x=123, y=345}\n\n    - if: ${hasVariable('resumeEvents')}\n      then:\n        - log: \"Event Name: ${resumeEvents[0]}\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/checkpointRestore2/concord.yml",
    "content": "flows:\n  default:\n    - log: \"#1 start\"\n\n    - checkpoint: \"first\"\n\n    - call: print\n      withItems:\n        - one\n        - two\n\n    - checkpoint: \"second\"\n\n    - log: \"#2 end\"\n\n  print:\n    - log: \"item: ${item}\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/checkpoints/concord.yml",
    "content": "flows:\n  default:\n    - checkpoint: \"A\"\n    - log: \"Hello, ${name}!\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/currentFlowName/concord.yml",
    "content": "flows:\n  default:\n    - log: \"default: ${currentFlowName()}\"\n    - call: myFlow\n\n    - checkpoint: \"first\"\n    - log: \"after checkpoint: ${currentFlowName()}\"\n\n    - task: faultyTask\n      error:\n        - log: \"current flow name in error block: ${currentFlowName()}\"\n\n  myFlow:\n    - log: \"myFlow: ${currentFlowName()}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/defaultVariables/concord.yml",
    "content": "flows:\n  default:\n    - log: \"workDir: '${workDir}'\"\n    - log: \"processInfo: '${processInfo}'\"\n    - log: \"projectInfo: '${projectInfo}'\"\n    - suspend: ev1\n    - log: \"workDir: '${workDir}'\"\n    - log: \"processInfo: '${processInfo}'\"\n    - log: \"projectInfo: '${projectInfo}'\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/doNotTouchFlowNameVariable/concord.yaml",
    "content": "flows:\n  default:\n    - set:\n        testvar1: \"test123\"\n        flowName: \"This is MY variable\"\n    - log: \"flowName: ${flowName}\"\n    - call: innerFlow\n\n  innerFlow:\n    - log: \"flowName in inner flow: '${flowName}'\"\n    - if: \"${flowName != 'This is MY variable'}\"\n      then:\n        - throw: \"Hands off my variable!!!!\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/doubleValues/concord.yaml",
    "content": "flows:\n  default:\n  - set:\n      pi1: 3.14159264\n      pi2: ${3.14159264}\n  - log: \"PI1=${pi1}\"\n  - log: \"PI2=${pi2}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/dryRunReadyAsExpression/concord.yml",
    "content": "flows:\n  default:\n    - expr: \"${simpleMethodTask.getValue()}\"\n      out: myValue\n      meta:\n        dryRunReady: ${true}\n\n    - log: \"myValue: ${myValue}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/entrySetSerialization/concord.yaml",
    "content": "flows:\n  default:\n    - set:\n        myMap:\n          k: v\n\n    - expr: ${myMap.entrySet().stream().toList()}\n      out: myList\n\n    - log: \"myList: ${myList}\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/errorBlockScoping/concord.yml",
    "content": "flows:\n  default:\n    - call: anotherFlow\n      out:\n        - myVar\n    - log: \"Hello, ${myVar}!\"\n\n  anotherFlow:\n    - try:\n        - call: createMyVar\n          out:\n            - myVar\n        - throw: kapow! kamblamo!\n      error:\n        - log: \"caught an error\"\n    - log: \"myVar should exist: ${myVar}\"\n\n  createMyVar:\n    - set:\n        myVar: 'world'"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/evalAsMap/concord.yml",
    "content": "flows:\n  default:\n    - script: js\n      body: |\n        var x = {'a.out1': 1, 'a.out2': 2, 'a.out3': '${a.out1}'};\n        context.variables().set('x', x);\n\n    - log: \"x: ${x}\"\n\n    - expr: ${evalAsMap(x)}\n      out: x\n\n    - log: \"eval: ${x}\" # eval: {a={out3=1, out1=1, out2=2}}\n\n    - log: \"1: ${x.a.out1}\"\n    - log: \"2: ${x.a.out2}\"\n    - log: \"3: ${x.a.out3}\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/exceptionFromExpression/concord.yml",
    "content": "flows:\n  default:\n    - expr: ${faultyTask.exception('BOOM')}\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/exit/concord.yml",
    "content": "flows:\n  default:\n    - log: \"before exit\"\n    - exit\n    - log: \"after exit\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/exitWithMeta/concord.yml",
    "content": "flows:\n  default:\n    - log: \"before exit\"\n    - exit\n    - log: \"after exit\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/exprOutExpr/concord.yml",
    "content": "flows:\n  default:\n    - expr: \"${ {'k': 'v'} }\"\n      out:\n        theThingIActuallyWanted: \"${result.k}\"\n\n    - log: \"result: ${theThingIActuallyWanted}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/faultyExpression/concord.yml",
    "content": "flows:\n  default:\n    - expr: ${faultyTask.fail('BOOM')}\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/faultyFlow/concord.yml",
    "content": "flows:\n  default:\n    - call: faulty\n      error:\n        - log: \"error occurred: ${lastError}\"\n\n  faulty:\n    - throw: \"BOO\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/faultyTask/concord.yml",
    "content": "flows:\n  default:\n    - task: faultyTask\n      error:\n        - log: \"error occurred: ${lastError}\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/faultyTask4/concord.yml",
    "content": "flows:\n  default:\n    - task: faultyTask3\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/faultyTaskOut/concord.yml",
    "content": "flows:\n  default:\n    - task: faultyTask\n      out: result\n      error:\n        - log: \"result.key: ${result.key}\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/flowCallOutCompat/concord.yaml",
    "content": "flows:\n  default:\n    - checkpoint: \"first\"\n    - call: testOutAsArray\n    - call: testOutAsMap\n    - call: testOutAsArrayWithLoop\n    - call: testOutAsMapWithLoop\n\n  testOutAsArray:\n    - call: myFlow\n      out:  [\"myFlowOut\", \"myFlowOut2\"]\n\n    - log: \"out as array: ${myFlowOut}\"\n\n  testOutAsMap:\n    - call: myFlow\n      out:\n        myResult: \"${myFlowOut}\"\n\n    - log: \"out as map: ${myResult}\"\n\n  testOutAsArrayWithLoop:\n    - call: myFlow\n      out: [\"myFlowOut\"]\n      loop:\n        items: [1, 2]\n\n    - log: \"out as array with loop: ${myFlowOut}\"\n\n  testOutAsMapWithLoop:\n    - call: myFlow\n      out:\n        myResult: \"${myFlowOut}_${itemIndex}\"\n      loop:\n        items: [1, 2]\n\n    - log: \"out as map with loop: ${myResult}\"\n\n  ##\n  #  out:\n  #    myFlowOut: string, mandatory\n  ##\n  myFlow:\n    - set:\n        myFlowOut: \"abc\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/flowCallOutExpression/concord.yml",
    "content": "flows:\n  default:\n    - call: testOutAsArrayExpression\n    - call: testOutAsMapExpression\n    - call: testOutAsArrayExpressionWithLoop\n    - call: testOutAsMapExpressionWithLoop\n\n  testOutAsArrayExpression:\n    - set:\n        outArray: [\"myFlowOut\", \"myFlowOut2\"]\n\n    - call: myFlow\n      out: \"${outArray}\"\n\n    - log: \"out as expression (array): ${myFlowOut}\"\n\n  testOutAsMapExpression:\n    - set:\n        outMap:\n          myResult: \"\\\\${myFlowOut}\"\n\n    - call: myFlow\n      out: \"${outMap}\"\n\n    - log: \"out as expression (map): ${myResult}\"\n\n  testOutAsArrayExpressionWithLoop:\n    - set:\n        outArray: [\"myFlowOut\"]\n\n    - call: myFlow\n      out: \"${outArray}\"\n      loop:\n        items: [1, 2]\n\n    - log: \"out as expression (array) with loop: ${myFlowOut}\"\n\n  testOutAsMapExpressionWithLoop:\n    - set:\n        outMap:\n          myResult: \"\\\\${myFlowOut}_\\\\${itemIndex}\"\n\n    - call: myFlow\n      out: \"${outMap}\"\n      loop:\n        items: [1, 2]\n\n    - log: \"out as expression (map) with loop: ${myResult}\"\n\n  ##\n  #  out:\n  #    myFlowOut: string, mandatory\n  ##\n  myFlow:\n    - set:\n        myFlowOut: \"abc\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/form/concord.yml",
    "content": "flows:\n  default:\n    - set:\n        formName: \"my Form\"\n    - log: \"Before\"\n    - form: \"${formName}\"\n    - log: \"After: ${allVariables()['my Form']}\"\n\nforms:\n  \"my Form\":\n    - fullName: { label: \"Name\", type: \"string\", pattern: \".* .*\", readonly: true, placeholder: \"Place name here\" }\n    - age: { label: \"Age\", type: \"int\", min: 21, max: 100 }\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/hasFlow/concord.yml",
    "content": "flows:\n  default:\n    - log: \"123: ${hasFlow('123')}\"\n    - log: \"myFlow: ${hasFlow('myFlow')}\"\n\n  myFlow:\n    - log: \"my\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/hasNonNullVariable/concord.yml",
    "content": "flows:\n  default:\n    - set:\n        k: v\n        k2: ${null}\n\n    - log: \"true == ${hasNonNullVariable('k')}\"\n    - log: \"false == ${hasNonNullVariable('k2')}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/hello/concord.yml",
    "content": "flows:\n  default:\n    - log: \"Hello, ${name}!\"\n    - task: testDefaults\n    - logYaml:\n        k: \"value\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/ifExpression/concord.yml",
    "content": "flows:\n  default:\n    - if: \"${myVar > 0}\"\n      then:\n        - log: \"it's clearly non-zero\"\n      else:\n        - log: \"zero or less\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/ifExpressionAsNull/concord.yml",
    "content": "flows:\n  default:\n    - if: \"${myVar.a}\"\n      then:\n        - log: \"it's not null\"\n      else:\n        - log: \"it's null\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/ifExpressionAsString/concord.yml",
    "content": "flows:\n  default:\n    - if: \"${myVar.str}\"\n      then:\n        - log: \"it's true\"\n      else:\n        - log: \"it's not true\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/incVariable/concord.yaml",
    "content": "flows:\n  default:\n    - set:\n        counter: ${counter + 1}\n\n    - log: \"counter: ${counter}\"\n\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/initiator/concord.yml",
    "content": "flows:\n  default:\n    - log: \"${name}\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/injectorTest/concord.yml",
    "content": "flows:\n  default:\n    - task: injectorTestTask\n      out: x\n    - log: done! ${x}\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/invalidExpression/concord.yml",
    "content": "flows:\n  default:\n    - set:\n        str: |\n          a\n          b\n          c\n\n    - expr: ${str.split('\\n')}\n      out: lines\n\n    - log: ${lines}\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/isDebug/concord.yml",
    "content": "flows:\n  default:\n    - log: \"isDebug: ${isDebug()}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/lazyEvalMapInArgs/concord.yaml",
    "content": "flows:\n  default:\n    - set:\n        cloud1:\n          - dev: \"dev-cloud1\"\n          - prod: \"prod-cloud1\"\n          - test: \"test-cloud1\"\n        cloud2:\n          - perf: \"perf-cloud2\"\n          - ci: \"perf-ci\"\n        allClouds: ${[cloud1, cloud2].stream().flatMap(p -> p.stream()).toList()}\n\n    - log: \"all: ${allClouds}\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logExceptionTests/failResultFromTask/concord.yaml",
    "content": "flows:\n  default:\n      - task: \"faultyTask\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logExceptionTests/fromExpression/concord.yaml",
    "content": "flows:\n  default:\n    - expr: \"${faultyTask.exception('BOOM')}\"  # throws Exception\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logExceptionTests/fromParallel/concord.yaml",
    "content": "flows:\n  default:\n    - parallel:\n        - task: \"faultyTask3\"\n        - log: \"OK\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logExceptionTests/fromParallelParallel/concord.yaml",
    "content": "flows:\n  default:\n    - call: inner\n      loop:\n        items: [1, 2]\n        mode: parallel\n        parallelism: 2\n\n  inner:\n    - parallel:\n        - task: \"userDefinedExceptionTask\"\n        - log: \"OK\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logExceptionTests/fromScript/concord.yaml",
    "content": "flows:\n  default:\n    - script: \"js\"\n      body: |\n        log.info('ready...');\n        throw new java.lang.RuntimeException(\"Something went wrong\");\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logExceptionTests/fromTask/concord.yaml",
    "content": "flows:\n  default:\n    - task: \"faultyTask2\"  # throws RuntimeException\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logExceptionTests/invalidArgs/concord.yaml",
    "content": "flows:\n  default:\n    - log: \"OK\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logExceptionTests/methodNotFound/concord.yaml",
    "content": "flows:\n  default:\n    - expr: \"${faultyTask.unknownMethod('BOOM')}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logExceptionTests/userDefinedExceptionFromExpression/concord.yaml",
    "content": "flows:\n  default:\n    - expr: \"${userDefinedExceptionTask.exception('BOOM')}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logExceptionTests/userDefinedExceptionFromTask/concord.yaml",
    "content": "flows:\n  default:\n      - task: \"userDefinedExceptionTask\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logExceptionTests/userDefinedExceptionFromTaskParallel/concord.yaml",
    "content": "flows:\n  default:\n    - parallel:\n        - task: \"userDefinedExceptionTask\"\n        - log: \"OK\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logExceptionTests/variableNotFound/concord.yaml",
    "content": "flows:\n  default:\n    - expr: \"${unknown}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/call/concord.yaml",
    "content": "flows:\n  default:\n    - call: \"inner\"\n\n  inner:\n    - log: \"in inner flow\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/callWithError/concord.yaml",
    "content": "flows:\n  default:\n    - call: \"inner\"\n      in:\n        fail: true\n      error:\n        - log: \"in error block\"\n        - return\n\n  inner:\n    - if: \"${fail}\"\n      then:\n        - throw: \"FAIL\"\n\n    - log: \"in inner flow\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/callWithErrorThrowError/concord.yaml",
    "content": "flows:\n  default:\n    - call: \"inner\"\n      in:\n        fail: true\n      error:\n        - log: \"in error block\"\n        - throw: \"${lastError}\"\n\n  inner:\n    - if: \"${fail}\"\n      then:\n        - throw: \"FAIL\"\n\n    - log: \"in inner flow\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/callWithName/concord.yaml",
    "content": "flows:\n  default:\n    - name: \"My segment\"\n      call: \"inner\"\n\n  inner:\n    - log: \"in inner flow\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/callWithNameLoop/concord.yaml",
    "content": "flows:\n  default:\n    - name: \"My segment\"\n      call: \"inner\"\n      in:\n        item: \"${item}\"\n      loop:\n        items: [1, 2, 3]\n\n  inner:\n    - log: \"in inner flow, item: ${item}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/callWithRetry/concord.yaml",
    "content": "flows:\n  default:\n    - call: \"inner\"\n      in:\n        fail: true\n      retry:\n        times: 2\n        delay: 1\n        in:\n          fail: false\n\n  inner:\n    - if: \"${fail}\"\n      then:\n        - throw: \"FAIL\"\n\n    - log: \"in inner flow\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/checkpointInvalid/concord.yaml",
    "content": "flows:\n  default:\n    - checkpoint: \"${undefined}\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/expr/concord.yaml",
    "content": "flows:\n  default:\n    - expr: \"${loggingExample.logString('simple')}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/exprInvalid/concord.yaml",
    "content": "flows:\n  default:\n    - expr: \"${undefined}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/exprWithName/concord.yaml",
    "content": "flows:\n  default:\n    - expr: \"${loggingExample.logString('simple')}\"\n      name: \"My Segment\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/script/concord.yml",
    "content": "flows:\n  default:\n    - script: js\n      body: |\n        log.info('log message');\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/scriptInvalid/concord.yml",
    "content": "flows:\n  default:\n    - script: js234\n      body: |\n        log.info('log message');\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/scriptInvalidBody/concord.yml",
    "content": "flows:\n  default:\n    - script: js\n      body: |\n        context.unknownMethod();\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/task/concord.yaml",
    "content": "flows:\n  default:\n    - log: \"Message\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/taskError/concord.yaml",
    "content": "flows:\n  default:\n    - task: \"faultyTask\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/taskErrorWithError/concord.yaml",
    "content": "flows:\n  default:\n    - task: \"faultyTask\"\n      error:\n        - log: \"in error block\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/taskErrorWithLoop/concord.yaml",
    "content": "flows:\n  default:\n    - task: \"faultyTask\"\n      loop:\n        items: [1, 2, 3, 4, 5, 6]\n        mode: parallel\n        parallelism: 6"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/taskErrorWithReturn/concord.yaml",
    "content": "flows:\n  default:\n    - task: \"faultyTask\"\n      error:\n        - log: \"in error block\"\n        - return\n        - log: \"unreachable\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/taskLoopInvalid/concord.yaml",
    "content": "flows:\n  default:\n    - task: \"log\"\n      in:\n        msg: \"test ${item}\"\n      loop:\n        items: \"${undefined}\"\n        mode: serial"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/taskLoopParallel/concord.yaml",
    "content": "flows:\n  default:\n    - task: \"log\"\n      in:\n        msg: \"test\"\n      loop:\n        items: [1, 2, 3]\n        mode: parallel"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/taskLoopSerial/concord.yaml",
    "content": "flows:\n  default:\n    - task: \"log\"\n      in:\n        msg: \"test ${item}\"\n      loop:\n        items: [1, 2, 3]\n        mode: serial"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/taskLoopWithError/concord.yaml",
    "content": "flows:\n  default:\n    - task: \"conditionallyFailTask\"\n      in:\n        fail: \"${item == 2}\"\n      loop:\n        items: [1, 2, 3]\n        mode: serial"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/taskOutInvalid/concord.yaml",
    "content": "flows:\n  default:\n    - task: \"log\"\n      in:\n        msg: \"test\"\n      out:\n        k: \"${undefined}\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/taskUndefined/concord.yaml",
    "content": "flows:\n  default:\n    - task: \"undefinedTask\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/taskWithInvalidName/concord.yaml",
    "content": "flows:\n  default:\n    - name: \"${undefined}\"\n      task: \"faultyTask\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/taskWithReentrantSuspend/concord.yaml",
    "content": "flows:\n  default:\n    - task: reentrantTask\n      in:\n        action: \"boo\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/taskWithRetry/concord.yaml",
    "content": "flows:\n  default:\n    - task: \"conditionallyFailTask\"\n      in:\n        fail: true\n      retry:\n        times: 2\n        delay: 1\n        in:\n          fail: false"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/taskWithRetryInvalid/concord.yaml",
    "content": "flows:\n  default:\n    - task: \"faultyTask\"\n      retry:\n        times: \"${undefined}\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/taskWithSensitiveDataInName/concord.yaml",
    "content": "flows:\n  default:\n    - name: \"My masked password: ${sensitiveTask.get('password')}\"\n      log: \"Still masked: ${sensitiveTask.get('password')}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/taskWithSuspend/concord.yaml",
    "content": "flows:\n  default:\n    - task: suspendTask\n      in:\n        eventName: \"sleepingBeauty\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logSegments/throw/concord.yaml",
    "content": "flows:\n  default:\n    - throw: \"BOOM\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/logging/concord.yml",
    "content": "flows:\n  default:\n    - log: \"Hello!\"\n    - task: \"loggingExample\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/loopBlock/concord.yml",
    "content": "flows:\n  default:\n    - block:\n        - set:\n            x: \"${item * 10}\"\n      out: x\n      loop:\n        items:\n          - 1\n          - 2\n          - 3\n\n    - log: \"result: ${x}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/loopSerializationError/concord.yml",
    "content": "flows:\n  default:\n    - set:\n        items:\n          - name: one\n          - name: two\n        filteredItems: ${items.stream().filter(i -> i.name.equals('one')).toList()}\n\n    - call: test\n      loop:\n        items: ${filteredItems}\n\n  test:\n    - log: ${item}"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/loopSet/concord.yml",
    "content": "flows:\n  default:\n    - set:\n        items: [1,2,3]\n\n    - call: test\n      loop:\n        items: ${items}\n\n  test:\n    - set:\n        arr: []\n    - log: \"empty: ${arr}\"\n    - ${arr.add(item)}\n    - log: \"after add: ${arr}\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/multipleWithItems/concord.yml",
    "content": "flows:\n  default:\n    - call: myFlow\n      in:\n        item: \"${item}\"\n      withItems:\n        - 1\n        - 2\n        - 3\n\n    - call: myFlow\n      in:\n        item: \"${item}\"\n      withItems:\n        - 4\n        - 5\n        - 6\n\n  myFlow:\n    - log: \"item: ${item}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/nestedSet/concord.yml",
    "content": "flows:\n  default:\n    - set:\n        a: ${x.taskOut}\n        a2: ${x.deep.beep}\n        x.y: 123\n        x.taskOut: \"${simpleMethodTask.getValue()}\"\n        x.taskOut2: \"${simpleMethodTask.getDerivedValue(x.y)}\"\n        x.fromArgs: \"${x.z}\"\n        x.deep.beep: 1\n\n    - log: \"x: ${x}\"\n    - log: \"a: ${a}\"\n    - log: \"a2: ${a2}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/nonSerializableLocal/concord.yml",
    "content": "flows:\n  default:\n    - script: js\n      body: |\n        var z = new java.util.zip.ZipEntry('123');\n        execution.variables().set('x', z)\n      error:\n        - log: \"error occurred: ${lastError}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/npeInExpression/concord.yml",
    "content": "flows:\n  default:\n    - set:\n        m:\n          k: v\n\n    - expr: ${'a' += m.n += 'b'}\n      out: str\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/orDefault/concord.yml",
    "content": "flows:\n  default:\n    - log: \"x: ${orDefault('x', 'defaultValue')}\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/parallelEmptyCall/concord.yml",
    "content": "flows:\n  default:\n    - call: EmptyFlow\n      out: outVar\n      loop:\n        items:\n          - \"one\"\n          - \"two\"\n        mode: parallel\n        parallelism: \"${2}\"\n\n    - log: \"outVar: ${outVar}\"\n\n  EmptyFlow:\n    - return\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/parallelForm/concord.yml",
    "content": "flows:\n  default:\n    - log: \"Before parallel...\"\n    - parallel:\n        - block:\n            - form: form1\n            - log: \"form1 in block: ${form1.firstName}\"\n        - form: form2\n      out:\n        - form1\n        - form2\n\n    - log: \"form1: ${form1.firstName}\"\n    - log: \"form2: ${form2.firstName}\"\n\nforms:\n  form1:\n    - firstName: { type: \"string\" }\n\n  form2:\n    - firstName: { type: \"string\" }"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/parallelIn/concord.yml",
    "content": "flows:\n  default:\n    - parallel:\n        - log: \"thread A, x: ${x}\"\n        - log: \"thread B, y: ${y}\"\n\n        - block:\n            - set:\n                x: 999\n            - log: \"thread C, x: ${x}\"\n\n    - log: \"main, x: ${x}\"\n    - log: \"main, y: ${y}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/parallelLoopExit/concord.yml",
    "content": "configuration:\n  debug: true\n  runtime: concord-v2\n\nflows:\n  default:\n    - call: \"inner\"\n      loop:\n        items:\n          - \"one\"\n          - \"two\"\n          - \"three\"\n          - \"four\"\n        mode: parallel\n\n    - log: \"should not reach here\"\n\n  inner:\n    - log: \"inner start: ${item}\"\n\n    - if: ${item == \"four\"}\n      then:\n        - expr: ${sleep.ms(1000)}\n        - checkpoint: \"${item}\"\n\n    - if: ${item == \"two\"}\n      then:\n        - exit\n\n    - if: ${item == \"three\"}\n      then:\n        - expr: ${sleep.ms(1000)}\n\n    - log: \"inner end: ${item}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/parallelLoopItemIndex/concord.yml",
    "content": "flows:\n  default:\n    # serial\n    - call: main\n      in:\n        item: \"${item}\"\n        index: \"${itemIndex}\"\n        prefix: \"serial\"\n      out: x\n      loop:\n        mode: serial\n        items: ['one', 'two', 'three', 'four', 'five']\n\n    # parallel\n    - call: main\n      in:\n        item: \"${item}\"\n        index: \"${itemIndex}\"\n        prefix: \"parallel\"\n      out: x\n      loop:\n        mode: parallel\n        items: ['one', 'two', 'three', 'four', 'five']\n        parallelism: 2\n\n  main:\n    - log: \"${prefix += ': ' += item += '==' += (itemIndex+1)}\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/parallelLoopTask/concord.yml",
    "content": "flows:\n  default:\n    - task: resultTask\n      in:\n        result: \"${item * 10}\"\n      out: x\n      loop:\n        mode: parallel\n        items:\n          - 1\n          - 2\n          - 3\n\n    - log: \"result: ${x.stream().map(v -> v.result).sorted().toList()}\"\n    - log: \"threadIds: ${x.stream().map(v -> v.threadId).sorted().toList()}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/parallelOut/concord.yml",
    "content": "flows:\n  default:\n    - parallel:\n        - set:\n            x: 123\n\n        - set:\n            y: 234\n      out:\n        - x\n        - y\n\n    - log: \"x: ${x}\"\n    - log: \"y: ${y}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/parallelOutExpr/concord.yml",
    "content": "flows:\n  default:\n    - parallel:\n        - set:\n            x:\n              inner: 123\n\n        - set:\n            y:\n              inner:\n                234\n      out:\n        x: ${x.inner}\n        y: ${y}\n\n    - log: \"x: ${x}\"\n    - log: \"y: ${y}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/parallelWithError/concord.yml",
    "content": "flows:\n  default:\n    - task: faultyTask\n      loop:\n        mode: parallel\n        parallelism: 2\n        items:\n          - \"foo\"\n          - \"bar\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/parallelWithItemsTask/concord.yml",
    "content": "flows:\n  default:\n    - task: resultTask\n      in:\n        result: \"${item * 10}\"\n      out: x\n      parallelWithItems:\n        - 1\n        - 2\n        - 3\n\n    - log: \"result: ${x.stream().map(v -> v.result).sorted().toList()}\"\n    - log: \"threadIds: ${x.stream().map(v -> v.threadId).sorted().toList()}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/prefixedFunctions/concord.yml",
    "content": "flows:\n  default:\n    - log: \"${testGreet(name)}\"\n    - log: \"${testFunction:greet(name)}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/reentrantTask/concord.yml",
    "content": "flows:\n  default:\n    - log: \"Before\"\n    - task: reentrantTask\n      in:\n        action: ${actionName}\n      out:\n        result: ${result}\n        resultAction: ${result.action}\n    - log: \"result.ok: ${result.ok}\"\n    - log: \"result.action: ${result.action}\"\n    - log: \"resultAction: ${resultAction}\"\n    - log: \"result.k: ${result.k}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/reentrantTaskSchemaValidation/concord.yml",
    "content": "configuration:\n  validation:\n    taskCalls:\n      out: fail\n\nflows:\n  default:\n    - log: \"Before\"\n    - task: reentrantTask\n      in:\n        action: ${actionName}\n      out:\n        result: ${result}\n    - log: \"result.ok: ${result.ok}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/reentrantTaskWithError/concord.yml",
    "content": "flows:\n  default:\n    - log: \"Before\"\n    - task: reentrantTask\n      in:\n        action: ${actionName}\n        errorOnResume: ${errorOnResume}\n      error:\n        - log: \"error handled: ${lastError}\"\n    - log: \"process finished\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/retry/concord.yml",
    "content": "flows:\n  default:\n    # should trigger twice\n    - task: faultyOnceTask\n      retry:\n        times: 3\n        delay: 1\n\n    # should trigger once\n    - task: neverFailTask\n      retry:\n        times: 3\n        delay: 1\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/retryInput/concord.yml",
    "content": "flows:\n  default:\n    - task: conditionallyFailTask\n      in:\n        fail: true\n      retry:\n        in:\n          fail: false\n        times: 3\n        delay: 1"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/return/concord.yml",
    "content": "flows:\n  default:\n    - log: \"before return\"\n    - return\n    - log: \"after return\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/scriptAttached/.concord.yml",
    "content": "flows:\n  default:\n    - script: \"scripts/myscript.js\"\n    - log: \"x: ${x.a}\"\n\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/scriptAttached/scripts/myscript.js",
    "content": "var x = {a: 1};\nvar HashMap = Java.type('java.util.HashMap');\nexecution.variables().set('x', new HashMap(x));"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/scriptError/concord.yml",
    "content": "flows:\n  default:\n    - script: js\n      body: |\n        throw new Error('this is an error');\n      error:\n        - log: \"error occurred: ${lastError}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/scriptEsVersion/concord.yml",
    "content": "flows:\n  default:\n    - set:\n        phrase: \"mama mia!\"\n    - script: \"index.js\"\n      in:\n        esVersion: 2022\n    - log: \"${json}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/scriptEsVersion/index.js",
    "content": "// dummy script to exercise newer ECMAScript features\nconst vars = context.variables();\nconst phrase = vars.get(\"phrase\");\n\n// lambda expressions\nconst varsByKey = [...context.variables().toMap().entrySet()].reduce(\n  (acc, it) =>\n    // splat operator\n    ({ ...acc, [it[0]]: it[1] }),\n  {}\n);\n\n// sets, string interpolation\nconst uniqueKeys = new Set(Object.keys(varsByKey));\nconsole.log(`uniqueKeys size: ${uniqueKeys.size}`)\n\n// array methods, string methods\nconst charCountProduct = Object.values(\n  `${phrase}`\n    .split(\"\")\n    .flatMap((char) => char.at(-1))\n    .reduce((acc, char) => {\n      const count = char in acc ? acc[char] : 0;\n      return { ...acc, [char]: count + 1 };\n    }, {})\n).reduce((acc, n) => acc * n, 1);\n\n// destructuring, JSON methods\nvars.set(\"json\", JSON.stringify({ charCountProduct, varsByKey }));\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/scriptEsVersionInvalid/concord.yml",
    "content": "flows:\n  default:\n    - script: js\n      in:\n        esVersion: 1985\n      body: |\n        console.log(\"foo\");\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/scriptInline/concord.yml",
    "content": "flows:\n  default:\n    - script: js\n      body: |\n        var x = {a: 1};\n        execution.variables().set('x', x);\n    - log: \"x: ${x.a}\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/scriptOut/concord.yml",
    "content": "flows:\n  default:\n    - script: js\n      body: |\n        result.set('k1', 'value1')\n              .set('k2', 'value2')\n              .set('boom', {});\n      out: result\n\n    - checkpoint: \"test\"\n\n    - log: \"result.boom: ${result.boom}\"\n    - log: \"result.k1: ${result.k1}\"\n    - log: \"result.k2: ${result.k2}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/scriptOutExpr/concord.yml",
    "content": "flows:\n  default:\n    - script: js\n      body: |\n        result.set('k1', 'value1')\n              .set('k2', 'value2')\n              .set('boom', {});\n      out:\n        k1: ${result.k1}\n\n    - checkpoint: \"test\"\n\n    - log: \"k1: ${k1}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/scriptUnboundedInputMapOk/concord.yml",
    "content": "flows:\n  default:\n    - script: js\n      in:\n        # front load many KV pairs into the input map, assert all accepted\n        foo: foo\n        bar: bar\n        baz: baz\n        esVersion: 2022\n      body: console.log(\"ok\")\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/scriptVariablesSanitize/concord.yml",
    "content": "flows:\n  default:\n    - script: js\n      body: |\n        var kv = context.variables().get(\"kv\");\n        kv.boom = {}\n    - checkpoint: \"booom\"\n    - log: \"boom: ${kv.boom}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/sensitiveData/concord.yaml",
    "content": "flows:\n  default:\n    - log: \"sensitive: ${sensitiveTask.getSensitive('BOOM')}\"\n\n    - expr: \"${sensitiveTask.getSensitive('MASK_ME')}\"\n      out: mySecret\n\n    - log: \"log value: ${mySecret}\"\n\n    - log: \"hack: ${mySecret.replaceAll('.', '$0 ')}\"\n\n    - log: \"map: ${sensitiveTask.getSensitiveMap('XXX-MAP')}\"\n    - log: \"map: ${sensitiveTask.getSensitiveMapStrict('XXX-MAP')}\"\n    - log: \"map.nested: ${sensitiveTask.getSensitiveMapWithNested('top-secret-nested-value')}\"\n    - log: \"map.path: ${sensitiveTask.getSensitiveMapWithPath('mask-this-value')}\"\n\n    - log: \"plain: ${sensitiveTask.getPlain('plain')}\"\n\n    - log: \"secret from map: ${sensitiveTask.mySecretKey}\"\n    - set:\n        fromGet: ${sensitiveTask.get('mySecretKey2')}\n    - log: \"secret from map with get method: ${fromGet}\"\n\n    - task: sensitiveTask\n      out: taskResult\n    - log: \"secret from task execute: ${taskResult}\"\n\n    - suspend: ev1\n\n    - log: \"mySecret after suspend: ${mySecret}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/sensitiveFunction/concord.yml",
    "content": "flows:\n  default:\n    - set:\n        regularValue: \"regularValue\"\n        secretValue: \"${sensitive(regularValue)}\"\n    - log: \"The 'regularValue' is masked now\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/serialEmptyCall/concord.yml",
    "content": "flows:\n  default:\n    - call: EmptyFlow\n      out: outVar\n      loop:\n        items:\n          - \"one\"\n          - \"two\"\n        mode: serial\n\n    - log: \"outVar: ${outVar}\"\n\n  EmptyFlow:\n    - return\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/serialLoopExit/concord.yml",
    "content": "configuration:\n  debug: true\n  runtime: concord-v2\n\nflows:\n  default:\n    - call: \"inner\"\n      loop:\n        items:\n          - \"one\"\n          - \"two\"\n          - \"three\"\n          - \"four\"\n\n    - log: \"should not reach here\"\n\n  inner:\n    - log: \"inner start: ${item}\"\n\n    - if: ${item == \"four\"}\n      then:\n        - checkpoint: \"${item}\"\n\n    - if: ${item == \"two\"}\n      then:\n        - exit\n\n    - if: ${item == \"three\"}\n      then:\n        - expr: ${sleep.ms(1000)}\n\n    - log: \"inner end: ${item}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/setVariableOverride/concord.yaml",
    "content": "flows:\n  default:\n    - set:\n        myMap:\n          x: 1\n\n    - set:\n        myMap:\n          y: 2\n\n    - log: \"myMap1: ${myMap}\" # _must_ print \"{y=2}\"\n\n    - set:\n        myMap.z: 3\n\n    - log: \"myMap2: ${myMap}\" # _must_ print \"{y=2, z=3}\"\n\n    - set:\n        myMap:\n          z: 4\n\n    - log: \"myMap3: ${myMap}\" # _must_ print \"{z=4}\"\n\n    - call: inner\n      in:\n        myMap: \"${myMap}\"\n\n  ##\n  #  in:\n  #    myMap: object, mandatory, my map\n  ##\n  inner:\n    - set:\n        myMap:\n          k: \"v\"\n\n    - log: \"myMap4: ${myMap}\" # _must_ print \"{k=v}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/setVariables/concord.yml",
    "content": "flows:\n  default:\n    - set:\n        k1: \"k1-value\"\n        k3: \"k3-value\"\n    - log: \"${k1}, ${k2}, ${k3}\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/stackTrace/concord.yml",
    "content": "flows:\n  default:\n    - call: flowA\n    - log: \"end\"\n\n  flowA:\n    - log: \"in flowA\"\n\n    - call: \"flowB\"\n\n  flowB:\n    - log: \"in flowB\"\n    - call: flowC\n    - throw: \"boom\"\n    - log: \"end flow C\"\n\n  flowC:\n    - log: \"in flowC\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/stackTrace2/concord.yml",
    "content": "flows:\n  default:\n    - parallel:\n        - call: \"flowA\"\n        - call: \"flowB\"\n\n  flowA:\n    - log: \"in flowA\"\n\n    - call: \"flowB\"\n\n  flowB:\n    - log: \"in flowB\"\n    - call: flowC\n    - throw: \"boom\"\n    - log: \"end flow C\"\n\n  flowC:\n    - log: \"in flowC\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/stackTrace3/concord.yml",
    "content": "flows:\n  default:\n    - call: \"${item}\"\n      loop:\n        items:\n          - \"flowC\"\n          - \"flowA\"\n        mode: parallel\n\n  flowA:\n    - log: \"in flowA\"\n\n    - call: \"flowB\"\n\n  flowB:\n    - log: \"in flowB\"\n    - call: flowC\n    - throw: \"boom\"\n    - log: \"end flow C\"\n\n  flowC:\n    - log: \"in flowC\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/stackTrace4/concord.yml",
    "content": "flows:\n  default:\n    - call: flowA\n\n  flowA:\n    - log: \"in flowA\"\n\n    - call: \"flowB\"\n\n  flowB:\n    - log: \"in flowB\"\n    - call: flowC\n    - try:\n      - throw: \"boom\"\n      error:\n        - call: flowThrow\n        - log: \"error\"\n    - log: \"end flow B\"\n\n  flowC:\n    - log: \"in flowC\"\n\n  flowThrow:\n    - throw: \"boom\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/stackTrace5/concord.yml",
    "content": "flows:\n  default:\n    - call: flow0\n\n  flow0:\n    - call: flowA\n\n  flowA:\n    - log: \"in A\"\n    - try:\n        - call: flowB\n          in:\n            flowBInput: ${item}\n      loop:\n        items:\n          - first\n          - second\n        mode: parallel\n\n  flowB:\n    - call: flowC\n      in:\n        sa: ${flowBInput}-${item}\n      loop:\n        items:\n          - first\n          - second\n        mode: parallel\n\n  flowC:\n    - log: \"in C\"\n    - log: ${item}\n    - if: ${sa == 'second-second'}\n      then:\n        - throw: \"BOOM\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/stackTrace6/concord.yml",
    "content": "flows:\n  default:\n    - call: \"flowA\"\n    - throw: \"qweq\"\n\n  flowA:\n    - try:\n      - log: \"flowA\"\n      error:\n        - log: \"ignore\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/stackTrace7/concord.yml",
    "content": "flows:\n  default:\n    - call: \"flowA\"\n    - throw: \"qweq\"\n\n  flowA:\n    - return"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/suspend/concord.yml",
    "content": "flows:\n  default:\n    - ${log.info(\"aaaa\")}\n    - suspend: ev1\n    - ${log.info(\"bbbb\")}\n    - ${log.info(testValue)}\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/switchExpressionCaseExpression/concord.yml",
    "content": "flows:\n  default:\n    - switch: ${myVar}\n      ${aKnownValue}:\n        - log: \"Yes, I recognize this ${aKnownValue}\"\n      default:\n        - log: \"Nope\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/switchExpressionDefault/concord.yml",
    "content": "flows:\n  default:\n    - switch: \"${myVar}\"\n      default:\n        - log: \"I don't know what it is\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/switchExpressionFull/concord.yml",
    "content": "flows:\n  default:\n    - switch: \"${myVar}\"\n      red:\n        - log: \"It's red!\"\n      green:\n        - log: \"It's definitely green\"\n      default:\n        - log: \"I don't know what it is\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/systemOutRedirect/concord.yml",
    "content": "flows:\n  default:\n    - script: groovy\n      body: |\n        System.out.println(\"System.out in a script\")\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/taskIgnoreErrors/concord.yml",
    "content": "flows:\n  default:\n    - task: faultyTask\n      ignoreErrors: true\n      out: result\n\n    - log: \"ok: ${result.ok}\"\n    - log: \"error: ${result.error}\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/taskIgnoreErrors2/concord.yml",
    "content": "flows:\n  default:\n    - task: faultyTask2\n      ignoreErrors: true\n      out: result\n\n    - log: \"ok: ${result.ok}\"\n    - log: \"error: ${result.error}\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/taskInputInterpolate/concord.yml",
    "content": "flows:\n  default:\n    - task: wrapExpression\n      in:\n        expression: \"myFavoriteExpression\"\n      out: name\n\n    - task: log\n      in:\n        msg: \"Hello, ${name.expression}!\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/taskOut/concord.yml",
    "content": "flows:\n  default:\n    # single out\n    - task: testTask\n      in:\n        k: \"some-value\"\n      out: x\n\n    - log: \"single out x.ok=${x.ok}\"\n    - log: \"single out x.k=${x.k}\"\n\n    # expression out\n    - task: testTask\n      in:\n        k: \"some-value\"\n      out:\n        x: \"${result.ok ? result.k : 'oops'}\"\n\n    - log: \"expression out x=${x}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/taskOutWithItems/concord.yml",
    "content": "flows:\n  default:\n    # single out\n    - task: resultTask\n      in:\n        result: \"${item * 10}\"\n      out: x\n      withItems:\n        - 1\n        - 2\n        - 3\n\n    - log: \"single out x=${x.stream().map(v -> v.result).toList()}\"\n\n    # expression out\n    - task: resultTask\n      in:\n        result: \"${item * 10}\"\n      out:\n        x: \"${result.ok ? result.result : 'oops'}\"\n        y: \"boom\"\n      withItems: [1, 2, 3]\n\n    - log: \"expression out x=${x}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/taskResultPolicy/.concord/policy.json",
    "content": "{\n  \"task\": {\n    \"deny\": [\n      {\n        \"method\": \"execute\",\n        \"taskResults\": [\n          {\n            \"task\": \"testTask\",\n            \"result\": \"k\",\n            \"values\": [\n              \"v\"\n            ]\n          }\n        ],\n        \"name\": \"log.*\"\n      }\n    ],\n    \"warn\": [\n    ],\n    \"allow\": [\n    ]\n  }\n}\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/taskResultPolicy/concord.yml",
    "content": "flows:\n  default:\n    - task: testTask\n      in:\n        k: v\n    - log: \"Hello!\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/tasks/reentrantTask.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"out\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"ok\": {\n        \"type\": \"boolean\"\n      },\n      \"resumeOnly\": {\n        \"type\": \"string\"\n      }\n    },\n    \"required\": [\"ok\", \"resumeOnly\"]\n  }\n}\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/threadLocals/concord.yml",
    "content": "flows:\n  default:\n    - block:\n      - expr: \"${threadLocals.put('myKey', 'myValue' += item)}\"\n      - log: \"value: ${threadLocals.get('myKey')}\"\n      - expr: \"${threadLocals.remove('myKey')}\"\n      loop:\n        items: [1, 2, 3]\n        mode: parallel\n        parallelism: 3\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/throwExpression/concord.yml",
    "content": "flows:\n  default:\n    - set:\n        items: [1, 2, 3]\n        filteredItems: ${items.stream().filter(i -> i == 42).findFirst().orElseGet(() -> throw('42 not found'))}\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/tryError/concord.yml",
    "content": "flows:\n  default:\n    - try:\n        - task: faultyTask\n      error:\n        - call: handleError\n          in:\n            msg: \"error occurred: ${lastError}\"\n        - throw: \"${lastError}\"\n\n  handleError:\n    - log: \"${msg}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/unknownMethod/concord.yml",
    "content": "flows:\n  default:\n    - log: \"${unknownMethod.sayGoodbye()}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/unknownTask/concord.yml",
    "content": "flows:\n  default:\n    - task: unknown"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/unresolvedVarInLoop/concord.yml",
    "content": "flows:\n  default:\n    - call: test\n\n  test:\n    - call: logFlow\n      loop:\n        items: ${undefined}\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/unresolvedVarInRetry/concord.yml",
    "content": "flows:\n  default:\n    - call: test\n\n  test:\n    - call: logFlow\n      retry:\n        times: ${undefined}\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/unresolvedVarInStepName/concord.yml",
    "content": "flows:\n  default:\n    - name: \"${undefined}\"\n      task: log\n      in:\n        msg: \"test\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/uuid/concord.yml",
    "content": "flows:\n  default:\n    - log: \"uuid: ${uuid()}\""
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/varScoping/concord.yml",
    "content": "flows:\n  default:\n    - log: \"1: ${x}\"\n    - call: flowA\n    - log: \"2: ${hasVariable('y')}\"\n    - call: flowB\n      out: y\n    - log: \"3: ${y}\"\n    - set:\n        z: 456\n    - log: \"4: ${z}\"\n    - call: flowC\n      out: z\n    - log: \"5: ${z}\"\n\n  flowA:\n    - log: \"a: ${x}\"\n    - set:\n        y: 234\n\n  flowB:\n    - set:\n        y: 345\n\n  flowC:\n    - log: \"c: ${z}\"\n    - call: flowD\n      out: z\n\n  flowD:\n    - set:\n        z: 567\n\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/variablesAfterResume/concord.yaml",
    "content": "configuration:\n  runtime: concord-v2\n\nflows:\n  default:\n    - log: \"workDir1: ${workDir}\"\n\n    - call: inner\n\n    - log: \"workDir2: ${workDir}\"\n\n  inner:\n    - log: \"workDir3: ${workDir}\"\n    - form: myForm\n    - log: \"workDir4: ${workDir}\"\n\nforms:\n  myForm:\n    - fullName: { label: \"Name\", type: \"string\", placeholder: \"Place name here\" }\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/withItemsBlock/concord.yml",
    "content": "flows:\n  default:\n    - block:\n        - set:\n            x: \"${item * 10}\"\n      out: x\n      withItems:\n        - 1\n        - 2\n        - 3\n\n    - log: \"result: ${x}\"\n"
  },
  {
    "path": "runtime/v2/runner-test/src/test/resources/com/walmartlabs/concord/runtime/v2/runner/withItemsSet/concord.yml",
    "content": "flows:\n  default:\n    - set:\n        items: [1,2,3]\n\n    - call: test\n      withItems: ${items}\n\n  test:\n    - set:\n        arr: []\n    - log: \"empty: ${arr}\"\n    - ${arr.add(item)}\n    - log: \"after add: ${arr}\""
  },
  {
    "path": "runtime/v2/sdk/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-runtime-sdk-v2</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-model-v2</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-vm-v2</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.errorprone</groupId>\n            <artifactId>error_prone_annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-javadoc-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/AllowNulls.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Target;\n\n@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.TYPE_USE})\npublic @interface AllowNulls {}"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/ApiConfiguration.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n/**\n * Provides basic configuration details of Concord API.\n */\npublic interface ApiConfiguration {\n\n    /**\n     * @return the base URL of the API, e.g. https://concord.example.com/\n     */\n    String baseUrl();\n\n    /**\n     * @return connection timeout (ms)\n     */\n    int connectTimeout();\n\n    /**\n     * @return socket read timeout (ms)\n     */\n    int readTimeout();\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/Compiler.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.svm.Command;\n\npublic interface Compiler {\n\n    Command compile(ProcessDefinition processDefinition, Step step);\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/Constants.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic final class Constants {\n\n    public static final class Runtime {\n\n        /**\n         * A frame-local variable which contains the current \"retry\" attempt number.\n         * Zero indicates the first attempt to execute a command.\n         * Not defined outside of \"retry\" blocks.\n         */\n        public static final String RETRY_ATTEMPT_NUMBER = \"__retry_attemptNo\";\n\n        private Runtime() {\n        }\n    }\n\n    private Constants() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/Context.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\nimport java.nio.file.Path;\nimport java.util.Map;\nimport java.util.UUID;\n\n/**\n * Provides access to the current call's environment.\n * Can be injected into task classes using {@link javax.inject.Inject} annotation.\n */\npublic interface Context {\n\n    /**\n     * @return absolute path to the working directory of the current process.\n     */\n    Path workingDirectory();\n\n    /**\n     * @return the current process ID.\n     */\n    UUID processInstanceId();\n\n    // TODO parentInstanceId?\n\n    /**\n     * Returns all variables declared before the current call.\n     *\n     * @return current variables.\n     */\n    Variables variables();\n\n    /**\n     * TODO\n     *\n     * @return default task parameters.\n     */\n    Variables defaultVariables();\n\n    /**\n     * Provides access to the filesystem utilities.\n     *\n     * @return {@link FileService}\n     */\n    FileService fileService();\n\n    /**\n     * Allows running Docker containers in Concord processes.\n     *\n     * @return {@link DockerService}\n     */\n    DockerService dockerService();\n\n    /**\n     * Provides access to Concord secrets.\n     *\n     * @return {@link SecretService}\n     */\n    SecretService secretService();\n\n    /**\n     * Project-level locking.\n     *\n     * @return {@link LockService}\n     */\n    LockService lockService();\n\n    /**\n     * @return configuration parameters for accessing Concord API.\n     */\n    ApiConfiguration apiConfiguration();\n\n    /**\n     * @return the current process' configuration.\n     */\n    ProcessConfiguration processConfiguration();\n\n    /**\n     * Provides access to the low-level details of the current process.\n     * <p>\n     * Unstable API, subject to change.\n     *\n     * @return {@link Execution}\n     */\n    Execution execution();\n\n    /**\n     * Provides low-level access to the DSL compiler.\n     * <p>\n     * Unstable API, subject to change.\n     *\n     * @return {@link Compiler}\n     */\n    Compiler compiler();\n\n    // TODO add \"evaluate\" method as well?\n\n    /**\n     * \"Evaluates\" the specified value, resolving all variables.\n     * All expressions are evaluated and replaced with resulting values.\n     * Accepts strings (including expressions), lists, sets, arrays and maps.\n     *\n     * @param <T>  the expected type of the result.\n     * @param v    the expression or an object containing expressions (lists, maps, etc).\n     * @param type the expected type of the result.\n     * @return the result of evaluation of the specified type.\n     */\n    <T> T eval(Object v, Class<T> type);\n\n    /**\n     * Same as {@link #eval(Object, Class)}, but allows providing additional variables.\n     *\n     * @param <T>                 the expected type of the result.\n     * @param v                   the expression or an object containing expressions\n     *                            (lists, maps, etc).\n     * @param additionalVariables a {@link Map} of additional variables that will be\n     *                            made available during evaluation.\n     * @param type                the expected type of the result.\n     * @return the result of evaluation of the specified type.\n     */\n    <T> T eval(Object v, Map<String, Object> additionalVariables, Class<T> type);\n\n    /**\n     * Suspends the current execution thread.\n     * After the calling this method, the process will be stopped after\n     * the current command's execution is complete.\n     * On resume, the process execution will continue from\n     * the next planned step.\n     *\n     * @param eventName name of the event. Should be unique across the process. The same\n     *                  name must be specified to resume the process.\n     */\n    void suspend(String eventName);\n\n    /**\n     * Suspends the current task execution and resumes a {@link ReentrantTask}\n     * with the provided payload.\n     * <p>\n     * Unstable API, subject to change.\n     *\n     * @param eventName the name of the event on which the process will be suspended on.\n     * @param payload   passed to the {@link ReentrantTask#resume(ResumeEvent)} method\n     *                  once the process is resumed.\n     */\n    void reentrantSuspend(String eventName, Map<String, Serializable> payload);\n\n    // TODO FormService\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/ContextUtils.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.svm.Frame;\nimport com.walmartlabs.concord.svm.State;\nimport com.walmartlabs.concord.svm.ThreadId;\n\nimport java.io.Serializable;\n\npublic final class ContextUtils {\n\n    /**\n     * Returns the current \"retry\" attempt number (if applicable).\n     *\n     * @param ctx current {@link Context}\n     * @return the current attemp number of {@code null} if the current call is a retry.\n     * @see Constants.Runtime#RETRY_ATTEMPT_NUMBER\n     */\n    public static Integer getCurrentRetryAttemptNumber(Context ctx) {\n        Execution execution = ctx.execution();\n\n        State state = execution.state();\n        ThreadId threadId = execution.currentThreadId();\n        Frame frame = state.peekFrame(threadId);\n\n        Serializable value = frame.getLocal(Constants.Runtime.RETRY_ATTEMPT_NUMBER);\n        if (value == null) {\n            return null;\n        }\n\n        if (!(value instanceof Integer)) {\n            throw new IllegalStateException(String.format(\"%s is expected to be a number, got: %s. This is most likely a bug\",\n                    Constants.Runtime.RETRY_ATTEMPT_NUMBER, value.getClass()));\n        }\n\n        return (Integer) value;\n    }\n\n    private ContextUtils() {\n    }\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/CustomBeanMethodResolver.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n/**\n * Unstable API, subject to change.\n */\npublic interface CustomBeanMethodResolver {\n\n    Invocation resolve(Object base, String method, Class<?>[] paramTypes, Object[] params);\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/CustomTaskMethodResolver.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n/**\n * Unstable API, subject to change.\n */\npublic interface CustomTaskMethodResolver {\n\n    TaskInvocation resolve(Task base, String method, Class<?>[] paramTypes, Object[] params);\n\n    interface TaskInvocation extends Invocation{\n\n        String taskName();\n\n        Class<? extends Task> taskClass();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/DependencyManager.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Path;\n\n/**\n * Provides a way for the runtime and plugins to retrieve and cache\n * various external artifacts.\n * Supports all dependency types as the regular \"dependencies\" configuration.\n */\npublic interface DependencyManager {\n\n    /**\n     * Downloads the specified URI or returns a previously cached copy.\n     *\n     * @param uri target {@link URI}\n     * @return absolute path to the downloaded file.\n     * @throws IOException if an error occurs during downloading or saving the file.\n     */\n    Path resolve(URI uri) throws IOException;\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/DockerContainerSpec.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.util.List;\nimport java.util.Map;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\npublic interface DockerContainerSpec {\n\n    String image();\n\n    @Nullable\n    String name();\n\n    @Nullable\n    String user();\n\n    @Nullable\n    String workdir();\n\n    @Nullable\n    String entryPoint();\n\n    @Nullable\n    String cpu();\n\n    @Nullable\n    String memory();\n\n    @Nullable\n    String stdOutFilePath();\n\n    @Nullable\n    List<String> args();\n\n    @Nullable\n    Map<String, String> env();\n\n    @Nullable\n    String envFile();\n\n    @Nullable\n    Map<String, String> labels();\n\n    @Nullable\n    Options options();\n\n    @Value.Default\n    default int pullRetryCount() {\n        return 3;\n    }\n\n    @Value.Default\n    default long pullRetryInterval() {\n        return 10_000;\n    }\n\n    @Value.Default\n    default boolean debug() {\n        return false;\n    }\n\n    @Value.Default\n    default boolean forcePull() {\n        return true;\n    }\n\n    @Value.Default\n    default boolean redirectErrorStream() {\n        return true;\n    }\n\n    static ImmutableDockerContainerSpec.Builder builder() {\n        return ImmutableDockerContainerSpec.builder();\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface Options {\n\n        /**\n         * Extra {@code /etc/hosts} entries.\n         * Same as {@code --add-host} option in {@code docker run}\n         *\n         * @return list of extra {@code /etc/hosts} entries.\n         */\n        @Nullable\n        List<String> hosts();\n\n        static ImmutableOptions.Builder builder() {\n            return ImmutableOptions.builder();\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        static Options from(Map<String, Object> m) {\n            ImmutableOptions.Builder b = ImmutableOptions.builder();\n\n            if (m == null) {\n                return b.build();\n            }\n\n            Object v = m.get(\"hosts\");\n            if (v == null) {\n                return b.build();\n            }\n\n            if (!(v instanceof Iterable)) {\n                throw new IllegalArgumentException(\"Unexpected 'hosts' value: \" + v);\n            }\n\n            b.hosts((Iterable<String>) v);\n\n            return b.build();\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/DockerService.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\n\npublic interface DockerService {\n\n    /**\n     * Starts a new Docker container using the provided {@code spec}.\n     *\n     * @param spec        the container's specification.\n     * @param outCallback callback for stdout.\n     * @param errCallback callback for stderr.\n     * @return exit code of the `docker run` command.\n     * @throws IOException          if an error occurs during the start of the container.\n     * @throws InterruptedException if the thread was interrupted during the start of the container.\n     */\n    int start(DockerContainerSpec spec, LogCallback outCallback, LogCallback errCallback) throws IOException, InterruptedException; // TODO throw Exception instead?\n\n    interface LogCallback {\n\n        void onLog(String line);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/DryRunReady.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.TYPE)\npublic @interface DryRunReady {\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/ELFunction.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Marks a static method as a function that can be called in JUEL expressions.\n * The method must be static and will be registered with the provided name or\n * using the method's name if omitted.\n * Use \":\" to separate the prefix and the local name of the function.\n */\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.METHOD)\npublic @interface ELFunction {\n\n    /**\n     * The name of the function to use in JUEL expressions.\n     */\n    String value() default \"\";\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/EvalContext.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\n\n/**\n * Expression evaluation context.\n */\n@Value.Immutable\n@Value.Style(jdkOnly = true,\n        visibility = Value.Style.ImplementationVisibility.PACKAGE, overshadowImplementation = true)\npublic interface EvalContext {\n\n    /**\n     * Required if expressions call other tasks.\n     */\n    @Nullable\n    Context context();\n\n    /**\n     * Variables that will be made available during the evaluation.\n     *\n     * @see #useIntermediateResults()\n     */\n    @Value.Default\n    default Variables variables() {\n        return new NoopVariables();\n    }\n\n    /**\n     * If {@code true} then intermediate results will be available during\n     * the evaluation.\n     */\n    @Value.Default\n    default boolean useIntermediateResults() {\n        return false;\n    }\n\n    /**\n     * If {@code true} then undefined variables will be resolved to\n     * {@code null} instead of throwing an exception.\n     */\n    @Value.Default\n    default boolean undefinedVariableAsNull() {\n        return false;\n    }\n\n    class Builder extends ImmutableEvalContext.Builder {}\n\n    static EvalContext.Builder builder() {\n        return new Builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/EvalContextFactory.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Map;\n\npublic interface EvalContextFactory {\n\n    /**\n     * Includes all flow variables.\n     * Allows access to tasks.\n     * Allows intermediate results, e.g.\n     * <pre>{@code\n     * - set:\n     *      name: \"Concord\"\n     *      msg: \"Hello, ${name}!\" # evaluated to \"Hello, Concord!\"\n     * }</pre>\n     */\n    EvalContext scope(Context ctx);\n\n    /**\n     * Includes all flow variables.\n     * Allows access to tasks.\n     * Doesn't allow access to intermediate results.\n     */\n    EvalContext global(Context ctx);\n\n    /**\n     * Includes all flow variables and additional variables.\n     * Allows access to tasks.\n     * Doesn't allow access to intermediate results.\n     */\n    EvalContext global(Context ctx, Map<String, Object> additionalVariables);\n\n    /**\n     * Includes only the specified variables.\n     * No flow variables allowed.\n     * Allows access to tasks.\n     * Doesn't allow access to intermediate results.\n     */\n    EvalContext strict(Context ctx, Map<String, Object> variables);\n\n    /**\n     * Includes only the specified variables.\n     * No flow variables allowed.\n     * Doesn't allow access to tasks.\n     * Doesn't allow access to intermediate results.\n     */\n    EvalContext strict(Map<String, Object> variables);\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/Execution.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.v2.model.Step;\nimport com.walmartlabs.concord.svm.Runtime;\nimport com.walmartlabs.concord.svm.State;\nimport com.walmartlabs.concord.svm.ThreadId;\n\nimport javax.annotation.Nullable;\nimport java.util.UUID;\n\npublic interface Execution {\n\n    ThreadId currentThreadId();\n\n    Runtime runtime();\n\n    State state();\n\n    ProcessDefinition processDefinition();\n\n    @Nullable\n    Step currentStep();\n\n    String currentFlowName();\n\n    /**\n     * ID of the current task or the expression call. Can be used by plugins to\n     * correlate their events with the task event.\n     *\n     * @return the event correlation ID of the current step.\n     */\n    UUID correlationId();\n\n    // TODO add suspend()\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/ExpressionEvaluator.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.List;\nimport java.util.Map;\n\npublic interface ExpressionEvaluator {\n\n    /**\n     * Non-recursively evaluates the specified value as an expression.\n     *\n     * For example:\n     * <pre>{@code\n     *  // returns a string with ${name} replaced as its value\n     *  eval(ctx, \"Hello, ${name}\", String.class);\n     *\n     *  List<String> items = Arrays.asList(\"Hello, ${name}\", \"Bye, ${name}\");\n     *  // returns a list where each element is replaced with the evaluated value\n     *  eval(ctx, items, List.class);\n     * }</pre>\n     */\n    <T> T eval(EvalContext ctx, Object value, Class<T> expectedType);\n\n    /**\n     * Same as {@link #eval(EvalContext, Object, Class)}, but allows assigning\n     * the result to a generic Map without unchecked casts.\n     */\n    @SuppressWarnings(\"unchecked\")\n    default <K, V> Map<K, V> evalAsMap(EvalContext ctx, Object value) {\n        return eval(ctx, value, Map.class);\n    }\n\n    /**\n     * Same as {@link #eval(EvalContext, Object, Class)}, but allows assigning\n     * the result to a generic List without unchecked casts.\n     */\n    @SuppressWarnings(\"unchecked\")\n    default <T> List<T> evalAsList(EvalContext ctx, Object value) {\n        return eval(ctx, value, List.class);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/FileService.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.nio.file.Path;\n\npublic interface FileService {\n\n    Path createTempFile(String prefix, String suffix) throws IOException;\n\n    Path createTempDirectory(String prefix) throws IOException;\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/Invocation.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface Invocation {\n\n    Object invoke(InvocationContext context);\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/InvocationContext.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface InvocationContext {\n\n    MethodInvoker invoker();\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/LockService.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface LockService {\n\n    void projectLock(String lockName) throws Exception;\n\n    void projectUnlock(String lockName) throws Exception;\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/MapBackedVariables.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Collections;\nimport java.util.Map;\n\npublic class MapBackedVariables implements Variables {\n\n    private final Map<String, Object> delegate;\n\n    public MapBackedVariables(Map<String, Object> delegate) {\n        this.delegate = delegate != null ? Collections.unmodifiableMap(delegate) : Collections.emptyMap();\n    }\n\n    @Override\n    public Object get(String key) {\n        return delegate.get(key);\n    }\n\n    @Override\n    public void set(String key, Object value) {\n        throw new IllegalStateException(\"Not supported\");\n    }\n\n    @Override\n    public boolean has(String key) {\n        return delegate.containsKey(key);\n    }\n\n    @Override\n    public Map<String, Object> toMap() {\n        return delegate;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/MethodInvoker.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface MethodInvoker {\n\n    Object invoke(Object base, String method, Class<?>[] paramTypes, Object[] params);\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/NoopVariables.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.sdk.Variables;\n\nimport java.util.Collections;\nimport java.util.Map;\n\npublic class NoopVariables implements Variables {\n\n    @Override\n    public Object get(String key) {\n        return null;\n    }\n\n    @Override\n    public void set(String key, Object value) {\n        throw new IllegalStateException(\"Not supported\");\n    }\n\n    @Override\n    public boolean has(String key) {\n        return false;\n    }\n\n    @Override\n    public Map<String, Object> toMap() {\n        return Collections.emptyMap();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/ProcessConfiguration.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.runtime.v2.model.EventConfiguration;\nimport com.walmartlabs.concord.sdk.Constants;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonIgnoreProperties(ignoreUnknown = true)\n@JsonSerialize(as = ImmutableProcessConfiguration.class)\n@JsonDeserialize(as = ImmutableProcessConfiguration.class)\npublic interface ProcessConfiguration extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    UUID instanceId();\n\n    @Value.Default\n    default boolean debug() {\n        return false;\n    }\n\n    @Value.Default\n    default boolean dryRun() {\n        return false;\n    }\n\n    @Value.Default\n    default String entryPoint() {\n        return Constants.Request.DEFAULT_ENTRY_POINT_NAME;\n    }\n\n    @Value.Default\n    @AllowNulls\n    default Map<String, Object> arguments() {\n        return Collections.emptyMap();\n    }\n\n    @Value.Default\n    default Map<String, Object> meta() {\n        return Collections.emptyMap();\n    }\n\n    // TODO types\n    @Nullable\n    Map<String, Object> initiator();\n\n    // TODO types\n    @Nullable\n    Map<String, Object> currentUser();\n\n    @Value.Default\n    default ProcessInfo processInfo() {\n        return ProcessInfo.builder().build();\n    }\n\n    @Value.Default\n    default ProjectInfo projectInfo() {\n        return ProjectInfo.builder().build();\n    }\n\n    @Value.Default\n    default EventConfiguration events() {\n        return EventConfiguration.builder().build();\n    }\n\n    @Value.Default\n    default Map<String, Map<String, Object>> defaultTaskVariables() {\n        return Collections.emptyMap();\n    }\n\n    @Value.Default\n    default List<String> out() {\n        return Collections.emptyList();\n    }\n\n    static ImmutableProcessConfiguration.Builder builder() {\n        return ImmutableProcessConfiguration.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/ProcessInfo.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonAlias;\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonIgnoreProperties(ignoreUnknown = true)\n@JsonSerialize(as = ImmutableProcessInfo.class)\n@JsonDeserialize(as = ImmutableProcessInfo.class)\npublic interface ProcessInfo extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    @JsonAlias(\"sessionKey\")\n    String sessionToken();\n\n    @Value.Default\n    default List<String> activeProfiles() {\n        return Collections.singletonList(\"default\");\n    }\n\n    static ImmutableProcessInfo.Builder builder() {\n        return ImmutableProcessInfo.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/ProjectInfo.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonIgnoreProperties(ignoreUnknown = true)\n@JsonSerialize(as = ImmutableProjectInfo.class)\n@JsonDeserialize(as = ImmutableProjectInfo.class)\npublic interface ProjectInfo extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    UUID orgId();\n\n    @Nullable\n    String orgName();\n\n    @Nullable\n    UUID projectId();\n\n    @Nullable\n    String projectName();\n\n    @Nullable\n    UUID repoId();\n\n    @Nullable\n    String repoName();\n\n    @Nullable\n    String repoUrl();\n\n    @Nullable\n    String repoBranch();\n\n    @Nullable\n    String repoPath();\n\n    @Nullable\n    String repoCommitId();\n\n    @Nullable\n    String repoCommitAuthor();\n\n    @Nullable\n    String repoCommitMessage();\n\n    static ImmutableProjectInfo.Builder builder() {\n        return ImmutableProjectInfo.builder();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/ReentrantTask.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface ReentrantTask extends Task {\n\n    TaskResult resume(ResumeEvent event) throws Exception;\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/ResumeEvent.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\nimport java.util.Map;\n\npublic interface ResumeEvent extends Serializable {\n\n    String eventName();\n\n    Map<String, Serializable> state();\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/SecretNotFoundException.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class SecretNotFoundException extends IllegalArgumentException {\n\n    // for backward compatibility (java8 concord 1.92.0 version)\n    private static final long serialVersionUID = -1760693642714616365L;\n\n    private final String orgName;\n\n    private final String secretName;\n\n    public SecretNotFoundException(String orgName, String secretName) {\n        super(\"Secret not found: \" + orgName + \"/\" + secretName);\n        this.orgName = orgName;\n        this.secretName = secretName;\n    }\n\n    public String getOrgName() {\n        return orgName;\n    }\n\n    public String getSecretName() {\n        return secretName;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/SecretService.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.nio.file.Path;\nimport java.util.UUID;\n\npublic interface SecretService {\n\n    SecretCreationResult createKeyPair(SecretParams secret, KeyPair keyPair) throws Exception;\n\n    SecretCreationResult createUsernamePassword(SecretParams secret, UsernamePassword usernamePassword) throws Exception;\n\n    SecretCreationResult createData(SecretParams secret, byte[] data) throws Exception;\n\n    String exportAsString(String orgName, String secretName, String password) throws Exception;\n\n    KeyPair exportKeyAsFile(String orgName, String secretName, String password) throws Exception;\n\n    UsernamePassword exportCredentials(String orgName, String secretName, String password) throws Exception;\n\n    Path exportAsFile(String orgName, String secretName, String password) throws Exception;\n\n    String decryptString(String encryptedValue) throws Exception;\n\n    String encryptString(String orgName, String projectName, String value) throws Exception;\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface SecretCreationResult {\n\n        UUID id();\n\n        @Nullable\n        String password();\n\n        static ImmutableSecretCreationResult.Builder builder() {\n            return ImmutableSecretCreationResult.builder();\n        }\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface SecretParams extends Serializable {\n\n        long serialVersionUID = 1L;\n\n        String orgName();\n\n        @Nullable\n        String project();\n\n        String secretName();\n\n        @Nullable\n        String storePassword();\n\n        @Value.Default\n        default boolean generatePassword() {\n            return false;\n        }\n\n        enum Visibility {\n            PUBLIC, PRIVATE\n        }\n\n        @Nullable\n        Visibility visibility();\n\n        static ImmutableSecretParams.Builder builder() {\n            return ImmutableSecretParams.builder();\n        }\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface KeyPair {\n\n        long serialVersionUID = 1L;\n\n        Path privateKey();\n\n        Path publicKey();\n\n        static ImmutableKeyPair.Builder builder() {\n            return ImmutableKeyPair.builder();\n        }\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface UsernamePassword {\n\n        long serialVersionUID = 1L;\n\n        String username();\n\n        String password();\n\n        static UsernamePassword of(String username, String password) {\n            return ImmutableUsernamePassword.builder()\n                    .username(username)\n                    .password(password)\n                    .build();\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/SensitiveData.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\nimport static java.lang.annotation.ElementType.METHOD;\nimport static java.lang.annotation.ElementType.PARAMETER;\n\n/**\n * This annotation can be used to:\n * <ul>\n * <li>prevent task arguments values from being recorded in process events;</li>\n * <li>prevent task method result from being logged.</li>\n * </ul>\n * <p>\n * Currently, it is applicable only for task methods called directly\n * via expressions. For example:\n * <pre>{@code\n * flows:\n *   default:\n *     - \"${crypto.exportAsString('myOrg', 'mySecret', 'mySecretPassword')}\"\n * }</pre>\n * Assuming the task method has the third argument annotated with\n * {@link SensitiveData}, running this flow will result with\n * the third argument's value being masked in process events.\n */\n@Target({PARAMETER, METHOD})\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface SensitiveData {\n\n    String[] keys() default {};\n\n    boolean includeNestedValues() default false;\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/SensitiveDataHolder.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Collection;\nimport java.util.Set;\n\npublic interface SensitiveDataHolder {\n\n    Set<String> get();\n\n    void add(String sensitiveData);\n\n    void addAll(Collection<String> sensitiveData);\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/Task.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n/**\n * Task interface. All implementations should be annotated with {@code @Named}\n * using process-wide unique names.\n */\npublic interface Task {\n\n    /**\n     * This method is called when the task is invoked using the {@code task} syntax.\n     *\n     * @param input {@code in} parameters.\n     * @return {@link TaskResult} instance.\n     * @throws Exception any error.\n     */\n    default TaskResult execute(Variables input) throws Exception {\n        throw new IllegalStateException(\"The task doesn't support full task syntax yet. \" +\n                \"Please call the task using expressions.\");\n    }\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/TaskProvider.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Set;\n\n/**\n * Provider for tasks. Responsible for creating task instances using\n * the supplied {@link Context} and the key.\n * <p>\n * Multiple task providers can exist in the same injector.\n * The {@code @Priority} annotation can be used to specify the order\n * in which each provider is called. Providers with lowest numbers are\n * called first.\n */\npublic interface TaskProvider {\n\n    Task createTask(Context ctx, String key);\n\n    Class<? extends Task> getTaskClass(Context ctx, String key);\n\n    boolean hasTask(String key);\n\n    Set<String> names();\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/TaskResult.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.annotation.Nullable;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.ObjectOutputStream;\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic interface TaskResult extends Serializable {\n\n    /**\n     * Creates a new instance of {@link TaskResult} with {@link SimpleResult#ok()} set to {@code true}.\n     *\n     * @return {@link TaskResult}\n     */\n    static SimpleResult success() {\n        return new SimpleResult(true, null, null);\n    }\n\n    static SimpleFailResult fail(String error) {\n        return new SimpleFailResult(error);\n    }\n\n    static TaskResult fail(Exception e) {\n        return new SimpleFailResult(e);\n    }\n\n    @Deprecated\n    static SimpleResult of(boolean success) {\n        return new SimpleResult(success, null, null);\n    }\n\n    @Deprecated\n    static SimpleResult of(boolean success, String error) {\n        return new SimpleResult(success, error, null);\n    }\n\n    @Deprecated\n    static SimpleResult of(boolean success, String error, Map<String, Object> values) {\n        return new SimpleResult(success, error, values);\n    }\n\n    /**\n     * Creates a new instance of {@link TaskResult} with {@link SimpleResult#ok()} set to {@code false}\n     * and with the provided error message.\n     *\n     * @param message error message.\n     * @return {@link TaskResult}\n     */\n    @Deprecated\n    static SimpleResult error(String message) {\n        return new SimpleResult(false, message, null);\n    }\n\n    static TaskResult suspend(String eventName) {\n        return new SuspendResult(eventName);\n    }\n\n    static TaskResult reentrantSuspend(String eventName, Map<String, Serializable> payload) {\n        return new ReentrantSuspendResult(eventName, payload);\n    }\n\n    /**\n     * Result of a task call. Provides some common fields such as {@link #ok()}\n     * and {@link #error()}, allows arbitrary data in {@link #values()}.\n     * <p>\n     * All values must be {@link Serializable}, including collection types.\n     * Avoid using custom types/classes as values.\n     */\n    class SimpleResult implements TaskResult {\n\n        // for backward compatibility (java8 concord 1.92.0 version)\n        private static final long serialVersionUID = -2964915156894763525L;\n\n        private final boolean ok;\n        private final String error;\n        private final Map<String, Object> values;\n\n        SimpleResult(boolean ok, String error, Map<String, Object> values) {\n            this.ok = ok;\n            this.error = error;\n            this.values = new HashMap<>();\n\n            values(values);\n        }\n\n        public boolean ok() {\n            return ok;\n        }\n\n        @Nullable\n        public String error() {\n            return error;\n        }\n\n        public Map<String, Object> values() {\n            return values;\n        }\n\n        public SimpleResult value(String key, Object value) {\n            assertValue(key, value);\n\n            values.put(key, value);\n            return this;\n        }\n\n        public SimpleResult values(Map<String, Object> items) {\n            if (items == null) {\n                return this;\n            }\n\n            for (Map.Entry<String, Object> e : items.entrySet()) {\n                value(e.getKey(), e.getValue());\n            }\n\n            return this;\n        }\n\n        /**\n         * Returns a combined map of all values plus additional fields:\n         * <ul>\n         *     <li>{@code ok} - a boolean value, same as {@link #ok()}</li>\n         *     <li>{@code error} - a string value, same as {@link #error()}</li>\n         * </ul>\n         * <p>\n         * Those fields will override any values with the same keys.\n         *\n         * @return {@link Map} of all result values.\n         */\n        public Map<String, Object> toMap() {\n            Map<String, Object> result = new HashMap<>(values != null ? values : Collections.emptyMap());\n            result.put(\"ok\", ok);\n            if (error != null) {\n                result.put(\"error\", error);\n            }\n            return result;\n        }\n    }\n\n    class SimpleFailResult extends SimpleResult {\n\n        // for backward compatibility (java8 concord 1.92.0 version)\n        private static final long serialVersionUID = 6405940706761648254L;\n\n        private final Exception cause;\n\n        SimpleFailResult(Exception e) {\n            super(false, e.getMessage(), null);\n\n            this.cause = e;\n        }\n\n        SimpleFailResult(String error) {\n            super(false, error, null);\n\n            this.cause = null;\n        }\n\n        public Exception cause() {\n            return cause;\n        }\n    }\n\n    class SuspendResult implements TaskResult {\n\n        // for backward compatibility (java8 concord 1.92.0 version)\n        private static final long serialVersionUID = 7103889933507730380L;\n\n        private final String eventName;\n\n        SuspendResult(String eventName) {\n            this.eventName = eventName;\n        }\n\n        public String eventName() {\n            return eventName;\n        }\n    }\n\n    class ReentrantSuspendResult implements TaskResult {\n\n        // for backward compatibility (java8 concord 1.92.0 version)\n        private static final long serialVersionUID = -4652050582959900705L;\n\n        private final String eventName;\n        private final Map<String, Serializable> payload;\n\n        ReentrantSuspendResult(String eventName, Map<String, Serializable> payload) {\n            this.eventName = eventName;\n            this.payload = payload;\n        }\n\n        public String eventName() {\n            return eventName;\n        }\n\n        public Map<String, Serializable> payload() {\n            return payload;\n        }\n    }\n\n    static void assertValue(String key, Object value) {\n        if (value == null) {\n            return;\n        }\n\n        try (ObjectOutputStream oos = new ObjectOutputStream(new ByteArrayOutputStream())) {\n            oos.writeObject(value);\n        } catch (IOException e) {\n            throw new IllegalArgumentException(String.format(\"Can't set the '%s' key: \" +\n                    \"not a serializable value: %s (class: %s). \" +\n                    \"Error: %s\", key, value, value.getClass(), e.getMessage()));\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/UserDefinedException.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.PrintStream;\nimport java.io.PrintWriter;\nimport java.util.Map;\n\n/**\n * Doesn't produce a stack trace in process logs.\n */\npublic class UserDefinedException extends RuntimeException {\n\n    // for backward compatibility (java8 concord 1.92.0 version)\n    private static final long serialVersionUID = 8152584338845805365L;\n\n    private final Map<String, Object> payload;\n\n    public UserDefinedException(String message) {\n        this(message, null);\n    }\n\n    public UserDefinedException(String message, Map<String, Object> payload) {\n        super(message);\n        this.payload = payload;\n    }\n\n    public Map<String, Object> getPayload() {\n        return payload;\n    }\n\n    @Override\n    public String toString() {\n        var m = getLocalizedMessage();\n        if (payload != null && !payload.isEmpty()) {\n            m = m + \": \" + payload;\n        }\n        return m;\n    }\n\n    @Override\n    public StackTraceElement[] getStackTrace() {\n        return new StackTraceElement[0];\n    }\n\n    @Override\n    public void printStackTrace(PrintWriter s) {\n        // do nothing\n    }\n\n    @Override\n    public void printStackTrace(PrintStream s) {\n        // do nothing\n    }\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/Variables.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic interface Variables {\n\n    Object get(String key);\n\n    void set(String key, Object value);\n\n    boolean has(String key);\n\n    Map<String, Object> toMap();\n\n    default String getString(String key) {\n        return getString(key, null);\n    }\n\n    default String getString(String key, String defaultValue) {\n        return get(key, defaultValue, String.class);\n    }\n\n    default String assertString(String key) {\n        return assertVariable(key, String.class);\n    }\n\n    default String assertString(String message, String key) {\n        return assertVariable(message, key, String.class);\n    }\n\n    default Number getNumber(String key, Number defaultValue) {\n        return get(key, defaultValue, Number.class);\n    }\n\n    default Number assertNumber(String key) {\n        return assertVariable(key, Number.class);\n    }\n\n    default boolean getBoolean(String key, boolean defaultValue) {\n        Boolean result = get(key, defaultValue, Boolean.class);\n        if (result == null) {\n            return defaultValue;\n        }\n        return result;\n    }\n\n    default boolean assertBoolean(String key) {\n        return assertVariable(key, Boolean.class);\n    }\n\n    default int getInt(String key, int defaultValue) {\n        return getNumber(key, defaultValue).intValue();\n    }\n\n    default int assertInt(String key) {\n        return assertNumber(key).intValue();\n    }\n\n    default long getLong(String key, long defaultValue) {\n        return getNumber(key, defaultValue).longValue();\n    }\n\n    default long assertLong(String key) {\n        return assertNumber(key).longValue();\n    }\n\n    default UUID getUUID(String key) {\n        Object o = get(key);\n        if (o == null) {\n            return null;\n        }\n\n        if (o instanceof String) {\n            return UUID.fromString((String) o);\n        }\n\n        if (o instanceof UUID) {\n            return (UUID) o;\n        }\n\n        throw new IllegalArgumentException(\"Invalid variable '\" + key + \"' type, expected: string/uuid, got: \" + o.getClass());\n    }\n\n    default UUID assertUUID(String key) {\n        UUID result = getUUID(key);\n        if (result != null) {\n            return result;\n        }\n        throw new IllegalArgumentException(\"Mandatory variable '\" + key + \"' is required\");\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    default <E> Collection<E> getCollection(String key, Collection<E> defaultValue) {\n        return get(key, defaultValue, Collection.class);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    default <E> Collection<E> assertCollection(String key) {\n        return assertVariable(key, Collection.class);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    default <K, V> Map<K, V> getMap(String key, Map<K, V> defaultValue) {\n        return get(key, defaultValue, Map.class);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    default <K, V> Map<K, V> assertMap(String name) {\n        return assertVariable(null, name, Map.class);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    default <T> List<T> getList(String key, List<T> defaultValue) {\n        return get(key, defaultValue, List.class);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    default <T> List<T> assertList(String key) {\n        return assertVariable(key, List.class);\n    }\n\n    default <T> T assertVariable(String key, Class<T> type) {\n        return assertVariable(null, key, type);\n    }\n\n    default <T> T assertVariable(String message, String key, Class<T> type) {\n        T result = get(key, null, type);\n\n        if (result != null) {\n            return result;\n        }\n\n        throw new IllegalArgumentException(message != null ? message : \"Mandatory variable '\" + key + \"' is required\");\n    }\n\n    default <T> T get(String key, T defaultValue, Class<T> type) {\n        Object value = get(key);\n        if (value == null) {\n            return defaultValue;\n        }\n\n        if (type.isInstance(value)) {\n            return type.cast(value);\n        }\n\n        throw new IllegalArgumentException(\"Invalid variable '\" + key + \"' type, expected: \" + type + \", got: \" + value.getClass());\n    }\n}\n"
  },
  {
    "path": "runtime/v2/sdk/src/main/java/com/walmartlabs/concord/runtime/v2/sdk/WorkingDirectory.java",
    "content": "package com.walmartlabs.concord.runtime.v2.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.nio.file.Path;\n\n/**\n * Contains path to the current process' working directory.\n * Can be @Inject-ed into services.\n */\npublic class WorkingDirectory {\n\n    private final Path value;\n\n    public WorkingDirectory(Path value) {\n        this.value = value;\n    }\n\n    public Path getValue() {\n        return value;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/vm/README.md",
    "content": "# Concord VM\n\nA virtual machine implementation used to run Concord flows v2."
  },
  {
    "path": "runtime/v2/vm/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-runtime-vm-v2</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n    </dependencies>\n</project>\n"
  },
  {
    "path": "runtime/v2/vm/src/main/java/com/walmartlabs/concord/svm/Command.java",
    "content": "package com.walmartlabs.concord.svm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\n\npublic interface Command extends Serializable {\n\n    void eval(Runtime runtime, State state, ThreadId threadId) throws Exception;\n}\n"
  },
  {
    "path": "runtime/v2/vm/src/main/java/com/walmartlabs/concord/svm/EvalResult.java",
    "content": "package com.walmartlabs.concord.svm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serial;\nimport java.io.Serializable;\n\npublic class EvalResult implements Serializable {\n\n    @Serial\n    private static final long serialVersionUID = 1L;\n\n    private final Frame lastFrame;\n\n    public EvalResult(Frame lastFrame) {\n        this.lastFrame = lastFrame;\n    }\n\n    public Frame lastFrame() {\n        return lastFrame;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/vm/src/main/java/com/walmartlabs/concord/svm/ExecutionListener.java",
    "content": "package com.walmartlabs.concord.svm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n/**\n * Provides a way to listen for different execution stages.\n * Each method can decide whether to continue the specific execution stage\n * or stop it by returning a {@link Result} value.\n */\npublic interface ExecutionListener {\n\n    /**\n     * Called before the next command in the stack is executed.\n     */\n    default Result beforeCommand(Runtime runtime, VM vm, State state, ThreadId threadId, Command cmd) {\n        return Result.CONTINUE;\n    }\n\n    /**\n     * Called after the command in the stack was executed.\n     */\n    default Result afterCommand(Runtime runtime, VM vm, State state, ThreadId threadId, Command cmd) {\n        return Result.CONTINUE;\n    }\n\n    /**\n     *  Called after the command execution ended with exception/error.\n     */\n    default Result onCommandError(Runtime runtime, VM vm, State state, ThreadId threadId, Command cmd, Exception e) {\n        return Result.CONTINUE;\n    }\n\n    /**\n     * Called after each eval loop iteration.\n     */\n    default Result afterEval(Runtime runtime, VM vm, State state) {\n        return Result.CONTINUE;\n    }\n\n    /**\n     * Called after suspended threads are woken up.\n     */\n    default Result afterWakeUp(Runtime runtime, VM vm, State state) {\n        return Result.CONTINUE;\n    }\n\n    /**\n     * Called before the process executes its first step.\n     */\n    default void beforeProcessStart(Runtime runtime, State state) {\n    }\n\n    /**\n     * Called before the process resumes the execution.\n     */\n    default void beforeProcessResume(Runtime runtime, State state) {\n    }\n\n    /**\n     * Called after the process calls the last step.\n     */\n    default void afterProcessEnds(Runtime runtime, State state, Frame lastFrame) {\n    }\n\n    /**\n     * Called after process ends with error.\n     */\n    default void onProcessError(Runtime runtime, State state, Exception e) {\n    }\n\n    enum Result {\n        CONTINUE,\n        BREAK\n    }\n}\n"
  },
  {
    "path": "runtime/v2/vm/src/main/java/com/walmartlabs/concord/svm/ExecutionListenerHolder.java",
    "content": "package com.walmartlabs.concord.svm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.ArrayList;\nimport java.util.Collection;\n\nimport static com.walmartlabs.concord.svm.ExecutionListener.Result.BREAK;\nimport static com.walmartlabs.concord.svm.ExecutionListener.Result.CONTINUE;\n\npublic class ExecutionListenerHolder {\n\n    private final VM vm;\n    private final Collection<ExecutionListener> listeners;\n\n    public ExecutionListenerHolder(VM vm, Collection<ExecutionListener> listeners) {\n        this.vm = vm;\n        this.listeners = new ArrayList<>(listeners);\n    }\n\n    public ExecutionListener.Result fireBeforeCommand(Runtime runtime, State state, ThreadId threadId, Command cmd) {\n        ExecutionListener.Result result = CONTINUE;\n\n        for (ExecutionListener l : listeners) {\n            ExecutionListener.Result r = l.beforeCommand(runtime, vm, state, threadId, cmd);\n            if (r == BREAK && result != BREAK) {\n                result = BREAK;\n            }\n        }\n\n        return result;\n    }\n\n    public ExecutionListener.Result fireAfterCommand(Runtime runtime, State state, ThreadId threadId, Command cmd) {\n        ExecutionListener.Result result = CONTINUE;\n\n        for (ExecutionListener l : listeners) {\n            ExecutionListener.Result r = l.afterCommand(runtime, vm, state, threadId, cmd);\n            if (r == BREAK && result != BREAK) {\n                result = BREAK;\n            }\n        }\n\n        return result;\n    }\n\n    public ExecutionListener.Result fireAfterCommandWithError(Runtime runtime, State state, ThreadId threadId, Command cmd, Exception e) {\n        ExecutionListener.Result result = CONTINUE;\n\n        for (ExecutionListener l : listeners) {\n            ExecutionListener.Result r = l.onCommandError(runtime, vm, state, threadId, cmd, e);\n            if (r == BREAK && result != BREAK) {\n                result = BREAK;\n            }\n        }\n\n        return result;\n    }\n\n\n    public ExecutionListener.Result fireAfterEval(Runtime runtime, State state) {\n        ExecutionListener.Result result = CONTINUE;\n\n        for (ExecutionListener l : listeners) {\n            ExecutionListener.Result r = l.afterEval(runtime, vm, state);\n            if (r == BREAK && result != BREAK) {\n                result = BREAK;\n            }\n        }\n\n        return result;\n    }\n\n    public ExecutionListener.Result fireAfterWakeUp(Runtime runtime, State state) {\n        ExecutionListener.Result result = CONTINUE;\n\n        for (ExecutionListener l : listeners) {\n            ExecutionListener.Result r = l.afterWakeUp(runtime, vm, state);\n            if (r == BREAK && result != BREAK) {\n                result = BREAK;\n            }\n        }\n\n        return result;\n    }\n\n    public void fireBeforeProcessStart(Runtime runtime, State state) {\n        for (ExecutionListener l : listeners) {\n            l.beforeProcessStart(runtime, state);\n        }\n    }\n\n    public void fireBeforeProcessResume(Runtime runtime, State state) {\n        for (ExecutionListener l : listeners) {\n            l.beforeProcessResume(runtime, state);\n        }\n    }\n\n    public void fireAfterProcessEnds(Runtime runtime, State state, Frame lastFrame) {\n        for (ExecutionListener l : listeners) {\n            l.afterProcessEnds(runtime, state, lastFrame);\n        }\n    }\n\n    public void fireOnProcessError(Runtime runtime, State state, Exception e) {\n        for (ExecutionListener l : listeners) {\n            l.onProcessError(runtime, state, e);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/vm/src/main/java/com/walmartlabs/concord/svm/Frame.java",
    "content": "package com.walmartlabs.concord.svm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\nimport java.util.*;\n\n/**\n * Frame or \"call frame\" represents a scope with a list of commands, local\n * variables and an optional exception handling command.\n */\npublic class Frame implements Serializable {\n\n    public static Builder builder() {\n        return new Builder();\n    }\n\n    /**\n     * The last handled exception is stored as a local frame variable using this key.\n     */\n    public static final String LAST_EXCEPTION_KEY = \"__lastException\";\n\n    private static final long serialVersionUID = 1L;\n\n    private final FrameId id;\n    private final FrameType type;\n    private final List<Command> commandStack;\n    private final Map<String, Serializable> locals;\n    private final Command finallyHandler;\n\n    private Command exceptionHandler;\n\n    private Frame(Builder b) {\n        this.id = new FrameId(UUID.randomUUID());\n        this.type = b.type;\n\n        this.commandStack = new LinkedList<>();\n        if (b.commands != null) {\n            for (Command cmd : b.commands) {\n                push(cmd);\n            }\n        }\n\n        this.locals = Collections.synchronizedMap(new LinkedHashMap<>(b.locals != null ? b.locals : Collections.emptyMap()));\n\n        this.exceptionHandler = b.exceptionHandler;\n        this.finallyHandler = b.finallyHandler;\n    }\n\n    public FrameId id() {\n        return id;\n    }\n\n    public Command peek() {\n        if (commandStack.isEmpty()) {\n            return null;\n        }\n\n        return commandStack.get(0);\n    }\n\n    public FrameType getType() {\n        return type;\n    }\n\n    public void push(Command cmd) {\n        commandStack.add(0, cmd);\n    }\n\n    public void pop() {\n        commandStack.remove(0);\n    }\n\n    public Command getExceptionHandler() {\n        return exceptionHandler;\n    }\n\n    public Command getFinallyHandler() {\n        return finallyHandler;\n    }\n\n    public void setExceptionHandler(Command exceptionHandler) {\n        this.exceptionHandler = exceptionHandler;\n    }\n\n    public void clearExceptionHandler() {\n        this.exceptionHandler = null;\n    }\n\n    public boolean hasLocal(String k) {\n        return locals.containsKey(k);\n    }\n\n    public void setLocal(String k, Serializable v) {\n        locals.put(k, v);\n    }\n\n    public Serializable getLocal(String k) {\n        return locals.get(k);\n    }\n\n    public Map<String, Serializable> getLocals() {\n        return Collections.unmodifiableMap(locals);\n    }\n\n    public static class Builder {\n\n        private FrameType type = FrameType.ROOT;\n        private Command exceptionHandler;\n        private Command finallyHandler;\n        private List<Command> commands;\n        private Map<String, Serializable> locals;\n\n        private Builder() {\n        }\n\n        public Builder root() {\n            this.type = FrameType.ROOT;\n            return this;\n        }\n\n        public Builder nonRoot() {\n            this.type = FrameType.NON_ROOT;\n            return this;\n        }\n\n        public Builder exceptionHandler(Command exceptionHandler) {\n            this.exceptionHandler = exceptionHandler;\n            return this;\n        }\n\n        public Builder finallyHandler(Command finallyHandler) {\n            this.finallyHandler = finallyHandler;\n            return this;\n        }\n\n        public Builder locals(Map<String, Object> locals) {\n            if (locals == null || locals.isEmpty()) {\n                return this;\n            }\n\n            if (this.locals == null) {\n                this.locals = new LinkedHashMap<>(); // preserve order of the keys\n            }\n\n            locals.forEach((k, v) -> {\n                if (v == null || v instanceof Serializable) {\n                    this.locals.put(k, (Serializable) v);\n                } else {\n                    throw new IllegalStateException(\"Can't set a non-serializable local variable: \" + k + \" -> \" + v.getClass());\n                }\n            });\n\n            return this;\n        }\n\n        /**\n         * Add one or more commands to the frame's stack. Commands will be pushed\n         * to the stack in the original order which means that the first command\n         * in the {@code cmds} array will be executed last.\n         */\n        public Builder commands(Command... cmds) {\n            if (cmds == null || cmds.length == 0) {\n                return this;\n            }\n\n            if (this.commands == null) {\n                this.commands = new ArrayList<>();\n            }\n\n            this.commands.addAll(Arrays.asList(cmds));\n\n            return this;\n        }\n\n        public Frame build() {\n            return new Frame(this);\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/vm/src/main/java/com/walmartlabs/concord/svm/FrameId.java",
    "content": "package com.walmartlabs.concord.svm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\nimport java.util.Objects;\nimport java.util.UUID;\n\npublic class FrameId implements Serializable, Comparable<FrameId> {\n\n    private static final long serialVersionUID = 1L;\n\n    private final UUID id;\n\n    public FrameId(UUID id) {\n        this.id = id;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        FrameId frameId = (FrameId) o;\n        return Objects.equals(id, frameId.id);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(id);\n    }\n\n    @Override\n    public int compareTo(FrameId o) {\n        return id.compareTo(o.id);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/vm/src/main/java/com/walmartlabs/concord/svm/FrameType.java",
    "content": "package com.walmartlabs.concord.svm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n/**\n * Type of a {@link Frame}.\n */\npublic enum FrameType {\n\n    /**\n     * \"Root\" {@link Frame}. The frame's locals start from a root frame.\n     */\n    ROOT,\n\n    /**\n     * \"Non-root\" {@link Frame}. The frame's locals start from the nearest {@link #ROOT} ancestor.\n     */\n    NON_ROOT\n}\n"
  },
  {
    "path": "runtime/v2/vm/src/main/java/com/walmartlabs/concord/svm/InMemoryState.java",
    "content": "package com.walmartlabs.concord.svm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.Serializable;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\n/**\n * Simple in-memory implementation of {@link State}\n */\npublic class InMemoryState implements Serializable, State {\n\n    private static final long serialVersionUID = 1L;\n\n    private static final Logger log = LoggerFactory.getLogger(InMemoryState.class);\n\n    private final Map<ThreadId, List<Frame>> frames = new HashMap<>();\n    private final Map<ThreadId, ThreadStatus> threadStatus = new HashMap<>();\n    private final Map<ThreadId, Set<ThreadId>> children = new HashMap<>();\n    private final Map<ThreadId, String> eventRefs = new HashMap<>();\n    private final Map<ThreadId, ThreadError> threadErrors = new HashMap<>();\n    private final Map<ThreadId, List<StackTraceItem>> stackTrace = new HashMap<>();\n    private final Map<ThreadId, Map<String, Serializable>> threadLocals = new HashMap<>();\n\n    private final ThreadId rootThreadId;\n\n    private long threadIdSeq = 0;\n\n    public InMemoryState(Frame rootFrame) {\n        this.rootThreadId = nextThreadId();\n        pushFrame(rootThreadId, rootFrame);\n    }\n\n    public InMemoryState(Command cmd) {\n        this(Frame.builder()\n                .root()\n                .commands(cmd)\n                .build());\n    }\n\n    @Override\n    public void pushFrame(ThreadId threadId, Frame frame) {\n        log.trace(\"pushFrame {}\", threadId);\n\n        synchronized (this) {\n            List<Frame> l = frames.computeIfAbsent(threadId, key -> new LinkedList<>());\n            l.add(0, frame);\n        }\n    }\n\n    @Override\n    public Frame peekFrame(ThreadId threadId) {\n        synchronized (this) {\n            List<Frame> l = frames.get(threadId);\n            if (l == null || l.isEmpty()) {\n                return null;\n            }\n\n            return l.get(0);\n        }\n    }\n\n    @Override\n    public void popFrame(ThreadId threadId) {\n        log.trace(\"popFrame {}\", threadId);\n\n        synchronized (this) {\n            List<Frame> l = frames.get(threadId);\n            if (l == null) {\n                throw new IllegalStateException(\"Call frame doesn't exist: \" + threadId);\n            }\n\n            Frame removed = l.remove(0);\n            unwindStackTrace(threadId, removed);\n        }\n    }\n\n    @Override\n    public List<Frame> getFrames(ThreadId threadId) {\n        synchronized (this) {\n            List<Frame> l = this.frames.get(threadId);\n            if (l == null) {\n                return Collections.emptyList();\n            }\n\n            return Collections.unmodifiableList(new ArrayList<>(l));\n        }\n    }\n\n    @Override\n    public void dropAllFrames() {\n        synchronized (this) {\n            frames.clear();\n        }\n    }\n\n    @Override\n    public void setStatus(ThreadId threadId, ThreadStatus status) {\n        synchronized (this) {\n            threadStatus.put(threadId, status);\n        }\n    }\n\n    @Override\n    public ThreadStatus getStatus(ThreadId threadId) {\n        synchronized (this) {\n            return threadStatus.get(threadId);\n        }\n    }\n\n    @Override\n    public ThreadId getRootThreadId() {\n        return rootThreadId;\n    }\n\n    @Override\n    public void fork(ThreadId parentThreadId, ThreadId threadId, Command... cmds) {\n        synchronized (this) {\n            setStatus(threadId, ThreadStatus.READY);\n            pushFrame(threadId, Frame.builder()\n                    .root()\n                    .commands(cmds)\n                    .build());\n\n            children.computeIfAbsent(parentThreadId, k -> new HashSet<>())\n                    .add(threadId);\n        }\n    }\n\n    @Override\n    public Map<ThreadId, ThreadStatus> threadStatus() {\n        synchronized (this) {\n            return new HashMap<>(threadStatus);\n        }\n    }\n\n    @Override\n    public ThreadId nextThreadId() {\n        synchronized (this) {\n            long id = threadIdSeq++;\n            return new ThreadId(id);\n        }\n    }\n\n    @Override\n    public void setEventRef(ThreadId threadId, String eventRef) {\n        // TODO check for uniqueness\n\n        synchronized (this) {\n            String old = eventRefs.put(threadId, eventRef);\n            if (old != null) {\n                throw new IllegalStateException(\"Thread \" + threadId + \" already had an unprocessed event ref registered: \" + old);\n            }\n        }\n    }\n\n    @Override\n    public ThreadId removeEventRef(String eventRef) {\n        ThreadId threadId = null;\n\n        synchronized (this) {\n            for (Map.Entry<ThreadId, String> e : eventRefs.entrySet()) {\n                String s = e.getValue();\n                if (eventRef.equals(s)) {\n                    threadId = e.getKey();\n                    break;\n                }\n            }\n\n            if (threadId != null) {\n                eventRefs.remove(threadId);\n            }\n        }\n\n        return threadId;\n    }\n\n    @Override\n    public Map<ThreadId, String> getEventRefs() {\n        synchronized (this) {\n            return Collections.unmodifiableMap(eventRefs);\n        }\n    }\n\n    @Override\n    public ThreadError getThreadError(ThreadId threadId) {\n        synchronized (this) {\n            Object result = threadErrors.get(threadId);\n            return StateBackwardCompatibility.processThreadError(result, threadId);\n        }\n    }\n\n    @Override\n    public void setThreadError(ThreadId threadId, Exception error) {\n        setThreadError(threadId, null, error);\n    }\n\n    @Override\n    public void setThreadError(ThreadId threadId, Command cmd, Exception error) {\n        synchronized (this) {\n            threadErrors.put(threadId, new ThreadError(threadId, cmd, error));\n        }\n    }\n\n    @Override\n    public ThreadError clearThreadError(ThreadId threadId) {\n        synchronized (this) {\n            Object result = threadErrors.remove(threadId);\n            return StateBackwardCompatibility.processThreadError(result, threadId);\n        }\n    }\n\n    @Override\n    public List<StackTraceItem> getStackTrace(ThreadId threadId) {\n        synchronized (this) {\n            // for backward compatibility\n            if (stackTrace == null) {\n                return Collections.emptyList();\n            }\n            List<ThreadId> threads = collectParents(threadId);\n            threads.add(0, threadId);\n\n            List<StackTraceItem> result = new ArrayList<>();\n            threads.forEach(tid -> result.addAll(stackTrace.getOrDefault(tid, Collections.emptyList())));\n            return result;\n        }\n    }\n\n    @Override\n    public void pushStackTraceItem(ThreadId threadId, StackTraceItem item) {\n        synchronized (this) {\n            // for backward compatibility\n            if (stackTrace == null) {\n                return;\n            }\n\n            List<StackTraceItem> l = stackTrace.computeIfAbsent(threadId, key -> new LinkedList<>());\n            l.add(0, item);\n        }\n    }\n\n    @Override\n    public void clearStackTrace(ThreadId threadId) {\n        synchronized (this) {\n            // for backward compatibility\n            if (stackTrace == null) {\n                return;\n            }\n\n            stackTrace.remove(threadId);\n        }\n    }\n\n    @Override\n    public void setThreadLocal(ThreadId threadId, String key, Serializable value) {\n        synchronized (this) {\n            // for backward compatibility\n            if (threadLocals == null) {\n                return;\n            }\n\n            Map<String, Serializable> locals = threadLocals.computeIfAbsent(threadId, v -> new HashMap<>());\n            locals.put(key, value);\n        }\n    }\n\n    @Override\n    public <T extends Serializable> T getThreadLocal(ThreadId threadId, String key) {\n        synchronized (this) {\n            // for backward compatibility\n            if (threadLocals == null) {\n                return null;\n            }\n\n            Map<String, Serializable> locals = threadLocals.get(threadId);\n            if (locals == null) {\n                return null;\n            }\n            return (T) locals.get(key);\n        }\n    }\n\n    @Override\n    public void removeThreadLocal(ThreadId threadId, String key) {\n        synchronized (this) {\n            // for backward compatibility\n            if (threadLocals == null) {\n                return;\n            }\n\n            Map<String, Serializable> locals = threadLocals.get(threadId);\n            locals.remove(key);\n            if (locals.isEmpty()) {\n                threadLocals.remove(threadId);\n            }\n        }\n    }\n\n    @Override\n    public void gc() {\n        synchronized (this) {\n            Stream<ThreadId> done = threadStatus.entrySet().stream()\n                    .filter(e -> e.getValue() == ThreadStatus.DONE)\n                    .map(Map.Entry::getKey);\n\n            Stream<ThreadId> handled = threadStatus.entrySet().stream()\n                    .filter(e -> e.getValue() == ThreadStatus.FAILED)\n                    .filter(e -> !threadErrors.containsKey(e.getKey()))\n                    .map(Map.Entry::getKey);\n\n            Stream.concat(done, handled)\n                    .collect(Collectors.toList()) // avoid races by eagerly calculating the list of IDs\n                    .forEach(k -> {\n                        threadErrors.remove(k);\n                        threadStatus.remove(k);\n                        frames.remove(k);\n                        eventRefs.remove(k);\n                        children.remove(k);\n                        if (stackTrace != null) {\n                            stackTrace.remove(k);\n                        }\n                        if (threadLocals != null) {\n                            threadLocals.remove(k);\n                        }\n                    });\n        }\n    }\n\n    private List<ThreadId> collectParents(ThreadId threadId) {\n        List<ThreadId> result = new ArrayList<>();\n        ThreadId current = threadId;\n        while (true) {\n            ThreadId parent = findParent(current);\n            if (parent != null) {\n                result.add(parent);\n                current = parent;\n            } else {\n                break;\n            }\n        }\n        return result;\n    }\n\n    private ThreadId findParent(ThreadId threadId) {\n        return children.entrySet().stream()\n                .filter(e -> e.getValue().contains(threadId))\n                .map(Map.Entry::getKey)\n                .findFirst()\n                .orElse(null);\n    }\n\n    private void unwindStackTrace(ThreadId threadId, Frame removed) {\n        // for backward compatibility\n        if (stackTrace == null || removed.id() == null) {\n            return;\n        }\n        List<StackTraceItem> items = stackTrace.get(threadId);\n        if (items == null) {\n            return;\n        }\n\n        int itemIndex = -1;\n        for (int i = 0; i < items.size(); i++) {\n            StackTraceItem item = items.get(i);\n            if (removed.id().equals(item.getFrameId())) {\n                itemIndex = i;\n            }\n        }\n\n        if (itemIndex >= 0) {\n            if (itemIndex + 1 == items.size()) {\n                stackTrace.remove(threadId);\n            } else {\n                stackTrace.put(threadId, new LinkedList<>(items.subList(itemIndex + 1, items.size())));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/vm/src/main/java/com/walmartlabs/concord/svm/ParallelExecutionException.java",
    "content": "package com.walmartlabs.concord.svm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.PrintStream;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * An exception that is thrown when multiple exceptions are thrown\n * in {@code parallel} blocks.\n */\n@Deprecated\npublic class ParallelExecutionException extends RuntimeException {\n\n    private static final long serialVersionUID = 1L;\n    private static final int MAX_STACK_TRACE_ELEMENTS = 3;\n    private final List<Exception> exceptions;\n\n    public ParallelExecutionException(Collection<Exception> causes) {\n        super(\"Parallel execution errors: \\n\" + toMessage(causes));\n        this.exceptions = new ArrayList<>(causes);\n    }\n\n    public List<Exception> getExceptions() {\n        return exceptions;\n    }\n\n    private static String toMessage(Collection<Exception> causes) {\n        return causes.stream()\n                .map(ParallelExecutionException::stacktraceToString)\n                .collect(Collectors.joining(\"\\n\"));\n    }\n\n    @Override\n    public void printStackTrace(PrintStream s) {\n        s.println(getMessage());\n    }\n\n    @Override\n    public void printStackTrace(PrintWriter s) {\n        s.println(getMessage());\n    }\n\n    private static String stacktraceToString(Exception e) {\n        StringWriter sw = new StringWriter();\n        sw.append(e.toString()).append(\"\\n\");\n        StackTraceElement[] elements = e.getStackTrace();\n        int maxElements = Math.min(elements.length, MAX_STACK_TRACE_ELEMENTS);\n        for (int i = 0; i < maxElements; i++) {\n            StackTraceElement element = elements[i];\n            sw.append(\"\\tat \").append(element.toString()).append(\"\\n\");\n        }\n        if (maxElements != elements.length) {\n            sw.append(\"\\t...\").append(String.valueOf(elements.length - maxElements)).append(\" more\\n\");\n        }\n        return sw.toString();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/vm/src/main/java/com/walmartlabs/concord/svm/PopFrameCommand.java",
    "content": "package com.walmartlabs.concord.svm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class PopFrameCommand implements Command {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean skipFinallyHandler;\n\n    public PopFrameCommand() {\n        this(false);\n    }\n\n    private PopFrameCommand(boolean ignoreFinallyHandler) {\n        this.skipFinallyHandler = ignoreFinallyHandler;\n    }\n\n    @Override\n    public void eval(Runtime runtime, State state, ThreadId threadId) {\n        if (skipFinallyHandler) {\n            popFrame(state, threadId);\n\n            return;\n        }\n\n        Frame frame = state.peekFrame(threadId);\n        frame.pop();\n\n        Command finallyHandler = frame.getFinallyHandler();\n\n        if (finallyHandler == null) {\n            // no 'finally' block, just pop the frame\n            popFrame(state, threadId);\n\n            return;\n        }\n\n        // actually pop the frame\n        frame.push(new PopFrameCommand(true));\n\n        // execute the 'finally' block\n        frame.push(finallyHandler);\n    }\n\n    private static void popFrame(State state, ThreadId threadId) {\n        state.popFrame(threadId);\n\n        if (state.getStatus(threadId) == ThreadStatus.UNWINDING) {\n            Frame f = state.peekFrame(threadId);\n            if (f != null) {\n                f.push(new PopFrameCommand());\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtime/v2/vm/src/main/java/com/walmartlabs/concord/svm/Runtime.java",
    "content": "package com.walmartlabs.concord.svm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface Runtime {\n\n    /**\n     * Spawns a new \"real\" thread and runs the specified \"vm\" thread in it.\n     */\n    void spawn(State state, ThreadId threadId);\n\n    /**\n     * Runs the specified \"vm\" thread on current java thread.\n     */\n    EvalResult eval(State state, ThreadId threadId) throws Exception;\n\n    /**\n     * Returns an instance of the specified service using the underlying injector.\n     */\n    <T> T getService(Class<T> klass);\n}\n"
  },
  {
    "path": "runtime/v2/vm/src/main/java/com/walmartlabs/concord/svm/RuntimeFactory.java",
    "content": "package com.walmartlabs.concord.svm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface RuntimeFactory {\n\n    Runtime create(VM vm);\n}\n"
  },
  {
    "path": "runtime/v2/vm/src/main/java/com/walmartlabs/concord/svm/StackTraceItem.java",
    "content": "package com.walmartlabs.concord.svm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\n\npublic class StackTraceItem implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final FrameId frameId;\n\n    private final ThreadId threadId;\n\n    private final String fileName;\n\n    private final String flowName;\n\n    private final int lineNumber;\n\n    private final int columnNumber;\n\n    public StackTraceItem(FrameId frameId, ThreadId threadId, String fileName, String flowName, int lineNumber, int columnNumber) {\n        this.frameId = frameId;\n        this.threadId = threadId;\n        this.fileName = fileName;\n        this.flowName = flowName;\n        this.lineNumber = lineNumber;\n        this.columnNumber = columnNumber;\n    }\n\n    public FrameId getFrameId() {\n        return frameId;\n    }\n\n    public ThreadId getThreadId() {\n        return threadId;\n    }\n\n    public String getFileName() {\n        return fileName;\n    }\n\n    public String getFlowName() {\n        return flowName;\n    }\n\n    public int getLineNumber() {\n        return lineNumber;\n    }\n\n    public int getColumnNumber() {\n        return columnNumber;\n    }\n\n    @Override\n    public String toString() {\n        return String.format(\"(%s) @ line: %d, col: %d, thread: %s, flow: %s\",\n                nullToNa(fileName), lineNumber, columnNumber, threadId.id(), flowName);\n    }\n\n    private static String nullToNa(String value) {\n        if (value == null) {\n            return \"n/a\";\n        }\n        return value;\n    }\n\n}\n"
  },
  {
    "path": "runtime/v2/vm/src/main/java/com/walmartlabs/concord/svm/State.java",
    "content": "package com.walmartlabs.concord.svm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Describes the state of the VM.\n */\npublic interface State extends Serializable {\n\n    /**\n     * Adds a frame to the specified thread. The added frame becomes the current frame of the thread.\n     */\n    void pushFrame(ThreadId threadId, Frame frame);\n\n    /**\n     * Returns the current frame of the specified thread. Or {@code null} if there are no frames left.\n     */\n    Frame peekFrame(ThreadId threadId);\n\n    /**\n     * Removes the current frame of the specified thread. The next frame becomes the current frame\n     * of the thread.\n     * @apiNote Prefer using {@link PopFrameCommand} instead of calling this method\n     * directly -- the command supports proper \"finally\" semantics.\n     */\n    void popFrame(ThreadId threadId);\n\n    /**\n     * Returns an unmodifiable list of frames for the specified thread.\n     * The most recent frame is always the first element in the list.\n     */\n    List<Frame> getFrames(ThreadId threadId);\n\n    /**\n     * Removes all frames in all threads effectively stopping the execution.\n     */\n    void dropAllFrames();\n\n    /**\n     * Sets the execution status of the specified thread.\n     */\n    void setStatus(ThreadId threadId, ThreadStatus status);\n\n    /**\n     * Returns the execution status of the specified thread.\n     */\n    ThreadStatus getStatus(ThreadId threadId);\n\n    /**\n     * Returns the root thread ID of the current {@link State} instance.\n     */\n    ThreadId getRootThreadId();\n\n    /**\n     * Creates new \"logical\" thread using the provided parent thread ID and an initial command.\n     */\n    void fork(ThreadId parentThreadId, ThreadId threadId, Command... cmds);\n\n    /**\n     * Returns a snapshot of the current thread statuses.\n     */\n    Map<ThreadId, ThreadStatus> threadStatus();\n\n    /**\n     * Returns a next thread ID. Unique per {@link State} instance.\n     */\n    ThreadId nextThreadId();\n\n    /**\n     * Adds a new event reference to the specified thread. If the thread suspends\n     * those event references can be used to resume it.\n     * The event reference must be unique across all threads in the current {@link State}.\n     */\n    void setEventRef(ThreadId threadId, String eventRef);\n\n    /**\n     * Removes the specified event reference and return the ID of a thread that was\n     * associated with the event. Returns {@code null} if no threads had the specified event.\n     */\n    ThreadId removeEventRef(String eventRef);\n\n    /**\n     * Returns a map of thread IDs and event references associated with those threads.\n     */\n    Map<ThreadId, String> getEventRefs();\n\n    /**\n     * @return the error for the specified thread. Returns {@code null} if no error was set.\n     */\n    ThreadError getThreadError(ThreadId threadId);\n\n    /**\n     * Sets an error for the specified thread. Those errors are used to propagate unhandled\n     * exceptions from child threads to the parent thread with a {@link com.walmartlabs.concord.svm.commands.Join}.\n     */\n    void setThreadError(ThreadId threadId, Exception error);\n\n    void setThreadError(ThreadId threadId, Command cmd, Exception error);\n\n    /**\n     * Clears and returns the thread's error. Null if no error was set.\n     */\n    ThreadError clearThreadError(ThreadId threadId);\n\n    /**\n     * Returns call stack trace for thread id.\n     */\n    List<StackTraceItem> getStackTrace(ThreadId threadId);\n\n    /**\n     * Adds a stack trace item to the specified thread.\n     */\n    void pushStackTraceItem(ThreadId threadId, StackTraceItem item);\n\n    /**\n     * Clears stack trace for the specific thread.\n     */\n    void clearStackTrace(ThreadId threadId);\n\n    /**\n     * Sets a thread-local variable for the specified thread.\n     */\n    void setThreadLocal(ThreadId threadId, String key, Serializable value);\n\n    /**\n     * Retrieves the value of a thread-local variable for the specified thread.\n     */\n    <T extends Serializable> T getThreadLocal(ThreadId threadId, String key);\n\n    /**\n     * Removes a thread-local variable for the specified thread.\n     */\n    void removeThreadLocal(ThreadId threadId, String key);\n\n    /**\n     * Performs state maintenance and cleanup.\n     */\n    void gc();\n}\n"
  },
  {
    "path": "runtime/v2/vm/src/main/java/com/walmartlabs/concord/svm/StateBackwardCompatibility.java",
    "content": "package com.walmartlabs.concord.svm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n/**\n * Backward compatibility for processing old {@link State} instances.\n */\npublic class StateBackwardCompatibility {\n\n    // 2.21.1-SNAPSHOT\n    public static ThreadError processThreadError(Object threadError, ThreadId threadId) {\n        if (threadError == null) {\n            return null;\n        }\n\n        if (threadError instanceof Exception e) {\n            return new ThreadError(threadId, null, e);\n        }\n\n        return (ThreadError) threadError;\n    }\n}\n"
  },
  {
    "path": "runtime/v2/vm/src/main/java/com/walmartlabs/concord/svm/ThreadError.java",
    "content": "package com.walmartlabs.concord.svm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serial;\nimport java.io.Serializable;\n\npublic record ThreadError(ThreadId threadId, Command cmd, Exception exception) implements Serializable {\n\n    @Serial\n    private static final long serialVersionUID = 1L;\n\n    public StackTraceElement[] getStackTrace() {\n        return exception.getStackTrace();\n    }\n}\n"
  },
  {
    "path": "runtime/v2/vm/src/main/java/com/walmartlabs/concord/svm/ThreadId.java",
    "content": "package com.walmartlabs.concord.svm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\nimport java.util.Objects;\n\npublic class ThreadId implements Serializable, Comparable<ThreadId> {\n\n    // for backward compatibility (java8 concord 1.92.0 version)\n    private static final long serialVersionUID = -4315435932994207071L;\n\n    private final long id;\n\n    ThreadId(long id) {\n        this.id = id;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        ThreadId threadId = (ThreadId) o;\n        return id == threadId.id;\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(id);\n    }\n\n    public long id() {\n        return id;\n    }\n\n    @Override\n    public String toString() {\n        return \"ThreadId{\" +\n                \"id=\" + id +\n                '}';\n    }\n\n    @Override\n    public int compareTo(ThreadId o) {\n        return Long.compare(id, o.id);\n    }\n}\n"
  },
  {
    "path": "runtime/v2/vm/src/main/java/com/walmartlabs/concord/svm/ThreadStatus.java",
    "content": "package com.walmartlabs.concord.svm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic enum ThreadStatus {\n\n    /**\n     * Ready for execution or is running.\n     */\n    READY,\n\n    /**\n     * Suspended, waiting for {@link VM#resume(State, java.util.Set)}.\n     */\n    SUSPENDED,\n\n    /**\n     * The thread is being terminated and is currently unwinding the stack.\n     */\n    UNWINDING,\n\n    /**\n     * Completed successfully.\n     */\n    DONE,\n\n    /**\n     * Completed unsuccessfully.\n     */\n    FAILED\n}\n"
  },
  {
    "path": "runtime/v2/vm/src/main/java/com/walmartlabs/concord/svm/VM.java",
    "content": "package com.walmartlabs.concord.svm;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collection;\nimport java.util.Map;\nimport java.util.Set;\n\nimport static com.walmartlabs.concord.svm.ExecutionListener.Result.BREAK;\n\npublic class VM {\n\n    private static final Logger log = LoggerFactory.getLogger(VM.class);\n\n    private final RuntimeFactory runtimeFactory;\n    private final ExecutionListenerHolder listeners;\n\n    public VM(RuntimeFactory runtimeFactory, Collection<ExecutionListener> listeners) {\n        this.runtimeFactory = runtimeFactory;\n        this.listeners = new ExecutionListenerHolder(this, listeners);\n    }\n\n    /**\n     * Starts the execution using the provided state.\n     */\n    public void start(State state) throws Exception {\n        log.debug(\"start -> start\");\n\n        Runtime runtime = runtimeFactory.create(this);\n\n        listeners.fireBeforeProcessStart(runtime, state);\n\n        EvalResult result;\n        try {\n            result = execute(runtime, state);\n        } catch (Exception e) {\n            listeners.fireOnProcessError(runtime, state, e);\n            throw e;\n        }\n\n        listeners.fireAfterProcessEnds(runtime, state, result.lastFrame());\n\n        log.debug(\"start -> done\");\n    }\n\n    /**\n     * Resumes the execution using the provided state and an event reference.\n     */\n    public void resume(State state, Set<String> eventRefs) throws Exception {\n        log.debug(\"resume ['{}'] -> start\", eventRefs);\n\n        eventRefs.forEach(eventRef -> {\n            ThreadId eventThreadId = state.removeEventRef(eventRef);\n            if (eventThreadId == null) {\n                throw new IllegalStateException(\"Can't find eventRef: \" + eventRef);\n            }\n        });\n\n        wakeSuspended(state);\n\n        Runtime runtime = runtimeFactory.create(this);\n\n        listeners.fireBeforeProcessResume(runtime, state);\n\n        EvalResult result;\n        try {\n            result = execute(runtime, state);\n        } catch (Exception e) {\n            listeners.fireOnProcessError(runtime, state, e);\n            throw e;\n        }\n\n        listeners.fireAfterProcessEnds(runtime, state, result.lastFrame());\n\n        log.debug(\"resume ['{}'] -> done\", eventRefs);\n    }\n\n    /**\n     * Executes a single command using the provided state.\n     * Doesn't fire any {@link #listeners} and doesn't unwind the stack in\n     * case or errors.\n     */\n    public void run(State state, Command cmd) throws Exception {\n        log.debug(\"run ['{}'] -> start\", cmd);\n\n        Runtime rt = runtimeFactory.create(this);\n        ThreadId threadId = state.getRootThreadId();\n        cmd.eval(rt, state, threadId);\n\n        log.debug(\"run ['{}'] -> done\", cmd);\n    }\n\n    public EvalResult eval(Runtime runtime, State state, ThreadId threadId) throws Exception {\n        Frame lastFrame = null;\n\n        try {\n            while (true) {\n                ThreadStatus status = state.getStatus(threadId);\n                if (status == ThreadStatus.DONE) {\n                    throw new IllegalStateException(\"The thread is already complete: \" + threadId);\n                } else if (status == ThreadStatus.SUSPENDED) {\n                    log.trace(\"eval [{}] -> the thread is suspended, stopping the execution\", threadId);\n                    break;\n                } else if (status == ThreadStatus.FAILED) {\n                    throw new IllegalStateException(\"The thread has failed, can't continue the execution: \" + threadId);\n                }\n\n                Frame frame = state.peekFrame(threadId);\n                if (frame == null) {\n                    if (status == ThreadStatus.UNWINDING) {\n                        // no more frames to unwind, looks like there is no exception handler\n                        state.setStatus(threadId, ThreadStatus.FAILED);\n                        ThreadError threadError = state.getThreadError(threadId);\n                        if (threadError == null) {\n                            throw new IllegalStateException(\"The thread is unwinding the stack but no error was set: \" + threadId);\n                        }\n                        throw threadError.exception();\n                    }\n\n                    log.trace(\"eval [{}] -> the thread is done, stopping the execution\", threadId);\n                    state.setStatus(threadId, ThreadStatus.DONE);\n                    break;\n                }\n\n                if (status == ThreadStatus.UNWINDING) {\n                    Command handler = frame.getExceptionHandler();\n                    if (handler != null) {\n                        ThreadError threadError = state.clearThreadError(threadId);\n                        if (threadError == null) {\n                            throw new IllegalStateException(\"The thread is unwinding the stack but no error was set: \" + threadId);\n                        }\n\n                        state.setStatus(threadId, ThreadStatus.READY);\n\n                        // avoid issues with exceptions thrown in exception handlers\n                        frame.clearExceptionHandler();\n\n                        // save the exception as a local frame variable, so it can be retrieved\n                        // by the error handling command\n                        frame.setLocal(Frame.LAST_EXCEPTION_KEY, threadError.exception());\n\n                        // and run the error handler next\n                        frame.push(handler);\n                    }\n                }\n\n                lastFrame = frame;\n\n                Command cmd = frame.peek();\n\n                if (cmd == null) {\n                    log.trace(\"eval [{}] -> the frame is complete\", threadId);\n                    frame.push(new PopFrameCommand());\n                    continue;\n                }\n\n                boolean stop = false;\n                boolean callListeners = status != ThreadStatus.UNWINDING;\n\n                try {\n                    if (callListeners && listeners.fireBeforeCommand(runtime, state, threadId, cmd) == BREAK) {\n                        stop = true;\n                    }\n\n                    try {\n                        cmd.eval(runtime, state, threadId);\n                    } catch (Exception e) {\n                        if (callListeners && listeners.fireAfterCommandWithError(runtime, state, threadId, cmd, e) == BREAK) {\n                            stop = true;\n                        }\n                        throw e;\n                    }\n\n                    if (callListeners && listeners.fireAfterCommand(runtime, state, threadId, cmd) == BREAK) {\n                        stop = true;\n                    }\n                } catch (Exception e) {\n                    frame.push(new PopFrameCommand());\n                    state.setStatus(threadId, ThreadStatus.UNWINDING);\n                    state.setThreadError(threadId, cmd, e);\n                }\n\n                if (stop) {\n                    break;\n                }\n            }\n        } finally {\n            state.gc();\n        }\n\n        return new EvalResult(lastFrame);\n    }\n\n    private EvalResult execute(Runtime runtime, State state) throws Exception {\n        EvalResult result;\n\n        while (true) {\n            // if we're restoring from a previously saved state or we had new threads created\n            // on the previous iteration we need to spawn all READY threads\n            for (Map.Entry<ThreadId, ThreadStatus> e : state.threadStatus().entrySet()) {\n                if (e.getKey() != state.getRootThreadId() && e.getValue() == ThreadStatus.READY) {\n                    runtime.spawn(state, e.getKey());\n                }\n            }\n\n            result = eval(runtime, state, state.getRootThreadId());\n\n            if (listeners.fireAfterEval(runtime, state) == BREAK) {\n                break;\n            }\n\n            wakeSuspended(state);\n\n            if (listeners.fireAfterWakeUp(runtime, state) == BREAK) {\n                break;\n            }\n        }\n\n        return result;\n    }\n\n    private static void wakeSuspended(State state) {\n        Map<ThreadId, String> events = state.getEventRefs();\n        for (Map.Entry<ThreadId, ThreadStatus> e : state.threadStatus().entrySet()) {\n            if (!events.containsKey(e.getKey()) && e.getValue() == ThreadStatus.SUSPENDED) {\n                state.setStatus(e.getKey(), ThreadStatus.READY);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "sdk/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-sdk</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n</project>\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/ApiConfiguration.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n/**\n * Provides basic configuration details of Concord API.\n * Can be injected into a task plugin using {@code @Inject}.\n */\npublic interface ApiConfiguration {\n\n    /**\n     * @return the base URL of the API, e.g. https://concord.example.com/\n     */\n    String getBaseUrl();\n\n    /**\n     * @return connection timeout (ms)\n     */\n    int connectTimeout();\n\n    /**\n     * @return socket read timeout (ms)\n     */\n    int readTimeout();\n\n    /**\n     * @deprecated\n     * @return the current session token that can be used to talk to the API.\n     * @apiNote can still be used for compatibility with v1 tasks while running in the runtime v1.\n     */\n    @Deprecated\n    String getSessionToken(Context ctx);\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/ClientException.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic class ClientException extends Exception {\n\n    // for backward compatibility (java8 concord 1.92.0 version)\n    private static final long serialVersionUID = 2690318957293887651L;\n\n    private final String instanceId;\n\n    public ClientException(String message) {\n        this(null, message, null);\n    }\n\n    public ClientException(String message, Throwable cause) {\n        this(null, message, cause);\n    }\n\n    public ClientException(String instanceId, String message, Throwable cause) {\n        super(message, cause);\n        this.instanceId = instanceId;\n    }\n\n    public String getInstanceId() {\n        return instanceId;\n    }\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/Constants.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\n/**\n * Project and process constants.\n */\npublic class Constants {\n\n    /**\n     * Process context variables.\n     */\n    public static class Context {\n\n        /**\n         * Execution context.\n         */\n        public static final String CONTEXT_KEY = \"context\";\n\n        /**\n         * Path (a string) to a local directory which contains agent's payload.\n         */\n        public static final String WORK_DIR_KEY = \"workDir\";\n\n        /**\n         * ID of the current process instance.\n         */\n        public static final String TX_ID_KEY = \"txId\";\n\n        /**\n         * List of OUT variables of a process.\n         */\n        public static final String OUT_EXPRESSIONS_KEY = \"_out\";\n\n        /**\n         * Stores the last caught exception.\n         */\n        public static final String LAST_ERROR_KEY = \"lastError\";\n\n        /**\n         * Execution context.\n         *\n         * @deprecated use {@link Constants.Context#CONTEXT_KEY}\n         */\n        @Deprecated\n        public static final String EXECUTION_CONTEXT_KEY = \"execution\";\n\n        /**\n         * Correlation ID of process events. Can be used to create \"pre-\" and \"post-action\" event records.\n         */\n        public static final String EVENT_CORRELATION_KEY = \"__eventCorrelationId\";\n\n        /**\n         * \"pre-\" event creation time. Can be used to calculate event durations.\n         */\n        public static final String EVENT_CREATED_AT_KEY = \"__eventCreatedAt\";\n\n        /**\n         * The maximum number of retries of the current `retry` block.\n         */\n        public static final String RETRY_COUNTER = \"__retryCount\";\n\n        /**\n         * The current number of retries of the current `retry` block.\n         */\n        public static final String CURRENT_RETRY_COUNTER = \"__currentRetryCount\";\n    }\n\n    /**\n     * Request data keys.\n     */\n    public static class Request {\n\n        public static final String CONFIGURATION_KEY = \"configuration\";\n\n        /**\n         * Runtime to use.\n         */\n        public static final String RUNTIME_KEY = \"runtime\";\n\n        /**\n         * Key of a process arguments object in a request data JSON.\n         */\n        public static final String ARGUMENTS_KEY = \"arguments\";\n\n        /**\n         * Key of a process dependencies list in a request data JSON.\n         */\n        public static final String DEPENDENCIES_KEY = \"dependencies\";\n\n        /**\n         * Key of a process dependencies list in a request data JSON.\n         */\n        public static final String EXTRA_DEPENDENCIES_KEY = \"extraDependencies\";\n\n        /**\n         * Process entry point.\n         */\n        public static final String ENTRY_POINT_KEY = \"entryPoint\";\n\n        /**\n         * Name of the default entry point.\n         */\n        public static final String DEFAULT_ENTRY_POINT_NAME = \"default\";\n\n        /**\n         * Run process in dry-run mode?.\n         */\n        public static final String DRY_RUN_MODE_KEY = \"dryRun\";\n\n        /**\n         * Active profiles.\n         */\n        public static final String ACTIVE_PROFILES_KEY = \"activeProfiles\";\n\n        /**\n         * Template name.\n         */\n        public static final String TEMPLATE_KEY = \"template\";\n\n        /**\n         * Process initiator's info.\n         */\n        public static final String INITIATOR_KEY = \"initiator\";\n\n        /**\n         * Process current user's info.\n         */\n        public static final String CURRENT_USER_KEY = \"currentUser\";\n\n        /**\n         * User request's metadata.\n         */\n        public static final String REQUEST_INFO_KEY = \"requestInfo\";\n\n        /**\n         * Project's metadata\n         */\n        public static final String PROJECT_INFO_KEY = \"projectInfo\";\n\n        /**\n         * Process's metadata\n         */\n        public static final String PROCESS_INFO_KEY = \"processInfo\";\n\n        /**\n         * Process tags.\n         */\n        public static final String TAGS_KEY = \"tags\";\n\n        /**\n         * ID of a parent process instance.\n         */\n        public static final String PARENT_INSTANCE_ID_KEY = \"parentInstanceId\";\n\n        /**\n         * If {@code true}, then `onCancel` flow will be ignored.\n         */\n        public static final String DISABLE_ON_CANCEL_KEY = \"disableOnCancel\";\n\n        /**\n         * If {@code true}, then `onFailure` flow will be ignored.\n         */\n        public static final String DISABLE_ON_FAILURE_KEY = \"disableOnFailure\";\n\n        /**\n         * If {@code true}, then `onTimeout` flow will be ignored.\n         */\n        public static final String DISABLE_ON_TIMEOUT_KEY = \"disableOnTimeout\";\n\n        /**\n         * Declares a list of OUT variables or expressions.\n         */\n        public static final String OUT_EXPRESSIONS_KEY = \"out\";\n\n        /**\n         * Enables additional debug logging for various states of execution.\n         */\n        public static final String DEBUG_KEY = \"debug\";\n\n        /**\n         * Schedules a process on the specified date and time.\n         */\n        public static final String START_AT_KEY = \"startAt\";\n\n        /**\n         * Process requirements (e.g. agent flavor, JVM args, etc).\n         */\n        public static final String REQUIREMENTS = \"requirements\";\n\n        /**\n         * Process metadata.\n         */\n        public static final String META = \"meta\";\n\n        /**\n         * Container configuration.\n         * @deprecated the container-per-process execution mode is deprecated.\n         */\n        @Deprecated\n        public static final String CONTAINER = \"container\";\n\n        /**\n         * Process timeout.\n         */\n        public static final String PROCESS_TIMEOUT = \"processTimeout\";\n\n        /**\n         * Handler process timeout.\n         */\n        public static final String HANDLER_PROCESS_TIMEOUT = \"handlerProcessTimeout\";\n\n        /**\n         * Timeout for process in Suspended state.\n         */\n        public static final String SUSPEND_TIMEOUT = \"suspendTimeout\";\n\n        /**\n         * A specific GIT commit ID to use.\n         */\n        public static final String REPO_COMMIT_ID = \"repoCommitId\";\n\n        /**\n         * A specific branch name or a tag to use.\n         */\n        public static final String REPO_BRANCH_OR_TAG = \"repoBranchOrTag\";\n\n        /**\n         * Exclusive params.\n         */\n        public static final String EXCLUSIVE = \"exclusive\";\n\n        /**\n         * Resume event name. Filled in for processes resumed after being suspended.\n         * @deprecated see {@link #RESUME_EVENTS_KEY}\n         */\n        @Deprecated\n        public static final String EVENT_NAME_KEY = \"resumeEventName\";\n\n        /**\n         * Resume events. Filled in for processes resumed after being suspended.\n         */\n        public static final String RESUME_EVENTS_KEY = \"resumeEvents\";\n\n        /**\n         * The runner's configuration section.\n         */\n        public static final String RUNNER_KEY = \"runner\";\n\n        /**\n         * The process' session token that can be used to talk to the API.\n         */\n        public static final String SESSION_TOKEN_KEY = \"sessionToken\";\n    }\n\n    public static class Trigger {\n\n        /**\n         * Cron expression.\n         */\n        public static final String CRON_SPEC = \"spec\";\n\n        /**\n         * Cron target time zone.\n         */\n        public static final String CRON_TIMEZONE = \"timezone\";\n\n        /**\n         * The time a cron event was scheduled for.\n         */\n        public static final String CRON_EVENT_FIREAT = \"fireAt\";\n\n        /**\n         * Whether to use the trigger's initiator as the process' initiator.\n         */\n        public static final String USE_INITIATOR = \"useInitiator\";\n\n        /**\n         * Use the trigger's commit id to start the process.\n         */\n        public static final String USE_EVENT_COMMIT_ID = \"useEventCommitId\";\n\n        /**\n         * Ignore empty {@code push} notifications\n         * (events with \"after\" and \"before\" pointing to the same commit ID).\n         */\n        public static final String IGNORE_EMPTY_PUSH = \"ignoreEmptyPush\";\n\n        /**\n         * Used to match on the registered repositories in GitHub triggers.\n         */\n        public static final String REPOSITORY_INFO = \"repositoryInfo\";\n\n        /**\n         * {@code conditions} field. Used to match the specified parameters\n         * with the incoming event's payload.\n         */\n        public static final String CONDITIONS = \"conditions\";\n\n        /**\n         * {@code version} field. Used to specify the trigger's syntax version.\n         */\n        public static final String VERSION = \"version\";\n    }\n\n    /**\n     * Process metadata keys.\n     */\n    public static class Meta {\n\n        /**\n         * Internal metadata group.\n         */\n        public static final String SYSTEM_GROUP = \"_system\";\n\n        /**\n         * ID of the request which created the process.\n         */\n        public static final String REQUEST_ID = \"requestId\";\n    }\n\n    /**\n     * Project files and directories.\n     */\n    public static class Files {\n\n        /**\n         * Directory which contains payload data.\n         */\n        @Deprecated\n        public static final String PAYLOAD_DIR_NAME = \"payload\";\n\n        /**\n         * File which contains the ID of a process.\n         */\n        public static final String INSTANCE_ID_FILE_NAME = \"_instanceId\";\n\n        /**\n         * Directory containing libraries (dependencies) of a payload.\n         */\n        public static final String LIBRARIES_DIR_NAME = \"lib\";\n\n        /**\n         * Directories which contain process definitions of a payload.\n         */\n        public static final String[] DEFINITIONS_DIR_NAMES = {\"flows\", \"processes\"}; // NOSONAR\n\n        /**\n         * Directory with Concord project definitions.\n         */\n        public static final String PROJECT_FILES_DIR_NAME = \"concord\";\n\n        /**\n         * Directory containing process profiles.\n         */\n        public static final String PROFILES_DIR_NAME = \"profiles\";\n\n        /**\n         * File which contains request data of a payload: process arguments, entry point name, etc.\n         * @deprecated see {@link #CONFIGURATION_FILE_NAME}\n         */\n        @Deprecated\n        public static final String REQUEST_DATA_FILE_NAME = \"_main.json\";\n\n        /**\n         * File which contains process configuration: process arguments, entry point name, etc.\n         */\n        public static final String CONFIGURATION_FILE_NAME = \"_main.json\";\n\n        /**\n         * Directory which contains job \"attachments\": reports, stats, etc.\n         */\n        public static final String JOB_ATTACHMENTS_DIR_NAME = \"_attachments\";\n\n        /**\n         * Directory which contains job \"checkpoints\": reports, stats, etc.\n         */\n        public static final String JOB_CHECKPOINTS_DIR_NAME = \"_checkpoints\";\n\n        /**\n         * Directory which contains job \"session files\":\n         *  files that can be downloaded only with session key.\n         */\n        public static final String JOB_SESSION_FILES_DIR_NAME = \"_session_files\";\n\n        /**\n         * Directory which contains process' state.\n         */\n        public static final String JOB_STATE_DIR_NAME = \"_state\";\n\n        /**\n         * Directory which contains process' forms.\n         */\n        public static final String JOB_FORMS_DIR_NAME = \"forms\";\n\n        /**\n         * Directory which contains process' V2 forms.\n         */\n        public static final String JOB_FORMS_V2_DIR_NAME = \"V2forms\";\n\n        /**\n         * Directory which contains submitted form files.\n         */\n        public static final String FORM_FILES = \"_form_files\";\n\n        /**\n         * File which contains data of process' OUT variables.\n         */\n        public static final String OUT_VALUES_FILE_NAME = \"out.json\";\n\n        /**\n         * Marker file, indicating that a process was suspended.\n         * It contains the list of waiting events.\n         */\n        public static final String SUSPEND_MARKER_FILE_NAME = \"_suspend\";\n\n        /**\n         * Marker file, indicating that a process should be resumed.\n         * It contains the name of a resuming event.\n         */\n        public static final String RESUME_MARKER_FILE_NAME = \"_resume\";\n\n        /**\n         * Snapshot of the process' variables, taken each time the process stops.\n         */\n        public static final String LAST_KNOWN_VARIABLES_FILE_NAME = \"_lastVariables\";\n\n        /**\n         * The last unhandled error of the process, serialized to a file.\n         */\n        public static final String LAST_ERROR_FILE_NAME = \"_lastError\";\n\n        /**\n         * Concord system files.\n         */\n        public static final String CONCORD_SYSTEM_DIR_NAME = \".concord\";\n\n        /**\n         * Concord TMP files.\n         */\n        public static final String CONCORD_TMP_DIR_NAME = \".concord_tmp\";\n\n        /**\n         * File which contains process session token.\n         * @deprecated use {@code ProcessConfiguration#sessionToken()}\n         */\n        @Deprecated\n        public static final String SESSION_TOKEN_FILE_NAME = \".session_token\";\n\n        /**\n         * File which contains custom error messages for forms.\n         */\n        public static final String ERROR_MESSAGES_FILE_NAME = \"locale.properties\";\n\n        /**\n         * File which contains checkpoint metadata.\n         */\n        public static final String CHECKPOINT_META_FILE_NAME = \".checkpoint\";\n\n        /**\n         * Policy file.\n         */\n        public static final String POLICY_FILE_NAME = \"policy.json\";\n\n        /**\n         * Properties file with a list of default dependency versions.\n         * @deprecated replaced with the \"dependencyVersions\" policy.\n         */\n        @Deprecated\n        public static final String DEPENDENCY_VERSIONS_FILE_NAME = \"dependencyversions.properties\";\n\n        /**\n         * File which contains sensitive data of process.\n         */\n        public static final String SENSITIVE_DATA_FILE_NAME = \"sensitive_data.json\";\n    }\n\n    public static class Flows {\n\n        /**\n         * Failure-handling flow.\n         */\n        public static final String ON_FAILURE_FLOW = \"onFailure\";\n\n        /**\n         * Cancel-handling flow.RUN_AS\n         */\n        public static final String ON_CANCEL_FLOW = \"onCancel\";\n\n        /**\n         * Timeout-handling flow.\n         */\n        public static final String ON_TIMEOUT_FLOW = \"onTimeout\";\n    }\n\n    public static class Forms {\n\n        /**\n         * The form wizard will stop on the form with {@code yield=true}.\n         */\n        public static final String YIELD_KEY = \"yield\";\n\n        public static final String FIELDS_KEY = \"fields\";\n\n        /**\n         * Additional values provided for the form.\n         */\n        public static final String VALUES_KEY = \"values\";\n\n        /**\n         * User qualifiers of forms.\n         */\n        public static final String RUN_AS_KEY = \"runAs\";\n\n        public static final String RUN_AS_USERNAME_KEY = \"username\";\n\n        public static final String RUN_AS_LDAP_KEY = \"ldap\";\n\n        public static final String RUN_AS_GROUP_KEY = \"group\";\n\n        public static final String RUN_AS_KEEP_KEY = \"keep\";\n\n        /**\n         * Form data field containing the submitter's user data.\n         */\n        public static final String SUBMITTED_BY_KEY = \"submittedBy\";\n\n        /**\n         * If {@code true} then the submitter's data will be stored in the {@link Forms#SUBMITTED_BY_KEY} field.\n         */\n        public static final String SAVE_SUBMITTED_BY_KEY = \"saveSubmittedBy\";\n    }\n\n    /**\n     * Typical part names in multipart/form-data requests, e.g. the start process request.\n     */\n    public static class Multipart {\n\n        public static final String PROJECT_ID = \"projectId\";\n\n        // Contains list of project ids\n        public static final String PROJECT_IDS = \"projectIds\";\n\n        public static final String PROJECT_NAMES = \"projects\";\n\n        public static final String PROJECT_NAME = \"project\";\n\n        public static final String STORE_TYPE = \"storeType\";\n\n        public static final String PUBLIC = \"public\";\n\n        public static final String PRIVATE = \"private\";\n\n        public static final String DATA = \"data\";\n\n        public static final String NAME = \"name\";\n\n        public static final String USERNAME = \"username\";\n\n        public static final String PASSWORD = \"password\"; // NOSONAR\n\n        public static final String TYPE = \"type\";\n\n        public static final String GENERATE_PASSWORD = \"generatePassword\"; // NOSONAR\n\n        public static final String STORE_PASSWORD = \"storePassword\"; // NOSONAR\n\n        public static final String NEW_STORE_PASSWORD = \"newStorePassword\";\n\n        public static final String VISIBILITY = \"visibility\";\n\n        public static final String PARENT_INSTANCE_ID = \"parentInstanceId\";\n\n        public static final String ENTRY_POINT = \"entryPoint\";\n\n        public static final String ORG_ID = \"orgId\";\n\n        public static final String ORG_NAME = \"org\";\n\n        public static final String OUT_EXPR = \"out\";\n\n        public static final String REPO_ID = \"repoId\";\n\n        public static final String REPO_NAME = \"repo\";\n\n        public static final String SYNC = \"sync\";\n\n        public static final String META = \"meta\";\n\n        public static final String ORDER_ID = \"orderId\";\n    }\n\n    public static class Headers {\n\n        public static final String SESSION_TOKEN = \"X-Concord-SessionToken\";\n\n        public static final String SECRET_TYPE = \"X-Concord-SecretType\";\n\n        public static final String ENABLE_HTTP_SESSION = \"X-Concord-EnableSession\";\n\n        public static final String REMEMBER_ME_HEADER = \"X-Concord-RememberMe\";\n    }\n\n    /**\n     * Agent parameters.\n     */\n    public static class Agent {\n\n        /**\n         * File which contains runtime parameters for agents: heap size, JVM arguments, etc.\n         */\n        public static final String AGENT_PARAMS_FILE_NAME = \"_agent.json\";\n\n        /**\n         * JVM parameters for an agent's job.\n         */\n        public static final String JVM_ARGS_KEY = \"jvmArgs\";\n    }\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/Context.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.UUID;\n\n/**\n * Process execution context.\n */\npublic interface Context {\n\n    /**\n     * Returns a process variable {@code key} or {@code null}, if no such variable exists.\n     *\n     * @param key\n     * @return\n     */\n    Object getVariable(String key);\n\n    /**\n     * Sets the value of the specified variable.\n     *\n     * @param key\n     * @param value\n     */\n    void setVariable(String key, Object value);\n\n    /**\n     * Removes the specified variable if it exists.\n     *\n     * @param key\n     */\n    void removeVariable(String key);\n\n    /**\n     * Sets a \"protected\" variable. Such variables can be set only by the tasks that are\n     * listed in \"protectedTask\" category in the process' policy.\n     *\n     * @param key\n     * @param value\n     */\n    void setProtectedVariable(String key, Object value);\n\n    /**\n     * Returns the value of a \"protected\" variable.\n     *\n     * @param key\n     * @return\n     * @see #setProtectedVariable(String, Object)\n     */\n    Object getProtectedVariable(String key);\n\n    /**\n     * Returns names of all existing variables of the process.\n     *\n     * @return\n     */\n    Set<String> getVariableNames();\n\n    /**\n     * Evaluates the expression, returning the results of the specified type.\n     *\n     * @param expr\n     * @param type\n     * @param <T>\n     * @return\n     */\n    <T> T eval(String expr, Class<T> type);\n\n    /**\n     * \"Interpolates\" the specified value, resolving all variables.\n     * All expression strings will be evaluated and replaced with resulting values.\n     *\n     * @param v\n     * @return\n     */\n    Object interpolate(Object v);\n\n    /**\n     * \"Interpolates\" the specified value, using the specified map's keys as\n     * variables. All expression strings will be evaluated and replaced with\n     * resulting values.\n     *\n     * @param v\n     * @param variables\n     * @return\n     */\n    Object interpolate(Object v, Map<String, Object> variables);\n\n    /**\n     * Returns process variables as a {@code Map}.\n     *\n     * @return\n     */\n    Map<String, Object> toMap();\n\n    /**\n     * Requests process suspension. After the task is finished, the process will be suspended and can be resumed\n     * with the specified {@code eventName}.\n     * <p>\n     * Calling {@code suspend(null)} will \"undo\" the process suspension request.\n     *\n     * @param eventName name of the event which can be used to resume the process. Should be unique for the process.\n     */\n    void suspend(String eventName);\n\n    /**\n     * Reserved for future use.\n     *\n     * @see #suspend(String)\n     */\n    void suspend(String eventName, Object payload, boolean resumeFromSameStep);\n\n    /**\n     * Creates a new form and suspends the process.\n     *\n     * @param formName    the form's name\n     * @param formOptions the form's options. The data structure is the same as the form call's syntax.\n     */\n    void form(String formName, Map<String, Object> formOptions);\n\n    /**\n     * Returns the ID of a current process definition (flow).\n     *\n     * @return\n     */\n    String getProcessDefinitionId();\n\n    /**\n     * Returns the ID of a current process element (flow step).\n     *\n     * @return\n     */\n    String getElementId();\n\n    /**\n     * Returns the ID of the parent process event. Can be {@code null}.\n     *\n     * @return\n     */\n    default UUID getEventCorrelationId() {\n        throw new IllegalStateException(\"Not supported\");\n    }\n\n    /**\n     * Return current flow name.\n     */\n    String getCurrentFlowName();\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/ContextUtils.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic final class ContextUtils {\n\n    public static UUID getUUID(Context ctx, String name) {\n        Object o = ctx.getVariable(name);\n        if (o == null) {\n            return null;\n        }\n        if (o instanceof String) {\n            return UUID.fromString((String) o);\n        }\n        if (o instanceof UUID) {\n            return (UUID) o;\n        }\n        throw new IllegalArgumentException(\"Invalid variable '\" + name + \"' type, expected: string/uuid, got: \" + o.getClass());\n    }\n\n    public static int getInt(Context ctx, String name, int defaultValue) {\n        Integer result = getVariable(ctx, name, defaultValue, Integer.class);\n        if (result == null) {\n            return defaultValue;\n        }\n        return result;\n    }\n\n    public static Number getNumber(Context ctx, String name, Number defaultValue) {\n        return getVariable(ctx, name, defaultValue, Number.class);\n    }\n\n    public static String getString(Context ctx, String name) {\n        return getString(ctx, name, null);\n    }\n\n    public static String getString(Context ctx, String name, String defaultValue) {\n        return getVariable(ctx, name, defaultValue, String.class);\n    }\n\n    public static boolean getBoolean(Context ctx, String name, boolean defaultValue) {\n        Boolean result = getVariable(ctx, name, defaultValue, Boolean.class);\n        if (result == null) {\n            return defaultValue;\n        }\n        return result;\n    }\n\n    public static <K, V> Map<K, V> getMap(Context ctx, String key) {\n        return getMap(ctx, key, null);\n    }\n\n    public static <K, V> Map<K, V> getMap(Context ctx, HasKey k, Map<K, V> defaultValue) {\n        return getMap(ctx, k.getKey(), defaultValue);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <K, V> Map<K, V> getMap(Context ctx, String name, Map<K, V> defaultValue) {\n        return getVariable(ctx, name, defaultValue, Map.class);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <E> List<E> getList(Context ctx, String name, List<E> defaultValue) {\n        return getVariable(ctx, name, defaultValue, List.class);\n    }\n\n    public static int assertInt(Context ctx, String name) {\n        return assertVariable(ctx, name, Integer.class);\n    }\n\n    public static Number assertNumber(Context ctx, String name) {\n        return assertVariable(ctx, name, Number.class);\n    }\n\n    public static String assertString(Context ctx, String name) {\n        return assertVariable(ctx, name, String.class);\n    }\n\n    public static String assertString(String message, Context ctx, String name) {\n        return assertVariable(message, ctx, name, String.class);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <K, V> Map<K, V> assertMap(Context ctx, String name) {\n        return assertVariable(ctx, name, Map.class);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <E> List<E> assertList(Context ctx, String name) {\n        return assertVariable(ctx, name, List.class);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <T> T getVariable(Context ctx, String name, T defaultValue) {\n        if (ctx == null) {\n            return defaultValue;\n        }\n\n        T result = (T) ctx.getVariable(name);\n\n        if (result != null) {\n            return result;\n        }\n\n        return defaultValue;\n    }\n\n    public static <T> T getVariable(Context ctx, String name, T defaultValue, Class<T> type) {\n        Object value = getVariable(ctx, name, defaultValue);\n        if (value == null) {\n            return null;\n        }\n\n        if (type.isInstance(value)) {\n            return type.cast(value);\n        }\n\n        throw new IllegalArgumentException(\"Invalid variable '\" + name + \"' type, expected: \" + type + \", got: \" + value.getClass());\n    }\n\n    public static <T> T assertVariable(Context ctx, String name, Class<T> type) {\n        return assertVariable(null, ctx, name, type);\n    }\n\n    public static <T> T assertVariable(String message, Context ctx, String name, Class<T> type) {\n        T result = getVariable(ctx, name, null, type);\n\n        if (result != null) {\n            return result;\n        }\n\n        throw new IllegalArgumentException(message != null ? message : \"Mandatory variable '\" + name + \"' is required\");\n    }\n\n    public static ProjectInfo getProjectInfo(Context ctx) {\n        Map<String, Object> pi = getMap(ctx, Constants.Request.PROJECT_INFO_KEY, null);\n        if (pi == null) {\n            return null;\n        }\n\n        UUID projectId = MapUtils.getUUID(pi, \"projectId\");\n        if (projectId == null) {\n            return null;\n        }\n\n        return ImmutableProjectInfo.builder()\n                .orgId(MapUtils.assertUUID(pi, \"orgId\"))\n                .orgName(MapUtils.getString(pi, \"orgName\"))\n                .id(projectId)\n                .name(MapUtils.getString(pi, \"projectName\"))\n                .build();\n    }\n\n    public static RepositoryInfo getRepositoryInfo(Context ctx) {\n        Map<String, Object> pi = getMap(ctx, Constants.Request.PROJECT_INFO_KEY, null);\n        if (pi == null) {\n            return null;\n        }\n\n        UUID repoId = MapUtils.getUUID(pi, \"repoId\");\n        if (repoId == null) {\n            return null;\n        }\n\n        return ImmutableRepositoryInfo.builder()\n                .id(repoId)\n                .name(MapUtils.getString(pi, \"repoName\"))\n                .url(MapUtils.getString(pi, \"repoUrl\"))\n                .build();\n    }\n\n    public static Path getWorkDir(Context ctx) {\n        Object workDir = assertVariable(ctx, Constants.Context.WORK_DIR_KEY, Object.class);\n        if (workDir instanceof Path) {\n            return (Path) workDir;\n        }\n        if (workDir instanceof String) {\n            return Paths.get((String) workDir);\n        }\n        throw new IllegalArgumentException(\"Invalid variable '\" + Constants.Context.WORK_DIR_KEY + \"' type, expected: string/path, got: \" + workDir.getClass());\n    }\n\n    public static UUID getTxId(Context ctx) {\n        Object txId = assertVariable(ctx, Constants.Context.TX_ID_KEY, Object.class);\n        if (txId instanceof String) {\n            return UUID.fromString((String) txId);\n        }\n        if (txId instanceof UUID) {\n            return (UUID) txId;\n        }\n        throw new IllegalArgumentException(\"Invalid variable '\" + Constants.Context.TX_ID_KEY + \"' type, expected: string/uuid, got: \" + txId.getClass());\n    }\n\n    public static String getSessionToken(Context ctx) {\n        String result = sessionTokenOrNull(ctx);\n        if (result == null) {\n            throw new IllegalArgumentException(\"Session key not found in the process info: \" + ctx.getVariable(Constants.Request.PROCESS_INFO_KEY));\n        }\n        return result;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static String sessionTokenOrNull(Context ctx) {\n        Map<String, Object> processInfo = (Map<String, Object>) ctx.getVariable(Constants.Request.PROCESS_INFO_KEY);\n        if (processInfo == null) {\n            return null;\n        }\n        return (String) processInfo.get(\"sessionKey\");\n    }\n\n    private ContextUtils() {\n    }\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/DependencyManager.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Path;\n\n/**\n * Provides a way for the runtime and plugins to retrieve and cache\n * various external artifacts.\n * Supports all dependency types as the regular \"dependencies\" configuration.\n */\npublic interface DependencyManager {\n\n    /**\n     * Downloads the URL or returns a previously cached copy.\n     */\n    Path resolve(URI uri) throws IOException;\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/DockerContainerSpec.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.util.List;\nimport java.util.Map;\n\n@Value.Immutable\npublic interface DockerContainerSpec {\n\n    String image();\n\n    @Nullable\n    String name();\n\n    @Nullable\n    String user();\n\n    @Nullable\n    String workdir();\n\n    @Nullable\n    String entryPoint();\n\n    @Nullable\n    String cpu();\n\n    @Nullable\n    String memory();\n\n    @Nullable\n    String stdOutFilePath();\n\n    @Nullable\n    List<String> args();\n\n    @Nullable\n    Map<String, String> env();\n\n    @Nullable\n    String envFile();\n\n    @Nullable\n    Map<String, String> labels();\n\n    @Nullable\n    Options options();\n\n    @Value.Default\n    default int pullRetryCount() {\n        return 3;\n    }\n\n    @Value.Default\n    default long pullRetryInterval() {\n        return 10_000;\n    }\n\n    @Value.Default\n    default boolean debug() {\n        return false;\n    }\n\n    @Value.Default\n    default boolean forcePull() {\n        return true;\n    }\n\n    @Value.Default\n    default boolean redirectErrorStream() {\n        return true;\n    }\n\n    static ImmutableDockerContainerSpec.Builder builder() {\n        return ImmutableDockerContainerSpec.builder();\n    }\n\n    @Value.Immutable\n    interface Options {\n\n        /**\n         * Extra /etc/hosts entries.\n         * Same as {@code --add-host} option in {@code docker run}\n         * @return\n         */\n        @Nullable\n        List<String> hosts();\n\n        static ImmutableOptions.Builder builder() {\n            return ImmutableOptions.builder();\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        static Options from(Map<String, Object> m) {\n            ImmutableOptions.Builder b = ImmutableOptions.builder();\n\n            if (m == null) {\n                return b.build();\n            }\n\n            Object v = m.get(\"hosts\");\n            if (v == null) {\n                return b.build();\n            }\n\n            if (!(v instanceof Iterable)) {\n                throw new IllegalArgumentException(\"Unexpected 'hosts' value: \" + v);\n            }\n\n            b.hosts((Iterable<String>) v);\n\n            return b.build();\n        }\n    }\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/DockerService.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\n\npublic interface DockerService {\n\n    /**\n     * Start a new Docker container using the provided specification.\n     */\n    @Deprecated\n    Process start(Context ctx, DockerContainerSpec spec) throws IOException;\n\n    /**\n     * Starts a new Docker container using the provided {@code spec}.\n     * @param ctx the process' context\n     * @param spec the container's specification\n     * @param outCallback callback for stdout\n     * @param errCallback callback for stderr\n     * @return exit code of the `docker run` command\n     */\n    int start(Context ctx, DockerContainerSpec spec, LogCallback outCallback, LogCallback errCallback) throws IOException, InterruptedException;\n\n    interface LogCallback {\n\n        void onLog(String line);\n    }\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/EventService.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\nimport java.util.Date;\n\n/**\n * Reserved for the future use.\n */\npublic interface EventService {\n\n    void onEvent(String instanceId, Date date, EventType type, Serializable data) throws ClientException;\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/EventType.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic enum EventType {\n    /**\n     * Events produced by the Ansible plugin.\n     */\n    ANSIBLE,\n\n    /**\n     * Flow events: step executions, task calls, etc.\n     */\n    PROCESS_ELEMENT,\n\n    /**\n     * Process status change events.\n     */\n    PROCESS_STATUS,\n\n    /**\n     * Process wait conditions.\n     */\n    PROCESS_WAIT,\n\n\n    /**\n     * Process checkpoint restore events.\n     */\n    CHECKPOINT_RESTORE\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/HasKey.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface HasKey {\n\n    String getKey();\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/InjectVariable.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Target({ElementType.PARAMETER, ElementType.FIELD})\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface InjectVariable {\n    String value();\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/LockService.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface LockService {\n\n    void projectLock(Context ctx, String lockName) throws Exception;\n\n    void projectUnlock(Context ctx, String lockName) throws Exception;\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/LogTags.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.UUID;\n\npublic final class LogTags {\n\n    private static final String INSTANCE_ID_TAG = \"<concord:instanceId>%s</concord:instanceId>\";\n\n    public static String instanceId(UUID instanceId) {\n        if (instanceId == null) {\n            return null;\n        }\n\n        return String.format(INSTANCE_ID_TAG, instanceId.toString());\n    }\n\n    private LogTags() {\n    }\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/MapUtils.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\n\npublic final class MapUtils {\n\n    public static <E extends Enum<E>> E getEnum(Map<String, Object> m, String name, Class<E> enumData, E defaultValue) {\n        String v = getString(m, name);\n        if (v == null) {\n            return defaultValue;\n        }\n\n        for (Enum<E> enumVal : enumData.getEnumConstants()) {\n            if (enumVal.name().equals(v)) {\n                return Enum.valueOf(enumData, v);\n            }\n        }\n        throw new IllegalArgumentException(\"Invalid variable '\" + name + \"' type, expected one of: \" +\n                Arrays.stream(enumData.getEnumConstants()).map(Enum::name).collect(Collectors.toList()) +\n                \", got: \" + v);\n    }\n\n    public static UUID getUUID(Map<String, Object> m, String name) {\n        Object o = m.get(name);\n        if (o == null) {\n            return null;\n        }\n        if (o instanceof String) {\n            return UUID.fromString((String) o);\n        }\n        if (o instanceof UUID) {\n            return (UUID) o;\n        }\n        throw new IllegalArgumentException(\"Invalid variable '\" + name + \"' type, expected: string/uuid, got: \" + o.getClass());\n    }\n\n    public static String getString(Map<String, Object> m, HasKey k) {\n        return getString(m, k.getKey());\n    }\n\n    public static String getString(Map<String, Object> m, String name) {\n        return getString(m, name, null);\n    }\n\n    public static String getString(Map<String, Object> m, HasKey k, String defaultValue) {\n        return getString(m, k.getKey(), defaultValue);\n    }\n\n    public static String getString(Map<String, Object> m, String name, String defaultValue) {\n        return get(m, name, defaultValue, String.class);\n    }\n\n    public static <K, V> Map<K, V> getMap(Map<String, Object> m, HasKey k, Map<K, V> defaultValue) {\n        return getMap(m, k.getKey(), defaultValue);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <K, V> Map<K, V> getMap(Map<String, Object> m, String name, Map<K, V> defaultValue) {\n        return get(m, name, defaultValue, Map.class);\n    }\n\n    public static <E> List<E> getList(Map<String, Object> m, HasKey k, List<E> defaultValue) {\n        return getList(m, k.getKey(), defaultValue);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <E> List<E> getList(Map<String, Object> m, String name, List<E> defaultValue) {\n        return get(m, name, defaultValue, List.class);\n    }\n\n    public static boolean getBoolean(Map<String, Object> m, HasKey k, boolean defaultValue) {\n        return getBoolean(m, k.getKey(), defaultValue);\n    }\n\n    public static boolean getBoolean(Map<String, Object> m, String name, boolean defaultValue) {\n        Boolean result = get(m, name, defaultValue, Boolean.class);\n        if (result == null) {\n            return defaultValue;\n        }\n        return result;\n    }\n\n    public static int getInt(Map<String, Object> m, String name, int defaultValue) {\n        Integer result = get(m, name, defaultValue, Integer.class);\n        if (result == null) {\n            return defaultValue;\n        }\n        return result;\n    }\n\n    public static Number getNumber(Map<String, Object> m, String name, Number defaultValue) {\n        return get(m, name, defaultValue, Number.class);\n    }\n\n    public static UUID assertUUID(Map<String, Object> m, String name) {\n        UUID uuid = getUUID(m, name);\n        if (uuid != null) {\n            return uuid;\n        }\n        throw new IllegalArgumentException(\"Mandatory variable '\" + name + \"' is required\");\n    }\n\n    public static int assertInt(Map<String, Object> m, String name) {\n        return assertVariable(m, name, Integer.class);\n    }\n\n    public static Number assertNumber(Map<String, Object> m, String name) {\n        return assertVariable(m, name, Number.class);\n    }\n\n    public static String assertString(Map<String, Object> m, String name) {\n        return assertVariable(m, name, String.class);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <K, V> Map<K, V> assertMap(Map<String, Object> m, String name) {\n        return assertVariable(m, name, Map.class);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <E> List<E> assertList(Map<String, Object> m, String name) {\n        return assertVariable(m, name, List.class);\n    }\n\n    public static <T> T assertVariable(Map<String, Object> m, String name, Class<T> type) {\n        T result = get(m, name, null, type);\n\n        if (result != null) {\n            return result;\n        }\n\n        throw new IllegalArgumentException(\"Mandatory variable '\" + name + \"' is required\");\n    }\n\n    public static <T> T get(Map<String, Object> m, String name, T defaultValue, Class<T> type) {\n        Object value = get(m, name, defaultValue);\n        if (value == null) {\n            return defaultValue;\n        }\n\n        if (type.isInstance(value)) {\n            return type.cast(value);\n        }\n\n        throw new IllegalArgumentException(\"Invalid variable '\" + name + \"' type, expected: \" + type + \", got: \" + value.getClass());\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <T> T get(Map<String, Object> m, String name, T defaultValue) {\n        if (m == null) {\n            return defaultValue;\n        }\n\n        return (T) m.getOrDefault(name, defaultValue);\n    }\n\n    private MapUtils() {\n    }\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/MockContext.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * Mock implementation of {@link Context}.\n * Useful for unit testing of plugins.\n */\npublic class MockContext implements Context {\n\n    private final Map<String, Object> delegate;\n\n    public MockContext(Map<String, Object> delegate) {\n        this.delegate = delegate;\n    }\n\n    @Override\n    public Object getVariable(String key) {\n        return delegate.get(key);\n    }\n\n    @Override\n    public void setVariable(String key, Object value) {\n        delegate.put(key, value);\n    }\n\n    @Override\n    public void removeVariable(String key) {\n        delegate.remove(key);\n    }\n\n    @Override\n    public Set<String> getVariableNames() {\n        return delegate.keySet();\n    }\n\n    @Override\n    public void setProtectedVariable(String key, Object value) {\n        throw new IllegalArgumentException(\"Not supported\");\n    }\n\n    @Override\n    public Object getProtectedVariable(String key) {\n        throw new IllegalArgumentException(\"Not supported\");\n    }\n\n    @Override\n    public <T> T eval(String expr, Class<T> type) {\n        throw new IllegalStateException(\"Not supported\");\n    }\n\n    @Override\n    public Object interpolate(Object v) {\n        throw new IllegalStateException(\"Not supported\");\n    }\n\n    @Override\n    public Object interpolate(Object v, Map<String, Object> variables) {\n        throw new IllegalStateException(\"Not supported\");\n    }\n\n    @Override\n    public Map<String, Object> toMap() {\n        return new HashMap<>(delegate);\n    }\n\n    @Override\n    public void suspend(String eventName) {\n        throw new IllegalStateException(\"Not supported\");\n    }\n\n    @Override\n    public void suspend(String eventName, Object payload, boolean restoreFromSameStep) {\n        throw new IllegalStateException(\"Not supported\");\n    }\n\n    @Override\n    public String getProcessDefinitionId() {\n        throw new IllegalStateException(\"Not supported\");\n    }\n\n    @Override\n    public String getElementId() {\n        throw new IllegalStateException(\"Not supported\");\n    }\n\n    @Override\n    public void form(String formName, Map<String, Object> formOptions) {\n        throw new IllegalStateException(\"Not supported\");\n    }\n\n    @Override\n    public String getCurrentFlowName() {\n        return \"n/a\";\n    }\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/ObjectStorage.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\n/**\n * @deprecated replaced by the JSON storage API\n */\n@Deprecated\npublic interface ObjectStorage {\n\n    BucketInfo createBucket(Context ctx, String name) throws Exception;\n\n    /**\n     * @deprecated replaced by the JSON storage API\n     */\n    @Deprecated\n    @Value.Immutable\n    interface BucketInfo {\n\n        String address();\n    }\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/ProjectInfo.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport java.util.UUID;\n\n@Value.Immutable\npublic interface ProjectInfo {\n\n    UUID orgId();\n\n    String orgName();\n\n    UUID id();\n\n    String name();\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/RepositoryInfo.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport java.util.UUID;\n\n@Value.Immutable\npublic interface RepositoryInfo {\n\n    UUID id();\n\n    String name();\n\n    String url();\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/Secret.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\n\npublic interface Secret extends Serializable {\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/SecretNotFoundException.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class SecretNotFoundException extends IllegalArgumentException {\n\n    // for backward compatibility (java8 concord 1.92.0 version)\n    private static final long serialVersionUID = 6544929210725956736L;\n\n    private final String orgName;\n\n    private final String secretName;\n\n    public SecretNotFoundException(String orgName, String secretName) {\n        super(\"Secret not found: \" + orgName + \"/\" + secretName);\n        this.orgName = orgName;\n        this.secretName = secretName;\n    }\n\n    public String getOrgName() {\n        return orgName;\n    }\n\n    public String getSecretName() {\n        return secretName;\n    }\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/SecretService.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Map;\n\npublic interface SecretService {\n\n    String exportAsString(Context ctx,\n                          String instanceId,\n                          String name,\n                          String password) throws Exception;\n\n    String exportAsString(Context ctx,\n                          String instanceId,\n                          String orgName,\n                          String name,\n                          String password) throws Exception;\n\n    Map<String, String> exportKeyAsFile(Context ctx,\n                                        String instanceId,\n                                        String workDir,\n                                        String name,\n                                        String password) throws Exception;\n\n    Map<String, String> exportKeyAsFile(Context ctx,\n                                        String instanceId,\n                                        String workDir,\n                                        String orgName,\n                                        String name,\n                                        String password) throws Exception;\n\n    Map<String, String> exportCredentials(Context ctx,\n                                          String instanceId,\n                                          String workDir,\n                                          String name,\n                                          String password) throws Exception;\n\n    Map<String, String> exportCredentials(Context ctx,\n                                          String instanceId,\n                                          String workDir,\n                                          String orgName,\n                                          String name,\n                                          String password) throws Exception;\n\n    String exportAsFile(Context ctx,\n                        String instanceId,\n                        String workDir,\n                        String name,\n                        String password) throws Exception;\n\n    String exportAsFile(Context ctx,\n                        String instanceId,\n                        String workDir,\n                        String orgName,\n                        String name,\n                        String password) throws Exception;\n\n    String decryptString(Context ctx, String instanceId, String s) throws Exception;\n\n    String encryptString(Context ctx,\n                         String instanceId,\n                         String orgName,\n                         String projectName,\n                         String value) throws Exception;\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/Task.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n/**\n * Default interface for Concord tasks.\n */\npublic interface Task {\n\n    default void execute(Context ctx) throws Exception {\n        throw new Exception(\"Not implemented\");\n    }\n}\n"
  },
  {
    "path": "sdk/src/main/java/com/walmartlabs/concord/sdk/UserDefinedException.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n/**\n * Doesn't produce a stack trace in process logs.\n */\npublic class UserDefinedException extends RuntimeException {\n\n    // for backward compatibility (java8 concord 1.92.0 version)\n    private static final long serialVersionUID = -3926184420837445120L;\n\n    public UserDefinedException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "sdk/src/test/java/com/walmartlabs/concord/sdk/ContextUtilsTest.java",
    "content": "package com.walmartlabs.concord.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class ContextUtilsTest {\n\n    @Test\n    public void testGetInt() {\n        Context ctx = new Ctx();\n        ctx.setVariable(\"k\", 123);\n        ctx.setVariable(\"not-int\", 123L);\n\n        // ---\n        Integer result = ContextUtils.getVariable(ctx, \"k\", null, Integer.class);\n        assertNotNull(result);\n        assertEquals((Integer) 123, result);\n\n        // ---\n        result = ContextUtils.getVariable(ctx, \"not-found\", null, Integer.class);\n        assertNull(result);\n\n        // ---\n        result = ContextUtils.getVariable(ctx, \"not-found\", -1, Integer.class);\n        assertNotNull(result);\n        assertEquals((Integer) (-1), result);\n\n        // ---\n        int res = ContextUtils.getInt(ctx, \"k\", -1);\n        assertEquals(123, res);\n\n        // ---\n        res = ContextUtils.getInt(ctx, \"not-found\", -1);\n        assertEquals(-1, res);\n\n        // ---\n        try {\n            ContextUtils.getVariable(ctx, \"not-int\", null, Integer.class);\n            fail(\"exception expected\");\n        } catch (IllegalArgumentException e) {\n            // do nothing\n        }\n    }\n\n    @Test\n    public void testGetNumber() {\n        Context ctx = new Ctx();\n        ctx.setVariable(\"k\", 123L);\n        ctx.setVariable(\"not-number\", \"boom\");\n\n        // ---\n        Number result = ContextUtils.getVariable(ctx, \"k\", null, Number.class);\n        assertNotNull(result);\n        assertEquals(123L, result);\n\n        // ---\n        result = ContextUtils.getVariable(ctx, \"not-found\", null, Number.class);\n        assertNull(result);\n\n        // ---\n        result = ContextUtils.getVariable(ctx, \"not-found\", -1, Number.class);\n        assertNotNull(result);\n        assertEquals(-1, result);\n\n\n        // ---\n        result = ContextUtils.getNumber(ctx, \"k\", -1);\n        assertEquals(123L, result);\n\n        // ---\n        result = ContextUtils.getNumber(ctx, \"not-found\", -1);\n        assertEquals(-1, result);\n\n        // ---\n        try {\n            ContextUtils.getVariable(ctx, \"not-number\", null, Number.class);\n            fail(\"exception expected\");\n        } catch (IllegalArgumentException e) {\n            // do nothing\n        }\n\n        // ---\n        result = ContextUtils.assertNumber(ctx, \"k\");\n        assertNotNull(result);\n        assertEquals(123L, result);\n\n        // ---\n        try {\n            ContextUtils.assertNumber(ctx, \"not-found\");\n            fail(\"exception expected\");\n        } catch (IllegalArgumentException e) {\n            // do nothing\n        }\n    }\n\n    private static class Ctx implements Context {\n\n        private final Map<String, Object> vars = new HashMap<>();\n\n        @Override\n        public Object getVariable(String key) {\n            return vars.get(key);\n        }\n\n        @Override\n        public void setVariable(String key, Object value) {\n            vars.put(key, value);\n        }\n\n        @Override\n        public void setProtectedVariable(String key, Object value) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public Object getProtectedVariable(String key) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public void removeVariable(String key) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public Set<String> getVariableNames() {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public <T> T eval(String expr, Class<T> type) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public Object interpolate(Object v) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public Object interpolate(Object v, Map<String, Object> variables) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public Map<String, Object> toMap() {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public void suspend(String eventName) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public void suspend(String eventName, Object payload, boolean restoreFromSameStep) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public String getProcessDefinitionId() {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public String getElementId() {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public void form(String formName, Map<String, Object> formOptions) {\n            throw new UnsupportedOperationException();\n        }\n\n        @Override\n        public String getCurrentFlowName() {\n            throw new UnsupportedOperationException();\n        }\n    }\n}\n"
  },
  {
    "path": "server/README.md",
    "content": "# Concord Server\n\n- [db](./db) - main database module, included into [impl](./impl)\nto autorollout on start;\n- [dist](./dist) - the server's distribution. Includes the main\nmodule, the default cfg file and core plugins;\n- [impl](./impl) - main app module;\n- [plugins](./plugins) - core server-side plugins;\n- [queue-client](./queue-client) - WebSocket based client for\nthe process queue. Used by the Agent;\n- [sdk](./sdk) - set of APIs for server plugins.\n"
  },
  {
    "path": "server/db/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.server</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-server-db</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <properties>\n        <db.baseDir>${project.build.directory}/db</db.baseDir>\n        <db.changeLogPath>com/walmartlabs/concord/server/db/liquibase.xml</db.changeLogPath>\n        <db.host>localhost</db.host>\n        <db.username>postgres</db.username>\n        <db.password>q1</db.password>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server-sdk</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>liquibase-ext</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.postgresql</groupId>\n            <artifactId>postgresql</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.inject</groupId>\n            <artifactId>guice</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.zaxxer</groupId>\n            <artifactId>HikariCP</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.jooq</groupId>\n            <artifactId>jooq</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.liquibase</groupId>\n            <artifactId>liquibase-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.dropwizard.metrics</groupId>\n            <artifactId>metrics-core</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.testcontainers</groupId>\n            <artifactId>testcontainers-postgresql</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>ch.qos.logback</groupId>\n            <artifactId>logback-classic</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <testResources>\n            <testResource>\n                <directory>src/test/resources</directory>\n                <filtering>false</filtering>\n            </testResource>\n            <testResource>\n                <directory>src/test/filtered-resources</directory>\n                <filtering>true</filtering>\n            </testResource>\n        </testResources>\n\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>build-helper-maven-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>reserve-ports</id>\n                        <phase>generate-sources</phase>\n                        <goals>\n                            <goal>reserve-network-port</goal>\n                        </goals>\n                        <configuration>\n                            <portNames>\n                                <portName>db.port</portName>\n                            </portNames>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>io.fabric8</groupId>\n                <artifactId>docker-maven-plugin</artifactId>\n                <extensions>true</extensions>\n                <executions>\n                    <execution>\n                        <id>start</id>\n                        <phase>generate-sources</phase>\n                        <goals>\n                            <goal>start</goal>\n                        </goals>\n                    </execution>\n                    <execution>\n                        <id>stop</id>\n                        <phase>prepare-package</phase>\n                        <goals>\n                            <goal>stop</goal>\n                        </goals>\n                    </execution>\n                </executions>\n                <configuration>\n                    <images>\n                        <image>\n                            <name>${db.image}</name>\n                            <alias>db</alias>\n                            <run>\n                                <ports>\n                                    <port>${db.port}:5432</port>\n                                </ports>\n                                <env>\n                                    <POSTGRES_PASSWORD>${db.password}</POSTGRES_PASSWORD>\n                                    <POSTGRES_INITDB_ARGS>--no-sync</POSTGRES_INITDB_ARGS>\n                                </env>\n                                <wait>\n                                    <log>(?s).*ready for start up.*ready to accept connections.*</log>\n                                    <time>60000</time>\n                                </wait>\n                            </run>\n                        </image>\n                    </images>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.liquibase</groupId>\n                <artifactId>liquibase-maven-plugin</artifactId>\n                <version>${liquibase.version}</version>\n                <executions>\n                    <execution>\n                        <phase>generate-sources</phase>\n                        <goals>\n                            <goal>update</goal>\n                        </goals>\n                    </execution>\n                </executions>\n                <configuration>\n                    <changeLogFile>src/main/resources/${db.changeLogPath}</changeLogFile>\n                    <driver>org.postgresql.Driver</driver>\n                    <url>jdbc:postgresql://${db.host}:${db.port}/postgres</url>\n                    <username>${db.username}</username>\n                    <password>${db.password}</password>\n                    <promptOnNonLocalDatabase>false</promptOnNonLocalDatabase>\n                    <contexts>codegen</contexts>\n                    <expressionVariables>\n                        <superuserAvailable>true</superuserAvailable>\n                        <createExtensionAvailable>true</createExtensionAvailable>\n                    </expressionVariables>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.jooq</groupId>\n                <artifactId>jooq-codegen-maven</artifactId>\n                <executions>\n                    <execution>\n                        <phase>generate-sources</phase>\n                        <goals>\n                            <goal>generate</goal>\n                        </goals>\n                    </execution>\n                </executions>\n                <configuration>\n                    <logging>WARN</logging>\n                    <jdbc>\n                        <driver>org.postgresql.Driver</driver>\n                        <url>jdbc:postgresql://${db.host}:${db.port}/postgres</url>\n                        <user>${db.username}</user>\n                        <password>${db.password}</password>\n                    </jdbc>\n                    <generator>\n                        <database>\n                            <name>org.jooq.meta.postgres.PostgresDatabase</name>\n                            <inputSchema>public</inputSchema>\n                            <includes>.*</includes>\n                            <excludes>DATABASECHANGELOG.*</excludes>\n                        </database>\n                        <target>\n                            <packageName>com.walmartlabs.concord.server.jooq</packageName>\n                            <directory>target/generated-sources/jooq</directory>\n                        </target>\n                        <generate>\n                            <deprecationOnUnknownTypes>false</deprecationOnUnknownTypes>\n                        </generate>\n                    </generator>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>cobertura-maven-plugin</artifactId>\n                <configuration>\n                    <skip>true</skip>\n                    <check />\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n    <profiles>\n        <profile>\n            <id>looper</id>\n            <activation>\n                <activeByDefault>false</activeByDefault>\n            </activation>\n            <properties>\n                <db.host>${env.DB_CONTAINER_NAME}</db.host>\n                <db.port>5432</db.port>\n            </properties>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.codehaus.mojo</groupId>\n                        <artifactId>build-helper-maven-plugin</artifactId>\n                        <executions>\n                            <execution>\n                                <id>reserve-ports</id>\n                                <phase>none</phase>\n                            </execution>\n                        </executions>\n                    </plugin>\n                    <plugin>\n                        <groupId>io.fabric8</groupId>\n                        <artifactId>docker-maven-plugin</artifactId>\n                        <extensions>true</extensions>\n                        <configuration>\n                            <skip>true</skip>\n                        </configuration>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n    </profiles>\n</project>\n"
  },
  {
    "path": "server/db/src/main/java/com/walmartlabs/concord/db/AbstractDao.java",
    "content": "package com.walmartlabs.concord.db;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.jooq.exception.DataAccessException;\nimport org.jooq.impl.DSL;\nimport org.jooq.tools.jdbc.JDBCUtils;\n\nimport java.io.InputStream;\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLException;\nimport java.util.function.Function;\n\npublic abstract class AbstractDao {\n\n    protected final Configuration cfg;\n\n    protected AbstractDao(Configuration cfg) {\n        this.cfg = cfg;\n    }\n\n    protected DSLContext dsl() {\n        return DSL.using(cfg);\n    }\n\n    protected void tx(Tx t) {\n        dsl().transaction(cfg -> {\n            DSLContext tx = DSL.using(cfg);\n            t.run(tx);\n        });\n    }\n\n    protected <T> T txResult(TxResult<T> t) {\n        return dsl().transactionResult(cfg -> {\n            DSLContext tx = DSL.using(cfg);\n            return t.run(tx);\n        });\n    }\n\n    protected InputStream getData(Function<DSLContext, String> sqlFn, PreparedStatementHandler h, int columnIndex) {\n        String sql = sqlFn.apply(dsl());\n\n        Connection conn = cfg.connectionProvider().acquire(); // NOSONAR\n        PreparedStatement ps = null;\n        try {\n            ps = conn.prepareStatement(sql);\n            h.apply(ps);\n\n            InputStream in = ResultSetInputStream.open(conn, ps, columnIndex);\n            if (in == null) { // NOSONAR\n                JDBCUtils.safeClose(ps);\n                JDBCUtils.safeClose(conn);\n                return null;\n            }\n            return in;\n        } catch (SQLException e) {\n            JDBCUtils.safeClose(ps);\n            JDBCUtils.safeClose(conn);\n            throw new DataAccessException(\"Error while opening a stream\", e);\n        }\n    }\n\n    public interface Tx {\n\n        void run(DSLContext tx) throws Exception;\n    }\n\n    public interface TxResult<T> {\n\n        T run(DSLContext tx) throws Exception;\n    }\n\n    public interface PreparedStatementHandler {\n\n        void apply(PreparedStatement ps) throws SQLException;\n    }\n}\n"
  },
  {
    "path": "server/db/src/main/java/com/walmartlabs/concord/db/DataSourceUtils.java",
    "content": "package com.walmartlabs.concord.db;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.MetricRegistry;\nimport com.zaxxer.hikari.HikariDataSource;\nimport liquibase.Liquibase;\nimport liquibase.Scope;\nimport liquibase.database.Database;\nimport liquibase.database.DatabaseFactory;\nimport liquibase.database.jvm.JdbcConnection;\nimport liquibase.resource.ClassLoaderResourceAccessor;\nimport liquibase.ui.LoggerUIService;\nimport org.jooq.Configuration;\nimport org.jooq.SQLDialect;\nimport org.jooq.conf.RenderNameStyle;\nimport org.jooq.conf.Settings;\nimport org.jooq.impl.DefaultConfiguration;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.sql.DataSource;\nimport java.sql.Connection;\nimport java.sql.DriverManager;\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\nimport java.time.Duration;\nimport java.util.Map;\n\npublic final class DataSourceUtils {\n\n    private static final Logger log = LoggerFactory.getLogger(DataSourceUtils.class);\n\n    private static final int MIGRATION_MAX_RETRIES = 10;\n    private static final Duration MIGRATION_RETRY_DELAY = Duration.ofSeconds(10);\n    private static final Duration MAX_LOCK_WAIT_TIME = Duration.ofMinutes(5);\n    private static final Duration FAILED_LOCK_ATTEMPT_DELAY = Duration.ofSeconds(3);\n\n    public static DataSource createDataSource(DatabaseConfiguration cfg,\n                                              String poolName,\n                                              String username,\n                                              String password,\n                                              MetricRegistry metricRegistry) {\n\n        HikariDataSource ds = new HikariDataSource();\n        ds.setPoolName(poolName);\n        ds.setJdbcUrl(cfg.url());\n        ds.setDriverClassName(cfg.driverClassName());\n        ds.setUsername(username);\n        ds.setPassword(password);\n        ds.setAutoCommit(false);\n        ds.setMaxLifetime(cfg.maxLifetime().toMillis());\n        ds.setMinimumIdle(1);\n        ds.setMaximumPoolSize(cfg.maxPoolSize());\n        ds.setLeakDetectionThreshold(30000);\n        ds.setMetricRegistry(metricRegistry);\n        return ds;\n    }\n\n    /**\n     * Migrate a database using the provided changelog and Liquibase parameters.\n     *\n     * @param cfg               database config\n     * @param changeLogProvider provider of the changelog\n     */\n    public static void migrateDb(DatabaseConfiguration cfg,\n                                 DatabaseChangeLogProvider changeLogProvider) {\n\n        try {\n            Class.forName(cfg.driverClassName());\n        } catch (Exception e) {\n            throw new RuntimeException(\"Failed to initialize the JDBC driver: \" + e.getMessage());\n        }\n\n        int retries = MIGRATION_MAX_RETRIES;\n        for (int i = 0; i < retries; i++) {\n            try (Connection conn = DriverManager.getConnection(cfg.url(), cfg.username(), cfg.password())) {\n                String logPath = changeLogProvider.getChangeLogPath();\n                String logTable = changeLogProvider.getChangeLogTable();\n                String lockTable = changeLogProvider.getLockTable();\n\n                long start = System.currentTimeMillis();\n                while (!tryLock(conn, logTable)) {\n                    if (System.currentTimeMillis() - start >= MAX_LOCK_WAIT_TIME.toMillis()) {\n                        throw new Exception(\"Timeout to obtain the lock for \" + logTable);\n                    }\n\n                    log.info(\"migrateDb -> waiting for the lock for {}\", logTable);\n                    Thread.sleep(FAILED_LOCK_ATTEMPT_DELAY.toMillis());\n                }\n\n                log.info(\"migrateDb -> performing '{}' migration...\", changeLogProvider);\n                applyMigrations(conn, logPath, logTable, lockTable, cfg.changeLogParameters());\n                log.info(\"migrateDb -> completed '{}' migration..\", changeLogProvider);\n                break;\n            } catch (Exception e) {\n                if (i + 1 >= retries) {\n                    log.error(\"migrateDb -> db migration error, giving up\", e);\n                    throw new RuntimeException(e);\n                }\n\n                log.warn(\"migrateDb -> db migration error, retrying in {}ms: {}\", MIGRATION_RETRY_DELAY, e.getMessage());\n                try {\n                    Thread.sleep(MIGRATION_RETRY_DELAY.toMillis());\n                } catch (InterruptedException ee) {\n                    Thread.currentThread().interrupt();\n                }\n            }\n        }\n    }\n\n    public static Configuration createJooqConfiguration(DataSource ds) {\n        Settings settings = new Settings();\n        settings.setRenderSchema(false);\n        settings.setRenderCatalog(false);\n        settings.setRenderNameStyle(RenderNameStyle.AS_IS);\n        return new DefaultConfiguration()\n                .set(settings)\n                .set(ds)\n                .set(SQLDialect.POSTGRES);\n    }\n\n    private static void applyMigrations(Connection conn,\n                                        String logPath,\n                                        String logTable,\n                                        String lockTable,\n                                        Map<String, Object> params) throws Exception {\n\n        Database db = DatabaseFactory.getInstance()\n                .findCorrectDatabaseImplementation(new JdbcConnection(conn));\n\n        db.setDatabaseChangeLogTableName(logTable);\n        db.setDatabaseChangeLogLockTableName(lockTable);\n\n        Liquibase lb = new Liquibase(logPath, new ClassLoaderResourceAccessor(), db);\n\n        if (params != null) {\n            params.forEach(lb::setChangeLogParameter);\n        }\n\n        Scope.enter(Map.of(Scope.Attr.ui.name(), new LoggerUIService()));\n        lb.update((String) null);\n    }\n\n    private static boolean tryLock(Connection conn, String logTable) throws Exception {\n        log.info(\"tryLock -> trying to take the lock for {}\", logTable);\n        try (PreparedStatement ps = conn.prepareStatement(\"SELECT pg_try_advisory_lock(?)\")) {\n            ps.setInt(1, logTable.hashCode());\n            try (ResultSet rs = ps.executeQuery()) {\n                if (rs.next()) {\n                    boolean result = rs.getBoolean(1);\n                    if (result) {\n                        log.info(\"tryLock -> successfully grabbed the lock for {}\", logTable);\n                    }\n                    return result;\n                }\n            }\n        }\n        return false;\n    }\n\n    private DataSourceUtils() {\n    }\n}\n"
  },
  {
    "path": "server/db/src/main/java/com/walmartlabs/concord/db/DatabaseChangeLogProvider.java",
    "content": "package com.walmartlabs.concord.db;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface DatabaseChangeLogProvider {\n\n    String getChangeLogPath();\n\n    default String getChangeLogTable() {\n        return \"SERVER_DB_LOG\";\n    }\n\n    default String getLockTable() {\n        return \"SERVER_DB_LOCK\";\n    }\n\n    default int order() {\n        return 1;\n    }\n}\n"
  },
  {
    "path": "server/db/src/main/java/com/walmartlabs/concord/db/DatabaseConfiguration.java",
    "content": "package com.walmartlabs.concord.db;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.time.Duration;\nimport java.util.Collections;\nimport java.util.Map;\n\npublic interface DatabaseConfiguration {\n\n    default String driverClassName() {\n        return \"org.postgresql.Driver\";\n    }\n\n    String url();\n\n    String username();\n\n    String password();\n\n    int maxPoolSize();\n\n    Duration maxLifetime();\n\n    default Map<String, Object> changeLogParameters() {\n        return Collections.emptyMap();\n    }\n}\n"
  },
  {
    "path": "server/db/src/main/java/com/walmartlabs/concord/db/DatabaseModule.java",
    "content": "package com.walmartlabs.concord.db;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.MetricRegistry;\nimport com.google.inject.Binder;\nimport com.google.inject.Provides;\nimport org.jooq.Configuration;\n\nimport javax.inject.Singleton;\nimport javax.sql.DataSource;\nimport java.util.Comparator;\nimport java.util.Set;\n\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\n\npublic class DatabaseModule implements com.google.inject.Module {\n\n    private final boolean migrateDb;\n\n    public DatabaseModule() {\n        this(true);\n    }\n\n    public DatabaseModule(boolean migrateDb) {\n        this.migrateDb = migrateDb;\n    }\n\n    @Override\n    public void configure(Binder binder) {\n        newSetBinder(binder, DatabaseChangeLogProvider.class).addBinding().to(MainDBChangeLogProvider.class);\n    }\n\n    @Provides\n    @MainDB\n    @Singleton\n    public DataSource appDataSource(@MainDB DatabaseConfiguration cfg,\n                                    MetricRegistry metricRegistry,\n                                    Set<DatabaseChangeLogProvider> changeLogProviders) {\n\n        if (migrateDb) {\n            changeLogProviders.stream()\n                    // can't inject a set of objects with the same qualifier, filter manually\n                    .filter(p -> p.getClass().getAnnotation(MainDB.class) != null)\n                    .sorted(Comparator.comparingInt(DatabaseChangeLogProvider::order))\n                    .forEach(p -> DataSourceUtils.migrateDb(cfg, p));\n        }\n\n        return DataSourceUtils.createDataSource(cfg, \"app\", cfg.username(), cfg.password(), metricRegistry);\n    }\n\n    @Provides\n    @JsonStorageDB\n    @Singleton\n    public DataSource inventoryDataSource(@JsonStorageDB DatabaseConfiguration cfg, MetricRegistry metricRegistry) {\n        return DataSourceUtils.createDataSource(cfg, \"inventory\", cfg.username(), cfg.password(), metricRegistry);\n    }\n\n    @Provides\n    @MainDB\n    @Singleton\n    public Configuration appJooqConfiguration(@MainDB DataSource ds) {\n        return DataSourceUtils.createJooqConfiguration(ds);\n    }\n\n    @Provides\n    @JsonStorageDB\n    @Singleton\n    public Configuration inventoryJooqConfiguration(@JsonStorageDB DataSource ds) {\n        return DataSourceUtils.createJooqConfiguration(ds);\n    }\n}\n"
  },
  {
    "path": "server/db/src/main/java/com/walmartlabs/concord/db/JsonStorageDB.java",
    "content": "package com.walmartlabs.concord.db;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.inject.Qualifier;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n@Retention(RetentionPolicy.RUNTIME)\n@Qualifier\npublic @interface JsonStorageDB {\n}\n"
  },
  {
    "path": "server/db/src/main/java/com/walmartlabs/concord/db/LiquibaseLogService.java",
    "content": "package com.walmartlabs.concord.db;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport liquibase.logging.LogMessageFilter;\nimport liquibase.logging.LogService;\nimport liquibase.logging.Logger;\n\npublic class LiquibaseLogService implements LogService {\n\n    @Override\n    public int getPriority() {\n        return PRIORITY_SPECIALIZED;\n    }\n\n    @Override\n    public Logger getLog(Class clazz) {\n        return new LiquibaseLogger();\n    }\n\n    @Override\n    public void close() {\n\n    }\n\n    @Override\n    public LogMessageFilter getFilter() {\n        return null;\n    }\n\n    @Override\n    public void setFilter(LogMessageFilter filter) {\n\n    }\n}\n"
  },
  {
    "path": "server/db/src/main/java/com/walmartlabs/concord/db/LiquibaseLogger.java",
    "content": "package com.walmartlabs.concord.db;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport liquibase.logging.core.AbstractLogger;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.logging.Level;\n\nimport static java.util.logging.Level.CONFIG;\n\npublic class LiquibaseLogger extends AbstractLogger {\n\n    private static final Logger log = LoggerFactory.getLogger(\"liquibase\");\n\n    public LiquibaseLogger() {\n        super(null);\n    }\n\n    @Override\n    public void close() throws Exception {\n        super.close();\n    }\n\n    @Override\n    public void log(Level level, String message, Throwable e) {\n        var l = level.intValue();\n        if (l < CONFIG.intValue()) {\n            log.debug(message, e);\n        } else if (l < Level.WARNING.intValue()) {\n            log.info(message, e);\n        } else if (l < Level.SEVERE.intValue()) {\n            log.warn(message, e);\n        } else {\n            log.error(message, e);\n        }\n    }\n}\n"
  },
  {
    "path": "server/db/src/main/java/com/walmartlabs/concord/db/MainDB.java",
    "content": "package com.walmartlabs.concord.db;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.inject.Qualifier;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n@Retention(RetentionPolicy.RUNTIME)\n@Qualifier\npublic @interface MainDB {\n}\n"
  },
  {
    "path": "server/db/src/main/java/com/walmartlabs/concord/db/MainDBChangeLogProvider.java",
    "content": "package com.walmartlabs.concord.db;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n@MainDB\npublic class MainDBChangeLogProvider implements DatabaseChangeLogProvider {\n\n    @Override\n    public String getChangeLogPath() {\n        return \"com/walmartlabs/concord/server/db/liquibase.xml\";\n    }\n\n    @Override\n    public int order() {\n        // we expect the server's DB to be migrated first\n        return 0;\n    }\n\n    @Override\n    public String toString() {\n        return \"server-db\";\n    }\n}\n"
  },
  {
    "path": "server/db/src/main/java/com/walmartlabs/concord/db/PgIntRange.java",
    "content": "package com.walmartlabs.concord.db;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.Range;\n\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic final class PgIntRange {\n\n    private static final Pattern PATTERN = Pattern.compile(\"([\\\\[(])(\\\\d+),(\\\\d+)([])])\");\n\n    public static Range parse(String s) {\n        Matcher m = PATTERN.matcher(s);\n        if (!m.matches()) {\n            throw new IllegalArgumentException(\"Invalid range value: \" + s);\n        }\n\n        Range.Mode lowerMode = parseMode(m.group(1));\n        int lower = Integer.parseInt(m.group(2));\n        int upper = Integer.parseInt(m.group(3));\n        Range.Mode upperMode = parseMode(m.group(4));\n\n        return Range.builder()\n                .lowerMode(lowerMode)\n                .lower(lower)\n                .upper(upper)\n                .upperMode(upperMode)\n                .build();\n    }\n\n    private static Range.Mode parseMode(String s) {\n        if (\"[\".equals(s) || \"]\".equals(s)) {\n            return Range.Mode.INCLUSIVE;\n        } else if (\"(\".equals(s) || \")\".equals(s)) {\n            return Range.Mode.EXCLUSIVE;\n        } else {\n            throw new IllegalArgumentException(\"Invalid range mode string: \" + s);\n        }\n    }\n\n    private PgIntRange() {\n    }\n}\n"
  },
  {
    "path": "server/db/src/main/java/com/walmartlabs/concord/db/PgUtils.java",
    "content": "package com.walmartlabs.concord.db;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.jooq.Condition;\nimport org.jooq.Field;\nimport org.jooq.JSONB;\nimport org.jooq.exception.DataAccessException;\nimport org.jooq.impl.DSL;\nimport org.postgresql.util.PSQLException;\n\nimport java.time.Duration;\nimport java.time.OffsetDateTime;\nimport java.util.List;\n\nimport static org.jooq.impl.DSL.*;\n\npublic final class PgUtils {\n\n    public static Condition contains(Field<String[]> left, String[] right) {\n        return DSL.condition(\"{0} @> {1}::text[]\", left, DSL.val(right, left.getDataType()));\n    }\n\n    public static Field<?> interval(String s) {\n        return field(\"interval '\" + s + \"'\");\n    }\n\n    public static Field<String> toChar(Field<OffsetDateTime> date, String format) {\n        return field(\"to_char({0}, {1})\", String.class, date, inline(format));\n    }\n\n    public static Field<String> jsonbText(Field<JSONB> field, String name) {\n        return field(\"{0}::jsonb->>{1}\", Object.class, field, inline(name)).cast(String.class);\n    }\n\n    public static Field<String> jsonbTextByPath(Field<JSONB> field, List<String> path) {\n        return field(\"{0}::jsonb #>> {1}\", Object.class, field, inline(toPath(path))).cast(String.class);\n    }\n\n    public static Condition jsonbEq(Field<JSONB> field, String key, String value) {\n        return DSL.condition(\"{0} @> jsonb_build_object({1}, {2})\", field, DSL.val(key), DSL.value(value));\n    }\n\n    public static Condition jsonbContains(Field<JSONB> field, JSONB value) {\n        return DSL.condition(\"{0} @> {1}\", field, DSL.value(value));\n    }\n\n    public static Field<Integer> upperRange(Field<Object> field) {\n        return DSL.field(\"upper({0})\", Integer.class, field);\n    }\n\n    public static Field<Long> length(Field<byte[]> field) {\n        return DSL.field(\"length({0})\", Long.class, field);\n    }\n\n    public static boolean isUniqueViolationError(DataAccessException e) {\n        Throwable cause = e.getCause();\n        // see https://www.postgresql.org/docs/10/errcodes-appendix.html\n        return cause instanceof PSQLException && ((PSQLException) e.getCause()).getSQLState().equals(\"23505\");\n    }\n\n    public static Condition jsonbTextExistsByPath(Field<JSONB> field, List<String> path, String value) {\n        return DSL.condition(\"{0} #> {1} ?? {2}\", field, inline(toPath(path)), DSL.value(value));\n    }\n\n    public static Condition jsonbTextNotExistsByPath(Field<JSONB> field, List<String> path, String value) {\n        return DSL.condition(\"not {0} #> {1} ?? {2}\", field, inline(toPath(path)), DSL.value(value));\n    }\n\n    public static Condition jsonbTextMatch(Field<JSONB> field, List<String> path, String value) {\n        return DSL.condition(\"{0} #>> {1} ~ {2}\", field, inline(toPath(path)), DSL.value(value));\n    }\n\n    /**\n     * Returns a JOOQ field \"now - d\" where \"d\" is the specified duration.\n     * The result is rounded down to seconds.\n     */\n    public static Field<OffsetDateTime> nowMinus(Duration d) {\n        return currentOffsetDateTime().minus(interval((d.toMillis() / 1000 ) + \" seconds\"));\n    }\n\n    public static Field<String> jsonbTypeOf(Field<JSONB> field) {\n        return DSL.field(\"jsonb_typeof({0})\", String.class, field);\n    }\n\n    public static <T> Field<JSONB> jsonbBuildArray(Field<T> field) {\n        return DSL.field(\"jsonb_build_array({0})\", JSONB.class, field);\n    }\n\n    public static Field<JSONB> jsonbBuildObject(Field<?>... kv) {\n        return DSL.function(\"jsonb_build_object\", JSONB.class, kv);\n    }\n\n    public static Field<JSONB> jsonbStripNulls(Field<JSONB> field) {\n        return DSL.function(\"jsonb_strip_nulls\", JSONB.class, field);\n    }\n\n    public static Field<JSONB> jsonbOrEmptyArray(Field<JSONB> field) {\n        return coalesce(field, field(\"?::jsonb\", JSONB.class, JSONB.valueOf(\"[]\")));\n    }\n\n    public static Field<JSONB> jsonbAppend(Field<JSONB> a, JSONB b) {\n        return field(a + \" || ?::jsonb\", JSONB.class, b);\n    }\n\n    public static Field<String> toJsonDate(Field<OffsetDateTime> date) {\n        return toChar(date, \"YYYY-MM-DD\\\"T\\\"HH24:MI:SS.MS\")\n                .concat(replace(toChar(date, \"OF\"), \":\", \"\"));\n    }\n\n    private static String toPath(List<String> path) {\n        return \"{\" + String.join(\",\", path) + \"}\";\n    }\n\n    private PgUtils() {\n    }\n}\n"
  },
  {
    "path": "server/db/src/main/java/com/walmartlabs/concord/db/ResultSetInputStream.java",
    "content": "package com.walmartlabs.concord.db;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\n\npublic class ResultSetInputStream extends InputStream {\n\n    private static final Logger log = LoggerFactory.getLogger(ResultSetInputStream.class);\n\n    public static InputStream open(Connection conn, PreparedStatement ps, int columnIndex) throws SQLException {\n        ResultSet rs = null; // NOSONAR\n        try {\n            rs = ps.executeQuery(); // NOSONAR\n            if (!rs.next()) {\n                closeSilently(rs);\n                return null;\n            }\n        } catch (SQLException e) {\n            closeSilently(rs);\n            throw e;\n        }\n        return new ResultSetInputStream(conn, rs, columnIndex);\n    }\n\n    private static void closeSilently(AutoCloseable c) {\n        if (c == null) {\n            return;\n        }\n\n        try {\n            c.close();\n        } catch (Exception e) {\n            log.error(\"Error while closing a resource\", e);\n        }\n    }\n\n    private final Connection conn;\n    private final ResultSet rs;\n    private final int columnIndex;\n\n    private InputStream delegate;\n\n    private ResultSetInputStream(Connection conn, ResultSet rs, int columnIndex) {\n        this.conn = conn;\n        this.rs = rs;\n        this.columnIndex = columnIndex;\n    }\n\n    @Override\n    public int read() throws IOException {\n        return ensureDelegate().read();\n    }\n\n    @Override\n    public int read(byte[] b) throws IOException {\n        return ensureDelegate().read(b);\n    }\n\n    @Override\n    public int read(byte[] b, int off, int len) throws IOException {\n        return ensureDelegate().read(b, off, len);\n    }\n\n    @Override\n    public int available() throws IOException {\n        return ensureDelegate().available();\n    }\n\n    @Override\n    public long skip(long n) throws IOException {\n        return ensureDelegate().skip(n);\n    }\n\n    @Override\n    public synchronized void mark(int readlimit) {\n        try {\n            ensureDelegate().mark(readlimit);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public synchronized void reset() throws IOException {\n        ensureDelegate().reset();\n    }\n\n    @Override\n    public boolean markSupported() {\n        try {\n            return ensureDelegate().markSupported();\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private InputStream ensureDelegate() throws IOException {\n        if (delegate == null) {\n            try {\n                delegate = rs.getBinaryStream(columnIndex);\n            } catch (SQLException e) {\n                throw new IOException(\"Can't open a stream\", e);\n            }\n        }\n        return delegate;\n    }\n\n    @Override\n    public void close() throws IOException {\n        closeSilently(delegate);\n        closeSilently(rs);\n        closeSilently(conn);\n    }\n}\n"
  },
  {
    "path": "server/db/src/main/resources/META-INF/services/liquibase.lockservice.LockService",
    "content": "com.walmartlabs.concord.server.liquibase.ext.NoopLockService\n"
  },
  {
    "path": "server/db/src/main/resources/META-INF/services/liquibase.logging.LogService",
    "content": "com.walmartlabs.concord.db.LiquibaseLogService\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/liquibase.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd\">\n\n    <include file=\"v0.0.1.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.2.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.12.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.13.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.14.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.17.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.18.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.20.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.21.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.22.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.23.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.24.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.27.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.31.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.38.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.39.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.41.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.44.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.45.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.46.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.47.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.48.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.52.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.56.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.58.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.59.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.60.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.61.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.64.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.65.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.66.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.67.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.69.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.70.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.71.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.74.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.77.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.79.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.80.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.81.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.83.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.84.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.85.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.86.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.87.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.88.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.89.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.90.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.92.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.93.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.95.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.97.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v0.99.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.0.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.5.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.7.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.8.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.10.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.11.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.12.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.13.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.18.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.21.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.22.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.24.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.27.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.28.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.32.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.33.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.34.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.34.1.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.34.2.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.34.3.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.35.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.38.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.40.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.41.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.43.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.45.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.48.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.49.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.56.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.57.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.58.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.60.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.66.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.69.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.75.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.76.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.78.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.79.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.81.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.83.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.85.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.86.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.88.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.90.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.91.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.94.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.95.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.96.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.98.1.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.99.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.102.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.103.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v1.104.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v2.8.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v2.9.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v2.10.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v2.12.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v2.14.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v2.21.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v2.22.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v2.23.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v2.31.0.xml\" relativeToChangelogFile=\"true\"/>\n    <include file=\"v2.35.0.xml\" relativeToChangelogFile=\"true\"/>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.0.1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!-- PROCESS_HISTORY -->\n\n    <!-- removed in 0.13.0+\n    <changeSet id=\"1000\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"PROCESS_HISTORY\" remarks=\"History of process instances\">\n            <column name=\"INSTANCE_ID\" type=\"varchar(36)\" remarks=\"Unique process ID\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"CREATED_DT\" type=\"timestamp\" remarks=\"Time of process creation\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"INITIATOR\" type=\"varchar(128)\"\n                    remarks=\"Identifier of the process initiator (user), can be null\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"CURRENT_STATUS\" type=\"varchar(20)\" remarks=\"Current status of a process\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"LAST_UPDATE_DT\" type=\"timestamp\" remarks=\"Time of last update\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"LOG_FILE_NAME\" type=\"varchar(256)\" remarks=\"Process' log file\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </createTable>\n    </changeSet>\n    -->\n\n    <!-- PROCESS_DEFINITIONS -->\n\n    <!-- deprecated\n    <changeSet id=\"1100\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"PROCESS_DEFINITIONS\" remarks=\"Process definitions (source files)\">\n            <column name=\"DEFINITION_ID\" type=\"varchar(36)\" remarks=\"Unique process definition ID\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"DEFINITION_TYPE\" type=\"varchar(128)\" remarks=\"Type of data\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"DEFINITION_DATA\" type=\"longblob\" remarks=\"Source file\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n    -->\n\n    <!-- USERS -->\n\n    <changeSet id=\"1200\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"USERS\" remarks=\"Users\">\n            <column name=\"USER_ID\" type=\"varchar(36)\" remarks=\"Unique user ID\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"USERNAME\" type=\"varchar(64)\" remarks=\"Unique name of a user (login)\">\n                <constraints unique=\"true\" nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"1210\" author=\"ibodrov@gmail.com\">\n        <insert tableName=\"USERS\">\n            <column name=\"USER_ID\">230c5c9c-d9a7-11e6-bcfd-bb681c07b26c</column>\n            <column name=\"USERNAME\">admin</column>\n        </insert>\n    </changeSet>\n\n    <!-- USER_PERMISSIONS -->\n\n    <!-- removed in 0.57.0+\n    <changeSet id=\"1300\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"USER_PERMISSIONS\" remarks=\"User permissions\">\n            <column name=\"USER_ID\" type=\"varchar(36)\" remarks=\"ID of a user\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"PERMISSION\" type=\"varchar(1024)\" remarks=\"Permission wildcard\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"1310\" author=\"ibodrov@gmail.com\">\n        <addForeignKeyConstraint constraintName=\"FK_PERMISSIONS_USER\"\n                                 baseTableName=\"USER_PERMISSIONS\" baseColumnNames=\"USER_ID\"\n                                 referencedTableName=\"USERS\" referencedColumnNames=\"USER_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n    <changeSet id=\"1320\" author=\"ibodrov@gmail.com\">\n        <insert tableName=\"USER_PERMISSIONS\">\n            <column name=\"USER_ID\">230c5c9c-d9a7-11e6-bcfd-bb681c07b26c</column>\n            <column name=\"PERMISSION\">*:*:*</column>\n        </insert>\n    </changeSet>\n    -->\n\n    <!-- API_KEYS -->\n\n    <changeSet id=\"1400\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"API_KEYS\" remarks=\"API access keys\">\n            <column name=\"KEY_ID\" type=\"varchar(36)\" remarks=\"Unique key ID\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"API_KEY\" type=\"varchar(64)\" remarks=\"SHA-256 hash of a key\">\n                <constraints unique=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"USER_ID\" type=\"varchar(36)\" remarks=\"ID of a key's user\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"1410\" author=\"ibodrov@gmail.com\">\n        <createIndex tableName=\"API_KEYS\" indexName=\"IDX_API_KEY\">\n            <column name=\"API_KEY\"/>\n        </createIndex>\n    </changeSet>\n\n    <changeSet id=\"1420\" author=\"ibodrov@gmail.com\">\n        <addForeignKeyConstraint constraintName=\"FK_API_KEY_USER\"\n                                 baseTableName=\"API_KEYS\" baseColumnNames=\"USER_ID\"\n                                 referencedTableName=\"USERS\" referencedColumnNames=\"USER_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n    <changeSet id=\"1430\" author=\"ibodrov@gmail.com\">\n        <createIndex tableName=\"API_KEYS\" indexName=\"IDX_API_KEY_USER\">\n            <column name=\"USER_ID\"/>\n        </createIndex>\n    </changeSet>\n\n    <!-- disabled in 1.89.0 -->\n    <!--\n    <changeSet id=\"1440\" author=\"ibodrov@gmail.com\">\n        <insert tableName=\"API_KEYS\">\n            <column name=\"KEY_ID\">d5165ca8-e8de-11e6-9bf5-136b5db23c32</column>\n            <column name=\"API_KEY\">KLI+ltQThpx6RQrOc2nDBaM/8tDyVGDw+UVYMXDrqaA</column>\n            <column name=\"USER_ID\">230c5c9c-d9a7-11e6-bcfd-bb681c07b26c</column>\n        </insert>\n    </changeSet>\n    -->\n\n    <!-- SECRETS -->\n\n    <changeSet id=\"1500\" author=\"ibodrov@gmail.com\">\n        <validCheckSum>7:85e7d9c068eb963a250712db421e04ba</validCheckSum>\n        <validCheckSum>7:a9c6beedd779af780cc8045ebf515bb9</validCheckSum>\n        <createTable tableName=\"SECRETS\">\n            <column name=\"SECRET_NAME\" type=\"varchar(128)\" remarks=\"Name (key) of a secret\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"SECRET_TYPE\" type=\"varchar(32)\" remarks=\"Type: SSH_KEY, HTTP_BASIC\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"SECRET_DATA\" type=\"longblob\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <!-- PROJECTS -->\n\n    <changeSet id=\"1600\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"PROJECTS\">\n            <column name=\"PROJECT_NAME\" type=\"varchar(128)\" remarks=\"Name (key) of a project\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <!-- REPOSITORIES -->\n\n    <changeSet id=\"1700\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"REPOSITORIES\">\n            <column name=\"PROJECT_NAME\" type=\"varchar(128)\" remarks=\"Name (key) of a project that owns this repository\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"REPO_NAME\" type=\"varchar(128)\" remarks=\"Name (key) of a repository\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"REPO_URL\" type=\"varchar(2048)\" remarks=\"URL of a repository\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"REPO_BRANCH\" type=\"varchar(255)\" remarks=\"Name of a repository's branch\"/>\n            <column name=\"SECRET_NAME\" type=\"varchar(128)\" remarks=\"ID of a secret used to access this repository\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"1720\" author=\"ibodrov@gmail.com\">\n        <addForeignKeyConstraint constraintName=\"FK_REPOS_PROJECT\"\n                                 baseTableName=\"REPOSITORIES\" baseColumnNames=\"PROJECT_NAME\"\n                                 referencedTableName=\"PROJECTS\" referencedColumnNames=\"PROJECT_NAME\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n    <changeSet id=\"1730\" author=\"ibodrov@gmail.com\">\n        <addForeignKeyConstraint constraintName=\"FK_REPOS_SECRET\"\n                                 baseTableName=\"REPOSITORIES\" baseColumnNames=\"SECRET_NAME\"\n                                 referencedTableName=\"SECRETS\" referencedColumnNames=\"SECRET_NAME\"\n                                 onDelete=\"SET NULL\"/>\n    </changeSet>\n\n    <!-- TEMPLATES -->\n\n    <!-- removed in 0.23.0+\n    <changeSet id=\"1900\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"TEMPLATES\">\n            <column name=\"TEMPLATE_NAME\" type=\"varchar(128)\" remarks=\"Name (key) of a project template\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"TEMPLATE_DATA\" type=\"longblob\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"2000\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"PROJECT_TEMPLATES\">\n            <column name=\"PROJECT_NAME\" type=\"varchar(128)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"TEMPLATE_NAME\" type=\"varchar(128)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"2010\" author=\"ibodrov@gmail.com\">\n        <addForeignKeyConstraint constraintName=\"FK_PROJ_TEMPLATE_PROJ\"\n                                 baseTableName=\"PROJECT_TEMPLATES\" baseColumnNames=\"PROJECT_NAME\"\n                                 referencedTableName=\"PROJECTS\" referencedColumnNames=\"PROJECT_NAME\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n    -->\n\n    <!-- PROJECT_ATTACHMENTS -->\n\n    <!-- removed in 0.18.0+\n    <changeSet id=\"2100\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"PROJECT_ATTACHMENTS\">\n            <column name=\"PROJECT_NAME\" type=\"varchar(128)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"ATTACHMENT_NAME\" type=\"varchar(128)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"ATTACHMENT_DATA\" type=\"longblob\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"2110\" author=\"ibodrov@gmail.com\">\n        <addForeignKeyConstraint constraintName=\"FK_ATTCH_PROJ_ID\"\n                                 baseTableName=\"PROJECT_ATTACHMENTS\" baseColumnNames=\"PROJECT_NAME\"\n                                 referencedTableName=\"PROJECTS\" referencedColumnNames=\"PROJECT_NAME\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n    -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.12.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!-- removed in 0.13.0+\n    <changeSet id=\"12000\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"PROCESS_HISTORY\">\n            <column name=\"PROJECT_NAME\" type=\"varchar(128)\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n    -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.13.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"13000\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"PROCESS_QUEUE\">\n            <column name=\"INSTANCE_ID\" type=\"varchar(36)\" remarks=\"Unique process ID\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"PROJECT_NAME\" type=\"varchar(128)\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"CREATED_AT\" type=\"timestamp\" remarks=\"Timestamp of process creation\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"INITIATOR\" type=\"varchar(128)\" remarks=\"Identifier of the process initiator (user)\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"CURRENT_STATUS\" type=\"varchar(32)\" remarks=\"Current status of a process\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"LAST_AGENT_ID\" type=\"varchar(128)\" remarks=\"ID of the last agent that was executing the process\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"LAST_UPDATED_AT\" type=\"timestamp\" remarks=\"Timestamp of the last update\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"13100\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"USERS\">\n            <column name=\"DISPLAY_NAME\" type=\"varchar(1024)\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.14.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"14000\" author=\"ibodrov@gmail.com\">\n        <createIndex tableName=\"PROCESS_QUEUE\" indexName=\"IDX_PROC_Q_POLL\">\n            <column name=\"CURRENT_STATUS\"/>\n            <column name=\"CREATED_AT\"/>\n        </createIndex>\n    </changeSet>\n\n    <changeSet id=\"14100\" author=\"ibodrov@gmail.com\">\n        <createIndex tableName=\"PROCESS_QUEUE\" indexName=\"IDX_PROC_Q_CR_AT\">\n            <column name=\"CREATED_AT\"/>\n        </createIndex>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.17.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"17000\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"REPOSITORIES\">\n            <column name=\"REPO_COMMIT_ID\" type=\"varchar(64)\" remarks=\"Repository's commit id\"/>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"17500\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"PROJECTS\">\n            <column name=\"DESCRIPTION\" type=\"varchar(1024)\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <!-- removed in 0.57.0+\n    <changeSet id=\"17600\" author=\"ibodrov@gmail.com\">\n        <insert tableName=\"ROLES\">\n            <column name=\"ROLE_NAME\">devtools-admin</column>\n            <column name=\"ROLE_DESCRIPTION\">DevTools - Admin</column>\n        </insert>\n\n        <insert tableName=\"ROLE_PERMISSIONS\">\n            <column name=\"ROLE_NAME\">devtools-admin</column>\n            <column name=\"PERMISSION\">*</column>\n        </insert>\n\n        <insert tableName=\"LDAP_GROUP_ROLES\">\n            <column name=\"MAPPING_ID\">8491c938-41a4-11e7-93cd-a3ce3ef913f9</column>\n            <column name=\"ROLE_NAME\">devtools-admin</column>\n        </insert>\n    </changeSet>\n    -->\n\n    <changeSet id=\"17900\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"PROJECT_KV_STORE\" remarks=\"KV store\">\n            <column name=\"PROJECT_NAME\" type=\"varchar(128)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"VALUE_KEY\" type=\"varchar(128)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"VALUE_LONG\" type=\"number(16)\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"VALUE_STRING\" type=\"varchar(1024)\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </createTable>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.18.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"18000\" author=\"ibodrov@gmail.com\">\n        <validCheckSum>8:1f438bcabb92d2e075e3d80256cebec7</validCheckSum>\n        <validCheckSum>7:23585689b24b220fb101b371a4124952</validCheckSum>\n        <validCheckSum>7:df2c5f86dc6836bf48def6c26bbcb89b</validCheckSum>\n        <addColumn tableName=\"PROJECTS\">\n            <column name=\"PROJECT_CFG\" type=\"longblob\"> <!-- \"longblob\" to force liquibase 3.5.x+ to use \"bytea\" here -->\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"18010\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <tableExists tableName=\"PROJECT_ATTACHMENTS\"/>\n        </preConditions>\n\n        <comment>Migrate project configuration from the attachments table to the main table.</comment>\n\n        <sql>\n            update PROJECTS as p\n            set PROJECT_CFG = a.ATTACHMENT_DATA\n            from PROJECT_ATTACHMENTS as a\n            where p.PROJECT_NAME = a.PROJECT_NAME;\n        </sql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.2.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!-- ROLES -->\n\n    <!-- removed in 0.57.0+\n    <changeSet id=\"3000\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"ROLES\" remarks=\"User roles\">\n            <column name=\"ROLE_NAME\" type=\"varchar(128)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"ROLE_DESCRIPTION\" type=\"varchar(2048)\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </createTable>\n    </changeSet>\n    -->\n\n    <!-- ROLE_PERMISSIONS -->\n\n    <!-- removed in 0.57.0+\n    <changeSet id=\"3100\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"ROLE_PERMISSIONS\" remarks=\"Mapping between roles and permissions\">\n            <column name=\"ROLE_NAME\" type=\"varchar(128)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"PERMISSION\" type=\"varchar(1024)\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"3110\" author=\"ibodrov@gmail.com\">\n        <addForeignKeyConstraint constraintName=\"FK_ROLE_PERM_ROLE\"\n                                 baseTableName=\"ROLE_PERMISSIONS\"\n                                 baseColumnNames=\"ROLE_NAME\"\n                                 referencedTableName=\"ROLES\"\n                                 referencedColumnNames=\"ROLE_NAME\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n    <changeSet id=\"3120\" author=\"ibodrov@gmail.com\">\n        <createIndex tableName=\"ROLE_PERMISSIONS\" indexName=\"IDX_ROLE_PERMS\">\n            <column name=\"ROLE_NAME\"/>\n        </createIndex>\n    </changeSet>\n    -->\n\n    <!-- LDAP_GROUP_MAPPINGS -->\n\n    <!-- removed in 0.57.0+\n    <changeSet id=\"3200\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"LDAP_GROUP_MAPPINGS\" remarks=\"LDAP groups used for mapping to permissions\">\n            <column name=\"MAPPING_ID\" type=\"varchar(36)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"LDAP_DN\" type=\"varchar(1024)\">\n                <constraints unique=\"true\" nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"3210\" author=\"ibodrov@gmail.com\">\n        <createIndex tableName=\"LDAP_GROUP_MAPPINGS\" indexName=\"IDX_LDAP_G_M_GR\" unique=\"true\">\n            <column name=\"LDAP_DN\"/>\n        </createIndex>\n    </changeSet>\n    -->\n\n    <!-- LDAP_GROUP_ROLES -->\n\n    <!-- removed in 0.57.0+\n    <changeSet id=\"3300\" author=\"ibodrov\">\n        <createTable tableName=\"LDAP_GROUP_ROLES\" remarks=\"Roles, associated with group mappings\">\n            <column name=\"MAPPING_ID\" type=\"varchar(36)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"ROLE_NAME\" type=\"varchar(128)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"3310\" author=\"ibodrov@gmail.com\">\n        <addForeignKeyConstraint constraintName=\"FK_LDAP_G_R_ROLE\"\n                                 baseTableName=\"LDAP_GROUP_ROLES\"\n                                 baseColumnNames=\"ROLE_NAME\"\n                                 referencedTableName=\"ROLES\"\n                                 referencedColumnNames=\"ROLE_NAME\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n    -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.20.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"20000\" author=\"ibodrov@gmail.com\">\n        <validCheckSum>7:6319e50c991241cbd35987dd32834a4e</validCheckSum>\n        <validCheckSum>7:b3d65d57ff394117447433f1912ce0ec</validCheckSum>\n        <createTable tableName=\"PROCESS_STATE\">\n            <column name=\"INSTANCE_ID\" type=\"varchar(36)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"ITEM_PATH\" type=\"varchar(2048)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"ITEM_DATA\" type=\"longblob\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.21.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!-- AGENT_COMMANDS -->\n\n    <changeSet id=\"21000\" author=\"ibodrov@gmail.com\">\n        <validCheckSum>7:8bbd692e592acdfe7fb37e647589a978</validCheckSum>\n        <createTable tableName=\"AGENT_COMMANDS\">\n            <column name=\"COMMAND_ID\" type=\"varchar(36)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"AGENT_ID\" type=\"varchar(36)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"COMMAND_STATUS\" type=\"varchar(32)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"CREATED_AT\" type=\"timestamp\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"COMMAND_DATA\" type=\"longblob\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <!-- removed in 1.35.0+\n    <changeSet id=\"21010\" author=\"ibodrov@gmail.com\">\n        <createIndex tableName=\"AGENT_COMMANDS\" indexName=\"IDX_A_CMD_A_ID\">\n            <column name=\"AGENT_ID\"/>\n            <column name=\"COMMAND_STATUS\"/>\n        </createIndex>\n    </changeSet>\n    -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.22.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!-- PROCESS_EVENTS -->\n\n    <changeSet id=\"22000\" author=\"brig@gmail.com\">\n        <createTable tableName=\"PROCESS_EVENTS\">\n            <column name=\"INSTANCE_ID\" type=\"varchar(36)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"EVENT_TYPE\" type=\"varchar(36)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"EVENT_DATE\" type=\"timestamp\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"EVENT_DATA\" type=\"jsonb\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <!-- PROCESS_LOGS -->\n\n    <!-- deprecated in 1.57.0+ -->\n    <!--\n    <changeSet id=\"22100\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"PROCESS_LOGS\">\n            <column name=\"INSTANCE_ID\" type=\"varchar(36)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"CHUNK_RANGE\" type=\"int4range\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"CHUNK_DATA\" type=\"longblob\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"22110\" author=\"ibodrov@gmail.com\">\n        <createIndex tableName=\"PROCESS_LOGS\" indexName=\"IDX_P_LOGS_IDS\">\n            <column name=\"INSTANCE_ID\"/>\n        </createIndex>\n    </changeSet>\n\n    <changeSet id=\"22120\" author=\"ibodrov@gmail.com\">\n        <createView viewName=\"V_PROCESS_LOGS_UPPER_BOUNDS\">\n            select\n                INSTANCE_ID,\n                coalesce(max(upper(CHUNK_RANGE)), 0) as UPPER_BOUND\n            from PROCESS_LOGS\n            group by INSTANCE_ID\n        </createView>\n    </changeSet>\n\n    <changeSet id=\"22130\" author=\"ibodrov@gmail.com\">\n        <createProcedure dbms=\"postgresql\">\n            create or replace function PROCESS_LOG_NEXT_RANGE(PROC_ID varchar, DATA_LEN int)\n            returns int4range as $$\n            declare\n                R_START int;\n            begin\n                select coalesce(UPPER_BOUND, 0) into R_START\n                from V_PROCESS_LOGS_UPPER_BOUNDS where INSTANCE_ID = PROC_ID;\n\n                if R_START is null then\n                    R_START := 0;\n                end if;\n\n                return int4range(R_START, R_START + DATA_LEN);\n            end;\n            $$ language plpgsql;\n        </createProcedure>\n    </changeSet>\n\n    <changeSet id=\"22140\" author=\"ibodrov@gmail.com\">\n        <createProcedure dbms=\"postgresql\">\n            create or replace function PROCESS_LOG_LAST_N_BYTES(PROC_ID varchar, DATA_LEN int)\n            returns int4range as $$\n            declare\n                R_START int;\n            begin\n                select coalesce(UPPER_BOUND, 0) into R_START\n                from V_PROCESS_LOGS_UPPER_BOUNDS where INSTANCE_ID = PROC_ID;\n\n                if R_START is null then\n                    R_START := 0;\n                end if;\n\n                return int4range(R_START - DATA_LEN, R_START);\n            end;\n            $$ language plpgsql;\n        </createProcedure>\n    </changeSet>\n    -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.23.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!-- TEMPLATE_ALIASES -->\n\n    <changeSet id=\"23000\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"TEMPLATE_ALIASES\">\n            <column name=\"TEMPLATE_ALIAS\" type=\"varchar(128)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"TEMPLATE_URL\" type=\"varchar(2048)\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.24.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!-- deprecated in 1.57.0+ -->\n    <!--\n    <changeSet id=\"24000\" author=\"ibodrov@gmail.com\">\n        <createView viewName=\"V_PROCESS_LOGS_SIZE\">\n            select\n                INSTANCE_ID,\n                max(upper(CHUNK_RANGE)) as SIZE\n            from PROCESS_LOGS\n            group by INSTANCE_ID\n        </createView>\n    </changeSet>\n    -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.27.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"27000\" author=\"ybrigo@gmail.com\">\n        <validCheckSum>7:d9ce10fb0c597b471aa74a1c6b213f1c</validCheckSum>\n        <sql>\n            ALTER TABLE PROJECTS ALTER COLUMN PROJECT_CFG TYPE jsonb USING replace(encode(project_cfg, 'escape'), '\\\\', '\\')::jsonb\n        </sql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.31.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!-- USER.USER_ID -->\n\n    <changeSet id=\"31000\" author=\"ibodrov@gmail.com\">\n        <dropForeignKeyConstraint baseTableName=\"API_KEYS\" constraintName=\"FK_API_KEY_USER\"/>\n    </changeSet>\n\n    <changeSet id=\"31010\" author=\"ibodrov@gmail.com\">\n        <modifyDataType tableName=\"API_KEYS\" columnName=\"USER_ID\" newDataType=\"uuid\"/>\n    </changeSet>\n\n    <!-- removed in 0.57.0+\n    <changeSet id=\"31020\" author=\"ibodrov@gmail.com\">\n        <dropForeignKeyConstraint baseTableName=\"USER_PERMISSIONS\" constraintName=\"FK_PERMISSIONS_USER\"/>\n    </changeSet>\n\n    <changeSet id=\"31030\" author=\"ibodrov@gmail.com\">\n        <modifyDataType tableName=\"USER_PERMISSIONS\" columnName=\"USER_ID\" newDataType=\"uuid\"/>\n    </changeSet>\n    -->\n\n    <changeSet id=\"31040\" author=\"ibodrov@gmail.com\">\n        <modifyDataType tableName=\"USERS\" columnName=\"USER_ID\" newDataType=\"uuid\"/>\n    </changeSet>\n\n    <changeSet id=\"31050\" author=\"ibodrov@gmail.com\">\n        <addForeignKeyConstraint constraintName=\"FK_API_KEY_USER\"\n                                 baseTableName=\"API_KEYS\" baseColumnNames=\"USER_ID\"\n                                 referencedTableName=\"USERS\" referencedColumnNames=\"USER_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n    <!-- removed in 0.57.0+\n    <changeSet id=\"31060\" author=\"ibodrov@gmail.com\">\n        <addForeignKeyConstraint constraintName=\"FK_PERMISSIONS_USER\"\n                                 baseTableName=\"USER_PERMISSIONS\" baseColumnNames=\"USER_ID\"\n                                 referencedTableName=\"USERS\" referencedColumnNames=\"USER_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n    -->\n\n    <!-- API_KEY.KEY_ID -->\n\n    <changeSet id=\"31070\" author=\"ibodrov@gmail.com\">\n        <modifyDataType tableName=\"API_KEYS\" columnName=\"KEY_ID\" newDataType=\"uuid\"/>\n    </changeSet>\n\n    <!-- LDAP_GROUP_MAPPINGS.MAPPING_ID -->\n\n    <!-- removed in 0.57.0+\n    <changeSet id=\"31080\" author=\"ibodrov@gmail.com\">\n        <modifyDataType tableName=\"LDAP_GROUP_MAPPINGS\" columnName=\"MAPPING_ID\" newDataType=\"uuid\"/>\n    </changeSet>\n\n    <changeSet id=\"31090\" author=\"ibodrov@gmail.com\">\n        <modifyDataType tableName=\"LDAP_GROUP_ROLES\" columnName=\"MAPPING_ID\" newDataType=\"uuid\"/>\n    </changeSet>\n\n    <changeSet id=\"31100\" author=\"ibodrov@gmail.com\">\n        <sql>\n            DELETE FROM LDAP_GROUP_ROLES R WHERE NOT EXISTS (SELECT * FROM LDAP_GROUP_MAPPINGS M WHERE M.MAPPING_ID = R.MAPPING_ID)\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"31110\" author=\"ibodrov@gmail.com\">\n        <addForeignKeyConstraint constraintName=\"FK_LDAP_G_R_MAPPING\"\n                                 baseTableName=\"LDAP_GROUP_ROLES\" baseColumnNames=\"MAPPING_ID\"\n                                 referencedTableName=\"LDAP_GROUP_MAPPINGS\" referencedColumnNames=\"MAPPING_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n    -->\n\n    <!-- PROCESS_QUEUE.INSTANCE_ID -->\n\n    <changeSet id=\"31120\" author=\"ibodrov@gmail.com\">\n        <modifyDataType tableName=\"PROCESS_QUEUE\" columnName=\"INSTANCE_ID\" newDataType=\"uuid\"/>\n    </changeSet>\n\n    <!-- PROCESS_STATE.INSTANCE_ID -->\n\n    <changeSet id=\"31130\" author=\"ibodrov@gmail.com\">\n        <modifyDataType tableName=\"PROCESS_STATE\" columnName=\"INSTANCE_ID\" newDataType=\"uuid\"/>\n    </changeSet>\n\n    <!-- AGENT_COMMANDS.COMMAND_ID -->\n\n    <changeSet id=\"31140\" author=\"ibodrov@gmail.com\">\n        <modifyDataType tableName=\"AGENT_COMMANDS\" columnName=\"COMMAND_ID\" newDataType=\"uuid\"/>\n    </changeSet>\n\n    <!-- PROCESS_EVENTS.INSTANCE_ID -->\n\n    <changeSet id=\"31150\" author=\"ibodrov@gmail.com\">\n        <modifyDataType tableName=\"PROCESS_EVENTS\" columnName=\"INSTANCE_ID\" newDataType=\"uuid\"/>\n    </changeSet>\n\n    <!-- PROCESS_LOGS.INSTANCE_ID -->\n\n    <!-- deprecated in 1.57.0+ -->\n    <!--\n    <changeSet id=\"31160\" author=\"ibodrov@gmail.com\">\n        <dropView viewName=\"V_PROCESS_LOGS_UPPER_BOUNDS\"/>\n    </changeSet>\n\n    <changeSet id=\"31170\" author=\"ibodrov@gmail.com\">\n        <dropView viewName=\"V_PROCESS_LOGS_SIZE\"/>\n    </changeSet>\n\n    <changeSet id=\"31180\" author=\"ibodrov@gmail.com\">\n        <modifyDataType tableName=\"PROCESS_LOGS\" columnName=\"INSTANCE_ID\" newDataType=\"uuid\"/>\n    </changeSet>\n\n    <changeSet id=\"31190\" author=\"ibodrov@gmail.com\">\n        <createView viewName=\"V_PROCESS_LOGS_UPPER_BOUNDS\">\n            select\n                INSTANCE_ID,\n                coalesce(max(upper(CHUNK_RANGE)), 0) as UPPER_BOUND\n            from PROCESS_LOGS\n            group by INSTANCE_ID\n        </createView>\n    </changeSet>\n\n    <changeSet id=\"31200\" author=\"ibodrov@gmail.com\">\n        <createView viewName=\"V_PROCESS_LOGS_SIZE\">\n            select\n                INSTANCE_ID,\n                max(upper(CHUNK_RANGE)) as SIZE\n            from PROCESS_LOGS\n            group by INSTANCE_ID\n        </createView>\n    </changeSet>\n\n    <changeSet id=\"31210\" author=\"ibodrov@gmail.com\">\n        <sql>\n            DROP FUNCTION PROCESS_LOG_NEXT_RANGE(PROC_ID varchar, DATA_LEN int)\n        </sql>\n        <createProcedure dbms=\"postgresql\">\n            create or replace function PROCESS_LOG_NEXT_RANGE(PROC_ID uuid, DATA_LEN int)\n            returns int4range as $$\n            declare\n                R_START int;\n            begin\n                select coalesce(UPPER_BOUND, 0) into R_START\n                from V_PROCESS_LOGS_UPPER_BOUNDS where INSTANCE_ID = PROC_ID;\n\n                if R_START is null then\n                    R_START := 0;\n                end if;\n\n                return int4range(R_START, R_START + DATA_LEN);\n            end;\n            $$ language plpgsql;\n        </createProcedure>\n    </changeSet>\n\n    <changeSet id=\"31220\" author=\"ibodrov@gmail.com\">\n        <sql>\n            DROP FUNCTION PROCESS_LOG_LAST_N_BYTES(PROC_ID varchar, DATA_LEN int)\n        </sql>\n        <createProcedure dbms=\"postgresql\">\n            create or replace function PROCESS_LOG_LAST_N_BYTES(PROC_ID uuid, DATA_LEN int)\n            returns int4range as $$\n            declare\n                R_START int;\n            begin\n                select coalesce(UPPER_BOUND, 0) into R_START\n                from V_PROCESS_LOGS_UPPER_BOUNDS where INSTANCE_ID = PROC_ID;\n\n                if R_START is null then\n                    R_START := 0;\n                end if;\n\n                return int4range(R_START - DATA_LEN, R_START);\n            end;\n            $$ language plpgsql;\n        </createProcedure>\n    </changeSet>\n    -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.38.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"38000\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"REPOSITORIES\">\n            <column name=\"REPO_PATH\" type=\"varchar(2048)\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.39.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"39000\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"SECRETS\">\n            <column name=\"SECRET_STORE_TYPE\" type=\"varchar(128)\" defaultValue=\"SERVER_KEY\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"39100\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"PARENT_INSTANCE_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"PROCESS_KIND\" type=\"varchar(128)\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"39110\" author=\"ibodrov@gmail.com\">\n        <createIndex tableName=\"PROCESS_QUEUE\" indexName=\"IDX_PROC_Q_PAR_ID\">\n            <column name=\"PARENT_INSTANCE_ID\"/>\n        </createIndex>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.41.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"41000\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"PROCESS_TAGS\" type=\"text[]\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.44.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!-- enable UUID generation -->\n    <!-- deprecated, starting from 2.25.0 we use server-generated UUIDs v7 -->\n    <changeSet id=\"44000-init\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"createExtensionAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"\n        </sql>\n    </changeSet>\n\n    <!-- remove old gunk -->\n    <changeSet id=\"44000-cleanup\" author=\"ibodrov@gmail.com\">\n        <sql>\n            drop table if exists PROJECT_ATTACHMENTS\n        </sql>\n        <sql>\n            drop table if exists PROJECT_TEMPLATES\n        </sql>\n    </changeSet>\n\n    <!-- PROJECTS -->\n\n    <!-- add new PK column for PROJECTS -->\n    <changeSet id=\"44000\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"PROJECTS\">\n            <column name=\"PROJECT_ID\" type=\"uuid\" defaultValueComputed=\"uuid_generate_v1()\">\n                <constraints nullable=\"false\" unique=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <!-- migrate REPOSITORIES to the new PROJECTS FK -->\n    <changeSet id=\"44010\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"REPOSITORIES\">\n            <column name=\"PROJECT_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n\n        <sql>\n            update REPOSITORIES set PROJECT_ID = (select P.PROJECT_ID from PROJECTS as P where P.PROJECT_NAME = REPOSITORIES.PROJECT_NAME)\n        </sql>\n\n        <addNotNullConstraint tableName=\"REPOSITORIES\" columnName=\"PROJECT_ID\"/>\n\n        <dropColumn tableName=\"REPOSITORIES\">\n            <column name=\"PROJECT_NAME\"/>\n        </dropColumn>\n    </changeSet>\n\n    <!-- migrate PROCESS_QUEUE to the new PROJECTS FK -->\n    <changeSet id=\"44020\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"PROJECT_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n\n        <sql>\n            update PROCESS_QUEUE set PROJECT_ID = (select P.PROJECT_ID from PROJECTS as P where P.PROJECT_NAME = PROCESS_QUEUE.PROJECT_NAME)\n        </sql>\n\n        <dropColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"PROJECT_NAME\"/>\n        </dropColumn>\n    </changeSet>\n\n    <!-- create a PROCESS_QUEUE view to simplify queries -->\n    <!-- deprecated in 1.31.0+\n    <changeSet id=\"44025\" author=\"ibodrov@gmail.com\">\n        <createView viewName=\"V_PROCESS_QUEUE\" replaceIfExists=\"true\">\n            select\n                INSTANCE_ID,\n                PROCESS_KIND,\n                PARENT_INSTANCE_ID,\n                PROJECT_ID,\n                (select PROJECT_NAME from PROJECTS where PROJECTS.PROJECT_ID = PROCESS_QUEUE.PROJECT_ID) as PROJECT_NAME,\n                CREATED_AT,\n                INITIATOR,\n                CURRENT_STATUS,\n                LAST_AGENT_ID,\n                LAST_UPDATED_AT,\n                PROCESS_TAGS\n            from PROCESS_QUEUE\n        </createView>\n    </changeSet>\n    -->\n\n    <!-- migrate PROJECT_KV_STORE to the new PROJECTS FK -->\n    <changeSet id=\"44030\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"PROJECT_KV_STORE\">\n            <column name=\"PROJECT_ID\" type=\"uuid\" defaultValueComputed=\"'00000000-0000-0000-0000-000000000000'::uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n\n        <sql>\n            update PROJECT_KV_STORE set PROJECT_ID =\n                coalesce(\n                    (select P.PROJECT_ID from PROJECTS as P where P.PROJECT_NAME = PROJECT_KV_STORE.PROJECT_NAME),\n                    '00000000-0000-0000-0000-000000000000'::uuid)\n        </sql>\n\n        <dropColumn tableName=\"PROJECT_KV_STORE\">\n            <column name=\"PROJECT_NAME\"/>\n        </dropColumn>\n\n        <addPrimaryKey constraintName=\"PK_PROJECT_KV\" tableName=\"PROJECT_KV_STORE\" columnNames=\"PROJECT_ID, VALUE_KEY\"/>\n    </changeSet>\n\n    <!-- set PROJECT_ID as a PK -->\n    <changeSet id=\"44050\" author=\"ibodrov@gmail.com\">\n        <sql>\n            alter table PROJECTS drop constraint PROJECTS_PROJECT_ID_KEY\n        </sql>\n        <dropPrimaryKey tableName=\"PROJECTS\"/>\n        <addPrimaryKey constraintName=\"PK_PROJECTS\" tableName=\"PROJECTS\" columnNames=\"PROJECT_ID\"/>\n    </changeSet>\n\n    <!-- make PROJECT_NAME unique -->\n    <changeSet id=\"44060\" author=\"ibodrov@gmail.com\">\n        <addUniqueConstraint tableName=\"PROJECTS\" columnNames=\"PROJECT_NAME\"/>\n    </changeSet>\n\n    <!-- REPOSITORIES -->\n\n    <!-- add new PK column for REPOSITORIES -->\n    <changeSet id=\"44100\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"REPOSITORIES\">\n            <column name=\"REPO_ID\" type=\"uuid\" defaultValueComputed=\"uuid_generate_v1()\">\n                <constraints nullable=\"false\" unique=\"true\"/>\n            </column>\n        </addColumn>\n\n        <addPrimaryKey constraintName=\"PK_REPOSITORIES\" tableName=\"REPOSITORIES\" columnNames=\"REPO_ID\"/>\n\n        <sql>\n            alter table REPOSITORIES drop constraint REPOSITORIES_REPO_ID_KEY\n        </sql>\n\n        <addUniqueConstraint tableName=\"REPOSITORIES\" columnNames=\"PROJECT_ID, REPO_NAME\"/>\n    </changeSet>\n\n    <!-- SECRETS -->\n\n    <!-- add new PK column for PROJECTS -->\n    <changeSet id=\"44200\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"SECRETS\">\n            <column name=\"SECRET_ID\" type=\"uuid\" defaultValueComputed=\"uuid_generate_v1()\">\n                <constraints nullable=\"false\" unique=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <!-- migrate REPOSITORIES to the new SECRETS FK -->\n    <changeSet id=\"44210\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"REPOSITORIES\">\n            <column name=\"SECRET_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n\n        <sql>\n            update REPOSITORIES set SECRET_ID = (select S.SECRET_ID from SECRETS as S where S.SECRET_NAME = REPOSITORIES.SECRET_NAME)\n        </sql>\n\n        <dropColumn tableName=\"REPOSITORIES\">\n            <column name=\"SECRET_NAME\"/>\n        </dropColumn>\n\n        <addUniqueConstraint tableName=\"SECRETS\" columnNames=\"SECRET_NAME\"/>\n    </changeSet>\n\n    <!-- set SECRET_ID as a PK -->\n    <changeSet id=\"44220\" author=\"ibodrov@gmail.com\">\n        <sql>\n            alter table SECRETS drop constraint SECRETS_SECRET_ID_KEY\n        </sql>\n        <dropPrimaryKey tableName=\"SECRETS\"/>\n        <addPrimaryKey constraintName=\"PK_SECRETS\" tableName=\"SECRETS\" columnNames=\"SECRET_ID\"/>\n    </changeSet>\n\n    <changeSet id=\"44300\" author=\"ibodrov@gmail.com\">\n        <addForeignKeyConstraint baseTableName=\"REPOSITORIES\"\n                                 baseColumnNames=\"SECRET_ID\"\n                                 constraintName=\"FK_RP_SCR_ID\"\n                                 referencedTableName=\"SECRETS\"\n                                 referencedColumnNames=\"SECRET_ID\"/>\n\n    </changeSet>\n\n    <changeSet id=\"44310\" author=\"ibodrov@gmail.com\">\n        <addForeignKeyConstraint baseTableName=\"PROCESS_QUEUE\"\n                                 baseColumnNames=\"PROJECT_ID\"\n                                 constraintName=\"FK_PQ_RPJ_ID\"\n                                 referencedTableName=\"PROJECTS\"\n                                 referencedColumnNames=\"PROJECT_ID\"/>\n    </changeSet>\n\n    <changeSet id=\"44320\" author=\"ibodrov@gmail.com\">\n        <addForeignKeyConstraint baseTableName=\"REPOSITORIES\"\n                                 baseColumnNames=\"PROJECT_ID\"\n                                 constraintName=\"FK_REPO_RPJ_ID\"\n                                 referencedTableName=\"PROJECTS\"\n                                 referencedColumnNames=\"PROJECT_ID\"/>\n    </changeSet>\n\n    <changeSet id=\"44500\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"PROCESS_STATE\">\n            <column name=\"UNIX_MODE\" type=\"number(4)\" defaultValueNumeric=\"420\"> <!-- 420 (hehe) is octal 0644 -->\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.45.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!-- TEAMS -->\n\n    <changeSet id=\"45000\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"TEAMS\" remarks=\"User teams\">\n            <column name=\"TEAM_ID\" type=\"uuid\" defaultValueComputed=\"uuid_generate_v1()\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"TEAM_NAME\" type=\"varchar(128)\">\n                <constraints nullable=\"false\" unique=\"true\"/>\n            </column>\n            <column name=\"DESCRIPTION\" type=\"varchar(2048)\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"IS_ACTIVE\" type=\"boolean\" defaultValueBoolean=\"true\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"45010\" author=\"ibodrov@gmail.com\">\n        <insert tableName=\"TEAMS\">\n            <column name=\"TEAM_ID\">00000000-0000-0000-0000-000000000000</column>\n            <column name=\"TEAM_NAME\">Default</column>\n            <column name=\"DESCRIPTION\">Default team</column>\n        </insert>\n    </changeSet>\n\n    <changeSet id=\"45020\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"PROJECTS\">\n            <column name=\"TEAM_ID\" type=\"uuid\" defaultValueComputed=\"'00000000-0000-0000-0000-000000000000'::uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n\n        <addForeignKeyConstraint baseTableName=\"PROJECTS\"\n                                 baseColumnNames=\"TEAM_ID\"\n                                 constraintName=\"FK_PRJ_TEAM_ID\"\n                                 referencedTableName=\"TEAMS\"\n                                 referencedColumnNames=\"TEAM_ID\"/>\n    </changeSet>\n\n    <changeSet id=\"45030\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"SECRETS\">\n            <column name=\"TEAM_ID\" type=\"uuid\" defaultValueComputed=\"'00000000-0000-0000-0000-000000000000'::uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n\n        <addForeignKeyConstraint baseTableName=\"SECRETS\"\n                                 baseColumnNames=\"TEAM_ID\"\n                                 constraintName=\"FK_PRJ_TEAM_ID\"\n                                 referencedTableName=\"TEAMS\"\n                                 referencedColumnNames=\"TEAM_ID\"/>\n    </changeSet>\n\n    <changeSet id=\"45040\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"USER_TEAMS\">\n            <column name=\"USER_ID\" type=\"uuid\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"TEAM_ID\" type=\"uuid\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <addForeignKeyConstraint baseTableName=\"USER_TEAMS\"\n                                 baseColumnNames=\"USER_ID\"\n                                 constraintName=\"FK_USR_TEAMS_USR\"\n                                 referencedTableName=\"USERS\"\n                                 referencedColumnNames=\"USER_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <addForeignKeyConstraint baseTableName=\"USER_TEAMS\"\n                                 baseColumnNames=\"TEAM_ID\"\n                                 constraintName=\"FK_USR_TEAMS_TEAM\"\n                                 referencedTableName=\"TEAMS\"\n                                 referencedColumnNames=\"TEAM_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <sql>\n            insert into USER_TEAMS\n            select USER_ID, '00000000-0000-0000-0000-000000000000'::uuid as TEAM_ID from USERS\n        </sql>\n    </changeSet>\n\n    <!-- USERS -->\n\n    <changeSet id=\"45500\" author=\"ibodrov@gmail.com\">\n        <addDefaultValue tableName=\"USERS\" columnName=\"USER_ID\" defaultValueComputed=\"uuid_generate_v1()\"/>\n    </changeSet>\n\n    <!-- API_KEYS -->\n\n    <changeSet id=\"45600\" author=\"ibodrov@gmail.com\">\n        <addDefaultValue tableName=\"API_KEYS\" columnName=\"KEY_ID\" defaultValueComputed=\"uuid_generate_v1()\"/>\n    </changeSet>\n\n    <!-- INVENTORY -->\n\n    <changeSet id=\"45700\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"INVENTORIES\" remarks=\"Inventories\">\n            <column name=\"INVENTORY_ID\" type=\"uuid\" defaultValueComputed=\"uuid_generate_v1()\"\n                    remarks=\"Unique ID of the inventory\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"INVENTORY_NAME\" type=\"varchar(128)\" remarks=\"Unique name of the inventory\">\n                <constraints unique=\"true\" nullable=\"false\"/>\n            </column>\n\n            <column name=\"PARENT_INVENTORY_ID\" type=\"uuid\" remarks=\"ID of the parent inventory\">\n                <constraints nullable=\"true\"/>\n            </column>\n\n            <column name=\"TEAM_ID\" type=\"uuid\" remarks=\"FK to a Concord team\"/>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"45701\" author=\"ybrigo@gmail.com\">\n        <addForeignKeyConstraint constraintName=\"FK_INVENTORIES_PARENT\"\n                                 baseTableName=\"INVENTORIES\" baseColumnNames=\"PARENT_INVENTORY_ID\"\n                                 referencedTableName=\"INVENTORIES\" referencedColumnNames=\"INVENTORY_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <addForeignKeyConstraint constraintName=\"FK_INVENTORIES_TEAMS\"\n                                 baseTableName=\"INVENTORIES\" baseColumnNames=\"TEAM_ID\"\n                                 referencedTableName=\"TEAMS\" referencedColumnNames=\"TEAM_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n    <changeSet id=\"45702\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"INVENTORY_DATA\" remarks=\"Inventory data\">\n            <column name=\"INVENTORY_ID\" type=\"uuid\" remarks=\"FK to an inventory\">\n                <constraints nullable=\"false\" primaryKey=\"true\"/>\n            </column>\n            <column name=\"ITEM_PATH\" type=\"varchar(1024)\" remarks=\"Unique (for an inventory) path to an entry\">\n                <constraints nullable=\"false\" primaryKey=\"true\"/>\n            </column>\n            <column name=\"ITEM_DATA\" type=\"jsonb\" remarks=\"JSON data\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"45703\" author=\"ybrigo@gmail.com\">\n        <addForeignKeyConstraint constraintName=\"FK_INVENTORY_DATA_INVENTORY\"\n                                 baseTableName=\"INVENTORY_DATA\" baseColumnNames=\"INVENTORY_ID\"\n                                 referencedTableName=\"INVENTORIES\" referencedColumnNames=\"INVENTORY_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n\n    <changeSet id=\"45704\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"INVENTORY_QUERIES\" remarks=\"Inventory queries\">\n            <column name=\"QUERY_ID\" type=\"uuid\" defaultValueComputed=\"uuid_generate_v1()\" remarks=\"Unique query index\">\n                <constraints nullable=\"false\" primaryKey=\"true\"/>\n            </column>\n            <column name=\"INVENTORY_ID\" type=\"uuid\" remarks=\"FK to an inventory\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"QUERY_NAME\" type=\"varchar(256)\" remarks=\"Unique (for an inventory) query name\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"QUERY_TEXT\" type=\"varchar(4000)\" remarks=\"Query text\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"45705\" author=\"ybrigo@gmail.com\">\n        <addForeignKeyConstraint constraintName=\"FK_INVENTORY_QUERIES_INVENTORY\"\n                                 baseTableName=\"INVENTORY_QUERIES\" baseColumnNames=\"INVENTORY_ID\"\n                                 referencedTableName=\"INVENTORIES\" referencedColumnNames=\"INVENTORY_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <addUniqueConstraint constraintName=\"UNQ_INVENTORY_QUERIES\"\n                             tableName=\"INVENTORY_QUERIES\" columnNames=\"INVENTORY_ID, QUERY_NAME\"/>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.46.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"46000\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"TEAMS\">\n            <column name=\"VISIBILITY\" type=\"varchar(128)\" defaultValue=\"PUBLIC\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"46010\" author=\"ibodrov@gmail.com\">\n        <dropPrimaryKey tableName=\"USER_TEAMS\"/>\n\n        <addColumn tableName=\"USER_TEAMS\">\n            <column name=\"TEAM_ROLE\" type=\"varchar(128)\" defaultValue=\"WRITER\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n\n        <addPrimaryKey tableName=\"USER_TEAMS\" columnNames=\"USER_ID, TEAM_ID, TEAM_ROLE\"/>\n\n        <sql>\n            update USER_TEAMS set TEAM_ROLE = 'OWNER' where USER_ID = '230c5c9c-d9a7-11e6-bcfd-bb681c07b26c'\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"46020\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"ADMINS\">\n            <column name=\"USER_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <addForeignKeyConstraint baseTableName=\"ADMINS\"\n                                 baseColumnNames=\"USER_ID\"\n                                 constraintName=\"FK_ADMINS_U_ID\"\n                                 referencedTableName=\"USERS\"\n                                 referencedColumnNames=\"USER_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <sql>\n            insert into ADMINS (USER_ID) values ('230c5c9c-d9a7-11e6-bcfd-bb681c07b26c')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"46100\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"PROJECTS\">\n            <column name=\"VISIBILITY\" type=\"varchar(128)\" defaultValue=\"PUBLIC\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <!-- deprecated in 1.26.0+ -->\n    <!--\n    <changeSet id=\"46200\" author=\"brig@gmail.com\">\n        <validCheckSum>7:4da9f8084cf605c5bb8dbf860e705266</validCheckSum>\n        <createTable tableName=\"LANDING_PAGE\">\n            <column name=\"LANDING_PAGE_ID\" type=\"uuid\" defaultValueComputed=\"uuid_generate_v1()\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"PROJECT_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"REPO_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"NAME\" type=\"varchar(128)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"DESCRIPTION\" type=\"varchar(512)\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"ICON\" type=\"bytea\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </createTable>\n\n        <addForeignKeyConstraint baseTableName=\"LANDING_PAGE\"\n                                 baseColumnNames=\"PROJECT_ID\"\n                                 constraintName=\"FK_LANDING_PAGE_PROJECT_ID\"\n                                 referencedTableName=\"PROJECTS\"\n                                 referencedColumnNames=\"PROJECT_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <addForeignKeyConstraint baseTableName=\"LANDING_PAGE\"\n                                 baseColumnNames=\"REPO_ID\"\n                                 constraintName=\"FK_LANDING_PAGE_REPO_ID\"\n                                 referencedTableName=\"REPOSITORIES\"\n                                 referencedColumnNames=\"REPO_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n    -->\n\n    <!-- deprecated in 1.58.0+ -->\n    <!--\n    <changeSet id=\"46300\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"REPOSITORIES\">\n            <column name=\"PUSH_EVENT_DATE\" type=\"timestamp\" defaultValueComputed=\"current_timestamp\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n    -->\n\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.47.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <property name=\"concordSystemTeamId\" value=\"ad76f1e2-c33c-11e7-8064-f7371c66fa77\"/>\n    <property name=\"concordTriggersProjectId\" value=\"ad76f1e2-c33c-11e7-8064-f7371c66fa77\"/>\n    <property name=\"concordTriggersRepoId\" value=\"b31b0b06-c33c-11e7-b0e9-8702fc03629f\"/>\n    <property name=\"concordRepositoryTriggersRepoId\" value=\"fb795b1f-bece-4fce-90dc-4e5aca44eeb2\"/>\n    <property name=\"concordGithubUserId\" value=\"acc17a02-b471-46af-9914-48cba3dd31ab\"/>\n\n    <changeSet id=\"47000\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"TRIGGERS\">\n            <column name=\"TRIGGER_ID\" type=\"uuid\" defaultValueComputed=\"uuid_generate_v1()\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"PROJECT_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"REPO_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"EVENT_SOURCE\" type=\"varchar(128)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"ENTRY_POINT\" type=\"varchar(128)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"ARGUMENTS\" type=\"jsonb\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"CONDITIONS\" type=\"jsonb\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </createTable>\n\n        <addForeignKeyConstraint baseTableName=\"TRIGGERS\"\n                                 baseColumnNames=\"PROJECT_ID\"\n                                 constraintName=\"FK_TRIGGERS_PROJECT_ID\"\n                                 referencedTableName=\"PROJECTS\"\n                                 referencedColumnNames=\"PROJECT_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <addForeignKeyConstraint baseTableName=\"TRIGGERS\"\n                                 baseColumnNames=\"REPO_ID\"\n                                 constraintName=\"FK_TRIGGERS_REPO_ID\"\n                                 referencedTableName=\"REPOSITORIES\"\n                                 referencedColumnNames=\"REPO_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n    <changeSet id=\"47010\" author=\"ibodrov@gmail.com\">\n        <createIndex tableName=\"TRIGGERS\" indexName=\"IDX_TRIG_EV_SRC\">\n            <column name=\"EVENT_SOURCE\"/>\n        </createIndex>\n    </changeSet>\n\n    <changeSet id=\"47100\" author=\"ybrigo@gmail.com\">\n        <insert tableName=\"TEAMS\">\n            <column name=\"TEAM_ID\">${concordSystemTeamId}</column>\n            <column name=\"TEAM_NAME\">Concord</column>\n            <column name=\"DESCRIPTION\">Concord System Team</column>\n            <column name=\"VISIBILITY\">PRIVATE</column>\n        </insert>\n\n        <insert tableName=\"PROJECTS\">\n            <column name=\"PROJECT_ID\" value=\"${concordTriggersProjectId}\"/>\n            <column name=\"TEAM_ID\" value=\"${concordSystemTeamId}\"/>\n            <column name=\"PROJECT_NAME\" value=\"concordTriggers\"/>\n            <column name=\"VISIBILITY\" value=\"PRIVATE\"/>\n        </insert>\n\n        <insert tableName=\"REPOSITORIES\">\n            <column name=\"PROJECT_ID\" value=\"${concordTriggersProjectId}\"/>\n            <column name=\"REPO_ID\" value=\"${concordTriggersRepoId}\"/>\n            <column name=\"REPO_NAME\" value=\"triggers\"/>\n            <column name=\"REPO_URL\" value=\"classpath://com/walmartlabs/concord/server/triggers/concord.yml\"/>\n        </insert>\n\n        <insert tableName=\"TRIGGERS\">\n            <column name=\"PROJECT_ID\" value=\"${concordTriggersProjectId}\"/>\n            <column name=\"REPO_ID\" value=\"${concordTriggersRepoId}\"/>\n            <column name=\"EVENT_SOURCE\" value=\"github\"/>\n            <column name=\"ENTRY_POINT\" value=\"onChange\"/>\n        </insert>\n\n        <insert tableName=\"TRIGGERS\">\n            <column name=\"PROJECT_ID\" value=\"${concordTriggersProjectId}\"/>\n            <column name=\"REPO_ID\" value=\"${concordTriggersRepoId}\"/>\n            <column name=\"EVENT_SOURCE\" value=\"concord\"/>\n            <column name=\"ENTRY_POINT\" value=\"onChange\"/>\n            <column name=\"CONDITIONS\">\n                {\"event\": \"repository.*\"}\n            </column>\n        </insert>\n    </changeSet>\n\n    <changeSet id=\"47200\" author=\"ibodrov@gmail.com\">\n        <sql>\n            alter table SECRETS drop constraint SECRETS_SECRET_NAME_KEY\n        </sql>\n        <addUniqueConstraint tableName=\"SECRETS\" columnNames=\"TEAM_ID, SECRET_NAME\"/>\n    </changeSet>\n\n    <changeSet id=\"47500\" author=\"ibodrov@gmail.com\">\n        <sql>\n            alter table PROCESS_QUEUE\n            drop constraint FK_PQ_RPJ_ID,\n            add constraint FK_PQ_PRJ_ID\n            foreign key (PROJECT_ID)\n            references PROJECTS(PROJECT_ID)\n            on delete set null\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"47600\" author=\"ybrigo@gmail.com\">\n        <insert tableName=\"USERS\">\n            <column name=\"USER_ID\" value=\"${concordGithubUserId}\"/>\n            <column name=\"USERNAME\" value=\"github\"/>\n        </insert>\n        <insert tableName=\"USER_TEAMS\">\n            <column name=\"USER_ID\" value=\"${concordGithubUserId}\"/>\n            <column name=\"TEAM_ID\" value=\"00000000-0000-0000-0000-000000000000\"/>\n            <column name=\"TEAM_ROLE\" value=\"WRITER\"/>\n        </insert>\n    </changeSet>\n\n    <changeSet id=\"47900\" author=\"ibodrov@gmail.com\">\n        <dropUniqueConstraint tableName=\"PROJECTS\" constraintName=\"PROJECTS_PROJECT_NAME_KEY\"/>\n        <addUniqueConstraint tableName=\"PROJECTS\" columnNames=\"TEAM_ID, PROJECT_NAME\"/>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.48.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <property name=\"concordDefaultOrgId\" value=\"0fac1b18-d179-11e7-b3e7-d7df4543ed4f\"/>\n    <property name=\"concordSystemOrgId\" value=\"94a35e54-d204-11e7-9a97-c32b8f0c3380\"/>\n\n    <changeSet id=\"48000\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"ORGANIZATIONS\">\n            <column name=\"ORG_ID\" type=\"uuid\" defaultValueComputed=\"uuid_generate_v1()\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"ORG_NAME\" type=\"varchar(128)\">\n                <constraints nullable=\"false\" unique=\"true\"/>\n            </column>\n            <column name=\"VISIBILITY\" type=\"varchar(128)\" defaultValue=\"PUBLIC\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <insert tableName=\"ORGANIZATIONS\">\n            <column name=\"ORG_ID\">${concordDefaultOrgId}</column>\n            <column name=\"ORG_NAME\">Default</column>\n        </insert>\n\n        <insert tableName=\"ORGANIZATIONS\">\n            <column name=\"ORG_ID\">${concordSystemOrgId}</column>\n            <column name=\"ORG_NAME\">ConcordSystem</column>\n        </insert>\n    </changeSet>\n\n    <changeSet id=\"48010\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"TEAMS\">\n            <column name=\"ORG_ID\" type=\"uuid\" defaultValue=\"'${concordDefaultOrgId}'\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"48020\" author=\"ibodrov@gmail.com\">\n        <dropColumn tableName=\"PROJECTS\" columnName=\"TEAM_ID\"/>\n\n        <addColumn tableName=\"PROJECTS\">\n            <column name=\"ORG_ID\" type=\"uuid\" defaultValue=\"'${concordDefaultOrgId}'\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n\n        <addForeignKeyConstraint baseTableName=\"PROJECTS\"\n                                 baseColumnNames=\"ORG_ID\"\n                                 constraintName=\"FK_PRJ_ORG_ID\"\n                                 referencedTableName=\"ORGANIZATIONS\"\n                                 referencedColumnNames=\"ORG_ID\"/>\n    </changeSet>\n\n    <changeSet id=\"48030\" author=\"ibodrov@gmail.com\">\n        <dropColumn tableName=\"SECRETS\" columnName=\"TEAM_ID\"/>\n\n        <addColumn tableName=\"SECRETS\">\n            <column name=\"ORG_ID\" type=\"uuid\" defaultValue=\"'${concordDefaultOrgId}'\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n\n        <addForeignKeyConstraint baseTableName=\"SECRETS\"\n                                 baseColumnNames=\"ORG_ID\"\n                                 constraintName=\"FK_SECRET_ORG_ID\"\n                                 referencedTableName=\"ORGANIZATIONS\"\n                                 referencedColumnNames=\"ORG_ID\"/>\n    </changeSet>\n\n    <changeSet id=\"48040\" author=\"ibodrov@gmail.com\">\n        <dropColumn tableName=\"INVENTORIES\" columnName=\"TEAM_ID\"/>\n\n        <addColumn tableName=\"INVENTORIES\">\n            <column name=\"ORG_ID\" type=\"uuid\" defaultValue=\"'${concordDefaultOrgId}'\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n\n        <addForeignKeyConstraint baseTableName=\"INVENTORIES\"\n                                 baseColumnNames=\"ORG_ID\"\n                                 constraintName=\"FK_INV_ORG_ID\"\n                                 referencedTableName=\"ORGANIZATIONS\"\n                                 referencedColumnNames=\"ORG_ID\"/>\n    </changeSet>\n\n    <changeSet id=\"48050\" author=\"ibodrov@gmail.com\">\n        <dropColumn tableName=\"TEAMS\" columnName=\"VISIBILITY\"/>\n    </changeSet>\n\n    <changeSet id=\"48060\" author=\"ibodrov@gmail.com\">\n        <dropUniqueConstraint tableName=\"TEAMS\" constraintName=\"TEAMS_TEAM_NAME_KEY\"/>\n        <addUniqueConstraint tableName=\"TEAMS\" columnNames=\"ORG_ID, TEAM_NAME\"/>\n    </changeSet>\n\n    <changeSet id=\"48070\" author=\"ibodrov@gmail.com\">\n        <dropColumn tableName=\"TEAMS\" columnName=\"IS_ACTIVE\"/>\n    </changeSet>\n\n    <changeSet id=\"48090\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"PROJECT_TEAM_ACCESS\">\n            <column name=\"PROJECT_ID\" type=\"uuid\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"TEAM_ID\" type=\"uuid\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"ACCESS_LEVEL\" type=\"varchar(128)\" defaultValue=\"READER\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <addForeignKeyConstraint baseTableName=\"PROJECT_TEAM_ACCESS\"\n                                 baseColumnNames=\"PROJECT_ID\"\n                                 constraintName=\"FK_PRJ_T_A_PRJ\"\n                                 referencedTableName=\"PROJECTS\"\n                                 referencedColumnNames=\"PROJECT_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <addForeignKeyConstraint baseTableName=\"PROJECT_TEAM_ACCESS\"\n                                 baseColumnNames=\"TEAM_ID\"\n                                 constraintName=\"FK_PRJ_T_A_T\"\n                                 referencedTableName=\"TEAMS\"\n                                 referencedColumnNames=\"TEAM_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n    <changeSet id=\"48100\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"PROJECTS\">\n            <column name=\"OWNER_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n\n        <addForeignKeyConstraint baseTableName=\"PROJECTS\"\n                                 baseColumnNames=\"OWNER_ID\"\n                                 constraintName=\"FK_PRJ_OWN_ID\"\n                                 referencedTableName=\"USERS\"\n                                 referencedColumnNames=\"USER_ID\"\n                                 onDelete=\"SET NULL\"/>\n\n        <addUniqueConstraint tableName=\"PROJECTS\"\n                             columnNames=\"ORG_ID, PROJECT_NAME\"/>\n    </changeSet>\n\n    <changeSet id=\"48110\" author=\"ibodrov@gmail.com\">\n        <sql>\n            update PROJECTS\n            set ORG_ID = '${concordSystemOrgId}'\n            where PROJECT_ID = '${concordTriggersProjectId}'\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"48120\" author=\"ibodrov@gmail.com\">\n        <sql>\n            update TEAMS\n            set TEAM_NAME = 'default'\n            where TEAM_ID = '00000000-0000-0000-0000-000000000000'\n        </sql>\n\n        <sql>\n            update TEAMS\n            set TEAM_NAME = 'default',\n            ORG_ID = '${concordSystemOrgId}'\n            where TEAM_ID = '${concordSystemTeamId}'\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"48130\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"SECRETS\">\n            <column name=\"OWNER_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n\n        <addForeignKeyConstraint baseTableName=\"SECRETS\"\n                                 baseColumnNames=\"OWNER_ID\"\n                                 constraintName=\"FK_SCRT_OWN_ID\"\n                                 referencedTableName=\"USERS\"\n                                 referencedColumnNames=\"USER_ID\"\n                                 onDelete=\"SET NULL\"/>\n    </changeSet>\n\n    <changeSet id=\"48140\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"SECRET_TEAM_ACCESS\">\n            <column name=\"SECRET_ID\" type=\"uuid\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"TEAM_ID\" type=\"uuid\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"ACCESS_LEVEL\" type=\"varchar(128)\" defaultValue=\"READER\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <addForeignKeyConstraint baseTableName=\"SECRET_TEAM_ACCESS\"\n                                 baseColumnNames=\"SECRET_ID\"\n                                 constraintName=\"FK_SCRT_T_A_SCRT\"\n                                 referencedTableName=\"SECRETS\"\n                                 referencedColumnNames=\"SECRET_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <addForeignKeyConstraint baseTableName=\"SECRET_TEAM_ACCESS\"\n                                 baseColumnNames=\"TEAM_ID\"\n                                 constraintName=\"FK_SCRT_T_A_T\"\n                                 referencedTableName=\"TEAMS\"\n                                 referencedColumnNames=\"TEAM_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n    <changeSet id=\"48150\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"SECRETS\">\n            <column name=\"VISIBILITY\" type=\"varchar(128)\" defaultValue=\"PUBLIC\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <!-- deprecated in 1.28.0+ -->\n    <!--\n    <changeSet id=\"48160\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"PROJECTS\">\n            <column name=\"ACCEPTS_RAW_PAYLOAD\" type=\"bool\" defaultValueBoolean=\"true\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n    -->\n\n    <!-- deprecated in 1.65.0+ -->\n    <!--\n    <changeSet id=\"48200\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"USERS\">\n            <column name=\"IS_ADMIN\" type=\"bool\" defaultValueBoolean=\"false\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n\n        <sql>\n            update USERS\n            set IS_ADMIN = true\n            where exists\n                (select USER_ID from ADMINS where ADMINS.USER_ID = USERS.USER_ID)\n        </sql>\n\n        <dropTable tableName=\"ADMINS\"/>\n    </changeSet>\n    -->\n\n    <changeSet id=\"48300\" author=\"ybrigo@gmail.com\">\n        <update tableName=\"REPOSITORIES\">\n            <column name=\"REPO_URL\" value=\"classpath://com/walmartlabs/concord/server/org/triggers/concord.yml\"/>\n            <where>REPO_ID = 'b31b0b06-c33c-11e7-b0e9-8702fc03629f'</where>\n        </update>\n    </changeSet>\n\n    <changeSet id=\"48301\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"INVENTORIES\">\n            <column name=\"VISIBILITY\" type=\"varchar(128)\" defaultValue=\"PUBLIC\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"48302\" author=\"ybrigo@gmail.com\">\n        <dropUniqueConstraint tableName=\"INVENTORIES\" constraintName=\"INVENTORIES_INVENTORY_NAME_KEY\"/>\n        <addUniqueConstraint tableName=\"INVENTORIES\" columnNames=\"ORG_ID, INVENTORY_NAME\"/>\n\n        <addColumn tableName=\"INVENTORIES\">\n            <column name=\"OWNER_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n\n        <addForeignKeyConstraint baseTableName=\"INVENTORIES\"\n                                 baseColumnNames=\"OWNER_ID\"\n                                 constraintName=\"FK_INVENTORIES_OWN_ID\"\n                                 referencedTableName=\"USERS\"\n                                 referencedColumnNames=\"USER_ID\"\n                                 onDelete=\"SET NULL\"/>\n    </changeSet>\n\n    <changeSet id=\"48303\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"INVENTORY_TEAM_ACCESS\">\n            <column name=\"INVENTORY_ID\" type=\"uuid\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"TEAM_ID\" type=\"uuid\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"ACCESS_LEVEL\" type=\"varchar(128)\" defaultValue=\"READER\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <addForeignKeyConstraint baseTableName=\"INVENTORY_TEAM_ACCESS\"\n                                 baseColumnNames=\"INVENTORY_ID\"\n                                 constraintName=\"FK_INV_T_A_INV\"\n                                 referencedTableName=\"INVENTORIES\"\n                                 referencedColumnNames=\"INVENTORY_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <addForeignKeyConstraint baseTableName=\"INVENTORY_TEAM_ACCESS\"\n                                 baseColumnNames=\"TEAM_ID\"\n                                 constraintName=\"FK_INV_T_A_T\"\n                                 referencedTableName=\"TEAMS\"\n                                 referencedColumnNames=\"TEAM_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n    <changeSet id=\"48500\" author=\"ibodrov@gmail.com\">\n        <sql>\n            update PROJECTS set VISIBILITY = 'PUBLIC' where PROJECT_ID = '${concordTriggersProjectId}'\n        </sql>\n    </changeSet>\n\n    <!-- deprecated in 1.31.0+\n    <changeSet id=\"48550\" author=\"ibodrov@gmail.com\">\n        <createView viewName=\"V_PROCESS_QUEUE\" replaceIfExists=\"true\">\n            select\n                INSTANCE_ID,\n                PROCESS_KIND,\n                PARENT_INSTANCE_ID,\n                prj.PROJECT_ID,\n                prj.PROJECT_NAME,\n                prj.ORG_ID,\n                prj.ORG_NAME,\n                CREATED_AT,\n                INITIATOR,\n                CURRENT_STATUS,\n                LAST_AGENT_ID,\n                LAST_UPDATED_AT,\n                PROCESS_TAGS\n            from PROCESS_QUEUE\n            left join\n                (select\n                    PROJECT_ID,\n                    PROJECT_NAME,\n                    ORG_ID,\n                    (select ORG_NAME from ORGANIZATIONS where ORGANIZATIONS.ORG_ID = PROJECTS.ORG_ID)\n                from PROJECTS) prj\n                on prj.PROJECT_ID = PROCESS_QUEUE.PROJECT_ID\n        </createView>\n    </changeSet>\n    -->\n\n    <changeSet id=\"48600\" author=\"ibodrov@gmail.com\">\n        <dropForeignKeyConstraint baseTableName=\"REPOSITORIES\" constraintName=\"FK_REPO_RPJ_ID\"/>\n        <addForeignKeyConstraint baseTableName=\"REPOSITORIES\"\n                                 baseColumnNames=\"PROJECT_ID\"\n                                 constraintName=\"FK_REPO_PRJ_ID\"\n                                 referencedTableName=\"PROJECTS\"\n                                 referencedColumnNames=\"PROJECT_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n    <!-- deprecated in 1.31.0+\n    <changeSet id=\"48700\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"REPOSITORIES\">\n            <column name=\"WEBHOOK_ID\" type=\"bigint\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n    -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.52.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!-- removed in 0.97.0+\n    <changeSet id=\"52000\" author=\"ybrigo@gmail.com\">\n        <dropColumn tableName=\"REPOSITORIES\" columnName=\"WEBHOOK_ID\"/>\n\n        <addColumn tableName=\"REPOSITORIES\">\n            <column name=\"HAS_WEBHOOK\" type=\"boolean\" defaultValue=\"false\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n    -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.56.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"56000\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"USERS\">\n            <column name=\"USER_TYPE\" type=\"varchar(32)\" defaultValue=\"LDAP\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n\n        <sql>\n            update USERS set USER_TYPE = 'LOCAL' where USERNAME in ('admin', 'github');\n        </sql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.58.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"58000\" author=\"ibodrov@gmail.com\">\n        <sql>\n            alter table USER_TEAMS alter column TEAM_ROLE set default 'MEMBER'\n        </sql>\n\n        <sql>\n            update USER_TEAMS set TEAM_ROLE = 'MEMBER' where USER_ID = '${concordGithubUserId}'::uuid\n        </sql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.59.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"59000\" author=\"ibodrov@gmail.com\">\n        <dropForeignKeyConstraint baseTableName=\"REPOSITORIES\"\n                                  constraintName=\"FK_RP_SCR_ID\"/>\n\n        <addForeignKeyConstraint baseTableName=\"REPOSITORIES\"\n                                 baseColumnNames=\"SECRET_ID\"\n                                 constraintName=\"FK_RP_SCR_ID\"\n                                 referencedTableName=\"SECRETS\"\n                                 referencedColumnNames=\"SECRET_ID\"\n                                 onDelete=\"SET NULL\"/>\n    </changeSet>\n\n    <changeSet id=\"59100\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"PROCESS_EVENTS\">\n            <column name=\"EVENT_ID\" type=\"uuid\" defaultValueComputed=\"uuid_generate_v1()\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"59110\" author=\"ibodrov@gmail.com\">\n        <createIndex tableName=\"PROCESS_EVENTS\" indexName=\"IDX_PROC_EV_I_ID_DT\">\n            <column name=\"INSTANCE_ID\"/>\n            <column name=\"EVENT_DATE\"/>\n        </createIndex>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.60.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"60000\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"START_AT\" type=\"timestamp\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.61.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"61200\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"POLICIES\">\n            <column name=\"POLICY_ID\" type=\"uuid\" defaultValueComputed=\"uuid_generate_v1()\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"POLICY_NAME\" type=\"varchar(256)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"RULES\" type=\"jsonb\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"61201\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"POLICY_LINKS\">\n            <column name=\"ORG_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"PROJECT_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"POLICY_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <addForeignKeyConstraint baseTableName=\"POLICY_LINKS\"\n                                 baseColumnNames=\"ORG_ID\"\n                                 constraintName=\"FK_POLICY_LINK_REPO_ID\"\n                                 referencedTableName=\"ORGANIZATIONS\"\n                                 referencedColumnNames=\"ORG_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <addForeignKeyConstraint baseTableName=\"POLICY_LINKS\"\n                                 baseColumnNames=\"PROJECT_ID\"\n                                 constraintName=\"FK_POLICY_LINK_PROJECT_ID\"\n                                 referencedTableName=\"PROJECTS\"\n                                 referencedColumnNames=\"PROJECT_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <addForeignKeyConstraint baseTableName=\"POLICY_LINKS\"\n                                 baseColumnNames=\"POLICY_ID\"\n                                 constraintName=\"FK_POLICY_LINK_POLICY_ID\"\n                                 referencedTableName=\"POLICIES\"\n                                 referencedColumnNames=\"POLICY_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n    <changeSet id=\"61202\" author=\"ybrigo@gmail.com\">\n        <createIndex tableName=\"POLICY_LINKS\" indexName=\"IDX_POLICY_LINK_1\" unique=\"true\">\n            <column name=\"ORG_ID\"/>\n            <column name=\"PROJECT_ID\"/>\n            <column name=\"POLICY_ID\"/>\n        </createIndex>\n        <modifySql>\n            <append value=\"where ORG_ID is not null and PROJECT_ID is not null\"/>\n        </modifySql>\n    </changeSet>\n    <changeSet id=\"61203\" author=\"ybrigo@gmail.com\">\n        <createIndex tableName=\"POLICY_LINKS\" indexName=\"IDX_POLICY_LINK_2\" unique=\"true\">\n            <column name=\"POLICY_ID\"/>\n        </createIndex>\n        <modifySql>\n            <append value=\"WHERE ORG_ID is null and PROJECT_ID is null\"/>\n        </modifySql>\n    </changeSet>\n    <changeSet id=\"61204\" author=\"ybrigo@gmail.com\">\n        <createIndex tableName=\"POLICY_LINKS\" indexName=\"IDX_POLICY_LINK_3\" unique=\"true\">\n            <column name=\"ORG_ID\"/>\n            <column name=\"POLICY_ID\"/>\n        </createIndex>\n        <modifySql>\n            <append value=\"WHERE ORG_ID is not null and PROJECT_ID is null\"/>\n        </modifySql>\n    </changeSet>\n    <changeSet id=\"61205\" author=\"ybrigo@gmail.com\">\n        <createIndex tableName=\"POLICY_LINKS\" indexName=\"IDX_POLICY_LINK_4\" unique=\"true\">\n            <column name=\"PROJECT_ID\"/>\n            <column name=\"POLICY_ID\"/>\n        </createIndex>\n        <modifySql>\n            <append value=\"WHERE ORG_ID is null and PROJECT_ID is not null\"/>\n        </modifySql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.64.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"64000\" author=\"ybrigo@gmail.com\">\n        <renameColumn tableName=\"SECRETS\" oldColumnName=\"SECRET_STORE_TYPE\" newColumnName=\"ENCRYPTED_BY\"/>\n        <addColumn tableName=\"SECRETS\">\n            <column name=\"STORE_TYPE\" type=\"varchar(128)\" defaultValue=\"CONCORD\"/>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"64001\" author=\"ybrigo@gmail.com\">\n        <dropNotNullConstraint tableName=\"SECRETS\" columnName=\"SECRET_DATA\"/>\n    </changeSet>\n\n    <changeSet id=\"64100\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"AUDIT_LOG\">\n            <!-- for external consumers -->\n            <column name=\"ENTRY_ID\" type=\"bigint\" autoIncrement=\"true\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n\n            <column name=\"ENTRY_DATE\" type=\"timestamp\" defaultValueComputed=\"current_timestamp\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"USER_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"ENTRY_OBJECT\" type=\"varchar(128)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"ENTRY_ACTION\" type=\"varchar(128)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"ENTRY_DETAILS\" type=\"jsonb\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <!-- replaced in 0.67.0 -->\n    <!--\n    <changeSet id=\"64101\" author=\"ibodrov@gmail.com\">\n        <createView viewName=\"V_AUDIT_LOG\">\n            select\n                ENTRY_ID,\n                ENTRY_DATE,\n                USER_ID,\n                (select USERNAME from USERS u where u.USER_ID = a.USER_ID) as USERNAME,\n                ENTRY_OBJECT,\n                ENTRY_ACTION,\n                ENTRY_DETAILS\n            from AUDIT_LOG a\n        </createView>\n    </changeSet>\n    -->\n\n    <changeSet id=\"64200\" author=\"ibodrov@gmail.com\">\n        <addUniqueConstraint tableName=\"POLICIES\" columnNames=\"POLICY_NAME\"/>\n    </changeSet>\n\n    <changeSet id=\"64300\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"ORGANIZATIONS\">\n            <column name=\"META\" type=\"jsonb\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.65.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd\">\n\n    <property name=\"concordCronUserId\" value=\"1f9ae527-e7ab-42c0-b0e5-0092f9285f22\"/>\n\n    <changeSet id=\"65000\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"TRIGGER_SCHEDULE\">\n            <column name=\"TRIGGER_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"FIRE_AT\" type=\"timestamp\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <addForeignKeyConstraint baseTableName=\"TRIGGER_SCHEDULE\"\n                                 baseColumnNames=\"TRIGGER_ID\"\n                                 constraintName=\"FK_TRIGGER_SCHEDULE_TR_ID\"\n                                 referencedTableName=\"TRIGGERS\"\n                                 referencedColumnNames=\"TRIGGER_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n    <changeSet id=\"65010\" author=\"ybrigo@gmail.com\">\n        <insert tableName=\"USERS\">\n            <column name=\"USER_ID\" value=\"${concordCronUserId}\"/>\n            <column name=\"USERNAME\" value=\"cron\"/>\n            <column name=\"USER_TYPE\" value=\"LOCAL\"/>\n        </insert>\n    </changeSet>\n\n    <changeSet id=\"65020\" author=\"ybrigo@gmail.com\">\n        <createIndex tableName=\"TRIGGER_SCHEDULE\" indexName=\"IDX_TRIGGER_SCHED_FIRE_DATE\">\n            <column name=\"FIRE_AT\" descending=\"true\"/>\n        </createIndex>\n    </changeSet>\n\n   <!-- deprecated in 1.31.0+\n   <changeSet id=\"65200\" author=\"ibodrov@gmail.com\">\n        <createView viewName=\"V_PROCESS_QUEUE\" replaceIfExists=\"true\">\n            select\n                q1.INSTANCE_ID,\n                q1.PROCESS_KIND,\n                q1.PARENT_INSTANCE_ID,\n                prj.PROJECT_ID,\n                prj.PROJECT_NAME,\n                prj.ORG_ID,\n                prj.ORG_NAME,\n                q1.CREATED_AT,\n                q1.INITIATOR,\n                q1.CURRENT_STATUS,\n                q1.LAST_AGENT_ID,\n                q1.LAST_UPDATED_AT,\n                q1.PROCESS_TAGS,\n                array(select q2.INSTANCE_ID from PROCESS_QUEUE as q2 where q2.PARENT_INSTANCE_ID = q1.INSTANCE_ID) as CHILDREN_IDS\n            from PROCESS_QUEUE as q1\n            left join\n                (select\n                    PROJECT_ID,\n                    PROJECT_NAME,\n                    ORG_ID,\n                    (select ORG_NAME from ORGANIZATIONS where ORGANIZATIONS.ORG_ID = PROJECTS.ORG_ID)\n                from PROJECTS) prj\n            on prj.PROJECT_ID = q1.PROJECT_ID\n        </createView>\n    </changeSet>\n    -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.66.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd\">\n\n    <changeSet id=\"66000\" author=\"ibodrov@gmail.com\">\n        <addUniqueConstraint tableName=\"SECRETS\" columnNames=\"ORG_ID, SECRET_NAME\"/>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.67.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!-- deprecated in 1.57.0+ -->\n    <!--\n    <changeSet id=\"67000\" author=\"matthew.kunkel@walmartlabs.com\">\n        <addColumn tableName=\"PROCESS_LOGS\">\n            <column name=\"LOG_SEQ\" type=\"bigserial\"\n                    remarks=\"Add sequences to enable forwarding\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n    -->\n\n    <changeSet id=\"67100\" author=\"matthew.kunkel@walmartlabs.com\">\n        <validCheckSum>7:b4f3446d4438acc4201a76cf9ab37959</validCheckSum>\n        <addColumn tableName=\"PROCESS_EVENTS\">\n            <column name=\"EVENT_SEQ\" type=\"bigserial\" autoIncrement=\"true\"\n                    remarks=\"Add sequences to enable forwarding\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"67200\" author=\"ibodrov@gmail.com\">\n        <validCheckSum>7:79a6d80f1acc2473d9de8080c1b9a20d</validCheckSum>\n        <validCheckSum>7:148f09276f67cb16502d0df0b9f4796c</validCheckSum>\n        <addColumn tableName=\"AUDIT_LOG\">\n            <column name=\"ENTRY_SEQ\" type=\"bigserial\" autoIncrement=\"true\"\n                    remarks=\"Add sequences to enable forwarding\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n\n        <!-- replaced in 1.58.0 -->\n        <!--\n        <createView viewName=\"V_AUDIT_LOG\" replaceIfExists=\"true\">\n            select\n                ENTRY_SEQ,\n                ENTRY_DATE,\n                USER_ID,\n                (select USERNAME from USERS u where u.USER_ID = a.USER_ID) as USERNAME,\n                ENTRY_OBJECT,\n                ENTRY_ACTION,\n                ENTRY_DETAILS\n            from AUDIT_LOG a\n        </createView>\n        -->\n\n        <dropColumn tableName=\"AUDIT_LOG\">\n            <column name=\"ENTRY_ID\"/>\n        </dropColumn>\n\n        <addPrimaryKey tableName=\"AUDIT_LOG\" columnNames=\"ENTRY_SEQ\"/>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.69.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!-- starting from 2.21.x, the default agent token is no longer associated with any user -->\n    <!--\n    <property name=\"concordAgentUserId\" value=\"d4f123c1-f8d4-40b2-8a12-b8947b9ce2d8\"/>\n    -->\n\n    <!-- moved to v1.86.0.xml (no more hard-coded default token) -->\n    <!-- starting from 2.21.x, the default agent token is no longer associated with any user -->\n    <!--\n    <changeSet id=\"69000\" author=\"ybrigo@gmail.com\">\n        <insert tableName=\"USERS\">\n            <column name=\"USER_ID\">${concordAgentUserId}</column>\n            <column name=\"USERNAME\">concordAgent</column>\n            <column name=\"USER_TYPE\">LOCAL</column>\n        </insert>\n\n        <insert tableName=\"API_KEYS\">\n            &lt;!&ndash; \"O+JMYwBsU797EKtlRQYu+Q\" &ndash;&gt;\n            <column name=\"API_KEY\">1sw9eLZ41EOK4w/iV3jFnn6cqeAMeFtxfazqVY04koY</column>\n            <column name=\"USER_ID\">${concordAgentUserId}</column>\n        </insert>\n    </changeSet>\n    -->\n\n    <!-- starting from 2.21.x, the default agent token is no longer associated with any user -->\n    <!-- Create agent user when not exist -->\n    <!--\n    <changeSet id=\"69001\" author=\"benjamin.broadaway@walmart.com.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <sqlCheck expectedResult=\"0\">\n                select count(USER_ID)\n                from USERS\n                where user_id = '${concordAgentUserId}';\n            </sqlCheck>\n        </preConditions>\n\n        <insert tableName=\"USERS\">\n            <column name=\"USER_ID\">${concordAgentUserId}</column>\n            <column name=\"USERNAME\">concordAgent</column>\n            <column name=\"USER_TYPE\">LOCAL</column>\n        </insert>\n    </changeSet>\n    -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.70.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!--\n    <property name=\"concordRunnerUserId\" value=\"2599c604-1384-4660-a767-8bc03baa7a31\"/>\n\n    <changeSet id=\"70000\" author=\"ybrigo@gmail.com\">\n        <insert tableName=\"USERS\">\n            <column name=\"USER_ID\">${concordRunnerUserId}</column>\n            <column name=\"USERNAME\">concordRunner</column>\n            <column name=\"USER_TYPE\">LOCAL</column>\n        </insert>\n\n        <insert tableName=\"API_KEYS\">\n            &lt;!&ndash; \"Gz0q/DeGlH8Zs7QJMj1v8g\" &ndash;&gt;\n            <column name=\"API_KEY\">DrRt3j6G7b6GHY/Prddu4voyKyZa17iFkEj99ac0q/A</column>\n            <column name=\"USER_ID\">${concordRunnerUserId}</column>\n        </insert>\n    </changeSet>\n    -->\n\n    <changeSet id=\"70100\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"REQUIREMENTS\" type=\"jsonb\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.71.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"71500\" author=\"ibodrov@gmail.com\">\n        <dropPrimaryKey tableName=\"USER_TEAMS\"/>\n        <addPrimaryKey tableName=\"USER_TEAMS\" columnNames=\"USER_ID, TEAM_ID\"/>\n    </changeSet>\n\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.74.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <property name=\"concordSystemReaderRoleId\" value=\"21d646b2-6a9c-11e8-acce-d37cf888abd9\"/>\n\n    <changeSet id=\"74000\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"ROLES\">\n            <column name=\"ROLE_ID\" type=\"uuid\" defaultValueComputed=\"uuid_generate_v1()\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"ROLE_NAME\" type=\"varchar(256)\">\n                <constraints nullable=\"false\" unique=\"true\"/>\n            </column>\n            <column name=\"GLOBAL_READER\" type=\"boolean\" defaultValue=\"false\"/>\n            <column name=\"GLOBAL_WRITER\" type=\"boolean\" defaultValue=\"false\"/>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"74010\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"USER_ROLES\">\n            <column name=\"USER_ID\" type=\"uuid\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"ROLE_ID\" type=\"uuid\" defaultValueComputed=\"uuid_generate_v1()\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <addForeignKeyConstraint baseTableName=\"USER_ROLES\"\n                                 baseColumnNames=\"USER_ID\"\n                                 constraintName=\"FK_U_R_USER\"\n                                 referencedTableName=\"USERS\"\n                                 referencedColumnNames=\"USER_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <addForeignKeyConstraint baseTableName=\"USER_ROLES\"\n                                 baseColumnNames=\"ROLE_ID\"\n                                 constraintName=\"FK_U_R_ROLE\"\n                                 referencedTableName=\"ROLES\"\n                                 referencedColumnNames=\"ROLE_ID\"/>\n    </changeSet>\n\n    <changeSet id=\"74020\" author=\"ibodrov@gmail.com\">\n        <insert tableName=\"ROLES\">\n            <column name=\"ROLE_ID\">${concordSystemReaderRoleId}</column>\n            <column name=\"ROLE_NAME\">concordSystemReader</column>\n            <column name=\"GLOBAL_READER\">true</column>\n        </insert>\n\n        <insert tableName=\"USER_ROLES\">\n            <column name=\"ROLE_ID\">${concordSystemReaderRoleId}</column>\n            <column name=\"USER_ID\">${concordGithubUserId}</column>\n        </insert>\n\n        <insert tableName=\"USER_ROLES\">\n            <column name=\"ROLE_ID\">${concordSystemReaderRoleId}</column>\n            <column name=\"USER_ID\">${concordCronUserId}</column>\n        </insert>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.77.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"77000\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"SECRETS\">\n            <column name=\"PROJECT_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n\n        <addForeignKeyConstraint baseTableName=\"SECRETS\"\n                                 baseColumnNames=\"PROJECT_ID\"\n                                 constraintName=\"FK_PRJ_ID_SECRETS\"\n                                 referencedTableName=\"PROJECTS\"\n                                 referencedColumnNames=\"PROJECT_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.79.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!-- starting from 2.21.x, the default agent token is no longer associated with any user -->\n    <!--\n    <changeSet id=\"79000\" author=\"ibodrov@gmail.com\">\n        <insert tableName=\"USER_ROLES\">\n            <column name=\"ROLE_ID\">${concordSystemReaderRoleId}</column>\n            <column name=\"USER_ID\">${concordAgentUserId}</column>\n        </insert>\n    </changeSet>\n    -->\n\n    <changeSet id=\"79100\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"API_KEYS\">\n            <column name=\"KEY_NAME\" type=\"varchar(128)\" defaultValue=\"n/a\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"EXPIRED_AT\" type=\"timestamp\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"79110\" author=\"ybrigo@gmail.com\">\n        <sql>\n            update api_keys a\n            set key_name = b.key_name\n            from (select key_id, 'key-' || row_number() OVER (PARTITION BY user_id ORDER BY user_id) AS key_name\n            from api_keys) AS b\n            where a.key_id = b.key_id\n        </sql>\n        <createIndex tableName=\"API_KEYS\" indexName=\"IDX_API_KEYS_NAME_USER\" unique=\"true\">\n            <column name=\"KEY_NAME\"/>\n            <column name=\"USER_ID\"/>\n        </createIndex>\n    </changeSet>\n\n    <changeSet id=\"79120\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"API_KEYS\">\n            <column name=\"LAST_NOTIFIED_AT\" type=\"timestamp\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"79130\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"USERS\">\n            <column name=\"USER_EMAIL\" type=\"varchar(512)\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <!-- removed in 0.81.0+\n    <changeSet id=\"79140\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"API_KEYS_NOTIFIER_LOCK\">\n            <column name=\"ID\" type=\"int\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"LOCKED\" type=\"boolean\" defaultValue=\"false\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"LOCKED_BY\" type=\"varchar(512)\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"LOCKED_AT\" type=\"timestamp\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </createTable>\n\n        <insert tableName=\"API_KEYS_NOTIFIER_LOCK\">\n            <column name=\"ID\" value=\"1\"/>\n            <column name=\"LOCKED\" value=\"false\"/>\n        </insert>\n    </changeSet>\n    -->\n\n    <changeSet id=\"79200\" author=\"ibodrov@gmail.com\">\n        <validCheckSum>ANY</validCheckSum>\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"REPO_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"REPO_URL\" type=\"varchar(2048)\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"REPO_PATH\" type=\"varchar(2048)\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"COMMIT_ID\" type=\"varchar(64)\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <!-- removed in 1.98.0+\n            <column name=\"COMMIT_MSG\" type=\"varchar(128)\">\n                <constraints nullable=\"true\"/>\n            </column>\n            -->\n        </addColumn>\n\n        <addForeignKeyConstraint baseTableName=\"PROCESS_QUEUE\"\n                                 baseColumnNames=\"REPO_ID\"\n                                 constraintName=\"FK_PQ_REPO_ID\"\n                                 referencedTableName=\"REPOSITORIES\"\n                                 referencedColumnNames=\"REPO_ID\"\n                                 onDelete=\"SET NULL\"/>\n    </changeSet>\n\n    <!-- deprecated in 1.31.0+\n    <changeSet id=\"79210\" author=\"ibodrov@gmail.com\">\n        <createView viewName=\"V_PROCESS_QUEUE\" replaceIfExists=\"true\">\n            select\n                q1.INSTANCE_ID,\n                q1.PROCESS_KIND,\n                q1.PARENT_INSTANCE_ID,\n                prj.ORG_ID,\n                prj.ORG_NAME,\n                prj.PROJECT_ID,\n                prj.PROJECT_NAME,\n                q1.REPO_ID,\n                repos.REPO_NAME,\n                q1.REPO_URL,\n                q1.REPO_PATH,\n                q1.COMMIT_ID,\n                q1.COMMIT_MSG,\n                q1.CREATED_AT,\n                q1.INITIATOR,\n                q1.CURRENT_STATUS,\n                q1.LAST_AGENT_ID,\n                q1.LAST_UPDATED_AT,\n                q1.PROCESS_TAGS,\n                array(select q2.INSTANCE_ID from PROCESS_QUEUE as q2 where q2.PARENT_INSTANCE_ID = q1.INSTANCE_ID) as CHILDREN_IDS\n\n            from PROCESS_QUEUE as q1\n\n            left join\n            (select\n                PROJECT_ID,\n                PROJECT_NAME,\n                ORG_ID,\n                (select ORG_NAME from ORGANIZATIONS where ORGANIZATIONS.ORG_ID = PROJECTS.ORG_ID)\n                from PROJECTS) prj\n            on prj.PROJECT_ID = q1.PROJECT_ID\n\n            left join REPOSITORIES repos\n            on repos.REPO_ID = q1.REPO_ID\n        </createView>\n    </changeSet>\n    -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.80.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <property name=\"concordSystemWriterRoleId\" value=\"c162d868-89ea-11e8-80be-97fd8a9f7419\"/>\n\n    <changeSet id=\"80000\" author=\"ibodrov@gmail.com\">\n        <validCheckSum>ANY</validCheckSum>\n        <insert tableName=\"ROLES\">\n            <column name=\"ROLE_ID\">${concordSystemWriterRoleId}</column>\n            <column name=\"ROLE_NAME\">concordSystemWriter</column>\n            <column name=\"GLOBAL_WRITER\">true</column>\n        </insert>\n\n        <!-- starting from 2.21.x, the default agent token is no longer associated with any user -->\n        <!--\n        <insert tableName=\"USER_ROLES\">\n            <column name=\"ROLE_ID\">${concordSystemWriterRoleId}</column>\n            <column name=\"USER_ID\">${concordAgentUserId}</column>\n        </insert>\n        -->\n    </changeSet>\n\n    <!-- removed in 1.9.0+\n    <changeSet id=\"80100\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"PROCESS_STATE_ARCHIVE\">\n            <column name=\"INSTANCE_ID\" type=\"uuid\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"STATUS\" type=\"varchar(64)\" defaultValue=\"IN_PROGRESS\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"LAST_UPDATED_AT\" type=\"timestamp\" defaultValueComputed=\"current_timestamp\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <addForeignKeyConstraint baseTableName=\"PROCESS_STATE_ARCHIVE\"\n                                 baseColumnNames=\"INSTANCE_ID\"\n                                 constraintName=\"FK_PSA_PQ_ID\"\n                                 referencedTableName=\"PROCESS_QUEUE\"\n                                 referencedColumnNames=\"INSTANCE_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <createIndex tableName=\"PROCESS_STATE_ARCHIVE\" indexName=\"IDX_PSA_LAST_UPD\">\n            <column name=\"LAST_UPDATED_AT\"/>\n        </createIndex>\n    </changeSet>\n    -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.81.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"81000\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"PROCESS_STATE\">\n            <column name=\"IS_ENCRYPTED\" type=\"boolean\" defaultValue=\"false\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"81100\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"TASK_LOCKS\">\n            <column name=\"LOCK_KEY\" type=\"varchar(64)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"LOCKED\" type=\"boolean\" defaultValue=\"false\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"LOCKED_AT\" type=\"timestamp\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"LOCK_COUNTER\" type=\"int\" defaultValueNumeric=\"0\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <insert tableName=\"TASK_LOCKS\">\n            <column name=\"LOCK_KEY\" value=\"apiKeyNotifier\"/>\n        </insert>\n\n        <insert tableName=\"TASK_LOCKS\">\n            <column name=\"LOCK_KEY\" value=\"githubWebhookService\"/>\n        </insert>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.83.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"83000\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"TRIGGERS\">\n            <column name=\"ACTIVE_PROFILES\" type=\"varchar[]\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.84.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"84000\" author=\"ybrigo@gmail.com\">\n        <validCheckSum>7:3eef780cd96bb43b6005272baa83c9b8</validCheckSum>\n        <validCheckSum>7:a8f71a204534c7b933683b7c35e434b9</validCheckSum>\n        <createTable tableName=\"PROCESS_CHECKPOINTS\">\n            <column name=\"CHECKPOINT_ID\" type=\"uuid\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"INSTANCE_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"CHECKPOINT_DATA\" type=\"longblob\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <addForeignKeyConstraint baseTableName=\"PROCESS_CHECKPOINTS\"\n                                 baseColumnNames=\"INSTANCE_ID\"\n                                 constraintName=\"FK_PCHECKP_PQ_ID\"\n                                 referencedTableName=\"PROCESS_QUEUE\"\n                                 referencedColumnNames=\"INSTANCE_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n    <!-- removed in 1.9.0+\n    <changeSet id=\"84100\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"PROCESS_CHECKPOINT_ARCHIVE\">\n            <column name=\"CHECKPOINT_ID\" type=\"uuid\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"STATUS\" type=\"varchar(64)\" defaultValue=\"IN_PROGRESS\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"LAST_UPDATED_AT\" type=\"timestamp\" defaultValueComputed=\"current_timestamp\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <addForeignKeyConstraint baseTableName=\"PROCESS_CHECKPOINT_ARCHIVE\"\n                                 baseColumnNames=\"CHECKPOINT_ID\"\n                                 constraintName=\"FK_PCA_PC_ID\"\n                                 referencedTableName=\"PROCESS_CHECKPOINTS\"\n                                 referencedColumnNames=\"CHECKPOINT_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <createIndex tableName=\"PROCESS_CHECKPOINT_ARCHIVE\" indexName=\"IDX_PCA_LAST_UPD\">\n            <column name=\"LAST_UPDATED_AT\"/>\n        </createIndex>\n    </changeSet>\n    -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.85.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"85000\" author=\"ibodrov@gmail.com\">\n        <createIndex tableName=\"PROCESS_QUEUE\" indexName=\"IDX_PROC_Q_CURR_STAT\">\n            <column name=\"CURRENT_STATUS\"/>\n        </createIndex>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.86.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"86000\" author=\"ibodrov@gmail.com\">\n        <validCheckSum>7:c24cac0e183b9520896b9e93207e3c49</validCheckSum>\n        <validCheckSum>7:0bd22f258406490fd222f5b634c16ce5</validCheckSum>\n        <addColumn tableName=\"PROJECTS\">\n            <column name=\"SECRET_KEY\" type=\"longblob\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"86200\" author=\"muhammad.wasi@walmart.com\">\n        <validCheckSum>7:d93133941dc7620cf83d20401d17cd57</validCheckSum>\n\n        <preConditions onFail=\"MARK_RAN\">\n            <columnExists tableName=\"PROCESS_QUEUE\" columnName=\"INITIATOR\"/>\n        </preConditions>\n\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"INITIATOR_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n\n        <addForeignKeyConstraint baseTableName=\"PROCESS_QUEUE\"\n                                 baseColumnNames=\"INITIATOR_ID\"\n                                 constraintName=\"FK_PQ_INITIATOR_ID\"\n                                 referencedTableName=\"USERS\"\n                                 referencedColumnNames=\"USER_ID\"\n                                 onDelete=\"SET NULL\"/>\n\n        <createProcedure>\n            create or replace function UPDATE_PROCESS_QUEUE_INITIATOR()\n            returns trigger as\n            $$\n            BEGIN\n                if NEW.INITIATOR is null then\n                    NEW.INITIATOR := (select U.USERNAME from USERS U where U.USER_ID = new.initiator_id);\n                end if;\n                return new;\n            end;\n            $$ language plpgsql;\n        </createProcedure>\n\n        <createProcedure>\n            create or replace function UPDATE_PROCESS_QUEUE_INITIATOR_ID()\n            returns trigger as\n            $$\n            begin\n                if NEW.INITIATOR_ID is null then\n                    NEW.INITIATOR_ID := (select U.USER_ID from USERS U where U.USERNAME = new.initiator);\n                end if;\n                return NEW;\n            end;\n            $$ language plpgsql;\n        </createProcedure>\n\n        <!-- temporary triggers to mitigate data inconsistency during blue-green deployment -->\n        <sql>\n            drop trigger if exists UPDATE_PROCESS_QUEUE_INITIATOR on PROCESS_QUEUE;\n            drop trigger if exists UPDATE_PROCESS_QUEUE_INITIATOR_ID on PROCESS_QUEUE;\n            create trigger UPDATE_PROCESS_QUEUE_INITIATOR_ID before insert on PROCESS_QUEUE for each row execute procedure UPDATE_PROCESS_QUEUE_INITIATOR_ID();\n        </sql>\n\n        <!-- deprecated in 1.31.0+\n        <createView viewName=\"V_PROCESS_QUEUE\" replaceIfExists=\"true\">\n            select\n                q1.INSTANCE_ID,\n                q1.PROCESS_KIND,\n                q1.PARENT_INSTANCE_ID,\n                prj.ORG_ID,\n                prj.ORG_NAME,\n                prj.PROJECT_ID,\n                prj.PROJECT_NAME,\n                q1.REPO_ID,\n                repos.REPO_NAME,\n                q1.REPO_URL,\n                q1.REPO_PATH,\n                q1.COMMIT_ID,\n                q1.COMMIT_MSG,\n                q1.CREATED_AT,\n                users.username as INITIATOR,\n                q1.INITIATOR_ID,\n                q1.CURRENT_STATUS,\n                q1.LAST_AGENT_ID,\n                q1.LAST_UPDATED_AT,\n                q1.PROCESS_TAGS,\n                array(select q2.INSTANCE_ID from PROCESS_QUEUE as q2 where q2.PARENT_INSTANCE_ID = q1.INSTANCE_ID) as CHILDREN_IDS\n\n            from PROCESS_QUEUE as q1\n\n            left join\n                (select\n                PROJECT_ID,\n                PROJECT_NAME,\n                ORG_ID,\n                (select ORG_NAME from ORGANIZATIONS where ORGANIZATIONS.ORG_ID = PROJECTS.ORG_ID)\n                from PROJECTS) prj\n                on prj.PROJECT_ID = q1.PROJECT_ID\n\n            left join\n                REPOSITORIES repos on repos.REPO_ID = q1.REPO_ID\n\n            left join\n                USERS users on users.user_id = q1.INITIATOR_ID\n        </createView>\n        -->\n\n        <sql>\n            update process_queue p\n            set initiator_id = u.user_id\n            from users u\n            where u.username = p.initiator\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"86300\" author=\"muhammad.wasi@walmart.com\">\n        <addColumn tableName=\"PROCESS_CHECKPOINTS\">\n            <column name=\"CHECKPOINT_NAME\" type=\"varchar(128)\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"CHECKPOINT_DATE\" type=\"timestamp\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.87.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!-- removed in 1.99.0+\n    <changeSet id=\"87000\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"META\" type=\"jsonb\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n    -->\n\n    <changeSet id=\"87100\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"PROJECTS\">\n            <column name=\"META\" type=\"jsonb\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <!-- deprecated in 1.31.0+\n    <changeSet id=\"87200\" author=\"ybrigo@gmail.com\">\n        <createView viewName=\"V_PROCESS_QUEUE\" replaceIfExists=\"true\">\n            select\n                q1.INSTANCE_ID,\n                q1.PROCESS_KIND,\n                q1.PARENT_INSTANCE_ID,\n                prj.ORG_ID,\n                prj.ORG_NAME,\n                prj.PROJECT_ID,\n                prj.PROJECT_NAME,\n                q1.REPO_ID,\n                repos.REPO_NAME,\n                q1.REPO_URL,\n                q1.REPO_PATH,\n                q1.COMMIT_ID,\n                q1.COMMIT_MSG,\n                q1.CREATED_AT,\n                users.username as INITIATOR,\n                q1.INITIATOR_ID,\n                q1.CURRENT_STATUS,\n                q1.LAST_AGENT_ID,\n                q1.LAST_UPDATED_AT,\n                q1.PROCESS_TAGS,\n                array(select q2.INSTANCE_ID from PROCESS_QUEUE as q2 where q2.PARENT_INSTANCE_ID = q1.INSTANCE_ID) as CHILDREN_IDS,\n                cast(q1.META as varchar) as META\n\n            from PROCESS_QUEUE as q1\n\n            left join\n                (select\n                PROJECT_ID,\n                PROJECT_NAME,\n                ORG_ID,\n                (select ORG_NAME from ORGANIZATIONS where ORGANIZATIONS.ORG_ID = PROJECTS.ORG_ID)\n                from PROJECTS) prj\n            on prj.PROJECT_ID = q1.PROJECT_ID\n\n            left join\n                REPOSITORIES repos on repos.REPO_ID = q1.REPO_ID\n\n            left join\n                USERS users on users.user_id = q1.INITIATOR_ID\n        </createView>\n    </changeSet>\n    -->\n\n    <changeSet id=\"87300\" author=\"muhammad.wasi@walmart.com\">\n        <sql>\n            drop trigger if exists UPDATE_PROCESS_QUEUE_INITIATOR_ID on PROCESS_QUEUE;\n            drop function if exists UPDATE_PROCESS_QUEUE_INITIATOR;\n            drop function if exists UPDATE_PROCESS_QUEUE_INITIATOR_ID;\n        </sql>\n        <dropColumn tableName=\"PROCESS_QUEUE\" columnName=\"INITIATOR\"/>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.88.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"88000\" author=\"ibodrov@gmail.com\">\n        <sql>\n            update TRIGGERS set CONDITIONS = '{\"org\": \".*\", \"project\": \".*\", \"repository\": \".*\"}'\n            where\n              PROJECT_ID = '${concordTriggersProjectId}'\n              and EVENT_SOURCE = 'github'\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"88010\" author=\"ibodrov@gmail.com\">\n        <sql>\n            insert into USER_ROLES (USER_ID, ROLE_ID) values ('${concordGithubUserId}', '${concordSystemWriterRoleId}')\n            on conflict do nothing\n        </sql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.89.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"89000\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"TIMEOUT\" type=\"bigint\" remarks=\"Timeout (in seconds)\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <!-- removed in 0.97.0+\n    <changeSet id=\"89100\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"PROCESS_STATUS_HISTORY\">\n            <column name=\"INSTANCE_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"STATUS\" type=\"varchar(32)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"CHANGE_DATE\" type=\"timestamp\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <addForeignKeyConstraint baseTableName=\"PROCESS_STATUS_HISTORY\"\n                                 baseColumnNames=\"INSTANCE_ID\"\n                                 constraintName=\"FK_PSH_INSTANCE_ID\"\n                                 referencedTableName=\"PROCESS_QUEUE\"\n                                 referencedColumnNames=\"INSTANCE_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <createIndex tableName=\"PROCESS_STATUS_HISTORY\" indexName=\"IDX_PROC_STAT_HIST_INST_ID\">\n            <column name=\"INSTANCE_ID\"/>\n            <column name=\"STATUS\"/>\n        </createIndex>\n    </changeSet>\n    -->\n\n    <changeSet id=\"89200\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"ORGANIZATIONS\">\n            <column name=\"org_cfg\" type=\"jsonb\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"89500\" author=\"muhammad.amir@walmart.com\">\n        <addColumn tableName=\"TRIGGERS\">\n            <column name=\"TRIGGER_CFG\" type=\"jsonb\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n\n        <dropNotNullConstraint tableName=\"TRIGGERS\" columnName=\"ENTRY_POINT\" />\n\n        <sql>\n            update TRIGGERS set TRIGGER_CFG = cast('{\"entryPoint\": \"' || entry_point || '\"}' as jsonb)\n        </sql>\n\n        <dropColumn tableName=\"TRIGGERS\" columnName=\"ENTRY_POINT\"/>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.90.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"90000\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"HANDLERS\" type=\"text[]\" remarks=\"List of process handlers\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <!-- deprecated in 1.31.0+\n    <changeSet id=\"90010\" author=\"ibodrov@gmail.com\">\n        <createView viewName=\"V_PROCESS_QUEUE\" replaceIfExists=\"true\">\n            select\n                q1.INSTANCE_ID,\n                q1.PROCESS_KIND,\n                q1.PARENT_INSTANCE_ID,\n                prj.ORG_ID,\n                prj.ORG_NAME,\n                prj.PROJECT_ID,\n                prj.PROJECT_NAME,\n                q1.REPO_ID,\n                repos.REPO_NAME,\n                q1.REPO_URL,\n                q1.REPO_PATH,\n                q1.COMMIT_ID,\n                q1.COMMIT_MSG,\n                q1.CREATED_AT,\n                users.username as INITIATOR,\n                q1.INITIATOR_ID,\n                q1.CURRENT_STATUS,\n                q1.LAST_AGENT_ID,\n                q1.LAST_UPDATED_AT,\n                q1.PROCESS_TAGS,\n                array(select q2.INSTANCE_ID from PROCESS_QUEUE as q2 where q2.PARENT_INSTANCE_ID = q1.INSTANCE_ID) as CHILDREN_IDS,\n                cast(q1.META as varchar) as META,\n                q1.HANDLERS\n\n            from PROCESS_QUEUE as q1\n\n            left join\n            (select\n                PROJECT_ID,\n                PROJECT_NAME,\n                ORG_ID,\n                (select ORG_NAME from ORGANIZATIONS where ORGANIZATIONS.ORG_ID = PROJECTS.ORG_ID)\n                from PROJECTS) prj\n            on prj.PROJECT_ID = q1.PROJECT_ID\n\n            left join\n            REPOSITORIES repos on repos.REPO_ID = q1.REPO_ID\n\n            left join\n            USERS users on users.user_id = q1.INITIATOR_ID\n        </createView>\n    </changeSet>\n    -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.92.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"92000\" author=\"muhammad.wasi@walmart.com\">\n        <addColumn tableName=\"POLICY_LINKS\">\n            <column name=\"USER_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n\n        <addForeignKeyConstraint baseTableName=\"POLICY_LINKS\"\n                                 baseColumnNames=\"USER_ID\"\n                                 constraintName=\"FK_POLICY_LINKS_USER_ID\"\n                                 referencedTableName=\"USERS\"\n                                 referencedColumnNames=\"USER_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n    <changeSet id=\"92010\" author=\"muhammad.wasi@walmart.com\">\n        <createIndex tableName=\"POLICY_LINKS\" indexName=\"IDX_POLICY_LINK_5\" unique=\"true\">\n            <column name=\"ORG_ID\"/>\n            <column name=\"PROJECT_ID\"/>\n            <column name=\"POLICY_ID\"/>\n            <column name=\"USER_ID\"/>\n        </createIndex>\n        <modifySql>\n            <append value=\"where ORG_ID is not null and PROJECT_ID is not null and USER_ID is not null\"/>\n        </modifySql>\n    </changeSet>\n\n    <changeSet id=\"92030\" author=\"muhammad.wasi@walmart.com\">\n        <createIndex tableName=\"POLICY_LINKS\" indexName=\"IDX_POLICY_LINK_6\" unique=\"true\">\n            <column name=\"USER_ID\"/>\n            <column name=\"POLICY_ID\"/>\n        </createIndex>\n        <modifySql>\n            <append value=\"WHERE ORG_ID is null and PROJECT_ID is null and USER_ID is not null\"/>\n        </modifySql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.93.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"93000\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"TASKS\">\n            <column name=\"TASK_ID\" type=\"varchar(64)\">\n                <constraints nullable=\"false\" primaryKey=\"true\"/>\n            </column>\n            <column name=\"TASK_INTERVAL\" type=\"bigint\" remarks=\"Start interval (in seconds)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"TASK_STATUS\" type=\"varchar(32)\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"STARTED_AT\" type=\"timestamp\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"FINISHED_AT\" type=\"timestamp\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"93100\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"TASKS\">\n            <column name=\"LAST_UPDATED_AT\" type=\"timestamp\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"93500\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"PROCESS_STATE\">\n            <column name=\"INSTANCE_CREATED_AT\" type=\"timestamp\" remarks=\"Same as PROCESS_QUEUE.CREATED_AT\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n\n        <sql>\n            lock table PROCESS_STATE;\n            update PROCESS_STATE s set INSTANCE_CREATED_AT = (select CREATED_AT from PROCESS_QUEUE q where q.INSTANCE_ID = s.INSTANCE_ID);\n        </sql>\n\n        <addNotNullConstraint tableName=\"PROCESS_STATE\" columnName=\"INSTANCE_CREATED_AT\"/>\n\n        <dropPrimaryKey tableName=\"PROCESS_STATE\"/>\n\n        <addPrimaryKey tableName=\"PROCESS_STATE\" columnNames=\"INSTANCE_ID, INSTANCE_CREATED_AT, ITEM_PATH\"/>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.95.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!-- deprecated in 1.57.0+ -->\n    <!--\n    <changeSet id=\"95000\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"PROCESS_LOGS\">\n            <column name=\"INSTANCE_CREATED_AT\" type=\"timestamp\" remarks=\"Same as PROCESS_QUEUE.CREATED_AT\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n\n        <sql>\n            lock table PROCESS_LOGS;\n            update PROCESS_LOGS s set INSTANCE_CREATED_AT = (select CREATED_AT from PROCESS_QUEUE q where q.INSTANCE_ID = s.INSTANCE_ID);\n        </sql>\n\n        <addNotNullConstraint tableName=\"PROCESS_LOGS\" columnName=\"INSTANCE_CREATED_AT\"/>\n\n        <dropIndex tableName=\"PROCESS_LOGS\" indexName=\"idx_p_logs_ids\"/>\n\n        <createIndex tableName=\"PROCESS_LOGS\" indexName=\"IDX_P_LOGS_IDS\">\n            <column name=\"INSTANCE_ID\"/>\n            <column name=\"INSTANCE_CREATED_AT\"/>\n        </createIndex>\n    </changeSet>\n\n    <changeSet id=\"95105\" author=\"ibodrov@gmail.com\">\n        <dropView viewName=\"V_PROCESS_LOGS_UPPER_BOUNDS\"/>\n        <createView viewName=\"V_PROCESS_LOGS_UPPER_BOUNDS\" replaceIfExists=\"true\">\n            select\n                INSTANCE_ID,\n                INSTANCE_CREATED_AT,\n                coalesce(max(upper(CHUNK_RANGE)), 0) as UPPER_BOUND\n            from PROCESS_LOGS\n            group by INSTANCE_ID, INSTANCE_CREATED_AT\n        </createView>\n    </changeSet>\n\n    <changeSet id=\"95115\" author=\"ibodrov@gmail.com\">\n        <dropView viewName=\"V_PROCESS_LOGS_SIZE\"/>\n        <createView viewName=\"V_PROCESS_LOGS_SIZE\" replaceIfExists=\"true\">\n            select\n                INSTANCE_ID,\n                INSTANCE_CREATED_AT,\n                max(upper(CHUNK_RANGE)) as SIZE\n            from PROCESS_LOGS\n            group by INSTANCE_ID, INSTANCE_CREATED_AT\n        </createView>\n    </changeSet>\n\n    <changeSet id=\"95200\" author=\"ibodrov@gmail.com\">\n        <createProcedure dbms=\"postgresql\">\n            create or replace function PROCESS_LOG_NEXT_RANGE(P_INSTANCE_ID uuid, P_CREATED_AT timestamp, P_DATA_LEN int)\n                returns int4range as $$\n            declare\n                R_START int;\n            begin\n                select coalesce(UPPER_BOUND, 0) into R_START\n                from V_PROCESS_LOGS_UPPER_BOUNDS where\n                    INSTANCE_ID = P_INSTANCE_ID and INSTANCE_CREATED_AT = P_CREATED_AT;\n\n                if R_START is null then\n                    R_START := 0;\n                end if;\n\n                return int4range(R_START, R_START + P_DATA_LEN);\n            end;\n            $$ language plpgsql;\n        </createProcedure>\n    </changeSet>\n\n    <changeSet id=\"95210\" author=\"ibodrov@gmail.com\">\n        <createProcedure dbms=\"postgresql\">\n            create or replace function PROCESS_LOG_LAST_N_BYTES(P_INSTANCE_ID uuid, P_CREATED_AT timestamp, P_DATA_LEN int)\n                returns int4range as $$\n            declare\n                R_START int;\n            begin\n                select coalesce(UPPER_BOUND, 0) into R_START\n                from V_PROCESS_LOGS_UPPER_BOUNDS where\n                    INSTANCE_ID = P_INSTANCE_ID and INSTANCE_CREATED_AT = P_CREATED_AT;\n\n                if R_START is null then\n                    R_START := 0;\n                end if;\n\n                return int4range(R_START - P_DATA_LEN, R_START);\n            end;\n            $$ language plpgsql;\n        </createProcedure>\n    </changeSet>\n    -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.97.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"97000\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"LAST_RUN_AT\" type=\"timestamp\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"97100\" author=\"ybrigo@gmail.com\">\n        <dropIndex tableName=\"PROCESS_EVENTS\" indexName=\"IDX_PROC_EV_I_ID_DT\"/>\n\n        <createIndex tableName=\"PROCESS_EVENTS\" indexName=\"IDX_PROC_EV_I_ID_DT_TYPE\">\n            <column name=\"INSTANCE_ID\"/>\n            <column name=\"EVENT_DATE\"/>\n            <column name=\"EVENT_TYPE\"/>\n        </createIndex>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v0.99.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"99000\" author=\"ibodrov@gmail.com\">\n        <sql>\n            lock table PROCESS_EVENTS;\n\n            alter table PROCESS_EVENTS add column INSTANCE_CREATED_AT timestamp;\n            update PROCESS_EVENTS as pe set INSTANCE_CREATED_AT = (select CREATED_AT from PROCESS_QUEUE as pq where pq.INSTANCE_ID = pe.INSTANCE_ID);\n            delete from PROCESS_EVENTS where INSTANCE_CREATED_AT is null;\n        </sql>\n\n        <addNotNullConstraint tableName=\"PROCESS_EVENTS\" columnName=\"INSTANCE_CREATED_AT\"/>\n\n        <dropIndex tableName=\"PROCESS_EVENTS\" indexName=\"IDX_PROC_EV_I_ID_DT_TYPE\"/>\n\n        <createIndex tableName=\"PROCESS_EVENTS\" indexName=\"IDX_PROC_EVENTS\">\n            <column name=\"INSTANCE_ID\"/>\n            <column name=\"INSTANCE_CREATED_AT\"/>\n            <column name=\"EVENT_DATE\"/>\n            <column name=\"EVENT_TYPE\"/>\n        </createIndex>\n    </changeSet>\n\n    <!-- moved into server plugins\n    <changeSet id=\"99100\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"ANSIBLE_HOSTS\">\n            <column name=\"INSTANCE_ID\" type=\"uuid\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"INSTANCE_CREATED_AT\" type=\"timestamp\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"HOST\" type=\"varchar(1024)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"HOST_GROUP\" type=\"varchar(1024)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"EVENT_SEQ\" type=\"bigint\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"STATUS\" type=\"varchar(32)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"DURATION\" type=\"bigint\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n    -->\n\n    <!-- removed in 1.0.0+\n    <changeSet id=\"99200\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"EVENT_PROCESSOR_MARKERS\">\n            <column name=\"PROCESSOR_NAME\" type=\"varchar(64)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"INSTANCE_CREATED_AT\" type=\"timestamp\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"EVENT_SEQ\" type=\"bigint\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </createTable>\n\n        <insert tableName=\"EVENT_PROCESSOR_MARKERS\">\n            <column name=\"PROCESSOR_NAME\" value=\"ansible-event-processor\"/>\n        </insert>\n    </changeSet>\n    -->\n\n    <changeSet id=\"99300\" author=\"ibodrov@gmail.com\">\n        <createIndex tableName=\"PROCESS_EVENTS\" indexName=\"IDX_PROC_EVENTS_FOLDING\">\n            <column name=\"EVENT_SEQ\"/>\n            <column name=\"EVENT_TYPE\"/>\n        </createIndex>\n    </changeSet>\n\n    <changeSet id=\"99400\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"POLICIES\">\n            <column name=\"PARENT_POLICY_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n\n        <addForeignKeyConstraint constraintName=\"FK_POLICIES_PARENT\"\n                                 baseTableName=\"POLICIES\" baseColumnNames=\"PARENT_POLICY_ID\"\n                                 referencedTableName=\"POLICIES\" referencedColumnNames=\"POLICY_ID\"\n                                 onDelete=\"SET NULL\"/>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.0.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!-- removed in 1.28.0+\n    <changeSet id=\"100100\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"EVENT_PROCESSOR_MARKER\">\n            <column name=\"PROCESSOR_NAME\" type=\"varchar(64)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"START_FROM\" type=\"timestamp\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"END_TO\" type=\"timestamp\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"EVENT_DATE\" type=\"timestamp\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"EVENT_SEQ\" type=\"bigint\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"STATUS\" type=\"varchar(64)\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n    -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.10.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1100000\" author=\"muhammad.amir@walmart.com\">\n        <addColumn tableName=\"REPOSITORIES\">\n            <column name=\"IS_DISABLED\" type=\"boolean\" defaultValue=\"false\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.102.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1102000\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"PROJECTS\">\n            <column name=\"CREATED_AT\" type=\"timestamptz\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.103.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1103000\" author=\"g0h04k0@walmart.com\">\n        <createTable tableName=\"PROJECT_SECRETS\">\n            <column name=\"SECRET_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"PROJECT_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n        <addForeignKeyConstraint baseTableName=\"PROJECT_SECRETS\"\n                                 baseColumnNames=\"SECRET_ID\"\n                                 constraintName=\"FK_PROJECT_SECRETS_SECRET\"\n                                 referencedTableName=\"SECRETS\"\n                                 referencedColumnNames=\"SECRET_ID\"\n                                 onDelete=\"CASCADE\"/>\n        <addForeignKeyConstraint baseTableName=\"PROJECT_SECRETS\"\n                                 baseColumnNames=\"PROJECT_ID\"\n                                 constraintName=\"FK_PROJECT_SECRETS_PROJECT\"\n                                 referencedTableName=\"PROJECTS\"\n                                 referencedColumnNames=\"PROJECT_ID\"\n                                 onDelete=\"CASCADE\"/>\n        <addUniqueConstraint tableName=\"PROJECT_SECRETS\" columnNames=\"SECRET_ID,PROJECT_ID\"/>\n        <createIndex tableName=\"PROJECT_SECRETS\" indexName=\"PROJECT_SECRETS_SECRET_IDX\">\n            <column name=\"SECRET_ID\"/>\n        </createIndex>\n        <createIndex tableName=\"PROJECT_SECRETS\" indexName=\"PROJECT_SECRETS_PROJECT_IDX\">\n            <column name=\"PROJECT_ID\"/>\n        </createIndex>\n        <sql>\n            insert into PROJECT_SECRETS (secret_id, project_id) select secret_id, project_id from secrets where project_id is not null\n        </sql>\n        <dropColumn  tableName=\"SECRETS\">\n            <column  name=\"PROJECT_ID\"/>\n        </dropColumn>\n    </changeSet>\n\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.104.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1104000\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"SECRETS\">\n            <column name=\"CREATED_AT\" type=\"timestamptz\" defaultValueComputed=\"now()\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.11.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!-- moved into server plugins\n    <changeSet id=\"1110000\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"ANSIBLE_HOSTS\">\n            <column name=\"RETRY_COUNT\" type=\"int\" defaultValue=\"0\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n    -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.12.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1120000\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"USERS\">\n            <column name=\"IS_DISABLED\" type=\"boolean\" defaultValue=\"false\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"1120010\" author=\"ybrigo@gmail.com\">\n        <validCheckSum>7:6a9c660fe9a69546e12e914fdc298bb0</validCheckSum>\n\n        <createTable tableName=\"USER_LDAP_GROUPS\">\n            <column name=\"USER_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"LDAP_GROUP\" type=\"varchar(1024)\">\n                <constraints nullable=\"false\"/>\n            </column>\n\n            <!-- deprecated in 1.21.0+ -->\n            <!--\n            <column name=\"UPDATED_AT\" type=\"timestamp\" defaultValueComputed=\"current_timestamp\">\n                <constraints nullable=\"false\"/>\n            </column>\n            -->\n        </createTable>\n\n        <addForeignKeyConstraint baseTableName=\"USER_LDAP_GROUPS\"\n                                 baseColumnNames=\"USER_ID\"\n                                 constraintName=\"FK_USER_LDAP_GROUPS_USER_ID\"\n                                 referencedTableName=\"USERS\"\n                                 referencedColumnNames=\"USER_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n    <changeSet id=\"1120020\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"TEAM_LDAP_GROUPS\">\n            <column name=\"TEAM_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"LDAP_GROUP\" type=\"varchar(1024)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"TEAM_ROLE\" type=\"varchar(128)\" defaultValue=\"MEMBER\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <addUniqueConstraint tableName=\"TEAM_LDAP_GROUPS\" columnNames=\"TEAM_ID, LDAP_GROUP\"/>\n    </changeSet>\n\n    <changeSet id=\"1120030\" author=\"ybrigo@gmail.com\">\n        <createView viewName=\"V_USER_TEAMS\" replaceIfExists=\"true\">\n            select USER_ID, TEAM_ID, TEAM_ROLE\n            from USER_TEAMS\n            union\n            select distinct ulg.USER_ID, tlg.TEAM_ID, tlg.TEAM_ROLE\n            from TEAM_LDAP_GROUPS tlg, USER_LDAP_GROUPS ulg\n            where\n              ulg.LDAP_GROUP = tlg.LDAP_GROUP\n              and not exists(select 1 from USER_TEAMS ut where ut.USER_ID = ulg.USER_ID and ut.TEAM_ID = tlg.TEAM_ID)\n        </createView>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.13.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1130000\" author=\"ibodrov@gmail.com\">\n        <!-- PROJECTS -->\n        <dropForeignKeyConstraint baseTableName=\"PROJECTS\" constraintName=\"FK_PRJ_ORG_ID\"/>\n        <addForeignKeyConstraint constraintName=\"FK_PRJ_ORG_ID\"\n                                 baseTableName=\"PROJECTS\"\n                                 baseColumnNames=\"ORG_ID\"\n                                 referencedTableName=\"ORGANIZATIONS\"\n                                 referencedColumnNames=\"ORG_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <!-- INVENTORIES -->\n        <dropForeignKeyConstraint baseTableName=\"INVENTORIES\" constraintName=\"FK_INV_ORG_ID\"/>\n        <addForeignKeyConstraint constraintName=\"FK_INV_ORG_ID\"\n                                 baseTableName=\"INVENTORIES\"\n                                 baseColumnNames=\"ORG_ID\"\n                                 referencedTableName=\"ORGANIZATIONS\"\n                                 referencedColumnNames=\"ORG_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <!-- SECRETS -->\n        <dropForeignKeyConstraint baseTableName=\"SECRETS\" constraintName=\"FK_SECRET_ORG_ID\"/>\n        <addForeignKeyConstraint constraintName=\"FK_SECRET_ORG_ID\"\n                                 baseTableName=\"SECRETS\"\n                                 baseColumnNames=\"ORG_ID\"\n                                 referencedTableName=\"ORGANIZATIONS\"\n                                 referencedColumnNames=\"ORG_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n    <changeSet id=\"1130100\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"ORGANIZATIONS\">\n            <column name=\"OWNER_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n\n        <addForeignKeyConstraint constraintName=\"FK_ORG_OWNER_ID\"\n                                 baseTableName=\"ORGANIZATIONS\"\n                                 baseColumnNames=\"OWNER_ID\"\n                                 referencedTableName=\"USERS\"\n                                 referencedColumnNames=\"USER_ID\"\n                                 onDelete=\"SET NULL\"/>\n    </changeSet>\n\n    <changeSet id=\"1130200\" author=\"muhammad.amir@walmart.com\">\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"IS_DISABLED\" type=\"boolean\" defaultValue=\"false\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.18.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1180000\" author=\"ibodrov@gmail.com\">\n        <createTable tableName=\"PERMISSIONS\" remarks=\"Dictionary of permissions\">\n            <column name=\"PERMISSION_ID\" type=\"uuid\" defaultValueComputed=\"uuid_generate_v1()\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"PERMISSION_NAME\" type=\"varchar(256)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"DESCRIPTION\" type=\"varchar(1024)\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <insert tableName=\"PERMISSIONS\">\n            <column name=\"PERMISSION_ID\" value=\"1880f2aa-6abf-11e9-bfc0-93407d3c5df1\"/>\n            <column name=\"PERMISSION_NAME\" value=\"getProcessQueueAllOrgs\"/>\n            <column name=\"DESCRIPTION\" value=\"Read-only access to the process queue for all organizations\"/>\n        </insert>\n\n        <createTable tableName=\"ROLE_PERMISSIONS\" remarks=\"Permissions of roles\">\n            <column name=\"ROLE_ID\" type=\"uuid\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"PERMISSION_ID\" type=\"uuid\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <addForeignKeyConstraint baseTableName=\"ROLE_PERMISSIONS\"\n                                 baseColumnNames=\"PERMISSION_ID\"\n                                 constraintName=\"FK_ROLE_PERMISSIONS\"\n                                 referencedTableName=\"PERMISSIONS\"\n                                 referencedColumnNames=\"PERMISSION_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n    <changeSet id=\"1180100\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"PROCESS_CHECKPOINTS\">\n            <column name=\"INSTANCE_CREATED_AT\" type=\"timestamp\" remarks=\"Same as PROCESS_QUEUE.CREATED_AT\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n\n        <sql>\n            lock table PROCESS_CHECKPOINTS;\n            update PROCESS_CHECKPOINTS s set INSTANCE_CREATED_AT = (select CREATED_AT from PROCESS_QUEUE q where q.INSTANCE_ID = s.INSTANCE_ID);\n        </sql>\n\n        <addNotNullConstraint tableName=\"PROCESS_CHECKPOINTS\" columnName=\"INSTANCE_CREATED_AT\"/>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.21.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1210000\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"USERS\">\n            <column name=\"LAST_GROUP_SYNC_DT\" type=\"timestamp\"\n                    remarks=\"Timestamp of the last user group synchronization attempt\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.22.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1220000\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"IMPORTS\" type=\"jsonb\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.24.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1240000\" author=\"ibodrov@gmail.com\">\n        <dropForeignKeyConstraint baseTableName=\"PROCESS_CHECKPOINTS\"\n                                  constraintName=\"FK_PCHECKP_PQ_ID\"/>\n    </changeSet>\n\n    <changeSet id=\"1240100\" author=\"muhammad.wasi@walmart.com\">\n        <addForeignKeyConstraint baseTableName=\"ROLE_PERMISSIONS\"\n                                 baseColumnNames=\"ROLE_ID\"\n                                 constraintName=\"FK_ROLE_PERMISSIONS_ROLES\"\n                                 referencedTableName=\"ROLES\"\n                                 referencedColumnNames=\"ROLE_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <dropForeignKeyConstraint baseTableName=\"USER_ROLES\" constraintName=\"FK_U_R_ROLE\"/>\n        <addForeignKeyConstraint baseTableName=\"USER_ROLES\"\n                                 baseColumnNames=\"ROLE_ID\"\n                                 constraintName=\"FK_U_R_ROLE\"\n                                 referencedTableName=\"ROLES\"\n                                 referencedColumnNames=\"ROLE_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n    <changeSet id=\"1240100\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"USERS\">\n            <column name=\"DOMAIN\" type=\"varchar(512)\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"1240110\" author=\"ybrigo@gmail.com\">\n        <dropUniqueConstraint tableName=\"USERS\" constraintName=\"USERS_USERNAME_KEY\"/>\n        <addUniqueConstraint tableName=\"USERS\" columnNames=\"USERNAME, DOMAIN, USER_TYPE\"/>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.27.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!-- updated in 1.40.0+\n    <changeSet id=\"1270000\" author=\"ibodrov@gmail.com\" runInTransaction=\"false\">\n        <sql>\n            create index concurrently IDX_PROC_WAIT_COND_NN\n            on PROCESS_QUEUE (WAIT_CONDITIONS)\n            where WAIT_CONDITIONS is not null\n        </sql>\n    </changeSet>\n    -->\n\n    <changeSet id=\"1270100\" author=\"ibodrov@gmail.com\">\n        <dropIndex tableName=\"PROCESS_QUEUE\" indexName=\"IDX_PROC_Q_CURR_STAT\"/>\n    </changeSet>\n\n    <!-- replaced with `exclusive` in 1.33.0+\n    <changeSet id=\"1270200\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"EXCLUSIVE_GROUP\" type=\"varchar(512)\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n    -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.28.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!-- removed in 1.99.0+\n    <changeSet id=\"1280000\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"TRIGGERED_BY\" type=\"jsonb\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n    -->\n\n    <!-- removed in 1.34.3+\n    <changeSet id=\"1280100\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"PROCESS_EVENT_STATS\">\n            <column name=\"INSTANCE_CREATED_DATE\" type=\"timestamp\">\n                <constraints nullable=\"false\" primaryKey=\"true\"/>\n            </column>\n            <column name=\"MAX_EVENT_SEQ\" type=\"bigint\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n    -->\n\n    <!-- removed in 1.34.3+\n    <changeSet id=\"1280200\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"EVENT_PROCESSOR_MARKERS\">\n            <column name=\"PROCESSOR_NAME\" type=\"varchar(64)\">\n                <constraints nullable=\"false\" primaryKey=\"true\"/>\n            </column>\n            <column name=\"INSTANCE_CREATED_DATE\" type=\"timestamp\">\n                <constraints nullable=\"false\" primaryKey=\"true\"/>\n            </column>\n            <column name=\"EVENT_SEQ\" type=\"bigint\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n    -->\n\n    <changeSet id=\"1280200\" author=\"ibodrov@gmail.com\">\n        <sql>\n            create type raw_payload_mode as enum ('DISABLED', 'OWNERS', 'TEAM_MEMBERS', 'ORG_MEMBERS', 'EVERYONE')\n        </sql>\n\n        <addColumn tableName=\"PROJECTS\">\n            <column name=\"RAW_PAYLOAD_MODE\" type=\"raw_payload_mode\" defaultValue=\"DISABLED\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"1280210\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <columnExists tableName=\"PROJECTS\" columnName=\"ACCEPTS_RAW_PAYLOAD\"/>\n        </preConditions>\n        <sql>\n            update PROJECTS set RAW_PAYLOAD_MODE = case\n                    when ACCEPTS_RAW_PAYLOAD is true then 'ORG_MEMBERS'::raw_payload_mode\n                    else 'DISABLED'::raw_payload_mode\n                end\n        </sql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.32.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <!-- removed in 1.34.1+\n    <changeSet id=\"1320000\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"TRIGGERS\">\n            <column name=\"TRIGGER_VERSION\" type=\"int\" defaultValue=\"1\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n    -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.33.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1330000\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"EXCLUSIVE\" type=\"jsonb\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.34.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1340000\" author=\"ybrigo@gmail.com\">\n        <validCheckSum>7:c4f908aabae131fcc9bcb034b1289a06</validCheckSum>\n\n        <preConditions onFail=\"MARK_RAN\">\n            <columnExists tableName=\"TRIGGERS\" columnName=\"TRIGGER_VERSION\"/>\n        </preConditions>\n\n        <sql>\n            update TRIGGERS set\n                CONDITIONS = '{\"githubOrg\": \".*\", \"githubRepo\": \".*\", \"branch\": \".*\", \"version\": 2, \"repositoryInfo\": [{\"repository\": \".*\"}]}',\n                TRIGGER_VERSION = 2\n            where\n            PROJECT_ID = '${concordTriggersProjectId}'\n            and EVENT_SOURCE = 'github'\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1340100\" author=\"ibodrov@gmail.com\">\n        <dropIndex tableName=\"API_KEYS\" indexName=\"IDX_API_KEY\"/>\n    </changeSet>\n\n    <changeSet id=\"1340110\" author=\"ibodrov@gmail.com\" runInTransaction=\"false\">\n        <sql>\n            create index concurrently IDX_PROC_Q_C_STATUS on PROCESS_QUEUE (CURRENT_STATUS)\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1340120\" author=\"ibodrov@gmail.com\">\n        <dropIndex tableName=\"PROCESS_QUEUE\" indexName=\"IDX_PROC_Q_POLL\"/>\n    </changeSet>\n\n    <changeSet id=\"1340130\" author=\"ibodrov@gmail.com\" runInTransaction=\"false\">\n        <sql>\n            create index concurrently IDX_PROC_Q_PRJ_ID on PROCESS_QUEUE (PROJECT_ID)\n        </sql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.34.1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1341000\" author=\"ybrigo@gmail.com\">\n        <sql>\n            update TRIGGERS set\n                CONDITIONS = '{\"githubOrg\": \".*\", \"githubRepo\": \".*\", \"branch\": \".*\", \"version\": 2, \"repositoryInfo\": [{\"repository\": \".*\"}]}'\n            where\n            PROJECT_ID = '${concordTriggersProjectId}'\n            and EVENT_SOURCE = 'github'\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1341100\" author=\"ybrigo@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <columnExists tableName=\"TRIGGERS\" columnName=\"TRIGGER_VERSION\"/>\n        </preConditions>\n\n        <dropColumn tableName=\"TRIGGERS\" columnName=\"TRIGGER_VERSION\"/>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.34.2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1342000\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <indexExists tableName=\"AGENT_COMMANDS\" indexName=\"IDX_A_CMD_A_ID\"/>\n        </preConditions>\n\n        <dropIndex tableName=\"AGENT_COMMANDS\" indexName=\"IDX_A_CMD_A_ID\"/>\n    </changeSet>\n\n    <changeSet id=\"1342010\" author=\"ibodrov@gmail.com\">\n        <createIndex tableName=\"AGENT_COMMANDS\" indexName=\"IDX_A_CMD_STATUS\">\n            <column name=\"COMMAND_STATUS\"/>\n        </createIndex>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.34.3.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1344000\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"createExtensionAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            CREATE EXTENSION IF NOT EXISTS \"pg_trgm\"\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1344010\" author=\"ibodrov@gmail.com\" runInTransaction=\"false\">\n        <sql>\n            create index concurrently IDX_PROC_INITIATOR_ID on PROCESS_QUEUE (INITIATOR_ID)\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1344020\" author=\"ibodrov@gmail.com\" runInTransaction=\"false\">\n        <sql>\n            create index concurrently IDX_USERS_USERNAME on USERS using gin (USERNAME gin_trgm_ops)\n        </sql>\n    </changeSet>\n\n    <!-- removed in 1.99.0+\n    <changeSet id=\"1344030\" author=\"ibodrov@gmail.com\" runInTransaction=\"false\">\n        <sql>\n            create index concurrently IDX_PROC_META on PROCESS_QUEUE using gin (META jsonb_path_ops)\n        </sql>\n    </changeSet>\n    -->\n\n    <changeSet id=\"1344100\" author=\"ybrigo@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <tableExists tableName=\"EVENT_PROCESSOR_MARKER\"/>\n        </preConditions>\n\n        <dropTable tableName=\"EVENT_PROCESSOR_MARKER\"/>\n    </changeSet>\n\n    <changeSet id=\"1344110\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"EVENT_PROCESSOR_MARKER\">\n            <column name=\"PROCESSOR_NAME\" type=\"varchar(64)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"EVENT_SEQ\" type=\"bigint\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"1344120\" author=\"ybrigo@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <tableExists tableName=\"EVENT_PROCESSOR_MARKERS\"/>\n        </preConditions>\n\n        <sql>\n            insert into event_processor_marker(processor_name, event_seq)\n            select processor_name, max(event_seq) from event_processor_markers group by processor_name;\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1344200\" author=\"ibodrov@gmail.com\" runInTransaction=\"false\">\n        <sql>\n            create index concurrently IDX_PROC_ENQUEUED on PROCESS_QUEUE (CURRENT_STATUS) where CURRENT_STATUS = 'ENQUEUED';\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1344300\" author=\"ibodrov@gmail.com\" runInTransaction=\"false\">\n        <!-- skip if the table is partitioned -->\n        <preConditions onFail=\"MARK_RAN\">\n            <sqlCheck expectedResult=\"0\">\n                select count(*)\n                from pg_inherits\n                    join pg_class parent ON pg_inherits.inhparent = parent.oid\n                    join pg_class child ON pg_inherits.inhrelid = child.oid\n                    join pg_namespace nmsp_parent ON nmsp_parent.oid = parent.relnamespace\n                    join pg_namespace nmsp_child ON nmsp_child.oid = child.relnamespace\n                where parent.relname = 'process_checkpoints'\n            </sqlCheck>\n        </preConditions>\n        <sql>\n            create index concurrently IDX_CHECKPOINTS_PROC_ID on PROCESS_CHECKPOINTS (INSTANCE_ID)\n        </sql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.35.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1350000\" author=\"ibodrov@gmail.com\" runInTransaction=\"false\">\n        <sql>\n            create index concurrently IDX_TRIGGER_PR_ID_REPO_ID on TRIGGERS (PROJECT_ID, REPO_ID)\n        </sql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.38.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1380000\" author=\"ibodrov@gmail.com\" runInTransaction=\"false\">\n        <sql>\n            update TRIGGERS set CONDITIONS = '{\"version\": 2, \"type\": \"push\", \"branch\": \".*\", \"githubOrg\": \".*\", \"githubRepo\": \".*\", \"repositoryInfo\": [{\"repository\": \".*\"}]}'\n            where\n                PROJECT_ID = '${concordTriggersProjectId}'\n                and EVENT_SOURCE = 'github'\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1380100\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"INVENTORY_DATA\">\n            <column name=\"ITEM_DATA_SIZE\" type=\"bigint\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"1380110\" author=\"ybrigo@gmail.com\">\n        <renameTable oldTableName=\"INVENTORIES\" newTableName=\"JSON_STORES\"/>\n        <renameTable oldTableName=\"INVENTORY_DATA\" newTableName=\"JSON_STORE_DATA\"/>\n        <renameTable oldTableName=\"INVENTORY_QUERIES\" newTableName=\"JSON_STORE_QUERIES\"/>\n        <renameTable oldTableName=\"INVENTORY_TEAM_ACCESS\" newTableName=\"JSON_STORE_TEAM_ACCESS\"/>\n    </changeSet>\n\n    <changeSet id=\"1380120\" author=\"ybrigo@gmail.com\">\n        <renameColumn tableName=\"JSON_STORES\" oldColumnName=\"INVENTORY_ID\" newColumnName=\"JSON_STORE_ID\"/>\n        <renameColumn tableName=\"JSON_STORES\" oldColumnName=\"INVENTORY_NAME\" newColumnName=\"JSON_STORE_NAME\"/>\n        <renameColumn tableName=\"JSON_STORE_DATA\" oldColumnName=\"INVENTORY_ID\" newColumnName=\"JSON_STORE_ID\"/>\n        <renameColumn tableName=\"JSON_STORE_QUERIES\" oldColumnName=\"INVENTORY_ID\" newColumnName=\"JSON_STORE_ID\"/>\n        <renameColumn tableName=\"JSON_STORE_TEAM_ACCESS\" oldColumnName=\"INVENTORY_ID\" newColumnName=\"JSON_STORE_ID\"/>\n    </changeSet>\n\n    <!-- backward campatibility -->\n    <changeSet id=\"1380130\" author=\"ybrigo@gmail.com\">\n        <createView viewName=\"INVENTORIES\" replaceIfExists=\"true\">\n            select\n                JSON_STORE_ID as INVENTORY_ID,\n                JSON_STORE_NAME as INVENTORY_NAME,\n                null as PARENT_INVENTORY_ID,\n                ORG_ID,\n                VISIBILITY,\n                OWNER_ID\n            from JSON_STORES\n        </createView>\n\n        <createView viewName=\"INVENTORY_DATA\" replaceIfExists=\"true\">\n            select JSON_STORE_ID as INVENTORY_ID, ITEM_PATH, ITEM_DATA from JSON_STORE_DATA\n        </createView>\n\n        <createView viewName=\"INVENTORY_QUERIES\" replaceIfExists=\"true\">\n            select\n                QUERY_ID,\n                JSON_STORE_ID as INVENTORY_ID,\n                QUERY_NAME,\n                QUERY_TEXT\n            from JSON_STORE_QUERIES\n        </createView>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.40.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1400000\" author=\"ybrigo@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <indexExists tableName=\"PROCESS_QUEUE\" indexName=\"IDX_PROC_WAIT_COND_NN\"/>\n        </preConditions>\n\n        <dropIndex tableName=\"PROCESS_QUEUE\" indexName=\"IDX_PROC_WAIT_COND_NN\"/>\n    </changeSet>\n\n    <!-- WAIT_CONDITIONS was moved to the new table \"PROCESS_WAIT_CONDITIONS\" in 1.83.0+\n    <changeSet id=\"1400010\" author=\"ybrigo@gmail.com\" runInTransaction=\"false\">\n        <sql>\n            create index concurrently IDX_PROC_WAIT_COND_NN\n            on PROCESS_QUEUE (INSTANCE_ID)\n            where WAIT_CONDITIONS is not null\n        </sql>\n    </changeSet>\n    -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.41.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1410000\" author=\"ibodrov@gmail.com\" runInTransaction=\"false\">\n        <!-- skip if the table is partitioned -->\n        <preConditions onFail=\"MARK_RAN\">\n            <sqlCheck expectedResult=\"0\">\n                select count(*)\n                from pg_inherits\n                join pg_class parent ON pg_inherits.inhparent = parent.oid\n                join pg_class child ON pg_inherits.inhrelid = child.oid\n                join pg_namespace nmsp_parent ON nmsp_parent.oid = parent.relnamespace\n                join pg_namespace nmsp_child ON nmsp_child.oid = child.relnamespace\n                where parent.relname = 'audit_log'\n            </sqlCheck>\n        </preConditions>\n        <sql>\n            create index concurrently IDX_AUDIT_LOG_DETAILS on AUDIT_LOG using gin (ENTRY_DETAILS)\n        </sql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.43.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1430000\" author=\"pranav.r.parikh@gmail.com\" runInTransaction=\"false\">\n        <sql>\n            update TRIGGERS set CONDITIONS = '{\"version\": 2, \"event\": \"repository.*\" }'\n            where\n                PROJECT_ID = '${concordTriggersProjectId}'\n                and EVENT_SOURCE = 'concord'\n        </sql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.45.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n        <!-- moved to v1.86.0.xml -->\n        <!--\n        <changeSet id=\"1450000\" author=\"ybrigo@gmail.com\" runInTransaction=\"false\" context=\"!codegen\">\n            <sql>\n                delete from API_KEYS where KEY_ID = 'd5165ca8-e8de-11e6-9bf5-136b5db23c32'\n            </sql>\n\n            <customChange class=\"com.walmartlabs.concord.server.liquibase.ext.ApiTokenCreator\">\n                &lt;!&ndash; default admin user ID &ndash;&gt;\n                <param name=\"userId\" value=\"230c5c9c-d9a7-11e6-bcfd-bb681c07b26c\"/>\n                <param name=\"username\" value=\"admin\"/>\n                &lt;!&ndash; value from concord-server.conf &ndash;&gt;\n                <param name=\"token\" value=\"${defaultAdminToken}\"/>\n            </customChange>\n        </changeSet>\n       -->\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.48.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1480000\" author=\"ibodrov@gmail.com\">\n        <sql>\n            update ORGANIZATIONS set VISIBILITY = 'PRIVATE' where ORG_ID = '${concordSystemOrgId}'\n        </sql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.49.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1490000\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"RUNTIME\" type=\"text\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"1490010\" author=\"ybrigo@gmail.com\">\n        <validCheckSum>7:b70de8118884fd89025bb9ed8a8b0572</validCheckSum>\n        <createTable tableName=\"PROCESS_LOG_SEGMENTS\">\n            <column name=\"INSTANCE_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"INSTANCE_CREATED_AT\" type=\"timestamp\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"SEGMENT_ID\" type=\"bigserial\" autoIncrement=\"true\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"SEGMENT_NAME\" type=\"text\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"CORRELATION_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"SEGMENT_TS\" type=\"timestamp\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"SEGMENT_STATUS\" type=\"text\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"SEGMENT_ERRORS\" type=\"int\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"SEGMENT_WARN\" type=\"int\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"1490011\" author=\"ybrigo@gmail.com\">\n        <createIndex tableName=\"PROCESS_LOG_SEGMENTS\" indexName=\"IDX_PLS_IDS\">\n            <column name=\"INSTANCE_ID\"/>\n            <column name=\"INSTANCE_CREATED_AT\"/>\n        </createIndex>\n    </changeSet>\n\n    <changeSet id=\"1490020\" author=\"ybrigo@gmail.com\">\n        <validCheckSum>7:10ab12d5b908de504f897e6e2a773036</validCheckSum>\n        <createTable tableName=\"PROCESS_LOG_DATA\">\n            <column name=\"INSTANCE_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"INSTANCE_CREATED_AT\" type=\"timestamp\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"SEGMENT_ID\" type=\"bigserial\" autoIncrement=\"true\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"LOG_RANGE\" type=\"int4range\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"SEGMENT_RANGE\" type=\"int4range\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"CHUNK_DATA\" type=\"longblob\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"LOG_SEQ\" type=\"bigserial\" autoIncrement=\"true\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"1490030\" author=\"ybrigo@gmail.com\">\n        <createIndex tableName=\"PROCESS_LOG_DATA\" indexName=\"IDX_PLD_IDS\">\n            <column name=\"INSTANCE_ID\"/>\n            <column name=\"INSTANCE_CREATED_AT\"/>\n            <column name=\"SEGMENT_ID\"/>\n        </createIndex>\n    </changeSet>\n\n    <changeSet id=\"1490040\" author=\"ybrigo@gmail.com\">\n        <createProcedure dbms=\"postgresql\">\n            create or replace function PROCESS_LOG_DATA_SEGMENT_NEXT_RANGE(P_INSTANCE_ID uuid, P_CREATED_AT timestamp, P_SEGMENT_ID bigint, P_DATA_LEN int)\n            returns int4range as $$\n            declare\n                R_START int;\n            begin\n                select coalesce(max(upper(SEGMENT_RANGE)), 0) into R_START\n                from PROCESS_LOG_DATA\n                where\n                    INSTANCE_ID = P_INSTANCE_ID and INSTANCE_CREATED_AT = P_CREATED_AT and SEGMENT_ID = P_SEGMENT_ID;\n\n                if R_START is null then\n                    R_START := 0;\n                end if;\n\n                return int4range(R_START, R_START + P_DATA_LEN);\n            end;\n            $$ language plpgsql;\n        </createProcedure>\n\n        <createProcedure dbms=\"postgresql\">\n            create or replace function PROCESS_LOG_DATA_NEXT_RANGE(P_INSTANCE_ID uuid, P_CREATED_AT timestamp, P_DATA_LEN int)\n            returns int4range as $$\n            declare\n            R_START int;\n            begin\n                select coalesce(max(upper(LOG_RANGE)), 0) into R_START\n                from PROCESS_LOG_DATA\n                where\n                    INSTANCE_ID = P_INSTANCE_ID and INSTANCE_CREATED_AT = P_CREATED_AT;\n\n                if R_START is null then\n                    R_START := 0;\n                end if;\n\n                return int4range(R_START, R_START + P_DATA_LEN);\n            end;\n            $$ language plpgsql;\n        </createProcedure>\n    </changeSet>\n\n    <changeSet id=\"1490050\" author=\"ybrigo@gmail.com\">\n        <createProcedure dbms=\"postgresql\">\n            create or replace function PROCESS_LOG_DATA_SEGMENT_LAST_N_BYTES(P_INSTANCE_ID uuid, P_CREATED_AT timestamp, P_SEGMENT_ID bigint, P_DATA_LEN int)\n            returns int4range as $$\n            declare\n                R_START int;\n            begin\n                select coalesce(max(upper(SEGMENT_RANGE)), 0) into R_START\n                from PROCESS_LOG_DATA\n                where\n                    INSTANCE_ID = P_INSTANCE_ID and INSTANCE_CREATED_AT = P_CREATED_AT and SEGMENT_ID = P_SEGMENT_ID;\n\n                if R_START is null then\n                    R_START := 0;\n                end if;\n\n                return int4range(R_START - P_DATA_LEN, R_START);\n            end;\n            $$ language plpgsql;\n        </createProcedure>\n    </changeSet>\n\n    <changeSet id=\"1490060\" author=\"ybrigo@gmail.com\">\n        <createProcedure dbms=\"postgresql\">\n            create or replace function PROCESS_LOG_DATA_LAST_N_BYTES(P_INSTANCE_ID uuid, P_CREATED_AT timestamp, P_DATA_LEN int)\n            returns int4range as $$\n            declare\n                R_START int;\n            begin\n                select coalesce(max(upper(LOG_RANGE)), 0) into R_START\n                from PROCESS_LOG_DATA\n                where\n                    INSTANCE_ID = P_INSTANCE_ID and INSTANCE_CREATED_AT = P_CREATED_AT;\n\n                if R_START is null then\n                    R_START := 0;\n                end if;\n\n                return int4range(R_START - P_DATA_LEN, R_START);\n            end;\n            $$ language plpgsql;\n        </createProcedure>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.5.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"150000\" author=\"muhammad.amir@walmart.com\">\n        <addColumn tableName=\"REPOSITORIES\">\n            <column name=\"META\" type=\"jsonb\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.56.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1560000\" author=\"ibodrov@gmail.com\">\n        <sql>\n            update USERS set USERNAME = lower(USERNAME), DOMAIN = lower(DOMAIN)\n            where USER_TYPE = 'LOCAL'\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1560010\" author=\"ibodrov@gmail.com\">\n        <sql>\n            update USERS set USERNAME = lower(USERNAME), DOMAIN = lower(DOMAIN)\n            where USER_TYPE = 'LDAP'\n            and not exists(select lower(USERNAME) from USERS where USER_TYPE = 'LDAP' group by 1 having count(*) > 1)\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1560020\" author=\"ibodrov@gmail.com\">\n        <dropUniqueConstraint tableName=\"USERS\" constraintName=\"USERS_USERNAME_DOMAIN_USER_TYPE_KEY\"/>\n    </changeSet>\n\n    <changeSet id=\"1560030\" author=\"ibodrov@gmail.com\">\n        <createIndex tableName=\"USERS\" indexName=\"IDX_USERS_UNIQ\" unique=\"true\">\n            <column name=\"lower(USERNAME)\"/>\n            <column name=\"lower(DOMAIN)\"/>\n            <column name=\"USER_TYPE\"/>\n        </createIndex>\n    </changeSet>\n\n    <changeSet id=\"1560040\" author=\"benjamin.broadaway@walmart.com\" runInTransaction=\"false\">\n        <sql>\n            create index concurrently IDX_USER_LDAP_GROUPS_GROUPS on USER_LDAP_GROUPS(LDAP_GROUP)\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1560050\" author=\"ybrigo@gmail.com\">\n        <validCheckSum>7:814490917fd400b881b94490dc4912e1</validCheckSum>\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"ID_SEQ\" type=\"bigserial\" autoIncrement=\"true\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.57.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <property name=\"createOrgPermissionId\" value=\"7833764b-eb62-4017-84aa-cd57d594836f\"/>\n\n    <changeSet id=\"1570000\" author=\"benjamin.broadaway@walmart.com\">\n        <insert tableName=\"PERMISSIONS\">\n            <column name=\"PERMISSION_ID\" value=\"${createOrgPermissionId}\"/>\n            <column name=\"PERMISSION_NAME\" value=\"createOrg\"/>\n            <column name=\"DESCRIPTION\" value=\"Permission to create organizations\"/>\n        </insert>\n\n        <sql>\n            insert into ROLE_PERMISSIONS values (\n                (select ROLE_ID from ROLES where ROLE_NAME = 'concordAdmin'),\n                '${createOrgPermissionId}'\n            )\n        </sql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.58.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1580000\" author=\"ibodrov@gmail.com\">\n        <!--\n            A hacky way to convert timestamp to timestamptz without rewriting the data.\n\n            Assuming the current data is in UTC, we can change all \"timestamp\" columns\n            and indices to \"timestamp with time zone\" just by updating the column's \"atttypid\".\n\n            Requires SUPERUSER privileges.\n        -->\n        <createProcedure dbms=\"postgresql\">\n            create or replace function ts_to_tstz(t text)\n                returns bool as $$\n            declare\n                v_cnt numeric;\n            begin\n                v_cnt := 0;\n\n                update pg_attribute\n                    set atttypid = 'timestamp with time zone'::regtype\n                from pg_class\n                where attrelid = pg_class.oid\n                    and relnamespace = current_schema()::regnamespace\n                    and atttypid = 'timestamp'::regtype\n                    and relname ilike t;\n\n                get diagnostics v_cnt = row_count;\n                if v_cnt = 0 then\n                    raise warning 'Relation not found (or is already converted): %', t;\n                end if;\n\n                update pg_index\n                    set indclass = array_to_string(array_replace(indclass::oid[], 3128::oid, 3127::oid), ' ')::oidvector\n                from pg_class\n                where indrelid = pg_class.oid\n                    and relnamespace = current_schema()::regnamespace\n                    and indclass::oid[] @> ARRAY[3128::oid]\n                    and relname ilike t;\n\n                return v_cnt > 0;\n            end;\n            $$ language plpgsql\n        </createProcedure>\n    </changeSet>\n\n    <!-- non-partitioned tables -->\n\n    <!-- AGENT_COMMANDS -->\n    <changeSet id=\"1580100\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('agent_commands')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1580100-a\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <sql>\n            alter table AGENT_COMMANDS alter column CREATED_AT type timestamptz using CREATED_AT at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- API_KEYS -->\n    <changeSet id=\"1580110\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('api_keys')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1580110-a\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <sql>\n            alter table API_KEYS\n                alter column EXPIRED_AT type timestamptz using EXPIRED_AT at time zone 'UTC',\n                alter column LAST_NOTIFIED_AT type timestamptz using LAST_NOTIFIED_AT at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- PROCESS_QUEUE -->\n    <changeSet id=\"1580120\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('process_queue')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1580120-a\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <sql>\n            alter table PROCESS_QUEUE\n                alter column CREATED_AT type timestamptz using CREATED_AT at time zone 'UTC',\n                alter column LAST_UPDATED_AT type timestamptz using LAST_UPDATED_AT at time zone 'UTC',\n                alter column START_AT type timestamptz using START_AT at time zone 'UTC',\n                alter column LAST_RUN_AT type timestamptz using LAST_RUN_AT at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- TASKS -->\n    <changeSet id=\"1580130\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('tasks')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1580130-a\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <sql>\n            alter table TASKS\n                alter column STARTED_AT type timestamptz using STARTED_AT at time zone 'UTC',\n                alter column FINISHED_AT type timestamptz using FINISHED_AT at time zone 'UTC',\n                alter column LAST_UPDATED_AT type timestamptz using LAST_UPDATED_AT at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- TASK_LOCKS -->\n    <changeSet id=\"1580140\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('task_locks')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1580140-a\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <sql>\n            alter table TASK_LOCKS\n                alter column LOCKED_AT type timestamptz using LOCKED_AT at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- TRIGGER_SCHEDULE -->\n    <changeSet id=\"1580150\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('trigger_schedule')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1580150-a\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <sql>\n            alter table TRIGGER_SCHEDULE\n                alter column FIRE_AT type timestamptz using FIRE_AT at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- USERS -->\n    <changeSet id=\"1580160\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('users')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1580160-a\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <sql>\n            alter table USERS\n                alter column LAST_GROUP_SYNC_DT type timestamptz using LAST_GROUP_SYNC_DT at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- tables that might be partitioned -->\n\n    <!-- AUDIT_LOG -->\n    <changeSet id=\"1580200\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('audit_log%')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1580200-a\" author=\"ibodrov@gmail.com\">\n        <validCheckSum>7:0415f1854d1f087f0e52563eddf066ef</validCheckSum>\n        <validCheckSum>8:7e38ae15657bf698e251a92f72916a83</validCheckSum>\n\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <!-- replaced below in 1580201\n        <dropView viewName=\"V_AUDIT_LOG\"/>\n        -->\n\n        <sql>\n            alter table AUDIT_LOG\n                alter column ENTRY_DATE type timestamptz using ENTRY_DATE at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- V_AUDIT_LOG -->\n    <changeSet id=\"1580201\" author=\"ibodrov@gmail.com\">\n        <createView viewName=\"V_AUDIT_LOG\" replaceIfExists=\"true\">\n            select\n                ENTRY_SEQ,\n                ENTRY_DATE,\n                USER_ID,\n                (select USERNAME from USERS u where u.USER_ID = a.USER_ID) as USERNAME,\n                ENTRY_OBJECT,\n                ENTRY_ACTION,\n                ENTRY_DETAILS\n            from AUDIT_LOG a\n        </createView>\n    </changeSet>\n\n    <!-- PROCESS_CHECKPOINTS -->\n    <changeSet id=\"1580210\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('process_checkpoints%')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1580210-a\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <sql>\n            alter table PROCESS_CHECKPOINTS\n                alter column INSTANCE_CREATED_AT type timestamptz using INSTANCE_CREATED_AT at time zone 'UTC',\n                alter column CHECKPOINT_DATE type timestamptz using CHECKPOINT_DATE at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- PROCESS_EVENTS -->\n    <changeSet id=\"1580220\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('process_events%')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1580220-a\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <sql>\n            alter table PROCESS_EVENTS\n                alter column INSTANCE_CREATED_AT type timestamptz using INSTANCE_CREATED_AT at time zone 'UTC',\n                alter column EVENT_DATE type timestamptz using EVENT_DATE at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- PROCESS_LOG_DATA -->\n    <changeSet id=\"1580230\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('process_log_data%')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1580230-a\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <sql>\n            alter table PROCESS_LOG_DATA\n                alter column INSTANCE_CREATED_AT type timestamptz using INSTANCE_CREATED_AT at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- PROCESS_LOG_SEGMENTS -->\n    <changeSet id=\"1580240\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('process_log_segments%')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1580240-a\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <sql>\n            alter table PROCESS_LOG_SEGMENTS\n                alter column INSTANCE_CREATED_AT type timestamptz using INSTANCE_CREATED_AT at time zone 'UTC',\n                alter column SEGMENT_TS type timestamptz using SEGMENT_TS at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- PROCESS_STATE -->\n    <changeSet id=\"1580250\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('process_state%')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1580250-a\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <sql>\n            alter table PROCESS_STATE\n                alter column INSTANCE_CREATED_AT type timestamptz using INSTANCE_CREATED_AT at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- the rest of it -->\n    <changeSet id=\"1580300\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            update pg_attribute\n                set atttypid = 'timestamp with time zone'::regtype\n            from pg_class\n            where attrelid = pg_class.oid\n                and relnamespace = current_schema()::regnamespace\n                and atttypid = 'timestamp'::regtype\n                and relname not in (\n                    'server_db_lock',\n                    'server_db_log'\n                );\n        </sql>\n    </changeSet>\n\n    <!-- functions and procedures -->\n\n    <changeSet id=\"1580500\" author=\"ibodrov@gmail.com\">\n        <createProcedure dbms=\"postgresql\">\n            create or replace function PROCESS_LOG_DATA_SEGMENT_NEXT_RANGE(P_INSTANCE_ID uuid, P_CREATED_AT timestamptz, P_SEGMENT_ID bigint, P_DATA_LEN int)\n                returns int4range as $$\n            declare\n                R_START int;\n            begin\n                select coalesce(max(upper(SEGMENT_RANGE)), 0) into R_START\n                from PROCESS_LOG_DATA\n                where\n                INSTANCE_ID = P_INSTANCE_ID and INSTANCE_CREATED_AT = P_CREATED_AT and SEGMENT_ID = P_SEGMENT_ID;\n\n                if R_START is null then\n                    R_START := 0;\n                end if;\n\n                return int4range(R_START, R_START + P_DATA_LEN);\n            end;\n            $$ language plpgsql;\n        </createProcedure>\n\n        <createProcedure dbms=\"postgresql\">\n            create or replace function PROCESS_LOG_DATA_NEXT_RANGE(P_INSTANCE_ID uuid, P_CREATED_AT timestamptz, P_DATA_LEN int)\n                returns int4range as $$\n            declare\n                R_START int;\n            begin\n                select coalesce(max(upper(LOG_RANGE)), 0) into R_START\n                from PROCESS_LOG_DATA\n                where\n                INSTANCE_ID = P_INSTANCE_ID and INSTANCE_CREATED_AT = P_CREATED_AT;\n\n                if R_START is null then\n                    R_START := 0;\n                end if;\n\n                return int4range(R_START, R_START + P_DATA_LEN);\n            end;\n            $$ language plpgsql;\n        </createProcedure>\n    </changeSet>\n\n    <changeSet id=\"1580510\" author=\"ibodrov@gmail.com\">\n        <createProcedure dbms=\"postgresql\">\n            create or replace function PROCESS_LOG_DATA_SEGMENT_LAST_N_BYTES(P_INSTANCE_ID uuid, P_CREATED_AT timestamptz, P_SEGMENT_ID bigint, P_DATA_LEN int)\n                returns int4range as $$\n            declare\n                R_START int;\n            begin\n                select coalesce(max(upper(SEGMENT_RANGE)), 0) into R_START\n                from PROCESS_LOG_DATA\n                where\n                INSTANCE_ID = P_INSTANCE_ID and INSTANCE_CREATED_AT = P_CREATED_AT and SEGMENT_ID = P_SEGMENT_ID;\n\n                if R_START is null then\n                R_START := 0;\n                end if;\n\n                return int4range(R_START - P_DATA_LEN, R_START);\n            end;\n            $$ language plpgsql;\n        </createProcedure>\n    </changeSet>\n\n    <changeSet id=\"1580520\" author=\"ibodrov@gmail.com\">\n        <createProcedure dbms=\"postgresql\">\n            create or replace function PROCESS_LOG_DATA_LAST_N_BYTES(P_INSTANCE_ID uuid, P_CREATED_AT timestamptz, P_DATA_LEN int)\n                returns int4range as $$\n            declare\n                R_START int;\n            begin\n                select coalesce(max(upper(LOG_RANGE)), 0) into R_START\n                from PROCESS_LOG_DATA\n                where\n                INSTANCE_ID = P_INSTANCE_ID and INSTANCE_CREATED_AT = P_CREATED_AT;\n\n                if R_START is null then\n                    R_START := 0;\n                end if;\n\n                return int4range(R_START - P_DATA_LEN, R_START);\n            end;\n            $$ language plpgsql;\n        </createProcedure>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.60.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1600000\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"DEPENDENCIES\" type=\"text[]\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.66.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1650000\" author=\"ybrigo@gmail.com\">\n        <sql>\n            create type out_variables_mode as enum ('DISABLED', 'OWNERS', 'TEAM_MEMBERS', 'ORG_MEMBERS', 'EVERYONE')\n        </sql>\n\n        <addColumn tableName=\"PROJECTS\">\n            <column name=\"OUT_VARIABLES_MODE\" type=\"out_variables_mode\" defaultValue=\"DISABLED\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.69.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1690000\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"TASKS\">\n            <column name=\"LAST_ERROR_AT\" type=\"timestamptz\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n        <addColumn tableName=\"TASKS\">\n            <column name=\"LAST_ERROR\" type=\"text\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n\n        <sql>\n            create type task_status_type as enum ('OK', 'ERROR', 'RUNNING', 'STALLED')\n        </sql>\n\n        <sql>\n            ALTER TABLE TASKS\n            ALTER COLUMN TASK_STATUS TYPE task_status_type\n            USING TASK_STATUS::task_status_type\n        </sql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.7.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"170000\" author=\"ibodrov@gmail.com\">\n        <validCheckSum>7:986edd322fffcdedbd5a76762c4cb1f3</validCheckSum>\n        <insert tableName=\"ROLES\">\n            <column name=\"ROLE_NAME\">concordAdmin</column>\n        </insert>\n\n        <!-- deprecated in 1.65.0+ -->\n        <!--\n        <sql>\n            insert into USER_ROLES\n                select\n                    USER_ID,\n                    (select ROLE_ID from ROLES where ROLE_NAME = 'concordAdmin')\n                from USERS where is_admin = true\n        </sql>\n        -->\n\n        <!-- default admin user ID -->\n        <sql>\n            insert into USER_ROLES (USER_ID, ROLE_ID)\n                values ('230c5c9c-d9a7-11e6-bcfd-bb681c07b26c', (select ROLE_ID from ROLES where ROLE_NAME = 'concordAdmin'))\n        </sql>\n    </changeSet>\n\n    <!-- disabled to keep the schema backward compatible\n    <changeSet id=\"170010\" author=\"ibodrov@gmail.com\">\n        <dropColumn tableName=\"ROLES\" columnName=\"GLOBAL_READER\"/>\n        <dropColumn tableName=\"ROLES\" columnName=\"GLOBAL_WRITER\"/>\n        <dropColumn tableName=\"USERS\" columnName=\"IS_ADMIN\"/>\n    </changeSet>\n    -->\n\n    <changeSet id=\"170100\" author=\"ybrigo@gmail.com\">\n        <validCheckSum>7:91607973cfe00ca1dedaaf3ac4ecdd91</validCheckSum>\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"WAIT_CONDITIONS\" type=\"jsonb\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <!-- deprecated in 1.32.0+\n            <column name=\"IS_EXCLUSIVE\" type=\"boolean\" defaultValue=\"false\">\n                <constraints nullable=\"false\"/>\n            </column>\n            -->\n        </addColumn>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.75.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1750000\" author=\"ybrigo@gmail.com\" runInTransaction=\"false\">\n        <sql>\n            update TRIGGERS set CONDITIONS = '{\"version\": 2, \"type\": \"push\", \"branch\": \".*\", \"githubOrg\": \".*\", \"githubRepo\": \".*\", \"repositoryInfo\": [{\"repository\": \".*\", \"enabled\": true}]}'\n            where\n            PROJECT_ID = '${concordTriggersProjectId}'\n            and EVENT_SOURCE = 'github'\n        </sql>\n    </changeSet>\n\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.76.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1760000\" author=\"amith.k.b@walmartlabs.com\">\n        <sql>\n            update REPOSITORIES set REPO_BRANCH = 'master' where REPO_BRANCH is NULL and REPO_COMMIT_ID is NULL\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1760100\" author=\"ybrigo@gmail.com\">\n        <sql>\n            alter table REPOSITORIES add constraint REPO_BRANCH_COMMIT_CHECK check (REPO_BRANCH is not null or REPO_COMMIT_ID is not null)\n        </sql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.78.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1780000\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"COMMIT_BRANCH\" type=\"varchar(255)\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"1780100\" author=\"ybrigo@gmail.com\">\n        <createView viewName=\"json_store_data_view_restricted\">\n            select *\n            from json_store_data\n            where\n                json_store_id = current_setting('jsonStoreQueryExec.json_store_id')::uuid;\n        </createView>\n\n        <createView viewName=\"INVENTORY_DATA\" replaceIfExists=\"true\">\n            select JSON_STORE_ID as INVENTORY_ID, ITEM_PATH, ITEM_DATA from JSON_STORE_DATA_VIEW_RESTRICTED\n        </createView>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.79.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1790000\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"SUSPEND_TIMEOUT\" type=\"bigint\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.8.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"180000\" author=\"ybrigo@gmail.com\">\n        <sql>\n            create type process_lock_scope as enum ('ORG', 'PROJECT')\n        </sql>\n\n        <createTable tableName=\"PROCESS_LOCKS\">\n            <column name=\"INSTANCE_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"ORG_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"PROJECT_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"LOCK_SCOPE\" type=\"process_lock_scope\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"LOCK_NAME\" type=\"varchar(128)\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <addForeignKeyConstraint baseTableName=\"PROCESS_LOCKS\"\n                                 baseColumnNames=\"INSTANCE_ID\"\n                                 constraintName=\"FK_PROCESS_LOCKS_INSTANCE_ID\"\n                                 referencedTableName=\"PROCESS_QUEUE\"\n                                 referencedColumnNames=\"INSTANCE_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <addForeignKeyConstraint baseTableName=\"PROCESS_LOCKS\"\n                                 baseColumnNames=\"ORG_ID\"\n                                 constraintName=\"FK_PROCESS_LOCKS_ORG_ID\"\n                                 referencedTableName=\"ORGANIZATIONS\"\n                                 referencedColumnNames=\"ORG_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <addForeignKeyConstraint baseTableName=\"PROCESS_LOCKS\"\n                                 baseColumnNames=\"PROJECT_ID\"\n                                 constraintName=\"FK_PROCESS_LOCKS_PROJECT_ID\"\n                                 referencedTableName=\"PROJECTS\"\n                                 referencedColumnNames=\"PROJECT_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n    <changeSet id=\"180010\" author=\"ybrigo@gmail.com\">\n        <createIndex tableName=\"PROCESS_LOCKS\" indexName=\"IDX_PROCESS_LOCKS_1\" unique=\"true\">\n            <column name=\"ORG_ID\"/>\n            <column name=\"LOCK_NAME\"/>\n        </createIndex>\n        <modifySql>\n            <append value=\"where LOCK_SCOPE = 'ORG'\"/>\n        </modifySql>\n    </changeSet>\n\n    <changeSet id=\"180020\" author=\"ybrigo@gmail.com\">\n        <createIndex tableName=\"PROCESS_LOCKS\" indexName=\"IDX_PROCESS_LOCKS_2\" unique=\"true\">\n            <column name=\"PROJECT_ID\"/>\n            <column name=\"LOCK_NAME\"/>\n        </createIndex>\n        <modifySql>\n            <append value=\"where LOCK_SCOPE = 'PROJECT'\"/>\n        </modifySql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.81.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1810000\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"PROCESS_LOG_SEGMENTS\">\n            <column name=\"STATUS_UPDATED_AT\" type=\"timestamptz\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.83.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1830000\" author=\"ybrigo@gmail.com\">\n        <validCheckSum>7:a08819b929a9488573c3ea5d8f2f67a9</validCheckSum>\n        <createTable tableName=\"PROCESS_WAIT_CONDITIONS\">\n            <column name=\"INSTANCE_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"INSTANCE_CREATED_AT\" type=\"timestamptz\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"IS_WAITING\" type=\"boolean\" defaultValueBoolean=\"false\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"WAIT_CONDITIONS\" type=\"jsonb\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"ID_SEQ\" type=\"bigserial\" autoIncrement=\"true\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"1830010\" author=\"ybrigo@gmail.com\">\n        <createIndex tableName=\"PROCESS_WAIT_CONDITIONS\" indexName=\"IDX_PROCESS_WAIT_CONDITIONS_IDS\">\n            <column name=\"INSTANCE_ID\"/>\n            <column name=\"INSTANCE_CREATED_AT\"/>\n        </createIndex>\n    </changeSet>\n\n    <changeSet id=\"1830020\" author=\"ybrigo@gmail.com\">\n        <sql>\n            create index IDX_PROCESS_WAIT_COND_POLL\n            on PROCESS_WAIT_CONDITIONS (INSTANCE_CREATED_AT, ID_SEQ)\n            where IS_WAITING = true\n        </sql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.85.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n<!-- removed in 1.88.0+\n    <changeSet id=\"1850000\" author=\"ybrigo@gmail.com\">\n        <createProcedure>\n            create or replace function PROCESS_STATUS_HISTORY()\n            returns trigger as\n            $$\n            declare\n                old_status varchar := null;\n                new_status varchar;\n            begin\n                if TG_OP = 'UPDATE' then\n                    old_status = OLD.current_status;\n                end if;\n\n                new_status = NEW.current_status;\n\n                if old_status is null or (new_status is not null and old_status != new_status) then\n                    insert into process_events(instance_id, instance_created_at, event_type, event_date, event_data)\n                    values (NEW.instance_id, NEW.created_at, 'PROCESS_STATUS', now(), json_build_object('oldStatus', old_status, 'newStatus', new_status));\n                end if;\n\n                return NEW;\n            end;\n            $$ language plpgsql;\n        </createProcedure>\n\n        <sql>\n            create trigger PROCESS_STATUS_HISTORY_EVENT before insert or update on PROCESS_QUEUE\n            for each row execute procedure PROCESS_STATUS_HISTORY();\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1850100\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"PROCESS_QUEUE_STATS\">\n            <column name=\"STATUS\" type=\"text\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"PROCESS_COUNT\" type=\"integer\" defaultValue=\"0\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <insert tableName=\"PROCESS_QUEUE_STATS\">\n            <column name=\"STATUS\">NEW</column>\n            <column name=\"PROCESS_COUNT\">0</column>\n        </insert>\n        <insert tableName=\"PROCESS_QUEUE_STATS\">\n            <column name=\"STATUS\">PREPARING</column>\n            <column name=\"PROCESS_COUNT\">0</column>\n        </insert>\n        <insert tableName=\"PROCESS_QUEUE_STATS\">\n            <column name=\"STATUS\">ENQUEUED</column>\n            <column name=\"PROCESS_COUNT\">0</column>\n        </insert>\n        <insert tableName=\"PROCESS_QUEUE_STATS\">\n            <column name=\"STATUS\">WAITING</column>\n            <column name=\"PROCESS_COUNT\">0</column>\n        </insert>\n        <insert tableName=\"PROCESS_QUEUE_STATS\">\n            <column name=\"STATUS\">STARTING</column>\n            <column name=\"PROCESS_COUNT\">0</column>\n        </insert>\n        <insert tableName=\"PROCESS_QUEUE_STATS\">\n            <column name=\"STATUS\">RUNNING</column>\n            <column name=\"PROCESS_COUNT\">0</column>\n        </insert>\n        <insert tableName=\"PROCESS_QUEUE_STATS\">\n            <column name=\"STATUS\">SUSPENDED</column>\n            <column name=\"PROCESS_COUNT\">0</column>\n        </insert>\n        <insert tableName=\"PROCESS_QUEUE_STATS\">\n            <column name=\"STATUS\">RESUMING</column>\n            <column name=\"PROCESS_COUNT\">0</column>\n        </insert>\n        <insert tableName=\"PROCESS_QUEUE_STATS\">\n            <column name=\"STATUS\">FINISHED</column>\n            <column name=\"PROCESS_COUNT\">0</column>\n        </insert>\n        <insert tableName=\"PROCESS_QUEUE_STATS\">\n            <column name=\"STATUS\">FAILED</column>\n            <column name=\"PROCESS_COUNT\">0</column>\n        </insert>\n        <insert tableName=\"PROCESS_QUEUE_STATS\">\n            <column name=\"STATUS\">CANCELLED</column>\n            <column name=\"PROCESS_COUNT\">0</column>\n        </insert>\n        <insert tableName=\"PROCESS_QUEUE_STATS\">\n            <column name=\"STATUS\">TIMED_OUT</column>\n            <column name=\"PROCESS_COUNT\">0</column>\n        </insert>\n    </changeSet>\n\n    <changeSet id=\"1850200\" author=\"ybrigo@gmail.com\">\n        <sql>\n            LOCK TABLE process_queue IN EXCLUSIVE MODE;\n\n            update PROCESS_QUEUE_STATS\n            set process_count = q.process_count\n            from (\n                select current_status as process_status, count(*) as process_count\n                from PROCESS_QUEUE\n                group by current_status\n            ) as q\n            where\n                q.process_status = status;\n\n            insert into event_processor_marker(processor_name, event_seq)\n            values ('process-queue-statistics-processor', (select coalesce(max(event_seq), 0) from process_events));\n        </sql>\n    </changeSet>\n-->\n    <changeSet id=\"1850600\" author=\"ybrigo@gmail.com\">\n        <sql>\n            alter table PROCESS_WAIT_CONDITIONS add column VERSION bigint;\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1850601\" author=\"ybrigo@gmail.com\">\n        <sql>\n            update PROCESS_WAIT_CONDITIONS set VERSION = 0;\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1850602\" author=\"ybrigo@gmail.com\">\n        <sql>\n            alter table PROCESS_WAIT_CONDITIONS alter column VERSION set not null;\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1850603\" author=\"ybrigo@gmail.com\">\n        <sql>\n            alter table PROCESS_WAIT_CONDITIONS alter column VERSION set default 0;\n        </sql>\n    </changeSet>\n\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.86.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <property name=\"concordAdminUserId\" value=\"230c5c9c-d9a7-11e6-bcfd-bb681c07b26c\"/>\n    <!-- starting from 2.21.x, the default agent token is no longer associated with any user -->\n    <!--\n    <property name=\"concordAgentUserId\" value=\"d4f123c1-f8d4-40b2-8a12-b8947b9ce2d8\"/>\n    -->\n    <property name=\"concordRunnerUserId\" value=\"2599c604-1384-4660-a767-8bc03baa7a31\"/>\n\n    <!-- delete old hard-coded default admin API token -->\n    <changeSet id=\"1860000\" author=\"ybrigo@gmail.com\" runInTransaction=\"false\" context=\"!codegen\">\n        <sql>\n            delete from API_KEYS where KEY_ID = 'd5165ca8-e8de-11e6-9bf5-136b5db23c32'\n        </sql>\n    </changeSet>\n\n    <!-- Set initial admin API token when not exist  -->\n    <changeSet id=\"1860100\" author=\"ybrigo@gmail.com\" runInTransaction=\"false\" context=\"!codegen\">\n        <validCheckSum>ANY</validCheckSum>\n\n        <preConditions onFail=\"MARK_RAN\">\n            <sqlCheck expectedResult=\"0\">\n                select count(key_id)\n                from API_KEYS\n                where user_id = '${concordAdminUserId}';\n            </sqlCheck>\n        </preConditions>\n\n        <customChange class=\"com.walmartlabs.concord.server.liquibase.ext.ApiTokenCreator\">\n            <!-- default admin user ID -->\n            <param name=\"userId\" value=\"${concordAdminUserId}\"/>\n            <param name=\"username\" value=\"admin\"/>\n            <!-- values from concord-server.conf -->\n            <param name=\"token\" value=\"${defaultAdminToken}\"/>\n            <param name=\"skip\" value=\"${skipAdminTokenGeneration}\"/>\n        </customChange>\n    </changeSet>\n\n    <!-- delete old hard-coded default agent API token -->\n    <!-- starting from 2.21.x, the default agent token is no longer associated with any user -->\n    <!--\n    <changeSet id=\"1860200\" author=\"benjamin.broadaway@walmart.com\" runInTransaction=\"false\" context=\"!codegen\">\n        <sql>\n            delete from API_KEYS\n            where USER_ID = '${concordAgentUserId}'\n            and API_KEY = '1sw9eLZ41EOK4w/iV3jFnn6cqeAMeFtxfazqVY04koY'\n        </sql>\n    </changeSet>\n    -->\n\n    <!-- Set initial agent API token when not exist  -->\n    <!-- starting from 2.21.x, the default agent token is no longer associated with any user -->\n    <!--\n    <changeSet id=\"1860300\" author=\"benjamin.broadaway@walmart.com\" runInTransaction=\"false\" context=\"!codegen\">\n        <validCheckSum>ANY</validCheckSum>\n\n        <preConditions onFail=\"MARK_RAN\">\n            <sqlCheck expectedResult=\"0\">\n                select count(key_id)\n                from API_KEYS\n                    join users u ON api_keys.user_id = u.user_id\n                where lower(USERNAME) = lower('concordAgent');\n            </sqlCheck>\n        </preConditions>\n\n        <customChange class=\"com.walmartlabs.concord.server.liquibase.ext.ApiTokenCreator\">\n            <param name=\"userId\" value=\"${concordAgentUserId}\"/>\n            <param name=\"username\" value=\"concordAgent\"/>\n            <param name=\"token\" value=\"${defaultAgentToken}\"/>\n            <param name=\"skip\" value=\"${skipAgentTokenGeneration}\"/>\n        </customChange>\n    </changeSet>\n    -->\n\n    <!-- Delete runner API tokens and user when exist  -->\n    <changeSet id=\"1860400\" author=\"benjamin.broadaway@walmart.com\" runInTransaction=\"false\" context=\"!codegen\">\n        <preConditions onFail=\"MARK_RAN\">\n            <sqlCheck expectedResult=\"1\">\n                select count(USER_ID)\n                from USERS\n                where lower(USERNAME) = lower('concordRunner');\n            </sqlCheck>\n        </preConditions>\n\n        <sql>\n            delete from API_KEYS where USER_ID = '${concordRunnerUserId}'\n        </sql>\n\n        <sql>\n            delete from USERS where USER_ID = '${concordRunnerUserId}'\n        </sql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.88.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1880000\" author=\"ybrigo@gmail.com\">\n        <sql>\n            drop trigger if exists PROCESS_STATUS_HISTORY_EVENT on PROCESS_QUEUE;\n            drop function if exists PROCESS_STATUS_HISTORY;\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1880100\" author=\"ybrigo@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <tableExists tableName=\"PROCESS_QUEUE_STATS\"/>\n        </preConditions>\n\n        <dropTable tableName=\"PROCESS_QUEUE_STATS\"/>\n    </changeSet>\n\n    <changeSet id=\"1880200\" author=\"ybrigo@gmail.com\">\n        <sql>\n            delete from EVENT_PROCESSOR_MARKER\n            where processor_name = 'process-queue-statistics-processor';\n        </sql>\n    </changeSet>\n\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.90.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1900000\" author=\"ybrigo@gmail.com\">\n        <sql>\n            delete from TASKS where task_id = 'process-queue-statistics-processor';\n        </sql>\n    </changeSet>\n\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.91.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1910000\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"REPOSITORIES\">\n            <column name=\"IS_TRIGGERS_DISABLED\" type=\"boolean\" defaultValue=\"false\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.94.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <property name=\"updateOrgPermissionId\" value=\"8fba1fbc-53da-4a4c-b47a-4d2b4cd0b9cf\"/>\n\n    <changeSet id=\"1940000\" author=\"ybrigo@gmail.com\">\n        <insert tableName=\"PERMISSIONS\">\n            <column name=\"PERMISSION_ID\" value=\"${updateOrgPermissionId}\"/>\n            <column name=\"PERMISSION_NAME\" value=\"updateOrg\"/>\n            <column name=\"DESCRIPTION\" value=\"Permission to update organizations\"/>\n        </insert>\n\n        <sql>\n            insert into ROLE_PERMISSIONS values (\n                (select ROLE_ID from ROLES where ROLE_NAME = 'concordAdmin'),\n                '${updateOrgPermissionId}'\n            )\n        </sql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.95.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <property name=\"updateOrgPermissionId\" value=\"8fba1fbc-53da-4a4c-b47a-4d2b4cd0b9cf\"/>\n\n    <changeSet id=\"1950000\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"PROCESS_CHECKPOINTS\">\n            <column name=\"CORRELATION_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.96.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1960000\" author=\"amith.k.b@walmart.com\">\n        <createTable tableName=\"ROLE_LDAP_GROUPS\">\n            <column name=\"ROLE_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"LDAP_GROUP\" type=\"varchar(1024)\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <addUniqueConstraint tableName=\"ROLE_LDAP_GROUPS\" columnNames=\"ROLE_ID, LDAP_GROUP\"/>\n    </changeSet>\n\n    <changeSet id=\"1960010\" author=\"amith.k.b@walmart.com\">\n        <createView viewName=\"V_USER_ROLES\" replaceIfExists=\"true\">\n            select USER_ID, ROLE_ID\n            from USER_ROLES\n            union\n            select distinct ulg.USER_ID, rlg.ROLE_ID\n            from ROLE_LDAP_GROUPS rlg, USER_LDAP_GROUPS ulg\n            where\n            ulg.LDAP_GROUP = rlg.LDAP_GROUP\n            and not exists(select 1 from USER_ROLES ur where ur.USER_ID = ulg.USER_ID and ur.ROLE_ID = rlg.ROLE_ID)\n        </createView>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.98.1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1981000\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"PROCESS_META\">\n            <column name=\"INSTANCE_ID\" type=\"uuid\" remarks=\"Unique process ID\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"INSTANCE_CREATED_AT\" type=\"timestamptz\" remarks=\"Timestamp of process creation\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"META\" type=\"jsonb\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </createTable>\n\n        <addPrimaryKey tableName=\"PROCESS_META\" columnNames=\"INSTANCE_ID, INSTANCE_CREATED_AT\"/>\n    </changeSet>\n\n    <changeSet id=\"1981010\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"PROCESS_TRIGGER_INFO\">\n            <column name=\"INSTANCE_ID\" type=\"uuid\" remarks=\"Unique process ID\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"INSTANCE_CREATED_AT\" type=\"timestamptz\" remarks=\"Timestamp of process creation\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"TRIGGERED_BY\" type=\"jsonb\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </createTable>\n\n        <addPrimaryKey tableName=\"PROCESS_TRIGGER_INFO\" columnNames=\"INSTANCE_ID, INSTANCE_CREATED_AT\"/>\n    </changeSet>\n\n    <changeSet id=\"1981020\" author=\"ybrigo@gmail.com\" runInTransaction=\"false\">\n        <sql>\n            create index concurrently IDX_PROC_META_META on PROCESS_META using gin (META jsonb_path_ops)\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"1981030\" author=\"g0h04k0@walmart.com\">\n        <validCheckSum>7:bc92a99fabceb83a0e43e3fb6190c30d</validCheckSum>\n        <validCheckSum>7:7a5abc67e92f25cb85f8e73fcd63a780</validCheckSum>\n        <addColumn tableName=\"SECRETS\">\n            <column name=\"LAST_UPDATED_AT\" type=\"timestamptz\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n        <addColumn tableName=\"SECRETS\">\n            <column name=\"SECRET_SALT\" type=\"longblob\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n        <addColumn tableName=\"SECRETS\">\n            <column name=\"hash_algorithm\" type=\"varchar(32)\" defaultValue=\"md5\">\n                <constraints nullable=\"true\"/>\n            </column>\n\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"1981031\" author=\"ybrigo@gmail.com\" context=\"!codegen\">\n        <validCheckSum>ANY</validCheckSum>\n        <addDefaultValue tableName=\"SECRETS\" columnName=\"SECRET_SALT\" defaultValueComputed=\"decode('${secretStoreSalt}', 'base64')\"/>\n    </changeSet>\n\n    <changeSet id=\"1981032\" author=\"ybrigo@gmail.com\" context=\"!codegen\">\n        <validCheckSum>ANY</validCheckSum>\n        <update tableName=\"SECRETS\">\n            <column name=\"SECRET_SALT\" valueComputed=\"decode('${secretStoreSalt}', 'base64')\"/>\n            <where>SECRET_SALT is NULL</where>\n        </update>\n    </changeSet>\n\n    <changeSet id=\"1981033\" author=\"ybrigo@gmail.com\">\n        <addNotNullConstraint tableName=\"SECRETS\" columnName=\"SECRET_SALT\"/>\n    </changeSet>\n\n    <changeSet id=\"1981034\" author=\"ginni.hema.sundara.rao@walmart.com\" context=\"!codegen\">\n        <validCheckSum>ANY</validCheckSum>\n        <customChange class=\"com.walmartlabs.concord.server.liquibase.ext.migration.SecretsHashMigrationTask\">\n            <param name=\"serverPassword\" value=\"${serverPassword}\"/>\n        </customChange>\n    </changeSet>\n\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.99.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"1990000\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"PROJECT_KV_STORE\">\n            <column name=\"LAST_UPDATED_AT\" type=\"timestamptz\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"1990010\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"USERS\">\n            <column name=\"DISABLED_DATE\" type=\"timestamptz\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v2.10.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"2001000\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"UI_PROCESS_CARDS\">\n            <column name=\"UI_PROCESS_CARD_ID\" type=\"uuid\" defaultValueComputed=\"uuid_generate_v1()\">\n                <constraints nullable=\"false\" primaryKey=\"true\"/>\n            </column>\n            <column name=\"PROJECT_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"REPO_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"NAME\" type=\"varchar(128)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"ENTRY_POINT\" type=\"varchar(256)\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"DESCRIPTION\" type=\"varchar(512)\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"ICON\" type=\"bytea\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </createTable>\n\n        <addForeignKeyConstraint baseTableName=\"UI_PROCESS_CARDS\"\n                                 baseColumnNames=\"PROJECT_ID\"\n                                 constraintName=\"FK_UI_PROCESS_CARDS_P_ID\"\n                                 referencedTableName=\"PROJECTS\"\n                                 referencedColumnNames=\"PROJECT_ID\"\n                                 onDelete=\"CASCADE\"/>\n        <addForeignKeyConstraint baseTableName=\"UI_PROCESS_CARDS\"\n                                 baseColumnNames=\"REPO_ID\"\n                                 constraintName=\"FK_UI_PROCESS_CARDS_R_ID\"\n                                 referencedTableName=\"REPOSITORIES\"\n                                 referencedColumnNames=\"REPO_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <createTable tableName=\"USER_UI_PROCESS_CARDS\">\n            <column name=\"USER_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"UI_PROCESS_CARD_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <addPrimaryKey tableName=\"USER_UI_PROCESS_CARDS\" columnNames=\"USER_ID, UI_PROCESS_CARD_ID\"/>\n\n        <addForeignKeyConstraint baseTableName=\"USER_UI_PROCESS_CARDS\"\n                                 baseColumnNames=\"USER_ID\"\n                                 constraintName=\"FK_USER_UI_PROCESS_CARDS_U_ID\"\n                                 referencedTableName=\"USERS\"\n                                 referencedColumnNames=\"USER_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <addForeignKeyConstraint baseTableName=\"USER_UI_PROCESS_CARDS\"\n                                 baseColumnNames=\"UI_PROCESS_CARD_ID\"\n                                 constraintName=\"FK_USER_UI_PROCESS_CARDS_UI_P_C_ID\"\n                                 referencedTableName=\"UI_PROCESS_CARDS\"\n                                 referencedColumnNames=\"UI_PROCESS_CARD_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <createTable tableName=\"TEAM_UI_PROCESS_CARDS\">\n            <column name=\"TEAM_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"UI_PROCESS_CARD_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <addPrimaryKey tableName=\"TEAM_UI_PROCESS_CARDS\" columnNames=\"TEAM_ID, UI_PROCESS_CARD_ID\"/>\n\n        <addForeignKeyConstraint baseTableName=\"TEAM_UI_PROCESS_CARDS\"\n                                 baseColumnNames=\"TEAM_ID\"\n                                 constraintName=\"FK_TEAM_UI_P_CARDS_T_ID\"\n                                 referencedTableName=\"TEAMS\"\n                                 referencedColumnNames=\"TEAM_ID\"\n                                 onDelete=\"CASCADE\"/>\n\n        <addForeignKeyConstraint baseTableName=\"USER_UI_PROCESS_CARDS\"\n                                 baseColumnNames=\"UI_PROCESS_CARD_ID\"\n                                 constraintName=\"FK_TEAM_UI_PROCESS_CARDS_UI_P_C_ID\"\n                                 referencedTableName=\"UI_PROCESS_CARDS\"\n                                 referencedColumnNames=\"UI_PROCESS_CARD_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n\n    <changeSet id=\"2001001\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"UI_PROCESS_CARDS\">\n            <column name=\"FORM\" type=\"bytea\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"DATA\" type=\"jsonb\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"2001002\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"UI_PROCESS_CARDS\">\n            <column name=\"OWNER_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n\n        <addForeignKeyConstraint baseTableName=\"UI_PROCESS_CARDS\"\n                                 baseColumnNames=\"OWNER_ID\"\n                                 constraintName=\"FK_UI_PROCESS_CARDS_OWNER_ID\"\n                                 referencedTableName=\"USERS\"\n                                 referencedColumnNames=\"USER_ID\"\n                                 onDelete=\"SET NULL\"/>\n    </changeSet>\n\n    <changeSet id=\"210000\" author=\"ybrigo@gmail.com\" runInTransaction=\"false\">\n        <sql>\n            create index concurrently IDX_WAIT_CONDITIONS on PROCESS_WAIT_CONDITIONS using gin (WAIT_CONDITIONS)\n        </sql>\n    </changeSet>\n\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v2.12.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"2120100\" author=\"benjamin.broadaway@walmart.com\" runInTransaction=\"false\">\n        <sql>\n            update TRIGGERS set CONDITIONS = '{\"version\": 2, \"type\": \"push\", \"branch\": \".*\", \"githubOrg\": \".*\", \"githubRepo\": \".*\", \"payload\": { \"deleted\": false }, \"repositoryInfo\": [{\"repository\": \".*\", \"enabled\": true}]}'\n            where\n                PROJECT_ID = '${concordTriggersProjectId}'\n              and EVENT_SOURCE = 'github'\n        </sql>\n    </changeSet>\n\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v2.14.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"2140000\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"PROCESS_QUEUE\">\n            <column name=\"TOTAL_RUNTIME_MS\" type=\"bigint\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v2.21.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"2210000\" author=\"ybrigo@gmail.com\">\n        <dropNotNullConstraint tableName=\"API_KEYS\" columnName=\"USER_ID\"/>\n    </changeSet>\n\n    <changeSet id=\"2210010\" author=\"ybrigo@gmail.com\">\n        <dropIndex tableName=\"API_KEYS \" indexName=\"IDX_API_KEYS_NAME_USER\"/>\n\n        <sql>\n            create unique index IDX_API_KEYS_NAME_USER_NULL on API_KEYS (KEY_NAME) where USER_ID is null\n        </sql>\n        <sql>\n            create unique index IDX_API_KEYS_NAME_USER_NOT_NULL on API_KEYS (KEY_NAME, USER_ID) where USER_ID is not null\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"2210020\" author=\"ybrigo@gmail.com\" runInTransaction=\"false\" context=\"!codegen\">\n        <validCheckSum>ANY</validCheckSum>\n\n        <preConditions onFail=\"MARK_RAN\">\n            <sqlCheck expectedResult=\"0\">\n                select count(key_id)\n                from API_KEYS\n                where KEY_NAME = 'concordAgentKey_autogenerated';\n            </sqlCheck>\n            <!-- concordAgentUserId='d4f123c1-f8d4-40b2-8a12-b8947b9ce2d8' -->\n            <sqlCheck expectedResult=\"0\">\n                select count(key_id)\n                from API_KEYS\n                where USER_ID = 'd4f123c1-f8d4-40b2-8a12-b8947b9ce2d8';\n            </sqlCheck>\n        </preConditions>\n\n        <customChange class=\"com.walmartlabs.concord.server.liquibase.ext.ApiTokenCreator\">\n            <param name=\"keyName\" value=\"concordAgentKey_autogenerated\"/>\n            <!-- values from concord-server.conf -->\n            <param name=\"token\" value=\"${defaultAgentToken}\"/>\n            <param name=\"skip\" value=\"${skipAgentTokenGeneration}\"/>\n        </customChange>\n    </changeSet>\n\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v2.22.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"2220000\" author=\"oalfonso@gmail.com\">\n        <addColumn tableName=\"UI_PROCESS_CARDS\">\n            <column name=\"ORDER_ID\" type=\"int\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n</databaseChangeLog>"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v2.23.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"2230000\" author=\"ibodrov@gmail.com\">\n        <sql>\n            create type process_exec_mode as enum ('DISABLED', 'READERS', 'WRITERS')\n        </sql>\n\n        <addColumn tableName=\"PROJECTS\">\n            <column name=\"PROCESS_EXEC_MODE\" type=\"process_exec_mode\" defaultValue=\"READERS\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v2.31.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <property name=\"apiKeySpecifyValuePermissionId\" value=\"0198541b-c4a7-71f4-8a55-f367bd4bfe4c\"/>\n\n    <changeSet id=\"2310000\" author=\"ibodrov@gmail.com\">\n        <insert tableName=\"PERMISSIONS\">\n            <column name=\"PERMISSION_ID\" value=\"${apiKeySpecifyValuePermissionId}\"/>\n            <column name=\"PERMISSION_NAME\" value=\"apiKeySpecifyValue\"/>\n            <column name=\"DESCRIPTION\" value=\"Allows users to specify the key value when creating new API keys\"/>\n        </insert>\n\n        <sql>\n            insert into ROLE_PERMISSIONS values (\n                (select ROLE_ID from ROLES where ROLE_NAME = 'concordAdmin'),\n                '${apiKeySpecifyValuePermissionId}'\n            )\n        </sql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v2.35.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"2510000\" author=\"benjamin.broadaway@walmart.com\">\n        <createTable tableName=\"EXTERNAL_APP_USERS\">\n            <column name=\"EXTERNAL_USER_ID\" type=\"varchar(512)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"USER_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n\n        <addForeignKeyConstraint baseTableName=\"EXTERNAL_APP_USERS\"\n                                 baseColumnNames=\"USER_ID\"\n                                 constraintName=\"FK_EXTERNAL_APP_USERS_USER_ID\"\n                                 referencedTableName=\"USERS\"\n                                 referencedColumnNames=\"USER_ID\"\n                                 onDelete=\"CASCADE\"/>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v2.8.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"280000\" author=\"ybrigo@gmail.com\">\n        <validCheckSum>7:3c18749b7f0a18dbf28ea1682c821138</validCheckSum>\n        <validCheckSum>7:1f544d2b211d758be8b7cec91502f84b</validCheckSum>\n        <createTable tableName=\"PROCESS_INITIAL_STATE\">\n            <column name=\"INSTANCE_ID\" type=\"uuid\" remarks=\"Unique process ID\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"INSTANCE_CREATED_AT\" type=\"timestamptz\" remarks=\"Timestamp of process creation\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"ITEM_PATH\" type=\"varchar(2048)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"ITEM_DATA\" type=\"longblob\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"IS_ENCRYPTED\" type=\"boolean\" defaultValue=\"false\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"UNIX_MODE\" type=\"number(4)\" defaultValueNumeric=\"420\"> <!-- 420 (hehe) is octal 0644 -->\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/main/resources/com/walmartlabs/concord/server/db/v2.9.0.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"290000\" author=\"benjamin.broadaway@walmart.com\">\n        <addColumn tableName=\"USERS\">\n            <column name=\"IS_PERMANENTLY_DISABLED\" type=\"boolean\" defaultValueComputed=\"false\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n</databaseChangeLog>\n"
  },
  {
    "path": "server/db/src/test/filtered-resources/db.properties",
    "content": "db.image=${db.image}\n"
  },
  {
    "path": "server/db/src/test/java/com/walmartlabs/concord/db/MigrationTest.java",
    "content": "package com.walmartlabs.concord.db;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.Timeout;\nimport org.testcontainers.containers.PostgreSQLContainer;\nimport org.testcontainers.containers.wait.strategy.Wait;\nimport org.testcontainers.utility.DockerImageName;\n\nimport java.time.Duration;\nimport java.util.Base64;\nimport java.util.Map;\nimport java.util.Properties;\nimport java.util.concurrent.TimeUnit;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\npublic class MigrationTest {\n\n    private static String dbImage;\n\n    @BeforeAll\n    public static void setUp() throws Exception {\n        var props = new Properties();\n        props.load(MigrationTest.class.getClassLoader().getResourceAsStream(\"db.properties\"));\n        dbImage = props.getProperty(\"db.image\");\n    }\n\n    @SuppressWarnings(\"resource\")\n    private PostgreSQLContainer<?> createDbContainer() {\n        return new PostgreSQLContainer<>(DockerImageName.parse(dbImage)\n                .asCompatibleSubstituteFor(\"postgres\"))\n                .waitingFor(Wait.forListeningPort());\n    }\n\n    @Test\n    @Timeout(value = 1, unit = TimeUnit.MINUTES)\n    public void regularMigration() {\n        try (var db = createDbContainer()) {\n            db.start();\n            applyMigrations(db);\n        }\n    }\n\n    @Test\n    @Timeout(value = 1, unit = TimeUnit.MINUTES)\n    public void concurrentMigrations() {\n        try (var db = createDbContainer()) {\n            db.start();\n\n            var threads = new Thread[5];\n\n            for (int i = 0; i < threads.length; i++) {\n                threads[i] = new Thread(() -> applyMigrations(db), \"migration#\" + i);\n            }\n\n            for (Thread value : threads) {\n                value.start();\n            }\n\n            for (Thread thread : threads) {\n                try {\n                    thread.join();\n                } catch (InterruptedException e) {\n                    throw new RuntimeException(e);\n                }\n            }\n        }\n    }\n\n    private void applyMigrations(PostgreSQLContainer<?> db) {\n        var cfg = new DatabaseConfigurationImpl(db.getJdbcUrl(), db.getUsername(), db.getPassword());\n        DataSourceUtils.migrateDb(cfg, new MainDBChangeLogProvider());\n    }\n\n    private static String base64(String s) {\n        return Base64.getEncoder().encodeToString(s.getBytes(UTF_8));\n    }\n\n    private static final class DatabaseConfigurationImpl implements DatabaseConfiguration {\n\n        private final String url;\n        private final String username;\n        private final String password;\n\n        private DatabaseConfigurationImpl(String url, String username, String password) {\n            this.url = url;\n            this.username = username;\n            this.password = password;\n        }\n\n        @Override\n        public String url() {\n            return url;\n        }\n\n        @Override\n        public String username() {\n            return username;\n        }\n\n        @Override\n        public String password() {\n            return password;\n        }\n\n        @Override\n        public int maxPoolSize() {\n            return 10;\n        }\n\n        @Override\n        public Duration maxLifetime() {\n            return Duration.ofMinutes(30);\n        }\n\n        @Override\n        public Map<String, Object> changeLogParameters() {\n            return Map.of(\n                    \"createExtensionAvailable\", \"true\",\n                    \"defaultAdminToken\", base64(\"foobar\"),\n                    \"skipAdminTokenGeneration\", \"false\",\n                    \"defaultAgentToken\", base64(\"barbaz\"),\n                    \"skipAgentTokenGeneration\", \"false\",\n                    \"secretStoreSalt\", base64(\"foo\"),\n                    \"serverPassword\", base64(\"bar\"));\n        }\n    }\n}\n"
  },
  {
    "path": "server/db/src/test/java/com/walmartlabs/concord/db/PgIntRangeTest.java",
    "content": "package com.walmartlabs.concord.db;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.Range;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class PgIntRangeTest {\n\n    @Test\n    public void test() throws Exception {\n        String s = \"[123,234)\";\n        Range r = PgIntRange.parse(s);\n        assertEquals(Range.Mode.INCLUSIVE, r.lowerMode());\n        assertEquals(123, r.lower());\n        assertEquals(234, r.upper());\n        assertEquals(Range.Mode.EXCLUSIVE, r.upperMode());\n    }\n}\n"
  },
  {
    "path": "server/db/src/test/resources/logback-test.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] [%-5level] [%X{instanceId:-}] %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "server/dist/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.server</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-server</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server-impl</artifactId>\n        </dependency>\n\n        <!-- plugins -->\n        <dependency>\n            <groupId>com.walmartlabs.concord.server.plugins.ansible</groupId>\n            <artifactId>concord-ansible-plugin</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server.plugins</groupId>\n            <artifactId>concord-oneops-plugin</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server.plugins.noderoster</groupId>\n            <artifactId>concord-noderoster-plugin</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server.plugins</groupId>\n            <artifactId>pfed-sso</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server.plugins</groupId>\n            <artifactId>oidc</artifactId>\n        </dependency>\n\n        <!-- ui -->\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-console2</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server.plugins</groupId>\n            <artifactId>webapp</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>jul-to-slf4j</artifactId>\n        </dependency>\n\n    </dependencies>\n\n    <build>\n        <resources>\n            <resource>\n                <filtering>false</filtering>\n                <directory>${project.basedir}/src/main/resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n            <resource>\n                <filtering>true</filtering>\n                <directory>${project.basedir}/src/main/filtered-resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n        </resources>\n\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-assembly-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>dist</id>\n                        <phase>package</phase>\n                        <goals>\n                            <goal>single</goal>\n                        </goals>\n                        <configuration>\n                            <descriptors>\n                                <descriptor>src/assembly/dist.xml</descriptor>\n                            </descriptors>\n                            <tarLongFileMode>posix</tarLongFileMode>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "server/dist/src/assembly/default.conf",
    "content": "concord-server {\n    db {\n        appPassword = \"q1\"\n        appPassword = ${?DB_PASSWORD}\n\n        inventoryPassword = \"q1\"\n        inventoryPassword = ${?DB_INVENTORY_PASSWORD}\n    }\n\n    secretStore {\n        # base64 of 'example'\n        serverPassword = \"ZXhhbXBsZQ==\"\n        serverPassword = ${?SECRET_STORE_SERVER_PASSWORD}\n\n        secretStoreSalt = \"ZXhhbXBsZQ==\"\n        secretStoreSalt = ${?SECRET_STORE_SALT}\n\n        projectSecretSalt = \"ZXhhbXBsZQ==\"\n        projectSecretSalt = ${?PROJECT_SECRET_SALT}\n    }\n\n    noderoster {\n        db {\n            password = \"q1\"\n            password = ${db.appPassword}\n            password = ${?NODEROSTER_DB_PASSWORD}\n        }\n    }\n}\n"
  },
  {
    "path": "server/dist/src/assembly/dist.xml",
    "content": "<assembly xmlns=\"http://maven.apache.org/ASSEMBLY/2.0.0\"\n          xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n          xsi:schemaLocation=\"http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd\">\n\n    <id>dist</id>\n\n    <formats>\n        <format>tar.gz</format>\n    </formats>\n\n    <includeBaseDirectory>false</includeBaseDirectory>\n\n    <dependencySets>\n        <dependencySet>\n            <outputDirectory>lib</outputDirectory>\n        </dependencySet>\n    </dependencySets>\n\n    <files>\n        <file>\n            <source>src/assembly/start.sh</source>\n            <outputDirectory>.</outputDirectory>\n            <fileMode>755</fileMode>\n        </file>\n        <file>\n            <source>src/assembly/default.conf</source>\n            <outputDirectory>.</outputDirectory>\n        </file>\n    </files>\n</assembly>\n"
  },
  {
    "path": "server/dist/src/assembly/start.sh",
    "content": "#!/bin/bash\n\nBASE_DIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\n\nMAIN_CLASS=\"com.walmartlabs.concord.server.dist.Main\"\nif [[ \"${CONCORD_COMMAND}\" = \"migrateDb\" ]]; then\n    MAIN_CLASS=\"com.walmartlabs.concord.server.MigrateDB\"\nfi\n\nif [[ -z \"${CONCORD_CFG_FILE}\" ]]; then\n    CONCORD_CFG_FILE=\"${BASE_DIR}/default.conf\"\nfi\necho \"CONCORD_CFG_FILE: ${CONCORD_CFG_FILE}\"\n\nGC_LOG_DIR=${GC_LOG_DIR:-\"${BASE_DIR}/logs/gc\"}\nmkdir -p ${GC_LOG_DIR}\necho \"GC logs: ${GC_LOG_DIR}\"\n\nif [[ -z \"${CONCORD_JAVA_OPTS}\" ]]; then\n    CONCORD_JAVA_OPTS=\"-Xms2g -Xmx2g\"\nfi\necho \"CONCORD_JAVA_OPTS: ${CONCORD_JAVA_OPTS}\"\n\nif [[ -z \"${CONCORD_TMP_DIR}\" ]]; then\n    export CONCORD_TMP_DIR=\"/tmp\"\nfi\n\necho \"Using $(which java)\"\njava -version\n\nJAVA_VERSION=$(java -version 2>&1 \\\n  | head -1 \\\n  | cut -d'\"' -f2 \\\n  | sed 's/^1\\.//' \\\n  | cut -d'.' -f1)\n\nJDK_SPECIFIC_OPTS=\"\"\nif (( $JAVA_VERSION > 8 )); then\n  echo \"Applying JDK 9+ specific options...\"\n  JDK_SPECIFIC_OPTS=\"--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED\"\nfi\n\nif [[ -z \"${EXTRA_CLASSPATH}\" ]]; then\n  EXTRA_CLASSPATH=\"\"\nfi\n\necho \"EXTRA_CLASSPATH: ${EXTRA_CLASSPATH}\"\n\nexec java \\\n${CONCORD_JAVA_OPTS} \\\n${JDK_SPECIFIC_OPTS} \\\n-Dfile.encoding=UTF-8 \\\n-Djava.net.preferIPv4Stack=true \\\n-Djava.security.egd=file:/dev/./urandom \\\n-Dconcord.conf=${CONCORD_CFG_FILE} \\\n-cp \"${BASE_DIR}/lib/*:${BASE_DIR}/ext/*:${BASE_DIR}/classes:${EXTRA_CLASSPATH}\" \\\n\"${MAIN_CLASS}\"\n"
  },
  {
    "path": "server/dist/src/main/java/com/walmartlabs/concord/server/dist/Main.java",
    "content": "package com.walmartlabs.concord.server.dist;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.ConcordServer;\nimport com.walmartlabs.concord.server.Version;\nimport org.eclipse.sisu.space.BeanScanning;\nimport org.eclipse.sisu.space.SpaceModule;\nimport org.eclipse.sisu.space.URLClassSpace;\nimport org.eclipse.sisu.wire.WireModule;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.slf4j.bridge.SLF4JBridgeHandler;\n\npublic class Main {\n\n    private static final Logger log = LoggerFactory.getLogger(Main.class);\n\n    public static void main(String[] args) throws Exception {\n        SLF4JBridgeHandler.install();\n\n        Version v = Version.getCurrent();\n        log.info(\"Starting Concord Server ({}, {}, {})...\", v.getVersion(), v.getCommitId(), v.getEnv());\n\n        long t1 = System.currentTimeMillis();\n\n        ConcordServer server = autoWire();\n        Runtime.getRuntime().addShutdownHook(new Thread(() -> {\n            log.info(\"Received SIGTERM, stopping...\");\n            try {\n                server.stop();\n            } catch (Exception e) {\n                log.warn(\"Failed to stop the server: {}\", e.getMessage());\n            }\n        }, \"shutdown-hook\"));\n        server.start();\n\n        long t2 = System.currentTimeMillis();\n        log.info(\"main -> started in {}ms\", (t2 - t1));\n    }\n\n    public static ConcordServer autoWire() throws Exception {\n        // works as a plugin system by automatically wiring all @Named modules and beans in the classpath\n        ClassLoader cl = ConcordServer.class.getClassLoader();\n        return ConcordServer.withModules(new WireModule(new SpaceModule(new URLClassSpace(cl), BeanScanning.GLOBAL_INDEX)));\n    }\n}\n"
  },
  {
    "path": "server/dist/src/main/resources/concord-server.conf",
    "content": "# Default configuration for Concord Server\n#\n# Note, time intervals accept the following formats:\n# \"20000\" (20000 milliseconds)\n# \"20 seconds\"\n# \"20 days\"\n#\n# The biggest unit allowed is \"days\"\n# See also com.typesafe.config.impl.SimpleConfig#parseDuration\n\nconcord-server {\n\n    server {\n        port = 8001\n        port = ${?API_PORT}\n\n        # mark server cookies as \"secure\"\n        # https://en.wikipedia.org/wiki/Secure_cookie\n        secureCookies = false\n        secureCookies = ${?SECURE_COOKIES}\n\n        # make the session cookie use SameSite=Lax policy\n        # see https://github.com/eclipse/jetty.project/issues/4247\n        cookieComment = \"__SAME_SITE_LAX__\"\n\n        # timeout of Jetty sessions\n        sessionTimeout = \"30 minutes\"\n        sessionTimeout = ${?SESSION_TIMEOUT}\n\n        # access log file pattern, e.g. /opt/concord/data/logs/server_yyyy_mm_dd.log\n        accessLogPath = ${?ACCESS_LOG_PATH}\n\n        # number of days to keep the logs\n        accessLogRetainDays = 7\n        accessLogRetainDays = ${?ACCESS_LOG_RETAIN_DAYS}\n\n        # maximum size of request headers, bytes\n        requestHeaderSize = 16384\n        requestHeaderSize = ${?REQUEST_HEADER_SIZE}\n\n        cors {\n            # change for production\n            allowOrigin = \"*\"\n        }\n    }\n\n    # main database connection\n    db {\n        # (optional) JDBC URL of the database\n        url = \"jdbc:postgresql://localhost:5432/postgres\"\n        url = ${?DB_URL}\n\n        # primary database user\n        appUsername = \"postgres\"\n        appUsername = ${?DB_USERNAME}\n\n        # appPassword = \"...\"\n        appPassword= ${?DB_PASSWORD}\n\n        # database user of the inventory system\n        inventoryUsername = \"postgres\"\n        inventoryUsername = ${?DB_INVENTORY_USERNAME}\n\n        # (mandatory)\n        # inventoryPassword = \"...\"\n        inventoryPassword = ${?DB_INVENTORY_PASSWORD}\n\n        # maximum number of connections per database user\n        maxPoolSize = 10\n\n        # maximum lifetime of a connection in the pool\n        maxLifetime = \"5 minutes\"\n\n        # parameters using during the DB schema migration\n        changeLogParameters {\n            # the default admin API token value\n            # must be a valid base64 value\n            #\n            # If empty a new random token will be generated.\n            #\n            # Effective only during the first DB migration\n            # If set the value must never change otherwise\n            # subsequent DB migrations may fail with an invalid checksum\n            defaultAdminToken = \"\"\n\n            # if \"true\" skip the admin token generation on start\n            # effective only during the first DB migration\n            skipAdminTokenGeneration = \"false\"\n\n            # the default agent API token value.\n            # Must be a valid base64 value\n            #\n            # If empty a new random token will be generated.\n            #\n            # Effective only during the first DB migration.\n            # If set the value must never change otherwise\n            # subsequent DB migrations may fail with an invalid checksum\n            defaultAgentToken = \"\"\n\n            # if \"true\" skip the agent token generation on start\n            # effective only during the first DB migration\n            skipAgentTokenGeneration = \"false\"\n\n            # some migrations can be done faster if the DB user has the SUPERUSER role\n            superuserAvailable = \"true\"\n\n            # if \"true\", Concord will try to install required PostgreSQL extensions automatically\n            # requires \"CREATE EXTENSION\" privileges\n            createExtensionAvailable = \"true\"\n\n            secretStoreSalt = ${secretStore.secretStoreSalt}\n            serverPassword = ${secretStore.serverPassword}\n        }\n    }\n\n    console {\n        cfgFile = ${?CONSOLE_CFG_FILE}\n    }\n\n    # \"remember me\" cookie support\n    rememberMe {\n        # max age of the \"remember me\" cookie\n        maxAge = \"14 days\"\n\n        # default value, change for production (base64)\n        # should be a valid AES key (16, 24 or 32 bytes)\n        # if not set, a new random key will be used\n        # cipherKey = \"...\"\n    }\n\n    forms {\n        baseDir = ${?FORM_SERVER_DIR}\n    }\n\n    # email notifications (API key expiration, etc)\n    # not related to notifications send from user flows\n    email {\n        enabled = false\n\n        host = \"localhost\"\n        port = \"25\"\n\n        connectTimeout = \"20 seconds\"\n        readTimeout = \"10 seconds\"\n\n        from = \"noreply@example.com\"\n    }\n\n    # process-related configuration\n    process {\n        # the period between checks for failed or stalled processes\n        # if zero the task is disabled\n        watchdogPeriod = \"3 seconds\"\n\n        # the state cleanup interval\n        # if zero the task is disabled\n        cleanupInterval = \"1 hour\"\n\n        # enable cleanup of the process queue\n        queueCleanup = true\n\n        # enable cleanup of the process state table\n        stateCleanup = true\n\n        # enable cleanup of the process events table\n        eventsCleanup = true\n\n        # enable cleanup of process logs\n        logsCleanup = true\n\n        # enable cleanup of process checkpoints\n        checkpointCleanup = true\n\n        # max age of the process state data (interval)\n        maxStateAge = \"7 days\"\n\n        # max age of failed processes to handle (interval)\n        maxFailureHandlingAge = \"3 days\"\n\n        # max age of stalled processes to handle (interval)\n        maxStalledAge = \"1 minute\"\n\n        # max age of processes which are failed to start (interval)\n        maxStartFailureAge = \"10 minutes\"\n\n        # list of process state files that must be encrypted before storing\n        secureFiles = [\"_main.json\"]\n\n        signingKeyAlgorithm = \"RSA\"\n        signingAlgorithm = \"SHA256withRSA\"\n        # (optional) a key used to sign important process data (such as initiator or currentUser IDs)\n        #signingKeyPath = \"...\"\n\n        # interval between checking for process wait conditions (interval)\n        waitCheckPeriod = \"5 seconds\"\n        waitCheckPollLimit = 1000\n\n        waitProcessLimitForStatusQuery = 5000\n\n        # hard limit for the process log size, bytes\n        # should be less than 2^31\n        logSizeLimit = 1073741824 # 1GB\n\n        # if true then the /api/v1/process/{id}/log endpoint performs additional permission checks\n        # if false all logs are readable by any authenticated user\n        checkLogPermissions = false\n    }\n\n    # process queue configuration\n    queue {\n        # number of threads to handle NEW -> ENQUEUED transition\n        enqueueWorkerCount = 2\n        enqueuePollInterval = \"1 second\"\n\n        # enable batching of NEW processes\n        # if \"true\" then Concord will try to group up processes with\n        # the same git URL to minimize the number of clone/fetch operations\n        enqueueBatchEnabled = false\n        enqueueBatchSize = 50\n\n        # responsible for dispatching ENQUEUED processes to agents\n        dispatcher {\n            # queue poll delay\n            pollDelay = \"2 seconds\"\n            # batch size (rows)\n            batchSize = 10\n        }\n    }\n\n    # agent management configuration\n    agent {\n        # polling delay for new agent commands\n        commandPollDelay = \"2 seconds\"\n\n        # the period between checks for stalled and old agent commands\n        # if zero the task is disabled\n        watchdogPeriod = \"1 minute\"\n\n        # max age of commands in DB (interval)\n        maxCommandAge = \"30 days\"\n\n        # max age of stalled commands to handle (interval)\n        maxStalledAge = \"10 minute\"\n    }\n\n    # audit logging\n    audit {\n        enabled = true\n\n        # the log cleanup interval\n        # if zero the task is disabled\n        cleanupPeriod = \"1 hour\"\n\n        # max age of the audit log data\n        maxLogAge = \"7 days\"\n\n        # max search interval\n        # maxSearchInterval\n    }\n\n    # local git repository cache\n    repositoryCache {\n        # directory to store the local repo cache\n        # created automatically if not specified\n        #cacheDir = \"/tmp/concord/repos\"\n\n        # check if concord.yml is present in the repo\n        concordFileValidationEnabled = false\n\n        # timeout for checkout operations\n        lockTimeout = \"3 minutes\"\n\n        # the allowed concurrency level when pulling Git data\n        lockCount = 256\n\n        # directory to store the local repo cache info\n        # created automatically if not specified\n        #cacheInfoDir = \"/tmp/concord/repos_info\"\n\n        # max cached repo age in\n        maxAge = \"1 day\"\n    }\n\n    # policy cache\n    policyCache {\n        # policy cache reload interval\n        reloadInterval = \"10 minutes\"\n    }\n\n    # external dependencies - templates, `imports`, etc\n    dependencies {\n        # directory to cache dependencies\n        # created automatically if not specified\n        #cacheDir = \"/tmp/concord/deps\"\n    }\n\n    templates {\n        allowScripting = true\n    }\n\n    imports {\n        # base git url for imports\n        src = \"\"\n\n        # list of disabled import processors\n        # e.g. \"dir\" is useful for local development, but potentially a security issue\n        disabledProcessors = [\n            \"dir\"\n        ]\n\n        # default branch for import\n        defaultBranch = \"main\"\n    }\n\n    # secrets and encrypted values\n    secretStore {\n        # the default store definition to use (see below)\n        # case insensitive\n        default = concord\n\n        # maximum allowed size of binary secrets (bytes)\n        maxSecretDataSize = 1048576\n\n        # maximum allowed size of encrypted strings (used in `crypto.decryptString`, bytes)\n        maxEncryptedStringLength = 102400\n\n        # (mandatory), base64 encoded values used to encrypt secrets\n        # serverPassword = \"...\"\n        # secretStoreSalt = \"...\"\n        # projectSecretSalt = \"...\"\n\n        # default DB store\n        concord {\n            enabled = true\n        }\n\n        # key size for the key pairs generated by Concord\n        keySize = 4096\n    }\n\n    # (external) process triggers\n    triggers {\n        # disabling all triggers mean that all events (including repository refresh)\n        # will be disabled\n        disableAll: false\n\n        # the specified event types will be ignored\n        # for example:\n        #   disabled: ['cron', 'github']\n        # will disable cron scheduling and GitHub notifications\n        disabled: []\n\n        # default values for trigger configurations\n        # the values specified in the trigger override the default values specified here\n        defaultConfiguration: {\n        }\n\n        # default values for trigger conditions (including the trigger version value if applicable)\n        # the syntax is\n        #\n        #  triggerTypeA: {\n        #    ...\n        #  }\n        #  triggerTypeB: {\n        #    ...\n        #  }\n        #\n        # \"_\" means any trigger type and will be used as fallback if a specific trigger type\n        # is not defined\n        defaultConditions: {\n            _: {\n                version = 2 # version 1 is deprecated and removed in Concord 1.59.0+\n            }\n        }\n    }\n\n    # API key authentication\n    apiKey {\n        # if disabled the keys are never expire\n        expirationEnabled = false\n\n        # default expiration period\n        expirationPeriod = \"30 days\"\n\n        # how often Concord will send expiration notifications (days)\n        notifyBeforeDays = [1, 3, 7, 15]\n\n        # (optional) load user API keys from the specified file on start\n        loadFrom = ${?CONCORD_API_KEYS_FILE}\n    }\n\n    # AD/LDAP authentication\n    ldap {\n        # AD/LDAP server URL\n        url = \"ldap://oldap:389\"\n        url = ${?LDAP_URL}\n\n        searchBase = \"dc=example,dc=org\"\n\n        # used to find the user's record on auth\n        # {0} - username@domain\n        # {1} - username\n        # {2} - domain\n        principalSearchFilter = \"(cn={0})\"\n\n        # used to find groups (e.g. on the team page in the UI)\n        # {0} is the search input\n        groupSearchFilter = \"(cn=*{0}*)\"\n        groupNameProperty = \"cn\"\n        groupDisplayNameProperty = \"cn\"\n\n        usernameProperty = \"cn\"\n        userPrincipalNameProperty = \"\"\n        mailProperty = \"mail\"\n\n        returningAttributes = []\n\n        # username and password for the initial bind\n        # mandatory\n        systemUsername = \"cn=admin,dc=example,dc=org\"\n        #systemPassword = \"...\"\n\n        # comma-separated list of attributes to expose as the user's data (${initiator.attributes})\n        #exposeAttributes =\n\n        # comma-separated list of attributed to exclude\n        #excludeAttributes =\n\n        # allows creation of new LDAP accounts\n        # admins can still create users via the API\n        autoCreateUsers = true\n\n        # principal cache duration\n        # cacheDuration = \"10 seconds\"\n\n        # interval to wait for establishing LDAP connection\n        connectTimeout = \"30 seconds\"\n\n        # interval to wait for receiving LDAP data\n        readTimeout = \"30 seconds\"\n\n        # ldap DNS Service Record name eg: _ldap._tcp.domain.com, Overrides url with SRV list domain controllers\n        #dnsSRVName = \"_ldap._tcp.domain.com\"\n\n        # if true, accept all SSL certificates for LDAP connections (insecure, for testing only)\n        # if false (default), enforce standard certificate validation\n        trustAllCertificates = false\n    }\n\n    # AD/LDAP group synchronization\n    ldapGroupSync {\n        # interval between runs\n        interval = \"1 day\"\n\n        # the number of users fetched at the time\n        fetchLimit = 100\n\n        # interval for group sync on login (interval)\n        minAgeLogin = \"1 hour\"\n\n        # interval for the automatic group sync (interval)\n        minAgeSync = \"1 day\"\n\n        # interval to delete disabled users (interval)\n        # disabledAge = \"30 days\"\n    }\n\n    # git clone config\n    git {\n        allowedSchemes = [ \"https\", \"ssh\", \"classpath\" ]\n\n        # GitHub auth token to use when cloning repositories without explicitly\n        # configured authentication. Deprecated in favor of systemAuth list of\n        # tokens or service-specific app config (e.g. github)\n        # oauth = \"...\"\n\n        # specific username to use for auth\n        # oauthUsername = \"\"\n\n        # regex to match against git server's hostname + port + path so oauth\n        # token isn't used for and unexpected host\n        # oauthUrlPattern = \"\"\n\n        # List of system-provided auth token configs\n        # {\n        #   type = \"OAUTH_TOKEN\",\n        #   token = \"...\",\n        #   username = \"...\", # optional, username to send with auth token\n        #   urlPattern = \"...\" # required, regex to match against target git host + port + path\n        # }\n        systemAuth = []\n\n        # use GIT's shallow clone\n        shallowClone = true\n\n        # do not execute fetch if the current HEAD is the latest commit ID\n        checkAlreadyFetched = true\n\n        # default timeout duration for any git operation\n        defaultOperationTimeout = \"10 minutes\"\n\n        # fetch timeout duration\n        fetchTimeout = \"10 minutes\"\n\n        # see GIT documentation for GIT_HTTP_LOW_SPEED_LIMIT and GIT_HTTP_LOW_SPEED_TIME\n        # use with caution, can cause performance issues\n        httpLowSpeedLimit = 0\n        httpLowSpeedTime = \"10 minutes\"\n\n        sshTimeoutRetryCount = 1\n        sshTimeout = \"10 minutes\"\n\n        # max bytes to keep from a git cli process output (distinct for stdout and stderr)\n        maxGitCliOutputBytes = 512\n    }\n\n    # GitHub app and webhook integration\n    github {\n        # default value, for testing only\n        secret = \"12345\"\n\n        # if enabled use the payload's 'sender.ldap_dn' to find the initiator\n        useSenderLdapDn = true\n\n        # if enabled, use the payload's 'sender.email' to find the initiator\n        useSenderEmail = false\n\n        # if enabled, stores a mapping of github user id -> concord user id in the DB\n        enableExternalUserIdMappingCache = false\n\n        # Number of webhook sender email lookups to cache\n        senderEmailCacheSize = 1024\n\n        # Duration of time to keep sender email lookups cached\n        senderEmailCacheDuration = \"3 hours\"\n\n        # save external events into the audit log\n        logEvents = true\n\n        # disable concord repos on push event with deleted ref (branch, tag)\n        disableReposOnDeletedRef = false\n\n        # App installation settings. Multiple auth (private key) definitions are supported,\n        # as each is matched to a particular url pattern.\n        appInstallation {\n            # Importantly, urlPattern must include a regex capture group named 'baseUrl'.\n            # This is necessary to detect where the owner/repo values begin in the path.\n            # {\n            #     id = \"my-gh-app\",  # identifier for the config, e.g. for metrics\n            #     type = \"GITHUB_APP_INSTALLATION\",\n            #     urlPattern = \"(?<baseUrl>github.com)\",  # regex\n            #     username = \"...\",  # optional, defaults to \"x-access-token\"\n            #     apiUrl = \"https://api.github.com\", # github API url, usually *not* the same as the repo url host/path\n            #     clientId = \"...\",\n            #     privateKey = \"/path/to/pk.pem\"\n            # }\n            # or static oauth config. Not exactly a \"GitHub App\", but can do some\n            # API interactions and cloning. Actual app installation is preferred.\n            # {\n            #     id = \"my-static-token\",  # identifier for the config, e.g. for metrics\n            #     type = \"OAUTH_TOKEN\",\n            #     token = \"...\",\n            #     username = \"...\",  # optional, usually not necessary\n            #     urlPattern = \"...\" # regex to match against git server's hostname + port + path\n            #     apiUrl = \"https://api.github.com\", # github API url, usually *not* the same as the repo url host/path\n            # }\n            auth = []\n\n            # Timeout duration for http calls from the app\n            httpClientTimeout = \"30 seconds\"\n\n            # Duration of time to keep tokens cached after creation. May be purged\n            # earlier depending on overall cache weight and usage. GitHub installation\n            # tokens expire after 1 hour\n            systemAuthCacheDuration = \"50 minutes\"\n\n            # Max \"weight\" of the token cache. Defaults to ~10MB which should hold\n            # between 3,500 and 10,000 tokens depending on associated secret size.\n            systemAuthCacheMaxWeight = 10240\n        }\n    }\n\n    # Ansible event processor configuration\n    ansibleEvents {\n        # how often the ansible event processing should run (sec)\n        # if zero the task is disabled\n        period = \"10 seconds\"\n\n        # how many records to fetch at the time\n        fetchLimit = 10000\n    }\n\n    # OneOps resource configuration\n    oneops {\n        # save OneOps events into the audit log\n        logEvents = false\n    }\n\n    # external events (/api/v1/event/{eventName} endpoint)\n    externalEvents {\n        # if set the endpoint will require the specified user role\n        # keys are regexes matched with eventNames, values are the required roles\n        # requiredRoles = { }\n\n        # max number of threads to use to process incoming events\n        workerThreads = 5\n\n        # save external events into the audit log\n        logEvents = true\n    }\n\n    # JWT-based SSO service support\n    sso {\n        pfed {\n            enabled = false\n            priority = 0\n\n            bearerToken {\n                # enable bearer tokens\n                enableBearerTokens = false\n\n                # allow all clientIds\n                allowAllClientIds = false\n\n                # list of allowed pingfed clientids for bearer tokens\n                allowedClientIds = [\"clientId1\", \"clientId2\"]\n            }\n        }\n        authEndpointUrl = \"http://auth.example.com/authorize\"\n        tokenEndpointUrl = \"http://auth.example.com/token\"\n        logoutEndpointUrl = \"http://auth.example.com/logout\"\n        redirectUrl = \"http://concord.example.com/api/service/sso/redirect\"\n        tokenSigningKeyUrl = \"http://auth.example.com/pf/JWKS\"\n        userInfoEndpointUrl = \"https://auth.example.com/idp/userinfo.openid\"\n        clientId = \"********\"\n        clientSecret = \"********\"\n        # allows to control auto create users via sso\n        autoCreateUsers = true\n        # JSON as a string\n        #tokenSigningKey = \"{}\"\n        \n        # enable to validate token signature\n        tokenSignatureValidation = false\n\n        # JSON as a string\n        #tokenEncryptionKey = \"{}\"\n\n        tokenServiceReadTimeout = \"5 seconds\"\n        tokenServiceConnectTimeout = \"500 milliseconds\"\n        validateNonce = false\n    }\n\n    # OpenID Connect support\n    oidc {\n        enabled = false\n        clientId = \"********\"\n        secret = \"********\"\n        discoveryUri = \"https://auth.example.com/.well-known/openid-configuration\"\n\n        # should point to the externally accessible FQDN and\n        # match the configured callbacks in the OIDC provider\n        urlBase = \"http://concord.example.com\"\n        afterLoginUrl = \"http://concord.example.com\"\n        afterLogoutUrl = \"http://concord.example.com/#/logout/done\"\n        onErrorUrl = \"http://concord.example.com/#/unauthorized\"\n\n        scopes = [ \"openid\", \"profile\", \"email\", \"groups\"]\n\n        teamMapping = {\n            \"00000000-0000-0000-0000-000000000000\" {\n                source = [\n                    \"groups.dev.*\",\n                    \"groups..*\",\n                ]\n                role = \"MEMBER\"\n            }\n\n#             teamId2 {\n#                 source = [\n#                     \"groups.user.*\",\n#                 ]\n#                 role = \"MEMBER\"\n#             }\n        }\n\n        roleMapping = {\n            \"concordAdmin\" {\n                source = [\n                    \"groups.admin.*\"\n                ]\n            }\n        }\n    }\n\n    # locking configuration\n    locking {\n        # max number of DB (advisory) locks\n        maxAdvisoryLocks = 16\n    }\n\n    # QoS filter configuration\n    qos {\n        maxRequests = -1\n        maxWait = \"50 milliseconds\"\n        suspend = \"1 second\"\n    }\n\n    # noderoster plugin configuration\n    noderoster {\n        db {\n            #url = \"jdbc:postgresql://localhost:5432/postgres\"\n            url = ${db.url}\n            url = ${?NODEROSTER_DB_URL}\n\n            username = \"postgres\"\n            username = ${db.appUsername}\n            username = ${?NODEROSTER_DB_USERNAME}\n\n            # password = \"...\"\n            password = ${db.appPassword}\n            password = ${?NODEROSTER_DB_PASSWORD}\n\n            maxPoolSize = 10\n        }\n\n        events {\n            # how often the ansible event processing should run\n            # if zero the task is disabled\n            period = \"10 seconds\"\n\n            # how many records to fetch at the time\n            fetchLimit = 10000\n\n            # date/time of the first event that should be processed (ISO 8601 timestamp)\n            # if partitioning is used then the value must be in the existing partition's range\n            # startTimestamp = \"2020-01-20T23:59:59.000Z\"\n\n            # Host id cache size. Each element (hostname[String] + id[UUID]) is\n            # up-to 2072 bytes each, but usually in the 200-300 bytes range.\n            # 50,000 entries will use approx 10-15MB of heap or worst-case 98MB\n            # if all hostnames are max-length. A value of 0 disables caching.\n            hostCacheSize = 10000\n\n            # Duration to keep host IDs in cache after last access\n            hostCacheDuration = \"1 hour\"\n        }\n    }\n\n    workerMetrics {\n        # property in worker \"capabilities\" which is used to group up the available workers\n        groupByCapabilitiesProperty = \"flavor\"\n    }\n\n    development {\n    }\n\n    production {\n    }\n}\n"
  },
  {
    "path": "server/impl/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.server</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-server-impl</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server-sdk</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-imports</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-forms</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-dependency-manager</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v1</groupId>\n            <artifactId>concord-runtime-model-v1</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n            <artifactId>concord-runtime-model-v2</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime</groupId>\n            <artifactId>concord-runtime-model</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.runtime</groupId>\n            <artifactId>concord-runtime-loader</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server-db</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-policy-engine</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-repository</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-config</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jsr310</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.uuid</groupId>\n            <artifactId>java-uuid-generator</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.guava</groupId>\n            <artifactId>guava</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.inject</groupId>\n            <artifactId>guice</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.github.mwiede</groupId>\n            <artifactId>jsch</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-github-app-installation</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.takari.bpm</groupId>\n            <artifactId>bpm-engine-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.takari.bpm</groupId>\n            <artifactId>bpm-engine-impl</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.jboss.resteasy</groupId>\n            <artifactId>resteasy-jackson2-provider</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.jaxrs</groupId>\n            <artifactId>jackson-jaxrs-base</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.module</groupId>\n            <artifactId>jackson-module-jaxb-annotations</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.jboss.resteasy</groupId>\n            <artifactId>resteasy-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.jboss.resteasy</groupId>\n            <artifactId>resteasy-jaxb-provider</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>javax.servlet</groupId>\n            <artifactId>javax.servlet-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.shiro</groupId>\n            <artifactId>shiro-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.shiro</groupId>\n            <artifactId>shiro-web</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty</groupId>\n            <artifactId>jetty-server</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty.ee8</groupId>\n            <artifactId>jetty-ee8-servlet</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty</groupId>\n            <artifactId>jetty-util</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty</groupId>\n            <artifactId>jetty-jmx</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty</groupId>\n            <artifactId>jetty-http</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>javax.validation</groupId>\n            <artifactId>validation-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.hibernate.validator</groupId>\n            <artifactId>hibernate-validator</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.sisu</groupId>\n            <artifactId>org.eclipse.sisu.inject</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.jboss.resteasy</groupId>\n            <artifactId>resteasy-multipart-provider</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.jboss.spec.javax.ws.rs</groupId>\n            <artifactId>jboss-jaxrs-api_2.1_spec</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.jooq</groupId>\n            <artifactId>jooq</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>ch.qos.logback</groupId>\n            <artifactId>logback-classic</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.dropwizard.metrics</groupId>\n            <artifactId>metrics-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>aopalliance</groupId>\n            <artifactId>aopalliance</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-compress</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.github.spullara.mustache.java</groupId>\n            <artifactId>compiler</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.dataformat</groupId>\n            <artifactId>jackson-dataformat-yaml</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.cronutils</groupId>\n            <artifactId>cron-utils</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.typesafe</groupId>\n            <artifactId>config</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.sun.mail</groupId>\n            <artifactId>javax.mail</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.swagger.core.v3</groupId>\n            <artifactId>swagger-annotations</artifactId>\n            <version>2.2.15</version>\n        </dependency>\n\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-core</artifactId>\n        </dependency>\n\n        <!-- Immutables -->\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>builder</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-guava</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jdk8</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.errorprone</groupId>\n            <artifactId>error_prone_annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <!-- Prometheus integration -->\n        <dependency>\n            <groupId>io.prometheus</groupId>\n            <artifactId>simpleclient</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.prometheus</groupId>\n            <artifactId>simpleclient_dropwizard</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.prometheus</groupId>\n            <artifactId>simpleclient_servlet</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.prometheus</groupId>\n            <artifactId>simpleclient_hotspot</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.javers</groupId>\n            <artifactId>javers-core</artifactId>\n        </dependency>\n\n        <!-- websockets -->\n        <dependency>\n            <groupId>org.eclipse.jetty.websocket</groupId>\n            <artifactId>jetty-websocket-jetty-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty.websocket</groupId>\n            <artifactId>jetty-websocket-jetty-server</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty.ee8.websocket</groupId>\n            <artifactId>jetty-ee8-websocket-jetty-server</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty.ee8.websocket</groupId>\n            <artifactId>jetty-ee8-websocket-javax-server</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-queue-client</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.graalvm.sdk</groupId>\n            <artifactId>graal-sdk</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.graalvm.js</groupId>\n            <artifactId>js</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.graalvm.js</groupId>\n            <artifactId>js-scriptengine</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-core</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-junit-jupiter</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <resources>\n            <resource>\n                <filtering>false</filtering>\n                <directory>${project.basedir}/src/main/resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n            <resource>\n                <filtering>true</filtering>\n                <directory>${project.basedir}/src/main/filtered-resources</directory>\n                <includes>\n                    <include>**/*</include>\n                </includes>\n            </resource>\n        </resources>\n\n        <plugins>\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>cobertura-maven-plugin</artifactId>\n            </plugin>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n            <plugin>\n                <groupId>io.swagger.core.v3</groupId>\n                <artifactId>swagger-maven-plugin</artifactId>\n                <configuration>\n                    <outputFileName>swagger</outputFileName>\n                    <outputPath>${project.build.directory}/classes/com/walmartlabs/concord/server/swagger</outputPath>\n                    <outputFormat>YAML</outputFormat>\n                    <readAllResources>false</readAllResources>\n                    <resourcePackages>\n                        <package>com.walmartlabs.concord.server</package>\n                    </resourcePackages>\n                    <configurationFilePath>${project.build.directory}/classes/openapi-server-config.yaml\n                    </configurationFilePath>\n                </configuration>\n                <executions>\n                    <execution>\n                        <phase>compile</phase>\n                        <goals>\n                            <goal>resolve</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>pl.project13.maven</groupId>\n                <artifactId>git-commit-id-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>get-commit-id</id>\n                        <goals>\n                            <goal>revision</goal>\n                        </goals>\n                        <phase>generate-resources</phase>\n                    </execution>\n                </executions>\n                <configuration>\n                    <verbose>false</verbose>\n                    <dotGitDirectory>${project.basedir}/../../.git</dotGitDirectory>\n                    <useNativeGit>true</useNativeGit>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "server/impl/src/main/filtered-resources/com/walmartlabs/concord/server/version.properties",
    "content": "version=${project.version}\ncommitId=${git.commit.id}"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/AgentWorkerUtils.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.server.agent.AgentWorkerEntry;\n\nimport java.util.Collection;\nimport java.util.Map;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\npublic final class AgentWorkerUtils {\n\n    public static Map<Object, Long> groupBy(Collection<AgentWorkerEntry> data, String[] path) {\n        return data.stream()\n                .map(e -> ConfigurationUtils.get(e.capabilities(), path))\n                .map(e -> e != null ? e : \"n/a\")\n                .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));\n    }\n\n    private AgentWorkerUtils() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/ApiEntity.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Target({ElementType.TYPE})\n@Retention(RetentionPolicy.CLASS)\n@Value.Style(throwForInvalidImmutableState = ApiEntityValidationException.class)\npublic @interface ApiEntity {\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/ApiEntityValidationException.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\n\npublic class ApiEntityValidationException extends ValidationErrorsException {\n\n    private static final long serialVersionUID = 1L;\n\n    public ApiEntityValidationException(String message) {\n        super(buildMessage(message));\n    }\n\n    public static String buildMessage(String message) {\n        int start = message.indexOf('[');\n        if (start < 0) {\n            return message;\n        }\n        int end = message.lastIndexOf(']');\n        if (end < 0) {\n            return message;\n        }\n        String attrs = message.substring(start, end + 1);\n        return \"some of required attributes are not set: \" + attrs;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/ApiServerModule.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.walmartlabs.concord.server.boot.*;\nimport com.walmartlabs.concord.server.boot.filters.*;\nimport com.walmartlabs.concord.server.boot.resteasy.ResteasyModule;\nimport com.walmartlabs.concord.server.boot.servlets.FormServletHolder;\nimport com.walmartlabs.concord.server.boot.statics.StaticResourcesConfigurator;\nimport com.walmartlabs.concord.server.boot.validation.ValidationModule;\nimport com.walmartlabs.concord.server.agent.websocket.ConcordWebSocketServlet;\nimport org.apache.shiro.mgt.SecurityManager;\nimport org.apache.shiro.web.mgt.WebSecurityManager;\nimport org.eclipse.jetty.ee8.servlet.FilterHolder;\n\nimport javax.servlet.ServletContextListener;\nimport javax.servlet.http.HttpServlet;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\nimport static com.walmartlabs.concord.server.Utils.*;\n\npublic class ApiServerModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        // Jetty\n\n        binder.bind(HttpServer.class).in(SINGLETON);\n\n        // Filter\n\n        bindServletFilter(binder, ConcordAuthenticatingFilter.class);\n        bindServletFilter(binder, CORSFilter.class);\n        bindServletFilter(binder, NoCacheFilter.class);\n        bindServletFilter(binder, QoSFilter.class);\n        bindServletFilter(binder, RequestContextFilter.class);\n\n        // FilterHolder\n\n        newSetBinder(binder, FilterHolder.class).addBinding().to(ShiroFilterHolder.class).in(SINGLETON);\n\n        // ServletHolder\n\n        bindServletHolder(binder, FormServletHolder.class);\n\n        // RequestErrorHandler\n\n        newSetBinder(binder, RequestErrorHandler.class).addBinding().to(FormRequestErrorHandler.class);\n        newSetBinder(binder, RequestErrorHandler.class).addBinding().to(GenericRequestErrorHandler.class);\n\n        // ContextHandlerConfigurator\n\n        newSetBinder(binder, ContextHandlerConfigurator.class).addBinding().to(StaticResourcesConfigurator.class);\n\n        // Shiro\n\n        newSetBinder(binder, ServletContextListener.class).addBinding().to(ShiroListener.class).in(SINGLETON);\n        newSetBinder(binder, FilterChainConfigurator.class).addBinding().to(ConcordFilterChainConfigurator.class).in(SINGLETON);\n\n        binder.bind(ConcordSecurityManager.class).in(SINGLETON);\n        binder.bind(SecurityManager.class).to(ConcordSecurityManager.class);\n        binder.bind(WebSecurityManager.class).to(ConcordSecurityManager.class);\n\n        binder.install(new ValidationModule());\n\n        // Resteasy\n\n        binder.install(new ResteasyModule());\n\n        // JAX-RS resources\n\n        bindJaxRsResource(binder, ServerResource.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/CommandType.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic enum CommandType {\n\n    CANCEL_JOB\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/ConcordObjectMapper.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.jooq.JSONB;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.util.Map;\nimport java.util.regex.Pattern;\n\npublic class ConcordObjectMapper {\n\n    public static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<Map<String, Object>>() {\n    };\n\n    private static final Pattern CONTROL_CHARS = Pattern.compile(\"((?<!\\\\\\\\)\\\\\\\\u0000|[\\\\x00-\\\\x1F])\");\n\n    private final ObjectMapper delegate;\n\n    @Inject\n    public ConcordObjectMapper(ObjectMapper objectMapper) {\n        this.delegate = objectMapper;\n    }\n\n    public JSONB toJSONB(Object m) {\n        if (m == null) {\n            return null;\n        }\n\n        return JSONB.valueOf(removeUnsupportedEscape(toString(m, null)));\n    }\n\n    public <T> JSONB toJSONB(T m, TypeReference<T> type) {\n        if (m == null) {\n            return null;\n        }\n\n        return JSONB.valueOf(removeUnsupportedEscape(toString(m, type)));\n    }\n\n    public JSONB jsonStringToJSONB(String json) {\n        if (json == null) {\n            return null;\n        }\n\n        return JSONB.valueOf(removeUnsupportedEscape(json));\n    }\n\n    public String toString(Object m) {\n        return toString(m, null);\n    }\n\n    public <T> String toString(T m, TypeReference<T> type) {\n        if (m == null) {\n            return null;\n        }\n\n        try {\n            if (type == null) {\n                return delegate.writeValueAsString(m);\n            } else {\n                return delegate.writerFor(type).writeValueAsString(m);\n            }\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public Map<String, Object> fromJSONB(JSONB o) {\n        return fromJSONB(o, MAP_TYPE);\n    }\n\n    public <T> T fromJSONB(JSONB o, TypeReference<T> valueTypeRef) {\n        if (o == null) {\n            return null;\n        }\n\n        return deserialize(o.toString(), valueTypeRef);\n    }\n\n    public <T> T fromJSONB(JSONB o, Class<T> valueType) {\n        if (o == null) {\n            return null;\n        }\n\n        return deserialize(o.toString(), valueType);\n    }\n\n    public <T> T fromString(String s, Class<T> valueType) {\n        return deserialize(s, valueType);\n    }\n\n    private static String removeUnsupportedEscape(String str) {\n        return CONTROL_CHARS.matcher(str).replaceAll(\"\");\n    }\n\n    private <T> T deserialize(String o, TypeReference<T> valueTypeRef) {\n        if (o == null) {\n            return null;\n        }\n\n        try {\n            return delegate.readValue(o, valueTypeRef);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private <T> T deserialize(Object o, Class<T> valueType) {\n        if (o == null) {\n            return null;\n        }\n\n        try {\n            return delegate.readValue(String.valueOf(o), valueType);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public Map<String, Object> convertToMap(Object o) {\n        return delegate.convertValue(o, MAP_TYPE);\n    }\n\n    public <T> T convertValue(Object o, TypeReference<T> valueTypeRef) {\n        return delegate.convertValue(o, valueTypeRef);\n    }\n\n    public <T> T convertValue(Object o, Class<T> toValueType) {\n        return delegate.convertValue(o, toValueType);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/ConcordServer.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Guice;\nimport com.google.inject.Injector;\nimport com.google.inject.Module;\nimport com.walmartlabs.concord.server.boot.BackgroundTasks;\nimport com.walmartlabs.concord.server.boot.HttpServer;\nimport com.walmartlabs.concord.server.message.MessageChannelManager;\n\nimport javax.inject.Inject;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\n\npublic final class ConcordServer {\n\n    @Inject\n    private Injector injector;\n\n    @Inject\n    private BackgroundTasks tasks;\n\n    @Inject\n    private HttpServer server;\n\n    @Inject\n    private MessageChannelManager messageChannelManager;\n\n    private final Lock controlMutex = new ReentrantLock();\n\n    public static ConcordServer withModules(Module... modules) throws Exception {\n        return withModules(List.of(modules));\n    }\n\n    /**\n     * Start ConcordServer using the provided modules.\n     */\n    public static ConcordServer withModules(Collection<Module> modules) throws Exception {\n        Injector injector = Guice.createInjector(modules);\n        ConcordServer instance = new ConcordServer();\n        injector.injectMembers(instance);\n        return instance;\n    }\n\n    public ConcordServer start() throws Exception {\n        controlMutex.lock();\n        try {\n            tasks.start();\n            server.start();\n        } finally {\n            controlMutex.unlock();\n        }\n        return this;\n    }\n\n    public void stop() throws Exception {\n        controlMutex.lock();\n        try {\n            if (messageChannelManager != null) {\n                messageChannelManager.shutdown();\n                messageChannelManager = null;\n            }\n\n            if (server != null) {\n                server.stop();\n                server = null;\n            }\n\n            if (tasks != null) {\n                tasks.stop();\n                tasks = null;\n            }\n        } finally {\n            controlMutex.unlock();\n        }\n    }\n\n    public Injector getInjector() {\n        return injector;\n    }\n\n    private ConcordServer() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/ConcordServerModule.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.typesafe.config.Config;\nimport com.walmartlabs.concord.common.AuthTokenProvider;\nimport com.walmartlabs.concord.common.ObjectMapperProvider;\nimport com.walmartlabs.concord.config.ConfigModule;\nimport com.walmartlabs.concord.db.DatabaseModule;\nimport com.walmartlabs.concord.dependencymanager.DependencyManagerConfiguration;\nimport com.walmartlabs.concord.server.agent.AgentModule;\nimport com.walmartlabs.concord.server.agent.websocket.WebSocketModule;\nimport com.walmartlabs.concord.server.audit.AuditLogModule;\nimport com.walmartlabs.concord.server.boot.BackgroundTasks;\nimport com.walmartlabs.concord.server.cfg.ConfigurationModule;\nimport com.walmartlabs.concord.server.cfg.DatabaseConfigurationModule;\nimport com.walmartlabs.concord.server.console.ConsoleModule;\nimport com.walmartlabs.concord.server.events.EventModule;\nimport com.walmartlabs.concord.server.message.MessageChannelManager;\nimport com.walmartlabs.concord.server.metrics.MetricModule;\nimport com.walmartlabs.concord.server.org.OrganizationModule;\nimport com.walmartlabs.concord.server.policy.PolicyModule;\nimport com.walmartlabs.concord.server.process.ProcessModule;\nimport com.walmartlabs.concord.server.repository.RepositoryModule;\nimport com.walmartlabs.concord.server.repository.ServerAuthTokenProvider;\nimport com.walmartlabs.concord.server.role.RoleModule;\nimport com.walmartlabs.concord.server.security.SecurityModule;\nimport com.walmartlabs.concord.server.security.apikey.ApiKeyModule;\nimport com.walmartlabs.concord.server.task.TaskSchedulerModule;\nimport com.walmartlabs.concord.server.template.TemplateModule;\nimport com.walmartlabs.concord.server.user.UserModule;\n\nimport javax.inject.Named;\nimport java.security.SecureRandom;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.walmartlabs.concord.server.Utils.bindJaxRsResource;\n\n/**\n * Main module to run Concord Server with all features enabled.\n */\n@Named\npublic class ConcordServerModule implements Module {\n\n    private final Config config;\n\n    public ConcordServerModule() {\n        this(loadDefaultConfig());\n    }\n\n    public ConcordServerModule(Config config) {\n        this.config = config;\n    }\n\n    @Override\n    public void configure(Binder binder) {\n        binder.bind(UuidGenerator.class).in(SINGLETON);\n        binder.bind(ObjectMapper.class).toProvider(ObjectMapperProvider.class);\n        binder.bind(ConcordObjectMapper.class).in(SINGLETON);\n\n        binder.install(new ConfigurationModule(config));\n        binder.install(new MetricModule());\n\n        binder.install(new DatabaseConfigurationModule());\n        binder.install(new DatabaseModule());\n\n        binder.install(new TaskSchedulerModule());\n        binder.bind(BackgroundTasks.class).in(SINGLETON);\n\n        binder.bind(Listeners.class).in(SINGLETON);\n        binder.bind(SecureRandom.class).toProvider(SecureRandomProvider.class);\n\n        binder.bind(AuthTokenProvider.class).to(ServerAuthTokenProvider.class).in(SINGLETON);\n\n        binder.bind(MessageChannelManager.class).in(SINGLETON);\n\n        binder.bind(DependencyManagerConfiguration.class).toProvider(DependencyManagerConfigurationProvider.class);\n\n        binder.install(new ApiServerModule());\n        binder.install(new AgentModule());\n        binder.install(new ApiKeyModule());\n        binder.install(new AuditLogModule());\n        binder.install(new ConsoleModule());\n        binder.install(new EventModule());\n        binder.install(new OrganizationModule());\n        binder.install(new PolicyModule());\n        binder.install(new ProcessModule());\n        binder.install(new RepositoryModule());\n        binder.install(new RoleModule());\n        binder.install(new SecurityModule());\n        binder.install(new TemplateModule());\n        binder.install(new UserModule());\n        binder.install(new WebSocketModule());\n\n        bindJaxRsResource(binder, ServerResource.class);\n    }\n\n    private static Config loadDefaultConfig() {\n        return ConfigModule.load(\"concord-server\");\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/DependencyManagerConfigurationProvider.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.dependencymanager.DependencyManagerConfiguration;\nimport com.walmartlabs.concord.server.cfg.DependenciesConfiguration;\n\nimport javax.inject.Inject;\nimport javax.inject.Provider;\nimport javax.inject.Singleton;\n\n@Singleton\npublic class DependencyManagerConfigurationProvider implements Provider<DependencyManagerConfiguration> {\n\n    private final DependenciesConfiguration cfg;\n\n    @Inject\n    public DependencyManagerConfigurationProvider(DependenciesConfiguration cfg) {\n        this.cfg = cfg;\n    }\n\n    @Override\n    public DependencyManagerConfiguration get() {\n        return DependencyManagerConfiguration.of(cfg.getCacheDir());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/GenericOperationResult.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.io.Serializable;\n\npublic class GenericOperationResult implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n    private final OperationResult result;\n\n    @JsonCreator\n    public GenericOperationResult(@JsonProperty(\"result\") OperationResult result) {\n        this.result = result;\n    }\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    public OperationResult getResult() {\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"GenericOperationResult{\" +\n                \"ok=\" + ok +\n                \", result=\" + result +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/GenericRequestErrorHandler.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.boot.RequestErrorHandler;\nimport com.walmartlabs.concord.server.console.ResponseTemplates;\nimport org.apache.http.HttpHeaders;\nimport org.eclipse.jetty.http.MimeTypes;\n\nimport javax.inject.Inject;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Map;\n\npublic class GenericRequestErrorHandler implements RequestErrorHandler {\n\n    private final ResponseTemplates responseTemplates;\n\n    @Inject\n    public GenericRequestErrorHandler(ResponseTemplates responseTemplates) {\n        this.responseTemplates = responseTemplates;\n    }\n\n    @Override\n    public boolean handle(HttpServletRequest request, HttpServletResponse response) throws IOException {\n        Map<String, Object> args = Map.of(\"statusCode\", response.getStatus());\n\n        OutputStream out = response.getOutputStream();\n        response.setCharacterEncoding(StandardCharsets.UTF_8.name());\n        String accept = request.getHeader(HttpHeaders.ACCEPT);\n\n        if (accept != null && accept.toLowerCase().startsWith(MimeTypes.Type.APPLICATION_JSON.asString())) {\n            response.setContentType(MimeTypes.Type.APPLICATION_JSON_UTF_8.asString());\n            out.write(\"{\\\"error\\\": \\\"An unexpected error occurred.\\\"}\".getBytes(StandardCharsets.UTF_8));\n        } else {\n            responseTemplates.genericError(out, args);\n            response.setContentType(MimeTypes.Type.TEXT_HTML.asString());\n        }\n\n        return true;\n    }\n\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/HttpUtils.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport javax.ws.rs.core.Response;\n\npublic final class HttpUtils {\n\n    @Value.Immutable\n    public interface Range {\n\n        @Nullable\n        Integer start();\n\n        @Nullable\n        Integer end();\n\n        static ImmutableRange.Builder builder() {\n            return ImmutableRange.builder();\n        }\n    }\n\n    public static Range parseRangeHeaderValue(String range) {\n        if (range == null || range.trim().isEmpty()) {\n            return Range.builder().build();\n        }\n\n        if (!range.startsWith(\"bytes=\")) {\n            throw new ConcordApplicationException(\"Invalid 'range' header: \" + range, Response.Status.BAD_REQUEST);\n        }\n\n        ImmutableRange.Builder builder = Range.builder();\n        String[] as = range.substring(\"bytes=\".length()).split(\"-\");\n        if (as.length > 0) {\n            try {\n                builder.start(Integer.parseInt(as[0]));\n            } catch (NumberFormatException ignored) {\n            }\n        }\n\n        if (as.length > 1) {\n            try {\n                builder.end(Integer.parseInt(as[1]));\n            } catch (NumberFormatException ignored) {\n            }\n        }\n        return builder.build();\n    }\n\n    private HttpUtils() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/Listeners.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.audit.AuditEvent;\nimport com.walmartlabs.concord.server.sdk.audit.AuditLogListener;\nimport com.walmartlabs.concord.server.sdk.events.ProcessEvent;\nimport com.walmartlabs.concord.server.sdk.events.ProcessEventListener;\nimport com.walmartlabs.concord.server.sdk.log.ProcessLogEntry;\nimport com.walmartlabs.concord.server.sdk.log.ProcessLogListener;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.concurrent.*;\n\npublic class Listeners {\n\n    private static final Logger log = LoggerFactory.getLogger(Listeners.class);\n\n    private static final int MAX_LISTENER_THREADS = 16;\n\n    private static final Duration MAX_LISTENER_TIME = Duration.ofSeconds(3);\n\n    private final Set<ProcessEventListener> eventListeners;\n    private final Set<ProcessLogListener> logListeners;\n    private final Set<AuditLogListener> auditLogListeners;\n\n    private final ForkJoinPool eventListenerPool;\n    private final ForkJoinPool logListenerPool;\n    private final ForkJoinPool auditLogListenerPool;\n\n    @Inject\n    public Listeners(Set<ProcessEventListener> eventListeners,\n                     Set<ProcessLogListener> logListeners,\n                     Set<AuditLogListener> auditLogListeners) {\n\n        this.eventListeners = eventListeners;\n        eventListeners.forEach(l -> log.info(\"Using process event listener: {}\", l));\n\n        this.logListeners = logListeners;\n        logListeners.forEach(l -> log.info(\"Using process log listener: {}\", l));\n\n        this.auditLogListeners = auditLogListeners;\n        auditLogListeners.forEach(l -> log.info(\"Using audit log listener: {}\", l));\n\n        this.eventListenerPool = new ForkJoinPool(MAX_LISTENER_THREADS);\n        this.logListenerPool = new ForkJoinPool(MAX_LISTENER_THREADS);\n        this.auditLogListenerPool = new ForkJoinPool(MAX_LISTENER_THREADS);\n    }\n\n    @WithTimer\n    public void onProcessEvent(List<ProcessEvent> events) {\n        ForkJoinTask<?> task = eventListenerPool.submit(() -> {\n            eventListeners.parallelStream().forEach(l -> l.onEvents(events));\n        });\n\n        waitFor(task);\n    }\n\n    @WithTimer\n    public void onProcessLogAppend(ProcessLogEntry entry) {\n        ForkJoinTask<?> task = logListenerPool.submit(() -> {\n            logListeners.parallelStream().forEach(l -> l.onAppend(entry));\n        });\n\n        waitFor(task);\n    }\n\n    @WithTimer\n    public void onAuditEvent(AuditEvent event) {\n        ForkJoinTask<?> task = auditLogListenerPool.submit(() -> {\n            auditLogListeners.parallelStream().forEach(l -> l.onEvent(event));\n        });\n\n        waitFor(task);\n    }\n\n    private static void waitFor(ForkJoinTask<?> task) {\n        try {\n            task.get(MAX_LISTENER_TIME.toMillis(), TimeUnit.MILLISECONDS);\n        } catch (InterruptedException | ExecutionException | TimeoutException e) {\n            task.cancel(true); // forcefully cancel the underlying task on exception\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/Locks.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.hash.HashCode;\nimport com.google.common.hash.Hashing;\nimport com.walmartlabs.concord.server.cfg.LockingConfiguration;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport org.jooq.DSLContext;\n\nimport javax.inject.Inject;\nimport java.sql.CallableStatement;\n\n/**\n * Locking mechanism based on DB (advisory) locks\n */\npublic class Locks {\n\n    private static final String LOCK_SQL = \"{ call pg_advisory_xact_lock(?) }\";\n\n    private final LockingConfiguration cfg;\n\n    @Inject\n    public Locks(LockingConfiguration cfg) {\n        this.cfg = cfg;\n    }\n\n    public void lock(DSLContext tx, String key) {\n        lock(tx, hash(key));\n    }\n\n    @WithTimer\n    public void lock(DSLContext tx, long key) {\n        tx.connection(conn -> {\n            try (CallableStatement cs = conn.prepareCall(LOCK_SQL)) {\n                cs.setLong(1, key);\n                cs.execute();\n            }\n        });\n    }\n\n    private long hash(String key) {\n        HashCode hc = HashCode.fromBytes(key.getBytes());\n        return Hashing.consistentHash(hc, cfg.getMaxAdvisoryLocks());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/MigrateDB.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Guice;\nimport com.google.inject.Injector;\nimport com.typesafe.config.Config;\nimport com.walmartlabs.concord.config.ConfigModule;\nimport com.walmartlabs.concord.db.DatabaseModule;\nimport com.walmartlabs.concord.db.MainDB;\nimport org.eclipse.sisu.space.BeanScanning;\nimport org.eclipse.sisu.space.SpaceModule;\nimport org.eclipse.sisu.space.URLClassSpace;\nimport org.eclipse.sisu.wire.WireModule;\n\nimport javax.inject.Inject;\nimport javax.sql.DataSource;\nimport java.sql.Connection;\nimport java.sql.Statement;\n\npublic class MigrateDB {\n\n    @Inject\n    @MainDB\n    private DataSource dataSource;\n\n    public static void main(String[] args) throws Exception {\n        Config cfg = ConfigModule.load(\"concord-server\");\n\n        Injector injector = Guice.createInjector(\n                new WireModule(\n                        new SpaceModule(new URLClassSpace(MigrateDB.class.getClassLoader()), BeanScanning.CACHE),\n                        new ConfigModule(\"com.walmartlabs.concord.server\", cfg),\n                        new DatabaseModule()));\n\n        new MigrateDB().run(injector);\n    }\n\n    public void run(Injector injector) throws Exception {\n        injector.injectMembers(this);\n\n        try (Connection conn = dataSource.getConnection();\n             Statement st = conn.createStatement()) {\n            st.execute(\"select 1\");\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/MultipartUtils.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport org.jboss.resteasy.plugins.providers.multipart.InputPart;\nimport org.jboss.resteasy.plugins.providers.multipart.MultipartInput;\n\nimport javax.ws.rs.core.HttpHeaders;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.MultivaluedMap;\nimport javax.ws.rs.core.Response;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.*;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic final class MultipartUtils {\n\n    private static final Pattern PART_NAME_PATTERN = Pattern.compile(\"name=\\\"(.*)\\\"\");\n    private static final String JSON_FIELD_SUFFIX = \"/jsonField\";\n\n    private static final ObjectMapper objectMapper = new ObjectMapper();\n\n    public static Map<String, Object> toMap(MultipartInput input) {\n        Map<String, Object> result = new HashMap<>();\n\n        try {\n            for (InputPart p : input.getParts()) {\n                String name = MultipartUtils.extractName(p);\n                if (name == null || name.startsWith(\"/\") || name.contains(\"..\")) {\n                    throw new ConcordApplicationException(\"Invalid attachment name: \" + name);\n                }\n\n                if (name.endsWith(JSON_FIELD_SUFFIX)) {\n                    name = name.substring(0, name.length() - JSON_FIELD_SUFFIX.length());\n                    Object v = objectMapper.readValue(p.getBodyAsString(), Object.class);\n                    result.put(name, v);\n                } else if (p.getMediaType().isCompatible(MediaType.TEXT_PLAIN_TYPE)) {\n                    String currentValue = p.getBodyAsString().trim();\n                    result.compute(name, (k, oldValue) -> computeStringMultipartEntry(oldValue, currentValue));\n                } else {\n                    result.put(name, p.getBody(InputStream.class, null));\n                }\n            }\n            return result;\n        } catch (IOException e) {\n            throw new ConcordApplicationException(\"Error parsing the request\", e);\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Object computeStringMultipartEntry(Object oldValue, String currentValue) {\n        if (Objects.isNull(oldValue)) {\n            return currentValue;\n        } else if (oldValue instanceof String) {\n            return new ArrayList<>(Arrays.asList((String) oldValue, currentValue));\n        } else if (oldValue instanceof List) {\n            List<String> out = (ArrayList) oldValue;\n            out.add(currentValue);\n            return out;\n        }\n\n        return null;\n    }\n\n    public static String extractName(InputPart p) {\n        MultivaluedMap<String, String> headers = p.getHeaders();\n        if (headers == null) {\n            return null;\n        }\n\n        String h = headers.getFirst(HttpHeaders.CONTENT_DISPOSITION);\n        if (h == null) {\n            return null;\n        }\n\n        String[] as = h.split(\";\");\n        for (String s : as) {\n            Matcher m = PART_NAME_PATTERN.matcher(s.trim());\n            if (m.matches()) {\n                return m.group(1);\n            }\n        }\n\n        return null;\n    }\n\n    public static boolean contains(MultipartInput input, String key) {\n        for (InputPart p : input.getParts()) {\n            String name = MultipartUtils.extractName(p);\n            if (key.equals(name)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public static String getString(MultipartInput input, String key) {\n        try {\n            for (InputPart p : input.getParts()) {\n                String n = MultipartUtils.extractName(p);\n                if (key.equalsIgnoreCase(n)) {\n                    String result = p.getBodyAsString().trim();\n                    if (result.isEmpty()) {\n                        return null;\n                    } else {\n                        return result;\n                    }\n                }\n            }\n        } catch (IOException e) {\n            throw new ConcordApplicationException(\"Error parsing the request\", e);\n        }\n        return null;\n    }\n\n    public static String assertString(MultipartInput input, String key) {\n        String result = getString(input, key);\n        if (result != null) {\n            return result;\n        }\n\n        throw new ConcordApplicationException(key + \" not specified\", Response.Status.BAD_REQUEST);\n    }\n\n    public static boolean getBoolean(MultipartInput input, String key, boolean defaultValue) {\n        String s = getString(input, key);\n        if (s == null) {\n            return defaultValue;\n        }\n        return Boolean.parseBoolean(s);\n    }\n\n    public static UUID getUuid(MultipartInput input, String key) {\n        String s = getString(input, key);\n        if (s == null) {\n            return null;\n        }\n        try {\n            return UUID.fromString(s);\n        } catch (IllegalArgumentException e) {\n            throw new ConcordApplicationException(\"Error parsing the request\", e);\n        }\n    }\n\n    public static UUID assertUuid(MultipartInput input, String key) {\n        UUID result = getUuid(input, key);\n        if (result != null) {\n            return result;\n        }\n        throw new ConcordApplicationException(key + \" not specified\", Response.Status.BAD_REQUEST);\n    }\n\n    public static Integer getInt(MultipartInput input, String key) {\n        String s = getString(input, key);\n        if (s == null) {\n            return null;\n        }\n        return Integer.parseInt(s);\n    }\n\n    public static InputStream getStream(MultipartInput input, String key) {\n        try {\n            for (InputPart p : input.getParts()) {\n                String name = MultipartUtils.extractName(p);\n                if (key.equals(name)) {\n                    return p.getBody(InputStream.class, null);\n                }\n            }\n        } catch (IOException e) {\n            throw new ConcordApplicationException(\"Error parsing the request\", e);\n        }\n        return null;\n    }\n\n    public static InputStream assertStream(MultipartInput input, String key) {\n        InputStream result = getStream(input, key);\n        if (result != null) {\n            return result;\n        }\n        throw new ConcordApplicationException(key + \" not specified\", Response.Status.BAD_REQUEST);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static Map<String, Object> getMap(MultipartInput input, String key) {\n        try {\n            for (InputPart p : input.getParts()) {\n                String n = MultipartUtils.extractName(p);\n                if (key.equalsIgnoreCase(n)) {\n                    String v = p.getBodyAsString().trim();\n                    if (v.isEmpty()) {\n                        return Collections.emptyMap();\n                    } else {\n                        return objectMapper.readValue(v, Map.class);\n                    }\n                }\n            }\n        } catch (IOException e) {\n            throw new ConcordApplicationException(\"Error parsing the request\", e);\n        }\n        return null;\n    }\n\n    public static List<String> getStringList(MultipartInput input, String key) {\n        String projectString = getString(input,key);\n        if(projectString == null) {\n            return new ArrayList<>();\n        }\n        try {\n            String[] projects = projectString.split(\",\");\n            return Stream.of(projects).map(String::trim).collect(Collectors.toList());\n        } catch (IllegalArgumentException e) {\n            throw new ConcordApplicationException(\"Error parsing the request\", e);\n        }\n    }\n    public static List<UUID> getUUIDList(MultipartInput input, String key) {\n        List<String> result = getStringList(input, key);\n        try {\n            return result.stream().map(UUID::fromString).collect(Collectors.toList());\n        } catch (IllegalArgumentException e) {\n            throw new ConcordApplicationException(\"Error parsing the request\", e);\n        }\n    }\n\n    private MultipartUtils() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/OffsetDateTimeParam.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.DateTimeUtils;\n\nimport java.io.Serializable;\nimport java.time.OffsetDateTime;\n\npublic class OffsetDateTimeParam implements Serializable, WrappedValue<OffsetDateTime> {\n\n    private static final long serialVersionUID = 1L;\n\n    public static OffsetDateTimeParam valueOf(String s) {\n        OffsetDateTime value = DateTimeUtils.fromIsoString(s);\n        return new OffsetDateTimeParam(value);\n    }\n\n    public OffsetDateTimeParam(OffsetDateTime value) {\n        this.value = value;\n    }\n\n    private final OffsetDateTime value;\n\n    @Override\n    public OffsetDateTime getValue() {\n        return value;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/OperationResult.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic enum OperationResult {\n\n    CREATED,\n    UPDATED,\n    DELETED,\n    ALREADY_EXISTS,\n    NOT_FOUND,\n    VALIDATED\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/PeriodicTask.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.BackgroundTask;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\npublic abstract class PeriodicTask implements BackgroundTask {\n\n    private static final Logger log = LoggerFactory.getLogger(PeriodicTask.class);\n\n    private final long interval;\n    private final long errorDelay;\n\n    private Thread worker;\n\n    public PeriodicTask(long interval, long errorDelay) {\n        this.interval = interval;\n        this.errorDelay = errorDelay;\n    }\n\n    @Override\n    public void start() {\n        if (interval <= 0) {\n            log.warn(\"start -> task is disabled: {}\", taskName());\n            return;\n        }\n\n        this.worker = new Thread(this::run, taskName());\n        this.worker.start();\n        log.info(\"start -> done: {}\", this.getClass());\n    }\n\n    @Override\n    public void stop() {\n        if (worker != null) {\n            worker.interrupt();\n            worker = null;\n        }\n\n        log.info(\"stop -> done: {}\", taskName());\n    }\n\n    private void run() {\n        while (!Thread.currentThread().isInterrupted()) {\n            try {\n                boolean isContinue = performTask();\n                if (!isContinue) {\n                    sleep(interval);\n                }\n            } catch (Exception e) {\n                log.warn(\"run -> task {} error: {}. Will retry in {}ms...\", taskName(), e.getMessage(), errorDelay, e);\n                sleep(errorDelay);\n            }\n        }\n    }\n\n    private String taskName() {\n        return this.getClass().getSimpleName();\n    }\n\n    protected abstract boolean performTask() throws Exception;\n\n    protected static void sleep(long ms) {\n        try {\n            Thread.sleep(ms);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n}\n\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/PingResponse.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.io.Serializable;\n\npublic class PingResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok;\n\n    @JsonCreator\n    public PingResponse(\n            @JsonProperty(\"ok\") boolean ok) {\n        this.ok = ok;\n    }\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    @Override\n    public String toString() {\n        return \"PingResponse{\" +\n                \"ok=\" + ok +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/RequestContext.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic final class RequestContext {\n\n    private static final ThreadLocal<RequestContext> holder = new ThreadLocal<>();\n\n    public static void set(UUID requestId, String remoteAddr, Map<String, String> extraHeaders) {\n        RequestContext ctx = new RequestContext(requestId, remoteAddr, extraHeaders);\n        holder.set(ctx);\n    }\n\n    public static void clear() {\n        holder.remove();\n    }\n\n    public static RequestContext get() {\n        return holder.get();\n    }\n\n    private final UUID requestId;\n    private final String remoteAddr;\n    private final Map<String, String> extraHeaders;\n\n    private RequestContext(UUID requestId, String remoteAddr, Map<String, String> extraHeaders) {\n        this.requestId = requestId;\n        this.remoteAddr = remoteAddr;\n        this.extraHeaders = Collections.unmodifiableMap(extraHeaders);\n    }\n\n    public UUID getRequestId() {\n        return requestId;\n    }\n\n    public Map<String, String> getExtraHeaders() {\n        return extraHeaders;\n    }\n\n    public String getRemoteAddr() {\n        return remoteAddr;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/RequestUtils.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Map;\nimport java.util.UUID;\n\npublic final class RequestUtils {\n\n    public static final String UI_REQUEST_HEADER = \"X-Concord-UI-Request\";\n\n    /**\n     * Returns the current request's unique ID. The ID is assigned when\n     * the request is first received.\n     */\n    public static UUID getRequestId() {\n        RequestContext ctx = RequestContext.get();\n        if (ctx == null) {\n            return null;\n        }\n\n        return ctx.getRequestId();\n    }\n\n    /**\n     * The UI sends a special header with all requests.\n     * The method returns {@code true} if the current request has the header.\n     */\n    public static boolean isItAUIRequest() {\n        RequestContext ctx = RequestContext.get();\n        if (ctx == null) {\n            return false;\n        }\n\n        Map<String, String> headers = ctx.getExtraHeaders();\n        if (headers == null || headers.isEmpty()) {\n            return false;\n        }\n\n        return headers.containsKey(UI_REQUEST_HEADER);\n    }\n\n    /**\n     * Returns request IP.\n     */\n    public static String getRequestIp() {\n        RequestContext ctx = RequestContext.get();\n        if (ctx == null) {\n            return null;\n        }\n        return ctx.getRemoteAddr();\n    }\n\n    private RequestUtils() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/SecureRandomProvider.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.inject.Provider;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.SecureRandom;\n\npublic class SecureRandomProvider implements Provider<SecureRandom> {\n\n    @Override\n    public SecureRandom get() {\n        try {\n            return SecureRandom.getInstance(\"NativePRNGNonBlocking\");\n        } catch (NoSuchAlgorithmException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/ServerResource.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.boot.BackgroundTasks;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.task.TaskScheduler;\nimport com.walmartlabs.concord.server.message.MessageChannelManager;\nimport org.jooq.Configuration;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.GET;\nimport javax.ws.rs.POST;\nimport javax.ws.rs.Path;\nimport javax.ws.rs.Produces;\nimport javax.ws.rs.core.MediaType;\nimport java.time.OffsetDateTime;\n\n@Path(\"/api/v1/server\")\npublic class ServerResource implements Resource {\n\n    private final TaskScheduler taskScheduler;\n    private final BackgroundTasks backgroundTasks;\n    private final MessageChannelManager messageChannelManager;\n    private final PingDao pingDao;\n\n    @Inject\n    public ServerResource(TaskScheduler taskScheduler,\n                          BackgroundTasks backgroundTasks,\n                          MessageChannelManager messageChannelManager,\n                          PingDao pingDao) {\n\n        this.taskScheduler = taskScheduler;\n        this.backgroundTasks = backgroundTasks;\n        this.messageChannelManager = messageChannelManager;\n        this.pingDao = pingDao;\n    }\n\n    @GET\n    @Path(\"/ping\")\n    @Produces(MediaType.APPLICATION_JSON)\n    public PingResponse ping() {\n        pingDao.ping();\n        return new PingResponse(true);\n    }\n\n    @GET\n    @Path(\"/version\")\n    @Produces(MediaType.APPLICATION_JSON)\n    public VersionResponse version() {\n        Version v = Version.getCurrent();\n        return new VersionResponse(v.getVersion(), v.getCommitId(), v.getEnv());\n    }\n\n    @POST\n    @Path(\"/maintenance-mode\")\n    public void maintenanceMode() {\n        messageChannelManager.shutdown();\n        backgroundTasks.stop();\n        taskScheduler.stop();\n    }\n\n    @GET\n    @Path(\"/test\")\n    @Produces(MediaType.APPLICATION_JSON)\n    public TestBean test() {\n        return new TestBean(OffsetDateTime.now());\n    }\n\n    @SuppressWarnings(\"resource\")\n    public static class PingDao extends AbstractDao {\n\n        @Inject\n        public PingDao(@MainDB Configuration cfg) {\n            super(cfg);\n        }\n\n        public void ping() {\n            dsl().selectOne().execute();\n        }\n    }\n\n    public record TestBean(OffsetDateTime now) {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/Utils.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.walmartlabs.concord.server.boot.resteasy.ExceptionMapperSupport;\nimport com.walmartlabs.concord.server.sdk.BackgroundTask;\nimport com.walmartlabs.concord.server.sdk.ScheduledTask;\nimport com.walmartlabs.concord.server.sdk.rest.Component;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport org.eclipse.jetty.ee8.servlet.ServletHolder;\n\nimport javax.servlet.Filter;\nimport javax.ws.rs.ext.ExceptionMapper;\nimport java.util.List;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\n\npublic final class Utils {\n\n    public static String[] toArray(List<String> l) {\n        if (l == null) {\n            return null;\n        }\n        return l.toArray(new String[0]);\n    }\n\n    public static String[] toString(Enum<?>... e) {\n        String[] as = new String[e.length];\n        for (int i = 0; i < e.length; i++) {\n            as[i] = e[i].toString();\n        }\n        return as;\n    }\n\n    public static String getEnv(String key, String defaultValue) {\n        String s = System.getenv(key);\n        if (s == null) {\n            return defaultValue;\n        }\n        return s;\n    }\n\n    public static <T> T unwrap(WrappedValue<T> v) {\n        if (v == null) {\n            return null;\n        }\n\n        return v.getValue();\n    }\n\n    public static void bindServletFilter(Binder binder, Class<? extends Filter> klass) {\n        binder.bind(klass).in(SINGLETON);\n        newSetBinder(binder, Filter.class).addBinding().to(klass);\n    }\n\n    public static void bindServletHolder(Binder binder, Class<? extends ServletHolder> klass) {\n        binder.bind(klass).in(SINGLETON);\n        newSetBinder(binder, ServletHolder.class).addBinding().to(klass);\n    }\n\n    public static void bindJaxRsResource(Binder binder, Class<? extends Resource> klass) {\n        binder.bind(klass).in(SINGLETON);\n        newSetBinder(binder, Component.class).addBinding().to(klass);\n    }\n\n    public static void bindSingletonBackgroundTask(Binder binder, Class<? extends BackgroundTask> klass) {\n        binder.bind(klass).in(SINGLETON);\n        newSetBinder(binder, BackgroundTask.class).addBinding().to(klass);\n    }\n\n    public static void bindScheduledTask(Binder binder, Class<? extends ScheduledTask> klass) {\n        newSetBinder(binder, ScheduledTask.class).addBinding().to(klass);\n    }\n\n    public static void bindSingletonScheduledTask(Binder binder, Class<? extends ScheduledTask> klass) {\n        binder.bind(klass).in(SINGLETON);\n        newSetBinder(binder, ScheduledTask.class).addBinding().to(klass);\n    }\n\n    public static void bindExceptionMapper(Binder binder, Class<? extends ExceptionMapperSupport<?>> klass) {\n        binder.bind(klass).in(SINGLETON);\n        newSetBinder(binder, Component.class).addBinding().to(klass);\n        newSetBinder(binder, ExceptionMapper.class).addBinding().to(klass);\n    }\n\n    private Utils() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/UuidGenerator.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.uuid.Generators;\nimport com.fasterxml.uuid.NoArgGenerator;\n\nimport java.security.NoSuchAlgorithmException;\nimport java.security.SecureRandom;\nimport java.util.UUID;\n\n/**\n * Generates UUID v7 values.\n */\npublic class UuidGenerator {\n\n    private final NoArgGenerator generator;\n\n    public UuidGenerator() {\n        try {\n            var rng = SecureRandom.getInstanceStrong();\n            this.generator = Generators.timeBasedEpochGenerator(rng);\n        } catch (NoSuchAlgorithmException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public UUID generate() {\n        return generator.generate();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/Version.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.Properties;\n\npublic final class Version {\n\n    private static final Version INSTANCE;\n    static {\n        Properties props = new Properties();\n\n        try (InputStream in = ServerResource.class.getResourceAsStream(\"version.properties\")) {\n            props.load(in);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n\n        String version = props.getProperty(\"version\");\n        String commitId = props.getProperty(\"commitId\");\n        String env = Utils.getEnv(\"CONCORD_ENV\", \"n/a\");\n\n        INSTANCE = new Version(version, commitId, env);\n    }\n\n    public static Version getCurrent() {\n        return INSTANCE;\n    }\n\n    private final String version;\n    private final String commitId;\n    private final String env;\n\n    public Version(String version, String commitId, String env) {\n        this.version = version;\n        this.commitId = commitId;\n        this.env = env;\n    }\n\n    public String getVersion() {\n        return version;\n    }\n\n    public String getCommitId() {\n        return commitId;\n    }\n\n    public String getEnv() {\n        return env;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/VersionResponse.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.io.Serializable;\n\npublic class VersionResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n    private final String version;\n    private final String commitId;\n    private final String env;\n\n    @JsonCreator\n    public VersionResponse(@JsonProperty(\"version\") String version,\n                           @JsonProperty(\"commitId\") String commitId,\n                           @JsonProperty(\"env\") String env) {\n\n        this.version = version;\n        this.commitId = commitId;\n        this.env = env;\n    }\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    public String getVersion() {\n        return version;\n    }\n\n    public String getCommitId() {\n        return commitId;\n    }\n\n    public String getEnv() {\n        return env;\n    }\n\n    @Override\n    public String toString() {\n        return \"VersionResponse{\" +\n                \"ok=\" + ok +\n                \", version='\" + version + '\\'' +\n                \", commitId='\" + commitId + '\\'' +\n                \", env='\" + env + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/WrappedValue.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface WrappedValue<T> {\n\n    T getValue();\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/agent/AgentCommand.java",
    "content": "package com.walmartlabs.concord.server.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\nimport java.time.OffsetDateTime;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic class AgentCommand implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final UUID commandId;\n    private final String agentId;\n    private final Status status;\n    private final OffsetDateTime createdAt;\n    private final Map<String, Object> data;\n\n    public AgentCommand(UUID commandId, String agentId, Status status, OffsetDateTime createdAt, Map<String, Object> data) {\n        this.commandId = commandId;\n        this.agentId = agentId;\n        this.status = status;\n        this.createdAt = createdAt;\n        this.data = data;\n    }\n\n    public UUID getCommandId() {\n        return commandId;\n    }\n\n    public String getAgentId() {\n        return agentId;\n    }\n\n    public Status getStatus() {\n        return status;\n    }\n\n    public OffsetDateTime getCreatedAt() {\n        return createdAt;\n    }\n\n    public Map<String, Object> getData() {\n        return data;\n    }\n\n    @Override\n    public String toString() {\n        return \"AgentCommand{\" +\n                \"commandId=\" + commandId +\n                \", agentId='\" + agentId + '\\'' +\n                \", status=\" + status +\n                \", createdAt=\" + createdAt +\n                \", data=\" + data +\n                '}';\n    }\n\n    public enum Status {\n        CREATED,\n        SENT,\n        FAILED\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/agent/AgentCommandWatchdog.java",
    "content": "package com.walmartlabs.concord.server.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.db.PgUtils;\nimport com.walmartlabs.concord.server.agent.AgentCommand.Status;\nimport com.walmartlabs.concord.server.cfg.AgentConfiguration;\nimport com.walmartlabs.concord.server.sdk.ScheduledTask;\nimport org.jooq.Configuration;\nimport org.jooq.Field;\n\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\n\nimport static com.walmartlabs.concord.server.jooq.tables.AgentCommands.AGENT_COMMANDS;\n\npublic class AgentCommandWatchdog implements ScheduledTask {\n\n    private final AgentConfiguration cfg;\n\n    private final WatchdogDao watchdogDao;\n\n    @Inject\n    public AgentCommandWatchdog(AgentConfiguration cfg, WatchdogDao watchdogDao) {\n        this.cfg = cfg;\n        this.watchdogDao = watchdogDao;\n    }\n\n    @Override\n    public String getId() {\n        return \"agent-command-watchdog\";\n    }\n\n    @Override\n    public long getIntervalInSec() {\n        return cfg.getWatchdogPeriod().getSeconds();\n    }\n\n    @Override\n    public void performTask() {\n        watchdogDao.failStalled(PgUtils.nowMinus(cfg.getMaxStalledAge()));\n        watchdogDao.cleanupOld(PgUtils.nowMinus(cfg.getMaxCommandAge()));\n    }\n\n    private static final class WatchdogDao extends AbstractDao {\n\n        @Inject\n        public WatchdogDao(@MainDB Configuration cfg) {\n            super(cfg);\n        }\n\n        public int failStalled(Field<OffsetDateTime> cutoff) {\n            return txResult(tx -> tx.update(AGENT_COMMANDS)\n                    .set(AGENT_COMMANDS.COMMAND_STATUS, Status.FAILED.toString())\n                    .where(AGENT_COMMANDS.COMMAND_STATUS.in(Status.CREATED.toString())\n                            .and(AGENT_COMMANDS.CREATED_AT.lessThan(cutoff)))\n                    .execute());\n        }\n\n        public int cleanupOld(Field<OffsetDateTime> cutoff) {\n            return txResult(tx -> tx.deleteFrom(AGENT_COMMANDS)\n                    .where(AGENT_COMMANDS.COMMAND_STATUS.in(Status.SENT.toString(), Status.FAILED.toString())\n                            .and(AGENT_COMMANDS.CREATED_AT.lessThan(cutoff)))\n                    .execute());\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/agent/AgentCommandsDao.java",
    "content": "package com.walmartlabs.concord.server.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.agent.AgentCommand.Status;\nimport org.jooq.BatchBindStep;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.sql.Timestamp;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.tables.AgentCommands.AGENT_COMMANDS;\nimport static org.jooq.impl.DSL.currentOffsetDateTime;\nimport static org.jooq.impl.DSL.value;\n\npublic class AgentCommandsDao extends AbstractDao {\n\n    private final ObjectMapper objectMapper;\n\n    @Inject\n    public AgentCommandsDao(@MainDB Configuration cfg) {\n        super(cfg);\n        this.objectMapper = new ObjectMapper();\n    }\n\n    @Override\n    public void tx(Tx t) {\n        super.tx(t);\n    }\n\n    public void insert(UUID commandId, String agentId, Map<String, Object> data) {\n        tx(tx -> insert(tx, commandId, agentId, data));\n    }\n\n    public void insert(DSLContext tx, UUID commandId, String agentId, Map<String, Object> data) {\n        tx.insertInto(AGENT_COMMANDS)\n                .columns(AGENT_COMMANDS.COMMAND_ID, AGENT_COMMANDS.AGENT_ID,\n                        AGENT_COMMANDS.COMMAND_STATUS, AGENT_COMMANDS.CREATED_AT,\n                        AGENT_COMMANDS.COMMAND_DATA)\n                .values(value(commandId), value(agentId),\n                        value(Status.CREATED.toString()), currentOffsetDateTime(),\n                        value(convert(data)))\n                .execute();\n    }\n\n    public void insertBatch(List<AgentCommand> ace) {\n        if (ace.isEmpty()) {\n            return;\n        }\n\n        tx(tx -> {\n            BatchBindStep q = tx.batch(tx.insertInto(AGENT_COMMANDS, AGENT_COMMANDS.COMMAND_ID, AGENT_COMMANDS.AGENT_ID,\n                    AGENT_COMMANDS.COMMAND_STATUS, AGENT_COMMANDS.CREATED_AT,\n                    AGENT_COMMANDS.COMMAND_DATA).values((UUID) null, null, null, null, null));\n\n            for (AgentCommand ac : ace) {\n                q.bind(value(ac.getCommandId()), value(ac.getAgentId()),\n                        value(ac.getStatus().toString()), new Timestamp(System.currentTimeMillis()),\n                        value(convert(ac.getData())));\n            }\n\n            q.execute();\n        });\n    }\n\n    private byte[] convert(Map<String, Object> m) {\n        try {\n            return objectMapper.writeValueAsBytes(m);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/agent/AgentManager.java",
    "content": "package com.walmartlabs.concord.server.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.message.MessageChannel;\nimport com.walmartlabs.concord.server.message.MessageChannelManager;\nimport com.walmartlabs.concord.server.agent.websocket.WebSocketChannel;\nimport com.walmartlabs.concord.server.UuidGenerator;\nimport com.walmartlabs.concord.server.queueclient.message.MessageType;\nimport com.walmartlabs.concord.server.queueclient.message.ProcessRequest;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport org.jooq.DSLContext;\n\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\nimport static java.util.Objects.requireNonNull;\n\npublic class AgentManager {\n\n    private final AgentCommandsDao commandQueue;\n    private final MessageChannelManager channelManager;\n    private final UuidGenerator uuidGenerator;\n\n    @Inject\n    public AgentManager(AgentCommandsDao commandQueue,\n                        MessageChannelManager channelManager,\n                        UuidGenerator uuidGenerator) {\n\n        this.commandQueue = requireNonNull(commandQueue);\n        this.channelManager = requireNonNull(channelManager);\n        this.uuidGenerator = requireNonNull(uuidGenerator);\n    }\n\n    public Collection<AgentWorkerEntry> getAvailableAgents() {\n        Map<MessageChannel, ProcessRequest> reqs = channelManager.getRequests(MessageType.PROCESS_REQUEST);\n        return reqs.entrySet().stream()\n                .filter(r -> r.getKey() instanceof WebSocketChannel) // TODO a better way\n                .map(r -> AgentWorkerEntry.builder()\n                        .channelId(r.getKey().getChannelId())\n                        .agentId(r.getKey().getAgentId())\n                        .userAgent(((WebSocketChannel) r.getKey()).getUserAgent())\n                        .capabilities(r.getValue().getCapabilities())\n                        .build())\n                .collect(Collectors.toList());\n    }\n\n    public void killProcess(ProcessKey processKey, String agentId) {\n        commandQueue.tx(tx -> killProcess(tx, processKey, agentId));\n    }\n\n    public void killProcess(DSLContext tx, ProcessKey processKey, String agentId) {\n        commandQueue.insert(tx, uuidGenerator.generate(), agentId, Commands.cancel(processKey));\n    }\n\n    public void killProcess(List<KeyAndAgent> processes) {\n        List<AgentCommand> commands = processes.stream()\n                .filter(p -> p.getAgentId() != null)\n                .map(p -> new AgentCommand(uuidGenerator.generate(), p.getAgentId(), AgentCommand.Status.CREATED,\n                        OffsetDateTime.now(), Commands.cancel(p.getProcessKey())))\n                .collect(Collectors.toList());\n\n        commandQueue.insertBatch(commands);\n    }\n\n    public static class KeyAndAgent {\n\n        private final ProcessKey processKey;\n\n        private final String agentId;\n\n        public KeyAndAgent(ProcessKey processKey, String agentId) {\n            this.processKey = processKey;\n            this.agentId = agentId;\n        }\n\n        public ProcessKey getProcessKey() {\n            return processKey;\n        }\n\n        public String getAgentId() {\n            return agentId;\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/agent/AgentModule.java",
    "content": "package com.walmartlabs.concord.server.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.walmartlabs.concord.server.agent.dispatcher.Dispatcher;\nimport com.walmartlabs.concord.server.sdk.BackgroundTask;\n\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\nimport static com.walmartlabs.concord.server.Utils.bindJaxRsResource;\nimport static com.walmartlabs.concord.server.Utils.bindScheduledTask;\n\npublic class AgentModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        newSetBinder(binder, BackgroundTask.class).addBinding().to(Dispatcher.class);\n        bindJaxRsResource(binder, AgentResource.class);\n        bindScheduledTask(binder, AgentCommandWatchdog.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/agent/AgentResource.java",
    "content": "package com.walmartlabs.concord.server.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.AgentWorkerUtils;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.GET;\nimport javax.ws.rs.Path;\nimport javax.ws.rs.Produces;\nimport javax.ws.rs.QueryParam;\nimport javax.ws.rs.core.MediaType;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.stream.Stream;\n\n@Path(\"/api/v1/agent\")\n@Tag(name = \"Agents\")\npublic class AgentResource implements Resource {\n\n    private final AgentManager agentManager;\n\n    @Inject\n    public AgentResource(AgentManager agentManager) {\n        this.agentManager = agentManager;\n    }\n\n    @GET\n    @Path(\"/all/workers\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"List currently available agent workers\")\n    public Collection<AgentWorkerEntry> listWorkers() {\n        return agentManager.getAvailableAgents();\n    }\n\n    @GET\n    @Path(\"/all/workersCount\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Counts the currently connected workers based on the specified capabilities property\")\n    public Map<Object, Long> aggregate(@QueryParam(\"capabilities\") String capabilities) {\n        if (capabilities == null || capabilities.isEmpty()) {\n            throw new ValidationErrorsException(\"'capabilities' filter is required\");\n        }\n\n        Collection<AgentWorkerEntry> data = agentManager.getAvailableAgents();\n        if (data == null || data.isEmpty()) {\n            return Collections.emptyMap();\n        }\n\n        String[] path = capabilities.split(\"\\\\.\");\n        if (path.length < 1 || Stream.of(path).anyMatch(p -> p.trim().isEmpty())) {\n            throw new ValidationErrorsException(\"Invalid 'capabilities' value. Expected a path to a property, got: \" + capabilities);\n        }\n\n        return AgentWorkerUtils.groupBy(data, path);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/agent/AgentWorkerEntry.java",
    "content": "package com.walmartlabs.concord.server.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.util.Map;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableAgentWorkerEntry.class)\n@JsonDeserialize(as = ImmutableAgentWorkerEntry.class)\npublic interface AgentWorkerEntry {\n\n    String channelId();\n\n    @Nullable // backward-compatibility with old agent versions\n    String agentId();\n\n    String userAgent();\n\n    @Nullable\n    Map<String, Object> capabilities();\n\n    static ImmutableAgentWorkerEntry.Builder builder() {\n        return ImmutableAgentWorkerEntry.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/agent/Commands.java",
    "content": "package com.walmartlabs.concord.server.agent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.CommandType;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic final class Commands {\n\n    public static final String TYPE_KEY = \"type\";\n\n    public static final String INSTANCE_ID_KEY = \"instanceId\";\n\n    public static Map<String, Object> cancel(ProcessKey processKey) {\n        Map<String, Object> m = new HashMap<>();\n        m.put(TYPE_KEY, CommandType.CANCEL_JOB);\n        m.put(INSTANCE_ID_KEY, processKey.getInstanceId().toString());\n        return m;\n    }\n\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/agent/dispatcher/Dispatcher.java",
    "content": "package com.walmartlabs.concord.server.agent.dispatcher;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.Histogram;\nimport com.codahale.metrics.MetricRegistry;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.CommandType;\nimport com.walmartlabs.concord.server.PeriodicTask;\nimport com.walmartlabs.concord.server.agent.AgentCommand;\nimport com.walmartlabs.concord.server.agent.Commands;\nimport com.walmartlabs.concord.server.message.MessageChannel;\nimport com.walmartlabs.concord.server.message.MessageChannelManager;\nimport com.walmartlabs.concord.server.cfg.AgentConfiguration;\nimport com.walmartlabs.concord.server.jooq.tables.records.AgentCommandsRecord;\nimport com.walmartlabs.concord.server.queueclient.message.CommandRequest;\nimport com.walmartlabs.concord.server.queueclient.message.CommandResponse;\nimport com.walmartlabs.concord.server.queueclient.message.MessageType;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.time.OffsetDateTime;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.jooq.tables.AgentCommands.AGENT_COMMANDS;\n\n/**\n * Dispatches commands to agents.\n */\npublic class Dispatcher extends PeriodicTask {\n\n    private static final Logger log = LoggerFactory.getLogger(Dispatcher.class);\n\n    private static final long ERROR_DELAY = 1 * 60 * 1000L; // 1 min\n    private static final int BATCH_SIZE = 10;\n\n    private final DispatcherDao dao;\n    private final MessageChannelManager channelManager;\n\n    @Inject\n    public Dispatcher(AgentConfiguration cfg,\n                      DispatcherDao dao,\n                      MessageChannelManager channelManager) {\n\n        super(cfg.getCommandPollDelay().toMillis(), ERROR_DELAY);\n        this.dao = dao;\n        this.channelManager = channelManager;\n    }\n\n    @Override\n    protected boolean performTask() {\n        Map<MessageChannel, CommandRequest> requests = this.channelManager.getRequests(MessageType.COMMAND_REQUEST);\n        if (requests.isEmpty()) {\n            return false;\n        }\n\n        List<Request> l = requests.entrySet().stream()\n                .map(e -> new Request(e.getKey(), e.getValue()))\n                .collect(Collectors.toList());\n\n        dispatch(l);\n\n        return false;\n    }\n\n    private void dispatch(List<Request> requests) {\n        // we need it modifiable\n        List<Request> inbox = new ArrayList<>(requests);\n\n        // run everything in a single transaction\n        dao.tx(tx -> {\n            int offset = 0;\n\n            while (true) {\n                // fetch the next few CREATED commands from the DB\n                List<AgentCommand> candidates = new ArrayList<>(dao.next(tx, offset, BATCH_SIZE));\n                if (candidates.isEmpty() || inbox.isEmpty()) {\n                    // no potential candidates or no requests left to process\n                    break;\n                }\n\n                List<Match> matches = match(inbox, candidates);\n                if (matches.isEmpty()) {\n                    // no matches, try fetching the next N records\n                    offset += BATCH_SIZE;\n                    continue;\n                }\n\n                for (Match m : matches) {\n                    sendResponse(m, m.command);\n                    dao.markAsSent(tx, m.command.getCommandId());\n\n                    inbox.remove(m.request);\n                }\n            }\n        });\n    }\n\n    private List<Match> match(List<Request> requests, List<AgentCommand> candidates) {\n        List<Match> results = new ArrayList<>();\n\n        for (Request req : requests) {\n            AgentCommand candidate = findCandidate(req.request, candidates);\n            if (candidate == null) {\n                continue;\n            }\n\n            // remove the matche from the list\n            candidates.remove(candidate);\n\n            results.add(new Match(req, candidate));\n        }\n\n        return results;\n    }\n\n    private AgentCommand findCandidate(CommandRequest req, List<AgentCommand> candidates) {\n        return candidates.stream()\n                .filter(c -> c.getAgentId().equals(req.getAgentId().toString()))\n                .findFirst()\n                .orElse(null);\n    }\n\n    private void sendResponse(Match match, AgentCommand response) {\n        long correlationId = match.request.request.getCorrelationId();\n\n        CommandType type = CommandType.valueOf((String) response.getData().remove(Commands.TYPE_KEY));\n\n        Map<String, Object> payload = new HashMap<>();\n        payload.put(\"type\", type.toString());\n        payload.putAll(response.getData());\n\n        MessageChannel channel = match.request.channel;\n        boolean success = channelManager.sendMessage(channel.getChannelId(), CommandResponse.cancel(correlationId, payload));\n        if (success) {\n            log.info(\"sendResponse ['{}'] -> done\", correlationId);\n        } else {\n            log.error(\"sendResponse ['{}'] -> failed\", correlationId);\n        }\n    }\n\n    public static class DispatcherDao extends AbstractDao {\n\n        private final ObjectMapper objectMapper;\n        private final Histogram offsetHistogram;\n\n        @Inject\n        public DispatcherDao(@MainDB Configuration cfg,\n                             MetricRegistry metricRegistry) {\n\n            super(cfg);\n            this.objectMapper = new ObjectMapper();\n            this.offsetHistogram = metricRegistry.histogram(\"agent-command-dispatcher-offset\");\n        }\n\n        @Override\n        protected void tx(Tx t) {\n            super.tx(t);\n        }\n\n        @WithTimer\n        public List<AgentCommand> next(DSLContext tx, int offset, int limit) {\n            offsetHistogram.update(offset);\n\n            return tx.selectFrom(AGENT_COMMANDS)\n                    .where(AGENT_COMMANDS.COMMAND_STATUS.eq(AgentCommand.Status.CREATED.toString()))\n                    .orderBy(AGENT_COMMANDS.CREATED_AT)\n                    .offset(offset)\n                    .limit(limit)\n                    .forUpdate()\n                    .skipLocked()\n                    .fetch(this::convert);\n        }\n\n        public void markAsSent(DSLContext tx, UUID commandId) {\n            tx.update(AGENT_COMMANDS)\n                    .set(AGENT_COMMANDS.COMMAND_STATUS, AgentCommand.Status.SENT.toString())\n                    .where(AGENT_COMMANDS.COMMAND_ID.eq(commandId))\n                    .execute();\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        private AgentCommand convert(AgentCommandsRecord r) {\n            UUID commandId = r.getCommandId();\n            String agentId = r.getAgentId();\n            OffsetDateTime createdAt = r.getCreatedAt();\n            AgentCommand.Status status = AgentCommand.Status.valueOf(r.getCommandStatus());\n\n            Map<String, Object> data;\n            try {\n                data = objectMapper.readValue(r.get(AGENT_COMMANDS.COMMAND_DATA), Map.class);\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n\n            return new AgentCommand(commandId, agentId, status, createdAt, data);\n        }\n    }\n\n    private record Match(Request request, AgentCommand command) {\n\n    }\n\n    private record Request(MessageChannel channel, CommandRequest request) {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/agent/websocket/ConcordWebSocketServlet.java",
    "content": "package com.walmartlabs.concord.server.agent.websocket;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.message.MessageChannelManager;\nimport com.walmartlabs.concord.server.security.apikey.ApiKeyDao;\nimport org.eclipse.jetty.ee8.websocket.server.JettyWebSocketServlet;\nimport org.eclipse.jetty.ee8.websocket.server.JettyWebSocketServletFactory;\n\nimport javax.inject.Inject;\nimport javax.servlet.annotation.WebServlet;\n\n@WebServlet(\"/websocket\")\npublic class ConcordWebSocketServlet extends JettyWebSocketServlet {\n\n    private static final long serialVersionUID = 1L;\n\n    private final MessageChannelManager channelManager;\n    private final ApiKeyDao apiKeyDao;\n\n    @Inject\n    public ConcordWebSocketServlet(MessageChannelManager channelManager, ApiKeyDao apiKeyDao) {\n        this.channelManager = channelManager;\n        this.apiKeyDao = apiKeyDao;\n    }\n\n    @Override\n    public void configure(JettyWebSocketServletFactory factory) {\n        factory.setCreator(new WebSocketCreator(channelManager, apiKeyDao));\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/agent/websocket/WebSocketChannel.java",
    "content": "package com.walmartlabs.concord.server.agent.websocket;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.message.MessageChannel;\nimport com.walmartlabs.concord.server.queueclient.MessageSerializer;\nimport com.walmartlabs.concord.server.queueclient.message.Message;\nimport com.walmartlabs.concord.server.queueclient.message.MessageType;\nimport org.eclipse.jetty.ee8.websocket.api.Session;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.nio.ByteBuffer;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.concurrent.ConcurrentHashMap;\n\npublic class WebSocketChannel implements MessageChannel {\n\n    private static final Logger log = LoggerFactory.getLogger(WebSocketChannel.class);\n\n    private final String channelId;\n    private final String agentId;\n    private final String userAgent;\n    private final Session session;\n\n    private final Map<Long, Message> requests = new ConcurrentHashMap<>();\n\n    public WebSocketChannel(String channelId, String agentId, Session session, String userAgent) {\n        this.channelId = channelId;\n        this.agentId = agentId;\n        this.session = session;\n        this.userAgent = userAgent;\n    }\n\n    @Override\n    public String getChannelId() {\n        return channelId;\n    }\n\n    public String getAgentId() {\n        return agentId;\n    }\n\n    public String getUserAgent() {\n        return userAgent;\n    }\n\n    public Session getSession() {\n        return session;\n    }\n\n    /**\n     * Sends the response and removes the associated request from the queue.\n     */\n    @Override\n    public boolean offerMessage(Message response) {\n        if (!session.isOpen()) {\n            log.warn(\"response ['{}', '{}'] -> session is closed\", channelId, response);\n            return false;\n        }\n\n        Message request = requests.remove(response.getCorrelationId());\n        if (request == null) {\n            log.warn(\"response ['{}', '{}'] -> request not found\", channelId, response);\n            return false;\n        }\n\n        try {\n            session.getRemote().sendString(MessageSerializer.serialize(response));\n            return true;\n        } catch (Exception e) {\n            log.error(\"response ['{}', '{}'] -> error\", channelId, response, e);\n            return false;\n        }\n    }\n\n    @Override\n    public Optional<Message> getMessage(MessageType requestType) {\n        return requests.values().stream()\n                .filter(m -> m.getMessageType() == requestType)\n                .findAny();\n    }\n\n    @Override\n    public void close() {\n        if (!session.isOpen()) {\n            return;\n        }\n\n        try {\n            session.close();\n            session.disconnect();\n        } catch (Exception e) {\n            log.warn(\"close ['{}'] -> error\", channelId, e);\n        }\n    }\n\n    void onRequest(Message request) {\n        Message old = requests.put(request.getCorrelationId(), request);\n        if (old != null) {\n            log.error(\"request ['{}', '{}'] -> duplicate request. closing channel\", channelId, request);\n            close();\n        }\n    }\n\n    void pong() {\n        try {\n            session.getRemote().sendPong(ByteBuffer.wrap(\"pong\".getBytes()));\n        } catch (Exception e) {\n            log.warn(\"pong ['{}'] -> error\", channelId, e);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/agent/websocket/WebSocketCreator.java",
    "content": "package com.walmartlabs.concord.server.agent.websocket;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.message.MessageChannelManager;\nimport com.walmartlabs.concord.server.queueclient.QueueClient;\nimport com.walmartlabs.concord.server.security.apikey.ApiKeyDao;\nimport com.walmartlabs.concord.server.security.apikey.ApiKeyEntry;\nimport org.eclipse.jetty.ee8.websocket.server.JettyServerUpgradeRequest;\nimport org.eclipse.jetty.ee8.websocket.server.JettyServerUpgradeResponse;\nimport org.eclipse.jetty.ee8.websocket.server.JettyWebSocketCreator;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.servlet.http.HttpServletResponse;\nimport javax.ws.rs.core.HttpHeaders;\nimport java.io.IOException;\nimport java.util.Base64;\nimport java.util.UUID;\n\npublic class WebSocketCreator implements JettyWebSocketCreator {\n\n    private static final Logger log = LoggerFactory.getLogger(WebSocketCreator.class);\n\n    private final MessageChannelManager channelManager;\n    private final ApiKeyDao apiKeyDao;\n\n    public WebSocketCreator(MessageChannelManager channelManager, ApiKeyDao apiKeyDao) {\n        this.channelManager = channelManager;\n        this.apiKeyDao = apiKeyDao;\n    }\n\n    @Override\n    public Object createWebSocket(JettyServerUpgradeRequest req, JettyServerUpgradeResponse resp) {\n        if (channelManager.isShutdown()) {\n            sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, \"Server is in the maintenance mode\", resp);\n            return null;\n        }\n\n        String auth = req.getHeader(HttpHeaders.AUTHORIZATION);\n        if (auth == null) {\n            sendError(HttpServletResponse.SC_UNAUTHORIZED, \"Missing \" + HttpHeaders.AUTHORIZATION + \" header\", resp);\n            return null;\n        }\n\n        if (invalidApiKey(auth)) {\n            sendError(HttpServletResponse.SC_FORBIDDEN, \"Invalid API key: '\" + auth + \"'\", resp);\n            return null;\n        }\n\n        ApiKeyEntry apiKey = apiKeyDao.find(auth);\n        if (apiKey == null) {\n            sendError(HttpServletResponse.SC_FORBIDDEN, \"Invalid API key or user not found\", resp);\n            return null;\n        }\n\n        String channelId = \"ws-\" + UUID.randomUUID();\n        String agentId = req.getHeader(QueueClient.AGENT_ID);\n        String userAgent = req.getHeader(QueueClient.AGENT_UA);\n        return new WebSocketListener(channelManager, channelId, agentId, userAgent);\n    }\n\n    private static boolean invalidApiKey(String s) {\n        try {\n            Base64.getDecoder().decode(s);\n            return false;\n        } catch (IllegalArgumentException e) {\n            return true;\n        }\n    }\n\n    private void sendError(int statusCode, String message, JettyServerUpgradeResponse resp) {\n        try {\n            resp.sendError(statusCode, message);\n        } catch (IOException e) {\n            log.error(\"sendError ['{}', '{}'] -> error\", statusCode, message, e);\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/agent/websocket/WebSocketListener.java",
    "content": "package com.walmartlabs.concord.server.agent.websocket;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.message.MessageChannelManager;\nimport com.walmartlabs.concord.server.queueclient.MessageSerializer;\nimport org.eclipse.jetty.ee8.websocket.api.Session;\nimport org.eclipse.jetty.ee8.websocket.api.WebSocketPingPongListener;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.nio.ByteBuffer;\n\nimport static java.util.Objects.requireNonNull;\n\npublic class WebSocketListener implements org.eclipse.jetty.ee8.websocket.api.WebSocketListener, WebSocketPingPongListener {\n\n    private static final Logger log = LoggerFactory.getLogger(WebSocketListener.class);\n\n    private final MessageChannelManager channelManager;\n\n    private final String channelId;\n    private final String agentId;\n    private final String userAgent;\n\n    public WebSocketListener(MessageChannelManager channelManager, String channelId, String agentId, String userAgent) {\n        this.channelManager = requireNonNull(channelManager);\n        this.channelId = requireNonNull(channelId);\n        this.agentId = requireNonNull(agentId);\n        this.userAgent = sanitize(userAgent);\n    }\n\n    @Override\n    public void onWebSocketPing(ByteBuffer payload) {\n        channelManager.getChannel(channelId, WebSocketChannel.class)\n                .ifPresentOrElse(WebSocketChannel::pong,\n                        () -> log.warn(\"onWebSocketPing ['{}'] -> channel not found\", channelId));\n    }\n\n    @Override\n    public void onWebSocketPong(ByteBuffer payload) {\n        // we don't expect pongs\n    }\n\n    @Override\n    public void onWebSocketBinary(byte[] payload, int offset, int len) {\n        log.error(\"onWebSocketBinary ['{}'] -> not supported, closing channel\", channelId);\n        channelManager.close(channelId);\n    }\n\n    @Override\n    public void onWebSocketText(String message) {\n        channelManager.getChannel(channelId, WebSocketChannel.class)\n                .ifPresentOrElse(c -> c.onRequest(MessageSerializer.deserialize(message)),\n                        () -> log.warn(\"onWebSocketText ['{}', '{}'] -> channel not found\", channelId, message));\n    }\n\n    @Override\n    public void onWebSocketClose(int statusCode, String reason) {\n        channelManager.close(channelId);\n        log.debug(\"onWebSocketClose ['{}', '{}', '{}'] -> ok\", channelId, statusCode, reason);\n    }\n\n    @Override\n    public void onWebSocketConnect(Session session) {\n        var channel = new WebSocketChannel(channelId, agentId, session, userAgent);\n        channelManager.add(channel);\n        log.debug(\"onWebSocketConnect ['{}'] -> '{}'\", channelId, userAgent);\n    }\n\n    @Override\n    public void onWebSocketError(Throwable cause) {\n        log.warn(\"onWebSocketError ['{}', '{}'] -> error: {}\", channelId, userAgent, cause.getMessage());\n    }\n\n    private static String sanitize(String s) {\n        if (s == null || s.isEmpty()) {\n            return s;\n        }\n        if (s.length() > 128) {\n            s = s.substring(0, 128) + \"...cut\";\n        }\n        return s.replace(\"\\n\", \"\\\\n\");\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/agent/websocket/WebSocketMetricsModule.java",
    "content": "package com.walmartlabs.concord.server.agent.websocket;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.Gauge;\nimport com.google.inject.AbstractModule;\nimport com.google.inject.multibindings.Multibinder;\nimport com.walmartlabs.concord.server.message.MessageChannelManager;\nimport com.walmartlabs.concord.server.queueclient.message.MessageType;\nimport com.walmartlabs.concord.server.sdk.metrics.GaugeProvider;\n\nimport javax.inject.Provider;\n\npublic class WebSocketMetricsModule extends AbstractModule {\n\n    @Override\n    protected void configure() {\n        Provider<MessageChannelManager> channelManagerProvider = getProvider(MessageChannelManager.class);\n\n        @SuppressWarnings(\"rawtypes\")\n        Multibinder<GaugeProvider> gauges = Multibinder.newSetBinder(binder(), GaugeProvider.class);\n        gauges.addBinding().toInstance(createGauge(channelManagerProvider));\n        gauges.addBinding().toInstance(create(channelManagerProvider));\n    }\n\n    private static GaugeProvider<Integer> createGauge(Provider<MessageChannelManager> channelManagerProvider) {\n        return new GaugeProvider<>() {\n            @Override\n            public String name() {\n                return \"websocket-clients\";\n            }\n\n            @Override\n            public Gauge<Integer> gauge() {\n                return () -> channelManagerProvider.get().connectedClientsCount();\n            }\n        };\n    }\n\n    private static GaugeProvider<Integer> create(Provider<MessageChannelManager> channelManagerProvider) {\n        return new GaugeProvider<>() {\n            @Override\n            public String name() {\n                return \"agent-workers-available\";\n            }\n\n            @Override\n            public Gauge<Integer> gauge() {\n                return () -> channelManagerProvider.get().getRequests(MessageType.PROCESS_REQUEST).size();\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/agent/websocket/WebSocketModule.java",
    "content": "package com.walmartlabs.concord.server.agent.websocket;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\n\nimport javax.servlet.http.HttpServlet;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\n\npublic class WebSocketModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        newSetBinder(binder, HttpServlet.class).addBinding().to(ConcordWebSocketServlet.class).in(SINGLETON);\n        binder.install(new WebSocketMetricsModule());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/audit/ActionSource.java",
    "content": "package com.walmartlabs.concord.server.audit;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic enum ActionSource {\n\n    /**\n     * Access using the API.\n     */\n    API_REQUEST,\n\n    /**\n     * Access from a process.\n     */\n    PROCESS,\n\n    /**\n     * Initiated by Concord.\n     */\n    SYSTEM,\n\n    /**\n     * Access from the UI\n     */\n    UI\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/audit/AuditAction.java",
    "content": "package com.walmartlabs.concord.server.audit;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic enum AuditAction {\n\n    CREATE,\n    UPDATE,\n    DELETE,\n    ACCESS\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/audit/AuditDao.java",
    "content": "package com.walmartlabs.concord.server.audit;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.db.PgUtils;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.jooq.tables.AuditLog;\nimport com.walmartlabs.concord.server.jooq.tables.Users;\nimport com.walmartlabs.concord.server.jooq.tables.records.AuditLogRecord;\nimport com.walmartlabs.concord.server.org.EntityOwner;\nimport com.walmartlabs.concord.server.sdk.audit.AuditEvent;\nimport com.walmartlabs.concord.server.user.UserType;\nimport org.jooq.Configuration;\nimport org.jooq.JSONB;\nimport org.jooq.Record9;\nimport org.jooq.SelectOnConditionStep;\n\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.tables.AuditLog.AUDIT_LOG;\nimport static com.walmartlabs.concord.server.jooq.tables.Users.USERS;\n\npublic class AuditDao extends AbstractDao {\n\n    private final ConcordObjectMapper objectMapper;\n\n    @Inject\n    public AuditDao(@MainDB Configuration cfg,\n                    ConcordObjectMapper objectMapper) {\n        super(cfg);\n\n        this.objectMapper = objectMapper;\n    }\n\n    /**\n     * Inserts a new audit log entry.\n     *\n     * @return the autogenerated ID.\n     */\n    public AuditEvent insert(UUID userId, AuditObject object, AuditAction action, Map<String, Object> details) {\n        AuditLogRecord r = txResult(tx -> tx.insertInto(AUDIT_LOG)\n                .columns(AUDIT_LOG.USER_ID,\n                        AUDIT_LOG.ENTRY_OBJECT,\n                        AUDIT_LOG.ENTRY_ACTION,\n                        AUDIT_LOG.ENTRY_DETAILS)\n                .values(userId,\n                        object.toString(),\n                        action.toString(),\n                        objectMapper.toJSONB(details))\n                .returning(AUDIT_LOG.ENTRY_DATE, AUDIT_LOG.ENTRY_SEQ)\n                .fetchOne());\n\n        return AuditEvent.builder()\n                .entrySeq(r.getEntrySeq())\n                .entryDate(r.getEntryDate())\n                .userId(userId)\n                .object(object.toString())\n                .action(action.toString())\n                .details(details)\n                .build();\n    }\n\n    public List<AuditLogEntry> list(AuditLogFilter filter) {\n        return txResult(tx -> {\n            AuditLog l = AUDIT_LOG.as(\"l\");\n            Users u = USERS.as(\"u\");\n\n            SelectOnConditionStep<Record9<OffsetDateTime, String, String, JSONB, UUID, String, String, String, String>> q = tx.select(l.ENTRY_DATE,\n                    l.ENTRY_ACTION,\n                    l.ENTRY_OBJECT,\n                    l.ENTRY_DETAILS,\n                    u.USER_ID,\n                    u.USERNAME,\n                    u.DOMAIN,\n                    u.USER_TYPE,\n                    u.DISPLAY_NAME)\n                    .from(l)\n                    .leftJoin(u).on(u.USER_ID.eq(l.USER_ID));\n\n            AuditObject object = filter.object();\n            if (object != null) {\n                q.where(l.ENTRY_OBJECT.eq(object.name()));\n            }\n\n            AuditAction action = filter.action();\n            if (action != null) {\n                q.where(l.ENTRY_ACTION.eq(action.name()));\n            }\n\n            UUID userId = filter.userId();\n            if (userId != null) {\n                q.where(l.USER_ID.eq(userId));\n            }\n\n            Map<String, Object> details = filter.details();\n            if (details != null) {\n                q.where(PgUtils.jsonbContains(l.ENTRY_DETAILS, objectMapper.toJSONB(details)));\n            }\n\n            OffsetDateTime after = filter.after();\n            if (after != null) {\n                q.where(l.ENTRY_DATE.greaterThan(after));\n            }\n\n            OffsetDateTime before = filter.before();\n            if (before != null) {\n                q.where(l.ENTRY_DATE.lessThan(before));\n            }\n\n            Integer limit = filter.limit();\n            if (limit != null) {\n                q.limit(limit);\n            }\n\n            Integer offset = filter.offset();\n            if (offset != null) {\n                q.offset(offset);\n            }\n\n            q.orderBy(l.ENTRY_DATE.desc(), l.ENTRY_SEQ.desc());\n\n            return q.fetch(this::toEntry);\n        });\n    }\n\n    private AuditLogEntry toEntry(Record9<OffsetDateTime, String, String, JSONB, UUID, String, String, String, String> r) {\n        ImmutableAuditLogEntry.Builder b = AuditLogEntry.builder()\n                .entryDate(r.get(0, OffsetDateTime.class))\n                .action(AuditAction.valueOf(r.get(1, String.class)))\n                .object(AuditObject.valueOf(r.get(2, String.class)))\n                .details(objectMapper.fromJSONB(r.get(3, JSONB.class)));\n\n        if (r.get(4) != null) {\n            b.user(EntityOwner.builder()\n                    .id(r.get(4, UUID.class))\n                    .username(r.get(5, String.class))\n                    .userDomain(r.get(6, String.class))\n                    .userType(UserType.valueOf(r.get(7, String.class)))\n                    .displayName(r.get(8, String.class))\n                    .build());\n        }\n\n        return b.build();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/audit/AuditLog.java",
    "content": "package com.walmartlabs.concord.server.audit;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.Listeners;\nimport com.walmartlabs.concord.server.RequestUtils;\nimport com.walmartlabs.concord.server.cfg.AuditConfiguration;\nimport com.walmartlabs.concord.server.org.project.DiffUtils;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.audit.AuditEvent;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.security.sessionkey.SessionKeyPrincipal;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic class AuditLog {\n\n    private static final Logger log = LoggerFactory.getLogger(AuditLog.class);\n\n    private static final ThreadLocal<ActionSourceParameters> threadLocalActionSource = new ThreadLocal<>();\n\n    /**\n     * Any audit log calls done inside the provided {@link Runnable} will have\n     * the specified {@link ActionSource} and its parameters added automatically.\n     */\n    public static void withActionSource(ActionSource source, Map<String, Object> params, Runnable runnable) {\n        try {\n            threadLocalActionSource.set(new ActionSourceParameters(source, params));\n            runnable.run();\n        } finally {\n            threadLocalActionSource.set(null);\n        }\n    }\n\n    private final AuditConfiguration cfg;\n    private final AuditDao auditDao;\n    private final Listeners listeners;\n\n    @Inject\n    public AuditLog(AuditConfiguration cfg, AuditDao auditDao, Listeners listeners) {\n        this.cfg = cfg;\n        this.auditDao = auditDao;\n        this.listeners = listeners;\n    }\n\n    public EntryBuilder add(AuditObject object, AuditAction action) {\n        return new EntryBuilder(object, action);\n    }\n\n    public class EntryBuilder {\n\n        private final AuditObject object;\n        private final AuditAction action;\n        private final Map<String, Object> details;\n        private Map<String, Object> changes;\n\n        private UUID userId;\n\n        private EntryBuilder(AuditObject object, AuditAction action) {\n            this.object = object;\n            this.action = action;\n            this.details = new HashMap<>();\n            this.changes = new HashMap<>();\n        }\n\n        public EntryBuilder userId(UUID userId) {\n            this.userId = userId;\n            return this;\n        }\n\n        public EntryBuilder field(String k, Object v) {\n            if (v == null) {\n                return this;\n            }\n\n            this.details.put(k, v);\n            return this;\n        }\n\n        public EntryBuilder changes(Object prevEntry, Object newEntry) {\n            if (prevEntry == null && newEntry == null) {\n                return this;\n            }\n\n            this.changes = DiffUtils.compare(prevEntry, newEntry);\n            return this;\n        }\n\n        public EntryBuilder actionSource(ActionSource source) {\n            return actionSource(source, Collections.emptyMap());\n        }\n\n        public EntryBuilder actionSource(ActionSource source, Map<String, Object> params) {\n            Map<String, Object> m = new HashMap<>(params != null ? params : Collections.emptyMap());\n            m.put(\"type\", source);\n            this.details.put(\"actionSource\", m);\n            return this;\n        }\n\n        public void log() {\n            if (!cfg.isEnabled()) {\n                return;\n            }\n\n            try {\n                doLog();\n            } catch (Exception e) {\n                log.error(\"log -> error while inserting an audit log entry: {}\", e.getMessage(), e);\n                throw e;\n            }\n        }\n\n        private void doLog() {\n            if (userId == null) {\n                UserPrincipal user = UserPrincipal.getCurrent();\n                if (user != null) {\n                    userId = user.getId();\n                }\n            }\n\n            // save the thread-local action source\n            ActionSourceParameters actionSourceParams = threadLocalActionSource.get();\n            if (actionSourceParams != null) {\n                actionSource(actionSourceParams.source, actionSourceParams.params);\n            } else if (!details.containsKey(\"actionSource\")) {\n                // automatically fill-in actionSource if it wasn't defined before\n                SessionKeyPrincipal sessionKey = SessionKeyPrincipal.getCurrent();\n                if (sessionKey != null) {\n                    // the request was made from within a process\n                    PartialProcessKey processKey = sessionKey.getProcessKey();\n                    actionSource(ActionSource.PROCESS, Collections.singletonMap(\"instanceId\", processKey.getInstanceId()));\n                } else if (RequestUtils.isItAUIRequest()) {\n                    // the request was sent by the UI\n                    actionSource(ActionSource.UI);\n                } else {\n                    // the request was made using the API\n                    actionSource(ActionSource.API_REQUEST);\n                }\n            }\n\n            details.put(\"requestId\", RequestUtils.getRequestId());\n            details.put(\"requestIp\", RequestUtils.getRequestIp());\n\n            if (changes != null && !changes.isEmpty()) {\n                details.put(\"changes\", changes);\n            }\n\n            AuditEvent ev = auditDao.insert(userId, object, action, details);\n            listeners.onAuditEvent(ev);\n        }\n    }\n\n    public static class ActionSourceParameters {\n\n        private final ActionSource source;\n        private final Map<String, Object> params;\n\n        private ActionSourceParameters(ActionSource source, Map<String, Object> params) {\n            this.source = source;\n            this.params = params;\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/audit/AuditLogCleaner.java",
    "content": "package com.walmartlabs.concord.server.audit;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.db.PgUtils;\nimport com.walmartlabs.concord.server.cfg.AuditConfiguration;\nimport com.walmartlabs.concord.server.sdk.ScheduledTask;\nimport org.jooq.Configuration;\nimport org.jooq.Field;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.time.Duration;\nimport java.time.OffsetDateTime;\n\nimport static com.walmartlabs.concord.server.jooq.tables.AuditLog.AUDIT_LOG;\n\npublic class AuditLogCleaner implements ScheduledTask {\n\n    private static final Logger log = LoggerFactory.getLogger(AuditLogCleaner.class);\n\n    private final AuditConfiguration cfg;\n    private final CleanerDao cleanerDao;\n\n    @Inject\n    public AuditLogCleaner(AuditConfiguration cfg, CleanerDao cleanerDao) {\n        this.cfg = cfg;\n        this.cleanerDao = cleanerDao;\n    }\n\n    @Override\n    public String getId() {\n        return \"audit-log-cleaner\";\n    }\n\n    @Override\n    public long getIntervalInSec() {\n        return cfg.getPeriod().getSeconds();\n    }\n\n    @Override\n    public void performTask() {\n        cleanerDao.deleteOldLogs(cfg.getMaxLogAge());\n    }\n\n    private static class CleanerDao extends AbstractDao {\n\n        @Inject\n        protected CleanerDao(@MainDB Configuration cfg) {\n            super(cfg);\n        }\n\n        void deleteOldLogs(Duration maxAge) {\n            Field<OffsetDateTime> cutoff = PgUtils.nowMinus(maxAge);\n\n            long t1 = System.currentTimeMillis();\n            tx(tx -> tx.deleteFrom(AUDIT_LOG).where(AUDIT_LOG.ENTRY_DATE.lessThan(cutoff)).execute());\n            long t2 = System.currentTimeMillis();\n\n            log.info(\"deleteOldLogs -> removed entries older than {}, took {}ms\", maxAge, (t2 - t1));\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/audit/AuditLogEntry.java",
    "content": "package com.walmartlabs.concord.server.audit;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.server.org.EntityOwner;\nimport com.walmartlabs.concord.server.sdk.AllowNulls;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.time.OffsetDateTime;\nimport java.util.Map;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableAuditLogEntry.class)\n@JsonDeserialize(as = ImmutableAuditLogEntry.class)\npublic interface AuditLogEntry {\n\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    OffsetDateTime entryDate();\n\n    AuditAction action();\n\n    AuditObject object();\n\n    @Nullable\n    @AllowNulls\n    Map<String, Object> details();\n\n    @Nullable\n    EntityOwner user();\n\n    static ImmutableAuditLogEntry.Builder builder() {\n        return ImmutableAuditLogEntry.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/audit/AuditLogFilter.java",
    "content": "package com.walmartlabs.concord.server.audit;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.time.OffsetDateTime;\nimport java.util.Map;\nimport java.util.UUID;\n\n@Value.Immutable\npublic interface AuditLogFilter {\n\n    @Nullable\n    AuditObject object();\n\n    @Nullable\n    AuditAction action();\n\n    @Nullable\n    UUID userId();\n\n    @Nullable\n    Map<String, Object> details();\n\n    @Nullable\n    OffsetDateTime after();\n\n    @Nullable\n    OffsetDateTime before();\n\n    @Nullable\n    Integer limit();\n\n    @Nullable\n    Integer offset();\n\n    static ImmutableAuditLogFilter.Builder builder() {\n        return ImmutableAuditLogFilter.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/audit/AuditLogModule.java",
    "content": "package com.walmartlabs.concord.server.audit;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.walmartlabs.concord.server.sdk.audit.AuditLogListener;\n\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\nimport static com.walmartlabs.concord.server.Utils.bindJaxRsResource;\nimport static com.walmartlabs.concord.server.Utils.bindSingletonScheduledTask;\n\npublic class AuditLogModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        newSetBinder(binder, AuditLogListener.class);\n        bindSingletonScheduledTask(binder, AuditLogCleaner.class);\n        bindJaxRsResource(binder, AuditLogResource.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/audit/AuditLogResource.java",
    "content": "package com.walmartlabs.concord.server.audit;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.OffsetDateTimeParam;\nimport com.walmartlabs.concord.server.cfg.AuditConfiguration;\nimport com.walmartlabs.concord.server.org.OrganizationEntry;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.org.jsonstore.JsonStoreAccessManager;\nimport com.walmartlabs.concord.server.org.jsonstore.JsonStoreEntry;\nimport com.walmartlabs.concord.server.org.project.ProjectAccessManager;\nimport com.walmartlabs.concord.server.org.project.ProjectEntry;\nimport com.walmartlabs.concord.server.org.secret.SecretEntryV2;\nimport com.walmartlabs.concord.server.org.secret.SecretManager;\nimport com.walmartlabs.concord.server.org.team.TeamDao;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport com.walmartlabs.concord.server.user.UserDao;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.*;\nimport java.time.Duration;\nimport java.time.OffsetDateTime;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.Utils.unwrap;\n\n@Path(\"/api/v1/audit\")\n@Tag(name = \"Audit Log\")\npublic class AuditLogResource implements Resource {\n\n    private static final String DETAILS_QUERY_PARAMETER_PREFIX = \"details.\";\n\n    // array of the allowed keys in \"/api/v1/audit?details.[fieldName]=[value]\"\n    private static final String[] ALLOWED_DETAILS_KEYS = {\n            \"eventId\",\n            \"githubEvent\",\n            \"fullRepoName\",\n            \"orgId\",\n            \"orgName\",\n            \"projectId\",\n            \"projectName\",\n            \"secretId\",\n            \"secretName\",\n            \"source\",\n            \"jsonStoreId\",\n            \"jsonStoreName\",\n            \"teamId\",\n            \"teamName\"\n    };\n\n    private final OrganizationManager orgManager;\n    private final ProjectAccessManager projectAccessManager;\n    private final SecretManager secretManager;\n    private final JsonStoreAccessManager jsonStoreAccessManager;\n    private final UserDao userDao;\n    private final TeamDao teamDao;\n    private final AuditDao auditDao;\n    private final AuditConfiguration cfg;\n\n    @Inject\n    public AuditLogResource(OrganizationManager orgManager,\n                            ProjectAccessManager projectAccessManager,\n                            SecretManager secretManager,\n                            JsonStoreAccessManager jsonStoreAccessManager,\n                            UserDao userDao,\n                            TeamDao teamDao,\n                            AuditDao auditDao,\n                            AuditConfiguration cfg) {\n\n        this.orgManager = orgManager;\n        this.projectAccessManager = projectAccessManager;\n        this.secretManager = secretManager;\n        this.jsonStoreAccessManager = jsonStoreAccessManager;\n        this.userDao = userDao;\n        this.teamDao = teamDao;\n        this.auditDao = auditDao;\n        this.cfg = cfg;\n    }\n\n    /**\n     * Returns a list of audit log events for the specified filters.\n     * <p>\n     * The endpoint performs additional permission checks if an entity filter\n     * (org, project, etc) is specified. If no filters specified the admin\n     * privileges are required.\n     * <p>\n     * The endpoint ignores all \"unknown\" filters. Only the {@link #ALLOWED_DETAILS_KEYS}\n     * are allowed.\n     */\n    @GET\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"List audit log entries for the specified organization\")\n    public List<AuditLogEntry> list(@QueryParam(\"object\") AuditObject object,\n                                    @QueryParam(\"action\") AuditAction action,\n                                    @QueryParam(\"userId\") UUID userId,\n                                    @QueryParam(\"username\") String username,\n                                    @QueryParam(\"after\") OffsetDateTimeParam afterTimestamp,\n                                    @QueryParam(\"before\") OffsetDateTimeParam beforeTimestamp,\n                                    @QueryParam(\"offset\") @DefaultValue(\"0\") int offset,\n                                    @QueryParam(\"limit\") @DefaultValue(\"30\") int limit,\n                                    @Context UriInfo uriInfo) {\n\n        Map<String, String> details = getDetails(uriInfo);\n\n        UUID effectiveUserId = userId;\n        if (effectiveUserId == null && username != null) {\n            effectiveUserId = userDao.getId(username, null, null);\n            if (effectiveUserId == null) {\n                // no such user in our DB, there shouldn't be any audit logs anyway\n                return Collections.emptyList();\n            }\n        }\n\n        UUID effectiveOrgId = getEffectiveOrgId(details);\n        UUID effectiveProjectId = getEffectiveProjectId(effectiveOrgId, details);\n        UUID effectiveSecretId = getEffectiveSecretId(effectiveOrgId, details);\n        UUID effectiveJsonStoreId = getEffectiveJsonStoreId(effectiveOrgId, details);\n        UUID effectiveTeamId = getEffectiveTeamId(effectiveOrgId, details);\n        String source = details.get(\"source\");\n        Object eventId = details.get(\"eventId\");\n\n        // only admins are allowed to proceed without any entity filters\n        if (effectiveOrgId == null\n                && effectiveProjectId == null\n                && effectiveSecretId == null\n                && effectiveJsonStoreId == null\n                && effectiveTeamId == null\n                && source == null\n                && eventId == null) {\n\n            if (!Roles.isAdmin()) {\n                throw new UnauthorizedException(\"Only admins can retrieve audit events without filtering by entity.\");\n            }\n        }\n\n        ImmutableAuditLogFilter.Builder filterBuilder = AuditLogFilter.builder();\n\n        if (effectiveOrgId != null) {\n            filterBuilder.putDetails(\"orgId\", effectiveOrgId);\n        }\n\n        if (effectiveProjectId != null) {\n            filterBuilder.putDetails(\"projectId\", effectiveProjectId);\n        }\n\n        if (effectiveSecretId != null) {\n            filterBuilder.putDetails(\"secretId\", effectiveSecretId);\n        }\n\n        if (effectiveJsonStoreId != null) {\n            filterBuilder.putDetails(\"jsonStoreId\", effectiveJsonStoreId);\n        }\n\n        if (effectiveTeamId != null) {\n            filterBuilder.putDetails(\"teamId\", effectiveTeamId);\n        }\n\n        if (source != null) {\n            filterBuilder.putDetails(\"source\", source);\n        }\n\n        if (eventId != null) {\n            filterBuilder.putDetails(\"eventId\", eventId);\n        }\n\n        if (details.get(\"githubEvent\") != null) {\n            filterBuilder.putDetails(\"githubEvent\", details.get(\"githubEvent\"));\n        }\n\n        if (details.get(\"fullRepoName\") != null) {\n            filterBuilder.putDetails(\"payload\",\n                    Collections.singletonMap(\"repository\",\n                            Collections.singletonMap(\"full_name\", details.get(\"fullRepoName\"))));\n        }\n\n        assertTimeInterval(unwrap(afterTimestamp), unwrap(beforeTimestamp));\n\n        return auditDao.list(filterBuilder\n                .userId(effectiveUserId)\n                .object(object)\n                .action(action)\n                .after(unwrap(afterTimestamp))\n                .before(unwrap(beforeTimestamp))\n                .limit(limit)\n                .offset(offset)\n                .build());\n    }\n\n    private UUID getEffectiveOrgId(Map<String, String> details) {\n        UUID orgId = getUUID(details, \"orgId\");\n        String orgName = details.get(\"orgName\");\n\n        if (orgId != null || orgName != null) {\n            OrganizationEntry org = orgManager.assertAccess(orgId, orgName, true);\n            return org.getId();\n        }\n\n        return null;\n    }\n\n    private UUID getEffectiveProjectId(UUID effectiveOrgId, Map<String, String> details) {\n        UUID projectId = getUUID(details, \"projectId\");\n        String projectName = details.get(\"projectName\");\n\n        if (effectiveOrgId == null && projectId == null && projectName != null) {\n            throw new ValidationErrorsException(\"'orgId' or 'orgName' is required\");\n        }\n\n        if (projectId != null || projectName != null) {\n            ProjectEntry project = projectAccessManager.assertAccess(effectiveOrgId, projectId, projectName, ResourceAccessLevel.READER, true);\n            return project.getId();\n        }\n\n        return null;\n    }\n\n    private UUID getEffectiveSecretId(UUID effectiveOrgId, Map<String, String> details) {\n        UUID secretId = getUUID(details, \"secretId\");\n        String secretName = details.get(\"secretName\");\n\n        if (effectiveOrgId == null && secretId == null && secretName != null) {\n            throw new ValidationErrorsException(\"'orgId' or 'orgName' is required\");\n        }\n\n        if (secretId != null || secretName != null) {\n            SecretEntryV2 secret = secretManager.assertAccess(effectiveOrgId, secretId, secretName, ResourceAccessLevel.READER, true);\n            return secret.getId();\n        }\n\n        return null;\n    }\n\n    private UUID getEffectiveJsonStoreId(UUID effectiveOrgId, Map<String, String> details) {\n        UUID jsonStoreId = getUUID(details, \"jsonStoreId\");\n        String jsonStoreName = details.get(\"jsonStoreName\");\n\n        if (effectiveOrgId == null && jsonStoreId == null && jsonStoreName != null) {\n            throw new ValidationErrorsException(\"'orgId' or 'orgName' is required\");\n        }\n\n        if (jsonStoreId != null || jsonStoreName != null) {\n            JsonStoreEntry store = jsonStoreAccessManager.assertAccess(effectiveOrgId, jsonStoreId, jsonStoreName, ResourceAccessLevel.READER, true);\n            return store.id();\n        }\n\n        return null;\n    }\n\n    private UUID getEffectiveTeamId(UUID effectiveOrgId, Map<String, String> details) {\n        UUID teamId = getUUID(details, \"teamId\");\n        String teamName = details.get(\"teamName\");\n\n        if (effectiveOrgId == null && teamId == null && teamName != null) {\n            throw new ValidationErrorsException(\"'orgId' or 'orgName' is required\");\n        }\n\n        if (teamId != null || teamName != null) {\n            UUID effectiveTeamId = teamDao.getId(effectiveOrgId, teamName);\n            if (effectiveTeamId == null) {\n                throw new ValidationErrorsException(\"Team not found: \" + teamName);\n            }\n\n            return effectiveTeamId;\n        }\n\n        return null;\n    }\n\n    private static Map<String, String> getDetails(UriInfo uriInfo) {\n        MultivaluedMap<String, String> params = uriInfo.getQueryParameters();\n        return params.entrySet().stream()\n                .filter(e -> {\n                    for (String k : ALLOWED_DETAILS_KEYS) {\n                        if (e.getKey().equals(DETAILS_QUERY_PARAMETER_PREFIX + k)) {\n                            return true;\n                        }\n                    }\n                    return false;\n                })\n                .collect(Collectors.toMap(e -> e.getKey().substring(DETAILS_QUERY_PARAMETER_PREFIX.length()), e -> e.getValue().get(0)));\n    }\n\n    private static UUID getUUID(Map<String, String> m, String k) {\n        String s = m.get(k);\n        if (s == null) {\n            return null;\n        }\n\n        try {\n            return UUID.fromString(s);\n        } catch (IllegalArgumentException e) {\n            throw new ValidationErrorsException(\"Invalid request parameters. Expected a UUID in '\" + k + \"'\");\n        }\n    }\n\n    private void assertTimeInterval(OffsetDateTime after, OffsetDateTime before) {\n        if (cfg.getMaxSearchInterval() == null) {\n            return;\n        }\n\n        if (before == null && after == null) {\n            return;\n        }\n\n        if (after != null && before != null) {\n            if (Duration.between(after, before).compareTo(cfg.getMaxSearchInterval()) > 0) {\n                throw new ConcordApplicationException(\"Max search interval exceeded. Current: \" + Duration.between(after, before) + \", max: \" + cfg.getMaxSearchInterval(), Response.Status.BAD_REQUEST);\n            }\n        } else if (after != null) {\n            if (Duration.between(after, OffsetDateTime.now()).compareTo(cfg.getMaxSearchInterval()) > 0) {\n                throw new ConcordApplicationException(\"Max search interval exceeded\", Response.Status.BAD_REQUEST);\n            }\n        } else {\n            throw new ConcordApplicationException(\"Specify after parameter\", Response.Status.BAD_REQUEST);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/audit/AuditObject.java",
    "content": "package com.walmartlabs.concord.server.audit;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic enum AuditObject {\n\n    API_KEY,\n    EXTERNAL_EVENT,\n    JSON_STORE,\n    JSON_STORE_DATA,\n    JSON_STORE_QUERY,\n    ORGANIZATION,\n    POLICY,\n    PROJECT,\n    PROCESS,\n    ROLE,\n    SECRET,\n    SYSTEM,\n    TEAM,\n    USER,\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/BackgroundTasks.java",
    "content": "package com.walmartlabs.concord.server.boot;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.BackgroundTask;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.util.Set;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\n\npublic class BackgroundTasks {\n\n    private static final Logger log = LoggerFactory.getLogger(BackgroundTasks.class);\n\n    private final Set<BackgroundTask> tasks;\n    private final Lock controlMutex = new ReentrantLock();\n\n    @Inject\n    public BackgroundTasks(Set<BackgroundTask> tasks) {\n        this.tasks = Set.copyOf(tasks);\n    }\n\n    public void start() {\n        controlMutex.lock();\n        log.info(\"start -> starting {} task(s)\", tasks.size());\n        try {\n            tasks.forEach(BackgroundTask::start);\n        } finally {\n            controlMutex.unlock();\n        }\n    }\n\n    public void stop() {\n        controlMutex.lock();\n        try {\n            tasks.forEach(BackgroundTask::stop);\n        } finally {\n            controlMutex.unlock();\n        }\n    }\n\n    public int count() {\n        return tasks.size();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/ConcordSecurityManager.java",
    "content": "package com.walmartlabs.concord.server.boot;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.security.rememberme.ConcordRememberMeManager;\nimport org.apache.shiro.realm.Realm;\nimport org.apache.shiro.web.mgt.DefaultWebSecurityManager;\nimport org.apache.shiro.web.session.mgt.ServletContainerSessionManager;\n\nimport javax.inject.Inject;\nimport java.util.Set;\n\npublic class ConcordSecurityManager extends DefaultWebSecurityManager {\n\n    @Inject\n    public ConcordSecurityManager(Set<Realm> realms, ConcordRememberMeManager rememberMeManager) {\n        super(realms);\n        setSessionManager(new ServletContainerSessionManager());\n        setRememberMeManager(rememberMeManager);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/ContextHandlerConfigurator.java",
    "content": "package com.walmartlabs.concord.server.boot;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.eclipse.jetty.server.handler.ContextHandlerCollection;\n\n/**\n * Provides a way to configure additional {@link org.eclipse.jetty.server.Handler}.\n */\npublic interface ContextHandlerConfigurator {\n\n    void configure(ContextHandlerCollection collection);\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/CustomErrorHandler.java",
    "content": "package com.walmartlabs.concord.server.boot;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.eclipse.jetty.ee8.nested.ErrorHandler;\nimport org.eclipse.jetty.ee8.nested.Request;\nimport org.eclipse.jetty.http.HttpHeader;\n\nimport javax.servlet.ServletException;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.util.Set;\n\npublic class CustomErrorHandler extends ErrorHandler {\n\n    private final Set<RequestErrorHandler> handlers;\n\n    public CustomErrorHandler(Set<RequestErrorHandler> handlers) {\n        this.handlers = handlers;\n    }\n\n    @Override\n    public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {\n        for (RequestErrorHandler h : handlers) {\n            if (h.handle(request, response)) {\n                // automatically set the correct Cache-Control headers\n                String cacheControl = getCacheControl();\n                if (cacheControl != null) {\n                    response.setHeader(HttpHeader.CACHE_CONTROL.asString(), cacheControl);\n                }\n                return;\n            }\n        }\n\n        super.handle(target, baseRequest, request, response);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/FilterChainConfigurator.java",
    "content": "package com.walmartlabs.concord.server.boot;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.apache.shiro.web.filter.mgt.FilterChainManager;\n\n/**\n * Provides a way to configure Shiro's filter chains.\n */\npublic interface FilterChainConfigurator {\n\n    void configure(FilterChainManager manager);\n\n    default int priority() {\n        return 0;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/FormRequestErrorHandler.java",
    "content": "package com.walmartlabs.concord.server.boot;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.console.ResponseTemplates;\nimport org.eclipse.jetty.http.MimeTypes;\n\nimport javax.inject.Inject;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Collections;\nimport java.util.Map;\n\npublic class FormRequestErrorHandler implements RequestErrorHandler {\n\n    private static final String FORM_PATH_PREFIX = \"/forms/\";\n\n    private final ResponseTemplates responseTemplates;\n\n    @Inject\n    public FormRequestErrorHandler(ResponseTemplates responseTemplates) {\n        this.responseTemplates = responseTemplates;\n    }\n\n    @Override\n    public boolean handle(HttpServletRequest request, HttpServletResponse response) throws IOException {\n        if (response.getStatus() != 404) {\n            return false;\n        }\n\n        String path = request.getPathInfo();\n        if (path == null || !path.startsWith(\"/forms/\") || !path.endsWith(\"/form/\")) {\n            return false;\n        }\n\n        String instanceId = getInstanceId(path);\n        if (instanceId == null) {\n            return false;\n        }\n\n        OutputStream out = response.getOutputStream();\n        Map<String, Object> args = Collections.singletonMap(\"instanceId\", instanceId);\n        responseTemplates.formNotFound(out, args);\n\n        response.setContentType(MimeTypes.Type.TEXT_HTML.asString());\n        response.setCharacterEncoding(StandardCharsets.UTF_8.name());\n\n        return true;\n    }\n\n    private static String getInstanceId(String path) {\n        int start = path.indexOf(FORM_PATH_PREFIX);\n\n        if (start < 0) {\n            return null;\n        }\n\n        int end = path.indexOf('/', start + FORM_PATH_PREFIX.length());\n        if (end < 0) {\n            return null;\n        }\n\n        return path.substring(start + FORM_PATH_PREFIX.length(), end);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/HttpServer.java",
    "content": "package com.walmartlabs.concord.server.boot;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.rest.ApiDescriptor;\nimport com.walmartlabs.concord.server.cfg.ServerConfiguration;\nimport org.eclipse.jetty.ee8.nested.SessionHandler;\nimport org.eclipse.jetty.ee8.servlet.FilterHolder;\nimport org.eclipse.jetty.ee8.servlet.ServletContextHandler;\nimport org.eclipse.jetty.ee8.servlet.ServletHolder;\nimport org.eclipse.jetty.ee8.websocket.server.config.JettyWebSocketServletContainerInitializer;\nimport org.eclipse.jetty.http.UriCompliance;\nimport org.eclipse.jetty.jmx.MBeanContainer;\nimport org.eclipse.jetty.server.*;\nimport org.eclipse.jetty.server.handler.ContextHandlerCollection;\nimport org.eclipse.jetty.server.handler.StatisticsHandler;\nimport org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.annotation.Priority;\nimport javax.inject.Inject;\nimport javax.servlet.*;\nimport javax.servlet.annotation.WebFilter;\nimport javax.servlet.annotation.WebListener;\nimport javax.servlet.annotation.WebServlet;\nimport javax.servlet.http.HttpServlet;\nimport java.lang.management.ManagementFactory;\nimport java.util.Comparator;\nimport java.util.EnumSet;\nimport java.util.Set;\n\npublic class HttpServer {\n\n    private static final Logger log = LoggerFactory.getLogger(HttpServlet.class);\n\n    private final Server server;\n    private final int port;\n\n    @Inject\n    public HttpServer(ServerConfiguration cfg,\n                      Set<RequestErrorHandler> requestErrorHandlers,\n                      Set<ServletContextListener> contextListeners,\n                      Set<ApiDescriptor> apiDescriptors,\n                      Set<HttpServlet> servlets,\n                      Set<ServletHolder> servletHolders,\n                      Set<Filter> filters,\n                      Set<FilterHolder> filterHolders,\n                      Set<ContextHandlerConfigurator> contextHandlerConfigurators) {\n\n        Server server = new Server();\n\n        // init JMX\n        MBeanContainer mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer());\n        server.addBean(mbeanContainer);\n\n        // configure the request log\n        server.setRequestLog(createRequestLog(cfg));\n\n        // init http transport\n        HttpConfiguration httpCfg = new HttpConfiguration();\n        httpCfg.setRequestHeaderSize(cfg.getRequestHeaderSize());\n        httpCfg.addCustomizer(new ForwardedRequestCustomizer());\n\n        int cores = Runtime.getRuntime().availableProcessors();\n        int acceptors = Math.max(1, cores / 4);\n        int selectors = -1; // use the default value\n        ServerConnector http = new ServerConnector(server, acceptors, selectors, new HttpConnectionFactory(httpCfg));\n        http.setName(\"http\");\n        this.port = cfg.getPort();\n        http.setPort(port);\n        // TODO remove once the '/' escaping is fixed in clients\n        http.getConnectionFactory(HttpConnectionFactory.class)\n                .getHttpConfiguration()\n                .setUriCompliance(UriCompliance.LEGACY);\n        server.addConnector(http);\n\n        // servlets, filters, etc...\n        ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);\n        contextHandler.setContextPath(\"/\");\n\n        // custom 404s and other error handlers\n        contextHandler.setErrorHandler(new CustomErrorHandler(requestErrorHandlers));\n\n        // session timeout\n        SessionHandler sessionHandler = contextHandler.getSessionHandler();\n        log.info(\"Session timeout: {}\", cfg.getSessionTimeout());\n        sessionHandler.setMaxInactiveInterval((int) cfg.getSessionTimeout().getSeconds());\n\n        // session cookies\n        ServletContext context = contextHandler.getServletContext();\n        SessionCookieConfig sessionCookieConfig = context.getSessionCookieConfig();\n        sessionCookieConfig.setHttpOnly(true);\n        sessionCookieConfig.setComment(cfg.getCookieComment());\n        if (cfg.isSecureCookies()) {\n            sessionCookieConfig.setSecure(true);\n        }\n\n        // init all @WebListeners\n        contextListeners.stream().sorted(byPriority()).forEachOrdered(listener -> {\n            WebListener annotation = listener.getClass().getAnnotation(WebListener.class);\n            if (annotation == null) {\n                return;\n            }\n\n            log.info(\"Event listener -> {}\", listener.getClass());\n            contextHandler.addEventListener(listener);\n        });\n\n        // init all Resteasy endpoints\n        apiDescriptors.forEach(api -> {\n            ServletHolder holder = new ServletHolder(HttpServletDispatcher.class);\n            for (String pathSpec : api.paths()) {\n                log.info(\"Serving API endpoints @ {}\", pathSpec);\n                contextHandler.addServlet(holder, pathSpec);\n            }\n        });\n\n        // init all @WebServlets\n        servlets.stream().sorted(byPriority()).forEachOrdered(servlet -> {\n            WebServlet annotation = servlet.getClass().getAnnotation(WebServlet.class);\n            if (annotation == null) {\n                return;\n            }\n\n            ServletHolder holder = new ServletHolder(servlet);\n            for (String pathSpec : annotation.value()) {\n                log.info(\"Servlet -> {} @ {}\", servlet.getClass(), pathSpec);\n                contextHandler.addServlet(holder, pathSpec);\n            }\n        });\n\n        servletHolders.stream().sorted(byPriority()).forEachOrdered(holder -> {\n            WebServlet annotation = holder.getClass().getAnnotation(WebServlet.class);\n            if (annotation == null) {\n                return;\n            }\n\n            for (String pathSpec : annotation.value()) {\n                log.info(\"Servlet -> {} @ {}\", holder.getClass(), pathSpec);\n                contextHandler.addServlet(holder, pathSpec);\n            }\n        });\n\n        // init all @WebFilters\n        filters.stream().sorted(byPriority()).forEachOrdered(filter -> {\n            WebFilter annotation = filter.getClass().getAnnotation(WebFilter.class);\n            if (annotation == null) {\n                return;\n            }\n\n            FilterHolder holder = new FilterHolder(filter);\n            for (String pathSpec : annotation.value()) {\n                log.info(\"Filter -> {} @ {}\", filter.getClass(), pathSpec);\n                contextHandler.addFilter(holder, pathSpec, EnumSet.allOf(DispatcherType.class));\n            }\n        });\n\n        filterHolders.stream().sorted(byPriority()).forEachOrdered(holder -> {\n            WebFilter annotation = holder.getClass().getAnnotation(WebFilter.class);\n            if (annotation == null) {\n                return;\n            }\n\n            for (String pathSpec : annotation.value()) {\n                log.info(\"Filter -> {} @ {}\", holder.getClass(), pathSpec);\n                contextHandler.addFilter(holder, pathSpec, EnumSet.allOf(DispatcherType.class));\n            }\n        });\n\n        JettyWebSocketServletContainerInitializer.configure(contextHandler, null);\n\n        ContextHandlerCollection contextHandlerCollection = new ContextHandlerCollection();\n        contextHandlerCollection.addHandler(contextHandler);\n\n        // additional handlers\n        contextHandlerConfigurators.stream().sorted(byPriority()).forEachOrdered(configurator -> {\n            log.info(\"Configuring additional context handlers {}...\", configurator.getClass());\n            configurator.configure(contextHandlerCollection);\n        });\n\n        StatisticsHandler statisticsHandler = new StatisticsHandler();\n        statisticsHandler.setHandler(contextHandlerCollection);\n        server.setHandler(statisticsHandler);\n\n        this.server = server;\n    }\n\n    public void start() throws Exception {\n        this.server.start();\n        log.info(\"start -> listening on http://0.0.0.0:{}\", port);\n    }\n\n    public void stop() throws Exception {\n        this.server.stop();\n    }\n\n    private static RequestLog createRequestLog(ServerConfiguration cfg) {\n        String path = cfg.getAccessLogPath();\n        if (path == null) {\n            log.warn(\"Access logs are not configured. Specify the ACCESS_LOG_PATH environment variable before starting the server.\");\n            return null;\n        }\n\n        log.info(\"Saving access logs into {}\", path);\n\n        RequestLogWriter writer = new AsyncRequestLogWriter(path);\n        writer.setAppend(true);\n        writer.setRetainDays(cfg.getAccessLogRetainDays());\n\n        return new CustomRequestLog(writer, ServerConfiguration.ACCESS_LOG_FORMAT);\n    }\n\n    private static Comparator<Object> byPriority() {\n        return Comparator.comparingInt(o -> {\n            var a = o.getClass().getAnnotation(Priority.class);\n            return a != null ? a.value() : 0;\n        });\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/RequestErrorHandler.java",
    "content": "package com.walmartlabs.concord.server.boot;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\npublic interface RequestErrorHandler {\n\n    boolean handle(HttpServletRequest request, HttpServletResponse response) throws IOException;\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/ShiroListener.java",
    "content": "package com.walmartlabs.concord.server.boot;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.apache.shiro.mgt.SecurityManager;\nimport org.apache.shiro.web.env.EnvironmentLoader;\nimport org.apache.shiro.web.env.WebEnvironment;\nimport org.apache.shiro.web.filter.mgt.FilterChainManager;\nimport org.apache.shiro.web.filter.mgt.FilterChainResolver;\nimport org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;\nimport org.apache.shiro.web.mgt.WebSecurityManager;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.servlet.ServletContext;\nimport javax.servlet.ServletContextEvent;\nimport javax.servlet.ServletContextListener;\nimport javax.servlet.annotation.WebListener;\nimport java.util.Comparator;\nimport java.util.Set;\n\n/**\n * Initializes Shiro's {@link org.apache.shiro.env.Environment}.\n * Responsible for applying all available {@link FilterChainConfigurator}.\n */\n@WebListener\npublic class ShiroListener implements ServletContextListener {\n\n    private static final Logger log = LoggerFactory.getLogger(ShiroListener.class);\n\n    private final WebSecurityManager webSecurityManager;\n    private final Set<FilterChainConfigurator> filterChainConfigurators;\n\n    @Inject\n    public ShiroListener(WebSecurityManager webSecurityManager,\n                         Set<FilterChainConfigurator> filterChainConfigurators) {\n\n        this.webSecurityManager = webSecurityManager;\n        this.filterChainConfigurators = filterChainConfigurators;\n    }\n\n    @Override\n    public void contextInitialized(ServletContextEvent sce) {\n        ServletContext context = sce.getServletContext();\n        context.setAttribute(EnvironmentLoader.ENVIRONMENT_ATTRIBUTE_KEY, new WebEnvironment() {\n            @Override\n            public FilterChainResolver getFilterChainResolver() {\n                PathMatchingFilterChainResolver resolver = new PathMatchingFilterChainResolver();\n                FilterChainManager manager = resolver.getFilterChainManager();\n\n                filterChainConfigurators.stream()\n                        .sorted(Comparator.comparingInt(FilterChainConfigurator::priority))\n                        .forEach(c -> {\n                            log.info(\"Configuring chains {}...\", c.getClass());\n                            c.configure(manager);\n                        });\n\n                return resolver;\n            }\n\n            @Override\n            public ServletContext getServletContext() {\n                return context;\n            }\n\n            @Override\n            public WebSecurityManager getWebSecurityManager() {\n                return webSecurityManager;\n            }\n\n            @Override\n            public SecurityManager getSecurityManager() {\n                return webSecurityManager;\n            }\n        });\n    }\n\n    @Override\n    public void contextDestroyed(ServletContextEvent sce) {\n        ServletContext context = sce.getServletContext();\n        context.removeAttribute(EnvironmentLoader.ENVIRONMENT_ATTRIBUTE_KEY);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/filters/AuthenticationHandler.java",
    "content": "package com.walmartlabs.concord.server.boot.filters;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.apache.shiro.authc.AuthenticationToken;\n\nimport javax.servlet.ServletRequest;\nimport javax.servlet.ServletResponse;\nimport java.io.IOException;\n\npublic interface AuthenticationHandler {\n\n    default AuthenticationToken createToken(ServletRequest request, ServletResponse response) {\n        return null;\n    }\n\n    default boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {\n        return false;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/filters/CORSFilter.java",
    "content": "package com.walmartlabs.concord.server.boot.filters;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.cfg.ServerConfiguration;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.servlet.*;\nimport javax.servlet.annotation.WebFilter;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\n@WebFilter({\"/api/*\", \"/forms/*\"})\npublic class CORSFilter implements Filter {\n\n    private static final Logger log = LoggerFactory.getLogger(CORSFilter.class);\n\n    private final ServerConfiguration cfg;\n\n    @Inject\n    public CORSFilter(ServerConfiguration cfg) {\n        this.cfg = cfg;\n    }\n\n    @Override\n    public void init(FilterConfig filterConfig) {\n        log.info(\"CORS filter enabled\");\n    }\n\n    @Override\n    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {\n        HttpServletResponse httpResp = (HttpServletResponse) response;\n        httpResp.setHeader(\"Access-Control-Allow-Origin\", cfg.getCORSConfiguration().getAllowOrigin());\n        httpResp.setHeader(\"Access-Control-Allow-Methods\", \"*\");\n        httpResp.setHeader(\"Access-Control-Allow-Headers\", \"Authorization, Content-Type, Range, Cookie, Origin\");\n        httpResp.setHeader(\"Access-Control-Expose-Headers\", \"cache-control,\" +\n                \"content-language,\" +\n                \"expires,\" +\n                \"last-modified,\" +\n                \"content-range,\" +\n                \"content-length,\" +\n                \"accept-ranges\");\n\n        HttpServletRequest httpReq = (HttpServletRequest) request;\n        if (\"OPTIONS\".equalsIgnoreCase(httpReq.getMethod())) {\n            httpResp.setHeader(\"Allow\", \"OPTIONS, GET, POST, PUT, DELETE\");\n            httpResp.setStatus(204);\n            return;\n        }\n\n        chain.doFilter(request, response);\n    }\n\n    @Override\n    public void destroy() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/filters/ConcordAuthenticatingFilter.java",
    "content": "package com.walmartlabs.concord.server.boot.filters;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.Meter;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.server.RequestUtils;\nimport com.walmartlabs.concord.server.sdk.metrics.InjectMeter;\nimport com.walmartlabs.concord.server.security.SecurityUtils;\nimport com.walmartlabs.concord.server.security.apikey.ApiKey;\nimport org.apache.shiro.authc.AuthenticationException;\nimport org.apache.shiro.authc.AuthenticationToken;\nimport org.apache.shiro.authc.UsernamePasswordToken;\nimport org.apache.shiro.subject.PrincipalCollection;\nimport org.apache.shiro.subject.Subject;\nimport org.apache.shiro.util.ThreadContext;\nimport org.apache.shiro.web.filter.authc.AuthenticatingFilter;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.servlet.ServletRequest;\nimport javax.servlet.ServletResponse;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport javax.ws.rs.core.HttpHeaders;\nimport javax.ws.rs.core.MediaType;\nimport java.io.IOException;\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.Set;\n\npublic class ConcordAuthenticatingFilter extends AuthenticatingFilter {\n\n    private static final Logger log = LoggerFactory.getLogger(ConcordAuthenticatingFilter.class);\n\n    /**\n     * List of URLs on which 'WWW-Authenticate: Basic' is not returned.\n     */\n    private static final String[] DO_NOT_FORCE_BASIC_AUTH_URLS = {\n            \"/api/service/console/whoami\"\n    };\n\n    private final Set<AuthenticationHandler> authenticationHandlers;\n\n    @InjectMeter\n    private final Meter successAuths;\n\n    @InjectMeter\n    private final Meter failedAuths;\n\n    @Inject\n    public ConcordAuthenticatingFilter(Set<AuthenticationHandler> authenticationHandlers,\n                                       Meter successAuths,\n                                       Meter failedAuths) {\n\n        this.authenticationHandlers = authenticationHandlers;\n        this.successAuths = successAuths;\n        this.failedAuths = failedAuths;\n    }\n\n    @Override\n    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {\n        Subject subject = SecurityUtils.getSubject();\n        if (subject != null && subject.isRemembered()) {\n            AuthenticationToken t = getFirstToken(subject);\n            if (t != null) {\n                return t;\n            }\n        }\n\n        // run plugins first, they might need to override the default behaviour\n        // e.g. use their own `Authorization: Bearer` headers\n        for (AuthenticationHandler handler : authenticationHandlers) {\n            AuthenticationToken token = handler.createToken(request, response);\n            if (token != null) {\n                return token;\n            }\n        }\n\n        // no dice\n        return new UnauthenticatedToken();\n    }\n\n    @Override\n    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {\n        HttpServletResponse resp = (HttpServletResponse) response;\n\n        try {\n            boolean loggedIn = executeLogin(request, response);\n            if (loggedIn) {\n                return true;\n            }\n\n            boolean handled = false;\n            for (AuthenticationHandler handler : authenticationHandlers) {\n                handled = handler.onAccessDenied(request, response);\n                if (handled) {\n                    break;\n                }\n            }\n\n            if (handled) {\n                return true;\n            }\n\n            sendUnauthorized(resp);\n            reportAuthSchemes(request, response);\n        } catch (Exception e) {\n            sendUnauthorized(resp, e);\n        }\n\n        return false;\n    }\n\n    @Override\n    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {\n        successAuths.mark();\n        return super.onLoginSuccess(token, subject, request, response);\n    }\n\n    @Override\n    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {\n        log.debug(\"onLoginFailure ['{}'] -> login failed ({}): {}\", token, request.getRemoteAddr(), e.getMessage());\n        failedAuths.mark();\n\n        Subject s = ThreadContext.getSubject();\n        if (s != null && (s.isRemembered() || s.isAuthenticated())) {\n            s.logout();\n        }\n\n        return super.onLoginFailure(token, e, request, response);\n    }\n\n    private static void sendUnauthorized(HttpServletResponse resp) {\n        resp.setContentType(MediaType.APPLICATION_JSON);\n        resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);\n    }\n\n    private static void sendUnauthorized(HttpServletResponse resp, Throwable t) throws IOException {\n        resp.setContentType(MediaType.APPLICATION_JSON);\n        resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);\n\n        Map<String, String> error = Collections.singletonMap(\"message\", t.getMessage());\n        resp.getWriter().write(new ObjectMapper().writeValueAsString(error));\n    }\n\n    private static AuthenticationToken getFirstToken(Subject subject) {\n        PrincipalCollection principals = subject.getPrincipals();\n        if (principals == null || principals.isEmpty()) {\n            return null;\n        }\n\n        AuthenticationToken t = principals.oneByType(UsernamePasswordToken.class);\n        if (t != null) {\n            return t;\n        }\n\n        return principals.oneByType(ApiKey.class);\n    }\n\n    private static void reportAuthSchemes(ServletRequest request, ServletResponse response) {\n        HttpServletRequest req = (HttpServletRequest) request;\n        String p = req.getRequestURI();\n        for (String s : DO_NOT_FORCE_BASIC_AUTH_URLS) {\n            if (p.matches(s)) {\n                return;\n            }\n        }\n\n        String uiRequest = req.getHeader(RequestUtils.UI_REQUEST_HEADER);\n        if (\"true\".equalsIgnoreCase(uiRequest)) {\n            // do not send the \"WWW-Authenticate\" header if the request originates from the UI\n            // we don't want the basic auth popup there\n            return;\n        }\n\n        HttpServletResponse resp = (HttpServletResponse) response;\n\n        String authHeader = req.getHeader(HttpHeaders.AUTHORIZATION);\n        if (authHeader == null || authHeader.contains(\"Basic\")) {\n            resp.addHeader(HttpHeaders.WWW_AUTHENTICATE, \"Basic\");\n            resp.addHeader(HttpHeaders.WWW_AUTHENTICATE, \"ConcordApiToken\");\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/filters/ConcordFilterChainConfigurator.java",
    "content": "package com.walmartlabs.concord.server.boot.filters;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.boot.FilterChainConfigurator;\nimport com.walmartlabs.concord.server.security.GithubAuthenticatingFilter;\nimport com.walmartlabs.concord.server.security.LocalRequestFilter;\nimport org.apache.shiro.web.filter.mgt.FilterChainManager;\n\nimport javax.inject.Inject;\n\npublic class ConcordFilterChainConfigurator implements FilterChainConfigurator {\n\n    private final ConcordAuthenticatingFilter concordAuthenticatingFilter;\n    private final GithubAuthenticatingFilter githubAuthenticatingFilter;\n\n    @Inject\n    public ConcordFilterChainConfigurator(ConcordAuthenticatingFilter concordAuthenticatingFilter,\n                                          GithubAuthenticatingFilter githubAuthenticatingFilter) {\n\n        this.concordAuthenticatingFilter = concordAuthenticatingFilter;\n        this.githubAuthenticatingFilter = githubAuthenticatingFilter;\n    }\n\n    @Override\n    public void configure(FilterChainManager manager) {\n        // allow access w/o auth\n        manager.createChain(\"/api/v1/server/ping\", \"anon\");\n        manager.createChain(\"/api/v1/server/version\", \"anon\");\n        manager.createChain(\"/api/service/console/logout\", \"anon\");\n        manager.createChain(\"/api/service/console/cfg\", \"anon\");\n\n        // local requests only\n        manager.addFilter(\"local\", new LocalRequestFilter());\n        manager.createChain(\"/api/v1/server/maintenance-mode\", \"local\");\n\n        // regular auth\n        manager.addFilter(\"concord\", concordAuthenticatingFilter);\n        manager.createChain(\"/api/**\", \"concord\");\n        manager.createChain(\"/forms/**\", \"concord\");\n\n        // special auth for GitHub\n        manager.addFilter(\"github\", githubAuthenticatingFilter);\n        manager.createChain(\"/events/github/**\", \"github\");\n    }\n\n    @Override\n    public int priority() {\n        // because most of the filters here are using \"wide\" patterns\n        // like /api/** we need to run this configurator last\n        return 100;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/filters/NoCacheFilter.java",
    "content": "package com.walmartlabs.concord.server.boot.filters;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.servlet.*;\nimport javax.servlet.annotation.WebFilter;\nimport javax.servlet.http.HttpServletResponse;\nimport javax.ws.rs.core.HttpHeaders;\nimport java.io.IOException;\n\n@WebFilter({\"/api/*\", \"/forms/*\", \"/cfg.js\"})\npublic class NoCacheFilter implements Filter {\n\n    private static final Logger log = LoggerFactory.getLogger(NoCacheFilter.class);\n\n    @Override\n    public void init(FilterConfig filterConfig) {\n        log.info(\"NoCache filter enabled\");\n    }\n\n    @Override\n    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {\n        HttpServletResponse httpResp = (HttpServletResponse) response;\n        httpResp.setHeader(HttpHeaders.CACHE_CONTROL, \"no-cache, no-store, must-revalidate\");\n        httpResp.setHeader(\"Pragma\", \"no-cache\");\n        httpResp.setHeader(HttpHeaders.EXPIRES, \"0\");\n        chain.doFilter(request, response);\n    }\n\n    @Override\n    public void destroy() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/filters/QoSFilter.java",
    "content": "package com.walmartlabs.concord.server.boot.filters;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.cfg.QosConfiguration;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport javax.servlet.*;\nimport javax.servlet.annotation.WebFilter;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport javax.servlet.http.HttpSession;\nimport java.io.IOException;\nimport java.util.Queue;\nimport java.util.concurrent.ConcurrentLinkedQueue;\nimport java.util.concurrent.Semaphore;\nimport java.util.concurrent.TimeUnit;\nimport java.util.regex.Pattern;\n\n/**\n * copy from {@link org.eclipse.jetty.servlets.QoSFilter} but with custom error code\n */\n@WebFilter(value = {\"/api/v1/process/*\", \"/api/v1/org/*\"})\npublic class QoSFilter implements Filter {\n\n    private static final int TOO_MANY_REQUESTS_CODE = 429;\n\n    private static final int DEFAULT_MAX_PRIORITY = 2;\n\n    // currently we only care about `POST /api/v1/process`\n    // and `GET /api/v1/org/{orgName}/project/{projectName}/repo/{repoName}/start/{entryPoint}`\n    // requests (i.e. process start requests)\n    private static UrlPattern[] PATTERNS = {\n            UrlPattern.prefix(\"/api/v1/process\", \"POST\"),\n            UrlPattern.regexp(\"^/api/v1/org/[^/]*/project/[^/]*/repo/[^/]*/start/[^/]+$\", \"GET\")\n    };\n\n    private final String _suspended = \"QoSFilter@\" + Integer.toHexString(hashCode()) + \".SUSPENDED\";\n    private final String _resumed = \"QoSFilter@\" + Integer.toHexString(hashCode()) + \".RESUMED\";\n\n    private final long waitMs;\n    private final long suspendMs;\n    private final int maxRequests;\n\n    private final Queue<AsyncContext>[] queues;\n    private final AsyncListener[] listeners;\n    private final Semaphore passes;\n\n    @Inject\n    public QoSFilter(QosConfiguration qosConfiguration) {\n        this.maxRequests = qosConfiguration.getMaxRequests();\n        this.waitMs = qosConfiguration.getMaxWait().toMillis();\n        this.suspendMs = qosConfiguration.getSuspend().toMillis();\n\n        this.queues = new AsyncContextQueue[DEFAULT_MAX_PRIORITY + 1];\n        this.listeners = new AsyncListener[this.queues.length];\n        this.passes = new Semaphore(this.maxRequests, true);\n    }\n\n    @Override\n    public void init(FilterConfig filterConfig) {\n        if (isDisabled()) {\n            return;\n        }\n\n        for (int p = 0; p < this.queues.length; ++p) {\n            this.queues[p] = new AsyncContextQueue();\n            this.listeners[p] = new QoSAsyncListener(p);\n        }\n    }\n\n    @Override\n    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {\n        if (isDisabled()) {\n            chain.doFilter(request, response);\n            return;\n        }\n\n        if (needProcessRequest(request)) {\n            filter(request, response, chain);\n        } else {\n            chain.doFilter(request, response);\n        }\n    }\n\n    @Override\n    public void destroy() {\n        // do nothing\n    }\n\n    private boolean needProcessRequest(ServletRequest request) {\n        HttpServletRequest req = (HttpServletRequest) request;\n\n        String uri = req.getRequestURI();\n        if (uri == null) {\n            return false;\n        }\n\n        String method = req.getMethod();\n        for (UrlPattern p : PATTERNS) {\n            if (p.method().equalsIgnoreCase(method)) {\n                if (p.prefix() != null && uri.startsWith(p.prefix())) {\n                    return true;\n                }\n                if (p.regexp() != null && p.regexp().matcher(uri).matches()) {\n                    return true;\n                }\n            }\n        }\n\n        return false;\n    }\n\n    private boolean isDisabled() {\n        return maxRequests < 0;\n    }\n\n    private void filter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {\n        boolean accepted = false;\n        try {\n            Boolean suspended = (Boolean) request.getAttribute(_suspended);\n            if (suspended == null) {\n                accepted = passes.tryAcquire(waitMs, TimeUnit.MILLISECONDS);\n                if (accepted) {\n                    request.setAttribute(_suspended, Boolean.FALSE);\n                } else {\n                    request.setAttribute(_suspended, Boolean.TRUE);\n                    int priority = getPriority(request);\n                    AsyncContext asyncContext = request.startAsync();\n                    if (suspendMs > 0) {\n                        asyncContext.setTimeout(suspendMs);\n                    }\n                    asyncContext.addListener(listeners[priority]);\n                    queues[priority].add(asyncContext);\n                    return;\n                }\n            } else {\n                if (suspended) {\n                    request.setAttribute(_suspended, Boolean.FALSE);\n                    Boolean resumed = (Boolean) request.getAttribute(_resumed);\n                    if (Boolean.TRUE.equals(resumed)) {\n                        passes.acquire();\n                        accepted = true;\n                    } else {\n                        // Timeout! try 1 more time.\n                        accepted = passes.tryAcquire(waitMs, TimeUnit.MILLISECONDS);\n                    }\n                } else {\n                    // Pass through resume of previously accepted request.\n                    passes.acquire();\n                    accepted = true;\n                }\n            }\n\n            if (accepted) {\n                chain.doFilter(request, response);\n            } else {\n                ((HttpServletResponse) response).sendError(TOO_MANY_REQUESTS_CODE);\n            }\n        } catch (InterruptedException e) {\n            ((HttpServletResponse) response).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);\n            Thread.currentThread().interrupt();\n        } finally {\n            if (accepted) {\n                for (int p = queues.length - 1; p >= 0; --p) {\n                    AsyncContext asyncContext = queues[p].poll();\n                    if (asyncContext != null) {\n                        ServletRequest candidate = asyncContext.getRequest();\n                        Boolean suspended = (Boolean) candidate.getAttribute(_suspended);\n                        if (Boolean.TRUE.equals(suspended)) {\n                            candidate.setAttribute(_resumed, Boolean.TRUE);\n                            asyncContext.dispatch();\n                            break;\n                        }\n                    }\n                }\n                passes.release();\n            }\n        }\n    }\n\n    private int getPriority(ServletRequest request) {\n        HttpServletRequest baseRequest = (HttpServletRequest) request;\n        if (baseRequest.getUserPrincipal() != null) {\n            return 2;\n        } else {\n            HttpSession session = baseRequest.getSession(false);\n            if (session != null && !session.isNew())\n                return 1;\n            else\n                return 0;\n        }\n    }\n\n    private static class AsyncContextQueue extends ConcurrentLinkedQueue<AsyncContext> {\n        private static final long serialVersionUID = 1L;\n    }\n\n    private class QoSAsyncListener implements AsyncListener {\n\n        private final int priority;\n\n        QoSAsyncListener(int priority) {\n            this.priority = priority;\n        }\n\n        @Override\n        public void onStartAsync(AsyncEvent event) {\n        }\n\n        @Override\n        public void onComplete(AsyncEvent event) {\n        }\n\n        @Override\n        public void onTimeout(AsyncEvent event) {\n            // Remove before it's redispatched, so it won't be\n            // redispatched again at the end of the filtering.\n            AsyncContext asyncContext = event.getAsyncContext();\n            queues[priority].remove(asyncContext);\n            asyncContext.dispatch();\n        }\n\n        @Override\n        public void onError(AsyncEvent event) {\n        }\n    }\n\n    @Value.Immutable\n    interface UrlPattern {\n\n        @Nullable\n        Pattern regexp();\n\n        @Nullable\n        String prefix();\n\n        String method();\n\n        static UrlPattern prefix(String prefix, String method) {\n            return ImmutableUrlPattern.builder()\n                    .prefix(prefix)\n                    .method(method)\n                    .build();\n        }\n\n        static UrlPattern regexp(String regexp, String method) {\n            return ImmutableUrlPattern.builder()\n                    .regexp(Pattern.compile(regexp))\n                    .method(method)\n                    .build();\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/filters/RequestContextFilter.java",
    "content": "package com.walmartlabs.concord.server.boot.filters;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.RequestContext;\nimport com.walmartlabs.concord.server.RequestUtils;\n\nimport javax.servlet.*;\nimport javax.servlet.annotation.WebFilter;\nimport javax.servlet.http.HttpServletRequest;\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\n\n/**\n * Handles the initialization of {@link RequestContext} for each request.\n */\n@WebFilter({\"/api/*\", \"/forms/*\"})\npublic class RequestContextFilter implements Filter {\n\n    private static final String REQUEST_ID_KEY = \"_requestId\";\n    private static final String[] EXTRA_HEADER_KEYS = {RequestUtils.UI_REQUEST_HEADER};\n\n    @Override\n    public void init(FilterConfig filterConfig) {\n    }\n\n    @Override\n    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {\n        UUID id = (UUID) request.getAttribute(REQUEST_ID_KEY);\n\n        if (id == null) {\n            id = UUID.randomUUID();\n            request.setAttribute(REQUEST_ID_KEY, id);\n        }\n\n        Map<String, String> extraHeaders = new HashMap<>(EXTRA_HEADER_KEYS.length);\n        if (request instanceof HttpServletRequest) {\n            HttpServletRequest httpReq = (HttpServletRequest) request;\n            for (String k : EXTRA_HEADER_KEYS) {\n                String s = httpReq.getHeader(k);\n                if (s != null && !s.isEmpty()) {\n                    extraHeaders.put(k, s);\n                }\n            }\n        }\n\n        try {\n            RequestContext.set(id, request.getRemoteAddr(), extraHeaders);\n            chain.doFilter(request, response);\n        } finally {\n            RequestContext.clear();\n        }\n    }\n\n    @Override\n    public void destroy() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/filters/ShiroFilterHolder.java",
    "content": "package com.walmartlabs.concord.server.boot.filters;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.apache.shiro.web.servlet.ShiroFilter;\nimport org.eclipse.jetty.ee8.servlet.FilterHolder;\n\nimport javax.servlet.annotation.WebFilter;\n\n/**\n * Binds {@link ShiroFilter} to Concord's API root path.\n */\n@WebFilter(\"/*\")\npublic class ShiroFilterHolder extends FilterHolder {\n\n    public ShiroFilterHolder() {\n        super(ShiroFilter.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/filters/UnauthenticatedToken.java",
    "content": "package com.walmartlabs.concord.server.boot.filters;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.apache.shiro.authc.AuthenticationToken;\n\npublic final class UnauthenticatedToken implements AuthenticationToken {\n\n    @Override\n    public Object getPrincipal() {\n        return \"\";\n    }\n\n    @Override\n    public Object getCredentials() {\n        return \"\";\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/resteasy/ConcordApiDescriptor.java",
    "content": "package com.walmartlabs.concord.server.boot.resteasy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.rest.ApiDescriptor;\n\npublic class ConcordApiDescriptor implements ApiDescriptor {\n\n    @Override\n    public String[] paths() {\n        return new String[]{\n                \"/api/*\",\n                \"/events/github/*\"\n        };\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/resteasy/ExceptionMapperSupport.java",
    "content": "package com.walmartlabs.concord.server.boot.resteasy;\n\n/*\n * Copyright (c) 2007-2014 Sonatype, Inc. All rights reserved.\n *\n * This program is licensed to you under the Apache License Version 2.0,\n * and you may not use this file except in compliance with the Apache License Version 2.0.\n * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the Apache License Version 2.0 is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.\n *\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.rest.Component;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.ws.rs.core.Response;\nimport javax.ws.rs.ext.ExceptionMapper;\n\n/**\n * Based on the original {@link org.sonatype.siesta.ExceptionMapperSupport}.\n */\npublic abstract class ExceptionMapperSupport<E extends Throwable> implements ExceptionMapper<E>, Component {\n\n    protected final Logger log = LoggerFactory.getLogger(getClass());\n\n    public Response toResponse(E exception) {\n        if (exception == null) {\n            throw new NullPointerException();\n        }\n\n        // debug/trace log exception details\n        if (log.isTraceEnabled()) {\n            log.trace(\"Mapping exception: \" + exception, exception);\n        } else {\n            log.debug(\"Mapping exception: \" + exception);\n        }\n\n        // Prepare the response\n        Response response;\n        try {\n            response = convert(exception);\n        } catch (Exception e) {\n            log.warn(\"Failed to map exception\", e);\n            response = Response.serverError().entity(e.getMessage()).build();\n        }\n\n        // Log terse (unless debug enabled) warning with fault details\n        Object entity = response.getEntity();\n        log.warn(\"Response: [{}] {}; mapped from: {}\",\n                response.getStatus(),\n                entity == null ? \"(no entity/body)\" : String.format(\"'%s'\", entity),\n                exception,\n                log.isDebugEnabled() ? exception : null\n        );\n\n        return response;\n    }\n\n    /**\n     * Convert the given exception into a response.\n     *\n     * @param exception The exception to convert.\n     */\n    protected abstract Response convert(E exception);\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/resteasy/GuiceResourceFactory.java",
    "content": "package com.walmartlabs.concord.server.boot.resteasy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Provider;\nimport org.jboss.resteasy.spi.*;\n\npublic class GuiceResourceFactory implements ResourceFactory {\n\n    private final Provider<?> provider;\n    private final Class<?> scannableClass;\n    private PropertyInjector propertyInjector;\n\n    public GuiceResourceFactory(Provider<?> provider, final Class<?> scannableClass) {\n        this.provider = provider;\n        this.scannableClass = scannableClass;\n    }\n\n    public Class<?> getScannableClass() {\n        return scannableClass;\n    }\n\n    public void registered(ResteasyProviderFactory factory) {\n        propertyInjector = factory.getInjectorFactory().createPropertyInjector(scannableClass, factory);\n    }\n\n    @Override\n    public Object createResource(final HttpRequest request, final HttpResponse response, final ResteasyProviderFactory factory) {\n        var resource = provider.get();\n        var propertyStage = propertyInjector.inject(request, response, resource, true);\n        return propertyStage == null ? resource : propertyStage\n                .thenApply(v -> resource);\n    }\n\n    public void requestFinished(final HttpRequest request, final HttpResponse response, final Object resource) {\n    }\n\n    public void unregistered() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/resteasy/ObjectMapperContextResolver.java",
    "content": "package com.walmartlabs.concord.server.boot.resteasy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.common.ObjectMapperProvider;\nimport com.walmartlabs.concord.server.sdk.rest.Component;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.ext.ContextResolver;\nimport javax.ws.rs.ext.Provider;\n\n@Provider\npublic class ObjectMapperContextResolver implements ContextResolver<ObjectMapper>, Component {\n\n    private final ObjectMapperProvider delegate;\n\n    @Inject\n    public ObjectMapperContextResolver(ObjectMapperProvider delegate) {\n        this.delegate = delegate;\n    }\n\n    @Override\n    public ObjectMapper getContext(Class<?> type) {\n        return delegate.get();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/resteasy/ResteasyBootstrapListener.java",
    "content": "package com.walmartlabs.concord.server.boot.resteasy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binding;\nimport com.google.inject.Inject;\nimport com.google.inject.Injector;\nimport org.jboss.resteasy.plugins.server.servlet.ListenerBootstrap;\nimport org.jboss.resteasy.spi.ResteasyDeployment;\nimport org.jboss.resteasy.util.GetRestful;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.servlet.ServletContextEvent;\nimport javax.servlet.ServletContextListener;\nimport javax.servlet.annotation.WebListener;\nimport javax.ws.rs.ext.Provider;\nimport java.util.ArrayList;\n\nimport static java.util.Objects.requireNonNull;\n\n/**\n * Based on the original code from {@code pkg:maven/org.jboss.resteasy/resteasy-guice@4.7.9.Final}.\n */\n@WebListener\npublic class ResteasyBootstrapListener implements ServletContextListener {\n\n    private static final Logger log = LoggerFactory.getLogger(ResteasyBootstrapListener.class);\n\n    private final Injector injector;\n    private ResteasyDeployment deployment;\n\n    @Inject\n    public ResteasyBootstrapListener(Injector injector) {\n        this.injector = requireNonNull(injector);\n    }\n\n    @Override\n    public void contextInitialized(ServletContextEvent event) {\n        var config = new ListenerBootstrap(event.getServletContext());\n\n        deployment = config.createDeployment();\n\n        var servletContext = event.getServletContext();\n        servletContext.setAttribute(ResteasyDeployment.class.getName(), deployment);\n\n        deployment.start();\n\n        for (var injector = this.injector; injector != null; injector = injector.getParent()) {\n            processInjector(deployment, injector);\n        }\n    }\n\n    @Override\n    public void contextDestroyed(ServletContextEvent sce) {\n        if (deployment == null) {\n            return;\n        }\n        deployment.stop();\n    }\n\n    private static void processInjector(ResteasyDeployment deployment, Injector injector) {\n        var rootResourceBindings = new ArrayList<Binding<?>>();\n\n        var providerFactory = deployment.getProviderFactory();\n        for (var binding : injector.getBindings().values()) {\n            var type = (Object) binding.getKey().getTypeLiteral().getRawType();\n            if (type instanceof Class<?> beanClass) {\n                if (GetRestful.isRootResource(beanClass)) {\n                    rootResourceBindings.add(binding);\n                }\n                if (beanClass.isAnnotationPresent(Provider.class)) {\n                    log.info(\"registering provider instance for {}\", beanClass.getName());\n                    providerFactory.registerProviderInstance(binding.getProvider().get());\n                }\n            }\n        }\n\n        var registry = deployment.getRegistry();\n        for (var binding : rootResourceBindings) {\n            var beanClass = (Class<?>) binding.getKey().getTypeLiteral().getType();\n            var resourceFactory = new GuiceResourceFactory(binding.getProvider(), beanClass);\n            log.info(\"registering factory for {}\", beanClass.getName());\n            registry.addResourceFactory(resourceFactory);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/resteasy/ResteasyModule.java",
    "content": "package com.walmartlabs.concord.server.boot.resteasy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.walmartlabs.concord.server.sdk.rest.ApiDescriptor;\nimport com.walmartlabs.concord.server.sdk.rest.Component;\n\nimport javax.servlet.ServletContextListener;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\nimport static com.walmartlabs.concord.server.Utils.bindExceptionMapper;\n\npublic class ResteasyModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        newSetBinder(binder, ApiDescriptor.class).addBinding().to(ConcordApiDescriptor.class);\n\n        newSetBinder(binder, ServletContextListener.class).addBinding().to(ResteasyBootstrapListener.class);\n\n        binder.bind(ObjectMapperContextResolver.class).in(SINGLETON);\n        newSetBinder(binder, Component.class).addBinding().to(ObjectMapperContextResolver.class);\n\n        bindExceptionMapper(binder, UnexpectedExceptionMapper.class);\n        bindExceptionMapper(binder, WebappExceptionMapper.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/resteasy/UnexpectedExceptionMapper.java",
    "content": "package com.walmartlabs.concord.server.boot.resteasy;\n\n/*\n * Copyright (c) 2007-2014 Sonatype, Inc. All rights reserved.\n *\n * This program is licensed to you under the Apache License Version 2.0,\n * and you may not use this file except in compliance with the Apache License Version 2.0.\n * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the Apache License Version 2.0 is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.\n *\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.ws.rs.core.Response;\nimport javax.ws.rs.ext.Provider;\n\nimport static javax.ws.rs.core.MediaType.TEXT_PLAIN;\n\n/**\n * Based on the original {@link org.sonatype.siesta.UnexpectedExceptionMapper}.\n */\n@Provider\npublic class UnexpectedExceptionMapper extends ExceptionMapperSupport<Throwable> {\n\n    @Override\n    protected Response convert(Throwable exception) {\n        // always log unexpected exception with stack\n        log.warn(\"Unexpected exception: {}\", exception.toString(), exception);\n\n        return Response.serverError()\n                .entity(String.format(\"ERROR: %s\", exception))\n                .type(TEXT_PLAIN)\n                .build();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/resteasy/WebappExceptionMapper.java",
    "content": "package com.walmartlabs.concord.server.boot.resteasy;\n\n/*\n * Copyright (c) 2007-2014 Sonatype, Inc. All rights reserved.\n *\n * This program is licensed to you under the Apache License Version 2.0,\n * and you may not use this file except in compliance with the Apache License Version 2.0.\n * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the Apache License Version 2.0 is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.\n *\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.ws.rs.WebApplicationException;\nimport javax.ws.rs.core.Response;\nimport javax.ws.rs.ext.Provider;\n\n/**\n * Based on the original {@link org.sonatype.siesta.WebappExceptionMapper}.\n */\n@Provider\npublic class WebappExceptionMapper extends ExceptionMapperSupport<WebApplicationException> {\n\n    @Override\n    protected Response convert(final WebApplicationException exception) {\n        return exception.getResponse();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/servlets/FormServletHolder.java",
    "content": "package com.walmartlabs.concord.server.boot.servlets;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.cfg.CustomFormConfiguration;\nimport org.eclipse.jetty.ee8.servlet.DefaultServlet;\nimport org.eclipse.jetty.ee8.servlet.ServletHolder;\n\nimport javax.inject.Inject;\nimport javax.servlet.annotation.WebServlet;\n\n@WebServlet(\"/forms/*\")\npublic class FormServletHolder extends ServletHolder {\n\n    @Inject\n    public FormServletHolder(CustomFormConfiguration cfg) {\n        super(DefaultServlet.class);\n\n        setInitParameter(\"acceptRanges\", \"true\");\n        setInitParameter(\"dirAllowed\", \"false\");\n        setInitParameter(\"resourceBase\", cfg.getBaseDir().toAbsolutePath().toString());\n        setInitParameter(\"pathInfoOnly\", \"true\");\n        setInitParameter(\"redirectWelcome\", \"false\");\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/statics/StaticResourcesConfigurator.java",
    "content": "package com.walmartlabs.concord.server.boot.statics;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.boot.ContextHandlerConfigurator;\nimport org.eclipse.jetty.server.handler.ContextHandler;\nimport org.eclipse.jetty.server.handler.ContextHandlerCollection;\nimport org.eclipse.jetty.server.handler.ResourceHandler;\nimport org.eclipse.jetty.util.resource.Resource;\nimport org.eclipse.jetty.util.resource.ResourceFactory;\n\n/**\n * Configures Jetty's {@link org.eclipse.jetty.server.Handler} to serve Concord's static resources.\n */\npublic class StaticResourcesConfigurator implements ContextHandlerConfigurator {\n\n    @Override\n    public void configure(ContextHandlerCollection collection) {\n        collection.addHandler(classpathResourceHandler(\"/resources/console\", \"/com/walmartlabs/concord/server/console/static\"));\n    }\n\n    private static ContextHandler classpathResourceHandler(String context, String path) {\n        ContextHandler handler = new ContextHandler();\n\n        ResourceHandler resourceHandler = new ResourceHandler();\n        resourceHandler.setDirAllowed(false);\n\n        Resource resource = ResourceFactory.root().newClassLoaderResource(path);\n        handler.setBaseResource(resource);\n        handler.setHandler(resourceHandler);\n        handler.setContextPath(context);\n\n        return handler;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/validation/ConstraintViolationExceptionMapper.java",
    "content": "/*\n * Copyright (c) 2007-2014 Sonatype, Inc. All rights reserved.\n *\n * This program is licensed to you under the Apache License Version 2.0,\n * and you may not use this file except in compliance with the Apache License Version 2.0.\n * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the Apache License Version 2.0 is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.\n *\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\npackage com.walmartlabs.concord.server.boot.validation;\n\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorXO;\n\nimport javax.validation.ConstraintViolation;\nimport javax.validation.ConstraintViolationException;\nimport javax.validation.ElementKind;\nimport javax.validation.Path;\nimport javax.ws.rs.core.Response.Status;\nimport javax.ws.rs.ext.Provider;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * Based on the original {@link org.sonatype.siesta.server.validation.ConstraintViolationExceptionMapper}.\n */\n@Provider\npublic class ConstraintViolationExceptionMapper extends ValidationExceptionMapperSupport<ConstraintViolationException> {\n\n    @Override\n    protected List<ValidationErrorXO> getValidationErrors(final ConstraintViolationException exception) {\n        return getValidationErrors(exception.getConstraintViolations());\n    }\n\n    @Override\n    protected Status getStatus(final ConstraintViolationException exception) {\n        return getResponseStatus(exception.getConstraintViolations());\n    }\n\n    private List<ValidationErrorXO> getValidationErrors(Set<ConstraintViolation<?>> violations) {\n        return violations.stream()\n                .map(v -> new ValidationErrorXO(getPath(v), v.getMessage()))\n                .toList();\n    }\n\n    private Status getResponseStatus(Set<ConstraintViolation<?>> violations) {\n        Iterator<ConstraintViolation<?>> iterator = violations.iterator();\n\n        if (iterator.hasNext()) {\n            return getResponseStatus(iterator.next());\n        } else {\n            return Status.BAD_REQUEST;\n        }\n    }\n\n    private Status getResponseStatus(ConstraintViolation<?> violation) {\n        for (Path.Node node : violation.getPropertyPath()) {\n            ElementKind kind = node.getKind();\n\n            if (ElementKind.RETURN_VALUE.equals(kind)) {\n                return Status.INTERNAL_SERVER_ERROR;\n            }\n        }\n\n        return Status.BAD_REQUEST;\n    }\n\n    private String getPath(ConstraintViolation<?> violation) {\n        String leafBeanName = violation.getLeafBean().getClass().getSimpleName();\n        int proxySuffix = leafBeanName.indexOf(\"$$EnhancerByGuice\");\n        if (proxySuffix > 0) {\n            leafBeanName = leafBeanName.substring(0, proxySuffix);\n        }\n\n        String propertyPath = violation.getPropertyPath().toString();\n\n        return leafBeanName + (!\"\".equals(propertyPath) ? '.' + propertyPath : \"\");\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/validation/DefaultGetterPropertySelectionStrategy.java",
    "content": "package com.walmartlabs.concord.server.boot.validation;\n\n/*\n * Hibernate Validator, declare and validate application constraints\n *\n * License: Apache License, Version 2.0\n * See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.\n *\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.hibernate.validator.spi.properties.ConstrainableExecutable;\nimport org.hibernate.validator.spi.properties.GetterPropertySelectionStrategy;\n\nimport java.util.HashSet;\nimport java.util.Optional;\nimport java.util.Set;\n\n/**\n * A version of the original org.hibernate.validator.internal.properties.DefaultGetterPropertySelectionStrategy\n * adapted for the project (mostly to make the \"takari-lifecycle-plugin\" happy).\n */\npublic class DefaultGetterPropertySelectionStrategy implements GetterPropertySelectionStrategy {\n\n    private static final String GETTER_PREFIX_GET = \"get\";\n    private static final String GETTER_PREFIX_IS = \"is\";\n    private static final String GETTER_PREFIX_HAS = \"has\";\n    private static final String[] GETTER_PREFIXES = {\n            GETTER_PREFIX_GET,\n            GETTER_PREFIX_IS,\n            GETTER_PREFIX_HAS\n    };\n\n    @Override\n    public Optional<String> getProperty(ConstrainableExecutable executable) {\n        if (!isGetter(executable)) {\n            return Optional.empty();\n        }\n\n        String methodName = executable.getName();\n\n        for (String prefix : GETTER_PREFIXES) {\n            if (methodName.startsWith(prefix)) {\n                return Optional.of(methodName.substring(prefix.length()).toLowerCase());\n            }\n        }\n\n        throw new AssertionError(\"Method \" + executable.getName() + \" was considered a getter but we couldn't extract a property name.\");\n    }\n\n    @Override\n    public Set<String> getGetterMethodNameCandidates(String propertyName) {\n\n        Set<String> nameCandidates = new HashSet<>(GETTER_PREFIXES.length);\n        for (String prefix : GETTER_PREFIXES) {\n            nameCandidates.add(prefix + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1));\n        }\n        return nameCandidates;\n    }\n\n    /**\n     * Checks whether the given executable is a valid JavaBean getter method, which\n     * is the case if\n     * <ul>\n     * <li>its name starts with \"get\" and it has a return type but no parameter or</li>\n     * <li>its name starts with \"is\", it has no parameter and is returning\n     * {@code boolean} or</li>\n     * <li>its name starts with \"has\", it has no parameter and is returning\n     * {@code boolean} (HV-specific, not mandated by the JavaBeans spec).</li>\n     * </ul>\n     *\n     * @param executable The executable of interest.\n     * @return {@code true}, if the given executable is a JavaBean getter method,\n     * {@code false} otherwise.\n     */\n    private static boolean isGetter(ConstrainableExecutable executable) {\n        if (executable.getParameterTypes().length != 0) {\n            return false;\n        }\n\n        String methodName = executable.getName();\n\n        //<PropertyType> get<PropertyName>()\n        if (methodName.startsWith(GETTER_PREFIX_GET) && executable.getReturnType() != void.class) {\n            return true;\n        }\n        //boolean is<PropertyName>()\n        else if (methodName.startsWith(GETTER_PREFIX_IS) && executable.getReturnType() == boolean.class) {\n            return true;\n        }\n        //boolean has<PropertyName>()\n        else if (methodName.startsWith(GETTER_PREFIX_HAS) && executable.getReturnType() == boolean.class) {\n            return true;\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/validation/ValidationErrorsExceptionMapper.java",
    "content": "package com.walmartlabs.concord.server.boot.validation;\n\n/*\n * Copyright (c) 2007-2014 Sonatype, Inc. All rights reserved.\n *\n * This program is licensed to you under the Apache License Version 2.0,\n * and you may not use this file except in compliance with the Apache License Version 2.0.\n * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the Apache License Version 2.0 is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.\n *\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorXO;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\n\nimport javax.ws.rs.ext.Provider;\nimport java.util.List;\n\n/**\n * Based on the original {@link org.sonatype.siesta.server.validation.ValidationErrorsExceptionMapper}.\n */\n@Provider\npublic class ValidationErrorsExceptionMapper extends ValidationExceptionMapperSupport<ValidationErrorsException> {\n\n    @Override\n    protected List<ValidationErrorXO> getValidationErrors(ValidationErrorsException exception) {\n        return exception.getValidationErrors();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/validation/ValidationExceptionMapperSupport.java",
    "content": "package com.walmartlabs.concord.server.boot.validation;\n\n/*\n * Copyright (c) 2007-2014 Sonatype, Inc. All rights reserved.\n *\n * This program is licensed to you under the Apache License Version 2.0,\n * and you may not use this file except in compliance with the Apache License Version 2.0.\n * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the Apache License Version 2.0 is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.\n *\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.boot.resteasy.ExceptionMapperSupport;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorXO;\n\nimport javax.ws.rs.core.*;\nimport javax.ws.rs.core.Response.ResponseBuilder;\nimport javax.ws.rs.core.Response.Status;\nimport java.util.List;\n\nimport static java.util.Objects.requireNonNull;\n\n/**\n * Based on the original {@link org.sonatype.siesta.server.validation.ValidationExceptionMapperSupport}.\n */\npublic abstract class ValidationExceptionMapperSupport<E extends Throwable> extends ExceptionMapperSupport<E> {\n\n    private final List<Variant> variants;\n\n    public ValidationExceptionMapperSupport() {\n        this.variants = Variant.mediaTypes(\n                new MediaType(\"application\", \"vnd.concord-validation-errors-v1+json\")\n        ).add().build();\n    }\n\n    @Override\n    protected Response convert(E exception) {\n        ResponseBuilder builder = Response.status(getStatus(exception));\n\n        List<ValidationErrorXO> errors = getValidationErrors(exception);\n        if (errors != null && !errors.isEmpty()) {\n            Variant variant = getRequest().selectVariant(variants);\n            if (variant != null) {\n                builder.type(variant.getMediaType())\n                        .entity(\n                                new GenericEntity<>(errors) {\n                                    @Override\n                                    public String toString() {\n                                        return getEntity().toString();\n                                    }\n                                }\n                        );\n            }\n        }\n\n        return builder.build();\n    }\n\n    protected Status getStatus(E exception) {\n        return Status.BAD_REQUEST;\n    }\n\n    protected abstract List<ValidationErrorXO> getValidationErrors(E exception);\n\n    private Request request;\n\n    @Context\n    public void setRequest(Request request) {\n        this.request = requireNonNull(request);\n    }\n\n    protected Request getRequest() {\n        return requireNonNull(request);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/validation/ValidationInterceptor.java",
    "content": "package com.walmartlabs.concord.server.boot.validation;\n\n/*\n * Copyright (c) 2007-2014 Sonatype, Inc. All rights reserved.\n *\n * This program is licensed to you under the Apache License Version 2.0,\n * and you may not use this file except in compliance with the Apache License Version 2.0.\n * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the Apache License Version 2.0 is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.\n *\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport org.aopalliance.intercept.MethodInterceptor;\nimport org.aopalliance.intercept.MethodInvocation;\n\nimport javax.inject.Inject;\nimport javax.validation.ConstraintViolation;\nimport javax.validation.ConstraintViolationException;\nimport javax.validation.executable.ExecutableValidator;\nimport java.lang.reflect.Method;\nimport java.util.Arrays;\nimport java.util.Set;\n\nimport static com.google.common.base.Preconditions.checkNotNull;\n\n/**\n * Based on the original {@link org.sonatype.siesta.server.validation.ValidationInterceptor}.\n */\npublic class ValidationInterceptor implements MethodInterceptor {\n\n    @Inject\n    private ExecutableValidator methodValidator;\n\n    public Object invoke(final MethodInvocation mi) throws Throwable {\n        checkNotNull(methodValidator);\n\n        final ClassLoader tccl = Thread.currentThread().getContextClassLoader();\n        try {\n            Thread.currentThread().setContextClassLoader(getClass().getClassLoader());\n            final Validate validate = mi.getMethod().getAnnotation(Validate.class);\n\n            validateParameters(mi.getThis(), mi.getMethod(), mi.getArguments(), validate.groups());\n\n            final Object result = mi.proceed();\n\n            validateReturnValue(mi.getThis(), mi.getMethod(), result, validate.groups());\n\n            return result;\n        } finally {\n            Thread.currentThread().setContextClassLoader(tccl);\n        }\n    }\n\n    private void validateParameters(final Object obj, final Method method, final Object[] args, final Class<?>[] groups) {\n        final Set<ConstraintViolation<Object>> violations = methodValidator.validateParameters(obj, method, args, groups);\n        if (!violations.isEmpty()) {\n            final String message = \"Invalid arguments calling '\" + method + \"' with \" + Arrays.deepToString(args);\n            throw new ConstraintViolationException(message, violations);\n        }\n    }\n\n    private void validateReturnValue(final Object obj, final Method method, final Object value, final Class<?>[] groups) {\n        final Set<ConstraintViolation<Object>> violations = methodValidator.validateReturnValue(obj, method, value, groups);\n        if (!violations.isEmpty()) {\n            final String message = \"Invalid value returned by '\" + method + \"' was \" + value;\n            throw new ConstraintViolationException(message, violations);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/boot/validation/ValidationModule.java",
    "content": "package com.walmartlabs.concord.server.boot.validation;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.google.inject.Provides;\nimport com.google.inject.matcher.Matchers;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport org.aopalliance.intercept.MethodInterceptor;\nimport org.hibernate.validator.HibernateValidator;\nimport org.hibernate.validator.spi.properties.ConstrainableExecutable;\nimport org.hibernate.validator.spi.properties.GetterPropertySelectionStrategy;\n\nimport javax.inject.Singleton;\nimport javax.validation.Validation;\nimport javax.validation.Validator;\nimport javax.validation.ValidatorFactory;\nimport javax.validation.executable.ExecutableValidator;\nimport java.util.HashSet;\nimport java.util.Optional;\nimport java.util.Set;\n\nimport static com.walmartlabs.concord.server.Utils.bindExceptionMapper;\n\npublic class ValidationModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        MethodInterceptor interceptor = new ValidationInterceptor();\n        binder.bindInterceptor(Matchers.any(), Matchers.annotatedWith(Validate.class), interceptor);\n        binder.requestInjection(interceptor);\n\n        bindExceptionMapper(binder, ConstraintViolationExceptionMapper.class);\n        bindExceptionMapper(binder, ValidationErrorsExceptionMapper.class);\n    }\n\n    @Provides\n    @Singleton\n    ValidatorFactory validatorFactory() {\n        return Validation.byProvider(HibernateValidator.class)\n                .configure()\n                .getterPropertySelectionStrategy(new GetterPropertySelectionStrategy() {\n\n                    private final GetterPropertySelectionStrategy delegate = new DefaultGetterPropertySelectionStrategy();\n\n                    @Override\n                    public Optional<String> getProperty(ConstrainableExecutable executable) {\n                        Optional<String> result = delegate.getProperty(executable);\n                        if (result.isPresent()) {\n                            return result;\n                        }\n\n                        if (executable.getParameterTypes().length != 0 || executable.getReturnType() == void.class) {\n                            return Optional.empty();\n                        }\n\n                        return Optional.of(executable.getName());\n                    }\n\n                    @Override\n                    public Set<String> getGetterMethodNameCandidates(String propertyName) {\n                        Set<String> getters = delegate.getGetterMethodNameCandidates(propertyName);\n                        Set<String> result = new HashSet<>(getters);\n                        result.add(propertyName);\n                        return result;\n                    }\n                })\n                .buildValidatorFactory();\n    }\n\n    @Provides\n    @Singleton\n    Validator validator(final ValidatorFactory validatorFactory) {\n        return validatorFactory.getValidator();\n    }\n\n    @Provides\n    @Singleton\n    ExecutableValidator executableValidator(final Validator validator) {\n        return validator.forExecutables();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/AgentConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\nimport java.time.Duration;\n\npublic class AgentConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"agent.commandPollDelay\")\n    private Duration commandPollDelay;\n\n    @Inject\n    @Config(\"agent.watchdogPeriod\")\n    private Duration watchdogPeriod;\n\n    @Inject\n    @Config(\"agent.maxCommandAge\")\n    private Duration maxCommandAge;\n\n    @Inject\n    @Config(\"agent.maxStalledAge\")\n    private Duration maxStalledAge;\n\n    public Duration getCommandPollDelay() {\n        return commandPollDelay;\n    }\n\n    public Duration getWatchdogPeriod() {\n        return watchdogPeriod;\n    }\n\n    public Duration getMaxCommandAge() {\n        return maxCommandAge;\n    }\n\n    public Duration getMaxStalledAge() {\n        return maxStalledAge;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/ApiKeyConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\nimport org.eclipse.sisu.Nullable;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.Duration;\nimport java.util.List;\n\npublic class ApiKeyConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"apiKey.expirationPeriod\")\n    private Duration expirationPeriod;\n\n    @Inject\n    @Config(\"apiKey.expirationEnabled\")\n    private boolean expirationEnabled;\n\n    @Inject\n    @Config(\"apiKey.notifyBeforeDays\")\n    private List<Integer> notifyBeforeDays;\n\n    private final Path loadFrom;\n\n    @Inject\n    public ApiKeyConfiguration(@Nullable @Config(\"apiKey.loadFrom\") String loadFrom) {\n        this.loadFrom = loadFrom != null ? Paths.get(loadFrom) : null;\n    }\n\n    public Duration getExpirationPeriod() {\n        return expirationPeriod;\n    }\n\n    public boolean isExpirationEnabled() {\n        return expirationEnabled;\n    }\n\n    public List<Integer> getNotifyBeforeDays() {\n        return notifyBeforeDays;\n    }\n\n    public Path getLoadFrom() {\n        return loadFrom;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/AuditConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport java.io.Serializable;\nimport java.time.Duration;\n\npublic class AuditConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"audit.enabled\")\n    private boolean enabled;\n\n    @Inject\n    @Config(\"audit.cleanupPeriod\")\n    private Duration period;\n\n    @Inject\n    @Config(\"audit.maxLogAge\")\n    private Duration maxLogAge;\n\n    @Nullable\n    @Inject\n    @Config(\"audit.maxSearchInterval\")\n    private Duration maxSearchInterval;\n\n    public boolean isEnabled() {\n        return enabled;\n    }\n\n    public Duration getPeriod() {\n        return period;\n    }\n\n    public Duration getMaxLogAge() {\n        return maxLogAge;\n    }\n\n    public Duration getMaxSearchInterval() {\n        return maxSearchInterval;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/ConcordSecretStoreConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\n\npublic class ConcordSecretStoreConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"secretStore.concord.enabled\")\n    private boolean enabled;\n\n    public boolean isEnabled() {\n        return enabled;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/ConfigurationModule.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.typesafe.config.Config;\nimport com.walmartlabs.concord.common.cfg.OauthTokenConfig;\nimport com.walmartlabs.concord.config.ConfigModule;\nimport com.walmartlabs.concord.github.appinstallation.cfg.GitHubAppInstallationConfig;\n\nimport static com.google.inject.Scopes.SINGLETON;\n\npublic class ConfigurationModule implements Module {\n\n    private final Config config;\n\n    public ConfigurationModule(Config config) {\n        this.config = config;\n    }\n\n    @Override\n    public void configure(Binder binder) {\n        binder.bind(Config.class).toInstance(config);\n\n        binder.install(new ConfigModule(\"com.walmartlabs.concord.server\", config));\n\n        binder.bind(AgentConfiguration.class).in(SINGLETON);\n        binder.bind(ApiKeyConfiguration.class).in(SINGLETON);\n        binder.bind(AuditConfiguration.class).in(SINGLETON);\n        binder.bind(ConcordSecretStoreConfiguration.class).in(SINGLETON);\n        binder.bind(ConsoleConfiguration.class).in(SINGLETON);\n        binder.bind(CustomFormConfiguration.class).in(SINGLETON);\n        binder.bind(DependenciesConfiguration.class).in(SINGLETON);\n        binder.bind(EmailNotifierConfiguration.class).in(SINGLETON);\n        binder.bind(EnqueueWorkersConfiguration.class).in(SINGLETON);\n        binder.bind(ExternalEventsConfiguration.class).in(SINGLETON);\n        binder.bind(GitConfiguration.class).in(SINGLETON);\n        binder.bind(OauthTokenConfig.class).to(GitConfiguration.class).in(SINGLETON);\n        binder.bind(GithubConfiguration.class).in(SINGLETON);\n        binder.bind(GitHubAppInstallationConfig.class).to(GithubConfiguration.class).in(SINGLETON);\n        binder.bind(ImportConfiguration.class).in(SINGLETON);\n        binder.bind(LdapConfiguration.class).in(SINGLETON);\n        binder.bind(LdapGroupSyncConfiguration.class).in(SINGLETON);\n        binder.bind(LockingConfiguration.class).in(SINGLETON);\n        binder.bind(PolicyCacheConfiguration.class).in(SINGLETON);\n        binder.bind(ProcessConfiguration.class).in(SINGLETON);\n        binder.bind(ProcessQueueConfiguration.class).in(SINGLETON);\n        binder.bind(ProcessWaitWatchdogConfiguration.class).in(SINGLETON);\n        binder.bind(ProcessWatchdogConfiguration.class).in(SINGLETON);\n        binder.bind(QosConfiguration.class).in(SINGLETON);\n        binder.bind(RememberMeConfiguration.class).in(SINGLETON);\n        binder.bind(RepositoryConfiguration.class).in(SINGLETON);\n        binder.bind(SecretStoreConfiguration.class).in(SINGLETON);\n        binder.bind(ServerConfiguration.class).in(SINGLETON);\n        binder.bind(TemplatesConfiguration.class).in(SINGLETON);\n        binder.bind(TriggersConfiguration.class).in(SINGLETON);\n        binder.bind(WorkerMetricsConfiguration.class).in(SINGLETON);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/ConsoleConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Paths;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\npublic class ConsoleConfiguration {\n\n    private static final Logger log = LoggerFactory.getLogger(ConsoleConfiguration.class);\n\n    private final String cfg;\n    private final boolean useDefaultCfg;\n\n    @Inject\n    public ConsoleConfiguration(@Nullable @Config(\"console.cfgFile\") String cfgFile) throws IOException {\n        if (cfgFile == null) {\n            this.cfg = \"\";\n            this.useDefaultCfg = true;\n        } else {\n            var path = Paths.get(cfgFile);\n            if (!Files.isReadable(path)) {\n                throw new IOException(\"The console cfgFile is not readable or doesn't exist: \" + cfgFile);\n            }\n\n            log.info(\"Using console configuration from {}\", path.normalize().toAbsolutePath());\n            try (var in = Files.newInputStream(path)) {\n                this.cfg = new String(in.readAllBytes(), UTF_8);\n                this.useDefaultCfg = false;\n            }\n        }\n    }\n\n    public String getCfg() {\n        return cfg;\n    }\n\n    public boolean isUseDefaultCfg() {\n        return useDefaultCfg;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/CustomFormConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\npublic class CustomFormConfiguration {\n\n    private final Path baseDir;\n\n    @Inject\n    public CustomFormConfiguration(@Nullable @Config(\"forms.baseDir\") String baseDir) throws IOException {\n        this.baseDir = baseDir != null ? Paths.get(baseDir) : PathUtils.createTempDir(\"formserv\");\n    }\n\n    public Path getBaseDir() {\n        return baseDir;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/DatabaseConfigurationModule.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.walmartlabs.concord.db.DatabaseConfiguration;\nimport com.walmartlabs.concord.db.JsonStorageDB;\nimport com.walmartlabs.concord.db.MainDB;\n\nimport static com.google.inject.Scopes.SINGLETON;\n\npublic class DatabaseConfigurationModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        binder.bind(DatabaseConfiguration.class).annotatedWith(MainDB.class).to(MainDBConfiguration.class).in(SINGLETON);\n        binder.bind(DatabaseConfiguration.class).annotatedWith(JsonStorageDB.class).to(JsonStorageDBConfiguration.class).in(SINGLETON);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/DependenciesConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\nimport org.eclipse.sisu.Nullable;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.Path;\n\nimport static com.walmartlabs.concord.server.cfg.Utils.getPath;\n\npublic class DependenciesConfiguration {\n\n    private static final Logger log = LoggerFactory.getLogger(DependenciesConfiguration.class);\n\n    private final Path cacheDir;\n\n    @Inject\n    public DependenciesConfiguration(@Config(\"dependencies.cacheDir\") @Nullable String cacheDir) throws IOException {\n        this.cacheDir = getPath(cacheDir, \"depsCache\");\n        log.info(\"init -> using {} to cache dependencies\", this.cacheDir);\n    }\n\n    public Path getCacheDir() {\n        return cacheDir;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/EmailNotifierConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\nimport java.time.Duration;\n\npublic class EmailNotifierConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"email.host\")\n    private String host;\n\n    @Inject\n    @Config(\"email.port\")\n    private int port;\n\n    @Inject\n    @Config(\"email.enabled\")\n    private boolean enabled;\n\n    @Inject\n    @Config(\"email.from\")\n    private String from;\n\n    @Inject\n    @Config(\"email.readTimeout\")\n    private Duration readTimeout;\n\n    @Inject\n    @Config(\"email.connectTimeout\")\n    private Duration connectTimeout;\n\n    public String getHost() {\n        return host;\n    }\n\n    public int getPort() {\n        return port;\n    }\n\n    public boolean isEnabled() {\n        return enabled;\n    }\n\n    public String getFrom() {\n        return from;\n    }\n\n    public Duration getReadTimeout() {\n        return readTimeout;\n    }\n\n    public Duration getConnectTimeout() {\n        return connectTimeout;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/EnqueueWorkersConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.inject.Inject;\nimport java.time.Duration;\n\npublic class EnqueueWorkersConfiguration {\n\n    @Inject\n    @Config(\"queue.enqueueWorkerCount\")\n    private int workersCount;\n\n    @Inject\n    @Config(\"queue.enqueueBatchSize\")\n    private int batchSize;\n\n    @Inject\n    @Config(\"queue.enqueueBatchEnabled\")\n    private boolean batchEnabled;\n\n    @Inject\n    @Config(\"queue.enqueuePollInterval\")\n    private Duration interval;\n\n    public int getWorkersCount() {\n        return workersCount;\n    }\n\n    public Duration getInterval() {\n        return interval;\n    }\n\n    public boolean isBatchEnabled() {\n        return batchEnabled;\n    }\n\n    public int getBatchSize() {\n        return batchSize;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/ExternalEventsConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\nimport org.eclipse.sisu.Nullable;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\nimport java.util.Map;\n\npublic class ExternalEventsConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"externalEvents.requiredRoles\")\n    @Nullable\n    private Map<String, String> requiredRoles;\n\n    @Inject\n    @Config(\"externalEvents.workerThreads\")\n    private int workerThreads;\n\n    @Inject\n    @Config(\"externalEvents.logEvents\")\n    private boolean logEvents;\n\n    public Map<String, String> getRequiredRoles() {\n        return requiredRoles;\n    }\n\n    public int getWorkerThreads() {\n        return workerThreads;\n    }\n\n    public boolean isLogEvents() {\n        return logEvents;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/GitConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.cfg.MappingAuthConfig;\nimport com.walmartlabs.concord.common.cfg.OauthTokenConfig;\nimport com.walmartlabs.concord.config.Config;\nimport org.eclipse.sisu.Nullable;\n\nimport javax.inject.Inject;\nimport java.io.Serial;\nimport java.io.Serializable;\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.Optional;\n\nimport static com.walmartlabs.concord.common.cfg.MappingAuthConfig.assertBaseUrlPattern;\nimport static com.walmartlabs.concord.server.cfg.Utils.getStringOrDefault;\n\npublic class GitConfiguration implements OauthTokenConfig, Serializable {\n\n    @Serial\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"git.oauth\")\n    @Nullable\n    private String oauthToken;\n\n    @Inject\n    @Config(\"git.oauthUsername\")\n    @Nullable\n    private String oauthUsername;\n\n    @Inject\n    @Config(\"git.oauthUrlPattern\")\n    @Nullable\n    private String oauthUrlPattern;\n\n    @Inject\n    @Config(\"git.shallowClone\")\n    private boolean shallowClone;\n\n    @Inject\n    @Config(\"git.checkAlreadyFetched\")\n    private boolean checkAlreadyFetched;\n\n    @Inject\n    @Config(\"git.defaultOperationTimeout\")\n    private Duration defaultOperationTimeout;\n\n    @Inject\n    @Config(\"git.fetchTimeout\")\n    private Duration fetchTimeout;\n\n    @Inject\n    @Config(\"git.httpLowSpeedLimit\")\n    private int httpLowSpeedLimit;\n\n    @Inject\n    @Config(\"git.httpLowSpeedTime\")\n    private Duration httpLowSpeedTime;\n\n    @Inject\n    @Config(\"git.sshTimeout\")\n    private Duration sshTimeout;\n\n    @Inject\n    @Config(\"git.sshTimeoutRetryCount\")\n    private int sshTimeoutRetryCount;\n\n    @Inject\n    @Config(\"git.maxGitCliOutputBytes\")\n    private long maxGitCliOutputBytes;\n\n    @Inject\n    @Config(\"git.allowedSchemes\")\n    private List<String> allowedSchemes;\n\n    @Inject\n    @Config(\"git.systemAuth\")\n    List<com.typesafe.config.Config> authConfigs;\n\n    public boolean isShallowClone() {\n        return shallowClone;\n    }\n\n    public boolean isCheckAlreadyFetched() {\n        return checkAlreadyFetched;\n    }\n\n    public Duration getDefaultOperationTimeout() {\n        return defaultOperationTimeout;\n    }\n\n    public Duration getFetchTimeout() {\n        return fetchTimeout;\n    }\n\n    @Override\n    public Optional<String> getOauthToken() {\n        return Optional.ofNullable(oauthToken);\n    }\n\n    @Override\n    public Optional<String> getOauthUsername() {\n        return Optional.ofNullable(oauthUsername);\n    }\n\n    @Override\n    public Optional<String> getOauthUrlPattern() {\n        return Optional.ofNullable(oauthUrlPattern);\n    }\n\n    public int getHttpLowSpeedLimit() {\n        return httpLowSpeedLimit;\n    }\n\n    public Duration getHttpLowSpeedTime() {\n        return httpLowSpeedTime;\n    }\n\n    public Duration getSshTimeout() {\n        return sshTimeout;\n    }\n\n    public int getSshTimeoutRetryCount() {\n        return sshTimeoutRetryCount;\n    }\n\n    public long maxGitCliOutputBytes() {\n        return maxGitCliOutputBytes;\n    }\n\n    public List<String> getAllowedSchemes() {\n        return allowedSchemes;\n    }\n\n    public List<MappingAuthConfig> getSystemAuth() {\n        return authConfigs.stream()\n                .map(o -> {\n                    AuthSource type = AuthSource.valueOf(o.getString(\"type\").toUpperCase());\n\n                    return (AuthConfig) switch (type) {\n                        case OAUTH_TOKEN -> OauthConfig.from(o);\n                    };\n                })\n                .map(AuthConfig::toGitAuth)\n                .toList();\n    }\n\n    enum AuthSource {\n        OAUTH_TOKEN\n    }\n\n    public interface AuthConfig {\n        MappingAuthConfig toGitAuth();\n    }\n\n    public record OauthConfig(String id,\n                              String urlPattern,\n                              String token) implements AuthConfig {\n\n        static OauthConfig from(com.typesafe.config.Config cfg) {\n            return new OauthConfig(\n                    getStringOrDefault(cfg, \"id\", () -> \"system-oauth-token\"),\n                    cfg.getString(\"urlPattern\"),\n                    cfg.getString(\"token\")\n            );\n        }\n\n        @Override\n        public MappingAuthConfig.OauthAuthConfig toGitAuth() {\n            return MappingAuthConfig.OauthAuthConfig.builder()\n                    .id(this.id())\n                    .urlPattern(assertBaseUrlPattern(this.urlPattern()))\n                    .token(this.token())\n                    .build();\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/GithubConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.cfg.MappingAuthConfig;\nimport com.walmartlabs.concord.config.Config;\nimport com.walmartlabs.concord.github.appinstallation.cfg.GitHubAppInstallationConfig;\nimport org.eclipse.sisu.Nullable;\n\nimport javax.inject.Inject;\nimport java.time.Duration;\nimport java.util.List;\n\npublic class GithubConfiguration implements GitHubAppInstallationConfig {\n\n    @Inject\n    @Config(\"github.secret\")\n    @Nullable\n    private String secret;\n\n    @Inject\n    @Config(\"github.useSenderLdapDn\")\n    private boolean useSenderLdapDn;\n\n    @Inject\n    @Config(\"github.useSenderEmail\")\n    private boolean useSenderEmail;\n\n    @Inject\n    @Config(\"github.senderEmailCacheSize\")\n    private long senderEmailCacheSize;\n\n    @Inject\n    @Config(\"github.senderEmailCacheDuration\")\n    private Duration senderEmailCacheDuration;\n\n    @Inject\n    @Config(\"github.enableExternalUserIdMappingCache\")\n    private boolean enableExternalUserIdMappingCache;\n\n    @Inject\n    @Config(\"github.logEvents\")\n    private boolean logEvents;\n\n    @Inject\n    @Config(\"github.disableReposOnDeletedRef\")\n    private boolean disableReposOnDeletedRef;\n\n    private final GitHubAppInstallationConfig appInstallation;\n\n    @Inject\n    public GithubConfiguration(com.typesafe.config.Config config) {\n        if (config.hasPath(\"github.appInstallation\")) {\n            var raw = config.getConfig(\"github.appInstallation\");\n            this.appInstallation = GitHubAppInstallationConfig.fromConfig(raw);\n        } else {\n            this.appInstallation = GitHubAppInstallationConfig.builder().authConfigs(List.of()).build();\n        }\n    }\n\n    public String getSecret() {\n        return secret;\n    }\n\n    public boolean isUseSenderLdapDn() {\n        return useSenderLdapDn;\n    }\n\n    public boolean isUseSenderEmail() {\n        return useSenderEmail;\n    }\n\n    public boolean isEnableExternalUserIdMappingCache() {\n        return enableExternalUserIdMappingCache;\n    }\n\n    public long senderEmailCacheSize() {\n        return senderEmailCacheSize;\n    }\n\n    public Duration senderEmailCacheDuration() {\n        return senderEmailCacheDuration;\n    }\n\n    public boolean isLogEvents() {\n        return logEvents;\n    }\n\n    public boolean isDisableReposOnDeletedRef() {\n        return disableReposOnDeletedRef;\n    }\n\n    @Override\n    public List<MappingAuthConfig> getAuthConfigs() {\n        return appInstallation.getAuthConfigs();\n    }\n\n    @Override\n    public Duration getHttpClientTimeout() {\n        return appInstallation.getHttpClientTimeout();\n    }\n\n    @Override\n    public Duration getSystemAuthCacheDuration() {\n        return appInstallation.getSystemAuthCacheDuration();\n    }\n\n    @Override\n    public long getSystemAuthCacheMaxWeight() {\n        return appInstallation.getSystemAuthCacheMaxWeight();\n    }\n\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/ImportConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.inject.Inject;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\n\npublic class ImportConfiguration {\n\n    @Inject\n    @Config(\"imports.src\")\n    private String src;\n\n    @Inject\n    @Config(\"imports.defaultBranch\")\n    private String defaultBranch;\n\n    private final Set<String> disabledProcessors;\n\n    @Inject\n    public ImportConfiguration(@Config(\"imports.disabledProcessors\") List<String> disabledProcessors) {\n        this.disabledProcessors = Collections.unmodifiableSet(new HashSet<>(disabledProcessors));\n    }\n\n    public String getSrc() {\n        return src;\n    }\n\n    public String getDefaultBranch() {\n        return defaultBranch;\n    }\n\n    public Set<String> getDisabledProcessors() {\n        return disabledProcessors;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/JsonStorageDBConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.DatabaseConfiguration;\nimport com.walmartlabs.concord.db.JsonStorageDB;\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.inject.Inject;\nimport java.time.Duration;\n\n@JsonStorageDB\npublic class JsonStorageDBConfiguration implements DatabaseConfiguration {\n\n    @Inject\n    @Config(\"db.url\")\n    private String url;\n\n    @Inject\n    @Config(\"db.inventoryUsername\")\n    private String username;\n\n    @Inject\n    @Config(\"db.inventoryPassword\")\n    private String password;\n\n    @Inject\n    @Config(\"db.maxPoolSize\")\n    private int maxPoolSize;\n\n    @Inject\n    @Config(\"db.maxLifetime\")\n    private Duration maxLifetime;\n\n    @Override\n    public String url() {\n        return url;\n    }\n\n    @Override\n    public String username() {\n        return username;\n    }\n\n    @Override\n    public String password() {\n        return password;\n    }\n\n    @Override\n    public int maxPoolSize() {\n        return maxPoolSize;\n    }\n\n    @Override\n    public Duration maxLifetime() {\n        return maxLifetime;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/LdapConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\nimport org.eclipse.sisu.Nullable;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\nimport java.time.Duration;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\n\npublic class LdapConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"ldap.url\")\n    private String url;\n\n    @Inject\n    @Config(\"ldap.searchBase\")\n    private String searchBase;\n\n    @Inject\n    @Config(\"ldap.principalSearchFilter\")\n    private String principalSearchFilter;\n\n    @Inject\n    @Config(\"ldap.groupSearchFilter\")\n    private String groupSearchFilter;\n\n    @Inject\n    @Config(\"ldap.groupNameProperty\")\n    private String groupNameProperty;\n\n    @Inject\n    @Config(\"ldap.groupDisplayNameProperty\")\n    private String groupDisplayNameProperty;\n\n    @Inject\n    @Config(\"ldap.systemUsername\")\n    private String systemUsername;\n\n    @Inject\n    @Config(\"ldap.systemPassword\")\n    @Nullable\n    private String systemPassword;\n\n    @Inject\n    @Config(\"ldap.userPrincipalNameProperty\")\n    private String userPrincipalNameProperty;\n\n    @Inject\n    @Config(\"ldap.usernameProperty\")\n    private String usernameProperty;\n\n    @Inject\n    @Config(\"ldap.mailProperty\")\n    private String mailProperty;\n\n    @Inject\n    @Config(\"ldap.autoCreateUsers\")\n    private boolean autoCreateUsers;\n\n    @Inject\n    @Nullable\n    @Config(\"ldap.returningAttributes\")\n    private List<String> returningAttributes;\n\n    @Inject\n    @Nullable\n    @Config(\"ldap.cacheDuration\")\n    private Duration cacheDuration;\n\n    @Inject\n    @Config(\"ldap.connectTimeout\")\n    private Duration connectTimeout;\n\n    @Inject\n    @Config(\"ldap.readTimeout\")\n    private Duration readTimeout;\n\n    @Inject\n    @Nullable\n    @Config(\"ldap.dnsSRVName\")\n    private String dnsSRVName;\n\n    @Inject\n    @Config(\"ldap.trustAllCertificates\")\n    private boolean trustAllCertificates;\n\n    private final Set<String> exposeAttributes;\n\n    private final Set<String> excludeAttributes;\n\n    @Inject\n    public LdapConfiguration(@Config(\"ldap.exposeAttributes\") @Nullable String exposeAttributes,\n                             @Config(\"ldap.excludeAttributes\") @Nullable String excludeAttributes) {\n\n        this.exposeAttributes = split(exposeAttributes);\n        this.excludeAttributes = split(excludeAttributes);\n    }\n\n    public String getUrl() {\n        return url;\n    }\n\n    public String getSearchBase() {\n        return searchBase;\n    }\n\n    public String getPrincipalSearchFilter() {\n        return principalSearchFilter;\n    }\n\n    public String getGroupSearchFilter() {\n        return groupSearchFilter;\n    }\n\n    public String getGroupNameProperty() {\n        return groupNameProperty;\n    }\n\n    public String getGroupDisplayNameProperty() {\n        return groupDisplayNameProperty;\n    }\n\n    public String getSystemUsername() {\n        return systemUsername;\n    }\n\n    public String getSystemPassword() {\n        return systemPassword;\n    }\n\n    public String getUserPrincipalNameProperty() {\n        return userPrincipalNameProperty;\n    }\n\n    public String getUsernameProperty() {\n        return usernameProperty;\n    }\n\n    public String getMailProperty() {\n        return mailProperty;\n    }\n\n    public Set<String> getExposeAttributes() {\n        return exposeAttributes;\n    }\n\n    public Set<String> getExcludeAttributes() {\n        return excludeAttributes;\n    }\n\n    public List<String> getReturningAttributes() {\n        return returningAttributes;\n    }\n\n    public boolean isAutoCreateUsers() {\n        return autoCreateUsers;\n    }\n\n    public Duration getCacheDuration() {\n        return cacheDuration;\n    }\n\n    public Duration getConnectTimeout() {\n        return connectTimeout;\n    }\n\n    public Duration getReadTimeout() {\n        return readTimeout;\n    }\n\n    public String getDnsSRVName() {\n        return dnsSRVName;\n    }\n\n    public boolean isTrustAllCertificates() {\n        return trustAllCertificates;\n    }\n\n    private static Set<String> split(String s) {\n        if (s == null || s.isEmpty()) {\n            return Collections.emptySet();\n        }\n\n        s = s.trim();\n        if (s.isEmpty()) {\n            return Collections.emptySet();\n        }\n\n        String[] as = s.split(\",\");\n        Set<String> result = new HashSet<>(as.length);\n        Collections.addAll(result, s.split(\",\"));\n\n        return Collections.unmodifiableSet(result);\n    }\n}"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/LdapGroupSyncConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport java.io.Serializable;\nimport java.time.Duration;\n\npublic class LdapGroupSyncConfiguration  implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"ldapGroupSync.interval\")\n    private Duration interval;\n\n    @Inject\n    @Config(\"ldapGroupSync.fetchLimit\")\n    private int fetchLimit;\n\n    @Inject\n    @Config(\"ldapGroupSync.minAgeLogin\")\n    private Duration minAgeLogin;\n\n    @Inject\n    @Config(\"ldapGroupSync.minAgeSync\")\n    private Duration minAgeSync;\n\n    @Inject\n    @Nullable\n    @Config(\"ldapGroupSync.disabledAge\")\n    private Duration disabledAge;\n\n    public Duration getInterval() {\n        return interval;\n    }\n\n    public int getFetchLimit() {\n        return fetchLimit;\n    }\n\n    public Duration getMinAgeLogin() {\n        return minAgeLogin;\n    }\n\n    public Duration getMinAgeSync() {\n        return minAgeSync;\n    }\n\n    public Duration getDisabledAge() {\n        return disabledAge;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/LockingConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.inject.Inject;\n\npublic class LockingConfiguration {\n\n    @Inject\n    @Config(\"locking.maxAdvisoryLocks\")\n    private int maxAdvisoryLocks;\n\n    public LockingConfiguration() {\n    }\n\n    public LockingConfiguration(int maxAdvisoryLocks) {\n        this.maxAdvisoryLocks = maxAdvisoryLocks;\n    }\n\n    public int getMaxAdvisoryLocks() {\n        return maxAdvisoryLocks;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/MainDBConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.DatabaseConfiguration;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.inject.Inject;\nimport java.time.Duration;\nimport java.util.Map;\n\n@MainDB\npublic class MainDBConfiguration implements DatabaseConfiguration {\n\n    @Inject\n    @Config(\"db.url\")\n    private String url;\n\n    @Inject\n    @Config(\"db.appUsername\")\n    private String username;\n\n    @Inject\n    @Config(\"db.appPassword\")\n    private String password;\n\n    @Inject\n    @Config(\"db.maxPoolSize\")\n    private int maxPoolSize;\n\n    @Inject\n    @Config(\"db.maxLifetime\")\n    private Duration maxLifetime;\n\n    @Inject\n    @Config(\"db.changeLogParameters\")\n    private Map<String, Object> changeLogParameters;\n\n    @Override\n    public String url() {\n        return url;\n    }\n\n    @Override\n    public String username() {\n        return username;\n    }\n\n    @Override\n    public String password() {\n        return password;\n    }\n\n    @Override\n    public int maxPoolSize() {\n        return maxPoolSize;\n    }\n\n    @Override\n    public Duration maxLifetime() {\n        return maxLifetime;\n    }\n\n    @Override\n    public Map<String, Object> changeLogParameters() {\n        return changeLogParameters;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/PolicyCacheConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\nimport java.time.Duration;\n\npublic class PolicyCacheConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"policyCache.reloadInterval\")\n    private Duration reloadInterval;\n\n    public Duration getReloadInterval() {\n        return reloadInterval;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/ProcessConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\nimport org.eclipse.sisu.Nullable;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.time.Duration;\nimport java.util.List;\n\npublic class ProcessConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"process.cleanupInterval\")\n    private Duration cleanupInterval;\n\n    @Inject\n    @Config(\"process.queueCleanup\")\n    private boolean queueCleanup;\n\n    @Inject\n    @Config(\"process.stateCleanup\")\n    private boolean stateCleanup;\n\n    @Inject\n    @Config(\"process.eventsCleanup\")\n    private boolean eventsCleanup;\n\n    @Inject\n    @Config(\"process.logsCleanup\")\n    private boolean logsCleanup;\n\n    @Inject\n    @Config(\"process.checkpointCleanup\")\n    private boolean checkpointCleanup;\n\n    @Inject\n    @Config(\"process.maxStateAge\")\n    private Duration maxStateAge;\n\n    @Inject\n    @Config(\"process.secureFiles\")\n    private List<String> secureFiles;\n\n    @Inject\n    @Config(\"process.signingKeyAlgorithm\")\n    @Nullable\n    private String signingKeyAlgorithm;\n\n    @Inject\n    @Config(\"process.signingAlgorithm\")\n    @Nullable\n    private String signingAlgorithm;\n\n    private Path signingKeyPath;\n\n    @Inject\n    @Config(\"process.logSizeLimit\")\n    private int logSizeLimit;\n\n    @Inject\n    @Config(\"process.checkLogPermissions\")\n    private boolean checkLogPermissions;\n\n    @Inject\n    public ProcessConfiguration(@Config(\"process.signingKeyPath\") @Nullable String signingKeyPath) {\n        this.signingKeyPath = signingKeyPath != null ? Paths.get(signingKeyPath) : null;\n    }\n\n    public ProcessConfiguration(Duration maxStateAge, List<String> secureFiles) {\n        this.maxStateAge = maxStateAge;\n        this.secureFiles = secureFiles;\n    }\n\n    public Duration getCleanupInterval() {\n        return cleanupInterval;\n    }\n\n    public boolean isQueueCleanup() {\n        return queueCleanup;\n    }\n\n    public boolean isStateCleanup() {\n        return stateCleanup;\n    }\n\n    public boolean isEventsCleanup() {\n        return eventsCleanup;\n    }\n\n    public boolean isLogsCleanup() {\n        return logsCleanup;\n    }\n\n    public boolean isCheckpointCleanup() {\n        return checkpointCleanup;\n    }\n\n    public Duration getMaxStateAge() {\n        return maxStateAge;\n    }\n\n    public List<String> getSecureFiles() {\n        return secureFiles;\n    }\n\n    public String getSigningAlgorithm() {\n        return signingAlgorithm;\n    }\n\n    public String getSigningKeyAlgorithm() {\n        return signingKeyAlgorithm;\n    }\n\n    public Path getSigningKeyPath() {\n        return signingKeyPath;\n    }\n\n    public int getLogSizeLimit() {\n        return logSizeLimit;\n    }\n\n    public boolean isCheckLogPermissions() {\n        return checkLogPermissions;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/ProcessQueueConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\nimport java.time.Duration;\n\npublic class ProcessQueueConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"queue.dispatcher.pollDelay\")\n    private Duration dispatcherPollDelay;\n\n    @Inject\n    @Config(\"queue.dispatcher.batchSize\")\n    private int dispatcherBatchSize;\n\n    public Duration getDispatcherPollDelay() {\n        return dispatcherPollDelay;\n    }\n\n    public int getDispatcherBatchSize() {\n        return dispatcherBatchSize;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/ProcessWaitWatchdogConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\nimport java.time.Duration;\n\npublic class ProcessWaitWatchdogConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"process.waitCheckPeriod\")\n    private Duration period;\n\n    @Inject\n    @Config(\"process.waitCheckPollLimit\")\n    private int pollLimit;\n\n    @Inject\n    @Config(\"process.waitProcessLimitForStatusQuery\")\n    private int processLimitForStatusQuery;\n\n    public Duration getPeriod() {\n        return period;\n    }\n\n    public int getPollLimit() {\n        return pollLimit;\n    }\n\n    public int getProcessLimitForStatusQuery() {\n        return processLimitForStatusQuery;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/ProcessWatchdogConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\nimport java.time.Duration;\n\npublic class ProcessWatchdogConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"process.watchdogPeriod\")\n    private Duration period;\n\n    @Inject\n    @Config(\"process.maxFailureHandlingAge\")\n    private Duration maxFailureHandlingAge;\n\n    @Inject\n    @Config(\"process.maxStalledAge\")\n    private Duration maxStalledAge;\n\n    @Inject\n    @Config(\"process.maxStartFailureAge\")\n    private Duration maxStartFailureAge;\n\n    public Duration getPeriod() {\n        return period;\n    }\n\n    public Duration getMaxFailureHandlingAge() {\n        return maxFailureHandlingAge;\n    }\n\n    public Duration getMaxStalledAge() {\n        return maxStalledAge;\n    }\n\n    public Duration getMaxStartFailureAge() {\n        return maxStartFailureAge;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/QosConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\nimport java.time.Duration;\n\npublic class QosConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"qos.maxRequests\")\n    public int maxRequests;\n\n    @Inject\n    @Config(\"qos.maxWait\")\n    public Duration maxWait;\n\n    @Inject\n    @Config(\"qos.suspend\")\n    public Duration suspend;\n\n    public int getMaxRequests() {\n        return maxRequests;\n    }\n\n    public Duration getMaxWait() {\n        return maxWait;\n    }\n\n    public Duration getSuspend() {\n        return suspend;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/RememberMeConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\nimport org.eclipse.sisu.Nullable;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\nimport java.time.Duration;\n\npublic class RememberMeConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"rememberMe.maxAge\")\n    private Duration rememberMeMaxAge;\n\n    @Inject\n    @Config(\"rememberMe.cipherKey\")\n    @Nullable\n    private byte[] cipherKey;\n\n    public Duration getRememberMeMaxAge() {\n        return rememberMeMaxAge;\n    }\n\n    public byte[] getCipherKey() {\n        return cipherKey;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/RepositoryConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\nimport org.eclipse.sisu.Nullable;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.time.Duration;\n\nimport static com.walmartlabs.concord.server.cfg.Utils.getPath;\n\npublic class RepositoryConfiguration {\n\n    private static final Logger log = LoggerFactory.getLogger(RepositoryConfiguration.class);\n\n    private final Path cacheDir;\n\n    private final Path cacheInfoDir;\n\n    @Inject\n    @Config(\"repositoryCache.concordFileValidationEnabled\")\n    private boolean concordFileValidationEnabled;\n\n    @Inject\n    @Config(\"repositoryCache.lockTimeout\")\n    private Duration lockTimeout;\n\n    @Inject\n    @Config(\"repositoryCache.maxAge\")\n    private Duration maxAge;\n\n    @Inject\n    @Config(\"repositoryCache.lockCount\")\n    private int lockCount;\n\n    @Inject\n    public RepositoryConfiguration(@Config(\"repositoryCache.cacheDir\") @Nullable String cacheDir,\n                                   @Config(\"repositoryCache.cacheInfoDir\") @Nullable String cacheInfoDir) throws IOException {\n\n        this.cacheDir = getPath(cacheDir, \"repoCache\");\n        this.cacheInfoDir = getPath(cacheInfoDir, \"repoCacheInfo\");\n\n        log.info(\"init -> using {} ({}) to cache repositories\", this.cacheDir, this.cacheInfoDir);\n    }\n\n    public Path getCacheDir() {\n        return cacheDir;\n    }\n\n    public Duration getLockTimeout() {\n        return lockTimeout;\n    }\n\n    public int getLockCount() {\n        return lockCount;\n    }\n\n    public boolean isConcordFileValidationEnabled() {\n        return concordFileValidationEnabled;\n    }\n\n    public Duration getMaxAge() {\n        return maxAge;\n    }\n\n    public Path getCacheInfoDir() {\n        return cacheInfoDir;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/SecretStoreConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\n\npublic class SecretStoreConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"secretStore.serverPassword\")\n    private byte[] serverPwd;\n\n    @Inject\n    @Config(\"secretStore.secretStoreSalt\")\n    private byte[] salt;\n\n    @Inject\n    @Config(\"secretStore.projectSecretSalt\")\n    private byte[] projectSecretsSalt;\n\n    @Inject\n    @Config(\"secretStore.maxEncryptedStringLength\")\n    private int maxEncryptedStringLength;\n\n    @Inject\n    @Config(\"secretStore.keySize\")\n    private int keySize;\n\n    public byte[] getServerPwd() {\n        return serverPwd;\n    }\n\n    public byte[] getSecretStoreSalt() {\n        return salt;\n    }\n\n    public byte[] getProjectSecretsSalt() {\n        return projectSecretsSalt;\n    }\n\n    public int getMaxEncryptedStringLength() {\n        return maxEncryptedStringLength;\n    }\n\n    public int getKeySize() {\n        return keySize;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/ServerConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\nimport org.eclipse.jetty.server.CustomRequestLog;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport java.io.Serializable;\nimport java.time.Duration;\n\npublic class ServerConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    public static final String ACCESS_LOG_FORMAT = CustomRequestLog.EXTENDED_NCSA_FORMAT + \" %{ms}T\";\n\n    @Inject\n    @Config(\"server.port\")\n    private int port;\n\n    @Inject\n    @Config(\"server.secureCookies\")\n    private boolean secureCookies;\n\n    @Inject\n    @Config(\"server.cookieComment\")\n    private String cookieComment;\n\n    @Inject\n    @Config(\"server.sessionTimeout\")\n    private Duration sessionTimeout;\n\n    @Inject\n    @Nullable\n    @Config(\"server.accessLogPath\")\n    private String accessLogPath;\n\n    @Inject\n    @Config(\"server.accessLogRetainDays\")\n    private int accessLogRetainDays;\n\n    @Inject\n    @Nullable\n    @Config(\"server.baseResourcePath\")\n    private String baseResourcePath;\n\n    @Inject\n    @Config(\"server.requestHeaderSize\")\n    private int requestHeaderSize;\n\n    @Inject\n    private CORSConfiguration corsConfiguration;\n\n    public int getPort() {\n        return port;\n    }\n\n    public boolean isSecureCookies() {\n        return secureCookies;\n    }\n\n    public String getCookieComment() {\n        return cookieComment;\n    }\n\n    public Duration getSessionTimeout() {\n        return sessionTimeout;\n    }\n\n    @Nullable\n    public String getAccessLogPath() {\n        return accessLogPath;\n    }\n\n    public int getAccessLogRetainDays() {\n        return accessLogRetainDays;\n    }\n\n    @Nullable\n    public String getBaseResourcePath() {\n        return baseResourcePath;\n    }\n\n    public int getRequestHeaderSize() {\n        return requestHeaderSize;\n    }\n\n    public CORSConfiguration getCORSConfiguration() {\n        return corsConfiguration;\n    }\n\n    public static class CORSConfiguration implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        @Inject\n        @Config(\"server.cors.allowOrigin\")\n        private String allowOrigin;\n\n        public String getAllowOrigin() {\n            return allowOrigin;\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/TemplatesConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.inject.Inject;\n\npublic class TemplatesConfiguration {\n\n    @Inject\n    @Config(\"templates.allowScripting\")\n    private boolean allowScripting;\n\n    public boolean isAllowScripting() {\n        return allowScripting;\n    }\n\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/TriggersConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.inject.Inject;\nimport java.util.List;\nimport java.util.Map;\n\npublic class TriggersConfiguration {\n\n    @Inject\n    @Config(\"triggers.disableAll\")\n    private boolean disableAll;\n\n    @Inject\n    @Config(\"triggers.disabled\")\n    private List<String> disabled;\n\n    @Inject\n    @Config(\"triggers.defaultConditions\")\n    private Map<String, Object> defaultConditions;\n\n    @Inject\n    @Config(\"triggers.defaultConfiguration\")\n    private Map<String, Object> defaultConfiguration;\n\n    public boolean isDisableAll() {\n        return disableAll;\n    }\n\n    public List<String> getDisabled(){\n        return disabled;\n    }\n\n    public Map<String, Object> getDefaultConditions() {\n        return defaultConditions;\n    }\n\n    public Map<String, Object> getDefaultConfiguration() {\n        return defaultConfiguration;\n    }\n\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/Utils.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.typesafe.config.Config;\nimport com.walmartlabs.concord.common.PathUtils;\n\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.function.Supplier;\n\npublic final class Utils {\n\n    public static Path getPath(String s, String defaultPrefix) throws IOException {\n        if (s == null) {\n            return PathUtils.createTempDir(defaultPrefix);\n        }\n        return Paths.get(s);\n    }\n\n    public static String getStringOrDefault(Config cfg, String key, Supplier<String> defaultValueSupplier) {\n        if (cfg.hasPath(key)) {\n            return cfg.getString(key);\n        }\n        return defaultValueSupplier.get();\n    }\n\n    private Utils() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/cfg/WorkerMetricsConfiguration.java",
    "content": "package com.walmartlabs.concord.server.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\n\npublic class WorkerMetricsConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"workerMetrics.groupByCapabilitiesProperty\")\n    private String groupByCapabilitiesProperty;\n\n    public String getGroupByCapabilitiesProperty() {\n        return groupByCapabilitiesProperty;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/console/ConsoleModule.java",
    "content": "package com.walmartlabs.concord.server.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.walmartlabs.concord.server.Utils.bindJaxRsResource;\nimport static com.walmartlabs.concord.server.Utils.bindServletHolder;\n\npublic class ConsoleModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        binder.bind(CustomFormServiceV1.class).in(SINGLETON);\n        binder.bind(CustomFormServiceV2.class).in(SINGLETON);\n\n        bindJaxRsResource(binder, ConsoleService.class);\n        bindJaxRsResource(binder, CustomFormService.class);\n        bindJaxRsResource(binder, ProcessCardResource.class);\n        bindJaxRsResource(binder, UserActivityResourceV2.class);\n\n        binder.bind(ResponseTemplates.class).in(SINGLETON);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/console/ConsoleService.java",
    "content": "package com.walmartlabs.concord.server.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.cfg.ConsoleConfiguration;\nimport com.walmartlabs.concord.server.org.OrganizationEntry;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.org.jsonstore.JsonStoreAccessManager;\nimport com.walmartlabs.concord.server.org.jsonstore.JsonStoreDao;\nimport com.walmartlabs.concord.server.org.jsonstore.JsonStoreEntry;\nimport com.walmartlabs.concord.server.org.jsonstore.JsonStoreQueryDao;\nimport com.walmartlabs.concord.server.org.project.ProjectAccessManager;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.org.project.ProjectEntry;\nimport com.walmartlabs.concord.server.org.project.RepositoryDao;\nimport com.walmartlabs.concord.server.org.secret.PasswordChecker;\nimport com.walmartlabs.concord.server.org.secret.SecretDao;\nimport com.walmartlabs.concord.server.org.team.TeamDao;\nimport com.walmartlabs.concord.server.org.team.TeamRole;\nimport com.walmartlabs.concord.server.repository.InvalidRepositoryPathException;\nimport com.walmartlabs.concord.server.repository.RepositoryManager;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport com.walmartlabs.concord.server.security.SecurityUtils;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.security.apikey.ApiKeyDao;\nimport com.walmartlabs.concord.server.security.ldap.LdapGroupSearchResult;\nimport com.walmartlabs.concord.server.security.ldap.LdapPrincipal;\nimport com.walmartlabs.concord.server.user.RoleEntry;\nimport com.walmartlabs.concord.server.user.UserEntry;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport org.jooq.Configuration;\n\nimport javax.inject.Inject;\nimport javax.validation.constraints.Size;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.HttpHeaders;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport javax.ws.rs.core.Response.Status;\nimport java.net.URI;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.server.jooq.tables.Organizations.ORGANIZATIONS;\nimport static com.walmartlabs.concord.server.jooq.tables.Teams.TEAMS;\nimport static com.walmartlabs.concord.server.jooq.tables.UserTeams.USER_TEAMS;\n\n@Path(\"/api/service/console\")\npublic class ConsoleService implements Resource {\n\n    private final ProjectDao projectDao;\n    private final RepositoryManager repositoryManager;\n    private final UserManager userManager;\n    private final SecretDao secretDao;\n    private final OrganizationManager orgManager;\n    private final RepositoryDao repositoryDao;\n    private final TeamDao teamDao;\n    private final ApiKeyDao apiKeyDao;\n    private final ProjectAccessManager projectAccessManager;\n    private final JsonStoreDao storageDao;\n    private final JsonStoreQueryDao storageQueryDao;\n    private final JsonStoreAccessManager jsonStoreAccessManager;\n    private final ConsoleServiceDao consoleServiceDao;\n    private final ConsoleConfiguration consoleCfg;\n\n    @Inject\n    public ConsoleService(ProjectDao projectDao,\n                          RepositoryManager repositoryManager,\n                          UserManager userManager,\n                          SecretDao secretDao,\n                          OrganizationManager orgManager,\n                          RepositoryDao repositoryDao,\n                          TeamDao teamDao,\n                          ApiKeyDao apiKeyDao,\n                          ProjectAccessManager projectAccessManager,\n                          JsonStoreDao storageDao,\n                          JsonStoreQueryDao storageQueryDao,\n                          JsonStoreAccessManager jsonStoreAccessManager,\n                          ConsoleServiceDao consoleServiceDao,\n                          ConsoleConfiguration consoleCfg) {\n\n        this.projectDao = projectDao;\n        this.repositoryManager = repositoryManager;\n        this.userManager = userManager;\n        this.repositoryDao = repositoryDao;\n        this.secretDao = secretDao;\n        this.orgManager = orgManager;\n        this.teamDao = teamDao;\n        this.apiKeyDao = apiKeyDao;\n        this.projectAccessManager = projectAccessManager;\n        this.storageDao = storageDao;\n        this.storageQueryDao = storageQueryDao;\n        this.jsonStoreAccessManager = jsonStoreAccessManager;\n        this.consoleServiceDao = consoleServiceDao;\n        this.consoleCfg = consoleCfg;\n    }\n\n    @GET\n    @Path(\"/cfg\")\n    @Produces(MediaType.TEXT_PLAIN)\n    public Response getCfg() {\n        if (consoleCfg.isUseDefaultCfg()) {\n            return Response.seeOther(URI.create(\"/cfg.js\")).build();\n        }\n        return Response.ok(consoleCfg.getCfg()).build();\n    }\n\n    @GET\n    @Path(\"/userInfo\")\n    @Produces(MediaType.APPLICATION_JSON)\n    public UserInfoResponse getUserInfo() {\n        UserPrincipal p = UserPrincipal.getCurrent();\n        if (p == null) {\n            throw new ConcordApplicationException(\"Can't determine current user: principal not found\",\n                    Status.INTERNAL_SERVER_ERROR);\n        }\n\n        UserEntry u = p.getUser();\n        if (u == null) {\n            throw new ConcordApplicationException(\"Can't determine current user: user entry not found\",\n                    Status.INTERNAL_SERVER_ERROR);\n        }\n\n        Set<String> userLdapGroups = Optional.ofNullable(LdapPrincipal.getCurrent())\n                .map(LdapPrincipal::getGroups)\n                .orElse(Set.of());\n\n        return UserInfoResponse.builder()\n                .id(u.getId())\n                .displayName(displayName(u, p))\n                .teams(consoleServiceDao.listTeams(p.getId()))\n                .roles(u.getRoles().stream().map(RoleEntry::getName).toList())\n                .ldapGroups(userLdapGroups)\n                .build();\n    }\n\n    @GET\n    @Path(\"/whoami\")\n    @Produces(MediaType.APPLICATION_JSON)\n    public UserResponse whoami() {\n        UserPrincipal p = UserPrincipal.getCurrent();\n        if (p == null) {\n            throw new ConcordApplicationException(\"Can't determine current user: principal not found\",\n                    Status.INTERNAL_SERVER_ERROR);\n        }\n\n        UserEntry u = p.getUser();\n        if (u == null) {\n            throw new ConcordApplicationException(\"Can't determine current user: user entry not found\",\n                    Status.INTERNAL_SERVER_ERROR);\n        }\n\n        UserEntry user = userManager.get(p.getId())\n                .orElseThrow(() -> new ConcordApplicationException(\"Unknown user: \" + p.getId()));\n\n        if (user.isDisabled()) {\n            throw new UnauthorizedException(\"User is disabled: \" + p.getId());\n        }\n\n        return new UserResponse(p.getRealm(), user.getName(), user.getDomain(), displayName(u, p), user.getOrgs());\n    }\n\n    @POST\n    @Path(\"/logout\")\n    public void logout() {\n        SecurityUtils.logout();\n    }\n\n    @GET\n    @Path(\"/org/{orgName}/project/{projectName}/exists\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    public boolean isProjectExists(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                   @PathParam(\"projectName\") String projectName) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n        return projectDao.getId(org.getId(), projectName) != null;\n    }\n\n    @GET\n    @Path(\"/org/{orgName}/jsonstore/{storageName}/exists\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    public boolean isStorageExists(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                   @PathParam(\"storageName\") String storageName) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n        return storageDao.getId(org.getId(), storageName) != null;\n    }\n\n    @GET\n    @Path(\"/org/{orgName}/jsonstore/{storeName}/query/{queryName}/exists\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    public boolean isStorageQueryExists(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                        @PathParam(\"storeName\") String storeName,\n                                        @PathParam(\"queryName\") String queryName) {\n\n        try {\n            OrganizationEntry org = orgManager.assertAccess(orgName, true);\n            JsonStoreEntry storage = jsonStoreAccessManager.assertAccess(org.getId(), null, storeName, ResourceAccessLevel.READER, true);\n            return storageQueryDao.getId(storage.id(), queryName) != null;\n        } catch (UnauthorizedException e) {\n            return false;\n        }\n    }\n\n    @GET\n    @Path(\"/org/{orgName}/secret/{secretName}/exists\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    public boolean isSecretExists(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                  @PathParam(\"secretName\") String secretName) {\n        try {\n            OrganizationEntry org = orgManager.assertAccess(orgName, true);\n            return secretDao.getId(org.getId(), secretName) != null;\n        } catch (UnauthorizedException e) {\n            return false;\n        }\n    }\n\n    @GET\n    @Path(\"/org/{orgName}/project/{projectName}/repo/{repoName}/exists\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    public boolean isRepositoryExists(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                      @PathParam(\"projectName\") @ConcordKey String projectName,\n                                      @PathParam(\"repoName\") String repoName) {\n        try {\n            OrganizationEntry org = orgManager.assertAccess(orgName, true);\n            UUID projectId = projectDao.getId(org.getId(), projectName);\n            if (projectId == null) {\n                throw new ConcordApplicationException(\"Project not found: \" + projectName, Status.BAD_REQUEST);\n            }\n            return repositoryDao.getId(projectId, repoName) != null;\n        } catch (UnauthorizedException e) {\n            return false;\n        }\n    }\n\n    @GET\n    @Path(\"/org/{orgName}/team/{teamName}/exists\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    public boolean isTeamExists(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                @PathParam(\"teamName\") @ConcordKey String teamName) {\n        try {\n            OrganizationEntry org = orgManager.assertAccess(orgName, true);\n            return teamDao.getId(org.getId(), teamName) != null;\n        } catch (UnauthorizedException e) {\n            return false;\n        }\n    }\n\n    @GET\n    @Path(\"/apikey/{name}/exists\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    public boolean isApiTokenExists(@PathParam(\"name\") @ConcordKey String tokenName) {\n        UserPrincipal currentUser = UserPrincipal.getCurrent();\n        if (currentUser == null) {\n            return false;\n        }\n\n        UUID userId = currentUser.getId();\n        return apiKeyDao.getId(userId, tokenName) != null;\n    }\n\n    @POST\n    @Path(\"/repository/test\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    public boolean testRepository(RepositoryTestRequest req) {\n        OrganizationEntry org = orgManager.assertAccess(null, req.getOrgName(), false);\n        ProjectEntry project = projectAccessManager.assertAccess(org.getId(), null, req.getProjectName(), ResourceAccessLevel.READER, false);\n\n        try {\n            String secretName = secretDao.getName(req.getSecretId());\n            repositoryManager.testConnection(project.getOrgId(), project.getId(), req.getUrl(), req.getBranch(), req.getCommitId(), req.getPath(), secretName);\n            return true;\n        } catch (InvalidRepositoryPathException e) {\n            Map<String, String> m = new HashMap<>();\n            m.put(\"message\", \"Repository validation error\");\n            m.put(\"level\", \"WARN\");\n            m.put(\"details\", e.getMessage());\n\n            throw new ConcordApplicationException(Response.status(Status.INTERNAL_SERVER_ERROR)\n                    .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)\n                    .entity(m)\n                    .build());\n        } catch (Exception e) {\n            String msg;\n            Throwable t = e;\n            while (true) {\n                msg = t.getMessage();\n                t = t.getCause();\n                if (t == null) {\n                    break;\n                }\n            }\n\n            if (msg == null) {\n                msg = \"Repository test error\";\n            }\n\n            throw new ConcordApplicationException(Response.status(Status.INTERNAL_SERVER_ERROR)\n                    .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN)\n                    .entity(msg)\n                    .build());\n        }\n    }\n\n    @GET\n    @Path(\"/search/ldapGroups\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @WithTimer\n    public List<LdapGroupSearchResult> searchLdapGroups(@QueryParam(\"filter\") @Size(min = 5, max = 256) String filter) {\n        if (filter == null) {\n            return Collections.emptyList();\n        }\n\n        filter = filter.trim();\n        if (filter.startsWith(\"*\")) {\n            // disallow \"starts-with\" filters, they can be too slow\n            return Collections.emptyList();\n        }\n\n        return userManager.searchLdapGroups(filter);\n    }\n\n    @POST\n    @Path(\"/validate-password\")\n    @Consumes(MediaType.TEXT_PLAIN)\n    @Produces(MediaType.APPLICATION_JSON)\n    public boolean validatePassword(String pwd) {\n        try {\n            PasswordChecker.check(pwd);\n        } catch (PasswordChecker.CheckerException e) {\n            return false;\n        }\n\n        return true;\n    }\n\n    private static String displayName(UserEntry u, UserPrincipal p) {\n        String displayName = u.getDisplayName();\n\n        if (displayName == null) {\n            LdapPrincipal l = LdapPrincipal.getCurrent();\n            if (l != null) {\n                displayName = l.getDisplayName();\n            }\n        }\n\n        if (displayName == null) {\n            displayName = p.getUsername();\n        }\n\n        return displayName;\n    }\n\n    public static class ConsoleServiceDao extends AbstractDao {\n\n        @Inject\n        public ConsoleServiceDao(@MainDB Configuration cfg) {\n            super(cfg);\n        }\n\n        @Override\n        protected <T> T txResult(TxResult<T> t) {\n            return super.txResult(t);\n        }\n\n        public List<UserInfoResponse.UserTeamInfo> listTeams(UUID userId) {\n            return txResult(tx -> tx.select(ORGANIZATIONS.ORG_NAME, TEAMS.TEAM_NAME, USER_TEAMS.TEAM_ROLE)\n                    .from(USER_TEAMS)\n                    .join(TEAMS).on(TEAMS.TEAM_ID.eq(USER_TEAMS.TEAM_ID))\n                    .join(ORGANIZATIONS).on(ORGANIZATIONS.ORG_ID.eq(TEAMS.ORG_ID))\n                    .where(USER_TEAMS.USER_ID.eq(userId))\n                    .fetch(r -> UserInfoResponse.UserTeamInfo.of(r.get(ORGANIZATIONS.ORG_NAME), r.get(TEAMS.TEAM_NAME), TeamRole.valueOf(r.get(USER_TEAMS.TEAM_ROLE)))));\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/console/CustomFormService.java",
    "content": "package com.walmartlabs.concord.server.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.state.ProcessStateManager;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport org.jboss.resteasy.plugins.providers.multipart.MultipartInput;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.Consumes;\nimport javax.ws.rs.POST;\nimport javax.ws.rs.PathParam;\nimport javax.ws.rs.Produces;\nimport javax.ws.rs.core.*;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.process.state.ProcessStateManager.path;\n\n@javax.ws.rs.Path(\"/api/service/custom_form\")\npublic class CustomFormService implements Resource {\n\n    private final ProcessStateManager stateManager;\n    private final CustomFormServiceV1 customFormServiceV1;\n    private final CustomFormServiceV2 customFormServiceV2;\n\n    @Inject\n    public CustomFormService(ProcessStateManager stateManager, CustomFormServiceV1 customFormServiceV1, CustomFormServiceV2 customFormServiceV2) {\n        this.stateManager = stateManager;\n        this.customFormServiceV1 = customFormServiceV1;\n        this.customFormServiceV2 = customFormServiceV2;\n    }\n\n    @POST\n    @javax.ws.rs.Path(\"{processInstanceId}/{formName}/start\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    public FormSessionResponse startSession(@PathParam(\"processInstanceId\") UUID processInstanceId,\n                                            @PathParam(\"formName\") String formName) {\n\n        if (isV2(processInstanceId)) {\n            return customFormServiceV2.startSession(processInstanceId, formName);\n        } else {\n            return customFormServiceV1.startSession(processInstanceId, formName);\n        }\n    }\n\n    @POST\n    @javax.ws.rs.Path(\"{processInstanceId}/{formName}/continue\")\n    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    public Response continueSession(@Context UriInfo uriInfo,\n                                    @Context HttpHeaders headers,\n                                    @PathParam(\"processInstanceId\") UUID processInstanceId,\n                                    @PathParam(\"formName\") String formName,\n                                    MultivaluedMap<String, String> data) {\n\n        if (isV2(processInstanceId)) {\n            return customFormServiceV2.continueSession(uriInfo, headers, processInstanceId, formName, data);\n        } else {\n            return customFormServiceV1.continueSession(uriInfo, headers, processInstanceId, formName, data);\n        }\n    }\n\n    @POST\n    @javax.ws.rs.Path(\"{processInstanceId}/{formName}/continue\")\n    @Consumes(MediaType.MULTIPART_FORM_DATA)\n    @Produces(MediaType.APPLICATION_JSON)\n    public Response continueSession(@Context UriInfo uriInfo,\n                                    @Context HttpHeaders headers,\n                                    @PathParam(\"processInstanceId\") UUID processInstanceId,\n                                    @PathParam(\"formName\") String formName,\n                                    MultipartInput data) {\n\n        try {\n            if (isV2(processInstanceId)) {\n                return customFormServiceV2.continueSession(uriInfo, headers, processInstanceId, formName, data);\n            } else {\n                return customFormServiceV1.continueSession(uriInfo, headers, processInstanceId, formName, data);\n            }\n        } finally {\n            data.close();\n        }\n    }\n\n    private boolean isV2(UUID processInstanceId) {\n        String resource = path(Constants.Files.JOB_ATTACHMENTS_DIR_NAME,\n                Constants.Files.JOB_STATE_DIR_NAME,\n                Constants.Files.JOB_FORMS_V2_DIR_NAME);\n        return stateManager.exists(PartialProcessKey.from(processInstanceId), resource);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/console/CustomFormServiceV1.java",
    "content": "package com.walmartlabs.concord.server.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.SerializationFeature;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.forms.ValidationError;\nimport com.walmartlabs.concord.server.MultipartUtils;\nimport com.walmartlabs.concord.server.cfg.CustomFormConfiguration;\nimport com.walmartlabs.concord.server.process.form.ExternalFileFormValidatorLocale;\nimport com.walmartlabs.concord.server.process.form.FormServiceV1;\nimport com.walmartlabs.concord.server.process.form.FormSubmitResult;\nimport com.walmartlabs.concord.server.process.form.FormUtils;\nimport com.walmartlabs.concord.server.process.form.FormUtils.ValidationException;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueDao;\nimport com.walmartlabs.concord.server.process.state.ProcessStateManager;\nimport com.walmartlabs.concord.server.sdk.*;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport io.takari.bpm.form.Form;\nimport io.takari.bpm.model.form.FormDefinition;\nimport io.takari.bpm.model.form.FormField;\nimport io.takari.bpm.model.form.FormField.Cardinality;\nimport org.immutables.value.Value;\nimport org.jboss.resteasy.plugins.providers.multipart.MultipartInput;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport javax.ws.rs.Consumes;\nimport javax.ws.rs.POST;\nimport javax.ws.rs.PathParam;\nimport javax.ws.rs.Produces;\nimport javax.ws.rs.core.*;\nimport javax.ws.rs.core.Response.Status;\nimport java.io.IOException;\nimport java.io.Serializable;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.process.state.ProcessStateManager.copyTo;\n\npublic class CustomFormServiceV1 {\n\n    private static final Logger log = LoggerFactory.getLogger(CustomFormServiceV1.class);\n\n    private static final String FORMS_PATH_PREFIX = \"/forms/\";\n    private static final String FORM_DIR_NAME = \"form\";\n    private static final String SHARED_DIR_NAME = \"shared\";\n    private static final String FORMS_PATH_TEMPLATE = FORMS_PATH_PREFIX + \"%s/%s/\" + FORM_DIR_NAME + \"/\";\n    private static final String DATA_FILE_TEMPLATE = \"data = %s;\";\n\n    private static final String NON_BRANDED_FORM_URL_TEMPLATE = \"/#/process/%s/form/%s/wizard?fullScreen=true\";\n    private static final String FORM_WIZARD_CONTINUE_URL_TEMPLATE = \"/api/service/custom_form/%s/%s/continue\";\n\n    private static final long STATUS_REFRESH_DELAY = 250;\n\n    private final CustomFormConfiguration cfg;\n    private final FormServiceV1 formService;\n    private final ProcessStateManager stateManager;\n    private final ProcessQueueDao queueDao;\n    private final ProcessKeyCache processKeyCache;\n    private final ObjectMapper objectMapper;\n\n    @Inject\n    public CustomFormServiceV1(CustomFormConfiguration cfg,\n                               FormServiceV1 formService,\n                               ProcessStateManager stateManager,\n                               ProcessQueueDao queueDao,\n                               ProcessKeyCache processKeyCache) {\n\n        this.cfg = cfg;\n        this.formService = formService;\n        this.stateManager = stateManager;\n        this.queueDao = queueDao;\n        this.processKeyCache = processKeyCache;\n\n        this.objectMapper = new ObjectMapper()\n                .enable(SerializationFeature.INDENT_OUTPUT);\n    }\n\n    @POST\n    @javax.ws.rs.Path(\"{processInstanceId}/{formName}/start\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    public FormSessionResponse startSession(@PathParam(\"processInstanceId\") UUID processInstanceId,\n                                            @PathParam(\"formName\") String formName) {\n\n        ProcessKey processKey = processKeyCache.assertKey(processInstanceId);\n        return startSession(processKey, formName);\n    }\n\n    private FormSessionResponse startSession(ProcessKey processKey, String formName) {\n\n        // TODO locking\n        Form form = assertForm(processKey, formName);\n\n        Path dst = cfg.getBaseDir()\n                .resolve(processKey.toString())\n                .resolve(formName);\n\n        try {\n            Path formDir = dst.resolve(FORM_DIR_NAME);\n            if (!Files.exists(formDir)) {\n                Files.createDirectories(formDir);\n            }\n\n            String resource = FormServiceV1.FORMS_RESOURCES_PATH + \"/\" + form.getFormDefinition().getName();\n            // copy original branding files into the target directory\n            boolean branded = stateManager.exportDirectory(processKey, resource, copyTo(formDir));\n            if (!branded) {\n                // not branded, redirect to the default wizard\n                String uri = String.format(NON_BRANDED_FORM_URL_TEMPLATE, processKey, formName);\n                return new FormSessionResponse(uri);\n            }\n\n            // create JS file containing the form's data\n            writeData(formDir, initialData(form));\n\n            // copy shared resources (if present)\n            copySharedResources(processKey, dst);\n        } catch (IOException e) {\n            log.warn(\"startSession ['{}', '{}'] -> error while preparing a custom form: {}\", processKey, formName, e.getMessage());\n            throw new ConcordApplicationException(\"Error while preparing a custom form\", e);\n        }\n\n        return new FormSessionResponse(formPath(processKey, formName));\n    }\n\n    @POST\n    @javax.ws.rs.Path(\"{processInstanceId}/{formName}/continue\")\n    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    public Response continueSession(@Context UriInfo uriInfo,\n                                    @Context HttpHeaders headers,\n                                    @PathParam(\"processInstanceId\") UUID processInstanceId,\n                                    @PathParam(\"formName\") String formName,\n                                    MultivaluedMap<String, String> data) {\n\n        ProcessKey processKey = processKeyCache.assertKey(processInstanceId);\n        return continueSession(uriInfo, headers, processKey, formName, FormUtils.convert(data));\n    }\n\n    @POST\n    @javax.ws.rs.Path(\"{processInstanceId}/{formName}/continue\")\n    @Consumes(MediaType.MULTIPART_FORM_DATA)\n    @Produces(MediaType.APPLICATION_JSON)\n    public Response continueSession(@Context UriInfo uriInfo,\n                                    @Context HttpHeaders headers,\n                                    @PathParam(\"processInstanceId\") UUID processInstanceId,\n                                    @PathParam(\"formName\") String formName,\n                                    MultipartInput data) {\n\n        try {\n            ProcessKey processKey = processKeyCache.assertKey(processInstanceId);\n            return continueSession(uriInfo, headers, processKey, formName, MultipartUtils.toMap(data));\n        } finally {\n            data.close();\n        }\n    }\n\n    private Response continueSession(UriInfo uriInfo, HttpHeaders headers, ProcessKey processKey,\n                                     String formName, Map<String, Object> data) {\n        // TODO locking\n        Form form = assertForm(processKey, formName);\n\n        // TODO constants\n        Map<String, Object> opts = form.getOptions();\n        boolean yield = opts != null && (boolean) opts.getOrDefault(\"yield\", false);\n\n        Path dst = cfg.getBaseDir()\n                .resolve(processKey.toString())\n                .resolve(formName);\n\n        Path formDir = dst.resolve(FORM_DIR_NAME);\n\n        try {\n            Map<String, Object> m = new HashMap<>();\n            try {\n                m = FormUtils.convert(new ExternalFileFormValidatorLocale(processKey, formName, stateManager), form, data);\n\n                FormSubmitResult r = formService.submit(processKey, formName, m);\n                if (r.isValid()) {\n                    if (yield) {\n                        // this was the last \"interactive\" form. The process will continue in \"background\"\n                        // and users should get a success page.\n                        writeData(formDir, success(form, m));\n                    } else {\n                        while (true) {\n                            ProcessStatus s = queueDao.getStatus(processKey);\n\n                            if (s == ProcessStatus.SUSPENDED) {\n                                String nextFormId = formService.nextFormId(processKey);\n                                if (nextFormId == null) {\n                                    writeData(formDir, success(form, m));\n                                    break;\n                                } else {\n                                    FormSessionResponse nextSession = startSession(processKey, nextFormId);\n                                    return redirectTo(nextSession.getUri());\n                                }\n                            } else if (s == ProcessStatus.FAILED || s == ProcessStatus.CANCELLED || s == ProcessStatus.TIMED_OUT) {\n                                writeData(formDir, processFailed(form, m));\n                                break;\n                            } else if (s == ProcessStatus.FINISHED) {\n                                writeData(formDir, success(form, m));\n                                break;\n                            }\n\n                            try {\n                                // TODO exp back off?\n                                Thread.sleep(STATUS_REFRESH_DELAY);\n                            } catch (InterruptedException e) {\n                                Thread.currentThread().interrupt();\n                            }\n                        }\n                    }\n                } else {\n                    writeData(formDir, prepareData(form, m, r.getErrors()));\n                }\n            } catch (ValidationException e) {\n                ValidationError err = ValidationError.of(e.getField().getName(), e.getMessage());\n                FormData d = prepareData(form, m, Collections.singletonList(err));\n                writeData(formDir, d);\n            }\n        } catch (Exception e) {\n            throw new ConcordApplicationException(\"Error while submitting a form\", e);\n        }\n\n        return redirectToForm(uriInfo, headers, processKey, formName);\n    }\n\n    private Form assertForm(ProcessKey processKey, String formName) {\n        Form form = formService.get(processKey, formName);\n        if (form == null) {\n            log.warn(\"assertForm ['{}', '{}'] -> not found\", processKey, formName);\n            throw new ConcordApplicationException(\"Form not found\", Status.NOT_FOUND);\n        }\n        return form;\n    }\n\n    private FormData initialData(Form form) {\n        return prepareData(false, false, form, null, false, null);\n    }\n\n    private FormData success(Form form, Map<String, Object> overrides) {\n        return prepareData(true, false, form, overrides, false, null);\n    }\n\n    private FormData processFailed(Form form, Map<String, Object> overrides) {\n        return prepareData(false, true, form, overrides, false, null);\n    }\n\n    private FormData prepareData(Form form, Map<String, Object> overrides, List<ValidationError> errors) {\n        return prepareData(false, false, form, overrides, true, errors);\n    }\n\n\n    private FormData prepareData(boolean success, boolean processFailed, Form form,\n                                 Map<String, Object> overrides, boolean skipMissingOverrides,\n                                 List<ValidationError> errors) {\n\n        String processInstanceId = form.getProcessBusinessKey();\n        String formName = form.getFormDefinition().getName();\n\n        String submitUrl = String.format(FORM_WIZARD_CONTINUE_URL_TEMPLATE, processInstanceId, formName);\n\n        // TODO merge with FormResource\n        Map<String, FormDataDefinition> _definitions = new HashMap<>();\n\n        // the order of precedence should be:\n        //   submitted value > form call value > field's default value > environment value\n        Map<String, String> _errors = new HashMap<>();\n        Map<String, Object> data = FormUtils.values(form);\n        Map<String, Object> extra = FormUtils.extraValues(form);\n        Map<String, Object> _values = new HashMap<>(extra);\n\n        Map<String, Object> allowedValues = form.getAllowedValues();\n        if (allowedValues == null) {\n            allowedValues = Collections.emptyMap();\n        }\n\n        FormDefinition fd = form.getFormDefinition();\n        List<String> fields = fd.getFields().stream()\n                .map(FormField::getName)\n                .collect(Collectors.toList());\n\n        for (FormField f : fd.getFields()) {\n            Object allowedValue = allowedValues.get(f.getName());\n\n            _definitions.put(f.getName(), new FormDataDefinition(f.getLabel(), f.getType(), f.getCardinality(), allowedValue));\n\n            Object v = overrides != null ? overrides.get(f.getName()) : null;\n            if (v == null && skipMissingOverrides) {\n                continue;\n            }\n\n            if (v == null) {\n                v = data.get(f.getName());\n            }\n\n            if (v == null) {\n                continue;\n            }\n\n            _values.put(f.getName(), v);\n        }\n\n        if (errors != null) {\n            for (ValidationError e : errors) {\n                _errors.put(e.fieldName(), e.error());\n            }\n        }\n\n        return FormData.builder()\n                .txId(processInstanceId)\n                .formName(formName)\n                .success(success)\n                .processFailed(processFailed)\n                .submitUrl(submitUrl)\n                .fields(fields)\n                .definitions(_definitions)\n                .values(_values)\n                .errors(_errors)\n                .build();\n    }\n\n    private void writeData(Path baseDir, Object data) throws IOException {\n        Path dst = baseDir.resolve(\"data.js\");\n\n        Path parent = dst.getParent();\n        if (!Files.exists(parent)) {\n            Files.createDirectories(parent);\n        }\n\n        String s = String.format(DATA_FILE_TEMPLATE, objectMapper.writeValueAsString(data));\n        Files.write(dst, s.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);\n    }\n\n    private void copySharedResources(ProcessKey processKey, Path dst) throws IOException {\n        Path sharedDir = dst.resolve(SHARED_DIR_NAME);\n        if (!Files.exists(sharedDir)) {\n            Files.createDirectories(sharedDir);\n        }\n\n        String resource = FormServiceV1.FORMS_RESOURCES_PATH + \"/\" + SHARED_DIR_NAME;\n        stateManager.exportDirectory(processKey, resource, copyTo(sharedDir));\n    }\n\n    private static Response redirectToForm(UriInfo uriInfo, HttpHeaders headers,\n                                           PartialProcessKey processKey, String formName) {\n\n        String scheme = uriInfo.getBaseUri().getScheme();\n\n        // check if we need to force https\n        String origin = headers.getHeaderString(\"Origin\");\n        if (origin != null) {\n            URI originUri = URI.create(origin);\n            String originScheme = originUri.getScheme();\n            if (\"https\".equalsIgnoreCase(originScheme)) {\n                scheme = originScheme;\n            }\n        }\n\n        UriBuilder b = UriBuilder.fromUri(uriInfo.getBaseUri())\n                .scheme(scheme)\n                .path(formPath(processKey, formName));\n\n        return redirectTo(b.build().toString());\n    }\n\n    private static Response redirectTo(String path) {\n        return Response.status(Status.MOVED_PERMANENTLY)\n                .header(HttpHeaders.LOCATION, path)\n                .build();\n    }\n\n    private static String formPath(PartialProcessKey processKey, String formName) {\n        return String.format(FORMS_PATH_TEMPLATE, processKey, formName);\n    }\n\n    @Value.Immutable\n    @JsonInclude(JsonInclude.Include.NON_EMPTY)\n    @JsonSerialize(as = ImmutableFormData.class)\n    @JsonDeserialize(as = ImmutableFormData.class)\n    public interface FormData extends Serializable {\n\n        long serialVersionUID = -1591440785695774602L;\n\n        String txId();\n\n        String formName();\n\n        boolean success();\n\n        boolean processFailed();\n\n        String submitUrl();\n\n        List<String> fields();\n\n        Map<String, FormDataDefinition> definitions();\n\n        @Nullable\n        Map<String, Object> values();\n\n        @Nullable\n        Map<String, String> errors();\n\n        static ImmutableFormData.Builder builder() {\n            return ImmutableFormData.builder();\n        }\n    }\n\n    @JsonInclude(Include.NON_NULL)\n    public static class FormDataDefinition implements Serializable {\n\n        private static final long serialVersionUID = -3887713057699620530L;\n\n        private final String label;\n        private final String type;\n        private final Cardinality cardinality;\n        private final Object allow;\n\n        public FormDataDefinition(String label, String type, Cardinality cardinality, Object allow) {\n            this.label = label;\n            this.type = type;\n            this.cardinality = cardinality;\n            this.allow = allow;\n        }\n\n        public String getLabel() {\n            return label;\n        }\n\n        public String getType() {\n            return type;\n        }\n\n        public Cardinality getCardinality() {\n            return cardinality;\n        }\n\n        public Object getAllow() {\n            return allow;\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/console/CustomFormServiceV2.java",
    "content": "package com.walmartlabs.concord.server.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.SerializationFeature;\nimport com.walmartlabs.concord.forms.Form;\nimport com.walmartlabs.concord.forms.FormField;\nimport com.walmartlabs.concord.forms.FormUtils;\nimport com.walmartlabs.concord.forms.ValidationError;\nimport com.walmartlabs.concord.server.MultipartUtils;\nimport com.walmartlabs.concord.server.cfg.CustomFormConfiguration;\nimport com.walmartlabs.concord.server.process.form.FormServiceV1;\nimport com.walmartlabs.concord.server.process.form.FormServiceV2;\nimport com.walmartlabs.concord.server.process.form.FormSubmitResult;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueDao;\nimport com.walmartlabs.concord.server.process.state.ProcessStateManager;\nimport com.walmartlabs.concord.server.sdk.*;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport org.jboss.resteasy.plugins.providers.multipart.MultipartInput;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.Consumes;\nimport javax.ws.rs.POST;\nimport javax.ws.rs.PathParam;\nimport javax.ws.rs.Produces;\nimport javax.ws.rs.core.*;\nimport javax.ws.rs.core.Response.Status;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.console.CustomFormServiceV1.FormData;\nimport static com.walmartlabs.concord.server.console.CustomFormServiceV1.FormDataDefinition;\nimport static com.walmartlabs.concord.server.process.state.ProcessStateManager.copyTo;\n\npublic class CustomFormServiceV2 {\n\n    private static final Logger log = LoggerFactory.getLogger(CustomFormServiceV2.class);\n\n    private static final String FORMS_PATH_PREFIX = \"/forms/\";\n    private static final String FORM_DIR_NAME = \"form\";\n    private static final String SHARED_DIR_NAME = \"shared\";\n    private static final String FORMS_PATH_TEMPLATE = FORMS_PATH_PREFIX + \"%s/%s/\" + FORM_DIR_NAME + \"/\";\n    private static final String DATA_FILE_TEMPLATE = \"data = %s;\";\n\n    private static final String NON_BRANDED_FORM_URL_TEMPLATE = \"/#/process/%s/form/%s/wizard?fullScreen=true\";\n    private static final String FORM_WIZARD_CONTINUE_URL_TEMPLATE = \"/api/service/custom_form/%s/%s/continue\";\n\n    private static final long STATUS_REFRESH_DELAY = 250;\n\n    private final CustomFormConfiguration cfg;\n    private final FormServiceV2 formService;\n    private final ProcessStateManager stateManager;\n    private final ProcessQueueDao queueDao;\n    private final ProcessKeyCache processKeyCache;\n    private final ObjectMapper objectMapper;\n\n    @Inject\n    public CustomFormServiceV2(CustomFormConfiguration cfg,\n                               FormServiceV2 formService,\n                               ProcessStateManager stateManager,\n                               ProcessQueueDao queueDao,\n                               ProcessKeyCache processKeyCache) {\n\n        this.cfg = cfg;\n        this.formService = formService;\n        this.stateManager = stateManager;\n        this.queueDao = queueDao;\n        this.processKeyCache = processKeyCache;\n\n        this.objectMapper = new ObjectMapper()\n                .enable(SerializationFeature.INDENT_OUTPUT);\n    }\n\n    @POST\n    @javax.ws.rs.Path(\"{processInstanceId}/{formName}/start\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    public FormSessionResponse startSession(@PathParam(\"processInstanceId\") UUID processInstanceId,\n                                            @PathParam(\"formName\") String formName) {\n\n        ProcessKey processKey = processKeyCache.assertKey(processInstanceId);\n        return startSession(processKey, formName);\n    }\n\n    private FormSessionResponse startSession(ProcessKey processKey, String formName) {\n        // TODO locking\n        Form form = assertForm(processKey, formName);\n\n        Path dst = cfg.getBaseDir()\n                .resolve(processKey.toString())\n                .resolve(formName);\n\n        try {\n            Path formDir = dst.resolve(FORM_DIR_NAME);\n            if (!Files.exists(formDir)) {\n                Files.createDirectories(formDir);\n            }\n\n            String resource = FormServiceV1.FORMS_RESOURCES_PATH + \"/\" + form.name();\n            // copy original branding files into the target directory\n            boolean branded = stateManager.exportDirectory(processKey, resource, copyTo(formDir));\n            if (!branded) {\n                // not branded, redirect to the default wizard\n                String uri = String.format(NON_BRANDED_FORM_URL_TEMPLATE, processKey, formName);\n                return new FormSessionResponse(uri);\n            }\n\n            // create JS file containing the form's data\n            writeData(formDir, initialData(form, processKey.getInstanceId()));\n\n            // copy shared resources (if present)\n            copySharedResources(processKey, dst);\n        } catch (IOException e) {\n            log.warn(\"startSession ['{}', '{}'] -> error while preparing a custom form: {}\", processKey, formName, e);\n            throw new ConcordApplicationException(\"Error while preparing a custom form\", e);\n        }\n\n        return new FormSessionResponse(formPath(processKey, formName));\n    }\n\n    @POST\n    @javax.ws.rs.Path(\"{processInstanceId}/{formName}/continue\")\n    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    public Response continueSession(@Context UriInfo uriInfo,\n                                    @Context HttpHeaders headers,\n                                    @PathParam(\"processInstanceId\") UUID processInstanceId,\n                                    @PathParam(\"formName\") String formName,\n                                    MultivaluedMap<String, String> data) {\n\n        ProcessKey processKey = processKeyCache.assertKey(processInstanceId);\n        return continueSession(uriInfo, headers, processKey, formName, com.walmartlabs.concord.server.process.form.FormUtils.convert(data));\n    }\n\n    @POST\n    @javax.ws.rs.Path(\"{processInstanceId}/{formName}/continue\")\n    @Consumes(MediaType.MULTIPART_FORM_DATA)\n    @Produces(MediaType.APPLICATION_JSON)\n    public Response continueSession(@Context UriInfo uriInfo,\n                                    @Context HttpHeaders headers,\n                                    @PathParam(\"processInstanceId\") UUID processInstanceId,\n                                    @PathParam(\"formName\") String formName,\n                                    MultipartInput data) {\n\n        try {\n            ProcessKey processKey = processKeyCache.assertKey(processInstanceId);\n            return continueSession(uriInfo, headers, processKey, formName, MultipartUtils.toMap(data));\n        } finally {\n            data.close();\n        }\n    }\n\n    private Response continueSession(UriInfo uriInfo, HttpHeaders headers, ProcessKey processKey,\n                                     String formName, Map<String, Object> data) {\n        // TODO locking\n        Form form = assertForm(processKey, formName);\n\n        boolean yield = form.options().isYield();\n\n        Path dst = cfg.getBaseDir()\n                .resolve(processKey.toString())\n                .resolve(formName);\n\n        Path formDir = dst.resolve(FORM_DIR_NAME);\n\n        try {\n            Map<String, Object> m = new HashMap<>();\n            try {\n                m = formService.convertData(processKey, formName, data);\n\n                FormSubmitResult r = formService.submit(processKey, formName, m);\n                if (r.isValid()) {\n                    if (yield) {\n                        // this was the last \"interactive\" form. The process will continue in \"background\"\n                        // and users should get a success page.\n                        writeData(formDir, success(form, m, processKey.getInstanceId()));\n                    } else {\n                        while (true) {\n                            ProcessStatus s = queueDao.getStatus(processKey);\n\n                            if (s == ProcessStatus.SUSPENDED) {\n                                String nextFormId = formService.nextFormId(processKey);\n                                if (nextFormId == null) {\n                                    writeData(formDir, success(form, m, processKey.getInstanceId()));\n                                    break;\n                                } else {\n                                    FormSessionResponse nextSession = startSession(processKey, nextFormId);\n                                    return redirectTo(nextSession.getUri());\n                                }\n                            } else if (s == ProcessStatus.FAILED || s == ProcessStatus.CANCELLED || s == ProcessStatus.TIMED_OUT) {\n                                writeData(formDir, processFailed(form, m, processKey.getInstanceId()));\n                                break;\n                            } else if (s == ProcessStatus.FINISHED) {\n                                writeData(formDir, success(form, m, processKey.getInstanceId()));\n                                break;\n                            }\n\n                            try {\n                                // TODO exp back off?\n                                Thread.sleep(STATUS_REFRESH_DELAY);\n                            } catch (InterruptedException e) {\n                                Thread.currentThread().interrupt();\n                            }\n                        }\n                    }\n                } else {\n                    writeData(formDir, prepareData(form, m, r.getErrors(), processKey.getInstanceId()));\n                }\n            } catch (FormUtils.ValidationException e) {\n                ValidationError err = ValidationError.of(e.getField().name(), e.getMessage());\n                FormData d = prepareData(form, m, Collections.singletonList(err), processKey.getInstanceId());\n                writeData(formDir, d);\n            }\n        } catch (Exception e) {\n            throw new ConcordApplicationException(\"Error while submitting a form\", e);\n        }\n\n        return redirectToForm(uriInfo, headers, processKey, formName);\n    }\n\n    private Form assertForm(ProcessKey processKey, String formName) {\n        Form form = formService.get(processKey, formName);\n        if (form == null) {\n            log.warn(\"assertForm ['{}', '{}'] -> not found\", processKey, formName);\n            throw new ConcordApplicationException(\"Form not found\", Status.NOT_FOUND);\n        }\n        return form;\n    }\n\n    private FormData initialData(Form form, UUID processInstanceId) {\n        return prepareData(false, false, form, null, false, null, processInstanceId);\n    }\n\n    private FormData success(Form form, Map<String, Object> overrides, UUID processInstanceId) {\n        return prepareData(true, false, form, overrides, false, null, processInstanceId);\n    }\n\n    private FormData processFailed(Form form, Map<String, Object> overrides, UUID processInstanceId) {\n        return prepareData(false, true, form, overrides, false, null, processInstanceId);\n    }\n\n    private FormData prepareData(Form form, Map<String, Object> overrides, List<ValidationError> errors, UUID processInstanceId) {\n        return prepareData(false, false, form, overrides, true, errors, processInstanceId);\n    }\n\n\n    private FormData prepareData(boolean success, boolean processFailed, Form form,\n                                 Map<String, Object> overrides, boolean skipMissingOverrides,\n                                 List<ValidationError> errors,\n                                 UUID processInstanceId) {\n\n        // TODO merge with FormResource\n        Map<String, FormDataDefinition> _definitions = new HashMap<>();\n\n        // the order of precedence should be:\n        //   submitted value > form call value > field's default value > environment value\n        Map<String, Object> _values = new HashMap<>(form.options().extraValues());\n        Map<String, String> _errors = new HashMap<>();\n\n        form.fields().stream().filter(f -> f.defaultValue() != null)\n                .forEach(f -> _values.put(f.name(), f.defaultValue()));\n\n        List<String> fields = form.fields().stream()\n                .map(FormField::name)\n                .collect(Collectors.toList());\n\n        for (FormField f : form.fields()) {\n            Object allowedValue = f.allowedValue();\n\n            _definitions.put(f.name(), new FormDataDefinition(f.label(), f.type(), convert(f.cardinality()), allowedValue));\n\n            Object v = overrides != null ? overrides.get(f.name()) : null;\n            if (v == null && skipMissingOverrides) {\n                continue;\n            }\n\n            if (v == null) {\n                continue;\n            }\n\n            _values.put(f.name(), v);\n        }\n\n        if (errors != null) {\n            for (ValidationError e : errors) {\n                _errors.put(e.fieldName(), e.error());\n            }\n        }\n\n        String submitUrl = String.format(FORM_WIZARD_CONTINUE_URL_TEMPLATE, processInstanceId, form.name());\n\n        return FormData.builder()\n                .txId(processInstanceId.toString())\n                .formName(form.name())\n                .success(success)\n                .processFailed(processFailed)\n                .submitUrl(submitUrl)\n                .fields(fields)\n                .definitions(_definitions)\n                .values(_values)\n                .errors(_errors)\n                .build();\n    }\n\n    private io.takari.bpm.model.form.FormField.Cardinality convert(FormField.Cardinality c) {\n        if (c == null) {\n            return null;\n        }\n\n        switch (c) {\n            case ANY:\n                return io.takari.bpm.model.form.FormField.Cardinality.ANY;\n            case AT_LEAST_ONE:\n                return io.takari.bpm.model.form.FormField.Cardinality.AT_LEAST_ONE;\n            case ONE_AND_ONLY_ONE:\n                return io.takari.bpm.model.form.FormField.Cardinality.ONE_AND_ONLY_ONE;\n            case ONE_OR_NONE:\n                return io.takari.bpm.model.form.FormField.Cardinality.ONE_OR_NONE;\n            default:\n                throw new IllegalArgumentException(\"Unsupported cardinality type: \" + c);\n        }\n    }\n\n    private void writeData(Path baseDir, Object data) throws IOException {\n        Path dst = baseDir.resolve(\"data.js\");\n\n        Path parent = dst.getParent();\n        if (!Files.exists(parent)) {\n            Files.createDirectories(parent);\n        }\n\n        String s = String.format(DATA_FILE_TEMPLATE, objectMapper.writeValueAsString(data));\n        Files.write(dst, s.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);\n    }\n\n    private void copySharedResources(ProcessKey processKey, Path dst) throws IOException {\n        Path sharedDir = dst.resolve(SHARED_DIR_NAME);\n        if (!Files.exists(sharedDir)) {\n            Files.createDirectories(sharedDir);\n        }\n\n        String resource = FormServiceV1.FORMS_RESOURCES_PATH + \"/\" + SHARED_DIR_NAME;\n        stateManager.exportDirectory(processKey, resource, copyTo(sharedDir));\n    }\n\n    private static Response redirectToForm(UriInfo uriInfo, HttpHeaders headers,\n                                           PartialProcessKey processKey, String formName) {\n\n        String scheme = uriInfo.getBaseUri().getScheme();\n\n        // check if we need to force https\n        String origin = headers.getHeaderString(\"Origin\");\n        if (origin != null) {\n            URI originUri = URI.create(origin);\n            String originScheme = originUri.getScheme();\n            if (\"https\".equalsIgnoreCase(originScheme)) {\n                scheme = originScheme;\n            }\n        }\n\n        UriBuilder b = UriBuilder.fromUri(uriInfo.getBaseUri())\n                .scheme(scheme)\n                .path(formPath(processKey, formName));\n\n        return redirectTo(b.build().toString());\n    }\n\n    private static Response redirectTo(String path) {\n        return Response.status(Status.MOVED_PERMANENTLY)\n                .header(HttpHeaders.LOCATION, path)\n                .build();\n    }\n\n    private static String formPath(PartialProcessKey processKey, String formName) {\n        return String.format(FORMS_PATH_TEMPLATE, processKey, formName);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/console/FormSessionResponse.java",
    "content": "package com.walmartlabs.concord.server.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.io.Serializable;\n\npublic class FormSessionResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String uri;\n\n    @JsonCreator\n    public FormSessionResponse(@JsonProperty(\"uri\") String uri) {\n        this.uri = uri;\n    }\n\n    public String getUri() {\n        return uri;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardAccessEntry.java",
    "content": "package com.walmartlabs.concord.server.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\nimport java.util.List;\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableProcessCardAccessEntry.class)\n@JsonDeserialize(as = ImmutableProcessCardAccessEntry.class)\npublic interface ProcessCardAccessEntry extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Value.Default\n    default List<UUID> userIds() {\n        return List.of();\n    }\n\n    @Value.Default\n    default List<UUID> teamIds() {\n        return List.of();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardEntry.java",
    "content": "package com.walmartlabs.concord.server.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.server.org.ImmutableEntityOwner;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport javax.validation.constraints.Size;\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableProcessCardEntry.class)\n@JsonDeserialize(as = ImmutableProcessCardEntry.class)\npublic interface ProcessCardEntry extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    UUID id();\n\n    @ConcordKey\n    @Nullable\n    String orgName();\n\n    @ConcordKey\n    @Nullable\n    String projectName();\n\n    @ConcordKey\n    @Nullable\n    String repoName();\n\n    @Size(max = 256)\n    @Nullable\n    String entryPoint();\n\n    @Size(max = 128)\n    String name();\n\n    @Size(max = 512)\n    @Nullable\n    String description();\n\n    @Nullable\n    String icon();\n\n    boolean isCustomForm();\n\n    @Nullable\n    Integer orderId();\n\n    static ImmutableProcessCardEntry.Builder builder() {\n        return ImmutableProcessCardEntry.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardManager.java",
    "content": "package com.walmartlabs.concord.server.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.UuidGenerator;\nimport com.walmartlabs.concord.server.jooq.tables.UiProcessCards;\nimport com.walmartlabs.concord.server.jooq.tables.Users;\nimport com.walmartlabs.concord.server.jooq.tables.records.UiProcessCardsRecord;\nimport com.walmartlabs.concord.server.org.EntityOwner;\nimport com.walmartlabs.concord.server.org.ResourceAccessUtils;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.user.UserType;\nimport org.jooq.*;\n\nimport javax.inject.Inject;\nimport java.io.InputStream;\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\nimport java.util.*;\nimport java.util.function.Function;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.*;\nimport static com.walmartlabs.concord.server.jooq.tables.Organizations.ORGANIZATIONS;\nimport static com.walmartlabs.concord.server.jooq.tables.Projects.PROJECTS;\nimport static com.walmartlabs.concord.server.jooq.tables.Users.USERS;\nimport static java.util.Objects.requireNonNull;\nimport static org.jooq.impl.DSL.*;\n\npublic class ProcessCardManager {\n\n    private final Dao dao;\n\n    @Inject\n    public ProcessCardManager(Dao dao) {\n        this.dao = dao;\n    }\n\n    public ProcessCardEntry get(UUID cardId) {\n        return dao.get(cardId);\n    }\n\n    public void delete(UUID cardId) {\n        dao.delete(cardId);\n    }\n\n    public void assertAccess(UUID cardId) {\n        if (Roles.isAdmin()) {\n            return;\n        }\n\n        UserPrincipal principal = UserPrincipal.assertCurrent();\n\n        EntityOwner owner = dao.getOwner(cardId);\n        if (ResourceAccessUtils.isSame(principal, owner)) {\n            return;\n        }\n\n        throw new UnauthorizedException(\"The current user (\" + principal.getUsername() + \") doesn't have \" +\n                                        \"access to the process card: \" + cardId);\n    }\n\n    public void updateAccess(UUID cardId, List<UUID> teamIds, List<UUID> userIds) {\n        dao.tx(tx -> {\n            dao.rewriteTeamAccess(tx, cardId, teamIds);\n            dao.rewriteUserAccess(tx, cardId, userIds);\n        });\n    }\n\n    public List<ProcessCardEntry> listUserCards(UUID userId) {\n        return dao.listCards(userId);\n    }\n\n    public ProcessCardOperationResponse createOrUpdate(UUID id, UUID projectId, UUID repoId, String name, Optional<String> entryPoint, String description, InputStream icon, InputStream form, Map<String, Object> data, Integer orderId) {\n        boolean exists;\n        if (id == null) {\n            if (projectId == null) {\n                throw new ConcordApplicationException(\"projectId or projectName is required\");\n            }\n            if (name == null) {\n                throw new ConcordApplicationException(\"name is required\");\n            }\n            id = dao.getIdByName(projectId, name).orElse(null);\n            exists = id != null;\n        } else {\n            exists = dao.get(id) != null;\n        }\n\n        if (!exists) {\n            UUID resultId = dao.insert(id, projectId, repoId, name, entryPoint.orElse(Constants.Request.DEFAULT_ENTRY_POINT_NAME), description, icon, form, data, orderId);\n            return new ProcessCardOperationResponse(resultId, OperationResult.CREATED);\n        } else {\n            assertAccess(id);\n\n            dao.update(id, projectId, repoId, name, entryPoint.orElse(null), description, icon, form, data, orderId);\n            return new ProcessCardOperationResponse(id, OperationResult.UPDATED);\n        }\n    }\n\n    public <T> Optional<T> getForm(UUID cardId, Function<InputStream, Optional<T>> converter) {\n        return dao.getForm(cardId, converter);\n    }\n\n    public Map<String, Object> getFormData(UUID cardId) {\n        return dao.getFormData(cardId);\n    }\n\n    @SuppressWarnings(\"resource\")\n    public static class Dao extends AbstractDao {\n\n        private final ConcordObjectMapper objectMapper;\n        private final UuidGenerator uuidGenerator;\n\n        @Inject\n        protected Dao(@MainDB Configuration cfg,\n                      ConcordObjectMapper objectMapper,\n                      UuidGenerator uuidGenerator) {\n            super(cfg);\n            this.objectMapper = requireNonNull(objectMapper);\n            this.uuidGenerator = requireNonNull(uuidGenerator);\n        }\n\n        protected void tx(Tx t) {\n            super.tx(t);\n        }\n\n        public ProcessCardEntry get(UUID cardId) {\n            return txResult(tx -> get(tx, cardId));\n        }\n\n        private ProcessCardEntry get(DSLContext tx, UUID cardId) {\n            return buildSelect(tx)\n                    .where(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID.eq(cardId))\n                    .fetchOne(this::toEntry);\n        }\n\n        public Optional<UUID> getIdByName(UUID projectId, String name) {\n            return dsl().select(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID)\n                    .from(UI_PROCESS_CARDS)\n                    .where(UI_PROCESS_CARDS.PROJECT_ID.eq(projectId)\n                            .and(UI_PROCESS_CARDS.NAME.eq(name)))\n                    .fetchOptional(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID);\n        }\n\n        public UUID insert(UUID cardId, UUID projectId, UUID repoId, String name, String entryPoint, String description, InputStream icon, InputStream form, Map<String, Object> data, Integer orderId) {\n            return txResult(tx -> {\n                String sql = tx.insertInto(UI_PROCESS_CARDS)\n                        .columns(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID,\n                                UI_PROCESS_CARDS.PROJECT_ID,\n                                UI_PROCESS_CARDS.REPO_ID,\n                                UI_PROCESS_CARDS.NAME,\n                                UI_PROCESS_CARDS.ENTRY_POINT,\n                                UI_PROCESS_CARDS.DESCRIPTION,\n                                UI_PROCESS_CARDS.ICON,\n                                UI_PROCESS_CARDS.FORM,\n                                UI_PROCESS_CARDS.DATA,\n                                UI_PROCESS_CARDS.OWNER_ID,\n                                UI_PROCESS_CARDS.ORDER_ID)\n                        .values((UUID) null, null, null, null, null, null, null, null, null, null, null)\n                        .returning(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID)\n                        .getSQL();\n\n                return tx.connectionResult(conn -> {\n                    try (PreparedStatement ps = conn.prepareStatement(sql)) {\n                        ps.setObject(1, cardId == null ? uuidGenerator.generate() : cardId);\n                        ps.setObject(2, projectId);\n                        ps.setObject(3, repoId);\n                        ps.setString(4, name);\n                        ps.setString(5, entryPoint);\n                        ps.setString(6, description);\n                        ps.setBinaryStream(7, icon);\n                        ps.setBinaryStream(8, form);\n                        ps.setObject(9, data != null ? objectMapper.toJSONB(data).data() : null);\n                        ps.setObject(10, UserPrincipal.assertCurrent().getId());\n                        ps.setObject(11, orderId);\n\n                        try (ResultSet rs = ps.executeQuery()) {\n                            if (!rs.next()) {\n                                throw new RuntimeException(\"Can't insert process card\");\n                            }\n\n                            return UUID.fromString(rs.getString(1));\n                        }\n                    }\n                });\n            });\n        }\n\n        public void update(UUID cardId, UUID projectId, UUID repoId, String name,\n                           String entryPoint, String description, InputStream icon, InputStream form,\n                           Map<String, Object> data, Integer orderId) {\n            tx(tx -> {\n                List<Object> params = new ArrayList<>();\n                UpdateSetStep<UiProcessCardsRecord> q = tx.update(UI_PROCESS_CARDS);\n\n                if (projectId != null) {\n                    q.set(UI_PROCESS_CARDS.PROJECT_ID, (UUID) null);\n                    params.add(projectId);\n                }\n\n                if (repoId != null) {\n                    q.set(UI_PROCESS_CARDS.REPO_ID, (UUID) null);\n                    params.add(repoId);\n                }\n\n                if (name != null) {\n                    q.set(UI_PROCESS_CARDS.NAME, (String) null);\n                    params.add(name);\n                }\n\n                if (entryPoint != null) {\n                    q.set(UI_PROCESS_CARDS.ENTRY_POINT, (String) null);\n                    params.add(entryPoint);\n                }\n\n                if (description != null) {\n                    q.set(UI_PROCESS_CARDS.DESCRIPTION, (String) null);\n                    params.add(description);\n                }\n\n                if (icon != null) {\n                    q.set(UI_PROCESS_CARDS.ICON, (byte[]) null);\n                    params.add(icon);\n                }\n\n                if (form != null) {\n                    q.set(UI_PROCESS_CARDS.FORM, (byte[]) null);\n                    params.add(form);\n                }\n\n                if (data != null) {\n                    q.set(UI_PROCESS_CARDS.DATA, (JSONB) null);\n                    params.add(objectMapper.toJSONB(data).data());\n                }\n\n                if (params.isEmpty()) {\n                    return;\n                }\n\n                if (orderId != null) {\n                    q.set(UI_PROCESS_CARDS.ORDER_ID, (Integer) null);\n                    params.add(orderId);\n                }\n\n                String sql = q.set(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID, cardId)\n                        .where(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID.eq(cardId))\n                        .getSQL();\n                params.add(cardId);\n                params.add(cardId);\n\n                tx.connection(conn -> {\n                    try (PreparedStatement ps = conn.prepareStatement(sql)) {\n                        for (int i = 0; i < params.size(); i++) {\n                            Object p = params.get(i);\n                            if (p instanceof InputStream) {\n                                ps.setBinaryStream(i + 1, (InputStream) p);\n                            } else {\n                                ps.setObject(i + 1, p);\n                            }\n                        }\n\n                        ps.executeUpdate();\n                    }\n                });\n            });\n        }\n\n        public void delete(UUID id) {\n            tx(tx -> tx.delete(UI_PROCESS_CARDS)\n                    .where(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID.eq(id))\n                    .execute());\n        }\n\n        public void rewriteTeamAccess(DSLContext tx, UUID cardId, List<UUID> teamIds) {\n            tx.delete(TEAM_UI_PROCESS_CARDS)\n                    .where(TEAM_UI_PROCESS_CARDS.UI_PROCESS_CARD_ID.eq(cardId))\n                    .execute();\n\n            for (UUID teamId : teamIds) {\n                tx.insertInto(TEAM_UI_PROCESS_CARDS)\n                        .columns(TEAM_UI_PROCESS_CARDS.TEAM_ID, TEAM_UI_PROCESS_CARDS.UI_PROCESS_CARD_ID)\n                        .values(teamId, cardId)\n                        .execute();\n            }\n        }\n\n        public void rewriteUserAccess(DSLContext tx, UUID cardId, List<UUID> userIds) {\n            tx.delete(USER_UI_PROCESS_CARDS)\n                    .where(USER_UI_PROCESS_CARDS.UI_PROCESS_CARD_ID.eq(cardId))\n                    .execute();\n\n            for (UUID userId : userIds) {\n                tx.insertInto(USER_UI_PROCESS_CARDS)\n                        .columns(USER_UI_PROCESS_CARDS.USER_ID, USER_UI_PROCESS_CARDS.UI_PROCESS_CARD_ID)\n                        .values(userId, cardId)\n                        .execute();\n            }\n        }\n\n        public EntityOwner getOwner(UUID cardId) {\n            Users u = USERS.as(\"u\");\n            UiProcessCards c = UI_PROCESS_CARDS.as(\"c\");\n\n            return txResult(tx -> tx.select(\n                    c.OWNER_ID,\n                    u.USER_ID,\n                    u.USERNAME,\n                    u.DOMAIN,\n                    u.DISPLAY_NAME,\n                    u.USER_TYPE)\n                    .from(c)\n                    .leftJoin(u).on(u.USER_ID.eq(c.OWNER_ID))\n                    .where(c.UI_PROCESS_CARD_ID.eq(cardId))\n                    .fetchOne(r -> toOwner(r.get(c.OWNER_ID), r.get(u.USERNAME), r.get(u.DOMAIN), r.get(u.DISPLAY_NAME), r.get(u.USER_TYPE))));\n        }\n\n        public List<ProcessCardEntry> listCards(UUID userId) {\n            return txResult(tx -> listCards(tx, userId));\n        }\n\n        private List<ProcessCardEntry> listCards(DSLContext tx, UUID userId) {\n            var userTeams = tx.select(V_USER_TEAMS.TEAM_ID)\n                    .from(V_USER_TEAMS)\n                    .where(V_USER_TEAMS.USER_ID.eq(userId));\n\n            var byUserFilter = tx.select(USER_UI_PROCESS_CARDS.UI_PROCESS_CARD_ID)\n                    .from(USER_UI_PROCESS_CARDS)\n                    .where(USER_UI_PROCESS_CARDS.USER_ID.eq(userId));\n\n            var byTeamFilter = tx.select(TEAM_UI_PROCESS_CARDS.UI_PROCESS_CARD_ID)\n                    .from(TEAM_UI_PROCESS_CARDS)\n                    .where(TEAM_UI_PROCESS_CARDS.TEAM_ID.in(userTeams));\n\n            var byOwnerFilter = tx.select(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID)\n                    .from(UI_PROCESS_CARDS)\n                    .where(UI_PROCESS_CARDS.OWNER_ID.isNotNull().and(UI_PROCESS_CARDS.OWNER_ID.eq(userId)));\n\n            var userCards = byUserFilter\n                    .unionAll(byTeamFilter)\n                    .unionAll(byOwnerFilter);\n\n            var userCardsFilter = tx.select(userCards.field(TEAM_UI_PROCESS_CARDS.UI_PROCESS_CARD_ID))\n                    .from(userCards);\n\n            var query = buildSelect(tx)\n                    .where(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID.in(userCardsFilter));\n\n            return query\n                    .orderBy(UI_PROCESS_CARDS.ORDER_ID, UI_PROCESS_CARDS.UI_PROCESS_CARD_ID)\n                    .fetch(this::toEntry);\n        }\n\n        public <T> Optional<T> getForm(UUID cardId, Function<InputStream, Optional<T>> converter) {\n            return txResult(tx -> getForm(tx, cardId, converter));\n        }\n\n        public <T> Optional<T> getForm(DSLContext tx, UUID cardId, Function<InputStream, Optional<T>> converter) {\n            String sql = tx.select(UI_PROCESS_CARDS.FORM)\n                    .from(UI_PROCESS_CARDS)\n                    .where(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID.eq((UUID) null)\n                            .and(UI_PROCESS_CARDS.FORM.isNotNull()))\n                    .getSQL();\n\n            return getInputStream(tx, sql, cardId, converter);\n        }\n\n        public Map<String, Object> getFormData(UUID cardId) {\n            return txResult(tx -> getFormData(tx, cardId));\n        }\n\n        public Map<String, Object> getFormData(DSLContext tx, UUID cardId) {\n            return tx.select(UI_PROCESS_CARDS.DATA)\n                    .from(UI_PROCESS_CARDS)\n                    .where(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID.eq(cardId))\n                    .fetchOne(r -> objectMapper.fromJSONB(r.get(UI_PROCESS_CARDS.DATA)));\n        }\n\n        private static <T> Optional<T> getInputStream(DSLContext tx, String sql, UUID cardId, Function<InputStream, Optional<T>> converter) {\n            return tx.connectionResult(conn -> {\n                try (PreparedStatement ps = conn.prepareStatement(sql)) {\n                    ps.setObject(1, cardId);\n\n                    try (ResultSet rs = ps.executeQuery()) {\n                        if (!rs.next()) {\n                            return Optional.empty();\n                        }\n                        try (InputStream in = rs.getBinaryStream(1)) {\n                            return converter.apply(in);\n                        }\n                    }\n                }\n            });\n        }\n\n        private static SelectOnConditionStep<Record13<UUID, UUID, String, UUID, String, UUID, String, String, String, String, byte[], Boolean, Integer>> buildSelect(DSLContext tx) {\n            Field<Boolean> isCustomForm = when(field(UI_PROCESS_CARDS.FORM).isNotNull(), true).otherwise(false);\n\n            return tx.select(\n                            UI_PROCESS_CARDS.UI_PROCESS_CARD_ID,\n                            PROJECTS.ORG_ID,\n                            ORGANIZATIONS.ORG_NAME,\n                            UI_PROCESS_CARDS.PROJECT_ID,\n                            PROJECTS.PROJECT_NAME,\n                            UI_PROCESS_CARDS.REPO_ID,\n                            REPOSITORIES.REPO_NAME,\n                            UI_PROCESS_CARDS.NAME,\n                            UI_PROCESS_CARDS.ENTRY_POINT,\n                            UI_PROCESS_CARDS.DESCRIPTION,\n                            UI_PROCESS_CARDS.ICON,\n                            isCustomForm.as(\"isCustomForm\"),\n                            UI_PROCESS_CARDS.ORDER_ID)\n                    .from(UI_PROCESS_CARDS)\n                    .leftJoin(REPOSITORIES).on(REPOSITORIES.REPO_ID.eq(UI_PROCESS_CARDS.REPO_ID))\n                    .leftJoin(PROJECTS).on(PROJECTS.PROJECT_ID.eq(UI_PROCESS_CARDS.PROJECT_ID))\n                    .leftJoin(ORGANIZATIONS).on(ORGANIZATIONS.ORG_ID.eq(PROJECTS.ORG_ID));\n        }\n\n        private ProcessCardEntry toEntry(Record13<UUID, UUID, String, UUID, String, UUID, String, String, String, String, byte[], Boolean, Integer> r) {\n            return ProcessCardEntry.builder()\n                    .id(r.get(UI_PROCESS_CARDS.UI_PROCESS_CARD_ID))\n                    .orgName(r.get(ORGANIZATIONS.ORG_NAME))\n                    .projectName(r.get(PROJECTS.PROJECT_NAME))\n                    .repoName(r.get(REPOSITORIES.REPO_NAME))\n                    .entryPoint(r.get(UI_PROCESS_CARDS.ENTRY_POINT))\n                    .name(r.get(UI_PROCESS_CARDS.NAME))\n                    .description(r.get(UI_PROCESS_CARDS.DESCRIPTION))\n                    .icon(encodeBase64(r.get(UI_PROCESS_CARDS.ICON)))\n                    .isCustomForm(r.get(\"isCustomForm\", Boolean.class))\n                    .orderId(r.get(UI_PROCESS_CARDS.ORDER_ID))\n                    .build();\n        }\n\n        static String encodeBase64(byte[] value) {\n            if (value == null) {\n                return null;\n            }\n\n            return Base64.getEncoder().encodeToString(value);\n        }\n\n        private static EntityOwner toOwner(UUID id, String username, String domain, String displayName, String userType) {\n            if (id == null) {\n                return null;\n            }\n            return EntityOwner.builder()\n                    .id(id)\n                    .username(username)\n                    .userDomain(domain)\n                    .displayName(displayName)\n                    .userType(UserType.valueOf(userType))\n                    .build();\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardOperationResponse.java",
    "content": "package com.walmartlabs.concord.server.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.server.OperationResult;\n\nimport java.io.Serializable;\nimport java.util.UUID;\n\npublic class ProcessCardOperationResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n    private final UUID id;\n    private final OperationResult result;\n\n    @JsonCreator\n    public ProcessCardOperationResponse(@JsonProperty(\"id\") UUID id,\n                                        @JsonProperty(\"result\") OperationResult result) {\n        this.id = id;\n        this.result = result;\n    }\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public OperationResult getResult() {\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"ProcessCardOperationResponse{\" +\n                \"ok=\" + ok +\n                \", id=\" + id +\n                \", result=\" + result +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardRequest.java",
    "content": "package com.walmartlabs.concord.server.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.MultipartUtils;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport org.jboss.resteasy.plugins.providers.multipart.MultipartInput;\n\nimport java.io.InputStream;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic class ProcessCardRequest {\n\n    public static ProcessCardRequest from(MultipartInput input) {\n        return new ProcessCardRequest(input);\n    }\n\n    private final MultipartInput input;\n\n    private ProcessCardRequest(MultipartInput input) {\n        this.input = input;\n    }\n\n    @Schema(name = Constants.Multipart.ORG_ID)\n    public UUID getOrgId() {\n        return MultipartUtils.getUuid(input, Constants.Multipart.ORG_ID);\n    }\n\n    @Schema(name = Constants.Multipart.ORG_NAME)\n    public String getOrgName() {\n        return MultipartUtils.getString(input, Constants.Multipart.ORG_NAME);\n    }\n\n    @Schema(name = Constants.Multipart.PROJECT_ID)\n    public UUID getProjectId() {\n        return MultipartUtils.getUuid(input, Constants.Multipart.PROJECT_ID);\n    }\n\n    @Schema(name = Constants.Multipart.PROJECT_NAME)\n    public String getProjectName() {\n        return MultipartUtils.getString(input, Constants.Multipart.PROJECT_NAME);\n    }\n\n    @Schema(name = Constants.Multipart.REPO_ID)\n    public UUID getRepoId() {\n        return MultipartUtils.getUuid(input, Constants.Multipart.REPO_ID);\n    }\n\n    @Schema(name = Constants.Multipart.REPO_NAME)\n    public String getRepoName() {\n        return MultipartUtils.getString(input, Constants.Multipart.REPO_NAME);\n    }\n\n    @Schema(name = Constants.Multipart.ENTRY_POINT)\n    public String getEntryPoint() {\n        return MultipartUtils.getString(input, Constants.Multipart.ENTRY_POINT);\n    }\n\n    @Schema(name = Constants.Multipart.ORDER_ID)\n    public Integer getOrderId() {\n        return MultipartUtils.getInt(input, Constants.Multipart.ORDER_ID);\n    }\n\n    @Schema(name = \"name\")\n    public String getName() {\n        return MultipartUtils.getString(input, \"name\");\n    }\n\n    @Schema(name = \"description\")\n    public String getDescription() {\n        return MultipartUtils.getString(input, \"description\");\n    }\n\n    @Schema(name = \"data\")\n    public Map<String, Object> getData() {\n        return MultipartUtils.getMap(input, \"data\");\n    }\n\n    @Schema(name = \"id\")\n    public UUID getId() {\n        return MultipartUtils.getUuid(input, \"id\");\n    }\n\n    @Schema(name = \"icon\")\n    public InputStream getIcon() {\n        return MultipartUtils.getStream(input, \"icon\");\n    }\n\n    @Schema(name = \"form\")\n    public InputStream getForm() {\n        return MultipartUtils.getStream(input, \"form\");\n    }\n\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/console/ProcessCardResource.java",
    "content": "package com.walmartlabs.concord.server.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.GenericOperationResult;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.org.project.RepositoryDao;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.Parameter;\nimport io.swagger.v3.oas.annotations.media.Content;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport io.swagger.v3.oas.annotations.responses.ApiResponse;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.jboss.resteasy.plugins.providers.multipart.MultipartInput;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport javax.ws.rs.core.StreamingOutput;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.StandardCopyOption;\nimport java.util.*;\n\n@Path(\"/api/v1/\")\n@Tag(name = \"ProcessCards\")\npublic class ProcessCardResource implements Resource {\n\n    private static final String DATA_FILE_TEMPLATE = \"data = %s;\";\n\n    private final ProcessCardManager processCardManager;\n    private final OrganizationManager organizationManager;\n    private final ProjectDao projectDao;\n    private final RepositoryDao repositoryDao;\n    private final ConcordObjectMapper objectMapper;\n\n    @Inject\n    public ProcessCardResource(OrganizationManager organizationManager,\n                               ProcessCardManager processCardManager,\n                               ProjectDao projectDao,\n                               RepositoryDao repositoryDao, ConcordObjectMapper objectMapper) {\n        this.organizationManager = organizationManager;\n        this.processCardManager = processCardManager;\n\n        this.projectDao = projectDao;\n        this.repositoryDao = repositoryDao;\n        this.objectMapper = objectMapper;\n    }\n\n    @GET\n    @Path(\"/processcard\")\n    @Consumes(MediaType.MULTIPART_FORM_DATA)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"List user process cards\", operationId = \"listUserProcessCards\")\n    public List<ProcessCardEntry> list() {\n\n        UserPrincipal user = UserPrincipal.assertCurrent();\n\n        return processCardManager.listUserCards(user.getId());\n    }\n\n    @GET\n    @Path(\"/processcard/{id}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Get process card\", operationId = \"getProcessCard\")\n    public ProcessCardEntry get(@PathParam(\"id\") UUID cardId) throws IOException {\n        return assertCard(cardId);\n    }\n\n    @DELETE\n    @Path(\"/processcard/{id}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Delete process card\", operationId = \"deleteProcessCard\")\n    public GenericOperationResult delete(@PathParam(\"id\") UUID cardId) throws IOException {\n\n        assertCard(cardId);\n        processCardManager.assertAccess(cardId);\n\n        processCardManager.delete(cardId);\n\n        return new GenericOperationResult(OperationResult.DELETED);\n    }\n\n    @POST\n    @Path(\"/processcard/{id}/access\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Process cards access\", operationId = \"processCardAccess\")\n    public GenericOperationResult access(\n            @PathParam(\"id\") UUID cardId,\n            ProcessCardAccessEntry entry) throws IOException {\n\n        assertCard(cardId);\n        processCardManager.assertAccess(cardId);\n\n        processCardManager.updateAccess(cardId, entry.teamIds(), entry.userIds());\n\n        return new GenericOperationResult(OperationResult.UPDATED);\n    }\n\n    @POST\n    @Path(\"/processcard\")\n    @Consumes(MediaType.MULTIPART_FORM_DATA)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Create or update process card\", operationId = \"createOrUpdateProcessCard\")\n    public ProcessCardOperationResponse createOrUpdate(\n            @Parameter(schema = @Schema(type = \"object\", implementation = ProcessCardRequest.class)) MultipartInput input) throws IOException {\n\n        try {\n            ProcessCardRequest r = ProcessCardRequest.from(input);\n\n            UUID orgId = organizationManager.assertAccess(r.getOrgId(), r.getOrgName(), false).getId();\n            UUID projectId = assertProject(orgId, r);\n            UUID repoId = getRepo(projectId, r);\n            String name = r.getName();\n            Optional<String> entryPoint = Optional.ofNullable(r.getEntryPoint());\n            String description = r.getDescription();\n            Map<String, Object> data = r.getData();\n            UUID id = r.getId();\n            Integer orderId = r.getOrderId();\n\n            try (InputStream icon = r.getIcon();\n                 InputStream form = r.getForm()) {\n                return processCardManager.createOrUpdate(id, projectId, repoId, name, entryPoint, description, icon, form, data, orderId);\n            }\n        } finally {\n            input.close();\n        }\n    }\n\n    @GET\n    @Path(\"/processcard/{cardId}/form\")\n    @Produces(MediaType.TEXT_HTML)\n    @WithTimer\n    @Operation(description = \"Get process card form\", operationId = \"getProcessCardForm\")\n    @ApiResponse(responseCode = \"200\", description = \"Process form content\",\n            content = @Content(mediaType = \"text/html\", schema = @Schema(type = \"string\", format = \"binary\")))\n    public Response getForm(@PathParam(\"cardId\") UUID cardId) {\n\n        assertCard(cardId);\n\n        Optional<java.nio.file.Path> o = processCardManager.getForm(cardId, src -> {\n            try {\n                java.nio.file.Path tmp = PathUtils.createTempFile(\"process-form\", \".html\");\n                Files.copy(src, tmp, StandardCopyOption.REPLACE_EXISTING);\n                return Optional.of(tmp);\n            } catch (IOException e) {\n                throw new ConcordApplicationException(\"Error while downloading custom process start form: \" + cardId, e);\n            }\n        });\n\n        if (o.isEmpty()) {\n            return Response.status(Response.Status.NOT_FOUND).build();\n        }\n\n        return toBinaryResponse(o.get());\n    }\n\n    @GET\n    @Path(\"/processcard/{cardId}/data.js\")\n    @Produces(\"text/javascript\")\n    @WithTimer\n    @Operation(description = \"Get process card form data\", operationId = \"getProcessCardFormData\")\n    @ApiResponse(responseCode = \"200\", description = \"Process form data content\",\n            content = @Content(mediaType = \"text/javascript\", schema = @Schema(type = \"string\", format = \"binary\")))\n    public Response getFormData(@PathParam(\"cardId\") UUID cardId) {\n        ProcessCardEntry card = assertCard(cardId);\n\n        Map<String, Object> customData = processCardManager.getFormData(cardId);\n\n        Map<String, Object> resultData = new HashMap<>(customData != null ? customData : Collections.emptyMap());\n        resultData.put(\"org\", card.orgName());\n        resultData.put(\"project\", card.projectName());\n        resultData.put(\"repo\", card.repoName());\n        resultData.put(\"entryPoint\", card.entryPoint());\n\n        return Response.ok(formatData(resultData))\n                .build();\n    }\n\n    private ProcessCardEntry assertCard(UUID id) {\n        ProcessCardEntry e = processCardManager.get(id);\n        if (e == null) {\n            throw new ConcordApplicationException(\"Process card not found: \" + id, Response.Status.NOT_FOUND);\n        }\n        return e;\n    }\n\n    private UUID assertProject(UUID orgId, ProcessCardRequest request) {\n        UUID id = request.getProjectId();\n        String name = request.getProjectName();\n        if (id == null && name != null) {\n            if (orgId == null) {\n                throw new ValidationErrorsException(\"Organization ID or name is required\");\n            }\n\n            id = projectDao.getId(orgId, name);\n            if (id == null) {\n                throw new ValidationErrorsException(\"Project not found: \" + name);\n            }\n        }\n        return id;\n    }\n\n    private UUID getRepo(UUID projectId, ProcessCardRequest request) {\n        UUID id = request.getRepoId();\n        String name = request.getRepoName();\n        if (id == null && name != null) {\n            if (projectId == null) {\n                throw new ValidationErrorsException(\"Project ID or name is required\");\n            }\n\n            id = repositoryDao.getId(projectId, name);\n            if (id == null) {\n                throw new ValidationErrorsException(\"Repository not found: \" + name);\n            }\n        }\n        return id;\n    }\n\n    private String formatData(Map<String, Object> data) {\n        return String.format(DATA_FILE_TEMPLATE, objectMapper.toString(data));\n    }\n\n    private static Response toBinaryResponse(java.nio.file.Path file) {\n        return Response.ok((StreamingOutput) out -> {\n            try (InputStream in = Files.newInputStream(file)) {\n                in.transferTo(out);\n            } finally {\n                Files.delete(file);\n            }\n        }).build();\n    }\n\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/console/RepositoryTestRequest.java",
    "content": "package com.walmartlabs.concord.server.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\n\nimport javax.validation.constraints.NotNull;\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@JsonIgnoreProperties(ignoreUnknown = true)\n@JsonInclude(Include.NON_NULL)\npublic class RepositoryTestRequest implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @ConcordKey\n    private final String orgName;\n\n    @ConcordKey\n    private final String projectName;\n\n    @NotNull\n    private final String url;\n\n    private final String branch;\n    private final String commitId;\n    private final String path;\n    private final UUID secretId;\n\n    @JsonCreator\n    public RepositoryTestRequest(@JsonProperty(\"orgName\") String orgName,\n                                 @JsonProperty(\"projectName\") String projectName,\n                                 @JsonProperty(\"url\") String url,\n                                 @JsonProperty(\"branch\") String branch,\n                                 @JsonProperty(\"commitId\") String commitId,\n                                 @JsonProperty(\"path\") String path,\n                                 @JsonProperty(\"secretId\") UUID secretId) {\n\n        this.orgName = orgName;\n        this.projectName = projectName;\n        this.url = url;\n        this.branch = branch;\n        this.commitId = commitId;\n        this.path = path;\n        this.secretId = secretId;\n    }\n\n    public String getOrgName() {\n        return orgName;\n    }\n\n    public String getProjectName() {\n        return projectName;\n    }\n\n    public String getUrl() {\n        return url;\n    }\n\n    public String getBranch() {\n        return branch;\n    }\n\n    public String getCommitId() {\n        return commitId;\n    }\n\n    public String getPath() {\n        return path;\n    }\n\n    public UUID getSecretId() {\n        return secretId;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/console/ResponseTemplates.java",
    "content": "package com.walmartlabs.concord.server.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.github.mustachejava.DefaultMustacheFactory;\nimport com.github.mustachejava.Mustache;\nimport com.github.mustachejava.MustacheFactory;\n\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response.ResponseBuilder;\nimport javax.ws.rs.core.StreamingOutput;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.io.OutputStreamWriter;\nimport java.util.Map;\n\npublic class ResponseTemplates {\n\n    private final Mustache processFinished;\n    private final Mustache badRequest;\n    private final Mustache processError;\n    private final Mustache inProgressWait;\n    private final Mustache formNotFound;\n    private final Mustache genericError;\n\n    public ResponseTemplates() {\n        MustacheFactory mf = new DefaultMustacheFactory();\n        processFinished = mf.compile(\"com/walmartlabs/concord/server/console/processFinished.html\");\n        badRequest = mf.compile(\"com/walmartlabs/concord/server/console/badRequest.html\");\n        processError = mf.compile(\"com/walmartlabs/concord/server/console/processError.html\");\n        inProgressWait = mf.compile(\"com/walmartlabs/concord/server/console/inProgress.html\");\n        formNotFound = mf.compile(\"com/walmartlabs/concord/server/console/formNotFound.html\");\n        genericError = mf.compile(\"com/walmartlabs/concord/server/console/genericError.html\");\n    }\n\n    private ResponseBuilder html(Mustache m, ResponseBuilder r, Map<String, Object> args) {\n        return r.type(MediaType.TEXT_HTML_TYPE)\n                .entity((StreamingOutput) output -> {\n                    try (OutputStreamWriter w = new OutputStreamWriter(output)) {\n                        m.execute(w, args);\n                    }\n                });\n    }\n\n    public ResponseBuilder processFinished(ResponseBuilder r, Map<String, Object> args) {\n        return html(processFinished, r, args);\n    }\n\n    public ResponseBuilder badRequest(ResponseBuilder r, Map<String, Object> args) {\n        return html(badRequest, r, args);\n    }\n\n    public ResponseBuilder processError(ResponseBuilder r, Map<String, Object> args) {\n        return html(processError, r, args);\n    }\n\n    public ResponseBuilder inProgressWait(ResponseBuilder r, Map<String, Object> args) {\n        return html(inProgressWait, r, args);\n    }\n\n    public void formNotFound(OutputStream out, Map<String, Object> args) throws IOException {\n        try (OutputStreamWriter w = new OutputStreamWriter(out)) {\n            formNotFound.execute(w, args);\n        }\n    }\n\n    public void genericError(OutputStream out, Map<String, Object> args) throws IOException {\n        try (OutputStreamWriter w = new OutputStreamWriter(out)) {\n            genericError.execute(w, args);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/console/UserActivityResourceV2.java",
    "content": "package com.walmartlabs.concord.server.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.ProcessEntry;\nimport com.walmartlabs.concord.server.process.queue.ProcessFilter;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueDao;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport java.util.List;\n\n@Path(\"/api/v2/service/console/user\")\npublic class UserActivityResourceV2 implements Resource {\n\n    private final ProcessQueueDao processDao;\n\n    @Inject\n    public UserActivityResourceV2(ProcessQueueDao processDao) {\n        this.processDao = processDao;\n    }\n\n    @GET\n    @Path(\"/activity\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    public UserActivityResponse activity(@QueryParam(\"maxOwnProcesses\") @DefaultValue(\"5\") int maxOwnProcesses) {\n\n        UserPrincipal user = UserPrincipal.assertCurrent();\n\n        ProcessFilter filter = ProcessFilter.builder()\n                .initiator(user.getUsername())\n                .includeWithoutProject(true)\n                .limit(maxOwnProcesses)\n                .build();\n        List<ProcessEntry> lastProcesses = processDao.list(filter);\n\n        return ImmutableUserActivityResponse.builder()\n                .processes(lastProcesses)\n                .build();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/console/UserActivityResponse.java",
    "content": "package com.walmartlabs.concord.server.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.server.process.ProcessEntry;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n@Value.Immutable\n@JsonInclude(Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableUserActivityResponse.class)\n@JsonDeserialize(as = ImmutableUserActivityResponse.class)\npublic interface UserActivityResponse extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Value.Default\n    default List<ProcessEntry> processes() {\n        return List.of();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/console/UserInfoResponse.java",
    "content": "package com.walmartlabs.concord.server.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.server.org.team.TeamRole;\nimport org.immutables.value.Value;\n\nimport java.io.Serial;\nimport java.io.Serializable;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonInclude(Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableUserInfoResponse.class)\n@JsonDeserialize(as = ImmutableUserInfoResponse.class)\npublic interface UserInfoResponse extends Serializable {\n\n    @Serial\n    long serialVersionUID = 1L;\n\n    @Value.Immutable\n    @JsonInclude(Include.NON_EMPTY)\n    @JsonSerialize(as = ImmutableUserTeamInfo.class)\n    @JsonDeserialize(as = ImmutableUserTeamInfo.class)\n    interface UserTeamInfo {\n\n        @Value.Parameter\n        String orgName();\n\n        @Value.Parameter\n        String teamName();\n\n        @Value.Parameter\n        TeamRole role();\n\n        static UserTeamInfo of(String orgName, String teamName, TeamRole role) {\n            return ImmutableUserTeamInfo.of(orgName, teamName, role);\n        }\n    }\n\n    UUID id();\n\n    String displayName();\n\n    List<UserTeamInfo> teams();\n\n    List<String> roles();\n\n    Set<String> ldapGroups();\n\n    static ImmutableUserInfoResponse.Builder builder() {\n        return ImmutableUserInfoResponse.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/console/UserResponse.java",
    "content": "package com.walmartlabs.concord.server.console;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.server.org.OrganizationEntry;\n\nimport java.io.Serializable;\nimport java.util.Set;\n\n@JsonInclude(Include.NON_EMPTY)\npublic class UserResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String realm;\n    private final String username;\n    private final String userDomain;\n    private final String displayName;\n    private final Set<OrganizationEntry> orgs;\n\n    @JsonCreator\n    public UserResponse(@JsonProperty(\"realm\") String realm,\n                        @JsonProperty(\"username\") String username,\n                        @JsonProperty(\"userDomain\") String userDomain,\n                        @JsonProperty(\"displayName\") String displayName,\n                        @JsonProperty(\"orgs\") Set<OrganizationEntry> orgs) {\n\n        this.realm = realm;\n        this.username = username;\n        this.userDomain = userDomain;\n        this.displayName = displayName;\n        this.orgs = orgs;\n    }\n\n    public String getRealm() {\n        return realm;\n    }\n\n    public String getUsername() {\n        return username;\n    }\n\n    public String getUserDomain() {\n        return userDomain;\n    }\n\n    public String getDisplayName() {\n        return displayName;\n    }\n\n    public Set<OrganizationEntry> getOrgs() {\n        return orgs;\n    }\n\n    @Override\n    public String toString() {\n        return \"UserResponse{\" +\n                \"realm='\" + realm + '\\'' +\n                \", username='\" + username + '\\'' +\n                \", userDomain='\" + userDomain + '\\'' +\n                \", displayName='\" + displayName + '\\'' +\n                \", orgs=\" + orgs +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/events/DefaultEventFilter.java",
    "content": "package com.walmartlabs.concord.server.events;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.Matcher;\nimport com.walmartlabs.concord.server.org.triggers.TriggerEntry;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Map;\n\npublic final class DefaultEventFilter {\n\n    private static final Logger log = LoggerFactory.getLogger(DefaultEventFilter.class);\n\n    public static boolean filter(Map<String, Object> conditions, TriggerEntry t) {\n        if (t.getConditions() == null || t.getConditions().isEmpty()) {\n            return true;\n        }\n\n        try {\n            return Matcher.matches(conditions, t.getConditions());\n        } catch (Exception e) {\n            log.warn(\"filter [{}, {}] -> error while matching events: {}\", conditions, t, e.getMessage());\n            return false;\n        }\n    }\n\n    private DefaultEventFilter() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/events/Event.java",
    "content": "package com.walmartlabs.concord.server.events;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AllowNulls;\nimport com.walmartlabs.concord.server.user.UserEntry;\nimport org.immutables.value.Value;\n\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.function.Supplier;\n\n@Value.Immutable\npublic interface Event {\n\n    String id();\n\n    String name();\n\n    @Value.Default\n    default Supplier<UserEntry> initiator() {\n        return () -> null;\n    }\n\n    @AllowNulls\n    @Value.Default\n    default Map<String, Object> attributes() {\n        return Collections.emptyMap();\n    }\n\n    static ImmutableEvent.Builder builder() {\n        return ImmutableEvent.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/events/EventInitiatorSupplier.java",
    "content": "package com.walmartlabs.concord.server.events;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.user.UserEntry;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport com.walmartlabs.concord.server.user.UserType;\n\nimport java.util.Map;\nimport java.util.function.Supplier;\n\npublic class EventInitiatorSupplier implements Supplier<UserEntry> {\n\n    private final String initiatorAttr;\n    private final UserManager userManager;\n    private final Map<String, Object> eventAttributes;\n\n    public EventInitiatorSupplier(String initiatorAttr, UserManager userManager, Map<String, Object> eventAttributes) {\n        this.initiatorAttr = initiatorAttr;\n        this.userManager = userManager;\n        this.eventAttributes = eventAttributes;\n    }\n\n    @Override\n    public UserEntry get() {\n        Object author = eventAttributes.get(initiatorAttr);\n        if (author == null) {\n            return null;\n        }\n\n        return userManager.getOrCreate(author.toString(), null, UserType.LDAP)\n                .orElseThrow(() -> new ConcordApplicationException(\"User not found: \" + author));\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/events/EventModule.java",
    "content": "package com.walmartlabs.concord.server.events;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.walmartlabs.concord.server.events.externalevent.ExternalEventTriggerProcessor;\nimport com.walmartlabs.concord.server.events.externalevent.ExternalEventTriggerV1Processor;\nimport com.walmartlabs.concord.server.events.externalevent.ExternalEventTriggerV2Processor;\nimport com.walmartlabs.concord.server.events.github.GithubTriggerProcessor;\nimport com.walmartlabs.concord.server.sdk.events.ProcessEventListener;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\nimport static com.walmartlabs.concord.server.Utils.bindJaxRsResource;\n\npublic class EventModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        newSetBinder(binder, ExternalEventTriggerProcessor.class).addBinding().to(ExternalEventTriggerV1Processor.class);\n        newSetBinder(binder, ExternalEventTriggerProcessor.class).addBinding().to(ExternalEventTriggerV2Processor.class);\n\n        binder.bind(TriggerProcessExecutor.class).in(SINGLETON);\n\n        newSetBinder(binder, ProcessEventListener.class);\n\n        binder.bind(GithubTriggerProcessor.class).in(SINGLETON);\n        newSetBinder(binder, GithubTriggerProcessor.EventEnricher.class).addBinding().to(GithubTriggerProcessor.RepositoryInfoEnricher.class);\n\n        bindJaxRsResource(binder, ExternalEventResource.class);\n        bindJaxRsResource(binder, GithubEventResource.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/events/ExpressionUtils.java",
    "content": "package com.walmartlabs.concord.server.events;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.*;\n\npublic final class ExpressionUtils {\n\n    public static Map<String, Object> escapeMap(Map<String, Object> map) {\n        if (map == null || map.isEmpty()) {\n            return map;\n        }\n\n        Map<String, Object> result = new LinkedHashMap<>(map.size());\n        for (Map.Entry<String, Object> e : map.entrySet()) {\n            result.put(e.getKey(), escape(e.getValue()));\n        }\n        return result;\n    }\n\n    public static String escapeString(String value) {\n        if (value == null) {\n            return null;\n        }\n\n        if (hasExpression(value)) {\n            return value.replace(\"${\", \"\\\\${\");\n        }\n        return value;\n    }\n\n    public static List<Object> escapeList(List<Object> value) {\n        if (value == null || value.isEmpty()) {\n            return value;\n        }\n\n        List<Object> dst = new ArrayList<>(value.size());\n        for (Object vv : value) {\n            dst.add(escape(vv));\n        }\n\n        return dst;\n    }\n\n    public static Set<Object> escapeSet(Set<Object> value) {\n        if (value == null || value.isEmpty()) {\n            return value;\n        }\n\n        Set<Object> dst = new HashSet<>(value.size());\n        for (Object vv : value) {\n            dst.add(escape(vv));\n        }\n\n        return dst;\n    }\n\n    public static Object[] escapeArray(Object[] value) {\n        if (value == null || value.length == 0) {\n            return value;\n        }\n\n        for (int i = 0; i < value.length; i++) {\n            value[i] = escape(value[i]);\n        }\n        return value;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Object escape(Object value) {\n        if (value instanceof String) {\n            return escapeString((String) value);\n        } else if (value instanceof Map) {\n            return escapeMap((Map<String, Object>) value);\n        } else if (value instanceof List) {\n            return escapeList((List<Object>) value);\n        } else if (value instanceof Set) {\n            return escapeSet((Set<Object>) value);\n        } else if (value instanceof Object[]) {\n            return escapeArray((Object[]) value);\n        }\n        return value;\n    }\n\n    public static boolean hasExpression(String s) {\n        return s.contains(\"${\");\n    }\n\n    private ExpressionUtils() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/events/ExternalEventResource.java",
    "content": "package com.walmartlabs.concord.server.events;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.UuidGenerator;\nimport com.walmartlabs.concord.server.audit.AuditAction;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.audit.AuditObject;\nimport com.walmartlabs.concord.server.cfg.ExternalEventsConfiguration;\nimport com.walmartlabs.concord.server.events.externalevent.ExternalEventTriggerProcessor;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.Consumes;\nimport javax.ws.rs.POST;\nimport javax.ws.rs.Path;\nimport javax.ws.rs.PathParam;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.common.MemoSupplier.memo;\nimport static java.util.Objects.requireNonNull;\n\n/**\n * Handles generic external events.\n * Receives arbitrary JSON bodies and matches them with whatever is configured\n * in the trigger.\n */\n@Path(\"/api/v1/events\")\n@Tag(name = \"External Events\")\npublic class ExternalEventResource implements Resource {\n\n    private static final Logger log = LoggerFactory.getLogger(ExternalEventResource.class);\n\n    private final ExternalEventsConfiguration cfg;\n    private final TriggerProcessExecutor executor;\n    private final UserManager userManager;\n    private final TriggerEventInitiatorResolver initiatorResolver;\n    private final Set<ExternalEventTriggerProcessor> processors;\n    private final AuditLog auditLog;\n    private final UuidGenerator uuidGenerator;\n\n    @Inject\n    public ExternalEventResource(ExternalEventsConfiguration cfg,\n                                 TriggerProcessExecutor executor,\n                                 UserManager userManager,\n                                 TriggerEventInitiatorResolver initiatorResolver,\n                                 Set<ExternalEventTriggerProcessor> processors,\n                                 AuditLog auditLog,\n                                 UuidGenerator uuidGenerator) {\n\n        this.cfg = requireNonNull(cfg);\n        this.executor = requireNonNull(executor);\n        this.userManager = requireNonNull(userManager);\n        this.initiatorResolver = requireNonNull(initiatorResolver);\n        this.processors = requireNonNull(processors);\n        this.auditLog = requireNonNull(auditLog);\n        this.uuidGenerator = requireNonNull(uuidGenerator);\n    }\n\n    @POST\n    @Path(\"/{eventName:.*}\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"Handles an external event\", operationId = \"externalEvent\")\n    public Response event(@PathParam(\"eventName\") String eventName,\n                          Map<String, Object> data) {\n\n        if (executor.isDisabled(eventName)) {\n            log.warn(\"event ['{}'] disabled\", eventName);\n            return Response.ok().build();\n        }\n\n        Map<String, Object> event = data != null ? data : new HashMap<>();\n\n        String eventId = event.computeIfAbsent(\"id\", s -> uuidGenerator.generate()).toString();\n\n        if (cfg.isLogEvents()) {\n            auditLog.add(AuditObject.EXTERNAL_EVENT, AuditAction.ACCESS)\n                    .field(\"source\", eventName)\n                    .field(\"eventId\", eventId)\n                    .field(\"payload\", event)\n                    .log();\n        }\n\n        List<ExternalEventTriggerProcessor.Result> results = new ArrayList<>();\n        processors.forEach(p -> p.process(eventName, event, results));\n\n        for (ExternalEventTriggerProcessor.Result r : results) {\n            Event e = Event.builder()\n                    .id(eventId)\n                    .name(eventName)\n                    .attributes(r.event())\n                    .initiator(memo(new EventInitiatorSupplier(\"author\", userManager, r.event())))\n                    .build();\n\n            List<PartialProcessKey> processKeys = executor.execute(e, initiatorResolver, r.triggers());\n            log.info(\"event ['{}', '{}', '{}'] -> done, {} processes started\", eventId, eventName, event, processKeys.size());\n        }\n\n        return Response.ok().build();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/events/GithubEventResource.java",
    "content": "package com.walmartlabs.concord.server.events;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.Gauge;\nimport com.codahale.metrics.Histogram;\nimport com.codahale.metrics.MetricRegistry;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.google.common.annotations.VisibleForTesting;\nimport com.google.common.cache.CacheBuilder;\nimport com.google.common.cache.CacheLoader;\nimport com.google.common.cache.LoadingCache;\nimport com.walmartlabs.concord.common.AuthTokenProvider;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.common.ExternalAuthToken;\nimport com.walmartlabs.concord.common.ObjectMapperProvider;\nimport com.walmartlabs.concord.common.cfg.MappingAuthConfig;\nimport com.walmartlabs.concord.runtime.v2.model.GithubTriggerExclusiveMode;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport com.walmartlabs.concord.server.audit.AuditAction;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.audit.AuditObject;\nimport com.walmartlabs.concord.server.cfg.GithubConfiguration;\nimport com.walmartlabs.concord.server.events.github.GithubTriggerProcessor;\nimport com.walmartlabs.concord.server.events.github.GithubUtils;\nimport com.walmartlabs.concord.server.events.github.Payload;\nimport com.walmartlabs.concord.server.org.triggers.TriggerEntry;\nimport com.walmartlabs.concord.server.org.triggers.TriggerUtils;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.security.ldap.LdapManager;\nimport com.walmartlabs.concord.server.security.ldap.LdapPrincipal;\nimport com.walmartlabs.concord.server.user.UserEntry;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport com.walmartlabs.concord.server.user.UserType;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.Parameter;\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport io.swagger.v3.oas.annotations.extensions.Extension;\nimport io.swagger.v3.oas.annotations.extensions.ExtensionProperty;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.annotation.Nonnull;\nimport javax.inject.Inject;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.Context;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.UriInfo;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.URI;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.net.http.HttpResponse.BodyHandlers;\nimport java.util.*;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutionException;\nimport java.util.function.Supplier;\n\nimport static com.walmartlabs.concord.common.MemoSupplier.memo;\nimport static com.walmartlabs.concord.server.events.github.Constants.COMMIT_ID_KEY;\nimport static com.walmartlabs.concord.server.events.github.Constants.EVENT_SOURCE;\nimport static com.walmartlabs.concord.server.events.github.Constants.NODE_ID_KEY;\nimport static com.walmartlabs.concord.server.events.github.Constants.SENDER_KEY;\nimport static com.walmartlabs.concord.server.events.github.Constants.URL_KEY;\n\n/**\n * Handles external GitHub events.\n * Uses a custom authentication mechanism,\n * see {@link com.walmartlabs.concord.server.security.GithubAuthenticatingFilter}.\n * <p>\n * See also <a href=\"https://developer.github.com/webhooks/\">developer.github.com/webhooks</a>\n */\n@Path(\"/events/github\")\n@Tag(name = \"GitHub Events\")\npublic class GithubEventResource implements Resource {\n\n    private static final Logger log = LoggerFactory.getLogger(GithubEventResource.class);\n\n    private static final String ERROR_USER_EMAIL_LOOKUP = \"Error looking up user info {}: {}\";\n\n    private final GithubConfiguration githubCfg;\n    private final TriggerProcessExecutor executor;\n    private final AuditLog auditLog;\n    private final GithubTriggerProcessor processor;\n    private final UserManager userManager;\n    private final LdapManager ldapManager;\n    private final TriggerEventInitiatorResolver initiatorResolver;\n    private final Histogram startedProcessesPerEvent;\n    private final Map<String, Long> rateLimitGauges;\n    private final LoadingCache<EmailCacheKey, Optional<String>> ghUserEmailCache;\n\n    @Inject\n    public GithubEventResource(GithubConfiguration githubCfg,\n                               TriggerProcessExecutor executor,\n                               AuditLog auditLog,\n                               GithubTriggerProcessor processor,\n                               UserManager userManager,\n                               LdapManager ldapManager,\n                               TriggerEventInitiatorResolver initiatorResolver,\n                               MetricRegistry metricRegistry,\n                               AuthTokenProvider authTokenProvider,\n                               ObjectMapperProvider objectMapperProvider) {\n\n        this.githubCfg = githubCfg;\n        this.executor = executor;\n        this.auditLog = auditLog;\n        this.processor = processor;\n        this.userManager = userManager;\n        this.ldapManager = ldapManager;\n        this.initiatorResolver = initiatorResolver;\n        this.startedProcessesPerEvent = metricRegistry.histogram(\"started-processes-per-github-event\");\n        this.rateLimitGauges = new ConcurrentHashMap<>(githubCfg.getAuthConfigs().size());\n        this.ghUserEmailCache = CacheBuilder.newBuilder()\n                .expireAfterWrite(githubCfg.senderEmailCacheDuration())\n                .maximumSize(githubCfg.senderEmailCacheSize())\n                .concurrencyLevel(32)\n                .recordStats()\n                .build(new EmailCacheLoader(githubCfg, rateLimitGauges, authTokenProvider, objectMapperProvider.get()));\n\n        for (MappingAuthConfig c : githubCfg.getAuthConfigs()) {\n            Gauge<Long> rateLimitGauge = () -> rateLimitGauges.getOrDefault(c.id(), -1L);\n            metricRegistry.gauge(\"github-rate-limit-\" + c.id(), () -> rateLimitGauge);\n        }\n    }\n\n    @POST\n    @Path(\"/webhook\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.TEXT_PLAIN)\n    @WithTimer\n    @Operation(description = \"Handles GitHub repository level events\")\n    @Parameter(name = \"query\", in = ParameterIn.QUERY,\n            schema = @Schema(implementation = Map.class),\n            extensions = @Extension(name = \"concord\", properties = @ExtensionProperty(name = \"customQueryParams\", value = \"true\")))\n    public String onEvent(Map<String, Object> data,\n                          @HeaderParam(\"X-GitHub-Delivery\") String deliveryId,\n                          @HeaderParam(\"X-GitHub-Event\") String eventName,\n                          @Context UriInfo uriInfo) {\n\n        log.info(\"onEvent ['{}', '{}'] -> processing...\", deliveryId, eventName);\n\n        if (\"ping\".equalsIgnoreCase(eventName)) {\n            return \"ok\";\n        }\n\n        if (executor.isDisabled(eventName)) {\n            log.warn(\"event ['{}', '{}'] -> disabled\", deliveryId, eventName);\n            return \"ok\";\n        }\n\n        if (githubCfg.isLogEvents()) {\n            auditLog.add(AuditObject.EXTERNAL_EVENT, AuditAction.ACCESS)\n                    .field(\"source\", EVENT_SOURCE)\n                    .field(\"eventId\", deliveryId)\n                    .field(\"githubEvent\", eventName)\n                    .field(\"payload\", data)\n                    .log();\n        }\n\n        Payload payload = Payload.from(eventName, data);\n        if (payload == null) {\n            log.warn(\"event ['{}', '{}'] -> can't parse payload\", deliveryId, eventName);\n            return \"ok\";\n        }\n\n        List<GithubTriggerProcessor.Result> results = new ArrayList<>();\n        processor.process(eventName, payload, uriInfo, results);\n\n        Supplier<UserEntry> initiatorSupplier = memo(new GithubEventInitiatorSupplier(githubCfg, userManager, ldapManager, payload, ghUserEmailCache));\n\n        int startedProcesses = 0;\n        for (GithubTriggerProcessor.Result r : results) {\n            Event e = Event.builder()\n                    .id(deliveryId)\n                    .name(EVENT_SOURCE)\n                    .attributes(r.event())\n                    .initiator(initiatorSupplier)\n                    .build();\n\n            List<PartialProcessKey> processes = executor.execute(e, r.triggers(), initiatorResolver, (t, cfg) -> {\n                // if `useEventCommitId` is true then the process is forced to use the specified commit ID\n                String commitId = MapUtils.getString(r.event(), COMMIT_ID_KEY);\n                if (commitId != null && TriggerUtils.isUseEventCommitId(t)) {\n                    cfg.put(Constants.Request.REPO_COMMIT_ID, commitId);\n                    cfg.put(Constants.Request.REPO_BRANCH_OR_TAG, payload.getHead());\n                }\n                return cfg;\n            }, new GithubExclusiveParamsResolver(payload));\n            startedProcesses += processes.size();\n        }\n        startedProcessesPerEvent.update(startedProcesses);\n\n        log.info(\"onEvent ['{}', '{}'] -> done, started process count: {}\", deliveryId, eventName, startedProcesses);\n\n        return \"ok\";\n    }\n\n    private static class GithubExclusiveParamsResolver implements TriggerProcessExecutor.TriggerExclusiveParamsResolver {\n\n        private static final ObjectMapper objectMapper = new ObjectMapper();\n        private final Payload payload;\n\n        public GithubExclusiveParamsResolver(Payload payload) {\n            this.payload = payload;\n        }\n\n        @Override\n        public Map<String, Object> resolve(TriggerEntry t) {\n            Map<String, Object> exclusive = TriggerUtils.getExclusive(t);\n            if (exclusive.isEmpty()) {\n                return exclusive;\n            }\n\n            GithubTriggerExclusiveMode e = objectMapper.convertValue(exclusive, GithubTriggerExclusiveMode.class);\n            String groupBy = e.groupByProperty();\n            if (groupBy == null) {\n                return exclusive;\n            }\n\n            String group;\n            if (\"branch\".equals(groupBy)) {\n                group = payload.getBranch();\n            } else if (groupBy.startsWith(\"event\")) {\n                String[] payloadPath = groupBy.split(\"\\\\.\");\n                if (payloadPath.length == 1) {\n                    throw new IllegalArgumentException(\"Invalid groupBy: '\" + groupBy + \"'\");\n                }\n\n                payloadPath = Arrays.copyOfRange(payloadPath, 1, payloadPath.length);\n                Object maybeString = ConfigurationUtils.get(payload.raw(), payloadPath);\n                if (maybeString == null || (maybeString instanceof String)) {\n                    group = (String) maybeString;\n                } else {\n                    String value = maybeString + \" (class: \" + maybeString.getClass() + \")\";\n                    throw new IllegalArgumentException(\"Expected string value for groupBy: '\" + groupBy + \"', got \" + value);\n                }\n            } else {\n                throw new IllegalArgumentException(\"Unknown groupBy: '\" + groupBy + \"'\");\n            }\n\n            if (group == null) {\n                return Collections.emptyMap();\n            }\n\n            Map<String, Object> result = new HashMap<>();\n            result.put(\"group\", group);\n            result.put(\"mode\", e.mode().name());\n            return result;\n        }\n    }\n\n    @VisibleForTesting\n    static class GithubEventInitiatorSupplier implements Supplier<UserEntry> {\n\n        private final GithubConfiguration githubCfg;\n        private final UserManager userManager;\n        private final LdapManager ldapManager;\n        private final LoadingCache<EmailCacheKey, Optional<String>> ghUserEmailCache;\n        private final Payload payload;\n        private final Supplier<UserEntry> fallback;\n\n        public GithubEventInitiatorSupplier(GithubConfiguration githubCfg,\n                                            UserManager userManager,\n                                            LdapManager ldapManager,\n                                            Payload payload,\n                                            LoadingCache<EmailCacheKey, Optional<String>> ghUserEmailCache) {\n\n            this.githubCfg = githubCfg;\n            this.userManager = userManager;\n            this.ldapManager = ldapManager;\n            this.ghUserEmailCache = ghUserEmailCache;\n            this.payload = payload;\n            this.fallback = () -> {\n                String initiator = payload.getSender();\n                if (initiator == null || initiator.trim().isEmpty()) {\n                    throw new ConcordApplicationException(\"Can't determine initiator: \" + payload);\n                }\n\n                UserEntry userEntry = userManager.getOrCreate(initiator, null, UserType.LDAP)\n                        .orElseThrow(() -> new ConcordApplicationException(\"User not found: \" + initiator));\n\n                // add to DB mapping cache (if enabled) so that next time we can\n                // find the user without LDAP lookup\n                addUserDbMapping(userEntry);\n\n                return userEntry;\n            };\n        }\n\n        @Override\n        public UserEntry get() {\n            // GitHub user -> Concord user mapping may already exist in DB\n            UserEntry fromDbMapping = findUserInDbMapping();\n\n            if (fromDbMapping != null) {\n                return fromDbMapping;\n            }\n\n            // don't try to match against payload sender's ldap_dn or email if they aren't present\n            if (!githubCfg.isUseSenderLdapDn() && !githubCfg.isUseSenderEmail()) {\n                return fallback.get();\n            }\n\n            // only LDAP users are supported in GitHub triggers\n            // ideally, match against exact LDAP DN (requires GitHub to be integrated with LDAP)\n            if (githubCfg.isUseSenderLdapDn()) {\n                UserEntry fromDn = findSenderDnInLdap();\n                if (fromDn != null) {\n                    addUserDbMapping(fromDn);\n                    return fromDn;\n                }\n            }\n\n            // alternatively, user email may work (e.g. from SSO provider which has upstream LDAP source)\n            if (githubCfg.isUseSenderEmail()) {\n                UserEntry fromEmail = findSenderEmailInLdap();\n                if (fromEmail != null) {\n                    addUserDbMapping(fromEmail);\n                    return fromEmail;\n                }\n            }\n\n            log.warn(\"getOrCreateUserEntry ['{}'] -> can't determine the sender's 'ldap_dn' or 'email', falling back to 'login'\", payload);\n            return fallback.get();\n        }\n\n        private UserEntry findUserInDbMapping() {\n            if (!githubCfg.isEnableExternalUserIdMappingCache()) {\n                return null;\n            }\n\n            String senderUrl = payload.getUrl(SENDER_KEY);\n            String senderNodeId = payload.getNodeId(SENDER_KEY);\n            String externalId = formatExternalId(senderUrl, senderNodeId);\n\n            if (externalId == null) {\n                return null;\n            }\n            return userManager.getUserFromExternalMapping(externalId).orElse(null);\n        }\n\n        private void addUserDbMapping(UserEntry user) {\n            if (!githubCfg.isEnableExternalUserIdMappingCache()) {\n                return;\n            }\n\n            String senderUrl = payload.getUrl(SENDER_KEY);\n            String senderNodeId = payload.getNodeId(SENDER_KEY);\n            String externalId = formatExternalId(senderUrl, senderNodeId);\n\n            if (externalId == null) {\n                return;\n            }\n\n            userManager.createExternalUserMapping(user.getId(), externalId);\n        }\n\n        private static String formatExternalId(String url, String userId) {\n            if (url == null || userId == null) {\n                return null;\n            }\n\n            if (url.isBlank() || userId.isBlank()) {\n                return null;\n            }\n\n            return String.format(\"github_%s=%s,%s=%s\", URL_KEY, url, NODE_ID_KEY, userId);\n        }\n\n        private UserEntry findSenderDnInLdap() {\n            String ldapDn = payload.getSenderLdapDn();\n            if (ldapDn == null || ldapDn.isBlank()) {\n                return null;\n            }\n\n            try {\n                LdapPrincipal p = ldapManager.getPrincipalByDn(ldapDn);\n\n                if (p == null) {\n                    log.warn(\"getOrCreateUserEntry ['{}'] -> can't find user by ldap DN ({})\", payload, ldapDn);\n                    return null;\n                }\n\n                return userManager.getOrCreate(p.getUsername(), p.getDomain(), UserType.LDAP)\n                        .orElseThrow(() -> new ConcordApplicationException(\"User not found: \" + p.getUsername()));\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        private UserEntry findSenderEmailInLdap() {\n            String email = getEmail();\n            if (email == null || email.isBlank()) {\n                return null;\n            }\n\n            try {\n                LdapPrincipal p = ldapManager.getPrincipalByMail(email);\n\n                if (p == null) {\n                    log.warn(\"getOrCreateUserEntry ['{}'] -> can't find user by ldap mail ({})\", payload, email);\n                    return null;\n                }\n\n                return userManager.getOrCreate(p.getUsername(), p.getDomain(), UserType.LDAP)\n                        .orElseThrow(() -> new ConcordApplicationException(\"User not found: \" + p.getUsername()));\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        private String getEmail() {\n            URI repoUrl = GithubUtils.getRepoCloneUrl(payload);\n            URI userUrl = GithubUtils.getSenderUrl(payload);\n\n            try {\n                return ghUserEmailCache.get(new EmailCacheKey(repoUrl, userUrl))\n                        .orElse(null);\n            } catch (ExecutionException ee) {\n                Throwable t = ee.getCause();\n                log.warn(ERROR_USER_EMAIL_LOOKUP, userUrl, t.getMessage());\n            }\n\n            return null;\n        }\n    }\n\n    private static class UserLookupException extends Exception {\n        public UserLookupException(String message) {\n            super(message);\n        }\n    }\n\n    private record GitHubUser(String email) {\n    }\n\n    @VisibleForTesting\n    record EmailCacheKey(@Nonnull URI repoUrl, @Nonnull URI userUrl) {\n\n        @Override\n        public boolean equals(Object o) {\n            if (o == null || getClass() != o.getClass()) return false;\n\n            EmailCacheKey that = (EmailCacheKey) o;\n            // userUrl is sufficient for equality for caching--it will point to\n            // the same user regardless of repoUrl.\n            // repoUrl is only necessary to acquire token, not for caching.\n            return userUrl().equals(that.userUrl());\n        }\n\n        @Override\n        public int hashCode() {\n            return userUrl().hashCode();\n        }\n    }\n\n    private static class EmailCacheLoader extends CacheLoader<EmailCacheKey, Optional<String>> {\n\n        private final Map<String, Long> rateLimitGauges;\n        private final AuthTokenProvider authTokenProvider;\n        private final ObjectMapper objectMapper;\n        private final HttpClient httpClient;\n\n        public EmailCacheLoader(GithubConfiguration githubCfg,\n                                Map<String, Long> rateLimitGauges,\n                                AuthTokenProvider authTokenProvider,\n                                ObjectMapper objectMapper) {\n\n            this.rateLimitGauges = rateLimitGauges;\n            this.authTokenProvider = authTokenProvider;\n            this.objectMapper = objectMapper;\n            this.httpClient = HttpClient.newBuilder()\n                    .connectTimeout(githubCfg.getHttpClientTimeout())\n                    .build();\n        }\n\n        @Override\n        public @Nonnull Optional<String> load(@Nonnull EmailCacheKey key) throws Exception {\n            URI repoUrl = key.repoUrl();\n            URI userUrl = key.userUrl();\n\n            Optional<ExternalAuthToken> t = authTokenProvider.getToken(repoUrl, null);\n\n            if (t.isEmpty()) {\n                return Optional.empty();\n            }\n\n            ExternalAuthToken externalAuth  = t.get();\n            HttpRequest req = HttpRequest.newBuilder(userUrl)\n                    .GET()\n                    .header(\"Authorization\", \"Bearer \" + externalAuth.token())\n                    .build();\n\n            HttpResponse<InputStream> resp = httpClient.send(req, BodyHandlers.ofInputStream());\n\n            if (externalAuth.authId() != null) {\n                long remaining = resp.headers()\n                        .firstValueAsLong(\"X-RateLimit-Remaining\")\n                        .orElse(-1L);\n                rateLimitGauges.put(externalAuth.authId(), remaining);\n            }\n\n            if (resp.statusCode() != 200) {\n                throw new UserLookupException(\"Non-200 response [: \" + resp.statusCode() + \"]: \" + readBody(resp));\n            }\n\n            GitHubUser m = objectMapper.readValue(resp.body(), GitHubUser.class);\n\n            return Optional.ofNullable(m.email());\n        }\n\n        private static String readBody(HttpResponse<InputStream> resp) {\n            try (InputStream is = resp.body()) {\n                return new String(is.readAllBytes());\n            } catch (IOException e) {\n                return \"error reading response body: \" + e.getMessage();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/events/TriggerEventInitiatorResolver.java",
    "content": "package com.walmartlabs.concord.server.events;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.org.triggers.TriggerEntry;\nimport com.walmartlabs.concord.server.org.triggers.TriggerUtils;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.user.UserEntry;\n\npublic class TriggerEventInitiatorResolver {\n\n    public UserEntry resolve(TriggerEntry trigger, Event event) {\n        boolean useInitiator = TriggerUtils.isUseInitiator(trigger);\n        if (useInitiator) {\n            UserEntry initiator = event.initiator().get();\n            if (initiator != null) {\n                return initiator;\n            }\n        }\n\n        return UserPrincipal.assertCurrent().getUser();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/events/TriggerProcessExecutor.java",
    "content": "package com.walmartlabs.concord.server.events;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.util.concurrent.SettableFuture;\nimport com.google.inject.Inject;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.cfg.ExternalEventsConfiguration;\nimport com.walmartlabs.concord.server.cfg.TriggersConfiguration;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.org.project.RepositoryDao;\nimport com.walmartlabs.concord.server.org.triggers.TriggerEntry;\nimport com.walmartlabs.concord.server.org.triggers.TriggerUtils;\nimport com.walmartlabs.concord.server.process.*;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.SecurityUtils;\nimport com.walmartlabs.concord.server.security.UserSecurityContext;\nimport com.walmartlabs.concord.server.user.UserEntry;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.ws.rs.core.Response;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.stream.Collectors;\n\npublic class TriggerProcessExecutor {\n\n    public interface ProcessConfigurationEnricher {\n\n        Map<String, Object> enrich(TriggerEntry t, Map<String, Object> cfg);\n    }\n\n    public interface TriggerExclusiveParamsResolver {\n\n        Map<String, Object> resolve(TriggerEntry t);\n    }\n\n    private static final Logger log = LoggerFactory.getLogger(TriggerProcessExecutor.class);\n\n    private final ExternalEventsConfiguration eventsCfg;\n    private final TriggersConfiguration triggersCfg;\n    private final ProcessManager processManager;\n    private final RepositoryDao repositoryDao;\n    private final ProjectDao projectDao;\n    private final UserSecurityContext userSecurityContext;\n    private final ExecutorService executor;\n\n    @Inject\n    public TriggerProcessExecutor(ExternalEventsConfiguration eventsCfg,\n                                  TriggersConfiguration triggersCfg,\n                                  ProcessManager processManager,\n                                  RepositoryDao repositoryDao,\n                                  ProjectDao projectDao,\n                                  UserSecurityContext userSecurityContext) {\n\n        this.eventsCfg = eventsCfg;\n        this.triggersCfg = triggersCfg;\n        this.processManager = processManager;\n        this.repositoryDao = repositoryDao;\n        this.projectDao = projectDao;\n        this.userSecurityContext = userSecurityContext;\n        this.executor = createExecutor(eventsCfg.getWorkerThreads());\n    }\n\n    public boolean isDisabled(String eventName) {\n        return triggersCfg.isDisableAll() || triggersCfg.getDisabled().contains(eventName);\n    }\n\n    public List<PartialProcessKey> execute(Event event,\n                                           TriggerEventInitiatorResolver initiatorResolver,\n                                           List<TriggerEntry> triggers) {\n\n        return execute(event, triggers, initiatorResolver, null, TriggerUtils::getExclusive);\n    }\n\n    @WithTimer\n    public List<PartialProcessKey> execute(Event event,\n                                           List<TriggerEntry> triggers,\n                                           TriggerEventInitiatorResolver initiatorResolver,\n                                           ProcessConfigurationEnricher cfgEnricher,\n                                           TriggerExclusiveParamsResolver exclusiveResolver) {\n\n        if (isDisabled(event.name())) {\n            log.warn(\"process ['{}'] event '{}' disabled\", event.id(), event.name());\n            return Collections.emptyList();\n        }\n\n        assertRoles(event.name());\n\n        return triggers.stream()\n                .filter(t -> !isRepositoryDisabled(t))\n                .map(t -> submitProcess(event, t, initiatorResolver, cfgEnricher, exclusiveResolver))\n                .toList() // collect all \"futures\"\n                .stream()\n                .map(TriggerProcessExecutor::resolve)\n                .filter(Objects::nonNull)\n                .collect(Collectors.toList());\n    }\n\n    private void assertRoles(String eventName) {\n        if (Roles.isAdmin()) {\n            return;\n        }\n\n        // optional feature: require a specific user role to access the external events endpoint\n        Map<String, String> requiredRoles = eventsCfg.getRequiredRoles();\n        if (requiredRoles == null || requiredRoles.isEmpty()) {\n            return;\n        }\n\n        requiredRoles.forEach((k, v) -> {\n            if (eventName.matches(k) && !SecurityUtils.hasRole(v)) {\n                throw new ConcordApplicationException(\"'\" + v + \"' role is required\", Response.Status.FORBIDDEN);\n            }\n        });\n    }\n\n    private boolean isRepositoryDisabled(TriggerEntry t) {\n        return repositoryDao.get(t.getRepositoryId()).isDisabled();\n    }\n\n    private Future<PartialProcessKey> submitProcess(Event event,\n                                                    TriggerEntry t,\n                                                    TriggerEventInitiatorResolver initiatorResolver,\n                                                    ProcessConfigurationEnricher cfgEnricher,\n                                                    TriggerExclusiveParamsResolver exclusiveResolver) {\n\n        UserEntry initiator;\n        try {\n            initiator = initiatorResolver.resolve(t, event);\n        } catch (Exception e) {\n            log.error(\"process ['{}', '{}', '{}'] -> error\", event.id(), event.name(), t.getId(), e);\n            SettableFuture<PartialProcessKey> f = SettableFuture.create();\n            f.set(null);\n            return f;\n        }\n\n        return executor.submit(() -> {\n            Map<String, Object> args = new HashMap<>();\n            if (t.getArguments() != null) {\n                args.putAll(t.getArguments());\n            }\n\n            Map<String, Object> eventAttributes = new LinkedHashMap<>(event.attributes());\n            eventAttributes.put(\"id\", event.id());\n\n            args.put(\"event\", ExpressionUtils.escapeMap(eventAttributes));\n\n            Map<String, Object> cfg = new HashMap<>();\n            cfg.put(Constants.Request.ARGUMENTS_KEY, args);\n\n            if (TriggerUtils.getEntryPoint(t) != null) {\n                cfg.put(Constants.Request.ENTRY_POINT_KEY, TriggerUtils.getEntryPoint(t));\n            }\n\n            if (t.getActiveProfiles() != null) {\n                cfg.put(Constants.Request.ACTIVE_PROFILES_KEY, t.getActiveProfiles());\n            }\n\n            Map<String, Object> exclusive = exclusiveResolver.resolve(t);\n            if (exclusive != null && !exclusive.isEmpty()) {\n                // avoid saving empty objects into the cfg\n                cfg.put(Constants.Request.EXCLUSIVE, exclusive);\n            }\n\n            if (cfgEnricher != null) {\n                cfg = cfgEnricher.enrich(t, cfg);\n            }\n\n            try {\n                UUID orgId = projectDao.getOrgId(t.getProjectId());\n\n                PartialProcessKey pk = startProcess(event.id(), orgId, t, cfg, initiator);\n                log.info(\"process ['{}'] -> new process ('{}') triggered by {}\", event.id(), pk, t);\n                return pk;\n            } catch (Exception e) {\n                log.error(\"process ['{}', '{}', '{}'] -> error\", event.id(), event.name(), t.getId(), e);\n                return null;\n            }\n        });\n    }\n\n    private PartialProcessKey startProcess(String eventId,\n                                           UUID orgId,\n                                           TriggerEntry t,\n                                           Map<String, Object> cfg,\n                                           UserEntry initiator) throws Exception {\n\n        PartialProcessKey processKey = PartialProcessKey.create();\n\n        userSecurityContext.runAs(initiator.getId(), () -> {\n            Payload payload = PayloadBuilder.start(processKey)\n                    .initiator(initiator.getId(), initiator.getName())\n                    .organization(orgId)\n                    .project(t.getProjectId())\n                    .repository(t.getRepositoryId())\n                    .configuration(cfg)\n                    .triggeredBy(TriggeredByEntry.builder()\n                            .externalEventId(eventId)\n                            .trigger(t)\n                            .build())\n                    .build();\n\n            processManager.start(payload);\n            return null;\n        });\n\n        return processKey;\n    }\n\n    private static <T> T resolve(Future<T> f) {\n        try {\n            return f.get();\n        } catch (InterruptedException | ExecutionException e) { // NOSONAR\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static ExecutorService createExecutor(int poolSize) {\n        ThreadPoolExecutor p = new ThreadPoolExecutor(1, poolSize, 30, TimeUnit.SECONDS, new SynchronousQueue<>());\n        p.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());\n        return p;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/events/externalevent/ExternalEventTriggerProcessor.java",
    "content": "package com.walmartlabs.concord.server.events.externalevent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.events.DefaultEventFilter;\nimport com.walmartlabs.concord.server.org.triggers.TriggerEntry;\nimport com.walmartlabs.concord.server.org.triggers.TriggersDao;\n\nimport javax.inject.Inject;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic abstract class ExternalEventTriggerProcessor {\n\n    private final TriggersDao dao;\n    private final int version;\n\n    @Inject\n    public ExternalEventTriggerProcessor(TriggersDao dao, int version) {\n        this.dao = dao;\n        this.version = version;\n    }\n\n    public void process(String eventName, Map<String, Object> event, List<Result> result) {\n        List<TriggerEntry> triggers = listTriggers(eventName);\n\n        Map<String, Object> updatedEvent = buildEvent(event);\n\n        for (TriggerEntry t : triggers) {\n            if (DefaultEventFilter.filter(updatedEvent, t)) {\n                result.add(Result.from(updatedEvent, t));\n            }\n        }\n    }\n\n    private Map<String, Object> buildEvent(Map<String, Object> event) {\n        Map<String, Object> m = new HashMap<>(event);\n        m.put(\"version\", version);\n        return m;\n    }\n\n    private List<TriggerEntry> listTriggers(String eventName) {\n        return dao.list(eventName, version, null);\n    }\n\n    public static class Result {\n\n        private final Map<String, Object> event;\n\n        private final List<TriggerEntry> triggers;\n\n        public Result(Map<String, Object> event, List<TriggerEntry> triggers) {\n            this.event = event;\n            this.triggers = triggers;\n        }\n\n        public Map<String, Object> event() {\n            return event;\n        }\n\n        public List<TriggerEntry> triggers() {\n            return triggers;\n        }\n\n        public static Result from(Map<String, Object> event, TriggerEntry trigger) {\n            return new Result(event, Collections.singletonList(trigger));\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/events/externalevent/ExternalEventTriggerV1Processor.java",
    "content": "package com.walmartlabs.concord.server.events.externalevent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.org.triggers.TriggersDao;\n\nimport javax.inject.Inject;\n\n@Deprecated\npublic class ExternalEventTriggerV1Processor extends ExternalEventTriggerProcessor {\n\n    @Inject\n    public ExternalEventTriggerV1Processor(TriggersDao dao) {\n        super(dao, 1);\n    }\n}"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/events/externalevent/ExternalEventTriggerV2Processor.java",
    "content": "package com.walmartlabs.concord.server.events.externalevent;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.org.triggers.TriggersDao;\n\nimport javax.inject.Inject;\n\npublic class ExternalEventTriggerV2Processor extends ExternalEventTriggerProcessor {\n\n    @Inject\n    public ExternalEventTriggerV2Processor(TriggersDao dao) {\n        super(dao, 2);\n    }\n}"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/events/github/Constants.java",
    "content": "package com.walmartlabs.concord.server.events.github;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic final class Constants {\n\n    public static final String EVENT_SOURCE = \"github\";\n\n    public static final String COMMIT_ID_KEY = \"commitId\";\n    public static final String IGNORE_EMPTY_PUSH_KEY = \"ignoreEmptyPush\";\n    public static final String ORGANIZATION_KEY = \"organization\";\n    public static final String PAYLOAD_KEY = \"payload\";\n    public static final String PROJECT_ID_KEY = \"projectId\";\n    public static final String PULL_REQUEST_EVENT = \"pull_request\";\n    public static final String PUSH_EVENT = \"push\";\n    public static final String REPO_BRANCH_KEY = \"branch\";\n    public static final String REPO_ID_KEY = \"repositoryId\";\n    public static final String REPO_NAME_KEY = \"repository\";\n    public static final String REPO_ENABLED_KEY = \"enabled\";\n    public static final String SENDER_KEY = \"sender\";\n    public static final String STATUS_KEY = \"status\";\n    public static final String TYPE_KEY = \"type\";\n    public static final String VERSION_KEY = \"version\";\n    public static final String FILES_KEY = \"files\";\n    public static final String QUERY_PARAMS_KEY = \"queryParams\";\n    public static final String BASE_KEY = \"base\";\n    public static final String HEAD_KEY = \"head\";\n    public static final String NODE_ID_KEY = \"node_id\";\n    public static final String REF_KEY = \"ref\";\n    public static final String URL_KEY = \"url\";\n\n    public static final String GITHUB_ORG_KEY = \"githubOrg\";\n    public static final String GITHUB_REPO_KEY = \"githubRepo\";\n    public static final String GITHUB_HOST_KEY = \"githubHost\";\n\n    private Constants() {\n\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/events/github/GithubRepoInfo.java",
    "content": "package com.walmartlabs.concord.server.events.github;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\n@Value.Immutable\npublic interface GithubRepoInfo {\n\n    String owner();\n\n    String name();\n\n    static ImmutableGithubRepoInfo.Builder builder() {\n        return ImmutableGithubRepoInfo.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/events/github/GithubTriggerProcessor.java",
    "content": "package com.walmartlabs.concord.server.events.github;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n *   \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.sdk.Constants.Trigger;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport com.walmartlabs.concord.server.cfg.GithubConfiguration;\nimport com.walmartlabs.concord.server.events.DefaultEventFilter;\nimport com.walmartlabs.concord.server.org.project.RepositoryDao;\nimport com.walmartlabs.concord.server.org.project.RepositoryEntry;\nimport com.walmartlabs.concord.server.org.triggers.TriggerEntry;\nimport com.walmartlabs.concord.server.org.triggers.TriggerUtils;\nimport com.walmartlabs.concord.server.org.triggers.TriggersDao;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.security.github.GithubKey;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.jooq.impl.DSL;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.core.UriInfo;\nimport java.util.*;\nimport java.util.function.Consumer;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.events.github.Constants.*;\n\npublic class GithubTriggerProcessor {\n\n    private static final int VERSION_ID = 2;\n    private static final Logger log = LoggerFactory.getLogger(GithubTriggerProcessor.class);\n\n    private final Dao dao;\n    private final Set<EventEnricher> eventEnrichers;\n    private final boolean isDisableReposOnDeletedRef;\n\n    @Inject\n    public GithubTriggerProcessor(Dao dao,\n                                  Set<EventEnricher> eventEnrichers,\n                                  GithubConfiguration githubCfg) {\n        this.dao = dao;\n        this.eventEnrichers = eventEnrichers;\n        this.isDisableReposOnDeletedRef = githubCfg.isDisableReposOnDeletedRef();\n    }\n\n    @WithTimer\n    public void process(String eventName, Payload payload, UriInfo uriInfo, List<Result> result) {\n        GithubKey githubKey = GithubKey.getCurrent();\n        UUID projectId = githubKey.getProjectId();\n\n        if (isDisableReposOnDeletedRef\n            && PUSH_EVENT.equals(payload.eventName())\n            && isRefDeleted(payload)) {\n\n            List<RepositoryEntry> repositories = dao.findRepos(payload.getFullRepoName());\n            // disable repos configured with the event branch\n            repositories.stream()\n                    .filter(r -> !r.isDisabled() && null != r.getBranch())\n                    .filter(r -> r.getBranch().equals(payload.getBranch()))\n                    .forEach(r -> disableRepo(r, payload));\n        }\n\n        List<TriggerEntry> triggers = listTriggers(projectId, payload.getOrg(), payload.getRepo());\n        for (TriggerEntry t : triggers) {\n            if (skipTrigger(t, eventName, payload)) {\n                continue;\n            }\n\n            Map<String, Object> event = buildEvent(eventName, uriInfo, payload);\n            enrichEventConditions(payload, t, event);\n\n            if (DefaultEventFilter.filter(event, t)) {\n                result.add(new Result(event, t));\n            }\n        }\n    }\n\n    static boolean skipTrigger(TriggerEntry t, String eventName, Payload payload) {\n        // skip empty push events if the trigger's configuration says so\n        if (GithubUtils.ignoreEmptyPush(t) && GithubUtils.isEmptyPush(eventName, payload)) {\n            return true;\n        }\n\n        // process is destined to fail if attempted to start from commit in another repo\n        // on an event from a pull request.\n        if (TriggerUtils.isUseEventCommitId(t)\n            && payload.hasPullRequestEntry()\n            && payload.isPullRequestFromDifferentRepo()) {\n\n            log.info(\"Skip start from {} event [{}, {}] -> Commit is in a different repository.\",\n                    eventName, payload.getPullRequestBaseUrl(), payload.getPullRequestHeadUrl());\n\n            return true;\n        }\n\n        return false;\n    }\n\n    private void enrichEventConditions(Payload payload, TriggerEntry trigger, Map<String, Object> result) {\n        for (EventEnricher e : eventEnrichers) {\n            e.enrich(payload, trigger, result);\n        }\n    }\n\n    private void disableRepo(RepositoryEntry repo, Payload payload) {\n        log.info(\"disable repo ['{}', '{}'] -> ref deleted\", repo.getId(), payload.getBranch());\n        dao.disable(repo.getProjectId(), repo.getId());\n    }\n\n    private static boolean isRefDeleted(Payload payload) {\n        Object val = payload.raw().get(\"deleted\");\n\n        if (val == null) {\n            return false;\n        }\n\n        if (val instanceof String str) {\n            return Boolean.parseBoolean(str);\n        }\n\n        return Boolean.TRUE.equals(val);\n    }\n\n    @WithTimer\n    List<TriggerEntry> listTriggers(UUID projectId, String org, String repo) {\n        return dao.listTriggers(projectId, org, repo);\n    }\n\n    private Map<String, Object> buildEvent(String eventName, UriInfo uriInfo, Payload payload) {\n        Map<String, Object> result = new HashMap<>();\n\n        result.put(GITHUB_ORG_KEY, payload.getOrg());\n        result.put(GITHUB_REPO_KEY, payload.getRepo());\n        result.put(GITHUB_HOST_KEY, payload.getHost());\n        String branch = payload.getBranch();\n        if (branch != null) {\n            result.put(REPO_BRANCH_KEY, payload.getBranch());\n        }\n\n        if (PULL_REQUEST_EVENT.equals(eventName)) {\n            Map<String, Object> pullRequest = MapUtils.getMap(payload.raw(), PULL_REQUEST_EVENT, Collections.emptyMap());\n            Map<String, Object> head = MapUtils.getMap(pullRequest, \"head\", Collections.emptyMap());\n            String sha = MapUtils.getString(head, \"sha\");\n            if (sha != null) {\n                result.put(COMMIT_ID_KEY, sha);\n            }\n        } else if (PUSH_EVENT.equals(eventName)) {\n            String after = payload.getString(\"after\");\n            if (after != null) {\n                result.put(COMMIT_ID_KEY, after);\n            }\n        }\n\n        result.put(SENDER_KEY, payload.getSender());\n        result.put(TYPE_KEY, eventName);\n        result.put(STATUS_KEY, payload.getAction());\n        result.put(PAYLOAD_KEY, payload.raw());\n        result.put(QUERY_PARAMS_KEY, new HashMap<>(uriInfo.getQueryParameters()));\n\n        // files\n        Map<String, Set<String>> files = new HashMap<>(payload.getFiles());\n        // alias for all files (changed/modified/deleted)\n        files.put(\"any\", files.values().stream()\n                .flatMap(Set::stream)\n                .collect(Collectors.toSet()));\n        result.put(FILES_KEY, files);\n\n        // match only with v2 triggers\n        result.put(VERSION_KEY, VERSION_ID);\n\n        return result;\n    }\n\n    public interface EventEnricher {\n\n        void enrich(Payload payload, TriggerEntry trigger, Map<String, Object> result);\n    }\n\n    /**\n     * Adds {@link Trigger#REPOSITORY_INFO} property to the event, but only if\n     * the trigger's conditions contained the clause with the same key.\n     */\n    public static class RepositoryInfoEnricher implements EventEnricher {\n\n        private final Dao dao;\n\n        @Inject\n        public RepositoryInfoEnricher(Dao dao) {\n            this.dao = dao;\n        }\n\n        @Override\n        @WithTimer\n        public void enrich(Payload payload, TriggerEntry trigger, Map<String, Object> result) {\n            Object projectInfoConditions = trigger.getConditions().get(Trigger.REPOSITORY_INFO);\n            if (projectInfoConditions == null || payload.getFullRepoName() == null) {\n                return;\n            }\n\n            List<Map<String, Object>> repositoryInfos = new ArrayList<>();\n            List<RepositoryEntry> repositories = dao.findRepos(payload.getFullRepoName());\n\n            for (RepositoryEntry r : repositories) {\n                if (r.isDisabled()) {\n                    continue;\n                }\n\n                Map<String, Object> repositoryInfo = new HashMap<>();\n                repositoryInfo.put(REPO_ID_KEY, r.getId());\n                repositoryInfo.put(REPO_NAME_KEY, r.getName());\n                repositoryInfo.put(PROJECT_ID_KEY, r.getProjectId());\n                if (r.getBranch() != null) {\n                    repositoryInfo.put(REPO_BRANCH_KEY, r.getBranch());\n                }\n                repositoryInfo.put(REPO_ENABLED_KEY, !r.isDisabled());\n\n                repositoryInfos.add(repositoryInfo);\n            }\n\n            if (!repositoryInfos.isEmpty()) {\n                result.put(Trigger.REPOSITORY_INFO, repositoryInfos);\n            }\n        }\n    }\n\n    public static class Dao {\n        private final RepositoryDao repoDao;\n        private final TriggersDao triggersDao;\n        private final Configuration cfg;\n\n        @Inject\n        public Dao(@MainDB Configuration cfg,\n                   RepositoryDao repoDao,\n                   TriggersDao triggersDao) {\n\n            this.cfg = cfg;\n            this.triggersDao = triggersDao;\n            this.repoDao = repoDao;\n        }\n\n        private List<RepositoryEntry> findRepos(String repoOrgAndName) {\n            String sshAndHttpPattern = \"%[/:]\" + repoOrgAndName + \"(.git)?/?\";\n            return repoDao.findSimilar(sshAndHttpPattern);\n        }\n\n        List<TriggerEntry> listTriggers(UUID projectId, String org, String repo) {\n            Map<String, String> conditions = new HashMap<>();\n\n            if (org != null) {\n                conditions.put(GITHUB_ORG_KEY, org);\n            }\n\n            if (repo != null) {\n                conditions.put(GITHUB_REPO_KEY, repo);\n            }\n\n            return triggersDao.list(projectId, EVENT_SOURCE, VERSION_ID, conditions);\n        }\n\n        void disable(UUID projectId, UUID repoId) {\n            tx(tx -> {\n                repoDao.disable(tx, repoId);\n                triggersDao.delete(tx, projectId, repoId);\n            });\n        }\n\n        private DSLContext dsl() {\n            return DSL.using(cfg);\n        }\n\n        private void tx(Consumer<DSLContext> c) {\n            dsl().transaction(localCfg -> {\n                DSLContext tx = DSL.using(localCfg);\n                c.accept(tx);\n            });\n        }\n    }\n\n    public record Result(Map<String, Object> event, List<TriggerEntry> triggers) {\n\n        private Result(Map<String, Object> event, TriggerEntry trigger) {\n            this(event, List.of(trigger));\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/events/github/GithubUtils.java",
    "content": "package com.walmartlabs.concord.server.events.github;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport com.walmartlabs.concord.server.org.triggers.TriggerEntry;\n\nimport java.net.URI;\nimport java.util.Objects;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic final class GithubUtils {\n\n    /**\n     * Same rules as used by git in shorten_unambiguous_ref\n     * see: https://github.com/git/git/blob/v2.19.1/refs.c#L483\n     */\n    private static final Pattern[] REF_PARSE_RULES = {\n            Pattern.compile(\"^(.*)$\"),\n            Pattern.compile(\"^refs/(.*)$\"),\n            Pattern.compile(\"^refs/tags/(.*)$\"),\n            Pattern.compile(\"^refs/heads/(.*)$\"),\n            Pattern.compile(\"^refs/remotes/(.*)$\"),\n            Pattern.compile(\"^refs/remotes/(.*)/HEAD$\")\n    };\n\n    public static String getRefShortName(String ref) {\n        String str = ref.trim();\n        String result = str;\n        for (Pattern p : REF_PARSE_RULES) {\n            Matcher m = p.matcher(str);\n            if (m.matches()) {\n                result = m.group(1);\n            }\n        }\n        return result;\n    }\n\n    public static String getRepositoryName(String repoUrl) {\n        GithubRepoInfo info = getRepositoryInfo(repoUrl);\n        if (info == null) {\n            return null;\n        }\n        return info.owner() + \"/\" + info.name();\n    }\n\n    public static GithubRepoInfo getRepositoryInfo(String repoUrl) {\n        String repoPath = getRepoPath(repoUrl);\n\n        String[] u = repoPath.split(\"/\");\n        if (u.length < 2) {\n            // a file path perhaps?\n            return null;\n        }\n\n        return GithubRepoInfo.builder()\n                .owner(owner(u[0]))\n                .name(name(u[1]))\n                .build();\n    }\n\n    /**\n     * Returns true if the specified event and its payload is an \"empty\" {@code push} event.\n     */\n    public static boolean isEmptyPush(String eventName, Payload payload) {\n        if (!Constants.PUSH_EVENT.equals(eventName)) {\n            return false;\n        }\n\n        return Objects.equals(payload.raw().get(\"after\"), payload.raw().get(\"before\"));\n    }\n\n    /**\n     * Returns the value of the trigger's {@code ignoreEmptyPush} parameter\n     * or {@code true} if it is not defined.\n     */\n    public static boolean ignoreEmptyPush(TriggerEntry triggerEntry) {\n        return MapUtils.getBoolean(triggerEntry.getCfg(), Constants.IGNORE_EMPTY_PUSH_KEY, true);\n    }\n\n    public static URI getSenderUrl(Payload p) {\n        Object rawUrl = ConfigurationUtils.get(p.raw(), \"sender\", \"url\");\n\n        if (rawUrl instanceof String url) {\n            return URI.create(url);\n        } else {\n            throw new IllegalArgumentException(\"Invalid url info url: \" + rawUrl);\n        }\n\n    }\n\n    public static URI getRepoCloneUrl(Payload p) {\n        Object rawUrl = ConfigurationUtils.get(p.raw(), \"repository\", \"clone_url\");\n\n        if (rawUrl instanceof String url) {\n            return URI.create(url);\n        } else {\n            throw new IllegalArgumentException(\"Invalid repository clone url: \" + rawUrl);\n        }\n    }\n\n    private static String getRepoPath(String repoUrl) {\n        // tests support\n        if (repoUrl.startsWith(\"/\")) {\n            String[] folders = repoUrl.split(\"/\");\n            if (folders.length < 2) {\n                return repoUrl;\n            }\n            return folders[folders.length - 2] + \"/\" + folders[folders.length - 1];\n        }\n\n        String u = removeSchema(repoUrl);\n        u = removeHost(u);\n        return u;\n    }\n\n    private static String removeSchema(String repoUrl) {\n        int index = repoUrl.indexOf(\"://\");\n        if (index > 0) {\n            return repoUrl.substring(index + \"://\".length());\n        }\n        index = repoUrl.indexOf(\"@\");\n        if (index > 0) {\n            return repoUrl.substring(index + \"@\".length());\n        }\n        return repoUrl;\n    }\n\n    private static String removeHost(String repoUrl) {\n        int index = repoUrl.indexOf(\":\");\n        if (index > 0) {\n            int portEndIndex = repoUrl.indexOf(\"/\", index);\n            if (portEndIndex > 0) {\n                String port = repoUrl.substring(index + 1, portEndIndex);\n                if (isPort(port)) {\n                    return repoUrl.substring(portEndIndex + \"/\".length());\n                }\n            }\n            return repoUrl.substring(index + \":\".length());\n        }\n        index = repoUrl.indexOf(\"/\");\n        if (index > 0) {\n            return repoUrl.substring(index + \"/\".length());\n        }\n        return repoUrl;\n    }\n\n    private static String name(String str) {\n        return str.replaceAll(\"^\\\\W+|\\\\.git$\", \"\");\n    }\n\n    private static String owner(String str) {\n        int idx = str.indexOf(':');\n        if (idx > 0) {\n            return str.substring(idx + 1);\n        }\n        return str;\n    }\n\n    private static boolean isPort(String str) {\n        try {\n            int port = Integer.parseInt(str);\n            return port > 0 && port <= 65535;\n        } catch (NumberFormatException e) {\n            return false;\n        }\n    }\n\n    private GithubUtils() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/events/github/Payload.java",
    "content": "package com.walmartlabs.concord.server.events.github;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.sdk.MapUtils;\n\nimport java.net.URI;\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\nimport static com.walmartlabs.concord.server.events.github.Constants.*;\n\npublic class Payload {\n\n    /**\n     * List of supported repository-level events.\n     */\n    private static final Set<String> REPOSITORY_EVENTS = Set.of(\n            \"check_run\",\n            \"check_suite\",\n            \"code_scanning_alert\",\n            \"commit_comment\",\n            \"create\",\n            \"delete\",\n            \"dependabot_alert\",\n            \"fork\",\n            \"issue_comment\",\n            \"issues\",\n            \"label\",\n            \"member\",\n            \"pull_request\",\n            \"pull_request_review\",\n            \"pull_request_review_comment\",\n            \"push\",\n            \"release\",\n            \"repository\",\n            \"secret_scanning_alert\",\n            \"secret_scanning_alert_location\",\n            \"star\",\n            \"status\",\n            \"team\",\n            \"team_add\",\n            \"watch\",\n            \"workflow_dispatch\",\n            \"workflow_job\",\n            \"workflow_run\"\n    );\n\n    /**\n     * List of supported organization-level events.\n     */\n    private static final Set<String> ORGANIZATION_EVENTS = Set.of(\n            \"membership\",\n            \"organization\",\n            \"org_block\"\n    );\n\n    public static Payload from(String eventName, Map<String, Object> data) {\n        if (data == null) {\n            return null;\n        }\n\n        String fullRepoName = null;\n        String org = null;\n        String repo = null;\n\n        if (REPOSITORY_EVENTS.contains(eventName)) {\n            Map<String, Object> m = MapUtils.getMap(data, REPO_NAME_KEY, Map.of());\n            fullRepoName = MapUtils.getString(m, \"full_name\");\n\n            if (fullRepoName != null) {\n                String[] as = fullRepoName.split(\"/\");\n                if (as.length < 2) {\n                    return null;\n                }\n\n                org = as[0];\n                repo = as[1];\n            }\n        } else if (ORGANIZATION_EVENTS.contains(eventName)) {\n            Map<String, Object> m = MapUtils.getMap(data, ORGANIZATION_KEY, Map.of());\n            org = MapUtils.getString(m, \"login\");\n        } else {\n            return null;\n        }\n\n        return new Payload(eventName, fullRepoName, org, repo, data);\n    }\n\n    private final String eventName;\n    private final Map<String, Object> data;\n    private final String fullRepoName;\n    private final String org;\n    private final String repo;\n\n    protected Payload(String eventName, String fullRepoName, String org, String repo, Map<String, Object> data) {\n        this.eventName = eventName;\n        this.data = data;\n        this.fullRepoName = fullRepoName;\n        this.org = org;\n        this.repo = repo;\n    }\n\n    public String getHost() {\n        try {\n            Map<String, Object> repository = MapUtils.getMap(data, REPO_NAME_KEY, Map.of());\n            String url = MapUtils.getString(repository, \"git_url\");\n            return new URI(url).getHost();\n        } catch (Exception e) {\n            return null;\n        }\n    }\n\n    public String getFullRepoName() {\n        return fullRepoName;\n    }\n\n    public String getOrg() {\n        return org;\n    }\n\n    public String getRepo() {\n        return repo;\n    }\n\n    public String eventName() {\n        return eventName;\n    }\n\n    public String getBranch() {\n        switch (eventName.toLowerCase()) {\n            case PUSH_EVENT:\n            case \"create\":\n            case \"delete\":\n                return getRef(data);\n            case PULL_REQUEST_EVENT:\n            case \"pull_request_review\":\n            case \"pull_request_review_comment\":\n                return getBranchPullRequest(data);\n            default:\n                return null;\n        }\n    }\n\n    public String getHead() {\n        switch (eventName.toLowerCase()) {\n            case PUSH_EVENT:\n            case \"create\":\n            case \"delete\":\n                return getRef(data);\n            case PULL_REQUEST_EVENT:\n            case \"pull_request_review\":\n            case \"pull_request_review_comment\":\n                return getPullRequestHead(data);\n            default:\n                return null;\n        }\n    }\n\n    public Map<String, Set<String>> getFiles() {\n        if (!PUSH_EVENT.equalsIgnoreCase(eventName)) {\n            return Map.of();\n        }\n\n        List<Map<String, Object>> commits = MapUtils.getList(data, \"commits\", List.of());\n        Map<String, Set<String>> files = new HashMap<>();\n        for (Map<String, Object> c : commits) {\n            append(c, \"added\", files);\n            append(c, \"removed\", files);\n            append(c, \"modified\", files);\n        }\n        return files;\n    }\n\n    public boolean isPullRequestFromDifferentRepo() {\n        Map<String, Object> pullRequest = getPullRequestAttribute(this.raw());\n        String baseCloneUrl = getPullRequestCloneUrl(pullRequest, BASE_KEY);\n        String headCloneUrl = getPullRequestCloneUrl(pullRequest, HEAD_KEY);\n\n        return !Objects.equals(baseCloneUrl, headCloneUrl);\n    }\n\n    /**\n     * @return <code>true</code> when event contains `pull_request` attribute.<br/>\n     *         NOTE: this does <em>not</em> indicate the payload is from a <code>pull_request</code>\n     *         event. It may be from another event related to a pull request such as\n     *         <code>pull_request_review</code> or <code>pull_request_review_comment</code>\n     */\n    public boolean hasPullRequestEntry() {\n        return raw().containsKey(PULL_REQUEST_EVENT);\n    }\n\n    public String getUrl(String attribute) {\n        Map<String, Object> m = MapUtils.getMap(raw(), attribute, Map.of());\n        return MapUtils.getString(m, URL_KEY);\n    }\n\n    /**\n     * @return graphql node id for the given attribute\n     */\n    public String getNodeId(String attribute) {\n        Map<String, Object> m = MapUtils.getMap(raw(), attribute, Map.of());\n        return MapUtils.getString(m, NODE_ID_KEY);\n    }\n\n    public String getPullRequestBaseUrl() {\n        Map<String, Object> pullRequest = getPullRequestAttribute(this.raw());\n        return getPullRequestCloneUrl(pullRequest, BASE_KEY);\n    }\n\n    public String getPullRequestHeadUrl() {\n        Map<String, Object> pullRequest = getPullRequestAttribute(this.raw());\n        return getPullRequestCloneUrl(pullRequest, HEAD_KEY);\n    }\n\n    private static void append(Map<String, Object> c, String name, Map<String, Set<String>> result) {\n        List<String> value = MapUtils.getList(c, name, List.of());\n        result.compute(name, (k, v) -> (v == null) ? new HashSet<>(value) : Stream.concat(v.stream(), value.stream()).collect(Collectors.toSet()));\n    }\n\n    public String getSender() {\n        Map<String, Object> sender = MapUtils.getMap(data, \"sender\", Map.of());\n        return MapUtils.getString(sender, \"login\");\n    }\n\n    public String getSenderLdapDn() {\n        Object result = ConfigurationUtils.get(data, \"sender\", \"ldap_dn\");\n        if (result instanceof String s) {\n            return s;\n        }\n        return null;\n    }\n\n    public String getAction() {\n        return getString(\"action\");\n    }\n\n    public String getString(String key) {\n        return MapUtils.getString(data, key);\n    }\n\n    public Map<String, Object> raw() {\n        return data;\n    }\n\n    private static String getRef(Map<String, Object> event) {\n        String ref = MapUtils.getString(event, REF_KEY);\n        if (ref == null) {\n            return null;\n        }\n\n        return GithubUtils.getRefShortName(ref);\n    }\n\n    private static String getPullRequestHead(Map<String, Object> event) {\n        Map<String, Object> pr = MapUtils.getMap(event, PULL_REQUEST_EVENT, Map.of());\n        Map<String, Object> base = MapUtils.getMap(pr, HEAD_KEY, Map.of());\n        return MapUtils.getString(base, REF_KEY);\n    }\n\n    private static String getBranchPullRequest(Map<String, Object> event) {\n        Map<String, Object> pr = MapUtils.getMap(event, PULL_REQUEST_EVENT, Map.of());\n        Map<String, Object> base = MapUtils.getMap(pr, BASE_KEY, Map.of());\n        return MapUtils.getString(base, REF_KEY);\n    }\n\n    private static Map<String, Object> getPullRequestAttribute(Map<String, Object> event) {\n        return MapUtils.getMap(event, PULL_REQUEST_EVENT, Map.of());\n    }\n\n    private static String getPullRequestCloneUrl(Map<String, Object> pullRequest, String baseOrHead) {\n        Map<String, Object> head = MapUtils.getMap(pullRequest, baseOrHead, Map.of());\n        Map<String, Object> headRepo = MapUtils.getMap(head, \"repo\", Map.of());\n        return MapUtils.getString(headRepo, \"clone_url\", \"\");\n    }\n\n    @Override\n    public String toString() {\n        return \"Payload{\" +\n                \"eventName='\" + eventName + '\\'' +\n                \", data=\" + data +\n                \", fullRepoName='\" + fullRepoName + '\\'' +\n                \", org='\" + org + '\\'' +\n                \", repo='\" + repo + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/message/MessageChannel.java",
    "content": "package com.walmartlabs.concord.server.message;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport com.walmartlabs.concord.server.queueclient.message.Message;\nimport com.walmartlabs.concord.server.queueclient.message.MessageType;\n\nimport java.io.Closeable;\nimport java.util.Optional;\n\n/**\n * Represents an open message channel between the server and a remote agent.\n */\npublic interface MessageChannel extends Closeable {\n\n    /**\n     * A unique identifier of a channel.\n     */\n    String getChannelId();\n\n    /**\n     * An identifier of the agent represented by this MessageChannel.\n     */\n    String getAgentId();\n\n    /**\n     * Attempts to send a message.\n     * @return {@code true} if the message was sent successfully.\n     * Returns {@code false} if the message cannot be sent at the moment.\n     * @apiNote The implementors must expect the server to re-attempt\n     * the sending of the same message.\n     */\n    boolean offerMessage(Message msg) throws Exception;\n\n    /**\n     * Attempts to grab a received message.\n     * @return an empty value if no messages of the specified type can be returned\n     * at the moment.\n     */\n    Optional<Message> getMessage(MessageType messageType) throws Exception;\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/message/MessageChannelManager.java",
    "content": "package com.walmartlabs.concord.server.message;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.queueclient.message.Message;\nimport com.walmartlabs.concord.server.queueclient.message.MessageType;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.concurrent.ConcurrentHashMap;\n\npublic class MessageChannelManager {\n\n    private static final Logger log = LoggerFactory.getLogger(MessageChannelManager.class);\n\n    private final Map<String, MessageChannel> channels = new ConcurrentHashMap<>();\n\n    private volatile boolean isShutdown;\n\n    public boolean isShutdown() {\n        return isShutdown;\n    }\n\n    public void shutdown() {\n        isShutdown = true;\n\n        channels.forEach((uuid, channel) -> {\n            try {\n                channel.close();\n            } catch (Exception e) {\n                log.warn(\"shutdown -> failed on channel {}: {}\", channel.getClass(), e.getMessage());\n            }\n        });\n        log.info(\"shutdown -> done\");\n    }\n\n    public void close(String channelId) {\n        MessageChannel channel = channels.remove(channelId);\n        if (channel == null) {\n            log.warn(\"close ['{}'] -> channel not found\", channelId);\n            return;\n        }\n\n        try {\n            channel.close();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n\n        log.info(\"close ['{}'] -> done\", channelId);\n    }\n\n    public boolean sendMessage(String channelId, Message response) {\n        MessageChannel channel = channels.get(channelId);\n        if (channel == null) {\n            log.warn(\"sendResponse ['{}', '{}'] -> channel not found\", channelId, response);\n            return false;\n        }\n\n        try {\n            return channel.offerMessage(response);\n        } catch (Exception e) {\n            log.warn(\"sendResponse ['{}', '{}'] -> failed: {}\", channelId, response, e.getMessage());\n            return false;\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public <M extends Message> Map<MessageChannel, M> getRequests(MessageType requestType) {\n        Map<MessageChannel, M> result = new HashMap<>();\n        channels.forEach((channelId, channel) -> {\n            try {\n                channel.getMessage(requestType).ifPresent(msg -> {\n                    result.put(channel, (M) msg);\n                });\n            } catch (Exception e) {\n                log.warn(\"getRequests ['{}'] -> failed on channel {}: {}\", requestType, channel.getClass(), e.getMessage());\n            }\n        });\n        return result;\n    }\n\n    public void add(MessageChannel channel) {\n        channels.put(channel.getChannelId(), channel);\n    }\n\n    public int connectedClientsCount() {\n        return channels.size();\n    }\n\n    public Map<String, MessageChannel> getChannels() {\n        return Collections.unmodifiableMap(channels);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public <C extends MessageChannel> Optional<C> getChannel(String channelId, Class<C> klass) {\n        return Optional.ofNullable(channels.get(channelId))\n                .filter(c -> klass.isAssignableFrom(c.getClass()))\n                .map(c -> (C) c);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/metrics/FailedTaskError.java",
    "content": "package com.walmartlabs.concord.server.metrics;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.time.OffsetDateTime;\n\npublic class FailedTaskError {\n\n    private final String taskId;\n    private final String taskError;\n    private final OffsetDateTime taskErrorAt;\n\n    public FailedTaskError(String taskId, String taskError, OffsetDateTime taskErrorAt) {\n        this.taskId = taskId;\n        this.taskError = taskError;\n        this.taskErrorAt = taskErrorAt;\n    }\n\n    public String getTaskId() {\n        return taskId;\n    }\n\n    public String getTaskError() {\n        return taskError;\n    }\n\n    public OffsetDateTime getTaskErrorAt() {\n        return taskErrorAt;\n    }\n\n    @Override\n    public String toString() {\n        return \"FailedTaskError{\" +\n                \"taskId='\" + taskId + '\\'' +\n                \", taskError='\" + taskError + '\\'' +\n                \", taskErrorAt=\" + taskErrorAt +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/metrics/FailedTaskMetrics.java",
    "content": "package com.walmartlabs.concord.server.metrics;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.BackgroundTask;\nimport com.walmartlabs.concord.server.task.SchedulerDao;\nimport io.prometheus.client.Collector;\nimport io.prometheus.client.CollectorRegistry;\nimport io.prometheus.client.GaugeMetricFamily;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\n\npublic class FailedTaskMetrics implements BackgroundTask {\n\n    private static final Logger log = LoggerFactory.getLogger(FailedTaskMetrics.class);\n\n    private final Collector collector;\n\n    @Inject\n    public FailedTaskMetrics(SchedulerDao schedulerDao){\n        collector = new Collector() {\n            @Override\n            public List<MetricFamilySamples> collect() {\n                Collection<FailedTaskError> data = schedulerDao.pollErrored();\n                GaugeMetricFamily f = new GaugeMetricFamily(\"failed_tasks\", \"Errors for failed server tasks\", Arrays.asList(\"task_id\", \"task_error\", \"task_error_at\"));\n                data.forEach( k ->\n                    f.addMetric(Arrays.asList(k.getTaskId(), k.getTaskError(), k.getTaskErrorAt().toString()), 1L)\n                );\n                return Collections.singletonList(f);\n            }\n        };\n    }\n\n    @Override\n    public void start() {\n        CollectorRegistry.defaultRegistry.register(collector);\n    }\n\n    @Override\n    public void stop() {\n        try {\n            CollectorRegistry.defaultRegistry.unregister(collector);\n        } catch (Exception e) {\n            log.warn(\"stop -> error while unregistering the collector: {}\", e.getMessage());\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/metrics/JettySessionMetricsModule.java",
    "content": "package com.walmartlabs.concord.server.metrics;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.Gauge;\nimport com.google.inject.AbstractModule;\nimport com.google.inject.multibindings.Multibinder;\nimport com.walmartlabs.concord.server.sdk.metrics.GaugeProvider;\n\nimport javax.management.MBeanServer;\nimport javax.management.ObjectName;\nimport java.lang.management.ManagementFactory;\n\npublic class JettySessionMetricsModule extends AbstractModule {\n\n    private static final String[] ATTRIBUTES = {\n            \"sessionsCurrent\",\n            \"sessionsMax\",\n            \"sessionsTotal\"\n    };\n\n    @Override\n    protected void configure() {\n        Multibinder<GaugeProvider> tasks = Multibinder.newSetBinder(binder(), GaugeProvider.class);\n        for (String a : ATTRIBUTES) {\n            tasks.addBinding().toInstance(attribute(a));\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static <T> GaugeProvider<T> attribute(String attribute) {\n        return new GaugeProvider<T>() {\n            @Override\n            public String name() {\n                return \"jetty-\" + attribute;\n            }\n\n            @Override\n            public Gauge<T> gauge() {\n                return () -> (T) getAttribute(attribute);\n            }\n        };\n    }\n\n    private static Object getAttribute(String attribute) {\n        try {\n            MBeanServer mBeans = ManagementFactory.getPlatformMBeanServer();\n            return mBeans.getAttribute(new ObjectName(\"org.eclipse.jetty.session:context=ROOT,id=0,type=defaultsessioncache\"), attribute);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/metrics/JettyStatisticsModule.java",
    "content": "package com.walmartlabs.concord.server.metrics;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.Gauge;\nimport com.google.inject.AbstractModule;\nimport com.google.inject.multibindings.Multibinder;\nimport com.walmartlabs.concord.server.sdk.metrics.GaugeProvider;\n\nimport javax.management.MBeanServer;\nimport javax.management.ObjectName;\nimport java.lang.management.ManagementFactory;\n\npublic class JettyStatisticsModule extends AbstractModule {\n\n    private static final String[] ATTRIBUTES = {\n            \"responses1xx\",\n            \"responses2xx\",\n            \"responses3xx\",\n            \"responses4xx\",\n            \"responses5xx\",\n\n            \"requestsActive\",\n            \"requestTimeMax\",\n            \"requestTimeMean\"\n    };\n\n    @Override\n    protected void configure() {\n        Multibinder<GaugeProvider> tasks = Multibinder.newSetBinder(binder(), GaugeProvider.class);\n        for (String a : ATTRIBUTES) {\n            tasks.addBinding().toInstance(attribute(a));\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static <T> GaugeProvider<T> attribute(String attribute) {\n        return new GaugeProvider<T>() {\n            @Override\n            public String name() {\n                return \"jetty-\" + attribute;\n            }\n\n            @Override\n            public Gauge<T> gauge() {\n                return () -> (T) getAttribute(attribute);\n            }\n        };\n    }\n\n    private static Object getAttribute(String attribute) {\n        try {\n            MBeanServer mBeans = ManagementFactory.getPlatformMBeanServer();\n            return mBeans.getAttribute(new ObjectName(\"org.eclipse.jetty.server.handler:type=statisticshandler,id=0\"), attribute);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/metrics/MetricInterceptor.java",
    "content": "package com.walmartlabs.concord.server.metrics;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.MetricRegistry;\nimport com.codahale.metrics.Timer;\nimport com.google.common.base.Suppliers;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport org.aopalliance.intercept.MethodInterceptor;\nimport org.aopalliance.intercept.MethodInvocation;\n\nimport javax.inject.Inject;\nimport javax.inject.Provider;\nimport java.lang.reflect.Method;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.function.Supplier;\n\npublic class MetricInterceptor implements MethodInterceptor {\n\n    private final Map<Method, String> timerNameCache = new ConcurrentHashMap<>();\n\n    @Inject\n    private Provider<MetricRegistry> registryProvider;\n\n    // because we had to use very late binding for MetricRegistry, let's cache the result\n    private final Supplier<MetricRegistry> registrySupplier = Suppliers.memoize(() -> this.registryProvider.get());\n\n    @Override\n    public Object invoke(MethodInvocation invocation) throws Throwable {\n        MetricRegistry registry = registrySupplier.get();\n\n        Timer t = registry.timer(timerName(invocation.getMethod()));\n        Timer.Context ctx = t.time();\n        try {\n            return invocation.proceed();\n        } finally {\n            ctx.stop();\n        }\n    }\n\n    private String timerName(Method m) {\n        return timerNameCache.computeIfAbsent(m, k -> {\n            WithTimer t = m.getAnnotation(WithTimer.class);\n            return MetricUtils.createFqn(\"timer\", m.getDeclaringClass(), m.getName(), t.suffix());\n        });\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/metrics/MetricModule.java",
    "content": "package com.walmartlabs.concord.server.metrics;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.MetricRegistry;\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.google.inject.matcher.Matchers;\nimport com.walmartlabs.concord.server.sdk.metrics.GaugeProvider;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\nimport static com.walmartlabs.concord.server.Utils.bindServletHolder;\nimport static com.walmartlabs.concord.server.Utils.bindSingletonBackgroundTask;\nimport static com.walmartlabs.concord.server.metrics.NoSyntheticMethodMatcher.INSTANCE;\n\npublic class MetricModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        // common\n\n        newSetBinder(binder, GaugeProvider.class);\n\n        // registry\n\n        binder.bind(MetricRegistry.class).toProvider(MetricRegistryProvider.class).in(SINGLETON);\n\n        // @WithTimer stuff\n\n        MetricInterceptor i = new MetricInterceptor();\n        binder.requestInjection(i);\n\n        binder.bindInterceptor(Matchers.any(), INSTANCE.and(Matchers.annotatedWith(WithTimer.class)), i);\n\n        binder.bindListener(Matchers.any(), new MetricTypeListener());\n\n        // tasks\n\n        bindSingletonBackgroundTask(binder, FailedTaskMetrics.class);\n        bindSingletonBackgroundTask(binder, WorkerMetrics.class);\n        bindSingletonBackgroundTask(binder, MetricsRegistrator.class);\n\n        // the /metrics endpoint\n\n        bindServletHolder(binder, MetricsServletHolder.class);\n\n        // Jetty stuff\n\n        binder.install(new JettyStatisticsModule());\n        binder.install(new JettySessionMetricsModule());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/metrics/MetricRegistryProvider.java",
    "content": "package com.walmartlabs.concord.server.metrics;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.LockFreeExponentiallyDecayingReservoir;\nimport com.codahale.metrics.MetricRegistry;\nimport com.codahale.metrics.Timer;\n\nimport javax.inject.Provider;\n\npublic class MetricRegistryProvider implements Provider<MetricRegistry> {\n\n    @Override\n    public MetricRegistry get() {\n        return new MetricRegistry() {\n\n            @Override\n            public Timer timer(String name) {\n                return super.timer(name, () -> new Timer(LockFreeExponentiallyDecayingReservoir.builder().build()));\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/metrics/MetricTypeListener.java",
    "content": "package com.walmartlabs.concord.server.metrics;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.Counter;\nimport com.codahale.metrics.Meter;\nimport com.codahale.metrics.MetricRegistry;\nimport com.google.inject.Injector;\nimport com.google.inject.MembersInjector;\nimport com.google.inject.Provider;\nimport com.google.inject.TypeLiteral;\nimport com.google.inject.spi.TypeEncounter;\nimport com.google.inject.spi.TypeListener;\nimport com.walmartlabs.concord.server.sdk.metrics.InjectCounter;\nimport com.walmartlabs.concord.server.sdk.metrics.InjectMeter;\n\nimport java.lang.reflect.Field;\n\npublic class MetricTypeListener implements TypeListener {\n\n    @Override\n    public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {\n        Class<?> clazz = type.getRawType();\n        while (clazz != null) {\n            for (Field f : clazz.getDeclaredFields()) {\n                processMeters(encounter, clazz, f);\n                processCounters(encounter, clazz, f);\n            }\n\n            clazz = clazz.getSuperclass();\n        }\n    }\n\n    private static <I> void processMeters(TypeEncounter<I> encounter, Class<?> clazz, Field f) {\n        InjectMeter i = f.getAnnotation(InjectMeter.class);\n        if (f.getType() != Meter.class || i == null) {\n            return;\n        }\n\n        String name = i.value();\n        if (name.isEmpty()) {\n            name = f.getName();\n        }\n\n        String fqn = MetricUtils.createFqn(\"meter\", clazz, name, null);\n\n        Provider<Injector> injector = encounter.getProvider(Injector.class);\n        encounter.register((MembersInjector<I>) instance -> {\n            MetricRegistry registry = injector.get().getInstance(MetricRegistry.class);\n            set(f, instance, registry.meter(fqn));\n        });\n    }\n\n    private static <I> void processCounters(TypeEncounter<I> encounter, Class<?> clazz, Field f) {\n        InjectCounter i = f.getAnnotation(InjectCounter.class);\n        if (f.getType() != Counter.class || i == null) {\n            return;\n        }\n\n        String name = i.value();\n        if (name.isEmpty()) {\n            name = f.getName();\n        }\n\n        String fqn = MetricUtils.createFqn(\"counter\", clazz, name, null);\n\n        Provider<Injector> injector = encounter.getProvider(Injector.class);\n        encounter.register((MembersInjector<I>) instance -> {\n            MetricRegistry registry = injector.get().getInstance(MetricRegistry.class);\n            set(f, instance, registry.counter(fqn));\n        });\n    }\n\n    private static void set(Field f, Object i, Object v) {\n        try {\n            boolean accessible = f.isAccessible();\n            f.setAccessible(true);\n            f.set(i, v);\n            f.setAccessible(accessible);\n        } catch (IllegalAccessException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/metrics/MetricUtils.java",
    "content": "package com.walmartlabs.concord.server.metrics;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.Timer;\n\npublic class MetricUtils {\n\n    private static final String SHARED_PREFIX = \"com.walmartlabs.concord.\";\n\n    public static String createFqn(String type, Class<?> owner, String name, String suffix) {\n        String n = owner.getName();\n        if (n.startsWith(SHARED_PREFIX)) {\n            n = n.substring(SHARED_PREFIX.length());\n        }\n        return type + \",\" + n + \".\" + name + (suffix != null ? suffix : \"\");\n    }\n\n    public static void withTimer(Timer timer, Runnable r) {\n        Timer.Context t = timer.time();\n        try {\n            r.run();\n        } finally {\n            t.stop();\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/metrics/MetricsRegistrator.java",
    "content": "package com.walmartlabs.concord.server.metrics;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.MetricRegistry;\nimport com.walmartlabs.concord.server.sdk.BackgroundTask;\nimport com.walmartlabs.concord.server.sdk.metrics.GaugeProvider;\nimport io.prometheus.client.CollectorRegistry;\nimport io.prometheus.client.dropwizard.DropwizardExports;\nimport io.prometheus.client.hotspot.DefaultExports;\n\nimport javax.inject.Inject;\nimport java.util.Set;\n\npublic class MetricsRegistrator implements BackgroundTask {\n\n    private final MetricRegistry registry;\n    private final Set<GaugeProvider> gauges;\n\n    @Inject\n    public MetricsRegistrator(MetricRegistry registry, Set<GaugeProvider> gauges) {\n        this.registry = registry;\n        this.gauges = gauges;\n    }\n\n    @Override\n    public void start() {\n        // prometheus integration\n        CollectorRegistry.defaultRegistry.register(new DropwizardExports(registry));\n\n        // initialize standard prometheus exports (hotspot, memory, etc)\n        DefaultExports.initialize();\n\n        // register gauges\n        gauges.forEach(g -> registry.register(g.name(), g.gauge()));\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/metrics/MetricsServletHolder.java",
    "content": "package com.walmartlabs.concord.server.metrics;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport io.prometheus.client.exporter.MetricsServlet;\nimport org.eclipse.jetty.ee8.servlet.ServletHolder;\n\nimport javax.inject.Singleton;\nimport javax.servlet.annotation.WebServlet;\n\n@Singleton\n@WebServlet(\"/metrics\")\npublic class MetricsServletHolder extends ServletHolder {\n\n    public MetricsServletHolder() {\n        super(MetricsServlet.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/metrics/NoSyntheticMethodMatcher.java",
    "content": "package com.walmartlabs.concord.server.metrics;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.matcher.AbstractMatcher;\n\nimport java.lang.reflect.Method;\n\npublic class NoSyntheticMethodMatcher extends AbstractMatcher<Method> {\n\n    public static final NoSyntheticMethodMatcher INSTANCE = new NoSyntheticMethodMatcher();\n\n    private NoSyntheticMethodMatcher() {}\n\n    @Override\n    public boolean matches(Method method) {\n        return !method.isSynthetic();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/metrics/WorkerMetrics.java",
    "content": "package com.walmartlabs.concord.server.metrics;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.AgentWorkerUtils;\nimport com.walmartlabs.concord.server.agent.AgentManager;\nimport com.walmartlabs.concord.server.agent.AgentWorkerEntry;\nimport com.walmartlabs.concord.server.cfg.WorkerMetricsConfiguration;\nimport com.walmartlabs.concord.server.sdk.BackgroundTask;\nimport io.prometheus.client.Collector;\nimport io.prometheus.client.CollectorRegistry;\nimport io.prometheus.client.GaugeMetricFamily;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.util.*;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\n\npublic class WorkerMetrics implements BackgroundTask {\n\n    private static final Logger log = LoggerFactory.getLogger(WorkerMetrics.class);\n\n    private final Collector collector;\n\n    @Inject\n    public WorkerMetrics(WorkerMetricsConfiguration cfg, AgentManager agentManager) {\n        String prop = cfg.getGroupByCapabilitiesProperty();\n        String[] path = prop.split(\"\\\\.\");\n\n        // \"persist\" some of the metrics\n        // if some \"flavor\" of agents disappear, we would want to show zero in the metric instead of no metric at all\n        // we keep keys of all agent metrics in memory\n        Set<Object> keys = new HashSet<>();\n        Lock mutex = new ReentrantLock();\n\n        collector = new Collector() {\n            @Override\n            public List<MetricFamilySamples> collect() {\n                Collection<AgentWorkerEntry> data = agentManager.getAvailableAgents();\n\n                Map<Object, Long> m = new HashMap<>();\n\n                Map<Object, Long> currentData = AgentWorkerUtils.groupBy(data, path);\n                mutex.lock();\n                try {\n                    keys.addAll(currentData.keySet());\n                    keys.forEach(k -> m.put(k, 0L));\n                    m.putAll(currentData);\n                } finally {\n                    mutex.unlock();\n                }\n\n                GaugeMetricFamily f = new GaugeMetricFamily(\"available_workers\", \"number of available workers for the \" + prop, Collections.singletonList(\"prop\"));\n                m.forEach((k, v) -> {\n                    String label = k.toString().replace(\"/\", \"_\").replace(\"-\", \"_\");\n                    f.addMetric(Collections.singletonList(label), v);\n                });\n\n                return Collections.singletonList(f);\n            }\n        };\n    }\n\n    @Override\n    public void start() {\n        CollectorRegistry.defaultRegistry.register(collector);\n    }\n\n    @Override\n    public void stop() {\n        try {\n            CollectorRegistry.defaultRegistry.unregister(collector);\n        } catch (Exception e) {\n            log.warn(\"stop -> error while unregistering the collector: {}\", e.getMessage());\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/CreateOrganizationResponse.java",
    "content": "package com.walmartlabs.concord.server.org;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.server.OperationResult;\n\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@JsonInclude(Include.NON_NULL)\npublic class CreateOrganizationResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n    private final UUID id;\n    private final OperationResult result;\n\n    @JsonCreator\n    public CreateOrganizationResponse(@JsonProperty(\"id\") UUID id,\n                                      @JsonProperty(\"result\") OperationResult result) {\n        this.id = id;\n        this.result = result;\n    }\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public OperationResult getResult() {\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"CreateOrganizationResponse{\" +\n                \"ok=\" + ok +\n                \", id=\" + id +\n                \", result=\" + result +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/EntityOwner.java",
    "content": "package com.walmartlabs.concord.server.org;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.server.user.UserType;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableEntityOwner.class)\n@JsonDeserialize(as = ImmutableEntityOwner.class)\npublic interface EntityOwner extends Serializable {\n\n    @Nullable\n    UUID id();\n\n    @Nullable\n    String username();\n\n    @Nullable\n    String userDomain();\n\n    @Nullable\n    String displayName();\n\n    @Nullable\n    UserType userType();\n\n    static ImmutableEntityOwner.Builder builder() {\n        return ImmutableEntityOwner.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/OrganizationDao.java",
    "content": "package com.walmartlabs.concord.server.org;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.UuidGenerator;\nimport com.walmartlabs.concord.server.jooq.tables.Organizations;\nimport com.walmartlabs.concord.server.jooq.tables.Users;\nimport com.walmartlabs.concord.server.jooq.tables.records.OrganizationsRecord;\nimport com.walmartlabs.concord.server.org.team.TeamRole;\nimport com.walmartlabs.concord.server.user.UserType;\nimport org.jooq.*;\n\nimport javax.inject.Inject;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.V_USER_TEAMS;\nimport static com.walmartlabs.concord.server.jooq.tables.Organizations.ORGANIZATIONS;\nimport static com.walmartlabs.concord.server.jooq.tables.Teams.TEAMS;\nimport static com.walmartlabs.concord.server.jooq.tables.Users.USERS;\nimport static java.util.Objects.requireNonNull;\nimport static org.jooq.impl.DSL.*;\n\npublic class OrganizationDao extends AbstractDao {\n\n    private final ConcordObjectMapper objectMapper;\n    private final UuidGenerator uuidGenerator;\n\n    @Inject\n    public OrganizationDao(@MainDB Configuration cfg,\n                           ConcordObjectMapper objectMapper,\n                           UuidGenerator uuidGenerator) {\n\n        super(cfg);\n        this.objectMapper = requireNonNull(objectMapper);\n        this.uuidGenerator = requireNonNull(uuidGenerator);\n    }\n\n    @Override\n    protected void tx(Tx t) {\n        super.tx(t);\n    }\n\n    @Override\n    public <T> T txResult(TxResult<T> t) {\n        return super.txResult(t);\n    }\n\n    public OrganizationEntry get(UUID id) {\n        return get(dsl(), id);\n    }\n\n    public OrganizationEntry get(DSLContext tx, UUID id) {\n        Organizations o = ORGANIZATIONS.as(\"o\");\n        Users u = USERS.as(\"u\");\n\n        return tx.select(o.ORG_ID, o.ORG_NAME, o.OWNER_ID, u.USERNAME, u.DOMAIN, u.DISPLAY_NAME, u.USER_TYPE, o.VISIBILITY, o.META, o.ORG_CFG)\n                .from(o)\n                .leftJoin(u).on(u.USER_ID.eq(o.OWNER_ID))\n                .where(o.ORG_ID.eq(id))\n                .fetchOne(this::toEntry);\n    }\n\n    public UUID getId(String name) {\n        return getId(dsl(), name);\n    }\n\n    public UUID getId(DSLContext tx, String name) {\n        return tx.select(ORGANIZATIONS.ORG_ID)\n                .from(ORGANIZATIONS)\n                .where(ORGANIZATIONS.ORG_NAME.eq(name))\n                .fetchOne(ORGANIZATIONS.ORG_ID);\n    }\n\n    public OrganizationEntry getByName(String name) {\n        return getByName(dsl(), name);\n    }\n\n    public OrganizationEntry getByName(DSLContext tx, String name) {\n        Organizations o = ORGANIZATIONS.as(\"o\");\n        Users u = USERS.as(\"u\");\n\n        return tx.select(o.ORG_ID, o.ORG_NAME, o.OWNER_ID, u.USERNAME, u.DOMAIN, u.DISPLAY_NAME, u.USER_TYPE, o.VISIBILITY, o.META, o.ORG_CFG)\n                .from(o)\n                .leftJoin(u).on(u.USER_ID.eq(o.OWNER_ID))\n                .where(o.ORG_NAME.eq(name))\n                .fetchOne(this::toEntry);\n    }\n\n    public Map<String, Object> getConfiguration(UUID orgId) {\n        return dsl().select(ORGANIZATIONS.ORG_CFG)\n                .from(ORGANIZATIONS)\n                .where(ORGANIZATIONS.ORG_ID.eq(orgId))\n                .fetchOne(e -> objectMapper.fromJSONB(e.value1()));\n    }\n\n    public UUID insert(String name, UUID ownerId, OrganizationVisibility visibility, Map<String, Object> meta, Map<String, Object> cfg) {\n        return txResult(tx -> insert(tx, name, ownerId, visibility, meta, cfg));\n    }\n\n    public UUID insert(DSLContext tx, String name, UUID ownerId, OrganizationVisibility visibility, Map<String, Object> meta, Map<String, Object> cfg) {\n        if (visibility == null) {\n            visibility = OrganizationVisibility.PUBLIC;\n        }\n\n        UUID orgId = uuidGenerator.generate();\n\n        return tx.insertInto(ORGANIZATIONS)\n                .columns(ORGANIZATIONS.ORG_ID,\n                        ORGANIZATIONS.ORG_NAME,\n                        ORGANIZATIONS.OWNER_ID,\n                        ORGANIZATIONS.VISIBILITY,\n                        ORGANIZATIONS.META,\n                        ORGANIZATIONS.ORG_CFG)\n                .values(orgId,\n                        name,\n                        ownerId,\n                        visibility.toString(),\n                        objectMapper.toJSONB(meta),\n                        objectMapper.toJSONB(cfg))\n                .returning()\n                .fetchOne()\n                .getOrgId();\n    }\n\n    public void update(UUID id, String name, UUID ownerId, OrganizationVisibility visibility, Map<String, Object> meta, Map<String, Object> cfg) {\n        tx(tx -> update(tx, id, name, ownerId, visibility, meta, cfg));\n    }\n\n    public void update(DSLContext tx, UUID id, String name, UUID ownerId, OrganizationVisibility visibility, Map<String, Object> meta, Map<String, Object> cfg) {\n        UpdateQuery<OrganizationsRecord> q = tx.updateQuery(ORGANIZATIONS);\n\n        if (name != null) {\n            q.addValue(ORGANIZATIONS.ORG_NAME, name);\n        }\n\n        if (ownerId != null) {\n            q.addValue(ORGANIZATIONS.OWNER_ID, ownerId);\n        }\n\n        if (visibility != null) {\n            q.addValue(ORGANIZATIONS.VISIBILITY, visibility.toString());\n        }\n\n        if (meta != null) {\n            q.addValue(ORGANIZATIONS.META, objectMapper.toJSONB(meta));\n        }\n\n        if (cfg != null) {\n            q.addValue(ORGANIZATIONS.ORG_CFG, objectMapper.toJSONB(cfg));\n        }\n\n        q.addConditions(ORGANIZATIONS.ORG_ID.eq(id));\n        q.execute();\n    }\n\n    public List<OrganizationEntry> list(UUID currentUserId, boolean onlyCurrent, int offset, int limit, String filter) {\n        Organizations o = ORGANIZATIONS.as(\"o\");\n        Users u = USERS.as(\"u\");\n\n        SelectOnConditionStep<Record10<UUID, String, UUID, String, String, String, String, String, JSONB, JSONB>> q = dsl().select(o.ORG_ID,\n                        o.ORG_NAME,\n                        o.OWNER_ID,\n                        u.USERNAME,\n                        u.DOMAIN,\n                        u.DISPLAY_NAME,\n                        u.USER_TYPE,\n                        o.VISIBILITY,\n                        o.META,\n                        o.ORG_CFG)\n                .from(o)\n                .leftJoin(u).on(u.USER_ID.eq(o.OWNER_ID));\n\n        if (currentUserId != null) {\n            // public orgs are visible for anyone\n            // but show them only if onlyCurrent == false\n            // i.e. when the user specifically asked to show all available orgs\n            Condition isPublic = value(onlyCurrent).isFalse()\n                    .and(o.VISIBILITY.eq(OrganizationVisibility.PUBLIC.toString()));\n\n            // check if the user belongs to a team in an org\n            SelectConditionStep<Record1<UUID>> teamIds = select(TEAMS.TEAM_ID)\n                    .from(TEAMS)\n                    .where(TEAMS.ORG_ID.eq(o.ORG_ID));\n\n            Condition isInATeam = exists(selectOne().from(V_USER_TEAMS)\n                    .where(V_USER_TEAMS.USER_ID.eq(currentUserId)\n                            .and(V_USER_TEAMS.TEAM_ID.in(teamIds))));\n\n            // check if the user owns orgs\n            Condition ownsOrgs = o.OWNER_ID.eq(currentUserId);\n\n            // if any of those conditions true then the org must be visible\n            q.where(or(isPublic, isInATeam, ownsOrgs));\n        }\n\n        if (filter != null) {\n            q.where(o.ORG_NAME.containsIgnoreCase(filter));\n        }\n\n        if (offset >= 0) {\n            q.offset(offset);\n        }\n\n        if (limit > 0) {\n            q.limit(limit);\n        }\n\n        return q.orderBy(o.ORG_NAME)\n                .fetch(this::toEntry);\n    }\n\n    public boolean hasOwner(DSLContext tx, UUID orgId) {\n        return tx.select(ORGANIZATIONS.OWNER_ID).from(ORGANIZATIONS)\n                       .where(ORGANIZATIONS.ORG_ID.eq(orgId))\n                       .fetchOne() != null;\n    }\n\n    public boolean hasRole(DSLContext tx, UUID orgId, TeamRole role) {\n        SelectConditionStep<Record1<UUID>> teamIds = select(TEAMS.TEAM_ID).from(TEAMS).where(TEAMS.ORG_ID.eq(orgId));\n\n        return tx.fetchExists(select(V_USER_TEAMS.USER_ID)\n                .from(V_USER_TEAMS)\n                .where(V_USER_TEAMS.TEAM_ROLE.eq(role.toString())\n                        .and(V_USER_TEAMS.TEAM_ID.in(teamIds))));\n    }\n\n    public void delete(UUID orgId) {\n        tx(tx -> tx.deleteFrom(ORGANIZATIONS)\n                .where(ORGANIZATIONS.ORG_ID.eq(orgId))\n                .execute());\n    }\n\n    private OrganizationEntry toEntry(Record10<UUID, String, UUID, String, String, String, String, String, JSONB, JSONB> r) {\n        Map<String, Object> meta = objectMapper.fromJSONB(r.value9());\n        Map<String, Object> cfg = objectMapper.fromJSONB(r.value10());\n        return new OrganizationEntry(r.value1(), r.value2(),\n                toOwner(r.value3(), r.value4(), r.value5(), r.value6(), r.value7()),\n                OrganizationVisibility.valueOf(r.value8()), meta, cfg);\n    }\n\n    private EntityOwner toOwner(UUID id, String username, String domain, String displayName, String userType) {\n        if (id == null) {\n            return null;\n        }\n        return EntityOwner.builder()\n                .id(id)\n                .username(username)\n                .userDomain(domain)\n                .displayName(displayName)\n                .userType(UserType.valueOf(userType))\n                .build();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/OrganizationEntry.java",
    "content": "package com.walmartlabs.concord.server.org;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.Map;\nimport java.util.UUID;\n\n@JsonInclude(Include.NON_NULL)\npublic class OrganizationEntry implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final UUID id;\n\n    @ConcordKey\n    private final String name;\n\n    @Nullable\n    private final EntityOwner owner;\n\n    private final OrganizationVisibility visibility;\n\n    private final Map<String, Object> meta;\n\n    private final Map<String, Object> cfg;\n\n    public OrganizationEntry(String name) {\n        this(null, name, null, null, null, null);\n    }\n\n    public OrganizationEntry(String name, OrganizationVisibility visibility) {\n        this(null, name, null, visibility, null, null);\n    }\n\n    public OrganizationEntry(String name, Map<String, Object> meta) {\n        this(null, name, null, null, meta, null);\n    }\n\n    @JsonCreator\n    public OrganizationEntry(@JsonProperty(\"id\") UUID id,\n                             @JsonProperty(\"name\") String name,\n                             @JsonProperty(\"owner\") EntityOwner owner,\n                             @JsonProperty(\"visibility\") OrganizationVisibility visibility,\n                             @JsonProperty(\"meta\") Map<String, Object> meta,\n                             @JsonProperty(\"cfg\") Map<String, Object> cfg) {\n        this.id = id;\n        this.name = name;\n        this.owner = owner;\n        this.visibility = visibility;\n        this.meta = meta;\n        this.cfg = cfg;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    @Nullable\n    public EntityOwner getOwner() {\n        return owner;\n    }\n\n    public OrganizationVisibility getVisibility() {\n        return visibility;\n    }\n\n    public Map<String, Object> getMeta() {\n        return meta;\n    }\n\n    public Map<String, Object> getCfg() {\n        return cfg;\n    }\n\n    @Override\n    public String toString() {\n        return \"OrganizationEntry{\" +\n                \"id=\" + id +\n                \", name='\" + name + '\\'' +\n                \", owner=\" + owner +\n                \", visibility=\" + visibility +\n                \", meta=\" + meta +\n                \", cfg=\" + cfg +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/OrganizationManager.java",
    "content": "package com.walmartlabs.concord.server.org;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.Locks;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.audit.AuditAction;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.audit.AuditObject;\nimport com.walmartlabs.concord.server.org.project.DiffUtils;\nimport com.walmartlabs.concord.server.org.team.TeamDao;\nimport com.walmartlabs.concord.server.org.team.TeamManager;\nimport com.walmartlabs.concord.server.org.team.TeamRole;\nimport com.walmartlabs.concord.server.policy.EntityAction;\nimport com.walmartlabs.concord.server.policy.EntityType;\nimport com.walmartlabs.concord.server.policy.PolicyManager;\nimport com.walmartlabs.concord.server.policy.PolicyUtils;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.Permission;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.user.UserEntry;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport com.walmartlabs.concord.server.user.UserType;\nimport org.apache.shiro.authz.AuthorizationException;\nimport org.jooq.DSLContext;\n\nimport javax.inject.Inject;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic class OrganizationManager {\n\n    // as defined in com/walmartlabs/concord/server/db/0.48.0.xml\n    public static final UUID DEFAULT_ORG_ID = UUID.fromString(\"0fac1b18-d179-11e7-b3e7-d7df4543ed4f\");\n    public static final String DEFAULT_ORG_NAME = \"Default\";\n\n    private final PolicyManager policyManager;\n    private final OrganizationDao orgDao;\n    private final TeamDao teamDao;\n    private final UserManager userManager;\n    private final Locks locks;\n    private final AuditLog auditLog;\n\n    @Inject\n    public OrganizationManager(PolicyManager policyManager,\n                               OrganizationDao orgDao,\n                               TeamDao teamDao,\n                               UserManager userManager,\n                               Locks locks,\n                               AuditLog auditLog) {\n\n        this.policyManager = policyManager;\n        this.orgDao = orgDao;\n        this.teamDao = teamDao;\n        this.userManager = userManager;\n        this.locks = locks;\n        this.auditLog = auditLog;\n    }\n\n    /**\n     * Creates a new organization or updates an existing one.\n     * <p/>\n     * To update an existing organization, a {@link OrganizationEntry#getId()}\n     * value must be provided.\n     * <p/>\n     * When updating an existing organization, only non-null properties are updated.\n     * E.g. if you wish to update only the org's visibility, set only the ID and the\n     * visibility values.\n     *\n     * @apiNote the method uses DB advisory locks, it is thread-safe across the cluster.\n     */\n    public OrganizationOperationResult createOrUpdate(OrganizationEntry entry) {\n        return orgDao.txResult(tx -> createOrUpdate(tx, entry));\n    }\n\n    /**\n     * @see #createOrUpdate(OrganizationEntry)\n     */\n    public OrganizationOperationResult createOrUpdate(DSLContext tx, OrganizationEntry entry) {\n        // use advisory locks to avoid races\n        locks.lock(tx, \"OrganizationManager#createOrX\");\n\n        UUID orgId = entry.getId();\n        if (orgId == null) {\n            orgId = orgDao.getId(tx, entry.getName());\n        }\n\n        if (orgId == null) {\n            orgId = create(tx, entry);\n            return OrganizationOperationResult.builder()\n                    .orgId(orgId)\n                    .result(OperationResult.CREATED)\n                    .build();\n        } else {\n            update(tx, orgId, entry);\n            return OrganizationOperationResult.builder()\n                    .orgId(orgId)\n                    .result(OperationResult.UPDATED)\n                    .build();\n        }\n    }\n\n    public OrganizationOperationResult createOrGet(String orgName) {\n        return orgDao.txResult(tx -> createOrGet(tx, orgName));\n    }\n\n    public OrganizationOperationResult createOrGet(DSLContext tx, String orgName) {\n        // use advisory locks to avoid races\n        // TODO optimistic locking as a possible optimization\n        locks.lock(tx, \"OrganizationManager#createOrX\");\n\n        UUID orgId = orgDao.getId(tx, orgName);\n        if (orgId != null) {\n            return OrganizationOperationResult.builder()\n                    .orgId(orgId)\n                    .result(OperationResult.ALREADY_EXISTS)\n                    .build();\n        }\n\n        orgId = create(tx, new OrganizationEntry(orgName));\n        return OrganizationOperationResult.builder()\n                .orgId(orgId)\n                .result(OperationResult.CREATED)\n                .build();\n    }\n\n    private UUID create(DSLContext tx, OrganizationEntry entry) {\n        assertPermission(Permission.CREATE_ORG);\n\n        UserEntry owner = getOwner(entry.getOwner(), UserPrincipal.assertCurrent().getUser());\n\n        policyManager.checkEntity(null, null, EntityType.ORGANIZATION, EntityAction.CREATE, owner, PolicyUtils.orgToMap(entry));\n\n        UUID orgId = orgDao.insert(tx, entry.getName(), owner.getId(), entry.getVisibility(), entry.getMeta(), entry.getCfg());\n\n        // ...add the owner user into the default team of the new org\n        UUID teamId = teamDao.insert(tx, orgId, TeamManager.DEFAULT_TEAM_NAME, \"Default team\");\n        teamDao.upsertUser(tx, teamId, owner.getId(), TeamRole.OWNER);\n\n        Map<String, Object> changes = DiffUtils.compare(null, entry);\n        addAuditLog(AuditAction.CREATE,\n                orgId,\n                entry.getName(),\n                changes);\n\n        return orgId;\n    }\n\n    private void update(DSLContext tx, UUID orgId, OrganizationEntry entry) {\n        OrganizationEntry prevEntry = assertUpdateAccess(orgId);\n\n        UserEntry owner = getOwner(entry.getOwner(), null);\n\n        policyManager.checkEntity(orgId, null, EntityType.ORGANIZATION, EntityAction.UPDATE, owner, PolicyUtils.orgToMap(entry));\n\n        UUID ownerId = owner != null ? owner.getId() : null;\n        orgDao.update(tx, orgId, entry.getName(), ownerId, entry.getVisibility(), entry.getMeta(), entry.getCfg());\n\n        OrganizationEntry newEntry = orgDao.get(orgId);\n\n        Map<String, Object> changes = DiffUtils.compare(prevEntry, newEntry);\n        addAuditLog(\n                AuditAction.UPDATE,\n                prevEntry.getId(),\n                prevEntry.getName(),\n                changes);\n    }\n\n    public void delete(String orgName) {\n        assertAdmin();\n\n        OrganizationEntry org = assertExisting(null, orgName);\n\n        orgDao.delete(org.getId());\n\n        addAuditLog(\n                AuditAction.DELETE,\n                org.getId(),\n                org.getName(),\n                null);\n    }\n\n    public OrganizationEntry assertExisting(UUID orgId, String orgName) {\n        return orgDao.txResult(tx -> assertExisting(tx, orgId, orgName));\n    }\n\n    public OrganizationEntry assertExisting(DSLContext tx, UUID orgId, String orgName) {\n        if (orgId != null) {\n            OrganizationEntry e = orgDao.get(tx, orgId);\n            if (e == null) {\n                throw new ValidationErrorsException(\"Organization not found: \" + orgId);\n            }\n            return e;\n        }\n\n        if (orgName != null) {\n            OrganizationEntry e = orgDao.getByName(tx, orgName);\n            if (e == null) {\n                throw new ValidationErrorsException(\"Organization not found: \" + orgName);\n            }\n            return e;\n        }\n\n        throw new ValidationErrorsException(\"Organization ID or name is required\");\n    }\n\n    public OrganizationEntry assertAccess(UUID orgId, boolean orgMembersOnly) {\n        return assertAccess(orgId, null, orgMembersOnly);\n    }\n\n    public OrganizationEntry assertAccess(DSLContext tx, UUID orgId, boolean orgMembersOnly) {\n        return assertAccess(tx, orgId, null, orgMembersOnly);\n    }\n\n    public OrganizationEntry assertAccess(String orgName, boolean orgMembersOnly) {\n        return assertAccess(null, orgName, orgMembersOnly);\n    }\n\n    @WithTimer\n    public OrganizationEntry assertAccess(UUID orgId, String orgName, boolean orgMembersOnly) {\n        return orgDao.txResult(tx -> assertAccess(tx, orgId, orgName, orgMembersOnly));\n    }\n\n    @WithTimer\n    public OrganizationEntry assertAccess(DSLContext tx, UUID orgId, String orgName, boolean orgMembersOnly) {\n        OrganizationEntry e = assertExisting(tx, orgId, orgName);\n\n        if (Roles.isAdmin()) {\n            // an admin can access any organization\n            return e;\n        }\n\n        if (Roles.isGlobalReader() || Roles.isGlobalWriter()) {\n            return e;\n        }\n\n        UserPrincipal p = UserPrincipal.assertCurrent();\n\n        EntityOwner owner = e.getOwner();\n        if (ResourceAccessUtils.isSame(p, owner)) {\n            // the owner can do anything with his organization\n            return e;\n        }\n\n        if (orgMembersOnly) {\n            if (!userManager.isInOrganization(tx, e.getId())) {\n                throw new UnauthorizedException(\"The current user (\" + p.getUsername() + \") doesn't belong to the specified organization: \" + e.getName());\n            }\n        }\n\n        return e;\n    }\n\n    private OrganizationEntry assertUpdateAccess(UUID orgId) {\n        OrganizationEntry entry = assertExisting(orgId, null);\n\n        UserEntry owner = getOwner(entry.getOwner(), null);\n        UUID ownerId = owner != null ? owner.getId() : null;\n        UserPrincipal p = UserPrincipal.assertCurrent();\n        if (p.getId().equals(ownerId)) {\n            return entry;\n        }\n\n        assertPermission(Permission.UPDATE_ORG);\n\n        return entry;\n    }\n\n    private static void assertAdmin() {\n        if (!Roles.isAdmin()) {\n            throw new AuthorizationException(\"Only admins are allowed to update organizations\");\n        }\n    }\n\n    private static void assertPermission(Permission p) {\n        if (p.isPermitted()) {\n            return;\n        }\n\n        throw new AuthorizationException(\n                String.format(\"Only roles with '%s' permission are allowed to update organizations\", p.getKey())\n        );\n    }\n\n    private UserEntry getOwner(EntityOwner owner, UserEntry defaultOwner) {\n        if (owner == null) {\n            return defaultOwner;\n        }\n\n        if (owner.id() != null) {\n            return userManager.get(owner.id())\n                    .orElseThrow(() -> new ValidationErrorsException(\"User not found: \" + owner.id()));\n        }\n\n        if (owner.username() != null) {\n            UserType t = owner.userType() != null ? owner.userType() : UserPrincipal.assertCurrent().getType();\n            return userManager.get(owner.username(), owner.userDomain(), t)\n                    .orElseThrow(() -> new ConcordApplicationException(\"User not found: \" + owner.username()));\n        }\n\n        return defaultOwner;\n    }\n\n    private void addAuditLog(AuditAction auditAction, UUID orgId, String orgName, Map<String, Object> changes) {\n        auditLog.add(AuditObject.ORGANIZATION, auditAction)\n                .field(\"orgId\", orgId)\n                .field(\"name\", orgName)\n                .field(\"changes\", changes)\n                .log();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/OrganizationModule.java",
    "content": "package com.walmartlabs.concord.server.org;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.walmartlabs.concord.server.org.inventory.InventoryModule;\nimport com.walmartlabs.concord.server.org.jsonstore.JsonStoreModule;\nimport com.walmartlabs.concord.server.org.policy.PolicyModule;\nimport com.walmartlabs.concord.server.org.project.ProjectModule;\nimport com.walmartlabs.concord.server.org.secret.SecretModule;\nimport com.walmartlabs.concord.server.org.team.TeamModule;\nimport com.walmartlabs.concord.server.org.triggers.TriggersModule;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.walmartlabs.concord.server.Utils.bindJaxRsResource;\n\npublic class OrganizationModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        binder.bind(OrganizationDao.class).in(SINGLETON);\n        binder.bind(OrganizationManager.class).in(SINGLETON);\n\n        bindJaxRsResource(binder, OrganizationResource.class);\n        bindJaxRsResource(binder, ProjectProcessResource.class);\n\n        binder.install(new InventoryModule());\n        binder.install(new JsonStoreModule());\n        binder.install(new PolicyModule());\n        binder.install(new ProjectModule());\n        binder.install(new SecretModule());\n        binder.install(new TeamModule());\n        binder.install(new TriggersModule());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/OrganizationOperationResult.java",
    "content": "package com.walmartlabs.concord.server.org;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.server.OperationResult;\nimport org.immutables.value.Value;\n\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonSerialize(as = ImmutableOrganizationOperationResult.class)\n@JsonDeserialize(as = ImmutableOrganizationOperationResult.class)\npublic interface OrganizationOperationResult {\n\n    @Value.Default\n    default boolean ok() {\n        return true;\n    }\n\n    OperationResult result();\n\n    UUID orgId();\n\n    static ImmutableOrganizationOperationResult.Builder builder() {\n        return ImmutableOrganizationOperationResult.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/OrganizationResource.java",
    "content": "package com.walmartlabs.concord.server.org;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.server.GenericOperationResult;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.validation.Valid;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport java.util.List;\nimport java.util.UUID;\n\n@Path(\"/api/v1/org\")\n@Tag(name = \"Organizations\")\npublic class OrganizationResource implements Resource {\n\n    private final OrganizationDao orgDao;\n    private final OrganizationManager orgManager;\n\n    @Inject\n    public OrganizationResource(OrganizationDao orgDao, OrganizationManager orgManager) {\n        this.orgDao = orgDao;\n        this.orgManager = orgManager;\n    }\n\n    @POST\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Create or update an organization\", operationId = \"createOrUpdateOrg\")\n    public CreateOrganizationResponse createOrUpdate(@Valid OrganizationEntry entry) {\n        OrganizationOperationResult result = orgManager.createOrUpdate(entry);\n        return new CreateOrganizationResponse(result.orgId(), result.result());\n    }\n\n    @GET\n    @Path(\"/{orgName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Get an existing organization\", operationId = \"getOrg\")\n    public OrganizationEntry get(@PathParam(\"orgName\") @ConcordKey String orgName) {\n        return orgDao.getByName(orgName);\n    }\n\n    @GET\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"List organizations\", operationId = \"listOrgs\")\n    public List<OrganizationEntry> find(@QueryParam(\"onlyCurrent\") @DefaultValue(\"false\") boolean onlyCurrent,\n                                        @QueryParam(\"offset\") int offset,\n                                        @QueryParam(\"limit\") int limit,\n                                        @QueryParam(\"filter\") String filter) {\n\n        UserPrincipal p = UserPrincipal.assertCurrent();\n        UUID userId = p.getId();\n\n        if (Roles.isAdmin() || Roles.isGlobalReader() || Roles.isGlobalWriter()) {\n            // admins and global readers/writers see all orgs regardless of the onlyCurrent value\n            userId = null;\n        }\n\n        return orgDao.list(userId, onlyCurrent, offset, limit, filter);\n    }\n\n    @DELETE\n    @Path(\"/{orgName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Remove an existing organization\", operationId = \"deleteOrg\")\n    public GenericOperationResult delete(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                         @QueryParam(\"confirmation\") String confirmation) {\n\n        if (!\"yes\".equalsIgnoreCase(confirmation)) {\n            throw new ConcordApplicationException(\"Operation must be confirmed\", Response.Status.BAD_REQUEST);\n        }\n\n        orgManager.delete(orgName);\n        return new GenericOperationResult(OperationResult.DELETED);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/OrganizationVisibility.java",
    "content": "package com.walmartlabs.concord.server.org;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic enum OrganizationVisibility {\n\n    PUBLIC,\n    PRIVATE\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/ProjectProcessResource.java",
    "content": "package com.walmartlabs.concord.server.org;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.console.ResponseTemplates;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.org.project.RepositoryDao;\nimport com.walmartlabs.concord.server.process.*;\nimport com.walmartlabs.concord.server.process.form.FormServiceV1;\nimport com.walmartlabs.concord.server.process.form.FormServiceV2;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueDao;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueManager;\nimport com.walmartlabs.concord.server.process.state.ProcessStateManager;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.Context;\nimport javax.ws.rs.core.HttpHeaders;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.server.process.state.ProcessStateManager.path;\nimport static javax.ws.rs.core.Response.Status;\n\n@Path(\"/api/v1/org\")\n@Tag(name = \"Project Processes\")\npublic class ProjectProcessResource implements Resource {\n\n    private static final Logger log = LoggerFactory.getLogger(ProjectProcessResource.class);\n\n    private final ProcessManager processManager;\n    private final OrganizationDao orgDao;\n    private final ProcessQueueDao queueDao;\n    private final ProcessQueueManager processQueueManager;\n    private final FormServiceV1 formServiceV1;\n    private final FormServiceV2 formServiceV2;\n    private final ResponseTemplates responseTemplates;\n    private final ProjectDao projectDao;\n    private final RepositoryDao repositoryDao;\n    private final ProcessStateManager stateManager;\n\n    @Inject\n    public ProjectProcessResource(ProcessManager processManager,\n                                  OrganizationDao orgDao,\n                                  ProcessQueueDao queueDao,\n                                  ProcessQueueManager processQueueManager,\n                                  FormServiceV1 formServiceV1,\n                                  FormServiceV2 formServiceV2,\n                                  ResponseTemplates responseTemplates,\n                                  ProjectDao projectDao,\n                                  RepositoryDao repositoryDao,\n                                  ProcessStateManager stateManager) {\n\n        this.processManager = processManager;\n        this.orgDao = orgDao;\n        this.queueDao = queueDao;\n        this.processQueueManager = processQueueManager;\n        this.formServiceV1 = formServiceV1;\n        this.formServiceV2 = formServiceV2;\n        this.responseTemplates = responseTemplates;\n        this.projectDao = projectDao;\n        this.repositoryDao = repositoryDao;\n        this.stateManager = stateManager;\n    }\n\n    /**\n     * Starts a new process instance.\n     */\n    @GET\n    @Path(\"/{orgName}/project/{projectName}/repo/{repoName}/start/{entryPoint}\")\n    @Validate\n    @Operation(description = \"Start a new process\", operationId = \"startProjectProcess\")\n    public Response start(@PathParam(\"orgName\") String orgName,\n                          @PathParam(\"projectName\") String projectName,\n                          @PathParam(\"repoName\") String repoName,\n                          @QueryParam(\"repoBranchOrTag\") String repoBranchOrTag,\n                          @QueryParam(\"repoCommitId\") String repoCommitId,\n                          @PathParam(\"entryPoint\") String entryPoint,\n                          @QueryParam(\"activeProfiles\") String activeProfiles,\n                          @Context HttpServletRequest request) {\n\n        try {\n            UUID orgId = getOrgId(orgName);\n            UUID projectId = getProjectId(orgId, projectName);\n            UUID repoId = getRepoId(projectId, repoName);\n\n            return doStartProcess(orgId, projectId, repoId, repoBranchOrTag, repoCommitId, entryPoint, activeProfiles, request);\n        } catch (Exception e) {\n            log.warn(\"startProcess ['{}', '{}', '{}', '{}', '{}'] -> error: {}\", orgName, projectName, repoName, entryPoint, activeProfiles, e.getMessage());\n            return processError(null, \"Process error: \" + e.getMessage(), e);\n        }\n    }\n\n    private Response doStartProcess(UUID orgId,\n                                    UUID projectId,\n                                    UUID repoId,\n                                    String branchOrTag,\n                                    String commitId,\n                                    String entryPoint,\n                                    String activeProfiles,\n                                    HttpServletRequest request) {\n\n        Map<String, Object> cfg = new HashMap<>();\n\n        if (branchOrTag != null) {\n            cfg.put(Constants.Request.REPO_BRANCH_OR_TAG, branchOrTag);\n        }\n\n        if (commitId != null) {\n            cfg.put(Constants.Request.REPO_COMMIT_ID, commitId);\n        }\n\n        if (activeProfiles != null) {\n            String[] as = activeProfiles.split(\",\");\n            cfg.put(Constants.Request.ACTIVE_PROFILES_KEY, Arrays.asList(as));\n        }\n\n        PartialProcessKey processKey = PartialProcessKey.create();\n\n        try {\n            UserPrincipal initiator = UserPrincipal.assertCurrent();\n\n            Payload payload = PayloadBuilder.start(processKey)\n                    .organization(orgId)\n                    .project(projectId)\n                    .repository(repoId)\n                    .entryPoint(entryPoint)\n                    .initiator(initiator.getId(), initiator.getUsername())\n                    .configuration(cfg)\n                    .request(request)\n                    .build();\n\n            processManager.start(payload);\n        } catch (Exception e) {\n            return processError(processKey, e.getMessage(), e);\n        }\n\n        return proceed(processKey);\n    }\n\n    @POST\n//    @ApiOperation(\"Proceed to next step for the process\")\n    @Path(\"{processInstanceId}/next\")\n    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)\n    @Produces(MediaType.APPLICATION_JSON)\n    public Response proceed(@PathParam(\"processInstanceId\") UUID processInstanceId) {\n        PartialProcessKey processKey = PartialProcessKey.from(processInstanceId);\n        return proceed(processKey);\n    }\n\n    private Response proceed(PartialProcessKey processKey) {\n        ProcessEntry entry = processQueueManager.get(processKey);\n        if (entry == null) {\n            throw new ConcordApplicationException(\"Process not found: \" + processKey, Status.NOT_FOUND);\n        }\n\n        ProcessKey pk = new ProcessKey(entry.instanceId(), entry.createdAt());\n\n        ProcessStatus s = entry.status();\n        if (s == ProcessStatus.FAILED || s == ProcessStatus.CANCELLED || s == ProcessStatus.TIMED_OUT) {\n            return processError(processKey, \"Process failed: \" + s, null);\n        } else if (s == ProcessStatus.FINISHED) {\n            return processFinished(processKey);\n        } else if (s == ProcessStatus.SUSPENDED) {\n            String nextFormId = nextFormId(pk);\n            if (nextFormId == null) {\n                return processError(processKey, \"Invalid process state: no forms found\", null);\n            }\n\n            String url = \"/#/process/\" + entry.instanceId() + \"/wizard\";\n            return Response.status(Status.MOVED_PERMANENTLY)\n                    .header(HttpHeaders.LOCATION, url)\n                    .build();\n        } else {\n            Map<String, Object> args = prepareArgumentsForInProgressTemplate(entry);\n            return responseTemplates.inProgressWait(Response.ok(), args).build();\n        }\n    }\n\n    private String nextFormId(ProcessKey processKey) {\n        if (isV2(processKey)) {\n            return formServiceV2.nextFormId(processKey);\n        } else {\n            return formServiceV1.nextFormId(processKey);\n        }\n    }\n\n    private static Map<String, Object> prepareArgumentsForInProgressTemplate(ProcessEntry entry) {\n        Map<String, Object> args = new HashMap<>();\n        args.put(\"orgName\", entry.orgName());\n        args.put(\"projectName\", entry.projectName());\n        args.put(\"instanceId\", entry.instanceId().toString());\n        args.put(\"parentInstanceId\", entry.parentInstanceId());\n        args.put(\"initiator\", entry.initiator());\n        args.put(\"createdAt\", entry.createdAt());\n        args.put(\"lastUpdatedAt\", entry.lastUpdatedAt());\n        args.put(\"status\", entry.status().toString());\n        return args;\n    }\n\n    private UUID getOrgId(String orgName) {\n        UUID id = orgDao.getId(orgName);\n        if (id == null) {\n            throw new ValidationErrorsException(\"Organization not found: \" + orgName);\n        }\n        return id;\n    }\n\n    private UUID getProjectId(UUID orgId, String projectName) {\n        if (projectName == null) {\n            return null;\n        }\n\n        if (orgId == null) {\n            throw new ValidationErrorsException(\"Organization name is required\");\n        }\n\n        UUID id = projectDao.getId(orgId, projectName);\n        if (id == null) {\n            throw new ValidationErrorsException(\"Project not found: \" + projectName);\n        }\n        return id;\n    }\n\n    private UUID getRepoId(UUID projectId, String repoName) {\n        if (repoName == null) {\n            return null;\n        }\n\n        if (projectId == null) {\n            throw new ValidationErrorsException(\"Project name is required\");\n        }\n\n        UUID id = repositoryDao.getId(projectId, repoName);\n        if (id == null) {\n            throw new ValidationErrorsException(\"Repository not found: \" + repoName);\n        }\n        return id;\n    }\n\n    private Response processFinished(PartialProcessKey processKey) {\n        return responseTemplates.processFinished(Response.ok(),\n                        Collections.singletonMap(\"instanceId\", processKey.getInstanceId()))\n                .build();\n    }\n\n    private Response processError(PartialProcessKey processKey, String message, Throwable t) {\n        Map<String, Object> args = new HashMap<>();\n\n        if (processKey != null && queueDao.exists(processKey)) {\n            UUID instanceId = processKey.getInstanceId();\n            if (instanceId != null) {\n                args.put(\"instanceId\", instanceId);\n            }\n        }\n\n        args.put(\"message\", message);\n\n        if (t != null) {\n            t = unwrapCause(t);\n            args.put(\"stacktrace\", stacktraceToString(t));\n        }\n\n        return responseTemplates.processError(Response.status(Status.INTERNAL_SERVER_ERROR), args)\n                .build();\n    }\n\n    private static Throwable unwrapCause(Throwable t) {\n        if (t instanceof ProcessException && t.getCause() != null) {\n            return t.getCause();\n        }\n\n        return t;\n    }\n\n    private static String stacktraceToString(Throwable t) {\n        StringWriter w = new StringWriter();\n        t.printStackTrace(new PrintWriter(w));\n        return w.toString();\n    }\n\n    private boolean isV2(ProcessKey processKey) {\n        String resource = path(Constants.Files.JOB_ATTACHMENTS_DIR_NAME,\n                Constants.Files.JOB_STATE_DIR_NAME,\n                Constants.Files.JOB_FORMS_V2_DIR_NAME);\n        return stateManager.exists(processKey, resource);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/ResourceAccessEntry.java",
    "content": "package com.walmartlabs.concord.server.org;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\n\nimport javax.validation.constraints.NotNull;\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@JsonInclude(Include.NON_NULL)\npublic class ResourceAccessEntry implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final UUID teamId;\n\n    @ConcordKey\n    private final String orgName;\n\n    @ConcordKey\n    private final String teamName;\n\n    @NotNull\n    private final ResourceAccessLevel level;\n\n    public ResourceAccessEntry(String teamName, ResourceAccessLevel level) {\n        this(null, null, teamName, level);\n    }\n\n\n    public ResourceAccessEntry(String orgName, String teamName, ResourceAccessLevel level) {\n        this(null, orgName, teamName, level);\n    }\n\n    @JsonCreator\n    public ResourceAccessEntry(@JsonProperty(\"teamId\") UUID teamId,\n                               @JsonProperty(\"orgName\") String orgName,\n                               @JsonProperty(\"teamName\") String teamName,\n                               @JsonProperty(\"level\") ResourceAccessLevel level) {\n\n        this.teamId = teamId;\n        this.orgName = orgName;\n        this.teamName = teamName;\n        this.level = level;\n    }\n\n    public UUID getTeamId() {\n        return teamId;\n    }\n\n    public String getOrgName() {\n        return orgName;\n    }\n\n    public String getTeamName() {\n        return teamName;\n    }\n\n    public ResourceAccessLevel getLevel() {\n        return level;\n    }\n\n    @Override\n    public String toString() {\n        return \"ResourceAccessEntry{\" +\n                \"teamId=\" + teamId +\n                \", orgName='\" + orgName + '\\'' +\n                \", teamName='\" + teamName + '\\'' +\n                \", level=\" + level +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/ResourceAccessLevel.java",
    "content": "package com.walmartlabs.concord.server.org;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic enum ResourceAccessLevel {\n\n    /**\n     * Can use, modify or delete the resource.\n     */\n    OWNER,\n\n    /**\n     * Can use or modify the resource.\n     */\n    WRITER,\n\n    /**\n     * Can use the resource.\n     */\n    READER;\n\n    /**\n     * @return an array of access levels that are the same or higher as the specified level.\n     */\n    public static ResourceAccessLevel[] atLeast(ResourceAccessLevel r) {\n        switch (r) {\n            case OWNER:\n                return new ResourceAccessLevel[]{OWNER};\n            case WRITER:\n                return new ResourceAccessLevel[]{OWNER, WRITER};\n            case READER:\n                return new ResourceAccessLevel[]{OWNER, WRITER, READER};\n            default:\n                throw new IllegalArgumentException(\"Unknown access level: \" + r);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/ResourceAccessUtils.java",
    "content": "package com.walmartlabs.concord.server.org;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.org.team.TeamDao;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\n\nimport java.util.UUID;\n\npublic final class ResourceAccessUtils {\n\n    public static UUID getTeamId(OrganizationDao orgDao, TeamDao teamDao, UUID baseOrgId, ResourceAccessEntry e) {\n        UUID id = e.getTeamId();\n        if (id != null) {\n            return id;\n        }\n\n        if (e.getTeamName() == null) {\n            throw new IllegalArgumentException(\"You must specify an organization and/or a team name.\");\n        }\n\n        UUID orgId = baseOrgId;\n        if (e.getOrgName() != null) {\n            orgId = orgDao.getId(e.getOrgName());\n        }\n\n        if (orgId == null) {\n            throw new IllegalArgumentException(\"Organization not found: \" + e.getOrgName());\n        }\n\n        UUID teamId = teamDao.getId(orgId, e.getTeamName());\n        if (teamId == null) {\n            throw new IllegalArgumentException(\"Team not found: \" + e.getTeamName());\n        }\n\n        return teamId;\n    }\n\n    public static boolean isSame(UserPrincipal p, EntityOwner owner) {\n        if (p == null || owner == null) {\n            return false;\n        }\n\n        UUID userId = p.getId();\n        UUID ownerId = owner.id();\n        return userId.equals(ownerId);\n    }\n\n    private ResourceAccessUtils() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/inventory/CreateInventoryQueryResponse.java",
    "content": "package com.walmartlabs.concord.server.org.inventory;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.server.OperationResult;\n\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@Deprecated\npublic class CreateInventoryQueryResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n\n    private final OperationResult result;\n    private final UUID id;\n\n    @JsonCreator\n    public CreateInventoryQueryResponse(@JsonProperty(\"result\") OperationResult result,\n                                        @JsonProperty(\"id\") UUID id) {\n\n        this.result = result;\n        this.id = id;\n    }\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    public OperationResult getResult() {\n        return result;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    @Override\n    public String toString() {\n        return \"CreateInventoryQueryResponse{\" +\n                \"ok=\" + ok +\n                \", result=\" + result +\n                \", id=\" + id +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/inventory/CreateInventoryResponse.java",
    "content": "package com.walmartlabs.concord.server.org.inventory;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.server.OperationResult;\n\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@JsonInclude(Include.NON_NULL)\n@Deprecated\npublic class CreateInventoryResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n    private final OperationResult result;\n    private final UUID id;\n\n    @JsonCreator\n    public CreateInventoryResponse(@JsonProperty(\"result\") OperationResult result,\n                                   @JsonProperty(\"id\") UUID id) {\n\n        this.result = result;\n        this.id = id;\n    }\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    public OperationResult getResult() {\n        return result;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    @Override\n    public String toString() {\n        return \"CreateInventoryResponse{\" +\n                \"result=\" + result +\n                \", id=\" + id +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/inventory/DeleteInventoryDataResponse.java",
    "content": "package com.walmartlabs.concord.server.org.inventory;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\n\n@Deprecated\npublic class DeleteInventoryDataResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    @Override\n    public String toString() {\n        return \"DeleteInventoryDataResponse{\" +\n                \"ok=\" + ok +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/inventory/DeleteInventoryQueryResponse.java",
    "content": "package com.walmartlabs.concord.server.org.inventory;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\n\n@Deprecated\npublic class DeleteInventoryQueryResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    @Override\n    public String toString() {\n        return \"DeleteInventoryQueryResponse{\" +\n                \"ok=\" + ok +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/inventory/InventoryDataDao.java",
    "content": "package com.walmartlabs.concord.server.org.inventory;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.jooq.tables.JsonStoreData;\nimport com.walmartlabs.concord.server.jooq.tables.JsonStores;\nimport org.jooq.Record;\nimport org.jooq.*;\n\nimport javax.inject.Inject;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.JSON_STORES;\nimport static com.walmartlabs.concord.server.jooq.Tables.JSON_STORE_DATA;\nimport static org.jooq.impl.DSL.*;\n\n@Deprecated\npublic class InventoryDataDao extends AbstractDao {\n\n    private final ConcordObjectMapper objectMapper;\n\n    // TODO shouldn't it be @InventoryDB?\n    @Inject\n    public InventoryDataDao(@MainDB Configuration cfg,\n                            ConcordObjectMapper objectMapper) {\n        super(cfg);\n        this.objectMapper = objectMapper;\n    }\n\n    public Object getSingleItem(UUID id, String itemPath) {\n        return txResult(tx -> {\n            JsonStoreData i = JSON_STORE_DATA.as(\"i\");\n            return tx.select(i.ITEM_DATA.cast(String.class))\n                    .from(i)\n                    .where(i.JSON_STORE_ID.eq(id).and(i.ITEM_PATH.eq(itemPath)))\n                    .fetchOne(Record1::value1);\n        });\n    }\n\n    public List<InventoryDataItem> get(UUID inventoryId, String path) {\n        return get(dsl(), inventoryId, path);\n    }\n\n    public void merge(UUID inventoryId, String itemPath, Object data) {\n        tx(tx -> merge(tx, inventoryId, itemPath, data));\n    }\n\n    public void delete(UUID inventoryId, String itemPath) {\n        tx(tx -> delete(tx, inventoryId, itemPath));\n    }\n\n    public List<Map<String, Object>> list(UUID inventoryId) {\n        return dsl().select(JSON_STORE_DATA.ITEM_PATH, JSON_STORE_DATA.ITEM_DATA)\n                .from(JSON_STORE_DATA)\n                .where(JSON_STORE_DATA.JSON_STORE_ID.eq(inventoryId))\n                .fetch(this::toListItem);\n    }\n\n    private Map<String, Object> toListItem(Record2<String, JSONB> r) {\n        Map<String, Object> result = new HashMap<>();\n        result.put(\"path\", r.value1());\n        result.put(\"data\", objectMapper.fromJSONB(r.value2()));\n        return result;\n    }\n\n    private List<InventoryDataItem> get(DSLContext tx, UUID inventoryId, String path) {\n        Table<Record> nodes = table(\"nodes\");\n        JsonStores i1 = JSON_STORES.as(\"i1\");\n        JsonStores i2 = JSON_STORES.as(\"i2\");\n\n        SelectConditionStep<Record3<UUID, UUID, Integer>> s1 =\n                select(i1.JSON_STORE_ID, i1.PARENT_INVENTORY_ID, value(1))\n                        .from(i1)\n                        .where(i1.JSON_STORE_ID.eq(inventoryId));\n\n        Field<Integer> levelField = field(\"level\", Integer.class);\n\n        SelectConditionStep<Record3<UUID, UUID, Integer>> s2 =\n                select(i2.JSON_STORE_ID, i2.PARENT_INVENTORY_ID, levelField.add(1))\n                        .from(i2, nodes)\n                        .where(i2.JSON_STORE_ID.eq(JSON_STORES.as(\"nodes\").PARENT_INVENTORY_ID));\n\n        SelectConditionStep<Record3<String, JSONB, Integer>> s = tx.withRecursive(\"nodes\", JSON_STORES.JSON_STORE_ID.getName(), JSON_STORES.PARENT_INVENTORY_ID.getName(), levelField.getName())\n                .as(s1.unionAll(s2))\n                .select(JSON_STORE_DATA.ITEM_PATH, JSON_STORE_DATA.ITEM_DATA, levelField.add(1))\n                .from(JSON_STORE_DATA, nodes)\n                .where(JSON_STORE_DATA.JSON_STORE_ID.eq(JSON_STORES.as(\"nodes\").JSON_STORE_ID)\n                        .and(JSON_STORE_DATA.ITEM_PATH.startsWith(path)));\n        return s.fetch(this::toEntry);\n    }\n\n    private void merge(DSLContext tx, UUID inventoryId, String itemPath, Object data) {\n        tx.insertInto(JSON_STORE_DATA)\n                .columns(JSON_STORE_DATA.JSON_STORE_ID, JSON_STORE_DATA.ITEM_PATH, JSON_STORE_DATA.ITEM_DATA)\n                .values(inventoryId, itemPath, objectMapper.toJSONB(data))\n                .onDuplicateKeyUpdate()\n                .set(JSON_STORE_DATA.ITEM_DATA, objectMapper.toJSONB(data))\n                .execute();\n    }\n\n    private void delete(DSLContext tx, UUID inventoryId, String itemPath) {\n        tx.deleteFrom(JSON_STORE_DATA)\n                .where(JSON_STORE_DATA.JSON_STORE_ID.eq(inventoryId)\n                        .and(JSON_STORE_DATA.ITEM_PATH.eq(itemPath)))\n                .execute();\n    }\n\n    private InventoryDataItem toEntry(Record3<String, JSONB, Integer> r) {\n        return new InventoryDataItem(r.value1(), r.value3(), objectMapper.fromJSONB(r.value2()));\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/inventory/InventoryDataItem.java",
    "content": "package com.walmartlabs.concord.server.org.inventory;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\n\n@Deprecated\npublic class InventoryDataItem implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String path;\n\n    private final int level;\n\n    private final Object data;\n\n    public InventoryDataItem(String path, int level, Object data) {\n        this.path = path;\n        this.level = level;\n        this.data = data;\n    }\n\n    public String getPath() {\n        return path;\n    }\n\n    public Object getData() {\n        return data;\n    }\n\n    public int getLevel() {\n        return level;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/inventory/InventoryDataResource.java",
    "content": "package com.walmartlabs.concord.server.org.inventory;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.org.OrganizationEntry;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.org.jsonstore.JsonStoreAccessManager;\nimport com.walmartlabs.concord.server.org.jsonstore.JsonStoreEntry;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\n@Path(\"/api/v1/org\")\n@Deprecated\n@Tag(name = \"Inventory Data\")\npublic class InventoryDataResource implements Resource {\n\n    private final OrganizationManager orgManager;\n    private final JsonStoreAccessManager inventoryManager;\n    private final InventoryDataDao inventoryDataDao;\n\n    @Inject\n    public InventoryDataResource(OrganizationManager orgManager,\n                                 JsonStoreAccessManager inventoryManager,\n                                 InventoryDataDao inventoryDataDao) {\n        this.orgManager = orgManager;\n        this.inventoryManager = inventoryManager;\n        this.inventoryDataDao = inventoryDataDao;\n    }\n\n    /**\n     * Returns an existing inventory data.\n     *\n     * @param orgName       organization's name\n     * @param inventoryName inventory's name\n     * @param itemPath      data item path\n     * @return\n     */\n    @GET\n    @Path(\"/{orgName}/inventory/{inventoryName}/data/{itemPath:.*}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Get inventory data\", operationId = \"getInventoryData\")\n    public Object get(@PathParam(\"orgName\") String orgName,\n                      @PathParam(\"inventoryName\") String inventoryName,\n                      @PathParam(\"itemPath\") String itemPath,\n                      @QueryParam(\"singleItem\") @DefaultValue(\"false\") boolean singleItem) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n        JsonStoreEntry inventory = inventoryManager.assertAccess(org.getId(), null, inventoryName, ResourceAccessLevel.READER, true);\n\n        if (singleItem) {\n            return inventoryDataDao.getSingleItem(inventory.id(), itemPath);\n        } else {\n            return build(inventory.id(), itemPath);\n        }\n    }\n\n    /**\n     * Returns all inventory data for inventory.\n     *\n     * @param orgName       organization's name\n     * @param inventoryName inventory's name\n     * @return\n     */\n    @GET\n    @Path(\"/{orgName}/inventory/{inventoryName}/data\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"List inventory data\", operationId = \"listInventoryData\")\n    public List<Map<String, Object>> list(@PathParam(\"orgName\") String orgName,\n                                          @PathParam(\"inventoryName\") String inventoryName) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n        JsonStoreEntry inventory = inventoryManager.assertAccess(org.getId(), null, inventoryName, ResourceAccessLevel.READER, true);\n\n        return inventoryDataDao.list(inventory.id());\n    }\n\n    /**\n     * Modifies inventory data\n     *\n     * @param orgName       organization's name\n     * @param inventoryName inventory's name\n     * @param itemPath      inventory's data path\n     * @param data          inventory's data\n     * @return full inventory data by path\n     */\n    @POST\n    @Path(\"/{orgName}/inventory/{inventoryName}/data/{itemPath:.*}\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Modify inventory data\", operationId = \"updateInventoryData\")\n    public Object data(@PathParam(\"orgName\") String orgName,\n                       @PathParam(\"inventoryName\") String inventoryName,\n                       @PathParam(\"itemPath\") String itemPath,\n                       Object data) {\n\n        // we expect all top-level entries to be JSON objects\n        if (!itemPath.contains(\"/\") && !(data instanceof Map)) {\n            throw new ValidationErrorsException(\"Top-level inventory entries must be JSON objects. Got: \" + data.getClass());\n        }\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n        JsonStoreEntry inventory = inventoryManager.assertAccess(org.getId(), null, inventoryName, ResourceAccessLevel.WRITER, true);\n\n        inventoryDataDao.merge(inventory.id(), itemPath, data);\n        return build(inventory.id(), itemPath);\n    }\n\n    /**\n     * Deletes inventory data\n     *\n     * @param orgName       organization's name\n     * @param inventoryName inventory's name\n     * @param itemPath      inventory's data path\n     * @return\n     */\n    @DELETE\n    @Path(\"/{orgName}/inventory/{inventoryName}/data/{itemPath:.*}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Delete inventory data\", operationId = \"deleteInventoryData\")\n    public DeleteInventoryDataResponse delete(@PathParam(\"orgName\") String orgName,\n                                              @PathParam(\"inventoryName\") String inventoryName,\n                                              @PathParam(\"itemPath\") String itemPath) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n        JsonStoreEntry inventory = inventoryManager.assertAccess(org.getId(), null, inventoryName, ResourceAccessLevel.WRITER, true);\n\n        inventoryDataDao.delete(inventory.id(), itemPath);\n\n        return new DeleteInventoryDataResponse();\n    }\n\n    private Object build(UUID inventoryId, String itemPath) {\n        try {\n            return JsonBuilder.build(inventoryDataDao.get(inventoryId, itemPath));\n        } catch (IOException e) {\n            throw new ConcordApplicationException(\"Error while building the response: \" + e.getMessage(), e);\n        }\n    }\n}"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/inventory/InventoryEntry.java",
    "content": "package com.walmartlabs.concord.server.org.inventory;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.server.org.jsonstore.JsonStoreVisibility;\n\nimport javax.validation.constraints.NotNull;\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@JsonInclude(Include.NON_NULL)\n@Deprecated\npublic class InventoryEntry implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final UUID id;\n\n    @NotNull\n    @ConcordKey\n    private final String name;\n\n    private final UUID orgId;\n\n    @ConcordKey\n    private final String orgName;\n\n    private final JsonStoreVisibility visibility;\n\n    private final InventoryOwner owner;\n\n    private final InventoryEntry parent;\n\n    public InventoryEntry(String name) {\n        this(null, name, null, null, null, null, null);\n    }\n\n    public InventoryEntry(String name, JsonStoreVisibility visibility) {\n        this(null, name, null, null, visibility, null, null);\n    }\n\n    @JsonCreator\n    public InventoryEntry(@JsonProperty(\"id\") UUID id,\n                          @JsonProperty(\"name\") String name,\n                          @JsonProperty(\"orgId\") UUID orgId,\n                          @JsonProperty(\"orgName\") String orgName,\n                          @JsonProperty(\"visibility\") JsonStoreVisibility visibility,\n                          @JsonProperty(\"owner\") InventoryOwner owner,\n                          @JsonProperty(\"parent\") InventoryEntry parent) {\n\n        this.id = id;\n        this.name = name;\n        this.orgId = orgId;\n        this.orgName = orgName;\n        this.visibility = visibility;\n        this.owner = owner;\n        this.parent = parent;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public UUID getOrgId() {\n        return orgId;\n    }\n\n    public String getOrgName() {\n        return orgName;\n    }\n\n    public JsonStoreVisibility getVisibility() {\n        return visibility;\n    }\n\n    public UUID getParentId() {\n        if (parent == null) {\n            return null;\n        }\n        return parent.getId();\n    }\n\n    public InventoryEntry getParent() {\n        return parent;\n    }\n\n    public InventoryOwner getOwner() {\n        return owner;\n    }\n\n    @Override\n    public String toString() {\n        return \"InventoryEntry{\" +\n                \"id=\" + id +\n                \", name='\" + name + '\\'' +\n                \", orgId=\" + orgId +\n                \", orgName='\" + orgName + '\\'' +\n                \", visibility=\" + visibility +\n                \", owner=\" + owner +\n                \", parent=\" + parent +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/inventory/InventoryModule.java",
    "content": "package com.walmartlabs.concord.server.org.inventory;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.walmartlabs.concord.server.Utils.bindJaxRsResource;\n\n@Deprecated\npublic class InventoryModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        binder.bind(InventoryDataDao.class).in(SINGLETON);\n\n        bindJaxRsResource(binder, InventoryResource.class);\n        bindJaxRsResource(binder, InventoryDataResource.class);\n        bindJaxRsResource(binder, InventoryQueryResource.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/inventory/InventoryOwner.java",
    "content": "package com.walmartlabs.concord.server.org.inventory;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@JsonInclude(Include.NON_NULL)\n@Deprecated\npublic class InventoryOwner implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final UUID id;\n    private final String username;\n\n    @JsonCreator\n    public InventoryOwner(@JsonProperty(\"id\") UUID id,\n                          @JsonProperty(\"username\") String username) {\n\n        this.id = id;\n        this.username = username;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public String getUsername() {\n        return username;\n    }\n\n    @Override\n    public String toString() {\n        return \"InventoryOwner{\" +\n                \"id=\" + id +\n                \", username='\" + username + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/inventory/InventoryQueryEntry.java",
    "content": "package com.walmartlabs.concord.server.org.inventory;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\n\nimport javax.validation.constraints.NotNull;\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@JsonInclude(Include.NON_NULL)\n@Deprecated\npublic class InventoryQueryEntry implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final UUID id;\n\n    private final UUID inventoryId;\n\n    @NotNull\n    @ConcordKey\n    private final String name;\n\n    @NotNull\n    private final String text;\n\n    @JsonCreator\n    public InventoryQueryEntry(@JsonProperty(\"id\") UUID id,\n                               @JsonProperty(\"name\") String name,\n                               @JsonProperty(\"inventoryId\") UUID inventoryId,\n                               @JsonProperty(\"text\") String text) {\n        this.id = id;\n        this.name = name;\n        this.inventoryId = inventoryId;\n        this.text = text;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public UUID getInventoryId() {\n        return inventoryId;\n    }\n\n    public String getText() {\n        return text;\n    }\n\n    @Override\n    public String toString() {\n        return \"InventoryQueryEntry{\" +\n                \"id=\" + id +\n                \", inventoryId='\" + inventoryId + '\\'' +\n                \", name='\" + name + '\\'' +\n                \", text='\" + text + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/inventory/InventoryQueryResource.java",
    "content": "package com.walmartlabs.concord.server.org.inventory;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.server.GenericOperationResult;\nimport com.walmartlabs.concord.server.org.OrganizationEntry;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.jsonstore.*;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.validation.Valid;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\n\n@Path(\"/api/v1/org\")\n@Deprecated\n@Tag(name = \"Inventory Queries\")\npublic class InventoryQueryResource implements Resource {\n\n    private final JsonStoreQueryResource storageQueryResource;\n    private final OrganizationManager organizationManager;\n    private final JsonStoreDao storageDao;\n    private final JsonStoreQueryDao storageQueryDao;\n\n    @Inject\n    public InventoryQueryResource(JsonStoreQueryResource storageQueryResource, OrganizationManager organizationManager, JsonStoreDao storageDao, JsonStoreQueryDao storageQueryDao) {\n        this.storageQueryResource = storageQueryResource;\n        this.organizationManager = organizationManager;\n        this.storageDao = storageDao;\n        this.storageQueryDao = storageQueryDao;\n    }\n\n    /**\n     * Returns inventory query\n     *\n     * @param orgName       organization's name\n     * @param inventoryName inventory's name\n     * @param queryName     query's name\n     * @return query text\n     */\n    @GET\n    @Path(\"/{orgName}/inventory/{inventoryName}/query/{queryName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Get inventory query\", operationId = \"getInventoryQuery\")\n    public InventoryQueryEntry get(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                   @PathParam(\"inventoryName\") @ConcordKey String inventoryName,\n                                   @PathParam(\"queryName\") @ConcordKey String queryName) {\n\n        return convert(storageQueryResource.get(orgName, inventoryName, queryName));\n    }\n\n    /**\n     * Creates or updates inventory query\n     *\n     * @param orgName       organization's name\n     * @param inventoryName inventory's name\n     * @param queryName     query's name\n     * @param text          query text\n     * @return\n     */\n    @POST\n    @Consumes({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})\n    @Produces(MediaType.APPLICATION_JSON)\n    @Path(\"/{orgName}/inventory/{inventoryName}/query/{queryName}\")\n    @Operation(description = \"Create or update inventory query\", operationId = \"createOrUpdateInventoryQuery\")\n    public CreateInventoryQueryResponse createOrUpdate(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                       @PathParam(\"inventoryName\") @ConcordKey String inventoryName,\n                                                       @PathParam(\"queryName\") @ConcordKey String queryName,\n                                                       String text) {\n\n        GenericOperationResult res = storageQueryResource.createOrUpdate(orgName, inventoryName, JsonStoreQueryRequest.builder()\n                .name(queryName)\n                .text(text)\n                .build());\n\n        OrganizationEntry org = organizationManager.assertExisting(null, orgName);\n        JsonStoreEntry storage = storageDao.get(org.getId(), inventoryName);\n        UUID id = storageQueryDao.getId(storage.id(), queryName);\n\n        return new CreateInventoryQueryResponse(res.getResult(), id);\n    }\n\n    /**\n     * List inventory queries.\n     *\n     * @param orgName       organization's name\n     * @param inventoryName inventory's name\n     * @return\n     */\n    @GET\n    @Path(\"/{orgName}/inventory/{inventoryName}/query\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"List inventory queries\", operationId = \"listInventoryQueries\")\n    public List<InventoryQueryEntry> list(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                          @PathParam(\"inventoryName\") @ConcordKey String inventoryName) {\n\n        return storageQueryResource.list(orgName, inventoryName, -1, -1, null)\n                .stream()\n                .map(InventoryQueryResource::convert)\n                .collect(Collectors.toList());\n    }\n\n    /**\n     * Deletes inventory query\n     *\n     * @param orgName       organization's name\n     * @param inventoryName inventory's name\n     * @param queryName     query's name\n     * @return\n     */\n    @DELETE\n    @Path(\"/{orgName}/inventory/{inventoryName}/query/{queryName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Delete inventory query\", operationId = \"deleteInventoryQuery\")\n    public DeleteInventoryQueryResponse delete(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                               @PathParam(\"inventoryName\") @ConcordKey String inventoryName,\n                                               @PathParam(\"queryName\") @ConcordKey String queryName) {\n\n        storageQueryResource.delete(orgName, inventoryName, queryName);\n        return new DeleteInventoryQueryResponse();\n    }\n\n    /**\n     * Executes inventory query\n     *\n     * @param orgName       organization's name\n     * @param inventoryName inventory's name\n     * @param queryName     query's name\n     * @param params        query params\n     * @return query result\n     */\n    @POST\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Path(\"/{orgName}/inventory/{inventoryName}/query/{queryName}/exec\")\n    @WithTimer\n    @Validate\n    @Operation(description = \"Execute inventory query\", operationId = \"executeInventoryQuery\")\n    public List<Object> exec(@PathParam(\"orgName\") @ConcordKey String orgName,\n                             @PathParam(\"inventoryName\") @ConcordKey String inventoryName,\n                             @PathParam(\"queryName\") @ConcordKey String queryName,\n                             @Valid Map<String, Object> params) {\n\n        return storageQueryResource.exec(orgName, inventoryName, queryName, params);\n    }\n\n    private static InventoryQueryEntry convert(JsonStoreQueryEntry query) {\n        if (query == null) {\n            return null;\n        }\n        return new InventoryQueryEntry(query.id(), query.name(), query.storeId(), query.text());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/inventory/InventoryResource.java",
    "content": "package com.walmartlabs.concord.server.org.inventory;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.server.GenericOperationResult;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.org.EntityOwner;\nimport com.walmartlabs.concord.server.org.OrganizationEntry;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.ResourceAccessEntry;\nimport com.walmartlabs.concord.server.org.jsonstore.*;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.validation.Valid;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\n\n@Path(\"/api/v1/org\")\n@Deprecated\n@Tag(name = \"Inventories\")\npublic class InventoryResource implements Resource {\n\n    private final JsonStoreManager storageManager;\n    private final JsonStoreDao storageDao;\n    private final OrganizationManager orgManager;\n\n    @Inject\n    public InventoryResource(JsonStoreManager storageManager, JsonStoreDao storageDao, OrganizationManager orgManager) {\n        this.storageManager = storageManager;\n        this.storageDao = storageDao;\n        this.orgManager = orgManager;\n    }\n\n    /**\n     * List existing inventories.\n     *\n     * @param orgName organization's name\n     * @return\n     */\n    @GET\n    @Path(\"/{orgName}/inventory\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"List existing inventories\", operationId = \"listInventories\")\n    public List<InventoryEntry> list(@PathParam(\"orgName\") String orgName) {\n        List<JsonStoreEntry> result = storageManager.list(orgName, -1, -1, null);\n        return result.stream()\n                .map(InventoryResource::convert)\n                .collect(Collectors.toList());\n    }\n\n    /**\n     * Returns an existing inventory.\n     *\n     * @param orgName       organization's name\n     * @param inventoryName inventory's name\n     * @return inventory\n     */\n    @GET\n    @Path(\"/{orgName}/inventory/{inventoryName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Get existing inventory\", operationId = \"getInventory\")\n    public InventoryEntry get(@PathParam(\"orgName\") String orgName,\n                              @PathParam(\"inventoryName\") String inventoryName) {\n        return convert(storageManager.get(orgName, inventoryName));\n    }\n\n    /**\n     * Create or update a inventory.\n     *\n     * @param orgName organization's name\n     * @param entry   inventory's data\n     * @return\n     */\n    @POST\n    @Path(\"/{orgName}/inventory\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Create or update inventory\", operationId = \"createOrUpdateInventory\")\n    public CreateInventoryResponse createOrUpdate(@PathParam(\"orgName\") String orgName,\n                                                  @Valid InventoryEntry entry) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n\n        OperationResult result = storageManager.createOrUpdate(orgName, JsonStoreRequest.builder()\n                .id(entry.getId())\n                .name(entry.getName())\n                .owner(toOwner(entry.getOwner()))\n                .visibility(entry.getVisibility() != null ? entry.getVisibility() : JsonStoreVisibility.PUBLIC)\n                .build());\n\n        UUID id = storageDao.getId(org.getId(), entry.getName());\n\n        return new CreateInventoryResponse(result, id);\n    }\n\n    @POST\n    @Path(\"/{orgName}/inventory/{inventoryName}/access\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Updates the access level for the specified inventory\", operationId = \"updateInventoryAccessLevel\")\n    public GenericOperationResult updateAccessLevel(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                    @PathParam(\"inventoryName\") @ConcordKey String inventoryName,\n                                                    @Valid ResourceAccessEntry entry) {\n\n        storageManager.updateAccessLevel(orgName, inventoryName, entry);\n\n        return new GenericOperationResult(OperationResult.UPDATED);\n    }\n\n    /**\n     * Creates a new inventory or updates an existing one.\n     *\n     * @param orgName       organization's name\n     * @param inventoryName inventory's name\n     * @return\n     */\n    @DELETE\n    @Path(\"/{orgName}/inventory/{inventoryName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Delete inventory\", operationId = \"deleteInventory\")\n    public GenericOperationResult delete(@PathParam(\"orgName\") String orgName,\n                                         @PathParam(\"inventoryName\") String inventoryName) {\n\n        storageManager.delete(orgName, inventoryName);\n\n        return new GenericOperationResult(OperationResult.DELETED);\n    }\n\n    private static InventoryEntry convert(JsonStoreEntry e) {\n        if (e == null) {\n            return null;\n        }\n\n        InventoryOwner owner = null;\n        if (e.owner() != null) {\n            owner = new InventoryOwner(e.owner().id(), e.owner().username());\n        }\n\n        return new InventoryEntry(e.id(), e.name(), e.orgId(), e.orgName(), e.visibility(), owner, null);\n    }\n\n    private static EntityOwner toOwner(InventoryOwner owner) {\n        if (owner == null) {\n            return null;\n        }\n\n        return EntityOwner.builder()\n                .id(owner.getId())\n                .username(owner.getUsername())\n                .build();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/inventory/JsonBuilder.java",
    "content": "package com.walmartlabs.concord.server.org.inventory;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Map;\n\n@Deprecated\npublic final class JsonBuilder {\n\n    private static final ObjectMapper mapper = new ObjectMapper();\n\n    public static Object build(List<InventoryDataItem> dataItems) throws IOException {\n        ObjectNode root = mapper.createObjectNode();\n\n        List<InventoryDataItem> items = new ArrayList<>(dataItems);\n        items.sort(Comparator.comparingInt(InventoryDataItem::getLevel).reversed());\n\n        for(InventoryDataItem item : items) {\n            String[] paths = normalizePath(item.getPath()).split(\"/\");\n            ObjectNode pathNode = root;\n            for(int i = 0; i < paths.length - 1; i++) {\n                String p = paths[i];\n                pathNode = getOrCreateNode(pathNode, p);\n            }\n            String fieldName = paths[paths.length - 1];\n            pathNode.set(fieldName, merge(pathNode.get(fieldName), item.getData()));\n        }\n\n        return mapper.treeToValue(root, Object.class);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static JsonNode merge(JsonNode node, Object data) throws IOException {\n        if (node == null || data == null) {\n            return mapper.valueToTree(data);\n        }\n\n        Object currentValue = mapper.treeToValue(node, Object.class);\n        if (currentValue instanceof Map && data instanceof Map) {\n            Map<String, Object> current = (Map<String, Object>) currentValue;\n            Map<String, Object> newData = (Map<String, Object>) data;\n\n            return mapper.valueToTree(ConfigurationUtils.deepMerge(current, newData));\n        }\n\n        return mapper.valueToTree(data);\n    }\n\n    private static ObjectNode getOrCreateNode(ObjectNode pathNode, String p) {\n        JsonNode result = pathNode.get(p);\n        if (result == null) {\n            result = mapper.createObjectNode();\n        }\n        pathNode.set(p, result);\n        if (!(result instanceof ObjectNode)) {\n            throw new RuntimeException(\"Can't add attributes into \" + result.getNodeType() + \" node, with path: '\" + p + \"'\");\n        }\n        return (ObjectNode)result;\n    }\n\n    private static String normalizePath(String path) {\n        if (path.startsWith(\"/\")) {\n            return path.substring(1);\n        }\n        return path;\n    }\n\n    private JsonBuilder() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/jsonstore/JsonStoreAccessManager.java",
    "content": "package com.walmartlabs.concord.server.org.jsonstore;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.org.OrganizationEntry;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.org.ResourceAccessUtils;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.user.UserManager;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.core.Response;\nimport java.util.UUID;\n\npublic class JsonStoreAccessManager {\n\n    private final UserManager userManager;\n    private final OrganizationManager orgManager;\n    private final JsonStoreDao storeDao;\n\n    @Inject\n    public JsonStoreAccessManager(UserManager userManager, OrganizationManager orgManager, JsonStoreDao storeDao) {\n        this.userManager = userManager;\n        this.orgManager = orgManager;\n        this.storeDao = storeDao;\n    }\n\n    public JsonStoreEntry assertAccess(UUID orgId, UUID storeId, String storeName, ResourceAccessLevel accessLevel, boolean orgMembersOnly) {\n        JsonStoreEntry store;\n\n        if (storeId != null) {\n            store = storeDao.get(storeId);\n        } else {\n            store = storeDao.get(orgId, storeName);\n        }\n\n        if (store == null) {\n            throw new ConcordApplicationException(\"JSON store not found: \" + storeName, Response.Status.NOT_FOUND);\n        }\n\n        if (!hasAccess(store, accessLevel, orgMembersOnly)) {\n            UserPrincipal p = UserPrincipal.getCurrent();\n            throw new UnauthorizedException(\"The current user (\" + p.getUsername() + \") doesn't have \" +\n                    \"the necessary access level (\" + accessLevel + \") to the JSON store: \" + store.name());\n        }\n\n        return store;\n    }\n\n    public boolean hasAccess(JsonStoreEntry store, ResourceAccessLevel accessLevel, boolean orgMembersOnly) {\n        if (Roles.isAdmin()) {\n            // an admin can access any store\n            return true;\n        }\n\n        if (accessLevel == ResourceAccessLevel.READER && (Roles.isGlobalReader() || Roles.isGlobalWriter())) {\n            return true;\n        } else if (accessLevel == ResourceAccessLevel.WRITER && Roles.isGlobalWriter()) {\n            return true;\n        }\n\n        UserPrincipal principal = UserPrincipal.assertCurrent();\n\n        if (ResourceAccessUtils.isSame(principal, store.owner())) {\n            // the owner can do anything with his store\n            return true;\n        }\n\n        if (orgMembersOnly && store.visibility() == JsonStoreVisibility.PUBLIC\n                && accessLevel == ResourceAccessLevel.READER\n                && userManager.isInOrganization(store.orgId())) {\n            // organization members can access any public store in the same organization\n            return true;\n        }\n\n        OrganizationEntry org = orgManager.assertAccess(store.orgId(), false);\n        if (ResourceAccessUtils.isSame(principal, org.getOwner())) {\n            // the org owner can do anything with the org's store\n            return true;\n        }\n\n        if (orgMembersOnly || store.visibility() != JsonStoreVisibility.PUBLIC) {\n            if (!storeDao.hasAccessLevel(store.id(), principal.getId(), ResourceAccessLevel.atLeast(accessLevel))) {\n                throw new UnauthorizedException(\"The current user (\" + principal.getUsername() + \") doesn't have \" +\n                        \"the necessary access level (\" + accessLevel + \") to the JSON store: \" + store.name());\n            }\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/jsonstore/JsonStoreCapacity.java",
    "content": "package com.walmartlabs.concord.server.org.jsonstore;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\n\n@Value.Immutable\n@JsonSerialize(as = ImmutableJsonStoreCapacity.class)\n@JsonDeserialize(as = ImmutableJsonStoreCapacity.class)\n@JsonInclude(JsonInclude.Include.NON_NULL)\npublic interface JsonStoreCapacity {\n\n    long size();\n\n    @Nullable\n    Long maxSize();\n\n    static ImmutableJsonStoreCapacity.Builder builder() {\n        return ImmutableJsonStoreCapacity.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/jsonstore/JsonStoreDao.java",
    "content": "package com.walmartlabs.concord.server.org.jsonstore;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.Utils;\nimport com.walmartlabs.concord.server.UuidGenerator;\nimport com.walmartlabs.concord.server.jooq.Tables;\nimport com.walmartlabs.concord.server.jooq.tables.JsonStores;\nimport com.walmartlabs.concord.server.jooq.tables.Organizations;\nimport com.walmartlabs.concord.server.jooq.tables.Users;\nimport com.walmartlabs.concord.server.jooq.tables.records.JsonStoresRecord;\nimport com.walmartlabs.concord.server.org.EntityOwner;\nimport com.walmartlabs.concord.server.org.ResourceAccessEntry;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.user.UserType;\nimport org.jooq.Record;\nimport org.jooq.*;\n\nimport javax.inject.Inject;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.tables.JsonStoreTeamAccess.JSON_STORE_TEAM_ACCESS;\nimport static com.walmartlabs.concord.server.jooq.tables.JsonStores.JSON_STORES;\nimport static com.walmartlabs.concord.server.jooq.tables.Organizations.ORGANIZATIONS;\nimport static com.walmartlabs.concord.server.jooq.tables.Teams.TEAMS;\nimport static com.walmartlabs.concord.server.jooq.tables.Users.USERS;\nimport static com.walmartlabs.concord.server.jooq.tables.VUserTeams.V_USER_TEAMS;\nimport static java.util.Objects.requireNonNull;\nimport static org.jooq.impl.DSL.*;\n\npublic class JsonStoreDao extends AbstractDao {\n\n    private final UuidGenerator uuidGenerator;\n\n    @Inject\n    public JsonStoreDao(@MainDB Configuration cfg, UuidGenerator uuidGenerator) {\n        super(cfg);\n        this.uuidGenerator = requireNonNull(uuidGenerator);\n    }\n\n    @Override\n    public void tx(Tx t) {\n        super.tx(t);\n    }\n\n    public JsonStoreEntry get(UUID storeId) {\n        return buildSelect(dsl())\n                .where(JSON_STORES.JSON_STORE_ID.eq(storeId))\n                .fetchOne(JsonStoreDao::toEntity);\n    }\n\n    public JsonStoreEntry get(UUID orgId, String storeName) {\n        return buildSelect(dsl())\n                .where(JSON_STORES.JSON_STORE_NAME.eq(storeName)\n                        .and(JSON_STORES.ORG_ID.eq(orgId)))\n                .fetchOne(JsonStoreDao::toEntity);\n    }\n\n    public UUID getId(UUID orgId, String storeName) {\n        return dsl().select(JSON_STORES.JSON_STORE_ID)\n                .from(JSON_STORES)\n                .where(JSON_STORES.JSON_STORE_NAME.eq(storeName).and(JSON_STORES.ORG_ID.eq(orgId)))\n                .fetchOne(JSON_STORES.JSON_STORE_ID);\n    }\n\n    public List<JsonStoreEntry> list(UUID orgId, UUID currentUserId, int offset, int limit, String filter) {\n        Organizations o = ORGANIZATIONS.as(\"o\");\n        JsonStores j = JSON_STORES.as(\"j\");\n        Users u = USERS.as(\"u\");\n\n        SelectJoinStep<Record10<UUID, String, UUID, String, String, UUID, String, String, String, String>> q = buildSelect(dsl(), o, j, u);\n\n        if (currentUserId != null) {\n            // public stores are visible for anyone\n            Condition isPublic = j.VISIBILITY.eq(JsonStoreVisibility.PUBLIC.toString());\n\n            // check if the user belongs to a team in the org\n            SelectConditionStep<Record1<UUID>> teamIds = select(TEAMS.TEAM_ID)\n                    .from(TEAMS)\n                    .where(TEAMS.ORG_ID.eq(orgId));\n\n            Condition isInATeam = exists(selectOne().from(Tables.V_USER_TEAMS)\n                    .where(Tables.V_USER_TEAMS.USER_ID.eq(currentUserId)\n                            .and(Tables.V_USER_TEAMS.TEAM_ID.in(teamIds))));\n\n            // check if the user owns stores in the org\n            Condition ownsStores = j.OWNER_ID.eq(currentUserId);\n\n            // check if the user owns the org\n            Condition ownsOrg = o.OWNER_ID.eq(currentUserId);\n\n            // if any of those conditions true then the store must be visible\n            q.where(or(isPublic, isInATeam, ownsStores, ownsOrg));\n        }\n\n        if (filter != null) {\n            q.where(j.JSON_STORE_NAME.containsIgnoreCase(filter));\n        }\n\n        if (offset >= 0) {\n            q.offset(offset);\n        }\n\n        if (limit > 0) {\n            q.limit(limit);\n        }\n\n        return q.where(j.ORG_ID.eq(orgId))\n                .orderBy(j.JSON_STORE_NAME)\n                .fetch(JsonStoreDao::toEntity);\n    }\n\n    public UUID insert(UUID orgId, String name, JsonStoreVisibility visibility, UUID ownerId) {\n        return txResult(tx -> insert(tx, orgId, name, visibility, ownerId));\n    }\n\n    public void update(UUID storeId, String name, JsonStoreVisibility visibility, UUID orgId, UUID ownerId) {\n        tx(tx -> update(tx, storeId, name, visibility, orgId, ownerId));\n    }\n\n    public void delete(UUID id) {\n        tx(tx -> delete(tx, id));\n    }\n\n\n    public boolean hasAccessLevel(UUID storeId, UUID userId, ResourceAccessLevel[] levels) {\n        return hasAccessLevel(dsl(), storeId, userId, levels);\n    }\n\n    public List<ResourceAccessEntry> getAccessLevel(UUID storeId) {\n        List<ResourceAccessEntry> resourceAccessList = new ArrayList<>();\n\n        Result<Record5<UUID, UUID, String, String, String>> teamsAccess = dsl().select(\n                JSON_STORE_TEAM_ACCESS.TEAM_ID,\n                JSON_STORE_TEAM_ACCESS.JSON_STORE_ID,\n                Tables.TEAMS.TEAM_NAME,\n                ORGANIZATIONS.ORG_NAME,\n                JSON_STORE_TEAM_ACCESS.ACCESS_LEVEL)\n                .from(JSON_STORE_TEAM_ACCESS)\n                .leftOuterJoin(Tables.TEAMS).on(Tables.TEAMS.TEAM_ID.eq(JSON_STORE_TEAM_ACCESS.TEAM_ID))\n                .leftOuterJoin(JSON_STORES).on(JSON_STORES.JSON_STORE_ID.eq(storeId))\n                .leftOuterJoin(ORGANIZATIONS).on(ORGANIZATIONS.ORG_ID.eq(JSON_STORES.ORG_ID))\n                .where(JSON_STORE_TEAM_ACCESS.JSON_STORE_ID.eq(storeId))\n                .fetch();\n\n        for (Record5<UUID, UUID, String, String, String> t : teamsAccess) {\n            resourceAccessList.add(new ResourceAccessEntry(t.get(JSON_STORE_TEAM_ACCESS.TEAM_ID),\n                    t.get(ORGANIZATIONS.ORG_NAME),\n                    t.get(Tables.TEAMS.TEAM_NAME),\n                    ResourceAccessLevel.valueOf(t.get(JSON_STORE_TEAM_ACCESS.ACCESS_LEVEL))));\n        }\n\n        return resourceAccessList;\n    }\n\n    public void deleteTeamAccess(DSLContext tx, UUID storeId) {\n        tx.deleteFrom(JSON_STORE_TEAM_ACCESS)\n                .where(JSON_STORE_TEAM_ACCESS.JSON_STORE_ID.eq(storeId))\n                .execute();\n    }\n\n    public void upsertAccessLevel(UUID storeId, UUID teamId, ResourceAccessLevel level) {\n        tx(tx -> upsertAccessLevel(tx, storeId, teamId, level));\n    }\n\n    public void upsertAccessLevel(DSLContext tx, UUID storeId, UUID teamId, ResourceAccessLevel level) {\n        tx.insertInto(JSON_STORE_TEAM_ACCESS)\n                .columns(JSON_STORE_TEAM_ACCESS.JSON_STORE_ID, JSON_STORE_TEAM_ACCESS.TEAM_ID, JSON_STORE_TEAM_ACCESS.ACCESS_LEVEL)\n                .values(storeId, teamId, level.toString())\n                .onDuplicateKeyUpdate()\n                .set(JSON_STORE_TEAM_ACCESS.ACCESS_LEVEL, level.toString())\n                .execute();\n    }\n\n    public Integer count(UUID orgId) {\n        return txResult(tx -> tx.selectCount()\n                .from(JSON_STORES)\n                .where(JSON_STORES.ORG_ID.eq(orgId))\n                .fetchOne(Record1::value1));\n    }\n\n    private UUID insert(DSLContext tx, UUID orgId, String name, JsonStoreVisibility visibility, UUID ownerId) {\n        UUID jsonStoreId = uuidGenerator.generate();\n        return tx.insertInto(JSON_STORES)\n                .columns(JSON_STORES.JSON_STORE_ID, JSON_STORES.OWNER_ID, JSON_STORES.JSON_STORE_NAME, JSON_STORES.ORG_ID, JSON_STORES.VISIBILITY)\n                .values(jsonStoreId, ownerId, name, orgId, visibility.name())\n                .returning(JSON_STORES.JSON_STORE_ID)\n                .fetchOne()\n                .getJsonStoreId();\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private void update(DSLContext tx, UUID storeId, String name, JsonStoreVisibility visibility, UUID orgId, UUID ownerId) {\n        UpdateSetFirstStep<JsonStoresRecord> s = tx.update(JSON_STORES);\n\n        if (name != null) {\n            s.set(JSON_STORES.JSON_STORE_NAME, name);\n        }\n\n        if (visibility != null) {\n            s.set(JSON_STORES.VISIBILITY, visibility.name());\n        }\n\n        if (ownerId != null) {\n            s.set(JSON_STORES.OWNER_ID, ownerId);\n        }\n        if (orgId != null) {\n            s.set(JSON_STORES.ORG_ID, orgId);\n        }\n\n        ((UpdateSetMoreStep<JsonStoresRecord>) s)\n                .where(JSON_STORES.JSON_STORE_ID.eq(storeId))\n                .execute();\n    }\n\n    private void delete(DSLContext tx, UUID id) {\n        tx.deleteFrom(JSON_STORES)\n                .where(JSON_STORES.JSON_STORE_ID.eq(id))\n                .execute();\n    }\n\n    private boolean hasAccessLevel(DSLContext tx, UUID storeId, UUID userId, ResourceAccessLevel... levels) {\n        SelectConditionStep<Record1<UUID>> teamIds = select(V_USER_TEAMS.TEAM_ID)\n                .from(V_USER_TEAMS)\n                .where(V_USER_TEAMS.USER_ID.eq(userId));\n\n        return tx.fetchExists(selectFrom(JSON_STORE_TEAM_ACCESS)\n                .where(JSON_STORE_TEAM_ACCESS.JSON_STORE_ID.eq(storeId)\n                        .and(JSON_STORE_TEAM_ACCESS.TEAM_ID.in(teamIds))\n                        .and(JSON_STORE_TEAM_ACCESS.ACCESS_LEVEL.in(Utils.toString(levels)))));\n    }\n\n    private static SelectJoinStep<Record10<UUID, String, UUID, String, String, UUID, String, String, String, String>> buildSelect(DSLContext tx) {\n        return buildSelect(tx, ORGANIZATIONS, JSON_STORES, USERS);\n    }\n\n    private static SelectJoinStep<Record10<UUID, String, UUID, String, String, UUID, String, String, String, String>> buildSelect(DSLContext tx,\n                                                                                                                                  Organizations orgAlias,\n                                                                                                                                  JsonStores jsonStoreAlias,\n                                                                                                                                  Users userAlias) {\n        return tx.select(jsonStoreAlias.JSON_STORE_ID,\n                jsonStoreAlias.JSON_STORE_NAME,\n                jsonStoreAlias.ORG_ID,\n                orgAlias.ORG_NAME,\n                jsonStoreAlias.VISIBILITY,\n                jsonStoreAlias.OWNER_ID,\n                userAlias.USERNAME,\n                userAlias.DOMAIN,\n                userAlias.DISPLAY_NAME,\n                userAlias.USER_TYPE)\n                .from(jsonStoreAlias)\n                .leftJoin(userAlias).on(userAlias.USER_ID.eq(jsonStoreAlias.OWNER_ID))\n                .leftJoin(orgAlias).on(orgAlias.ORG_ID.eq(jsonStoreAlias.ORG_ID));\n    }\n\n    private static JsonStoreEntry toEntity(Record r) {\n        return JsonStoreEntry.builder()\n                .id(r.getValue(JSON_STORES.JSON_STORE_ID))\n                .name(r.getValue(JSON_STORES.JSON_STORE_NAME))\n                .orgId(r.getValue(JSON_STORES.ORG_ID))\n                .orgName(r.getValue(ORGANIZATIONS.ORG_NAME))\n                .visibility(JsonStoreVisibility.valueOf(r.getValue(JSON_STORES.VISIBILITY)))\n                .owner(toOwner(r.get(JSON_STORES.OWNER_ID), r.get(USERS.USERNAME), r.get(USERS.DOMAIN), r.get(USERS.DISPLAY_NAME), r.get(USERS.USER_TYPE)))\n                .build();\n    }\n\n    private static EntityOwner toOwner(UUID id, String username, String domain, String displayName, String userType) {\n        if (id == null) {\n            return null;\n        }\n\n        return EntityOwner.builder()\n                .id(id)\n                .username(username)\n                .userDomain(domain)\n                .displayName(displayName)\n                .userType(UserType.valueOf(userType))\n                .build();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/jsonstore/JsonStoreDataDao.java",
    "content": "package com.walmartlabs.concord.server.org.jsonstore;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.jooq.tables.JsonStoreData;\nimport org.jooq.*;\n\nimport javax.inject.Inject;\nimport java.math.BigDecimal;\nimport java.util.List;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.tables.JsonStoreData.JSON_STORE_DATA;\nimport static org.jooq.impl.DSL.coalesce;\nimport static org.jooq.impl.DSL.sum;\n\npublic class JsonStoreDataDao extends AbstractDao {\n\n    private final ConcordObjectMapper objectMapper;\n\n    @Inject\n    public JsonStoreDataDao(@MainDB Configuration cfg,\n                            ConcordObjectMapper objectMapper) {\n\n        super(cfg);\n        this.objectMapper = objectMapper;\n    }\n\n    public Long getItemSize(UUID storeId, String itemPath) {\n        return dsl().select(JSON_STORE_DATA.ITEM_DATA_SIZE)\n                .from(JSON_STORE_DATA)\n                .where(JSON_STORE_DATA.JSON_STORE_ID.eq(storeId)\n                        .and(JSON_STORE_DATA.ITEM_PATH.eq(itemPath)))\n                .fetchOne(JSON_STORE_DATA.ITEM_DATA_SIZE);\n    }\n\n    public Object get(UUID storeId, String itemPath) {\n        return txResult(tx -> {\n            JsonStoreData i = JSON_STORE_DATA.as(\"i\");\n            return tx.select(i.ITEM_DATA.cast(String.class))\n                    .from(i)\n                    .where(i.JSON_STORE_ID.eq(storeId).and(i.ITEM_PATH.eq(itemPath)))\n                    .fetchOne(Record1::value1);\n        });\n    }\n\n    public List<JsonStoreDataEntry> list(UUID storeId) {\n        return dsl().select(JSON_STORE_DATA.ITEM_PATH, JSON_STORE_DATA.ITEM_DATA)\n                .from(JSON_STORE_DATA)\n                .where(JSON_STORE_DATA.JSON_STORE_ID.eq(storeId))\n                .fetch(this::toDataEntry);\n    }\n\n    public List<String> listPath(UUID storeId, int offset, int limit, String filter) {\n        SelectJoinStep<Record1<String>> q = dsl().select(JSON_STORE_DATA.ITEM_PATH)\n                .from(JSON_STORE_DATA);\n\n        if (filter != null) {\n            q.where(JSON_STORE_DATA.ITEM_PATH.containsIgnoreCase(filter));\n        }\n\n        if (offset >= 0) {\n            q.offset(offset);\n        }\n\n        if (limit > 0) {\n            q.limit(limit);\n        }\n\n        return q.where(JSON_STORE_DATA.JSON_STORE_ID.eq(storeId))\n                .orderBy(JSON_STORE_DATA.ITEM_PATH)\n                .fetch(Record1::value1);\n    }\n\n    public void upsert(UUID storeId, String itemPath, String data) {\n        tx(tx -> tx.insertInto(JSON_STORE_DATA)\n                .columns(JSON_STORE_DATA.JSON_STORE_ID, JSON_STORE_DATA.ITEM_PATH, JSON_STORE_DATA.ITEM_DATA, JSON_STORE_DATA.ITEM_DATA_SIZE)\n                .values(storeId, itemPath, objectMapper.jsonStringToJSONB(data), (long) data.length())\n                .onDuplicateKeyUpdate()\n                .set(JSON_STORE_DATA.ITEM_DATA, objectMapper.jsonStringToJSONB(data))\n                .set(JSON_STORE_DATA.ITEM_DATA_SIZE, (long) data.length())\n                .execute());\n    }\n\n    public Long getSize(UUID storeId) {\n        return txResult(tx -> tx.select(coalesce(sum(JSON_STORE_DATA.ITEM_DATA_SIZE), BigDecimal.ZERO))\n                .from(JSON_STORE_DATA)\n                .where(JSON_STORE_DATA.JSON_STORE_ID.eq(storeId))\n                .fetchOne(r -> r.value1().longValue()));\n    }\n\n    public boolean delete(UUID storeId, String itemPath) {\n        return txResult(tx -> tx.deleteFrom(JSON_STORE_DATA)\n                .where(JSON_STORE_DATA.JSON_STORE_ID.eq(storeId)\n                        .and(JSON_STORE_DATA.ITEM_PATH.eq(itemPath)))\n                .execute() > 0);\n    }\n\n    private JsonStoreDataEntry toDataEntry(Record2<String, JSONB> r) {\n        return JsonStoreDataEntry.builder()\n                .path(r.value1())\n                .data(objectMapper.fromJSONB(r.value2(), Object.class))\n                .build();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/jsonstore/JsonStoreDataEntry.java",
    "content": "package com.walmartlabs.concord.server.org.jsonstore;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.server.ApiEntity;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\n\n@Value.Immutable\n@JsonSerialize(as = ImmutableJsonStoreDataEntry.class)\n@JsonDeserialize(as = ImmutableJsonStoreDataEntry.class)\n@ApiEntity\npublic interface JsonStoreDataEntry extends Serializable {\n\n    String path();\n\n    Object data();\n\n    static ImmutableJsonStoreDataEntry.Builder builder() {\n        return ImmutableJsonStoreDataEntry.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/jsonstore/JsonStoreDataManager.java",
    "content": "package com.walmartlabs.concord.server.org.jsonstore;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.policyengine.CheckResult;\nimport com.walmartlabs.concord.policyengine.JsonStoreRule;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.audit.AuditAction;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.audit.AuditObject;\nimport com.walmartlabs.concord.server.org.OrganizationEntry;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.policy.EntityAction;\nimport com.walmartlabs.concord.server.policy.EntityType;\nimport com.walmartlabs.concord.server.policy.PolicyManager;\nimport com.walmartlabs.concord.server.policy.PolicyUtils;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\n\nimport javax.inject.Inject;\nimport java.text.MessageFormat;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.UUID;\n\npublic class JsonStoreDataManager {\n\n    private static final String DEFAULT_POLICY_MESSAGE = \"Maximum data size in the JSON store exceeded: current {0}, limit {1}\";\n\n    private final ConcordObjectMapper objectMapper;\n    private final PolicyManager policyManager;\n    private final OrganizationManager orgManager;\n    private final JsonStoreAccessManager jsonStoreAccessManager;\n    private final JsonStoreDataDao storeDataDao;\n    private final AuditLog auditLog;\n\n    @Inject\n    public JsonStoreDataManager(ConcordObjectMapper objectMapper,\n                                PolicyManager policyManager,\n                                OrganizationManager orgManager,\n                                JsonStoreAccessManager jsonStoreAccessManager,\n                                JsonStoreDataDao storeDataDao,\n                                AuditLog auditLog) {\n\n        this.objectMapper = objectMapper;\n        this.policyManager = policyManager;\n        this.orgManager = orgManager;\n        this.jsonStoreAccessManager = jsonStoreAccessManager;\n        this.storeDataDao = storeDataDao;\n        this.auditLog = auditLog;\n    }\n\n    public Object getItem(String orgName, String storeName, String itemPath) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n        JsonStoreEntry store = jsonStoreAccessManager.assertAccess(org.getId(), null, storeName, ResourceAccessLevel.READER, true);\n        return storeDataDao.get(store.id(), itemPath);\n    }\n\n    public List<String> listItems(String orgName, String storeName, int offset, int limit, String filter) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n        JsonStoreEntry store = jsonStoreAccessManager.assertAccess(org.getId(), null, storeName, ResourceAccessLevel.READER, true);\n        return storeDataDao.listPath(store.id(), offset, limit, filter);\n    }\n\n    public OperationResult createOrUpdate(String orgName, String storeName, String itemPath, Object data) {\n        if (data == null) {\n            throw new ValidationErrorsException(\"JSON Store entries cannot be null.\");\n        }\n\n        // we expect all entries to be proper JSON objects\n        if (!(data instanceof Map)) {\n            throw new ValidationErrorsException(\"All JSON Store entries must be valid JSON objects. Got: \" + data.getClass());\n        }\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n        JsonStoreEntry store = jsonStoreAccessManager.assertAccess(org.getId(), null, storeName, ResourceAccessLevel.WRITER, true);\n\n        String jsonData = objectMapper.toString(data);\n        policyManager.checkEntity(org.getId(), null, EntityType.JSON_STORE_ITEM, EntityAction.UPDATE, null, PolicyUtils.jsonStoreItemToMap(org, store, itemPath, jsonData));\n\n        Long currentItemSize = storeDataDao.getItemSize(store.id(), itemPath);\n        assertStorageDataPolicy(org.getId(), store.id(), currentItemSize == null ? 0 : currentItemSize, jsonData);\n\n        storeDataDao.upsert(store.id(), itemPath, jsonData);\n\n        addAuditLog(currentItemSize != null ? AuditAction.UPDATE : AuditAction.CREATE, org.getId(), store.id(), itemPath);\n        return currentItemSize != null ? OperationResult.UPDATED : OperationResult.CREATED;\n    }\n\n    public boolean delete(String orgName, String storeName, String itemPath) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n        JsonStoreEntry store = jsonStoreAccessManager.assertAccess(org.getId(), null, storeName, ResourceAccessLevel.WRITER, true);\n\n        boolean deleted = storeDataDao.delete(store.id(), itemPath);\n        if (deleted) {\n            addAuditLog(AuditAction.DELETE, org.getId(), store.id(), itemPath);\n        }\n\n        return deleted;\n    }\n\n    private void assertStorageDataPolicy(UUID orgId, UUID storeId, long currentItemSize, String jsonData) {\n        PolicyEngine policy = policyManager.get(orgId, null, UserPrincipal.assertCurrent().getUser().getId());\n        if (policy == null) {\n            return;\n        }\n\n        CheckResult<JsonStoreRule.StoreDataRule, Long> result;\n        try {\n            result = policy.getJsonStoragePolicy().checkStorageData(() -> storeDataDao.getSize(storeId) - currentItemSize + jsonData.length());\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n\n        if (!result.getDeny().isEmpty()) {\n            throw new ConcordApplicationException(\"Found JSON store policy violations: \" + buildErrorMessage(result.getDeny()));\n        }\n    }\n\n    private static String buildErrorMessage(List<CheckResult.Item<JsonStoreRule.StoreDataRule, Long>> errors) {\n        StringBuilder sb = new StringBuilder();\n        for (CheckResult.Item<JsonStoreRule.StoreDataRule, Long> e : errors) {\n            JsonStoreRule.StoreDataRule r = e.getRule();\n\n            String msg = r.msg() != null ? r.msg() : DEFAULT_POLICY_MESSAGE;\n            Long actual = e.getEntity();\n            Long max = r.maxSizeInBytes();\n\n            sb.append(MessageFormat.format(Objects.requireNonNull(msg), actual, max)).append(';');\n        }\n        return sb.toString();\n    }\n\n    private void addAuditLog(AuditAction auditAction, UUID orgId, UUID storeId, String itemPath) {\n        auditLog.add(AuditObject.JSON_STORE_DATA, auditAction)\n                .field(\"orgId\", orgId)\n                .field(\"jsonStoreId\", storeId)\n                .field(\"itemPath\", itemPath)\n                .log();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/jsonstore/JsonStoreDataResource.java",
    "content": "package com.walmartlabs.concord.server.org.jsonstore;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.GenericOperationResult;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport java.util.List;\n\n@Path(\"/api/v1/org\")\n@Tag(name = \"JsonStoreData\")\npublic class JsonStoreDataResource implements Resource {\n\n    private final JsonStoreDataManager storeDataManager;\n\n    @Inject\n    public JsonStoreDataResource(JsonStoreDataManager storeDataManager) {\n        this.storeDataManager = storeDataManager;\n    }\n\n    /**\n     * Returns an existing store data.\n     *\n     * @param orgName   organization's name\n     * @param storeName store's name\n     * @param itemPath  data item path\n     * @return\n     */\n    @GET\n    @Path(\"/{orgName}/jsonstore/{storeName}/item/{itemPath:.*}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Get store data\", operationId = \"getJsonStoreData\")\n    public Object get(@PathParam(\"orgName\") String orgName,\n                      @PathParam(\"storeName\") String storeName,\n                      @PathParam(\"itemPath\") String itemPath) {\n\n        return storeDataManager.getItem(orgName, storeName, itemPath);\n    }\n\n    /**\n     * List items in a store.\n     *\n     * @param orgName   organization's name\n     * @param storeName store's name\n     * @return\n     */\n    @GET\n    @Path(\"/{orgName}/jsonstore/{storeName}/item\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"List items in a JSON store\", operationId = \"listJsonStoreData\")\n    public List<String> list(@PathParam(\"orgName\") String orgName,\n                             @PathParam(\"storeName\") String storeName,\n                             @QueryParam(\"offset\") @DefaultValue(\"0\") int offset,\n                             @QueryParam(\"limit\") @DefaultValue(\"30\") int limit,\n                             @QueryParam(\"filter\") String filter) {\n\n        return storeDataManager.listItems(orgName, storeName, offset, limit, filter);\n    }\n\n    /**\n     * Update an item in a store.\n     *\n     * @param orgName   organization's name\n     * @param storeName store's name\n     * @param itemPath  store's data path\n     * @param data      store's data, must be a valid JSON object (represented by a Map)\n     * @return\n     */\n    @PUT\n    @Path(\"/{orgName}/jsonstore/{storeName}/item/{itemPath:.*}\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Update an item in a store\", operationId = \"updateJsonStoreData\")\n    public GenericOperationResult data(@PathParam(\"orgName\") String orgName,\n                                       @PathParam(\"storeName\") String storeName,\n                                       @PathParam(\"itemPath\") String itemPath,\n                                       Object data) {\n\n        OperationResult result = storeDataManager.createOrUpdate(orgName, storeName, itemPath, data);\n\n        return new GenericOperationResult(result);\n    }\n\n    /**\n     * Same as {@link #data(String, String, String, Object)}.\n     */\n    @POST\n    @Path(\"/{orgName}/jsonstore/{storeName}/item/{itemPath:.*}\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    public GenericOperationResult update(@PathParam(\"orgName\") String orgName,\n                                         @PathParam(\"storeName\") String storeName,\n                                         @PathParam(\"itemPath\") String itemPath,\n                                         Object data) {\n\n        return data(orgName, storeName, itemPath, data);\n    }\n\n    /**\n     * Remove an item from a store.\n     *\n     * @param orgName   organization's name\n     * @param storeName store's name\n     * @param itemPath  store's data path\n     * @return\n     */\n    @DELETE\n    @Path(\"/{orgName}/jsonstore/{storeName}/item/{itemPath:.*}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Remove an item from a store\", operationId = \"deleteJsonStoreDataItem\")\n    public GenericOperationResult delete(@PathParam(\"orgName\") String orgName,\n                                         @PathParam(\"storeName\") String storeName,\n                                         @PathParam(\"itemPath\") String itemPath) {\n\n        boolean deleted = storeDataManager.delete(orgName, storeName, itemPath);\n        return new GenericOperationResult(deleted ? OperationResult.DELETED : OperationResult.NOT_FOUND);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/jsonstore/JsonStoreEntry.java",
    "content": "package com.walmartlabs.concord.server.org.jsonstore;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.server.org.EntityOwner;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonSerialize(as = ImmutableJsonStoreEntry.class)\n@JsonDeserialize(as = ImmutableJsonStoreEntry.class)\n@JsonInclude(JsonInclude.Include.NON_NULL)\npublic interface JsonStoreEntry extends Serializable  {\n\n    long serialVersionUID = 1L;\n\n    UUID id();\n\n    @ConcordKey\n    String name();\n\n    UUID orgId();\n\n    @ConcordKey\n    String orgName();\n\n    @Value.Default\n    default JsonStoreVisibility visibility() {\n        return JsonStoreVisibility.PUBLIC;\n    }\n\n    @Nullable\n    EntityOwner owner();\n\n    static ImmutableJsonStoreEntry.Builder builder() {\n        return ImmutableJsonStoreEntry.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/jsonstore/JsonStoreManager.java",
    "content": "package com.walmartlabs.concord.server.org.jsonstore;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.collect.ImmutableMap;\nimport com.walmartlabs.concord.policyengine.CheckResult;\nimport com.walmartlabs.concord.policyengine.JsonStoreRule;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.audit.AuditAction;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.audit.AuditObject;\nimport com.walmartlabs.concord.server.org.*;\nimport com.walmartlabs.concord.server.org.team.TeamDao;\nimport com.walmartlabs.concord.server.policy.EntityAction;\nimport com.walmartlabs.concord.server.policy.EntityType;\nimport com.walmartlabs.concord.server.policy.PolicyManager;\nimport com.walmartlabs.concord.server.policy.PolicyUtils;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.user.UserEntry;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport com.walmartlabs.concord.server.user.UserType;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\nimport java.text.MessageFormat;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic class JsonStoreManager {\n\n    private static final String DEFAULT_POLICY_MESSAGE = \"Maximum number of JSON stores exceeded: current {0}, limit {1}\";\n\n    private final PolicyManager policyManager;\n    private final JsonStoreAccessManager jsonStoreAccessManager;\n    private final OrganizationManager orgManager;\n    private final UserManager userManager;\n    private final AuditLog auditLog;\n    private final JsonStoreDao storeDao;\n    private final JsonStoreDataDao storeDataDao;\n    private final OrganizationDao orgDao;\n    private final TeamDao teamDao;\n\n    @Inject\n    public JsonStoreManager(PolicyManager policyManager,\n                            JsonStoreAccessManager jsonStoreAccessManager,\n                            OrganizationManager orgManager,\n                            UserManager userManager,\n                            AuditLog auditLog,\n                            JsonStoreDao storeDao,\n                            JsonStoreDataDao storeDataDao,\n                            OrganizationDao orgDao,\n                            TeamDao teamDao) {\n\n        this.policyManager = policyManager;\n        this.jsonStoreAccessManager = jsonStoreAccessManager;\n        this.orgManager = orgManager;\n        this.userManager = userManager;\n        this.auditLog = auditLog;\n        this.storeDao = storeDao;\n        this.storeDataDao = storeDataDao;\n        this.orgDao = orgDao;\n        this.teamDao = teamDao;\n    }\n\n    public List<JsonStoreEntry> list(String orgName, int offset, int limit, String filter) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n\n        UserPrincipal p = UserPrincipal.assertCurrent();\n        UUID userId = p.getId();\n        if (Roles.isAdmin() || Roles.isGlobalReader() || Roles.isGlobalWriter()) {\n            // admins or \"global readers\" can see any stores, so we shouldn't filter stores by user\n            userId = null;\n        }\n\n        return storeDao.list(org.getId(), userId, offset, limit, filter);\n    }\n\n    public JsonStoreEntry get(String orgName, String storeName) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n        return jsonStoreAccessManager.assertAccess(org.getId(), null, storeName, ResourceAccessLevel.READER, true);\n    }\n\n    public JsonStoreCapacity getCapacity(String orgName, String storeName) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n        JsonStoreEntry store = jsonStoreAccessManager.assertAccess(org.getId(), null, storeName, ResourceAccessLevel.READER, true);\n\n        long currentSize = storeDataDao.getSize(store.id());\n\n        PolicyEngine policy = policyManager.get(store.id(), null, UserPrincipal.assertCurrent().getId());\n        Long maxSize = null;\n        if (policy != null) {\n            maxSize = policy.getJsonStoragePolicy().getMaxSize();\n        }\n\n        return JsonStoreCapacity.builder()\n                .size(currentSize)\n                .maxSize(maxSize)\n                .build();\n    }\n\n    public void delete(String orgName, String storeName) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n\n        JsonStoreEntry store = jsonStoreAccessManager.assertAccess(org.getId(), null, storeName, ResourceAccessLevel.OWNER, true);\n\n        storeDao.delete(store.id());\n\n        addAuditLog(AuditAction.DELETE, org.getId(), store.id(), store.name());\n    }\n\n    public OperationResult createOrUpdate(String orgName, JsonStoreRequest entry) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n\n        UUID storeId = entry.id();\n        String storeName = entry.name();\n\n        if (storeId == null && storeName != null) {\n            storeId = storeDao.getId(org.getId(), storeName);\n        }\n\n        if (storeId != null) {\n            update(orgName, storeId, entry);\n            return OperationResult.UPDATED;\n        } else {\n            JsonStoreVisibility visibility = entry.visibility();\n            if (visibility == null) {\n                visibility = JsonStoreVisibility.PRIVATE;\n            }\n\n            insert(orgName, entry.name(), visibility, entry.owner());\n            return OperationResult.CREATED;\n        }\n    }\n\n    public UUID insert(String orgName, String storeName, JsonStoreVisibility visibility, EntityOwner entityOwner) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n\n        UserEntry owner = getOwner(entityOwner, UserPrincipal.assertCurrent().getUser());\n\n        policyManager.checkEntity(org.getId(), null, EntityType.JSON_STORE, EntityAction.CREATE, owner, PolicyUtils.jsonStoreToMap(org.getId(), storeName, visibility, owner));\n\n        assertStoragePolicy(org.getId());\n\n        UUID id = storeDao.insert(org.getId(), storeName, visibility, owner.getId());\n\n        addAuditLog(AuditAction.CREATE, org.getId(), id, storeName);\n\n        return id;\n    }\n\n    public void update(String orgName, UUID storeId, JsonStoreRequest entry) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n\n        JsonStoreEntry prevStorage = storeDao.get(storeId);\n        String prevStoreName = prevStorage.name();\n\n        UserEntry owner = getOwner(entry.owner(), null);\n        policyManager.checkEntity(org.getId(), null, EntityType.JSON_STORE, EntityAction.UPDATE, owner, PolicyUtils.jsonStoreToMap(org.getId(), prevStoreName, entry.visibility(), owner));\n\n        UUID currentOwnerId = entry.owner() != null ? entry.owner().id() : null;\n        UUID updatedOwnerId = owner != null ? owner.getId() : null;\n\n        ResourceAccessLevel level = ResourceAccessLevel.WRITER;\n        if (updatedOwnerId != null && !updatedOwnerId.equals(currentOwnerId)) {\n            level = ResourceAccessLevel.OWNER;\n        }\n\n        prevStorage = jsonStoreAccessManager.assertAccess(org.getId(), null, prevStoreName, level, true);\n        if (prevStorage == null) {\n            throw new ValidationErrorsException(\"Can't find a JSON store '\" + prevStoreName + \"' in organization '\" + org.getName() + \"'\");\n        }\n\n        UUID orgIdToUpdate = null;\n        if (entry.orgName() != null) {\n            OrganizationEntry organizationEntry = orgManager.assertAccess(entry.orgName(), true);\n            if (organizationEntry.getId() != prevStorage.orgId()) {\n                orgIdToUpdate = organizationEntry.getId();\n            }\n        }\n\n        if (orgIdToUpdate != null) {\n            assertStoragePolicy(orgIdToUpdate);\n        }\n\n        storeDao.update(prevStorage.id(), entry.name(), entry.visibility(), orgIdToUpdate, updatedOwnerId);\n\n        JsonStoreEntry newStorage = storeDao.get(prevStorage.id());\n        addAuditLog(AuditAction.UPDATE, prevStorage.orgId(), prevStorage.id(), prevStoreName, prevStorage, newStorage);\n    }\n\n    public List<ResourceAccessEntry> getResourceAccess(String orgName, String storeName) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n\n        JsonStoreEntry store = jsonStoreAccessManager.assertAccess(org.getId(), null, storeName, ResourceAccessLevel.READER, false);\n        return storeDao.getAccessLevel(store.id());\n    }\n\n    public void updateAccessLevel(String orgName, String storeName, Collection<ResourceAccessEntry> entries, boolean isReplace) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n\n        JsonStoreEntry store = jsonStoreAccessManager.assertAccess(org.getId(), null, storeName, ResourceAccessLevel.OWNER, true);\n\n        storeDao.tx(tx -> {\n            if (isReplace) {\n                storeDao.deleteTeamAccess(tx, store.id());\n            }\n\n            for (ResourceAccessEntry e : entries) {\n                storeDao.upsertAccessLevel(tx, store.id(), e.getTeamId(), e.getLevel());\n            }\n        });\n\n        addAuditLog(store.id(), entries, isReplace);\n    }\n\n    public void updateAccessLevel(String orgName, String storeName, ResourceAccessEntry entry) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n\n        JsonStoreEntry store = jsonStoreAccessManager.assertAccess(org.getId(), null, storeName, ResourceAccessLevel.OWNER, true);\n\n        UUID teamId = ResourceAccessUtils.getTeamId(orgDao, teamDao, org.getId(), entry);\n        storeDao.upsertAccessLevel(store.id(), teamId, entry.getLevel());\n\n        addAuditLog(store.id(), Collections.singleton(new ResourceAccessEntry(teamId, null, null, entry.getLevel())), false);\n    }\n\n    private UserEntry getOwner(EntityOwner owner, UserEntry defaultOwner) {\n        if (owner == null) {\n            return defaultOwner;\n        }\n\n        if (owner.id() != null) {\n            return userManager.get(owner.id())\n                    .orElseThrow(() -> new ValidationErrorsException(\"User not found: \" + owner.id()));\n        }\n\n        if (owner.username() != null) {\n            return userManager.get(owner.username(), owner.userDomain(), UserType.LDAP)\n                    .orElseThrow(() -> new ConcordApplicationException(\"User not found: \" + owner.username()));\n        }\n\n        return defaultOwner;\n    }\n\n    private void assertStoragePolicy(UUID orgId) {\n        PolicyEngine policy = policyManager.get(orgId, null, UserPrincipal.assertCurrent().getUser().getId());\n        if (policy == null) {\n            return;\n        }\n\n        CheckResult<JsonStoreRule.StoreRule, Integer> result;\n        try {\n            result = policy.getJsonStoragePolicy().checkStorage(() -> storeDao.count(orgId));\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n\n        if (!result.getDeny().isEmpty()) {\n            throw new ConcordApplicationException(\"Found JSON store policy violations: \" + buildErrorMessage(result.getDeny()));\n        }\n    }\n\n    private String buildErrorMessage(List<CheckResult.Item<JsonStoreRule.StoreRule, Integer>> errors) {\n        StringBuilder sb = new StringBuilder();\n        for (CheckResult.Item<JsonStoreRule.StoreRule, Integer> e : errors) {\n            JsonStoreRule.StoreRule r = e.getRule();\n\n            String msg = r.msg() != null ? r.msg() : DEFAULT_POLICY_MESSAGE;\n            int actualCount = e.getEntity();\n            int max = r.maxNumberPerOrg();\n\n            sb.append(MessageFormat.format(Objects.requireNonNull(msg), actualCount, max)).append(';');\n        }\n        return sb.toString();\n    }\n\n    private void addAuditLog(AuditAction auditAction, UUID orgId, UUID id, String name) {\n        addAuditLog(auditAction, orgId, id, name, null, null);\n    }\n\n    private void addAuditLog(AuditAction auditAction, UUID orgId, UUID id, String name, Object prevEntity, Object newEntity) {\n        auditLog.add(AuditObject.JSON_STORE, auditAction)\n                .changes(prevEntity, newEntity)\n                .field(\"orgId\", orgId)\n                .field(\"jsonStoreId\", id)\n                .field(\"name\", name)\n                .log();\n    }\n\n    private void addAuditLog(UUID storeId, Collection<ResourceAccessEntry> entries, boolean isReplace) {\n        List<ImmutableMap<String, ? extends Serializable>> teams = entries.stream()\n                .map(e -> ImmutableMap.of(\"id\", e.getTeamId(), \"level\", e.getLevel()))\n                .collect(Collectors.toList());\n\n        auditLog.add(AuditObject.JSON_STORE, AuditAction.UPDATE)\n                .field(\"storeId\", storeId)\n                .field(\"access\", ImmutableMap.of(\n                        \"replace\", isReplace,\n                        \"teams\", teams))\n                .log();\n    }\n\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/jsonstore/JsonStoreModule.java",
    "content": "package com.walmartlabs.concord.server.org.jsonstore;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.walmartlabs.concord.server.Utils.bindJaxRsResource;\n\npublic class JsonStoreModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        binder.bind(JsonStoreDao.class).in(SINGLETON);\n        binder.bind(JsonStoreManager.class).in(SINGLETON);\n        binder.bind(JsonStoreAccessManager.class).in(SINGLETON);\n        binder.bind(JsonStoreDataDao.class).in(SINGLETON);\n        binder.bind(JsonStoreDataManager.class).in(SINGLETON);\n        binder.bind(JsonStoreQueryDao.class).in(SINGLETON);\n        binder.bind(JsonStoreQueryExecDao.class).in(SINGLETON);\n        binder.bind(JsonStoreQueryManager.class).in(SINGLETON);\n\n\n        bindJaxRsResource(binder, JsonStoreResource.class);\n        bindJaxRsResource(binder, JsonStoreDataResource.class);\n        bindJaxRsResource(binder, JsonStoreQueryResource.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/jsonstore/JsonStoreQueryDao.java",
    "content": "package com.walmartlabs.concord.server.org.jsonstore;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.UuidGenerator;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.jooq.Record4;\nimport org.jooq.SelectJoinStep;\n\nimport javax.inject.Inject;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.tables.JsonStoreQueries.JSON_STORE_QUERIES;\nimport static java.util.Objects.requireNonNull;\nimport static org.jooq.impl.DSL.value;\n\npublic class JsonStoreQueryDao extends AbstractDao {\n\n    private final UuidGenerator uuidGenerator;\n\n    @Inject\n    public JsonStoreQueryDao(@MainDB Configuration cfg, UuidGenerator uuidGenerator) {\n        super(cfg);\n        this.uuidGenerator = requireNonNull(uuidGenerator);\n    }\n\n    public UUID getId(UUID storeId, String queryName) {\n        return dsl().select(JSON_STORE_QUERIES.QUERY_ID)\n                .from(JSON_STORE_QUERIES)\n                .where(JSON_STORE_QUERIES.JSON_STORE_ID.eq(storeId)\n                        .and(JSON_STORE_QUERIES.QUERY_NAME.eq(queryName)))\n                .fetchOne(JSON_STORE_QUERIES.QUERY_ID);\n    }\n\n    public JsonStoreQueryEntry get(UUID storeId, String queryName) {\n        Record4<UUID, String, UUID, String> r = createSelect(dsl())\n                .where(JSON_STORE_QUERIES.JSON_STORE_ID.eq(storeId)\n                        .and(JSON_STORE_QUERIES.QUERY_NAME.eq(queryName)))\n                .fetchOne();\n\n        if (r == null) {\n            return null;\n        }\n\n        return toEntry(r);\n    }\n\n    public JsonStoreQueryEntry get(UUID queryId) {\n        return createSelect(dsl())\n                .where(JSON_STORE_QUERIES.QUERY_ID.eq(queryId))\n                .fetchOne(JsonStoreQueryDao::toEntry);\n    }\n\n    public List<JsonStoreQueryEntry> list(UUID storeId, int offset, int limit, String filter) {\n        SelectJoinStep<Record4<UUID, String, UUID, String>> q = createSelect(dsl());\n\n        if (filter != null) {\n            q.where(JSON_STORE_QUERIES.QUERY_NAME.containsIgnoreCase(filter));\n        }\n\n        if (offset >= 0) {\n            q.offset(offset);\n        }\n\n        if (limit > 0) {\n            q.limit(limit);\n        }\n\n        return q\n                .where(JSON_STORE_QUERIES.JSON_STORE_ID.eq(storeId))\n                .orderBy(JSON_STORE_QUERIES.QUERY_NAME)\n                .fetch(JsonStoreQueryDao::toEntry);\n    }\n\n    public UUID insert(UUID storeId, String queryName, String text) {\n        return txResult(tx -> insert(tx, storeId, queryName, text));\n    }\n\n    public void update(UUID queryId, String text) {\n        tx(tx -> update(tx, queryId, text));\n    }\n\n    public void delete(UUID storeId, String queryName) {\n        tx(tx -> delete(tx, storeId, queryName));\n    }\n\n    private UUID insert(DSLContext tx, UUID storeId, String queryName, String text) {\n        UUID queryId = uuidGenerator.generate();\n        return tx.insertInto(JSON_STORE_QUERIES)\n                .columns(JSON_STORE_QUERIES.QUERY_ID, JSON_STORE_QUERIES.JSON_STORE_ID, JSON_STORE_QUERIES.QUERY_NAME, JSON_STORE_QUERIES.QUERY_TEXT)\n                .values(value(queryId), value(storeId), value(queryName), value(text))\n                .returning(JSON_STORE_QUERIES.QUERY_ID)\n                .fetchOne()\n                .getQueryId();\n\n    }\n\n    private void update(DSLContext tx, UUID queryId, String text) {\n        tx.update(JSON_STORE_QUERIES)\n                .set(JSON_STORE_QUERIES.QUERY_TEXT, value(text))\n                .where(JSON_STORE_QUERIES.QUERY_ID.eq(queryId))\n                .execute();\n    }\n\n    private void delete(DSLContext tx, UUID storeId, String queryName) {\n        tx.deleteFrom(JSON_STORE_QUERIES)\n                .where(JSON_STORE_QUERIES.JSON_STORE_ID.eq(storeId).and(JSON_STORE_QUERIES.QUERY_NAME.eq(queryName)))\n                .execute();\n    }\n\n    private static JsonStoreQueryEntry toEntry(Record4<UUID, String, UUID, String> r) {\n        return JsonStoreQueryEntry.builder()\n                .id(r.get(JSON_STORE_QUERIES.QUERY_ID))\n                .name(r.get(JSON_STORE_QUERIES.QUERY_NAME))\n                .storeId(r.get(JSON_STORE_QUERIES.JSON_STORE_ID))\n                .text(r.get(JSON_STORE_QUERIES.QUERY_TEXT))\n                .build();\n    }\n\n    private static SelectJoinStep<Record4<UUID, String, UUID, String>> createSelect(DSLContext tx) {\n        return tx.select(JSON_STORE_QUERIES.QUERY_ID,\n                JSON_STORE_QUERIES.QUERY_NAME,\n                JSON_STORE_QUERIES.JSON_STORE_ID,\n                JSON_STORE_QUERIES.QUERY_TEXT)\n                .from(JSON_STORE_QUERIES);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/jsonstore/JsonStoreQueryEntry.java",
    "content": "package com.walmartlabs.concord.server.org.jsonstore;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonSerialize(as = ImmutableJsonStoreQueryEntry.class)\n@JsonDeserialize(as = ImmutableJsonStoreQueryEntry.class)\n@JsonInclude(Include.NON_NULL)\npublic interface JsonStoreQueryEntry extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    UUID id();\n\n    UUID storeId();\n\n    @ConcordKey\n    String name();\n\n    String text();\n\n    static ImmutableJsonStoreQueryEntry.Builder builder() {\n        return ImmutableJsonStoreQueryEntry.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/jsonstore/JsonStoreQueryExecDao.java",
    "content": "package com.walmartlabs.concord.server.org.jsonstore;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonParseException;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.JsonStorageDB;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.jooq.QueryPart;\nimport org.jooq.Record;\nimport org.jooq.impl.DSL;\n\nimport javax.inject.Inject;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static org.jooq.impl.DSL.val;\n\n/**\n * Executes JSON Store queries.\n * <p/>\n * Uses a separate DB connection pool {@link @JsonStorageDB} to allow\n * more fine-tuned security configuration.\n * @see #execSql(UUID, String, Map, Integer)\n */\npublic class JsonStoreQueryExecDao extends AbstractDao {\n\n    private final ConcordObjectMapper objectMapper;\n    private final JsonStoreQueryDao storeQueryDao;\n\n    @Inject\n    public JsonStoreQueryExecDao(@JsonStorageDB Configuration cfg,\n                                 ConcordObjectMapper objectMapper,\n                                 JsonStoreQueryDao storeQueryDao) {\n\n        super(cfg);\n        this.objectMapper = objectMapper;\n        this.storeQueryDao = storeQueryDao;\n    }\n\n    public List<Object> exec(UUID storeId, String queryName, Map<String, Object> params)\n    {\n        JsonStoreQueryEntry q = storeQueryDao.get(storeId, queryName);\n        if (q == null) {\n            throw new ValidationErrorsException(\"Query not found: \" + queryName);\n        }\n\n        return execSql(q.storeId(), q.text(), params, null);\n    }\n\n    /**\n     * Executes the provided query. The method replaces any usage of {@code JSON_STORE_DATA}\n     * table with a restricted view {@code JSON_STORE_DATA_VIEW_RESTRICTED}. The view\n     * requires the store ID to be passed as a session parameter.\n     * <p/>\n     * This is similar to how row-level security works in PostgreSQL, but doesn't exclude\n     * the possibility of SQL injections. For production deployment it is recommended to\n     * perform additional steps:\n     * <ul>\n     * <li>configure a separate user (see {@code db.inventoryUsername} in the Server's\n     * configuration file)</li>\n     * <li>install a RLS policy similar to <pre>{@code\n     * CREATE POLICY json_store_data_restriction ON json_store_data\n     * FOR SELECT\n     * USING (json_store_id = current_setting('jsonStoreQueryExec.json_store_id'::text)::uuid);\n     * }</pre></li>\n     * <li>attach the policy to the new user {@code db.inventoryUsername}</li>\n     * </ul>\n     */\n    public List<Object> execSql(UUID storeId, String query, Map<String, Object> params, Integer maxLimit)\n    {\n        String sql = query.replaceAll(\"(?i)json_store_data\", \"json_store_data_view_restricted\");\n        if (maxLimit != null) {\n            sql = \"select * from (\" + trimEnd(sql, ';') + \") a limit \" + maxLimit;\n        }\n\n        QueryPart[] args = params != null ? new QueryPart[] {val(objectMapper.toString(params))} : new QueryPart[0];\n\n        String finalSql = sql;\n        try {\n            return dsl().transactionResult(cfg -> {\n                DSLContext tx = DSL.using(cfg);\n                tx.execute(\"set local jsonStoreQueryExec.json_store_id='\" + storeId + \"'\");\n                return tx.resultQuery(finalSql, args)\n                        .fetch(this::toExecResult);\n            });\n        } catch (Exception e) {\n            String message = restoreOriginalQuery(e.getMessage());\n            if (message == null) {\n                throw e;\n            }\n\n            throw new RuntimeException(message, e.getCause());\n        }\n    }\n\n    private Object toExecResult(Record record) {\n        Object value = record.get(0);\n        if (value == null) {\n            return null;\n        }\n\n        if (record.size() > 1) {\n            throw new ValidationErrorsException(\"Invalid query result type: expected a single column, got \" + record.size() + \" columns. \" +\n                    \"Change the query to return a single column or to build a JSON object.\");\n        }\n\n        try {\n            return objectMapper.fromString(value.toString(), Object.class);\n        } catch (RuntimeException e) {\n            if (e.getCause() instanceof JsonParseException) {\n                throw new RuntimeException(\"Invalid JSON value: \" + value + \". Expected a valid JSON object.\");\n            }\n\n            throw e;\n        }\n    }\n\n    private static String restoreOriginalQuery(String msg) {\n        if (msg == null) {\n            return null;\n        }\n        return msg.replace(\"json_store_data_view_restricted\", \"json_store_data\");\n    }\n\n    private static String trimEnd(String value, char c) {\n        int len = value.length();\n        int i = 0;\n        while ((i < len) && (value.charAt(len - 1) == c || Character.isWhitespace(value.charAt(len - 1)))) {\n            len--;\n        }\n        return value.substring(0, len);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/jsonstore/JsonStoreQueryManager.java",
    "content": "package com.walmartlabs.concord.server.org.jsonstore;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.audit.AuditAction;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.audit.AuditObject;\nimport com.walmartlabs.concord.server.org.OrganizationEntry;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.policy.EntityAction;\nimport com.walmartlabs.concord.server.policy.EntityType;\nimport com.walmartlabs.concord.server.policy.PolicyManager;\nimport com.walmartlabs.concord.server.policy.PolicyUtils;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\n\nimport javax.inject.Inject;\nimport java.util.*;\n\npublic class JsonStoreQueryManager {\n\n    private final PolicyManager policyManager;\n    private final OrganizationManager orgManager;\n    private final JsonStoreAccessManager jsonStoreAccessManager;\n    private final JsonStoreQueryDao queryDao;\n    private final JsonStoreQueryExecDao execDao;\n    private final AuditLog auditLog;\n\n    @Inject\n    public JsonStoreQueryManager(PolicyManager policyManager,\n                                 OrganizationManager orgManager,\n                                 JsonStoreAccessManager jsonStoreAccessManager,\n                                 JsonStoreQueryDao queryDao,\n                                 JsonStoreQueryExecDao execDao,\n                                 AuditLog auditLog) {\n\n        this.policyManager = policyManager;\n        this.orgManager = orgManager;\n        this.jsonStoreAccessManager = jsonStoreAccessManager;\n        this.queryDao = queryDao;\n        this.execDao = execDao;\n        this.auditLog = auditLog;\n    }\n\n    public JsonStoreQueryEntry get(String orgName, String storeName, String queryName) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n        JsonStoreEntry store = jsonStoreAccessManager.assertAccess(org.getId(), null, storeName, ResourceAccessLevel.READER, true);\n        return queryDao.get(store.id(), queryName);\n    }\n\n    public List<JsonStoreQueryEntry> list(String orgName, String storeName, int offset, int limit, String filter) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n        JsonStoreEntry store = jsonStoreAccessManager.assertAccess(org.getId(), null, storeName, ResourceAccessLevel.READER, true);\n        return queryDao.list(store.id(), offset, limit, filter);\n    }\n\n    public OperationResult createOrUpdate(String orgName, String storeName, JsonStoreQueryRequest entry) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n        JsonStoreEntry store = jsonStoreAccessManager.assertAccess(org.getId(), null, storeName, ResourceAccessLevel.READER, true);\n\n        UUID queryId = entry.id();\n        String queryName = entry.name();\n\n        String text = entry.text();\n        validateQuery(text);\n\n        if (queryId == null && queryName != null) {\n            queryId = queryDao.getId(store.id(), queryName);\n        }\n\n        if (queryId == null) {\n            policyManager.checkEntity(org.getId(), null, EntityType.JSON_STORE_QUERY, EntityAction.CREATE, null, PolicyUtils.jsonStoreQueryToMap(org, store, queryName, text));\n            queryDao.insert(store.id(), queryName, text);\n            addAuditLog(AuditAction.CREATE, org.getId(), store.id(), queryName, null, text);\n            return OperationResult.CREATED;\n        } else {\n            policyManager.checkEntity(org.getId(), null, EntityType.JSON_STORE_QUERY, EntityAction.UPDATE, null, PolicyUtils.jsonStoreQueryToMap(org, store, queryName, text));\n\n            JsonStoreQueryEntry prevEntry = queryDao.get(queryId);\n\n            String prevText = prevEntry.text();\n            if (Objects.equals(text, prevText)) {\n                // no changes\n                return OperationResult.ALREADY_EXISTS;\n            }\n\n            queryDao.update(queryId, text);\n            addAuditLog(AuditAction.UPDATE, org.getId(), store.id(), queryName, prevEntry.text(), text);\n\n            return OperationResult.UPDATED;\n        }\n    }\n\n    public void delete(String orgName, String storeName, String queryName) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n        JsonStoreEntry store = jsonStoreAccessManager.assertAccess(org.getId(), null, storeName, ResourceAccessLevel.READER, true);\n\n        UUID id = queryDao.getId(store.id(), queryName);\n        if (id == null) {\n            throw new ValidationErrorsException(\"Query not found: \" + queryName);\n        }\n\n        queryDao.delete(store.id(), queryName);\n\n        addAuditLog(AuditAction.DELETE, org.getId(), store.id(), queryName);\n    }\n\n    public List<Object> exec(String orgName, String storeName, String queryName, Map<String, Object> params) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n        JsonStoreEntry store = jsonStoreAccessManager.assertAccess(org.getId(), null, storeName, ResourceAccessLevel.READER, true);\n        return execDao.exec(store.id(), queryName, params);\n    }\n\n    public List<Object> exec(String orgName, String storeName, String text, int maxLimit) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n        JsonStoreEntry store = jsonStoreAccessManager.assertAccess(org.getId(), null, storeName, ResourceAccessLevel.READER, true);\n        return execDao.execSql(store.id(), text, null, maxLimit);\n    }\n\n    private static void validateQuery(String text) {\n        if (text == null || text.trim().isEmpty()) {\n            throw new ValidationErrorsException(\"Query should not be empty\");\n        }\n    }\n\n    private void addAuditLog(AuditAction auditAction, UUID orgId, UUID storeId, String queryName) {\n        addAuditLog(auditAction, orgId, storeId, queryName, null, null);\n    }\n\n    private void addAuditLog(AuditAction auditAction, UUID orgId, UUID storeId, String queryName, String prevQuery, String newQuery) {\n        Map<String, Object> changes = new HashMap<>();\n        changes.put(\"prevQuery\", prevQuery);\n        changes.put(\"newQuery\", newQuery);\n        auditLog.add(AuditObject.JSON_STORE_QUERY, auditAction)\n                .field(\"orgId\", orgId)\n                .field(\"jsonStoreId\", storeId)\n                .field(\"queryName\", queryName)\n                .field(\"changes\", changes)\n                .log();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/jsonstore/JsonStoreQueryRequest.java",
    "content": "package com.walmartlabs.concord.server.org.jsonstore;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.server.ApiEntity;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonSerialize(as = ImmutableJsonStoreQueryRequest.class)\n@JsonDeserialize(as = ImmutableJsonStoreQueryRequest.class)\n@JsonInclude(JsonInclude.Include.NON_NULL)\n@ApiEntity\npublic interface JsonStoreQueryRequest extends Serializable {\n\n    @Nullable\n    UUID id();\n\n    @ConcordKey\n    @Nullable\n    String name();\n\n    String text();\n\n    static ImmutableJsonStoreQueryRequest.Builder builder() {\n        return ImmutableJsonStoreQueryRequest.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/jsonstore/JsonStoreQueryResource.java",
    "content": "package com.walmartlabs.concord.server.org.jsonstore;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.server.GenericOperationResult;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.validation.Valid;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport java.util.List;\nimport java.util.Map;\n\n@Path(\"/api/v1/org\")\n@Tag(name = \"JsonStoreQuery\")\npublic class JsonStoreQueryResource implements Resource {\n\n    private final JsonStoreQueryManager storeQueryManager;\n\n    @Inject\n    public JsonStoreQueryResource(JsonStoreQueryManager storeQueryManager) {\n        this.storeQueryManager = storeQueryManager;\n    }\n\n    /**\n     * Returns an existing JSON store query\n     *\n     * @param orgName   organization's name\n     * @param storeName store's name\n     * @param queryName query's name\n     * @return query text\n     */\n    @GET\n    @Path(\"/{orgName}/jsonstore/{storeName}/query/{queryName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Get an existing JSON store query\", operationId = \"getJsonStoreQuery\")\n    public JsonStoreQueryEntry get(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                   @PathParam(\"storeName\") @ConcordKey String storeName,\n                                   @PathParam(\"queryName\") @ConcordKey String queryName) {\n\n        return storeQueryManager.get(orgName, storeName, queryName);\n    }\n\n    /**\n     * Creates or updates a JSON store query\n     *\n     * @param orgName   organization's name\n     * @param storeName store's name\n     * @param entry     the query's entry\n     * @return\n     */\n    @POST\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Path(\"/{orgName}/jsonstore/{storeName}/query\")\n    @Validate\n    @Operation(description = \"Create or update a JSON store query\", operationId = \"createOrUpdateJsonStoreQuery\")\n    public GenericOperationResult createOrUpdate(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                 @PathParam(\"storeName\") @ConcordKey String storeName,\n                                                 @Valid JsonStoreQueryRequest entry) {\n\n        OperationResult result = storeQueryManager.createOrUpdate(orgName, storeName, entry);\n        return new GenericOperationResult(result);\n    }\n\n    /**\n     * List existing queries in a JSON store.\n     *\n     * @param orgName   organization's name\n     * @param storeName store's name\n     * @return\n     */\n    @GET\n    @Path(\"/{orgName}/jsonstore/{storeName}/query\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"List JSON Store queries\", operationId = \"listJsonStoreQueries\")\n    public List<JsonStoreQueryEntry> list(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                          @PathParam(\"storeName\") @ConcordKey String storeName,\n                                          @QueryParam(\"offset\") @DefaultValue(\"0\") int offset,\n                                          @QueryParam(\"limit\") @DefaultValue(\"30\") int limit,\n                                          @QueryParam(\"filter\") String filter) {\n\n        return storeQueryManager.list(orgName, storeName, offset, limit, filter);\n    }\n\n    /**\n     * Deletes an existing JSON query query\n     *\n     * @param orgName   organization's name\n     * @param storeName store's name\n     * @param queryName query's name\n     * @return\n     */\n    @DELETE\n    @Path(\"/{orgName}/jsonstore/{storeName}/query/{queryName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Delete an existing JSON query query\", operationId = \"deleteJsonStoreQuery\")\n    public GenericOperationResult delete(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                         @PathParam(\"storeName\") @ConcordKey String storeName,\n                                         @PathParam(\"queryName\") @ConcordKey String queryName) {\n\n        storeQueryManager.delete(orgName, storeName, queryName);\n        return new GenericOperationResult(OperationResult.DELETED);\n    }\n\n    /**\n     * Executes an existing JSON store query\n     *\n     * @param orgName   organization's name\n     * @param storeName store's name\n     * @param queryName query's name\n     * @param params    query params\n     * @return query result\n     */\n    @POST\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Path(\"/{orgName}/jsonstore/{storeName}/query/{queryName}/exec\")\n    @WithTimer\n    @Validate\n    @Operation(description = \"Execute an existing JSON store query\", operationId = \"execJsonStoreQuery\")\n    public List<Object> exec(@PathParam(\"orgName\") @ConcordKey String orgName,\n                             @PathParam(\"storeName\") @ConcordKey String storeName,\n                             @PathParam(\"queryName\") @ConcordKey String queryName,\n                             @Valid Map<String, Object> params) {\n\n        try {\n            return storeQueryManager.exec(orgName, storeName, queryName, params);\n        } catch (ValidationErrorsException e) {\n            throw e;\n        } catch (Exception e) {\n            throw new ConcordApplicationException(\"Error while executing a query: \" + e.getMessage(), e);\n        }\n    }\n\n    /**\n     * Executes a JSON store query.\n     *\n     * @param orgName   organization's name\n     * @param storeName store's name\n     * @param text      query's text\n     */\n    @POST\n    @Consumes(MediaType.TEXT_PLAIN)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Path(\"/{orgName}/jsonstore/{storeName}/execQuery\")\n    @WithTimer\n    public List<Object> execQuery(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                  @PathParam(\"storeName\") @ConcordKey String storeName,\n                                  @QueryParam(\"maxLimit\") @DefaultValue(\"10\") int maxLimit,\n                                  String text) {\n\n        try {\n            return storeQueryManager.exec(orgName, storeName, text, maxLimit);\n        } catch (Exception e) {\n            throw new ConcordApplicationException(\"Error while executing a query: \" + e.getMessage(), e);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/jsonstore/JsonStoreRequest.java",
    "content": "package com.walmartlabs.concord.server.org.jsonstore;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.server.ApiEntity;\nimport com.walmartlabs.concord.server.org.EntityOwner;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonSerialize(as = ImmutableJsonStoreRequest.class)\n@JsonDeserialize(as = ImmutableJsonStoreRequest.class)\n@JsonInclude(JsonInclude.Include.NON_NULL)\n@ApiEntity\npublic interface JsonStoreRequest extends Serializable {\n\n    @Nullable\n    UUID id();\n\n    @ConcordKey\n    @Nullable\n    String name();\n\n    @Nullable\n    JsonStoreVisibility visibility();\n\n    @ConcordKey\n    @Nullable\n    String orgName();\n\n    @Nullable\n    EntityOwner owner();\n\n    static ImmutableJsonStoreRequest.Builder builder() {\n        return ImmutableJsonStoreRequest.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/jsonstore/JsonStoreResource.java",
    "content": "package com.walmartlabs.concord.server.org.jsonstore;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.server.GenericOperationResult;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.org.ResourceAccessEntry;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.validation.Valid;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport java.util.Collection;\nimport java.util.List;\n\n@Path(\"/api/v1/org\")\n@Tag(name = \"JsonStore\")\npublic class JsonStoreResource implements Resource {\n\n    private final JsonStoreManager storeManager;\n\n    @Inject\n    public JsonStoreResource(JsonStoreManager storeManager) {\n        this.storeManager = storeManager;\n    }\n\n    /**\n     * List existing stores.\n     *\n     * @param orgName organization's name\n     * @return list of stores\n     */\n    @GET\n    @Path(\"/{orgName}/jsonstore\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"List existing stores\", operationId = \"listJsonStores\")\n    public List<JsonStoreEntry> list(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                     @QueryParam(\"offset\") @DefaultValue(\"0\") int offset,\n                                     @QueryParam(\"limit\") @DefaultValue(\"30\") int limit,\n                                     @QueryParam(\"filter\") String filter) {\n\n        return storeManager.list(orgName, offset, limit, filter);\n    }\n\n    /**\n     * Returns an existing store.\n     *\n     * @param orgName   organization's name\n     * @param storeName store's name\n     * @return store\n     */\n    @GET\n    @Path(\"/{orgName}/jsonstore/{storeName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Get an existing store\", operationId = \"getJsonStore\")\n    public JsonStoreEntry get(@PathParam(\"orgName\") @ConcordKey String orgName,\n                              @PathParam(\"storeName\") @ConcordKey String storeName) {\n\n        return storeManager.get(orgName, storeName);\n    }\n\n    /**\n     * Create or update a store.\n     *\n     * @param entry store's definition\n     * @return\n     */\n    @POST\n    @Path(\"/{orgName}/jsonstore\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Create or update a store\", operationId = \"createOrUpdateJsonStore\")\n    public GenericOperationResult createOrUpdate(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                 @Valid JsonStoreRequest entry) {\n\n        OperationResult result = storeManager.createOrUpdate(orgName, entry);\n        return new GenericOperationResult(result);\n    }\n\n    /**\n     * Delete an existing store.\n     *\n     * @param storeName store's name\n     * @return\n     */\n    @DELETE\n    @Path(\"/{orgName}/jsonstore/{storeName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Delete an existing store\", operationId = \"deleteJsonStore\")\n    public GenericOperationResult delete(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                         @PathParam(\"storeName\") @ConcordKey String storeName) {\n\n        storeManager.delete(orgName, storeName);\n        return new GenericOperationResult(OperationResult.DELETED);\n    }\n\n    /**\n     * Get the store's capacity.\n     *\n     * @param storeName store's name\n     * @return\n     */\n    @GET\n    @Path(\"/{orgName}/jsonstore/{storeName}/capacity\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Get an existing store capacity\", operationId = \"getJsonStoreCapacity\")\n    public JsonStoreCapacity getCapacity(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                         @PathParam(\"storeName\") @ConcordKey String storeName) {\n\n        return storeManager.getCapacity(orgName, storeName);\n    }\n\n    @GET\n    @Path(\"/{orgName}/jsonstore/{storeName}/access\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Get a store's team access parameters\", operationId = \"getJsonStoreAccessLevel\")\n    public List<ResourceAccessEntry> getAccessLevel(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                    @PathParam(\"storeName\") @ConcordKey String storeName) {\n\n        return storeManager.getResourceAccess(orgName, storeName);\n    }\n\n    @POST\n    @Path(\"/{orgName}/jsonstore/{storeName}/access/bulk\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Updates the access level for the specified store and team\", operationId = \"bulkUpdateJsonStoreAccessLevel\")\n    public GenericOperationResult updateAccessLevel(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                    @PathParam(\"storeName\") @ConcordKey String storeName,\n                                                    @Valid Collection<ResourceAccessEntry> entries) {\n\n        if (entries == null) {\n            throw new ConcordApplicationException(\"List of teams is null.\", Response.Status.BAD_REQUEST);\n        }\n\n        storeManager.updateAccessLevel(orgName, storeName, entries, true);\n        return new GenericOperationResult(OperationResult.UPDATED);\n    }\n\n\n    @POST\n    @Path(\"/{orgName}/jsonstore/{storeName}/access\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Updates the access level for the specified store and team\", operationId = \"updateJsonStoreAccessLevel\")\n    public GenericOperationResult updateAccessLevel(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                    @PathParam(\"storeName\") @ConcordKey String storeName,\n                                                    @Valid ResourceAccessEntry entry) {\n\n        storeManager.updateAccessLevel(orgName, storeName, entry);\n        return new GenericOperationResult(OperationResult.UPDATED);\n    }\n}"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/jsonstore/JsonStoreVisibility.java",
    "content": "package com.walmartlabs.concord.server.org.jsonstore;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic enum JsonStoreVisibility {\n\n    PUBLIC,\n    PRIVATE\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/policy/PolicyCheckResource.java",
    "content": "package com.walmartlabs.concord.server.org.policy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.org.OrganizationEntry;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.project.ProjectEntry;\nimport com.walmartlabs.concord.server.org.project.ProjectManager;\nimport com.walmartlabs.concord.server.policy.EntityAction;\nimport com.walmartlabs.concord.server.policy.EntityType;\nimport com.walmartlabs.concord.server.policy.PolicyManager;\nimport com.walmartlabs.concord.server.policy.PolicyUtils;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.user.UserEntry;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport java.util.UUID;\n\n@Path(\"/api/v1\")\n@Tag(name = \"PolicyCheck\")\npublic class PolicyCheckResource implements Resource {\n\n    private final OrganizationManager orgManager;\n    private final ProjectManager projectManager;\n    private final PolicyManager policyManager;\n\n    @Inject\n    public PolicyCheckResource(OrganizationManager orgManager,\n                               ProjectManager projectManager,\n                               PolicyManager policyManager) {\n\n        this.orgManager = orgManager;\n        this.projectManager = projectManager;\n        this.policyManager = policyManager;\n    }\n\n    @GET\n    @Path(\"/{entityType}/canCreate\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Check for new entity creation\")\n    public boolean canCreate(@PathParam(\"entityType\") EntityType type,\n                             @QueryParam(\"orgName\") String orgName) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n        return isCreateEnabled(org.getId(), orgName, type);\n    }\n\n    private boolean isCreateEnabled(UUID orgId, String orgName, EntityType entity) {\n        try {\n            switch (entity) {\n                case PROJECT: {\n                    ProjectEntry entry = new ProjectEntry(entity.toString());\n                    UserEntry owner = projectManager.getOwner(entry.getOwner(), UserPrincipal.assertCurrent().getUser());\n                    policyManager.checkEntity(orgId, null, EntityType.PROJECT, EntityAction.CREATE, owner, PolicyUtils.projectToMap(orgId, orgName, entry));\n                    break;\n                }\n                case SECRET: {\n                    UserEntry owner = UserPrincipal.assertCurrent().getUser();\n                    policyManager.checkEntity(orgId, null, EntityType.SECRET, EntityAction.CREATE, owner, PolicyUtils.secretToMap(orgId, null, null, null, null));\n                }\n                default: {\n                    // nothing to do, the implementation supports only projects and secrets\n\n                    // when the UI supports creation of other types of entities then this code\n                    // can be extended to support it\n                }\n            }\n\n            return true;\n        } catch (ValidationErrorsException v) {\n            return false;\n        } catch (Exception e) {\n            throw new RuntimeException(e.getMessage());\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/policy/PolicyDao.java",
    "content": "package com.walmartlabs.concord.server.org.policy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.UuidGenerator;\nimport com.walmartlabs.concord.server.jooq.tables.records.PolicyLinksRecord;\nimport org.jooq.*;\n\nimport javax.inject.Inject;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.POLICIES;\nimport static com.walmartlabs.concord.server.jooq.Tables.POLICY_LINKS;\nimport static java.util.Objects.requireNonNull;\n\npublic class PolicyDao extends AbstractDao {\n\n    private final ConcordObjectMapper objectMapper;\n    private final UuidGenerator uuidGenerator;\n\n    @Inject\n    public PolicyDao(@MainDB Configuration cfg,\n                     ConcordObjectMapper objectMapper,\n                     UuidGenerator uuidGenerator) {\n        super(cfg);\n        this.objectMapper = requireNonNull(objectMapper);\n        this.uuidGenerator = requireNonNull(uuidGenerator);\n    }\n\n    public UUID getId(String name) {\n        return dsl().select(POLICIES.POLICY_ID)\n                .from(POLICIES)\n                .where(POLICIES.POLICY_NAME.eq(name))\n                .fetchOne(POLICIES.POLICY_ID);\n    }\n\n    public PolicyEntry get(UUID policyId) {\n        return dsl().select(POLICIES.POLICY_ID,\n                POLICIES.PARENT_POLICY_ID,\n                POLICIES.POLICY_NAME,\n                POLICIES.RULES)\n                .from(POLICIES)\n                .where(POLICIES.POLICY_ID.eq(policyId))\n                .fetchOne(this::toEntry);\n    }\n\n    public PolicyEntry get(String policyName) {\n        return dsl().select(POLICIES.POLICY_ID,\n                POLICIES.PARENT_POLICY_ID,\n                POLICIES.POLICY_NAME,\n                POLICIES.RULES)\n                .from(POLICIES)\n                .where(POLICIES.POLICY_NAME.eq(policyName))\n                .fetchOne(this::toEntry);\n    }\n\n    public PolicyEntry getLinked(UUID orgId, UUID projectId, UUID userId) {\n        return getLinked(dsl(), orgId, projectId, userId);\n    }\n\n    public PolicyEntry getLinked(DSLContext tx, UUID orgId, UUID projectId, UUID userId) {\n        SelectOnConditionStep<Record7<UUID, UUID, String, JSONB, UUID, UUID, UUID>> q =\n                tx.select(POLICIES.POLICY_ID,\n                        POLICIES.PARENT_POLICY_ID,\n                        POLICIES.POLICY_NAME,\n                        POLICIES.RULES,\n                        POLICY_LINKS.ORG_ID,\n                        POLICY_LINKS.PROJECT_ID,\n                        POLICY_LINKS.USER_ID)\n                        .from(POLICY_LINKS)\n                        .leftOuterJoin(POLICIES).on(POLICY_LINKS.POLICY_ID.eq(POLICIES.POLICY_ID));\n\n        // system policy\n        Condition c = ((POLICY_LINKS.ORG_ID.isNull().and(POLICY_LINKS.PROJECT_ID.isNull()).and(POLICY_LINKS.USER_ID.isNull())));\n\n        if (projectId != null) {\n            c = c.or((POLICY_LINKS.PROJECT_ID.eq(projectId).and(POLICY_LINKS.USER_ID.isNull())));\n        }\n\n        if (orgId != null) {\n            c = c.or((POLICY_LINKS.ORG_ID.eq(orgId).and(POLICY_LINKS.PROJECT_ID.isNull()).and(POLICY_LINKS.USER_ID.isNull())));\n        }\n\n        if (userId != null) {\n            if (projectId != null) {\n                c = c.or((POLICY_LINKS.USER_ID.eq(userId)).and(POLICY_LINKS.PROJECT_ID.eq(projectId)));\n            }\n\n            if (orgId != null) {\n                c = c.or((POLICY_LINKS.USER_ID.eq(userId)).and(POLICY_LINKS.ORG_ID.eq(orgId)).and(POLICY_LINKS.PROJECT_ID.isNull()));\n            }\n\n            c = c.or((POLICY_LINKS.USER_ID.eq(userId)).and(POLICY_LINKS.ORG_ID.isNull()).and(POLICY_LINKS.PROJECT_ID.isNull()));\n        }\n\n        q.where(c);\n\n        return findPolicyEntry(q.fetch(this::toRule));\n    }\n\n    public UUID insert(String name, UUID parentId, Map<String, Object> rules) {\n        return txResult(tx -> insert(tx, name, parentId, rules));\n    }\n\n    public UUID insert(DSLContext tx, String name, UUID parentId, Map<String, Object> rules) {\n        UUID policyId = uuidGenerator.generate();\n        return tx.insertInto(POLICIES)\n                .columns(POLICIES.POLICY_ID, POLICIES.POLICY_NAME, POLICIES.PARENT_POLICY_ID, POLICIES.RULES)\n                .values(policyId, name, parentId, objectMapper.toJSONB(rules))\n                .returning(POLICIES.POLICY_ID)\n                .fetchOne()\n                .getPolicyId();\n    }\n\n    public void update(UUID policyId, String name, UUID parentId, Map<String, Object> rules) {\n        tx(tx -> update(tx, policyId, name, parentId, rules));\n    }\n\n    public void update(DSLContext tx, UUID policyId, String name, UUID parentId, Map<String, Object> rules) {\n        tx.update(POLICIES)\n                .set(POLICIES.POLICY_NAME, name)\n                .set(POLICIES.RULES, objectMapper.toJSONB(rules))\n                .set(POLICIES.PARENT_POLICY_ID, parentId)\n                .where(POLICIES.POLICY_ID.eq(policyId))\n                .execute();\n    }\n\n    public void delete(UUID policyId) {\n        tx(tx -> tx.deleteFrom(POLICIES)\n                .where(POLICIES.POLICY_ID.eq(policyId))\n                .execute());\n    }\n\n    public void link(UUID policyId, UUID orgId, UUID projectId, UUID userId) {\n        tx(tx -> tx.insertInto(POLICY_LINKS)\n                .columns(POLICY_LINKS.POLICY_ID, POLICY_LINKS.ORG_ID, POLICY_LINKS.PROJECT_ID, POLICY_LINKS.USER_ID)\n                .values(policyId, orgId, projectId, userId)\n                .execute());\n    }\n\n    public void unlink(UUID policyId, UUID orgId, UUID projectId, UUID userId) {\n        tx(tx -> {\n            DeleteConditionStep<PolicyLinksRecord> q = tx.deleteFrom(POLICY_LINKS)\n                    .where(POLICY_LINKS.POLICY_ID.eq(policyId));\n\n            if (projectId != null) {\n                q.and(POLICY_LINKS.PROJECT_ID.eq(projectId));\n            } else if (orgId != null) {\n                q.and(POLICY_LINKS.ORG_ID.eq(orgId));\n            }\n\n            if (userId != null) {\n                q.and(POLICY_LINKS.USER_ID.eq(userId));\n            }\n\n            q.execute();\n        });\n    }\n\n    public List<PolicyEntry> list() {\n        return dsl().select(POLICIES.POLICY_ID,\n                POLICIES.PARENT_POLICY_ID,\n                POLICIES.POLICY_NAME,\n                POLICIES.RULES)\n                .from(POLICIES)\n                .fetch(this::toEntry);\n    }\n\n    private PolicyEntry findPolicyEntry(List<PolicyRule> rules) {\n        PolicyRule userRule = findUserLevelRule(rules);\n        if (userRule != null) {\n            return toEntry(userRule);\n        }\n\n        PolicyRule prjRule = rules.stream().filter(r -> r.prjId != null).findFirst().orElse(null);\n        if (prjRule != null) {\n            return toEntry(prjRule);\n        }\n\n        PolicyRule orgRule = rules.stream().filter(r -> r.orgId != null && r.prjId == null).findFirst().orElse(null);\n        if (orgRule != null) {\n            return toEntry(orgRule);\n        }\n\n        PolicyRule systemRule = rules.stream().filter(r -> r.orgId == null && r.prjId == null).findFirst().orElse(null);\n        if (systemRule != null) {\n            return toEntry(systemRule);\n        }\n\n        return null;\n    }\n\n    private PolicyRule findUserLevelRule(List<PolicyRule> rules) {\n        PolicyRule userPrjRule = rules.stream().filter(r -> r.userId != null && r.prjId != null).findFirst().orElse(null);\n        if (userPrjRule != null) {\n            return userPrjRule;\n        }\n\n        PolicyRule userOrgRule = rules.stream().filter(r -> r.userId != null && r.orgId != null && r.prjId == null).findFirst().orElse(null);\n        if (userOrgRule != null) {\n            return userOrgRule;\n        }\n\n        PolicyRule userRule = rules.stream().filter(r -> r.userId != null && r.orgId == null && r.prjId == null).findFirst().orElse(null);\n        if (userRule != null) {\n            return userRule;\n        }\n\n        return null;\n    }\n\n    private static PolicyEntry toEntry(PolicyRule r) {\n        return ImmutablePolicyEntry.builder()\n                .id(r.policyId)\n                .parentId(r.parentPolicyId)\n                .name(r.policyName)\n                .rules(r.rules)\n                .build();\n    }\n\n    private PolicyEntry toEntry(Record4<UUID, UUID, String, JSONB> r) {\n        return ImmutablePolicyEntry.builder()\n                .id(r.get(POLICIES.POLICY_ID))\n                .parentId(r.get(POLICIES.PARENT_POLICY_ID))\n                .name(r.get(POLICIES.POLICY_NAME))\n                .rules(objectMapper.fromJSONB(r.value4()))\n                .build();\n    }\n\n    private PolicyRule toRule(Record7<UUID, UUID, String, JSONB, UUID, UUID, UUID> r) {\n        return new PolicyRule(\n                r.get(POLICY_LINKS.ORG_ID),\n                r.get(POLICY_LINKS.PROJECT_ID),\n                r.get(POLICY_LINKS.USER_ID),\n                r.get(POLICIES.POLICY_ID),\n                r.get(POLICIES.PARENT_POLICY_ID),\n                r.get(POLICIES.POLICY_NAME),\n                objectMapper.fromJSONB(r.value4()));\n    }\n\n    private static class PolicyRule {\n\n        private final UUID orgId;\n        private final UUID prjId;\n        private final UUID userId;\n        private final UUID policyId;\n        private final UUID parentPolicyId;\n        private final String policyName;\n        private final Map<String, Object> rules;\n\n        private PolicyRule(UUID orgId, UUID prjId, UUID userId, UUID policyId, UUID parentPolicyId, String policyName, Map<String, Object> rules) {\n            this.orgId = orgId;\n            this.prjId = prjId;\n            this.userId = userId;\n            this.policyId = policyId;\n            this.parentPolicyId = parentPolicyId;\n            this.policyName = policyName;\n            this.rules = rules;\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/policy/PolicyEntry.java",
    "content": "package com.walmartlabs.concord.server.org.policy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.Map;\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonSerialize(as = ImmutablePolicyEntry.class)\n@JsonDeserialize(as = ImmutablePolicyEntry.class)\n@JsonInclude(Include.NON_NULL)\npublic interface PolicyEntry extends Serializable {\n\n    @Nullable\n    UUID id();\n\n    @Nullable\n    UUID parentId();\n\n    String name();\n\n    Map<String, Object> rules();\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/policy/PolicyLinkEntry.java",
    "content": "package com.walmartlabs.concord.server.org.policy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\n\nimport java.io.Serializable;\n\n@JsonInclude(Include.NON_NULL)\npublic class PolicyLinkEntry implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @ConcordKey\n    private final String orgName;\n\n    @ConcordKey\n    private final String projectName;\n\n    @ConcordKey\n    private final String userName;\n\n    private final String userDomain;\n\n    public PolicyLinkEntry() {\n        this(null, null, null, null);\n    }\n\n    public PolicyLinkEntry(String orgName) {\n        this(orgName, null, null, null);\n    }\n\n    @JsonCreator\n    public PolicyLinkEntry(@JsonProperty(\"orgName\") String orgName,\n                           @JsonProperty(\"projectName\") String projectName,\n                           @JsonProperty(\"userName\") String userName,\n                           @JsonProperty(\"userDomain\") String userDomain) {\n\n        this.orgName = orgName;\n        this.projectName = projectName;\n        this.userName = userName;\n        this.userDomain = userDomain;\n    }\n\n    public String getOrgName() {\n        return orgName;\n    }\n\n    public String getProjectName() {\n        return projectName;\n    }\n\n    public String getUserName() {\n        return userName;\n    }\n\n    public String getUserDomain() {\n        return userDomain;\n    }\n\n    @Override\n    public String toString() {\n        return \"PolicyLinkEntry{\" +\n                \"orgName='\" + orgName + '\\'' +\n                \", projectName='\" + projectName + '\\'' +\n                \", userName='\" + userName + '\\'' +\n                \", userDomain='\" + userDomain + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/policy/PolicyModule.java",
    "content": "package com.walmartlabs.concord.server.org.policy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.walmartlabs.concord.server.Utils.bindJaxRsResource;\n\npublic class PolicyModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        binder.bind(PolicyDao.class).in(SINGLETON);\n\n        bindJaxRsResource(binder, PolicyResource.class);\n        bindJaxRsResource(binder, PolicyCheckResource.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/policy/PolicyOperationResponse.java",
    "content": "package com.walmartlabs.concord.server.org.policy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.server.OperationResult;\n\nimport java.io.Serializable;\nimport java.util.UUID;\n\npublic class PolicyOperationResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n    private final UUID id;\n    private final OperationResult result;\n\n    @JsonCreator\n    public PolicyOperationResponse(@JsonProperty(\"id\") UUID id,\n                                   @JsonProperty(\"result\") OperationResult result) {\n        this.id = id;\n        this.result = result;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public OperationResult getResult() {\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"PolicyOperationResponse{\" +\n                \"ok=\" + ok +\n                \", id=\" + id +\n                \", result=\" + result +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/policy/PolicyResource.java",
    "content": "package com.walmartlabs.concord.server.org.policy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.server.GenericOperationResult;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.audit.AuditAction;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.audit.AuditObject;\nimport com.walmartlabs.concord.server.org.OrganizationDao;\nimport com.walmartlabs.concord.server.org.OrganizationEntry;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.policy.PolicyManager;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport com.walmartlabs.concord.server.user.UserType;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.apache.shiro.authz.AuthorizationException;\n\nimport javax.inject.Inject;\nimport javax.validation.Valid;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response.Status;\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.UUID;\n\n@Path(\"/api/v2/policy\")\n@Tag(name = \"Policy\")\npublic class PolicyResource implements Resource {\n\n    private final OrganizationManager orgManager;\n    private final OrganizationDao orgDao;\n    private final ProjectDao projectDao;\n    private final PolicyManager policyManager;\n    private final UserManager userManager;\n    private final AuditLog auditLog;\n\n    @Inject\n    public PolicyResource(OrganizationManager orgManager,\n                          OrganizationDao orgDao,\n                          ProjectDao projectDao,\n                          UserManager userManager,\n                          PolicyManager policyManager,\n                          AuditLog auditLog) {\n\n        this.orgManager = orgManager;\n        this.orgDao = orgDao;\n        this.projectDao = projectDao;\n        this.policyManager = policyManager;\n        this.userManager = userManager;\n        this.auditLog = auditLog;\n    }\n\n    @GET\n    @Path(\"/{policyName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Get an existing policy\", operationId = \"getPolicy\")\n    public PolicyEntry get(@PathParam(\"policyName\") @ConcordKey String policyName) {\n        assertAdmin();\n        PolicyEntry p = policyManager.get(policyName);\n        if (p == null) {\n            throw new ConcordApplicationException(\"Policy not found: \" + policyName, Status.NOT_FOUND);\n        }\n\n        return p;\n    }\n\n    @POST\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Create or update a policy\", operationId = \"createOrUpdatePolicy\")\n    public PolicyOperationResponse createOrUpdate(@Valid PolicyEntry entry) {\n        assertAdmin();\n\n        UUID id = entry.id();\n        if (id == null && entry.name() != null) {\n            id = policyManager.getId(entry.name());\n        }\n\n        if (id == null) {\n            id = policyManager.insert(entry.name(), entry.parentId(), entry.rules());\n\n            auditLog.add(AuditObject.POLICY, AuditAction.CREATE)\n                    .field(\"policyId\", id)\n                    .field(\"parentId\", entry.parentId())\n                    .field(\"name\", entry.name())\n                    .log();\n\n            return new PolicyOperationResponse(id, OperationResult.CREATED);\n        } else {\n            policyManager.update(id, entry.name(), entry.parentId(), entry.rules());\n\n            auditLog.add(AuditObject.POLICY, AuditAction.UPDATE)\n                    .field(\"policyId\", id)\n                    .field(\"parentId\", entry.parentId())\n                    .field(\"name\", entry.name())\n                    .log();\n\n            return new PolicyOperationResponse(id, OperationResult.UPDATED);\n        }\n    }\n\n    @DELETE\n    @Path(\"/{policyName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Delete an existing policy\", operationId = \"deletePolicy\")\n    public GenericOperationResult delete(@PathParam(\"policyName\") @ConcordKey String policyName) {\n        assertAdmin();\n\n        UUID id = policyManager.getId(policyName);\n        if (id == null) {\n            throw new ConcordApplicationException(\"Policy not found: \" + policyName, Status.NOT_FOUND);\n        }\n\n        policyManager.delete(id);\n\n        auditLog.add(AuditObject.POLICY, AuditAction.DELETE)\n                .field(\"policyId\", id)\n                .field(\"name\", policyName)\n                .log();\n\n        return new GenericOperationResult(OperationResult.DELETED);\n    }\n\n    @PUT\n    @Path(\"/{policyName}/link\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Link an existing policy to an organization, a project or user\", operationId = \"linkPolicy\")\n    public GenericOperationResult link(@PathParam(\"policyName\") @ConcordKey String policyName,\n                                       @Valid PolicyLinkEntry entry) {\n\n        assertAdmin();\n\n        // TODO: add user type into request\n        UserType userType = UserPrincipal.assertCurrent().getType();\n        PolicyLink l = assertLink(policyName, entry.getOrgName(), entry.getProjectName(), entry.getUserName(), entry.getUserDomain(), userType);\n        policyManager.link(l.policyId, l.orgId, l.projectId, l.userId);\n\n        auditLog.add(AuditObject.POLICY, AuditAction.UPDATE)\n                .field(\"policyId\", l.policyId)\n                .field(\"name\", policyName)\n                .field(\"link\", l)\n                .field(\"action\", \"link\")\n                .log();\n\n        return new GenericOperationResult(OperationResult.UPDATED);\n    }\n\n    @DELETE\n    @Path(\"/{policyName}/link\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Unlink an existing policy\", operationId = \"unlinkPolicy\")\n    public GenericOperationResult unlink(@PathParam(\"policyName\") @ConcordKey String policyName,\n                                         @QueryParam(\"orgName\") @ConcordKey String orgName,\n                                         @QueryParam(\"projectName\") @ConcordKey String projectName,\n                                         @QueryParam(\"userName\") @ConcordKey String userName,\n                                         @QueryParam(\"userDomain\") String domain,\n                                         @QueryParam(\"userType\") UserType userType) {\n\n        assertAdmin();\n\n        if (userType == null) {\n            userType = UserPrincipal.assertCurrent().getType();\n        }\n        PolicyLink l = assertLink(policyName, orgName, projectName, userName, domain, userType);\n        policyManager.unlink(l.policyId, l.orgId, l.projectId, l.userId);\n\n        auditLog.add(AuditObject.POLICY, AuditAction.UPDATE)\n                .field(\"policyId\", l.policyId)\n                .field(\"name\", policyName)\n                .field(\"link\", l)\n                .field(\"action\", \"unlink\")\n                .log();\n\n        return new GenericOperationResult(OperationResult.DELETED);\n    }\n\n    @GET\n    @Path(\"/\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"List policies, optionally filtering by organization, project and/or user links\", operationId = \"listPolicies\")\n    public List<PolicyEntry> list(@QueryParam(\"orgName\") @ConcordKey String orgName,\n                                  @QueryParam(\"projectName\") @ConcordKey String projectName,\n                                  @QueryParam(\"userName\") @ConcordKey String userName,\n                                  @QueryParam(\"userDomain\") String userDomain,\n                                  @QueryParam(\"userType\") UserType userType) {\n\n        assertAdmin();\n\n        if (orgName == null && projectName == null && userName == null) {\n            return policyManager.list();\n        }\n\n        UUID orgId = null;\n        if (orgName != null) {\n            orgId = orgDao.getId(orgName);\n            if (orgId == null) {\n                throw new ConcordApplicationException(\"Organization not found: \" + orgName, Status.BAD_REQUEST);\n            }\n        }\n\n        UUID projectId = null;\n        if (projectName != null) {\n            if (orgId == null) {\n                throw new ConcordApplicationException(\"Organization name is required\", Status.BAD_REQUEST);\n            }\n\n            projectId = projectDao.getId(orgId, projectName);\n            if (projectId == null) {\n                throw new ConcordApplicationException(\"Project not found: \" + projectName, Status.BAD_REQUEST);\n            }\n        }\n\n        UUID userId = null;\n        if (userName != null) {\n            if (userType == null) {\n                userType = UserPrincipal.assertCurrent().getType();\n            }\n            userId = assertUser(userName, userDomain, userType);\n        }\n\n        PolicyEntry e = policyManager.getLinked(orgId, projectId, userId);\n        if (e == null) {\n            return Collections.emptyList();\n        }\n\n        // TODO consider returning multiple entries?\n        return Collections.singletonList(e);\n    }\n\n    @POST\n    @Path(\"/refresh\")\n    @Operation(description = \"Refresh policy\", operationId = \"refreshPolicy\")\n    public void refresh() {\n        if (!Roles.isAdmin()) {\n            throw new AuthorizationException(\"Only admins are allowed to refresh polices\");\n        }\n\n        policyManager.refresh();\n    }\n\n    private UUID assertProject(UUID orgId, String projectName) {\n        UUID id = projectDao.getId(orgId, projectName);\n        if (id == null) {\n            throw new ConcordApplicationException(\"Project not found: \" + projectName);\n        }\n        return id;\n    }\n\n    private UUID assertUser(String userName, String doamin, UserType userType) {\n        return userManager.getId(userName, doamin, userType)\n                .orElseThrow(() -> new ConcordApplicationException(\"User '\" + userName + \"' with domain '\" + doamin + \"' (\" + userType + \") not found\", Status.BAD_REQUEST));\n    }\n\n    private PolicyLink assertLink(String policyName, String orgName, String projectName, String userName, String domain, UserType userType) {\n        UUID policyId = policyManager.getId(policyName);\n        if (policyId == null) {\n            throw new ConcordApplicationException(\"Policy not found: \" + policyName, Status.NOT_FOUND);\n        }\n\n        UUID orgId = null;\n        if (orgName != null) {\n            OrganizationEntry org = orgManager.assertAccess(orgName, true);\n            orgId = org.getId();\n        }\n\n        UUID projectId = null;\n        if (projectName != null) {\n            if (orgId != null) {\n                projectId = assertProject(orgId, projectName);\n            } else {\n                throw new ConcordApplicationException(\"Organization name is required\", Status.BAD_REQUEST);\n            }\n        }\n\n        if (projectId != null) {\n            // projectId is enough to make a proper reference\n            orgId = null;\n        }\n\n        UUID userId = null;\n        if (userName != null && userType != null) {\n            userId = assertUser(userName, domain, userType);\n        }\n\n        return new PolicyLink(policyId, orgId, projectId, userId);\n    }\n\n    private static void assertAdmin() {\n        if (!Roles.isAdmin()) {\n            throw new UnauthorizedException(\"Only admins can do that\");\n        }\n    }\n\n    @JsonInclude(Include.NON_NULL)\n    private static class PolicyLink implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        private final UUID policyId;\n        private final UUID orgId;\n        private final UUID projectId;\n        private final UUID userId;\n\n        private PolicyLink(UUID policyId, UUID orgId, UUID projectId, UUID userId) {\n            this.policyId = policyId;\n            this.orgId = orgId;\n            this.projectId = projectId;\n            this.userId = userId;\n        }\n\n        // needed for audit_log data\n\n        public UUID getOrgId() {\n            return orgId;\n        }\n\n        public UUID getProjectId() {\n            return projectId;\n        }\n\n        public UUID getUserId() {\n            return userId;\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/DiffUtils.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.javers.core.Javers;\nimport org.javers.core.JaversBuilder;\nimport org.javers.core.changelog.ChangeProcessor;\nimport org.javers.core.commit.CommitMetadata;\nimport org.javers.core.diff.Change;\nimport org.javers.core.diff.Diff;\nimport org.javers.core.diff.changetype.*;\nimport org.javers.core.diff.changetype.container.ArrayChange;\nimport org.javers.core.diff.changetype.container.ContainerChange;\nimport org.javers.core.diff.changetype.container.ListChange;\nimport org.javers.core.diff.changetype.container.SetChange;\nimport org.javers.core.diff.changetype.map.MapChange;\nimport org.javers.core.metamodel.object.GlobalId;\nimport org.javers.core.metamodel.object.ValueObjectId;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class DiffUtils {\n\n    private static final String KEY_PREVIOUS = \"prev\";\n    private static final String KEY_NEW = \"new\";\n\n    public static Map<String, Object> compare(Object left, Object right) {\n        Javers javers = JaversBuilder.javers().build();\n\n        Diff diff = javers.compare(left, right);\n\n        CustomChangeProcessor changeProcessor = new CustomChangeProcessor();\n        javers.processChangeList(diff.getChanges(), changeProcessor);\n\n        Map<String, Object> m = changeProcessor.result();\n        removeIfEmpty(m, KEY_PREVIOUS);\n        removeIfEmpty(m, KEY_NEW);\n        return m;\n    }\n\n    private static void removeIfEmpty(Map<String, Object> m, String k) {\n        Object v = m.get(k);\n        if (v == null) {\n            return;\n        }\n\n        if (v instanceof Map) {\n            Map mv = (Map) v;\n            if (mv.isEmpty()) {\n                m.remove(k);\n            }\n        }\n    }\n\n    private static class CustomChangeProcessor implements ChangeProcessor<Map<String, Object>> {\n        private final Map<String, Object> result = new HashMap<>();\n\n        private CustomChangeProcessor() {\n            result.put(KEY_PREVIOUS, new HashMap<String, Object>());\n            result.put(KEY_NEW, new HashMap<String, Object>());\n        }\n\n        @Override\n        @SuppressWarnings(\"unchecked\")\n        public void onValueChange(ValueChange valueChange) {\n            GlobalId id = valueChange.getAffectedGlobalId();\n            Map<String, Object> prevO = getPrevious();\n            Map<String, Object> nextO = getNew();\n\n            if (id instanceof ValueObjectId) {\n                ValueObjectId idV = (ValueObjectId) id;\n                String[] path = idV.getFragment().split(\"/\");\n\n\n                for (String p : path) {\n                    if (valueChange.getLeft() != null) {\n                        prevO.putIfAbsent(p, new HashMap<String, Object>());\n                        prevO = (Map<String, Object>) prevO.get(p);\n                    }\n\n                    if (valueChange.getRight() != null) {\n                        nextO.putIfAbsent(p, new HashMap<String, Object>());\n                        nextO = (Map<String, Object>) nextO.get(p);\n                    }\n                }\n            }\n\n            if (valueChange.getLeft() != null) {\n                prevO.put(valueChange.getPropertyName(), valueChange.getLeft());\n            }\n\n            if (valueChange.getRight() != null) {\n                nextO.put(valueChange.getPropertyName(), valueChange.getRight());\n            }\n        }\n\n        @Override\n        @SuppressWarnings(\"unchecked\")\n        public void onNewObject(NewObject newObject) {\n            GlobalId id = newObject.getAffectedGlobalId();\n\n            Map<String, Object> newObject2 = (new ObjectMapper()).convertValue(\n                    newObject.getAffectedObject().orElse(null),\n                    new TypeReference<Map<String, Object>>() {\n                    });\n\n            if (id instanceof ValueObjectId) {\n                ValueObjectId idV = (ValueObjectId) id;\n                String[] path = idV.getFragment().split(\"/\");\n\n                Map<String, Object> nextO = getNew();\n                for (int n = 0; n < path.length - 1; n++) {\n                    nextO.putIfAbsent(path[n], new HashMap<String, Object>());\n\n                    Object o = nextO.get(path[n]);\n                    if (!(o instanceof Map)) {\n                        continue;\n                    }\n\n                    nextO = (Map<String, Object>) o;\n                }\n\n                nextO.put(path[path.length - 1], newObject2);\n            } else {\n                result.put(KEY_NEW, newObject2);\n            }\n        }\n\n        @Override\n        @SuppressWarnings(\"unchecked\")\n        public void onObjectRemoved(ObjectRemoved objectRemoved) {\n            GlobalId id = objectRemoved.getAffectedGlobalId();\n\n            Map<String, Object> newObject2 = (new ObjectMapper()).convertValue(\n                    objectRemoved.getAffectedObject().orElse(null),\n                    new TypeReference<Map<String, Object>>() {\n                    });\n\n            if (id instanceof ValueObjectId) {\n                ValueObjectId idV = (ValueObjectId) id;\n                String[] path = idV.getFragment().split(\"/\");\n\n                Map<String, Object> prevO = getPrevious();\n                for (int n = 0; n < path.length - 1; n++) {\n                    prevO.putIfAbsent(path[n], new HashMap<String, Object>());\n\n                    Object o = prevO.get(path[n]);\n                    if (!(o instanceof Map)) {\n                        continue;\n                    }\n\n                    prevO = (Map<String, Object>) o;\n                }\n\n                prevO.put(path[path.length - 1], newObject2);\n            } else {\n                result.put(KEY_PREVIOUS, newObject2);\n            }\n        }\n\n        @Override\n        public Map<String, Object> result() {\n            return result;\n        }\n\n\n        @SuppressWarnings(\"unchecked\")\n        private Map<String, Object> getPrevious() {\n            return (Map<String, Object>) result.get(KEY_PREVIOUS);\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        private Map<String, Object> getNew() {\n            return (Map<String, Object>) result.get(KEY_NEW);\n        }\n\n        @Override\n        public void onCommit(CommitMetadata commitMetadata) {\n        }\n\n        @Override\n        public void onAffectedObject(GlobalId globalId) {\n        }\n\n        @Override\n        public void beforeChangeList() {\n        }\n\n        @Override\n        public void afterChangeList() {\n        }\n\n        @Override\n        public void beforeChange(Change change) {\n        }\n\n        @Override\n        public void afterChange(Change change) {\n        }\n\n        @Override\n        public void onPropertyChange(PropertyChange propertyChange) {\n        }\n\n        @Override\n        public void onReferenceChange(ReferenceChange referenceChange) {\n        }\n\n        @Override\n        public void onContainerChange(ContainerChange containerChange) {\n        }\n\n        @Override\n        public void onSetChange(SetChange setChange) {\n        }\n\n        @Override\n        public void onArrayChange(ArrayChange arrayChange) {\n        }\n\n        @Override\n        public void onListChange(ListChange listChange) {\n        }\n\n        @Override\n        public void onMapChange(MapChange mapChange) {\n        }\n    }\n}\n\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/EncryptValueResponse.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport io.swagger.v3.oas.annotations.media.Schema;\n\nimport java.io.Serializable;\nimport java.util.Arrays;\n\npublic class EncryptValueResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n\n    @Schema(type = \"string\", format = \"string\")\n    private final byte[] data;\n\n    @JsonCreator\n    public EncryptValueResponse(@JsonProperty(\"data\") byte[] data) { // NOSONAR\n        this.data = data;\n    }\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    public byte[] getData() {\n        return data;\n    }\n\n    @Override\n    public String toString() {\n        return \"EncryptValueResponse{\" +\n                \"ok=\" + ok +\n                \", data=\" + Arrays.toString(data) +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/EncryptedProjectValueManager.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.secret.SecretUtils;\nimport com.walmartlabs.concord.server.cfg.SecretStoreConfiguration;\n\nimport javax.crypto.BadPaddingException;\nimport javax.crypto.IllegalBlockSizeException;\nimport javax.inject.Inject;\nimport java.security.SecureRandom;\nimport java.util.UUID;\n\npublic class EncryptedProjectValueManager {\n\n    private static final int PROJECT_SECRET_KEY_LENGTH = 128;\n\n    private final SecretStoreConfiguration secretCfg;\n    private final ProjectDao projectDao;\n    private final SecureRandom secureRandom;\n\n    @Inject\n    public EncryptedProjectValueManager(SecretStoreConfiguration secretCfg, ProjectDao projectDao, SecureRandom secureRandom) {\n        this.secretCfg = secretCfg;\n        this.projectDao = projectDao;\n        this.secureRandom = secureRandom;\n    }\n\n    /**\n     * Encrypts the specified data using the project's key.\n     *\n     * @param projectId\n     * @param data\n     * @return\n     */\n    public byte[] encrypt(UUID projectId, byte[] data) {\n        byte[] key = getDecryptedSecretKey(projectId);\n        return SecretUtils.encrypt(data, key, secretCfg.getProjectSecretsSalt());\n    }\n\n    /**\n     * Decrypts the specified data using the project's key.\n     *\n     * @param projectId\n     * @param data\n     * @return\n     */\n    public byte[] decrypt(UUID projectId, byte[] data) {\n        try {\n            byte[] key = getDecryptedSecretKey(projectId);\n            return SecretUtils.decrypt(data, key, secretCfg.getProjectSecretsSalt());\n        } catch (SecurityException e) {\n            String message = e.getMessage();\n\n            Throwable cause = e.getCause();\n            if (cause instanceof IllegalBlockSizeException) {\n                message = \"Invalid encrypted value, please verify that the value is a correct encrypted string\";\n            } else if (cause instanceof BadPaddingException) {\n                message = \"Please verify that the value is correct and is encrypted using the correct project\";\n            }\n\n            throw new SecurityException(\"Decrypt error: \" + message);\n        }\n    }\n\n    private byte[] getDecryptedSecretKey(UUID projectId) {\n        byte[] ab = projectDao.getOrUpdateSecretKey(projectId, () -> {\n            // use the old name-based key for the existing projects\n            // new projects must be created with a more secure key\n            String name = projectDao.getName(projectId);\n            byte[] key = name.getBytes();\n            return SecretUtils.encrypt(key, null, secretCfg.getSecretStoreSalt());\n        });\n\n        return SecretUtils.decrypt(ab, null, secretCfg.getSecretStoreSalt());\n    }\n\n    public byte[] createEncryptedSecretKey() {\n        byte[] key = new byte[PROJECT_SECRET_KEY_LENGTH];\n        secureRandom.nextBytes(key);\n        return SecretUtils.encrypt(key, null, secretCfg.getSecretStoreSalt());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/KvDao.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.Locks;\nimport com.walmartlabs.concord.server.jooq.tables.ProjectKvStore;\nimport org.jooq.Configuration;\nimport org.jooq.Record1;\nimport org.jooq.Record4;\nimport org.jooq.SelectJoinStep;\nimport org.jooq.exception.DataAccessException;\n\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\nimport java.util.List;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.tables.ProjectKvStore.PROJECT_KV_STORE;\nimport static org.jooq.impl.DSL.currentOffsetDateTime;\nimport static org.jooq.impl.DSL.value;\n\npublic class KvDao extends AbstractDao {\n\n    private final Locks locks;\n\n    @Inject\n    public KvDao(@MainDB Configuration cfg, Locks locks) {\n        super(cfg);\n        this.locks = locks;\n    }\n\n    public void remove(UUID projectId, String key) {\n        ProjectKvStore kv = PROJECT_KV_STORE.as(\"kv\");\n        tx(tx -> tx.deleteFrom(kv)\n                .where(kv.PROJECT_ID.eq(projectId)\n                        .and(kv.VALUE_KEY.eq(key)))\n                .execute());\n    }\n\n    public void putString(UUID projectId, String key, String value) {\n        ProjectKvStore kv = PROJECT_KV_STORE.as(\"kv\");\n        tx(tx -> {\n            int rows = tx.insertInto(kv)\n                    .columns(kv.PROJECT_ID, kv.VALUE_KEY, kv.VALUE_STRING, kv.LAST_UPDATED_AT)\n                    .values(value(projectId), value(key), value(value), currentOffsetDateTime())\n                    .onConflict(kv.PROJECT_ID, kv.VALUE_KEY)\n                    .doUpdate()\n                        .set(kv.VALUE_STRING, value)\n                        .set(kv.LAST_UPDATED_AT, currentOffsetDateTime())\n                    .execute();\n\n            if (rows != 1) {\n                throw new DataAccessException(\"Invalid number of rows: \" + rows);\n            }\n        });\n    }\n\n    public void putLong(UUID projectId, String key, long value) {\n        ProjectKvStore kv = PROJECT_KV_STORE.as(\"kv\");\n        tx(tx -> {\n            int rows = tx.insertInto(kv)\n                    .columns(kv.PROJECT_ID, kv.VALUE_KEY, kv.VALUE_LONG, kv.LAST_UPDATED_AT)\n                    .values(value(projectId), value(key), value(value), currentOffsetDateTime())\n                    .onConflict(kv.PROJECT_ID, kv.VALUE_KEY)\n                    .doUpdate()\n                        .set(kv.VALUE_LONG, value)\n                        .set(kv.LAST_UPDATED_AT, currentOffsetDateTime())\n                    .execute();\n\n            if (rows != 1) {\n                throw new DataAccessException(\"Invalid number of rows: \" + rows);\n            }\n        });\n    }\n\n    public String getString(UUID projectId, String key) {\n        ProjectKvStore kv = PROJECT_KV_STORE.as(\"kv\");\n        return dsl().select(kv.VALUE_STRING)\n                .from(kv)\n                .where(kv.PROJECT_ID.eq(projectId)\n                        .and(kv.VALUE_KEY.eq(key)))\n                .fetchOne(kv.VALUE_STRING);\n    }\n\n    public Long getLong(UUID projectId, String key) {\n        ProjectKvStore kv = PROJECT_KV_STORE.as(\"kv\");\n\n        Record1<Long> r = dsl().select(kv.VALUE_LONG)\n                .from(kv)\n                .where(kv.PROJECT_ID.eq(projectId)\n                        .and(kv.VALUE_KEY.eq(key)))\n                .fetchOne();\n\n        if (r == null) {\n            return null;\n        }\n\n        return r.value1();\n    }\n\n    public long inc(UUID projectId, String key) {\n        ProjectKvStore kv = PROJECT_KV_STORE.as(\"kv\");\n        return txResult(tx -> {\n            // grab a lock, it will be released when the transaction ends\n            locks.lock(tx, projectId + \"/\" + key);\n\n            // \"upsert\" the record\n            tx.insertInto(kv)\n                    .columns(kv.PROJECT_ID, kv.VALUE_KEY, kv.VALUE_LONG, kv.LAST_UPDATED_AT)\n                    .values(value(projectId), value(key), value(1L), currentOffsetDateTime())\n                    .onConflict(kv.PROJECT_ID, kv.VALUE_KEY)\n                    .doUpdate()\n                        .set(kv.VALUE_LONG, kv.VALUE_LONG.plus(1))\n                        .set(kv.LAST_UPDATED_AT, currentOffsetDateTime())\n                    .execute();\n\n            // get an updated value\n            return tx.select(kv.VALUE_LONG)\n                    .from(kv)\n                    .where(kv.PROJECT_ID.eq(projectId)\n                            .and(kv.VALUE_KEY.eq(key)))\n                    .fetchOne(kv.VALUE_LONG);\n        });\n    }\n\n    public List<KvEntry> list(UUID projectId, int offset, int limit, String filter) {\n        ProjectKvStore kv = PROJECT_KV_STORE.as(\"kv\");\n\n        return txResult(tx -> {\n            SelectJoinStep<Record4<String, Long, String, OffsetDateTime>> q = tx.select(kv.VALUE_KEY, kv.VALUE_LONG, kv.VALUE_STRING, kv.LAST_UPDATED_AT)\n                    .from(kv);\n\n            if (filter != null) {\n                q.where(kv.VALUE_KEY.containsIgnoreCase(filter));\n            }\n\n            if (offset > 0) {\n                q.offset(offset);\n            }\n\n            if (limit > 0) {\n                q.limit(limit);\n            }\n\n            return q\n                    .where(kv.PROJECT_ID.eq(projectId))\n                    .orderBy(kv.VALUE_KEY)\n                    .fetch(KvDao::toEntry);\n        });\n    }\n\n    public int count(UUID projectId) {\n        return txResult(tx -> tx.selectCount()\n                .from(PROJECT_KV_STORE)\n                .where(PROJECT_KV_STORE.PROJECT_ID.eq(projectId))\n                .fetchOne(0, int.class));\n    }\n\n    public boolean exists(UUID projectId, String key) {\n        return txResult(tx -> tx.fetchExists((tx.selectOne()\n                .from(PROJECT_KV_STORE)\n                .where(PROJECT_KV_STORE.PROJECT_ID.eq(projectId)\n                        .and(PROJECT_KV_STORE.VALUE_KEY.eq(key))))));\n    }\n\n    private static KvEntry toEntry(Record4<String, Long, String, OffsetDateTime> r) {\n        Object value = r.get(PROJECT_KV_STORE.VALUE_STRING);\n        if (value == null) {\n            value = r.get(PROJECT_KV_STORE.VALUE_LONG);\n        }\n        return KvEntry.builder()\n                .key(r.get(PROJECT_KV_STORE.VALUE_KEY))\n                .value(value)\n                .lastUpdatedAt(r.get(PROJECT_KV_STORE.LAST_UPDATED_AT))\n                .build();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/KvEntry.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.time.OffsetDateTime;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableKvEntry.class)\n@JsonDeserialize(as = ImmutableKvEntry.class)\npublic interface KvEntry extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    String key();\n\n    Object value();\n\n    @Nullable\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    OffsetDateTime lastUpdatedAt();\n\n    static ImmutableKvEntry.Builder builder() {\n        return ImmutableKvEntry.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/KvManager.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.policyengine.CheckResult;\nimport com.walmartlabs.concord.policyengine.KvRule;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.server.policy.PolicyManager;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\n\nimport javax.inject.Inject;\nimport java.text.MessageFormat;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.UUID;\n\npublic class KvManager {\n\n    private static final String DEFAULT_POLICY_MESSAGE = \"Maximum KV entries exceeded: current {0}, limit {1}\";\n\n    private final KvDao kvDao;\n    private final ProjectDao projectDao;\n    private final PolicyManager policyManager;\n\n    @Inject\n    public KvManager(KvDao kvDao, ProjectDao projectDao, PolicyManager policyManager) {\n        this.kvDao = kvDao;\n        this.projectDao = projectDao;\n        this.policyManager = policyManager;\n    }\n\n    public void remove(UUID projectId, String key) {\n        // TODO: assert project access\n        kvDao.remove(projectId, key);\n    }\n\n    public void putString(UUID projectId, String key, String value) {\n        // TODO: assert project access\n\n        assertPolicy(projectId, key);\n\n        kvDao.putString(projectId, key, value);\n    }\n\n    public String getString(UUID projectId, String key) {\n        // TODO: assert project access\n        return kvDao.getString(projectId, key);\n    }\n\n    public void putLong(UUID projectId, String key, long value) {\n        // TODO: assert project access\n\n        assertPolicy(projectId, key);\n\n        kvDao.putLong(projectId, key, value);\n    }\n\n    public Long getLong(UUID projectId, String key) {\n        // TODO: assert project access\n        return kvDao.getLong(projectId, key);\n    }\n\n    public long inc(UUID projectId, String key) {\n        // TODO: assert project access\n\n        assertPolicy(projectId, key);\n\n        return kvDao.inc(projectId, key);\n    }\n\n    private void assertPolicy(UUID projectId, String key) {\n        UUID orgId = projectDao.getOrgId(projectId);\n        if (orgId == null) {\n            return;\n        }\n\n        PolicyEngine policyEngine = policyManager.get(orgId, projectId, UserPrincipal.assertCurrent().getUser().getId());\n        if (policyEngine == null) {\n            return;\n        }\n\n        CheckResult<KvRule, Integer> result = policyEngine.getKvPolicy().check(() -> kvDao.count(projectId), () -> kvDao.exists(projectId, key));\n        if (!result.getDeny().isEmpty()) {\n            throw new ValidationErrorsException(\"Found KV policy violations: \" + buildErrorMessage(result.getDeny()));\n        }\n    }\n\n    private static String buildErrorMessage(List<CheckResult.Item<KvRule, Integer>> errors) {\n        StringBuilder sb = new StringBuilder();\n        for (CheckResult.Item<KvRule, Integer> e : errors) {\n            KvRule r = e.getRule();\n\n            String msg = r.msg() != null ? r.msg() : DEFAULT_POLICY_MESSAGE;\n            int actual = e.getEntity();\n            int max = r.maxEntries();\n\n            sb.append(MessageFormat.format(Objects.requireNonNull(msg), actual, max)).append(';');\n        }\n        return sb.toString();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/ProjectAccessManager.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.collect.ImmutableMap;\nimport com.walmartlabs.concord.server.audit.AuditAction;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.audit.AuditObject;\nimport com.walmartlabs.concord.server.org.*;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.user.UserDao;\nimport org.jooq.DSLContext;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\n\npublic class ProjectAccessManager {\n\n    private final OrganizationManager orgManager;\n    private final ProjectDao projectDao;\n    private final UserDao userDao;\n    private final AuditLog auditLog;\n\n    @Inject\n    public ProjectAccessManager(OrganizationManager orgManager, ProjectDao projectDao, UserDao userDao, AuditLog auditLog) {\n        this.orgManager = orgManager;\n        this.projectDao = projectDao;\n        this.userDao = userDao;\n        this.auditLog = auditLog;\n    }\n\n    public void updateAccessLevel(UUID projectId, UUID teamId, ResourceAccessLevel level) {\n        assertAccess(projectId, ResourceAccessLevel.OWNER, true);\n        projectDao.upsertAccessLevel(projectId, teamId, level);\n\n        addAuditLog(projectId, Collections.singletonList(new ResourceAccessEntry(teamId, null, null, level)), false);\n    }\n\n    public ProjectEntry assertAccess(UUID orgId, UUID projectId, String projectName, ResourceAccessLevel level, boolean orgMembersOnly) {\n        if (projectId == null && projectName == null) {\n            throw new ValidationErrorsException(\"Project ID or name is required\");\n        }\n\n        if (projectId == null) {\n            projectId = projectDao.getId(orgId, projectName);\n            if (projectId == null) {\n                throw new ValidationErrorsException(\"Project not found: \" + projectName);\n            }\n        }\n\n        return assertAccess(projectId, level, orgMembersOnly);\n    }\n\n    public ProjectEntry assertAccess(UUID projectId, ResourceAccessLevel level, boolean orgMembersOnly) {\n        return projectDao.txResult(tx -> assertAccess(tx, projectId, level, orgMembersOnly));\n    }\n\n    public ProjectEntry assertAccess(DSLContext tx, UUID projectId, ResourceAccessLevel level, boolean orgMembersOnly) {\n        ProjectEntry project = projectDao.get(tx, projectId);\n        if (project == null) {\n            throw new ValidationErrorsException(\"Project not found: \" + projectId);\n        }\n\n        if (!hasAccess(project, level, orgMembersOnly)) {\n            UserPrincipal p = UserPrincipal.getCurrent();\n            throw new UnauthorizedException(\"The current user (\" + p.getUsername() + \") doesn't have \" +\n                    \"the necessary access level (\" + level + \") to the project: \" + project.getName());\n        }\n\n        return project;\n    }\n\n    @WithTimer\n    public boolean hasAccess(ProjectEntry project, ResourceAccessLevel level, boolean orgMembersOnly) {\n        if (Roles.isAdmin()) {\n            // an admin can access any project\n            return true;\n        }\n\n        UserPrincipal principal = UserPrincipal.assertCurrent();\n\n        if (level == ResourceAccessLevel.READER && (Roles.isGlobalReader() || Roles.isGlobalWriter())) {\n            return true;\n        } else if (level == ResourceAccessLevel.WRITER && Roles.isGlobalWriter()) {\n            return true;\n        }\n\n        EntityOwner owner = project.getOwner();\n        if (ResourceAccessUtils.isSame(principal, owner)) {\n            // the owner can do anything with his projects\n            return true;\n        }\n\n        if (orgMembersOnly && project.getVisibility() == ProjectVisibility.PUBLIC\n                && level == ResourceAccessLevel.READER\n                && userDao.isInOrganization(principal.getId(), project.getOrgId())) {\n            // organization members can READ any public project in the same organization\n            return true;\n        }\n\n        OrganizationEntry org = orgManager.assertAccess(project.getOrgId(), false);\n        if (ResourceAccessUtils.isSame(principal, org.getOwner())) {\n            // the org owner can do anything with the org's projects\n            return true;\n        }\n\n        if (orgMembersOnly || project.getVisibility() != ProjectVisibility.PUBLIC) {\n            // we need to check the resource's access level if the access is limited to\n            // the organization's members or the project is not public\n            if (!projectDao.hasAccessLevel(project.getId(), principal.getId(), ResourceAccessLevel.atLeast(level))) {\n                throw new UnauthorizedException(\"The current user (\" + principal.getUsername() + \") doesn't have \" +\n                        \"the necessary access level (\" + level + \") to the project: \" + project.getName());\n            }\n        }\n\n        return true;\n    }\n\n    public List<ResourceAccessEntry> getResourceAccess(UUID projectId) {\n        assertAccess(projectId, ResourceAccessLevel.READER, false);\n        return projectDao.getAccessLevel(projectId);\n    }\n\n    public void updateAccessLevel(UUID projectId, Collection<ResourceAccessEntry> entries, boolean isReplace) {\n        assertAccess(projectId, ResourceAccessLevel.OWNER, true);\n\n        projectDao.tx(tx -> {\n            if (isReplace) {\n                projectDao.deleteTeamAccess(tx, projectId);\n            }\n\n            for (ResourceAccessEntry e : entries) {\n                projectDao.upsertAccessLevel(tx, projectId, e.getTeamId(), e.getLevel());\n            }\n        });\n\n        addAuditLog(projectId, entries, isReplace);\n    }\n\n    /**\n     * @return {@code true} if the current user is a member of a team that has\n     * access to the specified project.\n     */\n    public boolean isTeamMember(UUID projectId) {\n        UserPrincipal principal = UserPrincipal.assertCurrent();\n        return projectDao.hasAccessLevel(projectId, principal.getId(), ResourceAccessLevel.atLeast(ResourceAccessLevel.READER));\n    }\n\n    private void addAuditLog(UUID projectId, Collection<ResourceAccessEntry> entries, boolean isReplace) {\n        List<ImmutableMap<String, ? extends Serializable>> teams = entries.stream()\n                .map(e -> ImmutableMap.of(\"id\", e.getTeamId(), \"level\", e.getLevel()))\n                .collect(Collectors.toList());\n\n        auditLog.add(AuditObject.PROJECT, AuditAction.UPDATE)\n                .field(\"projectId\", projectId)\n                .field(\"access\", ImmutableMap.of(\n                        \"replace\", isReplace,\n                        \"teams\", teams))\n                .log();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/ProjectDao.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.Utils;\nimport com.walmartlabs.concord.server.UuidGenerator;\nimport com.walmartlabs.concord.server.jooq.enums.OutVariablesMode;\nimport com.walmartlabs.concord.server.jooq.enums.ProcessExecMode;\nimport com.walmartlabs.concord.server.jooq.enums.RawPayloadMode;\nimport com.walmartlabs.concord.server.jooq.tables.Organizations;\nimport com.walmartlabs.concord.server.jooq.tables.Projects;\nimport com.walmartlabs.concord.server.jooq.tables.Users;\nimport com.walmartlabs.concord.server.jooq.tables.records.ProjectsRecord;\nimport com.walmartlabs.concord.server.org.EntityOwner;\nimport com.walmartlabs.concord.server.org.ResourceAccessEntry;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.user.UserType;\nimport org.jooq.*;\n\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\nimport java.util.*;\nimport java.util.function.Supplier;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.V_USER_TEAMS;\nimport static com.walmartlabs.concord.server.jooq.tables.Organizations.ORGANIZATIONS;\nimport static com.walmartlabs.concord.server.jooq.tables.ProjectKvStore.PROJECT_KV_STORE;\nimport static com.walmartlabs.concord.server.jooq.tables.ProjectTeamAccess.PROJECT_TEAM_ACCESS;\nimport static com.walmartlabs.concord.server.jooq.tables.Projects.PROJECTS;\nimport static com.walmartlabs.concord.server.jooq.tables.Teams.TEAMS;\nimport static com.walmartlabs.concord.server.jooq.tables.Users.USERS;\nimport static java.util.Objects.requireNonNull;\nimport static org.jooq.impl.DSL.*;\n\npublic class ProjectDao extends AbstractDao {\n\n    private final ConcordObjectMapper objectMapper;\n    private final UuidGenerator uuidGenerator;\n\n    @Inject\n    public ProjectDao(@MainDB Configuration cfg,\n                      ConcordObjectMapper objectMapper,\n                      UuidGenerator uuidGenerator) {\n        super(cfg);\n        this.objectMapper = requireNonNull(objectMapper);\n        this.uuidGenerator = requireNonNull(uuidGenerator);\n    }\n\n    @Override\n    public void tx(Tx t) {\n        super.tx(t);\n    }\n\n    @Override\n    public <T> T txResult(TxResult<T> t) {\n        return super.txResult(t);\n    }\n\n    public String getName(UUID projectId) {\n        return dsl().select(PROJECTS.PROJECT_NAME)\n                .from(PROJECTS)\n                .where(PROJECTS.PROJECT_ID.eq(projectId))\n                .fetchOne(PROJECTS.PROJECT_NAME);\n    }\n\n    public UUID getId(UUID orgId, String projectName) {\n        return getId(dsl(), orgId, projectName);\n    }\n\n    public UUID getId(DSLContext tx, UUID orgId, String projectName) {\n        return tx.select(PROJECTS.PROJECT_ID)\n                .from(PROJECTS)\n                .where(PROJECTS.ORG_ID.eq(orgId)\n                        .and(PROJECTS.PROJECT_NAME.eq(projectName)))\n                .fetchOne(PROJECTS.PROJECT_ID);\n    }\n\n    public UUID getOrgId(UUID projectId) {\n        return dsl().select(PROJECTS.ORG_ID)\n                .from(PROJECTS)\n                .where(PROJECTS.PROJECT_ID.eq(projectId))\n                .fetchOne(PROJECTS.ORG_ID);\n    }\n\n    public String getOrgName(UUID projectId) {\n        return dsl().select(ORGANIZATIONS.ORG_ID)\n                .from(PROJECTS, ORGANIZATIONS)\n                .where(ORGANIZATIONS.ORG_ID.eq(PROJECTS.ORG_ID)\n                        .and(PROJECTS.PROJECT_ID.eq(projectId)))\n                .fetchOne(ORGANIZATIONS.ORG_NAME);\n    }\n\n    public ProjectEntry get(UUID projectId) {\n        return get(dsl(), projectId);\n    }\n\n    public ProjectEntry get(DSLContext tx, UUID projectId) {\n        Projects p = PROJECTS.as(\"p\");\n        Users u = USERS.as(\"u\");\n\n        Field<String> orgNameField = select(ORGANIZATIONS.ORG_NAME).from(ORGANIZATIONS).where(ORGANIZATIONS.ORG_ID.eq(p.ORG_ID)).asField();\n\n        Record17<UUID, String, String, UUID, String, JSONB, String, UUID, String, String, String, String, RawPayloadMode, JSONB, OutVariablesMode, ProcessExecMode, OffsetDateTime> r = tx.select(\n                p.PROJECT_ID,\n                p.PROJECT_NAME,\n                p.DESCRIPTION,\n                p.ORG_ID,\n                orgNameField,\n                p.PROJECT_CFG,\n                p.VISIBILITY,\n                p.OWNER_ID,\n                u.USERNAME,\n                u.DOMAIN,\n                u.USER_TYPE,\n                u.DISPLAY_NAME,\n                p.RAW_PAYLOAD_MODE,\n                p.META,\n                p.OUT_VARIABLES_MODE,\n                p.PROCESS_EXEC_MODE,\n                p.CREATED_AT)\n                .from(p)\n                .leftJoin(u).on(u.USER_ID.eq(p.OWNER_ID))\n                .where(p.PROJECT_ID.eq(projectId))\n                .fetchOne();\n\n        if (r == null) {\n            return null;\n        }\n\n        Map<String, Object> cfg = objectMapper.fromJSONB(r.get(p.PROJECT_CFG));\n\n        return new ProjectEntry(projectId,\n                r.get(p.PROJECT_NAME),\n                r.get(p.DESCRIPTION),\n                r.get(p.ORG_ID),\n                r.get(orgNameField),\n                null,\n                cfg,\n                ProjectVisibility.valueOf(r.get(p.VISIBILITY)),\n                toOwner(r.get(p.OWNER_ID), r.get(u.USERNAME), r.get(u.DOMAIN), r.get(u.DISPLAY_NAME), r.get(u.USER_TYPE)),\n                r.get(p.RAW_PAYLOAD_MODE),\n                objectMapper.fromJSONB(r.get(p.META)),\n                r.get(p.OUT_VARIABLES_MODE),\n                r.get(p.PROCESS_EXEC_MODE),\n                r.get(p.CREATED_AT));\n    }\n\n    public UUID insert(UUID orgId, String name, String description, UUID ownerId, Map<String, Object> cfg,\n                       ProjectVisibility visibility, RawPayloadMode rawPayloadMode, byte[] encryptedKey, Map<String, Object> meta,\n                       OutVariablesMode outVariablesMode, ProcessExecMode processExecMode) {\n\n        return txResult(tx -> insert(tx, orgId, name, description, ownerId, cfg, visibility, rawPayloadMode, encryptedKey, meta, outVariablesMode, processExecMode));\n    }\n\n    public UUID insert(DSLContext tx, UUID orgId, String name, String description, UUID ownerId, Map<String, Object> cfg,\n                       ProjectVisibility visibility, RawPayloadMode rawPayloadMode, byte[] encryptedKey, Map<String, Object> meta,\n                       OutVariablesMode outVariablesMode, ProcessExecMode processExecMode) {\n\n        if (visibility == null) {\n            visibility = ProjectVisibility.PUBLIC;\n        }\n\n        UUID projectId = uuidGenerator.generate();\n\n        return tx.insertInto(PROJECTS)\n                .columns(PROJECTS.PROJECT_ID,\n                        PROJECTS.PROJECT_NAME,\n                        PROJECTS.DESCRIPTION,\n                        PROJECTS.ORG_ID,\n                        PROJECTS.PROJECT_CFG,\n                        PROJECTS.VISIBILITY,\n                        PROJECTS.OWNER_ID,\n                        PROJECTS.RAW_PAYLOAD_MODE,\n                        PROJECTS.SECRET_KEY,\n                        PROJECTS.META,\n                        PROJECTS.OUT_VARIABLES_MODE,\n                        PROJECTS.PROCESS_EXEC_MODE,\n                        PROJECTS.CREATED_AT)\n                .values(value(projectId),\n                        value(name),\n                        value(description),\n                        value(orgId),\n                        value(objectMapper.toJSONB(cfg)),\n                        value(visibility.toString()),\n                        value(ownerId),\n                        value(rawPayloadMode != null ? rawPayloadMode : RawPayloadMode.DISABLED),\n                        value(encryptedKey),\n                        value(objectMapper.toJSONB(meta)),\n                        value(outVariablesMode != null ? outVariablesMode : OutVariablesMode.DISABLED),\n                        value(processExecMode != null ? processExecMode : ProcessExecMode.READERS),\n                        currentOffsetDateTime())\n                .returning(PROJECTS.PROJECT_ID)\n                .fetchOne()\n                .getProjectId();\n    }\n\n    public void update(DSLContext tx, UUID orgId, UUID id, ProjectVisibility visibility,\n                       String name, String description, Map<String, Object> cfg, RawPayloadMode rawPayloadMode,\n                       UUID ownerId, Map<String, Object> meta, OutVariablesMode outVariablesMode,\n                       ProcessExecMode processExecMode) {\n\n        UpdateSetFirstStep<ProjectsRecord> q = tx.update(PROJECTS);\n\n        if (name != null) {\n            q.set(PROJECTS.PROJECT_NAME, name);\n        }\n\n        if (description != null) {\n            q.set(PROJECTS.DESCRIPTION, description);\n        }\n\n        if (cfg != null) {\n            q.set(PROJECTS.PROJECT_CFG, objectMapper.toJSONB(cfg));\n        }\n\n        if (visibility != null) {\n            q.set(PROJECTS.VISIBILITY, visibility.toString());\n        }\n\n        if (rawPayloadMode != null) {\n            q.set(PROJECTS.RAW_PAYLOAD_MODE, rawPayloadMode);\n        }\n\n        if (ownerId != null) {\n            q.set(PROJECTS.OWNER_ID, ownerId);\n        }\n\n        if (meta != null) {\n            q.set(PROJECTS.META, objectMapper.toJSONB(meta));\n        }\n\n        if (outVariablesMode != null) {\n            q.set(PROJECTS.OUT_VARIABLES_MODE, outVariablesMode);\n        }\n\n        if (processExecMode != null) {\n            q.set(PROJECTS.PROCESS_EXEC_MODE, processExecMode);\n        }\n\n        q.set(PROJECTS.ORG_ID, orgId)\n                .where(PROJECTS.PROJECT_ID.eq(id))\n                .execute();\n    }\n\n    public void updateCfg(UUID id, Map<String, Object> cfg) {\n        tx(tx -> updateCfg(tx, id, cfg));\n    }\n\n    public void updateCfg(DSLContext tx, UUID id, Map<String, Object> cfg) {\n        tx.update(PROJECTS)\n                .set(PROJECTS.PROJECT_CFG, objectMapper.toJSONB(cfg))\n                .where(PROJECTS.PROJECT_ID.eq(id))\n                .execute();\n    }\n\n    public void delete(UUID projectId) {\n        tx(tx -> delete(tx, projectId));\n    }\n\n    public void delete(DSLContext tx, UUID projectId) {\n        tx.deleteFrom(PROJECT_KV_STORE)\n                .where(PROJECT_KV_STORE.PROJECT_ID.eq(projectId))\n                .execute();\n\n        tx.deleteFrom(PROJECTS)\n                .where(PROJECTS.PROJECT_ID.eq(projectId))\n                .execute();\n    }\n\n    public List<ProjectEntry> list(UUID orgId, UUID currentUserId, Field<?> sortField,\n                                   boolean asc, int offset, int limit, String filter) {\n\n        Users u = USERS.as(\"u\");\n        Projects p = PROJECTS.as(\"p\");\n        Organizations o = ORGANIZATIONS.as(\"o\");\n\n        sortField = p.field(sortField);\n\n\n        SelectOnConditionStep<Record15<UUID, String, String, UUID, String, String, UUID, String, String, String, String, RawPayloadMode, OutVariablesMode, ProcessExecMode, OffsetDateTime>> q = dsl().select(\n                p.PROJECT_ID,\n                p.PROJECT_NAME,\n                p.DESCRIPTION,\n                p.ORG_ID,\n                o.ORG_NAME,\n                p.VISIBILITY,\n                p.OWNER_ID,\n                u.USERNAME,\n                u.DOMAIN,\n                u.DISPLAY_NAME,\n                u.USER_TYPE,\n                p.RAW_PAYLOAD_MODE,\n                p.OUT_VARIABLES_MODE,\n                p.PROCESS_EXEC_MODE,\n                p.CREATED_AT)\n                .from(p)\n                .leftJoin(u).on(u.USER_ID.eq(p.OWNER_ID))\n                .leftJoin(o).on(o.ORG_ID.eq(p.ORG_ID));\n\n        if (currentUserId != null) {\n            // public projects are visible for anyone\n            Condition isPublic = p.VISIBILITY.eq(ProjectVisibility.PUBLIC.toString());\n\n            // check if the user belongs to a team in the org\n            SelectConditionStep<Record1<UUID>> teamIds = select(TEAMS.TEAM_ID)\n                    .from(TEAMS)\n                    .where(TEAMS.ORG_ID.eq(orgId));\n\n            Condition isInATeam = exists(selectOne().from(V_USER_TEAMS)\n                    .where(V_USER_TEAMS.USER_ID.eq(currentUserId)\n                            .and(V_USER_TEAMS.TEAM_ID.in(teamIds))));\n\n            // check if the user owns projects in the org\n            Condition ownsProjects = p.OWNER_ID.eq(currentUserId);\n\n            // check if the user owns the org\n            Condition ownsOrg = o.OWNER_ID.eq(currentUserId);\n\n            // if any of those conditions true then the project must be visible\n            q.where(or(isPublic, isInATeam, ownsProjects, ownsOrg));\n        }\n\n        if (orgId != null) {\n            q.where(p.ORG_ID.eq(orgId));\n        }\n\n        if (sortField != null) {\n            q.orderBy(asc ? sortField.asc() : sortField.desc());\n        }\n\n        if (filter != null) {\n            q.where(p.PROJECT_NAME.containsIgnoreCase(filter));\n        }\n\n        if (offset > 0) {\n            q.offset(offset);\n        }\n\n        if (limit > 0) {\n            q.limit(limit);\n        }\n\n        return q.fetch(ProjectDao::toEntry);\n    }\n\n    public Map<String, Object> getConfiguration(UUID projectId) {\n        return dsl().select(PROJECTS.PROJECT_CFG)\n                .from(PROJECTS)\n                .where(PROJECTS.PROJECT_ID.eq(projectId))\n                .fetchOne(e -> objectMapper.fromJSONB(e.value1()));\n    }\n\n    public Object getConfigurationValue(UUID projectId, String... path) {\n        Map<String, Object> cfg = getConfiguration(projectId);\n        return ConfigurationUtils.get(cfg, path);\n    }\n\n    public boolean hasAccessLevel(UUID projectId, UUID userId, ResourceAccessLevel... levels) {\n        return hasAccessLevel(dsl(), projectId, userId, levels);\n    }\n\n    public boolean hasAccessLevel(DSLContext tx, UUID projectId, UUID userId, ResourceAccessLevel... levels) {\n        SelectConditionStep<Record1<UUID>> teamIds = select(V_USER_TEAMS.TEAM_ID)\n                .from(V_USER_TEAMS)\n                .where(V_USER_TEAMS.USER_ID.eq(userId));\n\n        return tx.fetchExists(selectFrom(PROJECT_TEAM_ACCESS)\n                .where(PROJECT_TEAM_ACCESS.PROJECT_ID.eq(projectId)\n                        .and(PROJECT_TEAM_ACCESS.TEAM_ID.in(teamIds))\n                        .and(PROJECT_TEAM_ACCESS.ACCESS_LEVEL.in(Utils.toString(levels)))));\n    }\n\n    public void upsertAccessLevel(UUID projectId, UUID teamId, ResourceAccessLevel level) {\n        tx(tx -> upsertAccessLevel(tx, projectId, teamId, level));\n    }\n\n    public void upsertAccessLevel(DSLContext tx, UUID projectId, UUID teamId, ResourceAccessLevel level) {\n        tx.insertInto(PROJECT_TEAM_ACCESS)\n                .columns(PROJECT_TEAM_ACCESS.PROJECT_ID, PROJECT_TEAM_ACCESS.TEAM_ID, PROJECT_TEAM_ACCESS.ACCESS_LEVEL)\n                .values(projectId, teamId, level.toString())\n                .onDuplicateKeyUpdate()\n                .set(PROJECT_TEAM_ACCESS.ACCESS_LEVEL, level.toString())\n                .execute();\n    }\n\n    public byte[] getOrUpdateSecretKey(UUID projectId, Supplier<byte[]> keySupplier) {\n        return txResult(tx -> {\n            byte[] data = tx.select(PROJECTS.SECRET_KEY).from(PROJECTS)\n                    .where(PROJECTS.PROJECT_ID.eq(projectId))\n                    .fetchOne(PROJECTS.SECRET_KEY);\n\n            // fast path\n            if (data != null) {\n                return data;\n            }\n\n            // add the key if not exists\n            data = tx.select(PROJECTS.SECRET_KEY).from(PROJECTS)\n                    .where(PROJECTS.PROJECT_ID.eq(projectId))\n                    .forUpdate()\n                    .fetchOne(PROJECTS.SECRET_KEY);\n\n            if (data == null) {\n                data = keySupplier.get();\n\n                tx.update(PROJECTS)\n                        .set(PROJECTS.SECRET_KEY, data)\n                        .where(PROJECTS.PROJECT_ID.eq(projectId))\n                        .execute();\n            }\n\n            return data;\n        });\n    }\n\n    public List<ResourceAccessEntry> getAccessLevel(UUID projectId) {\n        List<ResourceAccessEntry> resourceAccessList = new ArrayList<>();\n\n        Result<Record5<UUID, UUID, String, String, String>> teamsAccess = dsl().select(\n                PROJECT_TEAM_ACCESS.TEAM_ID,\n                PROJECT_TEAM_ACCESS.PROJECT_ID,\n                TEAMS.TEAM_NAME,\n                ORGANIZATIONS.ORG_NAME,\n                PROJECT_TEAM_ACCESS.ACCESS_LEVEL)\n                .from(PROJECT_TEAM_ACCESS)\n                .leftOuterJoin(TEAMS).on(TEAMS.TEAM_ID.eq(PROJECT_TEAM_ACCESS.TEAM_ID))\n                .leftOuterJoin(PROJECTS).on(PROJECTS.PROJECT_ID.eq(projectId))\n                .leftOuterJoin(ORGANIZATIONS).on(ORGANIZATIONS.ORG_ID.eq(PROJECTS.ORG_ID))\n                .where(PROJECT_TEAM_ACCESS.PROJECT_ID.eq(projectId))\n                .fetch();\n\n        for (Record5<UUID, UUID, String, String, String> t : teamsAccess) {\n            resourceAccessList.add(new ResourceAccessEntry(t.get(PROJECT_TEAM_ACCESS.TEAM_ID),\n                    t.get(ORGANIZATIONS.ORG_NAME),\n                    t.get(TEAMS.TEAM_NAME),\n                    ResourceAccessLevel.valueOf(t.get(PROJECT_TEAM_ACCESS.ACCESS_LEVEL))));\n        }\n\n        return resourceAccessList;\n    }\n\n    public void deleteTeamAccess(DSLContext tx, UUID projectId) {\n        tx.deleteFrom(PROJECT_TEAM_ACCESS)\n                .where(PROJECT_TEAM_ACCESS.PROJECT_ID.eq(projectId))\n                .execute();\n    }\n\n    private static ProjectEntry toEntry(Record15<UUID, String, String, UUID, String, String, UUID, String, String, String, String, RawPayloadMode, OutVariablesMode, ProcessExecMode, OffsetDateTime> r) {\n        return new ProjectEntry(r.get(PROJECTS.PROJECT_ID),\n                r.get(PROJECTS.PROJECT_NAME),\n                r.get(PROJECTS.DESCRIPTION),\n                r.get(PROJECTS.ORG_ID),\n                r.value5(),\n                null,\n                null,\n                ProjectVisibility.valueOf(r.get(PROJECTS.VISIBILITY)),\n                toOwner(r.get(PROJECTS.OWNER_ID), r.get(USERS.USERNAME), r.get(USERS.DOMAIN), r.get(USERS.DISPLAY_NAME), r.get(USERS.USER_TYPE)),\n                r.get(PROJECTS.RAW_PAYLOAD_MODE),\n                null,\n                r.get(PROJECTS.OUT_VARIABLES_MODE),\n                r.get(PROJECTS.PROCESS_EXEC_MODE),\n                r.get(PROJECTS.CREATED_AT));\n    }\n\n    private static EntityOwner toOwner(UUID id, String username, String domain, String displayName, String userType) {\n        if (id == null) {\n            return null;\n        }\n        return EntityOwner.builder()\n                .id(id)\n                .username(username)\n                .userDomain(domain)\n                .displayName(displayName)\n                .userType(UserType.valueOf(userType))\n                .build();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/ProjectEntry.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.server.jooq.enums.OutVariablesMode;\nimport com.walmartlabs.concord.server.jooq.enums.ProcessExecMode;\nimport com.walmartlabs.concord.server.jooq.enums.RawPayloadMode;\nimport com.walmartlabs.concord.server.org.EntityOwner;\n\nimport javax.validation.Valid;\nimport javax.validation.constraints.Size;\nimport java.io.Serial;\nimport java.io.Serializable;\nimport java.time.OffsetDateTime;\nimport java.util.Map;\nimport java.util.UUID;\n\n@JsonInclude(Include.NON_NULL)\npublic class ProjectEntry implements Serializable {\n\n    @Serial\n    private static final long serialVersionUID = 1L;\n\n    public static ProjectEntry replace(ProjectEntry e, Map<String, RepositoryEntry> repos) {\n        return new ProjectEntry(e.id, e.name, e.description, e.orgId, e.orgName, repos,\n                e.cfg, e.visibility, e.owner, e.rawPayloadMode, e.meta, e.outVariablesMode,\n                e.processExecMode, e.createdAt);\n    }\n\n    private final UUID id;\n\n    @ConcordKey\n    // TODO it should be final, but swagger makes it readOnly and provides no way to set the value\n    private String name;\n\n    @Size(max = 1024)\n    private final String description;\n\n    private final UUID orgId;\n\n    @ConcordKey\n    private final String orgName;\n\n    @Deprecated\n    @Valid\n    private final Map<String, RepositoryEntry> repositories;\n\n    private final Map<String, Object> cfg;\n\n    private final ProjectVisibility visibility;\n\n    private final EntityOwner owner;\n\n    private final RawPayloadMode rawPayloadMode;\n\n    private final OutVariablesMode outVariablesMode;\n\n    private final ProcessExecMode processExecMode;\n\n    private final Map<String, Object> meta;\n\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    private final OffsetDateTime createdAt;\n\n    public ProjectEntry(String name) {\n        this(null, name, null, null, null, null, null, null, null, RawPayloadMode.DISABLED, null, OutVariablesMode.DISABLED, ProcessExecMode.READERS, null);\n    }\n\n    public ProjectEntry(String name, ProjectVisibility visibility) {\n        this(null, name, null, null, null, null, null, visibility, null, RawPayloadMode.DISABLED, null, OutVariablesMode.DISABLED, ProcessExecMode.READERS, null);\n    }\n\n    public ProjectEntry(String name, Map<String, RepositoryEntry> repositories) {\n        this(null, name, null, null, null, repositories, null, null, null, RawPayloadMode.DISABLED, null, OutVariablesMode.DISABLED, ProcessExecMode.READERS, null);\n    }\n\n    public ProjectEntry(String name, UUID id) {\n        this(id, name, null, null, null, null, null, null, null, RawPayloadMode.DISABLED, null, OutVariablesMode.DISABLED, ProcessExecMode.READERS, null);\n    }\n\n    @JsonCreator\n    public ProjectEntry(@JsonProperty(\"id\") UUID id,\n                        @JsonProperty(\"name\") String name,\n                        @JsonProperty(\"description\") String description,\n                        @JsonProperty(\"orgId\") UUID orgId,\n                        @JsonProperty(\"orgName\") String orgName,\n                        @JsonProperty(\"repositories\") Map<String, RepositoryEntry> repositories,\n                        @JsonProperty(\"cfg\") Map<String, Object> cfg,\n                        @JsonProperty(\"visibility\") ProjectVisibility visibility,\n                        @JsonProperty(\"owner\") EntityOwner owner,\n                        @JsonProperty(\"rawPayloadMode\") RawPayloadMode rawPayloadMode,\n                        @JsonProperty(\"meta\") Map<String, Object> meta,\n                        @JsonProperty(\"outVariablesMode\") OutVariablesMode outVariablesMode,\n                        @JsonProperty(\"processExecMode\") ProcessExecMode processExecMode,\n                        @JsonProperty(\"createdAt\") OffsetDateTime createdAt) {\n\n        this.id = id;\n        this.name = name;\n        this.description = description;\n        this.orgId = orgId;\n        this.orgName = orgName;\n        this.repositories = repositories;\n        this.cfg = cfg;\n        this.visibility = visibility;\n        this.owner = owner;\n        this.rawPayloadMode = rawPayloadMode;\n        this.meta = meta;\n        this.outVariablesMode = outVariablesMode;\n        this.processExecMode = processExecMode;\n        this.createdAt = createdAt;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getDescription() {\n        return description;\n    }\n\n    public UUID getOrgId() {\n        return orgId;\n    }\n\n    public String getOrgName() {\n        return orgName;\n    }\n\n    public Map<String, RepositoryEntry> getRepositories() {\n        return repositories;\n    }\n\n    public Map<String, Object> getCfg() {\n        return cfg;\n    }\n\n    public ProjectVisibility getVisibility() {\n        return visibility;\n    }\n\n    public EntityOwner getOwner() {\n        return owner;\n    }\n\n    public RawPayloadMode getRawPayloadMode() {\n        return rawPayloadMode;\n    }\n\n    public OutVariablesMode getOutVariablesMode() {\n        return outVariablesMode;\n    }\n\n    public ProcessExecMode getProcessExecMode() {\n        return processExecMode;\n    }\n\n    public Map<String, Object> getMeta() {\n        return meta;\n    }\n\n    public OffsetDateTime getCreatedAt() {\n        return createdAt;\n    }\n\n    @Override\n    public String toString() {\n        return \"ProjectEntry{\" +\n                \"id=\" + id +\n                \", name='\" + name + '\\'' +\n                \", description='\" + description + '\\'' +\n                \", orgId=\" + orgId +\n                \", orgName='\" + orgName + '\\'' +\n                \", repositories=\" + repositories +\n                \", cfg=\" + cfg +\n                \", visibility=\" + visibility +\n                \", owner=\" + owner +\n                \", rawPayloadMode=\" + rawPayloadMode +\n                \", outVariablesMode=\" + outVariablesMode +\n                \", meta=\" + meta +\n                \", createdAt=\" + createdAt +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/ProjectKvCapacity.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\n\n@Value.Immutable\n@JsonSerialize(as = ImmutableProjectKvCapacity.class)\n@JsonDeserialize(as = ImmutableProjectKvCapacity.class)\n@JsonInclude(JsonInclude.Include.NON_NULL)\npublic interface ProjectKvCapacity {\n\n    long size();\n\n    @Nullable\n    Integer maxSize();\n\n    static ImmutableProjectKvCapacity.Builder builder() {\n        return ImmutableProjectKvCapacity.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/ProjectManager.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.runtime.model.ProcessDefinition;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.audit.AuditAction;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.audit.AuditObject;\nimport com.walmartlabs.concord.server.jooq.enums.RawPayloadMode;\nimport com.walmartlabs.concord.server.org.EntityOwner;\nimport com.walmartlabs.concord.server.org.OrganizationEntry;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.org.secret.SecretDao;\nimport com.walmartlabs.concord.server.policy.EntityAction;\nimport com.walmartlabs.concord.server.policy.EntityType;\nimport com.walmartlabs.concord.server.policy.PolicyManager;\nimport com.walmartlabs.concord.server.policy.PolicyUtils;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.user.UserEntry;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport com.walmartlabs.concord.server.user.UserType;\nimport org.jooq.DSLContext;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.core.Response;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.server.jooq.tables.Projects.PROJECTS;\n\npublic class ProjectManager {\n\n    private final OrganizationManager orgManager;\n    private final PolicyManager policyManager;\n    private final ProjectDao projectDao;\n    private final RepositoryDao repositoryDao;\n    private final SecretDao secretDao;\n    private final KvDao kvDao;\n    private final ProjectRepositoryManager projectRepositoryManager;\n    private final ProjectAccessManager accessManager;\n    private final AuditLog auditLog;\n    private final EncryptedProjectValueManager encryptedValueManager;\n    private final UserManager userManager;\n\n    @Inject\n    public ProjectManager(OrganizationManager orgManager,\n                          PolicyManager policyManager,\n                          ProjectDao projectDao,\n                          RepositoryDao repositoryDao,\n                          SecretDao secretDao,\n                          KvDao kvDao,\n                          ProjectRepositoryManager projectRepositoryManager,\n                          ProjectAccessManager accessManager,\n                          AuditLog auditLog,\n                          EncryptedProjectValueManager encryptedValueManager,\n                          UserManager userManager) {\n\n        this.policyManager = policyManager;\n        this.projectDao = projectDao;\n        this.repositoryDao = repositoryDao;\n        this.secretDao = secretDao;\n        this.kvDao = kvDao;\n        this.projectRepositoryManager = projectRepositoryManager;\n        this.accessManager = accessManager;\n        this.auditLog = auditLog;\n        this.encryptedValueManager = encryptedValueManager;\n        this.userManager = userManager;\n        this.orgManager = orgManager;\n    }\n\n    public ProjectEntry get(String orgName, String projectName) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n\n        UUID projectId = projectDao.getId(org.getId(), projectName);\n        if (projectId == null) {\n            throw new ConcordApplicationException(\"Project not found: \" + projectName, Response.Status.NOT_FOUND);\n        }\n\n        return get(projectId);\n    }\n\n    public ProjectEntry get(UUID projectId) {\n        return projectDao.txResult(tx -> get(tx, projectId));\n    }\n\n    public ProjectEntry get(DSLContext tx, UUID projectId) {\n        return accessManager.assertAccess(tx, projectId, ResourceAccessLevel.READER, false);\n    }\n\n    /**\n     * Creates a new project or updates an existing one.\n     * <p/>\n     * To update an existing project, a {@link ProjectEntry#getId()}\n     * value must be provided.\n     * <p/>\n     * When updating an existing project, only non-null properties are updated.\n     * E.g. if you wish to update only the project's visibility, set only the ID\n     * and the visibility values.\n     */\n    public ProjectOperationResult createOrUpdate(String orgName, ProjectEntry entry) {\n        entry = normalize(entry);\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n\n        UUID projectId = entry.getId();\n        if (projectId == null) {\n            assertName(entry);\n            projectId = projectDao.getId(org.getId(), entry.getName());\n        }\n\n        if (projectId == null) {\n            projectId = insert(org.getId(), org.getName(), entry);\n            return ProjectOperationResult.builder()\n                    .result(OperationResult.CREATED)\n                    .projectId(projectId)\n                    .build();\n        } else {\n            update(projectId, entry);\n            return ProjectOperationResult.builder()\n                    .result(OperationResult.UPDATED)\n                    .projectId(projectId)\n                    .build();\n        }\n    }\n\n    public ProjectKvCapacity getKvCapacity(String orgName, String projectName) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n\n        ProjectEntry project = accessManager.assertAccess(org.getId(), null, projectName, ResourceAccessLevel.READER, false);\n\n        long currentSize = kvDao.count(project.getId());\n\n        PolicyEngine policy = policyManager.get(org.getId(), project.getId(), UserPrincipal.assertCurrent().getId());\n        Integer maxEntries = null;\n        if (policy != null) {\n            maxEntries = policy.getKvPolicy().getMaxEntries();\n        }\n\n        return ProjectKvCapacity.builder()\n                .size(currentSize)\n                .maxSize(maxEntries)\n                .build();\n    }\n\n    private UUID insert(UUID orgId, String orgName, ProjectEntry entry) {\n        UserEntry owner = getOwner(entry.getOwner(), UserPrincipal.assertCurrent().getUser());\n        policyManager.checkEntity(orgId, null, EntityType.PROJECT, EntityAction.CREATE, owner, PolicyUtils.projectToMap(orgId, orgName, entry));\n\n        byte[] encryptedKey = encryptedValueManager.createEncryptedSecretKey();\n        Map<String, ProcessDefinition> processDefinitions = loadProcessDefinitions(orgId, null, entry);\n        UUID id = projectDao.txResult(tx -> insert(tx, orgId, owner, entry, encryptedKey, processDefinitions));\n\n        Map<String, Object> changes = DiffUtils.compare(null, entry);\n        addAuditLog(\n                AuditAction.CREATE,\n                orgId,\n                orgName,\n                id,\n                entry.getName(),\n                changes);\n\n        return id;\n    }\n\n    private UUID insert(DSLContext tx, UUID orgId, UserEntry owner, ProjectEntry entry, byte[] encryptedKey, Map<String, ProcessDefinition> processDefinitions) {\n        UUID id = projectDao.insert(tx, orgId, entry.getName(), entry.getDescription(), owner.getId(), entry.getCfg(),\n                entry.getVisibility(), entry.getRawPayloadMode(), encryptedKey, entry.getMeta(), entry.getOutVariablesMode(), entry.getProcessExecMode());\n\n        Map<String, RepositoryEntry> repos = entry.getRepositories();\n        if (repos != null) {\n            projectRepositoryManager.replace(tx, orgId, id, repos.values(), processDefinitions);\n        }\n        return id;\n    }\n\n    private Map<String, ProcessDefinition> loadProcessDefinitions(UUID orgId, UUID projectId, ProjectEntry projectEntry) {\n        if (projectEntry.getRepositories() == null || projectEntry.getRepositories().isEmpty()) {\n            return Collections.emptyMap();\n        }\n\n        Map<String, ProcessDefinition> result = new HashMap<>();\n        for (Map.Entry<String, RepositoryEntry> e : projectEntry.getRepositories().entrySet()) {\n            if (e.getValue().isDisabled()) {\n                continue;\n            }\n            ProcessDefinition processDefinition = projectRepositoryManager.processDefinition(orgId, projectId, e.getValue());\n            result.put(e.getKey(), processDefinition);\n        }\n        return result;\n    }\n\n    private void update(UUID projectId, ProjectEntry entry) {\n        ProjectEntry e = projectDao.get(projectId);\n        if (e == null) {\n            throw new ValidationErrorsException(\"Project not found: \" + projectId);\n        }\n\n        UserEntry owner = getOwner(entry.getOwner(), null);\n        policyManager.checkEntity(e.getOrgId(), projectId, EntityType.PROJECT, EntityAction.UPDATE, owner, PolicyUtils.projectToMap(e.getOrgId(), e.getOrgName(), entry));\n\n        UUID currentOwnerId = e.getOwner() != null ? e.getOwner().id() : null;\n        UUID updatedOwnerId = owner != null ? owner.getId() : null;\n\n        ResourceAccessLevel level = ResourceAccessLevel.WRITER;\n        if (updatedOwnerId != null && !updatedOwnerId.equals(currentOwnerId)) {\n            level = ResourceAccessLevel.OWNER;\n        }\n\n        ProjectEntry prevEntry = accessManager.assertAccess(projectId, level, true);\n        UUID orgId = prevEntry.getOrgId();\n\n        OrganizationEntry organizationEntry = null;\n\n        if (entry.getOrgId() != null) {\n            organizationEntry = orgManager.assertAccess(entry.getOrgId(), true);\n        } else if (entry.getOrgName() != null) {\n            organizationEntry = orgManager.assertAccess(entry.getOrgName(), true);\n        }\n\n        UUID orgIdUpdate = organizationEntry != null ? organizationEntry.getId() : orgId;\n\n        RawPayloadMode rawPayloadMode;\n        if (entry.getRawPayloadMode() == null) {\n            rawPayloadMode = RawPayloadMode.ORG_MEMBERS;\n        } else {\n            rawPayloadMode = entry.getRawPayloadMode();\n        }\n\n        Map<String, ProcessDefinition> processDefinitions = loadProcessDefinitions(e.getOrgId(), projectId, entry);\n\n        ProjectEntry newEntry = projectDao.txResult(tx -> update(tx, orgIdUpdate, orgId, projectId, entry, updatedOwnerId, rawPayloadMode, processDefinitions));\n\n        Map<String, Object> changes = DiffUtils.compare(prevEntry, newEntry);\n        addAuditLog(\n                AuditAction.UPDATE,\n                prevEntry.getOrgId(),\n                prevEntry.getOrgName(),\n                prevEntry.getId(),\n                prevEntry.getName(),\n                changes);\n    }\n\n    private ProjectEntry update(DSLContext tx, UUID orgIdUpdate, UUID orgIdPrev, UUID projectId, ProjectEntry entry, UUID updatedOwnerId, RawPayloadMode rawPayloadMode, Map<String, ProcessDefinition> processDefinitions) {\n        if (!orgIdUpdate.equals(orgIdPrev)) {\n            secretDao.updateProjectScopeByProjectId(tx, orgIdPrev, projectId, null);\n            repositoryDao.clearSecretMappingByProjectId(tx, projectId);\n        }\n\n        projectDao.update(tx, orgIdUpdate, projectId, entry.getVisibility(), entry.getName(),\n                entry.getDescription(), entry.getCfg(), rawPayloadMode, updatedOwnerId, entry.getMeta(), entry.getOutVariablesMode(),\n                entry.getProcessExecMode());\n\n        Map<String, RepositoryEntry> repos = entry.getRepositories();\n        if (repos != null) {\n            projectRepositoryManager.replace(tx, orgIdUpdate, projectId, entry.getRepositories().values(), processDefinitions);\n        }\n\n        return projectDao.get(tx, projectId);\n    }\n\n    public void delete(UUID projectId) {\n        ProjectEntry e = accessManager.assertAccess(projectId, ResourceAccessLevel.OWNER, true);\n\n        projectDao.delete(projectId);\n\n        addAuditLog(\n                AuditAction.DELETE,\n                e.getOrgId(),\n                e.getOrgName(),\n                e.getId(),\n                e.getName(),\n                null);\n    }\n\n    public List<ProjectEntry> list(UUID orgId, int offset, int limit, String filter) {\n        UserPrincipal p = UserPrincipal.assertCurrent();\n        UUID userId = p.getId();\n        if (Roles.isAdmin() || Roles.isGlobalReader() || Roles.isGlobalWriter()) {\n            // admins or \"global readers\" can see any project, so we shouldn't filter projects by user\n            userId = null;\n        }\n\n        return projectDao.list(orgId, userId, PROJECTS.PROJECT_NAME, true, offset, limit, filter);\n    }\n\n    public UserEntry getOwner(EntityOwner owner, UserEntry defaultOwner) {\n        if (owner == null) {\n            return defaultOwner;\n        }\n\n        if (owner.id() != null) {\n            return userManager.get(owner.id())\n                    .orElseThrow(() -> new ValidationErrorsException(\"User not found: \" + owner.id()));\n        }\n\n        if (owner.username() != null) {\n            return userManager.get(owner.username(), owner.userDomain(), UserType.LDAP)\n                    .orElseThrow(() -> new ConcordApplicationException(\"User not found: \" + owner.username()));\n        }\n\n        return defaultOwner;\n    }\n\n    private void addAuditLog(AuditAction auditAction, UUID orgId, String orgName, UUID projectId, String projectName, Map<String, Object> changes) {\n        auditLog.add(AuditObject.PROJECT, auditAction)\n                .field(\"orgId\", orgId)\n                .field(\"orgName\", orgName)\n                .field(\"projectId\", projectId)\n                .field(\"name\", projectName)\n                .field(\"changes\", changes)\n                .log();\n    }\n\n    private static ProjectEntry normalize(ProjectEntry e) {\n        Map<String, RepositoryEntry> repos = e.getRepositories();\n        if (repos == null) {\n            return e;\n        }\n\n        Map<String, RepositoryEntry> m = new HashMap<>(repos);\n\n        repos.forEach((k, v) -> {\n            if (v.getName() == null) {\n                RepositoryEntry r = new RepositoryEntry(k, v);\n                m.put(k, r);\n            }\n        });\n\n        return ProjectEntry.replace(e, m);\n    }\n\n    private static void assertName(ProjectEntry p) {\n        String s = p.getName();\n        if (s == null || s.trim().isEmpty()) {\n            throw new ValidationErrorsException(\"'name' is required\");\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/ProjectModule.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.walmartlabs.concord.server.Utils.bindExceptionMapper;\nimport static com.walmartlabs.concord.server.Utils.bindJaxRsResource;\n\npublic class ProjectModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        binder.bind(EncryptedProjectValueManager.class).in(SINGLETON);\n        binder.bind(KvDao.class).in(SINGLETON);\n        binder.bind(KvManager.class).in(SINGLETON);\n        binder.bind(ProjectAccessManager.class).in(SINGLETON);\n        binder.bind(ProjectDao.class).in(SINGLETON);\n        binder.bind(ProjectManager.class).in(SINGLETON);\n        binder.bind(ProjectRepositoryManager.class).in(SINGLETON);\n        binder.bind(RepositoryDao.class).in(SINGLETON);\n        \n        bindJaxRsResource(binder, ProjectResource.class);\n        bindJaxRsResource(binder, ProjectResourceV2.class);\n        bindJaxRsResource(binder, RepositoryResource.class);\n        bindJaxRsResource(binder, RepositoryResourceV2.class);\n        \n        bindExceptionMapper(binder, RepositoryValidationExceptionMapper.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/ProjectOperationResponse.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.server.OperationResult;\n\nimport java.io.Serializable;\nimport java.util.UUID;\n\npublic class ProjectOperationResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n    private final UUID id;\n    private final OperationResult result;\n\n    @JsonCreator\n    public ProjectOperationResponse(@JsonProperty(\"id\") UUID id,\n                                    @JsonProperty(\"result\") OperationResult result) {\n        this.id = id;\n        this.result = result;\n    }\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public OperationResult getResult() {\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"ProjectOperationResponse{\" +\n                \"ok=\" + ok +\n                \", id=\" + id +\n                \", result=\" + result +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/ProjectOperationResult.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.server.OperationResult;\nimport org.immutables.value.Value;\n\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonSerialize(as = ImmutableProjectOperationResult.class)\n@JsonDeserialize(as = ImmutableProjectOperationResult.class)\npublic interface ProjectOperationResult {\n\n    @Value.Default\n    default boolean ok() {\n        return true;\n    }\n\n    OperationResult result();\n\n    UUID projectId();\n\n    static ImmutableProjectOperationResult.Builder builder() {\n        return ImmutableProjectOperationResult.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/ProjectRepositoryManager.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.ProcessDefinition;\nimport com.walmartlabs.concord.server.audit.AuditAction;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.audit.AuditObject;\nimport com.walmartlabs.concord.server.jooq.Tables;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.org.secret.SecretEntryV2;\nimport com.walmartlabs.concord.server.org.secret.SecretManager;\nimport com.walmartlabs.concord.server.org.triggers.TriggerManager;\nimport com.walmartlabs.concord.server.policy.EntityAction;\nimport com.walmartlabs.concord.server.policy.EntityType;\nimport com.walmartlabs.concord.server.policy.PolicyManager;\nimport com.walmartlabs.concord.server.policy.PolicyUtils;\nimport com.walmartlabs.concord.server.repository.RepositoryRefresher;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport org.immutables.value.Value;\nimport org.jooq.DSLContext;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport javax.ws.rs.core.Response;\nimport java.util.*;\n\npublic class ProjectRepositoryManager {\n\n    private final ProjectAccessManager projectAccessManager;\n    private final SecretManager secretManager;\n    private final RepositoryDao repositoryDao;\n    private final AuditLog auditLog;\n    private final PolicyManager policyManager;\n    private final RepositoryRefresher repositoryRefresher;\n    private final TriggerManager triggerManager;\n\n    @Inject\n    public ProjectRepositoryManager(ProjectAccessManager projectAccessManager,\n                                    SecretManager secretManager,\n                                    RepositoryDao repositoryDao,\n                                    AuditLog auditLog,\n                                    PolicyManager policyManager,\n                                    RepositoryRefresher repositoryRefresher,\n                                    TriggerManager triggerManager) {\n\n        this.projectAccessManager = projectAccessManager;\n        this.secretManager = secretManager;\n        this.repositoryDao = repositoryDao;\n        this.auditLog = auditLog;\n        this.policyManager = policyManager;\n        this.repositoryRefresher = repositoryRefresher;\n        this.triggerManager = triggerManager;\n    }\n\n    public RepositoryEntry get(UUID projectId, String repositoryName) {\n        RepositoryEntry r = repositoryDao.get(projectId, repositoryName);\n\n        if (r == null) {\n            throw new ConcordApplicationException(\"Repository not found: \" + repositoryName, Response.Status.NOT_FOUND);\n        }\n\n        return r;\n    }\n\n    public RepositoryEntry get(UUID orgId, String projectName, String repositoryName) {\n        ProjectEntry project = projectAccessManager.assertAccess(orgId, null, projectName, ResourceAccessLevel.READER, false);\n        return get(project.getId(), repositoryName);\n    }\n\n    public List<RepositoryEntry> list(UUID projectId) {\n        return repositoryDao.list(projectId);\n    }\n\n    public List<RepositoryEntry> list(UUID orgId, String projectName, int offset, int limit, String filter) {\n        ProjectEntry project = projectAccessManager.assertAccess(orgId, null, projectName, ResourceAccessLevel.READER, false);\n        return repositoryDao.list(project.getId(), Tables.REPOSITORIES.REPO_NAME, true, offset, limit, filter);\n    }\n\n    public void createOrUpdate(UUID projectId, RepositoryEntry entry) {\n        ProjectEntry project = projectAccessManager.assertAccess(projectId, ResourceAccessLevel.WRITER, true);\n\n        UUID repoId = entry.getId() != null ? entry.getId() : repositoryDao.getId(projectId, entry.getName());\n\n        SecretEntryV2 secret = assertSecret(project.getOrgId(), entry);\n\n        policyManager.checkEntity(project.getOrgId(), project.getId(), EntityType.REPOSITORY, repoId == null ? EntityAction.CREATE : EntityAction.UPDATE,\n                null, PolicyUtils.repositoryToMap(project, entry, secret));\n\n        ProcessDefinition processDefinition = entry.isDisabled()\n                ? null\n                : processDefinition(project.getOrgId(), projectId, entry);\n\n        InsertUpdateResult result = repositoryDao.txResult(tx -> insertOrUpdate(tx, projectId, repoId, entry, secret, processDefinition));\n        addAuditLog(project, result.prevEntry(), result.newEntry());\n    }\n\n    public void replace(DSLContext tx, UUID orgId, UUID projectId, Collection<RepositoryEntry> repos, Map<String, ProcessDefinition> processDefinitions) {\n        repositoryDao.deleteAll(tx, projectId);\n\n        for (RepositoryEntry re : repos) {\n            SecretEntryV2 secret = assertSecret(orgId, re);\n            insertOrUpdate(tx, projectId, null, re, secret, processDefinitions.get(re.getName()));\n        }\n    }\n\n    public void delete(UUID projectId, String repoName) {\n        ProjectEntry projEntry = projectAccessManager.assertAccess(projectId, ResourceAccessLevel.WRITER, false);\n\n        RepositoryEntry r = repositoryDao.get(projectId, repoName);\n        if (r == null) {\n            throw new ValidationErrorsException(\"Repository not found: \" + repoName);\n        }\n\n        repositoryDao.delete(r.getId());\n        addAuditLog(projEntry, r, null);\n    }\n\n    public ProjectValidator.Result validateRepository(UUID orgId, RepositoryEntry repo) {\n        try {\n            ProcessDefinition pd = processDefinition(orgId, repo.getProjectId(), repo);\n            return ProjectValidator.validate(pd);\n        } catch (Exception e) {\n            throw new RepositoryValidationException(\"Validation failed: \" + repo.getName(), e);\n        }\n    }\n\n    public ProcessDefinition processDefinition(UUID orgId, UUID projectId, RepositoryEntry repositoryEntry) {\n        return repositoryRefresher.processDefinition(orgId, projectId, repositoryEntry);\n    }\n\n    @Value.Immutable\n    interface InsertUpdateResult {\n\n        @Nullable\n        RepositoryEntry prevEntry();\n\n        RepositoryEntry newEntry();\n    }\n\n    private InsertUpdateResult insertOrUpdate(DSLContext tx, UUID projectId, UUID repoId, RepositoryEntry entry, SecretEntryV2 secret, ProcessDefinition processDefinition) {\n        if (!entry.isDisabled() && processDefinition == null) {\n            // should have already thrown and exception by this point, but just in case\n            // something went wrong cloning/loading process definition\n            throw new ConcordApplicationException(\"Error while loading process definition\", Response.Status.INTERNAL_SERVER_ERROR);\n        }\n\n        RepositoryEntry prevEntry = null;\n        if (repoId == null) {\n            repoId = repositoryDao.insert(tx, projectId,\n                    entry.getName(), entry.getUrl(),\n                    trim(entry.getBranch()),\n                    trim(entry.getCommitId()),\n                    trim(entry.getPath()),\n                    secret == null ? null : secret.getId(),\n                    entry.isDisabled(),\n                    entry.getMeta(),\n                    entry.isTriggersDisabled());\n        } else {\n            prevEntry = repositoryDao.get(tx, projectId, repoId);\n\n            repositoryDao.update(tx, repoId,\n                    entry.getName(),\n                    entry.getUrl(),\n                    trim(entry.getBranch()),\n                    trim(entry.getCommitId()),\n                    trim(entry.getPath()),\n                    secret == null ? null : secret.getId(),\n                    entry.isDisabled(),\n                    entry.isTriggersDisabled());\n        }\n\n        if (entry.isDisabled()) {\n            triggerManager.clearTriggers(tx, projectId, repoId);\n        } else {\n            repositoryRefresher.refresh(tx, projectId, entry.getName(), processDefinition);\n        }\n\n        return ImmutableInsertUpdateResult.builder()\n                .prevEntry(prevEntry)\n                .newEntry(repositoryDao.get(tx, projectId, repoId))\n                .build();\n    }\n\n    private SecretEntryV2 assertSecret(UUID orgId, RepositoryEntry entry) {\n        if (entry.getSecretId() == null && entry.getSecretName() == null) {\n            return null;\n        }\n        return secretManager.assertAccess(orgId, entry.getSecretId(), entry.getSecretName(), ResourceAccessLevel.READER, false);\n    }\n\n    private static String trim(String s) {\n        if (s == null) {\n            return null;\n        }\n\n        s = s.trim();\n\n        if (s.isEmpty()) {\n            return null;\n        }\n\n        return s;\n    }\n\n    private void addAuditLog(ProjectEntry project, RepositoryEntry prevRepoEntry, RepositoryEntry newRepoEntry) {\n        ProjectEntry prevEntry = new ProjectEntry(null, new HashMap<>());\n        if (prevRepoEntry != null) {\n            prevEntry.getRepositories().put(prevRepoEntry.getName(), prevRepoEntry);\n        }\n\n        ProjectEntry newEntry = new ProjectEntry(null, new HashMap<>());\n        if (newRepoEntry != null) {\n            newEntry.getRepositories().put(newRepoEntry.getName(), newRepoEntry);\n        }\n\n        Map<String, Object> changes = DiffUtils.compare(prevEntry, newEntry);\n\n        auditLog.add(AuditObject.PROJECT, AuditAction.UPDATE)\n                .field(\"orgId\", project.getOrgId())\n                .field(\"orgName\", project.getOrgName())\n                .field(\"projectId\", project.getId())\n                .field(\"name\", project.getName())\n                .field(\"changes\", changes)\n                .log();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/ProjectResource.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.base.Splitter;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.server.GenericOperationResult;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.org.*;\nimport com.walmartlabs.concord.server.org.team.TeamDao;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.validation.Valid;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response.Status;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\n@Path(\"/api/v1/org\")\n@Tag(name = \"Projects\")\npublic class ProjectResource implements Resource {\n\n    private final OrganizationManager orgManager;\n    private final ProjectDao projectDao;\n    private final ProjectManager projectManager;\n    private final ProjectAccessManager accessManager;\n    private final OrganizationDao orgDao;\n    private final TeamDao teamDao;\n    private final KvDao kvDao;\n    private final EncryptedProjectValueManager encryptedValueManager;\n\n    private final ProjectRepositoryManager projectRepositoryManager;\n\n    @Inject\n    public ProjectResource(OrganizationManager orgManager,\n                           ProjectDao projectDao,\n                           ProjectManager projectManager,\n                           ProjectAccessManager accessManager,\n                           OrganizationDao orgDao,\n                           TeamDao teamDao,\n                           KvDao kvDao,\n                           EncryptedProjectValueManager encryptedValueManager,\n                           ProjectRepositoryManager projectRepositoryManager) {\n\n        this.orgManager = orgManager;\n        this.projectDao = projectDao;\n        this.projectManager = projectManager;\n        this.accessManager = accessManager;\n        this.kvDao = kvDao;\n        this.encryptedValueManager = encryptedValueManager;\n        this.orgDao = orgDao;\n        this.teamDao = teamDao;\n        this.projectRepositoryManager = projectRepositoryManager;\n    }\n\n    @POST\n    @Path(\"/{orgName}/project\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Creates a new project or updates an existing one\", operationId = \"createOrUpdateProject\")\n    public ProjectOperationResponse createOrUpdate(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                   @Valid ProjectEntry entry) {\n\n        ProjectOperationResult result = projectManager.createOrUpdate(orgName, entry);\n        return new ProjectOperationResponse(result.projectId(), result.result());\n    }\n\n    /**\n     * @deprecated use {@link ProjectResourceV2#get(String, String)}\n     */\n    @Deprecated\n    @GET\n    @Path(\"/{orgName}/project/{projectName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    public ProjectEntry get(@PathParam(\"orgName\") @ConcordKey String orgName,\n                            @PathParam(\"projectName\") @ConcordKey String projectName) {\n\n        ProjectEntry p = projectManager.get(orgName, projectName);\n        List<RepositoryEntry> repositories = projectRepositoryManager.list(p.getId());\n        return ProjectEntry.replace(p, repositories.stream().collect(Collectors.toMap(RepositoryEntry::getName, r -> r)));\n    }\n\n    @GET\n    @Path(\"/{orgName}/project\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"List existing projects\", operationId = \"findProjects\")\n    public List<ProjectEntry> find(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                   @QueryParam(\"offset\") int offset,\n                                   @QueryParam(\"limit\") int limit,\n                                   @QueryParam(\"filter\") String filter) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n        return projectManager.list(org.getId(), offset, limit, filter);\n    }\n\n    @GET\n    @Path(\"/{orgName}/project/{projectName}/kv\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"List KV\", operationId = \"listProjectKv\")\n    public List<KvEntry> findKV(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                @PathParam(\"projectName\") @ConcordKey String projectName,\n                                @QueryParam(\"offset\") int offset,\n                                @QueryParam(\"limit\") int limit,\n                                @QueryParam(\"filter\") String filter) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n\n        ProjectEntry project = accessManager.assertAccess(org.getId(), null, projectName, ResourceAccessLevel.READER, false);\n\n        return kvDao.list(project.getId(), offset, limit, filter);\n    }\n\n    /**\n     * Get the KV capacity.\n     */\n    @GET\n    @Path(\"/{orgName}/project/{projectName}/kv/capacity\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Get a project's KV capacity\", operationId = \"getProjectKVCapacity\")\n    public ProjectKvCapacity getCapacity(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                         @PathParam(\"projectName\") @ConcordKey String projectName) {\n\n        return projectManager.getKvCapacity(orgName, projectName);\n    }\n\n    @GET\n    @Path(\"/{orgName}/project/{projectName}/cfg{path: (.*)?}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Get a project's configuration\", operationId = \"getProjectConfiguration\")\n    public Object getConfiguration(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                   @PathParam(\"projectName\") @ConcordKey String projectName,\n                                   @PathParam(\"path\") String path) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n\n        UUID projectId = projectDao.getId(org.getId(), projectName);\n        if (projectId == null) {\n            throw new ConcordApplicationException(\"Project not found: \" + projectName, Status.NOT_FOUND);\n        }\n\n        // TODO move to ProjectManager\n        accessManager.assertAccess(projectId, ResourceAccessLevel.READER, false);\n\n        String[] ps = cfgPath(path);\n        Object v = projectDao.getConfigurationValue(projectId, ps);\n\n        if (v == null) {\n            if (path == null || path.isEmpty()) {\n                return Collections.emptyMap();\n            }\n\n            throw new ConcordApplicationException(\"Value not found: \" + path, Status.NOT_FOUND);\n        }\n\n        return v;\n    }\n\n    @PUT\n    @Path(\"/{orgName}/project/{projectName}/cfg{path: (.*)?}\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Update a project's configuration parameter\", operationId = \"updateProjectConfiguration\")\n    @SuppressWarnings(\"unchecked\")\n    public GenericOperationResult updateConfiguration(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                      @PathParam(\"projectName\") @ConcordKey String projectName,\n                                                      @PathParam(\"path\") String path,\n                                                      Object data) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n\n        UUID projectId = projectDao.getId(org.getId(), projectName);\n        if (projectId == null) {\n            throw new ConcordApplicationException(\"Project not found: \" + projectName, Status.NOT_FOUND);\n        }\n\n        // TODO move to ProjectManager\n        accessManager.assertAccess(projectId, ResourceAccessLevel.WRITER, true);\n\n        Map<String, Object> cfg = projectDao.getConfiguration(projectId);\n        if (cfg == null) {\n            cfg = new HashMap<>();\n        }\n\n        String[] ps = cfgPath(path);\n        if (ps.length < 1) {\n            if (!(data instanceof Map)) {\n                throw new ValidationErrorsException(\"Expected a JSON object: \" + data);\n            }\n            cfg = (Map<String, Object>) data;\n        } else {\n            ConfigurationUtils.set(cfg, data, ps);\n        }\n\n        Map<String, Object> newCfg = cfg;\n        projectDao.updateCfg(projectId, newCfg);\n        return new GenericOperationResult(OperationResult.UPDATED);\n    }\n\n    @PUT\n    @Path(\"/{orgName}/project/{projectName}/cfg/\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Update a project's configuration parameter\", operationId = \"updateProjectConfiguration\")\n    public GenericOperationResult updateConfiguration(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                      @PathParam(\"projectName\") @ConcordKey String projectName,\n                                                      Object data) {\n\n        return updateConfiguration(orgName, projectName, \"/\", data);\n    }\n\n    @DELETE\n    @Path(\"/{orgName}/project/{projectName}/cfg{path: (.*)?}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Delete a project's configuration parameter\", operationId = \"deleteProjectConfiguration\")\n    public GenericOperationResult deleteConfiguration(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                      @PathParam(\"projectName\") @ConcordKey String projectName,\n                                                      @PathParam(\"path\") String path) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n\n        UUID projectId = projectDao.getId(org.getId(), projectName);\n        if (projectId == null) {\n            throw new ConcordApplicationException(\"Project not found: \" + projectName, Status.NOT_FOUND);\n        }\n\n        // TODO move to ProjectManager\n        accessManager.assertAccess(projectId, ResourceAccessLevel.WRITER, true);\n\n        Map<String, Object> cfg = projectDao.getConfiguration(projectId);\n        if (cfg == null) {\n            cfg = new HashMap<>();\n        }\n\n        String[] ps = cfgPath(path);\n        if (ps.length == 0) {\n            cfg = null;\n        } else {\n            ConfigurationUtils.delete(cfg, ps);\n        }\n\n        Map<String, Object> newCfg = cfg;\n        projectDao.updateCfg(projectId, newCfg);\n        return new GenericOperationResult(OperationResult.DELETED);\n    }\n\n    @DELETE\n    @Path(\"/{orgName}/project/{projectName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Delete an existing project\", operationId = \"deleteProject\")\n    public GenericOperationResult delete(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                         @PathParam(\"projectName\") @ConcordKey String projectName) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n\n        UUID projectId = projectDao.getId(org.getId(), projectName);\n        if (projectId == null) {\n            throw new ConcordApplicationException(\"Project not found: \" + projectName, Status.NOT_FOUND);\n        }\n\n        projectManager.delete(projectId);\n        return new GenericOperationResult(OperationResult.DELETED);\n    }\n\n    @GET\n    @Path(\"/{orgName}/project/{projectName}/access\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Get project team access\", operationId = \"getProjectAccessLevel\")\n    public List<ResourceAccessEntry> getAccessLevel(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                    @PathParam(\"projectName\") @ConcordKey String projectName) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n\n        UUID projectId = projectDao.getId(org.getId(), projectName);\n        if (projectId == null) {\n            throw new ConcordApplicationException(\"Project not found: \" + projectName, Status.NOT_FOUND);\n        }\n        return accessManager.getResourceAccess(projectId);\n    }\n\n    @POST\n    @Path(\"/{orgName}/project/{projectName}/access\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Updates the access level for the specified project and team\", operationId = \"updateProjectAccessLevel\")\n    public GenericOperationResult updateAccessLevel(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                    @PathParam(\"projectName\") @ConcordKey String projectName,\n                                                    @Valid ResourceAccessEntry entry) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n\n        UUID projectId = projectDao.getId(org.getId(), projectName);\n        if (projectId == null) {\n            throw new ConcordApplicationException(\"Project not found: \" + projectName, Status.NOT_FOUND);\n        }\n\n        UUID teamId = ResourceAccessUtils.getTeamId(orgDao, teamDao, org.getId(), entry);\n\n        accessManager.updateAccessLevel(projectId, teamId, entry.getLevel());\n        return new GenericOperationResult(OperationResult.UPDATED);\n    }\n\n    @POST\n    @Path(\"/{orgName}/project/{projectName}/access/bulk\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Updates the access level for the specified project and team\", operationId = \"updateProjectAccessLevelBulk\")\n    public GenericOperationResult updateAccessLevel(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                    @PathParam(\"projectName\") @ConcordKey String projectName,\n                                                    @Valid Collection<ResourceAccessEntry> entries) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n\n        UUID projectId = projectDao.getId(org.getId(), projectName);\n        if (projectId == null) {\n            throw new ConcordApplicationException(\"Project not found: \" + projectName, Status.NOT_FOUND);\n        }\n\n        if (entries == null) {\n            throw new ConcordApplicationException(\"List of teams is null.\", Status.BAD_REQUEST);\n        }\n\n        accessManager.updateAccessLevel(projectId, entries, true);\n\n        return new GenericOperationResult(OperationResult.UPDATED);\n    }\n\n    @POST\n    @Path(\"/{orgName}/project/{projectName}/encrypt\")\n    @Consumes(MediaType.TEXT_PLAIN)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Encrypts a string with the project's key\")\n    public EncryptValueResponse encrypt(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                        @PathParam(\"projectName\") @ConcordKey String projectName,\n                                        String value) {\n\n        if (value == null) {\n            throw new ValidationErrorsException(\"Value is required\");\n        }\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n\n        UUID projectId = projectDao.getId(org.getId(), projectName);\n        if (projectId == null) {\n            throw new ConcordApplicationException(\"Project not found: \" + projectName, Status.NOT_FOUND);\n        }\n\n        accessManager.assertAccess(projectId, ResourceAccessLevel.READER, true);\n\n        byte[] input = value.getBytes();\n        byte[] result = encryptedValueManager.encrypt(projectId, input);\n\n        return new EncryptValueResponse(result);\n    }\n\n    private static String[] cfgPath(String s) {\n        if (s == null) {\n            return new String[0];\n        }\n\n        List<String> l = Splitter.on(\"/\").omitEmptyStrings().splitToList(s);\n        return l.toArray(new String[0]);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/ProjectResourceV2.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.GET;\nimport javax.ws.rs.Path;\nimport javax.ws.rs.PathParam;\nimport javax.ws.rs.Produces;\nimport javax.ws.rs.core.MediaType;\n\n@Path(\"/api/v2/org\")\n@Tag(name = \"Projects\")\npublic class ProjectResourceV2 implements Resource {\n\n    private final ProjectManager projectManager;\n\n    @Inject\n    public ProjectResourceV2(ProjectManager projectManager) {\n\n        this.projectManager = projectManager;\n    }\n\n    @GET\n    @Path(\"/{orgName}/project/{projectName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Get an existing project\", operationId = \"getProject\")\n    public ProjectEntry get(@PathParam(\"orgName\") @ConcordKey String orgName,\n                            @PathParam(\"projectName\") @ConcordKey String projectName) {\n\n        return projectManager.get(orgName, projectName);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/ProjectValidator.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.cronutils.model.CronType;\nimport com.cronutils.model.definition.CronDefinitionBuilder;\nimport com.cronutils.parser.CronParser;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.google.common.base.Strings;\nimport com.walmartlabs.concord.runtime.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.model.SourceMap;\nimport com.walmartlabs.concord.runtime.model.Trigger;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\n\nimport java.io.Serializable;\nimport java.util.*;\nimport java.util.regex.Pattern;\n\npublic class ProjectValidator {\n\n    private static final CronParser parser =\n            new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.UNIX));\n\n    public static Result validate(ProcessDefinition pd) {\n        List<String> errors = new ArrayList<>();\n        List<String> warnings = new ArrayList<>();\n\n        for (Trigger t : pd.triggers()) {\n            validateTrigger(pd, t, errors, warnings);\n        }\n\n        return new Result(errors, warnings);\n    }\n\n    private static void validateTrigger(ProcessDefinition pd, Trigger t, List<String> errors, List<String> warnings) {\n        validateEntryPoint(pd, t, errors, warnings);\n\n        validateSpec(t, errors);\n\n        validateTimezone(t, errors);\n\n        if (Objects.isNull(t.conditions())) {\n            return;\n        }\n\n        t.conditions().entrySet().stream()\n                .filter(v -> v.getValue() instanceof String)\n                .filter(v -> !Constants.Trigger.CRON_SPEC.equals(v.getKey()))\n                .forEach(v -> validateRegex(t, errors, v));\n    }\n\n    private static void validateEntryPoint(ProcessDefinition pd, Trigger t, List<String> errors, List<String> warnings) {\n        String entryPoint = getEntryPoint(t);\n\n        if (Strings.isNullOrEmpty(entryPoint)) {\n            errors.add(makeErrorMessage(t, Constants.Request.ENTRY_POINT_KEY, \"is missing\"));\n            return;\n        }\n\n        Map<String, ?> flows = pd.flows();\n        if (Objects.isNull(flows) || !flows.containsKey(entryPoint)) {\n            warnings.add(makeErrorMessage(t, Constants.Request.ENTRY_POINT_KEY, \"does not point to a valid flow. \" +\n                    \"This warning can be ignored if the entryPoint references a flow from a template.\"));\n        }\n    }\n\n    private static String getEntryPoint(Trigger t) {\n        Map<String, Object> cfg = t.configuration();\n\n        if (cfg == null) {\n            return null;\n        }\n\n        return (String) cfg.get(Constants.Request.ENTRY_POINT_KEY);\n    }\n\n    private static void validateSpec(Trigger t, List<String> errors) {\n        String triggerName = t.name();\n        if (!triggerName.equals(\"cron\")) {\n            return;\n        }\n\n        String k = Constants.Trigger.CRON_SPEC;\n\n        if (Objects.isNull(t.conditions())) {\n            errors.add(makeErrorMessage(t, k, \"is missing\"));\n            return;\n        }\n\n        Object spec = t.conditions().get(k);\n        if (Objects.isNull(spec)) {\n            errors.add(makeErrorMessage(t, k, \"is missing\"));\n            return;\n        }\n\n        try {\n            parser.parse((String) spec);\n        } catch (ValidationErrorsException e) {\n            errors.add(makeErrorMessage(t, k, \"is not valid: \" + e.getMessage()));\n        }\n    }\n\n    private static void validateTimezone(Trigger t, List<String> errors) {\n        String triggerName = t.name();\n        if (!triggerName.equals(\"cron\")) {\n            return;\n        }\n\n        if (Objects.isNull(t.conditions())) {\n            return;\n        }\n\n        Object timezone = t.conditions().get(\"timezone\");\n        if (Objects.isNull(timezone)) {\n            return;\n        }\n\n        if (!(timezone instanceof String)) {\n            errors.add(makeErrorMessage(t, \"timezone\", \"invalid type: string expected\"));\n            return;\n        }\n\n        if (!Arrays.asList(TimeZone.getAvailableIDs()).contains(timezone)) {\n            errors.add(makeErrorMessage(t, \"timezone\", \"with value '\" + timezone + \"' not found\"));\n        }\n    }\n\n    private static void validateRegex(Trigger t, List<String> errors, Map.Entry<String, Object> entry) {\n        try {\n            Pattern.compile(entry.getValue().toString());\n        } catch (Exception e) {\n            errors.add(makeErrorMessage(t, entry.getKey(), \"is not a valid regular expression: \" + e.getMessage()));\n        }\n    }\n\n    private static String location(SourceMap m) {\n        return \"@ src: \" + m.source() + \", line: \" + m.line() + \", col: \" + m.column();\n    }\n\n    private static String makeErrorMessage(Trigger t, String property, String message) {\n        return \"trigger: \" + t.name() + \" -> \" + property + \" \" + (message) + \" \" + location(t.sourceMap());\n    }\n\n    @JsonInclude(JsonInclude.Include.NON_EMPTY)\n    public static class Result implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        private final List<String> errors;\n        private final List<String> warnings;\n\n        public Result(List<String> errors, List<String> warnings) {\n            this.errors = errors;\n            this.warnings = warnings;\n        }\n\n        public List<String> getErrors() {\n            return errors;\n        }\n\n        public List<String> getWarnings() {\n            return warnings;\n        }\n\n        public boolean isValid() {\n            return errors == null || errors.isEmpty();\n        }\n    }\n}\n\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/ProjectVisibility.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic enum ProjectVisibility {\n\n    PUBLIC,\n    PRIVATE\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/RepositoryDao.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.UuidGenerator;\nimport org.jooq.*;\nimport org.jooq.exception.DataAccessException;\n\nimport javax.inject.Inject;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.tables.Repositories.REPOSITORIES;\nimport static com.walmartlabs.concord.server.jooq.tables.Secrets.SECRETS;\nimport static java.util.Objects.requireNonNull;\nimport static org.jooq.impl.DSL.*;\n\npublic class RepositoryDao extends AbstractDao {\n\n    private final ConcordObjectMapper objectMapper;\n    private final UuidGenerator uuidGenerator;\n\n    @Inject\n    public RepositoryDao(@MainDB Configuration cfg,\n                         ConcordObjectMapper objectMapper,\n                         UuidGenerator uuidGenerator) {\n        super(cfg);\n        this.objectMapper = requireNonNull(objectMapper);\n        this.uuidGenerator = requireNonNull(uuidGenerator);\n    }\n\n    @Override\n    protected void tx(Tx t) {\n        super.tx(t);\n    }\n\n    @Override\n    protected <T> T txResult(TxResult<T> t) {\n        return super.txResult(t);\n    }\n\n    public UUID getId(UUID projectId, String repoName) {\n        return dsl().select(REPOSITORIES.REPO_ID)\n                .from(REPOSITORIES)\n                .where(REPOSITORIES.PROJECT_ID.eq(projectId)\n                        .and(REPOSITORIES.REPO_NAME.eq(repoName)))\n                .fetchOne(REPOSITORIES.REPO_ID);\n    }\n\n    public UUID getProjectId(UUID repoId) {\n        return dsl().select(REPOSITORIES.PROJECT_ID)\n                .from(REPOSITORIES)\n                .where(REPOSITORIES.REPO_ID.eq(repoId))\n                .fetchOne(REPOSITORIES.PROJECT_ID);\n    }\n\n    public RepositoryEntry get(UUID projectId, UUID repoId) {\n        return txResult(tx -> get(tx, projectId, repoId));\n    }\n\n    public RepositoryEntry get(DSLContext tx, UUID projectId, UUID repoId) {\n        return selectRepositoryEntry(tx)\n                .where(REPOSITORIES.PROJECT_ID.eq(projectId)\n                        .and(REPOSITORIES.REPO_ID.eq(repoId)))\n                .fetchOne(this::toEntry);\n    }\n\n    public RepositoryEntry get(UUID projectId, String repoName) {\n        return txResult(tx -> get(tx, projectId, repoName));\n    }\n\n    public RepositoryEntry get(DSLContext tx, UUID projectId, String repoName) {\n        return selectRepositoryEntry(tx)\n                .where(REPOSITORIES.PROJECT_ID.eq(projectId)\n                        .and(REPOSITORIES.REPO_NAME.eq(repoName)))\n                .fetchOne(this::toEntry);\n    }\n\n    public RepositoryEntry get(UUID repoId) {\n        return txResult(tx -> selectRepositoryEntry(tx)\n                .where(REPOSITORIES.REPO_ID.eq(repoId))\n                .fetchOne(this::toEntry));\n    }\n\n    public RepositoryEntry get(DSLContext tx, UUID repoId) {\n        return selectRepositoryEntry(tx)\n                .where(REPOSITORIES.REPO_ID.eq(repoId))\n                .fetchOne(this::toEntry);\n    }\n\n    public UUID insert(UUID projectId, String repositoryName, String url, String branch, String commitId, String path, UUID secretId, boolean disabled, Map<String, Object> meta, boolean isTriggersDisabled) {\n        return txResult(tx -> insert(tx, projectId, repositoryName, url, branch, commitId, path, secretId, disabled, meta, isTriggersDisabled));\n    }\n\n    public UUID insert(DSLContext tx, UUID projectId, String repositoryName, String url, String branch, String commitId, String path, UUID secretId, boolean disabled, Map<String, Object> meta, boolean isTriggersDisabled) {\n        UUID repoId = uuidGenerator.generate();\n        return tx.insertInto(REPOSITORIES)\n                .columns(REPOSITORIES.REPO_ID, REPOSITORIES.PROJECT_ID, REPOSITORIES.REPO_NAME,\n                        REPOSITORIES.REPO_URL, REPOSITORIES.REPO_BRANCH, REPOSITORIES.REPO_COMMIT_ID,\n                        REPOSITORIES.REPO_PATH, REPOSITORIES.SECRET_ID, REPOSITORIES.META, REPOSITORIES.IS_DISABLED, REPOSITORIES.IS_TRIGGERS_DISABLED)\n                .values(repoId, projectId, repositoryName, url, branch, commitId, path, secretId, objectMapper.toJSONB(meta), disabled, isTriggersDisabled)\n                .returning(REPOSITORIES.REPO_ID)\n                .fetchOne()\n                .getRepoId();\n    }\n\n    public void update(DSLContext tx, UUID repoId, String repositoryName, String url, String branch, String commitId, String path, UUID secretId, boolean disabled, boolean isTriggersDisabled) {\n        int i = tx.update(REPOSITORIES)\n                .set(REPOSITORIES.REPO_NAME, repositoryName)\n                .set(REPOSITORIES.REPO_URL, url)\n                .set(REPOSITORIES.SECRET_ID, secretId)\n                .set(REPOSITORIES.REPO_BRANCH, branch)\n                .set(REPOSITORIES.REPO_COMMIT_ID, commitId)\n                .set(REPOSITORIES.REPO_PATH, path)\n                .set(REPOSITORIES.IS_DISABLED, disabled)\n                .set(REPOSITORIES.IS_TRIGGERS_DISABLED, isTriggersDisabled)\n                .where(REPOSITORIES.REPO_ID.eq(repoId))\n                .execute();\n\n        if (i != 1) {\n            throw new DataAccessException(\"Invalid number of rows: \" + i);\n        }\n    }\n\n    public void disable(DSLContext tx, UUID repoId) {\n        tx.update(REPOSITORIES)\n            .set(REPOSITORIES.IS_DISABLED, true)\n            .where(REPOSITORIES.REPO_ID.eq(repoId))\n            .execute();\n    }\n\n    public void clearSecretMappingBySecretId(DSLContext tx, UUID secretId) {\n        tx.update(REPOSITORIES)\n                .setNull(REPOSITORIES.SECRET_ID)\n                .where(REPOSITORIES.SECRET_ID.eq(secretId))\n                .execute();\n    }\n\n    public void clearSecretMappingByProjectId(DSLContext tx, UUID projectId) {\n        tx.update(REPOSITORIES)\n                .setNull(REPOSITORIES.SECRET_ID)\n                .where(REPOSITORIES.PROJECT_ID.eq(projectId))\n                .execute();\n    }\n\n    public void delete(UUID repoId) {\n        tx(tx -> delete(tx, repoId));\n    }\n\n    public void delete(DSLContext tx, UUID repoId) {\n        tx.deleteFrom(REPOSITORIES)\n                .where(REPOSITORIES.REPO_ID.eq(repoId))\n                .execute();\n    }\n\n    public void deleteAll(DSLContext tx, UUID projectId) {\n        tx.deleteFrom(REPOSITORIES)\n                .where(REPOSITORIES.PROJECT_ID.eq(projectId))\n                .execute();\n    }\n\n    public List<RepositoryEntry> list() {\n        return selectRepositoryEntry(dsl())\n                .fetch(this::toEntry);\n    }\n\n    public List<RepositoryEntry> list(UUID projectId) {\n        return txResult(tx -> list(tx, projectId));\n    }\n\n    public List<RepositoryEntry> list(DSLContext tx, UUID projectId) {\n        SelectConditionStep<Record13<UUID, UUID, String, String, String, String, String, Boolean, JSONB, UUID, String, String, Boolean>> query = selectRepositoryEntry(tx)\n                .where(REPOSITORIES.PROJECT_ID.eq(projectId));\n        return query.fetch(this::toEntry);\n    }\n\n    public List<RepositoryEntry> list(UUID projectId, Field<?> sortField,\n                                      boolean asc, int offset, int limit,\n                                      String filter) {\n\n        SelectConditionStep<Record13<UUID, UUID, String, String, String, String, String, Boolean, JSONB, UUID, String, String, Boolean>> q = selectRepositoryEntry(dsl())\n                .where(REPOSITORIES.PROJECT_ID.eq(projectId));\n\n        if (sortField != null) {\n            q.orderBy(asc ? sortField.asc() : sortField.desc());\n        }\n\n        if (filter != null) {\n            q.and(REPOSITORIES.REPO_NAME.containsIgnoreCase(filter));\n        }\n\n        if (offset > 0) {\n            q.offset(offset);\n        }\n\n        if (limit > 0) {\n            q.limit(limit);\n        }\n\n        return q.fetch(this::toEntry);\n    }\n\n    public List<RepositoryEntry> find(String repoUrl) {\n        return find(null, repoUrl);\n    }\n\n    public List<RepositoryEntry> find(UUID projectId, String repoUrl) {\n        SelectConditionStep<Record13<UUID, UUID, String, String, String, String, String, Boolean, JSONB, UUID, String, String, Boolean>> select = selectRepositoryEntry(dsl())\n                .where(REPOSITORIES.REPO_URL.contains(repoUrl));\n\n        if (projectId != null) {\n            select.and(REPOSITORIES.PROJECT_ID.eq(projectId));\n        }\n\n        return select.fetch(this::toEntry);\n    }\n\n    public List<RepositoryEntry> findSimilar(String repoUrlPattern) {\n        return findSimilar(null, repoUrlPattern);\n    }\n\n    public List<RepositoryEntry> findSimilar(UUID projectId, String pattern) {\n        SelectConditionStep<Record13<UUID, UUID, String, String, String, String, String, Boolean, JSONB, UUID, String, String, Boolean>> select = selectRepositoryEntry(dsl())\n                .where(REPOSITORIES.REPO_URL.similarTo(pattern));\n\n        if (projectId != null) {\n            select.and(REPOSITORIES.PROJECT_ID.eq(projectId));\n        }\n\n        return select.fetch(this::toEntry);\n    }\n\n    public void updateMeta(DSLContext tx, UUID id, Map<String, Object> meta) {\n        Field<JSONB> updateJson = field(coalesce(REPOSITORIES.META, field(\"?\", JSONB.class, JSONB.valueOf(\"{}\"))) + \" || ?::jsonb\", JSONB.class, objectMapper.toJSONB(meta));\n        Field<JSONB> metaField = function(\"jsonb_strip_nulls\", JSONB.class, updateJson);\n\n        tx.update(REPOSITORIES)\n                .set(REPOSITORIES.META, metaField)\n                .where(REPOSITORIES.REPO_ID.eq(id))\n                .execute();\n    }\n\n    private static SelectJoinStep<Record13<UUID, UUID, String, String, String, String, String, Boolean, JSONB, UUID, String, String, Boolean>> selectRepositoryEntry(DSLContext tx) {\n        return tx.select(REPOSITORIES.REPO_ID,\n                REPOSITORIES.PROJECT_ID,\n                REPOSITORIES.REPO_NAME,\n                REPOSITORIES.REPO_URL,\n                REPOSITORIES.REPO_BRANCH,\n                REPOSITORIES.REPO_COMMIT_ID,\n                REPOSITORIES.REPO_PATH,\n                REPOSITORIES.IS_DISABLED,\n                REPOSITORIES.META,\n                SECRETS.SECRET_ID,\n                SECRETS.SECRET_NAME,\n                SECRETS.STORE_TYPE,\n                REPOSITORIES.IS_TRIGGERS_DISABLED)\n                .from(REPOSITORIES)\n                .leftOuterJoin(SECRETS).on(SECRETS.SECRET_ID.eq(REPOSITORIES.SECRET_ID));\n    }\n\n    private RepositoryEntry toEntry(Record13<UUID, UUID, String, String, String, String, String, Boolean, JSONB, UUID, String, String, Boolean> r) {\n        return new RepositoryEntry(r.get(REPOSITORIES.REPO_ID),\n                r.get(REPOSITORIES.PROJECT_ID),\n                r.get(REPOSITORIES.REPO_NAME),\n                r.get(REPOSITORIES.REPO_URL),\n                r.get(REPOSITORIES.REPO_BRANCH),\n                r.get(REPOSITORIES.REPO_COMMIT_ID),\n                r.get(REPOSITORIES.REPO_PATH),\n                r.get(REPOSITORIES.IS_DISABLED),\n                r.get(SECRETS.SECRET_ID),\n                r.get(SECRETS.SECRET_NAME),\n                r.get(SECRETS.STORE_TYPE),\n                objectMapper.fromJSONB(r.get(REPOSITORIES.META)),\n                r.get(REPOSITORIES.IS_TRIGGERS_DISABLED));\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/RepositoryEntry.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.*;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\n\nimport javax.validation.constraints.NotNull;\nimport javax.validation.constraints.Size;\nimport java.io.Serializable;\nimport java.util.Map;\nimport java.util.UUID;\n\n@JsonInclude(JsonInclude.Include.NON_NULL)\n@JsonIgnoreProperties(ignoreUnknown = true)\npublic class RepositoryEntry implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final UUID id;\n\n    private final UUID projectId;\n\n    @ConcordKey\n    private final String name;\n\n    @NotNull\n    @Size(max = 2048)\n    private final String url;\n\n    @Size(max = 255)\n    private final String branch;\n\n    @Size(max = 64)\n    private final String commitId;\n\n    @Size(max = 2048)\n    private final String path;\n\n    private final UUID secretId;\n\n    @ConcordKey\n    @JsonAlias(\"secret\")\n    private final String secretName;\n\n    private final String secretStoreType;\n\n    private final boolean disabled;\n\n    private final Map<String, Object> meta;\n\n    private final boolean triggersDisabled;\n\n    public RepositoryEntry(String name, String url) {\n        this(null, null, name, url, null, null, null, false, null, null, null, null, false);\n    }\n\n    public RepositoryEntry(String name, RepositoryEntry e) {\n        this(e.id, e.projectId, name, e.url, e.branch, e.commitId, e.path, e.disabled, e.getSecretId(), e.secretName, e.secretStoreType, e.meta, e.triggersDisabled);\n    }\n\n    public RepositoryEntry(RepositoryEntry e, String branch, String commitId) {\n        this(e.id,\n                e.projectId,\n                e.name,\n                e.url,\n                branch,\n                commitId,\n                e.path,\n                e.disabled,\n                e.getSecretId(),\n                e.secretName,\n                e.secretStoreType,\n                e.meta,\n                e.triggersDisabled);\n    }\n\n    @JsonCreator\n    public RepositoryEntry(@JsonProperty(\"id\") UUID id,\n                           @JsonProperty(\"projectId\") UUID projectId,\n                           @JsonProperty(\"name\") String name,\n                           @JsonProperty(\"url\") String url,\n                           @JsonProperty(\"branch\") String branch,\n                           @JsonProperty(\"commitId\") String commitId,\n                           @JsonProperty(\"path\") String path,\n                           @JsonProperty(\"disabled\") boolean disabled,\n                           @JsonProperty(\"secretId\") UUID secretId,\n                           @JsonProperty(\"secretName\") String secretName,\n                           @JsonProperty(\"secretStoreType\") String secretStoreType,\n                           @JsonProperty(\"meta\") Map<String, Object> meta,\n                           @JsonProperty(\"triggersDisabled\") boolean triggersDisabled) {\n\n        this.id = id;\n        this.projectId = projectId;\n        this.name = name;\n        this.url = url;\n        this.branch = branch;\n        this.commitId = commitId;\n        this.path = path;\n        this.secretId = secretId;\n        this.secretName = secretName;\n        this.secretStoreType = secretStoreType;\n        this.disabled = disabled;\n        this.meta = meta;\n        this.triggersDisabled = triggersDisabled;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public UUID getProjectId() {\n        return projectId;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public String getUrl() {\n        return url;\n    }\n\n    public String getBranch() {\n        return branch;\n    }\n\n    public String getCommitId() {\n        return commitId;\n    }\n\n    public String getPath() {\n        return path;\n    }\n\n    public UUID getSecretId() {\n        return secretId;\n    }\n\n    public String getSecretName() {\n        return secretName;\n    }\n\n    public String getSecretStoreType() {\n        return secretStoreType;\n    }\n\n    public boolean isDisabled() {\n        return disabled;\n    }\n\n    public Map<String, Object> getMeta() {\n        return meta;\n    }\n\n    public boolean isTriggersDisabled() {\n        return triggersDisabled;\n    }\n\n    public RepositoryEntry withBranch(String branch) {\n        return new RepositoryEntry(id, projectId, name, url, branch, commitId, path, disabled, secretId, secretName, secretStoreType, meta, triggersDisabled);\n    }\n\n    public RepositoryEntry withPath(String path) {\n        return new RepositoryEntry(id, projectId, name, url, branch, commitId, path, disabled, secretId, secretName, secretStoreType, meta, triggersDisabled);\n    }\n\n    public RepositoryEntry withDisabled(boolean disabled) {\n        return new RepositoryEntry(id, projectId, name, url, branch, commitId, path, disabled, secretId, secretName, secretStoreType, meta, triggersDisabled);\n    }\n\n    @Override\n    public String toString() {\n        return \"RepositoryEntry{\" +\n                \"id=\" + id +\n                \", projectId=\" + projectId +\n                \", name='\" + name + '\\'' +\n                \", url='\" + url + '\\'' +\n                \", branch='\" + branch + '\\'' +\n                \", commitId='\" + commitId + '\\'' +\n                \", path='\" + path + '\\'' +\n                \", secretId=\" + secretId +\n                \", secretName='\" + secretName + '\\'' +\n                \", secretStoreType='\" + secretStoreType + '\\'' +\n                \", disabled=\" + disabled + '\\'' +\n                \", meta=\" + meta + '\\'' +\n                \", triggersDisabled=\" + triggersDisabled +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/RepositoryResource.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.server.GenericOperationResult;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.org.OrganizationEntry;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.repository.RepositoryRefresher;\nimport com.walmartlabs.concord.server.repository.RepositoryValidationResponse;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.validation.Valid;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response.Status;\nimport java.util.List;\nimport java.util.UUID;\n\n@Path(\"/api/v1/org\")\n@Tag(name = \"Repositories\")\npublic class RepositoryResource implements Resource {\n\n    private final OrganizationManager orgManager;\n    private final ProjectAccessManager accessManager;\n    private final ProjectDao projectDao;\n    private final RepositoryDao repositoryDao;\n    private final ProjectRepositoryManager projectRepositoryManager;\n    private final RepositoryRefresher repositoryRefresher;\n\n    @Inject\n    public RepositoryResource(OrganizationManager orgManager,\n                              ProjectAccessManager accessManager,\n                              ProjectDao projectDao,\n                              RepositoryDao repositoryDao,\n                              ProjectRepositoryManager projectRepositoryManager,\n                              RepositoryRefresher repositoryRefresher) {\n\n        this.orgManager = orgManager;\n        this.accessManager = accessManager;\n        this.projectDao = projectDao;\n        this.repositoryDao = repositoryDao;\n        this.projectRepositoryManager = projectRepositoryManager;\n        this.repositoryRefresher = repositoryRefresher;\n    }\n\n    @GET\n    @Path(\"/{orgName}/project/{projectName}/repository/{repositoryName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Get an existing repository\", operationId = \"getRepository\")\n    public RepositoryEntry get(@PathParam(\"orgName\") @ConcordKey String orgName,\n                               @PathParam(\"projectName\") @ConcordKey String projectName,\n                               @PathParam(\"repositoryName\") @ConcordKey String repositoryName) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n        return projectRepositoryManager.get(org.getId(), projectName, repositoryName);\n    }\n\n    @GET\n    @Path(\"/{orgName}/project/{projectName}/repository\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"List existing repositories\", operationId = \"listRepositories\")\n    public List<RepositoryEntry> find(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                      @PathParam(\"projectName\") @ConcordKey String projectName,\n                                      @QueryParam(\"offset\") int offset,\n                                      @QueryParam(\"limit\") int limit,\n                                      @QueryParam(\"filter\") String filter) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n        return projectRepositoryManager.list(org.getId(), projectName, offset, limit, filter);\n    }\n\n    @POST\n    @Path(\"/{orgName}/project/{projectName}/repository\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Creates a new repository or updates an existing one\", operationId = \"createOrUpdateRepository\")\n    public GenericOperationResult createOrUpdate(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                 @PathParam(\"projectName\") @ConcordKey String projectName,\n                                                 @Valid RepositoryEntry entry) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n\n        UUID projectId = projectDao.getId(org.getId(), projectName);\n        if (projectId == null) {\n            throw new ConcordApplicationException(\"Project not found: \" + projectName, Status.NOT_FOUND);\n        }\n\n        projectRepositoryManager.createOrUpdate(projectId, entry);\n        return new GenericOperationResult(entry.getId() == null ? OperationResult.CREATED : OperationResult.UPDATED);\n    }\n\n    @DELETE\n    @Path(\"/{orgName}/project/{projectName}/repository/{repositoryName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Delete an existing repository\", operationId = \"deleteRepository\")\n    public GenericOperationResult delete(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                         @PathParam(\"projectName\") @ConcordKey String projectName,\n                                         @PathParam(\"repositoryName\") @ConcordKey String repositoryName) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n\n        UUID projectId = projectDao.getId(org.getId(), projectName);\n        if (projectId == null) {\n            throw new ConcordApplicationException(\"Project not found: \" + projectName, Status.NOT_FOUND);\n        }\n\n        projectRepositoryManager.delete(projectId, repositoryName);\n\n        return new GenericOperationResult(OperationResult.DELETED);\n    }\n\n    /**\n     * Refresh a local copy of the repository.\n     */\n    @POST\n    @Path(\"/{orgName}/project/{projectName}/repository/{repositoryName}/refresh\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Refresh a local copy of the repository\")\n    public GenericOperationResult refreshRepository(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                    @PathParam(\"projectName\") @ConcordKey String projectName,\n                                                    @PathParam(\"repositoryName\") @ConcordKey String repositoryName,\n                                                    @QueryParam(\"sync\") @DefaultValue(\"false\") boolean sync) {\n\n        repositoryRefresher.refresh(orgName, projectName, repositoryName);\n        return new GenericOperationResult(OperationResult.UPDATED);\n    }\n\n    /**\n     * Validate a repository.\n     */\n    @POST\n    @Path(\"/{orgName}/project/{projectName}/repository/{repositoryName}/validate\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Validate an existing repository\")\n    public RepositoryValidationResponse validateRepository(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                           @PathParam(\"projectName\") @ConcordKey String projectName,\n                                                           @PathParam(\"repositoryName\") @ConcordKey String repositoryName) {\n\n        UUID orgId = orgManager.assertAccess(orgName, true).getId();\n        UUID projectId = projectDao.getId(orgId, projectName);\n        if (projectId == null) {\n            throw new ConcordApplicationException(\"Project not found: \" + projectName, Status.NOT_FOUND);\n        }\n\n        accessManager.assertAccess(projectId, ResourceAccessLevel.READER, true);\n\n        RepositoryEntry repo = repositoryDao.get(projectId, repositoryName);\n        if (repo == null) {\n            throw new ConcordApplicationException(\"Repository not found: \" + repositoryName, Status.NOT_FOUND);\n        }\n\n        ProjectValidator.Result result = projectRepositoryManager.validateRepository(orgId, repo);\n\n        return new RepositoryValidationResponse(result.isValid(), OperationResult.VALIDATED, result.getErrors(), result.getWarnings());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/RepositoryResourceV2.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.GenericOperationResult;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.repository.RepositoryRefresher;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.POST;\nimport javax.ws.rs.Path;\nimport javax.ws.rs.Produces;\nimport javax.ws.rs.QueryParam;\nimport javax.ws.rs.core.MediaType;\nimport java.util.List;\nimport java.util.UUID;\n\n@Path(\"/api/v2/repository\")\n@Tag(name = \"RepositoriesV2\")\npublic class RepositoryResourceV2 implements Resource {\n\n    private final RepositoryRefresher repositoryRefresher;\n\n    @Inject\n    public RepositoryResourceV2(RepositoryRefresher repositoryRefresher) {\n        this.repositoryRefresher = repositoryRefresher;\n    }\n\n    /**\n     * Refresh repositories by their IDs.\n     */\n    @POST\n    @Path(\"/refresh\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Refresh repositories by their IDs\", operationId = \"refreshRepositoryV2\")\n    public GenericOperationResult refreshRepository(@QueryParam(\"ids\") List<UUID> repositoryIds) {\n        repositoryRefresher.refresh(repositoryIds);\n        return new GenericOperationResult(OperationResult.UPDATED);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/RepositoryValidationException.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport javax.ws.rs.core.Response;\nimport javax.ws.rs.core.Response.StatusType;\n\n\npublic class RepositoryValidationException extends RuntimeException {\n\n    private static final long serialVersionUID = 1L;\n\n    private final StatusType status;\n\n    public RepositoryValidationException(String message, Throwable cause) {\n        this(message, cause, Response.Status.INTERNAL_SERVER_ERROR);\n    }\n\n    public RepositoryValidationException(String message, Throwable cause, StatusType status) {\n        super(message, cause);\n        this.status = status;\n    }\n\n    public StatusType getStatus() {\n        return status;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/project/RepositoryValidationExceptionMapper.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.boot.resteasy.ExceptionMapperSupport;\nimport com.walmartlabs.concord.server.process.ErrorMessage;\n\nimport javax.ws.rs.core.Context;\nimport javax.ws.rs.core.HttpHeaders;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\n\npublic class RepositoryValidationExceptionMapper extends ExceptionMapperSupport<RepositoryValidationException> {\n\n    private static final int MAX_CAUSE_DEPTH = 5;\n\n    public static final String TRACE_ENABLED_KEY = \"X-Concord-Trace-Enabled\";\n\n    @Context\n    HttpHeaders headers;\n\n    @Override\n    protected Response convert(RepositoryValidationException e) {\n        String details = getDetails(e.getCause());\n\n        String stacktrace = null;\n        if (traceEnabled()) {\n            StringWriter w = new StringWriter();\n            e.printStackTrace(new PrintWriter(w));\n            stacktrace = w.toString();\n        }\n\n        ErrorMessage msg = new ErrorMessage(null, e.getMessage(), details, stacktrace);\n        return Response.status(e.getStatus())\n                .entity(msg)\n                .type(MediaType.APPLICATION_JSON_TYPE)\n                .build();\n    }\n\n    private boolean traceEnabled() {\n        String s = headers.getHeaderString(TRACE_ENABLED_KEY);\n        return Boolean.parseBoolean(s);\n    }\n\n    private static String getDetails(Throwable t) {\n        if (t == null) {\n            return null;\n        }\n\n        int currentDepth = 0;\n        StringBuilder result = new StringBuilder();\n        while (t != null && currentDepth < MAX_CAUSE_DEPTH) {\n            result.append(t.getMessage()).append(\"\\n\");\n            t = t.getCause();\n            currentDepth++;\n        }\n        return result.toString();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/GetDataRequest.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.MultipartUtils;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport org.jboss.resteasy.plugins.providers.multipart.MultipartInput;\n\npublic class GetDataRequest {\n\n    public static GetDataRequest from(MultipartInput input) {\n        return new GetDataRequest(input);\n    }\n\n    private final MultipartInput input;\n\n    private GetDataRequest(MultipartInput input) {\n        this.input = input;\n    }\n\n    @Schema(name = Constants.Multipart.STORE_PASSWORD)\n    public String getPassword() {\n        return MultipartUtils.getString(input, Constants.Multipart.STORE_PASSWORD);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/KeyPairUtils.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.jcraft.jsch.JSch;\nimport com.jcraft.jsch.JSchException;\nimport com.walmartlabs.concord.common.secret.KeyPair;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\nimport java.util.function.Consumer;\n\npublic final class KeyPairUtils {\n\n    private static final int DEFAULT_KEY_TYPE = com.jcraft.jsch.KeyPair.RSA;\n    private static final String DEFAULT_KEY_COMMENT = \"concord-server\";\n\n    private static final JSch jsch = new JSch();\n    private static final Lock mutex = new ReentrantLock();\n\n    public static KeyPair create(int keySize) {\n        com.jcraft.jsch.KeyPair k;\n\n        mutex.lock();\n        try {\n            k = com.jcraft.jsch.KeyPair.genKeyPair(jsch, DEFAULT_KEY_TYPE, keySize);\n        } catch (JSchException e) {\n            throw new SecurityException(e);\n        } finally {\n            mutex.unlock();\n        }\n\n        byte[] publicKey = array(out -> k.writePublicKey(out, DEFAULT_KEY_COMMENT));\n        byte[] privateKey = array(k::writePrivateKey);\n\n        return new KeyPair(publicKey, privateKey);\n    }\n\n    public static KeyPair create(InputStream publicIn, InputStream privateIn) throws IOException {\n        byte[] publicKey = publicIn.readAllBytes();\n        byte[] privateKey = privateIn.readAllBytes();\n        return new KeyPair(publicKey, privateKey);\n    }\n\n    private static byte[] array(Consumer<OutputStream> c) {\n        ByteArrayOutputStream out = new ByteArrayOutputStream();\n        c.accept(out);\n        return out.toByteArray();\n    }\n\n    private KeyPairUtils() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/PasswordChecker.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.security.UserPrincipal;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Predicate;\n\npublic final class PasswordChecker {\n\n    private static final int CONSECUTIVE_CHARS_IN_USERNAME = 3;\n\n    private enum Rule {\n        LENGTH(\"Password must be at least seven (7) characters\", p -> p.length() >= 7),\n        UPPER(\"Password must contain an UPPER character\", p -> p.chars().anyMatch(Character::isUpperCase)),\n        LOWER(\"Password must contain a lowercase character\", p -> p.chars().anyMatch(Character::isLowerCase)),\n        DIGIT(\"Password must contain a numeric character\", p -> p.chars().anyMatch(Character::isDigit)),\n        USERNAME(\"Passwords may NOT contain three (\" + CONSECUTIVE_CHARS_IN_USERNAME + \") consecutive characters from your user account name\",\n                p -> {\n                    UserPrincipal currentUser = UserPrincipal.getCurrent();\n                    if (currentUser == null) {\n                        return true;\n                    }\n                    return split(currentUser.getUsername(), CONSECUTIVE_CHARS_IN_USERNAME).stream()\n                        .noneMatch(p::contains);\n        });\n\n        private final String message;\n        private final Predicate<String> p;\n\n        Rule(String message, Predicate<String> p) {\n            this.message = message;\n            this.p = p;\n        }\n\n        public Predicate<String> getItem() {\n            return p;\n        }\n\n        public String getMessage() {\n            return message;\n        }\n    }\n\n    public static void check(String password) throws CheckerException {\n        for (Rule r : Rule.values()) {\n            if (!r.getItem().test(password)) {\n                throw new CheckerException(r.getMessage());\n            }\n        }\n    }\n\n    public static class CheckerException extends Exception {\n\n        private static final long serialVersionUID = 1L;\n\n        public CheckerException(String msg) {\n            super(msg);\n        }\n    }\n\n    private static List<String> split(String text, int charsCount) {\n        List<String> result = new ArrayList<>();\n        for (int i = 0; i <= text.length() - charsCount; i++) {\n            result.add(text.substring(i, i + charsCount));\n        }\n        return result;\n    }\n\n    private PasswordChecker() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/PasswordGenerator.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.security.SecureRandom;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Random;\n\npublic final class PasswordGenerator {\n\n    private static final Random RANDOM = new SecureRandom();\n\n    private static final int PASSWORD_LENGTH = 12;\n\n    private static final String UPPER_CHARS = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\";\n    private static final String LOWER_CHARS = \"abcdefghijklmnopqrstuvwxyz\";\n    private static final String NUMBER_CHARS = \"0123456789\";\n    private static final String OTHER_CHARS = \"~`!@#$%^&*()-_=+[{]}|,<.>/?\\\\\";\n\n    private static final String CHARS = UPPER_CHARS + LOWER_CHARS + NUMBER_CHARS + OTHER_CHARS;\n\n    public static String generate() {\n        List<String> alphanumericChars = Arrays.asList(UPPER_CHARS, LOWER_CHARS, NUMBER_CHARS);\n\n        Collections.shuffle(alphanumericChars, RANDOM);\n\n        StringBuilder result = new StringBuilder();\n        for (String chars : alphanumericChars) {\n            result.append(chars.charAt(RANDOM.nextInt(chars.length())));\n        }\n\n        for (int i = 0; i < PASSWORD_LENGTH - alphanumericChars.size(); i++) {\n            result.append(CHARS.charAt(RANDOM.nextInt(CHARS.length())));\n        }\n\n        return result.toString();\n    }\n\n    private PasswordGenerator() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/PublicKeyResponse.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.server.OperationResult;\n\nimport java.util.UUID;\n\n@JsonInclude(Include.NON_NULL)\npublic class PublicKeyResponse extends SecretOperationResponse {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String publicKey;\n\n    @JsonCreator\n    public PublicKeyResponse(@JsonProperty(\"id\") UUID id,\n                             @JsonProperty(\"result\") OperationResult result,\n                             @JsonProperty(\"password\") String password,\n                             @JsonProperty(\"publicKey\") String publicKey) {\n\n        super(id, result, password);\n        this.publicKey = publicKey;\n    }\n\n    public String getPublicKey() {\n        return publicKey;\n    }\n\n    @Override\n    public String toString() {\n        return \"PublicKeyResponse{\" +\n                \"publicKey='\" + publicKey + '\\'' +\n                \"} \" + super.toString();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/SecretDao.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.secret.HashAlgorithm;\nimport com.walmartlabs.concord.common.secret.SecretEncryptedByType;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.Utils;\nimport com.walmartlabs.concord.server.UuidGenerator;\nimport com.walmartlabs.concord.server.jooq.tables.Organizations;\nimport com.walmartlabs.concord.server.jooq.tables.Secrets;\nimport com.walmartlabs.concord.server.jooq.tables.Users;\nimport com.walmartlabs.concord.server.jooq.tables.records.SecretsRecord;\nimport com.walmartlabs.concord.server.org.EntityOwner;\nimport com.walmartlabs.concord.server.org.ResourceAccessEntry;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.org.project.ProjectEntry;\nimport com.walmartlabs.concord.server.user.UserType;\nimport org.jooq.*;\nimport org.jooq.exception.DataAccessException;\n\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.PROJECT_SECRETS;\nimport static com.walmartlabs.concord.server.jooq.Tables.V_USER_TEAMS;\nimport static com.walmartlabs.concord.server.jooq.tables.Organizations.ORGANIZATIONS;\nimport static com.walmartlabs.concord.server.jooq.tables.Projects.PROJECTS;\nimport static com.walmartlabs.concord.server.jooq.tables.SecretTeamAccess.SECRET_TEAM_ACCESS;\nimport static com.walmartlabs.concord.server.jooq.tables.Secrets.SECRETS;\nimport static com.walmartlabs.concord.server.jooq.tables.Teams.TEAMS;\nimport static com.walmartlabs.concord.server.jooq.tables.Users.USERS;\nimport static java.util.Objects.requireNonNull;\nimport static org.jooq.impl.DSL.*;\n\npublic class SecretDao extends AbstractDao {\n\n    public enum InsertMode {\n        INSERT,\n        UPSERT\n    }\n\n    private final UuidGenerator uuidGenerator;\n\n    @Inject\n    public SecretDao(@MainDB Configuration cfg, UuidGenerator uuidGenerator) {\n        super(cfg);\n        this.uuidGenerator = requireNonNull(uuidGenerator);\n    }\n\n    @Override\n    public void tx(Tx t) {\n        super.tx(t);\n    }\n\n    @Override\n    protected <T> T txResult(TxResult<T> t) {\n        return super.txResult(t);\n    }\n\n    public UUID getId(DSLContext tx, UUID orgId, String name) {\n        return tx.select(SECRETS.SECRET_ID)\n                .from(SECRETS)\n                .where(SECRETS.ORG_ID.eq(orgId)\n                        .and(SECRETS.SECRET_NAME.eq(name)))\n                .fetchOne(SECRETS.SECRET_ID);\n    }\n\n    public UUID getId(UUID orgId, String name) {\n        return getId(dsl(), orgId, name);\n    }\n\n    public String getName(UUID id) {\n        return dsl().select(SECRETS.SECRET_NAME)\n                .from(SECRETS)\n                .where(SECRETS.SECRET_ID.eq(id))\n                .fetchOne(SECRETS.SECRET_NAME);\n    }\n\n    public UUID getOrgId(UUID id) {\n        return dsl().select(SECRETS.ORG_ID)\n                .from(SECRETS)\n                .where(SECRETS.SECRET_ID.eq(id))\n                .fetchOne(SECRETS.ORG_ID);\n    }\n\n    public UUID insert(UUID orgId, String name, UUID ownerId, SecretType type,\n                       SecretEncryptedByType encryptedBy, String storeType,\n                       SecretVisibility visibility,\n                       byte[] secretSalt, HashAlgorithm hashAlgorithm, InsertMode insertMode) {\n\n        return txResult(tx -> insert(tx, orgId, name, ownerId, type, encryptedBy, storeType, visibility, secretSalt, hashAlgorithm, insertMode));\n\n    }\n\n    public UUID insert(DSLContext tx, UUID orgId, String name, UUID ownerId, SecretType type,\n                       SecretEncryptedByType encryptedBy, String storeType,\n                       SecretVisibility visibility, byte[] secretSalt, HashAlgorithm hashAlgorithm, InsertMode insertMode) {\n\n        UUID secretId = uuidGenerator.generate();\n\n        InsertOnDuplicateStep<SecretsRecord> builder = tx.insertInto(SECRETS)\n                .columns(SECRETS.SECRET_ID,\n                        SECRETS.SECRET_NAME,\n                        SECRETS.SECRET_TYPE,\n                        SECRETS.ORG_ID,\n                        SECRETS.OWNER_ID,\n                        SECRETS.ENCRYPTED_BY,\n                        SECRETS.STORE_TYPE,\n                        SECRETS.VISIBILITY,\n                        SECRETS.SECRET_SALT,\n                        SECRETS.HASH_ALGORITHM)\n                .values(secretId, name, type.toString(), orgId, ownerId, encryptedBy.toString(), storeType, visibility.toString(), secretSalt, hashAlgorithm.getName());\n\n        if (insertMode == InsertMode.UPSERT) {\n            Optional<SecretsRecord> secretsRecord = builder.onDuplicateKeyIgnore()\n                    .returning(SECRETS.SECRET_ID)\n                    .fetchOptional();\n            return secretsRecord.map(SecretsRecord::getSecretId).orElseGet(() -> tx.select(SECRETS.SECRET_ID)\n                    .from(SECRETS)\n                    .where(SECRETS.SECRET_NAME.eq(name).and(SECRETS.ORG_ID.eq(orgId)))\n                    .fetchOne()\n                    .value1());\n        }\n\n        return builder\n                .returning(SECRETS.SECRET_ID)\n                .fetchOne()\n                .getSecretId();\n    }\n\n    public SecretEntryV2 get(UUID id) {\n        DSLContext tx = dsl();\n        return selectEntry(tx)\n                .where(SECRETS.SECRET_ID.eq(id))\n                .fetchOne(secretRecord -> toEntry(tx, secretRecord));\n    }\n\n    public byte[] getData(UUID id) {\n        return dsl().select(SECRETS.SECRET_DATA)\n                .from(SECRETS)\n                .where(SECRETS.SECRET_ID.eq(id))\n                .fetchOne(SECRETS.SECRET_DATA);\n    }\n\n    public SecretEntryV2 getByName(UUID orgId, String name) {\n        DSLContext tx = dsl();\n        return selectEntry(tx)\n                .where(SECRETS.ORG_ID.eq(orgId)\n                        .and(SECRETS.SECRET_NAME.eq(name)))\n                .fetchOne(secretRecord -> toEntry(tx, secretRecord));\n    }\n\n    public void updateProjectScopeByProjectId(DSLContext tx, UUID orgId, UUID projectId, UUID newProjectId) {\n        if (newProjectId == null) {\n            tx.deleteFrom(PROJECT_SECRETS).where(PROJECT_SECRETS.PROJECT_ID.eq(projectId));\n        } else {\n            tx.update(PROJECT_SECRETS).set(PROJECT_SECRETS.PROJECT_ID, newProjectId).where(PROJECT_SECRETS.PROJECT_ID.eq(projectId));\n        }\n    }\n\n    public void updateData(UUID id, byte[] data) {\n        tx(tx -> updateData(tx, id, data));\n    }\n\n    public void updateData(DSLContext tx, UUID id, byte[] data) {\n        int i = tx.update(SECRETS)\n                .set(SECRETS.SECRET_DATA, data)\n                .where(SECRETS.SECRET_ID.eq(id))\n                .execute();\n\n        if (i != 1) {\n            throw new DataAccessException(\"Invalid number of rows updated: \" + i);\n        }\n    }\n\n    public void update(UUID id, String newName, UUID ownerId, SecretType newType, byte[] data, SecretVisibility visibility, UUID orgId, HashAlgorithm hashAlgorithm) {\n        tx(tx -> update(tx, id, newName, ownerId, newType, data, visibility, orgId, hashAlgorithm));\n    }\n\n    public void updateSecretProjects(UUID id, Set<UUID> projectIds) {\n        tx(tx -> updateSecretProjects(tx, id, projectIds));\n    }\n\n    public void updateSecretProjects(DSLContext tx, UUID id, Set<UUID> projectIds) {\n        if (projectIds != null) {\n            tx.deleteFrom(PROJECT_SECRETS).where(PROJECT_SECRETS.SECRET_ID.eq(id)).execute();\n            for (UUID projectId : projectIds) {\n                tx.insertInto(PROJECT_SECRETS).columns(PROJECT_SECRETS.SECRET_ID, PROJECT_SECRETS.PROJECT_ID).values(id, projectId).execute();\n            }\n        }\n    }\n\n    public void update(DSLContext tx, UUID id, String newName, UUID ownerId, SecretType newType, byte[] data, SecretVisibility visibility, UUID orgId, HashAlgorithm hashAlgorithm) {\n        UpdateSetFirstStep<SecretsRecord> u = tx.update(SECRETS);\n        boolean needUpdate = false;\n        if (newName != null) {\n            u.set(SECRETS.SECRET_NAME, newName);\n            needUpdate = true;\n        }\n\n        if (ownerId != null) {\n            u.set(SECRETS.OWNER_ID, ownerId);\n            needUpdate = true;\n        }\n\n        if (visibility != null) {\n            u.set(SECRETS.VISIBILITY, visibility.toString());\n            needUpdate = true;\n        }\n\n        if (newType != null) {\n            u.set(SECRETS.SECRET_TYPE, newType.name());\n            needUpdate = true;\n        }\n\n        if (data != null) {\n            u.set(SECRETS.SECRET_DATA, data);\n            u.set(SECRETS.LAST_UPDATED_AT, currentOffsetDateTime());\n            needUpdate = true;\n        }\n\n        if (orgId != null) {\n            u.set(SECRETS.ORG_ID, orgId);\n            needUpdate = true;\n        }\n\n        if (hashAlgorithm != null) {\n            u.set(SECRETS.HASH_ALGORITHM, hashAlgorithm.getName());\n            needUpdate = true;\n        }\n        if (needUpdate) {\n            int i = ((UpdateSetMoreStep) u).where(SECRETS.SECRET_ID.eq(id))\n                    .execute();\n\n            if (i != 1) {\n                throw new DataAccessException(\"Invalid number of rows updated: \" + i);\n            }\n        }\n\n    }\n\n    public List<SecretEntryV2> list(UUID orgId, UUID currentUserId, Field<?> sortField, boolean asc, int offset, int limit, String filter) {\n\n        Organizations o = ORGANIZATIONS.as(\"o\");\n        Secrets s = SECRETS.as(\"s\");\n        Users u = USERS.as(\"u\");\n\n        sortField = s.field(sortField);\n\n        DSLContext tx = dsl();\n        SelectOnConditionStep<Record17<UUID, String, UUID, String, String, String, String, String, UUID, String, String, String, String, byte[], String, OffsetDateTime, OffsetDateTime>> q = selectEntry(tx, o, s, u);\n\n        if (currentUserId != null) {\n            // public secrets are visible for anyone\n            Condition isPublic = s.VISIBILITY.eq(SecretVisibility.PUBLIC.toString());\n\n            // check if the user belongs to a team in the org\n            SelectConditionStep<Record1<UUID>> teamIds = select(TEAMS.TEAM_ID)\n                    .from(TEAMS)\n                    .where(TEAMS.ORG_ID.eq(orgId));\n\n            Condition isInATeam = exists(selectOne().from(V_USER_TEAMS)\n                    .where(V_USER_TEAMS.USER_ID.eq(currentUserId)\n                            .and(V_USER_TEAMS.TEAM_ID.in(teamIds))));\n\n            // check if the user owns secrets in the org\n            Condition ownsSecrets = s.OWNER_ID.eq(currentUserId);\n\n            // check if the user owns the org\n            Condition ownsOrg = o.OWNER_ID.eq(currentUserId);\n\n            // if any of those conditions true then the secret must be visible\n            q.where(or(isPublic, isInATeam, ownsSecrets, ownsOrg));\n        }\n\n        if (orgId != null) {\n            q.where(s.ORG_ID.eq(orgId));\n        }\n\n        if (filter != null) {\n            q.where(s.SECRET_NAME.containsIgnoreCase(filter));\n        }\n\n        if (sortField != null) {\n            q.orderBy(asc ? sortField.asc() : sortField.desc());\n        }\n\n        if (offset >= 0) {\n            q.offset(offset);\n        }\n\n        if (limit > 0) {\n            q.limit(limit);\n        }\n\n        return q.fetch(secretRecord -> toEntry(tx, secretRecord));\n    }\n\n    public void delete(UUID id) {\n        tx(tx -> delete(tx, id));\n    }\n\n    public void delete(DSLContext tx, UUID id) {\n        tx.deleteFrom(SECRETS)\n                .where(SECRETS.SECRET_ID.eq(id))\n                .execute();\n    }\n\n    public List<ResourceAccessEntry> getAccessLevel(UUID orgId, String name) {\n        List<ResourceAccessEntry> resourceAccessList = new ArrayList<>();\n\n        Result<Record4<UUID, UUID, String, String>> teamAccess = dsl().select(\n                        SECRET_TEAM_ACCESS.TEAM_ID,\n                        SECRET_TEAM_ACCESS.SECRET_ID,\n                        TEAMS.TEAM_NAME,\n                        SECRET_TEAM_ACCESS.ACCESS_LEVEL)\n                .from(SECRET_TEAM_ACCESS)\n                .leftOuterJoin(TEAMS).on(TEAMS.TEAM_ID.eq(SECRET_TEAM_ACCESS.TEAM_ID))\n                .where(SECRET_TEAM_ACCESS.SECRET_ID.eq(getByName(orgId, name).getId()))\n                .fetch();\n\n        for (Record4<UUID, UUID, String, String> t : teamAccess) {\n            resourceAccessList.add(new ResourceAccessEntry(t.get(SECRET_TEAM_ACCESS.TEAM_ID),\n                    null,\n                    t.get(TEAMS.TEAM_NAME),\n                    ResourceAccessLevel.valueOf(t.get(SECRET_TEAM_ACCESS.ACCESS_LEVEL))));\n        }\n\n        return resourceAccessList;\n    }\n\n    public void deleteTeamAccess(DSLContext tx, UUID secretId) {\n        tx.deleteFrom(SECRET_TEAM_ACCESS)\n                .where(SECRET_TEAM_ACCESS.SECRET_ID.eq(secretId))\n                .execute();\n    }\n\n    public boolean hasAccessLevel(UUID secretId, UUID userId, ResourceAccessLevel... levels) {\n        return hasAccessLevel(dsl(), secretId, userId, levels);\n    }\n\n    private boolean hasAccessLevel(DSLContext tx, UUID secretId, UUID userId, ResourceAccessLevel... levels) {\n        SelectConditionStep<Record1<UUID>> teamIds = select(V_USER_TEAMS.TEAM_ID)\n                .from(V_USER_TEAMS)\n                .where(V_USER_TEAMS.USER_ID.eq(userId));\n\n        return tx.fetchExists(selectFrom(SECRET_TEAM_ACCESS)\n                .where(SECRET_TEAM_ACCESS.SECRET_ID.eq(secretId)\n                        .and(SECRET_TEAM_ACCESS.TEAM_ID.in(teamIds))\n                        .and(SECRET_TEAM_ACCESS.ACCESS_LEVEL.in(Utils.toString(levels)))));\n    }\n\n    public void upsertAccessLevel(UUID secretId, UUID teamId, ResourceAccessLevel level) {\n        tx(tx -> upsertAccessLevel(tx, secretId, teamId, level));\n    }\n\n    public void upsertAccessLevel(DSLContext tx, UUID secretId, UUID teamId, ResourceAccessLevel level) {\n        tx.insertInto(SECRET_TEAM_ACCESS)\n                .columns(SECRET_TEAM_ACCESS.SECRET_ID, SECRET_TEAM_ACCESS.TEAM_ID, SECRET_TEAM_ACCESS.ACCESS_LEVEL)\n                .values(secretId, teamId, level.toString())\n                .onDuplicateKeyUpdate()\n                .set(SECRET_TEAM_ACCESS.ACCESS_LEVEL, level.toString())\n                .execute();\n    }\n\n    private static SelectOnConditionStep<Record17<UUID, String, UUID, String, String, String, String, String, UUID, String, String, String, String, byte[], String, OffsetDateTime, OffsetDateTime>> selectEntry(DSLContext tx) {\n        return selectEntry(tx, ORGANIZATIONS, SECRETS, USERS);\n    }\n\n    private static SelectOnConditionStep<Record17<UUID, String, UUID, String, String, String, String, String, UUID, String, String, String, String, byte[], String, OffsetDateTime, OffsetDateTime>> selectEntry(\n            DSLContext tx,\n            Organizations orgAlias,\n            Secrets secretAlias, Users userAlias) {\n        return tx.select(secretAlias.SECRET_ID,\n                        secretAlias.SECRET_NAME,\n                        secretAlias.ORG_ID,\n                        orgAlias.ORG_NAME,\n                        secretAlias.SECRET_TYPE,\n                        secretAlias.ENCRYPTED_BY,\n                        secretAlias.STORE_TYPE,\n                        secretAlias.VISIBILITY,\n                        userAlias.USER_ID,\n                        userAlias.USERNAME,\n                        userAlias.DOMAIN,\n                        userAlias.DISPLAY_NAME,\n                        userAlias.USER_TYPE,\n                        secretAlias.SECRET_SALT,\n                        secretAlias.HASH_ALGORITHM,\n                        secretAlias.CREATED_AT,\n                        secretAlias.LAST_UPDATED_AT\n                )\n\n                .from(secretAlias)\n                .leftJoin(userAlias).on(secretAlias.OWNER_ID.eq(userAlias.USER_ID))\n                .leftJoin(orgAlias).on(orgAlias.ORG_ID.eq(secretAlias.ORG_ID));\n    }\n\n    private static SecretEntryV2 toEntry(DSLContext tx, Record17<UUID, String, UUID, String, String, String, String, String, UUID, String, String, String, String, byte[], String, OffsetDateTime, OffsetDateTime> r) {\n        UUID secretId = r.get(SECRETS.SECRET_ID);\n        Set<ProjectEntry> projects = tx.select(PROJECTS.PROJECT_ID, PROJECTS.PROJECT_NAME).from(PROJECTS).leftJoin(PROJECT_SECRETS).on(PROJECTS.PROJECT_ID.eq(PROJECT_SECRETS.PROJECT_ID))\n                .where(PROJECT_SECRETS.SECRET_ID.eq(secretId)).stream()\n                .map(projectRecord -> new ProjectEntry(projectRecord.get(PROJECTS.PROJECT_NAME), projectRecord.get(PROJECTS.PROJECT_ID))).collect(Collectors.toSet());\n        return new SecretEntryV2(r.get(SECRETS.SECRET_ID),\n                r.get(SECRETS.SECRET_NAME),\n                r.get(SECRETS.ORG_ID),\n                r.value4(),\n                projects,\n                SecretType.valueOf(r.get(SECRETS.SECRET_TYPE)),\n                SecretEncryptedByType.valueOf(r.get(SECRETS.ENCRYPTED_BY)),\n                r.get(SECRETS.STORE_TYPE),\n                SecretVisibility.valueOf(r.get(SECRETS.VISIBILITY)),\n                toOwner(r),\n                r.get(SECRETS.CREATED_AT),\n                r.get(SECRETS.LAST_UPDATED_AT),\n                r.get(SECRETS.SECRET_SALT),\n                HashAlgorithm.getByName(r.get(SECRETS.HASH_ALGORITHM))\n        );\n    }\n\n    private static EntityOwner toOwner(Record17<UUID, String, UUID, String, String, String, String, String, UUID, String, String, String, String, byte[], String, OffsetDateTime, OffsetDateTime> r) {\n\n        UUID id = r.get(USERS.USER_ID);\n        if (id == null) {\n            return null;\n        }\n\n        return EntityOwner.builder()\n                .id(id)\n                .username(r.get(USERS.USERNAME))\n                .userDomain(r.get(USERS.DOMAIN))\n                .displayName(r.get(USERS.DISPLAY_NAME))\n                .userType(UserType.valueOf(r.get(USERS.USER_TYPE)))\n                .build();\n    }\n\n    public static class SecretDataEntry extends SecretEntryV2 {\n\n        private static final long serialVersionUID = 1L;\n\n        private final byte[] data;\n\n        public SecretDataEntry(SecretEntryV2 s, byte[] data) { // NOSONAR\n            super(s.getId(), s.getName(), s.getOrgId(), s.getOrgName(), s.getProjects(),\n                    s.getType(), s.getEncryptedBy(), s.getStoreType(), s.getVisibility(), s.getOwner(),\n                    s.getCreatedAt(), s.getLastUpdatedAt(), s.getSecretSalt(), s.getHashAlgorithm());\n            this.data = data;\n        }\n\n        public byte[] getData() {\n            return data;\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/SecretEntryV2.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.*;\nimport com.walmartlabs.concord.common.secret.HashAlgorithm;\nimport com.walmartlabs.concord.common.secret.SecretEncryptedByType;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.server.org.EntityOwner;\nimport com.walmartlabs.concord.server.org.project.ProjectEntry;\n\nimport javax.validation.constraints.NotNull;\nimport java.io.Serial;\nimport java.io.Serializable;\nimport java.time.OffsetDateTime;\nimport java.util.Set;\nimport java.util.UUID;\n\n@JsonInclude(JsonInclude.Include.NON_NULL)\npublic class SecretEntryV2 implements Serializable {\n\n    @Serial\n    private static final long serialVersionUID = 1L;\n\n    private final UUID id;\n\n    @NotNull\n    @ConcordKey\n    private final String name;\n\n    private final UUID orgId;\n\n    @ConcordKey\n    private final String orgName;\n\n    @NotNull\n    private final SecretType type;\n\n    @NotNull\n    private final String storeType;\n\n    private final SecretEncryptedByType encryptedBy;\n\n    private final SecretVisibility visibility;\n\n    private final EntityOwner owner;\n\n    @JsonIgnore\n    private final byte[] secretSalt;\n\n    @JsonIgnore\n    private final HashAlgorithm hashAlgorithm;\n\n    private final Set<ProjectEntry> projects;\n\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    private final OffsetDateTime createdAt;\n\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    private final OffsetDateTime lastUpdatedAt;\n\n    @JsonCreator\n    public SecretEntryV2(@JsonProperty(\"id\") UUID id,\n                         @JsonProperty(\"name\") String name,\n                         @JsonProperty(\"orgId\") UUID orgId,\n                         @JsonProperty(\"orgName\") String orgName,\n                         @JsonProperty(\"projects\") Set<ProjectEntry> projects,\n                         @JsonProperty(\"type\") SecretType type,\n                         @JsonProperty(\"encryptedBy\") SecretEncryptedByType encryptedBy,\n                         @JsonProperty(\"storeType\") String storeType,\n                         @JsonProperty(\"visibility\") SecretVisibility visibility,\n                         @JsonProperty(\"owner\") EntityOwner owner,\n                         @JsonProperty(\"createdAt\") OffsetDateTime createdAt,\n                         @JsonProperty(\"lastUpdatedAt\") OffsetDateTime lastUpdatedAt,\n                         byte[] secretSalt,\n                         HashAlgorithm hashAlgorithm\n    ) {\n        this.id = id;\n        this.name = name;\n        this.orgId = orgId;\n        this.orgName = orgName;\n        this.type = type;\n        this.encryptedBy = encryptedBy;\n        this.storeType = storeType;\n        this.visibility = visibility;\n        this.owner = owner;\n        this.secretSalt = secretSalt;\n        this.hashAlgorithm = hashAlgorithm;\n        this.projects = projects;\n        this.createdAt = createdAt;\n        this.lastUpdatedAt = lastUpdatedAt;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public UUID getOrgId() {\n        return orgId;\n    }\n\n    public String getOrgName() {\n        return orgName;\n    }\n\n    public SecretType getType() {\n        return type;\n    }\n\n    public SecretEncryptedByType getEncryptedBy() {\n        return encryptedBy;\n    }\n\n    public String getStoreType() {\n        return storeType;\n    }\n\n    public SecretVisibility getVisibility() {\n        return visibility;\n    }\n\n    public EntityOwner getOwner() {\n        return owner;\n    }\n\n    public byte[] getSecretSalt() {\n        return secretSalt;\n    }\n\n    public HashAlgorithm getHashAlgorithm() {\n        return hashAlgorithm;\n    }\n\n    public Set<ProjectEntry> getProjects() {\n        return projects;\n    }\n\n    public OffsetDateTime getCreatedAt() {\n        return createdAt;\n    }\n\n    public OffsetDateTime getLastUpdatedAt() {\n        return lastUpdatedAt;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/SecretException.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic class SecretException extends RuntimeException {\n\n    private static final long serialVersionUID = 1L;\n\n    public SecretException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/SecretExceptionMapper.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.boot.resteasy.ExceptionMapperSupport;\n\nimport javax.ws.rs.core.Context;\nimport javax.ws.rs.core.HttpHeaders;\nimport javax.ws.rs.core.Response;\nimport javax.ws.rs.ext.Provider;\n\n@Provider\npublic class SecretExceptionMapper extends ExceptionMapperSupport<SecretException> {\n\n    @Context\n    HttpHeaders headers;\n\n    @Override\n    protected Response convert(SecretException e) {\n        return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/SecretManager.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.collect.ImmutableMap;\nimport com.google.common.io.ByteStreams;\nimport com.walmartlabs.concord.common.secret.*;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.Secret;\nimport com.walmartlabs.concord.server.audit.AuditAction;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.audit.AuditObject;\nimport com.walmartlabs.concord.server.cfg.SecretStoreConfiguration;\nimport com.walmartlabs.concord.server.org.*;\nimport com.walmartlabs.concord.server.org.project.DiffUtils;\nimport com.walmartlabs.concord.server.org.project.ProjectAccessManager;\nimport com.walmartlabs.concord.server.org.project.ProjectEntry;\nimport com.walmartlabs.concord.server.org.project.RepositoryDao;\nimport com.walmartlabs.concord.server.org.secret.SecretDao.SecretDataEntry;\nimport com.walmartlabs.concord.server.org.secret.provider.SecretStoreProvider;\nimport com.walmartlabs.concord.server.org.secret.store.SecretStore;\nimport com.walmartlabs.concord.server.policy.EntityAction;\nimport com.walmartlabs.concord.server.policy.EntityType;\nimport com.walmartlabs.concord.server.policy.PolicyManager;\nimport com.walmartlabs.concord.server.policy.PolicyUtils;\nimport com.walmartlabs.concord.server.process.ProcessEntry;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueManager;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.security.apikey.ApiKeyDao;\nimport com.walmartlabs.concord.server.security.apikey.ApiKeyEntry;\nimport com.walmartlabs.concord.server.security.sessionkey.SessionKeyPrincipal;\nimport com.walmartlabs.concord.server.user.UserDao;\nimport com.walmartlabs.concord.server.user.UserEntry;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport org.jooq.DSLContext;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.WebApplicationException;\nimport javax.ws.rs.core.Response.Status;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.Serializable;\nimport java.nio.charset.StandardCharsets;\nimport java.util.*;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.SECRETS;\nimport static com.walmartlabs.concord.server.org.secret.SecretDao.InsertMode.INSERT;\n\npublic class SecretManager {\n\n    private final PolicyManager policyManager;\n    private final AuditLog auditLog;\n    private final OrganizationManager orgManager;\n    private final ProcessQueueManager processQueueManager;\n    private final SecretDao secretDao;\n    private final SecretStoreConfiguration secretCfg;\n    private final SecretStoreProvider secretStoreProvider;\n    private final UserDao userDao;\n    private final ProjectAccessManager projectAccessManager;\n    private final RepositoryDao repositoryDao;\n    private final UserManager userManager;\n    private final ApiKeyDao apiKeyDao;\n\n    private static final int SALT_LENGTH = 16;\n\n    @Inject\n    public SecretManager(PolicyManager policyManager,\n                         AuditLog auditLog,\n                         OrganizationManager orgManager,\n                         ProcessQueueManager processQueueManager,\n                         SecretDao secretDao,\n                         SecretStoreConfiguration secretCfg,\n                         SecretStoreProvider secretStoreProvider,\n                         UserDao userDao,\n                         ProjectAccessManager projectAccessManager,\n                         RepositoryDao repositoryDao,\n                         UserManager userManager,\n                         ApiKeyDao apiKeyDao) {\n\n        this.policyManager = policyManager;\n        this.processQueueManager = processQueueManager;\n        this.secretDao = secretDao;\n        this.secretCfg = secretCfg;\n        this.orgManager = orgManager;\n        this.userDao = userDao;\n        this.secretStoreProvider = secretStoreProvider;\n        this.auditLog = auditLog;\n        this.projectAccessManager = projectAccessManager;\n        this.repositoryDao = repositoryDao;\n        this.userManager = userManager;\n        this.apiKeyDao = apiKeyDao;\n    }\n\n    @WithTimer\n    public SecretEntryV2 assertAccess(UUID orgId, UUID secretId, String secretName, ResourceAccessLevel level, boolean orgMembersOnly) {\n        if (secretId == null && (orgId == null || secretName == null)) {\n            throw new ValidationErrorsException(\"Secret ID or an organization ID and a secret name is required\");\n        }\n\n        SecretEntryV2 e = null;\n\n        if (secretId != null) {\n            e = secretDao.get(secretId);\n            if (e == null) {\n                throw new WebApplicationException(\"Secret not found: \" + secretId, Status.NOT_FOUND);\n            }\n        }\n\n        if (e == null) {\n            e = secretDao.getByName(orgId, secretName);\n            if (e == null) {\n                throw new WebApplicationException(\"Secret not found: \" + secretName, Status.NOT_FOUND);\n            }\n        }\n\n        if (Roles.isAdmin()) {\n            // an admin can access any secret\n            return e;\n        }\n\n        if (level == ResourceAccessLevel.READER && (Roles.isGlobalReader() || Roles.isGlobalWriter())) {\n            return e;\n        } else if (level == ResourceAccessLevel.WRITER && Roles.isGlobalWriter()) {\n            return e;\n        }\n\n        UserPrincipal p = UserPrincipal.assertCurrent();\n\n        EntityOwner owner = e.getOwner();\n        if (owner != null && p.getId().equals(owner.id())) {\n            // the owner can do anything with his secrets\n            return e;\n        }\n\n        if (orgMembersOnly && e.getVisibility() == SecretVisibility.PUBLIC\n                && level == ResourceAccessLevel.READER\n                && userDao.isInOrganization(p.getId(), e.getOrgId())) {\n            // organization members can access any public secret in the same organization\n            return e;\n        }\n\n        OrganizationEntry org = orgManager.assertAccess(e.getOrgId(), false);\n        if (ResourceAccessUtils.isSame(p, org.getOwner())) {\n            // the org owner can do anything with the org's secrets\n            return e;\n        }\n\n        if (orgMembersOnly || e.getVisibility() != SecretVisibility.PUBLIC) {\n            // we need to check the resource's access level if the access is limited to\n            // the organization's members or the secret is not public\n            if (!secretDao.hasAccessLevel(e.getId(), p.getId(), ResourceAccessLevel.atLeast(level))) {\n                throw new UnauthorizedException(\"The current user doesn't have \" +\n                        \"the necessary access level (\" + level + \") to the secret: \" + e.getName());\n            }\n        }\n\n        return e;\n    }\n\n    /**\n     * Generates and stores a new SSH key pair.\n     */\n    public DecryptedKeyPair createKeyPair(UUID orgId, Set<UUID> projectIds, String name, String storePassword,\n                                          SecretVisibility visibility, String secretStoreType) {\n\n        orgManager.assertAccess(orgId, true);\n\n        KeyPair k = generateKeyPair();\n        UUID id = create(name, orgId, projectIds, k, storePassword, visibility, secretStoreType, INSERT);\n        return new DecryptedKeyPair(id, k.getPublicKey());\n    }\n\n    public KeyPair generateKeyPair() {\n        return KeyPairUtils.create(secretCfg.getKeySize());\n    }\n\n    /**\n     * Stores a new SSH key pair using the provided public and private keys.\n     */\n    public DecryptedKeyPair createKeyPair(UUID orgId, Set<UUID> projectIds, String name, String storePassword,\n                                          InputStream publicKey,\n                                          InputStream privateKey, SecretVisibility visibility,\n                                          String secretStoreType) throws IOException {\n\n        orgManager.assertAccess(orgId, true);\n\n        KeyPair k = buildKeyPair(publicKey, privateKey);\n        UUID id = create(name, orgId, projectIds, k, storePassword, visibility, secretStoreType, INSERT);\n\n        return new DecryptedKeyPair(id, k.getPublicKey());\n    }\n\n    public KeyPair buildKeyPair(InputStream publicKey, InputStream privateKey) throws IOException {\n        return KeyPairUtils.create(publicKey, privateKey);\n    }\n\n    /**\n     * Stores a new username and password secret.\n     */\n    public DecryptedUsernamePassword createUsernamePassword(UUID orgId, Set<UUID> projectIds, String name, String storePassword,\n                                                            String username, char[] password, SecretVisibility visibility,\n                                                            String secretStoreType) {\n\n        orgManager.assertAccess(orgId, true);\n\n        UsernamePassword p = buildUsernamePassword(username, password);\n        UUID id = create(name, orgId, projectIds, p, storePassword, visibility, secretStoreType, INSERT);\n        return new DecryptedUsernamePassword(id);\n    }\n\n    public UsernamePassword buildUsernamePassword(String username, char[] password) {\n        return new UsernamePassword(username, password);\n    }\n\n    /**\n     * Stores a new single value secret.\n     */\n    public DecryptedBinaryData createBinaryData(UUID orgId, Set<UUID> projectIds, String name, String storePassword,\n                                                InputStream data, SecretVisibility visibility,\n                                                String storeType) {\n\n        return createBinaryData(orgId, projectIds, name, storePassword, data, visibility, storeType, INSERT);\n    }\n\n    /**\n     * Stores a new single value secret.\n     */\n    public DecryptedBinaryData createBinaryData(UUID orgId,\n                                                Set<UUID> projectIds,\n                                                String name,\n                                                String storePassword,\n                                                InputStream data,\n                                                SecretVisibility visibility,\n                                                String storeType,\n                                                SecretDao.InsertMode insertMode) {\n\n        return secretDao.txResult(tx -> createBinaryData(tx, orgId, projectIds, name, storePassword, data, visibility, storeType, insertMode));\n    }\n\n    /**\n     * Stores a new single value secret.\n     */\n    public DecryptedBinaryData createBinaryData(DSLContext tx,\n                                                UUID orgId,\n                                                Set<UUID> projectIds,\n                                                String name,\n                                                String storePassword,\n                                                InputStream data,\n                                                SecretVisibility visibility,\n                                                String storeType,\n                                                SecretDao.InsertMode insertMode) throws IOException {\n\n        orgManager.assertAccess(tx, orgId, true);\n\n        BinaryDataSecret d = buildBinaryData(data);\n        UUID id = create(tx, name, orgId, projectIds, d, storePassword, visibility, storeType, insertMode);\n        return new DecryptedBinaryData(id);\n    }\n\n    public BinaryDataSecret buildBinaryData(InputStream data) throws IOException {\n        int maxSecretDataSize = secretStoreProvider.getMaxSecretDataSize();\n        InputStream limitedDataInputStream = ByteStreams.limit(data, maxSecretDataSize + 1L);\n        BinaryDataSecret secret = new BinaryDataSecret(ByteStreams.toByteArray(limitedDataInputStream));\n        if (secret.getData().length > maxSecretDataSize) {\n            throw new IllegalArgumentException(\"File size exceeds limit of \" + maxSecretDataSize + \" bytes\");\n        }\n        return secret;\n    }\n\n    public ApiKeyEntry assertApiKey(AccessScope accessScope, UUID orgId, String secretName, String password) {\n        DecryptedSecret secret = getSecret(accessScope, orgId, secretName, password, SecretType.DATA);\n        BinaryDataSecret data = (BinaryDataSecret) secret.getSecret();\n        ApiKeyEntry result = apiKeyDao.find(new String(data.getData()));\n        if (result == null) {\n            throw new ConcordApplicationException(\"Api key from secret '\" + secretName + \"' not found\", Status.NOT_FOUND);\n        }\n        return result;\n    }\n\n    /**\n     * Decrypts a stored SSH key pair.\n     */\n    public DecryptedKeyPair getKeyPair(AccessScope accessScope, UUID orgId, String name) {\n        DecryptedSecret e = getSecret(accessScope, orgId, name, null, SecretType.KEY_PAIR);\n        if (e == null) {\n            return null;\n        }\n\n        Secret s = e.getSecret();\n        KeyPair k = (KeyPair) s;\n        return new DecryptedKeyPair(e.getId(), k.getPublicKey());\n    }\n\n    public void update(UUID orgId, String secretName, SecretUpdateParams params) {\n        SecretEntryV2 e = assertAccess(orgId, null, secretName, ResourceAccessLevel.WRITER, false);\n\n        UUID newOrgId = validateOrgId(params.newOrgId(), params.newOrgName(), e);\n        Set<UUID> newProjectIds = validateProjectIds(params.newProjectIds(), params.removeProjectLink(), newOrgId, e);\n        UserEntry newOwner = validateOwner(params.newOwnerId(), e);\n\n        String currentPassword = params.currentPassword();\n        String newPassword = params.newPassword();\n\n        if (e.getEncryptedBy() == SecretEncryptedByType.SERVER_KEY && (currentPassword != null || newPassword != null)) {\n            throw new ConcordApplicationException(\"The secret is encrypted with the server's key, the '\" + Constants.Multipart.STORE_PASSWORD + \"' cannot be changed\", Status.BAD_REQUEST);\n        }\n\n        Map<String, Object> updated = new HashMap<>();\n        byte[] newData = null;\n        SecretType newType = null;\n        if (params.newSecret() != null) {\n            // updating the data and/or the store password\n            if (e.getEncryptedBy() == SecretEncryptedByType.PASSWORD && currentPassword == null) {\n                throw new ConcordApplicationException(\"Updating the secret's data requires the original '\" + Constants.Multipart.STORE_PASSWORD + \"'\", Status.BAD_REQUEST);\n            }\n\n            // validate the current password\n            decryptData(e, currentPassword);\n\n            newData = serialize(params.newSecret());\n            newType = secretType(params.newSecret());\n\n            updated.put(\"data\", true);\n        } else if (newPassword != null) {\n            // keeping the old data, just changing the store password\n            newData = decryptData(e, currentPassword);\n        }\n\n        String pwd = currentPassword;\n        if (newPassword != null && !newPassword.equals(currentPassword)) {\n            pwd = newPassword;\n            updated.put(\"password\", true);\n        }\n\n        byte[] newEncryptedData;\n        HashAlgorithm hashAlgorithm;\n        if (newData != null) {\n            // encrypt the supplied data\n            byte[] salt = e.getSecretSalt();\n            newEncryptedData = SecretUtils.encrypt(newData, getPwd(pwd), salt, HashAlgorithm.SHA256);\n            hashAlgorithm = HashAlgorithm.SHA256;\n        } else {\n            newEncryptedData = null;\n            hashAlgorithm = null;\n        }\n        if(newProjectIds == null || newProjectIds.isEmpty()){\n            policyManager.checkEntity(e.getOrgId(), null, EntityType.SECRET, EntityAction.UPDATE, newOwner,\n                    PolicyUtils.secretToMap(e.getOrgId(), e.getName(), e.getType(), e.getVisibility(), e.getStoreType()));\n        } else {\n            newProjectIds.stream().forEach(newProjectId -> {\n                policyManager.checkEntity(e.getOrgId(), newProjectId, EntityType.SECRET, EntityAction.UPDATE, newOwner,\n                        PolicyUtils.secretToMap(e.getOrgId(), e.getName(), e.getType(), e.getVisibility(), e.getStoreType()));\n            });\n        }\n\n        String newName = validateName(params.newName(), newOrgId, e);\n\n        SecretType finalNewType = newType;\n        secretDao.tx(tx -> {\n            if (newOrgId != null) {\n                // update repository mapping to null when org is changing\n                repositoryDao.clearSecretMappingBySecretId(tx, e.getId());\n            }\n\n            secretDao.update(tx, e.getId(), params.newName(), newOwner != null ? newOwner.getId() : null,\n                    finalNewType, newEncryptedData, params.newVisibility(), newOrgId, hashAlgorithm);\n            secretDao.updateSecretProjects(tx, e.getId(), params.removeProjectLink() ? Collections.emptySet() : newProjectIds);\n        });\n\n        Map<String, Object> changes = DiffUtils.compare(e, secretDao.get(e.getId()));\n        changes.put(\"updated\", updated);\n\n        auditLog.add(AuditObject.SECRET, AuditAction.UPDATE)\n                .field(\"orgId\", e.getOrgId())\n                .field(\"secretId\", e.getId())\n                .field(\"name\", e.getName())\n                .field(\"changes\", changes)\n                .log();\n    }\n\n    private String validateName(String newName, UUID newOrgId, SecretEntryV2 e) {\n        if (newOrgId == null && newName == null) {\n            return null;\n        }\n\n        UUID orgId = newOrgId != null ? newOrgId : e.getOrgId();\n        String name = newName != null ? newName : e.getName();\n        if (secretDao.getId(orgId, name) != null) {\n            throw new ValidationErrorsException(\"Secret already exists: \" + e.getName());\n        }\n        return newName;\n    }\n\n    /**\n     * Removes an existing secret.\n     */\n    public void delete(DSLContext tx, UUID orgId, String secretName) {\n        SecretEntryV2 e = assertAccess(orgId, null, secretName, ResourceAccessLevel.OWNER, true);\n\n        // delete the content first\n        getSecretStore(e.getStoreType()).delete(tx, e.getId());\n        // now delete secret information from secret table\n        secretDao.delete(tx, e.getId());\n\n        auditLog.add(AuditObject.SECRET, AuditAction.DELETE)\n                .field(\"orgId\", e.getOrgId())\n                .field(\"secretId\", e.getId())\n                .field(\"name\", e.getName())\n                .log();\n    }\n\n    /**\n     * Removes an existing secret.\n     */\n    public void delete(UUID orgId, String secretName) {\n        secretDao.tx(tx -> delete(tx, orgId, secretName));\n    }\n\n    /**\n     * Decrypts and returns an existing secret.\n     */\n    public DecryptedSecret getSecret(AccessScope accessScope, UUID orgId, String name, String password, SecretType expectedType) {\n        SecretDataEntry e = getRaw(accessScope, orgId, name, password);\n\n        if (expectedType != null && e.getType() != expectedType) {\n            throw new IllegalArgumentException(\"Invalid secret type: \" + name + \", expected \" + expectedType + \", got: \" + e.getType());\n        }\n\n        Secret s = deserialize(e.getType(), e.getData());\n        return new DecryptedSecret(e.getId(), s);\n    }\n\n    private byte[] decryptData(SecretEntryV2 e, String password) {\n        byte[] data = getSecretStore(e.getStoreType()).get(e.getId());\n        if (data == null) {\n            throw new IllegalStateException(\"Can't find the secret's data in the store \" + e.getStoreType() + \" : \" + e.getId());\n        }\n\n        byte[] pwd = getPwd(password);\n        byte[] salt = e.getSecretSalt();\n\n        return SecretUtils.decrypt(data, pwd, salt, e.getHashAlgorithm());\n    }\n\n    /**\n     * Returns a raw (unencrypted) secret value.\n     */\n    public SecretDataEntry getRaw(AccessScope accessScope, UUID orgId, String name, String password) {\n        SecretEntryV2 e = assertAccess(orgId, null, name, ResourceAccessLevel.READER, false);\n        if (e == null) {\n            return null;\n        }\n\n        // getting a decrypted secret requires additional checks in some cases\n        assertProjectScope(accessScope, e);\n\n        SecretEncryptedByType providedEncryptedByType = getEncryptedBy(password);\n        assertEncryptedByType(name, providedEncryptedByType, e.getEncryptedBy());\n\n        byte[] ab = decryptData(e, password);\n\n        if(e.getHashAlgorithm() == HashAlgorithm.LEGACY_MD5) {\n            byte[] newEncryptedData = SecretUtils.encrypt(ab, getPwd(password), e.getSecretSalt(), HashAlgorithm.SHA256);\n            secretDao.update(e.getId(), null, null, null, newEncryptedData, null, null, HashAlgorithm.SHA256);\n        }\n\n        auditLog.add(AuditObject.SECRET, AuditAction.ACCESS)\n                .field(\"orgId\", e.getOrgId())\n                .field(\"secretId\", e.getId())\n                .field(\"name\", e.getName())\n                .field(\"type\", e.getType())\n                .field(\"scope\", accessScope)\n                .log();\n\n        return new SecretDataEntry(e, ab);\n    }\n\n    /**\n     * Decrypts and returns an existing SSH key pair.\n     */\n    public KeyPair getKeyPair(AccessScope accessScope, UUID orgId, String name, String password) {\n        DecryptedSecret e = getSecret(accessScope, orgId, name, password, SecretType.KEY_PAIR);\n        if (e == null) {\n            return null;\n        }\n\n        Secret s = e.getSecret();\n        return (KeyPair) s;\n    }\n\n    /**\n     * Returns a list of secrets for the specified organization.\n     */\n    public List<SecretEntryV2> list(UUID orgId, int offset, int limit, String filter) {\n        UserPrincipal p = UserPrincipal.assertCurrent();\n        UUID userId = p.getId();\n        if (Roles.isAdmin() || Roles.isGlobalReader() || Roles.isGlobalWriter()) {\n            userId = null;\n        }\n\n        return secretDao.list(orgId, userId, SECRETS.SECRET_NAME, true, offset, limit, filter);\n    }\n\n    /**\n     * Updates a secret's access level for the specified team.\n     */\n    public void updateAccessLevel(UUID secretId, UUID teamId, ResourceAccessLevel level) {\n        assertAccess(null, secretId, null, ResourceAccessLevel.OWNER, true);\n        secretDao.upsertAccessLevel(secretId, teamId, level);\n    }\n\n    private UUID create(String name,\n                        UUID orgId,\n                        Set<UUID> projectIds,\n                        Secret s,\n                        String password,\n                        SecretVisibility visibility,\n                        String storeType,\n                        SecretDao.InsertMode insertMode) {\n        return secretDao.txResult(tx -> create(tx, name, orgId, projectIds, s, password, visibility, storeType, insertMode));\n    }\n\n    private UUID create(DSLContext tx,\n                        String name,\n                        UUID orgId,\n                        Set<UUID> projectIds,\n                        Secret s,\n                        String password,\n                        SecretVisibility visibility,\n                        String storeType,\n                        SecretDao.InsertMode insertMode) {\n\n        byte[] data = serialize(s);\n        SecretType type = secretType(s);\n\n        byte[] pwd = getPwd(password);\n        byte[] salt = SecretUtils.generateSalt(SALT_LENGTH);\n\n        byte[] ab = SecretUtils.encrypt(data, pwd, salt, HashAlgorithm.SHA256);\n        SecretEncryptedByType encryptedByType = getEncryptedBy(password);\n\n        storeType = storeType.toLowerCase();\n\n        UserEntry owner = UserPrincipal.assertCurrent().getUser();\n        if(projectIds != null) {\n            for(UUID projectId: projectIds) {\n                policyManager.checkEntity(orgId, projectId, EntityType.SECRET, EntityAction.CREATE, owner,\n                        PolicyUtils.secretToMap(orgId, name, type, visibility, storeType));\n            }\n        } else {\n            policyManager.checkEntity(orgId, null, EntityType.SECRET, EntityAction.CREATE, owner,PolicyUtils.secretToMap(orgId, name, type, visibility, storeType));\n        }\n\n        UUID id = secretDao.insert(tx, orgId, name, owner.getId(), type, encryptedByType, storeType, visibility, salt, HashAlgorithm.SHA256, insertMode);\n        try {\n            getSecretStore(storeType).store(tx, id, ab);\n        } catch (Exception e) {\n            // we can't use the transaction here because the store may update the record in the database independently,\n            // as our transaction has not yet finalized so we may end up having exception in that case\n            secretDao.delete(tx, id);\n            throw new RuntimeException(e);\n        }\n\n        secretDao.updateSecretProjects(tx, id, projectIds);\n\n        auditLog.add(AuditObject.SECRET, AuditAction.CREATE)\n                .field(\"orgId\", orgId)\n                .field(\"secretId\", id)\n                .field(\"type\", type)\n                .field(\"storeType\", storeType)\n                .field(\"name\", name)\n                .log();\n\n        return id;\n    }\n\n    public Collection<SecretStore> getActiveSecretStores() {\n        return secretStoreProvider.getActiveSecretStores();\n    }\n\n    public List<ResourceAccessEntry> getAccessLevel(UUID orgId, String secretName) {\n        assertAccess(orgId, null, secretName, ResourceAccessLevel.READER, false);\n        return secretDao.getAccessLevel(orgId, secretName);\n    }\n\n    public void updateAccessLevel(UUID secretId, Collection<ResourceAccessEntry> entries, boolean isReplace) {\n        assertAccess(null, secretId, null, ResourceAccessLevel.OWNER, true);\n\n        secretDao.tx(tx -> {\n            if (isReplace) {\n                secretDao.deleteTeamAccess(tx, secretId);\n            }\n\n            for (ResourceAccessEntry e : entries) {\n                secretDao.upsertAccessLevel(tx, secretId, e.getTeamId(), e.getLevel());\n            }\n        });\n\n        addAuditLog(secretId, entries, isReplace);\n    }\n\n    private byte[] getPwd(String pwd) {\n        if (pwd == null) {\n            return secretCfg.getServerPwd();\n        }\n        return pwd.getBytes(StandardCharsets.UTF_8);\n    }\n\n    private void assertProjectScope(AccessScope scope, SecretEntryV2 e) {\n        Set<UUID> projectIds = e.getProjects().stream().map(ProjectEntry::getId).collect(Collectors.toSet());\n        if (projectIds.isEmpty()) {\n            return;\n        }\n\n        // currently both the server and the agent access repositories and thus require access to secrets\n        // the agent uses its own API key which is typically a \"globalReader\". That is why we need to check both\n        // \"globalReaders\" and the current session token\n        // TODO create a separate role or move the repository cloning into the runner and use session tokens?\n        UserPrincipal u = UserPrincipal.getCurrent();\n        if (u != null && Roles.isGlobalReader()) {\n            return;\n        }\n\n        if (scope instanceof InternalAccessScope) {\n            return;\n        }\n\n        // internal access within a scope of a project\n        if (scope instanceof ProjectAccessScope) {\n            UUID scopeProjectId = ((ProjectAccessScope) scope).getProjectId();\n            if (projectIds.stream().noneMatch(scopeProjectId::equals)) {\n                throw new UnauthorizedException(\"Project-scoped secrets can only be accessed within the project they belong to. Secret: \" + e.getName());\n            }\n            return;\n        }\n\n        SessionKeyPrincipal session = SessionKeyPrincipal.getCurrent();\n        if (session == null) {\n            throw new UnauthorizedException(\"Project-scoped secrets can only be accessed within a running process. Secret: \" + e.getName());\n        }\n\n        ProcessEntry p = processQueueManager.get(session.getProcessKey());\n        if (p == null) {\n            throw new IllegalStateException(\"Process not found: \" + session.getProcessKey());\n        }\n        if (projectIds.stream().noneMatch(projectId -> projectId.equals(p.projectId()))) {\n            throw new UnauthorizedException(\"Project-scoped secrets can only be accessed within the project they belong to. Secret: \" + e.getName());\n\n        }\n    }\n\n    private UUID validateOrgId(UUID newOrgId, String newOrgName, SecretEntryV2 e) {\n        UUID orgId = null;\n        if (newOrgId != null) {\n            orgId = orgManager.assertAccess(newOrgId, true).getId();\n        } else if (newOrgName != null) {\n            orgId = orgManager.assertAccess(newOrgName, true).getId();\n        }\n\n        if (!e.getOrgId().equals(orgId)) {\n            return orgId;\n        }\n\n        return null;\n    }\n\n    private Set<UUID> validateProjectIds(Set<UUID> newProjectIds, boolean removeProjectLink, UUID newOrgId, SecretEntryV2 e) {\n        if (newOrgId != null) {\n            // set the project ID as null when the updated org ID is not same as the current org ID\n            // when a secret is changing orgs, the project link must be set to null\n            return Collections.emptySet();\n        }\n\n        if (removeProjectLink) {\n            return Collections.emptySet();\n        }\n\n        if (newProjectIds == null || newProjectIds.isEmpty()) {\n            return e.getProjects().stream().map(ProjectEntry::getId).collect(Collectors.toSet());\n        }\n        return newProjectIds.stream().map(newProjectId -> {\n            ProjectEntry entry = projectAccessManager.assertAccess(e.getOrgId(), newProjectId, null, ResourceAccessLevel.READER, true);\n            if (!entry.getOrgId().equals(e.getOrgId())) {\n                throw new ValidationErrorsException(\"Project '\" + entry.getName() + \"' does not belong to organization '\" + e.getOrgName() + \"'\");\n            }\n            return entry.getId();\n        }).collect(Collectors.toSet());\n    }\n\n    private UserEntry validateOwner(UUID newOwnerId, SecretEntryV2 e) {\n        if (newOwnerId == null) {\n            return null;\n        }\n\n        UUID currentOwnerId = e.getOwner() != null ? e.getOwner().id() : null;\n        if (newOwnerId.equals(currentOwnerId)) {\n            return null;\n        }\n\n        UserEntry owner = userManager.get(newOwnerId)\n                .orElseThrow(() -> new ValidationErrorsException(\"User not found: \" + newOwnerId));\n\n        assertAccess(e.getOrgId(), e.getId(), e.getName(), ResourceAccessLevel.OWNER, true);\n\n        return owner;\n    }\n\n    private static byte[] serialize(Secret secret) {\n        if (secret instanceof KeyPair) {\n            return KeyPair.serialize((KeyPair) secret);\n        } else if (secret instanceof UsernamePassword) {\n            return UsernamePassword.serialize((UsernamePassword) secret);\n        } else if (secret instanceof BinaryDataSecret) {\n            return ((BinaryDataSecret) secret).getData();\n        } else {\n            throw new IllegalArgumentException(\"Unknown secret type: \" + secret.getClass());\n        }\n    }\n\n    private static SecretType secretType(Secret secret) {\n        if (secret instanceof KeyPair) {\n            return SecretType.KEY_PAIR;\n        } else if (secret instanceof UsernamePassword) {\n            return SecretType.USERNAME_PASSWORD;\n        } else if (secret instanceof BinaryDataSecret) {\n            return SecretType.DATA;\n        } else {\n            throw new IllegalArgumentException(\"Unknown secret type: \" + secret.getClass());\n        }\n    }\n\n    private static void assertEncryptedByType(String name, SecretEncryptedByType provided, SecretEncryptedByType actual) {\n        if (provided == actual) {\n            return;\n        }\n\n        switch (actual) {\n            case SERVER_KEY: {\n                throw new SecurityException(\"Not a password-protected secret '\" + name + \"'\");\n            }\n            case PASSWORD: {\n                throw new SecurityException(\"The secret '\" + name + \"' requires a password to decrypt\");\n            }\n            default: {\n                throw new IllegalArgumentException(\"Unsupported secret encrypted by type: \" + actual);\n            }\n        }\n    }\n\n    private static Secret deserialize(SecretType type, byte[] data) {\n        Function<byte[], ? extends Secret> deserializer;\n        switch (type) {\n            case KEY_PAIR:\n                deserializer = KeyPair::deserialize;\n                break;\n            case USERNAME_PASSWORD:\n                deserializer = UsernamePassword::deserialize;\n                break;\n            case DATA:\n                deserializer = BinaryDataSecret::new;\n                break;\n            default:\n                throw new IllegalArgumentException(\"Unknown secret type: \" + type);\n        }\n        return deserializer.apply(data);\n    }\n\n    private static SecretEncryptedByType getEncryptedBy(String pwd) {\n        if (pwd == null) {\n            return SecretEncryptedByType.SERVER_KEY;\n        }\n        return SecretEncryptedByType.PASSWORD;\n    }\n\n    private SecretStore getSecretStore(String type) {\n        return secretStoreProvider.getSecretStore(type);\n    }\n\n    public String getDefaultSecretStoreType() {\n        return secretStoreProvider.getDefaultStoreType();\n    }\n\n    public String getSecretName(UUID secretId) {\n        return secretDao.getName(secretId);\n    }\n\n    public static class DecryptedSecret {\n\n        private final UUID id;\n        private final Secret secret;\n\n        public DecryptedSecret(UUID id, Secret secret) {\n            this.id = id;\n            this.secret = secret;\n        }\n\n        public UUID getId() {\n            return id;\n        }\n\n        public Secret getSecret() {\n            return secret;\n        }\n    }\n\n    public static class DecryptedKeyPair {\n\n        private final UUID id;\n        private final byte[] data;\n\n        public DecryptedKeyPair(UUID id, byte[] data) { // NOSONAR\n            this.id = id;\n            this.data = data;\n        }\n\n        public UUID getId() {\n            return id;\n        }\n\n        public byte[] getData() {\n            return data;\n        }\n    }\n\n    public static class DecryptedUsernamePassword {\n\n        private final UUID id;\n\n        public DecryptedUsernamePassword(UUID id) {\n            this.id = id;\n        }\n\n        public UUID getId() {\n            return id;\n        }\n    }\n\n    public static class DecryptedBinaryData {\n\n        private final UUID id;\n\n        public DecryptedBinaryData(UUID id) {\n            this.id = id;\n        }\n\n        public UUID getId() {\n            return id;\n        }\n    }\n\n    private void addAuditLog(UUID secretId, Collection<ResourceAccessEntry> entries, boolean isReplace) {\n        List<ImmutableMap<String, ? extends Serializable>> teams = entries.stream()\n                .map(e -> ImmutableMap.of(\"id\", e.getTeamId(), \"level\", e.getLevel()))\n                .collect(Collectors.toList());\n\n        auditLog.add(AuditObject.SECRET, AuditAction.UPDATE)\n                .field(\"secretId\", secretId)\n                .field(\"access\", ImmutableMap.of(\n                        \"replace\", isReplace,\n                        \"teams\", teams))\n                .log();\n    }\n\n    /**\n     * Scope in which access to secrets is performed. Some scopes require additional security checks.\n     */\n    public static abstract class AccessScope implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        /**\n         * External access via API. Requires additional security checks.\n         */\n        public static AccessScope apiRequest() {\n            return new ApiAccessScope();\n        }\n\n        /**\n         * Internal access. The server requires a secret for some internal operations related to a\n         * specific project (e.g. repository cloning).\n         */\n        public static AccessScope project(UUID projectId) {\n            return new ProjectAccessScope(projectId);\n        }\n\n        /**\n         * Generic internal access. Should be used sparingly.\n         */\n        public static AccessScope internal() {\n            return new InternalAccessScope();\n        }\n\n        protected AccessScope() {\n        }\n\n        public abstract String getName();\n    }\n\n\n    private static class ApiAccessScope extends AccessScope {\n\n        private static final long serialVersionUID = 1L;\n\n        @Override\n        public String getName() {\n            return \"apiAccess\";\n        }\n    }\n\n    public static class ProjectAccessScope extends AccessScope {\n\n        private static final long serialVersionUID = 1L;\n\n        private final UUID projectId;\n\n        public ProjectAccessScope(UUID projectId) {\n            super();\n            this.projectId = projectId;\n        }\n\n        public UUID getProjectId() {\n            return projectId;\n        }\n\n        @Override\n        public String getName() {\n            return \"projectAccess\";\n        }\n    }\n\n    public static class InternalAccessScope extends AccessScope {\n\n        private static final long serialVersionUID = 1L;\n\n        @Override\n        public String getName() {\n            return \"internal\";\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/SecretModule.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.walmartlabs.concord.server.org.secret.provider.SecretStoreProvider;\nimport com.walmartlabs.concord.server.org.secret.store.SecretStore;\nimport com.walmartlabs.concord.server.org.secret.store.concord.ConcordSecretStore;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\nimport static com.walmartlabs.concord.server.Utils.bindExceptionMapper;\nimport static com.walmartlabs.concord.server.Utils.bindJaxRsResource;\n\npublic class SecretModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        binder.bind(SecretManager.class).in(SINGLETON);\n        binder.bind(SecretDao.class).in(SINGLETON);\n        binder.bind(SecretStoreProvider.class).in(SINGLETON);\n\n        newSetBinder(binder, SecretStore.class).addBinding().to(ConcordSecretStore.class);\n\n        bindJaxRsResource(binder, SecretStoreResource.class);\n        bindJaxRsResource(binder, SecretResource.class);\n        bindJaxRsResource(binder, SecretResourceV2.class);\n\n        bindExceptionMapper(binder, SecretExceptionMapper.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/SecretOperationResponse.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.server.OperationResult;\n\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@JsonInclude(Include.NON_NULL)\n@JsonIgnoreProperties(ignoreUnknown = true)\npublic class SecretOperationResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n    private final UUID id;\n    private final OperationResult result;\n    private final String password;\n\n    @JsonCreator\n    public SecretOperationResponse(@JsonProperty(\"id\") UUID id,\n                                   @JsonProperty(\"result\") OperationResult result,\n                                   @JsonProperty(\"password\") String password) {\n        this.id = id;\n        this.result = result;\n        this.password = password;\n    }\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public OperationResult getResult() {\n        return result;\n    }\n\n    public String getPassword() {\n        return password;\n    }\n\n    @Override\n    public String toString() {\n        return \"SecretOperationResponse{\" +\n                \"ok=\" + ok +\n                \", id=\" + id +\n                \", result=\" + result +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/SecretResource.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.GenericOperationResult;\nimport com.walmartlabs.concord.server.MultipartUtils;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.org.*;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.org.secret.SecretManager.DecryptedKeyPair;\nimport com.walmartlabs.concord.server.org.team.TeamDao;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport com.walmartlabs.concord.server.user.UserType;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.Parameter;\nimport io.swagger.v3.oas.annotations.headers.Header;\nimport io.swagger.v3.oas.annotations.media.Content;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport io.swagger.v3.oas.annotations.responses.ApiResponse;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.jboss.resteasy.plugins.providers.multipart.MultipartInput;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.validation.Valid;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport javax.ws.rs.core.Response.Status;\nimport javax.ws.rs.core.StreamingOutput;\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\n@Path(\"/api/v1/org\")\n@Tag(name = \"Secrets\")\npublic class SecretResource implements Resource {\n\n    private static final Logger log = LoggerFactory.getLogger(SecretResource.class);\n\n    private final OrganizationManager orgManager;\n    private final OrganizationDao orgDao;\n    private final SecretManager secretManager;\n    private final SecretDao secretDao;\n    private final TeamDao teamDao;\n    private final ProjectDao projectDao;\n    private final UserManager userManager;\n\n    @Inject\n    public SecretResource(OrganizationManager orgManager,\n                          OrganizationDao orgDao,\n                          SecretManager secretManager,\n                          SecretDao secretDao,\n                          TeamDao teamDao,\n                          ProjectDao projectDao,\n                          UserManager userManager) {\n\n        this.orgManager = orgManager;\n        this.orgDao = orgDao;\n        this.secretManager = secretManager;\n        this.secretDao = secretDao;\n        this.teamDao = teamDao;\n        this.projectDao = projectDao;\n        this.userManager = userManager;\n    }\n\n    @POST\n    @Path(\"/{orgName}/secret\")\n    @Consumes(MediaType.MULTIPART_FORM_DATA)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Create secret\", operationId = \"createSecret\")\n    public SecretOperationResponse create(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                          @Parameter(schema = @Schema(type = \"object\", implementation = Object.class)) MultipartInput input) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n\n        try {\n            SecretType type = SecretResourceUtils.assertType(input);\n            String storeType = SecretResourceUtils.assertStoreType(secretManager, input);\n\n            String name = SecretResourceUtils.assertName(input);\n            SecretResourceUtils.assertUnique(secretDao, org.getId(), name);\n\n            boolean generatePwd = MultipartUtils.getBoolean(input, Constants.Multipart.GENERATE_PASSWORD, false);\n            String storePwd = SecretResourceUtils.getOrGenerateStorePassword(input, generatePwd);\n            SecretVisibility visibility = SecretResourceUtils.getVisibility(input);\n\n            Set<UUID> projectIds = getProjectIds(\n                    org.getId(),\n                    MultipartUtils.getUUIDList(input, Constants.Multipart.PROJECT_IDS),\n                    MultipartUtils.getStringList(input, Constants.Multipart.PROJECT_NAMES),\n                    MultipartUtils.getUuid(input, Constants.Multipart.PROJECT_ID),\n                    MultipartUtils.getString(input, Constants.Multipart.PROJECT_NAME)\n            );\n            switch (type) {\n                case KEY_PAIR: {\n                    return SecretResourceUtils.createKeyPair(secretManager, org.getId(), projectIds, name, storePwd, visibility, input, storeType);\n                }\n                case USERNAME_PASSWORD: {\n                    return SecretResourceUtils.createUsernamePassword(secretManager, org.getId(), projectIds, name, storePwd, visibility, input, storeType);\n                }\n                case DATA: {\n                    return SecretResourceUtils.createData(secretManager, org.getId(), projectIds, name, storePwd, visibility, input, storeType);\n                }\n                default:\n                    throw new ValidationErrorsException(\"Unsupported secret type: \" + type);\n            }\n        } catch (IOException e) {\n            throw new ConcordApplicationException(\"Error while processing the request: \" + e.getMessage(), e);\n        } finally {\n            input.close();\n        }\n    }\n\n\n    @POST\n    @Path(\"/{orgName}/secret/{secretName}\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Update secret\", operationId = \"updateSecretV1\")\n    public GenericOperationResult update(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                         @PathParam(\"secretName\") @ConcordKey String secretName,\n                                         @Valid SecretUpdateRequest req) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n\n        try {\n            UUID projectId = getProject(req.orgId(), req.projectId(), req.projectName());\n            SecretUpdateParams newSecretParams = SecretUpdateParams.builder()\n                    .newOrgId(req.orgId())\n                    .newOrgName(req.orgName())\n                    .newProjectIds(projectId == null ? null : Collections.singletonList(projectId))\n                    .removeProjectLink(req.projectName() != null && req.projectName().trim().isEmpty())\n                    .newOwnerId(getOwnerId(req.owner()))\n                    .currentPassword(req.storePassword())\n                    .newPassword(req.newStorePassword())\n                    .newSecret(req.data() != null ? secretManager.buildBinaryData(new ByteArrayInputStream(Objects.requireNonNull(req.data()))) : null)\n                    .newName(req.name())\n                    .newVisibility(req.visibility())\n                    .build();\n\n            secretManager.update(org.getId(), secretName, newSecretParams);\n        } catch (IOException e) {\n            throw new ConcordApplicationException(\"Error while processing the request: \" + e.getMessage(), e);\n        }\n\n        return new GenericOperationResult(OperationResult.UPDATED);\n    }\n\n    @GET\n    @Path(\"/{orgName}/secret/{secretName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Deprecated\n    public SecretEntryV2 get(@PathParam(\"orgName\") @ConcordKey String orgName,\n                             @PathParam(\"secretName\") @ConcordKey String secretName) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n        return secretManager.assertAccess(org.getId(), null, secretName, ResourceAccessLevel.READER, false);\n    }\n\n    @POST\n    @Path(\"/{orgName}/secret/{secretName}/data\")\n    @Consumes(MediaType.MULTIPART_FORM_DATA)\n    @Produces(MediaType.APPLICATION_OCTET_STREAM)\n    @WithTimer\n    @Operation(description = \"Get an existing secret's data\", operationId = \"getSecretData\")\n    @ApiResponse(description = \"Secret content\",\n            headers = @Header(name = Constants.Headers.SECRET_TYPE, schema = @Schema(type = \"string\")),\n            content = @Content(mediaType = MediaType.APPLICATION_OCTET_STREAM,\n                    schema = @Schema(type = \"string\", format = \"binary\"))\n    )\n    public Response getData(@PathParam(\"orgName\") @ConcordKey String orgName,\n                            @PathParam(\"secretName\") @ConcordKey String secretName,\n                            @Parameter(schema = @Schema(type = \"object\", implementation = GetDataRequest.class)) MultipartInput input) {\n\n        try {\n            GetDataRequest request = GetDataRequest.from(input);\n\n            OrganizationEntry org = orgManager.assertAccess(orgName, false);\n            String password = request.getPassword();\n\n            SecretDao.SecretDataEntry entry;\n            try {\n                entry = secretManager.getRaw(SecretManager.AccessScope.apiRequest(), org.getId(), secretName, password);\n                if (entry == null) {\n                    throw new WebApplicationException(\"Secret not found: \" + secretName, Status.NOT_FOUND);\n                }\n            } catch (SecurityException e) {\n                log.warn(\"fetchSecret -> error: {}\", e.getMessage());\n                throw new SecretException(\"Error while fetching a secret '\" + secretName + \"': \" + e.getMessage());\n            } catch (ValidationErrorsException e) {\n                log.warn(\"fetchSecret -> error: {}\", e.getMessage());\n                return null;\n            }\n\n            try {\n                return Response.ok((StreamingOutput) output -> output.write(entry.getData()),\n                                MediaType.APPLICATION_OCTET_STREAM)\n                        .header(Constants.Headers.SECRET_TYPE, entry.getType().name())\n                        .build();\n            } catch (Exception e) {\n                log.error(\"fetchSecret ['{}'] -> error while fetching a secret\", secretName, e);\n                throw new ConcordApplicationException(\"Error while fetching a secret '\" + secretName + \"': \" + e.getMessage());\n            }\n        } finally {\n            input.close();\n        }\n    }\n\n    @GET\n    @Path(\"/{orgName}/secret/{secretName}/public\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @WithTimer\n    @Operation(description = \"Retrieves the public key of a key pair\")\n    public PublicKeyResponse getPublicKey(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                          @PathParam(\"secretName\") @ConcordKey String secretName) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n        try {\n            DecryptedKeyPair k = secretManager.getKeyPair(SecretManager.AccessScope.internal(), org.getId(), secretName);\n            return new PublicKeyResponse(k.getId(), null, null, new String(k.getData()));\n        } catch (SecurityException | IllegalArgumentException e) {\n            log.warn(\"getPublicKey -> error: {}\", e.getMessage());\n            throw new SecretException(\"Error while fetching a secret '\" + secretName + \"': \" + e.getMessage());\n        }\n    }\n\n    @GET\n    @Path(\"/{orgName}/secret\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Deprecated\n    public List<SecretEntryV2> list(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                    @QueryParam(\"offset\") int offset,\n                                    @QueryParam(\"limit\") int limit,\n                                    @QueryParam(\"filter\") String filter) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n        return secretManager.list(org.getId(), offset, limit, filter);\n    }\n\n    @DELETE\n    @Path(\"/{orgName}/secret/{secretName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Delete an existing secret\")\n    public GenericOperationResult delete(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                         @PathParam(\"secretName\") @ConcordKey String secretName) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n        secretManager.delete(org.getId(), secretName);\n        return new GenericOperationResult(OperationResult.DELETED);\n    }\n\n    @POST\n    @Path(\"/{orgName}/secret/{secretName}/access\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Updates the access level for the specified secret and team\", operationId = \"updateSecretAccessLevel\")\n    public GenericOperationResult updateAccessLevel(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                    @PathParam(\"secretName\") @ConcordKey String secretName,\n                                                    @Valid ResourceAccessEntry entry) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n\n        UUID secretId = secretDao.getId(org.getId(), secretName);\n        if (secretId == null) {\n            throw new ConcordApplicationException(\"Secret not found: \" + secretName, Status.NOT_FOUND);\n        }\n\n        UUID teamId = ResourceAccessUtils.getTeamId(orgDao, teamDao, org.getId(), entry);\n\n        secretManager.updateAccessLevel(secretId, teamId, entry.getLevel());\n        return new GenericOperationResult(OperationResult.UPDATED);\n    }\n\n    @GET\n    @Path(\"/{orgName}/secret/{secretName}/access\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"Get secret team access\", operationId = \"getSecretAccessLevel\")\n    public List<ResourceAccessEntry> getAccessLevel(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                    @PathParam(\"secretName\") @ConcordKey String secretName) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n        return secretManager.getAccessLevel(org.getId(), secretName);\n    }\n\n    @POST\n    @Path(\"/{orgName}/secret/{secretName}/access/bulk\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Updates the access level for the specified secret and team\", operationId = \"updateSecretAccessLevelBulk\")\n    public GenericOperationResult updateAccessLevel(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                    @PathParam(\"secretName\") @ConcordKey String secretName,\n                                                    @Valid Collection<ResourceAccessEntry> entries) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n\n        UUID secretId = secretDao.getId(org.getId(), secretName);\n        if (secretId == null) {\n            throw new ConcordApplicationException(\"Secret not found: \" + secretName, Status.NOT_FOUND);\n        }\n\n        if (entries == null) {\n            throw new ConcordApplicationException(\"List of teams is null.\", Status.BAD_REQUEST);\n        }\n\n        secretManager.updateAccessLevel(secretId, entries, true);\n\n        return new GenericOperationResult(OperationResult.UPDATED);\n    }\n\n    private Set<UUID> getProjectIds(UUID orgId, List<UUID> projectIds, List<String> projectNames, UUID projectId, String projectName) {\n        if (projectIds == null || projectIds.isEmpty()) {\n            if (projectNames != null && !projectNames.isEmpty()) {\n                projectIds = projectNames.stream().map(name -> getProjectIdFromName(orgId, name)).collect(Collectors.toList());\n            } else {\n                if (projectId != null) {\n                    projectIds = Collections.singletonList(projectId);\n                } else if (projectName != null) {\n                    projectIds = Collections.singletonList(getProjectIdFromName(orgId, projectName));\n                }\n            }\n        }\n        return (projectIds == null) ? null : new HashSet<>(projectIds.stream().filter(Objects::nonNull).collect(Collectors.toSet()));\n    }\n\n    private UUID getProjectIdFromName(UUID orgId, String projectName) {\n        UUID id = projectDao.getId(orgId, projectName);\n        if (id == null) {\n            throw new ValidationErrorsException(\"Project not found: \" + projectName);\n        }\n        return id;\n    }\n\n    private UUID getProject(UUID orgId, UUID id, String name) {\n        if (id == null && (name != null && !name.trim().isEmpty())) {\n            id = projectDao.getId(orgId, name);\n            if (id == null) {\n                throw new ValidationErrorsException(\"Project not found: \" + name);\n            }\n        }\n        return id;\n    }\n\n    private UUID getOwnerId(EntityOwner owner) {\n        if (owner == null) {\n            return null;\n        }\n\n        if (owner.id() != null) {\n            return userManager.get(owner.id())\n                    .orElseThrow(() -> new ValidationErrorsException(\"User not found: \" + owner.id()))\n                    .getId();\n        }\n\n        if (owner.username() != null) {\n            // TODO don't assume LDAP here\n            return userManager.get(owner.username(), owner.userDomain(), UserType.LDAP)\n                    .orElseThrow(() -> new ConcordApplicationException(\"User not found: \" + owner.username()))\n                    .getId();\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/SecretResourceUtils.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.MultipartUtils;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport org.jboss.resteasy.plugins.providers.multipart.MultipartInput;\n\nimport javax.ws.rs.WebApplicationException;\nimport javax.ws.rs.core.Response;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.Set;\nimport java.util.UUID;\n\npublic final class SecretResourceUtils {\n\n    public static PublicKeyResponse createKeyPair(SecretManager secretManager, UUID orgId, Set<UUID> projectIds, String name, String storePassword, SecretVisibility visibility, MultipartInput input, String storeType) throws IOException {\n        SecretManager.DecryptedKeyPair k;\n\n        InputStream publicKey = MultipartUtils.getStream(input, Constants.Multipart.PUBLIC);\n        if (publicKey != null) {\n            InputStream privateKey = MultipartUtils.assertStream(input, Constants.Multipart.PRIVATE);\n            try {\n                k = secretManager.createKeyPair(orgId, projectIds, name, storePassword, publicKey, privateKey, visibility, storeType);\n            } catch (IllegalArgumentException e) {\n                throw new ValidationErrorsException(e.getMessage());\n            }\n        } else {\n            k = secretManager.createKeyPair(orgId, projectIds, name, storePassword, visibility, storeType);\n        }\n\n        return new PublicKeyResponse(k.getId(), OperationResult.CREATED, storePassword, new String(k.getData()));\n    }\n\n    public static SecretOperationResponse createUsernamePassword(SecretManager secretManager, UUID orgId,  Set<UUID> projectIds, String name, String storePassword,\n                                                           SecretVisibility visibility, MultipartInput input,\n                                                           String storeType) {\n\n        String username = MultipartUtils.assertString(input, Constants.Multipart.USERNAME);\n        String password = MultipartUtils.assertString(input, Constants.Multipart.PASSWORD);\n\n        SecretManager.DecryptedUsernamePassword e = secretManager.createUsernamePassword(orgId, projectIds, name, storePassword, username, password.toCharArray(), visibility, storeType);\n        return new SecretOperationResponse(e.getId(), OperationResult.CREATED, storePassword);\n    }\n\n    public static SecretOperationResponse createData(SecretManager secretManager, UUID orgId,  Set<UUID> projectIds, String name, String storePassword,\n                                               SecretVisibility visibility, MultipartInput input,\n                                               String storeType) throws IOException {\n\n        InputStream data = MultipartUtils.assertStream(input, Constants.Multipart.DATA);\n        SecretManager.DecryptedBinaryData e = secretManager.createBinaryData(orgId, projectIds, name, storePassword, data, visibility, storeType);\n        return new SecretOperationResponse(e.getId(), OperationResult.CREATED, storePassword);\n    }\n\n\n\n    public static String assertName(MultipartInput input) {\n        String s = MultipartUtils.assertString(input, Constants.Multipart.NAME);\n        if (s.trim().isEmpty()) {\n            throw new ValidationErrorsException(\"'name' is required\");\n        }\n\n        if (!s.matches(ConcordKey.PATTERN)) {\n            throw new ValidationErrorsException(\"Invalid secret name: \" + s + \". \" + ConcordKey.MESSAGE);\n        }\n\n        return s;\n    }\n\n    public static SecretType assertType(MultipartInput input) {\n        SecretType type = getType(input);\n        if (type == null) {\n            throw new ValidationErrorsException(\"'type' is required\");\n        }\n        return type;\n    }\n\n    public static SecretType getType(MultipartInput input) {\n        String s = MultipartUtils.getString(input, Constants.Multipart.TYPE);\n        if (s == null) {\n            return null;\n        }\n\n        try {\n            return SecretType.valueOf(s.toUpperCase());\n        } catch (IllegalArgumentException e) {\n            throw new ValidationErrorsException(\"Unsupported secret type: \" + s);\n        }\n    }\n\n    public static String assertStoreType(SecretManager secretManager, MultipartInput input) {\n        String s = MultipartUtils.getString(input, Constants.Multipart.STORE_TYPE);\n        if (s == null) {\n            return secretManager.getDefaultSecretStoreType();\n        }\n\n        // check if the given secret source type is enabled or not\n        boolean isStoreActive = secretManager.getActiveSecretStores().stream()\n                .anyMatch(store -> store.getType().equalsIgnoreCase(s));\n\n        if (!isStoreActive) {\n            throw new ValidationErrorsException(\"Secret store of type \" + s + \" is not available!\");\n        }\n\n        return s;\n    }\n\n    public static SecretVisibility getVisibility(MultipartInput input) {\n        String s = MultipartUtils.getString(input, Constants.Multipart.VISIBILITY);\n        if (s == null) {\n            return SecretVisibility.PUBLIC;\n        }\n\n        try {\n            return SecretVisibility.valueOf(s);\n        } catch (IllegalArgumentException e) {\n            throw new ConcordApplicationException(\"Invalid visibility value: \" + s, Response.Status.BAD_REQUEST);\n        }\n    }\n\n    public static String getOrGenerateStorePassword(MultipartInput input, boolean generatePassword) {\n        String password;\n        try {\n            password = MultipartUtils.getString(input, Constants.Multipart.STORE_PASSWORD);\n        } catch (WebApplicationException e) {\n            throw new ConcordApplicationException(\"Can't get a password from the request\", e);\n        }\n\n        if (password != null) {\n            try {\n                PasswordChecker.check(password);\n            } catch (PasswordChecker.CheckerException e) {\n                throw new ConcordApplicationException(\"Invalid password: \" + e.getMessage(), Response.Status.BAD_REQUEST);\n            }\n        }\n\n        if (password == null && generatePassword) {\n            return PasswordGenerator.generate();\n        }\n\n        return password;\n    }\n\n    public static void assertUnique(SecretDao secretDao, UUID orgId, String name) {\n        if (secretDao.getId(orgId, name) != null) {\n            throw new ValidationErrorsException(\"Secret already exists: \" + name);\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/SecretResourceV2.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.Secret;\nimport com.walmartlabs.concord.server.GenericOperationResult;\nimport com.walmartlabs.concord.server.MultipartUtils;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.org.OrganizationEntry;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.Parameter;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.jboss.resteasy.plugins.providers.multipart.MultipartInput;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\n@Path(\"/api/v2/org\")\n@Tag(name = \"SecretsV2\")\npublic class SecretResourceV2 implements Resource {\n\n    private final OrganizationManager orgManager;\n    private final SecretManager secretManager;\n    private final ProjectDao projectDao;\n\n    @Inject\n    public SecretResourceV2(OrganizationManager orgManager,\n                            SecretManager secretManager,\n                            ProjectDao projectDao,\n                            SecretDao secretDao) {\n\n        this.orgManager = orgManager;\n        this.secretManager = secretManager;\n        this.projectDao = projectDao;\n    }\n\n    @GET\n    @Path(\"/{orgName}/secret/{secretName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"Get an existing secret\", operationId = \"getSecret\")\n    public SecretEntryV2 get(@PathParam(\"orgName\") @ConcordKey String orgName,\n                             @PathParam(\"secretName\") @ConcordKey String secretName) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n        return secretManager.assertAccess(org.getId(), null, secretName, ResourceAccessLevel.READER, false);\n    }\n\n\n    @GET\n    @Path(\"/{orgName}/secret\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"List secrets\", operationId = \"listSecrets\")\n    public List<SecretEntryV2> list(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                    @QueryParam(\"offset\") int offset,\n                                    @QueryParam(\"limit\") int limit,\n                                    @QueryParam(\"filter\") String filter) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n        return secretManager.list(org.getId(), offset, limit, filter);\n    }\n\n    @POST\n    @Path(\"/{orgName}/secret/{secretName}\")\n    @Consumes(MediaType.MULTIPART_FORM_DATA)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Updates an existing secret\", operationId = \"updateSecret\")\n    public GenericOperationResult update(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                         @PathParam(\"secretName\") @ConcordKey String secretName,\n                                         @Parameter(schema = @Schema(type = \"object\", implementation = Object.class)) MultipartInput input) {\n\n        try {\n            OrganizationEntry org = orgManager.assertAccess(orgName, true);\n            Set<UUID> projectIds = getProjectIds(\n                    org.getId(),\n                    MultipartUtils.getUUIDList(input, Constants.Multipart.PROJECT_IDS),\n                    MultipartUtils.getStringList(input, Constants.Multipart.PROJECT_NAMES),\n                    MultipartUtils.getUuid(input, Constants.Multipart.PROJECT_ID),\n                    MultipartUtils.getString(input, Constants.Multipart.PROJECT_NAME)\n            );\n            try {\n                SecretUpdateParams newSecretParams = SecretUpdateParams.builder()\n                        .newOrgId(MultipartUtils.getUuid(input, Constants.Multipart.ORG_ID))\n                        .newOrgName(MultipartUtils.getString(input, Constants.Multipart.ORG_NAME))\n                        .newProjectIds(projectIds)\n                        .removeProjectLink(MultipartUtils.getBoolean(input, \"removeProjectLink\", false))\n                        .newOwnerId(MultipartUtils.getUuid(input, \"ownerId\"))\n                        .currentPassword(MultipartUtils.getString(input, Constants.Multipart.STORE_PASSWORD))\n                        .newPassword(MultipartUtils.getString(input, Constants.Multipart.NEW_STORE_PASSWORD))\n                        .newSecret(buildSecret(input))\n                        .newName(MultipartUtils.getString(input, Constants.Multipart.NAME))\n                        .newVisibility(SecretResourceUtils.getVisibility(input))\n                        .build();\n\n                secretManager.update(org.getId(), secretName, newSecretParams);\n            } catch (IOException e) {\n                throw new ConcordApplicationException(\"Error while processing the request: \" + e.getMessage(), e);\n            }\n\n            return new GenericOperationResult(OperationResult.UPDATED);\n        } finally {\n            input.close();\n        }\n    }\n\n    public Secret buildSecret(MultipartInput input) throws IOException {\n        SecretType type = SecretResourceUtils.getType(input);\n        if (type == null) {\n            return null;\n        }\n\n        switch (type) {\n            case KEY_PAIR: {\n                InputStream publicKey = MultipartUtils.assertStream(input, Constants.Multipart.PUBLIC);\n                InputStream privateKey = MultipartUtils.assertStream(input, Constants.Multipart.PRIVATE);\n                return secretManager.buildKeyPair(publicKey, privateKey);\n            }\n            case USERNAME_PASSWORD: {\n                String username = MultipartUtils.assertString(input, Constants.Multipart.USERNAME);\n                String password = MultipartUtils.assertString(input, Constants.Multipart.PASSWORD);\n                return secretManager.buildUsernamePassword(username, password.toCharArray());\n            }\n            case DATA: {\n                InputStream data = MultipartUtils.assertStream(input, Constants.Multipart.DATA);\n                return secretManager.buildBinaryData(data);\n            }\n            default:\n                throw new ValidationErrorsException(\"Unsupported secret type: \" + type);\n        }\n    }\n\n    private Set<UUID> getProjectIds(UUID orgId, List<UUID> projectIds, List<String> projectNames, UUID projectId, String projectName) {\n        if (projectIds == null || projectIds.isEmpty()) {\n            if (projectNames != null && !projectNames.isEmpty()) {\n                projectIds = projectNames.stream().map(name -> getProjectIdFromName(orgId, name)).collect(Collectors.toList());\n            } else {\n                if (projectId != null) {\n                    projectIds = Collections.singletonList(projectId);\n                } else if (projectName != null) {\n                    projectIds = Collections.singletonList(getProjectIdFromName(orgId, projectName));\n                }\n            }\n        }\n        return (projectIds == null) ? null : new HashSet<>(projectIds.stream().filter(Objects::nonNull).collect(Collectors.toSet()));\n    }\n\n    private UUID getProjectIdFromName(UUID orgId, String projectName) {\n        UUID id = projectDao.getId(orgId, projectName);\n        if (id == null) {\n            throw new ValidationErrorsException(\"Project not found: \" + projectName);\n        }\n        return id;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/SecretStoreEntry.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport javax.validation.constraints.NotNull;\nimport java.io.Serializable;\n\n@JsonInclude(Include.NON_NULL)\npublic class SecretStoreEntry implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @NotNull\n    private final String storeType;\n    @NotNull\n    private final String description;\n\n    @JsonCreator\n    public SecretStoreEntry(@JsonProperty(\"storeType\") String storeType,\n                            @JsonProperty(\"description\") String description) {\n\n        this.storeType = storeType;\n        this.description = description;\n    }\n\n    public String getStoreType() {\n        return storeType;\n    }\n\n    public String getDescription() {\n        return description;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/SecretStoreResource.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.GET;\nimport javax.ws.rs.Path;\nimport javax.ws.rs.Produces;\nimport javax.ws.rs.core.MediaType;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n@Path(\"/api/v1/secret/store\")\n@Tag(name = \"Secret stores\")\npublic class SecretStoreResource implements Resource {\n\n    private final SecretManager secretManager;\n\n    @Inject\n    public SecretStoreResource(SecretManager secretManager) {\n        this.secretManager = secretManager;\n    }\n\n    @GET\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"List of active secret stores\")\n    public List<SecretStoreEntry> listActiveStores() {\n        return secretManager.getActiveSecretStores().stream()\n                .map(s -> new SecretStoreEntry(s.getType(), s.getDescription()))\n                .collect(Collectors.toList());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/SecretType.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic enum SecretType {\n\n    /**\n     * A SSH key pair.\n     */\n    KEY_PAIR,\n\n    /**\n     * An username and a password.\n     */\n    USERNAME_PASSWORD,\n\n    /**\n     * Binary data.\n     */\n    DATA\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/SecretUpdateParams.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Secret;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.Set;\nimport java.util.UUID;\n\n@Value.Immutable\npublic interface SecretUpdateParams extends Serializable {\n\n    @Nullable\n    UUID newOrgId();\n\n    @Nullable\n    String newOrgName();\n\n    @Nullable\n    Set<UUID> newProjectIds();\n\n    @Nullable\n    @Deprecated\n    UUID newProjectId();\n\n    @Nullable\n    @Deprecated\n    String newProjectName();\n\n    @Value.Default\n    default boolean removeProjectLink() {\n        return false;\n    }\n\n    @Nullable\n    UUID newOwnerId();\n\n    @Nullable\n    String currentPassword();\n\n    @Nullable\n    String newPassword();\n\n    @Nullable\n    Secret newSecret();\n\n    @Nullable\n    String newName();\n\n    @Nullable\n    SecretVisibility newVisibility();\n\n    static ImmutableSecretUpdateParams.Builder builder() {\n        return ImmutableSecretUpdateParams.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/SecretUpdateRequest.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.server.org.EntityOwner;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableSecretUpdateRequest.class)\n@JsonDeserialize(as = ImmutableSecretUpdateRequest.class)\npublic interface SecretUpdateRequest extends Serializable {\n\n    @Nullable\n    UUID id();\n\n    @Nullable\n    @ConcordKey\n    String name();\n\n    @Nullable\n    EntityOwner owner();\n\n    @Nullable\n    SecretVisibility visibility();\n\n    @Nullable\n    String storePassword();\n\n    @Nullable\n    String newStorePassword();\n\n    @Nullable\n    String projectName();\n\n    @Nullable\n    UUID projectId();\n\n    @Nullable\n    UUID orgId();\n\n    @Nullable\n    String orgName();\n\n    @Nullable\n    @Schema(type = \"string\")\n    byte[] data();\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/SecretVisibility.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic enum SecretVisibility {\n\n    PUBLIC,\n    PRIVATE\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/provider/SecretStoreProvider.java",
    "content": "package com.walmartlabs.concord.server.org.secret.provider;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.org.secret.store.SecretStore;\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.inject.Inject;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Set;\n\npublic class SecretStoreProvider {\n\n    private final Set<SecretStore> stores;\n    private final int maxSecretDataSize;\n    private final String defaultSecretStoreType;\n\n    @Inject\n    public SecretStoreProvider(Set<SecretStore> stores,\n                               @Config(\"secretStore.maxSecretDataSize\") int maxSecretDataSize,\n                               @Config(\"secretStore.default\") String defaultStore) {\n        this.stores = stores;\n        this.maxSecretDataSize = maxSecretDataSize;\n        this.defaultSecretStoreType = defaultStore;\n    }\n\n    public SecretStore getSecretStore(String secretSourceType) {\n        for (SecretStore secretStore : stores) {\n            if (secretStore.getType().equalsIgnoreCase(secretSourceType)) {\n                if (secretStore.isEnabled()) {\n                    return secretStore;\n                }\n\n                throw new IllegalArgumentException(\"Secret store of type \" + secretStore.getType() + \" is not enabled!\");\n            }\n        }\n\n        throw new IllegalArgumentException(\"Secret store of type \" + secretSourceType + \" is not found!\");\n    }\n\n    public Collection<SecretStore> getActiveSecretStores() {\n        Collection<SecretStore> activeStores = new ArrayList<>();\n\n        for (SecretStore secretStore : stores) {\n            if (secretStore.isEnabled()) {\n                activeStores.add(secretStore);\n            }\n        }\n\n        return activeStores;\n    }\n\n    public String getDefaultStoreType() {\n        return defaultSecretStoreType;\n    }\n\n    public int getMaxSecretDataSize() {\n        return maxSecretDataSize;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/store/SecretStore.java",
    "content": "package com.walmartlabs.concord.server.org.secret.store;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.jooq.DSLContext;\n\nimport java.util.UUID;\n\npublic interface SecretStore {\n\n    boolean isEnabled();\n\n    void store(DSLContext tx, UUID id, byte[] data);\n\n    void delete(DSLContext tx, UUID id);\n\n    byte[] get(UUID id);\n\n    String getDescription();\n\n    String getType();\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/secret/store/concord/ConcordSecretStore.java",
    "content": "package com.walmartlabs.concord.server.org.secret.store.concord;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.cfg.ConcordSecretStoreConfiguration;\nimport com.walmartlabs.concord.server.org.secret.SecretDao;\nimport com.walmartlabs.concord.server.org.secret.store.SecretStore;\nimport org.jooq.DSLContext;\n\nimport javax.inject.Inject;\nimport java.util.UUID;\n\npublic class ConcordSecretStore implements SecretStore {\n\n    private static final String TYPE = \"concord\";\n    private static final String DESCRIPTION = \"Concord\";\n\n    private final boolean enabled;\n    private final SecretDao secretDao;\n\n    @Inject\n    public ConcordSecretStore(ConcordSecretStoreConfiguration cfg, SecretDao secretDao) {\n        this.enabled = cfg.isEnabled();\n        this.secretDao = secretDao;\n    }\n\n    @Override\n    public boolean isEnabled() {\n        return enabled;\n    }\n\n    @Override\n    public String getDescription() {\n        return DESCRIPTION;\n    }\n\n    @Override\n    public String getType() {\n        return TYPE;\n    }\n\n    @Override\n    public void store(DSLContext tx, UUID id, byte[] data) {\n        secretDao.updateData(tx, id, data);\n    }\n\n    @Override\n    public void delete(DSLContext tx, UUID id) {\n        // do nothing, the data will be deleted when the secret's entry is removed from the DB table\n    }\n\n    @Override\n    public byte[] get(UUID id) {\n        return secretDao.getData(id);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/team/AddTeamLdapGroupsResponse.java",
    "content": "package com.walmartlabs.concord.server.org.team;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\n\npublic class AddTeamLdapGroupsResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    @Override\n    public String toString() {\n        return \"AddTeamLdapGroupsResponse{\" +\n                \"ok=\" + ok +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/team/AddTeamUsersResponse.java",
    "content": "package com.walmartlabs.concord.server.org.team;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\n\npublic class AddTeamUsersResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    @Override\n    public String toString() {\n        return \"AddTeamUsersResponse{\" +\n                \"ok=\" + ok +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/team/CreateTeamResponse.java",
    "content": "package com.walmartlabs.concord.server.org.team;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.server.OperationResult;\n\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@JsonInclude(Include.NON_NULL)\npublic class CreateTeamResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n    private final OperationResult result;\n    private final UUID id;\n\n    @JsonCreator\n    public CreateTeamResponse(@JsonProperty(\"result\") OperationResult result,\n                              @JsonProperty(\"id\") UUID id) {\n\n        this.result = result;\n        this.id = id;\n    }\n\n    public OperationResult getResult() {\n        return result;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    @Override\n    public String toString() {\n        return \"CreateTeamResponse{\" +\n                \"ok=\" + ok +\n                \", result=\" + result +\n                \", id=\" + id +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/team/RemoveTeamUsersResponse.java",
    "content": "package com.walmartlabs.concord.server.org.team;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\n\npublic class RemoveTeamUsersResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    @Override\n    public String toString() {\n        return \"RemoveTeamUsersResponse{\" +\n                \"ok=\" + ok +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/team/TeamDao.java",
    "content": "package com.walmartlabs.concord.server.org.team;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.Utils;\nimport com.walmartlabs.concord.server.UuidGenerator;\nimport com.walmartlabs.concord.server.jooq.tables.TeamLdapGroups;\nimport com.walmartlabs.concord.server.jooq.tables.records.TeamsRecord;\nimport com.walmartlabs.concord.server.jooq.tables.records.VUserTeamsRecord;\nimport com.walmartlabs.concord.server.user.UserType;\nimport org.jooq.*;\n\nimport javax.inject.Inject;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.*;\nimport static com.walmartlabs.concord.server.jooq.tables.Organizations.ORGANIZATIONS;\nimport static com.walmartlabs.concord.server.jooq.tables.Teams.TEAMS;\nimport static com.walmartlabs.concord.server.jooq.tables.UserTeams.USER_TEAMS;\nimport static com.walmartlabs.concord.server.jooq.tables.Users.USERS;\nimport static java.util.Objects.requireNonNull;\nimport static org.jooq.impl.DSL.select;\nimport static org.jooq.impl.DSL.selectFrom;\n\npublic class TeamDao extends AbstractDao {\n\n    private final UuidGenerator uuidGenerator;\n\n    @Inject\n    public TeamDao(@MainDB Configuration cfg, UuidGenerator uuidGenerator) {\n        super(cfg);\n        this.uuidGenerator = requireNonNull(uuidGenerator);\n    }\n\n    @Override\n    public void tx(Tx t) {\n        super.tx(t);\n    }\n\n    @Override\n    protected <T> T txResult(TxResult<T> t) {\n        return super.txResult(t);\n    }\n\n    public UUID getId(UUID orgId, String name) {\n        return dsl().select(TEAMS.TEAM_ID)\n                .from(TEAMS)\n                .where(TEAMS.ORG_ID.eq(orgId).and(TEAMS.TEAM_NAME.eq(name)))\n                .fetchOne(TEAMS.TEAM_ID);\n    }\n\n    public UUID getOrgId(UUID teamId) {\n        return dsl().select(TEAMS.ORG_ID)\n                .from(TEAMS)\n                .where(TEAMS.TEAM_ID.eq(teamId))\n                .fetchOne(TEAMS.ORG_ID);\n    }\n\n    public UUID insert(UUID orgId, String name, String description) {\n        return txResult(tx -> insert(tx, orgId, name, description));\n    }\n\n    public UUID insert(DSLContext tx, UUID orgId, String name, String description) {\n        UUID teamId = uuidGenerator.generate();\n        return tx.insertInto(TEAMS)\n                .columns(TEAMS.TEAM_ID, TEAMS.ORG_ID, TEAMS.TEAM_NAME, TEAMS.DESCRIPTION)\n                .values(teamId, orgId, name, description)\n                .returning(TEAMS.TEAM_ID)\n                .fetchOne()\n                .getTeamId();\n    }\n\n    public void update(UUID id, String name, String description) {\n        tx(tx -> update(tx, id, name, description));\n    }\n\n    public void update(DSLContext tx, UUID id, String name, String description) {\n        UpdateSetFirstStep<TeamsRecord> q = tx.update(TEAMS);\n\n        if (description != null) {\n            q.set(TEAMS.DESCRIPTION, description);\n        }\n\n        q.set(TEAMS.TEAM_NAME, name)\n                .where(TEAMS.TEAM_ID.eq(id))\n                .execute();\n    }\n\n    public void delete(UUID id) {\n        tx(tx -> delete(tx, id));\n    }\n\n    public void delete(DSLContext tx, UUID id) {\n        tx.deleteFrom(TEAMS)\n                .where(TEAMS.TEAM_ID.eq(id))\n                .execute();\n    }\n\n    public TeamEntry get(UUID id) {\n        return get(dsl(), id);\n    }\n\n    private static SelectJoinStep<Record5<UUID, UUID, String, String, String>> selectTeams(DSLContext tx) {\n        Field<String> orgNameField = select(ORGANIZATIONS.ORG_NAME)\n                .from(ORGANIZATIONS)\n                .where(ORGANIZATIONS.ORG_ID.eq(TEAMS.ORG_ID)).asField();\n\n        return tx.select(TEAMS.TEAM_ID,\n                TEAMS.ORG_ID,\n                orgNameField,\n                TEAMS.TEAM_NAME,\n                TEAMS.DESCRIPTION)\n                .from(TEAMS);\n    }\n\n    public TeamEntry get(DSLContext tx, UUID id) {\n        return selectTeams(tx)\n                .where(TEAMS.TEAM_ID.eq(id))\n                .fetchOne(TeamDao::toEntry);\n    }\n\n    public TeamEntry getByName(UUID orgId, String name) {\n        return getByName(dsl(), orgId, name);\n    }\n\n    public TeamEntry getByName(DSLContext tx, UUID orgId, String name) {\n        return selectTeams(tx)\n                .where(TEAMS.ORG_ID.eq(orgId).and(TEAMS.TEAM_NAME.eq(name)))\n                .fetchOne(TeamDao::toEntry);\n    }\n\n    public List<TeamEntry> list(UUID orgId) {\n        return list(dsl(), orgId);\n    }\n\n    public List<TeamEntry> list(DSLContext tx, UUID orgId) {\n        return selectTeams(tx)\n                .where(TEAMS.ORG_ID.eq(orgId))\n                .orderBy(TEAMS.TEAM_NAME)\n                .fetch(TeamDao::toEntry);\n    }\n\n    public List<TeamUserEntry> listUsers(UUID teamId) {\n        DSLContext tx = dsl();\n\n        List<TeamUserEntry> l = new LinkedList<>();\n        l.addAll(listMembers(tx, teamId));\n        l.addAll(listLdapGroupMembers(tx, teamId));\n\n        return l;\n    }\n\n    public List<TeamUserEntry> listMembers(DSLContext tx, UUID teamId) {\n        return tx.selectDistinct(USERS.USER_ID, USERS.USERNAME, USERS.DOMAIN, USERS.DISPLAY_NAME, USERS.USER_TYPE, USER_TEAMS.TEAM_ROLE)\n                .from(USER_TEAMS)\n                .innerJoin(USERS).on(USERS.USER_ID.eq(USER_TEAMS.USER_ID))\n                .where(USER_TEAMS.TEAM_ID.eq(teamId))\n                .orderBy(USERS.USERNAME)\n                .fetch((Record6<UUID, String, String, String, String, String> r) ->\n                        new TeamUserEntry(r.value1(),\n                                r.value2(),\n                                r.value3(),\n                                r.value4(),\n                                UserType.valueOf(r.value5()),\n                                TeamRole.valueOf(r.value6()),\n                                TeamMemberType.SINGLE,\n                                null));\n    }\n\n    public List<TeamUserEntry> listLdapGroupMembers(DSLContext tx, UUID teamId) {\n        return tx.select(USERS.USER_ID, USERS.USERNAME, USERS.DOMAIN, USERS.DISPLAY_NAME, USERS.USER_TYPE, TEAM_LDAP_GROUPS.TEAM_ROLE, USER_LDAP_GROUPS.LDAP_GROUP)\n                .from(USERS)\n                .innerJoin(USER_LDAP_GROUPS).on(USER_LDAP_GROUPS.USER_ID.eq(USERS.USER_ID))\n                .innerJoin(TEAM_LDAP_GROUPS).on(TEAM_LDAP_GROUPS.LDAP_GROUP.eq(USER_LDAP_GROUPS.LDAP_GROUP))\n                .innerJoin(TEAMS).on(TEAMS.TEAM_ID.eq(TEAM_LDAP_GROUPS.TEAM_ID))\n                .where(TEAMS.TEAM_ID.eq(teamId))\n                .orderBy(USERS.USERNAME)\n                .fetch((Record7<UUID, String, String, String, String, String, String> r) ->\n                        new TeamUserEntry(r.value1(),\n                                r.value2(),\n                                r.value3(),\n                                r.value4(),\n                                UserType.valueOf(r.value5()),\n                                TeamRole.valueOf(r.value6()),\n                                TeamMemberType.LDAP_GROUP,\n                                r.value7()));\n    }\n\n    public List<TeamLdapGroupEntry> listLdapGroups(UUID teamId) {\n        TeamLdapGroups t = TEAM_LDAP_GROUPS.as(\"t\");\n        return txResult(tx -> tx.select(t.LDAP_GROUP, t.TEAM_ROLE)\n                .from(t)\n                .where(t.TEAM_ID.eq(teamId))\n                .orderBy(t.LDAP_GROUP)\n                .fetch(r -> TeamLdapGroupEntry.builder()\n                        .group(r.value1())\n                        .role(TeamRole.valueOf(r.value2()))\n                        .build()));\n    }\n\n    public void upsertUser(UUID teamId, UUID userId, TeamRole role) {\n        tx(tx -> upsertUser(tx, teamId, userId, role));\n    }\n\n    public void upsertUser(DSLContext tx, UUID teamId, UUID userId, TeamRole role) {\n        tx.insertInto(USER_TEAMS)\n                .columns(USER_TEAMS.TEAM_ID, USER_TEAMS.USER_ID, USER_TEAMS.TEAM_ROLE)\n                .values(teamId, userId, role.toString())\n                .onConflict(USER_TEAMS.TEAM_ID, USER_TEAMS.USER_ID)\n                .doUpdate().set(USER_TEAMS.TEAM_ROLE, role.toString())\n                .execute();\n    }\n\n    public void removeUsers(DSLContext tx, UUID teamId) {\n        tx.deleteFrom(USER_TEAMS)\n                .where(USER_TEAMS.TEAM_ID.eq(teamId))\n                .execute();\n    }\n\n    public void removeUsers(UUID teamId, Collection<UUID> userIds) {\n        tx(tx -> removeUsers(tx, teamId, userIds));\n    }\n\n    public void removeUsers(DSLContext tx, UUID teamId, Collection<UUID> userIds) {\n        if (userIds == null || userIds.isEmpty()) {\n            return;\n        }\n\n        tx.deleteFrom(USER_TEAMS)\n                .where(USER_TEAMS.TEAM_ID.eq(teamId)\n                        .and(USER_TEAMS.USER_ID.in(userIds)))\n                .execute();\n    }\n\n    public void upsertLdapGroup(DSLContext tx, UUID teamId, String ldapGroup, TeamRole role) {\n        tx.insertInto(TEAM_LDAP_GROUPS)\n                .columns(TEAM_LDAP_GROUPS.TEAM_ID, TEAM_LDAP_GROUPS.LDAP_GROUP, TEAM_LDAP_GROUPS.TEAM_ROLE)\n                .values(teamId, ldapGroup, role.toString())\n                .onConflict(TEAM_LDAP_GROUPS.TEAM_ID, TEAM_LDAP_GROUPS.LDAP_GROUP)\n                .doUpdate().set(TEAM_LDAP_GROUPS.TEAM_ROLE, role.toString())\n                .execute();\n    }\n\n    public void removeLdapGroups(DSLContext tx, UUID teamId) {\n        tx.deleteFrom(TEAM_LDAP_GROUPS)\n                .where(TEAM_LDAP_GROUPS.TEAM_ID.eq(teamId))\n                .execute();\n    }\n\n    public boolean isInAnyTeam(UUID orgId, UUID userId, TeamRole... roles) {\n        return isInAnyTeam(dsl(), orgId, userId, roles);\n    }\n\n    public boolean isInAnyTeam(DSLContext tx, UUID orgId, UUID userId, TeamRole... roles) {\n        SelectConditionStep<Record1<UUID>> teamIds = select(TEAMS.TEAM_ID).from(TEAMS).where(TEAMS.ORG_ID.eq(orgId));\n        return tx.fetchExists(selectFrom(V_USER_TEAMS)\n                .where(V_USER_TEAMS.USER_ID.eq(userId)\n                        .and(V_USER_TEAMS.TEAM_ID.in(teamIds))\n                        .and(V_USER_TEAMS.TEAM_ROLE.in(Utils.toString(roles)))));\n    }\n\n    public boolean hasUser(UUID teamId, UUID userId, TeamRole... roles) {\n        return hasUser(dsl(), teamId, userId, roles);\n    }\n\n    public boolean hasUser(DSLContext tx, UUID teamId, UUID userId, TeamRole... roles) {\n        SelectConditionStep<VUserTeamsRecord> q = tx.selectFrom(V_USER_TEAMS)\n                .where(V_USER_TEAMS.TEAM_ID.eq(teamId)\n                        .and(V_USER_TEAMS.USER_ID.eq(userId)));\n\n        if (roles != null && roles.length != 0) {\n            q.and(V_USER_TEAMS.TEAM_ROLE.in(Utils.toString(roles)));\n        }\n\n        return tx.fetchExists(q);\n    }\n\n    private static TeamEntry toEntry(Record5<UUID, UUID, String, String, String> r) {\n        return new TeamEntry(r.value1(), r.value2(), r.value3(), r.value4(), r.value5());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/team/TeamEntry.java",
    "content": "package com.walmartlabs.concord.server.org.team;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\n\nimport javax.validation.constraints.NotNull;\nimport javax.validation.constraints.Size;\nimport java.io.Serializable;\nimport java.util.UUID;\n\nimport static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;\n\n@JsonInclude(NON_NULL)\npublic class TeamEntry implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final UUID id;\n\n    private final UUID orgId;\n\n    @ConcordKey\n    private final String orgName;\n\n    @NotNull\n    @ConcordKey\n    private final String name;\n\n    @Size(max = 2048)\n    private final String description;\n\n    public TeamEntry(String name) {\n        this(null, null, null, name, null);\n    }\n\n    @JsonCreator\n    public TeamEntry(@JsonProperty(\"id\") UUID id,\n                     @JsonProperty(\"orgId\") UUID orgId,\n                     @JsonProperty(\"orgName\") String orgName,\n                     @JsonProperty(\"name\") String name,\n                     @JsonProperty(\"description\") String description) {\n\n        this.id = id;\n        this.orgId = orgId;\n        this.orgName = orgName;\n        this.name = name;\n        this.description = description;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public UUID getOrgId() {\n        return orgId;\n    }\n\n    public String getOrgName() {\n        return orgName;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public String getDescription() {\n        return description;\n    }\n\n    @Override\n    public String toString() {\n        return \"TeamEntry{\" +\n                \"id=\" + id +\n                \", orgId=\" + orgId +\n                \", orgName='\" + orgName + '\\'' +\n                \", name='\" + name + '\\'' +\n                \", description='\" + description + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/team/TeamLdapGroupEntry.java",
    "content": "package com.walmartlabs.concord.server.org.team;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\n\n@Value.Immutable\n@JsonInclude(Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableTeamLdapGroupEntry.class)\n@JsonDeserialize(as = ImmutableTeamLdapGroupEntry.class)\npublic interface TeamLdapGroupEntry extends Serializable {\n\n    String group();\n\n    TeamRole role();\n\n    static ImmutableTeamLdapGroupEntry.Builder builder() {\n        return ImmutableTeamLdapGroupEntry.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/team/TeamManager.java",
    "content": "package com.walmartlabs.concord.server.org.team;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.audit.AuditAction;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.audit.AuditObject;\nimport com.walmartlabs.concord.server.org.OrganizationDao;\nimport com.walmartlabs.concord.server.org.OrganizationEntry;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.ResourceAccessUtils;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.user.User;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport com.walmartlabs.concord.server.user.UserType;\nimport org.jooq.DSLContext;\n\nimport javax.inject.Inject;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\n\npublic class TeamManager {\n\n    /**\n     * Default organization's team ID\n     */\n    public static final UUID DEFAULT_ORG_TEAM_ID = UUID.fromString(\"00000000-0000-0000-0000-000000000000\");\n\n    /**\n     * Name of the default team in each organization.\n     */\n    public static final String DEFAULT_TEAM_NAME = \"default\";\n\n    private final TeamDao teamDao;\n    private final OrganizationDao orgDao;\n    private final OrganizationManager orgManager;\n    private final UserManager userManager;\n    private final AuditLog auditLog;\n\n    @Inject\n    public TeamManager(TeamDao teamDao,\n                       OrganizationDao orgDao,\n                       OrganizationManager orgManager,\n                       UserManager userManager,\n                       AuditLog auditLog) {\n\n        this.teamDao = teamDao;\n        this.orgDao = orgDao;\n        this.orgManager = orgManager;\n        this.userManager = userManager;\n        this.auditLog = auditLog;\n    }\n\n    public UUID insert(UUID orgId, String teamName, String description) {\n        assertAccess(orgId, TeamRole.OWNER);\n\n        UUID teamId = teamDao.txResult(tx -> {\n            UUID tId = teamDao.insert(tx, orgId, teamName, description);\n\n            // add the current user as a team maintainer\n            UUID userId = UserPrincipal.assertCurrent().getId();\n            teamDao.upsertUser(tx, tId, userId, TeamRole.MAINTAINER);\n\n            return tId;\n        });\n\n        auditLog.add(AuditObject.TEAM, AuditAction.CREATE)\n                .field(\"orgId\", orgId)\n                .field(\"teamId\", teamId)\n                .field(\"name\", teamName)\n                .changes(null, new TeamEntry(teamId, orgId, null, teamName, description))\n                .log();\n\n        return teamId;\n    }\n\n    public void update(UUID teamId, String teamName, String description) {\n        UUID orgId = teamDao.getOrgId(teamId);\n        TeamEntry prevEntry = assertAccess(orgId, teamId, null, TeamRole.MAINTAINER, true);\n\n        teamDao.update(teamId, teamName, description);\n\n        auditLog.add(AuditObject.TEAM, AuditAction.UPDATE)\n                .field(\"orgId\", orgId)\n                .field(\"teamId\", prevEntry.getId())\n                .field(\"name\", teamName)\n                .changes(prevEntry, teamDao.get(teamId))\n                .log();\n    }\n\n    public void delete(String orgName, String teamName) {\n        TeamEntry t = assertTeam(orgName, teamName, TeamRole.OWNER, true, true);\n\n        teamDao.delete(t.getId());\n\n        auditLog.add(AuditObject.TEAM, AuditAction.DELETE)\n                .field(\"orgId\", t.getOrgId())\n                .field(\"teamId\", t.getId())\n                .field(\"name\", t.getName())\n                .log();\n    }\n\n    public void addUsers(String orgName, String teamName, boolean replace, Collection<TeamUserEntry> users) {\n        TeamEntry t = assertTeam(orgName, teamName, TeamRole.MAINTAINER, true, true);\n\n        Map<UUID, TeamRole> effectiveUsers = new HashMap<>();\n        for (TeamUserEntry u : users) {\n            UserType type = u.getUserType();\n            if (type == null) {\n                type = UserPrincipal.assertCurrent().getType();\n            }\n\n            UUID id = u.getUserId();\n            if (id == null) {\n                id = userManager.getId(u.getUsername(), u.getUserDomain(), type)\n                        .orElseThrow(() -> new ConcordApplicationException(\"User not found: \" + u.getUsername()));\n            }\n\n            TeamRole role = u.getRole();\n            if (role == null) {\n                role = TeamRole.MEMBER;\n            }\n\n            effectiveUsers.put(id, role);\n        }\n\n        teamDao.tx(tx -> {\n            if (replace) {\n                teamDao.removeUsers(tx, t.getId());\n            }\n\n            for (Map.Entry<UUID, TeamRole> u : effectiveUsers.entrySet()) {\n                UUID id = u.getKey();\n                TeamRole role = u.getValue();\n                teamDao.upsertUser(tx, t.getId(), id, role);\n            }\n\n            validateUsers(tx, t.getOrgId());\n        });\n\n        auditLog.add(AuditObject.TEAM, AuditAction.UPDATE)\n                .field(\"orgId\", t.getOrgId())\n                .field(\"teamId\", t.getId())\n                .field(\"name\", t.getName())\n                .field(\"action\", \"addUsers\")\n                .field(\"users\", users)\n                .field(\"replace\", replace)\n                .log();\n    }\n\n    public void addLdapGroups(String orgName, String teamName, boolean replace, Collection<TeamLdapGroupEntry> groups) {\n        TeamEntry t = assertTeam(orgName, teamName, TeamRole.MAINTAINER, true, true);\n\n        teamDao.tx(tx -> {\n            if (replace) {\n                teamDao.removeLdapGroups(tx, t.getId());\n            }\n\n            for (TeamLdapGroupEntry g : groups) {\n                TeamRole role = g.role();\n                if (role == null) {\n                    role = TeamRole.MEMBER;\n                }\n\n                teamDao.upsertLdapGroup(tx, t.getId(), g.group(), role);\n            }\n\n            validateUsers(tx, t.getOrgId());\n        });\n\n        auditLog.add(AuditObject.TEAM, AuditAction.UPDATE)\n                .field(\"orgId\", t.getOrgId())\n                .field(\"teamId\", t.getId())\n                .field(\"name\", t.getName())\n                .field(\"action\", \"addLdapGroups\")\n                .field(\"groups\", groups)\n                .field(\"replace\", replace)\n                .log();\n    }\n\n    private void validateUsers(DSLContext tx, UUID orgId) {\n        if (orgDao.hasOwner(tx, orgId)) {\n            return;\n        }\n\n        if (!orgDao.hasRole(tx, orgId, TeamRole.OWNER)) {\n            throw new ValidationErrorsException(\"Organization must have at least one OWNER\");\n        }\n    }\n\n    public void removeUsers(String orgName, String teamName, Collection<User> users) {\n        TeamEntry t = assertTeam(orgName, teamName, TeamRole.MAINTAINER, true, true);\n\n        Collection<UUID> userIds = users.stream()\n                .map(u -> userManager.getId(u.username(), u.domain(), u.type()))\n                .flatMap(id -> id.map(Stream::of).orElseGet(Stream::empty))\n                .collect(Collectors.toSet());\n\n        teamDao.removeUsers(t.getId(), userIds);\n\n        auditLog.add(AuditObject.TEAM, AuditAction.UPDATE)\n                .field(\"orgId\", t.getOrgId())\n                .field(\"teamId\", t.getId())\n                .field(\"name\", t.getName())\n                .field(\"action\", \"removeUsers\")\n                .field(\"users\", users)\n                .log();\n    }\n\n    public TeamEntry assertExisting(UUID orgId, UUID teamId, String teamName) {\n        if (teamId != null) {\n            TeamEntry e = teamDao.get(teamId);\n            if (e == null) {\n                throw new ValidationErrorsException(\"Team not found: \" + teamId);\n            }\n            return e;\n        }\n\n        if (teamName != null) {\n            TeamEntry e = teamDao.getByName(orgId, teamName);\n            if (e == null) {\n                throw new ValidationErrorsException(\"Team not found: \" + teamName);\n            }\n            return e;\n        }\n\n        throw new ValidationErrorsException(\"Team ID or name is required\");\n    }\n\n    public void assertAccess(UUID orgId, TeamRole requiredRole) {\n        if (Roles.isAdmin()) {\n            return;\n        }\n\n        UserPrincipal p = UserPrincipal.assertCurrent();\n\n        OrganizationEntry org = orgManager.assertAccess(orgId, false);\n        if (ResourceAccessUtils.isSame(p, org.getOwner())) {\n            // the org owner can do anything with the org's teams\n            return;\n        }\n\n        if (!teamDao.isInAnyTeam(orgId, p.getId(), TeamRole.atLeast(requiredRole))) {\n            throw new UnauthorizedException(\"The current user (\" + p.getUsername() + \") does not have the required role: \" + requiredRole);\n        }\n    }\n\n    public TeamEntry assertAccess(UUID orgId, String teamName, TeamRole requiredRole, boolean teamMembersOnly) {\n        return assertAccess(orgId, null, teamName, requiredRole, teamMembersOnly);\n    }\n\n    public TeamEntry assertAccess(UUID orgId, UUID teamId, String teamName, TeamRole requiredRole, boolean teamMembersOnly) {\n        TeamEntry e = assertExisting(orgId, teamId, teamName);\n\n        if (Roles.isAdmin()) {\n            return e;\n        }\n\n        UserPrincipal p = UserPrincipal.assertCurrent();\n\n        OrganizationEntry org = orgManager.assertAccess(e.getOrgId(), false);\n        if (ResourceAccessUtils.isSame(p, org.getOwner())) {\n            // the org owner can do anything with the org's inventories\n            return e;\n        }\n\n        if (requiredRole != null && teamMembersOnly) {\n            if (!teamDao.hasUser(e.getId(), p.getId(), TeamRole.atLeast(requiredRole))) {\n                throw new UnauthorizedException(\"The current user (\" + p.getUsername() + \") does not have the required role: \" + requiredRole);\n            }\n        }\n\n        return e;\n    }\n\n    private TeamEntry assertTeam(String orgName, String teamName, TeamRole requiredRole,\n                                 boolean orgMembersOnly, boolean teamMembersOnly) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, orgMembersOnly);\n        return assertAccess(org.getId(), teamName, requiredRole, teamMembersOnly);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/team/TeamMemberType.java",
    "content": "package com.walmartlabs.concord.server.org.team;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic enum TeamMemberType {\n    SINGLE,\n    LDAP_GROUP\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/team/TeamModule.java",
    "content": "package com.walmartlabs.concord.server.org.team;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.walmartlabs.concord.server.Utils.bindJaxRsResource;\n\npublic class TeamModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        binder.bind(TeamDao.class).in(SINGLETON);\n        binder.bind(TeamManager.class).in(SINGLETON);\n\n        bindJaxRsResource(binder, TeamResource.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/team/TeamResource.java",
    "content": "package com.walmartlabs.concord.server.org.team;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.server.GenericOperationResult;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.org.OrganizationEntry;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.user.User;\nimport com.walmartlabs.concord.server.user.UserType;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.validation.Valid;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\n\n@Path(\"/api/v1/org\")\n@Tag(name = \"Teams\")\npublic class TeamResource implements Resource {\n\n    private final TeamDao teamDao;\n    private final TeamManager teamManager;\n    private final OrganizationManager orgManager;\n\n    @Inject\n    public TeamResource(TeamDao teamDao,\n                        TeamManager teamManager,\n                        OrganizationManager orgManager) {\n\n        this.teamDao = teamDao;\n        this.teamManager = teamManager;\n        this.orgManager = orgManager;\n    }\n\n    /**\n     * Create or update a team.\n     */\n    @POST\n    @Path(\"/{orgName}/team\")\n    @Operation(description = \"Create or update a team\", operationId = \"createOrUpdateTeam\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    public CreateTeamResponse createOrUpdate(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                             @Valid TeamEntry entry) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n        UUID teamId = entry.getId();\n        if (teamId == null) {\n            teamId = teamDao.getId(org.getId(), entry.getName());\n        }\n        if (teamId != null) {\n            teamManager.update(teamId, entry.getName(), entry.getDescription());\n            return new CreateTeamResponse(OperationResult.UPDATED, teamId);\n        } else {\n            teamId = teamManager.insert(org.getId(), entry.getName(), entry.getDescription());\n            return new CreateTeamResponse(OperationResult.CREATED, teamId);\n        }\n    }\n\n    /**\n     * Get an existing team.\n     */\n    @GET\n    @Path(\"/{orgName}/team/{teamName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Get an existing team\", operationId = \"getTeam\")\n    public TeamEntry get(@PathParam(\"orgName\") @ConcordKey String orgName,\n                         @PathParam(\"teamName\") @ConcordKey String teamName) {\n        return assertTeam(orgName, teamName, null, true, false);\n    }\n\n    /**\n     * Delete an existing team.\n     */\n    @DELETE\n    @Path(\"/{orgName}/team/{teamName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Delete an existing team\", operationId = \"deleteTeam\")\n    public GenericOperationResult delete(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                         @PathParam(\"teamName\") @ConcordKey String teamName) {\n\n        teamManager.delete(orgName, teamName);\n        return new GenericOperationResult(OperationResult.DELETED);\n    }\n\n    /**\n     * List teams.\n     */\n    @GET\n    @Path(\"/{orgName}/team\")\n    @Operation(description = \"List teams\", operationId = \"listTeams\")\n    @Produces(MediaType.APPLICATION_JSON)\n    public List<TeamEntry> list(@PathParam(\"orgName\") @ConcordKey String orgName) {\n        OrganizationEntry org = orgManager.assertAccess(orgName, false);\n        return teamDao.list(org.getId());\n    }\n\n    /**\n     * List users of a team.\n     */\n    @GET\n    @Path(\"/{orgName}/team/{teamName}/users\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"List users of a team\", operationId = \"listUserTeams\")\n    public List<TeamUserEntry> listUsers(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                         @PathParam(\"teamName\") @ConcordKey String teamName) {\n\n        TeamEntry t = assertTeam(orgName, teamName, TeamRole.MEMBER, true, false);\n        return teamDao.listUsers(t.getId());\n    }\n\n    /**\n     * List LDAP roles of a team.\n     */\n    @GET\n    @Path(\"/{orgName}/team/{teamName}/ldapGroups\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"List ldap roles of a team\")\n    public List<TeamLdapGroupEntry> listLdapGroups(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                   @PathParam(\"teamName\") @ConcordKey String teamName) {\n\n        TeamEntry t = assertTeam(orgName, teamName, TeamRole.MEMBER, true, false);\n        return teamDao.listLdapGroups(t.getId());\n    }\n\n    /**\n     * Add users to the specified team.\n     */\n    @PUT\n    @Path(\"/{orgName}/team/{teamName}/users\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Add users to a team\", operationId = \"addUsersToTeam\")\n    public AddTeamUsersResponse addUsers(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                         @PathParam(\"teamName\") @ConcordKey String teamName,\n                                         @QueryParam(\"replace\") @DefaultValue(\"false\") boolean replace,\n                                         @Valid Collection<TeamUserEntry> users) {\n\n        boolean isEmptyUsers = users == null || users.isEmpty();\n        if (isEmptyUsers && !replace) {\n            throw new ValidationErrorsException(\"Empty user list\");\n        }\n\n        teamManager.addUsers(orgName, teamName, replace, users);\n        return new AddTeamUsersResponse();\n    }\n\n    /**\n     * Add LDAP groups to the specified team.\n     */\n    @PUT\n    @Path(\"/{orgName}/team/{teamName}/ldapGroups\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Add LDAP groups to a team\")\n    public AddTeamLdapGroupsResponse addLdapGroups(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                                   @PathParam(\"teamName\") @ConcordKey String teamName,\n                                                   @QueryParam(\"replace\") @DefaultValue(\"false\") boolean replace,\n                                                   @Valid Collection<TeamLdapGroupEntry> roles) {\n\n        boolean isEmptyRoles = roles == null || roles.isEmpty();\n        if (isEmptyRoles && !replace) {\n            throw new ValidationErrorsException(\"Empty LDAP group list\");\n        }\n\n        teamManager.addLdapGroups(orgName, teamName, replace, roles);\n        return new AddTeamLdapGroupsResponse();\n    }\n\n    /**\n     * Remove users from the specified team.\n     */\n    @DELETE\n    @Path(\"/{orgName}/team/{teamName}/users\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Remove users from a team\", operationId = \"removeUsersFromTeam\")\n    public RemoveTeamUsersResponse removeUsers(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                               @PathParam(\"teamName\") @ConcordKey String teamName,\n                                               Collection<String> usernames) {\n\n        if (usernames == null || usernames.isEmpty()) {\n            throw new ValidationErrorsException(\"Empty user list\");\n        }\n\n        // TODO: add user type into request params\n        UserType type = UserPrincipal.assertCurrent().getType();\n        // TODO: add user domain into request params\n        String domain = null;\n        teamManager.removeUsers(orgName, teamName, usernames.stream().map(n -> User.of(n, domain, type)).collect(Collectors.toList()));\n        return new RemoveTeamUsersResponse();\n    }\n\n    private TeamEntry assertTeam(String orgName, String teamName, TeamRole requiredRole,\n                                 boolean orgMembersOnly, boolean teamMembersOnly) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, orgMembersOnly);\n        return teamManager.assertAccess(org.getId(), teamName, requiredRole, teamMembersOnly);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/team/TeamRole.java",
    "content": "package com.walmartlabs.concord.server.org.team;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic enum TeamRole {\n\n    /**\n     * Can change the organization's settings, create new teams, etc.\n     */\n    OWNER,\n\n    /**\n     * Can add or remove other users to/from the team.\n     */\n    MAINTAINER,\n\n    /**\n     * Can access all team resources.\n     */\n    MEMBER;\n\n    public static TeamRole[] atLeast(TeamRole r) {\n        switch (r) {\n            case OWNER:\n                return new TeamRole[]{OWNER};\n            case MAINTAINER:\n                return new TeamRole[]{OWNER, MAINTAINER};\n            case MEMBER:\n                return new TeamRole[]{OWNER, MAINTAINER, MEMBER};\n            default:\n                throw new IllegalArgumentException(\"Unknown role: \" + r);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/team/TeamUserEntry.java",
    "content": "package com.walmartlabs.concord.server.org.team;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.server.user.UserEntry;\nimport com.walmartlabs.concord.server.user.UserType;\n\nimport javax.validation.constraints.Size;\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@JsonInclude(Include.NON_NULL)\npublic class TeamUserEntry implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final UUID userId;\n\n    @Size(max = UserEntry.MAX_USERNAME_LENGTH)\n    private final String username;\n\n    @Size(max = UserEntry.MAX_DOMAIN_LENGTH)\n    private final String userDomain;\n\n    private final String displayName;\n\n    private final UserType userType;\n\n    private final TeamRole role;\n\n    private final TeamMemberType memberType;\n\n    private final String ldapGroupSource;\n\n    @JsonCreator\n    public TeamUserEntry(@JsonProperty(\"userId\") UUID userId,\n                         @JsonProperty(\"username\") String username,\n                         @JsonProperty(\"userDomain\") String userDomain,\n                         @JsonProperty(\"displayName\") String displayName,\n                         @JsonProperty(\"userType\") UserType userType,\n                         @JsonProperty(\"role\") TeamRole role,\n                         @JsonProperty(\"memberType\") TeamMemberType memberType,\n                         @JsonProperty(\"ldapGroupSource\") String ldapGroupSource) {\n\n        this.userId = userId;\n        this.username = username;\n        this.userDomain = userDomain;\n        this.displayName = displayName;\n        this.userType = userType;\n        this.role = role;\n        this.memberType = memberType;\n        this.ldapGroupSource = ldapGroupSource;\n    }\n\n    public UUID getUserId() {\n        return userId;\n    }\n\n    public String getUsername() {\n        return username;\n    }\n\n    public String getUserDomain() {\n        return userDomain;\n    }\n\n    public String getDisplayName() {\n        return displayName;\n    }\n\n    public UserType getUserType() {\n        return userType;\n    }\n\n    public TeamRole getRole() {\n        return role;\n    }\n\n    public TeamMemberType getMemberType() {\n        return memberType;\n    }\n\n    public String getLdapGroupSource() {\n        return ldapGroupSource;\n    }\n\n    @Override\n    public String toString() {\n        return \"TeamUserEntry{\" +\n                \"userId=\" + userId +\n                \", username='\" + username + '\\'' +\n                \", userDomain='\" + userDomain + '\\'' +\n                \", displayName='\" + displayName + '\\'' +\n                \", userType=\" + userType +\n                \", role=\" + role +\n                \", memberType=\" + memberType +\n                \", ldapGroupSource='\" + ldapGroupSource + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/triggers/CronTriggerProcessor.java",
    "content": "package com.walmartlabs.concord.server.org.triggers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Trigger;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport org.jooq.DSLContext;\n\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\nimport java.time.ZoneId;\nimport java.util.Arrays;\nimport java.util.Map;\nimport java.util.TimeZone;\nimport java.util.UUID;\n\npublic class CronTriggerProcessor {\n\n    private final TriggerScheduleDao schedulerDao;\n\n    @Inject\n    public CronTriggerProcessor(TriggerScheduleDao schedulerDao) {\n        this.schedulerDao = schedulerDao;\n    }\n\n    public void process(DSLContext tx, UUID triggerId, Trigger t) {\n        Map<String, Object> conditions = t.conditions();\n        if (conditions == null) {\n            throw new ValidationErrorsException(\"cron trigger '\" + triggerId + \"' without params\");\n        }\n\n        String spec = (String) conditions.get(Constants.Trigger.CRON_SPEC);\n\n        if (spec == null) {\n            throw new ValidationErrorsException(\"cron trigger '\" + triggerId + \"' without spec\");\n        }\n\n        ZoneId zoneId = null;\n        String timezone = (String) conditions.get(Constants.Trigger.CRON_TIMEZONE);\n        if (timezone != null) {\n            if (!validTimeZone(timezone)) {\n                throw new ValidationErrorsException(\"cron trigger '\" + triggerId + \"' invalid timezone '\" + timezone + \"'\");\n            }\n\n            zoneId = TimeZone.getTimeZone(timezone).toZoneId();\n        }\n\n        OffsetDateTime fireAt = CronUtils.nextExecution(schedulerDao.now(), spec, zoneId);\n        if (fireAt == null) {\n            throw new ValidationErrorsException(\"cron trigger '\" + triggerId + \"' spec is empty\");\n        }\n\n        schedulerDao.insert(tx, triggerId, fireAt);\n    }\n\n    private static boolean validTimeZone(String timezone) {\n        return Arrays.asList(TimeZone.getAvailableIDs()).contains(timezone);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/triggers/CronUtils.java",
    "content": "package com.walmartlabs.concord.server.org.triggers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.cronutils.model.CronType;\nimport com.cronutils.model.definition.CronDefinitionBuilder;\nimport com.cronutils.model.time.ExecutionTime;\nimport com.cronutils.parser.CronParser;\n\nimport java.time.OffsetDateTime;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\n\npublic final class CronUtils {\n\n    public static OffsetDateTime nextExecution(OffsetDateTime now, String expression, ZoneId zone) {\n        if (zone == null) {\n            zone = ZoneId.systemDefault();\n        }\n        return nextExecution(now.atZoneSameInstant(zone), expression);\n    }\n\n    private static OffsetDateTime nextExecution(ZonedDateTime now, String expression) {\n        CronParser parser = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.UNIX));\n        ExecutionTime executionTime = ExecutionTime.forCron(parser.parse(expression));\n        return executionTime.nextExecution(now).map(ZonedDateTime::toOffsetDateTime).orElse(null);\n    }\n\n    private CronUtils() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/triggers/GithubTriggerEnricher.java",
    "content": "package com.walmartlabs.concord.server.org.triggers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport com.walmartlabs.concord.server.events.github.GithubRepoInfo;\nimport com.walmartlabs.concord.server.events.github.GithubUtils;\nimport com.walmartlabs.concord.server.org.project.RepositoryDao;\nimport com.walmartlabs.concord.server.org.project.RepositoryEntry;\nimport org.jooq.DSLContext;\n\nimport javax.inject.Inject;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.events.github.Constants.*;\n\n/**\n * Enrich \"github\" trigger definitions. The main goal is to save trigger\n * definitions in the DB so they can be used later to match with external events\n * (GitHub push notifications, for example).\n */\npublic class GithubTriggerEnricher {\n\n    private final RepositoryDao repositoryDao;\n\n    @Inject\n    public GithubTriggerEnricher(RepositoryDao repositoryDao) {\n        this.repositoryDao = repositoryDao;\n    }\n\n    public Map<String, Object> enrich(DSLContext tx, UUID repoId, Map<String, Object> conditions) {\n        if (conditions == null) {\n            return conditions;\n        }\n\n        // determine the trigger definition's version\n        // version 1 is the original implementation\n        // version 2 is a streamlined implementation that doesn't support some of the corner cases of v1\n        int triggerVersion = MapUtils.getInt(conditions, VERSION_KEY, 1);\n        if (triggerVersion == 2) {\n            conditions = enrichTriggerConditions(tx, repoId, conditions);\n        }\n        return conditions;\n    }\n\n    private Map<String, Object> enrichTriggerConditions(DSLContext tx, UUID repoId, Map<String, Object> conditions) {\n        if (conditions.containsKey(GITHUB_ORG_KEY) && conditions.containsKey(GITHUB_REPO_KEY) && conditions.containsKey(REPO_BRANCH_KEY)) {\n            return conditions;\n        }\n\n        RepositoryEntry repo = repositoryDao.get(tx, repoId);\n        GithubRepoInfo githubRepoInfo = GithubUtils.getRepositoryInfo(repo.getUrl());\n        if (githubRepoInfo == null) {\n            return conditions;\n        }\n\n        Map<String, Object> newParams = new HashMap<>(conditions);\n        newParams.putIfAbsent(GITHUB_ORG_KEY, githubRepoInfo.owner());\n        newParams.putIfAbsent(GITHUB_REPO_KEY, githubRepoInfo.name());\n\n        Object eventType = conditions.get(TYPE_KEY);\n        if ((PULL_REQUEST_EVENT.equals(eventType) || PUSH_EVENT.equals(eventType)) && (repo.getBranch() != null)) {\n            newParams.putIfAbsent(REPO_BRANCH_KEY, repo.getBranch());\n        }\n        return newParams;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/triggers/TriggerEntry.java",
    "content": "package com.walmartlabs.concord.server.org.triggers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport org.immutables.builder.Builder;\n\nimport javax.validation.constraints.NotNull;\nimport java.io.Serializable;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\n@JsonInclude(Include.NON_NULL)\n@JsonIgnoreProperties(ignoreUnknown = true)\npublic class TriggerEntry implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final UUID id;\n\n    @ConcordKey\n    private final UUID orgId;\n\n    private final String orgName;\n\n    private final UUID projectId;\n\n    @ConcordKey\n    private final String projectName;\n\n    private final UUID repositoryId;\n\n    @ConcordKey\n    private final String repositoryName;\n\n    @NotNull\n    @ConcordKey\n    private final String eventSource;\n\n    private final List<String> activeProfiles;\n\n    private final Map<String, Object> arguments;\n\n    private final Map<String, Object> conditions;\n\n    @NotNull\n    private final Map<String, Object> cfg;\n\n    @JsonCreator\n    @Builder.Constructor\n    public TriggerEntry(@JsonProperty(\"id\") UUID id,\n                        @JsonProperty(\"orgId\") UUID orgId,\n                        @JsonProperty(\"orgName\") String orgName,\n                        @JsonProperty(\"projectId\") UUID projectId,\n                        @JsonProperty(\"projectName\") String projectName,\n                        @JsonProperty(\"repositoryId\") UUID repositoryId,\n                        @JsonProperty(\"repositoryName\") String repositoryName,\n                        @JsonProperty(\"eventSource\") String eventSource,\n                        @JsonProperty(\"activeProfiles\") List<String> activeProfiles,\n                        @JsonProperty(\"arguments\") Map<String, Object> arguments,\n                        @JsonProperty(\"conditions\") Map<String, Object> conditions,\n                        @JsonProperty(\"cfg\") Map<String, Object> cfg) {\n\n        this.id = id;\n        this.orgId = orgId;\n        this.orgName = orgName;\n        this.projectId = projectId;\n        this.projectName = projectName;\n        this.repositoryId = repositoryId;\n        this.repositoryName = repositoryName;\n        this.eventSource = eventSource;\n        this.activeProfiles = activeProfiles;\n        this.arguments = arguments;\n        this.conditions = conditions;\n        this.cfg = cfg;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public UUID getOrgId() {\n        return orgId;\n    }\n\n    public String getOrgName() {\n        return orgName;\n    }\n\n    public UUID getProjectId() {\n        return projectId;\n    }\n\n    public String getProjectName() {\n        return projectName;\n    }\n\n    public UUID getRepositoryId() {\n        return repositoryId;\n    }\n\n    public String getRepositoryName() {\n        return repositoryName;\n    }\n\n    public String getEventSource() {\n        return eventSource;\n    }\n\n    public List<String> getActiveProfiles() {\n        return activeProfiles;\n    }\n\n    public Map<String, Object> getArguments() {\n        return arguments;\n    }\n\n    public Map<String, Object> getConditions() {\n        return conditions;\n    }\n\n    public Map<String, Object> getCfg() {\n        return cfg;\n    }\n\n    @Override\n    public String toString() {\n        return \"TriggerEntry{\" +\n                \"id=\" + id +\n                \", orgId=\" + orgId +\n                \", orgName='\" + orgName + '\\'' +\n                \", projectId=\" + projectId +\n                \", projectName='\" + projectName + '\\'' +\n                \", repositoryId=\" + repositoryId +\n                \", repositoryName='\" + repositoryName + '\\'' +\n                \", eventSource='\" + eventSource + '\\'' +\n                \", activeProfiles=\" + activeProfiles +\n                \", arguments=\" + arguments +\n                \", conditions=\" + conditions +\n                \", cfg=\" + cfg +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/triggers/TriggerInternalIdCalculator.java",
    "content": "package com.walmartlabs.concord.server.org.triggers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.hash.HashFunction;\nimport com.google.common.hash.Hasher;\nimport com.google.common.hash.Hashing;\n\nimport java.nio.charset.StandardCharsets;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\n\npublic final class TriggerInternalIdCalculator {\n\n    @SuppressWarnings(\"UnstableApiUsage\")\n    public static String getId(String name, List<String> activeProfiles, Map<String, Object> arguments, Map<String, Object> conditions, Map<String, Object> cfg) {\n        HashFunction hf = Hashing.sha256();\n        return hf.newHasher()\n                .putUnencodedChars(name)\n                .putObject(ensureList(activeProfiles), (from, into) -> from.stream().sorted().forEach(p -> into.putString(p, StandardCharsets.UTF_8)))\n                .putUnencodedChars(objectToString(arguments))\n                .putUnencodedChars(objectToString(conditions))\n                .putUnencodedChars(objectToString(cfg))\n                .hash()\n                .toString();\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static String objectToString(Object o) {\n        if (o == null) {\n            return \"\";\n        }\n\n        if (o instanceof Collection) {\n            return hashCollection((Collection<Object>) o);\n        } else if (o instanceof Map) {\n            return hashMap((Map<String, Object>) o);\n        }\n\n        return o.toString();\n    }\n\n    @SuppressWarnings(\"UnstableApiUsage\")\n    private static String hashCollection(Collection<Object> collection) {\n        Hasher hasher = Hashing.sha256().newHasher();\n        collection.stream()\n                .map(TriggerInternalIdCalculator::objectToString)\n                .sorted()\n                .forEach(hasher::putUnencodedChars);\n        return hasher.hash().toString();\n    }\n\n    @SuppressWarnings(\"UnstableApiUsage\")\n    private static String hashMap(Map<String, Object> map) {\n        Hasher hasher = Hashing.sha256().newHasher();\n\n        map.entrySet().stream()\n                .sorted(Map.Entry.comparingByKey())\n                .forEach(e -> hasher.putUnencodedChars(e.getKey())\n                        .putUnencodedChars(objectToString(e.getValue())));\n\n        return hasher.hash().toString();\n    }\n\n    private static <E> List<E> ensureList(List<E> list) {\n        if (list != null) {\n            return list;\n        }\n        return Collections.emptyList();\n    }\n\n    private TriggerInternalIdCalculator() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/triggers/TriggerManager.java",
    "content": "package com.walmartlabs.concord.server.org.triggers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.collect.ArrayListMultimap;\nimport com.google.common.collect.ListMultimap;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.runtime.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.model.Trigger;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport com.walmartlabs.concord.server.cfg.TriggersConfiguration;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.policy.EntityAction;\nimport com.walmartlabs.concord.server.policy.EntityType;\nimport com.walmartlabs.concord.server.policy.PolicyManager;\nimport com.walmartlabs.concord.server.policy.PolicyUtils;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\n\npublic class TriggerManager extends AbstractDao {\n\n    private static final Logger log = LoggerFactory.getLogger(TriggerManager.class);\n\n    private final ProjectDao projectDao;\n    private final TriggersDao triggersDao;\n    private final PolicyManager policyManager;\n    private final TriggersConfiguration triggersCfg;\n\n    private final CronTriggerProcessor cronTriggerProcessor;\n    private final GithubTriggerEnricher githubTriggerEnricher;\n\n    @Inject\n    public TriggerManager(@MainDB Configuration cfg,\n                          ProjectDao projectDao,\n                          TriggersDao triggersDao,\n                          PolicyManager policyManager,\n                          TriggersConfiguration triggersCfg,\n                          CronTriggerProcessor cronTriggerProcessor,\n                          GithubTriggerEnricher githubTriggerEnricher) {\n\n        super(cfg);\n\n        this.projectDao = projectDao;\n        this.triggersDao = triggersDao;\n        this.policyManager = policyManager;\n        this.triggersCfg = triggersCfg;\n\n        this.cronTriggerProcessor = cronTriggerProcessor;\n        this.githubTriggerEnricher = githubTriggerEnricher;\n    }\n\n    public void refresh(UUID projectId, UUID repoId, ProcessDefinition pd) {\n        tx(tx -> refresh(projectId, repoId, pd));\n    }\n\n    public void refresh(DSLContext tx, UUID projectId, UUID repoId, ProcessDefinition pd) {\n        UUID orgId = projectDao.getOrgId(projectId);\n        for (Trigger t : pd.triggers()) {\n            policyManager.checkEntity(orgId, projectId, EntityType.TRIGGER, EntityAction.CREATE, null, PolicyUtils.triggerToMap(orgId, projectId, t));\n        }\n\n        List<TriggerEntry> currentTriggers = triggersDao.list(tx, projectId, repoId);\n        ListMultimap<String, TriggerEntry> triggerIds = toTriggerIds(currentTriggers);\n\n        pd.triggers().forEach(t -> {\n            t = enrichTriggerDefinition(tx, repoId, t);\n\n            String internalId = TriggerInternalIdCalculator.getId(t.name(), t.activeProfiles(), t.arguments(), t.conditions(), t.configuration());\n            List<TriggerEntry> triggers = triggerIds.get(internalId);\n            if (!triggers.isEmpty()) {\n                triggers.remove(0);\n                return;\n            }\n\n            UUID triggerId = triggersDao.insert(tx,\n                    projectId,\n                    repoId,\n                    t.name(),\n                    t.activeProfiles(),\n                    t.arguments(),\n                    t.conditions(),\n                    t.configuration());\n\n            postProcessTrigger(tx, triggerId, t);\n        });\n\n        if (!triggerIds.isEmpty()) {\n            triggersDao.delete(tx, triggerIds.values().stream().map(TriggerEntry::getId).collect(Collectors.toList()));\n        }\n\n        log.info(\"refresh ['{}', '{}'] -> done, triggers count: {}\", projectId, repoId, pd.triggers().size());\n    }\n\n    public void clearTriggers(DSLContext tx, UUID projectId, UUID repoId) {\n        triggersDao.delete(tx, projectId, repoId);\n    }\n\n    private Trigger enrichTriggerDefinition(DSLContext tx, UUID repoId, Trigger t) {\n        Map<String, Object> conditions = merge(triggersCfg.getDefaultConditions(), t.name(), t.conditions());\n        Map<String, Object> cfg = merge(triggersCfg.getDefaultConfiguration(), t.name(), t.configuration());\n\n        // when we add more trigger types requiring a similar post-processing mechanism\n        // then we should consider creating appropriate interfaces/listeners\n        if (\"github\".equals(t.name())) {\n            conditions = githubTriggerEnricher.enrich(tx, repoId, conditions);\n        }\n\n        return Trigger.builder().from(t)\n                .conditions(conditions)\n                .configuration(cfg)\n                .build();\n    }\n\n    private void postProcessTrigger(DSLContext tx, UUID triggerId, Trigger t) {\n        if (\"cron\".equals(t.name())) {\n            cronTriggerProcessor.process(tx, triggerId, t);\n        }\n    }\n\n    private static ListMultimap<String, TriggerEntry> toTriggerIds(List<TriggerEntry> triggers) {\n        ListMultimap<String, TriggerEntry> result = ArrayListMultimap.create();\n        for (TriggerEntry t : triggers) {\n            String internalId = TriggerInternalIdCalculator.getId(t.getEventSource(), t.getActiveProfiles(), t.getArguments(), t.getConditions(), t.getCfg());\n            result.put(internalId, t);\n        }\n        return result;\n    }\n\n    private static Map<String, Object> merge(Map<String, Object> cfg, String key, Map<String, Object> original) {\n        if (cfg == null) {\n            return original;\n        }\n\n        Map<String, Object> m = MapUtils.getMap(cfg, key, null);\n        if (m == null) {\n            m = MapUtils.getMap(cfg, \"_\", Collections.emptyMap());\n        }\n\n        return ConfigurationUtils.deepMerge(m, original);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/triggers/TriggerResource.java",
    "content": "package com.walmartlabs.concord.server.org.triggers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.imports.ImportsListener;\nimport com.walmartlabs.concord.process.loader.DelegatingProjectLoader;\nimport com.walmartlabs.concord.process.loader.ProjectLoader;\nimport com.walmartlabs.concord.runtime.model.ProcessDefinition;\nimport com.walmartlabs.concord.repository.Repository;\nimport com.walmartlabs.concord.server.org.OrganizationEntry;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.org.project.*;\nimport com.walmartlabs.concord.server.process.ImportsNormalizerFactory;\nimport com.walmartlabs.concord.server.repository.RepositoryManager;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.GET;\nimport javax.ws.rs.POST;\nimport javax.ws.rs.PathParam;\nimport javax.ws.rs.Produces;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport java.util.List;\nimport java.util.UUID;\n\n@javax.ws.rs.Path(\"/api/v1/org\")\n@Tag(name = \"Triggers\")\npublic class TriggerResource implements Resource {\n\n    private static final Logger log = LoggerFactory.getLogger(TriggerResource.class);\n\n    private final RepositoryDao repositoryDao;\n    private final TriggersDao triggersDao;\n    private final RepositoryManager repositoryManager;\n    private final ProjectAccessManager projectAccessManager;\n    private final OrganizationManager orgManager;\n    private final TriggerManager triggerManager;\n    private final DelegatingProjectLoader projectLoader;\n    private final ImportsNormalizerFactory importsNormalizerFactory;\n\n    private final ProjectRepositoryManager projectRepositoryManager;\n\n    @Inject\n    public TriggerResource(RepositoryDao repositoryDao,\n                           TriggersDao triggersDao,\n                           RepositoryManager repositoryManager,\n                           ProjectAccessManager projectAccessManager,\n                           OrganizationManager orgManager,\n                           TriggerManager triggerManager,\n                           DelegatingProjectLoader projectLoader,\n                           ImportsNormalizerFactory importsNormalizerFactory,\n                           ProjectRepositoryManager projectRepositoryManager) {\n\n        this.repositoryDao = repositoryDao;\n        this.triggersDao = triggersDao;\n        this.repositoryManager = repositoryManager;\n        this.projectAccessManager = projectAccessManager;\n        this.orgManager = orgManager;\n        this.triggerManager = triggerManager;\n        this.projectLoader = projectLoader;\n        this.importsNormalizerFactory = importsNormalizerFactory;\n        this.projectRepositoryManager = projectRepositoryManager;\n    }\n\n    /**\n     * List process trigger definitions for the specified project and repository.\n     */\n    @GET\n    @javax.ws.rs.Path(\"/{orgName}/project/{projectName}/repo/{repositoryName}/trigger\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"List trigger definitions\", operationId = \"listTriggers\")\n    public List<TriggerEntry> list(@PathParam(\"orgName\") @ConcordKey String orgName,\n                                   @PathParam(\"projectName\") @ConcordKey String projectName,\n                                   @PathParam(\"repositoryName\") @ConcordKey String repositoryName) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n        ProjectEntry p = assertProject(org.getId(), projectName, ResourceAccessLevel.READER, true);\n        RepositoryEntry r = projectRepositoryManager.get(p.getId(), repositoryName);\n        if (r == null) {\n            throw new ValidationErrorsException(\"Repository not found: \" + repositoryName);\n        }\n        return triggersDao.list(p.getId(), r.getId());\n    }\n\n    /**\n     * Refresh process trigger definitions for all projects.\n     */\n    @POST\n    @javax.ws.rs.Path(\"/trigger/refresh\")\n    @Operation(description = \"Refresh trigger definitions for all projects\", operationId = \"refreshAllTriggers\")\n    public Response refreshAll() {\n        assertAdmin();\n        repositoryDao.list().parallelStream().forEach(r -> {\n            try {\n                refresh(r);\n            } catch (Exception e) {\n                log.warn(\"refreshAll -> {} refresh failed: {}\", r.getId(), e.getMessage());\n            }\n        });\n        return Response.ok().build();\n    }\n\n    /**\n     * Refresh process trigger definitions for the specified project and repository.\n     */\n    @POST\n    @Operation(description = \"Refresh trigger definitions for the specified project and repository\", operationId = \"refreshTriggers\")\n    @javax.ws.rs.Path(\"/{orgName}/project/{projectName}/repo/{repositoryName}/trigger\")\n    public Response refresh(@PathParam(\"orgName\") @ConcordKey String orgName,\n                            @PathParam(\"projectName\") @ConcordKey String projectName,\n                            @PathParam(\"repositoryName\") @ConcordKey String repositoryName) {\n\n        OrganizationEntry org = orgManager.assertAccess(orgName, true);\n\n        // allow READERs to refresh triggers - it helps with troubleshooting\n        ProjectEntry p = assertProject(org.getId(), projectName, ResourceAccessLevel.READER, true);\n        RepositoryEntry r = projectRepositoryManager.get(p.getId(), repositoryName);\n\n        refresh(r);\n\n        return Response.ok().build();\n    }\n\n    private void refresh(RepositoryEntry repo) {\n        ProcessDefinition pd;\n        try {\n            pd = repositoryManager.withLock(repo.getUrl(), () -> {\n                Repository repository = repositoryManager.fetch(repo.getProjectId(), repo);\n                ProjectLoader.Result result = projectLoader.loadProject(repository.path(), importsNormalizerFactory.forProject(repo.getProjectId()), ImportsListener.NOP_LISTENER);\n                return result.projectDefinition();\n            });\n\n            ProjectValidator.Result result = ProjectValidator.validate(pd);\n            if (!result.isValid()) {\n                throw new ValidationErrorsException(String.join(\"\\n\", result.getErrors()));\n            }\n        } catch (Exception e) {\n            log.error(\"refresh ['{}'] -> project load error\", repo.getId(), e);\n            throw new ConcordApplicationException(\"Refresh failed (repository ID: \" + repo.getId() + \"): \" + e.getMessage(), e);\n        }\n\n        triggerManager.refresh(repo.getProjectId(), repo.getId(), pd);\n    }\n\n    private ProjectEntry assertProject(UUID orgId, String projectName, ResourceAccessLevel accessLevel, boolean orgMembersOnly) {\n        if (projectName == null) {\n            throw new ValidationErrorsException(\"Invalid project name\");\n        }\n\n        return projectAccessManager.assertAccess(orgId, null, projectName, accessLevel, orgMembersOnly);\n    }\n\n    private static void assertAdmin() {\n        if (!Roles.isAdmin()) {\n            throw new UnauthorizedException(\"Not authorized, admin access required\");\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/triggers/TriggerRunAs.java",
    "content": "package com.walmartlabs.concord.server.org.triggers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.MapUtils;\n\nimport java.util.Map;\n\npublic class TriggerRunAs {\n\n    public static TriggerRunAs from(Map<String, Object> runAs) {\n        if (runAs == null || runAs.isEmpty()) {\n            return null;\n        }\n\n        return new TriggerRunAs(runAs);\n    }\n\n    private final Map<String, Object> params;\n\n    private TriggerRunAs(Map<String, Object> params) {\n        this.params = params;\n    }\n\n    public String secretName() {\n        return MapUtils.assertString(params, \"withSecret\");\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/triggers/TriggerScheduleDao.java",
    "content": "package com.walmartlabs.concord.server.org.triggers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.jooq.tables.Organizations;\nimport com.walmartlabs.concord.server.jooq.tables.Projects;\nimport com.walmartlabs.concord.server.jooq.tables.Repositories;\nimport com.walmartlabs.concord.server.jooq.tables.Triggers;\nimport org.jooq.*;\n\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\nimport java.time.ZoneId;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.db.PgUtils.jsonbStripNulls;\nimport static com.walmartlabs.concord.server.jooq.Tables.*;\nimport static com.walmartlabs.concord.server.jooq.tables.TriggerSchedule.TRIGGER_SCHEDULE;\nimport static com.walmartlabs.concord.server.jooq.tables.Triggers.TRIGGERS;\nimport static org.jooq.impl.DSL.*;\n\npublic class TriggerScheduleDao extends AbstractDao {\n\n    private final ConcordObjectMapper objectMapper;\n\n    @Inject\n    public TriggerScheduleDao(@MainDB Configuration cfg,\n                              ConcordObjectMapper objectMapper) {\n\n        super(cfg);\n        this.objectMapper = objectMapper;\n    }\n\n    public TriggerSchedulerEntry findNext() {\n        return txResult(tx -> {\n            // TODO fetch everything in a single request?\n            Map<String, Object> e = tx.select(TRIGGER_SCHEDULE.TRIGGER_ID, TRIGGER_SCHEDULE.FIRE_AT)\n                    .from(TRIGGER_SCHEDULE)\n                    .where(TRIGGER_SCHEDULE.FIRE_AT.le(currentOffsetDateTime()))\n                    .limit(1)\n                    .forUpdate()\n                    .skipLocked()\n                    .fetchOneMap();\n\n            if (e == null) {\n                return null;\n            }\n\n            UUID id = (UUID) e.get(TRIGGER_SCHEDULE.TRIGGER_ID.getName());\n            OffsetDateTime fireAt = (OffsetDateTime) e.get(TRIGGER_SCHEDULE.FIRE_AT.getName());\n\n            Triggers t = TRIGGERS.as(\"t\");\n            Projects p = PROJECTS.as(\"p\");\n            Repositories r = REPOSITORIES.as(\"r\");\n            Organizations o = ORGANIZATIONS.as(\"o\");\n\n            Field<UUID> orgIdField = select(p.ORG_ID).from(p).where(p.PROJECT_ID.eq(t.PROJECT_ID)).asField();\n\n            Record13<UUID, UUID, String, UUID, String, UUID, String, String[], JSONB, JSONB, JSONB, OffsetDateTime, String> record = tx.select(\n                    t.TRIGGER_ID,\n                    orgIdField,\n                    o.ORG_NAME,\n                    t.PROJECT_ID,\n                    p.PROJECT_NAME,\n                    t.REPO_ID,\n                    r.REPO_NAME,\n                    t.ACTIVE_PROFILES,\n                    jsonbStripNulls(t.ARGUMENTS),\n                    jsonbStripNulls(t.TRIGGER_CFG),\n                    jsonbStripNulls(t.CONDITIONS),\n                    currentOffsetDateTime(),\n                    t.EVENT_SOURCE)\n                    .from(t, p, r, o)\n                    .where(t.TRIGGER_ID.eq(id).\n                            and(t.PROJECT_ID.eq(p.PROJECT_ID)).\n                            and(p.PROJECT_ID.eq(r.PROJECT_ID)).\n                            and(p.ORG_ID.eq(o.ORG_ID)).\n                            and(t.REPO_ID.eq(r.REPO_ID)))\n                    .fetchOne();\n\n            if (record == null) {\n                return null;\n            }\n\n            OffsetDateTime now = record.value12();\n            Map<String, Object> conditions = objectMapper.fromJSONB(record.value11());\n\n            ZoneId zoneId = null;\n            if (conditions.get(Constants.Trigger.CRON_TIMEZONE) != null) {\n                zoneId = TimeZone.getTimeZone((String) conditions.get(Constants.Trigger.CRON_TIMEZONE)).toZoneId();\n            }\n\n            OffsetDateTime nextExecutionAt = CronUtils.nextExecution(now, (String) conditions.get(Constants.Trigger.CRON_SPEC), zoneId);\n            updateFireAt(tx, id, nextExecutionAt);\n\n            Map<String, Object> arguments = objectMapper.fromJSONB(record.value9());\n            Map<String, Object> cfg = objectMapper.fromJSONB(record.value10());\n\n            TriggerEntry triggerEntry = new TriggerEntryBuilder()\n                    .id(record.value1())\n                    .orgId(record.value2())\n                    .orgName(record.value3())\n                    .projectId(record.value4())\n                    .projectName(record.value5())\n                    .repositoryId(record.value6())\n                    .repositoryName(record.value7())\n                    .eventSource(record.value13())\n                    .activeProfiles(toList(record.value8()))\n                    .arguments(arguments != null ? arguments : Collections.emptyMap())\n                    .conditions(conditions)\n                    .cfg(cfg != null ? cfg : Collections.emptyMap())\n                    .build();\n\n            return TriggerSchedulerEntry.builder()\n                    .fireAt(fireAt)\n                    .nextExecutionAt(nextExecutionAt)\n                    .trigger(triggerEntry)\n                    .build();\n        });\n    }\n\n    public OffsetDateTime now() {\n        return txResult(tx -> tx.select(currentOffsetDateTime().as(\"now\"))\n                .fetchOne(field(\"now\", OffsetDateTime.class)));\n    }\n\n    public void insert(DSLContext tx, UUID triggerId, OffsetDateTime fireAt) {\n        tx.insertInto(TRIGGER_SCHEDULE)\n                .columns(TRIGGER_SCHEDULE.TRIGGER_ID, TRIGGER_SCHEDULE.FIRE_AT)\n                .values(triggerId, fireAt)\n                .execute();\n    }\n\n    public void remove(UUID triggerId) {\n        tx(tx -> tx.deleteFrom(TRIGGER_SCHEDULE)\n                .where(TRIGGER_SCHEDULE.TRIGGER_ID.eq(triggerId))\n                .execute());\n    }\n\n    private static void updateFireAt(DSLContext tx, UUID triggerId, OffsetDateTime fireAt) {\n        tx.update(TRIGGER_SCHEDULE)\n                .set(TRIGGER_SCHEDULE.FIRE_AT, fireAt)\n                .where(TRIGGER_SCHEDULE.TRIGGER_ID.eq(triggerId))\n                .execute();\n    }\n\n    private static <E> List<E> toList(E[] arr) {\n        if (arr == null) {\n            return Collections.emptyList();\n        }\n        return Arrays.asList(arr);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/triggers/TriggerScheduler.java",
    "content": "package com.walmartlabs.concord.server.org.triggers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.DateTimeUtils;\nimport com.walmartlabs.concord.policyengine.CheckResult;\nimport com.walmartlabs.concord.policyengine.CronTriggerRule;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport com.walmartlabs.concord.server.audit.ActionSource;\nimport com.walmartlabs.concord.server.audit.AuditAction;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.audit.AuditObject;\nimport com.walmartlabs.concord.server.cfg.TriggersConfiguration;\nimport com.walmartlabs.concord.server.org.project.RepositoryDao;\nimport com.walmartlabs.concord.server.org.secret.SecretManager;\nimport com.walmartlabs.concord.server.policy.PolicyManager;\nimport com.walmartlabs.concord.server.process.*;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.ScheduledTask;\nimport com.walmartlabs.concord.server.security.UserSecurityContext;\nimport com.walmartlabs.concord.server.security.apikey.ApiKeyEntry;\nimport com.walmartlabs.concord.server.user.UserEntry;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport org.immutables.value.Value;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.text.MessageFormat;\nimport java.time.Duration;\nimport java.time.OffsetDateTime;\nimport java.util.*;\nimport java.util.concurrent.TimeUnit;\n\npublic class TriggerScheduler implements ScheduledTask {\n\n    private static final Logger log = LoggerFactory.getLogger(TriggerScheduler.class);\n\n    private static final String DEFAULT_POLICY_MESSAGE = \"process start interval can''t be less then {2}\";\n\n    private static final Initiator CRON = Initiator.of(UUID.fromString(\"1f9ae527-e7ab-42c0-b0e5-0092f9285f22\"), \"cron\");\n\n    private static final String EVENT_SOURCE = \"cron\";\n\n    private final OffsetDateTime startedAt;\n    private final TriggerScheduleDao scheduleDao;\n    private final RepositoryDao repositoryDao;\n    private final ProcessManager processManager;\n    private final UserSecurityContext userSecurityContext;\n    private final TriggersConfiguration triggerCfg;\n    private final SecretManager secretManager;\n    private final UserManager userManager;\n    private final AuditLog auditLog;\n    private final PolicyManager policyManager;\n\n    @Inject\n    public TriggerScheduler(TriggerScheduleDao scheduleDao,\n                            RepositoryDao repositoryDao,\n                            ProcessManager processManager,\n                            UserSecurityContext userSecurityContext,\n                            TriggersConfiguration triggerCfg,\n                            SecretManager secretManager,\n                            UserManager userManager,\n                            AuditLog auditLog,\n                            PolicyManager policyManager) {\n        this.secretManager = secretManager;\n        this.userManager = userManager;\n        this.auditLog = auditLog;\n        this.policyManager = policyManager;\n\n        this.startedAt = OffsetDateTime.now();\n        this.scheduleDao = scheduleDao;\n        this.repositoryDao = repositoryDao;\n        this.processManager = processManager;\n        this.userSecurityContext = userSecurityContext;\n        this.triggerCfg = triggerCfg;\n    }\n\n    @Override\n    public String getId() {\n        return \"trigger-scheduler\";\n    }\n\n    @Override\n    public long getIntervalInSec() {\n        return TimeUnit.MINUTES.toSeconds(1);\n    }\n\n    @Override\n    public void performTask() {\n        while (!Thread.currentThread().isInterrupted()) {\n            TriggerSchedulerEntry e = scheduleDao.findNext();\n            if (e == null) {\n                break;\n            }\n            if (e.fireAt().isAfter(startedAt)) {\n                startProcess(e);\n            }\n        }\n    }\n\n    private void startProcess(TriggerSchedulerEntry triggerSchedulerEntry) {\n        if (isDisabled(EVENT_SOURCE)) {\n            log.warn(\"startProcess ['{}'] -> disabled, skipping\", triggerSchedulerEntry);\n            return;\n        }\n\n        TriggerEntry t = triggerSchedulerEntry.trigger();\n        if (isRepositoryDisabled(t.getRepositoryId())) {\n            log.warn(\"startProcess ['{}'] -> repository is disabled, skipping\", triggerSchedulerEntry);\n            scheduleDao.remove(t.getId());\n            return;\n        }\n\n        List<CheckResult.Item<CronTriggerRule, Duration>> deny = checkPolicy(triggerSchedulerEntry);\n        if (!deny.isEmpty()) {\n            log.warn(\"startProcess ['{}'] -> policy violated\", triggerSchedulerEntry);\n            logFailedToStart(t, buildErrorMessage(deny));\n            scheduleDao.remove(t.getId());\n            return;\n        }\n\n        log.info(\"run -> starting {}...\", t);\n\n        Map<String, Object> args = new HashMap<>();\n        if (t.getArguments() != null) {\n            args.putAll(t.getArguments());\n        }\n        args.put(\"event\", makeEvent(triggerSchedulerEntry.fireAt(), t));\n\n        Map<String, Object> cfg = new HashMap<>(t.getCfg());\n        cfg.put(Constants.Request.ARGUMENTS_KEY, args);\n\n        PartialProcessKey processKey = PartialProcessKey.create();\n        UUID triggerId = t.getId();\n        UUID orgId = t.getOrgId();\n        UUID projectId = t.getProjectId();\n        UUID repoId = t.getRepositoryId();\n        String entryPoint = TriggerUtils.getEntryPoint(t);\n        Collection<String> activeProfiles = t.getActiveProfiles();\n\n        Initiator initiator;\n        try {\n            initiator = getInitiator(t);\n        } catch (Exception e) {\n            log.error(\"startProcess ['{}', '{}', '{}', '{}', '{}', {}] -> error getting initiator: {}\",\n                    triggerId, orgId, projectId, repoId, entryPoint, activeProfiles, e.getMessage());\n            logFailedToStart(t, e.getMessage());\n            return;\n        }\n\n        Payload payload;\n        try {\n            payload = PayloadBuilder.start(processKey)\n                    .initiator(initiator.id(), initiator.name())\n                    .organization(orgId)\n                    .project(projectId)\n                    .repository(repoId)\n                    .entryPoint(entryPoint)\n                    .activeProfiles(activeProfiles)\n                    .triggeredBy(TriggeredByEntry.builder().trigger(t).build())\n                    .configuration(cfg)\n                    .build();\n        } catch (Exception e) {\n            log.error(\"startProcess ['{}', '{}', '{}', '{}', '{}', {}] -> error creating a payload\",\n                    triggerId, orgId, projectId, repoId, entryPoint, activeProfiles, e);\n            logFailedToStart(t, e.getMessage());\n            return;\n        }\n\n        try {\n            userSecurityContext.runAs(initiator.id(), () -> processManager.start(payload));\n        } catch (Exception e) {\n            log.error(\"startProcess ['{}', '{}', '{}', '{}', '{}'] -> error starting process\",\n                    triggerId, orgId, projectId, repoId, entryPoint, e);\n            logFailedToStart(t, e.getMessage());\n            return;\n        }\n\n        log.info(\"startProcess ['{}', '{}', '{}', '{}', '{}'] -> process '{}' started\",\n                triggerId, orgId, projectId, repoId, entryPoint, processKey);\n    }\n\n    private List<CheckResult.Item<CronTriggerRule, Duration>> checkPolicy(TriggerSchedulerEntry entry) {\n        PolicyEngine policy = policyManager.get(entry.trigger().getOrgId(), entry.trigger().getProjectId(), null);\n        if (policy == null) {\n            return Collections.emptyList();\n        }\n\n        CheckResult<CronTriggerRule, Duration> result = policy.getCronTriggerPolicy().check(entry.fireAt(), entry.nextExecutionAt());\n        return result.getDeny();\n    }\n\n    private void logFailedToStart(TriggerEntry t, String msg) {\n        try {\n            userSecurityContext.runAs(CRON.id(), () -> {\n                AuditLog.withActionSource(ActionSource.SYSTEM, Collections.emptyMap(), () -> auditLog.add(AuditObject.PROCESS, AuditAction.CREATE)\n                        .field(\"orgId\", t.getOrgId())\n                        .field(\"projectId\", t.getProjectId())\n                        .field(\"status\", \"FAILED\")\n                        .field(\"reason\", msg)\n                        .log());\n                return null;\n            });\n        } catch (Exception e) {\n            log.error(\"logFailedToStart ['{}'] -> error\", t.getId(), e);\n        }\n    }\n\n    private Initiator getInitiator(TriggerEntry t) throws Exception {\n        TriggerRunAs runAs = getRunAs(t);\n        if (runAs == null) {\n            return CRON;\n        }\n\n        ApiKeyEntry apiKey = userSecurityContext.runAs(CRON.id(), () -> secretManager.assertApiKey(SecretManager.AccessScope.project(t.getProjectId()), t.getOrgId(), runAs.secretName(), null));\n        UserEntry u = userManager.get(apiKey.getUserId()).orElse(null);\n        if (u == null) {\n            throw new RuntimeException(\"Can't find user with API token from secret '\" + runAs.secretName() + \"'\");\n        }\n        if (u.isDisabled()) {\n            throw new RuntimeException(\"User '\" + u.getName() + \"' (\" + u.getId() + \") disabled\");\n        }\n        return Initiator.of(u.getId(), u.getName());\n    }\n\n    private boolean isRepositoryDisabled(UUID repositoryId) {\n        return repositoryDao.get(repositoryId).isDisabled();\n    }\n\n    private boolean isDisabled(String eventName) {\n        return triggerCfg.isDisableAll() || triggerCfg.getDisabled().contains(eventName);\n    }\n\n    private static TriggerRunAs getRunAs(TriggerEntry t) {\n        return TriggerRunAs.from(MapUtils.getMap(t.getCfg(), \"runAs\", Collections.emptyMap()));\n    }\n\n    private static Map<String, Object> makeEvent(OffsetDateTime fireAt, TriggerEntry t) {\n        Map<String, Object> m = new HashMap<>();\n        m.put(Constants.Trigger.CRON_SPEC, t.getConditions().get(Constants.Trigger.CRON_SPEC));\n        m.put(Constants.Trigger.CRON_TIMEZONE, t.getConditions().get(Constants.Trigger.CRON_TIMEZONE));\n        m.put(Constants.Trigger.CRON_EVENT_FIREAT, DateTimeUtils.toIsoString(fireAt));\n        return m;\n    }\n\n\n    private static String buildErrorMessage(List<CheckResult.Item<CronTriggerRule, Duration>> errors) {\n        StringBuilder sb = new StringBuilder();\n        for (CheckResult.Item<CronTriggerRule, Duration> e : errors) {\n            CronTriggerRule r = e.getRule();\n\n            String msg = r.msg() != null ? r.msg() : DEFAULT_POLICY_MESSAGE;\n            Duration actual = e.getEntity();\n            long min = r.minInterval();\n\n            sb.append(MessageFormat.format(Objects.requireNonNull(msg), actual, min, Duration.ofSeconds(min))).append(';');\n        }\n        return sb.toString();\n    }\n\n    @Value.Immutable\n    interface Initiator {\n\n        @Value.Parameter\n        UUID id();\n\n        @Value.Parameter\n        String name();\n\n        static Initiator of(UUID id, String name) {\n            return ImmutableInitiator.of(id, name);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/triggers/TriggerSchedulerEntry.java",
    "content": "package com.walmartlabs.concord.server.org.triggers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport java.time.OffsetDateTime;\n\n@Value.Immutable\npublic interface TriggerSchedulerEntry {\n\n    long serialVersionUID = 1L;\n\n    OffsetDateTime fireAt();\n\n    OffsetDateTime nextExecutionAt();\n\n    TriggerEntry trigger();\n\n    static ImmutableTriggerSchedulerEntry.Builder builder() {\n        return ImmutableTriggerSchedulerEntry.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/triggers/TriggerUtils.java",
    "content": "package com.walmartlabs.concord.server.org.triggers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\n\nimport java.util.Collections;\nimport java.util.Map;\n\npublic final class TriggerUtils {\n\n    public static String getEntryPoint(TriggerEntry t) {\n        return MapUtils.getString(t.getCfg(), Constants.Request.ENTRY_POINT_KEY);\n    }\n\n    public static boolean isUseInitiator(TriggerEntry t) {\n        return MapUtils.getBoolean(t.getCfg(), Constants.Trigger.USE_INITIATOR, false);\n    }\n\n    public static boolean isUseEventCommitId(TriggerEntry t) {\n        return MapUtils.getBoolean(t.getCfg(), Constants.Trigger.USE_EVENT_COMMIT_ID, false);\n    }\n\n    public static Map<String, Object> getExclusive(TriggerEntry t) {\n        return MapUtils.getMap(t.getCfg(), Constants.Request.EXCLUSIVE, Collections.emptyMap());\n    }\n\n    private TriggerUtils() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/triggers/TriggerV2Resource.java",
    "content": "package com.walmartlabs.concord.server.org.triggers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.server.org.OrganizationDao;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.org.project.*;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.GET;\nimport javax.ws.rs.Path;\nimport javax.ws.rs.Produces;\nimport javax.ws.rs.QueryParam;\nimport javax.ws.rs.core.MediaType;\nimport java.util.List;\nimport java.util.UUID;\n\n@Path(\"/api/v2/trigger\")\n@Tag(name = \"TriggersV2\")\npublic class TriggerV2Resource implements Resource {\n\n    private final OrganizationDao orgDao;\n    private final ProjectDao projectDao;\n    private final TriggersDao triggersDao;\n    private final ProjectAccessManager projectAccessManager;\n\n    private final ProjectRepositoryManager projectRepositoryManager;\n\n    @Inject\n    public TriggerV2Resource(OrganizationDao orgDao,\n                             ProjectDao projectDao,\n                             TriggersDao triggersDao,\n                             ProjectAccessManager projectAccessManager,\n                             ProjectRepositoryManager projectRepositoryManager) {\n\n        this.orgDao = orgDao;\n        this.projectDao = projectDao;\n        this.triggersDao = triggersDao;\n        this.projectAccessManager = projectAccessManager;\n        this.projectRepositoryManager = projectRepositoryManager;\n    }\n\n    /**\n     * List process trigger definitions for the specified type.\n     */\n    @GET\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"List trigger definitions\", operationId = \"listTriggersV2\")\n    public List<TriggerEntry> list(@QueryParam(\"type\") @ConcordKey String type,\n                                   @QueryParam(\"orgId\") UUID orgId,\n                                   @QueryParam(\"orgName\") @ConcordKey String orgName,\n                                   @QueryParam(\"projectId\") UUID projectId,\n                                   @QueryParam(\"projectName\") @ConcordKey String projectName,\n                                   @QueryParam(\"repoId\") UUID repoId,\n                                   @QueryParam(\"repoName\") @ConcordKey String repoName) {\n\n        // TODO: assert org/project access\n\n        if (type != null && (type.isEmpty() || type.length() > 128)) {\n            throw new ValidationErrorsException(\"Invalid type value: \" + type);\n        }\n\n        if (orgId == null && orgName != null) {\n            orgId = orgDao.getId(orgName);\n            if (orgId == null) {\n                throw new ValidationErrorsException(\"Organization not found: \" + orgName);\n            }\n        }\n\n        if (projectId == null && projectName != null) {\n            if (orgId == null) {\n                throw new IllegalArgumentException(\"Organization ID or name is required\");\n            }\n\n            projectId = projectDao.getId(orgId, projectName);\n            if (projectId == null) {\n                throw new ValidationErrorsException(\"Project not found: \" + projectName);\n            }\n        }\n\n        if (repoId == null && repoName != null) {\n            ProjectEntry p = assertProject(projectId);\n            RepositoryEntry r = projectRepositoryManager.get(p.getId(), repoName);\n            if (r == null) {\n                throw new ValidationErrorsException(\"Repository not found\");\n            }\n            repoId = r.getId();\n        }\n\n        return triggersDao.list(orgId, projectId, repoId, type);\n    }\n\n\n    private ProjectEntry assertProject(UUID projectId) {\n        if (projectId == null) {\n            throw new ValidationErrorsException(\"Invalid project ID or name\");\n        }\n\n        return projectAccessManager.assertAccess(projectId, ResourceAccessLevel.READER, false);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/triggers/TriggersDao.java",
    "content": "package com.walmartlabs.concord.server.org.triggers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.db.PgUtils;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.Utils;\nimport com.walmartlabs.concord.server.UuidGenerator;\nimport com.walmartlabs.concord.server.jooq.tables.Organizations;\nimport com.walmartlabs.concord.server.jooq.tables.Projects;\nimport com.walmartlabs.concord.server.jooq.tables.Repositories;\nimport org.jooq.*;\n\nimport javax.inject.Inject;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.db.PgUtils.jsonbText;\nimport static com.walmartlabs.concord.server.jooq.Tables.*;\nimport static com.walmartlabs.concord.server.jooq.tables.Triggers.TRIGGERS;\nimport static java.util.Objects.requireNonNull;\nimport static org.jooq.impl.DSL.select;\nimport static org.jooq.impl.DSL.value;\n\npublic class TriggersDao extends AbstractDao {\n\n    private final ConcordObjectMapper objectMapper;\n    private final UuidGenerator uuidGenerator;\n\n    @Inject\n    public TriggersDao(@MainDB Configuration cfg,\n                       ConcordObjectMapper objectMapper,\n                       UuidGenerator uuidGenerator) {\n        super(cfg);\n        this.objectMapper = requireNonNull(objectMapper);\n        this.uuidGenerator = requireNonNull(uuidGenerator);\n    }\n\n    public TriggerEntry get(UUID id) {\n        SelectJoinStep<Record12<UUID, UUID, String, UUID, String, UUID, String, String, String[], JSONB, JSONB, JSONB>> query = selectTriggers(dsl());\n\n        return query.where(TRIGGERS.TRIGGER_ID.eq(id))\n                .fetchOne(this::toEntity);\n    }\n\n    public UUID insert(DSLContext tx, UUID projectId, UUID repositoryId, String eventSource, List<String> activeProfiles, Map<String, Object> args, Map<String, Object> conditions, Map<String, Object> config) {\n        UUID triggerId = uuidGenerator.generate();\n        return tx.insertInto(TRIGGERS)\n                .columns(TRIGGERS.TRIGGER_ID, TRIGGERS.PROJECT_ID, TRIGGERS.REPO_ID, TRIGGERS.EVENT_SOURCE, TRIGGERS.ACTIVE_PROFILES, TRIGGERS.ARGUMENTS, TRIGGERS.CONDITIONS, TRIGGERS.TRIGGER_CFG)\n                .values(triggerId, projectId, repositoryId, eventSource, Utils.toArray(activeProfiles), objectMapper.toJSONB(args), objectMapper.toJSONB(conditions), objectMapper.toJSONB(config))\n                .returning(TRIGGERS.TRIGGER_ID)\n                .fetchOne()\n                .getTriggerId();\n    }\n\n    public void delete(UUID projectId, UUID repositoryId) {\n        tx(tx -> delete(tx, projectId, repositoryId));\n    }\n\n    public void delete(DSLContext tx, List<UUID> triggerIds) {\n        tx.delete(TRIGGERS)\n                .where(TRIGGERS.TRIGGER_ID.in(triggerIds))\n                .execute();\n    }\n\n    public void delete(DSLContext tx, UUID projectId, UUID repositoryId) {\n        tx.delete(TRIGGERS)\n                .where(TRIGGERS.PROJECT_ID.eq(projectId).and(TRIGGERS.REPO_ID.eq(repositoryId)))\n                .execute();\n    }\n\n    public List<TriggerEntry> list(UUID projectId, UUID repositoryId) {\n        return list(dsl(), projectId, repositoryId);\n    }\n\n    public List<TriggerEntry> list(DSLContext tx, UUID projectId, UUID repositoryId) {\n        SelectJoinStep<Record12<UUID, UUID, String, UUID, String, UUID, String, String, String[], JSONB, JSONB, JSONB>> query = selectTriggers(tx);\n\n        return query.where(TRIGGERS.PROJECT_ID.eq(projectId).and(TRIGGERS.REPO_ID.eq(repositoryId)))\n                .fetch(this::toEntity);\n    }\n\n    public List<TriggerEntry> list(String eventSource, Integer version) {\n        return list(dsl(), null, eventSource, version, null);\n    }\n\n    public List<TriggerEntry> list(String eventSource, Integer version, Map<String, String> conditions) {\n        return list(dsl(), null, eventSource, version, conditions);\n    }\n\n    public List<TriggerEntry> list(UUID projectId, String eventSource, Integer version, Map<String, String> conditions) {\n        return list(dsl(), projectId, eventSource, version, conditions);\n    }\n\n    private List<TriggerEntry> list(DSLContext tx, UUID projectId, String eventSource, Integer version, Map<String, String> conditions) {\n        SelectJoinStep<Record12<UUID, UUID, String, UUID, String, UUID, String, String, String[], JSONB, JSONB, JSONB>> query = selectTriggers(tx);\n\n        Condition w = TRIGGERS.EVENT_SOURCE.eq(eventSource);\n        if (projectId != null) {\n            w = w.and(TRIGGERS.PROJECT_ID.eq(projectId));\n        }\n\n        if (version != null) {\n            Condition v = jsonbText(TRIGGERS.CONDITIONS, \"version\").eq(String.valueOf(version));\n            if (version == 1) {\n                v = v.or(PgUtils.jsonbText(TRIGGERS.CONDITIONS, \"version\").isNull());\n            }\n            w = w.and(v);\n        }\n\n        return query.where(appendConditionClause(conditions, w))\n                .fetch(this::toEntity);\n    }\n\n    public List<TriggerEntry> list(UUID orgId, UUID projectId, UUID repositoryId, String type) {\n        SelectJoinStep<Record12<UUID, UUID, String, UUID, String, UUID, String, String, String[], JSONB, JSONB, JSONB>> query = selectTriggers(dsl());\n\n        if (orgId != null) {\n            SelectConditionStep<Record1<UUID>> projectIds = select(PROJECTS.PROJECT_ID)\n                    .from(PROJECTS)\n                    .where(PROJECTS.ORG_ID.eq(orgId));\n\n            query.where(TRIGGERS.PROJECT_ID.in(projectIds));\n        }\n\n        if (projectId != null) {\n            query.where(TRIGGERS.PROJECT_ID.eq(projectId));\n        }\n\n        if (repositoryId != null) {\n            query.where(TRIGGERS.REPO_ID.eq(repositoryId));\n        }\n\n        if (type != null) {\n            query.where(TRIGGERS.EVENT_SOURCE.eq(type));\n        }\n\n        return query.fetch(this::toEntity);\n    }\n\n    private SelectJoinStep<Record12<UUID, UUID, String, UUID, String, UUID, String, String, String[], JSONB, JSONB, JSONB>> selectTriggers(DSLContext tx) {\n        Organizations o = ORGANIZATIONS.as(\"o\");\n        Projects p = PROJECTS.as(\"p\");\n        Repositories r = REPOSITORIES.as(\"r\");\n\n        return tx.select(\n                TRIGGERS.TRIGGER_ID,\n                o.ORG_ID,\n                o.ORG_NAME,\n                TRIGGERS.PROJECT_ID,\n                p.PROJECT_NAME,\n                TRIGGERS.REPO_ID,\n                r.REPO_NAME,\n                TRIGGERS.EVENT_SOURCE,\n                TRIGGERS.ACTIVE_PROFILES,\n                TRIGGERS.ARGUMENTS,\n                TRIGGERS.CONDITIONS,\n                TRIGGERS.TRIGGER_CFG)\n                .from(TRIGGERS)\n                .leftJoin(p).on(p.PROJECT_ID.eq(TRIGGERS.PROJECT_ID))\n                .leftJoin(o).on(o.ORG_ID.eq(p.ORG_ID))\n                .leftJoin(r).on(r.REPO_ID.eq(TRIGGERS.REPO_ID));\n    }\n\n    private Condition appendConditionClause(Map<String, String> conditions, Condition w) {\n        if (conditions == null) {\n            return w;\n        }\n\n        for (Map.Entry<String, String> e : conditions.entrySet()) {\n            w = w.and(\n                    jsonbText(TRIGGERS.CONDITIONS, e.getKey()).isNull()\n                            .or(\n                                    value(e.getValue()).likeRegex(jsonbText(TRIGGERS.CONDITIONS, e.getKey()).cast(String.class))));\n        }\n        return w;\n    }\n\n    private TriggerEntry toEntity(Record12<UUID, UUID, String, UUID, String, UUID, String, String, String[], JSONB, JSONB, JSONB> item) {\n        List<String> activeProfiles = null;\n        if (item.value9() != null) {\n            activeProfiles = Arrays.asList(item.value9());\n        }\n\n        return new TriggerEntry(item.value1(), item.value2(), item.value3(), item.value4(), item.value5(), item.value6(),\n                item.value7(), item.value8(), activeProfiles,\n                objectMapper.fromJSONB(item.value10()),\n                objectMapper.fromJSONB(item.value11()),\n                objectMapper.fromJSONB(item.value12()));\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/org/triggers/TriggersModule.java",
    "content": "package com.walmartlabs.concord.server.org.triggers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.walmartlabs.concord.server.Utils.bindJaxRsResource;\nimport static com.walmartlabs.concord.server.Utils.bindSingletonScheduledTask;\n\npublic class TriggersModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        binder.bind(CronTriggerProcessor.class).in(SINGLETON);\n        binder.bind(GithubTriggerEnricher.class).in(SINGLETON);\n        binder.bind(TriggerManager.class).in(SINGLETON);\n        binder.bind(TriggerScheduleDao.class).in(SINGLETON);\n        binder.bind(TriggersDao.class).in(SINGLETON);\n\n        bindJaxRsResource(binder, TriggerResource.class);\n        bindJaxRsResource(binder, TriggerV2Resource.class);\n\n        bindSingletonScheduledTask(binder, TriggerScheduler.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/package-info.java",
    "content": "@Value.Style(jdkOnly = true)\npackage com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/policy/EntityAction.java",
    "content": "package com.walmartlabs.concord.server.policy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic enum EntityAction {\n\n    CREATE (\"create\"),\n    UPDATE (\"update\");\n\n    private final String id;\n\n    EntityAction(String id) {\n        this.id = id;\n    }\n\n    public String id() {\n        return id;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/policy/EntityType.java",
    "content": "package com.walmartlabs.concord.server.policy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic enum EntityType {\n\n    ORGANIZATION(\"org\"),\n    PROJECT(\"project\"),\n    REPOSITORY(\"repository\"),\n    SECRET(\"secret\"),\n    JSON_STORE(\"jsonStore\"),\n    JSON_STORE_ITEM(\"jsonStoreItem\"),\n    JSON_STORE_QUERY(\"jsonStoreQuery\"),\n    TRIGGER(\"trigger\");\n\n    private final String id;\n\n    EntityType(String id) {\n        this.id = id;\n    }\n\n    public String id() {\n        return id;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/policy/PolicyCache.java",
    "content": "package com.walmartlabs.concord.server.policy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.policyengine.PolicyEngineRules;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.cfg.PolicyCacheConfiguration;\nimport com.walmartlabs.concord.server.sdk.BackgroundTask;\nimport org.immutables.value.Value;\nimport org.jooq.Configuration;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport java.util.*;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReadWriteLock;\nimport java.util.concurrent.locks.ReentrantLock;\nimport java.util.concurrent.locks.ReentrantReadWriteLock;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.POLICIES;\nimport static com.walmartlabs.concord.server.jooq.Tables.POLICY_LINKS;\nimport static java.util.concurrent.TimeUnit.MILLISECONDS;\n\npublic class PolicyCache implements BackgroundTask {\n\n    private static final Logger log = LoggerFactory.getLogger(PolicyCache.class);\n    private static final long ERROR_DELAY = 10000;\n\n    private final ObjectMapper objectMapper;\n    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();\n    private final Lock refreshMutex = new ReentrantLock();\n\n    private final PolicyCacheConfiguration cacheCfg;\n    private final Dao dao;\n\n    private PolicyEngine defaultPolicy;\n    private Map<UUID, PolicyEngine> byOrg = Collections.emptyMap();\n    private Map<UUID, PolicyEngine> byProject = Collections.emptyMap();\n    private Map<UUID, PolicyEngine> byUser = Collections.emptyMap();\n    private List<PolicyItem> otherUserPolicies = Collections.emptyList();\n\n    private volatile long lastRefreshRequestAt = -1;\n    private Thread loader;\n\n    @Inject\n    public PolicyCache(ObjectMapper objectMapper, PolicyCacheConfiguration cacheCfg, Dao dao) {\n        this.objectMapper = objectMapper.copy()\n                .setSerializationInclusion(JsonInclude.Include.ALWAYS);\n        this.cacheCfg = cacheCfg;\n        this.dao = dao;\n    }\n\n    @Override\n    public void start() {\n        this.loader = new Thread(this::run, \"policy-cache-loader\");\n        this.loader.start();\n    }\n\n    @Override\n    public void stop() {\n        if (loader != null) {\n            loader.interrupt();\n            loader = null;\n        }\n    }\n\n    public void refresh() {\n        try {\n            reloadPolicies();\n        } catch (Exception e) {\n            refreshMutex.lock();\n            try {\n                lastRefreshRequestAt = System.currentTimeMillis();\n                refreshMutex.notifyAll();\n            } finally {\n                refreshMutex.unlock();\n            }\n        }\n    }\n\n    public PolicyEngine get(UUID orgId, UUID projectId, UUID userId) {\n        Lock l = rwLock.readLock();\n        l.lock();\n        try {\n            return getUnsafe(orgId, projectId, userId);\n        } finally {\n            l.unlock();\n        }\n    }\n\n    private PolicyEngine getUnsafe(UUID orgId, UUID projectId, UUID userId) {\n        if (userId != null) {\n            if (projectId != null) {\n                PolicyEngine result = otherUserPolicies.stream()\n                        .filter(p -> userId.equals(p.link().userId()))\n                        .filter(p -> projectId.equals(p.link().projectId()))\n                        .findAny()\n                        .map(PolicyItem::engine)\n                        .orElse(null);\n\n                if (result != null) {\n                    return result;\n                }\n            }\n\n            if (orgId != null) {\n                PolicyEngine result = otherUserPolicies.stream()\n                        .filter(p -> userId.equals(p.link().userId()))\n                        .filter(p -> orgId.equals(p.link().orgId()))\n                        .findAny()\n                        .map(PolicyItem::engine)\n                        .orElse(null);\n\n                if (result != null) {\n                    return result;\n                }\n            }\n\n            PolicyEngine result = byUser.get(userId);\n            if (result != null) {\n                return result;\n            }\n        }\n\n        if (projectId != null) {\n            PolicyEngine result = byProject.get(projectId);\n            if (result != null) {\n                return result;\n            }\n        }\n\n        if (orgId != null) {\n            PolicyEngine result = byOrg.get(orgId);\n            if (result != null) {\n                return result;\n            }\n        }\n\n        return defaultPolicy;\n    }\n\n    private void run() {\n        while (!Thread.currentThread().isInterrupted()) {\n            try {\n                long now = System.currentTimeMillis();\n                reloadPolicies();\n\n                refreshMutex.lock();\n                try {\n                    if (lastRefreshRequestAt > now) {\n                        lastRefreshRequestAt = now;\n                    } else {\n                        //noinspection ResultOfMethodCallIgnored\n                        refreshMutex.newCondition()\n                                .await(cacheCfg.getReloadInterval().toMillis(), MILLISECONDS);\n                    }\n                } finally {\n                    refreshMutex.unlock();\n                }\n            } catch (InterruptedException e) {\n                Thread.currentThread().interrupt();\n            } catch (Exception e) {\n                log.warn(\"run -> error\", e);\n\n                try {\n                    Thread.sleep(ERROR_DELAY);\n                } catch (InterruptedException ex) {\n                    Thread.currentThread().interrupt();\n                }\n            }\n        }\n    }\n\n    private void reloadPolicies() {\n        PolicyEngine defaultPolicy = null;\n        Map<UUID, PolicyEngine> byOrg = new HashMap<>();\n        Map<UUID, PolicyEngine> byProject = new HashMap<>();\n        Map<UUID, PolicyEngine> byUser = new HashMap<>();\n        List<PolicyItem> otherUserPolicies = new ArrayList<>();\n\n        List<PolicyLink> links = dao.listLinks();\n        if (links.isEmpty()) {\n            setPolicies(defaultPolicy, byOrg, byProject, byUser, otherUserPolicies);\n            return;\n        }\n\n        Map<UUID, Policy> policies = mergePolicies(dao.listPolicies());\n        for (PolicyLink l : links) {\n            Policy policy = policies.get(l.policyId());\n            if (policy == null) {\n                continue;\n            }\n            PolicyEngine pe = new PolicyEngine(policy.policyNames(), policy.rules());\n            if (l.orgId() == null && l.projectId() == null && l.userId() == null) {\n                defaultPolicy = pe;\n            } else if (l.orgId() != null && l.projectId() == null && l.userId() == null) {\n                byOrg.put(l.orgId(), pe);\n            } else if (l.orgId() == null && l.projectId() != null && l.userId() == null) {\n                byProject.put(l.projectId(), pe);\n            } else if (l.orgId() == null && l.projectId() == null && l.userId() != null) {\n                byUser.put(l.userId(), pe);\n            } else if (l.userId() != null) {\n                otherUserPolicies.add(PolicyItem.of(l, pe));\n            } else {\n                log.warn(\"Unexpected policy link: {}\", l);\n            }\n        }\n\n        setPolicies(defaultPolicy, byOrg, byProject, byUser, otherUserPolicies);\n    }\n\n    private void setPolicies(PolicyEngine defaultPolicy,\n                             Map<UUID, PolicyEngine> byOrg,\n                             Map<UUID, PolicyEngine> byProject,\n                             Map<UUID, PolicyEngine> byUser,\n                             List<PolicyItem> otherUserPolicies) {\n\n        Lock l = rwLock.writeLock();\n        l.lock();\n        try {\n            this.defaultPolicy = defaultPolicy;\n            this.byOrg = byOrg;\n            this.byProject = byProject;\n            this.byUser = byUser;\n            this.otherUserPolicies = otherUserPolicies;\n        } finally {\n            l.unlock();\n        }\n    }\n\n    Map<UUID, Policy> mergePolicies(List<PolicyRules> policies) {\n        Map<UUID, Policy> result = new HashMap<>();\n        for (PolicyRules p : policies) {\n            List<PolicyRules> rules = combinePolicies(p, policies);\n            Map<String, Object> mergedRules = mergeRules(rules);\n\n            result.put(p.id(), ImmutablePolicy.builder()\n                    .id(p.id())\n                    .addAllPolicyNames(rules.stream().map(PolicyRules::name).collect(Collectors.toList()))\n                    .rules(objectMapper.convertValue(mergedRules, PolicyEngineRules.class))\n                    .build());\n        }\n        return result;\n    }\n\n    private static Map<String, Object> mergeRules(List<PolicyRules> rules) {\n        Map<String, Object> result = new HashMap<>();\n        for (int i = rules.size() - 1; i >= 0; i--) {\n            result = ConfigurationUtils.deepMerge(result, rules.get(i).rules());\n        }\n        return result;\n    }\n\n    private static List<PolicyRules> combinePolicies(PolicyRules p, List<PolicyRules> policies) {\n        List<PolicyRules> result = new ArrayList<>();\n        result.add(p);\n\n        PolicyRules current = p;\n        while (current != null) {\n            UUID parentId = current.parentId();\n            if (parentId == null) {\n                return result;\n            }\n            PolicyRules parent = policies.stream()\n                    .filter(r -> r.id().equals(parentId))\n                    .findAny().orElse(null);\n            if (parent != null) {\n                result.add(parent);\n            }\n            current = parent;\n        }\n        return result;\n    }\n\n    static class Dao extends AbstractDao {\n\n        private final ConcordObjectMapper objectMapper;\n\n        @Inject\n        public Dao(@MainDB Configuration cfg,\n                   ConcordObjectMapper objectMapper) {\n            super(cfg);\n\n            this.objectMapper = objectMapper;\n        }\n\n        public List<PolicyLink> listLinks() {\n            return txResult(tx -> tx.selectFrom(POLICY_LINKS)\n                    .fetch(r -> ImmutablePolicyLink.builder()\n                            .policyId(r.getPolicyId())\n                            .orgId(r.getOrgId())\n                            .projectId(r.getProjectId())\n                            .userId(r.getUserId())\n                            .build()));\n        }\n\n        public List<PolicyRules> listPolicies() {\n            return txResult(tx -> tx.selectFrom(POLICIES)\n                    .fetch(r -> ImmutablePolicyRules.builder()\n                            .id(r.getPolicyId())\n                            .parentId(r.getParentPolicyId())\n                            .name(r.getPolicyName())\n                            .rules(objectMapper.fromJSONB(r.getRules()))\n                            .build()));\n        }\n    }\n\n    @Value.Immutable\n    interface PolicyLink {\n\n        UUID policyId();\n\n        @Nullable\n        UUID orgId();\n\n        @Nullable\n        UUID projectId();\n\n        @Nullable\n        UUID userId();\n    }\n\n    @Value.Immutable\n    interface PolicyRules {\n\n        UUID id();\n\n        @Nullable\n        UUID parentId();\n\n        String name();\n\n        Map<String, Object> rules();\n\n        static ImmutablePolicyRules.Builder builder() {\n            return ImmutablePolicyRules.builder();\n        }\n    }\n\n    @Value.Immutable\n    interface PolicyItem {\n\n        @Value.Parameter\n        PolicyLink link();\n\n        @Value.Parameter\n        PolicyEngine engine();\n\n        static PolicyItem of(PolicyLink link, PolicyEngine engine) {\n            return ImmutablePolicyItem.of(link, engine);\n        }\n    }\n\n    @Value.Immutable\n    interface Policy {\n\n        UUID id();\n\n        List<String> policyNames();\n\n        PolicyEngineRules rules();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/policy/PolicyException.java",
    "content": "package com.walmartlabs.concord.server.policy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class PolicyException extends RuntimeException {\n\n    private static final long serialVersionUID = 1L;\n\n    public PolicyException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/policy/PolicyManager.java",
    "content": "package com.walmartlabs.concord.server.policy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.policyengine.CheckResult;\nimport com.walmartlabs.concord.policyengine.EntityRule;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.server.org.policy.PolicyDao;\nimport com.walmartlabs.concord.server.org.policy.PolicyEntry;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.user.UserEntry;\nimport com.walmartlabs.concord.server.user.UserInfoProvider;\nimport com.walmartlabs.concord.server.user.UserManager;\n\nimport javax.inject.Inject;\nimport java.util.*;\n\npublic class PolicyManager {\n\n    private final PolicyCache policyCache;\n    private final PolicyDao policyDao;\n    private final UserManager userManager;\n\n    @Inject\n    public PolicyManager(PolicyCache policyCache, PolicyDao policyDao, UserManager userManager) {\n        this.policyCache = policyCache;\n        this.policyDao = policyDao;\n        this.userManager = userManager;\n    }\n\n    public void refresh() {\n        policyCache.refresh();\n    }\n\n    public PolicyEngine get(UUID orgId, UUID projectId, UUID userId) {\n        return policyCache.get(orgId, projectId, userId);\n    }\n\n    public PolicyEntry get(String policyName) {\n        return policyDao.get(policyName);\n    }\n\n    public UUID getId(String policyName) {\n        return policyDao.getId(policyName);\n    }\n\n    public List<PolicyEntry> list() {\n        return policyDao.list();\n    }\n\n    public PolicyEntry getLinked(UUID orgId, UUID projectId, UUID userId) {\n        return policyDao.getLinked(orgId, projectId, userId);\n    }\n\n    public UUID insert(String name, UUID parentId, Map<String, Object> rules) {\n        return policyDao.insert(name, parentId, rules);\n    }\n\n    public void update(UUID id, String name, UUID parentId, Map<String, Object> rules) {\n        policyDao.update(id, name, parentId, rules);\n        policyCache.refresh();\n    }\n\n    public void delete(UUID id) {\n        policyDao.delete(id);\n        policyCache.refresh();\n    }\n\n    public void link(UUID policyId, UUID orgId, UUID projectId, UUID userId) {\n        policyDao.link(policyId, orgId, projectId, userId);\n        policyCache.refresh();\n    }\n\n    public void unlink(UUID policyId, UUID orgId, UUID projectId, UUID userId) {\n        policyDao.unlink(policyId, orgId, projectId, userId);\n        policyCache.refresh();\n    }\n\n    public void checkEntity(UUID orgId, UUID projectId,\n                            EntityType entityType, EntityAction action,\n                            UserEntry owner, Map<String, Object> entityAttrs) {\n\n        PolicyEngine pe = get(orgId, projectId, UserPrincipal.assertCurrent().getId());\n        if (pe == null) {\n            return;\n        }\n\n        CheckResult<EntityRule, Map<String, Object>> result = pe.getEntityPolicy().check(entityType.id(), action.id(), () -> {\n            Map<String, Object> attrs = new HashMap<>();\n            attrs.put(\"owner\", getOwnerAttrs(owner));\n            attrs.put(\"entity\", entityAttrs);\n            return attrs;\n        });\n\n        if (!result.getDeny().isEmpty()) {\n            throw new ValidationErrorsException(\"Action forbidden: \" + result.getDeny().get(0).getRule().msg());\n        }\n    }\n\n    public PolicyEngine getPolicyEngine(Payload payload) {\n        return get(payload.getHeader(Payload.ORGANIZATION_ID), payload.getHeader(Payload.PROJECT_ID), payload.getHeader(Payload.INITIATOR_ID));\n    }\n\n    private Map<String, Object> getOwnerAttrs(UserEntry owner) {\n        if (owner == null) {\n            return Collections.emptyMap();\n        }\n\n        Map<String, Object> attrs = new HashMap<>();\n        attrs.put(\"id\", owner.getId());\n        attrs.put(\"username\", owner.getName());\n        attrs.put(\"userType\", owner.getType().name());\n\n        UserInfoProvider.UserInfo userInfo = userManager.getInfo(owner.getName(), owner.getDomain(), owner.getType());\n        if (userInfo == null) {\n            throw new ValidationErrorsException(\"User not found: \" + owner.getId());\n        }\n\n        attrs.put(\"email\", userInfo.email());\n        attrs.put(\"displayName\", userInfo.displayName());\n        if (userInfo.attributes() != null) {\n            attrs.putAll(Objects.requireNonNull(userInfo.attributes()));\n        }\n\n        return attrs;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/policy/PolicyModule.java",
    "content": "package com.walmartlabs.concord.server.policy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\n\nimport static com.walmartlabs.concord.server.Utils.bindSingletonBackgroundTask;\n\npublic class PolicyModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        bindSingletonBackgroundTask(binder, PolicyCache.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/policy/PolicyUtils.java",
    "content": "package com.walmartlabs.concord.server.policy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.Trigger;\nimport com.walmartlabs.concord.server.org.OrganizationEntry;\nimport com.walmartlabs.concord.server.org.jsonstore.JsonStoreEntry;\nimport com.walmartlabs.concord.server.org.jsonstore.JsonStoreVisibility;\nimport com.walmartlabs.concord.server.org.project.ProjectEntry;\nimport com.walmartlabs.concord.server.org.project.RepositoryEntry;\nimport com.walmartlabs.concord.server.org.secret.SecretEntryV2;\nimport com.walmartlabs.concord.server.org.secret.SecretType;\nimport com.walmartlabs.concord.server.org.secret.SecretVisibility;\nimport com.walmartlabs.concord.server.user.UserEntry;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic final class PolicyUtils {\n\n    public static Map<String, Object> orgToMap(OrganizationEntry entry) {\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"id\", entry.getId());\n        m.put(\"name\", entry.getName());\n        if (entry.getMeta() != null) {\n            m.put(\"meta\", entry.getMeta());\n        }\n        if (entry.getCfg() != null) {\n            m.put(\"cfg\", entry.getCfg());\n        }\n        return m;\n    }\n\n    public static Map<String, Object> projectToMap(UUID orgId,\n                                                   String orgName,\n                                                   ProjectEntry entry) {\n\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"id\", entry.getId());\n        m.put(\"name\", entry.getName());\n        m.put(\"orgId\", orgId);\n        m.put(\"orgName\", orgName);\n        if (entry.getVisibility() != null) {\n            m.put(\"visibility\", entry.getVisibility().name());\n        }\n        if (entry.getMeta() != null) {\n            m.put(\"meta\", entry.getMeta());\n        }\n        if (entry.getCfg() != null) {\n            m.put(\"cfg\", entry.getCfg());\n        }\n        return m;\n    }\n\n    public static Map<String, Object> repositoryToMap(ProjectEntry project,\n                                                      RepositoryEntry repo,\n                                                      SecretEntryV2 secret) {\n        return repositoryToMap(project.getOrgId(), project.getOrgName(), project.getId(), project.getName(), repo, secret);\n    }\n\n    public static Map<String, Object> repositoryToMap(UUID orgId,\n                                                      String orgName,\n                                                      UUID projectId,\n                                                      String projectName,\n                                                      RepositoryEntry repo,\n                                                      SecretEntryV2 secret) {\n\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"orgId\", orgId);\n        m.put(\"orgName\", orgName);\n        m.put(\"projectId\", projectId);\n        m.put(\"projectName\", projectName);\n        m.put(\"name\", repo.getName());\n        m.put(\"url\", repo.getUrl());\n        m.put(\"branch\", repo.getBranch());\n        if (secret != null) {\n            m.put(\"secret\", secretToMap(secret.getOrgId(), secret.getName(), secret.getType(), secret.getVisibility(), secret.getStoreType()));\n        }\n        return m;\n    }\n\n    public static Map<String, Object> secretToMap(UUID orgId,\n                                                  String secretName,\n                                                  SecretType type,\n                                                  SecretVisibility visibility,\n                                                  String storeType) {\n\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"name\", secretName);\n        m.put(\"orgId\", orgId);\n        if (type != null) {\n            m.put(\"type\", type.name());\n        }\n        if (visibility != null) {\n            m.put(\"visibility\", visibility.name());\n        }\n        if (storeType != null) {\n            m.put(\"storeType\", storeType);\n        }\n        return m;\n    }\n\n    public static Map<String, Object> triggerToMap(UUID orgId,\n                                                   UUID projectId,\n                                                   Trigger trigger) {\n\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"eventSource\", trigger.name());\n        m.put(\"orgId\", orgId);\n        m.put(\"projectId\", projectId);\n        m.put(\"arguments\", trigger.arguments() != null ? trigger.arguments() : Collections.emptyMap());\n        m.put(\"params\", trigger.conditions() != null ? trigger.conditions() : Collections.emptyMap());\n        m.put(\"cfg\", trigger.configuration() != null ? trigger.configuration() : Collections.emptyList());\n        return m;\n    }\n\n    public static Map<String, Object> jsonStoreToMap(UUID orgId,\n                                                     String storeName,\n                                                     JsonStoreVisibility visibility,\n                                                     UserEntry owner) {\n\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"orgId\", orgId);\n        m.put(\"name\", storeName);\n        if (visibility != null) {\n            m.put(\"visibility\", visibility.name());\n        }\n        m.putAll(ownerToMap(owner));\n        return m;\n    }\n\n    public static Map<String, Object> jsonStoreQueryToMap(OrganizationEntry org,\n                                                          JsonStoreEntry store,\n                                                          String queryName,\n                                                          String query) {\n\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"name\", queryName);\n        m.put(\"text\", query);\n        m.put(\"orgId\", org.getId());\n        m.put(\"orgName\", org.getName());\n        m.put(\"jsonStoreId\", store.id());\n        m.put(\"jsonStoreName\", store.name());\n        return m;\n    }\n\n    public static Map<String, Object> jsonStoreItemToMap(OrganizationEntry org,\n                                                         JsonStoreEntry store,\n                                                         String itemPath,\n                                                         Object data) {\n\n        Map<String, Object> attrs = new HashMap<>();\n        attrs.put(\"orgId\", org.getId());\n        attrs.put(\"orgName\", org.getName());\n        attrs.put(\"jsonStoreId\", store.id());\n        attrs.put(\"jsonStoreName\", store.name());\n        attrs.put(\"path\", itemPath);\n        attrs.put(\"data\", data);\n        return attrs;\n    }\n\n    private static Map<String, Object> ownerToMap(UserEntry owner) {\n        if (owner == null) {\n            return Collections.emptyMap();\n        }\n        Map<String, Object> result = new HashMap<>();\n        result.put(\"ownerId\", owner.getId());\n        result.put(\"ownerName\", owner.getName());\n        if (owner.getDomain() != null) {\n            result.put(\"ownerDomain\", owner.getDomain());\n        }\n        result.put(\"ownerType\", owner.getType().name());\n        return result;\n    }\n\n    private PolicyUtils() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/ErrorMessage.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@JsonInclude(Include.NON_NULL)\npublic class ErrorMessage implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final UUID instanceId;\n    private final String message;\n    private final String details;\n    private final String stacktrace;\n\n    @JsonCreator\n    public ErrorMessage(@JsonProperty(\"instanceId\") UUID instanceId,\n                        @JsonProperty(\"message\") String message,\n                        @JsonProperty(\"details\") String details,\n                        @JsonProperty(\"stacktrace\") String stacktrace) {\n\n        this.instanceId = instanceId;\n        this.message = message;\n        this.details = details;\n        this.stacktrace = stacktrace;\n    }\n\n    public UUID getInstanceId() {\n        return instanceId;\n    }\n\n    public String getMessage() {\n        return message;\n    }\n\n    public String getDetails() {\n        return details;\n    }\n\n    public String getStacktrace() {\n        return stacktrace;\n    }\n\n    @Override\n    public String toString() {\n        return \"ErrorMessage{\" +\n                \"instanceId=\" + instanceId +\n                \", message='\" + message + '\\'' +\n                \", details='\" + details + '\\'' +\n                \", stacktrace='\" + stacktrace + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/ImportManagerProvider.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.dependencymanager.DependencyManager;\nimport com.walmartlabs.concord.imports.Import.GitDefinition;\nimport com.walmartlabs.concord.imports.Import.SecretDefinition;\nimport com.walmartlabs.concord.imports.ImportManager;\nimport com.walmartlabs.concord.imports.ImportManagerFactory;\nimport com.walmartlabs.concord.imports.RepositoryExporter;\nimport com.walmartlabs.concord.repository.Repository;\nimport com.walmartlabs.concord.repository.Snapshot;\nimport com.walmartlabs.concord.sdk.Secret;\nimport com.walmartlabs.concord.server.cfg.ImportConfiguration;\nimport com.walmartlabs.concord.server.org.OrganizationDao;\nimport com.walmartlabs.concord.server.org.secret.SecretManager;\nimport com.walmartlabs.concord.server.repository.RepositoryManager;\n\nimport javax.inject.Inject;\nimport javax.inject.Provider;\nimport java.nio.file.Path;\nimport java.util.Objects;\nimport java.util.UUID;\n\npublic class ImportManagerProvider implements Provider<ImportManager> {\n\n    private final ImportManagerFactory factory;\n\n    @Inject\n    public ImportManagerProvider(DependencyManager dependencyManager,\n                                 OrganizationDao organizationDao,\n                                 SecretManager secretManager,\n                                 RepositoryManager repositoryManager,\n                                 ImportConfiguration cfg) {\n\n        RepositoryExporterImpl exporter = new RepositoryExporterImpl(organizationDao, secretManager, repositoryManager);\n        this.factory = new ImportManagerFactory(dependencyManager, exporter, cfg.getDisabledProcessors());\n    }\n\n    @Override\n    public ImportManager get() {\n        return factory.create();\n    }\n\n    private static class RepositoryExporterImpl implements RepositoryExporter {\n\n        private final OrganizationDao organizationDao;\n        private final SecretManager secretManager;\n        private final RepositoryManager repositoryManager;\n\n        private RepositoryExporterImpl(OrganizationDao organizationDao, SecretManager secretManager, RepositoryManager repositoryManager) {\n            this.organizationDao = organizationDao;\n            this.secretManager = secretManager;\n            this.repositoryManager = repositoryManager;\n        }\n\n        @Override\n        public Snapshot export(GitDefinition entry, Path workDir) {\n            Secret secret = getSecret(entry.secret());\n            return repositoryManager.withLock(entry.url(), () -> {\n                Repository repository = repositoryManager.fetch(entry.url(), entry.version(), null, entry.path(), secret, false);\n                Path dst = workDir;\n                if (entry.dest() != null) {\n                    dst = dst.resolve(Objects.requireNonNull(entry.dest()));\n                }\n                return repository.export(dst, entry.exclude());\n            });\n        }\n\n        private Secret getSecret(SecretDefinition secret) {\n            if (secret == null) {\n                return null;\n            }\n\n            UUID orgId = organizationDao.getId(secret.org());\n            if (orgId == null) {\n                throw new RuntimeException(\"Error fetching secret '\" + secret.name() + \"': organization '\" + secret.org() + \"' not found\");\n            }\n\n            SecretManager.DecryptedSecret s = secretManager.getSecret(null, orgId, secret.name(), secret.password(), null);\n            if (s == null) {\n                throw new RuntimeException(\"Secret not found: \" + secret.name());\n            }\n\n            return s.getSecret();\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/ImportsNormalizerFactory.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Import;\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.process.loader.ImportsNormalizer;\nimport com.walmartlabs.concord.server.cfg.ImportConfiguration;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\n\nimport javax.inject.Inject;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\n\n/**\n * Creates a {@link ImportsNormalizer} instance to fill the necessary fields\n * in the process' {@code imports} based on the project.\n */\npublic class ImportsNormalizerFactory {\n\n    private static final String DEFAULT_DEST = \"concord\";\n\n    private final ImportConfiguration cfg;\n    private final ProjectDao projectDao;\n\n    @Inject\n    public ImportsNormalizerFactory(ImportConfiguration cfg, ProjectDao projectDao) {\n        this.cfg = cfg;\n        this.projectDao = projectDao;\n    }\n\n    public ImportsNormalizer forProject(UUID projectId) {\n        return imports -> ImportsNormalizerFactory.this.normalize(ImportContext.ofProject(projectId), imports);\n    }\n\n    private Imports normalize(ImportContext ctx, Imports imports) {\n        List<Import> items = imports != null ? imports.items() : null;\n        if (items == null || items.isEmpty()) {\n            return Imports.builder().build();\n        }\n\n        return Imports.of(items.stream()\n                .map(i -> normalize(ctx, i))\n                .collect(Collectors.toList()));\n    }\n\n    private Import normalize(ImportContext ctx, Import i) {\n        switch (i.type()) {\n            case Import.MvnDefinition.TYPE: {\n                Import.MvnDefinition src = (Import.MvnDefinition) i;\n                return Import.MvnDefinition.builder()\n                        .from(src)\n                        .dest(src.dest() != null ? src.dest() : DEFAULT_DEST)\n                        .build();\n            }\n            case Import.GitDefinition.TYPE: {\n                Import.GitDefinition src = (Import.GitDefinition) i;\n                return normalize(ctx, src);\n            }\n            case Import.DirectoryDefinition.TYPE: {\n                return i;\n            }\n            default: {\n                throw new IllegalArgumentException(\"Unsupported import type: '\" + i.type() + \"'\");\n            }\n        }\n    }\n\n    private Import.GitDefinition normalize(ImportContext ctx, Import.GitDefinition e) {\n        String url = e.url();\n        if (url == null) {\n            String name = e.name();\n            url = normalizeUrl(cfg.getSrc()) + name;\n        }\n\n        Import.SecretDefinition secret = e.secret();\n        if (secret != null && secret.org() == null) {\n            String secretOrgName = getOrgName(ctx);\n            if (secretOrgName == null) {\n                throw new IllegalStateException(\"Can't determine the secret's organization: '\" + secret.name() + \"' \" +\n                        \"which is used in one of the 'imports': \" + e);\n            }\n\n            secret = Import.SecretDefinition.builder().from(secret)\n                    .org(secretOrgName)\n                    .build();\n        }\n       \n        return Import.GitDefinition.builder().from(e)\n                .url(url)\n                .version(e.version() != null ? e.version() : cfg.getDefaultBranch())\n                .dest(e.dest() != null ? e.dest() : DEFAULT_DEST)\n                .secret(secret)\n                .build();\n    }\n\n    private String getOrgName(ImportContext ctx) {\n        UUID projectId = ctx.projectId();\n        if (projectId == null) {\n            return null;\n        }\n\n        return projectDao.getOrgName(projectId);\n    }\n\n    private static String normalizeUrl(String u) {\n        if (u.endsWith(\"/\")) {\n            return u;\n        }\n        return u + \"/\";\n    }\n\n    public interface ImportContext {\n\n        static ImportContext ofProject(UUID projectId) {\n            return () -> projectId;\n        }\n\n        UUID projectId();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/LogSegment.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.time.OffsetDateTime;\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableLogSegment.class)\n@JsonDeserialize(as = ImmutableLogSegment.class)\npublic interface LogSegment {\n\n    long id();\n\n    @Nullable\n    UUID correlationId();\n\n    String name();\n\n    @Nullable\n    Status status();\n\n    @Nullable\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    OffsetDateTime statusUpdatedAt();\n\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    OffsetDateTime createdAt();\n\n    @Nullable\n    Integer warnings();\n\n    @Nullable\n    Integer errors();\n\n    enum Status {\n        OK,\n        FAILED,\n        RUNNING,\n        SUSPENDED\n    }\n\n    static ImmutableLogSegment.Builder builder() {\n        return ImmutableLogSegment.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/LogSegmentOperationResponse.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.server.OperationResult;\n\nimport java.io.Serializable;\n\npublic class LogSegmentOperationResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n    private final long id;\n    private final OperationResult result;\n\n    @JsonCreator\n    public LogSegmentOperationResponse(@JsonProperty(\"id\") long id,\n                                       @JsonProperty(\"result\") OperationResult result) {\n        this.id = id;\n        this.result = result;\n    }\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    public long getId() {\n        return id;\n    }\n\n    public OperationResult getResult() {\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"LogSegmentOperationResponse{\" +\n                \"ok=\" + ok +\n                \", id=\" + id +\n                \", result=\" + result +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/LogSegmentRequest.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.time.OffsetDateTime;\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableLogSegmentRequest.class)\n@JsonDeserialize(as = ImmutableLogSegmentRequest.class)\npublic interface LogSegmentRequest {\n\n    @Nullable\n    UUID correlationId();\n\n    String name();\n\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    @Nullable\n    OffsetDateTime createdAt();\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/LogSegmentUpdateRequest.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableLogSegmentUpdateRequest.class)\n@JsonDeserialize(as = ImmutableLogSegmentUpdateRequest.class)\npublic interface LogSegmentUpdateRequest {\n\n    @Nullable\n    LogSegment.Status status();\n\n    @Nullable\n    Integer warnings();\n\n    @Nullable\n    Integer errors();\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/OutVariablesUtils.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.sdk.Constants;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.Map;\n\npublic final class OutVariablesUtils {\n\n    private static final ObjectMapper objectMapper = new ObjectMapper();\n\n    @SuppressWarnings(\"unchecked\")\n    public static Map<String, Object> read(Path attachmentsDir) throws IOException {\n        Path processOut = attachmentsDir.resolve(Constants.Files.OUT_VALUES_FILE_NAME);\n        if (!Files.exists(processOut)) {\n            return Collections.emptyMap();\n        }\n\n        return objectMapper.readValue(processOut.toFile(), Map.class);\n    }\n\n    private OutVariablesUtils() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/Payload.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.runtime.model.ProcessDefinition;\nimport com.walmartlabs.concord.repository.Repository;\nimport com.walmartlabs.concord.repository.Snapshot;\nimport com.walmartlabs.concord.server.process.keys.AttachmentKey;\nimport com.walmartlabs.concord.server.process.keys.HeaderKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\n\nimport javax.servlet.http.HttpServletRequest;\nimport java.nio.file.Path;\nimport java.util.*;\n\npublic class Payload {\n\n    public static final HeaderKey<HttpServletRequest> SERVLET_REQUEST = HeaderKey.register(\"_servletRequest\", HttpServletRequest.class);\n    public static final HeaderKey<Imports> IMPORTS = HeaderKey.register(\"_imports\", Imports.class);\n    public static final HeaderKey<List<Snapshot>> REPOSITORY_SNAPSHOT = HeaderKey.registerList(\"_repositorySnapshot\");\n    public static final HeaderKey<List<String>> ACTIVE_PROFILES = HeaderKey.registerList(\"_activeProfiles\");\n    public static final HeaderKey<List<String>> DEPENDENCIES = HeaderKey.registerList(\"_dependencies\");\n    public static final HeaderKey<Map<String, Object>> CONFIGURATION = HeaderKey.registerMap(\"_cfg\");\n    public static final HeaderKey<Path> BASE_DIR = HeaderKey.register(\"_baseDir\", Path.class);\n    public static final HeaderKey<Path> WORKSPACE_DIR = HeaderKey.register(\"_workspace\", Path.class);\n    public static final HeaderKey<PolicyEngine> POLICY = HeaderKey.register(\"_policy\", PolicyEngine.class);\n    public static final HeaderKey<ProcessDefinition> PROJECT_DEFINITION = HeaderKey.register(\"_projectDef\", ProcessDefinition.class);\n    public static final HeaderKey<ProcessKind> PROCESS_KIND = HeaderKey.register(\"_processKind\", ProcessKind.class);\n    public static final HeaderKey<Repository> REPOSITORY = HeaderKey.register(\"_repository\", Repository.class);\n    public static final HeaderKey<Set<String>> OUT_EXPRESSIONS = HeaderKey.registerSet(\"_outExpr\");\n    public static final HeaderKey<Set<String>> PROCESS_HANDLERS = HeaderKey.registerSet(\"_processHandlers\");\n    public static final HeaderKey<Set<String>> PROCESS_TAGS = HeaderKey.registerSet(\"_processTags\");\n    public static final HeaderKey<String> ENTRY_POINT = HeaderKey.register(\"_entryPoint\", String.class);\n    public static final HeaderKey<Set<String>> RESUME_EVENTS = HeaderKey.registerSet(\"_resumeEvents\");\n    public static final HeaderKey<String> INITIATOR = HeaderKey.register(\"_initiator\", String.class);\n    public static final HeaderKey<String> RUNTIME = HeaderKey.register(\"_runtime\", String.class);\n    public static final HeaderKey<String> SESSION_TOKEN = HeaderKey.register(\"_sessionToken\", String.class);\n    public static final HeaderKey<TriggeredByEntry> TRIGGERED_BY = HeaderKey.register(\"_triggeredBy\", TriggeredByEntry.class);\n    public static final HeaderKey<UUID> INITIATOR_ID = HeaderKey.register(\"_initiatorId\", UUID.class);\n    public static final HeaderKey<UUID> ORGANIZATION_ID = HeaderKey.register(\"_orgId\", UUID.class);\n    public static final HeaderKey<UUID> PARENT_INSTANCE_ID = HeaderKey.register(\"_parentInstanceId\", UUID.class);\n    public static final HeaderKey<UUID> PROJECT_ID = HeaderKey.register(\"_projectId\", UUID.class);\n    public static final HeaderKey<UUID> REPOSITORY_ID = HeaderKey.register(\"_repoId\", UUID.class);\n    public static final HeaderKey<List<Runnable>> RESUME_HOOKS = HeaderKey.registerList(\"_resumeHooks\");\n\n    public static final AttachmentKey WORKSPACE_ARCHIVE = AttachmentKey.register(\"archive\");\n\n    private final ProcessKey processKey;\n    private final Map<String, Object> headers;\n    private final Map<String, Path> attachments;\n\n    public Payload(ProcessKey processKey) {\n        this.processKey = processKey;\n        this.headers = Collections.emptyMap();\n        this.attachments = Collections.emptyMap();\n    }\n\n    private Payload(Payload old, Map<String, Object> headers, Map<String, Path> attachments) {\n        this.processKey = old.processKey;\n        this.headers = Objects.requireNonNull(headers, \"Headers map cannot be null\");\n        this.attachments = Objects.requireNonNull(attachments, \"Attachments map cannot be null\");\n    }\n\n    public ProcessKey getProcessKey() {\n        return processKey;\n    }\n\n    public <T> T getHeader(HeaderKey<T> key) {\n        return key.cast(headers.get(key.name()));\n    }\n\n    public <T> T getHeader(HeaderKey<T> key, T defaultValue) {\n        Object v = headers.get(key.name());\n        if (v == null) {\n            return defaultValue;\n        }\n        return key.cast(v);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public <T> T getHeader(String key) {\n        return (T) headers.get(key);\n    }\n\n    public Map<String, Object> getHeaders() {\n        return Collections.unmodifiableMap(headers);\n    }\n\n    public <T> Payload putHeader(HeaderKey<T> key, T value) {\n        Map<String, Object> m = new HashMap<>(headers);\n        m.put(key.name(), key.cast(value));\n        return new Payload(this, m, this.attachments);\n    }\n\n    public Payload putHeaders(Map<String, Object> values) {\n        Map<String, Object> m = new HashMap<>(headers);\n        m.putAll(values);\n        return new Payload(this, m, this.attachments);\n    }\n\n    public Payload removeHeader(HeaderKey<?> key) {\n        Map<String, Object> m = new HashMap<>(headers);\n        m.remove(key.name());\n        return new Payload(this, m, this.attachments);\n    }\n\n    public Payload mergeValues(HeaderKey<Map<String, Object>> key, Map<String, ?> values) {\n        Map<String, Object> o = getHeader(key);\n        Map<String, Object> n = new HashMap<>(o != null ? o : Collections.emptyMap());\n        n.putAll(values);\n        return putHeader(key, n);\n    }\n\n    public Path getAttachment(AttachmentKey key) {\n        return key.cast(attachments.get(key.name()));\n    }\n\n    public Map<String, Path> getAttachments() {\n        return Collections.unmodifiableMap(attachments);\n    }\n\n    public Payload putAttachment(AttachmentKey key, Path value) {\n        Map<String, Path> m = new HashMap<>(attachments);\n        m.put(key.name(), value);\n        return new Payload(this, this.headers, m);\n    }\n\n    public Payload putAttachments(Map<String, Path> values) {\n        Map<String, Path> m = new HashMap<>(attachments);\n        m.putAll(values);\n        return new Payload(this, this.headers, m);\n    }\n\n    public Payload removeAttachment(AttachmentKey key) {\n        if (!attachments.containsKey(key.name())) {\n            return this;\n        }\n\n        return removeAttachment(key.name());\n    }\n\n    public Payload removeAttachment(String key) {\n        Map<String, Path> m = new HashMap<>(attachments);\n        m.remove(key);\n        return new Payload(this, this.headers, m);\n    }\n\n    @Override\n    public String toString() {\n        return \"Payload{\" +\n                \"processKey=\" + processKey +\n                \", headers=\" + headers +\n                \", attachments=\" + attachments +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/PayloadBuilder.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.MultipartUtils;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport org.jboss.resteasy.plugins.providers.multipart.InputPart;\nimport org.jboss.resteasy.plugins.providers.multipart.MultipartInput;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.OffsetDateTime;\nimport java.util.*;\nimport java.util.function.Function;\n\nimport static java.time.temporal.ChronoUnit.MICROS;\n\npublic final class PayloadBuilder {\n\n    public static PayloadBuilder start(ProcessKey processKey) {\n        return new PayloadBuilder(processKey);\n    }\n\n    public static PayloadBuilder start(PartialProcessKey processKey) {\n        OffsetDateTime createdAt = OffsetDateTime.now();\n        // round up and truncate to microseconds\n        if (createdAt.getNano() >= 500) {\n            createdAt = createdAt.plus(1, MICROS)\n                    .truncatedTo(MICROS);\n        } else {\n            createdAt = createdAt.truncatedTo(MICROS);\n        }\n\n        ProcessKey pk = new ProcessKey(processKey.getInstanceId(), createdAt);\n        return new PayloadBuilder(pk);\n    }\n\n    public static PayloadBuilder resume(ProcessKey processKey) {\n        return new PayloadBuilder(processKey);\n    }\n\n    public static PayloadBuilder basedOn(Payload payload) {\n        return new PayloadBuilder(payload);\n    }\n\n    private Payload payload;\n    private final Map<String, Path> attachments = new HashMap<>();\n\n    private PayloadBuilder(Payload payload) {\n        this.payload = payload;\n    }\n\n    private PayloadBuilder(ProcessKey processKey) {\n        this.payload = new Payload(processKey);\n    }\n\n    public ProcessKey processKey() {\n        return payload.getProcessKey();\n    }\n\n    public PayloadBuilder parentInstanceId(UUID parentInstanceId) {\n        if (parentInstanceId != null) {\n            payload = payload.putHeader(Payload.PARENT_INSTANCE_ID, parentInstanceId);\n        }\n        return this;\n    }\n\n    public PayloadBuilder kind(ProcessKind kind) {\n        if (kind != null) {\n            payload = payload.putHeader(Payload.PROCESS_KIND, kind);\n        }\n        return this;\n    }\n\n    public PayloadBuilder apply(Function<PayloadBuilder, PayloadBuilder> f) {\n        return f.apply(this);\n    }\n\n    /**\n     * Parses the provided {@code input} using the following logic:\n     * <ul>\n     *     <li>part names should not start with '/' or contain '..'</li>\n     *     <li>any {@code text/plain} is converted into the process' configuration value.\n     *     Dots '.' in the part's name are used as the property's path. E.g. {@code x.y.z} is\n     *     converted into a nested Map object {@code {\"x\": {\"y\": {\"z\": ...}}}}</li>\n     *     <li>parts of other types are saved as files in the payload's {@code ${workDir}}.\n     *     The part's name will be used as the file's path.</li>\n     * </ul>\n     */\n    public PayloadBuilder with(MultipartInput input) throws IOException {\n        Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION);\n        cfg = new HashMap<>(cfg != null ? cfg : Collections.emptyMap());\n\n        ProcessKey pk = payload.getProcessKey();\n\n        for (InputPart p : input.getParts()) {\n            String name = MultipartUtils.extractName(p);\n            if (name == null || name.startsWith(\"/\") || name.contains(\"..\")) {\n                throw new ProcessException(pk, \"Invalid attachment name: \" + name, Response.Status.BAD_REQUEST);\n            }\n\n            if (p.getMediaType().isCompatible(MediaType.TEXT_PLAIN_TYPE)) {\n                String v = p.getBodyAsString().trim();\n                Map<String, Object> m = ConfigurationUtils.toNested(name, v);\n                cfg = ConfigurationUtils.deepMerge(cfg, m);\n            } else {\n                try (InputStream in = p.getBody(InputStream.class, null)) {\n                    addAttachment(name, in);\n                }\n            }\n        }\n\n        payload = payload.putHeader(Payload.CONFIGURATION, cfg)\n                .putAttachments(attachments);\n\n        return this;\n    }\n\n    public PayloadBuilder workspace(Path workDir) {\n        if (workDir != null) {\n            payload = payload.putHeader(Payload.WORKSPACE_DIR, workDir);\n        }\n        return this;\n    }\n\n    public PayloadBuilder workspace(InputStream in) throws IOException {\n        if (in == null) {\n            return this;\n        }\n\n        Path baseDir = ensureBaseDir();\n\n        Path archive = baseDir.resolve(Payload.WORKSPACE_ARCHIVE.name());\n        Files.copy(in, archive);\n\n        payload = payload.putAttachment(Payload.WORKSPACE_ARCHIVE, archive);\n\n        return this;\n    }\n\n    public PayloadBuilder imports(Imports imports) {\n        if (imports != null && !imports.isEmpty()) {\n            payload = payload.putHeader(Payload.IMPORTS, imports);\n        }\n        return this;\n    }\n\n    public PayloadBuilder configuration(Map<String, Object> cfg) {\n        if (cfg == null) {\n            cfg = Collections.emptyMap();\n        }\n\n        Map<String, Object> prev = payload.getHeader(Payload.CONFIGURATION);\n        if (prev != null) {\n            cfg = ConfigurationUtils.deepMerge(prev, cfg);\n        }\n\n        payload = payload.putHeader(Payload.CONFIGURATION, cfg);\n        return this;\n    }\n\n    public PayloadBuilder initiator(UUID initiatorId, String initiator) {\n        if (initiatorId != null) {\n            payload = payload.putHeader(Payload.INITIATOR_ID, initiatorId);\n        }\n\n        if (initiator != null) {\n            payload = payload.putHeader(Payload.INITIATOR, initiator);\n        }\n\n        return this;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public PayloadBuilder meta(Map<String, Object> meta) {\n        if (meta == null) {\n            meta = Collections.emptyMap();\n        }\n\n        Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION);\n        if (cfg == null) {\n            cfg = new HashMap<>();\n        }\n\n        Object v = cfg.getOrDefault(Constants.Request.META, Collections.emptyMap());\n        if (!(v instanceof Map)) {\n            throw new ValidationErrorsException(\"Expected a JSON object in '\" + Constants.Request.META + \"', got: \" + v);\n        }\n\n        Map<String, Object> prev = (Map<String, Object>) v;\n        meta = ConfigurationUtils.deepMerge(prev, meta);\n\n        if (!meta.isEmpty()) {\n            cfg.put(Constants.Request.META, meta);\n        }\n\n        payload = payload.putHeader(Payload.CONFIGURATION, cfg);\n\n        return this;\n    }\n\n    public PayloadBuilder outExpressions(String[] out) {\n        if (out != null && out.length != 0) {\n            payload = payload.putHeader(Payload.OUT_EXPRESSIONS, new HashSet<>(Arrays.asList(out)));\n        }\n        return this;\n    }\n\n    public PayloadBuilder mergeOutExpressions(String[] out) {\n        if (out == null || out.length == 0) {\n            return this;\n        }\n\n        Set<String> s = payload.getHeader(Payload.OUT_EXPRESSIONS);\n\n        if (s == null) {\n            s = new HashSet<>(out.length);\n        }\n\n        s.addAll(Arrays.asList(out));\n        payload = payload.putHeader(Payload.OUT_EXPRESSIONS, s);\n\n        return this;\n    }\n\n    public PayloadBuilder organization(UUID orgId) {\n        if (orgId != null) {\n            payload = payload.putHeader(Payload.ORGANIZATION_ID, orgId);\n        }\n        return this;\n    }\n\n    public PayloadBuilder project(UUID projectId) {\n        if (projectId != null) {\n            payload = payload.putHeader(Payload.PROJECT_ID, projectId);\n        }\n        return this;\n    }\n\n    public PayloadBuilder repository(UUID repoId) {\n        if (repoId != null) {\n            payload = payload.putHeader(Payload.REPOSITORY_ID, repoId);\n        }\n        return this;\n    }\n\n    public PayloadBuilder entryPoint(String entryPoint) {\n        if (entryPoint != null) {\n            payload = payload.putHeader(Payload.ENTRY_POINT, entryPoint);\n        }\n        return this;\n    }\n\n    public PayloadBuilder resumeEvents(Set<String> events) {\n        payload = payload.putHeader(Payload.RESUME_EVENTS, events);\n        return this;\n    }\n\n    public PayloadBuilder triggeredBy(TriggeredByEntry t) {\n        payload = payload.putHeader(Payload.TRIGGERED_BY, t);\n        return this;\n    }\n\n    public PayloadBuilder activeProfiles(Collection<String> activeProfiles) {\n        Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION);\n        if (cfg == null) {\n            cfg = new HashMap<>();\n        }\n\n        cfg.put(Constants.Request.ACTIVE_PROFILES_KEY, activeProfiles);\n\n        payload = payload.putHeader(Payload.CONFIGURATION, cfg);\n        return this;\n    }\n\n    public PayloadBuilder handlers(Set<String> handlers) {\n        if (handlers == null || handlers.isEmpty()) {\n            return this;\n        }\n\n        payload = payload.putHeader(Payload.PROCESS_HANDLERS, handlers);\n        return this;\n    }\n\n    public PayloadBuilder request(HttpServletRequest request) {\n        if (request == null) {\n            return this;\n        }\n\n        payload = payload.putHeader(Payload.SERVLET_REQUEST, request);\n        return this;\n    }\n\n    /**\n     * Add a file to the payload's directory.\n     * If the destination file already exists, it will be overwritten.\n     * <p/>\n     * Example:\n     * <pre>{@code\n     * Payload p = PayloadBuilder.start(...)\n     *      .file(\"concord.yml\", \"flows:\\n  default:\\n    - log: 'Hello!'\\n\")\n     *      .build();\n     * }</pre>\n     * <p>\n     * The file is stored as an \"attachment\" first and moved into\n     * the process' workDir when the process transitions into ENQUEUED status.\n     *\n     * @param name    the file's destination name\n     * @param content the file's content\n     */\n    public PayloadBuilder file(String name, String content) throws IOException {\n        try (InputStream in = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) {\n            addAttachment(name, in);\n        }\n\n        return this;\n    }\n\n    private void addAttachment(String name, InputStream in) throws IOException {\n        Path dst = ensureBaseDir().resolve(name);\n        Files.createDirectories(dst.getParent());\n        try (OutputStream out = Files.newOutputStream(dst)) {\n            in.transferTo(out);\n        }\n\n        attachments.put(name, dst);\n    }\n\n    private Path ensureBaseDir() throws IOException {\n        Path baseDir = payload.getHeader(Payload.BASE_DIR);\n\n        if (baseDir == null) {\n            baseDir = PathUtils.createTempDir(\"payload\");\n            payload = payload.putHeader(Payload.BASE_DIR, baseDir);\n        }\n\n        return baseDir;\n    }\n\n    private Path ensureWorkDir() throws IOException {\n        Path workDir = payload.getHeader(Payload.WORKSPACE_DIR);\n\n        if (workDir == null) {\n            Path baseDir = ensureBaseDir();\n\n            workDir = baseDir.resolve(\"workspace\");\n            if (!Files.exists(workDir)) {\n                Files.createDirectories(workDir);\n            }\n\n            payload = payload.putHeader(Payload.WORKSPACE_DIR, workDir);\n        }\n\n        return workDir;\n    }\n\n    public Payload build() throws IOException {\n        ensureWorkDir();\n        return payload.putAttachments(attachments);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/PayloadManager.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.MultipartUtils;\nimport com.walmartlabs.concord.server.org.OrganizationDao;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.org.project.RepositoryDao;\nimport com.walmartlabs.concord.server.process.state.ProcessStateManager;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport org.jboss.resteasy.plugins.providers.multipart.MultipartInput;\n\nimport javax.inject.Inject;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.ws.rs.core.Response;\nimport java.io.IOException;\nimport java.io.Serializable;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.function.Function;\n\nimport static com.walmartlabs.concord.server.process.state.ProcessStateManager.copyTo;\nimport static com.walmartlabs.concord.server.process.state.ProcessStateManager.exclude;\n\npublic class PayloadManager {\n\n    private static final String FORMS_PATH_PATTERN = String.format(\"%s/%s/%s/.*\", Constants.Files.JOB_ATTACHMENTS_DIR_NAME,\n            Constants.Files.JOB_STATE_DIR_NAME, Constants.Files.JOB_FORMS_DIR_NAME);\n\n    private final ProcessStateManager stateManager;\n    private final OrganizationDao orgDao;\n    private final ProjectDao projectDao;\n    private final RepositoryDao repositoryDao;\n\n    @Inject\n    public PayloadManager(ProcessStateManager stateManager,\n                          OrganizationDao orgDao,\n                          ProjectDao projectDao,\n                          RepositoryDao repositoryDao) {\n\n        this.stateManager = stateManager;\n        this.orgDao = orgDao;\n        this.projectDao = projectDao;\n        this.repositoryDao = repositoryDao;\n    }\n\n    @WithTimer\n    public Payload createPayload(MultipartInput input, HttpServletRequest request) throws IOException {\n        PartialProcessKey processKey = PartialProcessKey.create();\n\n        UUID parentInstanceId = MultipartUtils.getUuid(input, Constants.Multipart.PARENT_INSTANCE_ID);\n\n        UUID orgId = getOrg(input);\n        UUID projectId = getProject(input, orgId);\n\n        UUID repoId = getRepo(input, projectId);\n        if (repoId != null && projectId == null) {\n            // allow starting processes by specifying repository IDs without project IDs or names\n            projectId = repositoryDao.getProjectId(repoId);\n        }\n\n        String entryPoint = MultipartUtils.getString(input, Constants.Multipart.ENTRY_POINT);\n\n        UserPrincipal initiator = UserPrincipal.assertCurrent();\n\n        String[] out = getOutExpressions(input);\n\n        Map<String, Object> meta = MultipartUtils.getMap(input, Constants.Multipart.META);\n        if (meta == null) {\n            meta = Collections.emptyMap();\n        }\n\n        return PayloadBuilder.start(processKey)\n                .parentInstanceId(parentInstanceId)\n                .with(input)\n                .organization(orgId)\n                .project(projectId)\n                .repository(repoId)\n                .entryPoint(entryPoint)\n                .outExpressions(out)\n                .initiator(initiator.getId(), initiator.getUsername())\n                .meta(meta)\n                .request(request)\n                .build();\n    }\n\n    /**\n     * Creates a payload to resume a suspended process, pulling the necessary data from the state storage.\n     */\n    public Payload createResumePayload(ProcessKey processKey, String eventName, Map<String, Object> req) throws IOException {\n        return createResumePayload(processKey, eventName != null ? Collections.singleton(eventName) : Collections.emptySet(), req);\n    }\n\n    /**\n     * Creates a payload to resume a suspended process, pulling the necessary data from the state storage.\n     */\n    public Payload createResumePayload(ProcessKey processKey, Set<String> events, Map<String, Object> req) throws IOException {\n        Path tmpDir = PathUtils.createTempDir(\"payload\");\n        if (!stateManager.export(processKey, copyTo(tmpDir))) {\n            throw new ProcessException(processKey, \"Can't resume '\" + processKey + \"', state snapshot not found\", Response.Status.NOT_FOUND);\n        }\n\n        return PayloadBuilder.resume(processKey)\n                .workspace(tmpDir)\n                .configuration(req)\n                .resumeEvents(events)\n                .build();\n    }\n\n    /**\n     * Creates a payload to fork an existing process.\n     */\n    public Payload createFork(PartialProcessKey processKey, ProcessKey parentProcessKey, ProcessKind kind,\n                              UUID initiatorId, String initiator, UUID projectId, Map<String, Object> req, String[] out,\n                              Set<String> handlers, Imports imports) throws IOException {\n\n        Path tmpDir = PathUtils.createTempDir(\"payload\");\n\n        // skip forms and the parent process' arguments\n        if (!stateManager.export(parentProcessKey, exclude(copyTo(tmpDir), FORMS_PATH_PATTERN))) {\n            throw new ProcessException(processKey, \"Can't fork '\" + parentProcessKey + \"', the state snapshot not found\");\n        }\n\n        return PayloadBuilder.start(processKey)\n                .parentInstanceId(parentProcessKey.getInstanceId())\n                .kind(kind)\n                .initiator(initiatorId, initiator)\n                .project(projectId)\n                .configuration(req)\n                .outExpressions(out)\n                .workspace(tmpDir)\n                .handlers(handlers)\n                .imports(imports)\n                .build();\n    }\n\n    private UUID getOrg(MultipartInput input) {\n        UUID id = MultipartUtils.getUuid(input, Constants.Multipart.ORG_ID);\n        String name = MultipartUtils.getString(input, Constants.Multipart.ORG_NAME);\n        if (id == null && name != null) {\n            id = orgDao.getId(name);\n            if (id == null) {\n                throw new ValidationErrorsException(\"Organization not found: \" + name);\n            }\n        }\n        return id;\n    }\n\n    private UUID getProject(MultipartInput input, UUID orgId) {\n        UUID id = MultipartUtils.getUuid(input, Constants.Multipart.PROJECT_ID);\n        String name = MultipartUtils.getString(input, Constants.Multipart.PROJECT_NAME);\n        if (id == null && name != null) {\n            if (orgId == null) {\n                throw new ValidationErrorsException(\"Organization ID or name is required\");\n            }\n\n            id = projectDao.getId(orgId, name);\n            if (id == null) {\n                throw new ValidationErrorsException(\"Project not found: \" + name);\n            }\n        }\n        return id;\n    }\n\n    private UUID getRepo(MultipartInput input, UUID projectId) {\n        UUID id = MultipartUtils.getUuid(input, Constants.Multipart.REPO_ID);\n        String name = MultipartUtils.getString(input, Constants.Multipart.REPO_NAME);\n        if (id == null && name != null) {\n            if (projectId == null) {\n                throw new ValidationErrorsException(\"Project ID or name is required\");\n            }\n\n            id = repositoryDao.getId(projectId, name);\n            if (id == null) {\n                throw new ValidationErrorsException(\"Repository not found: \" + name);\n            }\n        }\n        return id;\n    }\n\n    private String[] getOutExpressions(MultipartInput input) {\n        String s = MultipartUtils.getString(input, Constants.Multipart.OUT_EXPR);\n        if (s == null) {\n            return null;\n        }\n        return s.split(\",\");\n    }\n\n    private static Function<PayloadBuilder, PayloadBuilder> p(EntryPoint p) {\n        if (p == null) {\n            return Function.identity();\n        }\n\n        return (b) -> b.organization(p.orgId)\n                .project(p.projectId)\n                .repository(p.repoId)\n                .entryPoint(p.flow);\n    }\n\n    public static class EntryPoint implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        private final UUID orgId;\n        private final UUID projectId;\n        private final UUID repoId;\n        private final String flow;\n\n        public EntryPoint(UUID orgId, UUID projectId, UUID repoId, String flow) {\n            this.orgId = orgId;\n            this.projectId = projectId;\n            this.repoId = repoId;\n            this.flow = flow;\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/PayloadUtils.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.DateTimeUtils;\nimport com.walmartlabs.concord.repository.Snapshot;\nimport com.walmartlabs.concord.runtime.v2.model.ExclusiveMode;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\n\nimport javax.xml.bind.DatatypeConverter;\nimport java.time.OffsetDateTime;\nimport java.time.format.DateTimeParseException;\nimport java.util.*;\n\npublic final class PayloadUtils {\n\n    public static ExclusiveMode getExclusive(Payload p) {\n        Map<String, Object> cfg = p.getHeader(Payload.CONFIGURATION);\n        Map<String, Object> exclusive = MapUtils.getMap(cfg, Constants.Request.EXCLUSIVE, Collections.emptyMap());\n        if (exclusive.isEmpty()) {\n            return null;\n        }\n        String group = MapUtils.getString(exclusive, \"group\");\n        if (group == null || group.trim().isEmpty()) {\n            throw new ProcessException(p.getProcessKey(), \"Invalid exclusive mode: exclusive group not specified or empty\");\n        }\n        ExclusiveMode.Mode mode = MapUtils.getEnum(exclusive, \"mode\", ExclusiveMode.Mode.class, ExclusiveMode.Mode.cancel);\n        return ExclusiveMode.of(group, mode);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static Map<String, Object> getRequirements(Payload p) {\n        Map<String, Object> cfg = p.getHeader(Payload.CONFIGURATION);\n        return (Map<String, Object>) cfg.get(Constants.Request.REQUIREMENTS);\n    }\n\n    public static OffsetDateTime getStartAt(Payload p) {\n        Map<String, Object> cfg = p.getHeader(Payload.CONFIGURATION);\n        if (cfg == null) {\n            return null;\n        }\n\n        String k = Constants.Request.START_AT_KEY;\n        Object v = cfg.get(k);\n        if (v == null) {\n            return null;\n        }\n\n        if (v instanceof String iso) {\n            OffsetDateTime t;\n            try {\n                t = DateTimeUtils.fromIsoString(iso);\n            } catch (DateTimeParseException e) {\n                throw new ProcessException(p.getProcessKey(), \"Invalid '\" + k + \"' format, expected an ISO-8601 value, got: \" + v);\n            }\n\n            if (t.isBefore(OffsetDateTime.now())) {\n                throw new ProcessException(p.getProcessKey(), \"Invalid '\" + k + \"' value, can't be in the past: \" + v +\n                        \" Current server time: \" + DatatypeConverter.printDateTime(Calendar.getInstance()));\n            }\n\n            return t;\n        }\n\n        throw new ProcessException(p.getProcessKey(), \"Invalid '\" + k + \"' value, expected an ISO-8601 value, got: \" + v);\n    }\n\n    public static Payload addSnapshots(Payload payload, List<Snapshot> l) {\n        if (l == null || l.isEmpty()) {\n            return payload;\n        }\n\n        List<Snapshot> result = new ArrayList<>();\n\n        List<Snapshot> snapshots = payload.getHeader(Payload.REPOSITORY_SNAPSHOT);\n        if (snapshots != null) {\n            result.addAll(snapshots);\n        }\n        result.addAll(l);\n\n        return payload.putHeader(Payload.REPOSITORY_SNAPSHOT, result);\n    }\n\n    private PayloadUtils() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/ProcessAccessManager.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.org.project.ProjectAccessManager;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueManager;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.security.sessionkey.SessionKeyPrincipal;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.core.Response.Status;\nimport java.util.Collections;\nimport java.util.Set;\nimport java.util.UUID;\n\npublic class ProcessAccessManager {\n\n    private final ProjectAccessManager projectAccessManager;\n    private final ProcessQueueManager processQueueManager;\n\n    @Inject\n    public ProcessAccessManager(ProjectAccessManager projectAccessManager,\n                                ProcessQueueManager processQueueManager) {\n\n        this.projectAccessManager = projectAccessManager;\n        this.processQueueManager = processQueueManager;\n    }\n\n    public ProcessEntry assertAccess(UUID instanceId, Set<ProcessDataInclude> includes) {\n        var entry = processQueueManager.get(PartialProcessKey.from(instanceId), includes);\n        if (entry == null) {\n            throw new ConcordApplicationException(\"Process instance not found: \" + instanceId, Status.NOT_FOUND);\n        }\n\n        if (Roles.isAdmin() || Roles.isGlobalReader()) {\n            return entry;\n        }\n\n        var current = UserPrincipal.assertCurrent();\n        if (current.getId().equals(entry.initiatorId())) {\n            return entry;\n        }\n\n        var sessionKey = SessionKeyPrincipal.getCurrent();\n        if (sessionKey != null) {\n            var processKey = new ProcessKey(entry.instanceId(), entry.createdAt());\n            if (processKey.partOf(sessionKey.getProcessKey())) {\n                return entry;\n            }\n        }\n\n        if (entry.projectId() != null) {\n            projectAccessManager.assertAccess(entry.orgId(), entry.projectId(), null, ResourceAccessLevel.READER, false);\n            return entry;\n        }\n\n        throw new UnauthorizedException(\"The current user (\" + current.getUsername() + \") doesn't have \" +\n                                        \"the necessary permissions to access process: \" + entry.instanceId());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/ProcessCleaner.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.db.PgUtils;\nimport com.walmartlabs.concord.server.cfg.ProcessConfiguration;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport com.walmartlabs.concord.server.sdk.ScheduledTask;\nimport org.jooq.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.*;\nimport static com.walmartlabs.concord.server.jooq.tables.ProcessCheckpoints.PROCESS_CHECKPOINTS;\nimport static com.walmartlabs.concord.server.jooq.tables.ProcessEvents.PROCESS_EVENTS;\nimport static com.walmartlabs.concord.server.jooq.tables.ProcessQueue.PROCESS_QUEUE;\nimport static com.walmartlabs.concord.server.jooq.tables.ProcessState.PROCESS_STATE;\n\npublic class ProcessCleaner implements ScheduledTask {\n\n    private static final Logger log = LoggerFactory.getLogger(ProcessCleaner.class);\n\n    private static final String[] EXCLUDE_STATUSES = {\n            ProcessStatus.STARTING.toString(),\n            ProcessStatus.RUNNING.toString(),\n            ProcessStatus.RESUMING.toString()\n    };\n\n    private final ProcessConfiguration cfg;\n    private final CleanerDao cleanerDao;\n\n    @Inject\n    public ProcessCleaner(ProcessConfiguration cfg, @MainDB Configuration dbCfg) {\n        this.cfg = cfg;\n        this.cleanerDao = new CleanerDao(dbCfg);\n    }\n\n    @Override\n    public String getId() {\n        return \"process-cleaner\";\n    }\n\n    @Override\n    public long getIntervalInSec() {\n        return cfg.getCleanupInterval().getSeconds();\n    }\n\n    @Override\n    public void performTask() {\n        cleanerDao.deleteOldState(cfg);\n        cleanerDao.deleteOrphans(cfg);\n    }\n\n    private static class CleanerDao extends AbstractDao {\n\n        private CleanerDao(@MainDB Configuration cfg) {\n            super(cfg);\n        }\n\n        void deleteOldState(ProcessConfiguration jobCfg) {\n            long t1 = System.currentTimeMillis();\n\n            Field<OffsetDateTime> cutoff = PgUtils.nowMinus(jobCfg.getMaxStateAge());\n\n            tx(tx -> {\n                SelectConditionStep<Record1<UUID>> ids = tx.select(PROCESS_QUEUE.INSTANCE_ID)\n                        .from(PROCESS_QUEUE)\n                        .where(PROCESS_QUEUE.LAST_UPDATED_AT.lessThan(cutoff)\n                                .and(PROCESS_QUEUE.CURRENT_STATUS.notIn(EXCLUDE_STATUSES)));\n\n                int stateRecords = 0;\n                int initialStateRecords = 0;\n                if (jobCfg.isStateCleanup()) {\n                    stateRecords = tx.deleteFrom(PROCESS_STATE)\n                            .where(PROCESS_STATE.INSTANCE_ID.in(ids))\n                            .execute();\n\n                    initialStateRecords = tx.deleteFrom(PROCESS_INITIAL_STATE)\n                            .where(PROCESS_INITIAL_STATE.INSTANCE_ID.in(ids))\n                            .execute();\n                }\n\n                int events = 0;\n                if (jobCfg.isEventsCleanup()) {\n                    events = tx.deleteFrom(PROCESS_EVENTS)\n                            .where(PROCESS_EVENTS.INSTANCE_ID.in(ids))\n                            .execute();\n                }\n\n                int logDataEntries = 0;\n                int logSegmentEntries = 0;\n                if (jobCfg.isLogsCleanup()) {\n                    logDataEntries = tx.deleteFrom(PROCESS_LOG_DATA)\n                            .where(PROCESS_LOG_DATA.INSTANCE_ID.in(ids))\n                            .execute();\n\n                    logSegmentEntries = tx.deleteFrom(PROCESS_LOG_SEGMENTS)\n                            .where(PROCESS_LOG_SEGMENTS.INSTANCE_ID.in(ids))\n                            .execute();\n                }\n\n                int checkpoints = 0;\n                if (jobCfg.isCheckpointCleanup()) {\n                    checkpoints = tx.deleteFrom(PROCESS_CHECKPOINTS)\n                            .where(PROCESS_CHECKPOINTS.INSTANCE_ID.in(ids))\n                            .execute();\n                }\n\n                int queueEntries = 0;\n                if (jobCfg.isQueueCleanup()) {\n                    tx.deleteFrom(PROCESS_WAIT_CONDITIONS)\n                            .where(PROCESS_WAIT_CONDITIONS.INSTANCE_ID.in(ids))\n                            .execute();\n\n                    queueEntries = tx.deleteFrom(PROCESS_QUEUE)\n                            .where(PROCESS_QUEUE.INSTANCE_ID.in(ids))\n                            .execute();\n                }\n\n                log.info(\"deleteOldState -> removed older than {}: {} queue entries, {} log data entries, {} log segments, {} state item(s), {} initial state item(s), {} event(s), {} checkpoint(s)\",\n                        jobCfg.getMaxStateAge(), queueEntries, logDataEntries, logSegmentEntries, stateRecords, initialStateRecords, events, checkpoints);\n            });\n\n            long t2 = System.currentTimeMillis();\n            log.info(\"deleteOldState -> took {}ms\", (t2 - t1));\n        }\n\n        void deleteOrphans(ProcessConfiguration jobCfg) {\n            long t1 = System.currentTimeMillis();\n\n            tx(tx -> {\n                SelectJoinStep<Record1<UUID>> alive = tx.select(PROCESS_QUEUE.INSTANCE_ID).from(PROCESS_QUEUE);\n\n                int stateRecords = 0;\n                if (jobCfg.isStateCleanup()) {\n                    stateRecords = tx.deleteFrom(PROCESS_STATE)\n                            .where(PROCESS_STATE.INSTANCE_ID.notIn(alive))\n                            .execute();\n                }\n\n                int events = 0;\n                if (jobCfg.isEventsCleanup()) {\n                    events = tx.deleteFrom(PROCESS_EVENTS)\n                            .where(PROCESS_EVENTS.INSTANCE_ID.notIn(alive))\n                            .execute();\n                }\n\n                int checkpoints = 0;\n                if (jobCfg.isCheckpointCleanup()) {\n                    checkpoints = tx.deleteFrom(PROCESS_CHECKPOINTS)\n                            .where(PROCESS_CHECKPOINTS.INSTANCE_ID.notIn(alive))\n                            .execute();\n                }\n\n                int logDataEntries = 0;\n                int logSegmentEntries = 0;\n                if (jobCfg.isLogsCleanup()) {\n                    logDataEntries = tx.deleteFrom(PROCESS_LOG_DATA)\n                            .where(PROCESS_LOG_DATA.INSTANCE_ID.notIn(alive))\n                            .execute();\n\n                    logSegmentEntries = tx.deleteFrom(PROCESS_LOG_SEGMENTS)\n                            .where(PROCESS_LOG_SEGMENTS.INSTANCE_ID.notIn(alive))\n                            .execute();\n                }\n\n                log.info(\"deleteOrphans -> removed orphan data: {} log data entries, {} log segments, {} state item(s), {} event(s), {} checkpoint(s)\",\n                        logDataEntries, logSegmentEntries, stateRecords, events, checkpoints);\n            });\n\n            long t2 = System.currentTimeMillis();\n            log.info(\"deleteOrphans -> took {}ms\", (t2 - t1));\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/ProcessDataInclude.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic enum ProcessDataInclude {\n\n    CHECKPOINTS (\"checkpoints\"),\n    CHECKPOINTS_HISTORY (\"checkpointsHistory\"),\n    CHILDREN_IDS (\"childrenIds\"),\n    STATUS_HISTORY (\"history\");\n\n    private final String value;\n\n    ProcessDataInclude(String value) {\n        this.value = value;\n    }\n\n    public static ProcessDataInclude fromString(String str) {\n        for (ProcessDataInclude v : values()) {\n            if (v.value.equalsIgnoreCase(str)) {\n                return v;\n            }\n        }\n        throw new IllegalArgumentException(str + \" not found\");\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/ProcessEntry.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.time.OffsetDateTime;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonInclude(Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableProcessEntry.class)\n@JsonDeserialize(as = ImmutableProcessEntry.class)\npublic interface ProcessEntry extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    UUID instanceId();\n\n    ProcessKind kind();\n\n    @Nullable\n    UUID parentInstanceId();\n\n    @Nullable\n    UUID orgId();\n\n    @Nullable\n    String orgName();\n\n    @Nullable\n    UUID projectId();\n\n    @Nullable\n    String projectName();\n\n    @Nullable\n    UUID repoId();\n\n    @Nullable\n    String repoName();\n\n    @Nullable\n    String repoUrl();\n\n    @Nullable\n    String repoPath();\n\n    @Nullable\n    String commitId();\n\n    @Nullable\n    String commitBranch();\n\n    @Deprecated\n    @Nullable\n    String commitMsg();\n\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    OffsetDateTime createdAt();\n\n    @Nullable\n    String initiator();\n\n    @Nullable\n    UUID initiatorId();\n\n    ProcessStatus status();\n\n    @Nullable\n    String lastAgentId();\n\n    @Nullable\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    OffsetDateTime startAt();\n\n    @Nullable\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    OffsetDateTime lastUpdatedAt();\n\n    @Nullable\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    OffsetDateTime lastRunAt();\n\n    @Nullable\n    Long totalRuntimeMs();\n\n    @Nullable\n    String logFileName();\n\n    @Nullable\n    Set<String> tags();\n\n    @Nullable\n    Set<UUID> childrenIds();\n\n    @Nullable\n    Map<String, Object> meta();\n\n    @Nullable\n    Set<String> handlers();\n\n    @Nullable\n    Map<String, Object> requirements();\n\n    @Value.Default\n    default boolean disabled() {\n        return false;\n    }\n\n    @Nullable\n    List<ProcessCheckpointEntry> checkpoints();\n\n    @Nullable\n    List<CheckpointRestoreHistoryEntry> checkpointRestoreHistory();\n\n    @Nullable\n    List<ProcessStatusHistoryEntry> statusHistory();\n\n    @Nullable\n    TriggeredByEntry triggeredBy();\n\n    @Nullable\n    Long timeout();\n\n    @Nullable\n    Long suspendTimeout();\n\n    @Nullable\n    String runtime();\n\n    @Value.Immutable\n    @JsonInclude(Include.NON_EMPTY)\n    @JsonSerialize(as = ImmutableCheckpointRestoreHistoryEntry.class)\n    @JsonDeserialize(as = ImmutableCheckpointRestoreHistoryEntry.class)\n    interface CheckpointRestoreHistoryEntry extends Serializable{\n\n        long serialVersionUID = 1L;\n\n        long id();\n\n        UUID checkpointId();\n\n        ProcessStatus processStatus();\n\n        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n        OffsetDateTime changeDate();\n    }\n\n    @Value.Immutable\n    @JsonInclude(Include.NON_EMPTY)\n    @JsonSerialize(as = ImmutableProcessCheckpointEntry.class)\n    @JsonDeserialize(as = ImmutableProcessCheckpointEntry.class)\n    interface ProcessCheckpointEntry extends Serializable {\n\n        long serialVersionUID = 1L;\n\n        UUID id();\n\n        String name();\n\n        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n        OffsetDateTime createdAt();\n\n        @Nullable\n        UUID correlationId();\n    }\n\n    @Value.Immutable\n    @JsonInclude(Include.NON_EMPTY)\n    @JsonSerialize(as = ImmutableProcessStatusHistoryEntry.class)\n    @JsonDeserialize(as = ImmutableProcessStatusHistoryEntry.class)\n    interface ProcessStatusHistoryEntry extends Serializable {\n\n        long serialVersionUID = 1L;\n\n        UUID id();\n\n        ProcessStatus status();\n\n        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n        OffsetDateTime changeDate();\n    }\n\n    @Value.Immutable\n    @JsonInclude(Include.NON_EMPTY)\n    @JsonSerialize(as = ImmutableProcessWaitEntry.class)\n    @JsonDeserialize(as = ImmutableProcessWaitEntry.class)\n    interface ProcessWaitEntry extends Serializable {\n\n        long serialVersionUID = 1L;\n\n        @Value.Parameter\n        boolean isWaiting();\n\n        @Nullable\n        @Value.Parameter\n        // Can't use AbstractWaitCondition because swagger can't generate code :(\n        List<Map<String, Object>> waits();\n\n        static ProcessWaitEntry of(boolean isWaiting, List<Map<String, Object>> waits) {\n            return ImmutableProcessWaitEntry.of(isWaiting, waits);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/ProcessException.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\n\nimport javax.ws.rs.core.Response.Status;\nimport javax.ws.rs.core.Response.StatusType;\n\npublic class ProcessException extends RuntimeException {\n\n    private static final long serialVersionUID = 1L;\n\n    private final PartialProcessKey processKey;\n    private final StatusType status;\n\n    public ProcessException(PartialProcessKey processKey, String message) {\n        this(processKey, message, Status.INTERNAL_SERVER_ERROR);\n    }\n\n    public ProcessException(PartialProcessKey processKey, String message, StatusType status) {\n        super(message);\n        this.processKey = processKey;\n        this.status = status;\n    }\n\n    public ProcessException(PartialProcessKey processKey, String message, Throwable cause) {\n        this(processKey, message, cause, Status.INTERNAL_SERVER_ERROR);\n    }\n\n    public ProcessException(PartialProcessKey processKey, String message, Throwable cause, StatusType status) {\n        super(message, cause);\n        this.processKey = processKey;\n        this.status = status;\n    }\n\n    public PartialProcessKey getProcessKey() {\n        return processKey;\n    }\n\n    public StatusType getStatus() {\n        return status;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/ProcessExceptionMapper.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.boot.resteasy.ExceptionMapperSupport;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\n\nimport javax.ws.rs.core.Context;\nimport javax.ws.rs.core.HttpHeaders;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport javax.ws.rs.ext.Provider;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.util.UUID;\n\n@Provider\npublic class ProcessExceptionMapper extends ExceptionMapperSupport<ProcessException> {\n\n    private static final int MAX_CAUSE_DEPTH = 5;\n\n    public static final String TRACE_ENABLED_KEY = \"X-Concord-Trace-Enabled\";\n\n    @Context\n    HttpHeaders headers;\n\n    @Override\n    protected Response convert(ProcessException e) {\n        String details = getDetails(e.getCause());\n\n        String stacktrace = null;\n        if (traceEnabled()) {\n            StringWriter w = new StringWriter();\n            e.printStackTrace(new PrintWriter(w));\n            stacktrace = w.toString();\n        }\n\n        PartialProcessKey processKey = e.getProcessKey();\n        UUID instanceId = processKey.getInstanceId();\n\n        ErrorMessage msg = new ErrorMessage(instanceId, e.getMessage(), details, stacktrace);\n        return Response.status(e.getStatus())\n                .entity(msg)\n                .type(MediaType.APPLICATION_JSON_TYPE)\n                .build();\n    }\n\n    private boolean traceEnabled() {\n        String s = headers.getHeaderString(TRACE_ENABLED_KEY);\n        return Boolean.parseBoolean(s);\n    }\n\n    private static String getDetails(Throwable t) {\n        if (t == null) {\n            return null;\n        }\n\n        int currentDepth = 0;\n        StringBuilder result = new StringBuilder();\n        while (t != null && currentDepth < MAX_CAUSE_DEPTH) {\n            result.append(t.getMessage()).append(\"\\n\");\n            t = t.getCause();\n            currentDepth++;\n        }\n        return result.toString();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/ProcessHeartbeatResource.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueDao;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.POST;\nimport javax.ws.rs.Path;\nimport javax.ws.rs.PathParam;\nimport java.util.UUID;\n\n@Path(\"/api/v1/process\")\n@Tag(name = \"ProcessHeartbeat\")\npublic class ProcessHeartbeatResource implements Resource {\n\n    private final ProcessQueueDao queueDao;\n\n    @Inject\n    public ProcessHeartbeatResource(ProcessQueueDao queueDao) {\n        this.queueDao = queueDao;\n    }\n\n    @POST\n    @Path(\"{id}/ping\")\n    @Operation(description = \"Process heartbeat\", operationId = \"pingProcess\")\n    public void ping(@PathParam(\"id\") UUID instanceId) {\n        if (!queueDao.touch(instanceId)) {\n            throw new IllegalArgumentException(\"Process not found: \" + instanceId);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/ProcessKind.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic enum  ProcessKind {\n\n    /**\n     * Regular process.\n     */\n    DEFAULT,\n\n    /**\n     * Process running an failure-handling flow of a parent process.\n     * Created when a parent process crashes, exits with an error or\n     * otherwise fails.\n     */\n    FAILURE_HANDLER,\n\n    /**\n     * Process running a cancel-handling flow of a parent process.\n     * Created when a user cancels a process.\n     */\n    CANCEL_HANDLER,\n\n    /**\n     * Process running a timeout-handling flow of a parent process.\n     * Created when process terminated by timeout.\n     */\n    TIMEOUT_HANDLER\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/ProcessKvResource.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.org.project.KvManager;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueManager;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.Parameter;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport java.util.UUID;\n\n@Path(\"/api/v1/process\")\n@Tag(name = \"Process KV store\")\npublic class ProcessKvResource implements Resource {\n\n    private static final Logger log = LoggerFactory.getLogger(ProcessKvResource.class);\n\n    private static final UUID DEFAULT_PROJECT_ID = UUID.fromString(\"00000000-0000-0000-0000-000000000000\");\n\n    private final ProcessQueueManager processQueueManager;\n    private final KvManager kvManager;\n\n    @Inject\n    public ProcessKvResource(ProcessQueueManager processQueueManager, KvManager kvManager) {\n        this.processQueueManager = processQueueManager;\n        this.kvManager = kvManager;\n    }\n\n    @DELETE\n    @Path(\"{id}/kv/{key}\")\n    @Operation(description = \"Delete KV\", operationId = \"deleteKv\")\n    public void removeKey(@PathParam(\"id\") UUID instanceId,\n                          @PathParam(\"key\") String key) {\n\n        UUID projectId = assertProjectId(instanceId);\n        kvManager.remove(projectId, key);\n    }\n\n    @PUT\n    @Path(\"{id}/kv/{key}/string\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Put string KV\", operationId = \"putKvString\")\n    public void putString(@PathParam(\"id\") UUID instanceId,\n                          @PathParam(\"key\") String key,\n                          @Parameter(required = true) String value) {\n\n        UUID projectId = assertProjectId(instanceId);\n        kvManager.putString(projectId, key, value);\n    }\n\n    @GET\n    @Path(\"{id}/kv/{key}/string\")\n    @Produces(MediaType.TEXT_PLAIN)\n    @Operation(description = \"Get string KV\", operationId = \"getKvString\")\n    public String getString(@PathParam(\"id\") UUID instanceId,\n                            @PathParam(\"key\") String key) {\n\n        UUID projectId = assertProjectId(instanceId);\n        return kvManager.getString(projectId, key);\n    }\n\n    @PUT\n    @Path(\"{id}/kv/{key}/long\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Put long KV\", operationId = \"putKvLong\")\n    public void putLong(@PathParam(\"id\") UUID instanceId,\n                        @PathParam(\"key\") String key,\n                        @Parameter(required = true) long value) {\n\n        UUID projectId = assertProjectId(instanceId);\n        kvManager.putLong(projectId, key, value);\n    }\n\n    @GET\n    @Path(\"{id}/kv/{key}/long\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Get long KV\", operationId = \"getKvLong\")\n    public Long getLong(@PathParam(\"id\") UUID instanceId,\n                        @PathParam(\"key\") String key) {\n\n        UUID projectId = assertProjectId(instanceId);\n        return kvManager.getLong(projectId, key);\n    }\n\n    @POST\n    @Path(\"{id}/kv/{key}/inc\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Inc long KV\", operationId = \"incKvLong\")\n    public long incLong(@PathParam(\"id\") UUID instanceId,\n                        @PathParam(\"key\") String key) {\n\n        UUID projectId = assertProjectId(instanceId);\n        return kvManager.inc(projectId, key);\n    }\n\n    private UUID assertProjectId(UUID instanceId) {\n        UUID projectId = processQueueManager.getProjectId(PartialProcessKey.from(instanceId));\n        if (projectId == null) {\n            log.warn(\"assertProjectId ['{}'] -> no project found, using the default value\", instanceId);\n            projectId = DEFAULT_PROJECT_ID;\n        }\n\n        return projectId;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/ProcessLogResourceV2.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.HttpUtils;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.cfg.ProcessConfiguration;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogAccessManager;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.media.Content;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport io.swagger.v3.oas.annotations.parameters.RequestBody;\nimport io.swagger.v3.oas.annotations.responses.ApiResponse;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport javax.ws.rs.core.StreamingOutput;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.List;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.process.logs.ProcessLogsDao.ProcessLog;\nimport static com.walmartlabs.concord.server.process.logs.ProcessLogsDao.ProcessLogChunk;\n\n/**\n * API to work with segmented process logs.\n */\n@Path(\"/api/v2/process\")\n@Tag(name = \"ProcessLogV2\")\npublic class ProcessLogResourceV2 implements Resource {\n\n    private final ProcessManager processManager;\n    private final ProcessLogManager logManager;\n    private final ProcessLogAccessManager logAccessManager;\n    private final ProcessConfiguration processCfg;\n\n    @Inject\n    public ProcessLogResourceV2(ProcessManager processManager,\n                                ProcessLogManager logManager,\n                                ProcessLogAccessManager logAccessManager,\n                                ProcessConfiguration processCfg) {\n        this.processManager = processManager;\n        this.logManager = logManager;\n        this.logAccessManager = logAccessManager;\n        this.processCfg = processCfg;\n    }\n\n    /**\n     * List process log segments.\n     */\n    @GET\n    @Path(\"{id}/log/segment\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"List process log segments\", operationId = \"processLogSegments\")\n    public List<LogSegment> segments(@PathParam(\"id\") UUID instanceId,\n                                     @QueryParam(\"limit\") @DefaultValue(\"30\") int limit,\n                                     @QueryParam(\"offset\") @DefaultValue(\"0\") int offset) {\n\n        if (offset < 0) {\n            throw new ValidationErrorsException(\"'offset' must be a positive number or zero\");\n        }\n\n        ProcessKey processKey = logAccessManager.assertLogAccess(instanceId);\n        return logManager.listSegments(processKey, limit, offset);\n    }\n\n    /**\n     * Create a new process log segment.\n     */\n    @POST\n    @Path(\"{id}/log/segment\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"Create process log segment\", operationId = \"createProcessLogSegment\")\n    public LogSegmentOperationResponse segment(@PathParam(\"id\") UUID instanceId,\n                                               LogSegmentRequest request) {\n\n        ProcessKey processKey = logAccessManager.assertLogAccess(instanceId);\n        long segmentId = logManager.createSegment(processKey, request.correlationId(), request.name(), request.createdAt());\n        return new LogSegmentOperationResponse(segmentId, OperationResult.CREATED);\n    }\n\n    /**\n     * Update a process log segment.\n     */\n    @POST\n    @Path(\"{id}/log/segment/{segmentId}\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"Update process log segment\", operationId = \"updateProcessLogSegment\")\n    public LogSegmentOperationResponse updateSegment(@PathParam(\"id\") UUID instanceId,\n                                                     @PathParam(\"segmentId\") long segmentId,\n                                                     LogSegmentUpdateRequest request) {\n\n        ProcessKey processKey = logAccessManager.assertLogAccess(instanceId);\n        logManager.updateSegment(processKey, segmentId, request.status(), request.warnings(), request.errors());\n        return new LogSegmentOperationResponse(segmentId, OperationResult.UPDATED);\n    }\n\n    /**\n     * Retrieves a log segment' data.\n     */\n    @GET\n    @Path(\"/{id}/log/segment/{segmentId}/data\")\n    @Produces(MediaType.APPLICATION_OCTET_STREAM)\n    @WithTimer\n    @Operation(description = \"Retrieve segment the log\", operationId = \"getProcessLogSegmentData\")\n    @ApiResponse(description = \"Data of process log segment\",\n            content = @Content(mediaType = MediaType.APPLICATION_OCTET_STREAM,\n                    schema = @Schema(type = \"string\", format = \"binary\"))\n    )\n    public Response data(@PathParam(\"id\") UUID instanceId,\n                         @PathParam(\"segmentId\") long segmentId,\n                         @HeaderParam(\"range\") String rangeHeader) {\n\n        ProcessKey processKey = logAccessManager.assertLogAccess(instanceId);\n        HttpUtils.Range range = HttpUtils.parseRangeHeaderValue(rangeHeader);\n        ProcessLog l = logManager.segmentData(processKey, segmentId, range.start(), range.end());\n        return toResponse(instanceId, segmentId, l, range);\n    }\n\n    /**\n     * Appends a process' log.\n     */\n    @POST\n    @Path(\"{id}/log/segment/{segmentId}/data\")\n    @Consumes(MediaType.APPLICATION_OCTET_STREAM)\n    @WithTimer\n    @Operation(description = \"Appends a process' log\", operationId = \"appendProcessLogSegment\")\n    @RequestBody(description = \"Log content\", required = true,\n            content = @Content(mediaType = MediaType.APPLICATION_OCTET_STREAM,\n                    schema = @Schema(type = \"string\", format = \"binary\")\n            )\n    )\n    public void append(@PathParam(\"id\") UUID instanceId,\n                       @PathParam(\"segmentId\") long segmentId,\n                       InputStream data) {\n\n        ProcessKey processKey = logAccessManager.assertLogAccess(instanceId);\n\n        try {\n            byte[] ab = data.readAllBytes();\n            int upper = logManager.log(processKey, segmentId, ab);\n\n            int logSizeLimit = processCfg.getLogSizeLimit();\n            if (upper >= logSizeLimit) {\n                logManager.error(processKey, \"Maximum log size reached: {}. Process cancelled.\", logSizeLimit);\n                processManager.kill(processKey);\n            }\n        } catch (IOException e) {\n            throw new ConcordApplicationException(\"Error while appending a log: \" + e.getMessage());\n        }\n    }\n\n    public static Response toResponse(UUID instanceId, long segmentId, ProcessLog l, HttpUtils.Range range) {\n        List<ProcessLogChunk> data = l.getChunks();\n        if (data.isEmpty()) {\n            int actualStart = range.start() != null ? range.start() : 0;\n            int actualEnd = range.end() != null ? range.end() : actualStart;\n            return downloadableFile(instanceId, segmentId, null, actualStart, actualEnd, l.getSize());\n        }\n\n        ProcessLogChunk firstChunk = data.get(0);\n        int actualStart = firstChunk.getStart();\n\n        ProcessLogChunk lastChunk = data.get(data.size() - 1);\n        int actualEnd = lastChunk.getStart() + lastChunk.getData().length;\n\n        StreamingOutput out = output -> {\n            for (ProcessLogChunk e : data) {\n                output.write(e.getData());\n            }\n        };\n\n        return downloadableFile(instanceId, segmentId, out, actualStart, actualEnd, l.getSize());\n    }\n\n    private static Response downloadableFile(UUID instanceId, long segmentId, StreamingOutput out, int start, int end, int size) {\n        return (out != null ? Response.ok(out) : Response.ok())\n                .header(\"Content-Range\", \"bytes \" + start + \"-\" + end + \"/\" + size)\n                .header(\"Content-Type\", MediaType.APPLICATION_OCTET_STREAM)\n                .header(\"Content-Disposition\", \"attachment; filename=\\\"\" + instanceId + \"_\" + segmentId + \".log\\\"\")\n                .build();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/ProcessManager.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.ExclusiveMode;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.agent.AgentManager;\nimport com.walmartlabs.concord.server.audit.AuditAction;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.audit.AuditObject;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.org.project.ProjectAccessManager;\nimport com.walmartlabs.concord.server.org.project.RepositoryDao;\nimport com.walmartlabs.concord.server.org.project.RepositoryEntry;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.process.pipelines.ForkPipeline;\nimport com.walmartlabs.concord.server.process.pipelines.NewProcessPipeline;\nimport com.walmartlabs.concord.server.process.pipelines.ResumePipeline;\nimport com.walmartlabs.concord.server.process.pipelines.processors.Chain;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueDao;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueManager;\nimport com.walmartlabs.concord.server.process.state.ProcessCheckpointManager;\nimport com.walmartlabs.concord.server.process.state.ProcessStateManager;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.security.sessionkey.SessionKeyPrincipal;\nimport org.jooq.DSLContext;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.core.Response.Status;\nimport java.io.*;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.agent.AgentManager.KeyAndAgent;\nimport static com.walmartlabs.concord.server.process.state.ProcessStateManager.path;\n\npublic class ProcessManager {\n\n    private static final Logger log = LoggerFactory.getLogger(ProcessManager.class);\n\n    private final ProcessQueueDao queueDao;\n    private final ProcessStateManager stateManager;\n    private final AgentManager agentManager;\n    private final ProcessLogManager logManager;\n    private final ProjectAccessManager projectAccessManager;\n    private final ProcessCheckpointManager checkpointManager;\n    private final PayloadManager payloadManager;\n    private final RepositoryDao repositoryDao;\n    private final ProcessQueueManager queueManager;\n    private final AuditLog auditLog;\n\n    private final Chain processPipeline;\n    private final Chain resumePipeline;\n    private final Chain forkPipeline;\n\n    private static final List<ProcessStatus> SERVER_PROCESS_STATUSES = Arrays.asList(\n            ProcessStatus.NEW,\n            ProcessStatus.PREPARING,\n            ProcessStatus.ENQUEUED,\n            ProcessStatus.WAITING,\n            ProcessStatus.SUSPENDED);\n\n    private static final List<ProcessStatus> TERMINATED_PROCESS_STATUSES = Arrays.asList(\n            ProcessStatus.CANCELLED,\n            ProcessStatus.FAILED,\n            ProcessStatus.FINISHED,\n            ProcessStatus.TIMED_OUT);\n\n    private static final List<ProcessStatus> AGENT_PROCESS_STATUSES = Arrays.asList(\n            ProcessStatus.STARTING,\n            ProcessStatus.RUNNING,\n            ProcessStatus.RESUMING);\n\n    private static final Set<ProcessStatus> RESTORE_ALLOWED_STATUSES = new HashSet<>(Arrays.asList(\n            ProcessStatus.FAILED,\n            ProcessStatus.FINISHED,\n            ProcessStatus.TIMED_OUT,\n            ProcessStatus.CANCELLED));\n\n    @Inject\n    public ProcessManager(ProcessQueueDao queueDao,\n                          ProcessStateManager stateManager,\n                          AgentManager agentManager,\n                          ProcessLogManager logManager,\n                          ProjectAccessManager projectAccessManager,\n                          ProcessCheckpointManager checkpointManager,\n                          PayloadManager payloadManager,\n                          RepositoryDao repositoryDao,\n                          ProcessQueueManager queueManager,\n                          AuditLog auditLog,\n                          NewProcessPipeline processPipeline,\n                          ResumePipeline resumePipeline,\n                          ForkPipeline forkPipeline) {\n\n        this.queueDao = queueDao;\n        this.stateManager = stateManager;\n        this.agentManager = agentManager;\n        this.logManager = logManager;\n        this.queueManager = queueManager;\n        this.projectAccessManager = projectAccessManager;\n        this.checkpointManager = checkpointManager;\n        this.payloadManager = payloadManager;\n        this.repositoryDao = repositoryDao;\n        this.auditLog = auditLog;\n\n        this.processPipeline = processPipeline;\n        this.resumePipeline = resumePipeline;\n        this.forkPipeline = forkPipeline;\n    }\n\n    public ProcessResult start(Payload payload) {\n        return start(processPipeline, payload);\n    }\n\n    public ProcessResult startFork(Payload payload) {\n        return start(forkPipeline, payload);\n    }\n\n    public void restart(ProcessKey processKey) {\n        ProcessKey rootProcessKey = queueDao.getRootId(processKey);\n        if (rootProcessKey == null) {\n            throw new ProcessException(processKey, \"Process not found: \" + processKey, Status.NOT_FOUND);\n        }\n\n        ProcessEntry e = queueManager.get(rootProcessKey);\n        if (e == null) {\n            throw new ProcessException(processKey, \"Process not found: \" + processKey, Status.NOT_FOUND);\n        }\n\n        // TODO: rename to assertProcessOperationRights or something\n        assertKillOrDisableOrRestartRights(e);\n\n        ProcessStatus s = e.status();\n        if (!TERMINATED_PROCESS_STATUSES.contains(s)) {\n            throw new ProcessException(rootProcessKey, \"Can't restart running process: \" + processKey, Status.CONFLICT);\n        }\n\n        // put new attemptNO somewhere\n\n        queueDao.tx(tx -> {\n            boolean updated = queueManager.updateExpectedStatus(tx, rootProcessKey, e.status(), ProcessStatus.NEW);\n            if (updated) {\n                List<ProcessKey> allProcesses = queueDao.getCascade(tx, rootProcessKey)\n                        .stream()\n                        .filter(p -> !p.equals(rootProcessKey))\n                        .toList();\n                kill(tx, allProcesses);\n\n                stateManager.delete(tx, rootProcessKey);\n            }\n        });\n    }\n\n    public void resume(Payload payload) {\n        log.info(\"resume ['{}']\", payload.getProcessKey());\n        resumePipeline.process(payload);\n        log.info(\"resume ['{}'] -> done\", payload.getProcessKey());\n    }\n\n    public void disable(ProcessKey processKey, boolean disabled) {\n        ProcessEntry e = queueManager.get(processKey);\n        if (e == null) {\n            throw new ProcessException(processKey, \"Process not found: \" + processKey, Status.NOT_FOUND);\n        }\n\n        assertKillOrDisableOrRestartRights(e);\n\n        ProcessStatus s = e.status();\n        if (TERMINATED_PROCESS_STATUSES.contains(s)) {\n            queueDao.disable(processKey, disabled);\n        }\n    }\n\n    public void kill(ProcessKey processKey) {\n        queueDao.tx(tx -> kill(tx, processKey));\n    }\n\n    public void kill(DSLContext tx, ProcessKey processKey) {\n        ProcessEntry process = assertProcess(tx, processKey);\n\n        assertKillOrDisableOrRestartRights(process);\n\n        boolean cancelled = false;\n        while (!cancelled) {\n            if (TERMINATED_PROCESS_STATUSES.contains(process.status())) {\n                return;\n            }\n\n            boolean isServerProcess = SERVER_PROCESS_STATUSES.contains(process.status());\n            if (isServerProcess) {\n                cancelled = queueManager.updateExpectedStatus(tx, processKey, process.status(), ProcessStatus.CANCELLED);\n            }\n\n            if (!cancelled && process.lastAgentId() != null) {\n                agentManager.killProcess(tx, processKey, process.lastAgentId());\n                cancelled = true;\n            }\n\n            if (cancelled) {\n                auditLogOnCancelled(process);\n            } else {\n                process = assertProcess(tx, processKey);\n            }\n        }\n    }\n\n    public void kill(DSLContext tx, List<ProcessKey> processKeys) {\n        List<ProcessKey> keys = new ArrayList<>(processKeys);\n        while(!keys.isEmpty()) {\n            // TODO: better way\n            List<ProcessEntry> processes = keys.stream()\n                    .map(k -> queueDao.get(tx, k, Collections.emptySet()))\n                    .collect(Collectors.toList());\n\n            List<ProcessEntry> terminatedProcesses = filterProcesses(processes, TERMINATED_PROCESS_STATUSES);\n            terminatedProcesses.forEach(p -> keys.remove(new ProcessKey(p.instanceId(), p.createdAt())));\n\n            List<ProcessEntry> serverProcesses = filterProcesses(processes, SERVER_PROCESS_STATUSES);\n            if (!serverProcesses.isEmpty()) {\n                List<ProcessKey> serverProcessKeys = serverProcesses.stream()\n                        .map(p -> new ProcessKey(p.instanceId(), p.createdAt()))\n                        .collect(Collectors.toList());\n\n                List<ProcessKey> updated = queueManager.updateExpectedStatus(tx, serverProcessKeys, SERVER_PROCESS_STATUSES, ProcessStatus.CANCELLED);\n                serverProcesses.stream()\n                        .filter(p -> updated.contains(new ProcessKey(p.instanceId(), p.createdAt())))\n                        .forEach(this::auditLogOnCancelled);\n\n                keys.removeAll(updated);\n            }\n\n            List<ProcessEntry> agentProcesses = filterProcesses(processes, AGENT_PROCESS_STATUSES);\n            if (!agentProcesses.isEmpty()) {\n                agentManager.killProcess(agentProcesses.stream().map(p -> new KeyAndAgent(new ProcessKey(p.instanceId(), p.createdAt()), p.lastAgentId())).collect(Collectors.toList()));\n\n                agentProcesses.forEach(this::auditLogOnCancelled);\n\n                agentProcesses.forEach(p -> keys.remove(new ProcessKey(p.instanceId(), p.createdAt())));\n            }\n        }\n    }\n\n    public void killCascade(PartialProcessKey processKey) {\n        ProcessEntry e = queueManager.get(processKey);\n        if (e == null) {\n            throw new ProcessException(processKey, \"Process not found: \" + processKey, Status.NOT_FOUND);\n        }\n\n        assertKillOrDisableOrRestartRights(e);\n\n        List<ProcessKey> allProcesses = queueDao.getCascade(processKey);\n        queueDao.tx(tx -> kill(tx, allProcesses));\n    }\n\n    public void restoreFromCheckpoint(ProcessKey processKey, UUID checkpointId) {\n        ProcessEntry entry = queueManager.get(processKey);\n\n        checkpointManager.assertProcessAccess(entry);\n\n        if (checkpointId == null) {\n            throw new ConcordApplicationException(\"'checkpointId' is mandatory\");\n        }\n\n        if (entry.disabled()) {\n            throw new ConcordApplicationException(\"Checkpoint can not be restored as process is disabled -> \" + entry.instanceId());\n        }\n\n        ProcessStatus s = entry.status();\n        if (!RESTORE_ALLOWED_STATUSES.contains(s)) {\n            throw new ConcordApplicationException(\"Unable to restore a checkpoint, the process is \" + s);\n        }\n\n        ProcessCheckpointManager.CheckpointInfo checkpointInfo = checkpointManager.restoreCheckpoint(processKey, checkpointId);\n        if (checkpointInfo == null) {\n            throw new ConcordApplicationException(\"Checkpoint \" + checkpointId + \" not found\");\n        }\n\n        Payload payload;\n        try {\n            payload = payloadManager.createResumePayload(processKey, checkpointInfo.eventName(), null);\n        } catch (IOException e) {\n            log.error(\"restore ['{}', '{}'] -> error creating a payload\", processKey, checkpointInfo.name(), e);\n            throw new ConcordApplicationException(\"Error creating a payload\", e);\n        }\n\n        queueManager.restore(processKey, checkpointId, entry.status());\n\n        logManager.info(processKey, \"Restoring from checkpoint '{}'\", checkpointInfo.name());\n\n        resume(payload);\n    }\n\n    public void updateStatus(ProcessKey processKey, String agentId, ProcessStatus status) {\n        assertUpdateRights(processKey);\n\n        // TODO determine the correct status on the agent?\n        if (status == ProcessStatus.FINISHED && isSuspended(processKey)) {\n            status = ProcessStatus.SUSPENDED;\n        }\n\n        if (status == ProcessStatus.CANCELLED && isFinished(processKey)) {\n            log.info(\"updateStatus [{}, '{}', {}] -> ignored, process finished\", processKey, agentId, status);\n            return;\n        }\n\n        queueManager.updateAgentId(processKey, agentId, status);\n        logManager.info(processKey, \"Process status: {}\", status);\n\n        log.info(\"updateStatus [{}, '{}', {}] -> done\", processKey, agentId, status);\n    }\n\n    public ProcessEntry assertProcess(UUID instanceId) {\n        ProcessEntry p = queueManager.get(PartialProcessKey.from(instanceId));\n        if (p == null) {\n            throw new ConcordApplicationException(\"Process instance not found\", Status.NOT_FOUND);\n        }\n        return p;\n    }\n\n    public void assertResumeEvents(ProcessKey processKey, Set<String> events) {\n        if (events.isEmpty()) {\n            throw new ConcordApplicationException(\"Empty resume events\", Status.BAD_REQUEST);\n        }\n\n        Set<String> expectedEvents = getResumeEvents(processKey);\n\n        Set<String> unexpectedEvents = new HashSet<>(events);\n        unexpectedEvents.removeAll(expectedEvents);\n\n        if (!unexpectedEvents.isEmpty()) {\n            logManager.warn(processKey, \"Unexpected 'resume' events: {}, expected: {}\", unexpectedEvents, expectedEvents);\n            throw new ConcordApplicationException(\"Unexpected 'resume' events: \" + unexpectedEvents, Status.BAD_REQUEST);\n        }\n    }\n\n    public void updateExclusive(DSLContext tx, ProcessKey processKey, ExclusiveMode exclusive) {\n        queueDao.updateExclusive(tx, processKey, exclusive);\n    }\n\n    private ProcessEntry assertProcess(DSLContext tx, ProcessKey processKey) {\n        ProcessEntry process = queueDao.get(tx, processKey, Collections.emptySet());\n        if (process != null) {\n            return process;\n        }\n        throw new ProcessException(processKey, \"Process not found: \" + processKey, Status.NOT_FOUND);\n    }\n\n    private boolean isSuspended(ProcessKey processKey) {\n        String resource = path(Constants.Files.JOB_ATTACHMENTS_DIR_NAME,\n                Constants.Files.JOB_STATE_DIR_NAME,\n                Constants.Files.SUSPEND_MARKER_FILE_NAME);\n\n        return stateManager.exists(processKey, resource);\n    }\n\n    private boolean isFinished(PartialProcessKey processKey) {\n        ProcessStatus status = queueDao.getStatus(processKey);\n        if (status == null) {\n            return true;\n        }\n\n        return TERMINATED_PROCESS_STATUSES.contains(status);\n    }\n\n    private ProcessResult start(Chain pipeline, Payload payload) {\n        assertRepositoryDisabled(payload);\n\n        ProcessKey processKey = payload.getProcessKey();\n\n        try {\n            pipeline.process(payload);\n        } catch (ProcessException e) {\n            throw e;\n        } catch (Exception e) {\n            log.error(\"start ['{}'] -> error starting the process\", processKey, e);\n            throw new ProcessException(processKey, \"Error starting the process: \" + e.getMessage(), e, Status.INTERNAL_SERVER_ERROR);\n        }\n\n        UUID instanceId = processKey.getInstanceId();\n        return new ProcessResult(instanceId);\n    }\n\n    private void assertRepositoryDisabled(Payload payload) {\n        UUID repoId = payload.getHeader(Payload.REPOSITORY_ID);\n        if (repoId == null) {\n            return;\n        }\n\n        RepositoryEntry repo = repositoryDao.get(repoId);\n        if (repo.isDisabled()) {\n            throw new ConcordApplicationException(\"Repository is disabled -> \" + repo.getName());\n        }\n    }\n\n    private void assertKillOrDisableOrRestartRights(ProcessEntry e) {\n        if (Roles.isAdmin()) {\n            return;\n        }\n\n        UserPrincipal p = UserPrincipal.assertCurrent();\n        if (p.getId().equals(e.initiatorId())) {\n            // process owners can kill or disable their own processes\n            return;\n        }\n\n        UUID projectId = e.projectId();\n        if (projectId != null) {\n            // only org members with WRITER rights can kill/disable/restart the process\n            projectAccessManager.assertAccess(projectId, ResourceAccessLevel.WRITER, true);\n            return;\n        }\n\n        throw new UnauthorizedException(\"The current user (\" + p.getUsername() + \") does not have permissions \" +\n                \"to kill or disable the process: \" + e.instanceId());\n    }\n\n    private void assertUpdateRights(PartialProcessKey processKey) {\n        if (Roles.isAdmin() || Roles.isGlobalWriter()) {\n            return;\n        }\n\n        UserPrincipal p = UserPrincipal.assertCurrent();\n\n        SessionKeyPrincipal s = SessionKeyPrincipal.getCurrent();\n        if (s != null && processKey.partOf(s.getProcessKey())) {\n            // processes can update their own statuses\n            return;\n        }\n\n        throw new UnauthorizedException(\"The current user (\" + p.getUsername() + \") does not have permissions \" +\n                \"to update the process status: \" + processKey);\n    }\n\n    public void auditLogOnCancelled(ProcessEntry p) {\n        auditLog.add(AuditObject.PROCESS, AuditAction.DELETE)\n                .field(\"instanceId\", p.instanceId())\n                .field(\"status\", p.status())\n                .field(\"orgId\", p.orgId())\n                .field(\"projectId\", p.projectId())\n                .log();\n    }\n\n    private Set<String> getResumeEvents(ProcessKey processKey) {\n        String path = ProcessStateManager.path(Constants.Files.JOB_ATTACHMENTS_DIR_NAME,\n                Constants.Files.JOB_STATE_DIR_NAME,\n                Constants.Files.SUSPEND_MARKER_FILE_NAME);\n\n        return stateManager.get(processKey, path, ProcessManager::deserialize)\n                .orElse(Set.of());\n    }\n\n    private static Optional<Set<String>> deserialize(InputStream in) {\n        Set<String> result = new HashSet<>();\n        try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {\n            String line;\n            while ((line = reader.readLine()) != null) {\n                result.add(line);\n            }\n            return Optional.of(result);\n        } catch (IOException e) {\n            throw new RuntimeException(\"Error while deserializing a resume events: \" + e.getMessage(), e);\n        }\n    }\n\n    private static List<ProcessEntry> filterProcesses(List<ProcessEntry> l, List<ProcessStatus> expected) {\n        return l.stream()\n                .filter(r -> expected.contains(r.status()))\n                .collect(Collectors.toList());\n    }\n\n    public static final class ProcessResult implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        private final UUID instanceId;\n\n        public ProcessResult(UUID instanceId) {\n            this.instanceId = instanceId;\n        }\n\n        public UUID getInstanceId() {\n            return instanceId;\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/ProcessModule.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.walmartlabs.concord.imports.ImportManager;\nimport com.walmartlabs.concord.process.loader.ProjectLoader;\nimport com.walmartlabs.concord.runtime.v1.ProjectLoaderV1;\nimport com.walmartlabs.concord.runtime.v2.ProjectLoaderV2;\nimport com.walmartlabs.concord.server.process.checkpoint.ProcessCheckpointResource;\nimport com.walmartlabs.concord.server.process.checkpoint.ProcessCheckpointV2Resource;\nimport com.walmartlabs.concord.server.process.event.ProcessEventDao;\nimport com.walmartlabs.concord.server.process.event.ProcessEventManager;\nimport com.walmartlabs.concord.server.process.event.ProcessEventResource;\nimport com.walmartlabs.concord.server.process.form.FormModule;\nimport com.walmartlabs.concord.server.process.locks.ProcessLocksDao;\nimport com.walmartlabs.concord.server.process.locks.ProcessLocksResource;\nimport com.walmartlabs.concord.server.process.locks.ProcessLocksWatchdog;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogAccessManager;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.process.pipelines.processors.ExclusiveGroupProcessor;\nimport com.walmartlabs.concord.server.process.pipelines.processors.InvalidProcessStateExceptionMapper;\nimport com.walmartlabs.concord.server.process.pipelines.processors.TemplateScriptProcessor;\nimport com.walmartlabs.concord.server.process.pipelines.processors.policy.*;\nimport com.walmartlabs.concord.server.process.queue.*;\nimport com.walmartlabs.concord.server.process.queue.dispatcher.ConcurrentProcessFilter;\nimport com.walmartlabs.concord.server.process.queue.dispatcher.Dispatcher;\nimport com.walmartlabs.concord.server.process.queue.dispatcher.ExclusiveProcessFilter;\nimport com.walmartlabs.concord.server.process.queue.dispatcher.Filter;\nimport com.walmartlabs.concord.server.process.state.ProcessCheckpointDao;\nimport com.walmartlabs.concord.server.process.state.ProcessCheckpointManager;\nimport com.walmartlabs.concord.server.process.state.ProcessStateManager;\nimport com.walmartlabs.concord.server.process.waits.*;\nimport com.walmartlabs.concord.server.sdk.BackgroundTask;\nimport com.walmartlabs.concord.server.sdk.ProcessKeyCache;\nimport com.walmartlabs.concord.server.sdk.log.ProcessLogListener;\nimport com.walmartlabs.concord.server.sdk.process.CustomEnqueueProcessor;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\nimport static com.walmartlabs.concord.server.Utils.*;\n\npublic class ProcessModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        binder.bind(ImportManager.class).toProvider(ImportManagerProvider.class);\n        binder.bind(ProcessEventDao.class).in(SINGLETON);\n        binder.bind(ProcessEventManager.class).in(SINGLETON);\n        binder.bind(ProcessLocksDao.class).in(SINGLETON);\n        binder.bind(ProcessLocksDao.class).in(SINGLETON);\n        binder.bind(ProcessLogAccessManager.class).in(SINGLETON);\n        binder.bind(ProcessLogManager.class).in(SINGLETON);\n        binder.bind(ProcessSecurityContext.class).in(SINGLETON);\n        binder.bind(ProcessCheckpointDao.class).in(SINGLETON);\n        binder.bind(ProcessCheckpointManager.class).in(SINGLETON);\n        binder.bind(ProcessStateManager.class).in(SINGLETON);\n        binder.bind(ProcessWaitManager.class).in(SINGLETON);\n\n        newSetBinder(binder, ProjectLoader.class).addBinding().to(ProjectLoaderV1.class);\n        newSetBinder(binder, ProjectLoader.class).addBinding().to(ProjectLoaderV2.class);\n\n        bindSingletonScheduledTask(binder, ProcessCleaner.class);\n        bindSingletonScheduledTask(binder, ProcessLocksWatchdog.class);\n        bindSingletonScheduledTask(binder, ProcessQueueWatchdog.class);\n        bindSingletonScheduledTask(binder, ProcessWaitWatchdog.class);\n\n        binder.bind(Dispatcher.class).in(SINGLETON);\n        newSetBinder(binder, BackgroundTask.class).addBinding().to(Dispatcher.class);\n        newSetBinder(binder, BackgroundTask.class).addBinding().toProvider(EnqueuedTaskProvider.class).in(SINGLETON);\n\n        newSetBinder(binder, ProcessStatusListener.class).addBinding().to(WaitProcessStatusListener.class);\n        newSetBinder(binder, ProcessStatusListener.class).addBinding().to(ExternalProcessListenerHandler.class);\n        newSetBinder(binder, ProcessStatusListener.class).addBinding().to(WaitConditionUpdater.class);\n        newSetBinder(binder, ProcessStatusListener.class).addBinding().to(TotalRuntimeCalculator.class);\n\n        newSetBinder(binder, Filter.class).addBinding().to(ConcurrentProcessFilter.class);\n        newSetBinder(binder, Filter.class).addBinding().to(ExclusiveProcessFilter.class);\n\n        newSetBinder(binder, ProcessWaitHandler.class).addBinding().to(WaitProcessFinishHandler.class);\n        newSetBinder(binder, ProcessWaitHandler.class).addBinding().to(WaitProcessLockHandler.class);\n        newSetBinder(binder, ProcessWaitHandler.class).addBinding().to(WaitProcessSleepHandler.class);\n\n        newSetBinder(binder, ProcessLogListener.class);\n\n        newSetBinder(binder, PolicyApplier.class).addBinding().to(ContainerPolicyApplier.class);\n        newSetBinder(binder, PolicyApplier.class).addBinding().to(FilePolicyApplier.class);\n        newSetBinder(binder, PolicyApplier.class).addBinding().to(ProcessRuntimePolicyApplier.class);\n        newSetBinder(binder, PolicyApplier.class).addBinding().to(ProcessTimeoutPolicyApplier.class);\n        newSetBinder(binder, PolicyApplier.class).addBinding().to(WorkspacePolicyApplier.class);\n\n        newSetBinder(binder, CustomEnqueueProcessor.class);\n\n        bindJaxRsResource(binder, ProcessCheckpointResource.class);\n        bindJaxRsResource(binder, ProcessCheckpointV2Resource.class);\n        bindJaxRsResource(binder, ProcessEventResource.class);\n        bindJaxRsResource(binder, ProcessHeartbeatResource.class);\n        bindJaxRsResource(binder, ProcessKvResource.class);\n        bindJaxRsResource(binder, ProcessLocksResource.class);\n        bindJaxRsResource(binder, ProcessLogResourceV2.class);\n        bindJaxRsResource(binder, ProcessResource.class);\n        bindJaxRsResource(binder, ProcessResourceV2.class);\n\n        bindExceptionMapper(binder, InvalidProcessStateExceptionMapper.class);\n        bindExceptionMapper(binder, ProcessExceptionMapper.class);\n\n        binder.bind(TemplateScriptProcessor.class).in(SINGLETON);\n        binder.bind(ProcessKeyCache.class).to(com.walmartlabs.concord.server.process.queue.ProcessKeyCache.class).in(SINGLETON);\n\n        binder.install(new ExclusiveGroupProcessor.ModeProcessorModule());\n        binder.install(new FormModule());\n        binder.install(new ProcessQueueGaugeModule());\n        binder.install(new ProcessKeyCacheGaugeModule());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/ProcessResource.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.ZipUtils;\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.policyengine.AttachmentsRule;\nimport com.walmartlabs.concord.policyengine.CheckResult;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.HttpUtils;\nimport com.walmartlabs.concord.server.MultipartUtils;\nimport com.walmartlabs.concord.server.OffsetDateTimeParam;\nimport com.walmartlabs.concord.server.UuidGenerator;\nimport com.walmartlabs.concord.server.cfg.ProcessConfiguration;\nimport com.walmartlabs.concord.server.cfg.SecretStoreConfiguration;\nimport com.walmartlabs.concord.server.events.ExpressionUtils;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.org.project.EncryptedProjectValueManager;\nimport com.walmartlabs.concord.server.org.project.ProjectAccessManager;\nimport com.walmartlabs.concord.server.policy.PolicyException;\nimport com.walmartlabs.concord.server.policy.PolicyManager;\nimport com.walmartlabs.concord.server.process.ProcessEntry.ProcessStatusHistoryEntry;\nimport com.walmartlabs.concord.server.process.ProcessEntry.ProcessWaitEntry;\nimport com.walmartlabs.concord.server.process.ProcessManager.ProcessResult;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogAccessManager;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogsDao.ProcessLog;\nimport com.walmartlabs.concord.server.process.queue.ProcessFilter;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueDao;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueManager;\nimport com.walmartlabs.concord.server.process.state.ProcessStateManager;\nimport com.walmartlabs.concord.server.process.waits.AbstractWaitCondition;\nimport com.walmartlabs.concord.server.process.waits.ProcessWaitManager;\nimport com.walmartlabs.concord.server.sdk.*;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.security.sessionkey.SessionKeyPrincipal;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.Parameter;\nimport io.swagger.v3.oas.annotations.media.Content;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport io.swagger.v3.oas.annotations.parameters.RequestBody;\nimport io.swagger.v3.oas.annotations.responses.ApiResponse;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;\nimport org.jboss.resteasy.plugins.providers.multipart.MultipartInput;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.validation.constraints.NotNull;\nimport javax.validation.constraints.Size;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.*;\nimport javax.ws.rs.core.Response.Status;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.text.MessageFormat;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.process.state.ProcessStateManager.path;\nimport static com.walmartlabs.concord.server.process.state.ProcessStateManager.zipTo;\nimport static java.util.Objects.requireNonNull;\n\n@javax.ws.rs.Path(\"/api/v1/process\")\n@Tag(name = \"Process\")\npublic class ProcessResource implements Resource {\n\n    private static final Logger log = LoggerFactory.getLogger(ProcessResource.class);\n\n    private final ProcessWaitManager processWaitManager;\n    private final ProcessManager processManager;\n    private final ProcessQueueDao queueDao;\n    private final ProcessQueueManager processQueueManager;\n    private final PayloadManager payloadManager;\n    private final ProcessStateManager stateManager;\n    private final SecretStoreConfiguration secretStoreCfg;\n    private final EncryptedProjectValueManager encryptedValueManager;\n    private final ProcessKeyCache processKeyCache;\n    private final ObjectMapper objectMapper;\n    private final ProjectAccessManager projectAccessManager;\n    private final ProcessConfiguration processCfg;\n    private final ProcessLogManager logManager;\n    private final ProcessLogAccessManager logAccessManager;\n    private final ProcessLogManager processLogManager;\n    private final PolicyManager policyManager;\n    private final UuidGenerator uuidGenerator;\n\n    private final ProcessResourceV2 v2;\n\n    @Inject\n    public ProcessResource(ProcessWaitManager processWaitManager,\n                           ProcessManager processManager,\n                           ProcessQueueDao queueDao,\n                           ProcessQueueManager processQueueManager,\n                           PayloadManager payloadManager,\n                           ProcessStateManager stateManager,\n                           SecretStoreConfiguration secretStoreCfg,\n                           EncryptedProjectValueManager encryptedValueManager,\n                           ProjectAccessManager projectAccessManager,\n                           ProcessKeyCache processKeyCache,\n                           ObjectMapper objectMapper,\n                           ProcessConfiguration processCfg,\n                           ProcessLogManager logManager,\n                           ProcessLogAccessManager logAccessManager,\n                           ProcessLogManager processLogManager,\n                           PolicyManager policyManager,\n                           UuidGenerator uuidGenerator,\n                           ProcessResourceV2 v2) {\n\n        this.processWaitManager = requireNonNull(processWaitManager);\n        this.processManager = requireNonNull(processManager);\n        this.queueDao = requireNonNull(queueDao);\n        this.processQueueManager = requireNonNull(processQueueManager);\n        this.payloadManager = requireNonNull(payloadManager);\n        this.stateManager = requireNonNull(stateManager);\n        this.secretStoreCfg = requireNonNull(secretStoreCfg);\n        this.encryptedValueManager = requireNonNull(encryptedValueManager);\n        this.projectAccessManager = requireNonNull(projectAccessManager);\n        this.processKeyCache = requireNonNull(processKeyCache);\n        this.objectMapper = requireNonNull(objectMapper);\n        this.processCfg = requireNonNull(processCfg);\n        this.logManager = requireNonNull(logManager);\n        this.logAccessManager = requireNonNull(logAccessManager);\n        this.processLogManager = requireNonNull(processLogManager);\n        this.policyManager = requireNonNull(policyManager);\n        this.uuidGenerator = requireNonNull(uuidGenerator);\n\n        this.v2 = v2;\n    }\n\n    /**\n     * Starts a new process instance.\n     *\n     * @deprecated use {@link #start(MultipartInput, UUID, boolean, String[], HttpServletRequest)}\n     */\n    @POST\n    @Consumes(MediaType.APPLICATION_OCTET_STREAM)\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer(suffix = \"_octetstream\")\n    @Deprecated\n    public StartProcessResponse start(InputStream in,\n                                      @QueryParam(\"parentId\") UUID parentInstanceId,\n                                      @Deprecated @DefaultValue(\"false\") @QueryParam(\"sync\") boolean sync,\n                                      @QueryParam(\"out\") String[] out) {\n\n        throw new ConcordApplicationException(\"This API endpoint is no longer supported.\");\n    }\n\n    /**\n     * Starts a new process instance using the specified entry point and provided configuration.\n     *\n     * @deprecated use {@link #start(MultipartInput, UUID, boolean, String[], HttpServletRequest)}\n     */\n    @POST\n    @javax.ws.rs.Path(\"/{entryPoint}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer(suffix = \"_queryparams\")\n    @Deprecated\n    public StartProcessResponse start(@PathParam(\"entryPoint\") String entryPoint,\n                                      @QueryParam(\"parentId\") UUID parentInstanceId,\n                                      @Deprecated @DefaultValue(\"false\") @QueryParam(\"sync\") boolean sync,\n                                      @QueryParam(\"out\") String[] out) {\n\n        throw new ConcordApplicationException(\"This API endpoint is no longer supported.\");\n    }\n\n    /**\n     * Starts a new process instance using the specified entry point and provided configuration.\n     *\n     * @deprecated use {@link #start(MultipartInput, UUID, boolean, String[], HttpServletRequest)}\n     */\n    @POST\n    @javax.ws.rs.Path(\"/{entryPoint}\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer(suffix = \"_json\")\n    @Deprecated\n    public StartProcessResponse start(@PathParam(\"entryPoint\") String entryPoint,\n                                      Map<String, Object> req,\n                                      @QueryParam(\"parentId\") UUID parentInstanceId,\n                                      @Deprecated @DefaultValue(\"false\") @QueryParam(\"sync\") boolean sync,\n                                      @QueryParam(\"out\") String[] out) {\n\n        throw new ConcordApplicationException(\"This API endpoint is no longer supported.\");\n    }\n\n    /**\n     * Starts a new process instance.\n     */\n    @POST\n    @Consumes(MediaType.MULTIPART_FORM_DATA)\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"Start new process\", operationId = \"startProcess\")\n    public StartProcessResponse start(@Parameter(schema = @Schema(type = \"object\", implementation = Object.class)) MultipartInput input,\n                                      @Parameter(hidden = true) @Deprecated @QueryParam(\"parentId\") UUID parentInstanceId,\n                                      @Parameter(hidden = true) @Deprecated @DefaultValue(\"false\") @QueryParam(\"sync\") boolean sync,\n                                      @Parameter(hidden = true) @Deprecated @QueryParam(\"out\") String[] out,\n                                      @Context HttpServletRequest request) {\n\n        try {\n            boolean sync2 = MultipartUtils.getBoolean(input, Constants.Multipart.SYNC, false);\n            if (sync || sync2) {\n                throw syncIsForbidden();\n            }\n\n            Payload payload;\n            try {\n                payload = payloadManager.createPayload(input, request);\n\n                // TODO remove after deprecating the old endpoints\n                payload = PayloadBuilder.basedOn(payload)\n                        .parentInstanceId(parentInstanceId)\n                        .mergeOutExpressions(out)\n                        .build();\n            } catch (IOException e) {\n                log.error(\"start -> error creating a payload: {}\", e.getMessage());\n                throw new ConcordApplicationException(\"Error creating a payload\", e);\n            }\n\n            return toResponse(processManager.start(payload));\n        } finally {\n            input.close();\n        }\n    }\n\n    /**\n     * Starts a new process instance using the specified entry point and multipart request data.\n     *\n     * @deprecated use {@link #start(MultipartInput, UUID, boolean, String[], HttpServletRequest)}\n     */\n    @POST\n    @javax.ws.rs.Path(\"/{entryPoint}\")\n    @Consumes(MediaType.MULTIPART_FORM_DATA)\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer(suffix = \"_with_entrypoint\")\n    @Deprecated\n    public StartProcessResponse start(@PathParam(\"entryPoint\") String entryPoint,\n                                      MultipartInput input,\n                                      @QueryParam(\"parentId\") UUID parentInstanceId,\n                                      @Deprecated @DefaultValue(\"false\") @QueryParam(\"sync\") boolean sync,\n                                      @QueryParam(\"out\") String[] out) {\n\n        throw new ConcordApplicationException(\"This API endpoint is no longer supported.\");\n    }\n\n    /**\n     * Starts a new process instance using the specified entry point and payload archive.\n     *\n     * @deprecated use {@link #start(MultipartInput, UUID, boolean, String[], HttpServletRequest)}\n     */\n    @POST\n    @javax.ws.rs.Path(\"/{entryPoint}\")\n    @Consumes(MediaType.APPLICATION_OCTET_STREAM)\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer(suffix = \"_octetstream_and_entrypoint\")\n    @Deprecated\n    public StartProcessResponse start(@PathParam(\"entryPoint\") String entryPoint,\n                                      InputStream in,\n                                      @QueryParam(\"parentId\") UUID parentInstanceId,\n                                      @Deprecated @DefaultValue(\"false\") @QueryParam(\"sync\") boolean sync,\n                                      @QueryParam(\"out\") String[] out) {\n\n        throw new ConcordApplicationException(\"This API endpoint is no longer supported.\");\n    }\n\n    @POST\n    @javax.ws.rs.Path(\"/{id}/restart\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Restart process\", operationId = \"restartProcess\")\n    public StartProcessResponse restart(@PathParam(\"id\") UUID instanceId) {\n        ProcessKey processKey = assertProcessKey(instanceId);\n        processManager.restart(processKey);\n        return new StartProcessResponse(instanceId);\n    }\n\n    /**\n     * Resumes an existing process.\n     */\n    @POST\n    @javax.ws.rs.Path(\"/{id}/resume/{eventName}\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation\n    public ResumeProcessResponse resume(@PathParam(\"id\") UUID instanceId,\n                                        @PathParam(\"eventName\") @NotNull String eventName,\n                                        @QueryParam(\"saveAs\") String saveAs,\n                                        Map<String, Object> req) {\n\n        ProcessKey processKey = assertProcessKey(instanceId);\n\n        processManager.assertResumeEvents(processKey, Set.of(eventName));\n\n        if (saveAs != null && !saveAs.isEmpty() && req != null) {\n            req = ConfigurationUtils.toNested(saveAs, req);\n        }\n\n        req = ExpressionUtils.escapeMap(req);\n\n        Payload payload;\n        try {\n            payload = payloadManager.createResumePayload(processKey, eventName, req);\n        } catch (IOException e) {\n            log.error(\"resume ['{}', '{}'] -> error creating a payload: {}\", instanceId, eventName, e.getMessage());\n            throw new ConcordApplicationException(\"Error creating a payload\", e);\n        }\n\n        processManager.resume(payload);\n        return new ResumeProcessResponse();\n    }\n\n    /**\n     * Starts a new child process by forking the start of the specified parent process.\n     */\n    @POST\n    @javax.ws.rs.Path(\"/{id}/fork\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"Starts a new child process by forking the start of the specified parent process.\")\n    public StartProcessResponse fork(@PathParam(\"id\") UUID parentInstanceId,\n                                     Map<String, Object> req,\n                                     @Deprecated @DefaultValue(\"false\") @QueryParam(\"sync\") boolean sync,\n                                     @QueryParam(\"out\") String[] out) {\n\n        if (sync) {\n            throw syncIsForbidden();\n        }\n\n        ProcessEntry parent = processQueueManager.get(PartialProcessKey.from(parentInstanceId));\n        if (parent == null) {\n            throw new ValidationErrorsException(\"Unknown parent instance ID: \" + parentInstanceId);\n        }\n\n        PartialProcessKey processKey = PartialProcessKey.from(uuidGenerator.generate());\n        ProcessKey parentProcessKey = new ProcessKey(parent.instanceId(), parent.createdAt());\n\n        UUID projectId = parent.projectId();\n        UserPrincipal userPrincipal = UserPrincipal.assertCurrent();\n        Set<String> handlers = parent.handlers();\n        Imports imports = queueDao.getImports(parentProcessKey);\n\n        Payload payload;\n        try {\n            payload = payloadManager.createFork(processKey, parentProcessKey, ProcessKind.DEFAULT,\n                    userPrincipal.getId(), userPrincipal.getUsername(), projectId, req, out, handlers, imports);\n        } catch (IOException e) {\n            log.error(\"fork ['{}', '{}'] -> error creating a payload: {}\", processKey, parentProcessKey, e.getMessage());\n            throw new ConcordApplicationException(\"Error creating a payload\", e);\n        }\n\n        return toResponse(processManager.startFork(payload));\n    }\n\n    /**\n     * Waits for completion of a process.\n     */\n    @GET\n    @Produces(MediaType.APPLICATION_JSON)\n    @javax.ws.rs.Path(\"/{id}/waitForCompletion\")\n    @Operation\n    public ProcessEntry waitForCompletion(@PathParam(\"id\") UUID instanceId,\n                                          @QueryParam(\"timeout\") @DefaultValue(\"-1\") long timeout) {\n\n        log.info(\"waitForCompletion ['{}', {}] -> waiting...\", instanceId, timeout);\n\n        long t1 = System.currentTimeMillis();\n\n        ProcessEntry r;\n        while (true) {\n            r = get(instanceId);\n\n            ProcessStatus s = r.status();\n            if (s == ProcessStatus.FINISHED ||\n                s == ProcessStatus.FAILED ||\n                s == ProcessStatus.CANCELLED ||\n                s == ProcessStatus.TIMED_OUT) {\n                return r;\n            }\n\n            if (timeout > 0) {\n                long t2 = System.currentTimeMillis();\n                if (t2 - t1 >= timeout) {\n                    log.warn(\"waitForCompletion ['{}', {}] -> timeout, last status: {}\", instanceId, timeout, s);\n                    throw new ConcordApplicationException(Response.status(Status.REQUEST_TIMEOUT).entity(r).build());\n                }\n            }\n\n            try {\n                Thread.sleep(1000);\n            } catch (InterruptedException e) { // NOSONAR\n                throw new ConcordApplicationException(Response.status(Status.INTERNAL_SERVER_ERROR)\n                        .entity(\"Request was interrputed\")\n                        .build());\n            }\n        }\n    }\n\n    /**\n     * Disable a process.\n     */\n    @POST\n    @javax.ws.rs.Path(\"/{id}/disable/{disabled}\")\n    @WithTimer\n    @Operation\n    public void disable(@PathParam(\"id\") UUID instanceId,\n                        @PathParam(\"disabled\") boolean disabled) {\n        ProcessKey processKey = assertProcessKey(instanceId);\n        processManager.disable(processKey, disabled);\n    }\n\n    /**\n     * Forcefully stops a process.\n     */\n    @DELETE\n    @javax.ws.rs.Path(\"/{id}\")\n    @WithTimer\n    @Operation\n    public void kill(@PathParam(\"id\") UUID instanceId) {\n        ProcessKey processKey = assertProcessKey(instanceId);\n        processManager.kill(processKey);\n    }\n\n    /**\n     * Forcefully stops list of processes.\n     */\n    @DELETE\n    @javax.ws.rs.Path(\"/bulk\")\n    @WithTimer\n    @Operation(description = \"Forcefully stop processes\")\n    public void batchKill(List<UUID> instanceIdList) {\n        instanceIdList.forEach(this::kill);\n    }\n\n    /**\n     * Forcefully stops a process and all its children.\n     */\n    @DELETE\n    @javax.ws.rs.Path(\"/{id}/cascade\")\n    @WithTimer\n    @Operation(description = \"Forcefully stops a process and its all children\")\n    public void killCascade(@PathParam(\"id\") UUID instanceId) {\n        PartialProcessKey processKey = PartialProcessKey.from(instanceId);\n        processManager.killCascade(processKey);\n    }\n\n    /**\n     * Returns a process instance details.\n     *\n     * @deprecated use {@link ProcessResourceV2#get(UUID, Set)}\n     */\n    @GET\n    @javax.ws.rs.Path(\"/{id}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Deprecated\n    public ProcessEntry get(@PathParam(\"id\") UUID instanceId) {\n        return v2.get(instanceId, Set.of());\n    }\n\n    /**\n     * Returns a process status history.\n     */\n    @GET\n    @javax.ws.rs.Path(\"/{instanceId}/history\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"Get process status history\")\n    public List<ProcessStatusHistoryEntry> getStatusHistory(@PathParam(\"instanceId\") UUID instanceId) throws IOException {\n        ProcessKey pk = processKeyCache.assertKey(instanceId);\n        return queueDao.getStatusHistory(pk);\n    }\n\n    /**\n     * Returns current process' wait conditions.\n     */\n    @GET\n    @javax.ws.rs.Path(\"/{instanceId}/waits\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"Get process' wait conditions\")\n    public ProcessWaitEntry getWait(@PathParam(\"instanceId\") UUID instanceId) {\n        ProcessKey pk = processKeyCache.assertKey(instanceId);\n        return processWaitManager.getWait(pk);\n    }\n\n    /**\n     * Returns a process' attachment file.\n     */\n    @GET\n    @javax.ws.rs.Path(\"/{id}/attachment/{name:.*}\")\n    @Produces(MediaType.APPLICATION_OCTET_STREAM)\n    @Operation(description = \"Download a process' attachment\")\n    @ApiResponse(responseCode = \"200\", description = \"File content\",\n            content = @Content(mediaType = \"application/octet-stream\",\n                    schema = @Schema(type = \"string\", format = \"binary\"))\n    )\n    public Response downloadAttachment(@PathParam(\"id\") UUID instanceId,\n                                       @PathParam(\"name\") @NotNull @Size(min = 1) String attachmentName) {\n\n        ProcessEntry processEntry = processManager.assertProcess(instanceId);\n        assertProcessAccess(processEntry, \"attachment\");\n        PartialProcessKey processKey = new ProcessKey(processEntry.instanceId(), processEntry.createdAt());\n\n        // TODO replace with javax.validation\n        if (attachmentName.endsWith(\"/\")) {\n            throw new ConcordApplicationException(\"Invalid attachment name: \" + attachmentName, Status.BAD_REQUEST);\n        }\n\n        String resource = path(Constants.Files.JOB_ATTACHMENTS_DIR_NAME, attachmentName);\n        assertResourceAccess(processEntry, resource);\n\n        Optional<Path> o = stateManager.get(processKey, resource, src -> {\n            try {\n                Path tmp = PathUtils.createTempFile(\"attachment\", \".bin\");\n                try (OutputStream dst = Files.newOutputStream(tmp)) {\n                    src.transferTo(dst);\n                }\n                return Optional.of(tmp);\n            } catch (IOException e) {\n                throw new ConcordApplicationException(\"Error while exporting an attachment: \" + attachmentName, e);\n            }\n        });\n\n        if (!o.isPresent()) {\n            return Response.status(Status.NOT_FOUND).build();\n        }\n\n        Path tmp = o.get();\n\n        return Response.ok((StreamingOutput) out -> {\n            try (InputStream in = Files.newInputStream(tmp)) {\n                in.transferTo(out);\n            } finally {\n                Files.delete(tmp);\n            }\n        }).build();\n    }\n\n    /**\n     * Lists process attachments.\n     */\n    @GET\n    @javax.ws.rs.Path(\"/{id}/attachment\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"List attachments\")\n    public List<String> listAttachments(@PathParam(\"id\") UUID instanceId) {\n\n        ProcessEntry processEntry = processManager.assertProcess(instanceId);\n        assertProcessAccess(processEntry, \"attachments\");\n\n        PartialProcessKey processKey = new ProcessKey(processEntry.instanceId(), processEntry.createdAt());\n\n        String resource = Constants.Files.JOB_ATTACHMENTS_DIR_NAME + \"/\";\n        List<String> l = stateManager.list(processKey, resource);\n        return l.stream()\n                .map(s -> s.substring(resource.length()))\n                .collect(Collectors.toList());\n    }\n\n    /**\n     * List processes for all user's organizations\n     *\n     * @deprecated use {@link ProcessResourceV2#list(UUID, String, UUID, String, UUID, String, OffsetDateTimeParam, OffsetDateTimeParam, Set, ProcessStatus, String, UUID, Set, int, int, UriInfo)}\n     */\n    @GET\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Deprecated\n    public List<ProcessEntry> list(@QueryParam(\"org\") String orgName,\n                                   @QueryParam(\"project\") String projectName,\n                                   @QueryParam(\"projectId\") UUID projectId,\n                                   @QueryParam(\"afterCreatedAt\") OffsetDateTimeParam afterCreatedAt,\n                                   @QueryParam(\"beforeCreatedAt\") OffsetDateTimeParam beforeCreatedAt,\n                                   @QueryParam(\"tags\") Set<String> tags,\n                                   @QueryParam(\"status\") ProcessStatus processStatus,\n                                   @QueryParam(\"initiator\") String initiator,\n                                   @QueryParam(\"parentInstanceId\") UUID parentId,\n                                   @QueryParam(\"limit\") @DefaultValue(\"30\") int limit,\n                                   @QueryParam(\"offset\") @DefaultValue(\"0\") int offset,\n                                   @Context UriInfo uriInfo) {\n\n        return v2.list(null, orgName, projectId, projectName, null, null, afterCreatedAt, beforeCreatedAt, tags,\n                processStatus, initiator, parentId, Collections.singleton(ProcessDataInclude.CHILDREN_IDS),\n                limit, offset, uriInfo);\n    }\n\n    /**\n     * Returns a list of subprocesses for a given parent process.\n     */\n    @GET\n    @javax.ws.rs.Path(\"/{id}/subprocess\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"List subprocesses of a parent process\")\n    public List<ProcessEntry> listSubprocesses(@PathParam(\"id\") UUID parentInstanceId,\n                                               @QueryParam(\"tags\") Set<String> tags) {\n\n        assertPartialKey(parentInstanceId);\n        return queueDao.list(ProcessFilter.builder()\n                .parentId(parentInstanceId)\n                .tags(tags)\n                .build());\n    }\n\n    @GET\n    @javax.ws.rs.Path(\"/{id}/root\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"Get super parent for a process\")\n    public ProcessEntry getRoot(@PathParam(\"id\") UUID instanceId) {\n        PartialProcessKey processKey = assertPartialKey(instanceId);\n\n        ProcessKey rootId = queueDao.getRootId(processKey);\n        if (rootId == null) {\n            return null;\n        }\n\n        return queueDao.get(rootId, Set.of());\n    }\n\n    /**\n     * Updates a process' status\n     */\n    @POST\n    @javax.ws.rs.Path(\"{id}/status\")\n    @Consumes(MediaType.TEXT_PLAIN)\n    @WithTimer\n    @Operation(description = \"Update process status\")\n    public void updateStatus(@PathParam(\"id\") UUID instanceId,\n                             @Parameter(required = true) @QueryParam(\"agentId\") String agentId,\n                             @Parameter(required = true) ProcessStatus status) {\n\n        ProcessKey processKey = assertProcessKey(instanceId);\n        processManager.updateStatus(processKey, agentId, status);\n    }\n\n    /**\n     * Retrieves a process' log.\n     */\n    @GET\n    @javax.ws.rs.Path(\"/{id}/log\")\n    @Produces(MediaType.APPLICATION_OCTET_STREAM)\n    @WithTimer\n    @Operation(description = \"Retrieves a process' log\", operationId = \"getProcessLog\")\n    @ApiResponse(description = \"Process log content\",\n            content = @Content(mediaType = MediaType.APPLICATION_OCTET_STREAM,\n                    schema = @Schema(type = \"string\", format = \"binary\"))\n    )\n    public Response getLog(@PathParam(\"id\") UUID instanceId,\n                           @HeaderParam(\"range\") String rangeHeader) {\n\n        // check the permissions, logs can contain sensitive data\n        ProcessKey processKey = logAccessManager.assertLogAccess(instanceId);\n\n        HttpUtils.Range range = HttpUtils.parseRangeHeaderValue(rangeHeader);\n\n        ProcessLog l = logManager.get(processKey, range.start(), range.end());\n        return ProcessLogResourceV2.toResponse(instanceId, 0, l, range);\n    }\n\n    /**\n     * Appends a process' log.\n     *\n     * @deprecated in favor of the /api/v2/process/{id}/log* endpoints. The endpoint is still used for the runtime-v1.\n     */\n    @POST\n    @javax.ws.rs.Path(\"{id}/log\")\n    @Consumes(MediaType.APPLICATION_OCTET_STREAM)\n    @WithTimer\n    @Deprecated()\n    @Operation(description = \"Appends a process' log\", operationId = \"appendProcessLog\")\n    @RequestBody(description = \"Log content\", required = true,\n            content = @Content(mediaType = MediaType.APPLICATION_OCTET_STREAM,\n                    schema = @Schema(type = \"string\", format = \"binary\")\n            )\n    )\n    public void appendLog(@PathParam(\"id\") UUID instanceId, InputStream data) {\n        ProcessKey processKey = assertProcessKey(instanceId);\n\n        try {\n            byte[] ab = data.readAllBytes();\n            int upper = logManager.log(processKey, ab);\n\n            // whenever we accept logs from an external source (e.g. from an Agent) we need to check\n            // the log size limits\n            int logSizeLimit = processCfg.getLogSizeLimit();\n            if (upper >= logSizeLimit) {\n                logManager.error(processKey, \"Maximum log size reached: {}. Process cancelled.\", logSizeLimit);\n                processManager.kill(processKey);\n            }\n        } catch (IOException e) {\n            throw new ConcordApplicationException(\"Error while appending a log: \" + e.getMessage());\n        }\n    }\n\n    /**\n     * Downloads the current state snapshot of a process.\n     */\n    @GET\n    @javax.ws.rs.Path(\"/{id}/state/snapshot\")\n    @Produces(\"application/zip\")\n    @Operation(description = \"Download a process state snapshot\")\n    @ApiResponse(responseCode = \"200\", description = \"File content\",\n            content = @Content(mediaType = \"application/zip\",\n                    schema = @Schema(type = \"string\", format = \"binary\"))\n    )\n    public Response downloadState(@PathParam(\"id\") UUID instanceId) {\n        ProcessEntry entry = assertProcess(PartialProcessKey.from(instanceId));\n        ProcessKey processKey = new ProcessKey(entry.instanceId(), entry.createdAt());\n\n        assertProcessAccess(entry, \"state\");\n\n        StreamingOutput out = output -> {\n            try (ZipArchiveOutputStream dst = new ZipArchiveOutputStream(output)) {\n                stateManager.export(processKey, new ProcessStateManager.FilteringConsumer(zipTo(dst), s -> {\n                    if (!isSessionResource(s)) {\n                        return true;\n                    }\n                    return isSessionKeyAccess(instanceId);\n                }));\n            }\n        };\n\n        return Response.ok(out, \"application/zip\")\n                .header(HttpHeaders.CONTENT_DISPOSITION, \"attachment; filename=\\\"\" + instanceId + \".zip\\\"\")\n                .build();\n    }\n\n    /**\n     * Downloads a single file from the current state snapshot of a process.\n     */\n    @GET\n    @javax.ws.rs.Path(\"/{id}/state/snapshot/{name:.*}\")\n    @Produces(MediaType.APPLICATION_OCTET_STREAM)\n    @Validate\n    @Operation(description = \"Download a single file from a process state snapshot\")\n    @ApiResponse(responseCode = \"200\", description = \"File content\",\n            content = @Content(mediaType = \"application/octet-stream\",\n                    schema = @Schema(type = \"string\", format = \"binary\"))\n    )\n    public Response downloadStateFile(@PathParam(\"id\") UUID instanceId,\n                                      @PathParam(\"name\") @NotNull @Size(min = 1) String fileName) {\n\n        ProcessEntry p = assertProcess(PartialProcessKey.from(instanceId));\n        ProcessKey processKey = new ProcessKey(p.instanceId(), p.createdAt());\n\n        assertProcessAccess(p, \"state\");\n        assertResourceAccess(p, fileName);\n\n        StreamingOutput out = output -> {\n            Path tmp = stateManager.get(processKey, fileName, ProcessResource::copyToTmp)\n                    .orElseThrow(() -> new ConcordApplicationException(\"State file not found: \" + fileName, Status.NOT_FOUND));\n\n            try (InputStream in = Files.newInputStream(tmp)) {\n                in.transferTo(output);\n            } finally {\n                Files.delete(tmp);\n            }\n        };\n\n        return Response.ok(out)\n                .build();\n    }\n\n    /**\n     * Upload process attachments.\n     */\n    @POST\n    @javax.ws.rs.Path(\"{id}/attachment\")\n    @Consumes(MediaType.APPLICATION_OCTET_STREAM)\n    @Operation(description = \"Upload process attachments\", operationId = \"uploadProcessAttachments\")\n    @RequestBody(description = \"Attachment content\", required = true,\n            content = @Content(mediaType = MediaType.APPLICATION_OCTET_STREAM,\n                    schema = @Schema(type = \"string\", format = \"binary\")\n            )\n    )\n    public void uploadAttachments(@PathParam(\"id\") UUID instanceId, InputStream data) {\n        ProcessEntry entry = assertProcess(PartialProcessKey.from(instanceId));\n        ProcessKey processKey = new ProcessKey(entry.instanceId(), entry.createdAt());\n\n        Path tmpIn = null;\n        Path tmpDir = null;\n        try {\n            tmpIn = PathUtils.createTempFile(\"attachments\", \".zip\");\n            Files.copy(data, tmpIn, StandardCopyOption.REPLACE_EXISTING);\n\n            tmpDir = PathUtils.createTempDir(\"attachments\");\n            ZipUtils.unzip(tmpIn, tmpDir);\n\n            assertAttachmentsPolicy(tmpDir, entry);\n\n            Path finalTmpDir = tmpDir;\n            stateManager.tx(tx -> {\n                stateManager.deleteDirectory(tx, processKey, path(Constants.Files.JOB_ATTACHMENTS_DIR_NAME, Constants.Files.JOB_STATE_DIR_NAME));\n                stateManager.importPath(tx, processKey, Constants.Files.JOB_ATTACHMENTS_DIR_NAME, finalTmpDir, (p, attrs) -> true);\n            });\n\n            Map<String, Object> out = OutVariablesUtils.read(tmpDir);\n            if (out.isEmpty()) {\n                queueDao.removeMeta(processKey, \"out\");\n            } else {\n                queueDao.updateMeta(processKey, Collections.singletonMap(\"out\", out));\n            }\n        } catch (PolicyException e) {\n            throw new ConcordApplicationException(e.getMessage(), Status.FORBIDDEN);\n        } catch (IOException e) {\n            log.error(\"uploadAttachments ['{}'] -> error\", processKey, e);\n            throw new ConcordApplicationException(\"upload error: \" + e.getMessage());\n        } finally {\n            if (tmpDir != null) {\n                try {\n                    PathUtils.deleteRecursively(tmpDir);\n                } catch (IOException e) {\n                    log.warn(\"uploadAttachments -> cleanup error: {}\", e.getMessage());\n                }\n            }\n            if (tmpIn != null) {\n                try {\n                    Files.delete(tmpIn);\n                } catch (IOException e) {\n                    log.warn(\"uploadAttachments -> cleanup error: {}\", e.getMessage());\n                }\n            }\n        }\n    }\n\n    /**\n     * Decrypt a base64 string previosly encrypted with the process' project key.\n     */\n    @POST\n    @javax.ws.rs.Path(\"{id}/decrypt\")\n    @Consumes(MediaType.APPLICATION_OCTET_STREAM)\n    @Produces(MediaType.APPLICATION_OCTET_STREAM)\n    @WithTimer\n    @Operation(description = \"Decrypt a base64 string previously encrypted with the process' project key\", operationId = \"decryptString\")\n    @ApiResponse(responseCode = \"200\", description = \"Decrypted value\",\n            content = @Content(mediaType = MediaType.APPLICATION_OCTET_STREAM,\n                    schema = @Schema(type = \"string\", format = \"byte\")\n            )\n    )\n    public Response decrypt(@PathParam(\"id\") UUID instanceId,\n                            @Parameter(schema = @Schema(type = \"string\", format = \"byte\"))\n                            InputStream data) {\n        ProcessEntry entry = assertProcess(PartialProcessKey.from(instanceId));\n        if (entry.projectId() == null) {\n            throw new ConcordApplicationException(\"Project is required\", Status.BAD_REQUEST);\n        }\n\n        ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);\n        try {\n            int read;\n            byte[] buf = new byte[1024];\n            while ((read = data.read(buf)) > 0) {\n                baos.write(buf, 0, read);\n\n                if (baos.size() > secretStoreCfg.getMaxEncryptedStringLength()) {\n                    throw new ConcordApplicationException(\"Value too big, limit: \" + secretStoreCfg.getMaxEncryptedStringLength(),\n                            Status.BAD_REQUEST);\n                }\n            }\n        } catch (IOException e) {\n            throw new ConcordApplicationException(\"Error while reading encrypted data: \" + e.getMessage(), e);\n        }\n\n        try {\n            UUID projectId = entry.projectId();\n            byte[] result = encryptedValueManager.decrypt(projectId, baos.toByteArray());\n            return Response.ok((StreamingOutput) output -> output.write(result))\n                    .build();\n        } catch (SecurityException e) {\n            throw new ConcordApplicationException(e.getMessage(), Status.BAD_REQUEST);\n        } catch (Exception e) {\n            throw new ConcordApplicationException(\"Decrypt error: \" + e.getMessage());\n        }\n    }\n\n    /**\n     * Update process metadata.\n     */\n    @POST\n    @javax.ws.rs.Path(\"{id}/meta\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"Update process metadata\")\n    public Response updateMetadata(@PathParam(\"id\") UUID instanceId, Map<String, Object> meta) {\n        ProcessKey processKey = assertProcessKey(instanceId);\n\n        if (!queueDao.updateMeta(processKey, meta)) {\n            throw new ConcordApplicationException(\"Process instance not found\", Status.NOT_FOUND);\n        }\n\n        return Response.ok().build();\n    }\n\n    /**\n     * Set the process' wait condition.\n     */\n    @POST\n    @javax.ws.rs.Path(\"{id}/wait\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"Set the process' wait condition\")\n    public Response setWaitCondition(@PathParam(\"id\") UUID instanceId, Map<String, Object> waitCondition) {\n        ProcessKey processKey = assertProcessKey(instanceId);\n        AbstractWaitCondition condition = objectMapper.convertValue(waitCondition, AbstractWaitCondition.class);\n        if (condition == null) {\n            throw new ConcordApplicationException(\"Wait condition is required\", Status.BAD_REQUEST);\n        }\n        processWaitManager.addWait(processKey, condition);\n        return Response.ok().build();\n    }\n\n    private ProcessKey assertProcessKey(UUID instanceId) {\n        ProcessKey processKey = processKeyCache.get(instanceId);\n        if (processKey == null) {\n            throw new ConcordApplicationException(\"Process instance not found: \" + instanceId, Status.NOT_FOUND);\n        }\n        return processKey;\n    }\n\n    private void assertProcessAccess(ProcessEntry pe, String downloadEntity) {\n        UserPrincipal principal = UserPrincipal.assertCurrent();\n\n        UUID initiatorId = pe.initiatorId();\n        if (principal.getId().equals(initiatorId)) {\n            // process owners should be able to download the process' state\n            return;\n        }\n\n        if (Roles.isAdmin() || Roles.isGlobalReader()) {\n            return;\n        }\n\n        if (pe.projectId() != null) {\n            projectAccessManager.assertAccess(pe.projectId(), ResourceAccessLevel.OWNER, true);\n            return;\n        }\n\n        throw new UnauthorizedException(\"The current user (\" + principal.getUsername() + \") doesn't have \" +\n                                        \"the necessary permissions to the download \" + downloadEntity + \" : \" + pe.instanceId());\n    }\n\n    private void assertResourceAccess(ProcessEntry pe, String resource) {\n        if (!isSessionResource(resource)) {\n            return;\n        }\n\n        if (isSessionKeyAccess(pe.instanceId())) {\n            return;\n        }\n\n        throw new UnauthorizedException(\"Resource accessible with session process key only\");\n    }\n\n    private boolean isSessionResource(String resource) {\n        return resource.startsWith(path(Constants.Files.JOB_ATTACHMENTS_DIR_NAME, Constants.Files.JOB_SESSION_FILES_DIR_NAME));\n    }\n\n    private boolean isSessionKeyAccess(UUID instanceId) {\n        SessionKeyPrincipal principal = SessionKeyPrincipal.getCurrent();\n        return principal != null && principal.getProcessKey().getInstanceId().equals(instanceId);\n    }\n\n    private ProcessEntry assertProcess(PartialProcessKey processKey) {\n        ProcessEntry p = processQueueManager.get(processKey);\n        if (p == null) {\n            throw new ConcordApplicationException(\"Process instance not found\", Status.NOT_FOUND);\n        }\n        return p;\n    }\n\n    private StartProcessResponse toResponse(ProcessResult r) {\n        return new StartProcessResponse(r.getInstanceId());\n    }\n\n    private PartialProcessKey assertPartialKey(UUID id) {\n        if (id == null) {\n            return null;\n        }\n\n        PartialProcessKey k = PartialProcessKey.from(id);\n        if (!queueDao.exists(k)) {\n            throw new ValidationErrorsException(\"Unknown instance ID: \" + id);\n        }\n\n        return k;\n    }\n\n    private static Optional<Path> copyToTmp(InputStream in) {\n        try {\n            Path p = PathUtils.createTempFile(\"state\", \".bin\");\n            try (OutputStream out = Files.newOutputStream(p)) {\n                in.transferTo(out);\n            }\n            return Optional.of(p);\n        } catch (IOException e) {\n            throw new ConcordApplicationException(\"Error while copying a state file: \" + e.getMessage(), e);\n        }\n    }\n\n    private static RuntimeException syncIsForbidden() {\n        return new ConcordApplicationException(\"The 'sync' mode is no longer available. \" +\n                                               \"Please use sync=false and poll for the status updates.\", Status.BAD_REQUEST);\n    }\n\n    private void assertAttachmentsPolicy(Path tmpDir, ProcessEntry entry) throws IOException {\n        PolicyEngine policy = policyManager.get(entry.orgId(), entry.projectId(), UserPrincipal.assertCurrent().getUser().getId());\n        if (policy == null) {\n            return;\n        }\n\n        CheckResult<AttachmentsRule, Long> checkResult = policy.getAttachmentsPolicy().check(tmpDir);\n        if (!checkResult.getDeny().isEmpty()) {\n            String errorMessage = buildErrorMessage(checkResult.getDeny());\n            processLogManager.error(new ProcessKey(entry.instanceId(), entry.createdAt()), errorMessage);\n            throw new PolicyException(\"Found forbidden policy: \" + errorMessage);\n        }\n    }\n\n    private String buildErrorMessage(List<CheckResult.Item<AttachmentsRule, Long>> errors) {\n        String defaultMessage = \"Attachments too big: current {0} bytes, limit {1} bytes\";\n\n        StringBuilder sb = new StringBuilder();\n        for (CheckResult.Item<AttachmentsRule, Long> e : errors) {\n            AttachmentsRule r = e.getRule();\n\n            String msg = r.msg() != null ? r.msg() : defaultMessage;\n            long actualSize = e.getEntity();\n            long limit = r.maxSizeInBytes();\n\n            sb.append(MessageFormat.format(requireNonNull(msg), actualSize, limit)).append(';');\n        }\n        return sb.toString();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/ProcessResourceV2.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.OffsetDateTimeParam;\nimport com.walmartlabs.concord.server.org.OrganizationEntry;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.org.project.ProjectAccessManager;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.org.project.RepositoryDao;\nimport com.walmartlabs.concord.server.process.queue.*;\nimport com.walmartlabs.concord.server.process.queue.ProcessFilter.MetadataFilter;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.Permission;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.user.UserDao;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.Parameter;\nimport io.swagger.v3.oas.annotations.Parameters;\nimport io.swagger.v3.oas.annotations.enums.ParameterIn;\nimport io.swagger.v3.oas.annotations.extensions.Extension;\nimport io.swagger.v3.oas.annotations.extensions.ExtensionProperty;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.Context;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response.Status;\nimport javax.ws.rs.core.UriInfo;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.server.Utils.unwrap;\n\n@Path(\"/api/v2/process\")\n@Tag(name = \"ProcessV2\")\npublic class ProcessResourceV2 implements Resource {\n\n    private final ProcessQueueDao queueDao;\n    private final ProjectDao projectDao;\n    private final RepositoryDao repositoryDao;\n    private final UserDao userDao;\n    private final OrganizationManager orgManager;\n    private final ProjectAccessManager projectAccessManager;\n    private final ProcessAccessManager processAccessManager;\n\n    @Inject\n    public ProcessResourceV2(ProcessQueueDao queueDao,\n                             ProjectDao projectDao,\n                             RepositoryDao repositoryDao,\n                             UserDao userDao,\n                             OrganizationManager orgManager,\n                             ProjectAccessManager projectAccessManager,\n                             ProcessAccessManager processAccessManager) {\n\n        this.queueDao = queueDao;\n        this.projectDao = projectDao;\n        this.repositoryDao = repositoryDao;\n        this.userDao = userDao;\n        this.orgManager = orgManager;\n        this.projectAccessManager = projectAccessManager;\n        this.processAccessManager = processAccessManager;\n    }\n\n    /**\n     * Returns a process instance's details.\n     */\n    @GET\n    @Path(\"/{id}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"Get a process' details\", operationId = \"getProcess\")\n    public ProcessEntry get(@PathParam(\"id\") UUID instanceId,\n                            @QueryParam(\"include\") Set<ProcessDataInclude> includes) {\n\n        return processAccessManager.assertAccess(instanceId, includes);\n    }\n\n    /**\n     * Returns a list of processes applying the specified filters.\n     */\n    @GET\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"List processes\", operationId = \"listProcesses\",\n            extensions = @Extension(name = \"concord\",\n                    properties = {\n                            @ExtensionProperty(name = \"groupParams\", value = \"true\"),\n                            @ExtensionProperty(name = \"groupName\", value = \"ProcessListFilter\")\n                    }))\n    @Parameters()\n    @Parameter(name = \"meta\", in = ParameterIn.QUERY, schema = @Schema(implementation = Map.class), extensions = @Extension(name = \"concord\", properties = @ExtensionProperty(name = \"customQueryParams\", value = \"true\")))\n    public List<ProcessEntry> list(@QueryParam(\"orgId\") UUID orgId,\n                                   @QueryParam(\"orgName\") String orgName,\n                                   @QueryParam(\"projectId\") UUID projectId,\n                                   @QueryParam(\"projectName\") String projectName,\n                                   @QueryParam(\"repoId\") UUID repoId,\n                                   @QueryParam(\"repoName\") String repoName,\n                                   @QueryParam(\"afterCreatedAt\") OffsetDateTimeParam afterCreatedAt,\n                                   @QueryParam(\"beforeCreatedAt\") OffsetDateTimeParam beforeCreatedAt,\n                                   @QueryParam(\"tags\") Set<String> tags,\n                                   @QueryParam(\"status\") ProcessStatus processStatus,\n                                   @QueryParam(\"initiator\") String initiator,\n                                   @QueryParam(\"parentInstanceId\") UUID parentId,\n                                   @QueryParam(\"include\") Set<ProcessDataInclude> processData,\n                                   @QueryParam(\"limit\") @DefaultValue(\"30\") int limit,\n                                   @QueryParam(\"offset\") @DefaultValue(\"0\") int offset,\n                                   @Context UriInfo uriInfo) {\n\n        if (limit <= 0) {\n            throw new ValidationErrorsException(\"'limit' must be a positive number\");\n        }\n\n        if (offset < 0) {\n            throw new ValidationErrorsException(\"'offset' must be a positive number or zero\");\n        }\n\n        ProcessFilter filter = createProcessFilter(orgId, orgName, projectId, projectName, repoId, repoName,\n                afterCreatedAt, beforeCreatedAt, tags, processStatus, initiator, parentId, processData, limit, offset, uriInfo);\n\n        return queueDao.list(filter);\n    }\n\n    @GET\n    @Path(\"/requirements\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"List process requirements\")\n    public List<ProcessRequirementsEntry> listRequirements(@QueryParam(\"status\") ProcessStatus processStatus,\n                                                           @QueryParam(\"limit\") @DefaultValue(\"30\") int limit,\n                                                           @QueryParam(\"offset\") @DefaultValue(\"0\") int offset,\n                                                           @Context UriInfo uriInfo) {\n\n        if (limit <= 0) {\n            throw new ValidationErrorsException(\"'limit' must be a positive number\");\n        }\n\n        if (offset < 0) {\n            throw new ValidationErrorsException(\"'offset' must be a positive number or zero\");\n        }\n\n        if (processStatus == null) {\n            throw new ValidationErrorsException(\"'status' is required\");\n        }\n\n        return queueDao.listRequirements(processStatus, FilterUtils.parseDate(\"startAt\", uriInfo), limit, offset,\n                FilterUtils.parseJson(\"requirements\", uriInfo));\n    }\n\n    /**\n     * Counts processes applying the specified filters.\n     */\n    @GET\n    @Path(\"/count\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"Count processes\", operationId = \"countProcesses\")\n    public int count(@QueryParam(\"orgId\") UUID orgId,\n                     @QueryParam(\"orgName\") String orgName,\n                     @QueryParam(\"projectId\") UUID projectId,\n                     @QueryParam(\"projectName\") String projectName,\n                     @QueryParam(\"repoId\") UUID repoId,\n                     @QueryParam(\"repoName\") String repoName,\n                     @QueryParam(\"afterCreatedAt\") OffsetDateTimeParam afterCreatedAt,\n                     @QueryParam(\"beforeCreatedAt\") OffsetDateTimeParam beforeCreatedAt,\n                     @QueryParam(\"tags\") Set<String> tags,\n                     @QueryParam(\"status\") ProcessStatus processStatus,\n                     @QueryParam(\"initiator\") String initiator,\n                     @QueryParam(\"parentInstanceId\") UUID parentId,\n                     @Context UriInfo uriInfo) {\n\n        ProcessFilter filter = createProcessFilter(orgId, orgName, projectId, projectName, repoId, repoName,\n                afterCreatedAt, beforeCreatedAt, tags, processStatus, initiator, parentId, Collections.emptySet(),\n                null, null, uriInfo);\n\n        if (filter.projectId() == null) {\n            throw new ValidationErrorsException(\"A project ID or name is required\");\n        }\n\n        return queueDao.count(filter);\n    }\n\n    private ProcessFilter createProcessFilter(UUID orgId,\n                                              String orgName,\n                                              UUID projectId,\n                                              String projectName,\n                                              UUID repoId,\n                                              String repoName,\n                                              OffsetDateTimeParam afterCreatedAt,\n                                              OffsetDateTimeParam beforeCreatedAt,\n                                              Set<String> tags,\n                                              ProcessStatus processStatus,\n                                              String initiator,\n                                              UUID parentId,\n                                              Set<ProcessDataInclude> processData,\n                                              Integer limit,\n                                              Integer offset,\n                                              UriInfo uriInfo) {\n\n        UUID effectiveOrgId = orgId;\n\n        Set<UUID> orgIds = null;\n        if (orgId != null) {\n            // we got an org ID, use it as it is\n            orgIds = Collections.singleton(effectiveOrgId);\n        } else if (orgName != null) {\n            // we got an org name, validate it first by resolving its ID\n            OrganizationEntry org = orgManager.assertExisting(null, orgName);\n            effectiveOrgId = org.getId();\n            orgIds = Collections.singleton(effectiveOrgId);\n        } else {\n            // we got a query that is not limited to any specific org\n            // let's check if we can return all processes from all orgs or if we should limit it to the user's orgs\n            boolean canSeeAllOrgs = Roles.isAdmin() || Permission.GET_PROCESS_QUEUE_ALL_ORGS.isPermitted();\n            if (!canSeeAllOrgs) {\n                // non-admin users can only see their org's processes or processes w/o projects\n                orgIds = getCurrentUserOrgIds();\n            }\n        }\n\n        UUID effectiveProjectId = projectId;\n        if (effectiveProjectId == null && projectName != null) {\n            if (effectiveOrgId == null) {\n                throw new ValidationErrorsException(\"Organization name or ID is required\");\n            }\n\n            effectiveProjectId = projectDao.getId(effectiveOrgId, projectName);\n            if (effectiveProjectId == null) {\n                throw new ConcordApplicationException(\"Project not found: \" + projectName, Status.NOT_FOUND);\n            }\n        }\n\n        if (effectiveProjectId != null) {\n            projectAccessManager.assertAccess(effectiveProjectId, effectiveProjectId, null, ResourceAccessLevel.READER, false);\n        } else if (effectiveOrgId != null) {\n            orgManager.assertAccess(effectiveOrgId, null, false);\n        } else {\n            // we don't have to do the permissions check when neither the org or the project are specified\n            // it is done implicitly by calling getCurrentUserOrgIds for all non-admin users (see above)\n        }\n\n        UUID effectiveRepoId = repoId;\n        if (effectiveRepoId == null && repoName != null && effectiveProjectId != null) {\n            effectiveRepoId = repositoryDao.getId(effectiveProjectId, repoName);\n        }\n\n        // collect all metadata filters, we assume that they have \"meta.\" prefix in their query parameter names\n        List<MetadataFilter> metaFilters = MetadataUtils.parseMetadataFilters(uriInfo);\n\n        // can't allow seq scans, we don't index PROCESS_QUEUE.META (yet?)\n        if (!metaFilters.isEmpty() && (effectiveProjectId == null && parentId == null)) {\n            throw new ValidationErrorsException(\"Process metadata filters require a project name or project ID or a parent process ID to be included in the query.\");\n        }\n\n        return ProcessFilter.builder()\n                .parentId(parentId)\n                .projectId(effectiveProjectId)\n                .orgIds(orgIds)\n                .includeWithoutProject(effectiveOrgId == null && effectiveProjectId == null)\n                .afterCreatedAt(unwrap(afterCreatedAt))\n                .beforeCreatedAt(unwrap(beforeCreatedAt))\n                .repoId(effectiveRepoId)\n                .repoName(repoName)\n                .tags(tags)\n                .status(processStatus)\n                .initiator(initiator)\n                .metaFilters(metaFilters)\n                .requirements(FilterUtils.parseJson(\"requirements\", uriInfo))\n                .startAt(FilterUtils.parseDate(\"startAt\", uriInfo))\n                .includes(processData != null ? processData : Collections.emptySet())\n                .limit(limit)\n                .offset(offset)\n                .build();\n    }\n\n    private Set<UUID> getCurrentUserOrgIds() {\n        UserPrincipal p = UserPrincipal.assertCurrent();\n        return userDao.getOrgIds(p.getId());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/ProcessSecurityContext.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.cache.Cache;\nimport com.google.common.cache.CacheBuilder;\nimport com.walmartlabs.concord.server.process.state.ProcessStateManager;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.security.SecurityUtils;\nimport com.walmartlabs.concord.server.security.sessionkey.SessionKeyPrincipal;\nimport org.apache.shiro.mgt.SecurityManager;\nimport org.apache.shiro.subject.PrincipalCollection;\nimport org.apache.shiro.subject.SimplePrincipalCollection;\nimport org.apache.shiro.subject.Subject;\nimport org.apache.shiro.util.ThreadContext;\n\nimport javax.inject.Inject;\nimport java.util.Collection;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.TimeUnit;\n\npublic class ProcessSecurityContext {\n\n    private static final String PRINCIPAL_FILE_PATH = \".concord/current_user\";\n\n    private final SecurityManager securityManager;\n    private final ProcessStateManager stateManager;\n    private final Cache<PartialProcessKey, PrincipalCollection> principalCache;\n\n    @Inject\n    public ProcessSecurityContext(SecurityManager securityManager, ProcessStateManager stateManager) {\n        this.securityManager = securityManager;\n        this.stateManager = stateManager;\n        this.principalCache = CacheBuilder.newBuilder()\n                .expireAfterAccess(1, TimeUnit.MINUTES)\n                .build();\n    }\n\n    public byte[] serializePrincipals(PrincipalCollection src) {\n        // filter out transient principals\n        SimplePrincipalCollection dst = new SimplePrincipalCollection();\n        for (String realm : src.getRealmNames()) {\n            Collection<?> ps = src.fromRealm(realm);\n            for (Object p : ps) {\n                if (p instanceof SessionKeyPrincipal) {\n                    continue;\n                }\n\n                dst.add(p, realm);\n            }\n        }\n        return SecurityUtils.serialize(dst);\n    }\n\n    // TODO: invalidate cache for processKey?\n    public void storeCurrentSubject(ProcessKey processKey) {\n        Subject s = SecurityUtils.getSubject();\n        PrincipalCollection src = s.getPrincipals();\n        storeSubject(processKey, src);\n    }\n\n    // TODO: invalidate cache for processKey?\n    public void storeSubject(ProcessKey processKey, PrincipalCollection src) {\n        stateManager.replace(processKey, PRINCIPAL_FILE_PATH, serializePrincipals(src));\n    }\n\n    public PrincipalCollection getPrincipals(PartialProcessKey processKey) {\n        try {\n            return principalCache.get(processKey, () -> doGetPrincipals(processKey));\n        } catch (ExecutionException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private PrincipalCollection doGetPrincipals(PartialProcessKey processKey) {\n        return stateManager.get(processKey, PRINCIPAL_FILE_PATH, SecurityUtils::deserialize)\n                .orElse(null);\n    }\n\n    /**\n     * Run the specified {@link Callable} as if it was started by the current user (e.g. initiator)\n     * of the specified process.\n     */\n    public <T> T runAsCurrentUser(ProcessKey processKey, Callable<T> c) throws Exception {\n        PrincipalCollection principals = getPrincipals(processKey);\n\n        ThreadContext.bind(securityManager);\n\n        Subject subject = new Subject.Builder()\n                .sessionCreationEnabled(false)\n                .authenticated(true)\n                .principals(principals)\n                .buildSubject();\n\n        try {\n            ThreadContext.bind(subject);\n\n            return c.call();\n        } finally {\n            ThreadContext.unbindSubject();\n            ThreadContext.unbindSecurityManager();\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/ResumeProcessResponse.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\n\npublic class ResumeProcessResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    @Override\n    public String toString() {\n        return \"ResumeProcessResponse{\" +\n                \"ok=\" + ok +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/SessionTokenCreator.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.secret.SecretUtils;\nimport com.walmartlabs.concord.server.cfg.SecretStoreConfiguration;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\n\nimport javax.inject.Inject;\nimport java.util.Base64;\nimport java.util.UUID;\n\npublic class SessionTokenCreator {\n\n    private final SecretStoreConfiguration secretCfg;\n\n    @Inject\n    public SessionTokenCreator(SecretStoreConfiguration secretCfg) {\n        this.secretCfg = secretCfg;\n    }\n\n    public String create(ProcessKey processKey) {\n        byte[] salt = secretCfg.getSecretStoreSalt();\n        byte[] pwd = secretCfg.getServerPwd();\n\n        UUID instanceId = processKey.getInstanceId();\n\n        byte[] ab = SecretUtils.encrypt(instanceId.toString().getBytes(), pwd, salt);\n        return Base64.getEncoder().encodeToString(ab);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/StartProcessResponse.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@JsonInclude(Include.NON_NULL)\npublic class StartProcessResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n    private final UUID instanceId;\n\n    @JsonCreator\n    public StartProcessResponse(@JsonProperty(\"instanceId\") UUID instanceId) {\n\n        this.instanceId = instanceId;\n    }\n\n    public UUID getInstanceId() {\n        return instanceId;\n    }\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    @Override\n    public String toString() {\n        return \"StartProcessResponse{\" +\n                \"ok=\" + ok +\n                \", instanceId=\" + instanceId +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/TotalRuntimeCalculator.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueDao;\nimport com.walmartlabs.concord.server.process.queue.ProcessStatusListener;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport org.jooq.DSLContext;\n\nimport javax.inject.Inject;\nimport java.time.Duration;\nimport java.time.OffsetDateTime;\n\n/**\n * Updates the total running time of a process when it transitions to a terminal (or suspended) state.\n */\npublic class TotalRuntimeCalculator implements ProcessStatusListener {\n\n    private final ProcessQueueDao dao;\n\n    @Inject\n    public TotalRuntimeCalculator(ProcessQueueDao dao) {\n        this.dao = dao;\n    }\n\n    @Override\n    public void onStatusChange(DSLContext tx, ProcessKey processKey, ProcessStatus status) {\n\n        switch (status) {\n            case FINISHED, FAILED, CANCELLED, SUSPENDED, TIMED_OUT -> dao.getLastRunAt(tx, processKey)\n                    .ifPresent(lastRunAt -> {\n                        Duration duration = Duration.between(lastRunAt, OffsetDateTime.now());\n                        dao.addToTotalRunningTime(tx, processKey, duration);\n                    });\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/TriggeredByEntry.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.server.org.triggers.TriggerEntry;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableTriggeredByEntry.class)\n@JsonDeserialize(as = ImmutableTriggeredByEntry.class)\npublic interface TriggeredByEntry extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    @Nullable\n    String externalEventId();\n\n    TriggerEntry trigger();\n\n    static ImmutableTriggeredByEntry.Builder builder() {\n        return ImmutableTriggeredByEntry.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/checkpoint/GenericCheckpointResponse.java",
    "content": "package com.walmartlabs.concord.server.process.checkpoint;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\n\npublic class GenericCheckpointResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    @Override\n    public String toString() {\n        return \"GenericCheckpointResponse{\" +\n                \"ok=\" + ok +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/checkpoint/ProcessCheckpointResource.java",
    "content": "package com.walmartlabs.concord.server.process.checkpoint;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.TemporaryPath;\nimport com.walmartlabs.concord.server.MultipartUtils;\nimport com.walmartlabs.concord.server.process.ProcessEntry;\nimport com.walmartlabs.concord.server.process.ProcessEntry.ProcessCheckpointEntry;\nimport com.walmartlabs.concord.server.process.ProcessManager;\nimport com.walmartlabs.concord.server.process.ResumeProcessResponse;\nimport com.walmartlabs.concord.server.process.state.ProcessCheckpointManager;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.Parameter;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.jboss.resteasy.plugins.providers.multipart.MultipartInput;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.validation.Valid;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.StandardCopyOption;\nimport java.util.List;\nimport java.util.UUID;\n\n@Path(\"/api/v1/process\")\n@Tag(name = \"Checkpoint\")\npublic class ProcessCheckpointResource implements Resource {\n\n    private static final Logger log = LoggerFactory.getLogger(ProcessCheckpointResource.class);\n\n    private final ProcessManager processManager;\n    private final ProcessCheckpointManager checkpointManager;\n\n    @Inject\n    public ProcessCheckpointResource(ProcessManager processManager,\n                                     ProcessCheckpointManager checkpointManager) {\n        this.processManager = processManager;\n        this.checkpointManager = checkpointManager;\n    }\n\n    @GET\n    @Path(\"{id}/checkpoint\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"List the process checkpoints\", operationId = \"listCheckpoints\")\n    public List<ProcessCheckpointEntry> list(@PathParam(\"id\") UUID instanceId) {\n        ProcessEntry entry = processManager.assertProcess(instanceId);\n        ProcessKey processKey = new ProcessKey(entry.instanceId(), entry.createdAt());\n\n        checkpointManager.assertProcessAccess(entry);\n\n        return checkpointManager.list(processKey);\n    }\n\n    @POST\n    @Path(\"{id}/checkpoint/restore\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Validate\n    @Operation(description = \"Restore process from checkpoint\")\n    public ResumeProcessResponse restore(@PathParam(\"id\") UUID instanceId,\n                                         @Valid RestoreCheckpointRequest request) {\n\n        UUID checkpointId = request.getId();\n\n        // TODO replace with ProcessKeyCache\n        ProcessEntry entry = processManager.assertProcess(instanceId);\n        ProcessKey processKey = new ProcessKey(entry.instanceId(), entry.createdAt());\n\n        processManager.restoreFromCheckpoint(processKey, checkpointId);\n\n        return new ResumeProcessResponse();\n    }\n\n    @POST\n    @Path(\"{id}/checkpoint\")\n    @Consumes(MediaType.MULTIPART_FORM_DATA)\n    @Operation(description = \"Upload checkpoint\")\n    public void uploadCheckpoint(@PathParam(\"id\") UUID instanceId,\n                                 @Parameter(schema = @Schema(type = \"object\", implementation = Object.class)) MultipartInput input) {\n\n        try {\n            // TODO replace with ProcessKeyCache\n            ProcessEntry entry = processManager.assertProcess(instanceId);\n            ProcessKey processKey = new ProcessKey(entry.instanceId(), entry.createdAt());\n\n            UUID checkpointId = MultipartUtils.assertUuid(input, \"id\");\n            UUID correlationId = MultipartUtils.assertUuid(input, \"correlationId\");\n            String checkpointName = MultipartUtils.assertString(input, \"name\");\n            try (InputStream data = MultipartUtils.assertStream(input, \"data\");\n                 TemporaryPath tmpIn = PathUtils.tempFile(\"checkpoint\", \".zip\")) {\n\n                Files.copy(data, tmpIn.path(), StandardCopyOption.REPLACE_EXISTING);\n                checkpointManager.importCheckpoint(processKey, checkpointId, correlationId, checkpointName, tmpIn.path());\n            } catch (ValidationErrorsException e) {\n                throw new ConcordApplicationException(e.getMessage(), Response.Status.BAD_REQUEST);\n            } catch (IOException e) {\n                log.error(\"uploadCheckpoint ['{}'] -> error\", processKey, e);\n                throw new ConcordApplicationException(\"upload error: \" + e.getMessage());\n            }\n\n            log.info(\"uploadCheckpoint ['{}'] -> done\", processKey);\n        } finally {\n            input.close();\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/checkpoint/ProcessCheckpointV2Resource.java",
    "content": "package com.walmartlabs.concord.server.process.checkpoint;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.ProcessEntry;\nimport com.walmartlabs.concord.server.process.ProcessManager;\nimport com.walmartlabs.concord.server.process.state.ProcessCheckpointManager;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport java.util.UUID;\n\n@Path(\"/api/v2/process\")\n@Tag(name = \"CheckpointV2\")\npublic class ProcessCheckpointV2Resource implements Resource {\n    private static final Logger log = LoggerFactory.getLogger(ProcessCheckpointV2Resource.class);\n\n    private static final String RESTORE_PROCESS_ACTION = \"restore\";\n\n    private final ProcessManager processManager;\n    private final ProcessCheckpointManager checkpointManager;\n\n    @Inject\n    public ProcessCheckpointV2Resource(ProcessManager processManager,\n                                       ProcessCheckpointManager checkpointManager) {\n\n        this.processManager = processManager;\n        this.checkpointManager = checkpointManager;\n    }\n\n    @GET\n    @Path(\"{id}/checkpoint\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"Process checkpoint\")\n    public GenericCheckpointResponse processCheckpoint(@PathParam(\"id\") UUID instanceId,\n                                                       @QueryParam(\"name\") String checkpointName,\n                                                       @QueryParam(\"action\") String action) {\n\n        ProcessEntry entry = processManager.assertProcess(instanceId);\n        ProcessKey processKey = new ProcessKey(entry.instanceId(), entry.createdAt());\n\n        switch (action) {\n            case RESTORE_PROCESS_ACTION: {\n                return restore(processKey, checkpointName);\n            }\n            default: {\n                throw new ConcordApplicationException(\"Invalid action type: \" + action);\n            }\n        }\n    }\n\n    private GenericCheckpointResponse restore(ProcessKey processKey, String checkpointName) {\n        log.info(\"Restoring process from checkpoint: {}\", checkpointName);\n\n        UUID checkpointId = checkpointManager.getRecentCheckpointId(processKey, checkpointName);\n        processManager.restoreFromCheckpoint(processKey, checkpointId);\n\n        return new GenericCheckpointResponse();\n    }\n\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/checkpoint/RestoreCheckpointRequest.java",
    "content": "package com.walmartlabs.concord.server.process.checkpoint;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport javax.validation.constraints.NotNull;\nimport java.io.Serializable;\nimport java.util.UUID;\n\n@JsonInclude(Include.NON_NULL)\npublic class RestoreCheckpointRequest implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @NotNull\n    private final UUID id;\n\n    @JsonCreator\n    public RestoreCheckpointRequest(@JsonProperty(\"id\") UUID id) {\n        this.id = id;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    @Override\n    public String toString() {\n        return \"RestoreCheckpointRequest{\" +\n                \"id=\" + id +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/event/EventPhase.java",
    "content": "package com.walmartlabs.concord.server.process.event;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic enum EventPhase {\n\n    PRE(\"pre\"),\n    POST(\"post\");\n\n    private final String key;\n\n    EventPhase(String key) {\n        this.key = key;\n    }\n\n    public String getKey() {\n        return key;\n    }\n\n    public static EventPhase fromString(String s) {\n        if (s == null) {\n            return null;\n        }\n\n        return valueOf(s.toUpperCase());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/event/NewProcessEvent.java",
    "content": "package com.walmartlabs.concord.server.process.event;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.AllowNulls;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.time.OffsetDateTime;\nimport java.util.Map;\n\n@Value.Immutable\npublic interface NewProcessEvent {\n\n    ProcessKey processKey();\n\n    String eventType();\n\n    @Nullable\n    OffsetDateTime eventDate();\n\n    @Nullable\n    @AllowNulls\n    Map<String, Object> data();\n\n    static ImmutableNewProcessEvent.Builder builder() {\n        return ImmutableNewProcessEvent.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/event/ProcessEventDao.java",
    "content": "package com.walmartlabs.concord.server.process.event;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.db.PgUtils;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.UuidGenerator;\nimport com.walmartlabs.concord.server.jooq.tables.records.ProcessEventsRecord;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.events.ProcessEvent;\nimport org.jooq.*;\n\nimport javax.inject.Inject;\nimport java.sql.PreparedStatement;\nimport java.time.OffsetDateTime;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.PROCESS_EVENTS;\nimport static java.util.Objects.requireNonNull;\nimport static org.jooq.impl.DSL.*;\n\npublic class ProcessEventDao extends AbstractDao {\n\n    private final ConcordObjectMapper objectMapper;\n    private final UuidGenerator uuidGenerator;\n\n    @Inject\n    public ProcessEventDao(@MainDB Configuration cfg,\n                           ConcordObjectMapper objectMapper,\n                           UuidGenerator uuidGenerator) {\n        super(cfg);\n        this.objectMapper = requireNonNull(objectMapper);\n        this.uuidGenerator = requireNonNull(uuidGenerator);\n    }\n\n    @Override\n    protected void tx(Tx t) {\n        super.tx(t);\n    }\n\n    @Override\n    protected <T> T txResult(TxResult<T> t) {\n        return super.txResult(t);\n    }\n\n    public List<ProcessEventEntry> list(ProcessEventFilter filter) {\n        ProcessKey processKey = filter.processKey();\n\n        SelectConditionStep<Record5<Long, UUID, String, OffsetDateTime, JSONB>> q = dsl()\n                .select(PROCESS_EVENTS.EVENT_SEQ,\n                        PROCESS_EVENTS.EVENT_ID,\n                        PROCESS_EVENTS.EVENT_TYPE,\n                        PROCESS_EVENTS.EVENT_DATE,\n                        function(\"jsonb_strip_nulls\", JSONB.class, PROCESS_EVENTS.EVENT_DATA))\n                .from(PROCESS_EVENTS)\n                .where(PROCESS_EVENTS.INSTANCE_ID.eq(processKey.getInstanceId())\n                        .and(PROCESS_EVENTS.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt())));\n\n        OffsetDateTime after = filter.after();\n        if (after != null) {\n            q.and(PROCESS_EVENTS.EVENT_DATE.ge(after));\n        }\n\n        Long fromId = filter.fromId();\n        if (fromId != null) {\n            q.and(PROCESS_EVENTS.EVENT_SEQ.greaterThan(fromId));\n        }\n\n        String eventType = filter.eventType();\n        if (eventType != null) {\n            q.and(PROCESS_EVENTS.EVENT_TYPE.eq(eventType));\n        }\n\n        UUID eventCorrelationId = filter.eventCorrelationId();\n        if (eventCorrelationId != null) {\n            q.and(PgUtils.jsonbText(PROCESS_EVENTS.EVENT_DATA, \"correlationId\").eq(eventCorrelationId.toString()));\n        }\n\n        EventPhase eventPhase = filter.eventPhase();\n        if (eventPhase != null) {\n            q.and(PgUtils.jsonbText(PROCESS_EVENTS.EVENT_DATA, \"phase\").eq(eventPhase.getKey()));\n        }\n\n        int limit = filter.limit();\n        if (limit > 0) {\n            q.limit(limit);\n        }\n\n        int offset = filter.offset();\n        if (offset > 0) {\n            q.offset(offset);\n        }\n\n        return q.orderBy(PROCESS_EVENTS.EVENT_SEQ)\n                .fetch(this::toEntry);\n    }\n\n    public void insert(DSLContext tx, List<ProcessKey> processKeys, String eventType, Map<String, Object> data) {\n\n        String sql = tx.insertInto(PROCESS_EVENTS)\n                .set(PROCESS_EVENTS.EVENT_ID, (UUID) null)\n                .set(PROCESS_EVENTS.INSTANCE_ID, (UUID) null)\n                .set(PROCESS_EVENTS.INSTANCE_CREATED_AT, (OffsetDateTime) null)\n                .set(PROCESS_EVENTS.EVENT_TYPE, (String) null)\n                .set(PROCESS_EVENTS.EVENT_DATE, currentOffsetDateTime())\n                .set(PROCESS_EVENTS.EVENT_DATA, (JSONB) null)\n                .returning(PROCESS_EVENTS.EVENT_SEQ)\n                .getSQL();\n\n        tx.connection(conn -> {\n            try (PreparedStatement ps = conn.prepareStatement(sql)) {\n                for (ProcessKey pk : processKeys) {\n                    UUID eventId = uuidGenerator.generate();\n                    ps.setObject(1, eventId);\n                    ps.setObject(2, pk.getInstanceId());\n                    ps.setObject(3, pk.getCreatedAt());\n                    ps.setString(4, eventType);\n                    ps.setString(5, objectMapper.toJSONB(data).toString());\n                    ps.addBatch();\n                }\n                ps.executeBatch();\n            }\n        });\n    }\n\n    /**\n     * Batch inserts the provided {@code events}.\n     *\n     * @return the same events updated with autogenerated IDs\n     */\n    public List<ProcessEvent> insert(DSLContext tx, List<NewProcessEvent> events) {\n        if (events == null || events.isEmpty()) {\n            return Collections.emptyList();\n        }\n\n        if (events.size() == 1) {\n            NewProcessEvent ev = events.get(0);\n\n            UUID eventId = uuidGenerator.generate();\n            ProcessKey processKey = ev.processKey();\n            Field<OffsetDateTime> ts = ev.eventDate() != null ? value(ev.eventDate()) : currentOffsetDateTime();\n            Map<String, Object> m = ev.data() != null ? ev.data() : Collections.emptyMap();\n            String eventType = ev.eventType();\n\n            ProcessEventsRecord r = tx.insertInto(PROCESS_EVENTS)\n                    .set(PROCESS_EVENTS.EVENT_ID, eventId)\n                    .set(PROCESS_EVENTS.INSTANCE_ID, processKey.getInstanceId())\n                    .set(PROCESS_EVENTS.INSTANCE_CREATED_AT, processKey.getCreatedAt())\n                    .set(PROCESS_EVENTS.EVENT_TYPE, eventType)\n                    .set(PROCESS_EVENTS.EVENT_DATE, ts)\n                    .set(PROCESS_EVENTS.EVENT_DATA, objectMapper.toJSONB(m))\n                    .returning(PROCESS_EVENTS.EVENT_DATE, PROCESS_EVENTS.EVENT_SEQ)\n                    .fetchOne();\n\n            return Collections.singletonList(ProcessEvent.builder()\n                    .eventSeq(r.getEventSeq())\n                    .eventDate(r.getEventDate())\n                    .eventType(eventType)\n                    .processKey(processKey)\n                    .data(m)\n                    .build());\n        }\n\n        InsertSetStep<ProcessEventsRecord> q = tx.insertInto(PROCESS_EVENTS);\n        InsertSetMoreStep<ProcessEventsRecord> qq = null;\n        for (Iterator<NewProcessEvent> i = events.iterator(); i.hasNext(); ) {\n            NewProcessEvent ev = i.next();\n\n            ProcessKey pk = ev.processKey();\n\n            ProcessEventsRecord r = new ProcessEventsRecord();\n            r.setEventId(uuidGenerator.generate());\n            r.setInstanceId(pk.getInstanceId());\n            r.setInstanceCreatedAt(pk.getCreatedAt());\n            r.setEventType(ev.eventType());\n\n            // TODO replace with default = now()?\n            OffsetDateTime eventDate = ev.eventDate() != null ? ev.eventDate() : OffsetDateTime.now();\n            r.setEventDate(eventDate);\n\n            Map<String, Object> m = ev.data() != null ? ev.data() : Collections.emptyMap();\n            r.setEventData(objectMapper.toJSONB(m));\n\n            qq = q.set(r);\n            if (i.hasNext()) {\n                qq.newRecord();\n            }\n        }\n\n        // return only a subset of columns to save some traffic\n        Result<ProcessEventsRecord> records = qq.returning(PROCESS_EVENTS.EVENT_DATE, PROCESS_EVENTS.EVENT_SEQ)\n                .fetch();\n\n        if (records.size() != events.size()) {\n            throw new IllegalStateException(\"Invalid result. Returning records count doesn't match the number of events: \" + records.size() + \" != \" + events.size());\n        }\n\n        List<ProcessEvent> result = new ArrayList<>();\n        for (int i = 0; i < events.size(); i++) {\n            ProcessEventsRecord r = records.get(i);\n            NewProcessEvent ev = events.get(i);\n            Map<String, Object> data = ev.data();\n\n            result.add(ProcessEvent.builder()\n                    .eventDate(r.getEventDate())\n                    .eventSeq(r.getEventSeq())\n                    .eventType(ev.eventType())\n                    .processKey(ev.processKey())\n                    .data(data != null ? data : Collections.emptyMap())\n                    .build());\n        }\n\n        return result;\n    }\n\n    private ProcessEventEntry toEntry(Record5<Long, UUID, String, OffsetDateTime, JSONB> r) {\n        return ImmutableProcessEventEntry.builder()\n                .seqId(r.value1())\n                .id(r.value2())\n                .eventType(r.value3())\n                .eventDate(r.value4())\n                .data(objectMapper.fromJSONB(r.value5()))\n                .build();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/event/ProcessEventEntry.java",
    "content": "package com.walmartlabs.concord.server.process.event;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.time.OffsetDateTime;\nimport java.util.Map;\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_NULL)\n@JsonSerialize(as = ImmutableProcessEventEntry.class)\n@JsonDeserialize(as = ImmutableProcessEventEntry.class)\npublic interface ProcessEventEntry extends Serializable {\n\n    @Deprecated\n    UUID id();\n\n    long seqId();\n\n    String eventType();\n\n    @Nullable\n    Map<String, Object> data();\n\n    /**\n     * should match the format in {@link com.walmartlabs.concord.server.OffsetDateTimeParam}\n     */\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    OffsetDateTime eventDate();\n\n    static ImmutableProcessEventEntry.Builder builder() {\n        return ImmutableProcessEventEntry.builder();\n    }\n\n    static ImmutableProcessEventEntry.Builder from(ProcessEventEntry e) {\n        return ImmutableProcessEventEntry.builder().from(e);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/event/ProcessEventFilter.java",
    "content": "package com.walmartlabs.concord.server.process.event;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.time.OffsetDateTime;\nimport java.util.UUID;\n\n@Value.Immutable\npublic interface ProcessEventFilter {\n\n    ProcessKey processKey();\n\n    @Nullable\n    Long fromId();\n\n    @Nullable\n    OffsetDateTime after();\n\n    @Nullable\n    String eventType();\n\n    @Nullable\n    UUID eventCorrelationId();\n\n    @Nullable\n    EventPhase eventPhase();\n\n    @Value.Default\n    default int limit() {\n        return -1;\n    }\n\n    @Value.Default\n    default int offset() {\n        return -1;\n    }\n\n    static ImmutableProcessEventFilter.Builder builder() {\n        return ImmutableProcessEventFilter.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/event/ProcessEventManager.java",
    "content": "package com.walmartlabs.concord.server.process.event;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.Histogram;\nimport com.codahale.metrics.Meter;\nimport com.codahale.metrics.MetricRegistry;\nimport com.walmartlabs.concord.server.Listeners;\nimport com.walmartlabs.concord.server.sdk.events.ProcessEvent;\nimport com.walmartlabs.concord.server.sdk.metrics.InjectMeter;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport org.jooq.DSLContext;\n\nimport javax.inject.Inject;\nimport java.util.Collections;\nimport java.util.List;\n\npublic class ProcessEventManager {\n\n    private final ProcessEventDao eventDao;\n    private final Listeners listeners;\n\n    private final Histogram batchInsertHistogram;\n\n    @InjectMeter\n    private final Meter eventsReceived;\n\n    @Inject\n    public ProcessEventManager(ProcessEventDao eventDao,\n                               Listeners listeners,\n                               Meter eventsReceived,\n                               MetricRegistry metricRegistry) {\n\n        this.eventDao = eventDao;\n        this.listeners = listeners;\n        this.eventsReceived = eventsReceived;\n\n        this.batchInsertHistogram = metricRegistry.histogram(\"process-events-batch-insert\");\n    }\n\n    @WithTimer\n    public void event(List<NewProcessEvent> events) {\n        List<ProcessEvent> insertedEvents = eventDao.txResult(tx -> doEvent(tx, events));\n        listeners.onProcessEvent(insertedEvents);\n    }\n\n    public void event(DSLContext tx, NewProcessEvent event) {\n        event(tx, Collections.singletonList(event));\n    }\n\n    @WithTimer\n    public void event(DSLContext tx, List<NewProcessEvent> events) {\n        // TODO consider returning a callback that can be called outside of the transaction\n        List<ProcessEvent> insertedEvents = doEvent(tx, events);\n        listeners.onProcessEvent(insertedEvents);\n    }\n\n    public List<ProcessEventEntry> list(ProcessEventFilter filter) {\n        return eventDao.list(filter);\n    }\n\n    private List<ProcessEvent> doEvent(DSLContext tx, List<NewProcessEvent> events) {\n        if (events.isEmpty()) {\n            return Collections.emptyList();\n        }\n\n        List<ProcessEvent> insertedEvents = eventDao.insert(tx, events);\n\n        eventsReceived.mark(events.size());\n        batchInsertHistogram.update(events.size());\n\n        return insertedEvents;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/event/ProcessEventRequest.java",
    "content": "package com.walmartlabs.concord.server.process.event;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.io.Serializable;\nimport java.time.OffsetDateTime;\nimport java.util.Map;\n\n@JsonInclude(JsonInclude.Include.NON_NULL)\npublic class ProcessEventRequest implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String eventType;\n\n    private final OffsetDateTime eventDate;\n\n    private final Map<String, Object> data;\n\n    @JsonCreator\n    public ProcessEventRequest(@JsonProperty(\"eventType\") String eventType,\n                               @JsonProperty(\"eventDate\") OffsetDateTime eventDate,\n                               @JsonProperty(\"data\") Map<String, Object> data) {\n\n        this.eventType = eventType;\n        this.eventDate = eventDate;\n        this.data = data;\n    }\n\n    public String getEventType() {\n        return eventType;\n    }\n\n    public OffsetDateTime getEventDate() {\n        return eventDate;\n    }\n\n    public Map<String, Object> getData() {\n        return data;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/event/ProcessEventResource.java",
    "content": "package com.walmartlabs.concord.server.process.event;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.OffsetDateTimeParam;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.org.project.ProjectAccessManager;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueDao;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueDao.ProjectIdAndInitiator;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKeyCache;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.Utils.unwrap;\n\n@Path(\"/api/v1/process\")\n@Tag(name = \"Process Events\")\npublic class ProcessEventResource implements Resource {\n\n    // TODO this should be a common constant (in SDK maybe)\n    private static final String ELEMENT_EVENT_TYPE = \"ELEMENT\";\n\n    private final ProcessKeyCache processKeyCache;\n    private final ProcessQueueDao queueDao;\n    private final ProjectAccessManager projectAccessManager;\n    private final ProcessEventManager eventManager;\n\n    @Inject\n    public ProcessEventResource(ProcessKeyCache processKeyCache,\n                                ProcessQueueDao queueDao,\n                                ProjectAccessManager projectAccessManager,\n                                ProcessEventManager eventManager) {\n\n        this.processKeyCache = processKeyCache;\n        this.queueDao = queueDao;\n        this.projectAccessManager = projectAccessManager;\n        this.eventManager = eventManager;\n    }\n\n    /**\n     * Register a process event.\n     */\n    @POST\n    @Path(\"/{processInstanceId}/event\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"Register a process event\")\n    public void event(@PathParam(\"processInstanceId\") UUID processInstanceId,\n                      ProcessEventRequest req) {\n\n        ProcessKey processKey = assertProcessKey(processInstanceId);\n        NewProcessEvent e = NewProcessEvent.builder()\n                .processKey(processKey)\n                .eventType(req.getEventType())\n                .eventDate(req.getEventDate())\n                .data(req.getData())\n                .build();\n        eventManager.event(Collections.singletonList(e));\n    }\n\n    /**\n     * Register multiple events for the specified process.\n     */\n    @POST\n    @Path(\"/{processInstanceId}/eventBatch\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"Register multiple events for the specified process\")\n    public void batchEvent(@PathParam(\"processInstanceId\") UUID processInstanceId,\n                           List<ProcessEventRequest> data) {\n\n        ProcessKey processKey = assertProcessKey(processInstanceId);\n\n        List<NewProcessEvent> events = data.stream()\n                .map(req -> NewProcessEvent.builder()\n                        .processKey(processKey)\n                        .eventType(req.getEventType())\n                        .eventDate(req.getEventDate())\n                        .data(req.getData())\n                        .build())\n                .collect(Collectors.toList());\n\n        eventManager.event(events);\n    }\n\n    /**\n     * List process events.\n     */\n    @GET\n    @Path(\"/{processInstanceId}/event\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"List process events\", operationId = \"listProcessEvents\")\n    public List<ProcessEventEntry> list(@PathParam(\"processInstanceId\") UUID processInstanceId,\n                                        @QueryParam(\"type\") String eventType,\n                                        @QueryParam(\"after\") OffsetDateTimeParam after,\n                                        @QueryParam(\"fromId\") Long fromId,\n                                        @QueryParam(\"eventCorrelationId\") UUID eventCorrelationId,\n                                        @QueryParam(\"eventPhase\") EventPhase eventPhase,\n                                        @QueryParam(\"includeAll\") @DefaultValue(\"false\") boolean includeAll,\n                                        @QueryParam(\"limit\") @DefaultValue(\"-1\") int limit) {\n\n        ProcessKey processKey = assertProcessKey(processInstanceId);\n\n        if (includeAll) {\n            // verify that the user can access potentially sensitive data\n            assertAccessRights(processKey);\n        }\n\n        ProcessEventFilter f = ProcessEventFilter.builder()\n                .processKey(processKey)\n                .after(unwrap(after))\n                .eventType(eventType)\n                .eventCorrelationId(eventCorrelationId)\n                .eventPhase(eventPhase)\n                .limit(limit)\n                .fromId(fromId)\n                .build();\n\n        List<ProcessEventEntry> l = eventManager.list(f);\n        if (!includeAll) {\n            l = filterOutSensitiveData(l);\n        }\n\n        return l;\n    }\n\n    private ProcessKey assertProcessKey(UUID instanceId) {\n        ProcessKey processKey = processKeyCache.get(instanceId);\n        if (processKey == null) {\n            throw new ConcordApplicationException(\"Process instance not found\", Response.Status.NOT_FOUND);\n        }\n        return processKey;\n    }\n\n    private void assertAccessRights(PartialProcessKey processKey) {\n        if (Roles.isAdmin()) {\n            // an admin can access any project\n            return;\n        }\n\n        UserPrincipal p = UserPrincipal.getCurrent();\n        if (p == null) {\n            return;\n        }\n\n        ProjectIdAndInitiator ids = queueDao.getProjectIdAndInitiator(processKey);\n        if (ids.getProjectId() != null) {\n            // if the process belongs to a project, only those who have WRITER privileges can\n            // access extended event data\n            if (projectAccessManager.assertAccess(ids.getProjectId(), ResourceAccessLevel.WRITER, true) != null) {\n                return;\n            }\n        }\n\n        if (p.getId().equals(ids.getInitiatorId())) {\n            // if it is a standalone process, only the initator can access extended event data\n            return;\n        }\n\n        throw new UnauthorizedException(\"Only admins, process initiators and those who have READER access to \" +\n                \"the process' projects can access the extended process event data\");\n    }\n\n    private static List<ProcessEventEntry> filterOutSensitiveData(List<ProcessEventEntry> entries) {\n        if (entries == null || entries.isEmpty()) {\n            return entries;\n        }\n\n        List<ProcessEventEntry> result = new ArrayList<>(entries.size());\n        for (ProcessEventEntry e : entries) {\n            if (!ELEMENT_EVENT_TYPE.equals(e.eventType())) {\n                result.add(e);\n                continue;\n            }\n\n            Map<String, Object> data = e.data();\n            if (data == null) {\n                result.add(e);\n                continue;\n            }\n\n            // remove in/out variables\n            Map<String, Object> m = new HashMap<>(data);\n            m.remove(\"in\");\n            m.remove(\"out\");\n\n            result.add(ProcessEventEntry.from(e)\n                    .data(m)\n                    .build());\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/form/ExternalFileFormValidatorLocale.java",
    "content": "package com.walmartlabs.concord.server.process.form;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.form.ConcordFormValidatorLocale;\nimport com.walmartlabs.concord.common.form.DefaultConcordFormValidatorLocale;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.state.ProcessStateManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport io.takari.bpm.model.form.FormField;\n\nimport java.io.InputStream;\nimport java.text.MessageFormat;\nimport java.util.*;\nimport java.util.function.Function;\nimport java.util.stream.Stream;\n\npublic class ExternalFileFormValidatorLocale implements ConcordFormValidatorLocale {\n\n    private final ConcordFormValidatorLocale fallback = new DefaultConcordFormValidatorLocale();\n\n    private final Map<String, String> messages;\n\n    public ExternalFileFormValidatorLocale(ProcessKey processKey, String formName, ProcessStateManager stateManager) {\n        this.messages = loadMessages(processKey, formName, stateManager);\n    }\n\n    @Override\n    public String noFieldsDefined(String formId) {\n        return fallback.noFieldsDefined(formId);\n    }\n\n    @Override\n    public String invalidCardinality(String formId, FormField field, Object value) {\n        return getMessage(\"invalidCardinality\", field)\n                .map(m -> MessageFormat.format(m, fieldName(field, null), spell(field.getCardinality()), field.getCardinality(), value))\n                .orElse(fallback.invalidCardinality(formId, field, value));\n    }\n\n    @Override\n    public String expectedString(String formId, FormField field, Integer idx, Object value) {\n        return getMessage(\"expectedString\", field)\n                .map(m -> MessageFormat.format(m, fieldName(field, idx), value))\n                .orElse(fallback.expectedString(formId, field, idx, value));\n    }\n\n    @Override\n    public String expectedInteger(String formId, FormField field, Integer idx, Object value) {\n        return getMessage(\"expectedInteger\", field)\n                .map(m -> MessageFormat.format(m, fieldName(field, idx), value))\n                .orElse(fallback.expectedInteger(formId, field, idx, value));\n    }\n\n    @Override\n    public String expectedDecimal(String formId, FormField field, Integer idx, Object value) {\n        return getMessage(\"expectedDecimal\", field)\n                .map(m -> MessageFormat.format(m, fieldName(field, idx), value))\n                .orElse(fallback.expectedDecimal(formId, field, idx, value));\n    }\n\n    @Override\n    public String expectedBoolean(String formId, FormField field, Integer idx, Object value) {\n        return getMessage(\"expectedBoolean\", field)\n                .map(m -> MessageFormat.format(m, fieldName(field, idx), value))\n                .orElse(fallback.expectedBoolean(formId, field, idx, value));\n    }\n\n    public String expectedDate(String formId, FormField field, Integer idx, Object value) {\n        return getMessage(\"expectedDate\", field)\n                .map(m -> MessageFormat.format(m, fieldName(field, idx), value))\n                .orElse(fallback.expectedDate(formId, field, idx, value));\n    }\n\n    @Override\n    public String doesntMatchPattern(String formId, FormField field, Integer idx, String pattern, Object value) {\n        return getMessage(\"doesntMatchPattern\", field)\n                .map(m -> MessageFormat.format(m, fieldName(field, idx), value, pattern))\n                .orElse(fallback.doesntMatchPattern(formId, field, idx, pattern, value));\n    }\n\n    @Override\n    public String integerRangeError(String formId, FormField field, Integer idx, Long min, Long max, Object value) {\n        return getMessage(\"integerRangeError\", field)\n                .map(m -> MessageFormat.format(m, fieldName(field, idx), value, bounds(min, max), min, max))\n                .orElse(fallback.integerRangeError(formId, field, idx, min, max, value));\n    }\n\n    @Override\n    public String decimalRangeError(String formId, FormField field, Integer idx, Double min, Double max, Object value) {\n        return getMessage(\"decimalRangeError\", field)\n                .map(m -> MessageFormat.format(m, fieldName(field, idx), value, bounds(min, max), min, max))\n                .orElse(fallback.decimalRangeError(formId, field, idx, min, max, value));\n    }\n\n    @Override\n    public String valueNotAllowed(String formId, FormField field, Integer idx, Object allowed, Object value) {\n        return getMessage(\"valueNotAllowed\", field)\n                .map(m -> MessageFormat.format(m, fieldName(field, idx), value, allowed))\n                .orElse(fallback.valueNotAllowed(formId, field, idx, allowed, value));\n    }\n\n    private Optional<String> getMessage(String name, FormField field) {\n        String result = messages.get(field.getName() + \".\" + name);\n        if (result == null) {\n            result = messages.get(name);\n        }\n        return Optional.ofNullable(result);\n    }\n\n    private static Map<String, String> loadMessages(ProcessKey processKey, String formName, ProcessStateManager stateManager) {\n        Function<InputStream, Optional<Map<String, String>>> converter = inputStream -> {\n            try {\n                Map<String, String> result = new HashMap<>();\n                Properties p = new Properties();\n                p.load(inputStream);\n                for (final String name : p.stringPropertyNames()) {\n                    result.put(name, p.getProperty(name));\n                }\n                return Optional.of(result);\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n        };\n\n        String globalPath = Constants.Files.ERROR_MESSAGES_FILE_NAME;\n        String formSpecificPath = \"forms/\" + formName + \"/\" + Constants.Files.ERROR_MESSAGES_FILE_NAME;\n\n        return Stream.of(formSpecificPath, globalPath)\n                .map(p -> stateManager.get(processKey, p, converter).orElse(null))\n                .filter(Objects::nonNull)\n                .findFirst()\n                .orElse(Collections.emptyMap());\n    }\n\n    private static String spell(FormField.Cardinality c) {\n        if (c == null) {\n            throw new IllegalArgumentException(\"Cardinality can't be null\");\n        }\n        \n        switch (c) {\n            case ANY:\n                return \"any number of values\";\n            case ONE_AND_ONLY_ONE:\n                return \"a single value\";\n            case ONE_OR_NONE:\n                return \"a single optional value\";\n            case AT_LEAST_ONE:\n                return \"at least a single value\";\n            default:\n                throw new IllegalArgumentException(\"Unsupported cardinality type: \" + c);\n        }\n    }\n\n    private static String bounds(Object min, Object max) {\n        if (min != null && max != null) {\n            return String.format(\"within %s and %s (inclusive)\", min, max);\n        } else if (min == null) {\n            return String.format(\"less or equal than %s\", max);\n        } else {\n            return String.format(\"equal or greater than %s\", min);\n        }\n    }\n\n    private static String fieldName(FormField field, Integer idx) {\n        String s = field.getLabel();\n        if (s == null) {\n            s = field.getName();\n        }\n\n        if (idx != null) {\n            s = s + \" [\" + idx + \"]\";\n        }\n\n        return s;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/form/ExternalFileFormValidatorLocaleV2.java",
    "content": "package com.walmartlabs.concord.server.process.form;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.forms.DefaultFormValidatorLocale;\nimport com.walmartlabs.concord.forms.FormField;\nimport com.walmartlabs.concord.forms.FormValidatorLocale;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.state.ProcessStateManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\n\nimport java.io.InputStream;\nimport java.text.MessageFormat;\nimport java.util.*;\nimport java.util.function.Function;\nimport java.util.stream.Stream;\n\nimport static com.walmartlabs.concord.forms.DefaultFormValidatorLocale.*;\n\npublic class ExternalFileFormValidatorLocaleV2 implements FormValidatorLocale {\n\n    private final FormValidatorLocale fallback = new DefaultFormValidatorLocale();\n\n    private final Map<String, String> messages;\n\n    public ExternalFileFormValidatorLocaleV2(ProcessKey processKey, String formName, ProcessStateManager stateManager) {\n        this.messages = loadMessages(processKey, formName, stateManager);\n    }\n\n    @Override\n    public String noFieldsDefined(String formId) {\n        return fallback.noFieldsDefined(formId);\n    }\n\n    @Override\n    public String invalidCardinality(String formId, FormField field, Object value) {\n        return getMessage(\"invalidCardinality\", field)\n                .map(m -> MessageFormat.format(m, fieldName(field, null), spell(field.cardinality()), field.cardinality(), value))\n                .orElse(fallback.invalidCardinality(formId, field, value));\n    }\n\n    @Override\n    public String expectedString(String formId, FormField field, Integer idx, Object value) {\n        return getMessage(\"expectedString\", field)\n                .map(m -> MessageFormat.format(m, fieldName(field, idx), value))\n                .orElse(fallback.expectedString(formId, field, idx, value));\n    }\n\n    @Override\n    public String expectedInteger(String formId, FormField field, Integer idx, Object value) {\n        return getMessage(\"expectedInteger\", field)\n                .map(m -> MessageFormat.format(m, fieldName(field, idx), value))\n                .orElse(fallback.expectedInteger(formId, field, idx, value));\n    }\n\n    @Override\n    public String expectedDecimal(String formId, FormField field, Integer idx, Object value) {\n        return getMessage(\"expectedDecimal\", field)\n                .map(m -> MessageFormat.format(m, fieldName(field, idx), value))\n                .orElse(fallback.expectedDecimal(formId, field, idx, value));\n    }\n\n    @Override\n    public String expectedBoolean(String formId, FormField field, Integer idx, Object value) {\n        return getMessage(\"expectedBoolean\", field)\n                .map(m -> MessageFormat.format(m, fieldName(field, idx), value))\n                .orElse(fallback.expectedBoolean(formId, field, idx, value));\n    }\n\n    public String expectedDate(String formId, FormField field, Integer idx, Object value) {\n        return getMessage(\"expectedDate\", field)\n                .map(m -> MessageFormat.format(m, fieldName(field, idx), value))\n                .orElse(fallback.expectedDate(formId, field, idx, value));\n    }\n\n    @Override\n    public String doesntMatchPattern(String formId, FormField field, Integer idx, String pattern, Object value) {\n        return getMessage(\"doesntMatchPattern\", field)\n                .map(m -> MessageFormat.format(m, fieldName(field, idx), value, pattern))\n                .orElse(fallback.doesntMatchPattern(formId, field, idx, pattern, value));\n    }\n\n    @Override\n    public String integerRangeError(String formId, FormField field, Integer idx, Long min, Long max, Object value) {\n        return getMessage(\"integerRangeError\", field)\n                .map(m -> MessageFormat.format(m, fieldName(field, idx), value, bounds(min, max), min, max))\n                .orElse(fallback.integerRangeError(formId, field, idx, min, max, value));\n    }\n\n    @Override\n    public String decimalRangeError(String formId, FormField field, Integer idx, Double min, Double max, Object value) {\n        return getMessage(\"decimalRangeError\", field)\n                .map(m -> MessageFormat.format(m, fieldName(field, idx), value, bounds(min, max), min, max))\n                .orElse(fallback.decimalRangeError(formId, field, idx, min, max, value));\n    }\n\n    @Override\n    public String valueNotAllowed(String formId, FormField field, Integer idx, Object allowed, Object value) {\n        return getMessage(\"valueNotAllowed\", field)\n                .map(m -> MessageFormat.format(m, fieldName(field, idx), value, allowed))\n                .orElse(fallback.valueNotAllowed(formId, field, idx, allowed, value));\n    }\n\n    private Optional<String> getMessage(String name, FormField field) {\n        String result = messages.get(field.name() + \".\" + name);\n        if (result == null) {\n            result = messages.get(name);\n        }\n        return Optional.ofNullable(result);\n    }\n\n    private static Map<String, String> loadMessages(ProcessKey processKey, String formName, ProcessStateManager stateManager) {\n        Function<InputStream, Optional<Map<String, String>>> converter = inputStream -> {\n            try {\n                Map<String, String> result = new HashMap<>();\n                Properties p = new Properties();\n                p.load(inputStream);\n                for (final String name : p.stringPropertyNames()) {\n                    result.put(name, p.getProperty(name));\n                }\n                return Optional.of(result);\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n        };\n\n        String globalPath = Constants.Files.ERROR_MESSAGES_FILE_NAME;\n        String formSpecificPath = \"forms/\" + formName + \"/\" + Constants.Files.ERROR_MESSAGES_FILE_NAME;\n\n        return Stream.of(formSpecificPath, globalPath)\n                .map(p -> stateManager.get(processKey, p, converter).orElse(null))\n                .filter(Objects::nonNull)\n                .findFirst()\n                .orElse(Collections.emptyMap());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/form/FormAccessManager.java",
    "content": "package com.walmartlabs.concord.server.process.form;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.state.ProcessStateManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.user.UserInfoProvider;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport io.takari.bpm.form.Form;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.ObjectInputStream;\nimport java.io.Serializable;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport static com.walmartlabs.concord.server.process.state.ProcessStateManager.path;\n\npublic class FormAccessManager {\n\n    private static final Pattern GROUP_PATTERN = Pattern.compile(\"CN=(.*?),\", Pattern.CASE_INSENSITIVE);\n\n    private final ProcessStateManager stateManager;\n    private final UserManager userManager;\n\n    @Inject\n    public FormAccessManager(ProcessStateManager stateManager, UserManager userManager) {\n        this.stateManager = stateManager;\n        this.userManager = userManager;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public Form assertFormAccess(ProcessKey processKey, String formName) {\n        Form f = getForm(processKey, formName);\n        if (f == null) {\n            return null;\n        }\n\n        if (Roles.isAdmin()) {\n            return f;\n        }\n\n        Map<String, Object> opts = f.getOptions();\n        if (opts == null) {\n            return f;\n        }\n\n        Map<String, Serializable> runAsParams = (Map<String, Serializable>) opts.get(Constants.Forms.RUN_AS_KEY);\n\n        assertFormAccess(formName, runAsParams);\n\n        return f;\n    }\n\n    public void assertFormAccess(String formName, Map<String, Serializable> runAsParams) {\n        if (runAsParams == null || runAsParams.isEmpty()) {\n            return;\n        }\n\n        UserPrincipal p = UserPrincipal.assertCurrent();\n\n        Set<String> expectedUsers = com.walmartlabs.concord.forms.FormUtils.getRunAsUsers(formName, runAsParams);\n        if (!expectedUsers.isEmpty() && !expectedUsers.contains(p.getUsername())) {\n            throw new UnauthorizedException(\"The current user (\" + p.getUsername() + \") doesn't have \" +\n                    \"the necessary permissions to access the form.\");\n        }\n\n        Set<String> formRunAsGroups = com.walmartlabs.concord.forms.FormUtils.getRunAsLdapGroups(formName, runAsParams);\n        if (!formRunAsGroups.isEmpty()) {\n            Set<String> userLdapGroups = Optional.ofNullable(userManager.getCurrentUserInfo())\n                    .map(UserInfoProvider.UserInfo::groups)\n                    .orElseGet(Set::of);\n\n            boolean isGroupMatched = formRunAsGroups.stream()\n                    .anyMatch(group -> matchesLdapGroup(group, userLdapGroups));\n\n            if (!isGroupMatched) {\n                throw new UnauthorizedException(\"The current user (\" + p.getUsername() + \") doesn't have \" +\n                        \"the necessary permissions to resume process. Expected LDAP group(s) '\" + formRunAsGroups + \"'\");\n            }\n        }\n    }\n\n    // TODO: move to the formManager\n    private Form getForm(ProcessKey processKey, String formName) {\n        String resource = path(Constants.Files.JOB_ATTACHMENTS_DIR_NAME,\n                Constants.Files.JOB_STATE_DIR_NAME,\n                Constants.Files.JOB_FORMS_DIR_NAME,\n                formName);\n\n        Optional<Form> o = stateManager.get(processKey, resource, FormAccessManager::deserialize);\n        return o.orElse(null);\n    }\n\n    private static Optional<Form> deserialize(InputStream data) {\n        try (ObjectInputStream in = new ObjectInputStream(data)) {\n            return Optional.ofNullable((Form) in.readObject());\n        } catch (IOException | ClassNotFoundException e) {\n            throw new RuntimeException(\"Error while deserializing a form\", e);\n        }\n    }\n\n    private static boolean matchesLdapGroup(String pattern, Set<String> userLdapGroups) {\n        if (userLdapGroups == null) {\n            return false;\n        }\n\n        String normalizedPattern = normalizeGroup(pattern);\n        boolean isLdapGroupPattern = checkLdapGroupPattern(pattern);\n\n        return userLdapGroups.stream()\n                .anyMatch(g -> {\n                    if (isLdapGroupPattern && checkLdapGroupPattern(g)) {\n                        return Pattern.compile(pattern, Pattern.CASE_INSENSITIVE).matcher(g).matches();\n                    }\n                    return normalizedPattern.equalsIgnoreCase(normalizeGroup(g));\n                });\n    }\n\n    private static boolean checkLdapGroupPattern(String group) {\n        // memberOf attribute values(groups) starts with CN\n        return group.toUpperCase().trim().startsWith(\"CN=\");\n    }\n\n    private static String normalizeGroup(String group) {\n        Matcher matcher = GROUP_PATTERN.matcher(group);\n        if (matcher.find()) {\n            return matcher.group(1);\n        }\n        return group;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/form/FormInstanceEntry.java",
    "content": "package com.walmartlabs.concord.server.process.form;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.io.Serializable;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.stream.Collectors;\n\n@JsonInclude(Include.NON_NULL)\npublic class FormInstanceEntry implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String processInstanceId;\n    private final String name;\n    private final List<Field> fields;\n    private final boolean custom;\n    private final boolean yield;\n\n    @JsonCreator\n    public FormInstanceEntry(@JsonProperty(\"processInstanceId\") String processInstanceId,\n                             @JsonProperty(\"name\") String name,\n                             @JsonProperty(\"fields\") List<Field> fields,\n                             @JsonProperty(\"custom\") boolean custom,\n                             @JsonProperty(\"yield\") boolean yield) {\n\n        this.processInstanceId = processInstanceId;\n        this.name = name;\n        this.fields = fields;\n        this.custom = custom;\n        this.yield = yield;\n    }\n\n    public String getProcessInstanceId() {\n        return processInstanceId;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public List<Field> getFields() {\n        return fields;\n    }\n\n    public boolean isCustom() {\n        return custom;\n    }\n\n    public boolean isYield() {\n        return yield;\n    }\n\n    @Override\n    public String toString() {\n        return \"FormInstanceEntry{\" +\n                \"processInstanceId='\" + processInstanceId + '\\'' +\n                \", name='\" + name + '\\'' +\n                \", fields=\" + fields +\n                \", custom=\" + custom +\n                \", yield=\" + yield +\n                '}';\n    }\n\n    @JsonInclude(Include.NON_NULL)\n    public static class Field implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        private final String name;\n        private final String label;\n        private final String type;\n        private final Cardinality cardinality;\n        private final Object value;\n        private final Object allowedValue;\n        private final Map<String, Object> options;\n\n        @JsonCreator\n        public Field(String name, String label, String type, Cardinality cardinality, Object value, Object allowedValue, Map<String, Object> options) {\n            this.name = name;\n            this.label = label;\n            this.type = type;\n            this.cardinality = cardinality;\n            this.value = value;\n            this.allowedValue = allowedValue;\n            this.options = Optional.ofNullable(options)\n                    .map(m -> m.entrySet()\n                            .stream()\n                            .filter(e -> Objects.nonNull(e.getValue()))\n                            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))\n                    .orElse(null);\n        }\n\n        public String getName() {\n            return name;\n        }\n\n        public String getLabel() {\n            return label;\n        }\n\n        public String getType() {\n            return type;\n        }\n\n        public Cardinality getCardinality() {\n            return cardinality;\n        }\n\n        public Object getValue() {\n            return value;\n        }\n\n        public Object getAllowedValue() {\n            return allowedValue;\n        }\n\n        public Map<String, Object> getOptions() {\n            return options;\n        }\n\n        @Override\n        public String toString() {\n            return \"Field{\" +\n                    \"name='\" + name + '\\'' +\n                    \", label='\" + label + '\\'' +\n                    \", type='\" + type + '\\'' +\n                    \", cardinality=\" + cardinality +\n                    \", value=\" + value +\n                    \", allowedValue=\" + allowedValue +\n                    \", options=\" + options +\n                    '}';\n        }\n    }\n\n    public enum Cardinality {\n\n        ONE_OR_NONE,\n        ONE_AND_ONLY_ONE,\n        AT_LEAST_ONE,\n        ANY\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/form/FormListEntry.java",
    "content": "package com.walmartlabs.concord.server.process.form;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.io.Serializable;\nimport java.util.Map;\n\n@JsonInclude(Include.NON_NULL)\npublic class FormListEntry implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String name;\n    private final boolean custom;\n    private final boolean yield;\n    private final Map<String, Object> runAs;\n\n    @JsonCreator\n    public FormListEntry(@JsonProperty(\"name\") String name,\n                         @JsonProperty(\"custom\") boolean custom,\n                         @JsonProperty(\"yield\") boolean yield,\n                         @JsonProperty(\"runAs\") Map<String, Object> runAs) {\n\n        this.name = name;\n        this.custom = custom;\n        this.yield = yield;\n        this.runAs = runAs;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public boolean isCustom() {\n        return custom;\n    }\n\n    public boolean isYield() {\n        return yield;\n    }\n\n    public Map<String, Object> getRunAs() {\n        return runAs;\n    }\n\n    @Override\n    public String toString() {\n        return \"FormListEntry{\" +\n                \"name='\" + name + '\\'' +\n                \", custom=\" + custom +\n                \", yield=\" + yield +\n                \", runAs=\" + runAs +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/form/FormManager.java",
    "content": "package com.walmartlabs.concord.server.process.form;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.forms.Form;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.state.ProcessStateManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.ObjectInputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.function.Function;\n\nimport static com.walmartlabs.concord.server.process.state.ProcessStateManager.path;\n\npublic class FormManager {\n\n    private final ProcessStateManager stateManager;\n\n    @Inject\n    public FormManager(ProcessStateManager stateManager) {\n        this.stateManager = stateManager;\n    }\n\n    public Form get(ProcessKey processKey, String formName) {\n        String resource = path(Constants.Files.JOB_ATTACHMENTS_DIR_NAME,\n                Constants.Files.JOB_STATE_DIR_NAME,\n                Constants.Files.JOB_FORMS_V2_DIR_NAME,\n                formName);\n\n        Optional<Form> o = stateManager.get(processKey, resource, FormManager::deserialize);\n        return o.orElse(null);\n    }\n\n    public void delete(ProcessKey processKey, String formName) {\n        String resource = path(Constants.Files.JOB_ATTACHMENTS_DIR_NAME,\n                Constants.Files.JOB_STATE_DIR_NAME,\n                Constants.Files.JOB_FORMS_V2_DIR_NAME,\n                formName);\n\n        stateManager.deleteFile(processKey, resource);\n    }\n\n    public void delete(Path workDir, String formName) {\n        String resource = path(Constants.Files.JOB_ATTACHMENTS_DIR_NAME,\n                Constants.Files.JOB_STATE_DIR_NAME,\n                Constants.Files.JOB_FORMS_V2_DIR_NAME,\n                formName);\n\n        try {\n            Files.deleteIfExists(workDir.resolve(resource));\n        } catch (Exception e) {\n            throw new RuntimeException(\"Form delete error: \" + e.getMessage());\n        }\n    }\n\n    public List<Form> list(ProcessKey processKey) {\n        String resource = path(Constants.Files.JOB_ATTACHMENTS_DIR_NAME,\n                Constants.Files.JOB_STATE_DIR_NAME,\n                Constants.Files.JOB_FORMS_V2_DIR_NAME);\n\n        return stateManager.forEach(processKey, resource, FormManager::deserialize);\n    }\n\n    public String nextFormId(ProcessKey processKey) {\n        String resource = path(Constants.Files.JOB_ATTACHMENTS_DIR_NAME,\n                Constants.Files.JOB_STATE_DIR_NAME,\n                Constants.Files.JOB_FORMS_V2_DIR_NAME);\n\n        Function<String, Optional<String>> getId = s -> {\n            int i = s.lastIndexOf(\"/\");\n            if (i < 0 || i + 1 >= s.length()) {\n                return Optional.empty();\n            }\n            return Optional.of(s.substring(i + 1));\n        };\n\n        // TODO this probably should be replaced with ProcessStateManager#findFirst\n        Optional<String> o = stateManager.findPath(processKey, resource,\n                files -> files.findFirst().flatMap(getId));\n\n        return o.orElse(null);\n    }\n\n    private static Optional<Form> deserialize(InputStream data) {\n        try (ObjectInputStream in = new ObjectInputStream(data)) {\n            return Optional.ofNullable((Form) in.readObject());\n        } catch (IOException | ClassNotFoundException e) {\n            throw new RuntimeException(\"Error while deserializing a form\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/form/FormModule.java",
    "content": "package com.walmartlabs.concord.server.process.form;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.walmartlabs.concord.server.Utils.bindJaxRsResource;\n\npublic class FormModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        binder.bind(FormAccessManager.class).in(SINGLETON);\n        binder.bind(FormManager.class).in(SINGLETON);\n        binder.bind(FormResourceV1.class).in(SINGLETON);\n        binder.bind(FormResourceV2.class).in(SINGLETON);\n        binder.bind(FormServiceV1.class).in(SINGLETON);\n        binder.bind(FormServiceV1.class).in(SINGLETON);\n        binder.bind(FormServiceV2.class).in(SINGLETON);\n\n        bindJaxRsResource(binder, FormResource.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/form/FormResource.java",
    "content": "package com.walmartlabs.concord.server.process.form;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.state.ProcessStateManager;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.Parameter;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.jboss.resteasy.plugins.providers.multipart.MultipartInput;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.process.state.ProcessStateManager.path;\n\n@Path(\"/api/v1/process\")\n@Tag(name = \"Process Forms\")\npublic class FormResource implements Resource {\n\n    private final ProcessStateManager stateManager;\n    private final FormResourceV1 formResourceV1;\n    private final FormResourceV2 formResourceV2;\n\n    @Inject\n    public FormResource(ProcessStateManager stateManager, FormResourceV1 formResourceV1, FormResourceV2 formResourceV2) {\n        this.stateManager = stateManager;\n        this.formResourceV1 = formResourceV1;\n        this.formResourceV2 = formResourceV2;\n    }\n\n    @GET\n    @Path(\"/{processInstanceId}/form\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"List the available forms\", operationId = \"listProcessForms\")\n    public List<FormListEntry> list(@PathParam(\"processInstanceId\") UUID processInstanceId) {\n        if (isV2(processInstanceId)) {\n            return formResourceV2.list(processInstanceId);\n        } else {\n            return formResourceV1.list(processInstanceId);\n        }\n    }\n\n    /**\n     * Return the current state of a form instance.\n     */\n    @GET\n    @Path(\"/{processInstanceId}/form/{formName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Get the current state of a form\", operationId = \"getProcessForm\")\n    public FormInstanceEntry get(@PathParam(\"processInstanceId\") UUID processInstanceId,\n                                 @PathParam(\"formName\") String formName) {\n\n        if (isV2(processInstanceId)) {\n            return formResourceV2.get(processInstanceId, formName);\n        } else {\n            return formResourceV1.get(processInstanceId, formName);\n        }\n    }\n\n    /**\n     * Submit form instance's data, potentially resuming a suspended process.\n     */\n    @POST\n    @Path(\"/{processInstanceId}/form/{formName}\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Submit JSON form data\", operationId = \"submitForm\")\n    public FormSubmitResponse submit(@PathParam(\"processInstanceId\") UUID processInstanceId,\n                                     @PathParam(\"formName\") String formName,\n                                     Map<String, Object> data) {\n\n        if (isV2(processInstanceId)) {\n            return formResourceV2.submit(processInstanceId, formName, data);\n        } else {\n            return formResourceV1.submit(processInstanceId, formName, data);\n        }\n    }\n\n    /**\n     * Submit form instance's data, potentially resuming a suspended process.\n     * The method must have a different {@code @Path} than {@link #submit(UUID, String, Map)} to avoid\n     * conflicts in the Swagger spec/clients.\n     */\n    @POST\n    @Path(\"/{processInstanceId}/form/{formName}/multipart\")\n    @Consumes(MediaType.MULTIPART_FORM_DATA)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Submit JSON form data\", operationId = \"submitFormAsMultipart\")\n    public FormSubmitResponse submit(@PathParam(\"processInstanceId\") UUID processInstanceId,\n                                     @PathParam(\"formName\") String formName,\n                                     @Parameter(schema = @Schema(type = \"object\", implementation = Object.class)) MultipartInput data) {\n\n        try {\n            if (isV2(processInstanceId)) {\n                return formResourceV2.submit(processInstanceId, formName, data);\n            } else {\n                return formResourceV1.submit(processInstanceId, formName, data);\n            }\n        } finally {\n            data.close();\n        }\n    }\n\n    private boolean isV2(UUID processInstanceId) {\n        String resource = path(Constants.Files.JOB_ATTACHMENTS_DIR_NAME,\n                Constants.Files.JOB_STATE_DIR_NAME,\n                Constants.Files.JOB_FORMS_V2_DIR_NAME);\n        return stateManager.exists(PartialProcessKey.from(processInstanceId), resource);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/form/FormResourceV1.java",
    "content": "package com.walmartlabs.concord.server.process.form;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.form.ConcordFormValidatorLocale;\nimport com.walmartlabs.concord.common.form.DefaultConcordFormValidatorLocale;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport com.walmartlabs.concord.server.MultipartUtils;\nimport com.walmartlabs.concord.server.process.form.FormUtils.ValidationException;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport io.takari.bpm.form.Form;\nimport io.takari.bpm.model.form.FormDefinition;\nimport io.takari.bpm.model.form.FormField;\nimport org.jboss.resteasy.plugins.providers.multipart.MultipartInput;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response.Status;\nimport java.util.*;\n\n@Tag(name = \"FormsV1\")\npublic class FormResourceV1 {\n\n    private static final String FORMS_RESOURCES_PATH = \"forms\";\n\n    private final FormServiceV1 formService;\n    private final ConcordFormValidatorLocale validatorLocale;\n\n    @Inject\n    public FormResourceV1(FormServiceV1 formService) {\n        this.formService = formService;\n        this.validatorLocale = new DefaultConcordFormValidatorLocale();\n    }\n\n    @GET\n    @Path(\"/{processInstanceId}/form\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"List the available forms\", operationId = \"listFormsV1\")\n    public List<FormListEntry> list(@PathParam(\"processInstanceId\") UUID processInstanceId) {\n        PartialProcessKey processKey = PartialProcessKey.from(processInstanceId);\n\n        try {\n            return formService.list(processKey);\n        } catch (Exception e) {\n            throw new ConcordApplicationException(\"Error while retrieving a list of forms: \" + processKey, e);\n        }\n    }\n\n    /**\n     * Return the current state of a form instance.\n     */\n    @GET\n    @Path(\"/{processInstanceId}/form/{formName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Get the current state of a form\", operationId = \"getFormV1\")\n    public FormInstanceEntry get(@PathParam(\"processInstanceId\") UUID processInstanceId,\n                                 @PathParam(\"formName\") String formName) {\n\n        PartialProcessKey processKey = PartialProcessKey.from(processInstanceId);\n\n        Form form = formService.get(processKey, formName);\n        if (form == null) {\n            throw new ConcordApplicationException(\"Form \" + formName + \" not found. Process ID: \" + processKey, Status.NOT_FOUND);\n        }\n\n        Map<String, Object> data = FormUtils.values(form);\n        boolean yield = MapUtils.getBoolean(form.getOptions(), \"yield\", false);\n\n        Map<String, Object> allowedValues = form.getAllowedValues();\n        if (allowedValues == null) {\n            allowedValues = Collections.emptyMap();\n        }\n\n        List<FormInstanceEntry.Field> fields = new ArrayList<>();\n        FormDefinition fd = form.getFormDefinition();\n        for (FormField f : fd.getFields()) {\n            String fieldName = f.getName();\n\n            FormInstanceEntry.Cardinality c = map(f.getCardinality());\n            String type = f.getType();\n\n            Object value = data.get(fieldName);\n            Object allowedValue = allowedValues.get(fieldName);\n\n            fields.add(new FormInstanceEntry.Field(fieldName, f.getLabel(), type, c, value, allowedValue, f.getOptions()));\n        }\n\n        String pbk = form.getProcessBusinessKey();\n        String name = fd.getName();\n        String resourcePath = FORMS_RESOURCES_PATH + \"/\" + name;\n        boolean isCustomForm = formService.exists(processKey, resourcePath);\n\n        return new FormInstanceEntry(pbk, name, fields, isCustomForm, yield);\n    }\n\n    /**\n     * Submit form instance's data, potentially resuming a suspended process.\n     */\n    @POST\n    @Path(\"/{processInstanceId}/form/{formName}\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Submit JSON form data\", operationId = \"submitFormV1\")\n    public FormSubmitResponse submit(@PathParam(\"processInstanceId\") UUID processInstanceId,\n                                     @PathParam(\"formName\") String formName,\n                                     Map<String, Object> data) {\n\n        PartialProcessKey processKey = PartialProcessKey.from(processInstanceId);\n\n        Form form = formService.get(processKey, formName);\n        if (form == null) {\n            throw new ConcordApplicationException(\"Form \" + formName + \" not found. Process ID: \" + processInstanceId, Status.NOT_FOUND);\n        }\n\n        try {\n            data = FormUtils.convert(validatorLocale, form, data);\n        } catch (ValidationException e) {\n            Map<String, String> errors = Collections.singletonMap(e.getField().getName(), e.getMessage());\n            return new FormSubmitResponse(processInstanceId, errors);\n        }\n\n        FormSubmitResult result = formService.submit(processKey, formName, data);\n\n        Map<String, String> errors = FormUtils.mergeErrors(result.getErrors());\n        return new FormSubmitResponse(result.getProcessInstanceId(), errors);\n    }\n\n    /**\n     * Submit form instance's data, potentially resuming a suspended process.\n     * The method must have a different {@code @Path} than {@link #submit(UUID, String, Map)} to avoid\n     * conflicts in the Swagger spec/clients.\n     */\n    @POST\n//    @ApiOperation(value = \"Submit multipart form data\")\n    @Path(\"/{processInstanceId}/form/{formName}/multipart\")\n    @Consumes(MediaType.MULTIPART_FORM_DATA)\n    @Produces(MediaType.APPLICATION_JSON)\n    public FormSubmitResponse submit(@PathParam(\"processInstanceId\") UUID processInstanceId,\n                                     @PathParam(\"formName\") String formName,\n                                     MultipartInput data) {\n\n        try {\n            Map<String, Object> m = MultipartUtils.toMap(data);\n            return submit(processInstanceId, formName, m);\n        } finally {\n            data.close();\n        }\n    }\n\n    private static FormInstanceEntry.Cardinality map(FormField.Cardinality c) {\n        if (c == null) {\n            return null;\n        }\n\n        switch (c) {\n            case ANY:\n                return FormInstanceEntry.Cardinality.ANY;\n            case AT_LEAST_ONE:\n                return FormInstanceEntry.Cardinality.AT_LEAST_ONE;\n            case ONE_AND_ONLY_ONE:\n                return FormInstanceEntry.Cardinality.ONE_AND_ONLY_ONE;\n            case ONE_OR_NONE:\n                return FormInstanceEntry.Cardinality.ONE_OR_NONE;\n            default:\n                throw new IllegalArgumentException(\"Unsupported cardinality type: \" + c);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/form/FormResourceV2.java",
    "content": "package com.walmartlabs.concord.server.process.form;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.forms.Form;\nimport com.walmartlabs.concord.forms.FormField;\nimport com.walmartlabs.concord.forms.FormUtils;\nimport com.walmartlabs.concord.server.MultipartUtils;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport org.jboss.resteasy.plugins.providers.multipart.MultipartInput;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.core.Response.Status;\nimport java.io.Serializable;\nimport java.util.*;\n\npublic class FormResourceV2 {\n\n    private static final String FORMS_RESOURCES_PATH = \"forms\";\n\n    private final FormServiceV2 formService;\n\n    @Inject\n    public FormResourceV2(FormServiceV2 formService) {\n        this.formService = formService;\n    }\n\n    /**\n     * Return the current state of a form instance.\n     */\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    public FormInstanceEntry get(UUID processInstanceId, String formName) {\n\n        PartialProcessKey processKey = PartialProcessKey.from(processInstanceId);\n\n        Form form = formService.get(processKey, formName);\n        if (form == null) {\n            throw new ConcordApplicationException(\"Form \" + formName + \" not found. Process ID: \" + processKey, Status.NOT_FOUND);\n        }\n\n        List<FormInstanceEntry.Field> fields = new ArrayList<>();\n        for (FormField f : form.fields()) {\n            String fieldName = f.name();\n\n            FormInstanceEntry.Cardinality c = map(f.cardinality());\n            String type = f.type();\n\n            Serializable value = f.defaultValue();\n            Serializable allowedValue = f.allowedValue();\n            Map options = f.options();\n\n            fields.add(new FormInstanceEntry.Field(fieldName, f.label(), type, c, value, allowedValue, options));\n        }\n\n        String name = form.name();\n        boolean yield = form.options().isYield();\n        String resourcePath = FORMS_RESOURCES_PATH + \"/\" + name;\n        boolean isCustomForm = formService.exists(processKey, resourcePath);\n\n        return new FormInstanceEntry(processInstanceId.toString(), name, fields, isCustomForm, yield);\n    }\n\n    /**\n     * Submit form instance's data, potentially resuming a suspended process.\n     */\n    public FormSubmitResponse submit(UUID processInstanceId, String formName, Map<String, Object> data) {\n        PartialProcessKey processKey = PartialProcessKey.from(processInstanceId);\n\n        try {\n            FormSubmitResult result = formService.submit(processKey, formName, data);\n\n            Map<String, String> errors = com.walmartlabs.concord.server.process.form.FormUtils.mergeErrors(result.getErrors());\n            return new FormSubmitResponse(result.getProcessInstanceId(), errors);\n        } catch (FormUtils.ValidationException e) {\n            Map<String, String> errors = Collections.singletonMap(e.getField().name(), e.getMessage());\n            return new FormSubmitResponse(processInstanceId, errors);\n        }\n    }\n\n    /**\n     * Submit form instance's data, potentially resuming a suspended process.\n     * The method must have a different {@code @Path} than {@link #submit(UUID, String, Map)} to avoid\n     * conflicts in the Swagger spec/clients.\n     */\n    public FormSubmitResponse submit(UUID processInstanceId, String formName, MultipartInput data) {\n\n        Map<String, Object> m = MultipartUtils.toMap(data);\n        return submit(processInstanceId, formName, m);\n    }\n\n    public List<FormListEntry> list(UUID processInstanceId) {\n        PartialProcessKey processKey = PartialProcessKey.from(processInstanceId);\n\n        try {\n            return formService.list(processKey);\n        } catch (Exception e) {\n            throw new ConcordApplicationException(\"Error while retrieving a list of forms: \" + processKey, e);\n        }\n    }\n\n    private static FormInstanceEntry.Cardinality map(FormField.Cardinality c) {\n        if (c == null) {\n            return null;\n        }\n\n        switch (c) {\n            case ANY:\n                return FormInstanceEntry.Cardinality.ANY;\n            case AT_LEAST_ONE:\n                return FormInstanceEntry.Cardinality.AT_LEAST_ONE;\n            case ONE_AND_ONLY_ONE:\n                return FormInstanceEntry.Cardinality.ONE_AND_ONLY_ONE;\n            case ONE_OR_NONE:\n                return FormInstanceEntry.Cardinality.ONE_OR_NONE;\n            default:\n                throw new IllegalArgumentException(\"Unsupported cardinality type: \" + c);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/form/FormServiceV1.java",
    "content": "package com.walmartlabs.concord.server.process.form;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.common.form.ConcordFormValidator;\nimport com.walmartlabs.concord.forms.ValidationError;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.PayloadManager;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.ProcessManager;\nimport com.walmartlabs.concord.server.process.state.ProcessStateManager;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKeyCache;\nimport com.walmartlabs.concord.server.user.UserInfoProvider.UserInfo;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport io.takari.bpm.api.ExecutionException;\nimport io.takari.bpm.form.DefaultFormService;\nimport io.takari.bpm.form.DefaultFormService.ResumeHandler;\nimport io.takari.bpm.form.Form;\nimport io.takari.bpm.form.FormValidator;\nimport io.takari.bpm.form.FormValidatorLocale;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.ObjectInputStream;\nimport java.util.*;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.process.state.ProcessStateManager.path;\n\npublic class FormServiceV1 {\n\n    public static final String FORMS_RESOURCES_PATH = \"forms\";\n    public static final String INTERNAL_RUN_AS_KEY = \"_runAs\";\n\n    private final PayloadManager payloadManager;\n    private final ProcessStateManager stateManager;\n    private final UserManager userManager;\n    private final FormAccessManager formAccessManager;\n    private final ProcessManager processManager;\n    private final ProcessKeyCache processKeyCache;\n\n    @Inject\n    public FormServiceV1(PayloadManager payloadManager,\n                         ProcessStateManager stateManager,\n                         UserManager userManager,\n                         FormAccessManager formAccessManager,\n                         ProcessManager processManager,\n                         ProcessKeyCache processKeyCache) {\n\n        this.payloadManager = payloadManager;\n        this.stateManager = stateManager;\n        this.userManager = userManager;\n        this.formAccessManager = formAccessManager;\n        this.processManager = processManager;\n        this.processKeyCache = processKeyCache;\n    }\n\n    public Form get(PartialProcessKey partialProcessKey, String formName) {\n        ProcessKey processKey = processKeyCache.assertKey(partialProcessKey.getInstanceId());\n        return get(processKey, formName);\n    }\n\n    public Form get(ProcessKey processKey, String formName) {\n        return formAccessManager.assertFormAccess(processKey, formName);\n    }\n\n    private static Optional<Form> deserialize(InputStream data) {\n        try (ObjectInputStream in = new ObjectInputStream(data)) {\n            return Optional.ofNullable((Form) in.readObject());\n        } catch (IOException | ClassNotFoundException e) {\n            throw new RuntimeException(\"Error while deserializing a form\", e);\n        }\n    }\n\n    public List<FormListEntry> list(PartialProcessKey partialProcessKey) {\n        ProcessKey processKey = processKeyCache.assertKey(partialProcessKey.getInstanceId());\n        return list(processKey);\n    }\n\n    public List<FormListEntry> list(ProcessKey processKey) {\n        String resource = path(Constants.Files.JOB_ATTACHMENTS_DIR_NAME,\n                Constants.Files.JOB_STATE_DIR_NAME,\n                Constants.Files.JOB_FORMS_DIR_NAME);\n\n        List<Form> forms = stateManager.forEach(processKey, resource, FormServiceV1::deserialize);\n        return forms.stream().map(f -> {\n            String name = f.getFormDefinition().getName();\n\n            String s = FORMS_RESOURCES_PATH + \"/\" + f.getFormDefinition().getName();\n            boolean branding = stateManager.exists(processKey, s);\n\n            Map<String, Object> opts = f.getOptions();\n            boolean yield = MapUtils.getBoolean(opts, \"yield\", false);\n            Map<String, Object> runAs = MapUtils.getMap(opts, Constants.Forms.RUN_AS_KEY, null);\n\n            return new FormListEntry(name, branding, yield, runAs);\n        }).collect(Collectors.toList());\n    }\n\n    public String nextFormId(ProcessKey processKey) {\n        String resource = path(Constants.Files.JOB_ATTACHMENTS_DIR_NAME,\n                Constants.Files.JOB_STATE_DIR_NAME,\n                Constants.Files.JOB_FORMS_DIR_NAME);\n\n        Function<String, Optional<String>> getId = s -> {\n            int i = s.lastIndexOf(\"/\");\n            if (i < 0 || i + 1 >= s.length()) {\n                return Optional.empty();\n            }\n            return Optional.of(s.substring(i + 1));\n        };\n\n        // TODO this probably should be replaced with ProcessStateManager#findFirst\n        Optional<String> o = stateManager.findPath(processKey, resource,\n                files -> files.findFirst().flatMap(getId));\n\n        return o.orElse(null);\n    }\n\n    public FormSubmitResult submit(PartialProcessKey partialProcessKey, String formName, Map<String, Object> data) {\n        ProcessKey processKey = processKeyCache.assertKey(partialProcessKey.getInstanceId());\n        return submit(processKey, formName, data);\n    }\n\n    public FormSubmitResult submit(ProcessKey processKey, String formName, Map<String, Object> data) {\n        Form form = get(processKey, formName);\n        if (form == null) {\n            throw new ProcessException(processKey, \"Form not found: \" + formName);\n        }\n\n        ResumeHandler resumeHandler = (f, args) -> {\n            String resource = path(Constants.Files.JOB_ATTACHMENTS_DIR_NAME,\n                    Constants.Files.JOB_STATE_DIR_NAME,\n                    Constants.Files.JOB_FORMS_DIR_NAME,\n                    formName);\n\n            stateManager.deleteFile(processKey, resource);\n\n            @SuppressWarnings(\"unchecked\")\n            Map<String, Object> clearedData = (Map<String, Object>) args.get(f.getFormDefinition().getName());\n\n            args.put(f.getFormDefinition().getName(), clearedData);\n\n            // TODO refactor into the process manager\n            Map<String, Object> m = new HashMap<>();\n            m.put(Constants.Request.ARGUMENTS_KEY, args);\n            if (data != null) {\n                m.put(Constants.Files.FORM_FILES, data.remove(Constants.Files.FORM_FILES));\n            }\n\n            Map<String, Object> opts = f.getOptions();\n            Object runAs = opts != null ? opts.get(Constants.Forms.RUN_AS_KEY) : null;\n            if (runAs != null) {\n                m.put(INTERNAL_RUN_AS_KEY, runAs);\n            }\n\n            resume(processKey, f.getEventName(), m);\n        };\n\n        Map<String, Object> merged = merge(form, data);\n\n        // optionally save the user who submitted the form\n        boolean saveSubmittedBy = MapUtils.getBoolean(form.getOptions(), Constants.Forms.SAVE_SUBMITTED_BY_KEY, false);\n        if (saveSubmittedBy) {\n            UserInfo i = userManager.getCurrentUserInfo();\n            merged.put(Constants.Forms.SUBMITTED_BY_KEY, i);\n        }\n\n        try {\n            FormValidator validator = createFormValidator(processKey, formName);\n            return toResult(processKey, form, DefaultFormService.submit(resumeHandler, validator, form, merged));\n        } catch (ExecutionException e) {\n            throw new ProcessException(processKey, \"Form submit error: \" + e.getMessage(), e);\n        }\n    }\n\n    public boolean exists(PartialProcessKey processKey, String path) {\n        return stateManager.exists(processKey, path);\n    }\n\n    private static FormSubmitResult toResult(PartialProcessKey processKey, Form f, io.takari.bpm.form.FormSubmitResult r) {\n        return new FormSubmitResult(processKey.getInstanceId(), f.getFormDefinition().getName(), convert(r.getErrors()));\n    }\n\n    private static List<ValidationError> convert(List<io.takari.bpm.form.FormSubmitResult.ValidationError> errors) {\n        if (errors == null) {\n            return null;\n        }\n\n        List<ValidationError> result = new ArrayList<>();\n        for (io.takari.bpm.form.FormSubmitResult.ValidationError e : errors) {\n            result.add(ValidationError.of(e.getFieldName(), e.getError()));\n        }\n        return result;\n    }\n\n    private static Map<String, Object> merge(Form form, Map<String, Object> data) {\n        Map<String, Object> a = FormUtils.values(form);\n\n        // overwrite the collected values with the submitted data\n        Map<String, Object> c = new HashMap<>(data != null ? data : Collections.emptyMap());\n        ConfigurationUtils.merge(a, c);\n\n        return a;\n    }\n\n    private void resume(ProcessKey processKey, String eventName, Map<String, Object> req) throws ExecutionException {\n        Payload payload;\n        try {\n            payload = payloadManager.createResumePayload(processKey, eventName, req);\n        } catch (IOException e) {\n            throw new ExecutionException(\"Error while creating a payload for: \" + processKey, e);\n        }\n\n        processManager.resume(payload);\n    }\n\n    private FormValidator createFormValidator(ProcessKey processKey, String formName) {\n        FormValidatorLocale locale = new ExternalFileFormValidatorLocale(processKey, formName, stateManager);\n        return new ConcordFormValidator(locale);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/form/FormServiceV2.java",
    "content": "package com.walmartlabs.concord.server.process.form;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.forms.*;\nimport com.walmartlabs.concord.forms.FormUtils;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.PayloadManager;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.ProcessManager;\nimport com.walmartlabs.concord.server.process.state.ProcessStateManager;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKeyCache;\nimport com.walmartlabs.concord.server.user.UserInfoProvider.UserInfo;\nimport com.walmartlabs.concord.server.user.UserManager;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.util.*;\n\npublic class FormServiceV2 {\n\n    public static final String FORMS_RESOURCES_PATH = \"forms\";\n    public static final String INTERNAL_RUN_AS_KEY = \"_runAs\";\n\n    private final PayloadManager payloadManager;\n    private final ProcessStateManager stateManager;\n    private final UserManager userManager;\n    private final FormAccessManager formAccessManager;\n    private final ProcessManager processManager;\n    private final FormManager formManager;\n    private final ProcessKeyCache processKeyCache;\n\n    @Inject\n    public FormServiceV2(PayloadManager payloadManager,\n                         ProcessStateManager stateManager,\n                         UserManager userManager,\n                         FormAccessManager formAccessManager,\n                         ProcessManager processManager,\n                         FormManager formManager,\n                         ProcessKeyCache processKeyCache) {\n\n        this.payloadManager = payloadManager;\n        this.stateManager = stateManager;\n        this.userManager = userManager;\n        this.formAccessManager = formAccessManager;\n        this.processManager = processManager;\n        this.formManager = formManager;\n        this.processKeyCache = processKeyCache;\n    }\n\n    public Form get(PartialProcessKey partialProcessKey, String formName) {\n        ProcessKey processKey = processKeyCache.assertKey(partialProcessKey.getInstanceId());\n        return get(processKey, formName);\n    }\n\n    public Form get(ProcessKey processKey, String formName) {\n        Form form = formManager.get(processKey, formName);\n        if (form == null) {\n            return null;\n        }\n\n        formAccessManager.assertFormAccess(formName, form.options().runAs());\n\n        return form;\n    }\n\n    private Form assertForm(ProcessKey processKey, String formName) {\n        Form form = get(processKey, formName);\n        if (form == null) {\n            throw new ProcessException(processKey, \"Form not found: \" + formName);\n        }\n\n        return form;\n    }\n\n    public List<FormListEntry> list(PartialProcessKey partialProcessKey) {\n        ProcessKey processKey = processKeyCache.assertKey(partialProcessKey.getInstanceId());\n        return list(processKey);\n    }\n\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    public List<FormListEntry> list(ProcessKey processKey) {\n        List<Form> forms = formManager.list(processKey);\n        List<FormListEntry> result = new ArrayList<>();\n        for (Form f : forms) {\n            String name = f.name();\n            String s = FORMS_RESOURCES_PATH + \"/\" + f.name();\n            boolean branding = stateManager.exists(processKey, s);\n            Map runAs = f.options().runAs();\n\n            result.add(new FormListEntry(name, branding, f.options().isYield(), runAs));\n        }\n        return result;\n    }\n\n    public Map<String, Object> convertData(ProcessKey processKey, String formName, Map<String, Object> data) throws FormUtils.ValidationException {\n        Form form = assertForm(processKey, formName);\n        FormValidatorLocale locale = new ExternalFileFormValidatorLocaleV2(processKey, formName, stateManager);\n\n        return new LinkedHashMap<>(FormUtils.convert(locale, form, data));\n    }\n\n    public FormSubmitResult submit(PartialProcessKey partialProcessKey, String formName, Map<String, Object> data) throws FormUtils.ValidationException {\n        ProcessKey processKey = processKeyCache.assertKey(partialProcessKey.getInstanceId());\n\n        data = convertData(processKey, formName, data);\n\n        return submit(processKey, formName, data);\n    }\n\n    public FormSubmitResult submit(ProcessKey processKey, String formName, Map<String, Object> data) throws FormUtils.ValidationException {\n        Form form = assertForm(processKey, formName);\n        FormValidatorLocale locale = new ExternalFileFormValidatorLocaleV2(processKey, formName, stateManager);\n\n        // optionally save the user who submitted the form\n        boolean saveSubmittedBy = form.options().saveSubmittedBy();\n        if (saveSubmittedBy) {\n            UserInfo i = userManager.getCurrentUserInfo();\n            data.put(Constants.Forms.SUBMITTED_BY_KEY, i);\n        }\n\n        try {\n            FormValidator validator = new DefaultFormValidator(locale);\n            List<ValidationError> errors = validator.validate(form, data);\n            if (errors != null && !errors.isEmpty()) {\n                return new FormSubmitResult(processKey.getInstanceId(), form.name(), errors);\n            }\n\n            // the new form's values will be available under the form's name key\n            Map<String, Object> args = new LinkedHashMap<>();\n            args.put(form.name(), new LinkedHashMap<>(data));\n\n            // TODO refactor into the process manager\n            Map<String, Object> m = new HashMap<>();\n            m.put(Constants.Request.ARGUMENTS_KEY, args);\n            m.put(Constants.Files.FORM_FILES, data.remove(Constants.Files.FORM_FILES));\n\n            Object runAs = form.options().runAs();\n            if (runAs != null) {\n                m.put(INTERNAL_RUN_AS_KEY, runAs);\n            }\n\n            resume(processKey, form.eventName(), formName, m);\n\n            return new FormSubmitResult(processKey.getInstanceId(), form.name(), null);\n        } catch (Exception e) {\n            throw new ProcessException(processKey, \"Form submit error: \" + e.getMessage(), e);\n        }\n    }\n\n    public boolean exists(PartialProcessKey processKey, String path) {\n        return stateManager.exists(processKey, path);\n    }\n\n    public String nextFormId(ProcessKey processKey) {\n        return formManager.nextFormId(processKey);\n    }\n\n    private void resume(ProcessKey processKey, String eventName, String formName, Map<String, Object> req) {\n        Payload payload;\n        try {\n            payload = payloadManager.createResumePayload(processKey, eventName, req);\n        } catch (IOException e) {\n            throw new RuntimeException(\"Error while creating a payload for: \" + processKey, e);\n        }\n\n        // remove the form when the process resumes\n        Path workDir = payload.getHeader(Payload.WORKSPACE_DIR);\n        payload = payload.putHeader(Payload.RESUME_HOOKS, Collections.singletonList(() -> formManager.delete(workDir, formName)));\n\n        processManager.resume(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/form/FormSubmitResponse.java",
    "content": "package com.walmartlabs.concord.server.process.form;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.io.Serializable;\nimport java.util.Map;\nimport java.util.UUID;\n\n@JsonInclude(Include.NON_NULL)\npublic class FormSubmitResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok;\n    private final UUID processInstanceId;\n    private final Map<String, String> errors;\n\n    @JsonCreator\n    public FormSubmitResponse(@JsonProperty(\"processInstanceId\") UUID processInstanceId,\n                              @JsonProperty(\"errors\") Map<String, String> errors) {\n\n        this.ok = errors == null || errors.isEmpty();\n        this.processInstanceId = processInstanceId;\n        this.errors = errors;\n    }\n\n    public UUID getProcessInstanceId() {\n        return processInstanceId;\n    }\n\n    public Map<String, String> getErrors() {\n        return errors;\n    }\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    @Override\n    public String toString() {\n        return \"FormSubmitResponse{\" +\n                \"ok=\" + ok +\n                \", processInstanceId=\" + processInstanceId +\n                \", errors=\" + errors +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/form/FormSubmitResult.java",
    "content": "package com.walmartlabs.concord.server.process.form;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.forms.ValidationError;\n\nimport java.io.Serializable;\nimport java.util.List;\nimport java.util.UUID;\n\npublic final class FormSubmitResult implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final UUID processInstanceId;\n    private final String formName;\n    private final List<ValidationError> errors;\n\n    public FormSubmitResult(UUID processInstanceId, String formName, List<ValidationError> errors) {\n        this.processInstanceId = processInstanceId;\n        this.formName = formName;\n        this.errors = errors;\n    }\n\n    public UUID getProcessInstanceId() {\n        return processInstanceId;\n    }\n\n    public String getFormName() {\n        return formName;\n    }\n\n    public List<ValidationError> getErrors() {\n        return errors;\n    }\n\n    public boolean isValid() {\n        return errors == null || errors.isEmpty();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/form/FormUtils.java",
    "content": "package com.walmartlabs.concord.server.process.form;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.form.ConcordFormFields;\nimport com.walmartlabs.concord.common.form.ConcordFormValidatorLocale;\nimport com.walmartlabs.concord.forms.ValidationError;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport io.takari.bpm.form.Form;\nimport io.takari.bpm.model.form.DefaultFormFields;\nimport io.takari.bpm.model.form.FormDefinition;\nimport io.takari.bpm.model.form.FormField;\n\nimport javax.ws.rs.core.MultivaluedMap;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.time.ZoneId;\nimport java.time.ZonedDateTime;\nimport java.time.format.DateTimeFormatter;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.common.form.ConcordFormFields.FieldOptions.READ_ONLY;\nimport static java.nio.file.StandardCopyOption.REPLACE_EXISTING;\n\npublic final class FormUtils {\n\n    /**\n     * Date/time format used to pass date and dateTime fields between the Server and the process.\n     */\n    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(\"yyyy-MM-dd'T'HH:mm:ss.SSSX\", Locale.US);\n\n    /**\n     * All date/time values are converted into the default time zone.\n     */\n    private static final ZoneId DEFAULT_TIME_ZONE = ZoneId.of(\"UTC\");\n\n    public static Map<String, String> mergeErrors(List<ValidationError> errors) {\n        if (errors == null || errors.isEmpty()) {\n            return null;\n        }\n\n        // TODO merge multiple errors\n        Map<String, String> m = new HashMap<>();\n        for (ValidationError e : errors) {\n            m.put(e.fieldName(), e.error());\n        }\n        return m;\n    }\n\n    public static Map<String, Object> convert(MultivaluedMap<String, String> data) {\n        Map<String, Object> m = new HashMap<>();\n        if (data != null) {\n            data.forEach((k, v) -> {\n                if (v == null) {\n                    return;\n                }\n\n                int size = v.size();\n                if (size == 0) {\n                    return;\n                }\n\n                if (size == 1) {\n                    m.put(k, v.get(0));\n                } else {\n                    m.put(k, v);\n                }\n            });\n        }\n        return m;\n    }\n\n    // TODO this probably should be a part of the bpm engine's FormService\n    public static Map<String, Object> convert(ConcordFormValidatorLocale locale, Form form, Map<String, Object> m) throws ValidationException {\n        FormDefinition fd = form.getFormDefinition();\n\n        Map<String, String> tmpFiles = new HashMap<>();\n        Map<String, Object> m2 = new HashMap<>();\n        m2.put(Constants.Files.FORM_FILES, tmpFiles);\n\n        Map<String, Object> defaultData = FormUtils.values(form);\n\n        for (FormField f : fd.getFields()) {\n            String k = f.getName();\n\n            Object v = Boolean.TRUE.equals(f.getOption(READ_ONLY)) ? defaultData.get(k) : m.get(k);\n\n            boolean isOptional = f.getCardinality() == FormField.Cardinality.ONE_OR_NONE || f.getCardinality() == FormField.Cardinality.ANY;\n            if (v == null && !isOptional) {\n                Map<String, Object> allowedValues = form.getAllowedValues();\n                if (allowedValues == null) {\n                    allowedValues = Collections.emptyMap();\n                }\n\n                v = allowedValueAsFieldValue(allowedValues.get(f.getName()));\n            }\n\n            /*\n             * Use cardinality as an indicator to convert single value (coming as a string) into an array\n             * for the scenario when only one value was provided by the user\n             */\n            if (v instanceof String && (f.getCardinality() == FormField.Cardinality.ANY || f.getCardinality() == FormField.Cardinality.AT_LEAST_ONE)) {\n                v = Collections.singletonList(v);\n            }\n\n            v = convert(locale, fd.getName(), f, null, v);\n\n            if (v == null && !isOptional) {\n                continue;\n            }\n\n            if (v != null && f.getType().equals(ConcordFormFields.FileField.TYPE)) {\n                String wsFileName = \"_form_files/\" + form.getFormDefinition().getName() + \"/\" + f.getName();\n                tmpFiles.put(wsFileName, (String) v);\n                m2.put(k, wsFileName);\n            } else {\n                m2.put(k, v);\n            }\n        }\n        return m2;\n    }\n\n    public static Map<String, Object> values(Form form) {\n        String formName = form.getFormDefinition().getName();\n\n        Map<String, Object> env = form.getEnv();\n        if (env == null) {\n            env = Collections.emptyMap();\n        }\n\n        Map<String, Object> formState = MapUtils.getMap(env, formName, Collections.emptyMap());\n        Map<String, Object> extraValues = extraValues(form);\n\n        // merge the initial form values and the \"extra\" values, provided\n        // in the \"values\" option of the form\n        Map<String, Object> a = new HashMap<>(formState);\n        Map<String, Object> b = new HashMap<>(extraValues);\n        ConfigurationUtils.merge(a, b);\n        return a;\n    }\n\n    public static Map<String, Object> extraValues(Form form) {\n        return MapUtils.getMap(form.getOptions(), \"values\", Collections.emptyMap());\n    }\n\n    private static Object convert(ConcordFormValidatorLocale locale, String formName, FormField f, Integer idx, Object v) throws ValidationException {\n        if (v instanceof String) {\n            String s = (String) v;\n\n            switch (f.getType()) {\n                case DefaultFormFields.StringField.TYPE: {\n                    if (s.isEmpty()) {\n                        return null;\n                    }\n                    break;\n                }\n                case DefaultFormFields.IntegerField.TYPE: {\n                    if (s.isEmpty()) {\n                        return null;\n                    }\n\n                    try {\n                        return Long.parseLong(s);\n                    } catch (NumberFormatException e) {\n                        throw new ValidationException(f, s, locale.expectedInteger(formName, f, idx, s));\n                    }\n                }\n                case DefaultFormFields.DecimalField.TYPE: {\n                    if (s.isEmpty()) {\n                        return null;\n                    }\n\n                    try {\n                        return Double.parseDouble(s);\n                    } catch (NumberFormatException e) {\n                        throw new ValidationException(f, s, locale.expectedDecimal(formName, f, idx, s));\n                    }\n                }\n                case DefaultFormFields.BooleanField.TYPE: {\n                    if (s.isEmpty()) {\n                        // default HTML checkbox will be submitted as an empty value if checked\n                        return true;\n                    }\n                    return Boolean.parseBoolean(s);\n                }\n                case ConcordFormFields.FileField.TYPE: {\n                    try {\n                        Path tmp = PathUtils.createTempFile(f.getName(), \".tmp\");\n                        Files.write(tmp, s.getBytes(), StandardOpenOption.TRUNCATE_EXISTING);\n                        return tmp.toString();\n                    } catch (IOException e) {\n                        throw new ConcordApplicationException(\"Error reading file for form field '\" + f.getName() + \"'\", e);\n                    }\n                }\n                case ConcordFormFields.DateField.TYPE:\n                case ConcordFormFields.DateTimeField.TYPE: {\n                    if (s.isEmpty()) {\n                        return null;\n                    }\n\n                    // adjust the value to the default system timezone\n                    // on the process level those values are represented as java.util.Date (i.e. no TZ info retained)\n                    // so we assume all Date values are in the default system TZ (which is typically UTC)\n                    return ZonedDateTime.parse(s)\n                            .withZoneSameInstant(DEFAULT_TIME_ZONE)\n                            .format(DATE_TIME_FORMATTER);\n                }\n            }\n        } else if (v instanceof List) {\n            List<?> l = (List<?>) v;\n            if (l.isEmpty()) {\n                return null;\n            }\n\n            List<Object> ll = new ArrayList<>(l.size());\n            int i = 0;\n            for (Object o : l) {\n                ll.add(convert(locale, formName, f, i, o));\n                i++;\n            }\n            return ll;\n        } else if (v instanceof InputStream) {\n            if (f.getType().equals(ConcordFormFields.FileField.TYPE)) {\n                try (InputStream is = (InputStream) v) {\n                    Path tmp = PathUtils.createTempFile(f.getName(), \".tmp\");\n                    Files.copy(is, tmp, REPLACE_EXISTING);\n                    return tmp.toString();\n                } catch (IOException e) {\n                    throw new ConcordApplicationException(\"Error reading file for form field '\" + f.getName() + \"'\", e);\n                }\n            }\n        } else if (v == null) {\n            if (f.getType().equals(DefaultFormFields.BooleanField.TYPE)) {\n                return false;\n            }\n        }\n\n        return v;\n    }\n\n    private static Object allowedValueAsFieldValue(Object allowedValue) {\n        if (allowedValue instanceof Collection) {\n            if (((Collection<?>) allowedValue).size() == 1) {\n                return ((Collection<?>) allowedValue).iterator().next();\n            }\n\n            return null;\n        }\n\n        return allowedValue;\n    }\n\n    public static class ValidationException extends Exception {\n\n        private static final long serialVersionUID = 1L;\n\n        private final FormField field;\n        private final String input;\n        private final String message;\n\n        private ValidationException(FormField field, String input, String message) {\n            this.field = field;\n            this.input = input;\n            this.message = message;\n        }\n\n        public FormField getField() {\n            return field;\n        }\n\n        public String getInput() {\n            return input;\n        }\n\n        @Override\n        public String getMessage() {\n            return message;\n        }\n    }\n\n    private FormUtils() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/keys/AttachmentKey.java",
    "content": "package com.walmartlabs.concord.server.process.keys;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.nio.file.Path;\n\npublic final class AttachmentKey extends Key<Path> {\n\n    private static final long serialVersionUID = 1L;\n\n    private static final KeyIndex<AttachmentKey> index = new KeyIndex<>((n, type) -> new AttachmentKey(n));\n\n    public static AttachmentKey register(String name) {\n        return index.register(name, Path.class);\n    }\n\n    private AttachmentKey(String key) {\n        super(key, Path.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/keys/HeaderKey.java",
    "content": "package com.walmartlabs.concord.server.process.keys;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\npublic class HeaderKey<T> extends Key<T> {\n\n    private static final long serialVersionUID = 1L;\n\n    private static final KeyIndex<HeaderKey<?>> index = new KeyIndex<>(HeaderKey::new);\n\n    @SuppressWarnings(\"unchecked\")\n    public static <T> HeaderKey<T> register(String name, Class<T> type) {\n        return (HeaderKey<T>) index.register(name, type);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <T> HeaderKey<List<T>> registerList(String name) {\n        return (HeaderKey<List<T>>) index.register(name, Collection.class);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <T> HeaderKey<Set<T>> registerSet(String name) {\n        return (HeaderKey<Set<T>>) index.register(name, Set.class);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <K, V> HeaderKey<Map<K, V>> registerMap(String name) {\n        return (HeaderKey<Map<K, V>>) index.register(name, Map.class);\n    }\n\n    private HeaderKey(String key, Class<T> type) {\n        super(key, type);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/keys/Key.java",
    "content": "package com.walmartlabs.concord.server.process.keys;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\n\npublic abstract class Key<T> implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String name;\n    private final Class<T> type;\n\n    protected Key(String name, Class<T> type) {\n        this.name = name;\n        this.type = type;\n    }\n\n    public String name() {\n        return name;\n    }\n\n    public T cast(Object v) {\n        if (v == null) {\n            return null;\n        }\n\n        Class<?> other = v.getClass();\n        if (!type.isAssignableFrom(other)) {\n            throw new IllegalArgumentException(\"Invalid value type: expected \" + type + \", got \" + other);\n        }\n\n        return type.cast(v);\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        Key key1 = (Key) o;\n        return name.equals(key1.name);\n    }\n\n    @Override\n    public int hashCode() {\n        return name.hashCode();\n    }\n\n    @Override\n    public String toString() {\n        return getClass().getName() + \" [\" + name + \"]\";\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/keys/KeyIndex.java",
    "content": "package com.walmartlabs.concord.server.process.keys;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\nimport java.util.function.BiFunction;\n\n/**\n * @param <K> type of a key\n */\npublic final class KeyIndex<K extends Key<?>> {\n\n    private final Map<String, K> keys = new HashMap<>();\n    private final BiFunction<String, Class<?>, K> keyMaker;\n    private final Lock mutex = new ReentrantLock();\n\n    public KeyIndex(BiFunction<String, Class<?>, K> keyMaker) {\n        this.keyMaker = keyMaker;\n    }\n\n    public K register(String name, Class<?> type) {\n        mutex.lock();\n        try {\n            if (keys.containsKey(name)) {\n                throw new IllegalStateException(\"Key '\" + name + \"' is already registered. \" +\n                        \"Check for duplicate declarations in the code\");\n            }\n\n            K k = keyMaker.apply(name, type);\n            keys.put(name, k);\n            return k;\n        } finally {\n            mutex.unlock();\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/locks/LockEntry.java",
    "content": "package com.walmartlabs.concord.server.process.locks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.server.jooq.enums.ProcessLockScope;\nimport org.immutables.value.Value;\n\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonSerialize(as = ImmutableLockEntry.class)\n@JsonDeserialize(as = ImmutableLockEntry.class)\npublic interface LockEntry {\n\n    UUID instanceId();\n\n    UUID orgId();\n\n    UUID projectId();\n\n    String name();\n\n    ProcessLockScope scope();\n\n    static ImmutableLockEntry.Builder builder() {\n        return ImmutableLockEntry.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/locks/LockResult.java",
    "content": "package com.walmartlabs.concord.server.process.locks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\n@Value.Immutable\n@JsonSerialize(as = ImmutableLockResult.class)\n@JsonDeserialize(as = ImmutableLockResult.class)\npublic interface LockResult {\n\n    LockEntry info();\n\n    boolean acquired();\n\n    static ImmutableLockResult.Builder builder() {\n        return ImmutableLockResult.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/locks/ProcessLocksDao.java",
    "content": "package com.walmartlabs.concord.server.process.locks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.jooq.enums.ProcessLockScope;\nimport com.walmartlabs.concord.server.jooq.tables.ProcessLocks;\nimport com.walmartlabs.concord.server.jooq.tables.records.ProcessLocksRecord;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.jooq.SelectConditionStep;\n\nimport javax.inject.Inject;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.PROCESS_LOCKS;\n\npublic class ProcessLocksDao extends AbstractDao {\n\n    @Inject\n    protected ProcessLocksDao(@MainDB Configuration cfg) {\n        super(cfg);\n    }\n\n    public LockEntry tryLock(ProcessKey processKey, UUID orgId, UUID projectId, ProcessLockScope scope, String lockName) {\n        while (true) {\n            boolean locked = insert(processKey, orgId, projectId, scope, lockName);\n            if (locked) {\n                return LockEntry.builder()\n                        .instanceId(processKey.getInstanceId())\n                        .orgId(orgId)\n                        .projectId(projectId)\n                        .scope(scope)\n                        .name(lockName)\n                        .build();\n            } else {\n                LockEntry e = get(orgId, projectId, scope, lockName);\n                if (e != null) {\n                    return e;\n                }\n            }\n        }\n    }\n\n    public LockEntry get(UUID orgId, UUID projectId, ProcessLockScope scope, String lockName) {\n        return txResult(tx -> get(tx, orgId, projectId, scope, lockName));\n    }\n\n    public boolean insert(ProcessKey processKey, UUID orgId, UUID projectId, ProcessLockScope scope, String lockName) {\n        return txResult(tx -> insert(tx, processKey, orgId, projectId, scope, lockName));\n    }\n\n    public void delete(UUID instanceId, UUID orgId, UUID projectId, ProcessLockScope scope, String lockName) {\n        tx(tx -> delete(tx, instanceId, orgId, projectId, scope, lockName));\n    }\n\n    private boolean insert(DSLContext tx, ProcessKey processKey, UUID orgId, UUID projectId, ProcessLockScope scope, String lockName) {\n        ProcessLocks l = PROCESS_LOCKS.as(\"l\");\n        return tx.insertInto(l, l.INSTANCE_ID, l.ORG_ID, l.PROJECT_ID, l.LOCK_SCOPE, l.LOCK_NAME)\n                .values(processKey.getInstanceId(), orgId, projectId, scope, lockName)\n                .onConflictDoNothing()\n                .execute() == 1;\n    }\n\n    private LockEntry get(DSLContext tx, UUID orgId, UUID projectId, ProcessLockScope scope, String lockName) {\n        ProcessLocks l = PROCESS_LOCKS.as(\"l\");\n        SelectConditionStep<ProcessLocksRecord> q = tx.selectFrom(l)\n                .where(l.LOCK_NAME.eq(lockName)\n                        .and(l.LOCK_SCOPE.eq(scope)));\n\n        switch (scope) {\n            case ORG:\n                q.and(l.ORG_ID.eq(orgId));\n                break;\n            case PROJECT:\n                q.and(l.PROJECT_ID.eq(projectId));\n                break;\n            default:\n                throw new IllegalArgumentException(\"unknown lock scope: \" + scope);\n        }\n\n        return q.fetchOne(r -> LockEntry.builder()\n                .instanceId(r.getInstanceId())\n                .orgId(r.getOrgId())\n                .projectId(r.getProjectId())\n                .scope(r.getLockScope())\n                .name(r.getLockName())\n                .build());\n    }\n\n    private void delete(DSLContext tx, UUID instanceId, UUID orgId, UUID projectId, ProcessLockScope scope, String lockName) {\n        ProcessLocks l = PROCESS_LOCKS.as(\"l\");\n        tx.deleteFrom(l)\n                .where(l.INSTANCE_ID.eq(instanceId)\n                        .and(l.ORG_ID.eq(orgId))\n                        .and(l.PROJECT_ID.eq(projectId))\n                        .and(l.LOCK_SCOPE.eq(scope))\n                        .and(l.LOCK_NAME.eq(lockName)))\n                .execute();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/locks/ProcessLocksResource.java",
    "content": "package com.walmartlabs.concord.server.process.locks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.jooq.enums.ProcessLockScope;\nimport com.walmartlabs.concord.server.process.ProcessEntry;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueManager;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport java.util.UUID;\n\n@Path(\"/api/v1/process\")\n@Tag(name = \"Process Locks\")\npublic class ProcessLocksResource implements Resource {\n\n    private final ProcessQueueManager processQueueManager;\n    private final ProcessLocksDao dao;\n\n    @Inject\n    public ProcessLocksResource(ProcessQueueManager processQueueManager,\n                                ProcessLocksDao dao) {\n\n        this.processQueueManager = processQueueManager;\n        this.dao = dao;\n    }\n\n    /**\n     * Acquires the lock if it is available and returns the LockResult.acquired = true.\n     * If the lock is not available then this method will return the LockResult.acquired = false.\n     */\n    @POST\n    @Path(\"/{processInstanceId}/lock/{lockName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"Try lock\")\n    public LockResult tryLock(@PathParam(\"processInstanceId\") UUID instanceId,\n                              @PathParam(\"lockName\") String lockName,\n                              @QueryParam(\"scope\") @DefaultValue(\"PROJECT\") ProcessLockScope scope) {\n\n        ProcessEntry e = assertProcess(instanceId);\n\n        LockEntry lock = dao.tryLock(new ProcessKey(e.instanceId(), e.createdAt()), e.orgId(), e.projectId(), scope, lockName);\n        boolean acquired = lock.instanceId().equals(instanceId);\n        return LockResult.builder()\n                .acquired(acquired)\n                .info(lock)\n                .build();\n    }\n\n    /**\n     * Releases the lock.\n     */\n    @POST\n    @Path(\"/{processInstanceId}/unlock/{lockName}\")\n    @WithTimer\n    @Operation(description = \"Releases the lock\")\n    public void unlock(@PathParam(\"processInstanceId\") UUID instanceId,\n                       @PathParam(\"lockName\") String lockName,\n                       @QueryParam(\"scope\") @DefaultValue(\"PROJECT\") ProcessLockScope scope) {\n\n        ProcessEntry e = assertProcess(instanceId);\n        dao.delete(e.instanceId(), e.orgId(), e.projectId(), scope, lockName);\n    }\n\n    private ProcessEntry assertProcess(UUID instanceId) {\n        PartialProcessKey processKey = PartialProcessKey.from(instanceId);\n        ProcessEntry p = processQueueManager.get(processKey);\n\n        if (p == null) {\n            throw new ConcordApplicationException(\"Process not found: \" + instanceId, Response.Status.NOT_FOUND);\n        }\n\n        if (p.orgId() == null) {\n            throw new ConcordApplicationException(\"Organization is required\", Response.Status.BAD_REQUEST);\n        }\n\n        if (p.projectId() == null) {\n            throw new ConcordApplicationException(\"Project is required\", Response.Status.BAD_REQUEST);\n        }\n\n        return p;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/locks/ProcessLocksWatchdog.java",
    "content": "package com.walmartlabs.concord.server.process.locks;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.Utils;\nimport com.walmartlabs.concord.server.jooq.tables.ProcessLocks;\nimport com.walmartlabs.concord.server.jooq.tables.ProcessQueue;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport com.walmartlabs.concord.server.sdk.ScheduledTask;\nimport org.jooq.Configuration;\nimport org.jooq.Record1;\nimport org.jooq.SelectConditionStep;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.PROCESS_LOCKS;\nimport static com.walmartlabs.concord.server.jooq.tables.ProcessQueue.PROCESS_QUEUE;\n\n/**\n * Takes care of processes dead process locks.\n * E.g. removes locks for finished processes.\n */\npublic class ProcessLocksWatchdog implements ScheduledTask {\n\n    private static final Logger log = LoggerFactory.getLogger(ProcessLocksWatchdog.class);\n\n    private final WatchdogDao dao;\n\n    @Inject\n    public ProcessLocksWatchdog(@MainDB Configuration cfg) {\n        this.dao = new WatchdogDao(cfg);\n    }\n\n    @Override\n    public String getId() {\n        return \"process-locks-watchdog\";\n    }\n\n    @Override\n    public long getIntervalInSec() {\n        // TODO cfg?\n        return 5;\n    }\n\n    @Override\n    public void performTask() {\n        int count = dao.deleteStalledLocks();\n        log.debug(\"performTask -> {} locks deleted\", count);\n    }\n\n    private static final class WatchdogDao extends AbstractDao {\n\n        private static final ProcessStatus[] FINISHED_STATUSES = {\n                ProcessStatus.FINISHED,\n                ProcessStatus.FAILED,\n                ProcessStatus.CANCELLED,\n                ProcessStatus.TIMED_OUT\n        };\n\n        public WatchdogDao(@MainDB Configuration cfg) {\n            super(cfg);\n        }\n\n        public int deleteStalledLocks() {\n            return txResult(tx -> {\n                ProcessQueue q = PROCESS_QUEUE.as(\"q\");\n                ProcessLocks l = PROCESS_LOCKS.as(\"l\");\n\n                SelectConditionStep<Record1<UUID>> finishedProcesses = tx.select(q.INSTANCE_ID)\n                        .from(q)\n                        .where(q.INSTANCE_ID.eq(l.INSTANCE_ID)\n                                .and(q.CURRENT_STATUS.in(Utils.toString(FINISHED_STATUSES))));\n\n                return tx.deleteFrom(l)\n                        .where(l.INSTANCE_ID.in(finishedProcesses))\n                        .execute();\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/logs/ProcessLogAccessManager.java",
    "content": "package com.walmartlabs.concord.server.process.logs;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.cfg.ProcessConfiguration;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.org.project.ProjectAccessManager;\nimport com.walmartlabs.concord.server.process.ProcessEntry;\nimport com.walmartlabs.concord.server.process.ProcessManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.security.sessionkey.SessionKeyPrincipal;\n\nimport javax.inject.Inject;\nimport java.util.UUID;\n\npublic class ProcessLogAccessManager {\n\n    private final ProjectAccessManager projectAccessManager;\n    private final ProcessManager processManager;\n    private final ProcessConfiguration processCfg;\n\n    @Inject\n    public ProcessLogAccessManager(ProjectAccessManager projectAccessManager,\n                                   ProcessManager processManager,\n                                   ProcessConfiguration processCfg) {\n\n        this.projectAccessManager = projectAccessManager;\n        this.processManager = processManager;\n        this.processCfg = processCfg;\n    }\n\n    public ProcessKey assertLogAccess(UUID instanceId) {\n        ProcessEntry pe = processManager.assertProcess(instanceId);\n        ProcessKey pk = new ProcessKey(pe.instanceId(), pe.createdAt());\n\n        if (!processCfg.isCheckLogPermissions()) {\n            return pk;\n        }\n\n        if (Roles.isAdmin() || Roles.isGlobalReader()) {\n            return pk;\n        }\n\n        UserPrincipal principal = UserPrincipal.assertCurrent();\n\n        UUID initiatorId = pe.initiatorId();\n        if (principal.getId().equals(initiatorId)) {\n            // process owners should be able to view the process' logs\n            return pk;\n        }\n\n        SessionKeyPrincipal s = SessionKeyPrincipal.getCurrent();\n        if (s != null && pk.partOf(s.getProcessKey())) {\n            // processes can access their own logs\n            return pk;\n        }\n\n        if (pe.projectId() != null) {\n            projectAccessManager.assertAccess(pe.projectId(), ResourceAccessLevel.WRITER, true);\n            return pk;\n        }\n\n        throw new UnauthorizedException(\"The current user (\" + principal.getUsername() + \") doesn't have \" +\n                \"the necessary permissions to view the process log: \" + instanceId);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/logs/ProcessLogManager.java",
    "content": "package com.walmartlabs.concord.server.process.logs;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.Counter;\nimport com.walmartlabs.concord.common.LogUtils;\nimport com.walmartlabs.concord.server.Listeners;\nimport com.walmartlabs.concord.server.process.LogSegment;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.Range;\nimport com.walmartlabs.concord.server.sdk.log.ProcessLogEntry;\nimport com.walmartlabs.concord.server.sdk.metrics.InjectCounter;\nimport org.jooq.DSLContext;\n\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\nimport java.util.List;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.common.LogUtils.LogLevel;\nimport static com.walmartlabs.concord.server.process.logs.ProcessLogsDao.ProcessLog;\n\npublic class ProcessLogManager {\n\n    private static final long SYSTEM_SEGMENT_ID = 0;\n    private static final String SYSTEM_SEGMENT_NAME = \"system\";\n    private final ProcessLogsDao logsDao;\n    private final Listeners listeners;\n\n    @InjectCounter\n    private final Counter logBytesAppended;\n\n    @Inject\n    public ProcessLogManager(ProcessLogsDao logsDao,\n                             Listeners listeners,\n                             Counter logBytesAppended) {\n\n        this.logsDao = logsDao;\n        this.listeners = listeners;\n        this.logBytesAppended = logBytesAppended;\n    }\n\n    public void info(ProcessKey processKey, String log, Object... args) {\n        log(processKey, LogLevel.INFO, log, args);\n    }\n\n    public void warn(ProcessKey processKey, String log, Object... args) {\n        log(processKey, LogLevel.WARN, log, args);\n    }\n\n    public void error(ProcessKey processKey, String log, Object... args) {\n        log(processKey, LogLevel.ERROR, log, args);\n    }\n\n    public void log(ProcessKey processKey, String msg) {\n        log(processKey, msg.getBytes());\n    }\n\n    public int log(ProcessKey processKey, byte[] msg) {\n        return log(processKey, SYSTEM_SEGMENT_ID, msg);\n    }\n\n    public List<LogSegment> listSegments(ProcessKey processKey, int limit, int offset) {\n        return logsDao.listSegments(processKey, limit, offset);\n    }\n\n    public void createSystemSegment(DSLContext tx, ProcessKey processKey) {\n        logsDao.createSegment(tx, SYSTEM_SEGMENT_ID, processKey, null, SYSTEM_SEGMENT_NAME, null);\n    }\n\n    public long createSegment(ProcessKey processKey, UUID correlationId, String name, OffsetDateTime createdAt) {\n        if (SYSTEM_SEGMENT_NAME.equals(name)) {\n            return SYSTEM_SEGMENT_ID;\n        }\n        return logsDao.createSegment(processKey, correlationId, name, createdAt, LogSegment.Status.RUNNING.name());\n    }\n\n    public void updateSegment(ProcessKey processKey, long segmentId, LogSegment.Status status, Integer warnings, Integer errors) {\n        logsDao.updateSegment(processKey, segmentId, status, warnings, errors);\n    }\n\n    public ProcessLog segmentData(ProcessKey processKey, long segmentId, Integer start, Integer end) {\n        return logsDao.segmentData(processKey, segmentId, start, end);\n    }\n\n    public ProcessLog get(ProcessKey processKey, Integer start, Integer end) {\n        return logsDao.data(processKey, start, end);\n    }\n\n    public int log(ProcessKey processKey, long segmentId, byte[] msg) {\n        Range range = logsDao.append(processKey, segmentId, msg);\n        logBytesAppended.inc(msg.length);\n\n        ProcessLogEntry entry = ProcessLogEntry.builder()\n                .processKey(processKey)\n                .range(range)\n                .msg(msg)\n                .build();\n\n        listeners.onProcessLogAppend(entry);\n\n        return range.upper();\n    }\n\n    private void log(ProcessKey processKey, LogLevel level, String msg, Object... args) {\n        log(processKey, LogUtils.formatMessage(level, msg, args));\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/logs/ProcessLogsDao.java",
    "content": "package com.walmartlabs.concord.server.process.logs;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.db.PgIntRange;\nimport com.walmartlabs.concord.server.jooq.tables.records.ProcessLogDataRecord;\nimport com.walmartlabs.concord.server.jooq.tables.records.ProcessLogSegmentsRecord;\nimport com.walmartlabs.concord.server.process.LogSegment;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.Range;\nimport org.jooq.*;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\nimport java.time.OffsetDateTime;\nimport java.util.List;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.db.PgUtils.upperRange;\nimport static com.walmartlabs.concord.server.jooq.Routines.*;\nimport static com.walmartlabs.concord.server.jooq.Tables.PROCESS_LOG_DATA;\nimport static com.walmartlabs.concord.server.jooq.Tables.PROCESS_LOG_SEGMENTS;\nimport static org.jooq.impl.DSL.*;\n\npublic class ProcessLogsDao extends AbstractDao {\n\n    @Inject\n    public ProcessLogsDao(@MainDB Configuration cfg) {\n        super(cfg);\n    }\n\n    /**\n     * Appends a chunk to the process log. Automatically calculates the chunk's range.\n     *\n     * @return the new chunk range.\n     */\n    public Range append(ProcessKey processKey, long segmentId, byte[] data) {\n        UUID instanceId = processKey.getInstanceId();\n        OffsetDateTime createdAt = processKey.getCreatedAt();\n\n        ProcessLogDataRecord r = txResult(tx -> tx.insertInto(PROCESS_LOG_DATA)\n                .columns(PROCESS_LOG_DATA.INSTANCE_ID,\n                        PROCESS_LOG_DATA.INSTANCE_CREATED_AT,\n                        PROCESS_LOG_DATA.SEGMENT_ID,\n                        PROCESS_LOG_DATA.SEGMENT_RANGE,\n                        PROCESS_LOG_DATA.LOG_RANGE,\n                        PROCESS_LOG_DATA.CHUNK_DATA)\n                .values(value(instanceId),\n                        value(createdAt),\n                        value(segmentId),\n                        processLogDataSegmentNextRange2(instanceId, createdAt, segmentId, data.length),\n                        processLogDataNextRange2(instanceId, createdAt, data.length),\n                        value(data))\n                .returning(PROCESS_LOG_DATA.LOG_RANGE)\n                .fetchOne());\n\n        return PgIntRange.parse(r.getLogRange().toString());\n    }\n\n    public long createSegment(ProcessKey processKey, UUID correlationId, String name, OffsetDateTime createdAt, String status) {\n        return txResult(tx -> tx.insertInto(PROCESS_LOG_SEGMENTS)\n                .columns(PROCESS_LOG_SEGMENTS.INSTANCE_ID,\n                        PROCESS_LOG_SEGMENTS.INSTANCE_CREATED_AT,\n                        PROCESS_LOG_SEGMENTS.CORRELATION_ID,\n                        PROCESS_LOG_SEGMENTS.SEGMENT_NAME,\n                        PROCESS_LOG_SEGMENTS.SEGMENT_TS,\n                        PROCESS_LOG_SEGMENTS.SEGMENT_STATUS)\n                .values(value(processKey.getInstanceId()),\n                        value(processKey.getCreatedAt()),\n                        value(correlationId), value(name),\n                        createdAt != null ? value(createdAt) : currentOffsetDateTime(),\n                        value(status))\n                .returning(PROCESS_LOG_SEGMENTS.SEGMENT_ID)\n                .fetchOne()\n                .getSegmentId());\n    }\n\n    public void createSegment(DSLContext tx, long segmentId, ProcessKey processKey, UUID correlationId, String name, String status) {\n        tx.insertInto(PROCESS_LOG_SEGMENTS)\n                .columns(PROCESS_LOG_SEGMENTS.SEGMENT_ID, PROCESS_LOG_SEGMENTS.INSTANCE_ID, PROCESS_LOG_SEGMENTS.INSTANCE_CREATED_AT, PROCESS_LOG_SEGMENTS.CORRELATION_ID, PROCESS_LOG_SEGMENTS.SEGMENT_NAME, PROCESS_LOG_SEGMENTS.SEGMENT_TS, PROCESS_LOG_SEGMENTS.SEGMENT_STATUS)\n                .values(value(segmentId), value(processKey.getInstanceId()), value(processKey.getCreatedAt()), value(correlationId), value(name), currentOffsetDateTime(), value(status))\n                .execute();\n    }\n\n    public void updateSegment(ProcessKey processKey, long segmentId, LogSegment.Status status, Integer warnings, Integer errors) {\n        tx(tx -> updateSegment(tx, processKey, segmentId, status, warnings, errors));\n    }\n\n    private void updateSegment(DSLContext tx, ProcessKey processKey, long segmentId, LogSegment.Status status, Integer warnings, Integer errors) {\n        UpdateQuery<ProcessLogSegmentsRecord> q = tx.updateQuery(PROCESS_LOG_SEGMENTS);\n\n        if (status != null) {\n            q.addValue(PROCESS_LOG_SEGMENTS.SEGMENT_STATUS, status.name());\n            q.addValue(PROCESS_LOG_SEGMENTS.STATUS_UPDATED_AT, currentOffsetDateTime());\n        }\n\n        if (warnings != null) {\n            q.addValue(PROCESS_LOG_SEGMENTS.SEGMENT_WARN, warnings);\n        }\n\n        if (errors != null) {\n            q.addValue(PROCESS_LOG_SEGMENTS.SEGMENT_ERRORS, errors);\n        }\n\n        q.addConditions(\n                PROCESS_LOG_SEGMENTS.INSTANCE_ID.eq(processKey.getInstanceId())\n                        .and(PROCESS_LOG_SEGMENTS.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt())\n                                .and(PROCESS_LOG_SEGMENTS.SEGMENT_ID.eq(segmentId))));\n        q.execute();\n    }\n\n    public List<LogSegment> listSegments(ProcessKey processKey, int limit, int offset) {\n        UUID instanceId = processKey.getInstanceId();\n        OffsetDateTime createdAt = processKey.getCreatedAt();\n\n        SelectSeekStep2<Record8<Long, UUID, String, OffsetDateTime, String, OffsetDateTime, Integer, Integer>, OffsetDateTime, Long> q = dsl()\n                .select(PROCESS_LOG_SEGMENTS.SEGMENT_ID,\n                        PROCESS_LOG_SEGMENTS.CORRELATION_ID,\n                        PROCESS_LOG_SEGMENTS.SEGMENT_NAME,\n                        PROCESS_LOG_SEGMENTS.SEGMENT_TS,\n                        PROCESS_LOG_SEGMENTS.SEGMENT_STATUS,\n                        PROCESS_LOG_SEGMENTS.STATUS_UPDATED_AT,\n                        PROCESS_LOG_SEGMENTS.SEGMENT_WARN,\n                        PROCESS_LOG_SEGMENTS.SEGMENT_ERRORS)\n                .from(PROCESS_LOG_SEGMENTS)\n                .where(PROCESS_LOG_SEGMENTS.INSTANCE_ID.eq(instanceId)\n                        .and(PROCESS_LOG_SEGMENTS.INSTANCE_CREATED_AT.eq(createdAt)))\n                .orderBy(PROCESS_LOG_SEGMENTS.SEGMENT_TS, PROCESS_LOG_SEGMENTS.SEGMENT_ID);\n\n        if (limit >= 0) {\n            q.limit(limit);\n        }\n\n        return q.offset(offset)\n                .fetch(ProcessLogsDao::toSegment);\n    }\n\n    public ProcessLog segmentData(ProcessKey processKey, long segmentId, Integer start, Integer end) {\n        UUID instanceId = processKey.getInstanceId();\n        OffsetDateTime createdAt = processKey.getCreatedAt();\n\n        DSLContext tx = dsl();\n\n        List<ProcessLogChunk> chunks = getSegmentChunks(tx, processKey, segmentId, start, end);\n\n        Field<Integer> upperRange = max(upperRange(PROCESS_LOG_DATA.SEGMENT_RANGE));\n        int size = tx.select(upperRange)\n                .from(PROCESS_LOG_DATA)\n                .where(PROCESS_LOG_DATA.INSTANCE_ID.eq(instanceId)\n                        .and(PROCESS_LOG_DATA.INSTANCE_CREATED_AT.eq(createdAt))\n                        .and(PROCESS_LOG_DATA.SEGMENT_ID.eq(segmentId)))\n                .fetchOptional(upperRange)\n                .orElse(0);\n\n        return new ProcessLog(size, chunks);\n    }\n\n    public ProcessLog data(ProcessKey processKey, Integer start, Integer end) {\n        UUID instanceId = processKey.getInstanceId();\n        OffsetDateTime createdAt = processKey.getCreatedAt();\n\n        DSLContext tx = dsl();\n\n        List<ProcessLogChunk> chunks = getDataChunks(tx, processKey, start, end);\n\n        Field<Integer> upperRange = max(upperRange(PROCESS_LOG_DATA.LOG_RANGE));\n        int size = tx.select(upperRange)\n                .from(PROCESS_LOG_DATA)\n                .where(PROCESS_LOG_DATA.INSTANCE_ID.eq(instanceId)\n                        .and(PROCESS_LOG_DATA.INSTANCE_CREATED_AT.eq(createdAt)))\n                .fetchOptional(upperRange)\n                .orElse(0);\n\n        return new ProcessLog(size, chunks);\n    }\n\n    private List<ProcessLogChunk> getSegmentChunks(DSLContext tx, ProcessKey processKey, long segmentId, Integer start, Integer end) {\n        UUID instanceId = processKey.getInstanceId();\n        OffsetDateTime createdAt = processKey.getCreatedAt();\n\n        String lowerBoundExpr = \"lower(\" + PROCESS_LOG_DATA.SEGMENT_RANGE + \")\";\n\n        if (start == null && end == null) {\n            // entire file\n            return tx.select(field(lowerBoundExpr), PROCESS_LOG_DATA.CHUNK_DATA)\n                    .from(PROCESS_LOG_DATA)\n                    .where(PROCESS_LOG_DATA.INSTANCE_ID.eq(instanceId)\n                            .and(PROCESS_LOG_DATA.INSTANCE_CREATED_AT.eq(createdAt))\n                            .and(PROCESS_LOG_DATA.SEGMENT_ID.eq(segmentId)))\n                    .orderBy(PROCESS_LOG_DATA.SEGMENT_RANGE)\n                    .fetch(ProcessLogsDao::toChunk);\n\n        } else if (start != null) {\n            // ranges && [start, end)\n            String rangeExpr = PROCESS_LOG_DATA.SEGMENT_RANGE.getName() + \" && int4range(?, ?)\";\n            return tx.select(field(lowerBoundExpr), PROCESS_LOG_DATA.CHUNK_DATA)\n                    .from(PROCESS_LOG_DATA)\n                    .where(PROCESS_LOG_DATA.INSTANCE_ID.eq(instanceId)\n                            .and(PROCESS_LOG_DATA.INSTANCE_CREATED_AT.eq(createdAt))\n                            .and(PROCESS_LOG_DATA.SEGMENT_ID.eq(segmentId))\n                            .and(rangeExpr, start, end))\n                    .orderBy(PROCESS_LOG_DATA.SEGMENT_RANGE)\n                    .fetch(ProcessLogsDao::toChunk);\n\n        } else {\n            // ranges && [upper_bound - end, upper_bound)\n            String rangeExpr = PROCESS_LOG_DATA.SEGMENT_RANGE.getName() + \" && (select range from x)\";\n            return tx.with(\"x\").as(select(processLogDataSegmentLastNBytes2(instanceId, createdAt, segmentId, end).as(\"range\")))\n                    .select(field(lowerBoundExpr), PROCESS_LOG_DATA.CHUNK_DATA)\n                    .from(PROCESS_LOG_DATA)\n                    .where(PROCESS_LOG_DATA.INSTANCE_ID.eq(instanceId)\n                            .and(PROCESS_LOG_DATA.INSTANCE_CREATED_AT.eq(createdAt))\n                            .and(PROCESS_LOG_DATA.SEGMENT_ID.eq(segmentId))\n                            .and(rangeExpr, instanceId, end))\n                    .orderBy(PROCESS_LOG_DATA.SEGMENT_RANGE)\n                    .fetch(ProcessLogsDao::toChunk);\n        }\n    }\n\n    private List<ProcessLogChunk> getDataChunks(DSLContext tx, ProcessKey processKey, Integer start, Integer end) {\n        UUID instanceId = processKey.getInstanceId();\n        OffsetDateTime createdAt = processKey.getCreatedAt();\n\n        String lowerBoundExpr = \"lower(\" + PROCESS_LOG_DATA.LOG_RANGE + \")\";\n\n        if (start == null && end == null) {\n            // entire file\n            return tx.select(field(lowerBoundExpr), PROCESS_LOG_DATA.CHUNK_DATA)\n                    .from(PROCESS_LOG_DATA)\n                    .where(PROCESS_LOG_DATA.INSTANCE_ID.eq(instanceId)\n                            .and(PROCESS_LOG_DATA.INSTANCE_CREATED_AT.eq(createdAt)))\n                    .orderBy(PROCESS_LOG_DATA.LOG_RANGE)\n                    .fetch(ProcessLogsDao::toChunk);\n\n        } else if (start != null) {\n            // ranges && [start, end)\n            String rangeExpr = PROCESS_LOG_DATA.LOG_RANGE.getName() + \" && int4range(?, ?)\";\n            return tx.select(field(lowerBoundExpr), PROCESS_LOG_DATA.CHUNK_DATA)\n                    .from(PROCESS_LOG_DATA)\n                    .where(PROCESS_LOG_DATA.INSTANCE_ID.eq(instanceId)\n                            .and(PROCESS_LOG_DATA.INSTANCE_CREATED_AT.eq(createdAt))\n                            .and(rangeExpr, start, end))\n                    .orderBy(PROCESS_LOG_DATA.LOG_RANGE)\n                    .fetch(ProcessLogsDao::toChunk);\n\n        } else {\n            // ranges && [upper_bound - end, upper_bound)\n            String rangeExpr = PROCESS_LOG_DATA.LOG_RANGE.getName() + \" && (select range from x)\";\n            return tx.with(\"x\").as(select(processLogDataLastNBytes2(instanceId, createdAt, end).as(\"range\")))\n                    .select(field(lowerBoundExpr), PROCESS_LOG_DATA.CHUNK_DATA)\n                    .from(PROCESS_LOG_DATA)\n                    .where(PROCESS_LOG_DATA.INSTANCE_ID.eq(instanceId)\n                            .and(PROCESS_LOG_DATA.INSTANCE_CREATED_AT.eq(createdAt))\n                            .and(rangeExpr, instanceId, end))\n                    .orderBy(PROCESS_LOG_DATA.LOG_RANGE)\n                    .fetch(ProcessLogsDao::toChunk);\n        }\n    }\n\n    private static ProcessLogChunk toChunk(Record2<Object, byte[]> r) {\n        return new ProcessLogChunk((Integer) r.value1(), r.value2());\n    }\n\n    private static LogSegment toSegment(Record8<Long, UUID, String, OffsetDateTime, String, OffsetDateTime, Integer, Integer> r) {\n        String status = r.get(PROCESS_LOG_SEGMENTS.SEGMENT_STATUS);\n        return LogSegment.builder()\n                .id(r.get(PROCESS_LOG_SEGMENTS.SEGMENT_ID))\n                .correlationId(r.get(PROCESS_LOG_SEGMENTS.CORRELATION_ID))\n                .name(r.get(PROCESS_LOG_SEGMENTS.SEGMENT_NAME))\n                .createdAt(r.get(PROCESS_LOG_SEGMENTS.SEGMENT_TS))\n                .status(status != null ? LogSegment.Status.valueOf(status) : null)\n                .statusUpdatedAt(r.get(PROCESS_LOG_SEGMENTS.STATUS_UPDATED_AT))\n                .warnings(r.get(PROCESS_LOG_SEGMENTS.SEGMENT_WARN))\n                .errors(r.get(PROCESS_LOG_SEGMENTS.SEGMENT_ERRORS))\n                .build();\n    }\n\n    public static final class ProcessLogChunk implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        private final int start;\n        private final byte[] data;\n\n        public ProcessLogChunk(int start, byte[] data) { // NOSONAR\n            this.start = start;\n            this.data = data;\n        }\n\n        public int getStart() {\n            return start;\n        }\n\n        public byte[] getData() {\n            return data;\n        }\n    }\n\n    public static final class ProcessLog implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        private final int size;\n        private final List<ProcessLogChunk> chunks;\n\n        public ProcessLog(int size, List<ProcessLogChunk> chunks) {\n            this.size = size;\n            this.chunks = chunks;\n        }\n\n        public int getSize() {\n            return size;\n        }\n\n        public List<ProcessLogChunk> getChunks() {\n            return chunks;\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/EnqueueProcessPipeline.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Injector;\nimport com.walmartlabs.concord.server.process.pipelines.processors.*;\n\nimport javax.inject.Inject;\nimport java.util.List;\n\n/**\n * Handles NEW \"regular\" processes. Puts the processes into the ENQUEUED status.\n * Forks are processed by {@link ForkPipeline}.\n */\npublic class EnqueueProcessPipeline extends Pipeline {\n\n    private final ExceptionProcessor exceptionProcessor;\n    private final FinalizerProcessor finalizerProcessor;\n\n    @Inject\n    public EnqueueProcessPipeline(Injector injector,\n                                  CustomEnqueueProcessors customProcessors) {\n        super(List.of(\n                injector.getInstance(LoggingMDCProcessor.class),\n                injector.getInstance(PayloadRestoreProcessor.class),\n                injector.getInstance(RestoredPayloadValidationProcessor.class),\n                injector.getInstance(RunAsCurrentProcessUserProcessor.class),\n                injector.getInstance(PolicyExportProcessor.class),\n                injector.getInstance(WorkspaceArchiveProcessor.class),\n                injector.getInstance(RepositoryProcessor.class),\n                injector.getInstance(RepositoryInfoUpdateProcessor.class),\n                customProcessors.handleAttachments(),\n                injector.getInstance(AttachmentStoringProcessor.class),\n                injector.getInstance(ProcessDefinitionProcessor.class),\n                injector.getInstance(SessionTokenProcessor.class),\n                injector.getInstance(ConfigurationProcessor.class),\n                customProcessors.handleConfiguration(),\n                injector.getInstance(AssertOutVariablesProcessor.class),\n                injector.getInstance(ExclusiveGroupProcessor.class),\n                injector.getInstance(EntryPointProcessor.class),\n                injector.getInstance(TagsExtractingProcessor.class),\n                injector.getInstance(TemplateFilesProcessor.class),\n                injector.getInstance(TemplateScriptProcessor.class),\n                injector.getInstance(DependenciesProcessor.class),\n                injector.getInstance(InitiatorUserInfoProcessor.class),\n                injector.getInstance(OutVariablesSettingProcessor.class),\n                injector.getInstance(ResumeEventsProcessor.class),\n                injector.getInstance(ConfigurationStoringProcessor.class),\n                injector.getInstance(PolicyProcessor.class),\n                customProcessors.handleState(),\n                injector.getInstance(StateImportingProcessor.class),\n                injector.getInstance(ProcessHandlersProcessor.class),\n                injector.getInstance(EffectiveProcessDefinitionProcessor.class),\n                injector.getInstance(SecuritySubjectProcessor.class),\n                injector.getInstance(EnqueueingProcessor.class)\n        ));\n\n        this.exceptionProcessor = injector.getInstance(FailProcessor.class);\n        this.finalizerProcessor = injector.getInstance(CleanupProcessor.class);\n    }\n\n    @Override\n    protected ExceptionProcessor getExceptionProcessor() {\n        return exceptionProcessor;\n    }\n\n    @Override\n    protected FinalizerProcessor getFinalizerProcessor() {\n        return finalizerProcessor;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/ForkPipeline.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Injector;\nimport com.walmartlabs.concord.server.process.pipelines.processors.*;\n\nimport javax.inject.Inject;\nimport java.util.List;\n\n/**\n * Handles processes forked from a parent (including \"handler\"\n * processes such as \"onCancel\", \"onFailure\", etc).\n */\npublic class ForkPipeline extends Pipeline {\n\n    private final ExceptionProcessor exceptionProcessor;\n    private final FinalizerProcessor finalizerProcessor;\n\n    @Inject\n    public ForkPipeline(Injector injector) {\n        super(List.of(\n                injector.getInstance(LoggingMDCProcessor.class),\n                injector.getInstance(PolicyExportProcessor.class),\n                injector.getInstance(InitialQueueEntryProcessor.class),\n                injector.getInstance(ForkPolicyProcessor.class),\n                injector.getInstance(ForkCleanupProcessor.class),\n                injector.getInstance(SessionTokenProcessor.class),\n                injector.getInstance(ConfigurationProcessor.class),\n                injector.getInstance(TagsExtractingProcessor.class),\n                injector.getInstance(InitiatorUserInfoProcessor.class),\n                injector.getInstance(OutVariablesSettingProcessor.class),\n                injector.getInstance(ConfigurationStoringProcessor.class),\n                injector.getInstance(StateImportingProcessor.class),\n                injector.getInstance(ForkHandlersProcessor.class),\n                injector.getInstance(ForkRepositoryInfoProcessor.class),\n                injector.getInstance(RepositoryInfoUpdateProcessor.class),\n                injector.getInstance(ForkRuntimeProcessor.class),\n                injector.getInstance(EnqueueingProcessor.class)\n        ));\n\n        this.exceptionProcessor = injector.getInstance(FailProcessor.class);\n        this.finalizerProcessor = injector.getInstance(CleanupProcessor.class);\n    }\n\n    @Override\n    protected ExceptionProcessor getExceptionProcessor() {\n        return exceptionProcessor;\n    }\n\n    @Override\n    protected FinalizerProcessor getFinalizerProcessor() {\n        return finalizerProcessor;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/NewProcessPipeline.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Injector;\nimport com.walmartlabs.concord.server.process.pipelines.processors.*;\n\nimport javax.inject.Inject;\nimport java.util.List;\n\n/**\n * Initial handling of processes. Creates queue entries in the NEW status.\n */\npublic class NewProcessPipeline extends Pipeline {\n\n    private final ExceptionProcessor exceptionProcessor;\n    private final FinalizerProcessor finalizerProcessor;\n\n    @Inject\n    public NewProcessPipeline(Injector injector) {\n        super(List.of(\n                injector.getInstance(LoggingMDCProcessor.class),\n                injector.getInstance(AuthorizationProcessor.class),\n                injector.getInstance(AssertWorkspaceArchiveProcessor.class),\n                injector.getInstance(RawPayloadPolicyProcessor.class),\n                injector.getInstance(AssertOutVariablesProcessor.class),\n                injector.getInstance(RequestParametersProcessor.class),\n                injector.getInstance(PayloadStoreProcessor.class),\n                injector.getInstance(SecuritySubjectProcessor.class),\n                injector.getInstance(NewQueueEntryProcessor.class)\n        ));\n\n        this.exceptionProcessor = injector.getInstance(FailProcessor.class);\n        this.finalizerProcessor = injector.getInstance(CleanupProcessor.class);\n    }\n\n    @Override\n    protected ExceptionProcessor getExceptionProcessor() {\n        return exceptionProcessor;\n    }\n\n    @Override\n    protected FinalizerProcessor getFinalizerProcessor() {\n        return finalizerProcessor;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/ResumePipeline.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Injector;\nimport com.walmartlabs.concord.server.process.pipelines.processors.*;\n\nimport javax.inject.Inject;\nimport java.util.List;\n\n/**\n * Resumes the execution of previously suspended processes.\n */\npublic class ResumePipeline extends Pipeline {\n\n    private final ExceptionProcessor exceptionProcessor;\n    private final FinalizerProcessor finalizerProcessor;\n\n    @Inject\n    public ResumePipeline(Injector injector) {\n        super(List.of(\n                injector.getInstance(LoggingMDCProcessor.class),\n                injector.getInstance(ChangeUserProcessor.class),\n                injector.getInstance(ResumingProcessor.class),\n                injector.getInstance(ResumingHooksProcessor.class),\n                injector.getInstance(ResumeMarkerStoringProcessor.class),\n                injector.getInstance(FormFilesStoringProcessor.class),\n                injector.getInstance(ResumeConfigurationProcessor.class),\n                injector.getInstance(ResumeEventsProcessor.class),\n                injector.getInstance(ClearStartAtProcessor.class),\n                injector.getInstance(ConfigurationStoringProcessor.class),\n                injector.getInstance(PolicyExportProcessor.class),\n                injector.getInstance(StateImportingProcessor.class),\n                injector.getInstance(ResumeProcessor.class)\n        ));\n\n        this.exceptionProcessor = injector.getInstance(FailProcessor.class);\n        this.finalizerProcessor = injector.getInstance(CleanupProcessor.class);\n    }\n\n    @Override\n    protected ExceptionProcessor getExceptionProcessor() {\n        return exceptionProcessor;\n    }\n\n    @Override\n    protected FinalizerProcessor getFinalizerProcessor() {\n        return finalizerProcessor;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/AssertOutVariablesProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.jooq.enums.OutVariablesMode;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.org.project.ProjectAccessManager;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.org.project.ProjectEntry;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.user.UserManager;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.core.Response.Status;\nimport java.util.Set;\nimport java.util.UUID;\n\npublic class AssertOutVariablesProcessor implements PayloadProcessor {\n\n    private final ProjectDao projectDao;\n    private final ProjectAccessManager projectAccessManager;\n    private final UserManager userManager;\n\n    @Inject\n    public AssertOutVariablesProcessor(ProjectDao projectDao,\n                                       ProjectAccessManager projectAccessManager,\n                                       UserManager userManager) {\n\n        this.projectDao = projectDao;\n        this.projectAccessManager = projectAccessManager;\n        this.userManager = userManager;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        Set<String> outVars = payload.getHeader(Payload.OUT_EXPRESSIONS);\n\n        if (outVars == null || outVars.isEmpty()) {\n            return chain.process(payload);\n        }\n\n        assertOutVariables(payload);\n\n        return chain.process(payload);\n    }\n\n    private void assertOutVariables(Payload payload) {\n        if (!isOutVariablesAllowed(payload)) {\n            throw new ProcessException(payload.getProcessKey(),\n                    \"The project is not accepting custom out variables: \" + payload.getHeader(Payload.PROJECT_ID),\n                    Status.BAD_REQUEST);\n        }\n    }\n\n    private boolean isOutVariablesAllowed(Payload payload) {\n        UUID projectId = payload.getHeader(Payload.PROJECT_ID);\n        if (projectId == null) {\n            return true;\n        }\n\n        ProjectEntry p = projectDao.get(projectId);\n        if (p == null) {\n            throw new ProcessException(payload.getProcessKey(), \"Project not found: \" + projectId);\n        }\n\n        OutVariablesMode m = p.getOutVariablesMode();\n        switch (m) {\n            case DISABLED: {\n                return false;\n            }\n            case OWNERS: {\n                return projectAccessManager.hasAccess(p, ResourceAccessLevel.OWNER, false);\n            }\n            case TEAM_MEMBERS: {\n                return projectAccessManager.isTeamMember(p.getId());\n            }\n            case ORG_MEMBERS: {\n                return userManager.isInOrganization(p.getOrgId());\n            }\n            case EVERYONE: {\n                return true;\n            }\n            default:\n                throw new IllegalArgumentException(\"Unsupported out variables mode: \" + m);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/AssertWorkspaceArchiveProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.process.loader.StandardRuntimeTypes;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.jooq.enums.RawPayloadMode;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.org.project.ProjectAccessManager;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.org.project.ProjectEntry;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.user.UserManager;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.core.Response.Status;\nimport java.util.Arrays;\nimport java.util.HashSet;\nimport java.util.Set;\nimport java.util.UUID;\n\npublic class AssertWorkspaceArchiveProcessor implements PayloadProcessor {\n\n    private static final Set<String> PROJECT_ROOT_FILE_NAMES = new HashSet<>(Arrays.asList(StandardRuntimeTypes.PROJECT_ROOT_FILE_NAMES));\n\n    private final ProjectDao projectDao;\n    private final ProjectAccessManager projectAccessManager;\n    private final UserManager userManager;\n\n    @Inject\n    public AssertWorkspaceArchiveProcessor(ProjectDao projectDao,\n                                           ProjectAccessManager projectAccessManager,\n                                           UserManager userManager) {\n\n        this.projectDao = projectDao;\n        this.projectAccessManager = projectAccessManager;\n        this.userManager = userManager;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        if (!hasRawPayloadAttachment(payload)) {\n            return chain.process(payload);\n        }\n\n        assertAcceptsRawPayload(payload);\n\n        return chain.process(payload);\n    }\n\n    private void assertAcceptsRawPayload(Payload payload) {\n        UUID projectId = payload.getHeader(Payload.PROJECT_ID);\n\n        if (!isRawPayloadAllowed(payload)) {\n            throw new ProcessException(payload.getProcessKey(), \"The project is not accepting raw payloads: \" + projectId,\n                    Status.BAD_REQUEST);\n        }\n    }\n\n    private boolean isRawPayloadAllowed(Payload payload) {\n        UUID projectId = payload.getHeader(Payload.PROJECT_ID);\n        if (projectId == null) {\n            return true;\n        }\n\n        ProjectEntry p = projectDao.get(projectId);\n        if (p == null) {\n            throw new ProcessException(payload.getProcessKey(), \"Project not found: \" + projectId);\n        }\n\n        RawPayloadMode m = p.getRawPayloadMode();\n        switch (m) {\n            case DISABLED: {\n                return false;\n            }\n            case OWNERS: {\n                return projectAccessManager.hasAccess(p, ResourceAccessLevel.OWNER, false);\n            }\n            case TEAM_MEMBERS: {\n                return projectAccessManager.isTeamMember(p.getId());\n            }\n            case ORG_MEMBERS: {\n                return userManager.isInOrganization(p.getOrgId());\n            }\n            case EVERYONE: {\n                return true;\n            }\n            default:\n                throw new IllegalArgumentException(\"Unsupported raw payload mode: \" + m);\n        }\n    }\n\n    /**\n     * @return {@code true} if the payload contains any file we consider\n     * a \"raw payload\" attachment: workspace archives, concord.yml, etc.\n     */\n    private boolean hasRawPayloadAttachment(Payload payload) {\n        if (payload.getAttachment(Payload.WORKSPACE_ARCHIVE) != null) {\n            return true;\n        }\n\n        return payload.getAttachments().keySet().stream()\n                .anyMatch(k -> PROJECT_ROOT_FILE_NAMES.contains(k) ||\n                        (k.startsWith(Constants.Files.PROJECT_FILES_DIR_NAME + \"/\") && k.endsWith(\".yml\")));\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/AttachmentStoringProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.collect.ImmutableSet;\nimport com.walmartlabs.concord.server.jooq.enums.RawPayloadMode;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.org.project.ProjectEntry;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.regex.Pattern;\n\npublic class AttachmentStoringProcessor implements PayloadProcessor {\n\n    /**\n     * Attachments used by the server. They should not be added to the workspace.\n     */\n    private static final Set<String> SYSTEM_ATTACHMENT_NAMES = ImmutableSet.of(Payload.WORKSPACE_ARCHIVE.name(),\n            ConfigurationProcessor.REQUEST_ATTACHMENT_KEY.name());\n\n    private static final Pattern RAW_PAYLOAD_PATTERN = Pattern.compile(\"^(concord|flows)[/.](.*)(yml|yaml)$\");\n\n    private final ProjectDao projectDao;\n\n    @Inject\n    public AttachmentStoringProcessor(ProjectDao projectDao) {\n        this.projectDao = projectDao;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        Map<String, Path> m = payload.getAttachments();\n        if (m == null || m.isEmpty()) {\n            return chain.process(payload);\n        }\n\n        Path workspace = payload.getHeader(Payload.WORKSPACE_DIR);\n        for (Map.Entry<String, Path> entry : m.entrySet()) {\n            String name = entry.getKey();\n            if (SYSTEM_ATTACHMENT_NAMES.contains(name)) {\n                continue;\n            }\n\n            UUID projectId = payload.getHeader(Payload.PROJECT_ID);\n            if (hasRawPayload(name) && !canAcceptRawPayload(projectId)) {\n                throw new ProcessException(payload.getProcessKey(), \"Project is not accepting flows in attachments (\" + name + \"). Check the \\\"Allow payload archives\\\" setting.\");\n            }\n\n            Path src = entry.getValue();\n            Path dst = workspace.resolve(name);\n\n            try {\n                Files.createDirectories(dst.getParent());\n                Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING);\n                payload = payload.removeAttachment(name);\n            } catch (IOException e) {\n                throw new ProcessException(payload.getProcessKey(), \"Error while copying an attachment: \" + src, e);\n            }\n        }\n\n        return chain.process(payload);\n    }\n\n    private boolean hasRawPayload(String name) {\n        return RAW_PAYLOAD_PATTERN.matcher(name).matches();\n    }\n\n    private boolean canAcceptRawPayload(UUID projectId) {\n        if (projectId == null) {\n            return true;\n        }\n\n        ProjectEntry project = projectDao.get(projectId);\n        return project.getRawPayloadMode() != RawPayloadMode.DISABLED;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/AuthorizationProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.org.project.ProjectAccessManager;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\n\nimport javax.inject.Inject;\n\nimport static java.util.Objects.requireNonNull;\n\npublic class AuthorizationProcessor implements PayloadProcessor {\n\n    private final ProjectDao projectDao;\n    private final ProjectAccessManager accessManager;\n\n    @Inject\n    public AuthorizationProcessor(ProjectDao projectDao, ProjectAccessManager accessManager) {\n        this.projectDao = requireNonNull(projectDao);\n        this.accessManager = requireNonNull(accessManager);\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        var projectId = payload.getHeader(Payload.PROJECT_ID);\n        if (projectId == null) {\n            return chain.process(payload);\n        }\n\n        var projectEntry = projectDao.get(projectId);\n        if (projectEntry == null) {\n            throw new ValidationErrorsException(\"Project not found: \" + projectId);\n        }\n\n        switch (projectEntry.getProcessExecMode()) {\n            case DISABLED -> throw new ConcordApplicationException(\"Process execution is disabled for the project.\");\n            case READERS -> accessManager.assertAccess(projectId, ResourceAccessLevel.READER, false);\n            case WRITERS -> accessManager.assertAccess(projectId, ResourceAccessLevel.WRITER, false);\n        }\n\n        return chain.process(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/Chain.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.Payload;\n\npublic class Chain {\n\n    private final PayloadProcessor[] processors;\n    private final int current;\n\n    public Chain(PayloadProcessor... processors) {\n        this(processors, 0);\n    }\n\n    private Chain(PayloadProcessor[] processors, int current) { // NOSONAR\n        this.processors = processors;\n        this.current = current;\n    }\n\n    public Payload process(Payload payload) {\n        if (current >= processors.length) {\n            return payload;\n        }\n\n        PayloadProcessor p = processors[current];\n        Chain next = new Chain(processors, current + 1);\n\n        return p.process(next, payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/ChangeUserProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessSecurityContext;\nimport com.walmartlabs.concord.server.process.form.FormServiceV1;\n\nimport javax.inject.Inject;\nimport java.util.Map;\n\n/**\n * Responsible for saving the current user's security subject when\n * the process resumes with {@code runAs.keep} form option.\n */\npublic class ChangeUserProcessor implements PayloadProcessor {\n\n    private final ProcessSecurityContext securityContext;\n    private final CurrentUserInfoProcessor currentUserInfoProcessor;\n\n    @Inject\n    public ChangeUserProcessor(ProcessSecurityContext securityContext,\n                               CurrentUserInfoProcessor currentUserInfoProcessor) {\n        this.securityContext = securityContext;\n        this.currentUserInfoProcessor = currentUserInfoProcessor;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION);\n        if (cfg == null) {\n            return chain.process(payload);\n        }\n\n        Map<String, Object> runAsParams = MapUtils.getMap(cfg, FormServiceV1.INTERNAL_RUN_AS_KEY, null);\n        if (runAsParams == null) {\n            return chain.process(payload);\n        }\n\n        boolean keep = MapUtils.getBoolean(runAsParams, Constants.Forms.RUN_AS_KEEP_KEY, false);\n        if (keep) {\n            securityContext.storeCurrentSubject(payload.getProcessKey());\n            return currentUserInfoProcessor.process(chain, payload);\n        }\n\n        return chain.process(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/CleanupProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport org.slf4j.MDC;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\npublic class CleanupProcessor implements FinalizerProcessor {\n\n    private final ProcessLogManager logManager;\n\n    @Inject\n    public CleanupProcessor(ProcessLogManager logManager) {\n        this.logManager = logManager;\n    }\n\n    @Override\n    public void process(Payload payload) {\n        // cleanup the MDC\n        MDC.clear();\n\n        delete(payload.getProcessKey(), payload.getHeader(Payload.WORKSPACE_DIR));\n        delete(payload.getProcessKey(), payload.getHeader(Payload.BASE_DIR));\n    }\n\n    private void delete(ProcessKey processKey, Path p) {\n        if (p == null || !Files.exists(p)) {\n            return;\n        }\n\n        try {\n            PathUtils.deleteRecursively(p);\n        } catch (IOException e) {\n            logManager.warn(processKey, \"Unable to delete the working directory: {}\", p, e);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/ClearStartAtProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueDao;\n\nimport javax.inject.Inject;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * Removes the \"startAt\" parameter from the process configuration and\n * the process queue entry. Necessary when resuming a process - it doesn't make\n * any sense to re-use the same \"startAt\" value which might be in the past\n * already.\n */\npublic class ClearStartAtProcessor implements PayloadProcessor {\n\n    private final ProcessQueueDao queueDao;\n\n    @Inject\n    public ClearStartAtProcessor(ProcessQueueDao queueDao) {\n        this.queueDao = queueDao;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        payload = clearConfiguration(payload);\n        queueDao.clearStartAt(payload.getProcessKey());\n        return chain.process(payload);\n    }\n\n    private static Payload clearConfiguration(Payload payload) {\n        Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION);\n        if (cfg == null) {\n            return payload;\n        }\n\n        Map<String, Object> m = new HashMap<>(cfg);\n        m.remove(Constants.Request.START_AT_KEY);\n        return payload.putHeader(Payload.CONFIGURATION, m);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/ConfigurationProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.runtime.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.model.EffectiveConfiguration;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.org.OrganizationDao;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.org.project.ProjectEntry;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.ProcessKind;\nimport com.walmartlabs.concord.server.process.keys.AttachmentKey;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.process.pipelines.processors.cfg.ProcessConfigurationUtils;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.core.Response.Status;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.*;\n\n/**\n * Responsible for preparing the process' {@code configuration} object.\n */\npublic class ConfigurationProcessor implements PayloadProcessor {\n\n    public static final AttachmentKey REQUEST_ATTACHMENT_KEY = AttachmentKey.register(\"request\");\n\n    private final ProjectDao projectDao;\n    private final OrganizationDao orgDao;\n    private final ProcessLogManager logManager;\n\n    @Inject\n    public ConfigurationProcessor(ProjectDao projectDao, OrganizationDao orgDao, ProcessLogManager logManager) {\n        this.projectDao = projectDao;\n        this.orgDao = orgDao;\n        this.logManager = logManager;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        // default configuration from policy\n        Map<String, Object> policyDefCfg = getDefaultCfgFromPolicy(payload);\n\n        // org configuration\n        Map<String, Object> orgCfg = getOrgCfg(payload);\n\n        ProjectEntry projectEntry = getProject(payload);\n\n        // project configuration\n        Map<String, Object> projectCfg = getProjectCfg(projectEntry);\n\n        // _main.json file in the workspace\n        Map<String, Object> workspaceCfg = getWorkspaceCfg(payload);\n\n        // attached to the request JSON file\n        Map<String, Object> attachedCfg = getAttachedCfg(payload);\n\n        // existing configuration values from the payload\n        Map<String, Object> payloadCfg = payload.getHeader(Payload.CONFIGURATION, Collections.emptyMap());\n\n        // determine the active profile names\n        List<String> activeProfiles = ProcessConfigurationUtils.getActiveProfiles(payloadCfg, attachedCfg, workspaceCfg, projectCfg, orgCfg);\n        payload = payload.putHeader(Payload.ACTIVE_PROFILES, activeProfiles);\n\n        // consolidate out variables\n        Set<String> outVars = new HashSet<>(ProcessConfigurationUtils.getOutVars(payloadCfg, attachedCfg, workspaceCfg));\n        payload = payload.putHeader(Payload.OUT_EXPRESSIONS, outVars);\n\n        // merged profile data\n        Map<String, Object> profileCfg = getProfileCfg(payload, activeProfiles);\n\n        // automatically provided variables\n        Map<String, Object> providedCfg = ProcessConfigurationUtils.prepareProvidedCfg(payload, projectEntry);\n\n        // configuration from the policy\n        Map<String, Object> policyCfg = getPolicyCfg(payload);\n\n        // create the resulting configuration\n        Map<String, Object> m = ConfigurationUtils.deepMerge(policyDefCfg, orgCfg, projectCfg, profileCfg, workspaceCfg, attachedCfg, payloadCfg, providedCfg, policyCfg);\n        m.put(Constants.Request.ACTIVE_PROFILES_KEY, activeProfiles);\n\n        // handle handlers special params\n        processHandlersConfiguration(payload, m);\n\n        processDryRunModeConfiguration(payload, m);\n\n        payload = payload.putHeader(Payload.CONFIGURATION, m);\n\n        return chain.process(payload);\n    }\n\n    private ProjectEntry getProject(Payload payload) {\n        UUID projectId = payload.getHeader(Payload.PROJECT_ID);\n        if (projectId == null) {\n            return null;\n        }\n\n        ProjectEntry e = projectDao.get(projectId);\n        if (e == null) {\n            throw new ProcessException(payload.getProcessKey(), \"Project not found: \" + projectId, Status.BAD_REQUEST);\n        }\n\n        return e;\n    }\n\n    private Map<String, Object> getProjectCfg(ProjectEntry projectEntry) {\n        if (projectEntry == null) {\n            return Collections.emptyMap();\n        }\n\n        Map<String, Object> m = projectEntry.getCfg();\n        return m != null ? m : Collections.emptyMap();\n    }\n\n    private Map<String, Object> getOrgCfg(Payload payload) {\n        UUID orgId = payload.getHeader(Payload.ORGANIZATION_ID);\n        if (orgId == null) {\n            return Collections.emptyMap();\n        }\n\n        Map<String, Object> m = orgDao.getConfiguration(orgId);\n        return m != null ? m : Collections.emptyMap();\n    }\n\n    private Map<String, Object> getDefaultCfgFromPolicy(Payload payload) {\n        PolicyEngine policy = payload.getHeader(Payload.POLICY);\n\n        if (policy == null) {\n            return Collections.emptyMap();\n        }\n\n        return policy.getDefaultProcessCfgPolicy().get();\n    }\n\n    private Map<String, Object> getPolicyCfg(Payload payload) {\n        PolicyEngine policy = payload.getHeader(Payload.POLICY);\n\n        if (policy == null) {\n            return Collections.emptyMap();\n        }\n\n        return policy.getProcessCfgPolicy().get();\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Map<String, Object> getWorkspaceCfg(Payload payload) {\n        Path workspace = payload.getHeader(Payload.WORKSPACE_DIR);\n        Path src = workspace.resolve(Constants.Files.CONFIGURATION_FILE_NAME);\n        if (!Files.exists(src)) {\n            return Collections.emptyMap();\n        }\n\n        try (InputStream in = Files.newInputStream(src)) {\n            ObjectMapper om = new ObjectMapper();\n            return om.readValue(in, Map.class);\n        } catch (IOException e) {\n            throw new ProcessException(payload.getProcessKey(), \"Invalid request data format\", e, Status.BAD_REQUEST);\n        }\n    }\n\n    private static Map<String, Object> getProfileCfg(Payload payload, List<String> activeProfiles) {\n        if (activeProfiles == null) {\n            activeProfiles = Collections.emptyList();\n        }\n\n        ProcessDefinition pd = payload.getHeader(Payload.PROJECT_DEFINITION);\n        if (pd == null) {\n            return Collections.emptyMap();\n        }\n\n        Map<String, Object> m = EffectiveConfiguration.getEffectiveConfiguration(pd, activeProfiles);\n        return m != null ? m : Collections.emptyMap();\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Map<String, Object> getAttachedCfg(Payload payload) {\n        Path p = payload.getAttachment(REQUEST_ATTACHMENT_KEY);\n        if (p == null) {\n            return Collections.emptyMap();\n        }\n\n        try (InputStream in = Files.newInputStream(p)) {\n            ObjectMapper om = new ObjectMapper();\n            return om.readValue(in, Map.class);\n        } catch (IOException e) {\n            throw new ProcessException(payload.getProcessKey(), \"Invalid request data format\", e, Status.BAD_REQUEST);\n        }\n    }\n\n    private static void processHandlersConfiguration(Payload payload, Map<String, Object> m) {\n        ProcessKind processKind = payload.getHeader(Payload.PROCESS_KIND);\n        if (processKind != ProcessKind.FAILURE_HANDLER &&\n            processKind != ProcessKind.CANCEL_HANDLER &&\n            processKind != ProcessKind.TIMEOUT_HANDLER) {\n            return;\n        }\n\n        Object handlerTimeout = m.get(Constants.Request.HANDLER_PROCESS_TIMEOUT);\n        if (handlerTimeout != null) {\n            m.put(Constants.Request.PROCESS_TIMEOUT, handlerTimeout);\n        }\n    }\n\n    private void processDryRunModeConfiguration(Payload payload, Map<String, Object> m) {\n        Boolean dryRunMode = getBoolean(payload, m, Constants.Request.DRY_RUN_MODE_KEY);\n        if (dryRunMode == null) {\n            return;\n        }\n        m.put(Constants.Request.DRY_RUN_MODE_KEY, dryRunMode);\n        if (dryRunMode) {\n            logManager.info(payload.getProcessKey(), \"Dry-run mode: enabled\");\n        }\n    }\n\n    private Boolean getBoolean(Payload payload, Map<String, Object> m, String key) {\n        Object value = m.get(key);\n        if (value == null) {\n            return null;\n        } else if (value instanceof Boolean booleanValue) {\n            return booleanValue;\n        } else if (value instanceof String stringValue) {\n            if (\"true\".equalsIgnoreCase(stringValue)) {\n                return true;\n            } else if (\"false\".equalsIgnoreCase(stringValue)) {\n                return false;\n            }\n        }\n        throw new ProcessException(payload.getProcessKey(), String.format(\"Invalid '%s' mode value type. Expected a boolean value true or false, got: '%s'\", key, value), Status.BAD_REQUEST);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/ConfigurationStoringProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Map;\n\n/**\n * Stores the process' configuration as a JSON file.\n */\npublic class ConfigurationStoringProcessor implements PayloadProcessor {\n\n    private final ProcessLogManager logManager;\n\n    @Inject\n    public ConfigurationStoringProcessor(ProcessLogManager logManager) {\n        this.logManager = logManager;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        ProcessKey processKey = payload.getProcessKey();\n\n        Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION);\n        if (cfg == null) {\n            return chain.process(payload);\n        }\n\n        Path workspace = payload.getHeader(Payload.WORKSPACE_DIR);\n        Path dst = workspace.resolve(Constants.Files.CONFIGURATION_FILE_NAME);\n\n        try (OutputStream out = Files.newOutputStream(dst)) {\n            ObjectMapper om = new ObjectMapper();\n            om.writeValue(out, cfg);\n        } catch (IOException e) {\n            logManager.error(processKey, \"Error while saving a metadata file: \" + dst, e);\n            throw new ProcessException(processKey, \"Error while saving a metadata file: \" + dst, e);\n        }\n\n        return chain.process(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/CurrentUserInfoProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.pipelines.processors.signing.Signing;\nimport com.walmartlabs.concord.server.user.UserManager;\n\nimport javax.inject.Inject;\n\npublic class CurrentUserInfoProcessor extends UserInfoProcessor {\n\n    @Inject\n    public CurrentUserInfoProcessor(UserManager userManager, Signing signing, ObjectMapper objectMapper) {\n        super(Constants.Request.CURRENT_USER_KEY, userManager, signing, objectMapper);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/CustomEnqueueProcessors.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.process.CustomEnqueueProcessor;\n\nimport javax.inject.Inject;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Set;\n\npublic class CustomEnqueueProcessors {\n\n    private final Collection<CustomEnqueueProcessor> processors;\n\n    @Inject\n    public CustomEnqueueProcessors(Set<CustomEnqueueProcessor> processors) {\n        this.processors = List.copyOf(processors);\n    }\n\n    public PayloadProcessor handleAttachments() {\n        return (chain, payload) -> {\n            for (CustomEnqueueProcessor p : processors) {\n                payload = p.handleAttachments(payload);\n            }\n            return chain.process(payload);\n        };\n    }\n\n    public PayloadProcessor handleState() {\n        return (chain, payload) -> {\n            for (CustomEnqueueProcessor p : processors) {\n                payload = p.handleState(payload);\n            }\n            return chain.process(payload);\n        };\n    }\n\n    public PayloadProcessor handleConfiguration() {\n        return (chain, payload) -> {\n            for (CustomEnqueueProcessor p : processors) {\n                payload = p.handleConfiguration(payload);\n            }\n            return chain.process(payload);\n        };\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/DependenciesProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\n\nimport javax.inject.Inject;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Map;\n\npublic class DependenciesProcessor implements PayloadProcessor {\n\n    private final ProcessLogManager logManager;\n\n    @Inject\n    public DependenciesProcessor(ProcessLogManager logManager) {\n        this.logManager = logManager;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        ProcessKey processKey = payload.getProcessKey();\n        Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION);\n\n        // get a list of dependencies from the cfg data\n        Collection<String> deps = getListOfStrings(processKey, cfg, Constants.Request.DEPENDENCIES_KEY);\n        Collection<String> extraDeps = getListOfStrings(processKey, cfg, Constants.Request.EXTRA_DEPENDENCIES_KEY);\n        if (deps == null && extraDeps == null) {\n            return chain.process(payload);\n        }\n\n        Collection<String> allDeps = new ArrayList<>(deps != null ? deps : Collections.emptyList());\n        allDeps.addAll(extraDeps != null ? extraDeps : Collections.emptyList());\n\n        boolean failed = false;\n        for (String d : allDeps) {\n            try {\n                new URI(d);\n            } catch (URISyntaxException e) {\n                logManager.error(processKey, \"Invalid dependency URL: \" + d);\n                failed = true;\n            }\n        }\n\n        if (failed) {\n            throw new ProcessException(processKey, \"Invalid dependency list\");\n        }\n\n        cfg.put(Constants.Request.DEPENDENCIES_KEY, allDeps);\n        payload = payload.putHeader(Payload.CONFIGURATION, cfg);\n\n        return chain.process(payload);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private Collection<String> getListOfStrings(ProcessKey processKey, Map<String, Object> req, String key) {\n        Object o = req.get(key);\n        if (o == null) {\n            return null;\n        }\n\n        if (o instanceof Collection) {\n            return (Collection<String>) o;\n        }\n\n        logManager.error(processKey, \"Invalid '{}' object type. Expected an array or a collection, got: {}\", key, o.getClass());\n        throw new ProcessException(processKey, \"Invalid '\" + key + \"' object type. Expected an array or a collection, got: \" + o.getClass());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/EffectiveProcessDefinitionProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.policyengine.EffectiveYamlPolicy;\nimport com.walmartlabs.concord.runtime.model.Options;\nimport com.walmartlabs.concord.runtime.model.ProcessDefinition;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport com.walmartlabs.concord.server.cfg.ProcessConfiguration;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.process.state.ProcessStateManager;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.io.ByteArrayOutputStream;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class EffectiveProcessDefinitionProcessor implements PayloadProcessor {\n\n    private static final Logger log = LoggerFactory.getLogger(EffectiveProcessDefinitionProcessor.class);\n\n    private static final String EFFECTIVE_YAML_PATH = \".concord/effective.concord.yml\";\n\n    private final ProcessStateManager stateManager;\n    private final ProcessLogManager logManager;\n\n    @Inject\n    public EffectiveProcessDefinitionProcessor(ProcessStateManager stateManager, ProcessLogManager logManager) {\n        this.stateManager = stateManager;\n        this.logManager = logManager;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        EffectiveYamlPolicy policy = getPolicy(payload);\n\n        boolean renderEffectiveYaml = policy == null || policy.renderEffectiveYaml();\n        boolean logWarning = policy == null || policy.logWarning();\n\n        if (!renderEffectiveYaml) {\n            return chain.process(payload);\n        }\n\n        ProcessDefinition pd = payload.getHeader(Payload.PROJECT_DEFINITION);\n        if (pd == null) {\n            return chain.process(payload);\n        }\n\n        Options opts = Options.builder()\n                .instanceId(payload.getProcessKey().getInstanceId())\n                .entryPoint(payload.getHeader(Payload.ENTRY_POINT))\n                .parentInstanceId(payload.getHeader(Payload.PARENT_INSTANCE_ID))\n                .configuration(sanitizeConfiguration(payload.getHeader(Payload.CONFIGURATION, Collections.emptyMap())))\n                .activeProfiles(payload.getHeader(Payload.ACTIVE_PROFILES, Collections.emptyList()))\n                .build();\n\n        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {\n            pd.serialize(opts, out);\n\n            byte[] bytes = out.toByteArray();\n            boolean isTooLarge = policy == null ? false : policy.isTooLarge(bytes.length);\n\n            if (bytes.length == 0 || isTooLarge) {\n                if (isTooLarge && logWarning) {\n                    logManager.warn(payload.getProcessKey(), \"Effective concord.yml is too large to persist ({} bytes), skipping...\", bytes.length);\n                }\n\n                return chain.process(payload);\n            }\n\n            stateManager.tx(tx -> {\n                stateManager.deleteFile(tx, payload.getProcessKey(), EFFECTIVE_YAML_PATH);\n                stateManager.insert(tx, payload.getProcessKey(), EFFECTIVE_YAML_PATH, bytes);\n            });\n        } catch (Exception e) {\n            log.warn(\"process ['{}'] -> error: {}\", payload.getProcessKey(), e.getMessage());\n            throw new ProcessException(payload.getProcessKey(), \"Error while processing effective concord.yml: \" + e.getMessage(), e);\n        }\n        return chain.process(payload);\n    }\n\n    private static EffectiveYamlPolicy getPolicy(Payload payload) {\n        PolicyEngine policy = payload.getHeader(Payload.POLICY);\n        return policy == null ? null : policy.getEffectiveYamlPolicy();\n    }\n\n    private Map<String, Object> sanitizeConfiguration(Map<String, Object> cfg) {\n        Map<String, Object> m = new HashMap<>(cfg);\n\n        Map<String, Object> pi = MapUtils.getMap(m, Constants.Request.PROCESS_INFO_KEY, null);\n        if (pi != null) {\n            pi = new HashMap<>(pi);\n            sanitize(pi, \"sessionKey\");\n            sanitize(pi, Constants.Request.SESSION_TOKEN_KEY);\n            m.put(Constants.Request.PROCESS_INFO_KEY, pi);\n        }\n\n        return m;\n    }\n\n    private void sanitize(Map<String, Object> m, String key) {\n        if (m.containsKey(key)) {\n            m.put(key, \"***\");\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/EnqueueingProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.PayloadUtils;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\n\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\nimport java.util.Map;\n\n/**\n * Moves the process into ENQUEUED status, filling in the necessary attributes.\n */\npublic class EnqueueingProcessor implements PayloadProcessor {\n\n    private final ProcessQueueManager queueManager;\n    private final ProcessLogManager logManager;\n\n    @Inject\n    public EnqueueingProcessor(ProcessQueueManager queueManager, ProcessLogManager logManager) {\n        this.queueManager = queueManager;\n        this.logManager = logManager;\n    }\n\n    @Override\n    @WithTimer\n    public Payload process(Chain chain, Payload payload) {\n        boolean enqueued = queueManager.enqueue(payload);\n        if (!enqueued) {\n            // the process was rejected but it was not an error\n            // (e.g. \"exclusive\" processes can be rejected before reaching the ENQUEUED status)\n            return payload;\n        }\n\n        ProcessKey processKey = payload.getProcessKey();\n\n        Map<String, Object> requirements = PayloadUtils.getRequirements(payload);\n        OffsetDateTime startAt = PayloadUtils.getStartAt(payload);\n\n        if (startAt == null) {\n            logManager.info(processKey, \"Enqueued. Waiting for an agent (requirements={})...\", requirements);\n        } else {\n            logManager.info(processKey, \"Enqueued. Starting at {} (requirements={})...\", startAt, requirements);\n        }\n\n        return chain.process(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/EntryPointProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.model.Profile;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\n\nimport javax.inject.Inject;\nimport java.util.*;\n\npublic class EntryPointProcessor implements PayloadProcessor {\n\n    private final ProcessLogManager logManager;\n\n    @Inject\n    public EntryPointProcessor(ProcessLogManager logManager) {\n        this.logManager = logManager;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        String s = payload.getHeader(Payload.ENTRY_POINT);\n\n        Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION);\n        if (cfg == null) {\n            cfg = new HashMap<>();\n        }\n\n        if (s == null) {\n            s = (String) cfg.get(Constants.Request.ENTRY_POINT_KEY);\n        }\n\n        if (s == null) {\n            s = Constants.Request.DEFAULT_ENTRY_POINT_NAME;\n        }\n\n        if (!isValidEntryPoint(payload, s)) {\n            throw new ProcessException(\n                    payload.getProcessKey(),\n                    String.format(\"entryPoint '%s' is not a public flow\", s)\n            );\n        }\n\n        cfg.put(Constants.Request.ENTRY_POINT_KEY, s);\n        payload = payload.putHeader(Payload.CONFIGURATION, cfg)\n                .putHeader(Payload.ENTRY_POINT, s);\n\n        logManager.info(payload.getProcessKey(), \"Using entry point: {}\", s);\n\n        return chain.process(payload);\n    }\n\n    /**\n     * Determines validity of a given {@code entryPoint} based on\n     * the currently active profiles.\n     *\n     * @param payload    payload containing public flows definition\n     * @param entryPoint process {@code entryPoint} flow\n     * @return true if {@code entryPoint} is a valid value\n     */\n    private static boolean isValidEntryPoint(Payload payload, String entryPoint) {\n        ProcessDefinition pd = payload.getHeader(Payload.PROJECT_DEFINITION);\n\n        Set<String> publicFlows = new HashSet<>(pd.publicFlows() != null ? pd.publicFlows() : Collections.emptySet());\n\n        List<String> activeProfiles = payload.getHeader(Payload.ACTIVE_PROFILES);\n        if (activeProfiles != null) {\n            for (String profileName : activeProfiles) {\n                Profile p = pd.profiles().get(profileName);\n                if (p == null || p.publicFlows() == null) {\n                    continue;\n                }\n\n                publicFlows.addAll(p.publicFlows());\n            }\n        }\n\n        if (publicFlows.isEmpty()) {\n            // all flows are public when public definition not provided\n            return true;\n        }\n\n        return publicFlows.contains(entryPoint);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/ExceptionProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.Payload;\n\npublic interface ExceptionProcessor {\n\n    void process(Payload payload, Exception e);\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/ExclusiveGroupProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.runtime.v2.model.ExclusiveMode;\nimport com.walmartlabs.concord.server.Locks;\nimport com.walmartlabs.concord.server.jooq.tables.ProcessQueue;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.PayloadUtils;\nimport com.walmartlabs.concord.server.process.ProcessManager;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport org.jooq.*;\n\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\nimport static com.walmartlabs.concord.db.PgUtils.jsonbText;\nimport static com.walmartlabs.concord.server.jooq.Tables.PROCESS_QUEUE;\nimport static org.jooq.impl.DSL.*;\n\n/**\n * If the process has an \"exclusive group and mode cancel/cancelOld\" assigned to it, this processor\n * determines whether the process can continue to run or should be cancelled.\n * The processor uses a global (DB) lock {@link Locks}.\n */\npublic class ExclusiveGroupProcessor implements PayloadProcessor {\n\n    private final ProcessLogManager logManager;\n    private final Map<ExclusiveMode.Mode, ModeProcessor> processors;\n\n    @Inject\n    public ExclusiveGroupProcessor(ProcessLogManager logManager,\n                                   Set<ModeProcessor> processors) {\n\n        this.logManager = logManager;\n        this.processors = processors.stream().collect(Collectors.toMap(ModeProcessor::mode, v -> v));\n    }\n\n    @Override\n    @WithTimer\n    public Payload process(Chain chain, Payload payload) {\n        ExclusiveMode exclusive = PayloadUtils.getExclusive(payload);\n        if (exclusive == null) {\n            return chain.process(payload);\n        }\n\n        UUID projectId = payload.getHeader(Payload.PROJECT_ID);\n        if (projectId == null) {\n            return chain.process(payload);\n        }\n\n        ModeProcessor processor = processors.get(exclusive.mode());\n        if (processor == null) {\n            return chain.process(payload);\n        }\n\n        logManager.info(payload.getProcessKey(), \"Process' exclusive group: {}\", exclusive.group());\n\n        boolean canContinue = processor.process(payload, exclusive);\n        if (canContinue) {\n            return chain.process(payload);\n        }\n\n        return payload;\n    }\n\n    public static class ModeProcessorModule implements Module {\n\n        @Override\n        public void configure(Binder binder) {\n            newSetBinder(binder, ModeProcessor.class).addBinding().to(CancelModeProcessor.class);\n            newSetBinder(binder, ModeProcessor.class).addBinding().to(CancelOldModeProcessor.class);\n        }\n    }\n\n    interface ModeProcessor {\n\n        boolean process(Payload payload, ExclusiveMode exclusive);\n\n        ExclusiveMode.Mode mode();\n    }\n\n    static class CancelModeProcessor implements ModeProcessor {\n\n        private static final long LOCK_KEY = 1562319227723L;\n\n        private final CancelModeDao dao;\n        private final Locks exclusiveGroupLock;\n        private final ProcessLogManager logManager;\n        private final ProcessManager processManager;\n\n        @Inject\n        public CancelModeProcessor(CancelModeDao dao,\n                                   Locks exclusiveGroupLock,\n                                   ProcessLogManager logManager,\n                                   ProcessManager processManager) {\n            this.dao = dao;\n            this.exclusiveGroupLock = exclusiveGroupLock;\n            this.logManager = logManager;\n            this.processManager = processManager;\n        }\n\n        @Override\n        public boolean process(Payload payload, ExclusiveMode exclusive) {\n            ProcessKey processKey = payload.getProcessKey();\n            UUID parentInstanceId = payload.getHeader(Payload.PARENT_INSTANCE_ID);\n            UUID projectId = payload.getHeader(Payload.PROJECT_ID);\n\n            return dao.txResult(tx -> {\n                exclusiveGroupLock.lock(tx, LOCK_KEY);\n\n                if (dao.exists(tx, processKey.getInstanceId(), parentInstanceId, projectId, exclusive.group())) {\n                    logManager.warn(processKey, \"Process(es) with exclusive group '\" + exclusive.group() + \"' is already in the queue. \" +\n                            \"Current process has been cancelled\");\n\n                    processManager.kill(tx, processKey);\n\n                    return false;\n                }\n\n                processManager.updateExclusive(tx, processKey, exclusive);\n\n                return true;\n            });\n        }\n\n        @Override\n        public ExclusiveMode.Mode mode() {\n            return ExclusiveMode.Mode.cancel;\n        }\n    }\n\n    static class CancelOldModeProcessor implements ModeProcessor {\n\n        private static final long LOCK_KEY = 1562319227723L;\n        private static final String CANCELLED_MSG = \"Process '{}' with exclusive group '{}' is already in the queue. Current process has been cancelled\";\n\n        private final CancelOldModeDao dao;\n        private final ProcessManager processManager;\n        private final ProcessLogManager logManager;\n        private final Locks exclusiveGroupLock;\n\n        @Inject\n        public CancelOldModeProcessor(CancelOldModeDao dao,\n                                      ProcessManager processManager,\n                                      ProcessLogManager logManager,\n                                      Locks exclusiveGroupLock) {\n            this.dao = dao;\n            this.processManager = processManager;\n            this.logManager = logManager;\n            this.exclusiveGroupLock = exclusiveGroupLock;\n        }\n\n        @Override\n        public boolean process(Payload payload, ExclusiveMode exclusive) {\n            ProcessKey processKey = payload.getProcessKey();\n            UUID parentInstanceId = payload.getHeader(Payload.PARENT_INSTANCE_ID);\n            UUID projectId = payload.getHeader(Payload.PROJECT_ID);\n\n            Result result = dao.txResult(tx -> {\n                exclusiveGroupLock.lock(tx, LOCK_KEY);\n\n                List<ProcessKey> oldProcesses = dao.listOld(tx, processKey, parentInstanceId, projectId, exclusive.group());\n                processManager.kill(tx, oldProcesses);\n\n                ProcessKey newProcess = dao.anyNew(tx, processKey, projectId, exclusive.group());\n                if (newProcess != null) {\n                    processManager.kill(tx, processKey);\n                }\n\n                processManager.updateExclusive(tx, processKey, exclusive);\n\n                return new Result(oldProcesses, newProcess);\n            });\n\n            result.oldProcesses().forEach(pk -> logManager.warn(pk, CANCELLED_MSG, payload.getProcessKey(), exclusive.group()));\n\n            if (result.newProcess() != null) {\n                logManager.warn(processKey, CANCELLED_MSG, result.newProcess(), exclusive.group());\n            }\n\n            return result.newProcess() == null;\n        }\n\n        @Override\n        public ExclusiveMode.Mode mode() {\n            return ExclusiveMode.Mode.cancelOld;\n        }\n\n        private static class Result {\n\n            private final List<ProcessKey> oldProcesses;\n            private final ProcessKey newProcess;\n\n            private Result(List<ProcessKey> oldProcesses, ProcessKey newProcess) {\n                this.oldProcesses = oldProcesses;\n                this.newProcess = newProcess;\n            }\n\n            public List<ProcessKey> oldProcesses() {\n                return oldProcesses;\n            }\n\n            public ProcessKey newProcess() {\n                return newProcess;\n            }\n        }\n    }\n\n    static class CancelModeDao extends AbstractDao {\n\n        private static final List<ProcessStatus> RUNNING_STATUSES = Arrays.asList(\n                ProcessStatus.NEW,\n                ProcessStatus.PREPARING,\n                ProcessStatus.ENQUEUED,\n                ProcessStatus.WAITING,\n                ProcessStatus.STARTING,\n                ProcessStatus.RUNNING,\n                ProcessStatus.SUSPENDED,\n                ProcessStatus.RESUMING);\n\n        @Inject\n        protected CancelModeDao(@MainDB Configuration cfg) {\n            super(cfg);\n        }\n\n        @Override\n        public <T> T txResult(TxResult<T> t) {\n            return super.txResult(t);\n        }\n\n        public boolean exists(DSLContext tx, UUID currentInstanceId, UUID parentInstanceId, UUID projectId, String exclusiveGroup) {\n            SelectConditionStep<Record1<Integer>> s = tx.selectOne()\n                            .from(PROCESS_QUEUE)\n                            .where(jsonbText(PROCESS_QUEUE.EXCLUSIVE, \"group\").eq(exclusiveGroup)\n                                    .and(PROCESS_QUEUE.PROJECT_ID.eq(projectId)\n                                            .and(PROCESS_QUEUE.INSTANCE_ID.notEqual(currentInstanceId)\n                                                    .and(PROCESS_QUEUE.CURRENT_STATUS.in(RUNNING_STATUSES)))));\n\n            // parent's\n            if (parentInstanceId != null) {\n                s.and(PROCESS_QUEUE.INSTANCE_ID.notIn(parents(tx, parentInstanceId)));\n            }\n\n            return tx.fetchExists(s);\n        }\n    }\n\n    static class CancelOldModeDao extends AbstractDao {\n\n        private static final List<ProcessStatus> CANCELLABLE_STATUSES = Arrays.asList(\n                ProcessStatus.NEW,\n                ProcessStatus.PREPARING,\n                ProcessStatus.ENQUEUED,\n                ProcessStatus.WAITING,\n                ProcessStatus.STARTING,\n                ProcessStatus.RUNNING,\n                ProcessStatus.SUSPENDED,\n                ProcessStatus.RESUMING);\n\n        @Inject\n        protected CancelOldModeDao(@MainDB Configuration cfg) {\n            super(cfg);\n        }\n\n        @Override\n        public <T> T txResult(TxResult<T> t) {\n            return super.txResult(t);\n        }\n\n        public List<ProcessKey> listOld(DSLContext tx, ProcessKey currentProcessKey, UUID parentInstanceId, UUID projectId, String exclusiveGroup) {\n            SelectConditionStep<Record2<UUID, OffsetDateTime>> s = tx.select(PROCESS_QUEUE.INSTANCE_ID, PROCESS_QUEUE.CREATED_AT)\n                    .from(PROCESS_QUEUE)\n                    .where(jsonbText(PROCESS_QUEUE.EXCLUSIVE, \"group\").eq(exclusiveGroup)\n                            .and(PROCESS_QUEUE.PROJECT_ID.eq(projectId)\n                                    .and(PROCESS_QUEUE.INSTANCE_ID.notEqual(currentProcessKey.getInstanceId())\n                                            .and(PROCESS_QUEUE.CREATED_AT.lessOrEqual(currentProcessKey.getCreatedAt())\n                                                    .and(PROCESS_QUEUE.CURRENT_STATUS.in(CANCELLABLE_STATUSES))))));\n\n            // parent's\n            if (parentInstanceId != null) {\n                s.and(PROCESS_QUEUE.INSTANCE_ID.notIn(parents(tx, parentInstanceId)));\n            }\n\n            return s.fetch(r -> new ProcessKey(r.value1(), r.value2()));\n        }\n\n        public ProcessKey anyNew(DSLContext tx, ProcessKey currentProcessKey, UUID projectId, String exclusiveGroup) {\n            SelectConditionStep<Record2<UUID, OffsetDateTime>> s = tx.select(PROCESS_QUEUE.INSTANCE_ID, PROCESS_QUEUE.CREATED_AT)\n                    .from(PROCESS_QUEUE)\n                    .where(jsonbText(PROCESS_QUEUE.EXCLUSIVE, \"group\").eq(exclusiveGroup)\n                            .and(PROCESS_QUEUE.PROJECT_ID.eq(projectId)\n                                    .and(PROCESS_QUEUE.CREATED_AT.greaterOrEqual(currentProcessKey.getCreatedAt())\n                                            .and(PROCESS_QUEUE.CURRENT_STATUS.in(CANCELLABLE_STATUSES)))));\n\n            SelectJoinStep<Record1<UUID>> children = tx.withRecursive(\"children\").as(\n                    select(ProcessQueue.PROCESS_QUEUE.INSTANCE_ID, ProcessQueue.PROCESS_QUEUE.PARENT_INSTANCE_ID).from(ProcessQueue.PROCESS_QUEUE)\n                            .where(ProcessQueue.PROCESS_QUEUE.INSTANCE_ID.eq(currentProcessKey.getInstanceId()))\n                            .unionAll(\n                                    select(ProcessQueue.PROCESS_QUEUE.INSTANCE_ID, ProcessQueue.PROCESS_QUEUE.PARENT_INSTANCE_ID)\n                                            .from(ProcessQueue.PROCESS_QUEUE)\n                                            .join(name(\"children\"))\n                                            .on(ProcessQueue.PROCESS_QUEUE.PARENT_INSTANCE_ID.eq(\n                                                    field(name(\"children\", \"INSTANCE_ID\"), UUID.class)))))\n                    .select(field(\"children.INSTANCE_ID\", UUID.class))\n                    .from(name(\"children\"));\n\n            s.and(PROCESS_QUEUE.INSTANCE_ID.notIn(children));\n\n            return s.limit(1)\n                    .fetchOne(r -> new ProcessKey(r.value1(), r.value2()));\n        }\n    }\n\n    private static SelectJoinStep<Record1<UUID>> parents(DSLContext tx, UUID parentInstanceId) {\n        return tx.withRecursive(\"parents\").as(\n                select(ProcessQueue.PROCESS_QUEUE.INSTANCE_ID, ProcessQueue.PROCESS_QUEUE.PARENT_INSTANCE_ID).from(ProcessQueue.PROCESS_QUEUE)\n                        .where(ProcessQueue.PROCESS_QUEUE.INSTANCE_ID.eq(parentInstanceId))\n                        .unionAll(\n                                select(ProcessQueue.PROCESS_QUEUE.INSTANCE_ID, ProcessQueue.PROCESS_QUEUE.PARENT_INSTANCE_ID)\n                                        .from(ProcessQueue.PROCESS_QUEUE)\n                                        .join(name(\"parents\"))\n                                        .on(ProcessQueue.PROCESS_QUEUE.INSTANCE_ID.eq(\n                                                field(name(\"parents\", \"PARENT_INSTANCE_ID\"), UUID.class)))))\n                .select(field(\"parents.INSTANCE_ID\", UUID.class))\n                .from(name(\"parents\"));\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/FailProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueDao;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\n\nimport javax.inject.Inject;\n\npublic class FailProcessor implements ExceptionProcessor {\n\n    private final ProcessQueueDao queueDao;\n    private final ProcessQueueManager queueManager;\n    private final ProcessLogManager logManager;\n\n    @Inject\n    public FailProcessor(ProcessQueueDao queueDao, ProcessQueueManager queueManager, ProcessLogManager logManager) {\n        this.queueDao = queueDao;\n        this.logManager = logManager;\n        this.queueManager = queueManager;\n    }\n\n    @Override\n    public void process(Payload payload, Exception e) {\n        ProcessKey processKey = payload.getProcessKey();\n\n        boolean hasQueueRecord = queueDao.exists(processKey);\n        if (!hasQueueRecord) {\n            // the process failed before we had a chance to create the initial queue record\n            return;\n        }\n\n        if (!(e instanceof InvalidProcessStateException)) {\n            logManager.error(processKey, \"Process failed: {}\", e.getMessage());\n            queueManager.updateStatus(processKey, ProcessStatus.FAILED);\n        } else {\n            logManager.warn(processKey, \"Invalid process state: {}\", e.getMessage());\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/FinalizerProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.Payload;\n\npublic interface FinalizerProcessor {\n\n    void process(Payload payload);\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/ForkCleanupProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.util.Map;\n\n/**\n * Cleans up the fork's files copied from the parent process.\n */\npublic class ForkCleanupProcessor implements PayloadProcessor {\n\n    private static final String[] MARKER_FILES = {Constants.Files.SUSPEND_MARKER_FILE_NAME, Constants.Files.RESUME_MARKER_FILE_NAME};\n\n    private final ObjectMapper objectMapper;\n\n    @Inject\n    public ForkCleanupProcessor(ObjectMapper objectMapper) {\n        this.objectMapper = objectMapper;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        Path workspace = payload.getHeader(Payload.WORKSPACE_DIR);\n        Path stateDir = workspace.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(Constants.Files.JOB_STATE_DIR_NAME);\n\n        try {\n            // clear the parent process' suspend/resume markers\n            // otherwise the fork could try to resume a parent process' event\n            for (String m : MARKER_FILES) {\n                Path suspendMarker = stateDir.resolve(m);\n                Files.deleteIfExists(suspendMarker);\n            }\n\n            // remove the parent process' arguments file if a state snapshot is present\n            // we don't want the original process arguments to overwrite the process variables\n            Path stateSnapshot = stateDir.resolve(Constants.Files.LAST_KNOWN_VARIABLES_FILE_NAME);\n            if (Files.exists(stateSnapshot)) {\n                clearArguments(workspace);\n            }\n        } catch (IOException e) {\n            throw new ProcessException(payload.getProcessKey(), \"Error while preparing the fork's data\", e);\n        }\n\n        return chain.process(payload);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private void clearArguments(Path workspace) throws IOException {\n        Path requestFile = workspace.resolve(Constants.Files.CONFIGURATION_FILE_NAME);\n        if (!Files.exists(requestFile)) {\n            return;\n        }\n\n        Map<String, Object> m;\n        try (InputStream in = Files.newInputStream(requestFile)) {\n            m = objectMapper.readValue(in, Map.class);\n        }\n\n        if (m == null || m.isEmpty()) {\n            return;\n        }\n\n        m.remove(Constants.Request.ARGUMENTS_KEY);\n\n        try (OutputStream out = Files.newOutputStream(requestFile, StandardOpenOption.TRUNCATE_EXISTING)) {\n            objectMapper.writeValue(out, m);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/ForkHandlersProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.Payload;\n\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * Updates the fork's list of handlers according to the process arguments.\n */\npublic class ForkHandlersProcessor implements PayloadProcessor {\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        Set<String> handlers = payload.getHeader(Payload.PROCESS_HANDLERS);\n        handlers = new HashSet<>(handlers != null ? handlers : Collections.emptySet());\n\n        update(payload, handlers, Constants.Flows.ON_FAILURE_FLOW, Constants.Request.DISABLE_ON_FAILURE_KEY);\n        update(payload, handlers, Constants.Flows.ON_CANCEL_FLOW, Constants.Request.DISABLE_ON_CANCEL_KEY);\n        update(payload, handlers, Constants.Flows.ON_TIMEOUT_FLOW, Constants.Request.DISABLE_ON_TIMEOUT_KEY);\n\n        payload = payload.putHeader(Payload.PROCESS_HANDLERS, handlers);\n\n        return chain.process(payload);\n    }\n\n    private static void update(Payload payload, Set<String> handlers, String flow, String disableFlag) {\n        boolean suppressed = getBoolean(payload, disableFlag);\n        if (suppressed) {\n            handlers.remove(flow);\n        }\n    }\n\n    private static boolean getBoolean(Payload payload, String key) {\n        Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION);\n        if (cfg == null) {\n            return false;\n        }\n\n        Object v = cfg.get(key);\n        if (v == null) {\n            return false;\n        }\n\n        return (Boolean) v;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/ForkPolicyProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.policyengine.CheckResult;\nimport com.walmartlabs.concord.policyengine.ForkDepthRule;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport org.jooq.Configuration;\nimport org.jooq.Record1;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.text.MessageFormat;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.tables.ProcessQueue.PROCESS_QUEUE;\nimport static org.jooq.impl.DSL.*;\n\npublic class ForkPolicyProcessor implements PayloadProcessor {\n\n    private static final Logger log = LoggerFactory.getLogger(ForkPolicyProcessor.class);\n\n    private static final String DEFAULT_POLICY_MESSAGE = \"Maximum number of forks exceeded: current {0}, limit {1}\";\n\n    private final ProcessLogManager logManager;\n    private final ForkDepthDao forkDepthDao;\n\n    @Inject\n    public ForkPolicyProcessor(ProcessLogManager logManager, ForkDepthDao forkDepthDao) {\n        this.logManager = logManager;\n        this.forkDepthDao = forkDepthDao;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        ProcessKey processKey = payload.getProcessKey();\n        UUID parentInstanceId = payload.getHeader(Payload.PARENT_INSTANCE_ID);\n\n        PolicyEngine policy = payload.getHeader(Payload.POLICY);\n        if (policy == null) {\n            return chain.process(payload);\n        }\n\n        logManager.info(processKey, \"Applying fork policies...\");\n\n        CheckResult<ForkDepthRule, Integer> result;\n        try {\n            result = policy.getForkDepthPolicy()\n                    .check(() -> forkDepthDao.getDepth(parentInstanceId));\n        } catch (Exception e) {\n            log.error(\"process -> error\", e);\n            throw new ProcessException(processKey, \"Found fork policy check error\", e);\n        }\n\n        if (!result.getDeny().isEmpty()) {\n            logManager.error(processKey, buildErrorMessage(result.getDeny()));\n            throw new ProcessException(processKey, \"Found fork policy violations\");\n        }\n        return chain.process(payload);\n    }\n\n    private String buildErrorMessage(List<CheckResult.Item<ForkDepthRule, Integer>> errors) {\n        StringBuilder sb = new StringBuilder();\n        for (CheckResult.Item<ForkDepthRule, Integer> e : errors) {\n            ForkDepthRule r = e.getRule();\n\n            String msg = r.msg() != null ? r.msg() : DEFAULT_POLICY_MESSAGE;\n            int actualCount = e.getEntity();\n            int limit = r.max();\n\n            sb.append(MessageFormat.format(Objects.requireNonNull(msg), actualCount, limit)).append(';');\n        }\n        return sb.toString();\n    }\n\n    private static class ForkDepthDao extends AbstractDao {\n\n        @Inject\n        public ForkDepthDao(@MainDB Configuration cfg) {\n            super(cfg);\n        }\n\n        @WithTimer\n        public int getDepth(UUID parentInstanceId) {\n            return txResult(tx -> tx.withRecursive(\"ancestors\").as(\n                    select(PROCESS_QUEUE.INSTANCE_ID, PROCESS_QUEUE.PARENT_INSTANCE_ID, field(\"1\", Integer.class).as(\"depth\"))\n                            .from(PROCESS_QUEUE)\n                            .where(PROCESS_QUEUE.INSTANCE_ID.eq(parentInstanceId))\n                            .unionAll(\n                                    select(PROCESS_QUEUE.INSTANCE_ID, PROCESS_QUEUE.PARENT_INSTANCE_ID, field(\"1 + ancestors.depth\", Integer.class).as(\"depth\"))\n                                            .from(PROCESS_QUEUE)\n                                            .join(name(\"ancestors\"))\n                                            .on(PROCESS_QUEUE.INSTANCE_ID.eq(\n                                                    field(name(\"ancestors\", \"PARENT_INSTANCE_ID\"), UUID.class)))))\n                    .select(max(field(name(\"ancestors\", \"depth\"), Integer.class)))\n                    .from(name(\"ancestors\"))\n                    .fetchOne(Record1::value1));\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/ForkRepositoryInfoProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessEntry;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueManager;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\n\nimport javax.inject.Inject;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.process.pipelines.processors.RepositoryProcessor.REPOSITORY_INFO_KEY;\n\n/**\n * Adds the parent process' repository info to the forked process configuration.\n * This information will be used by the Agent to pull the repository files into\n * the fork's workspace.\n */\npublic class ForkRepositoryInfoProcessor implements PayloadProcessor {\n\n    private final ProcessQueueManager queueManager;\n\n    @Inject\n    public ForkRepositoryInfoProcessor(ProcessQueueManager queueManager) {\n        this.queueManager = queueManager;\n    }\n\n    @Override\n    @WithTimer\n    public Payload process(Chain chain, Payload payload) {\n        UUID parentInstanceId = payload.getHeader(Payload.PARENT_INSTANCE_ID);\n\n        ProcessEntry parent = queueManager.get(PartialProcessKey.from(parentInstanceId));\n        if (parent == null) {\n            throw new ProcessException(payload.getProcessKey(), \"Parent process '\" + parentInstanceId + \"' not found\");\n        }\n\n        RepositoryProcessor.CommitInfo ci = new RepositoryProcessor.CommitInfo(parent.commitId(), parent.commitBranch(), null, null);\n        RepositoryProcessor.RepositoryInfo i = new RepositoryProcessor.RepositoryInfo(\n                parent.repoId(), parent.repoName(), parent.repoUrl(),\n                parent.repoPath(), parent.commitBranch(), parent.commitId(), ci);\n\n        payload = payload.putHeader(REPOSITORY_INFO_KEY, i);\n\n        return chain.process(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/ForkRuntimeProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueDao;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\n\nimport javax.inject.Inject;\nimport java.util.UUID;\n\n/**\n * Get the runtime value from the parent process.\n */\npublic class ForkRuntimeProcessor implements PayloadProcessor {\n\n    private final ProcessQueueDao queueDao;\n\n    @Inject\n    public ForkRuntimeProcessor(ProcessQueueDao queueDao) {\n        this.queueDao = queueDao;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        UUID parentInstanceId = payload.getHeader(Payload.PARENT_INSTANCE_ID);\n        if (parentInstanceId == null) {\n            return chain.process(payload);\n        }\n\n        String runtime = queueDao.getRuntime(PartialProcessKey.from(parentInstanceId));\n        payload = payload.putHeader(Payload.RUNTIME, runtime);\n\n        return chain.process(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/FormFilesStoringProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardCopyOption;\nimport java.util.Map;\n\npublic class FormFilesStoringProcessor implements PayloadProcessor {\n\n    private final ProcessLogManager logManager;\n\n    @Inject\n    public FormFilesStoringProcessor(ProcessLogManager logManager) {\n        this.logManager = logManager;\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public Payload process(Chain chain, Payload payload) {\n        ProcessKey processKey = payload.getProcessKey();\n\n        Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION);\n        if (cfg == null) {\n            return chain.process(payload);\n        }\n\n        Map<String, String> formFiles = (Map<String, String>) cfg.get(Constants.Files.FORM_FILES);\n        if (formFiles == null) {\n            return chain.process(payload);\n        }\n\n        Path workspace = payload.getHeader(Payload.WORKSPACE_DIR);\n\n        try {\n            for (Map.Entry<String, String> e : formFiles.entrySet()) {\n                Path formFile = workspace.resolve(e.getKey());\n                Path tmpFile = Paths.get(e.getValue());\n\n                Path p = formFile.getParent();\n                if (!Files.exists(p)) {\n                    Files.createDirectories(p);\n                }\n\n                Files.move(tmpFile, formFile, StandardCopyOption.REPLACE_EXISTING);\n            }\n        } catch (IOException e) {\n            logManager.error(processKey, \"Error while saving form files\", e);\n            throw new ProcessException(processKey, \"Error while saving form files\", e);\n        }\n\n        return chain.process(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/InitialQueueEntryProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueManager;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\n\nimport javax.inject.Inject;\n\npublic class InitialQueueEntryProcessor implements PayloadProcessor {\n\n    private final ProcessQueueManager queueManager;\n\n    @Inject\n    public InitialQueueEntryProcessor(ProcessQueueManager queueManager) {\n        this.queueManager = queueManager;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        queueManager.insert(payload, ProcessStatus.PREPARING);\n        return chain.process(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/InitiatorUserInfoProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.pipelines.processors.signing.Signing;\nimport com.walmartlabs.concord.server.user.UserManager;\n\nimport javax.inject.Inject;\n\npublic class InitiatorUserInfoProcessor extends UserInfoProcessor {\n\n    @Inject\n    public InitiatorUserInfoProcessor(UserManager userManager, Signing signing, ObjectMapper objectMapper) {\n        super(Constants.Request.INITIATOR_KEY, userManager, signing, objectMapper);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/InvalidProcessStateException.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class InvalidProcessStateException extends RuntimeException {\n\n    private static final long serialVersionUID = 1L;\n\n    public InvalidProcessStateException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/InvalidProcessStateExceptionMapper.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.boot.resteasy.ExceptionMapperSupport;\n\nimport javax.ws.rs.core.Response;\nimport javax.ws.rs.ext.Provider;\n\n@Provider\npublic class InvalidProcessStateExceptionMapper extends ExceptionMapperSupport<InvalidProcessStateException> {\n\n    @Override\n    protected Response convert(InvalidProcessStateException e) {\n        return Response.status(Response.Status.CONFLICT).entity(e.getMessage()).build();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/LoggingMDCProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.Payload;\nimport org.slf4j.MDC;\n\npublic class LoggingMDCProcessor implements PayloadProcessor {\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        MDC.put(\"instanceId\", payload.getProcessKey().getInstanceId().toString());\n        return chain.process(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/NewQueueEntryProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueManager;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\n\nimport javax.inject.Inject;\n\npublic class NewQueueEntryProcessor implements PayloadProcessor {\n\n    private final ProcessQueueManager queueManager;\n\n    @Inject\n    public NewQueueEntryProcessor(ProcessQueueManager queueManager) {\n        this.queueManager = queueManager;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        queueManager.insert(payload, ProcessStatus.NEW);\n        return chain.process(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/OutVariablesSettingProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.Payload;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\n\npublic class OutVariablesSettingProcessor implements PayloadProcessor {\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        Set<String> outExpr = payload.getHeader(Payload.OUT_EXPRESSIONS);\n        if (outExpr == null || outExpr.isEmpty()) {\n            return chain.process(payload);\n        }\n\n        Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION);\n        if (cfg == null) {\n            cfg = new HashMap<>();\n        }\n\n        cfg.put(Constants.Request.OUT_EXPRESSIONS_KEY, outExpr);\n        payload = payload.mergeValues(Payload.CONFIGURATION,\n                Collections.singletonMap(Constants.Request.OUT_EXPRESSIONS_KEY, outExpr));\n\n        return chain.process(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/PayloadProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.Payload;\n\nimport java.util.function.Function;\n\npublic interface PayloadProcessor {\n\n    Payload process(Chain chain, Payload payload);\n\n    static PayloadProcessor function(Function<Payload, Payload> f) {\n        return (chain, p1) -> {\n            Payload p2 = f.apply(p1);\n            return chain.process(p2);\n        };\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/PayloadRestoreProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.guava.GuavaModule;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessSecurityContext;\nimport com.walmartlabs.concord.server.process.state.ProcessStateManager;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.security.SecurityUtils;\nimport org.apache.shiro.subject.PrincipalCollection;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.core.Response;\nimport java.io.InputStream;\nimport java.nio.file.Path;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\n\npublic class PayloadRestoreProcessor implements PayloadProcessor {\n\n    private final ObjectMapper objectMapper;\n    private final ProcessStateManager stateManager;\n    private final ProcessSecurityContext securityContext;\n\n    @Inject\n    public PayloadRestoreProcessor(ProcessStateManager stateManager,\n                                   ProcessSecurityContext securityContext) {\n        this.securityContext = securityContext;\n        this.objectMapper = new ObjectMapper()\n                .enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT)\n                .registerModule(new GuavaModule())\n                .registerModule(new Jdk8Module())\n                .registerModule(new JavaTimeModule());\n\n        this.stateManager = stateManager;\n    }\n\n    @Override\n    @WithTimer\n    public Payload process(Chain chain, Payload payload) {\n        ProcessKey processKey = payload.getProcessKey();\n\n        Map<String, Object> headers = stateManager.getInitial(processKey, \"payload.json\", inputStream -> {\n            Map<String, Object> result = deserialize(inputStream);\n            return Optional.ofNullable(result);\n        }).orElseThrow(() -> new ConcordApplicationException(\"Initial state not found\", Response.Status.INTERNAL_SERVER_ERROR));\n\n        payload = payload.putHeaders(headers);\n\n        Path baseDir = payload.getHeader(Payload.BASE_DIR);\n\n        ProcessStateManager.ItemConsumer cp = ProcessStateManager.copyTo(baseDir);\n        Map<String, Path> attachments = new HashMap<>();\n        stateManager.exportDirectoryInitial(processKey, \"attachments/\", (name, unixMode, src) -> {\n            cp.accept(name, unixMode, src);\n            attachments.put(name, baseDir.resolve(name));\n        });\n        payload = payload.putAttachments(attachments);\n\n        PrincipalCollection principals = stateManager.getInitial(processKey, \"initiator\", SecurityUtils::deserialize)\n                .orElseThrow(() -> new ConcordApplicationException(\"Process initiator not found\", Response.Status.INTERNAL_SERVER_ERROR));\n\n        securityContext.storeSubject(processKey, principals);\n\n        return chain.process(payload);\n    }\n\n    private Map<String, Object> deserialize(InputStream is) {\n        try {\n            return objectMapper.readValue(is, ConcordObjectMapper.MAP_TYPE);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/PayloadStoreProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.guava.GuavaModule;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessSecurityContext;\nimport com.walmartlabs.concord.server.process.state.ProcessStateManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.security.SecurityUtils;\n\nimport javax.inject.Inject;\nimport java.nio.file.Path;\nimport java.util.Arrays;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\n/**\n * Saves the current {@link Payload} data as a workspace file.\n * <p/>\n * It will be restored by {@link com.walmartlabs.concord.server.process.pipelines.EnqueueProcessPipeline},\n * when the process transitions from NEW to ENQUEUED.\n */\npublic class PayloadStoreProcessor implements PayloadProcessor {\n\n    private static final Set<String> EXCLUDED_HEADERS = new HashSet<>(Arrays.asList(Payload.POLICY.name(), Payload.REPOSITORY_SNAPSHOT.name()));\n\n    private final ObjectMapper objectMapper;\n    private final ProcessStateManager stateManager;\n    private final ProcessSecurityContext securityContext;\n\n    @Inject\n    public PayloadStoreProcessor(ProcessStateManager stateManager,\n                                 ProcessSecurityContext securityContext) {\n        this.objectMapper = new ObjectMapper()\n                .enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT)\n                .registerModule(new GuavaModule())\n                .registerModule(new Jdk8Module())\n                .registerModule(new JavaTimeModule());\n\n        this.stateManager = stateManager;\n        this.securityContext = securityContext;\n    }\n\n    @Override\n    @WithTimer\n    public Payload process(Chain chain, Payload payload) {\n        ProcessKey processKey = payload.getProcessKey();\n\n        // remove things that shouldn't be in the serialized payload\n        Map<String, Object> headers = payload.getHeaders().entrySet().stream()\n                .filter(e -> !(e.getValue() instanceof Path))\n                .filter(e -> !EXCLUDED_HEADERS.contains(e.getKey()))\n                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));\n\n        String serializedHeaders = serialize(headers);\n\n        stateManager.tx(tx -> {\n            stateManager.insertInitial(tx, processKey, \"payload.json\", serializedHeaders.getBytes());\n            stateManager.insertInitial(tx, processKey, \"initiator\", securityContext.serializePrincipals(SecurityUtils.getSubject().getPrincipals()));\n            stateManager.importPathInitial(tx, processKey, \"attachments/\", payload.getHeader(Payload.BASE_DIR), (path, basicFileAttributes) -> payload.getAttachments().containsValue(path));\n        });\n\n        return chain.process(payload);\n    }\n\n    private String serialize(Map<String, Object> in) {\n        try {\n            return objectMapper.writeValueAsString(in);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/Pipeline.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.Payload;\n\nimport java.util.List;\n\npublic abstract class Pipeline extends Chain {\n\n    public Pipeline(List<PayloadProcessor> processors) {\n        super(processors.toArray(PayloadProcessor[]::new));\n    }\n\n    @Override\n    public Payload process(Payload payload) {\n        try {\n            return super.process(payload);\n        } catch (Exception e) {\n            ExceptionProcessor p = getExceptionProcessor();\n            if (p != null) {\n                p.process(payload, e);\n            }\n            throw e;\n        } finally {\n            FinalizerProcessor p = getFinalizerProcessor();\n            if (p != null) {\n                p.process(payload);\n            }\n        }\n    }\n\n    protected ExceptionProcessor getExceptionProcessor() {\n        return null;\n    }\n\n    protected FinalizerProcessor getFinalizerProcessor() {\n        return null;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/PolicyExportProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.policy.PolicyManager;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.UUID;\n\n/**\n * Adds policy to a payload.\n */\npublic class PolicyExportProcessor implements PayloadProcessor {\n\n    private final ObjectMapper objectMapper;\n    private final PolicyManager policyManager;\n    private final ProcessLogManager logManager;\n\n    @Inject\n    public PolicyExportProcessor(ObjectMapper objectMapper, PolicyManager policyManager, ProcessLogManager logManager) {\n        this.objectMapper = objectMapper;\n        this.policyManager = policyManager;\n        this.logManager = logManager;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        ProcessKey processKey = payload.getProcessKey();\n\n        UUID orgId = payload.getHeader(Payload.ORGANIZATION_ID);\n        UUID projectId = payload.getHeader(Payload.PROJECT_ID);\n        UUID userId = payload.getHeader(Payload.INITIATOR_ID);\n\n        PolicyEngine policy = policyManager.get(orgId, projectId, userId);\n        if (policy == null) {\n            return chain.process(payload);\n        }\n\n        logManager.info(processKey, \"Storing policy '{}' data\", policy.policyNames());\n\n        Path ws = payload.getHeader(Payload.WORKSPACE_DIR);\n\n        try {\n            Path dst = Files.createDirectories(ws.resolve(Constants.Files.CONCORD_SYSTEM_DIR_NAME));\n            objectMapper.writeValue(dst.resolve(Constants.Files.POLICY_FILE_NAME).toFile(), policy.getRules());\n        } catch (IOException e) {\n            logManager.error(processKey, \"Error while storing process policy: {}\", e);\n            throw new ProcessException(processKey, \"Storing process policy error\", e);\n        }\n\n        payload = payload.putHeader(Payload.POLICY, policy);\n\n        return chain.process(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/PolicyProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.process.pipelines.processors.policy.PolicyApplier;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\n\nimport javax.inject.Inject;\nimport java.util.Set;\n\n/**\n * Applies policies.\n */\npublic class PolicyProcessor implements PayloadProcessor {\n\n    private final ProcessLogManager logManager;\n    private final Set<PolicyApplier> appliers;\n\n    @Inject\n    public PolicyProcessor(ProcessLogManager logManager, Set<PolicyApplier> appliers) {\n        this.logManager = logManager;\n        this.appliers = appliers;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        ProcessKey processKey = payload.getProcessKey();\n\n        PolicyEngine policy = payload.getHeader(Payload.POLICY);\n        if (policy == null) {\n            return chain.process(payload);\n        }\n\n        logManager.info(processKey, \"Applying policies...\");\n\n        try {\n            // TODO merge check results\n            for (PolicyApplier a : appliers) {\n                a.apply(payload, policy);\n            }\n        } catch (ProcessException e) {\n            throw e;\n        } catch (Exception e) {\n            logManager.error(processKey, \"Error while applying policy '{}': {}\", policy.policyNames(), e);\n            throw new ProcessException(processKey, \"Policy '\" + policy.policyNames() + \"' error\", e);\n        }\n\n        return chain.process(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/ProcessDefinitionProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Import;\nimport com.walmartlabs.concord.imports.ImportProcessingException;\nimport com.walmartlabs.concord.imports.ImportsListener;\nimport com.walmartlabs.concord.process.loader.DelegatingProjectLoader;\nimport com.walmartlabs.concord.process.loader.ProjectLoader;\nimport com.walmartlabs.concord.process.loader.ProjectLoaderUtils;\nimport com.walmartlabs.concord.repository.Snapshot;\nimport com.walmartlabs.concord.runtime.model.ProcessDefinition;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport com.walmartlabs.concord.server.process.ImportsNormalizerFactory;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.PayloadUtils;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.core.Response;\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.stream.Stream;\n\nimport static com.walmartlabs.concord.process.loader.StandardRuntimeTypes.CONCORD_V1_RUNTIME_TYPE;\n\n/**\n * Loads the process definition using the working directory and configured {@code imports}.\n */\npublic class ProcessDefinitionProcessor implements PayloadProcessor {\n\n    private static final Logger log = LoggerFactory.getLogger(ProcessDefinitionProcessor.class);\n\n    private static final int MAX_DEPENDENCIES_COUNT = 100;\n\n    private final DelegatingProjectLoader projectLoader;\n    private final ImportsNormalizerFactory importsNormalizer;\n    private final ProcessLogManager logManager;\n\n    @Inject\n    public ProcessDefinitionProcessor(DelegatingProjectLoader projectLoader,\n                                      ImportsNormalizerFactory importsNormalizer,\n                                      ProcessLogManager logManager) {\n\n        this.projectLoader = projectLoader;\n        this.importsNormalizer = importsNormalizer;\n        this.logManager = logManager;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        ProcessKey processKey = payload.getProcessKey();\n\n        Path workDir = payload.getHeader(Payload.WORKSPACE_DIR);\n        if (workDir == null) {\n            return chain.process(payload);\n        }\n\n        UUID projectId = payload.getHeader(Payload.PROJECT_ID);\n\n        try {\n            String runtime = getRuntimeType(payload);\n            if (!projectLoader.supports(runtime)) {\n                throw new ConcordApplicationException(\"Unsupported runtime type: \" + runtime, Response.Status.BAD_REQUEST);\n            }\n\n            ProjectLoader.Result result = projectLoader.loadProject(workDir, runtime, importsNormalizer.forProject(projectId),\n                    new ProcessImportsListener(processKey));\n\n            List<Snapshot> snapshots = result.snapshots();\n            payload = PayloadUtils.addSnapshots(payload, snapshots);\n\n            ProcessDefinition pd = result.projectDefinition();\n\n            // grab \"dependencies\" and add all \"extraDependencies\" from the active profiles\n            List<String> allDependencies = new ArrayList<>(pd.configuration().dependencies());\n            List<String> activeProfiles = payload.getHeader(Payload.ACTIVE_PROFILES, Collections.emptyList());\n            activeProfiles.stream().flatMap(profileName -> Stream.ofNullable(pd.profiles().get(profileName)))\n                    .forEach(profile -> allDependencies.addAll(profile.configuration().extraDependencies()));\n\n            int depsCount = allDependencies.size();\n            if (depsCount > MAX_DEPENDENCIES_COUNT) {\n                String msg = String.format(\"Too many dependencies. Current: %d, maximum allowed: %d\", depsCount, MAX_DEPENDENCIES_COUNT);\n                throw new ConcordApplicationException(msg, Response.Status.BAD_REQUEST);\n            }\n\n            payload = payload.putHeader(Payload.PROJECT_DEFINITION, pd)\n                    .putHeader(Payload.RUNTIME, pd.runtime())\n                    .putHeader(Payload.IMPORTS, pd.imports())\n                    .putHeader(Payload.DEPENDENCIES, allDependencies);\n\n            // save the runtime type in the process configuration\n            Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION, Collections.emptyMap());\n            cfg = new HashMap<>(cfg); // make mutable\n            cfg.put(Constants.Request.RUNTIME_KEY, runtime);\n            payload = payload.putHeader(Payload.CONFIGURATION, cfg);\n        } catch (ImportProcessingException e) {\n            throw new ProcessException(processKey, \"Error while processing import \" + e.getImport() + \". Error: \" + e.getMessage(), e);\n        } catch (Exception e) {\n            log.warn(\"process -> ({}) project loading error: {}\", workDir, e.getMessage());\n            throw new ProcessException(processKey, \"Error while loading the project, check the syntax. \" + e.getMessage(), e);\n        }\n        return chain.process(payload);\n    }\n\n    /**\n     * Returns the runtime type for the specified payload.\n     * <p/>\n     * The method looks for the process configuration's {@code runtime} key first\n     * and then into the root concord.yml's {@code configuration.runtime} value.\n     */\n    private static String getRuntimeType(Payload payload) throws IOException {\n        Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION);\n        if (cfg != null) {\n            String s = MapUtils.getString(cfg, Constants.Request.RUNTIME_KEY);\n            if (s != null) {\n                return s;\n            }\n        }\n\n        Path workDir = payload.getHeader(Payload.WORKSPACE_DIR);\n        return ProjectLoaderUtils.getRuntimeType(workDir).orElse(CONCORD_V1_RUNTIME_TYPE);\n    }\n\n    class ProcessImportsListener implements ImportsListener {\n\n        private final ProcessKey processKey;\n\n        ProcessImportsListener(ProcessKey processKey) {\n            this.processKey = processKey;\n        }\n\n        @Override\n        public void beforeImport(Import i) {\n            logManager.info(processKey, \"Processing import {}\", i);\n        }\n\n        @Override\n        public void onEnd(List<Import> items) {\n            logManager.info(processKey, \"All imports processed\");\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/ProcessHandlersProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.ProcessDefinition;\nimport com.walmartlabs.concord.runtime.model.Profile;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.Payload;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * Determines whether the process has onFailure/onCancel/onTimeout/etc handlers\n * configured. If there is a relevant flow defined and it is not disabled in\n * the process arguments, it will be added to the list of the process' handlers.\n */\npublic class ProcessHandlersProcessor implements PayloadProcessor {\n\n    private static final Logger log = LoggerFactory.getLogger(ProcessHandlersProcessor.class);\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        Set<String> handlers = new HashSet<>();\n\n        ProcessDefinition pd = payload.getHeader(Payload.PROJECT_DEFINITION);\n        if (pd == null) {\n            return chain.process(payload);\n        }\n\n        Collection<String> profiles = payload.getHeader(Payload.ACTIVE_PROFILES);\n\n        update(payload, pd, profiles, handlers, Constants.Flows.ON_FAILURE_FLOW, Constants.Request.DISABLE_ON_FAILURE_KEY);\n        update(payload, pd, profiles, handlers, Constants.Flows.ON_CANCEL_FLOW, Constants.Request.DISABLE_ON_CANCEL_KEY);\n        update(payload, pd, profiles, handlers, Constants.Flows.ON_TIMEOUT_FLOW, Constants.Request.DISABLE_ON_TIMEOUT_KEY);\n\n        payload = payload.putHeader(Payload.PROCESS_HANDLERS, handlers);\n\n        return chain.process(payload);\n    }\n\n    private static void update(Payload payload, ProcessDefinition pd, Collection<String> profiles, Set<String> handlers, String flow, String disableFlag) {\n        if (!hasFlow(pd, profiles, flow)) {\n            return;\n        }\n\n        boolean suppressed = getBoolean(payload, disableFlag);\n        if (suppressed) {\n            log.debug(\"process -> {} is suppressed, skipping...\", flow);\n        } else {\n            handlers.add(flow);\n            log.debug(\"process -> added {} handler\", flow);\n        }\n    }\n\n    private static boolean hasFlow(ProcessDefinition pd, Collection<String> activeProfiles, String flow) {\n        if (pd.flows() != null && pd.flows().containsKey(flow)) {\n            return true;\n        }\n\n        for (String activeProfile : activeProfiles) {\n            Profile profile = pd.profiles().get(activeProfile);\n            if (profile != null && profile.flows().containsKey(flow)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    private static boolean getBoolean(Payload payload, String key) {\n        Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION);\n        if (cfg == null) {\n            return false;\n        }\n\n        Object v = cfg.get(key);\n        if (v == null) {\n            return false;\n        }\n\n        return (Boolean) v;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/RawPayloadPolicyProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.policyengine.CheckResult;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.policyengine.RawPayloadRule;\nimport com.walmartlabs.concord.server.policy.PolicyManager;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.core.Response;\nimport java.text.MessageFormat;\nimport java.util.List;\nimport java.util.Objects;\n\npublic class RawPayloadPolicyProcessor implements PayloadProcessor{\n\n    private static final String DEFAULT_POLICY_MESSAGE = \"Raw payload size too big: current {0} byte(s), limit {1} byte(s)\";\n    \n    private static final Logger log = LoggerFactory.getLogger(RawPayloadPolicyProcessor.class);\n\n    private final PolicyManager policyManager;\n    \n    @Inject\n    public RawPayloadPolicyProcessor(PolicyManager policyManager) {\n        this.policyManager = policyManager;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        ProcessKey processKey = payload.getProcessKey();\n        \n        PolicyEngine policy = policyManager.getPolicyEngine(payload);\n        if (policy == null) {\n            return chain.process(payload);\n        }\n\n        if (payload.getAttachment(Payload.WORKSPACE_ARCHIVE) == null) {\n            return chain.process(payload);\n        }\n        \n        CheckResult<RawPayloadRule, Long> result;\n        try {\n            result = policy.getRawPayloadPolicy()\n                    .check(payload.getAttachment(Payload.WORKSPACE_ARCHIVE));\n        } catch (Exception e) {\n            log.error(\"process -> error\", e);\n            throw new ProcessException(processKey, \"Found raw payload policy check error\", e);\n        }\n\n        if (!result.getDeny().isEmpty()) {\n            throw new ProcessException(processKey, String.format(\"Found raw payload policy violations: %s\", buildErrorMessage(result.getDeny())), Response.Status.BAD_REQUEST);\n        }\n\n        return chain.process(payload);\n    }\n\n    private String buildErrorMessage(List<CheckResult.Item<RawPayloadRule, Long>> errors) {\n        StringBuilder sb = new StringBuilder();\n        for (CheckResult.Item<RawPayloadRule, Long> e : errors) {\n            RawPayloadRule r = e.getRule();\n\n            String msg = r.msg() != null ? r.msg() : DEFAULT_POLICY_MESSAGE;\n            long actualCount = e.getEntity();\n            long limit = r.maxSizeInBytes();\n\n            sb.append(MessageFormat.format(Objects.requireNonNull(msg), actualCount, limit)).append(';');\n        }\n        return sb.toString();\n    }\n    \n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/RepositoryInfoUpdateProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueDao;\n\nimport javax.inject.Inject;\n\nimport static com.walmartlabs.concord.server.process.pipelines.processors.RepositoryProcessor.REPOSITORY_INFO_KEY;\n\n/**\n * Updates the process queue entry according to the current process' repository data.\n */\npublic class RepositoryInfoUpdateProcessor implements PayloadProcessor {\n\n    private final ProcessQueueDao queueDao;\n\n    @Inject\n    public RepositoryInfoUpdateProcessor(ProcessQueueDao queueDao) {\n        this.queueDao = queueDao;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        RepositoryProcessor.RepositoryInfo i = payload.getHeader(REPOSITORY_INFO_KEY);\n        if (i == null) {\n            return chain.process(payload);\n        }\n\n        String commitId = i.getCommitId();\n        String commitBranch = i.getBranch();\n\n        RepositoryProcessor.CommitInfo ci = i.getCommitInfo();\n        if (ci != null) {\n            commitId = ci.getId();\n            commitBranch = ci.getBranch();\n        }\n\n        queueDao.updateRepositoryDetails(payload.getProcessKey(), i.getId(), i.getUrl(), i.getPath(), commitId, commitBranch);\n\n        return chain.process(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/RepositoryProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.repository.FetchResult;\nimport com.walmartlabs.concord.repository.Repository;\nimport com.walmartlabs.concord.repository.Snapshot;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport com.walmartlabs.concord.server.org.project.RepositoryDao;\nimport com.walmartlabs.concord.server.org.project.RepositoryEntry;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.keys.HeaderKey;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.repository.RepositoryManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.UUID;\n\n/**\n * Adds repository files to a payload.\n */\npublic class RepositoryProcessor implements PayloadProcessor {\n\n    private static final Logger log = LoggerFactory.getLogger(RepositoryProcessor.class);\n\n    /**\n     * Repository effective parameters.\n     */\n    public static final HeaderKey<RepositoryInfo> REPOSITORY_INFO_KEY = HeaderKey.register(\"_repositoryInfo\", RepositoryInfo.class);\n\n    private final RepositoryDao repositoryDao;\n    private final RepositoryManager repositoryManager;\n    private final ProcessLogManager logManager;\n\n    @Inject\n    public RepositoryProcessor(RepositoryDao repositoryDao,\n                               RepositoryManager repositoryManager,\n                               ProcessLogManager logManager) {\n\n        this.repositoryDao = repositoryDao;\n        this.repositoryManager = repositoryManager;\n        this.logManager = logManager;\n    }\n\n    @Override\n    @WithTimer\n    public Payload process(Chain chain, final Payload payload) {\n        ProcessKey processKey = payload.getProcessKey();\n\n        UUID projectId = payload.getHeader(Payload.PROJECT_ID);\n        RepositoryEntry repo = getRepositoryEntry(payload);\n        if (projectId == null || repo == null) {\n            return chain.process(payload);\n        }\n\n        logManager.info(processKey, \"Copying the repository's data: {} @ {}:{}, path: {}\",\n                repo.getUrl(),\n                repo.getBranch() != null ? repo.getBranch() : \"*\",\n                repo.getCommitId() != null ? repo.getCommitId() : \"head\",\n                repo.getPath() != null ? repo.getPath() : \"/\");\n\n        Path dst = payload.getHeader(Payload.WORKSPACE_DIR);\n\n        Payload newPayload = repositoryManager.withLock(repo.getUrl(), () -> {\n            try {\n                Repository repository = payload.getHeader(Payload.REPOSITORY);\n                if (repository == null) {\n                    repository = repositoryManager.fetch(projectId, repo, true);\n                }\n\n                Snapshot snapshot = repository.export(dst);\n\n                CommitInfo ci = null;\n                if (repository.fetchResult() != null) {\n                    FetchResult r = Objects.requireNonNull(repository.fetchResult());\n                    ci = new CommitInfo(r.head(), r.branchOrTag(), r.author(), r.message());\n                }\n                \n                RepositoryInfo i = new RepositoryInfo(repo.getId(), repo.getName(), repo.getUrl(), repo.getPath(), repo.getBranch(), repo.getCommitId(), ci);\n                return payload\n                        .putHeader(REPOSITORY_INFO_KEY, i)\n                        .putHeader(Payload.REPOSITORY, repository)\n                        .putHeader(Payload.REPOSITORY_SNAPSHOT, Collections.singletonList(snapshot));\n            } catch (Exception e) {\n                log.error(\"process -> repository error\", e);\n                logManager.error(processKey, \"Error while processing a repository: \" + repo.getUrl(), e);\n                throw new ProcessException(processKey, \"Error while processing a repository: \" + repo.getUrl(), e);\n            }\n        });\n\n        return chain.process(newPayload);\n    }\n\n    private RepositoryEntry getRepositoryEntry(Payload payload) {\n        UUID projectId = payload.getHeader(Payload.PROJECT_ID);\n        UUID repoId = payload.getHeader(Payload.REPOSITORY_ID);\n\n        if (projectId == null || repoId == null) {\n            return null;\n        }\n\n        RepositoryEntry repo = repositoryDao.get(projectId, repoId);\n        if (repo == null) {\n            return null;\n        }\n\n        Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION);\n        if (cfg != null) {\n            String commitId = MapUtils.getString(cfg, Constants.Request.REPO_COMMIT_ID, repo.getCommitId());\n            String branchOrTag = MapUtils.getString(cfg, Constants.Request.REPO_BRANCH_OR_TAG, commitId == null ? repo.getBranch() : null);\n            repo = new RepositoryEntry(repo, branchOrTag, commitId);\n        }\n\n        return repo;\n    }\n\n    public static final class RepositoryInfo implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        private final UUID id;\n        private final String name;\n        private final String url;\n        private final String path;\n        private final String branch;\n        private final String commitId;\n        private final CommitInfo commitInfo;\n\n        public RepositoryInfo(UUID id,\n                              String name,\n                              String url,\n                              String path,\n                              String branch,\n                              String commitId,\n                              CommitInfo commitInfo) {\n\n            this.id = id;\n            this.name = name;\n            this.url = url;\n            this.path = path;\n            this.branch = branch;\n            this.commitId = commitId;\n            this.commitInfo = commitInfo;\n        }\n\n        public UUID getId() {\n            return id;\n        }\n\n        public String getName() {\n            return name;\n        }\n\n        public String getUrl() {\n            return url;\n        }\n\n        public String getPath() {\n            return path;\n        }\n\n        public String getBranch() {\n            return branch;\n        }\n\n        public String getCommitId() {\n            return commitId;\n        }\n\n        public CommitInfo getCommitInfo() {\n            return commitInfo;\n        }\n\n        @Override\n        public String toString() {\n            return \"RepositoryInfo{\" +\n                    \"id=\" + id +\n                    \", name='\" + name + '\\'' +\n                    \", url='\" + url + '\\'' +\n                    \", path='\" + path + '\\'' +\n                    \", branch='\" + branch + '\\'' +\n                    \", commitId='\" + commitId + '\\'' +\n                    \", commitInfo=\" + commitInfo +\n                    '}';\n        }\n    }\n\n    public static final class CommitInfo implements Serializable {\n\n        private static final long serialVersionUID = 1L;\n\n        private final String id;\n        private final String branch;\n        private final String author;\n        private final String message;\n\n        public CommitInfo(String id, String branch, String author, String message) {\n            this.id = id;\n            this.branch = branch;\n            this.author = author;\n            this.message = message;\n        }\n\n        public String getId() {\n            return id;\n        }\n\n        public String getBranch() {\n            return branch;\n        }\n\n        public String getAuthor() {\n            return author;\n        }\n\n        public String getMessage() {\n            return message;\n        }\n\n        @Override\n        public String toString() {\n            return \"CommitInfo{\" +\n                    \"id='\" + id + '\\'' +\n                    \", branch='\" + branch + '\\'' +\n                    \", author='\" + author + '\\'' +\n                    \", message='\" + message + '\\'' +\n                    '}';\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/RequestParametersProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.Payload;\n\nimport javax.servlet.http.HttpServletRequest;\nimport javax.ws.rs.core.HttpHeaders;\nimport java.util.*;\n\n/**\n * Responsible for gathering process request parameters and converting\n * them into the process configuration.\n */\npublic class RequestParametersProcessor implements PayloadProcessor {\n\n    private static final Set<String> EXCLUDED_HEADERS = new HashSet<>(Arrays.asList(HttpHeaders.COOKIE, HttpHeaders.AUTHORIZATION));\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        HttpServletRequest request = payload.getHeader(Payload.SERVLET_REQUEST);\n\n        // configuration values from the servlet request\n        // remove the servlet request from the payload as it is not serializable\n        Map<String, Object> requestCfg = parseRequest(request);\n\n        Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION);\n        if (cfg == null) {\n            cfg = new HashMap<>();\n        }\n\n        Map<String, Object> m = ConfigurationUtils.deepMerge(cfg, requestCfg);\n        payload = payload.putHeader(Payload.CONFIGURATION, m);\n\n        // one off operation, remove the request object when we done with it\n        payload = payload.removeHeader(Payload.SERVLET_REQUEST);\n\n        return chain.process(payload);\n    }\n\n    private static Map<String, Object> parseRequest(HttpServletRequest request) {\n        Map<String, String[]> params = Collections.emptyMap();\n        if (request != null) {\n            params = request.getParameterMap();\n        }\n\n        Map<String, Object> args = new HashMap<>();\n        for (Map.Entry<String, String[]> e : params.entrySet()) {\n            String k = e.getKey();\n            if (!k.startsWith(\"arguments.\")) {\n                continue;\n            }\n            k = k.substring(\"arguments.\".length());\n\n            Object v = e.getValue();\n            if (e.getValue().length == 1) {\n                v = e.getValue()[0];\n            }\n\n            Map<String, Object> m = ConfigurationUtils.toNested(k, v);\n            args = ConfigurationUtils.deepMerge(args, m);\n        }\n\n        args.put(Constants.Request.REQUEST_INFO_KEY, getRequestInfo(request));\n\n        return Collections.singletonMap(Constants.Request.ARGUMENTS_KEY, args);\n    }\n\n    private static Map<String, Object> getRequestInfo(HttpServletRequest request) {\n        if (request == null) {\n            return Collections.emptyMap();\n        }\n\n        String queryString = request.getQueryString() == null ? \"\" : \"?\" + request.getQueryString();\n\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"remoteIp\", request.getRemoteAddr());\n        m.put(\"uri\", request.getRequestURL() + queryString);\n        m.put(\"headers\", getHeaders(request));\n        m.put(\"query\", getQueryParams(request));\n        return m;\n    }\n\n    private static Map<String, Object> getQueryParams(HttpServletRequest request) {\n        Map<String, Object> queryParams = new HashMap<>();\n        for (Map.Entry<String, String[]> e : request.getParameterMap().entrySet()) {\n            String k = e.getKey();\n\n            Object v = e.getValue();\n            if (e.getValue().length == 1) {\n                v = e.getValue()[0];\n            }\n\n            queryParams.put(k, v);\n        }\n        return queryParams;\n    }\n\n    private static Map<String, Object> getHeaders(HttpServletRequest request) {\n        Map<String, Object> headers = new HashMap<>();\n        List<String> headerNames = Collections.list(request.getHeaderNames());\n\n        for (String h : headerNames) {\n            if (EXCLUDED_HEADERS.contains(h)) {\n                continue;\n            }\n\n            List<String> headerList = Collections.list(request.getHeaders(h));\n            if (headerList.size() == 1) {\n                headers.put(h, headerList.get(0));\n            } else {\n                headers.put(h, headerList);\n            }\n        }\n\n        return headers;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/RestoredPayloadValidationProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.org.project.RepositoryDao;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\n\nimport javax.inject.Inject;\n\n/**\n * Validates that the IDs restored from the initial state still reference\n * existing entities. This catches cases where a project or repository\n * was deleted/recreated between the original run and a restart.\n */\npublic class RestoredPayloadValidationProcessor implements PayloadProcessor {\n\n    private final ProjectDao projectDao;\n    private final RepositoryDao repositoryDao;\n\n    @Inject\n    public RestoredPayloadValidationProcessor(ProjectDao projectDao,\n                                              RepositoryDao repositoryDao) {\n        this.projectDao = projectDao;\n        this.repositoryDao = repositoryDao;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        var processKey = payload.getProcessKey();\n\n        var projectId = payload.getHeader(Payload.PROJECT_ID);\n        var repoId = payload.getHeader(Payload.REPOSITORY_ID);\n\n        if (projectId != null) {\n            var project = projectDao.get(projectId);\n            if (project == null) {\n                throw new ProcessException(processKey,\n                        \"Project not found: \" + projectId + \". It may have been deleted or recreated since the process was originally started.\");\n            }\n        }\n\n        if (projectId != null && repoId != null) {\n            var repo = repositoryDao.get(projectId, repoId);\n            if (repo == null) {\n                throw new ProcessException(processKey,\n                        \"Repository not found: \" + repoId + \" in project \" + projectId\n                                + \". It may have been deleted or recreated since the process was originally started.\");\n            }\n        }\n\n        return chain.process(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/ResumeConfigurationProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\n\nimport javax.ws.rs.core.Response.Status;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.Map;\n\npublic class ResumeConfigurationProcessor implements PayloadProcessor {\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        // configuration from the user's request\n        Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION, Collections.emptyMap());\n\n        // _main.json file in the workspace\n        Map<String, Object> workspaceCfg = getWorkspaceCfg(payload);\n\n        // we'll use the arguments only from the request\n        workspaceCfg.remove(Constants.Request.ARGUMENTS_KEY);\n\n        // TODO automatically provided variables\n\n        // TODO overrides from policies\n\n        // create the resulting configuration\n        Map<String, Object> m = ConfigurationUtils.deepMerge(workspaceCfg, cfg);\n        payload = payload.putHeader(Payload.CONFIGURATION, m);\n\n        return chain.process(payload);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Map<String, Object> getWorkspaceCfg(Payload payload) {\n        Path workspace = payload.getHeader(Payload.WORKSPACE_DIR);\n        Path src = workspace.resolve(Constants.Files.CONFIGURATION_FILE_NAME);\n        if (!Files.exists(src)) {\n            return Collections.emptyMap();\n        }\n\n        try (InputStream in = Files.newInputStream(src)) {\n            ObjectMapper om = new ObjectMapper();\n            return om.readValue(in, Map.class);\n        } catch (IOException e) {\n            throw new ProcessException(payload.getProcessKey(), \"Invalid request data format\", e, Status.BAD_REQUEST);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/ResumeEventsProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\n\nimport javax.inject.Inject;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\n\npublic class ResumeEventsProcessor implements PayloadProcessor {\n\n    private final ProcessLogManager logManager;\n\n    @Inject\n    public ResumeEventsProcessor(ProcessLogManager logManager) {\n        this.logManager = logManager;\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public Payload process(Chain chain, Payload payload) {\n        Set<String> events = payload.getHeader(Payload.RESUME_EVENTS, Collections.emptySet());\n        if (events.isEmpty()) {\n            return chain.process(payload);\n        }\n\n        Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION, Collections.emptyMap());\n        if (cfg == null) {\n            cfg = new HashMap<>();\n        }\n\n        Map<String, Object> args = (Map<String, Object>) cfg.get(Constants.Request.ARGUMENTS_KEY);\n        if (args == null) {\n            args = new HashMap<>();\n            cfg.put(Constants.Request.ARGUMENTS_KEY, args);\n        }\n\n        args.put(Constants.Request.RESUME_EVENTS_KEY, events);\n        payload = payload.putHeader(Payload.CONFIGURATION, cfg);\n\n        String old = (String) args.get(Constants.Request.EVENT_NAME_KEY);\n        if (old != null) {\n            // don't overwrite the existing value for backward-compatibility reasons\n            logManager.warn(payload.getProcessKey(), \"Can't overwrite the system variable '{}', the value already exists.\", Constants.Request.EVENT_NAME_KEY);\n            return chain.process(payload);\n        }\n\n        if (events.size() == 1) {\n            args.put(Constants.Request.EVENT_NAME_KEY, events.iterator().next());\n        }\n\n        payload = payload.putHeader(Payload.CONFIGURATION, cfg);\n\n        return chain.process(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/ResumeMarkerStoringProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.Set;\n\n/**\n * Adds an event marker to the payload.\n * <p>\n * The event marker file is used by the runtime to determine whether the process\n * should be started using a previously saved state snapshot.\n */\npublic class ResumeMarkerStoringProcessor implements PayloadProcessor {\n\n    private final ProcessLogManager logManager;\n\n    @Inject\n    public ResumeMarkerStoringProcessor(ProcessLogManager logManager) {\n        this.logManager = logManager;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        ProcessKey processKey = payload.getProcessKey();\n\n        Set<String> events = payload.getHeader(Payload.RESUME_EVENTS, Collections.emptySet());\n        if (events.isEmpty()) {\n            return chain.process(payload);\n        }\n\n        Path workspace = payload.getHeader(Payload.WORKSPACE_DIR);\n        Path stateDir = workspace.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME)\n                .resolve(Constants.Files.JOB_STATE_DIR_NAME);\n\n        try {\n            if (!Files.exists(stateDir)) {\n                Files.createDirectories(stateDir);\n            }\n\n            Path resumeMarker = stateDir.resolve(Constants.Files.RESUME_MARKER_FILE_NAME);\n            Files.write(resumeMarker, events);\n        } catch (IOException e) {\n            logManager.error(processKey, \"Error while saving resume event\", e);\n            throw new ProcessException(processKey, \"Error while saving resume event\", e);\n        }\n\n        return chain.process(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/ResumeProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\n\n/**\n * Moves the process into ENQUEUED status if current process eq RESUMING.\n */\npublic class ResumeProcessor implements PayloadProcessor {\n\n    private static final Logger log = LoggerFactory.getLogger(ResumeProcessor.class);\n\n    private final ProcessQueueManager queueManager;\n\n    @Inject\n    public ResumeProcessor(ProcessQueueManager queueManager) {\n        this.queueManager = queueManager;\n    }\n\n    @Override\n    @WithTimer\n    public Payload process(Chain chain, Payload payload) {\n        ProcessKey processKey = payload.getProcessKey();\n\n        boolean updated = queueManager.updateExpectedStatus(processKey, ProcessStatus.RESUMING, ProcessStatus.ENQUEUED);\n        if (updated) {\n            return chain.process(payload);\n        }\n\n        log.warn(\"process ['{}'] -> process is not suspended, can't resume\", processKey);\n        throw new InvalidProcessStateException(\"Process is not suspended, can't resume\");\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/ResumingHooksProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\n\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Executes resume hooks.\n * <p/>\n * Resume hooks are used to execute additional logic when the process is resumed.\n * Hooks are typically added during the creation of the {@link Payload}.\n *\n * @see Payload#RESUME_HOOKS\n */\npublic class ResumingHooksProcessor implements PayloadProcessor {\n\n    @Override\n    @WithTimer\n    public Payload process(Chain chain, Payload payload) {\n        List<Runnable> hooks = payload.getHeader(Payload.RESUME_HOOKS, Collections.emptyList());\n        hooks.forEach(Runnable::run);\n        return chain.process(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/ResumingProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\n\n/**\n * Moves the process into RESUMING status if current process eq SUSPENDED.\n */\npublic class ResumingProcessor implements PayloadProcessor {\n\n    private static final Logger log = LoggerFactory.getLogger(ResumingProcessor.class);\n\n    private final ProcessQueueManager queueManager;\n\n    @Inject\n    public ResumingProcessor(ProcessQueueManager queueManager) {\n        this.queueManager = queueManager;\n    }\n\n    @Override\n    @WithTimer\n    public Payload process(Chain chain, Payload payload) {\n        ProcessKey processKey = payload.getProcessKey();\n\n        boolean updated = queueManager.updateExpectedStatus(processKey, ProcessStatus.SUSPENDED, ProcessStatus.RESUMING);\n        if (updated) {\n            return chain.process(payload);\n        }\n\n        log.warn(\"process ['{}'] -> process is not suspended, can't resume\", processKey);\n        throw new InvalidProcessStateException(\"Process is not suspended, can't resume\");\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/RunAsCurrentProcessUserProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessSecurityContext;\n\nimport javax.inject.Inject;\n\npublic class RunAsCurrentProcessUserProcessor implements PayloadProcessor {\n\n    private final ProcessSecurityContext securityContext;\n\n    @Inject\n    public RunAsCurrentProcessUserProcessor(ProcessSecurityContext securityContext) {\n        this.securityContext = securityContext;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        try {\n            return securityContext.runAsCurrentUser(payload.getProcessKey(), () -> chain.process(payload));\n        } catch (RuntimeException e) {\n            throw e;\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/SecuritySubjectProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessSecurityContext;\n\nimport javax.inject.Inject;\n\npublic class SecuritySubjectProcessor implements PayloadProcessor {\n\n    private final ProcessSecurityContext securityContext;\n\n    @Inject\n    public SecuritySubjectProcessor(ProcessSecurityContext securityContext) {\n        this.securityContext = securityContext;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        securityContext.storeCurrentSubject(payload.getProcessKey());\n        return chain.process(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/SessionTokenProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.SessionTokenCreator;\n\nimport javax.inject.Inject;\n\n/**\n * Generates and saves the process' session token (key).\n */\npublic class SessionTokenProcessor implements PayloadProcessor {\n\n    private final SessionTokenCreator sessionTokenCreator;\n\n    @Inject\n    public SessionTokenProcessor(SessionTokenCreator sessionTokenCreator) {\n        this.sessionTokenCreator = sessionTokenCreator;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        String token = sessionTokenCreator.create(payload.getProcessKey());\n        payload = payload.putHeader(Payload.SESSION_TOKEN, token);\n        return chain.process(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/StateImportingProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.repository.Snapshot;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.state.ProcessStateManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\n\nimport javax.inject.Inject;\nimport java.nio.file.Files;\nimport java.nio.file.LinkOption;\nimport java.nio.file.Path;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.util.List;\nimport java.util.ListIterator;\n\npublic class StateImportingProcessor implements PayloadProcessor {\n\n    private static final String PASS_THROUGH_PATH = \"forms/\";\n\n    private final ProcessStateManager stateManager;\n\n    @Inject\n    public StateImportingProcessor(ProcessStateManager stateManager) {\n        this.stateManager = stateManager;\n    }\n\n    @Override\n    @WithTimer\n    public Payload process(Chain chain, Payload payload) {\n        ProcessKey processKey = payload.getProcessKey();\n        Path workspace = payload.getHeader(Payload.WORKSPACE_DIR);\n        List<Snapshot> snapshots = payload.getHeader(Payload.REPOSITORY_SNAPSHOT);\n        stateManager.replacePath(processKey, workspace, (p, attrs) -> filter(p, attrs, snapshots, workspace));\n\n        return chain.process(payload);\n    }\n\n    /**\n     * Return {@code true} for each {@code p} that must be included into the process state.\n     */\n    private boolean filter(Path p, BasicFileAttributes attrs, List<Snapshot> snapshots, Path workDir) {\n        // those files we need to store in the DB regardless of whether they are from a repository or not\n        // e.g. custom forms: we need those files in the DB in order to serve custom form files\n        if (workDir.relativize(p).toString().startsWith(PASS_THROUGH_PATH)) {\n            return true;\n        }\n\n        Path resolved = p;\n        if (!p.isAbsolute()) {\n            resolved = workDir.resolve(p).normalize();\n        }\n\n        // we never store symlinks in the DB\n        if (!Files.isRegularFile(resolved, LinkOption.NOFOLLOW_LINKS)) {\n            return false;\n        }\n\n        if (snapshots != null) {\n            ListIterator<Snapshot> it = snapshots.listIterator(snapshots.size());\n            while (it.hasPrevious()) {\n                Snapshot s = it.previous();\n                if (s.contains(p)) {\n                    return s.isModified(p, attrs);\n                }\n            }\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/TagsExtractingProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\n\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic class TagsExtractingProcessor implements PayloadProcessor {\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION);\n\n        Object v = cfg.get(Constants.Request.TAGS_KEY);\n        if (v == null) {\n            return chain.process(payload);\n        }\n\n        Set<String> tags = parse(payload.getProcessKey(), v);\n        payload = payload.putHeader(Payload.PROCESS_TAGS, tags);\n\n        return chain.process(payload);\n    }\n\n    @SuppressWarnings(\"rawtypes\")\n    private static Set<String> parse(ProcessKey pk, Object v) {\n        if (v instanceof String) {\n            String[] as = ((String) v).split(\",\");\n            return trim(as);\n        } else if (v instanceof String[]) {\n            String[] as = (String[]) v;\n            return trim(as);\n        } else if (v instanceof Collection) {\n            Set<String> result = new HashSet<>();\n\n            Collection c = (Collection) v;\n            for (Object o : c) {\n                if (!(o instanceof String)) {\n                    throw new ProcessException(pk, \"Process tag must be a string value: \" + o);\n                }\n\n                result.add((String) o);\n            }\n\n            return result.stream().map(String::trim).collect(Collectors.toSet());\n        } else {\n            throw new ProcessException(pk, \"Process tags must be an array of string values: \" + v);\n        }\n    }\n\n    private static Set<String> trim(String[] as) {\n        return Arrays.stream(as).map(String::trim).collect(Collectors.toSet());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/TemplateFilesProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ZipUtils;\nimport com.walmartlabs.concord.dependencymanager.DependencyManager;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.template.TemplateAliasDao;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.nio.file.Path;\nimport java.util.Map;\nimport java.util.Optional;\n\n/**\n * Extracts template files into the workspace.\n */\npublic class TemplateFilesProcessor implements PayloadProcessor {\n\n    private final ProcessLogManager logManager;\n    private final DependencyManager dependencyManager;\n    private final TemplateAliasDao aliasDao;\n\n    @Inject\n    public TemplateFilesProcessor(DependencyManager dependencyManager,\n                                  ProcessLogManager logManager,\n                                  TemplateAliasDao aliasDao) {\n\n        this.logManager = logManager;\n        this.aliasDao = aliasDao;\n        this.dependencyManager = dependencyManager;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        ProcessKey processKey = payload.getProcessKey();\n        Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION);\n\n        Object s = cfg.get(Constants.Request.TEMPLATE_KEY);\n        if (!(s instanceof String)) {\n            return chain.process(payload);\n        }\n\n        try {\n            URI uri = getUri(processKey, (String) s);\n            Path template = dependencyManager.resolveSingle(uri).getPath();\n            extract(payload, template);\n\n            return chain.process(payload);\n        } catch (URISyntaxException | IOException e) {\n            logManager.error(processKey, \"Template error: \" + s, e);\n            throw new ProcessException(processKey, \"Error while processing a template: \" + s, e);\n        }\n    }\n\n    private URI getUri(PartialProcessKey processKey, String template) throws URISyntaxException {\n        try {\n            URI u = new URI(template);\n\n            String scheme = u.getScheme();\n            // doesn't look like a URI, let's try find an alias\n            if (scheme == null || scheme.trim().isEmpty()) {\n                return getByAlias(processKey, template);\n            }\n\n            return u;\n        } catch (URISyntaxException e) {\n            return getByAlias(processKey, template);\n        }\n    }\n\n    private URI getByAlias(PartialProcessKey processKey, String s) throws URISyntaxException {\n        Optional<String> o = aliasDao.get(s);\n        if (!o.isPresent()) {\n            throw new ProcessException(processKey, \"Invalid template URL or alias: \" + s);\n        }\n\n        return new URI(o.get());\n    }\n\n    private void extract(Payload payload, Path template) throws IOException {\n        Path workspacePath = payload.getHeader(Payload.WORKSPACE_DIR);\n\n        // copy template's files to the payload, skipping the existing files\n        ZipUtils.unzip(template, workspacePath, true);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/TemplateScriptProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport com.walmartlabs.concord.common.CycleChecker;\nimport com.walmartlabs.concord.server.cfg.TemplatesConfiguration;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport org.graalvm.polyglot.Context;\nimport org.graalvm.polyglot.HostAccess;\nimport org.graalvm.polyglot.Value;\n\nimport javax.inject.Inject;\nimport javax.script.Bindings;\nimport javax.script.ScriptEngine;\nimport javax.script.ScriptException;\nimport java.io.FileReader;\nimport java.io.IOException;\nimport java.io.Reader;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\n\npublic class TemplateScriptProcessor implements PayloadProcessor {\n\n    public static final String REQUEST_DATA_TEMPLATE_FILE_NAME = \"_main.js\";\n    public static final String INPUT_REQUEST_DATA_KEY = \"_input\";\n\n    private final ProcessLogManager logManager;\n    private final ScriptEngine scriptEngine;\n\n    @Inject\n    public TemplateScriptProcessor(ProcessLogManager logManager, TemplatesConfiguration cfg) {\n        this.logManager = logManager;\n        this.scriptEngine = cfg.isAllowScripting() ? createScriptEngine() : null;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        if (scriptEngine == null) {\n            return chain.process(payload);\n        }\n\n        Path workspace = payload.getHeader(Payload.WORKSPACE_DIR);\n\n        // process _main.js\n        Path scriptPath = workspace.resolve(REQUEST_DATA_TEMPLATE_FILE_NAME);\n        if (!Files.exists(scriptPath)) {\n            return chain.process(payload);\n        }\n\n        ProcessKey processKey = payload.getProcessKey();\n\n        Map<String, Object> in = payload.getHeader(Payload.CONFIGURATION);\n        Map<String, Object> out = processScript(processKey, in, scriptPath);\n\n        Map<String, Object> merged = ConfigurationUtils.deepMerge(in, out);\n\n        CycleChecker.CheckResult result = CycleChecker.check(INPUT_REQUEST_DATA_KEY, merged);\n        if (result.isHasCycle()) {\n            throw new ProcessException(processKey, \"Found cycle in \" + REQUEST_DATA_TEMPLATE_FILE_NAME + \": \" +\n                                                   result.getNode1() + \" <-> \" + result.getNode2());\n        }\n        payload = payload.putHeader(Payload.CONFIGURATION, merged);\n\n        return chain.process(payload);\n    }\n\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    private ScriptEngine createScriptEngine() {\n        // workaround for:\n        // Javascript array is converted in Java to an empty map #214 (https://github.com/oracle/graaljs/issues/214)\n        HostAccess access = HostAccess.newBuilder(HostAccess.ALL)\n                .targetTypeMapping(Value.class, Object.class, Value::hasArrayElements, v -> new LinkedList<>(v.as(List.class))).build();\n        return GraalJSScriptEngine.create(null,\n                Context.newBuilder(\"js\")\n                        .allowHostAccess(access));\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private Map<String, Object> processScript(ProcessKey processKey, Map<String, Object> meta, Path templateMeta) {\n        Object result;\n        try (Reader r = new FileReader(templateMeta.toFile())) {\n            Bindings b = scriptEngine.createBindings();\n            b.put(\"polyglot.js.allowAllAccess\", true);\n            b.put(INPUT_REQUEST_DATA_KEY, meta != null ? meta : Collections.emptyMap());\n\n            result = scriptEngine.eval(r, b);\n            if (!(result instanceof Map)) {\n                throw new ProcessException(processKey, \"Invalid template result. Expected a Java Map instance, got \" + result);\n            }\n        } catch (IOException | ScriptException e) {\n            logManager.error(processKey, \"Template script execution error\", e);\n            throw new ProcessException(processKey, \"Template script execution error\", e);\n        }\n        return (Map<String, Object>) result;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/UserInfoProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.pipelines.processors.signing.Signing;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.user.UserInfoProvider;\nimport com.walmartlabs.concord.server.user.UserManager;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\n\nimport static java.util.Objects.requireNonNull;\n\n/**\n * Collects and stores the current user's data.\n */\npublic abstract class UserInfoProcessor implements PayloadProcessor {\n\n    private final String key;\n    private final UserManager userManager;\n    private final Signing signing;\n    private final ObjectMapper objectMapper;\n\n    public UserInfoProcessor(String key,\n                             UserManager userManager,\n                             Signing signing,\n                             ObjectMapper objectMapper) {\n\n        this.key = requireNonNull(key);\n        this.userManager = requireNonNull(userManager);\n        this.signing = requireNonNull(signing);\n        this.objectMapper = requireNonNull(objectMapper);\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        var info = userManager.getCurrentUserInfo();\n        var user = userManager.get(info.id());\n\n        if (user.isPresent() && user.get().isDisabled()) {\n            throw new ConcordApplicationException(String.format(\"User '%s' is disabled\", info.id()));\n        }\n\n        if (signing.isEnabled()) {\n            var signature = Optional.ofNullable(info.username())\n                    .map(this::sign);\n\n            info = signature.isEmpty() ? info : UserInfoProvider.UserInfo.builder()\n                    .from(info)\n                    .usernameSignature(signature.get())\n                    .build();\n        }\n\n        Map<String, UserInfoProvider.UserInfo> m = new HashMap<>();\n        m.put(key, info);\n\n        payload = payload.mergeValues(Payload.CONFIGURATION, m);\n\n        return chain.process(payload);\n    }\n\n    private String sign(String username) {\n        try {\n            return signing.sign(username);\n        } catch (Exception e) {\n            throw new ConcordApplicationException(\"Error while singing process data: \" + e.getMessage(), e);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/WorkspaceArchiveProcessor.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.ZipUtils;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.core.Response.Status;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\n\n/**\n * Unpacks payload's workspace file, parses request data.\n */\npublic class WorkspaceArchiveProcessor implements PayloadProcessor {\n\n    private final ProcessLogManager logManager;\n\n    @Inject\n    public WorkspaceArchiveProcessor(ProcessLogManager logManager) {\n        this.logManager = logManager;\n    }\n\n    @Override\n    public Payload process(Chain chain, Payload payload) {\n        ProcessKey processKey = payload.getProcessKey();\n\n        Path archive = payload.getAttachment(Payload.WORKSPACE_ARCHIVE);\n        if (archive == null) {\n            return chain.process(payload);\n        }\n\n        if (!Files.exists(archive)) {\n            logManager.error(processKey, \"No input archive found: \" + archive);\n            throw new ProcessException(processKey, \"No input archive found: \" + archive, Status.BAD_REQUEST);\n        }\n\n        Path workspace = payload.getHeader(Payload.WORKSPACE_DIR);\n        try {\n            ZipUtils.unzip(archive, workspace, StandardCopyOption.REPLACE_EXISTING);\n        } catch (IOException e) {\n            logManager.error(processKey, \"Error while unpacking an archive: \" + archive, e);\n            throw new ProcessException(processKey, \"Error while unpacking an archive: \" + archive, e);\n        }\n\n        payload = payload.removeAttachment(Payload.WORKSPACE_ARCHIVE);\n        return chain.process(payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/cfg/ProcessConfigurationUtils.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.project.ProjectEntry;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.pipelines.processors.RepositoryProcessor;\n\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic final class ProcessConfigurationUtils {\n\n    private static final List<String> DEFAULT_PROFILES = Collections.singletonList(\"default\");\n\n    /**\n     * Creates a process {@code configuration} object with variables that\n     * are automatically provided by Concord for any process.\n     * <p/>\n     * Created values should be applied last (or before the configuration from a policy)\n     * to avoid users from changing them via request parameters.\n     */\n    public static Map<String, Object> prepareProvidedCfg(Payload payload, ProjectEntry projectEntry) {\n        Map<String, Object> args = new HashMap<>();\n\n        // TODO verify that all \"provided\" variables are set here and only here\n\n        UUID parentInstanceId = payload.getHeader(Payload.PARENT_INSTANCE_ID);\n        if (parentInstanceId != null) {\n            args.put(Constants.Request.PARENT_INSTANCE_ID_KEY, parentInstanceId.toString());\n        }\n\n        Map<String, Object> cfg = new HashMap<>();\n        cfg.put(Constants.Request.ARGUMENTS_KEY, args);\n\n        cfg.put(Constants.Request.PROCESS_INFO_KEY, createProcessInfo(payload));\n        cfg.put(Constants.Request.PROJECT_INFO_KEY, createProjectInfo(payload, projectEntry));\n\n        return cfg;\n    }\n\n    @SafeVarargs\n    @SuppressWarnings(\"unchecked\")\n    public static List<String> getActiveProfiles(Map<String, Object> ... mm) {\n        for (Map<String, Object> m : mm) {\n            Object o = m.get(Constants.Request.ACTIVE_PROFILES_KEY);\n            if (o == null) {\n                continue;\n            }\n\n            if (o instanceof String) {\n                String s = (String) o;\n                String[] as = s.trim().split(\",\");\n                return Arrays.asList(as);\n            } else if (o instanceof List) {\n                return assertListType(\"activeProfiles\", removeNulls((List<String>) o), String.class);\n            } else {\n                throw new IllegalArgumentException(\"Invalid '\" + Constants.Request.ACTIVE_PROFILES_KEY +\n                        \"' value. Expected a JSON array or a comma-delimited list of profiles, got: \" + o);\n            }\n        }\n\n        return DEFAULT_PROFILES;\n    }\n\n    @SafeVarargs\n    @SuppressWarnings(\"unchecked\")\n    public static Set<String> getOutVars(Map<String, Object> ... mm) {\n        Set<String> outVars = new HashSet<>();\n\n        for (Map<String, Object> m : mm) {\n            Object o = m.get(Constants.Request.OUT_EXPRESSIONS_KEY);\n\n            if (o == null) {\n                continue;\n            }\n\n            if (o instanceof List) {\n                outVars.addAll(assertListType(\"out\", removeNulls((List<String>) o), String.class));\n            } else if (o instanceof String) {\n                outVars.addAll(Arrays.asList(((String) o).split(\",\")));\n            } else {\n                throw new IllegalArgumentException(\"Invalid '\" + Constants.Request.OUT_EXPRESSIONS_KEY +\n                        \"' value. Expected JSON array, got: \" + o);\n            }\n        }\n\n        return outVars;\n    }\n\n    private static Map<String, Object> createProjectInfo(Payload payload, ProjectEntry projectEntry) {\n        if (projectEntry == null) {\n            Map<String, Object> m = new HashMap<>();\n            m.put(\"orgId\", OrganizationManager.DEFAULT_ORG_ID.toString());\n            m.put(\"orgName\", OrganizationManager.DEFAULT_ORG_NAME);\n            return m;\n        }\n\n        Map<String, Object> m = new HashMap<>();\n        m.put(\"orgId\", projectEntry.getOrgId());\n        m.put(\"orgName\", projectEntry.getOrgName());\n        m.put(\"projectId\", projectEntry.getId());\n        m.put(\"projectName\", projectEntry.getName());\n\n        RepositoryProcessor.RepositoryInfo r = payload.getHeader(RepositoryProcessor.REPOSITORY_INFO_KEY);\n        if (r != null) {\n            m.put(\"repoId\", r.getId());\n            m.put(\"repoName\", r.getName());\n            m.put(\"repoUrl\", r.getUrl());\n            m.put(\"repoBranch\", r.getBranch());\n            m.put(\"repoPath\", r.getPath());\n            RepositoryProcessor.CommitInfo ci = r.getCommitInfo();\n            if (ci != null) {\n                m.put(\"repoCommitId\", ci.getId());\n                m.put(\"repoCommitAuthor\", ci.getAuthor());\n                m.put(\"repoCommitMessage\", escapeExpression(ci.getMessage()));\n            }\n        }\n\n        return m;\n    }\n\n    private static Map<String, Object> createProcessInfo(Payload payload) {\n        Map<String, Object> m = new HashMap<>();\n\n        String token = payload.getHeader(Payload.SESSION_TOKEN);\n        if (token != null) {\n            // TODO rename to sessionToken? or deprecate in favor of the top-level \"sessionToken\" value\n            m.put(\"sessionKey\", token);\n        }\n\n        List<String> activeProfiles = payload.getHeader(Payload.ACTIVE_PROFILES);\n        if (activeProfiles == null) {\n            activeProfiles = DEFAULT_PROFILES;\n        }\n        m.put(\"activeProfiles\", activeProfiles);\n\n        String sessionToken = payload.getHeader(Payload.SESSION_TOKEN);\n        if (sessionToken != null) {\n            m.put(Constants.Request.SESSION_TOKEN_KEY, sessionToken);\n        }\n\n        return m;\n    }\n\n    private static String escapeExpression(String what) {\n        if (what == null) {\n            return null;\n        }\n        return what.replaceAll(\"\\\\$\\\\{\", \"\\\\\\\\\\\\${\");\n    }\n\n    private static <T> List<T> removeNulls(List<T> c) {\n        if (c == null) {\n            return Collections.emptyList();\n        }\n\n        return c.stream()\n                .filter(Objects::nonNull)\n                .collect(Collectors.toList());\n    }\n\n    private static <T> List<T> assertListType(String itemsName, List<T> items, Class<T> type) {\n        if (items == null) {\n            return Collections.emptyList();\n        }\n\n        for (T i : items) {\n            if (i == null) {\n                throw new IllegalArgumentException(\"Invalid \" + itemsName + \" values \" + items + \": expected 'String' type, got 'null'\");\n            }\n\n            if (!(type.isAssignableFrom(i.getClass()))) {\n                throw new IllegalArgumentException(\"Invalid \" + itemsName + \" values \" + items + \": expected 'String' type, got '\" + i.getClass() + \"'\");\n            }\n        }\n        return items;\n    }\n\n    private ProcessConfigurationUtils() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/policy/ContainerPolicyApplier.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors.policy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.Counter;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.policyengine.CheckResult;\nimport com.walmartlabs.concord.policyengine.ContainerRule;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.InjectCounter;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.server.process.pipelines.processors.policy.PolicyApplier.appendMsg;\n\npublic class ContainerPolicyApplier implements PolicyApplier {\n\n    private final ProcessLogManager logManager;\n\n    @InjectCounter\n    private final Counter policyWarn;\n\n    @InjectCounter\n    private final Counter policyDeny;\n\n    private final ObjectMapper objectMapper;\n\n    @Inject\n    public ContainerPolicyApplier(ProcessLogManager logManager, Counter policyWarn, Counter policyDeny) {\n        this.logManager = logManager;\n        this.policyWarn = policyWarn;\n        this.policyDeny = policyDeny;\n\n        this.objectMapper = new ObjectMapper();\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public void apply(Payload payload, PolicyEngine policy) {\n        ProcessKey processKey = payload.getProcessKey();\n        Path workDir = payload.getHeader(Payload.WORKSPACE_DIR);\n\n        Path p = workDir.resolve(Constants.Files.CONFIGURATION_FILE_NAME);\n        if (!Files.exists(p)) {\n            return;\n        }\n\n        Map<String, Object> containerOptions;\n        try (InputStream in = Files.newInputStream(p)) {\n            Map<String, Object> m = objectMapper.readValue(in, Map.class);\n            containerOptions = (Map<String, Object>) m.get(Constants.Request.CONTAINER);\n        } catch (IOException e) {\n            logManager.error(processKey, \"Error while reading container configuration: {}\", e);\n            throw new ProcessException(processKey, \"Error while reading container configuration\", e);\n        }\n\n        CheckResult<ContainerRule, Object> result = policy.getContainerPolicy().check(containerOptions);\n\n        result.getWarn().forEach(i -> {\n            policyWarn.inc();\n            logManager.warn(processKey, appendMsg(\"Potential container policy violation (policy: {})\", i.getMsg()), i.getRule());\n        });\n\n        result.getDeny().forEach(i -> {\n            policyDeny.inc();\n            logManager.error(processKey, appendMsg(\"Container policy violation\", i.getMsg()), i.getRule());\n        });\n\n        if (!result.getDeny().isEmpty()) {\n            throw new ProcessException(processKey, \"Found container policy violations\");\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/policy/FilePolicyApplier.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors.policy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.Counter;\nimport com.walmartlabs.concord.policyengine.CheckResult;\nimport com.walmartlabs.concord.policyengine.FileRule;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.InjectCounter;\n\nimport javax.inject.Inject;\nimport java.nio.file.Path;\n\npublic class FilePolicyApplier implements PolicyApplier {\n\n    private final ProcessLogManager logManager;\n\n    @InjectCounter\n    private final Counter policyWarn;\n\n    @InjectCounter\n    private final Counter policyDeny;\n\n    @Inject\n    public FilePolicyApplier(ProcessLogManager logManager, Counter policyWarn, Counter policyDeny) {\n        this.logManager = logManager;\n        this.policyWarn = policyWarn;\n        this.policyDeny = policyDeny;\n    }\n\n    @Override\n    public void apply(Payload payload, PolicyEngine policy) throws Exception {\n        ProcessKey processKey = payload.getProcessKey();\n        Path workDir = payload.getHeader(Payload.WORKSPACE_DIR);\n\n        CheckResult<FileRule, Path> result = policy.getFilePolicy().check(workDir);\n\n        result.getWarn().forEach(i -> {\n            policyWarn.inc();\n            logManager.warn(processKey, \"Potentially restricted file '{}' (file policy: {})\",\n                    workDir.relativize(i.getEntity()), i.getRule());\n        });\n\n        result.getDeny().forEach(i -> {\n            policyDeny.inc();\n            logManager.error(processKey, \"File '{}' is forbidden by the file policy {}\",\n                    workDir.relativize(i.getEntity()), i.getRule());\n        });\n\n        if (!result.getDeny().isEmpty()) {\n            throw new ProcessException(processKey, \"Found forbidden files\");\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/policy/PolicyApplier.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors.policy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.server.process.Payload;\n\npublic interface PolicyApplier {\n\n    void apply(Payload payload, PolicyEngine policyEngine) throws Exception;\n\n    static String appendMsg(String msg, String s) {\n        if (s == null) {\n            return msg;\n        }\n        return msg + \": \" + s;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/policy/ProcessRuntimePolicyApplier.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors.policy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.Counter;\nimport com.walmartlabs.concord.policyengine.CheckResult;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.policyengine.RuntimeRule;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.org.project.ProjectEntry;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.metrics.InjectCounter;\n\nimport javax.inject.Inject;\nimport java.text.MessageFormat;\nimport java.time.OffsetDateTime;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.function.Supplier;\n\npublic class ProcessRuntimePolicyApplier implements PolicyApplier {\n\n    private static final String DEFAULT_PROCESS_TIMEOUT_MSG = \"{0} runtime version is not allowed\";\n\n    private final ProcessLogManager logManager;\n\n    @InjectCounter\n    private final Counter policyDeny;\n    private final ProjectDao projectDao;\n\n    @Inject\n    public ProcessRuntimePolicyApplier(ProcessLogManager logManager, Counter policyDeny, ProjectDao projectDao) {\n        this.logManager = logManager;\n        this.policyDeny = policyDeny;\n        this.projectDao = projectDao;\n    }\n\n    @Override\n    public void apply(Payload payload, PolicyEngine policy) {\n        String runtime = payload.getHeader(Payload.RUNTIME);\n\n        Supplier<OffsetDateTime> projectCreatedAt = () -> {\n            UUID projectId = payload.getHeader(Payload.PROJECT_ID);\n            if (projectId == null) {\n                return null;\n            }\n            ProjectEntry projectEntry = projectDao.get(projectId);\n            if (projectEntry != null) {\n                return projectEntry.getCreatedAt();\n            }\n\n            return null;\n        };\n\n        CheckResult<RuntimeRule, String> result = policy.getRuntimePolicy().check(runtime, projectCreatedAt);\n\n        result.getDeny().forEach(i -> {\n            policyDeny.inc();\n\n            String msg = i.getRule().msg() != null ? i.getRule().msg() : DEFAULT_PROCESS_TIMEOUT_MSG;\n            Object actualRuntime = i.getEntity();\n            Set<String> allowed = i.getRule().allowedRuntimes();\n            logManager.error(payload.getProcessKey(), MessageFormat.format(Objects.requireNonNull(msg), actualRuntime, allowed));\n        });\n\n        if (!result.getDeny().isEmpty()) {\n            throw new ProcessException(payload.getProcessKey(), \"'configuration.runtime' value policy violation\");\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/policy/ProcessTimeoutPolicyApplier.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors.policy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.Counter;\nimport com.walmartlabs.concord.policyengine.CheckResult;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.policyengine.ProcessTimeoutRule;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.InjectCounter;\n\nimport javax.inject.Inject;\nimport java.text.MessageFormat;\nimport java.util.Map;\n\npublic class ProcessTimeoutPolicyApplier implements PolicyApplier {\n\n    private static final String DEFAULT_PROCESS_TIMEOUT_MSG = \"Maximum 'processTimeout' value exceeded: current {0}, limit {1}\";\n\n    private final ProcessLogManager logManager;\n\n    @InjectCounter\n    private final Counter policyDeny;\n\n    @Inject\n    public ProcessTimeoutPolicyApplier(ProcessLogManager logManager, Counter policyDeny) {\n        this.logManager = logManager;\n        this.policyDeny = policyDeny;\n    }\n\n    @Override\n    public void apply(Payload payload, PolicyEngine policy) {\n        ProcessKey processKey = payload.getProcessKey();\n\n        Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION);\n        if (cfg == null) {\n            return;\n        }\n\n        Object processTimeout = cfg.get(Constants.Request.PROCESS_TIMEOUT);\n        if (processTimeout == null) {\n            return;\n        }\n\n        CheckResult<ProcessTimeoutRule, Object> result = policy.getProcessTimeoutPolicy().check(processTimeout);\n\n        result.getDeny().forEach(i -> {\n            policyDeny.inc();\n\n            String msg = i.getRule().msg() != null ? i.getRule().msg() : DEFAULT_PROCESS_TIMEOUT_MSG;\n            Object actualTimeout = i.getEntity();\n            String limit = i.getRule().max();\n            logManager.error(processKey, MessageFormat.format(msg, actualTimeout, limit));\n        });\n\n        if (!result.getDeny().isEmpty()) {\n            throw new ProcessException(processKey, \"'processTimeout' value policy violation\");\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/policy/WorkspacePolicyApplier.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors.policy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.Counter;\nimport com.walmartlabs.concord.policyengine.CheckResult;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.policyengine.WorkspaceRule;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.InjectCounter;\n\nimport javax.inject.Inject;\nimport java.nio.file.Path;\n\nimport static com.walmartlabs.concord.server.process.pipelines.processors.policy.PolicyApplier.appendMsg;\n\npublic class WorkspacePolicyApplier implements PolicyApplier {\n\n    private final ProcessLogManager logManager;\n\n    @InjectCounter\n    private final Counter policyWarn;\n\n    @InjectCounter\n    private final Counter policyDeny;\n\n    @Inject\n    public WorkspacePolicyApplier(ProcessLogManager logManager, Counter policyWarn, Counter policyDeny) {\n        this.logManager = logManager;\n        this.policyWarn = policyWarn;\n        this.policyDeny = policyDeny;\n    }\n\n    @Override\n    public void apply(Payload payload, PolicyEngine policy) throws Exception {\n        ProcessKey processKey = payload.getProcessKey();\n        Path workDir = payload.getHeader(Payload.WORKSPACE_DIR);\n\n        CheckResult<WorkspaceRule, Path> result = policy.getWorkspacePolicy().check(workDir);\n\n        result.getWarn().forEach(i -> {\n            policyWarn.inc();\n            logManager.warn(processKey, appendMsg(\"Potential workspace policy violation (policy: {})\", i.getMsg()), i.getRule());\n        });\n\n        result.getDeny().forEach(i -> {\n            policyDeny.inc();\n            logManager.error(processKey, appendMsg(\"Workspace policy violation\", i.getMsg()), i.getRule());\n        });\n\n        if (!result.getDeny().isEmpty()) {\n            throw new ProcessException(processKey, \"Found workspace policy violations\");\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/pipelines/processors/signing/Signing.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors.signing;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.cfg.ProcessConfiguration;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.security.GeneralSecurityException;\nimport java.security.KeyFactory;\nimport java.security.PrivateKey;\nimport java.security.Signature;\nimport java.security.spec.PKCS8EncodedKeySpec;\nimport java.util.Base64;\nimport java.util.stream.Collectors;\n\npublic class Signing {\n\n    private static final Logger log = LoggerFactory.getLogger(Signing.class);\n\n    private final String keyAlgorithm;\n    private final String signingAlgorithm;\n    private final PrivateKey privateKey;\n\n    @Inject\n    public Signing(ProcessConfiguration cfg) throws Exception {\n        this.keyAlgorithm = cfg.getSigningKeyAlgorithm();\n        this.signingAlgorithm = cfg.getSigningAlgorithm();\n\n        Path p = cfg.getSigningKeyPath();\n        if (p != null) {\n            log.info(\"init -> using {} as the process data signing key\", p);\n            this.privateKey = loadKey(p);\n        } else {\n            this.privateKey = null;\n        }\n    }\n\n    public boolean isEnabled() {\n        return this.privateKey != null;\n    }\n\n    public String sign(String data) throws Exception {\n        byte[] ab = data.getBytes(StandardCharsets.UTF_8);\n        byte[] sign = sign(privateKey, ab);\n        return Base64.getEncoder().encodeToString(sign);\n    }\n\n    private byte[] sign(PrivateKey key, byte[] data) throws GeneralSecurityException {\n        Signature s = Signature.getInstance(this.signingAlgorithm);\n        s.initSign(key);\n        s.update(data);\n        return s.sign();\n    }\n\n    private PrivateKey loadKey(Path key) throws Exception {\n        if (!Files.exists(key)) {\n            throw new IllegalArgumentException(\"Can't load a private key, file not found: \" + key);\n        }\n\n        byte[] ab = readPemFile(key);\n        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(ab);\n        KeyFactory kf = KeyFactory.getInstance(this.keyAlgorithm);\n        return kf.generatePrivate(keySpec);\n    }\n\n    private static byte[] readPemFile(Path p) throws IOException {\n        String s = Files.readAllLines(p)\n                .stream()\n                .filter(l -> !l.startsWith(\"-----\"))\n                .collect(Collectors.joining());\n\n        return Base64.getDecoder().decode(s);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/EnqueuedBatchTask.java",
    "content": "package com.walmartlabs.concord.server.process.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.Histogram;\nimport com.codahale.metrics.MetricRegistry;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.repository.Repository;\nimport com.walmartlabs.concord.server.PeriodicTask;\nimport com.walmartlabs.concord.server.cfg.EnqueueWorkersConfiguration;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.PayloadBuilder;\nimport com.walmartlabs.concord.server.process.pipelines.EnqueueProcessPipeline;\nimport com.walmartlabs.concord.server.process.pipelines.processors.Pipeline;\nimport com.walmartlabs.concord.server.repository.RepositoryManager;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport org.immutables.value.Value;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.REPOSITORIES;\nimport static com.walmartlabs.concord.server.jooq.tables.ProcessQueue.PROCESS_QUEUE;\nimport static org.jooq.impl.DSL.currentOffsetDateTime;\nimport static org.jooq.impl.DSL.value;\n\npublic class EnqueuedBatchTask extends PeriodicTask {\n\n    private static final Logger log = LoggerFactory.getLogger(EnqueuedBatchTask.class);\n\n    private static final long ERROR_RETRY_INTERVAL = TimeUnit.SECONDS.toMillis(5);\n\n    private final Dao dao;\n    private final EnqueueWorkersConfiguration cfg;\n    private final Histogram batchHistogram;\n\n    private final ExecutorService executor;\n    private final BlockingQueue<Batch> queue;\n    private final List<String> inflightRepoUrls;\n    private final AtomicInteger freeWorkersCount;\n\n    private final Lock mutex = new ReentrantLock();\n\n    @Inject\n    public EnqueuedBatchTask(Dao dao,\n                             EnqueueWorkersConfiguration cfg,\n                             EnqueueProcessPipeline pipeline,\n                             RepositoryManager repositoryManager,\n                             MetricRegistry metricRegistry) {\n\n        super(cfg.getInterval().toMillis(), ERROR_RETRY_INTERVAL);\n\n        this.cfg = cfg;\n        this.dao = dao;\n        this.batchHistogram = metricRegistry.histogram(\"enqueued-task-batches-histogram\");\n\n        this.queue = new ArrayBlockingQueue<>(cfg.getWorkersCount());\n        this.freeWorkersCount = new AtomicInteger(cfg.getWorkersCount());\n        this.inflightRepoUrls = new ArrayList<>(cfg.getWorkersCount());\n\n        this.executor = Executors.newFixedThreadPool(cfg.getWorkersCount());\n        for (int i = 0; i < cfg.getWorkersCount(); i++) {\n            this.executor.submit(new Worker(pipeline, repositoryManager, queue));\n        }\n\n        metricRegistry.gauge(\"enqueued-workers-available\", () -> freeWorkersCount::get);\n        metricRegistry.gauge(\"enqueued-inflight-urls\", () -> () -> {\n            mutex.lock();\n            try {\n                return inflightRepoUrls.size();\n            } finally {\n                mutex.unlock();\n            }\n        });\n    }\n\n    @Override\n    public void stop() {\n        super.stop();\n\n        executor.shutdownNow();\n\n        try {\n            executor.awaitTermination(5, TimeUnit.MINUTES);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    @Override\n    @WithTimer\n    protected boolean performTask() {\n        if (freeWorkersCount.get() == 0) {\n            return false;\n        }\n\n        int limit = Math.min(freeWorkersCount.get(), cfg.getWorkersCount());\n        List<String> ignoreRepoUrls = copyInFlightRepos();\n        Collection<Batch> batches = dao.poll(ignoreRepoUrls, limit);\n        if (batches.isEmpty()) {\n            return false;\n        }\n\n        for (Batch b : batches) {\n            if (b.key() != null) {\n                List<ProcessKey> processes = dao.poll(b.key(), cfg.getBatchSize());\n                b.processes().addAll(processes);\n            }\n\n            batchHistogram.update(b.processes().size());\n        }\n\n        for (int i = 0; i < batches.size(); i++) {\n            freeWorkersCount.decrementAndGet();\n        }\n\n        mutex.lock();\n        try {\n            for (Batch b : batches) {\n                if (b.repoUrl() != null) {\n                    inflightRepoUrls.add(b.repoUrl());\n                }\n            }\n        } finally {\n            mutex.unlock();\n        }\n\n        queue.addAll(batches);\n\n        return freeWorkersCount.get() > 0;\n    }\n\n    private void onWorkerFree(String repoUrl) {\n        freeWorkersCount.incrementAndGet();\n        if (repoUrl != null) {\n            mutex.lock();\n            try {\n                inflightRepoUrls.remove(repoUrl);\n            } finally {\n                mutex.unlock();\n            }\n        }\n    }\n\n    private List<String> copyInFlightRepos() {\n        mutex.lock();\n        try {\n            return List.copyOf(inflightRepoUrls);\n        } finally {\n            mutex.unlock();\n        }\n    }\n\n    private class Worker implements Runnable {\n\n        private final Pipeline pipeline;\n        private final RepositoryManager repositoryManager;\n        private final BlockingQueue<Batch> queue;\n\n        private Worker(Pipeline pipeline, RepositoryManager repositoryManager, BlockingQueue<Batch> queue) {\n            this.pipeline = pipeline;\n            this.repositoryManager = repositoryManager;\n            this.queue = queue;\n        }\n\n        @Override\n        public void run() {\n            while (!Thread.currentThread().isInterrupted()) {\n                String repoUrl = null;\n                try {\n                    Batch keys = queue.take();\n                    repoUrl = keys.repoUrl();\n                    startProcessBatch(keys);\n                } catch (InterruptedException e) {\n                    log.warn(\"run -> interrupted\");\n                    Thread.currentThread().interrupt();\n                } catch (Exception e) {\n                    log.error(\"run -> error\", e);\n                    sleep(ERROR_RETRY_INTERVAL);\n                } finally {\n                    onWorkerFree(repoUrl);\n                }\n            }\n        }\n\n        private void startProcessBatch(Batch batch) {\n            try {\n                if (batch.repoUrl() != null && batch.processes().size() > 1) {\n                    repositoryManager.withLock(batch.repoUrl(), () -> {\n                        Repository repository = null;\n                        for (ProcessKey key : batch.processes()) {\n                            Payload payload = startProcess(key, repository);\n                            if (payload != null && repository == null) {\n                                repository = payload.getHeader(Payload.REPOSITORY);\n                            }\n                        }\n                        return null;\n                    });\n                } else {\n                    for (ProcessKey key : batch.processes()) {\n                        startProcess(key, null);\n                    }\n                }\n            } catch (Exception e) {\n                log.error(\"startProcessBatch ['{}'] -> error\", batch, e);\n            }\n        }\n\n        private Payload startProcess(ProcessKey key, Repository repository) {\n            try {\n                Payload payload = PayloadBuilder.start(key).build();\n                if (repository != null) {\n                    payload = payload.putHeader(Payload.REPOSITORY, repository);\n                }\n                return pipeline.process(payload);\n            } catch (Exception e) {\n                log.error(\"startProcess ['{}'] -> error\", key, e);\n            }\n            return null;\n        }\n    }\n\n    static class Batch {\n\n        /**\n         * Batch key: repositoryId + branch + commitId (users can override branch/commit )\n         */\n        @Value.Immutable\n        interface Key {\n\n            @Value.Parameter\n            UUID repoId();\n\n            @Nullable\n            @Value.Parameter\n            String branchOrTag();\n\n            @Nullable\n            @Value.Parameter\n            String commitId();\n\n            static Key of(UUID repoId, String branchOrTag, String commitId) {\n                return ImmutableKey.of(repoId, branchOrTag, commitId);\n            }\n        }\n\n        public static Batch singleProcess(ProcessKey key) {\n            return new Batch(null, null, Collections.singletonList(key));\n        }\n\n        @Nullable\n        private final Key key;\n\n        @Nullable\n        private final String repoUrl;\n\n        private final List<ProcessKey> processes;\n\n        private Batch(Key key, String repoUrl) {\n            this(key, repoUrl, Collections.emptyList());\n        }\n\n        private Batch(Key key, String repoUrl, List<ProcessKey> processes) {\n            this.key = key;\n            this.repoUrl = repoUrl;\n            this.processes = new ArrayList<>(processes);\n        }\n\n        public Key key() {\n            return key;\n        }\n\n        public String repoUrl() {\n            return repoUrl;\n        }\n\n        public List<ProcessKey> processes() {\n            return processes;\n        }\n    }\n\n    @Value.Immutable\n    interface ProcessItem {\n\n        ProcessKey key();\n\n        @Nullable\n        UUID repoId();\n\n        @Nullable\n        String repoUrl();\n\n        @Nullable\n        String branchOrTag();\n\n        @Nullable\n        String commitId();\n\n        static ImmutableProcessItem.Builder builder() {\n            return ImmutableProcessItem.builder();\n        }\n    }\n\n    static class Dao extends AbstractDao {\n\n        @Inject\n        public Dao(@MainDB Configuration cfg) {\n            super(cfg);\n        }\n\n        @WithTimer\n        public Collection<Batch> poll(List<String> ignoreRepoUrls, int limit) {\n            return txResult(tx -> {\n                List<ProcessItem> items = tx.select(PROCESS_QUEUE.INSTANCE_ID, PROCESS_QUEUE.CREATED_AT, PROCESS_QUEUE.REPO_ID, REPOSITORIES.REPO_URL, PROCESS_QUEUE.COMMIT_BRANCH, PROCESS_QUEUE.COMMIT_ID)\n                        .from(PROCESS_QUEUE).leftJoin(REPOSITORIES).on(REPOSITORIES.REPO_ID.eq(PROCESS_QUEUE.REPO_ID))\n                        .where(PROCESS_QUEUE.CURRENT_STATUS.eq(ProcessStatus.NEW.name())\n                                .and(REPOSITORIES.REPO_URL.isNull().or(REPOSITORIES.REPO_URL.notIn(ignoreRepoUrls))))\n                        .orderBy(PROCESS_QUEUE.CREATED_AT)\n                        .limit(limit)\n                        .forUpdate().of(PROCESS_QUEUE)\n                        .skipLocked()\n                        .fetch(r -> ProcessItem.builder()\n                                .key(new ProcessKey(r.value1(), r.value2()))\n                                .repoId(r.value3())\n                                .repoUrl(r.value4())\n                                .branchOrTag(r.value5())\n                                .commitId(r.value6())\n                                .build());\n\n                if (items.isEmpty()) {\n                    return Collections.emptyList();\n                }\n\n                Collection<Batch> batches = toBatches(items);\n                batches = removeDuplicateUrls(batches);\n\n                toPreparing(tx, batches.stream()\n                        .map(Batch::processes)\n                        .flatMap(Collection::stream)\n                        .map(PartialProcessKey::getInstanceId)\n                        .collect(Collectors.toList()));\n\n                return batches;\n            });\n        }\n\n        @WithTimer\n        public List<ProcessKey> poll(Batch.Key key, int limit) {\n            return txResult(tx -> {\n                List<ProcessKey> result = tx.select(PROCESS_QUEUE.INSTANCE_ID, PROCESS_QUEUE.CREATED_AT)\n                        .from(PROCESS_QUEUE)\n                        .where(PROCESS_QUEUE.CURRENT_STATUS.eq(ProcessStatus.NEW.name())\n                                .and(PROCESS_QUEUE.REPO_ID.eq(key.repoId()))\n                                .and(PROCESS_QUEUE.COMMIT_BRANCH.isNotDistinctFrom(key.branchOrTag()))\n                                .and(PROCESS_QUEUE.COMMIT_ID.isNotDistinctFrom(key.commitId())))\n                        .orderBy(PROCESS_QUEUE.CREATED_AT)\n                        .limit(limit)\n                        .forUpdate()\n                        .skipLocked()\n                        .fetch(r -> new ProcessKey(r.value1(), r.value2()));\n\n                if (result.isEmpty()) {\n                    return result;\n                }\n\n                toPreparing(tx, result.stream()\n                        .map(PartialProcessKey::getInstanceId)\n                        .collect(Collectors.toList()));\n\n                return result;\n            });\n        }\n\n        private static Collection<Batch> toBatches(List<ProcessItem> items) {\n            List<Batch> result = new ArrayList<>();\n\n            Map<Batch.Key, Batch> batches = new HashMap<>();\n            for (ProcessItem item : items) {\n                if (item.repoId() == null) {\n                    result.add(Batch.singleProcess(item.key()));\n                } else {\n                    Batch.Key key = Batch.Key.of(item.repoId(), item.branchOrTag(), item.commitId());\n                    batches.computeIfAbsent(key, k -> new Batch(key, item.repoUrl()))\n                            .processes().add(item.key());\n                }\n            }\n\n            result.addAll(batches.values());\n\n            return result;\n        }\n\n        private static Collection<Batch> removeDuplicateUrls(Collection<Batch> batches) {\n            List<Batch> result = new ArrayList<>();\n            Set<String> repoUrls = new HashSet<>();\n            for (Batch b : batches) {\n                if (b.repoUrl() == null) {\n                    result.add(b);\n                } else if (!repoUrls.contains(b.repoUrl())) {\n                    result.add(b);\n                    repoUrls.add(b.repoUrl());\n                }\n            }\n            return result;\n        }\n\n        private void toPreparing(DSLContext tx, List<UUID> processes) {\n            tx.update(PROCESS_QUEUE)\n                    .set(PROCESS_QUEUE.CURRENT_STATUS, value(ProcessStatus.PREPARING.name()))\n                    .set(PROCESS_QUEUE.LAST_UPDATED_AT, currentOffsetDateTime())\n                    .where(PROCESS_QUEUE.INSTANCE_ID.in(processes))\n                    .execute();\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/EnqueuedTask.java",
    "content": "package com.walmartlabs.concord.server.process.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.MetricRegistry;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.PeriodicTask;\nimport com.walmartlabs.concord.server.cfg.EnqueueWorkersConfiguration;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.PayloadBuilder;\nimport com.walmartlabs.concord.server.process.pipelines.EnqueueProcessPipeline;\nimport com.walmartlabs.concord.server.process.pipelines.processors.Pipeline;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport org.jooq.Configuration;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.util.List;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.jooq.tables.ProcessQueue.PROCESS_QUEUE;\nimport static org.jooq.impl.DSL.currentOffsetDateTime;\nimport static org.jooq.impl.DSL.value;\n\npublic class EnqueuedTask extends PeriodicTask {\n\n    private static final Logger log = LoggerFactory.getLogger(EnqueuedTask.class);\n\n    private static final long ERROR_RETRY_INTERVAL = TimeUnit.SECONDS.toMillis(5);\n\n    private final EnqueueWorkersConfiguration cfg;\n\n    private final Dao dao;\n\n    private final ExecutorService executor;\n    private final BlockingQueue<ProcessKey> queue;\n\n    private final AtomicInteger freeWorkersCount;\n\n    @Inject\n    public EnqueuedTask(EnqueueWorkersConfiguration cfg, Dao dao, EnqueueProcessPipeline pipeline, MetricRegistry metricRegistry) {\n        super(cfg.getInterval().toMillis(), ERROR_RETRY_INTERVAL);\n\n        this.cfg = cfg;\n        this.dao = dao;\n        this.queue = new ArrayBlockingQueue<>(cfg.getWorkersCount());\n        this.freeWorkersCount = new AtomicInteger(cfg.getWorkersCount());\n\n        executor = Executors.newFixedThreadPool(cfg.getWorkersCount());\n        for (int i = 0; i < cfg.getWorkersCount(); i++) {\n            executor.submit(new Worker(pipeline, queue));\n        }\n\n        metricRegistry.gauge(\"enqueued-workers-available\", () -> freeWorkersCount::get);\n    }\n\n    @Override\n    public void stop() {\n        super.stop();\n\n        executor.shutdownNow();\n\n        try {\n            executor.awaitTermination(5, TimeUnit.MINUTES);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    @Override\n    @WithTimer\n    protected boolean performTask() {\n        if (freeWorkersCount.get() == 0) {\n            return false;\n        }\n\n        int limit = Math.min(freeWorkersCount.get(), cfg.getWorkersCount());\n        List<ProcessKey> keys = dao.poll(limit);\n        log.debug(\"performTask ['{}'] -> size: {}\", limit, keys.size());\n        if (keys.isEmpty()) {\n            return false;\n        }\n\n        for (int i = 0; i < keys.size(); i++) {\n            freeWorkersCount.decrementAndGet();\n        }\n\n        queue.addAll(keys);\n\n        return keys.size() >= limit;\n    }\n\n    private void onWorkerFree() {\n        freeWorkersCount.incrementAndGet();\n    }\n\n    private class Worker implements Runnable {\n\n        private final Pipeline pipeline;\n\n        private final BlockingQueue<ProcessKey> queue;\n\n        private Worker(Pipeline pipeline, BlockingQueue<ProcessKey> queue) {\n            this.pipeline = pipeline;\n            this.queue = queue;\n        }\n\n        @Override\n        public void run() {\n            while (!Thread.currentThread().isInterrupted()) {\n                try {\n                    ProcessKey key = queue.take();\n                    startProcess(key);\n                } catch (InterruptedException e) {\n                    log.warn(\"run -> interrupted\");\n                    Thread.currentThread().interrupt();\n                } catch (Exception e) {\n                    log.error(\"run -> error\", e);\n                    sleep(ERROR_RETRY_INTERVAL);\n                } finally {\n                    onWorkerFree();\n                }\n            }\n        }\n\n        private void startProcess(ProcessKey key) {\n            try {\n                Payload payload = PayloadBuilder.start(key).build();\n                pipeline.process(payload);\n            } catch (Exception e) {\n                log.error(\"startProcess ['{}'] -> error\", key, e);\n            }\n        }\n    }\n\n    static class Dao extends AbstractDao {\n\n        @Inject\n        public Dao(@MainDB Configuration cfg) {\n            super(cfg);\n        }\n\n        public List<ProcessKey> poll(int limit) {\n            return txResult(tx -> {\n                List<ProcessKey> result = tx.select(PROCESS_QUEUE.INSTANCE_ID, PROCESS_QUEUE.CREATED_AT)\n                        .from(PROCESS_QUEUE)\n                        .where(PROCESS_QUEUE.CURRENT_STATUS.eq(ProcessStatus.NEW.name()))\n                        .orderBy(PROCESS_QUEUE.LAST_UPDATED_AT)\n                        .limit(limit)\n                        .forUpdate()\n                        .skipLocked()\n                        .fetch(r -> new ProcessKey(r.value1(), r.value2()));\n\n                if (result.isEmpty()) {\n                    return result;\n                }\n\n                tx.update(PROCESS_QUEUE)\n                        .set(PROCESS_QUEUE.CURRENT_STATUS, value(ProcessStatus.PREPARING.name()))\n                        .set(PROCESS_QUEUE.LAST_UPDATED_AT, currentOffsetDateTime())\n                        .where(PROCESS_QUEUE.INSTANCE_ID.in(result.stream().map(PartialProcessKey::getInstanceId).collect(Collectors.toList())))\n                        .execute();\n\n                return result;\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/EnqueuedTaskProvider.java",
    "content": "package com.walmartlabs.concord.server.process.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Injector;\nimport com.google.inject.Provider;\nimport com.walmartlabs.concord.server.cfg.EnqueueWorkersConfiguration;\nimport com.walmartlabs.concord.server.sdk.BackgroundTask;\n\nimport javax.inject.Inject;\n\npublic class EnqueuedTaskProvider implements Provider<BackgroundTask> {\n\n    private final BackgroundTask task;\n\n    @Inject\n    public EnqueuedTaskProvider(Injector injector, EnqueueWorkersConfiguration cfg) {\n        if (cfg.isBatchEnabled()) {\n            this.task = injector.getInstance(EnqueuedBatchTask.class);\n        } else {\n            this.task = injector.getInstance(EnqueuedTask.class);\n        }\n    }\n\n    @Override\n    public BackgroundTask get() {\n        return task;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/ExternalProcessListenerHandler.java",
    "content": "package com.walmartlabs.concord.server.process.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.EventType;\nimport com.walmartlabs.concord.server.process.event.NewProcessEvent;\nimport com.walmartlabs.concord.server.process.event.ProcessEventManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport org.jooq.DSLContext;\n\nimport javax.inject.Inject;\nimport java.util.Collections;\n\n/**\n * Sends {@link EventType#PROCESS_STATUS} events to registered {@link com.walmartlabs.concord.server.sdk.events.ProcessEventListener}\n * instances.\n */\npublic class ExternalProcessListenerHandler implements ProcessStatusListener {\n\n    private final ProcessEventManager eventManager;\n\n    @Inject\n    public ExternalProcessListenerHandler(ProcessEventManager eventManager) {\n        this.eventManager = eventManager;\n    }\n\n    @Override\n    public void onStatusChange(DSLContext tx, ProcessKey processKey, ProcessStatus status) {\n        eventManager.event(tx, NewProcessEvent.builder()\n                .processKey(processKey)\n                .eventType(EventType.PROCESS_STATUS.name())\n                .data(Collections.singletonMap(\"status\", status.name()))\n                .build());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/FilterUtils.java",
    "content": "package com.walmartlabs.concord.server.process.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.DateTimeUtils;\nimport org.immutables.value.Value;\nimport org.jooq.Field;\nimport org.jooq.JSONB;\nimport org.jooq.Record;\nimport org.jooq.SelectQuery;\n\nimport javax.ws.rs.core.UriInfo;\nimport java.time.OffsetDateTime;\nimport java.time.format.DateTimeParseException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.db.PgUtils.*;\nimport static org.jooq.impl.DSL.currentOffsetDateTime;\n\npublic final class FilterUtils {\n\n    public static final SuffixMapping[] SUFFIX_MAPPINGS = {\n            SuffixMapping.of(\".contains\", ProcessFilter.FilterType.CONTAINS),\n            SuffixMapping.of(\".notContains\", ProcessFilter.FilterType.NOT_CONTAINS),\n\n            SuffixMapping.of(\".eq\", ProcessFilter.FilterType.EQUALS),\n            SuffixMapping.of(\".notEq\", ProcessFilter.FilterType.NOT_EQUALS),\n\n            SuffixMapping.of(\".startsWith\", ProcessFilter.FilterType.STARTS_WITH),\n            SuffixMapping.of(\".notStartsWith\", ProcessFilter.FilterType.NOT_STARTS_WITH),\n\n            SuffixMapping.of(\".endsWith\", ProcessFilter.FilterType.ENDS_WITH),\n            SuffixMapping.of(\".notEndsWith\", ProcessFilter.FilterType.NOT_ENDS_WITH),\n\n            SuffixMapping.of(\".ge\", ProcessFilter.FilterType.GREATER_OR_EQUALS),\n            SuffixMapping.of(\".len\", ProcessFilter.FilterType.LESS_OR_EQUALS_OR_NULL),\n\n            SuffixMapping.of(\".regexp\", ProcessFilter.FilterType.REGEXP_MATCH)\n    };\n\n    public static List<ProcessFilter.DateFilter> parseDate(String paramName, UriInfo uriInfo) {\n        return uriInfo.getQueryParameters().entrySet().stream()\n                .filter(e -> isParam(e.getKey(), paramName))\n                .map(e -> {\n                    ImmutableDateFilter.Builder b = ImmutableDateFilter.builder()\n                            .value(parseDateValue(getValue(e)));\n\n                    ProcessFilter.FilterType type = parseFilterType(e.getKey());\n                    if (type != null) {\n                        b.type(type);\n                    }\n                    return b.build();\n                })\n                .collect(Collectors.toList());\n    }\n\n    public static List<ProcessFilter.JsonFilter> parseJson(String paramName, UriInfo uriInfo) {\n        return uriInfo.getQueryParameters().entrySet().stream()\n                .filter(e -> (isParam(e.getKey(), paramName)))\n                .map(e -> {\n                    ImmutableJsonFilter.Builder filter = ProcessFilter.JsonFilter.builder()\n                            .value(getValue(e));\n\n                    // skip paramName\n                    List<String> path = Arrays.stream(e.getKey().split(\"\\\\.\"))\n                            .skip(1)\n                            .collect(Collectors.toList());\n\n                    ProcessFilter.FilterType type = parseFilterType(e.getKey());\n                    if (type != null) {\n                        filter.type(type);\n\n                        path = new ArrayList<>(path);\n                        path.remove(path.size() - 1);\n                    }\n\n                    return filter.path(path)\n                            .build();\n                })\n                .collect(Collectors.toList());\n    }\n\n    public static void applyDate(SelectQuery<Record> q, Field<OffsetDateTime> field, List<ProcessFilter.DateFilter> filters) {\n        if (filters == null || filters.isEmpty()) {\n            return;\n        }\n\n        for (ProcessFilter.DateFilter filter : filters) {\n            switch (filter.type()) {\n                case GREATER_OR_EQUALS: {\n                    if (filter.value() == null) {\n                        q.addConditions(field.greaterOrEqual(currentOffsetDateTime()));\n                    } else {\n                        q.addConditions(field.greaterOrEqual(filter.value()));\n                    }\n                    break;\n                }\n                case LESS_OR_EQUALS_OR_NULL: {\n                    if (filter.value() == null) {\n                        q.addConditions(field.lessOrEqual(currentOffsetDateTime()).or(field.isNull()));\n                    } else {\n                        q.addConditions(field.lessOrEqual(filter.value()).or(field.isNull()));\n                    }\n                    break;\n                }\n                default:\n                    throw new IllegalArgumentException(\"Unsupported filter type: \" + filter.type());\n            }\n        }\n    }\n\n    public static void applyJson(SelectQuery<Record> q, Field<JSONB> column, List<ProcessFilter.JsonFilter> filters) {\n        if (filters == null || filters.isEmpty()) {\n            return;\n        }\n\n        for (ProcessFilter.JsonFilter f : filters) {\n            switch (f.type()) {\n                case CONTAINS: {\n                    q.addConditions(jsonbTextByPath(column, f.path()).contains(f.value()));\n                    break;\n                }\n                case NOT_CONTAINS: {\n                    q.addConditions(jsonbTextByPath(column, f.path()).notContains(f.value()));\n                    break;\n                }\n                case EQUALS: {\n                    q.addConditions(jsonbTextExistsByPath(column, f.path(), f.value()));\n                    break;\n                }\n                case NOT_EQUALS: {\n                    q.addConditions(jsonbTextNotExistsByPath(column, f.path(), f.value()));\n                    break;\n                }\n                case REGEXP_MATCH: {\n                    q.addConditions(jsonbTextMatch(column, f.path(), f.value()));\n                    break;\n                }\n                case STARTS_WITH: {\n                    q.addConditions(jsonbTextByPath(column, f.path()).startsWith(f.value()));\n                    break;\n                }\n                case NOT_STARTS_WITH: {\n                    q.addConditions(jsonbTextByPath(column, f.path()).startsWith(f.value()).not());\n                    break;\n                }\n                case ENDS_WITH: {\n                    q.addConditions(jsonbTextByPath(column, f.path()).endsWith(f.value()));\n                    break;\n                }\n                case NOT_ENDS_WITH: {\n                    q.addConditions(jsonbTextByPath(column, f.path()).endsWith(f.value()).not());\n                    break;\n                }\n            }\n        }\n    }\n\n    @Value.Immutable\n    public interface SuffixMapping {\n\n        @Value.Parameter\n        String suffix();\n\n        @Value.Parameter\n        ProcessFilter.FilterType filterType();\n\n        static SuffixMapping of(String suffix, ProcessFilter.FilterType filterType) {\n            return ImmutableSuffixMapping.of(suffix, filterType);\n        }\n    }\n\n    private static ProcessFilter.FilterType parseFilterType(String key) {\n        if (!key.contains(\".\")) {\n            return null;\n        }\n\n        for (SuffixMapping m : FilterUtils.SUFFIX_MAPPINGS) {\n            if (key.endsWith(m.suffix())) {\n                return m.filterType();\n            }\n        }\n\n        return null;\n    }\n\n    private static OffsetDateTime parseDateValue(String value) {\n        if (value == null || value.trim().isEmpty()) {\n            return null;\n        }\n\n        try {\n            return DateTimeUtils.fromIsoString(value);\n        } catch (DateTimeParseException e) {\n            throw new RuntimeException(\"Invalid date format, expected an ISO-8601 value, got: \" + value);\n        }\n    }\n\n    private static boolean isParam(String key, String paramName) {\n        return key.startsWith(paramName + \".\") || key.equals(paramName);\n    }\n\n    private static String getValue(Map.Entry<String, List<String>> e) {\n        if (e.getValue() != null && !e.getValue().isEmpty()) {\n            return e.getValue().get(0);\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/MetadataUtils.java",
    "content": "package com.walmartlabs.concord.server.process.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.queue.ProcessFilter.MetadataFilter;\nimport org.jooq.Field;\nimport org.jooq.JSONB;\nimport org.jooq.Record;\nimport org.jooq.SelectQuery;\n\nimport javax.ws.rs.core.UriInfo;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.db.PgUtils.jsonbEq;\nimport static com.walmartlabs.concord.db.PgUtils.jsonbText;\n\n// TODO: replace with FilterUtils and JsonFilter.\npublic final class MetadataUtils {\n\n    public static List<MetadataFilter> parseMetadataFilters(UriInfo uriInfo) {\n        return uriInfo.getQueryParameters().entrySet().stream()\n                .filter(e -> e.getKey().startsWith(\"meta.\"))\n                .map(e -> parseMetadataFilter(e.getKey().substring(\"meta.\".length()), e.getValue().get(0)))\n                .collect(Collectors.toList());\n    }\n\n    public static void apply(SelectQuery<Record> q, Field<JSONB> column, List<MetadataFilter> filters) {\n        if (filters == null || filters.isEmpty()) {\n            return;\n        }\n\n        for (MetadataFilter f : filters) {\n            switch (f.type()) {\n                case CONTAINS: {\n                    q.addConditions(jsonbText(column, f.key()).contains(f.value()));\n                    break;\n                }\n                case NOT_CONTAINS: {\n                    q.addConditions(jsonbText(column, f.key()).notContains(f.value()));\n                    break;\n                }\n                case EQUALS: {\n                    q.addConditions(jsonbEq(column, f.key(), f.value()));\n                    break;\n                }\n                case NOT_EQUALS: {\n                    q.addConditions(jsonbEq(column, f.key(), f.value()).not());\n                    break;\n                }\n                case STARTS_WITH: {\n                    q.addConditions(jsonbText(column, f.key()).startsWith(f.value()));\n                    break;\n                }\n                case NOT_STARTS_WITH: {\n                    q.addConditions(jsonbText(column, f.key()).startsWith(f.value()).not());\n                    break;\n                }\n                case ENDS_WITH: {\n                    q.addConditions(jsonbText(column, f.key()).endsWith(f.value()));\n                    break;\n                }\n                case NOT_ENDS_WITH: {\n                    q.addConditions(jsonbText(column, f.key()).endsWith(f.value()).not());\n                    break;\n                }\n            }\n        }\n    }\n\n    private static MetadataFilter parseMetadataFilter(String key, String value) {\n        ImmutableMetadataFilter.Builder b = MetadataFilter.builder()\n                .key(key)\n                .value(value);\n\n        if (!key.contains(\".\")) {\n            return b.build();\n        }\n\n        for (FilterUtils.SuffixMapping m : FilterUtils.SUFFIX_MAPPINGS) {\n            if (key.endsWith(m.suffix())) {\n                String k = key.substring(0, key.length() - m.suffix().length());\n                return b.key(k).type(m.filterType()).build();\n            }\n        }\n\n        // nested filter\n        return b.build();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/ProcessFilter.java",
    "content": "package com.walmartlabs.concord.server.process.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.ProcessDataInclude;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.time.OffsetDateTime;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.UUID;\n\n@Value.Immutable\npublic interface ProcessFilter {\n\n    @Nullable\n    OffsetDateTime afterCreatedAt();\n\n    @Nullable\n    OffsetDateTime beforeCreatedAt();\n\n    @Nullable\n    String initiator();\n\n    @Nullable\n    Set<UUID> orgIds();\n\n    @Nullable\n    UUID parentId();\n\n    @Nullable\n    UUID projectId();\n\n    @Nullable\n    UUID repoId();\n\n    @Nullable\n    String repoName();\n\n    /**\n     * Include processes without projects.\n     * Applied only when {@link #orgIds()} and/or {@link #projectId()} are defined.\n     */\n    @Value.Default\n    default boolean includeWithoutProject() {\n        return true;\n    }\n\n    @Nullable\n    ProcessStatus status();\n\n    @Nullable\n    Set<String> tags();\n\n    @Value.Default\n    default Set<ProcessDataInclude> includes() {\n        return Collections.emptySet();\n    }\n\n    @Nullable\n    List<MetadataFilter> metaFilters();\n\n    @Nullable\n    List<JsonFilter> requirements();\n\n    @Nullable\n    List<DateFilter> startAt();\n\n    @Nullable\n    Integer limit();\n\n    @Nullable\n    Integer offset();\n\n    static ImmutableProcessFilter.Builder builder() {\n        return ImmutableProcessFilter.builder();\n    }\n\n    @Value.Immutable\n    interface JsonFilter {\n\n        @Value.Default\n        default FilterType type() {\n            return FilterType.CONTAINS;\n        }\n\n        List<String> path();\n\n        @Nullable\n        String value();\n\n        static ImmutableJsonFilter.Builder builder() {\n            return ImmutableJsonFilter.builder();\n        }\n    }\n\n    @Value.Immutable\n    interface DateFilter {\n\n        @Value.Default\n        default FilterType type() {\n            return FilterType.EQUALS;\n        }\n\n        @Nullable\n        OffsetDateTime value();\n    }\n\n    @Value.Immutable\n    interface MetadataFilter {\n\n        @Value.Default\n        default FilterType type() {\n            return FilterType.CONTAINS;\n        }\n\n        String key();\n\n        String value();\n\n        static ImmutableMetadataFilter.Builder builder() {\n            return ImmutableMetadataFilter.builder();\n        }\n    }\n\n    enum FilterType {\n        CONTAINS,\n        NOT_CONTAINS,\n\n        EQUALS,\n        NOT_EQUALS,\n\n        ENDS_WITH,\n        NOT_ENDS_WITH,\n\n        STARTS_WITH,\n        NOT_STARTS_WITH,\n\n        GREATER_OR_EQUALS,\n        LESS_OR_EQUALS_OR_NULL,\n\n        REGEXP_MATCH\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/ProcessInitiatorEntry.java",
    "content": "package com.walmartlabs.concord.server.process.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.time.OffsetDateTime;\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableProcessInitiatorEntry.class)\n@JsonDeserialize(as = ImmutableProcessInitiatorEntry.class)\npublic interface ProcessInitiatorEntry extends Serializable {\n\n    UUID instanceId();\n\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    OffsetDateTime createdAt();\n\n    ProcessStatus status();\n\n    @Nullable\n    UUID initiatorId();\n\n    static ImmutableProcessInitiatorEntry.Builder builder() {\n        return ImmutableProcessInitiatorEntry.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/ProcessKeyCache.java",
    "content": "package com.walmartlabs.concord.server.process.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.cache.CacheBuilder;\nimport com.google.common.cache.CacheLoader;\nimport com.google.common.cache.CacheStats;\nimport com.google.common.cache.LoadingCache;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\n\nimport javax.inject.Inject;\nimport java.io.Serial;\nimport java.util.UUID;\nimport java.util.concurrent.ExecutionException;\n\npublic class ProcessKeyCache implements com.walmartlabs.concord.server.sdk.ProcessKeyCache {\n\n    private final LoadingCache<UUID, ProcessKey> cache;\n\n    @Inject\n    public ProcessKeyCache(ProcessQueueDao queueDao) {\n        this.cache = CacheBuilder.newBuilder()\n                .maximumSize(10 * 1024L)\n                .concurrencyLevel(32)\n                .recordStats()\n                .build(new CacheLoader<>() {\n                    @Override\n                    public ProcessKey load(UUID key) throws ProcessNotFoundException {\n                        ProcessKey pk = queueDao.getKey(key);\n                        if (pk != null) {\n                            return pk;\n                        }\n                        throw new ProcessNotFoundException();\n                    }\n                });\n    }\n\n    public CacheStats stats() {\n        return this.cache.stats();\n    }\n\n    @Override\n    public ProcessKey get(UUID instanceId) {\n        try {\n            return cache.get(instanceId);\n        } catch (ExecutionException e) {\n            if (e.getCause() instanceof ProcessNotFoundException) {\n                return null;\n            }\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public ProcessKey assertKey(UUID instanceId) {\n        ProcessKey processKey = get(instanceId);\n        if (processKey == null) {\n            throw new IllegalStateException(\"Can't determine the process key of \" + instanceId);\n        }\n        return processKey;\n    }\n\n    @Override\n    public long hitCount() {\n        return cache.stats().hitCount();\n    }\n\n    @Override\n    public long missCount() {\n        return cache.stats().missCount();\n    }\n\n    private static class ProcessNotFoundException extends Exception {\n\n        @Serial\n        private static final long serialVersionUID = 1L;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/ProcessKeyCacheGaugeModule.java",
    "content": "package com.walmartlabs.concord.server.process.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.Gauge;\nimport com.google.inject.AbstractModule;\nimport com.google.inject.Provider;\nimport com.google.inject.multibindings.Multibinder;\nimport com.walmartlabs.concord.server.sdk.metrics.GaugeProvider;\nimport com.walmartlabs.concord.server.sdk.ProcessKeyCache;\n\n\nimport java.util.function.ToLongFunction;\n\npublic class ProcessKeyCacheGaugeModule extends AbstractModule {\n\n    @Override\n    protected void configure() {\n        Provider<ProcessKeyCache> provider = getProvider(ProcessKeyCache.class);\n\n        Multibinder<GaugeProvider> gauges = Multibinder.newSetBinder(binder(), GaugeProvider.class);\n        gauges.addBinding().toInstance(create(\"hit-count\", provider, ProcessKeyCache::hitCount));\n        gauges.addBinding().toInstance(create(\"miss-count\", provider, ProcessKeyCache::missCount));\n    }\n\n    private static GaugeProvider<Long> create(String suffix, Provider<ProcessKeyCache> provider, ToLongFunction<ProcessKeyCache> value) {\n        return new GaugeProvider<>() {\n            @Override\n            public String name() {\n                return \"process-key-cache-\" + suffix;\n            }\n\n            @Override\n            public Gauge<Long> gauge() {\n                ProcessKeyCache cache = provider.get();\n                return () -> value.applyAsLong(cache);\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/ProcessQueueDao.java",
    "content": "package com.walmartlabs.concord.server.process.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.db.PgUtils;\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.runtime.v2.model.ExclusiveMode;\nimport com.walmartlabs.concord.sdk.EventType;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.Utils;\nimport com.walmartlabs.concord.server.jooq.Tables;\nimport com.walmartlabs.concord.server.jooq.tables.ProcessCheckpoints;\nimport com.walmartlabs.concord.server.jooq.tables.ProcessEvents;\nimport com.walmartlabs.concord.server.jooq.tables.ProcessQueue;\nimport com.walmartlabs.concord.server.jooq.tables.records.ProcessQueueRecord;\nimport com.walmartlabs.concord.server.process.*;\nimport com.walmartlabs.concord.server.process.ProcessEntry.CheckpointRestoreHistoryEntry;\nimport com.walmartlabs.concord.server.process.ProcessEntry.ProcessCheckpointEntry;\nimport com.walmartlabs.concord.server.process.ProcessEntry.ProcessStatusHistoryEntry;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport org.jooq.*;\nimport org.jooq.Record;\nimport org.jooq.exception.DataAccessException;\nimport org.jooq.impl.DSL;\nimport org.jooq.util.postgres.PostgresDSL;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport java.time.Duration;\nimport java.time.OffsetDateTime;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.db.PgUtils.*;\nimport static com.walmartlabs.concord.server.jooq.Tables.*;\nimport static com.walmartlabs.concord.server.jooq.tables.Organizations.ORGANIZATIONS;\nimport static com.walmartlabs.concord.server.jooq.tables.ProcessCheckpoints.PROCESS_CHECKPOINTS;\nimport static com.walmartlabs.concord.server.jooq.tables.ProcessEvents.PROCESS_EVENTS;\nimport static com.walmartlabs.concord.server.jooq.tables.ProcessQueue.PROCESS_QUEUE;\nimport static com.walmartlabs.concord.server.jooq.tables.Projects.PROJECTS;\nimport static org.jooq.impl.DSL.*;\n\npublic class ProcessQueueDao extends AbstractDao {\n\n    public static final String ENQUEUED_NOW_METRIC = \"ENQUEUED_NOW\";\n\n    private static final TypeReference<List<ProcessCheckpointEntry>> LIST_OF_CHECKPOINTS = new TypeReference<List<ProcessCheckpointEntry>>() {\n    };\n    private static final TypeReference<List<CheckpointRestoreHistoryEntry>> LIST_OF_CHECKPOINTS_HISTORY = new TypeReference<List<CheckpointRestoreHistoryEntry>>() {\n    };\n    private static final TypeReference<ProcessStatusHistoryEntry> STATUS_HISTORY_ENTRY = new TypeReference<ProcessStatusHistoryEntry>() {\n    };\n    private static final TypeReference<List<ProcessStatusHistoryEntry>> LIST_OF_STATUS_HISTORY = new TypeReference<List<ProcessStatusHistoryEntry>>() {\n    };\n\n    private static final Field<?>[] PROCESS_QUEUE_FIELDS = processEntryFields();\n\n    private final ConcordObjectMapper objectMapper;\n\n    @Inject\n    public ProcessQueueDao(@MainDB Configuration cfg, ConcordObjectMapper objectMapper) {\n        super(cfg);\n        this.objectMapper = objectMapper;\n    }\n\n    @Override\n    public void tx(Tx t) {\n        super.tx(t);\n    }\n\n    @Override\n    protected <T> T txResult(TxResult<T> t) {\n        return super.txResult(t);\n    }\n\n    public ProcessKey getKey(UUID instanceId) {\n        return dsl().select(PROCESS_QUEUE.CREATED_AT)\n                .from(PROCESS_QUEUE)\n                .where(PROCESS_QUEUE.INSTANCE_ID.eq(instanceId))\n                .fetchOne(r -> new ProcessKey(instanceId, r.value1()));\n    }\n\n    public void insert(DSLContext tx, ProcessKey processKey, ProcessStatus status, ProcessKind kind,\n                       UUID parentInstanceId, UUID projectId,\n                       UUID repoId, String branchOrTag, String commitId,\n                       UUID initiatorId, Map<String, Object> meta, TriggeredByEntry triggeredBy) {\n\n        tx.insertInto(PROCESS_QUEUE)\n                .set(PROCESS_QUEUE.INSTANCE_ID, processKey.getInstanceId())\n                .set(PROCESS_QUEUE.PROCESS_KIND, kind.toString())\n                .set(PROCESS_QUEUE.PARENT_INSTANCE_ID, parentInstanceId)\n                .set(PROCESS_QUEUE.PROJECT_ID, projectId)\n                .set(PROCESS_QUEUE.REPO_ID, repoId)\n                .set(PROCESS_QUEUE.COMMIT_ID, commitId)\n                .set(PROCESS_QUEUE.COMMIT_BRANCH, branchOrTag)\n                .set(PROCESS_QUEUE.CREATED_AT, processKey.getCreatedAt())\n                .set(PROCESS_QUEUE.INITIATOR_ID, initiatorId)\n                .set(PROCESS_QUEUE.CURRENT_STATUS, status.toString())\n                .set(PROCESS_QUEUE.LAST_UPDATED_AT, currentOffsetDateTime())\n                .execute();\n\n        tx.insertInto(PROCESS_META)\n                .set(PROCESS_META.INSTANCE_ID, processKey.getInstanceId())\n                .set(PROCESS_META.INSTANCE_CREATED_AT, processKey.getCreatedAt())\n                .set(PROCESS_META.META, objectMapper.toJSONB(meta))\n                .execute();\n\n        if (triggeredBy != null) {\n            tx.insertInto(PROCESS_TRIGGER_INFO)\n                    .set(PROCESS_TRIGGER_INFO.INSTANCE_ID, processKey.getInstanceId())\n                    .set(PROCESS_TRIGGER_INFO.INSTANCE_CREATED_AT, processKey.getCreatedAt())\n                    .set(PROCESS_TRIGGER_INFO.TRIGGERED_BY, objectMapper.toJSONB(triggeredBy))\n                    .execute();\n        }\n    }\n\n    public void updateAgentId(DSLContext tx, ProcessKey processKey, @Nullable String agentId, ProcessStatus status) {\n        UUID instanceId = processKey.getInstanceId();\n\n        int i = tx.update(PROCESS_QUEUE)\n                .set(PROCESS_QUEUE.CURRENT_STATUS, status.toString())\n                .set(PROCESS_QUEUE.LAST_AGENT_ID, agentId)\n                .set(PROCESS_QUEUE.LAST_UPDATED_AT, currentOffsetDateTime())\n                .set(PROCESS_QUEUE.LAST_RUN_AT, createRunningAtValue(status))\n                .where(PROCESS_QUEUE.INSTANCE_ID.eq(instanceId))\n                .execute();\n\n        if (i != 1) {\n            throw new DataAccessException(\"Invalid number of rows updated: \" + i);\n        }\n    }\n\n    public Optional<OffsetDateTime> getLastRunAt(DSLContext tx, ProcessKey processKey) {\n        return tx.select(PROCESS_QUEUE.LAST_RUN_AT)\n                .from(PROCESS_QUEUE)\n                .where(PROCESS_QUEUE.INSTANCE_ID.eq(processKey.getInstanceId()))\n                .fetchOptional(PROCESS_QUEUE.LAST_RUN_AT);\n    }\n\n    private static Field<OffsetDateTime> createRunningAtValue(ProcessStatus status) {\n        return when(PROCESS_QUEUE.CURRENT_STATUS.eq(ProcessStatus.RUNNING.toString()), PROCESS_QUEUE.LAST_RUN_AT)\n                .otherwise(when(value(status.toString()).eq(ProcessStatus.RUNNING.toString()), currentOffsetDateTime())\n                        .otherwise(PROCESS_QUEUE.LAST_RUN_AT));\n    }\n\n    public boolean enqueue(DSLContext tx, ProcessKey processKey, Set<String> tags, OffsetDateTime startAt,\n                           Map<String, Object> requirements, Long processTimeout, Set<String> handlers,\n                           Map<String, Object> meta, Imports imports, ExclusiveMode exclusive,\n                           String runtime, List<String> dependencies, Long suspendTimeout,\n                           Collection<ProcessStatus> expectedStatuses) {\n\n        UpdateSetMoreStep<ProcessQueueRecord> q = tx.update(PROCESS_QUEUE)\n                .set(PROCESS_QUEUE.CURRENT_STATUS, ProcessStatus.ENQUEUED.toString())\n                .set(PROCESS_QUEUE.LAST_UPDATED_AT, currentOffsetDateTime());\n\n        if (tags != null) {\n            q.set(PROCESS_QUEUE.PROCESS_TAGS, toArray(tags));\n        }\n\n        if (startAt != null) {\n            q.set(PROCESS_QUEUE.START_AT, startAt);\n        }\n\n        if (requirements != null) {\n            q.set(PROCESS_QUEUE.REQUIREMENTS, objectMapper.toJSONB(requirements));\n        }\n\n        if (processTimeout != null) {\n            q.set(PROCESS_QUEUE.TIMEOUT, processTimeout);\n        }\n\n        if (suspendTimeout != null) {\n            q.set(PROCESS_QUEUE.SUSPEND_TIMEOUT, suspendTimeout);\n        }\n\n        if (handlers != null) {\n            q.set(PROCESS_QUEUE.HANDLERS, toArray(handlers));\n        }\n\n        if (imports != null && !imports.isEmpty()) {\n            q.set(PROCESS_QUEUE.IMPORTS, objectMapper.toJSONB(imports));\n        }\n\n        if (exclusive != null) {\n            q.set(PROCESS_QUEUE.EXCLUSIVE, objectMapper.toJSONB(exclusive));\n        }\n\n        if (runtime != null) {\n            q.set(PROCESS_QUEUE.RUNTIME, runtime);\n        }\n\n        if (dependencies != null && !dependencies.isEmpty()) {\n            q.set(PROCESS_QUEUE.DEPENDENCIES, Utils.toArray(dependencies));\n        }\n\n        int i = q\n                .where(PROCESS_QUEUE.INSTANCE_ID.eq(processKey.getInstanceId())\n                        .and(PROCESS_QUEUE.CURRENT_STATUS.in(expectedStatuses.stream().map(Enum::name).collect(Collectors.toList()))))\n                .execute();\n\n        if (i == 1) {\n            if (meta != null) {\n                tx.update(PROCESS_META)\n                        .set(PROCESS_META.META, field(coalesce(PROCESS_META.META, field(\"?\", JSONB.class, JSONB.valueOf(\"{}\"))) + \" || ?::jsonb\", JSONB.class, objectMapper.toJSONB(meta)))\n                        .where(PROCESS_META.INSTANCE_ID.eq(processKey.getInstanceId())\n                                .and(PROCESS_META.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt())))\n                        .execute();\n            }\n        }\n\n        return i == 1;\n    }\n\n    public void updateRepositoryDetails(PartialProcessKey processKey, UUID repoId, String repoUrl,\n                                        String repoPath, String commitId, String commitBranch) {\n        tx(tx -> {\n            UpdateSetMoreStep<ProcessQueueRecord> q = tx.update(PROCESS_QUEUE)\n                    .set(PROCESS_QUEUE.LAST_UPDATED_AT, currentOffsetDateTime());\n\n            if (repoId != null) {\n                q.set(PROCESS_QUEUE.REPO_ID, repoId);\n            }\n\n            if (repoUrl != null) {\n                q.set(PROCESS_QUEUE.REPO_URL, repoUrl);\n            }\n\n            if (repoPath != null) {\n                q.set(PROCESS_QUEUE.REPO_PATH, repoPath);\n            }\n\n            if (commitId != null) {\n                q.set(PROCESS_QUEUE.COMMIT_ID, commitId);\n            }\n\n            if (commitBranch != null) {\n                q.set(PROCESS_QUEUE.COMMIT_BRANCH, commitBranch);\n            }\n\n            int i = q\n                    .where(PROCESS_QUEUE.INSTANCE_ID.eq(processKey.getInstanceId()))\n                    .execute();\n\n            if (i != 1) {\n                throw new DataAccessException(\"Invalid number of rows updated: \" + i);\n            }\n        });\n    }\n\n    public void updateStatus(DSLContext tx, ProcessKey processKey, ProcessStatus status) {\n        UUID instanceId = processKey.getInstanceId();\n\n        tx.update(PROCESS_QUEUE)\n                .set(PROCESS_QUEUE.CURRENT_STATUS, status.toString())\n                .set(PROCESS_QUEUE.LAST_RUN_AT, createRunningAtValue(status))\n                .set(PROCESS_QUEUE.LAST_UPDATED_AT, currentOffsetDateTime())\n                .where(PROCESS_QUEUE.INSTANCE_ID.eq(instanceId))\n                .execute();\n    }\n\n    public boolean updateStatus(DSLContext tx, ProcessKey processKey, ProcessStatus expected, ProcessStatus status) {\n        UUID instanceId = processKey.getInstanceId();\n\n        int i = tx.update(PROCESS_QUEUE)\n                .set(PROCESS_QUEUE.CURRENT_STATUS, status.toString())\n                .set(PROCESS_QUEUE.LAST_UPDATED_AT, currentOffsetDateTime())\n                .set(PROCESS_QUEUE.LAST_RUN_AT, createRunningAtValue(status))\n                .where(PROCESS_QUEUE.INSTANCE_ID.eq(instanceId)\n                        .and(PROCESS_QUEUE.CURRENT_STATUS.eq(expected.toString())))\n                .execute();\n\n        return i == 1;\n    }\n\n    public List<ProcessKey> updateStatus(List<ProcessKey> processKeys, List<ProcessStatus> expected, ProcessStatus status) {\n        return txResult(tx -> {\n            List<UUID> instanceIds = processKeys.stream()\n                    .map(PartialProcessKey::getInstanceId)\n                    .collect(Collectors.toList());\n\n            UpdateConditionStep<ProcessQueueRecord> q = tx.update(PROCESS_QUEUE)\n                    .set(PROCESS_QUEUE.CURRENT_STATUS, status.toString())\n                    .set(PROCESS_QUEUE.LAST_UPDATED_AT, currentOffsetDateTime())\n                    .set(PROCESS_QUEUE.LAST_RUN_AT, createRunningAtValue(status))\n                    .where(PROCESS_QUEUE.INSTANCE_ID.in(instanceIds));\n\n            if (expected != null) {\n                List<String> l = expected.stream()\n                        .map(Enum::toString)\n                        .collect(Collectors.toList());\n\n                q.and(PROCESS_QUEUE.CURRENT_STATUS.in(l));\n            }\n\n            return q.returningResult(PROCESS_QUEUE.INSTANCE_ID, PROCESS_QUEUE.CREATED_AT)\n                    .fetch().stream()\n                    .map(r -> new ProcessKey(r.value1(), r.value2()))\n                    .collect(Collectors.toList());\n        });\n    }\n\n    public boolean updateMeta(ProcessKey processKey, Map<String, Object> meta) {\n        return txResult(tx -> {\n            int i = tx.update(PROCESS_META)\n                    .set(PROCESS_META.META, field(coalesce(PROCESS_META.META, field(\"?::jsonb\", JSONB.class, JSONB.valueOf(\"{}\"))) + \" || ?::jsonb\", JSONB.class, objectMapper.toJSONB(meta)))\n                    .where(PROCESS_META.INSTANCE_ID.eq(processKey.getInstanceId())\n                            .and(PROCESS_META.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt())))\n                    .execute();\n\n            return i == 1;\n        });\n    }\n\n    public boolean removeMeta(ProcessKey processKey, String key) {\n        return txResult(tx -> {\n            Field<JSONB> v = field(\"{0}\", JSONB.class, PROCESS_META.META).minus(value(key));\n            int i = tx.update(PROCESS_META)\n                    .set(PROCESS_META.META, v)\n                    .where(PROCESS_META.INSTANCE_ID.eq(processKey.getInstanceId())\n                            .and(PROCESS_META.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt())))\n                    .execute();\n\n            return i == 1;\n        });\n    }\n\n    public void disable(ProcessKey processKey, boolean disabled) {\n        tx(tx -> disable(tx, processKey, disabled));\n    }\n\n    private void disable(DSLContext tx, ProcessKey processKey, boolean disabled) {\n        UUID instanceId = processKey.getInstanceId();\n\n        tx.update(PROCESS_QUEUE)\n                .set(PROCESS_QUEUE.IS_DISABLED, disabled)\n                .set(PROCESS_QUEUE.LAST_UPDATED_AT, currentOffsetDateTime())\n                .where(PROCESS_QUEUE.INSTANCE_ID.eq(instanceId))\n                .execute();\n\n    }\n\n    public void removeHandler(PartialProcessKey processKey, String handler) {\n        tx(tx -> tx.update(PROCESS_QUEUE)\n                .set(PROCESS_QUEUE.HANDLERS, PostgresDSL.arrayRemove(PROCESS_QUEUE.HANDLERS, field(\"{0}::text\", String.class, handler)))\n                .where(PROCESS_QUEUE.INSTANCE_ID.eq(processKey.getInstanceId()))\n                .execute());\n    }\n\n    public boolean touch(UUID instanceId) {\n        return txResult(tx -> {\n            int i = tx.update(PROCESS_QUEUE)\n                    .set(PROCESS_QUEUE.LAST_UPDATED_AT, currentOffsetDateTime())\n                    .where(PROCESS_QUEUE.INSTANCE_ID.eq(instanceId))\n                    .execute();\n\n            return i == 1;\n        });\n    }\n\n    public ProcessEntry get(ProcessKey processKey, Set<ProcessDataInclude> includes) {\n        return txResult(tx -> get(tx, processKey, includes));\n    }\n\n    public ProcessEntry get(DSLContext tx, ProcessKey key, Set<ProcessDataInclude> includes) {\n        SelectQuery<Record> query = buildSelect(tx, key, ProcessFilter.builder()\n                .includes(includes)\n                .build());\n\n        query.addConditions(PROCESS_QUEUE.INSTANCE_ID.eq(key.getInstanceId()));\n\n        return query.fetchOne(this::toEntry);\n    }\n\n    public String getRuntime(PartialProcessKey processKey) {\n        return dsl().select(PROCESS_QUEUE.RUNTIME).from(PROCESS_QUEUE)\n                .where(PROCESS_QUEUE.INSTANCE_ID.eq(processKey.getInstanceId()))\n                .fetchOne(PROCESS_QUEUE.RUNTIME);\n    }\n\n    public ProcessStatus getStatus(PartialProcessKey processKey) {\n        String status = dsl().select(PROCESS_QUEUE.CURRENT_STATUS)\n                .from(PROCESS_QUEUE)\n                .where(PROCESS_QUEUE.INSTANCE_ID.eq(processKey.getInstanceId()))\n                .fetchOne(PROCESS_QUEUE.CURRENT_STATUS);\n\n        if (status == null) {\n            return null;\n        }\n\n        return ProcessStatus.valueOf(status);\n    }\n\n    public List<ProcessEntry> get(List<PartialProcessKey> processKeys) {\n        if (processKeys.isEmpty()) {\n            return Collections.emptyList();\n        }\n\n        List<UUID> instanceIds = processKeys.stream()\n                .map(PartialProcessKey::getInstanceId)\n                .collect(Collectors.toList());\n\n        SelectQuery<Record> query = buildSelect(dsl(), ProcessFilter.builder().build());\n\n        query.addConditions(PROCESS_QUEUE.INSTANCE_ID.in(instanceIds));\n        return query.fetch(this::toEntry);\n    }\n\n    public List<ProcessKey> getCascade(PartialProcessKey parentKey) {\n        return txResult(tx -> getCascade(tx, parentKey));\n    }\n\n    public List<ProcessKey> getCascade(DSLContext tx, PartialProcessKey parentKey) {\n        UUID parentInstanceId = parentKey.getInstanceId();\n        return tx.withRecursive(\"children\").as(\n                        select(PROCESS_QUEUE.INSTANCE_ID, PROCESS_QUEUE.CREATED_AT).from(PROCESS_QUEUE)\n                                .where(PROCESS_QUEUE.INSTANCE_ID.eq(parentInstanceId))\n                                .unionAll(\n                                        select(PROCESS_QUEUE.INSTANCE_ID, PROCESS_QUEUE.CREATED_AT).from(PROCESS_QUEUE)\n                                                .join(name(\"children\"))\n                                                .on(PROCESS_QUEUE.PARENT_INSTANCE_ID.eq(\n                                                        field(name(\"children\", \"INSTANCE_ID\"), UUID.class)))))\n                .select()\n                .from(name(\"children\"))\n                .fetch(r -> new ProcessKey(r.get(0, UUID.class), r.get(1, OffsetDateTime.class)));\n    }\n\n    public ProcessKey getRootId(PartialProcessKey processKey) {\n        UUID instanceId = processKey.getInstanceId();\n\n        Name cteName = name(\"parent\");\n        Field<UUID> cteParentId = field(name(\"parent\", PROCESS_QUEUE.PARENT_INSTANCE_ID.getName()), UUID.class);\n\n        return dsl()\n                .withRecursive(cteName).as(\n                        select(PROCESS_QUEUE.INSTANCE_ID, PROCESS_QUEUE.CREATED_AT, PROCESS_QUEUE.PARENT_INSTANCE_ID)\n                                .from(PROCESS_QUEUE)\n                                .where(PROCESS_QUEUE.INSTANCE_ID.eq(instanceId))\n                                .unionAll(\n                                        select(PROCESS_QUEUE.INSTANCE_ID, PROCESS_QUEUE.CREATED_AT, PROCESS_QUEUE.PARENT_INSTANCE_ID).\n                                                from(PROCESS_QUEUE)\n                                                .join(cteName)\n                                                .on(PROCESS_QUEUE.INSTANCE_ID.eq(cteParentId))))\n                .select()\n                .from(cteName)\n                .where(cteParentId.isNull())\n                .fetchOne(r -> new ProcessKey(r.get(0, UUID.class), r.get(1, OffsetDateTime.class)));\n    }\n\n    public ProcessInitiatorEntry getInitiator(PartialProcessKey processKey) {\n        return dsl().select(PROCESS_QUEUE.INSTANCE_ID, PROCESS_QUEUE.CREATED_AT, PROCESS_QUEUE.CURRENT_STATUS, PROCESS_QUEUE.INITIATOR_ID)\n                .from(PROCESS_QUEUE)\n                .where(PROCESS_QUEUE.INSTANCE_ID.eq(processKey.getInstanceId()))\n                .fetchOne(r -> ProcessInitiatorEntry.builder()\n                        .instanceId(r.get(PROCESS_QUEUE.INSTANCE_ID))\n                        .createdAt(r.get(PROCESS_QUEUE.CREATED_AT))\n                        .status(ProcessStatus.valueOf(r.get(PROCESS_QUEUE.CURRENT_STATUS)))\n                        .initiatorId(r.get(PROCESS_QUEUE.INITIATOR_ID))\n                        .build());\n    }\n\n    public UUID getOrgId(UUID instanceId) {\n        Field<UUID> orgId = select(PROJECTS.ORG_ID)\n                .from(PROJECTS)\n                .where(PROJECTS.PROJECT_ID.eq(PROCESS_QUEUE.PROJECT_ID))\n                .asField();\n\n        return dsl().select(orgId)\n                .from(PROCESS_QUEUE)\n                .where(PROCESS_QUEUE.INSTANCE_ID.eq(instanceId))\n                .fetchOne(orgId);\n    }\n\n    public UUID getProjectId(UUID instanceId) {\n        return dsl().select(PROCESS_QUEUE.PROJECT_ID)\n                .from(PROCESS_QUEUE)\n                .where(PROCESS_QUEUE.INSTANCE_ID.eq(instanceId))\n                .fetchOne(PROCESS_QUEUE.PROJECT_ID);\n    }\n\n    public Imports getImports(PartialProcessKey processKey) {\n        return dsl().select(PROCESS_QUEUE.IMPORTS).from(PROCESS_QUEUE)\n                .where(PROCESS_QUEUE.INSTANCE_ID.eq(processKey.getInstanceId()))\n                .fetchOne(r -> objectMapper.fromJSONB(r.get(PROCESS_QUEUE.IMPORTS), Imports.class));\n    }\n\n    public List<ProcessEntry> list(ProcessFilter filter) {\n        SelectQuery<Record> query = buildSelect(dsl(), filter);\n\n        boolean findAdjacentToDateRows = filter.beforeCreatedAt() == null && filter.beforeCreatedAt() != null;\n        if (findAdjacentToDateRows) {\n            query.addOrderBy(PROCESS_QUEUE.CREATED_AT.asc());\n        } else {\n            query.addOrderBy(PROCESS_QUEUE.CREATED_AT.desc());\n        }\n\n        List<ProcessEntry> processEntries = query.fetch(this::toEntry);\n\n        if (findAdjacentToDateRows) {\n            Collections.reverse(processEntries);\n        }\n\n        return processEntries;\n    }\n\n    public List<ProcessRequirementsEntry> listRequirements(ProcessStatus processStatus, List<ProcessFilter.DateFilter> startAt, int limit, int offset, List<ProcessFilter.JsonFilter> requirements) {\n        SelectQuery<Record> query = dsl().selectQuery();\n\n        query.addSelect(PROCESS_QUEUE.INSTANCE_ID, PROCESS_QUEUE.CREATED_AT, PROCESS_QUEUE.REQUIREMENTS);\n        query.addFrom(PROCESS_QUEUE);\n        query.addConditions(PROCESS_QUEUE.CURRENT_STATUS.eq(processStatus.name()));\n        FilterUtils.applyDate(query, PROCESS_QUEUE.START_AT, startAt);\n        FilterUtils.applyJson(query, PROCESS_QUEUE.REQUIREMENTS, requirements);\n\n        query.addLimit(limit);\n        query.addOffset(offset);\n\n        return query.fetch(r -> ProcessRequirementsEntry.builder()\n                .instanceId(r.get(PROCESS_QUEUE.INSTANCE_ID))\n                .createdAt(r.get(PROCESS_QUEUE.CREATED_AT))\n                .requirements(objectMapper.fromJSONB(r.get(PROCESS_QUEUE.REQUIREMENTS)))\n                .build());\n    }\n\n    public int count(ProcessFilter filter) {\n        DSLContext tx = dsl();\n        SelectQuery<Record> query = buildSelect(tx, filter);\n        return tx.selectCount().from(query)\n                .fetchOptional()\n                .map(Record1::value1)\n                .orElse(0);\n    }\n\n    public Map<String, Integer> getStatistics() {\n        return dsl().select(PROCESS_QUEUE.CURRENT_STATUS, DSL.count(asterisk())).from(PROCESS_QUEUE)\n                .groupBy(PROCESS_QUEUE.CURRENT_STATUS)\n                .union(select(value(ENQUEUED_NOW_METRIC), DSL.count(asterisk())).from(PROCESS_QUEUE)\n                        .where(PROCESS_QUEUE.CURRENT_STATUS.eq(ProcessStatus.ENQUEUED.name()))\n                        .and(or(PROCESS_QUEUE.START_AT.isNull(), PROCESS_QUEUE.START_AT.lessOrEqual(currentOffsetDateTime()))))\n                .fetchMap(Record2::value1, Record2::value2);\n    }\n\n    // TODO move to EventDao?\n    public List<ProcessStatusHistoryEntry> getStatusHistory(ProcessKey processKey) {\n        ProcessEvents pe = PROCESS_EVENTS.as(\"pe\");\n        return dsl().select(statusHistoryEntryToJsonb(pe))\n                .from(pe)\n                .where(pe.INSTANCE_ID.eq(processKey.getInstanceId())\n                        .and(pe.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt()))\n                        .and(pe.EVENT_TYPE.eq(EventType.PROCESS_STATUS.name())))\n                .fetch(r -> objectMapper.fromJSONB(r.value1(), STATUS_HISTORY_ENTRY));\n    }\n\n    public ProjectIdAndInitiator getProjectIdAndInitiator(PartialProcessKey processKey) {\n        return dsl().select(PROCESS_QUEUE.PROJECT_ID, PROCESS_QUEUE.INITIATOR_ID).from(PROCESS_QUEUE)\n                .where(PROCESS_QUEUE.INSTANCE_ID.eq(processKey.getInstanceId()))\n                .fetchOne(r -> new ProjectIdAndInitiator(r.get(PROCESS_QUEUE.PROJECT_ID), r.get(PROCESS_QUEUE.INITIATOR_ID)));\n    }\n\n    public void clearStartAt(PartialProcessKey processKey) {\n        tx(tx -> tx.update(PROCESS_QUEUE)\n                .set(PROCESS_QUEUE.START_AT, val((OffsetDateTime) null))\n                .where(PROCESS_QUEUE.INSTANCE_ID.eq(processKey.getInstanceId()))\n                .execute());\n    }\n\n    private static Field<JSONB> statusHistoryEntryToJsonb(ProcessEvents pe) {\n        return jsonbStripNulls(\n                jsonbBuildObject(\n                        inline(\"id\"), pe.EVENT_ID,\n                        inline(\"changeDate\"), toJsonDate(pe.EVENT_DATE),\n                        inline(\"status\"), field(\"{0}->'status'\", Object.class, pe.EVENT_DATA),\n                        inline(\"payload\"), field(\"{0} - 'status'\", Object.class, pe.EVENT_DATA)));\n    }\n\n    private static Field<JSONB> checkpointHistoryEntryToJsonb(ProcessEvents pe) {\n        return jsonbStripNulls(\n                jsonbBuildObject(\n                        inline(\"id\"), pe.EVENT_SEQ,\n                        inline(\"changeDate\"), toJsonDate(pe.EVENT_DATE),\n                        inline(\"checkpointId\"), field(\"{0}->'checkpointId'\", Object.class, pe.EVENT_DATA),\n                        inline(\"processStatus\"), field(\"{0}->'processStatus'\", Object.class, pe.EVENT_DATA)));\n    }\n\n    private static void filterByTags(SelectQuery<Record> query, Set<String> tags) {\n        if (tags == null || tags.isEmpty()) {\n            return;\n        }\n\n        String[] as = tags.toArray(new String[0]);\n        query.addConditions(PgUtils.contains(PROCESS_QUEUE.PROCESS_TAGS, as));\n    }\n\n    public boolean exists(PartialProcessKey processKey) {\n        DSLContext tx = dsl();\n\n        UUID instanceId = processKey.getInstanceId();\n        return tx.fetchExists(tx.selectFrom(PROCESS_QUEUE)\n                .where(PROCESS_QUEUE.INSTANCE_ID.eq(instanceId)));\n    }\n\n    public void updateExclusive(DSLContext tx, ProcessKey key, ExclusiveMode exclusive) {\n        tx.update(PROCESS_QUEUE)\n                .set(PROCESS_QUEUE.EXCLUSIVE, objectMapper.toJSONB(exclusive))\n                .where(PROCESS_QUEUE.INSTANCE_ID.eq(key.getInstanceId()))\n                .execute();\n    }\n\n    public void addToTotalRunningTime(DSLContext tx, ProcessKey processKey, Duration duration) {\n        tx.update(PROCESS_QUEUE)\n                .set(PROCESS_QUEUE.TOTAL_RUNTIME_MS, coalesce(PROCESS_QUEUE.TOTAL_RUNTIME_MS, 0L).plus(duration.toMillis()))\n                .where(PROCESS_QUEUE.INSTANCE_ID.eq(processKey.getInstanceId()))\n                .execute();\n    }\n\n    private SelectQuery<Record> buildSelect(DSLContext tx, ProcessFilter filter) {\n        return buildSelect(tx, null, filter);\n    }\n\n    private SelectQuery<Record> buildSelect(DSLContext tx, ProcessKey key, ProcessFilter filter) {\n        SelectQuery<Record> query = tx.selectQuery();\n\n        // process_queue\n        query.addSelect(PROCESS_QUEUE_FIELDS);\n        query.addFrom(PROCESS_QUEUE);\n\n        // triggered by\n        query.addSelect(PROCESS_TRIGGER_INFO.TRIGGERED_BY);\n        query.addJoin(PROCESS_TRIGGER_INFO, JoinType.LEFT_OUTER_JOIN, PROCESS_TRIGGER_INFO.INSTANCE_ID.eq(PROCESS_QUEUE.INSTANCE_ID)\n                .and(PROCESS_TRIGGER_INFO.INSTANCE_CREATED_AT.eq(PROCESS_QUEUE.CREATED_AT)));\n\n        // meta\n        query.addSelect(function(\"jsonb_strip_nulls\", JSONB.class, PROCESS_META.META).as(PROCESS_META.META));\n        query.addJoin(PROCESS_META, JoinType.LEFT_OUTER_JOIN, PROCESS_META.INSTANCE_ID.eq(PROCESS_QUEUE.INSTANCE_ID)\n                .and(PROCESS_META.INSTANCE_CREATED_AT.eq(PROCESS_QUEUE.CREATED_AT)));\n\n        // users\n        query.addSelect(USERS.USERNAME);\n        query.addJoin(USERS, JoinType.LEFT_OUTER_JOIN, USERS.USER_ID.eq(PROCESS_QUEUE.INITIATOR_ID));\n\n        // repositories\n        query.addSelect(REPOSITORIES.REPO_NAME);\n        query.addJoin(REPOSITORIES, JoinType.LEFT_OUTER_JOIN, REPOSITORIES.REPO_ID.eq(PROCESS_QUEUE.REPO_ID));\n\n        // organizations\n        Field<String> orgNameField = select(ORGANIZATIONS.ORG_NAME)\n                .from(ORGANIZATIONS)\n                .where(ORGANIZATIONS.ORG_ID.eq(Tables.PROJECTS.ORG_ID)).asField(ORGANIZATIONS.ORG_NAME.getName());\n        query.addSelect(orgNameField);\n\n        // projects\n        query.addSelect(Tables.PROJECTS.PROJECT_NAME, Tables.PROJECTS.ORG_ID);\n        query.addJoin(Tables.PROJECTS, JoinType.LEFT_OUTER_JOIN, Tables.PROJECTS.PROJECT_ID.eq(PROCESS_QUEUE.PROJECT_ID));\n\n        Set<UUID> orgIds = filter.orgIds();\n        if (orgIds != null && !orgIds.isEmpty()) {\n            if (filter.includeWithoutProject()) {\n                query.addConditions(PROJECTS.ORG_ID.in(filter.orgIds()).or(PROCESS_QUEUE.PROJECT_ID.isNull()));\n            } else {\n                query.addConditions(PROJECTS.ORG_ID.in(filter.orgIds()));\n            }\n        }\n\n        if (filter.projectId() != null) {\n            if (filter.includeWithoutProject()) {\n                query.addConditions(PROCESS_QUEUE.PROJECT_ID.eq(filter.projectId()).or(PROCESS_QUEUE.PROJECT_ID.isNull()));\n            } else {\n                query.addConditions(PROCESS_QUEUE.PROJECT_ID.eq(filter.projectId()));\n            }\n        }\n\n        if (filter.repoId() != null) {\n            if (filter.includeWithoutProject()) {\n                query.addConditions(PROCESS_QUEUE.REPO_ID.eq(filter.repoId()).or(PROCESS_QUEUE.REPO_ID.isNull()));\n            } else {\n                query.addConditions(PROCESS_QUEUE.REPO_ID.eq(filter.repoId()));\n            }\n        }\n\n        if (filter.repoName() != null && filter.repoId() == null) {\n            SelectConditionStep<Record1<UUID>> repoIdSelect = select(REPOSITORIES.REPO_ID)\n                    .from(REPOSITORIES)\n                    .where(REPOSITORIES.REPO_NAME.startsWith(filter.repoName()));\n\n            if (filter.projectId() != null) {\n                repoIdSelect = repoIdSelect.and(REPOSITORIES.PROJECT_ID.eq(filter.projectId()));\n            }\n\n            query.addConditions(PROCESS_QUEUE.REPO_ID.in(repoIdSelect));\n        }\n\n        if (filter.initiator() != null) {\n            query.addConditions(USERS.USERNAME.startsWith(filter.initiator()));\n        }\n\n        if (filter.afterCreatedAt() != null) {\n            query.addConditions(PROCESS_QUEUE.CREATED_AT.greaterThan(filter.afterCreatedAt()));\n        }\n\n        if (filter.beforeCreatedAt() != null) {\n            query.addConditions(PROCESS_QUEUE.CREATED_AT.lessThan(filter.beforeCreatedAt()));\n        }\n\n        ProcessStatus status = filter.status();\n        if (status != null) {\n            query.addConditions(PROCESS_QUEUE.CURRENT_STATUS.eq(status.name()));\n        }\n\n        if (filter.parentId() != null) {\n            query.addConditions(PROCESS_QUEUE.PARENT_INSTANCE_ID.eq(filter.parentId()));\n        }\n\n        // process meta\n        MetadataUtils.apply(query, PROCESS_META.META, filter.metaFilters());\n\n        filterByTags(query, filter.tags());\n\n        FilterUtils.applyDate(query, PROCESS_QUEUE.START_AT, filter.startAt());\n        FilterUtils.applyJson(query, PROCESS_QUEUE.REQUIREMENTS, filter.requirements());\n\n        Set<ProcessDataInclude> includes = filter.includes();\n\n        if (includes.contains(ProcessDataInclude.CHILDREN_IDS)) {\n            ProcessQueue pq = PROCESS_QUEUE.as(\"pq\");\n            SelectConditionStep<Record1<UUID>> childIds = DSL.select(pq.INSTANCE_ID)\n                    .from(pq)\n                    .where(pq.PARENT_INSTANCE_ID.eq(PROCESS_QUEUE.INSTANCE_ID));\n\n            Field<UUID[]> childIdsField = DSL.field(\"array({0})\", UUID[].class, childIds).as(\"children_ids\");\n\n            query.addSelect(childIdsField);\n        }\n\n        if (includes.contains(ProcessDataInclude.CHECKPOINTS)) {\n            ProcessCheckpoints pc = PROCESS_CHECKPOINTS.as(\"pc\");\n            SelectJoinStep<Record1<JSONB>> checkpoints = tx.select(\n                            function(\"to_jsonb\", JSONB.class,\n                                    function(\"array_agg\", Object.class,\n                                            jsonbStripNulls(\n                                                    jsonbBuildObject(\n                                                            inline(\"id\"), pc.CHECKPOINT_ID,\n                                                            inline(\"name\"), pc.CHECKPOINT_NAME,\n                                                            inline(\"correlationId\"), pc.CORRELATION_ID,\n                                                            inline(\"createdAt\"), toJsonDate(pc.CHECKPOINT_DATE))))))\n                    .from(pc);\n\n            if (key != null) {\n                checkpoints.where(pc.INSTANCE_ID.eq(key.getInstanceId())\n                        .and(pc.INSTANCE_CREATED_AT.eq(key.getCreatedAt())));\n            } else {\n                checkpoints.where(pc.INSTANCE_ID.eq(PROCESS_QUEUE.INSTANCE_ID)\n                        .and(pc.INSTANCE_CREATED_AT.eq(PROCESS_QUEUE.CREATED_AT)));\n            }\n\n            query.addSelect(checkpoints.asField(\"checkpoints\"));\n        }\n\n        if (includes.contains(ProcessDataInclude.CHECKPOINTS_HISTORY)) {\n            ProcessEvents pe = PROCESS_EVENTS.as(\"pe\");\n\n            SelectJoinStep<Record1<JSONB>> history = tx.select(function(\"to_jsonb\", JSONB.class,\n                            function(\"array_agg\", Object.class, checkpointHistoryEntryToJsonb(pe))))\n                    .from(pe);\n\n            if (key != null) {\n                history.where(pe.INSTANCE_ID.eq(key.getInstanceId())\n                        .and(pe.INSTANCE_CREATED_AT.eq(key.getCreatedAt())\n                                .and(pe.EVENT_TYPE.eq(EventType.CHECKPOINT_RESTORE.name()))));\n            } else {\n                history.where(PROCESS_QUEUE.INSTANCE_ID.eq(pe.INSTANCE_ID)\n                        .and(pe.INSTANCE_CREATED_AT.eq(PROCESS_QUEUE.CREATED_AT)\n                                .and(pe.EVENT_TYPE.eq(EventType.CHECKPOINT_RESTORE.name()))));\n            }\n\n            query.addSelect(history.asField(\"checkpoints_history\"));\n        }\n\n        if (includes.contains(ProcessDataInclude.STATUS_HISTORY)) {\n            ProcessEvents pe = PROCESS_EVENTS.as(\"pe\");\n            SelectJoinStep<Record1<JSONB>> history = tx.select(function(\"to_jsonb\", JSONB.class,\n                            function(\"array_agg\", Object.class, statusHistoryEntryToJsonb(pe))))\n                    .from(pe);\n\n            if (key != null) {\n                history.where(pe.INSTANCE_ID.eq(key.getInstanceId())\n                        .and(pe.INSTANCE_CREATED_AT.eq(key.getCreatedAt())\n                                .and(pe.EVENT_TYPE.eq(EventType.PROCESS_STATUS.name()))));\n            } else {\n                history.where(PROCESS_QUEUE.INSTANCE_ID.eq(pe.INSTANCE_ID)\n                        .and(pe.INSTANCE_CREATED_AT.eq(PROCESS_QUEUE.CREATED_AT)\n                                .and(pe.EVENT_TYPE.eq(EventType.PROCESS_STATUS.name()))));\n            }\n\n            query.addSelect(history.asField(\"status_history\"));\n        }\n\n        Integer limit = filter.limit();\n        if (limit != null && limit > 0) {\n            query.addLimit(limit);\n        }\n\n        Integer offset = filter.offset();\n        if (offset != null && offset > 0) {\n            query.addOffset(offset);\n        }\n\n        return query;\n    }\n\n    private ProcessEntry toEntry(Record r) {\n        if (r == null) {\n            return null;\n        }\n\n        ProcessKind kind;\n        String s = r.get(PROCESS_QUEUE.PROCESS_KIND);\n        if (s != null) {\n            kind = ProcessKind.valueOf(s);\n        } else {\n            kind = ProcessKind.DEFAULT;\n        }\n\n        Set<String> tags = Collections.emptySet();\n        String[] as = r.get(PROCESS_QUEUE.PROCESS_TAGS);\n        if (as != null && as.length > 0) {\n            tags = new HashSet<>(as.length);\n            Collections.addAll(tags, as);\n        }\n\n        return ImmutableProcessEntry.builder()\n                .instanceId(r.get(PROCESS_QUEUE.INSTANCE_ID))\n                .kind(kind)\n                .parentInstanceId(r.get(PROCESS_QUEUE.PARENT_INSTANCE_ID))\n                .orgId(r.get(Tables.PROJECTS.ORG_ID))\n                .orgName(r.get(ORGANIZATIONS.ORG_NAME))\n                .projectId(r.get(PROCESS_QUEUE.PROJECT_ID))\n                .projectName(r.get(Tables.PROJECTS.PROJECT_NAME))\n                .repoId(r.get(PROCESS_QUEUE.REPO_ID))\n                .repoName(r.get(REPOSITORIES.REPO_NAME))\n                .repoUrl(r.get(PROCESS_QUEUE.REPO_URL))\n                .repoPath(r.get(PROCESS_QUEUE.REPO_PATH))\n                .commitId(r.get(PROCESS_QUEUE.COMMIT_ID))\n                .commitBranch(r.get(PROCESS_QUEUE.COMMIT_BRANCH))\n                .initiator(r.get(USERS.USERNAME))\n                .initiatorId(r.get(PROCESS_QUEUE.INITIATOR_ID))\n                .startAt(r.get(PROCESS_QUEUE.START_AT))\n                .lastUpdatedAt(r.get(PROCESS_QUEUE.LAST_UPDATED_AT))\n                .lastRunAt(r.get(PROCESS_QUEUE.LAST_RUN_AT))\n                .totalRuntimeMs(r.get(PROCESS_QUEUE.TOTAL_RUNTIME_MS))\n                .createdAt(r.get(PROCESS_QUEUE.CREATED_AT))\n                .status(ProcessStatus.valueOf(r.get(PROCESS_QUEUE.CURRENT_STATUS)))\n                .lastAgentId(r.get(PROCESS_QUEUE.LAST_AGENT_ID))\n                .tags(tags)\n                .childrenIds(toSet(getOrNull(r, \"children_ids\")))\n                .meta(objectMapper.fromJSONB(r.get(PROCESS_META.META)))\n                .handlers(toSet(r.get(PROCESS_QUEUE.HANDLERS)))\n                .requirements(objectMapper.fromJSONB(r.get(PROCESS_QUEUE.REQUIREMENTS)))\n                .disabled(r.get(PROCESS_QUEUE.IS_DISABLED))\n                .logFileName(r.get(PROCESS_QUEUE.INSTANCE_ID) + \".log\")\n                .checkpoints(objectMapper.fromJSONB(getOrNull(r, \"checkpoints\"), LIST_OF_CHECKPOINTS))\n                .statusHistory(objectMapper.fromJSONB(getOrNull(r, \"status_history\"), LIST_OF_STATUS_HISTORY))\n                .checkpointRestoreHistory(objectMapper.fromJSONB(getOrNull(r, \"checkpoints_history\"), LIST_OF_CHECKPOINTS_HISTORY))\n                .triggeredBy(objectMapper.fromJSONB(r.get(PROCESS_TRIGGER_INFO.TRIGGERED_BY), TriggeredByEntry.class))\n                .timeout(r.get(PROCESS_QUEUE.TIMEOUT))\n                .suspendTimeout(r.get(PROCESS_QUEUE.SUSPEND_TIMEOUT))\n                .runtime(r.get(PROCESS_QUEUE.RUNTIME))\n                .build();\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private <E> E getOrNull(Record r, String fieldName) {\n        Field<?> field = r.field(fieldName);\n        if (field == null) {\n            return null;\n        }\n\n        return (E) r.get(field);\n    }\n\n    private static <E> Set<E> toSet(E[] arr) {\n        if (arr == null) {\n            return Collections.emptySet();\n        }\n        return new HashSet<>(Arrays.asList(arr));\n    }\n\n    private static String[] toArray(Set<String> s) {\n        if (s == null || s.isEmpty()) {\n            return new String[0];\n        }\n\n        return s.toArray(new String[0]);\n    }\n\n    /**\n     * Returns an array of all fields of {@link ProcessQueue#PROCESS_QUEUE}, but\n     * replaces the meta field with a version with all null values stripped out.\n     */\n    private static Field<?>[] processEntryFields() {\n        Field<?>[] fields = PROCESS_QUEUE.fields();\n\n        List<Field<?>> l = new ArrayList<>(fields.length);\n        for (Field<?> f : fields) {\n            if (f == PROCESS_QUEUE.IMPORTS) {\n                continue;\n            }\n\n            l.add(f);\n        }\n\n        return l.toArray(new Field[0]);\n    }\n\n    public static class ProjectIdAndInitiator {\n\n        private final UUID projectId;\n        private final UUID initiatorId;\n\n        public ProjectIdAndInitiator(UUID projectId, UUID initiatorId) {\n            this.projectId = projectId;\n            this.initiatorId = initiatorId;\n        }\n\n        public UUID getProjectId() {\n            return projectId;\n        }\n\n        public UUID getInitiatorId() {\n            return initiatorId;\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/ProcessQueueEntry.java",
    "content": "package com.walmartlabs.concord.server.process.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.runtime.v2.model.ExclusiveMode;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.util.Map;\nimport java.util.UUID;\n\n@Value.Immutable\npublic interface ProcessQueueEntry {\n\n    ProcessKey key();\n\n    @Nullable\n    UUID projectId();\n\n    @Nullable\n    UUID orgId();\n\n    @Nullable\n    UUID parentInstanceId();\n\n    @Nullable\n    UUID initiatorId();\n\n    @Nullable\n    ExclusiveMode exclusive();\n\n    @Nullable\n    UUID repoId();\n\n    @Nullable\n    String repoPath();\n\n    @Nullable\n    String repoUrl();\n\n    @Nullable\n    String commitId();\n\n    @Nullable\n    String commitBranch();\n\n    @Nullable\n    Imports imports();\n\n    @Nullable\n    Map<String, Object> requirements();\n\n    static ImmutableProcessQueueEntry.Builder builder() {\n        return ImmutableProcessQueueEntry.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/ProcessQueueGaugeModule.java",
    "content": "package com.walmartlabs.concord.server.process.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.CachedGauge;\nimport com.codahale.metrics.DerivativeGauge;\nimport com.codahale.metrics.Gauge;\nimport com.google.inject.AbstractModule;\nimport com.google.inject.Provider;\nimport com.google.inject.multibindings.Multibinder;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport com.walmartlabs.concord.server.sdk.metrics.GaugeProvider;\n\nimport java.util.Map;\nimport java.util.concurrent.TimeUnit;\n\npublic class ProcessQueueGaugeModule extends AbstractModule {\n\n    @Override\n    @SuppressWarnings(\"rawtypes\")\n    protected void configure() {\n        Provider<ProcessQueueDao> queueDaoProvider = getProvider(ProcessQueueDao.class);\n\n        // create the base gauge that caches all individual values\n        Gauge<Map<String, Integer>> base = new CachedGauge<Map<String, Integer>>(15, TimeUnit.SECONDS) {\n            @Override\n            protected Map<String, Integer> loadValue() {\n                return queueDaoProvider.get().getStatistics();\n            }\n        };\n\n        Multibinder<GaugeProvider> gauges = Multibinder.newSetBinder(binder(), GaugeProvider.class);\n        gauges.addBinding().toInstance(createBaseProvider(base));\n\n        for (ProcessStatus s : ProcessStatus.values()) {\n            gauges.addBinding().toInstance(create(base, s.toString()));\n        }\n        gauges.addBinding().toInstance(create(base, ProcessQueueDao.ENQUEUED_NOW_METRIC));\n    }\n\n    private static GaugeProvider<Map<String, Integer>> createBaseProvider(Gauge<Map<String, Integer>> base) {\n        return new GaugeProvider<>() {\n            @Override\n            public String name() {\n                return \"process-queue-statistics\";\n            }\n\n            @Override\n            public Gauge<Map<String, Integer>> gauge() {\n                return base;\n            }\n        };\n    }\n\n    private static GaugeProvider<Integer> create(Gauge<Map<String, Integer>> base, String status) {\n        return new GaugeProvider<>() {\n            @Override\n            public String name() {\n                return \"process-queue-\" + status.toLowerCase();\n            }\n\n            @Override\n            public Gauge<Integer> gauge() {\n                return new DerivativeGauge<>(base) {\n                    @Override\n                    protected Integer transform(Map<String, Integer> value) {\n                        return value.getOrDefault(status, 0);\n                    }\n                };\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/ProcessQueueManager.java",
    "content": "package com.walmartlabs.concord.server.process.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.runtime.v2.model.ExclusiveMode;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.EventType;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport com.walmartlabs.concord.server.RequestUtils;\nimport com.walmartlabs.concord.server.process.*;\nimport com.walmartlabs.concord.server.process.event.NewProcessEvent;\nimport com.walmartlabs.concord.server.process.event.ProcessEventManager;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKeyCache;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport org.jooq.DSLContext;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport java.time.Duration;\nimport java.time.OffsetDateTime;\nimport java.time.temporal.ChronoUnit;\nimport java.util.*;\n\npublic class ProcessQueueManager {\n\n    private static final Set<ProcessDataInclude> DEFAULT_INCLUDES = Collections.singleton(ProcessDataInclude.CHILDREN_IDS);\n    private static final Set<ProcessStatus> TO_ENQUEUED_STATUSES = Set.of(\n            ProcessStatus.PREPARING,\n            ProcessStatus.RESUMING,\n            ProcessStatus.SUSPENDED\n    );\n\n    private final ProcessQueueDao queueDao;\n    private final ProcessKeyCache keyCache;\n    private final ProcessEventManager eventManager;\n    private final ProcessLogManager processLogManager;\n    private final Set<ProcessStatusListener> statusListeners;\n\n    @Inject\n    public ProcessQueueManager(ProcessQueueDao queueDao,\n                               ProcessKeyCache keyCache,\n                               ProcessEventManager eventManager,\n                               ProcessLogManager processLogManager,\n                               Set<ProcessStatusListener> statusListeners) {\n\n        this.queueDao = queueDao;\n        this.eventManager = eventManager;\n        this.keyCache = keyCache;\n        this.processLogManager = processLogManager;\n        this.statusListeners = statusListeners;\n    }\n\n    /**\n     * Creates the initial queue record for the specified process payload.\n     */\n    public void insert(Payload payload, ProcessStatus status) {\n        ProcessKey processKey = payload.getProcessKey();\n        ProcessKind kind = payload.getHeader(Payload.PROCESS_KIND, ProcessKind.DEFAULT);\n        UUID projectId = payload.getHeader(Payload.PROJECT_ID);\n        UUID repoId = payload.getHeader(Payload.REPOSITORY_ID);\n        UUID parentInstanceId = payload.getHeader(Payload.PARENT_INSTANCE_ID);\n        UUID initiatorId = payload.getHeader(Payload.INITIATOR_ID);\n        Map<String, Object> cfg = getCfg(payload);\n        Map<String, Object> meta = getMeta(cfg);\n        TriggeredByEntry triggeredBy = payload.getHeader(Payload.TRIGGERED_BY);\n        String branchOrTag = MapUtils.getString(cfg, Constants.Request.REPO_BRANCH_OR_TAG);\n        String commitId = MapUtils.getString(cfg, Constants.Request.REPO_COMMIT_ID);\n\n        queueDao.tx(tx -> {\n            queueDao.insert(tx, processKey, status, kind, parentInstanceId, projectId, repoId, branchOrTag, commitId, initiatorId, meta, triggeredBy);\n            notifyStatusChange(tx, processKey, status);\n            processLogManager.createSystemSegment(tx, payload.getProcessKey());\n        });\n    }\n\n    /**\n     * Updates an existing record, moving the process into the ENQUEUED status.\n     */\n    public boolean enqueue(Payload payload) {\n        ProcessKey processKey = payload.getProcessKey();\n\n        ProcessStatus s = queueDao.getStatus(processKey);\n        if (s == null) {\n            throw new ProcessException(processKey, \"Process not found: \" + processKey);\n        }\n\n        if (s == ProcessStatus.CANCELLED) {\n            // the process was cancelled while going through EnqueueProcessPipeline\n            // (e.g. it was a process in an \"exclusive\" group)\n            return false;\n        }\n\n        if (!TO_ENQUEUED_STATUSES.contains(s)) {\n            // something's wrong (e.g. someone tried to change the process' status directly in the DB and was unlucky)\n            throw new ProcessException(processKey, \"Invalid process status: \" + s);\n        }\n\n        Set<String> tags = payload.getHeader(Payload.PROCESS_TAGS);\n        OffsetDateTime startAt = PayloadUtils.getStartAt(payload);\n        Map<String, Object> requirements = PayloadUtils.getRequirements(payload);\n        Long processTimeout = getProcessTimeout(payload);\n        Long suspendTimeout = getSuspendTimeout(payload);\n        Set<String> handlers = payload.getHeader(Payload.PROCESS_HANDLERS);\n        Map<String, Object> meta = getMeta(getCfg(payload));\n        Imports imports = payload.getHeader(Payload.IMPORTS);\n        ExclusiveMode exclusive = PayloadUtils.getExclusive(payload);\n        String runtime = payload.getHeader(Payload.RUNTIME);\n        List<String> dependencies = payload.getHeader(Payload.DEPENDENCIES);\n\n        return queueDao.txResult(tx -> {\n            boolean updated = queueDao.enqueue(tx, processKey, tags, startAt, requirements, processTimeout, handlers, meta, imports, exclusive, runtime, dependencies, suspendTimeout, TO_ENQUEUED_STATUSES);\n            if (updated) {\n                notifyStatusChange(tx, processKey, ProcessStatus.ENQUEUED);\n            }\n            return updated;\n        });\n    }\n\n    /**\n     * @see #updateStatus(DSLContext, ProcessKey, ProcessStatus)\n     */\n    public void updateStatus(ProcessKey processKey, ProcessStatus status) {\n        queueDao.tx(tx -> updateStatus(tx, processKey, status));\n    }\n\n    /**\n     * Updates the process' status.\n     */\n    public void updateStatus(DSLContext tx, ProcessKey processKey, ProcessStatus status) {\n        queueDao.updateStatus(tx, processKey, status);\n        notifyStatusChange(tx, processKey, status);\n    }\n\n    /**\n     * Updates the process' status but only if it's in the {@code expected} status.\n     *\n     * @return {@code true} if the process was updated\n     */\n    public boolean updateExpectedStatus(ProcessKey processKey, ProcessStatus expected, ProcessStatus status) {\n        return queueDao.txResult(tx -> updateExpectedStatus(tx, processKey, expected, status));\n    }\n\n    public boolean updateExpectedStatus(DSLContext tx, ProcessKey processKey, ProcessStatus expected, ProcessStatus status) {\n        boolean success = queueDao.updateStatus(tx, processKey, expected, status);\n        if (success) {\n            notifyStatusChange(tx, processKey, status);\n        }\n        return success;\n    }\n\n    /**\n     * Updates status of multiple processes but only if their current status is\n     * in the {@code expected} list of statuses.\n     *\n     * @return {@code true} if every processes was updated\n     */\n    public List<ProcessKey> updateExpectedStatus(DSLContext tx, List<ProcessKey> processKeys, List<ProcessStatus> expected, ProcessStatus status) {\n        if (processKeys.isEmpty()) {\n            return Collections.emptyList();\n        }\n\n        List<ProcessKey> updated = queueDao.updateStatus(processKeys, expected, status);\n        updated.forEach(processKey -> statusListeners.forEach(l -> l.onStatusChange(tx, processKey, status)));\n        return updated;\n    }\n\n    /**\n     * @see #updateAgentId(DSLContext, ProcessKey, String, ProcessStatus)\n     */\n    public void updateAgentId(ProcessKey processKey, String agentId, ProcessStatus status) {\n        queueDao.tx(tx -> updateAgentId(tx, processKey, agentId, status));\n    }\n\n    /**\n     * Updates the process' agent ID and status.\n     */\n    public void updateAgentId(DSLContext tx, ProcessKey processKey, @Nullable String agentId, ProcessStatus status) {\n        queueDao.updateAgentId(tx, processKey, agentId, status);\n        notifyStatusChange(tx, processKey, status);\n    }\n\n    public ProcessEntry get(ProcessKey processKey) {\n        return queueDao.get(processKey, DEFAULT_INCLUDES);\n    }\n\n    public ProcessEntry get(PartialProcessKey partialProcessKey) {\n        ProcessKey key = keyCache.get(partialProcessKey.getInstanceId());\n        if (key == null) {\n            return null;\n        }\n        return queueDao.get(key, DEFAULT_INCLUDES);\n    }\n\n    public UUID getProjectId(PartialProcessKey partialProcessKey) {\n        return queueDao.getProjectId(partialProcessKey.getInstanceId());\n    }\n\n    public ProcessInitiatorEntry getInitiator(PartialProcessKey partialProcessKey) {\n        ProcessKey key = keyCache.get(partialProcessKey.getInstanceId());\n        if (key == null) {\n            return null;\n        }\n\n        return queueDao.getInitiator(key);\n    }\n\n    public ProcessEntry get(PartialProcessKey partialProcessKey, Set<ProcessDataInclude> includes) {\n        ProcessKey key = keyCache.get(partialProcessKey.getInstanceId());\n        if (key == null) {\n            return null;\n        }\n        return queueDao.get(key, includes);\n    }\n\n    public void restore(ProcessKey processKey, UUID checkpointId, ProcessStatus currentStatus) {\n        queueDao.tx(tx -> {\n            updateStatus(tx, processKey, ProcessStatus.SUSPENDED);\n            Map<String, Object> eventData = new HashMap<>();\n            eventData.put(\"checkpointId\", checkpointId);\n            eventData.put(\"processStatus\", currentStatus);\n\n            eventManager.event(tx, NewProcessEvent.builder()\n                    .processKey(processKey)\n                    .eventType(EventType.CHECKPOINT_RESTORE.name())\n                    .data(eventData)\n                    .build());\n        });\n    }\n\n    private static Map<String, Object> getCfg(Payload payload) {\n        return payload.getHeader(Payload.CONFIGURATION, Collections.emptyMap());\n    }\n\n    private static Map<String, Object> getMeta(Map<String, Object> cfg) {\n        Map<String, Object> m = MapUtils.getMap(cfg, Constants.Request.META, Collections.emptyMap());\n        m = new HashMap<>(m);\n        m.put(Constants.Meta.SYSTEM_GROUP, Collections.singletonMap(Constants.Meta.REQUEST_ID, RequestUtils.getRequestId()));\n        m.put(Constants.Request.ENTRY_POINT_KEY, cfg.get(Constants.Request.ENTRY_POINT_KEY));\n        return m;\n    }\n\n    private static Long getProcessTimeout(Payload p) {\n        return getTimeout(p, Constants.Request.PROCESS_TIMEOUT);\n    }\n\n    private static Long getSuspendTimeout(Payload p) {\n        return getTimeout(p, Constants.Request.SUSPEND_TIMEOUT);\n    }\n\n    private static Long getTimeout(Payload p, String paramName) {\n        Map<String, Object> cfg = p.getHeader(Payload.CONFIGURATION);\n        if (cfg == null) {\n            return null;\n        }\n\n        Object processTimeout = cfg.get(paramName);\n        if (processTimeout == null) {\n            return null;\n        }\n\n        if (processTimeout instanceof String) {\n            Duration duration = Duration.parse((CharSequence) processTimeout);\n            return duration.get(ChronoUnit.SECONDS);\n        }\n\n        if (processTimeout instanceof Number) {\n            return ((Number) processTimeout).longValue();\n        }\n\n        throw new IllegalArgumentException(\"Invalid '\" + paramName + \"' value: expected an ISO-8601 value, got: \" + processTimeout);\n    }\n\n    private void notifyStatusChange(DSLContext tx, ProcessKey processKey, ProcessStatus status) {\n        statusListeners.forEach(l -> l.onStatusChange(tx, processKey, status));\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/ProcessQueueWatchdog.java",
    "content": "package com.walmartlabs.concord.server.process.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.db.PgUtils;\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.sdk.LogTags;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.Utils;\nimport com.walmartlabs.concord.server.agent.AgentManager;\nimport com.walmartlabs.concord.server.cfg.ProcessWatchdogConfiguration;\nimport com.walmartlabs.concord.server.jooq.tables.ProcessQueue;\nimport com.walmartlabs.concord.server.process.*;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport com.walmartlabs.concord.server.sdk.ScheduledTask;\nimport com.walmartlabs.concord.server.security.UserSecurityContext;\nimport com.walmartlabs.concord.server.user.UserDao;\nimport org.jooq.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.db.PgUtils.interval;\nimport static com.walmartlabs.concord.server.jooq.tables.ProcessQueue.PROCESS_QUEUE;\nimport static org.jooq.impl.DSL.*;\n\npublic class ProcessQueueWatchdog implements ScheduledTask {\n\n    private static final Logger log = LoggerFactory.getLogger(ProcessQueueWatchdog.class);\n\n    private static final PollEntry[] POLL_ENTRIES = {\n            new PollEntry(ProcessStatus.FAILED,\n                    Constants.Flows.ON_FAILURE_FLOW,\n                    ProcessKind.FAILURE_HANDLER, 3),\n\n            new PollEntry(ProcessStatus.CANCELLED,\n                    Constants.Flows.ON_CANCEL_FLOW,\n                    ProcessKind.CANCEL_HANDLER, 3),\n\n            new PollEntry(ProcessStatus.TIMED_OUT,\n                    Constants.Flows.ON_TIMEOUT_FLOW,\n                    ProcessKind.TIMEOUT_HANDLER, 3)\n    };\n\n    private static final ProcessKind[] HANDLED_PROCESS_KINDS = {\n            ProcessKind.DEFAULT\n    };\n\n    private static final ProcessKind[] SPECIAL_HANDLERS = {\n            ProcessKind.FAILURE_HANDLER,\n            ProcessKind.CANCEL_HANDLER,\n            ProcessKind.TIMEOUT_HANDLER\n    };\n\n    private static final ProcessStatus[] ACTIVE_PROCESS_STATUSES = {\n            ProcessStatus.NEW,\n            ProcessStatus.PREPARING,\n            ProcessStatus.ENQUEUED,\n            ProcessStatus.WAITING,\n            ProcessStatus.STARTING,\n            ProcessStatus.RUNNING,\n            ProcessStatus.SUSPENDED,\n            ProcessStatus.RESUMING\n    };\n\n    private static final ProcessStatus[] POTENTIAL_STALLED_STATUSES = {\n            ProcessStatus.RUNNING\n    };\n\n    private static final ProcessStatus[] FAILED_TO_START_STATUSES = {\n            ProcessStatus.PREPARING,\n            ProcessStatus.STARTING,\n            ProcessStatus.RESUMING\n    };\n\n    private final ProcessWatchdogConfiguration cfg;\n    private final ProcessQueueDao queueDao;\n    private final AgentManager agentManager;\n    private final ProcessLogManager logManager;\n    private final WatchdogDao watchdogDao;\n    private final UserDao userDao;\n    private final PayloadManager payloadManager;\n    private final ProcessManager processManager;\n    private final ProcessQueueManager queueManager;\n    private final UserSecurityContext userSecurityContext;\n\n    @Inject\n    public ProcessQueueWatchdog(ProcessWatchdogConfiguration cfg,\n                                ProcessQueueDao queueDao,\n                                AgentManager agentManager,\n                                ProcessLogManager logManager,\n                                WatchdogDao watchdogDao,\n                                UserDao userDao,\n                                PayloadManager payloadManager,\n                                ProcessManager processManager,\n                                ProcessQueueManager queueManager,\n                                UserSecurityContext userSecurityContext) {\n        this.cfg = cfg;\n\n        this.queueDao = queueDao;\n        this.agentManager = agentManager;\n        this.logManager = logManager;\n        this.watchdogDao = watchdogDao;\n        this.userDao = userDao;\n        this.payloadManager = payloadManager;\n        this.processManager = processManager;\n        this.queueManager = queueManager;\n        this.userSecurityContext = userSecurityContext;\n    }\n\n    @Override\n    public String getId() {\n        return \"process-queue-watchdog\";\n    }\n\n    @Override\n    public long getIntervalInSec() {\n        return cfg.getPeriod().getSeconds();\n    }\n\n    @Override\n    public void performTask() {\n        new ProcessHandlersWorker().run();\n        new ProcessStalledWorker().run();\n        new ProcessStartFailuresWorker().run();\n        new ProcessTimedOutWorker(ProcessStatus.RUNNING, () -> watchdogDao.pollExpired(1)).run();\n        new ProcessTimedOutWorker(ProcessStatus.SUSPENDED, () -> watchdogDao.pollSuspendExpired(1)).run();\n    }\n\n    private final class ProcessHandlersWorker implements Runnable {\n\n        @Override\n        public void run() {\n            Field<OffsetDateTime> maxAge = PgUtils.nowMinus(cfg.getMaxFailureHandlingAge());\n            boolean doWork = true;\n\n            for (PollEntry e : POLL_ENTRIES) {\n                List<ProcessEntry> parents = watchdogDao.poll(e, maxAge, 1);\n\n                for (ProcessEntry parent : parents) {\n                    doWork = process(e, parent);\n                    if (!doWork) {\n                        break;\n                    }\n                }\n\n                if (!doWork) {\n                    break;\n                }\n            }\n        }\n\n        /**\n         * @return {@code true} on success, {@code false} on error\n         */\n        private boolean process(PollEntry entry, ProcessEntry parent) {\n            Map<String, Object> req = new HashMap<>();\n            req.put(Constants.Request.ENTRY_POINT_KEY, entry.flow);\n            req.put(Constants.Request.TAGS_KEY, null); // clear tags\n\n            PartialProcessKey childKey = PartialProcessKey.create();\n            try {\n                String username = assertUserEnabled(parent.initiatorId);\n                Payload payload = payloadManager.createFork(childKey, parent.processKey, entry.handlerKind,\n                        parent.initiatorId, username, parent.projectId, req, null,\n                        null, parent.imports);\n\n                userSecurityContext.runAs(parent.initiatorId, () -> processManager.startFork(payload));\n\n                logManager.info(parent.processKey, \"{} started: {}\", toString(entry.handlerKind), LogTags.instanceId(payload.getProcessKey().getInstanceId()));\n\n                log.info(\"process -> created a new child process '{}' (parent '{}', entryPoint: '{}')\",\n                        childKey, parent.processKey, entry.flow);\n                return true;\n            } catch (Exception e) {\n                // remove the handler from the parent process to avoid infinite retries\n                queueDao.removeHandler(parent.processKey, entry.flow);\n                logManager.warn(parent.processKey, \"Error while starting {} handler: {}\", entry.flow, e.getMessage());\n                log.warn(\"Error while starting {}/{} handler: {}\", parent.processKey, entry.flow, e.getMessage());\n                return false;\n            }\n        }\n\n        /**\n         * @return username\n         */\n        private String assertUserEnabled(UUID initiatorId) {\n            var user = userDao.get(initiatorId);\n\n            if (user == null) {\n                throw new IllegalStateException(\"initiator not found\");\n            }\n\n            if (user.isDisabled()) {\n                throw new IllegalArgumentException(\"initiator is disabled\");\n            }\n\n            return user.getName();\n        }\n\n        private String toString(ProcessKind kind) {\n            switch (kind) {\n                case CANCEL_HANDLER:\n                    return \"Cancel handler\";\n                case FAILURE_HANDLER:\n                    return \"Failure handler\";\n                case TIMEOUT_HANDLER:\n                    return \"Timeout handler\";\n                default:\n                    throw new RuntimeException(\"Unknown process kind: \" + kind);\n            }\n        }\n    }\n\n    private final class ProcessStalledWorker implements Runnable {\n        @Override\n        public void run() {\n            watchdogDao.transaction(tx -> {\n                Field<OffsetDateTime> cutoff = PgUtils.nowMinus(cfg.getMaxStalledAge());\n\n                List<ProcessKey> pks = watchdogDao.pollStalled(tx, POTENTIAL_STALLED_STATUSES, cutoff, 1);\n                for (ProcessKey pk : pks) {\n                    queueManager.updateAgentId(tx, pk, null, ProcessStatus.FAILED);\n                    logManager.warn(pk, \"Process stalled, no heartbeat for more than '{}'\", cfg.getMaxStalledAge());\n                    log.info(\"processStalled -> marked as failed: {}\", pk);\n                }\n            });\n        }\n    }\n\n    private final class ProcessStartFailuresWorker implements Runnable {\n        @Override\n        public void run() {\n            watchdogDao.transaction(tx -> {\n                Field<OffsetDateTime> cutOff = PgUtils.nowMinus(cfg.getMaxStartFailureAge());\n\n                List<ProcessKey> pks = watchdogDao.pollStalled(tx, FAILED_TO_START_STATUSES, cutOff, 1);\n                for (ProcessKey pk : pks) {\n                    queueManager.updateAgentId(tx, pk, null, ProcessStatus.FAILED);\n                    logManager.warn(pk, \"Process failed to start for more than '{}'\", cfg.getMaxStartFailureAge());\n                    log.info(\"processStartFailures -> marked as failed: {}\", pk);\n                }\n            });\n        }\n    }\n\n    interface TimedOutProcessPoller {\n\n        List<TimedOutEntry> poll();\n    }\n\n    class ProcessTimedOutWorker implements Runnable {\n\n        private final ProcessStatus expectedProcessStatus;\n        private final TimedOutProcessPoller poller;\n\n        public ProcessTimedOutWorker(ProcessStatus expectedProcessStatus, TimedOutProcessPoller poller) {\n            this.expectedProcessStatus = expectedProcessStatus;\n            this.poller = poller;\n        }\n\n        @Override\n        public void run() {\n            List<TimedOutEntry> items = poller.poll();\n            for (TimedOutEntry i : items) {\n                boolean updated = queueManager.updateExpectedStatus(i.processKey, expectedProcessStatus, ProcessStatus.TIMED_OUT);\n                if (updated) {\n                    agentManager.killProcess(i.processKey, i.agentId);\n                    logManager.warn(i.processKey, \"Process timed out ({}s limit)\", i.timeout);\n                    log.info(\"processTimedOut -> marked as timed out: {}\", i.processKey);\n                }\n            }\n        }\n    }\n\n    private static final class PollEntry {\n\n        private final ProcessStatus status;\n        private final String flow;\n        private final ProcessKind handlerKind;\n        private final int maxTries;\n\n        private PollEntry(ProcessStatus status, String flow, ProcessKind handlerKind, int maxTries) {\n            this.status = status;\n            this.flow = flow;\n            this.handlerKind = handlerKind;\n            this.maxTries = maxTries;\n        }\n    }\n\n    private static final class WatchdogDao extends AbstractDao {\n\n        private final ConcordObjectMapper objectMapper;\n\n        @Inject\n        public WatchdogDao(@MainDB Configuration cfg, ConcordObjectMapper objectMapper) {\n            super(cfg);\n            this.objectMapper = objectMapper;\n        }\n\n        private void transaction(Tx t) {\n            tx(t);\n        }\n\n        public List<ProcessEntry> poll(PollEntry entry, Field<OffsetDateTime> maxAge, int maxEntries) {\n            ProcessQueue q = PROCESS_QUEUE.as(\"q\");\n\n            return txResult(tx -> tx.select(q.INSTANCE_ID, q.CREATED_AT, q.PROJECT_ID, q.INITIATOR_ID, q.IMPORTS)\n                    .from(q)\n                    .where(q.PROCESS_KIND.in(Utils.toString(HANDLED_PROCESS_KINDS))\n                            .and(q.CURRENT_STATUS.eq(entry.status.toString()))\n                            .and(q.CREATED_AT.greaterOrEqual(maxAge))\n                            .and(PgUtils.contains(q.HANDLERS, new String[]{entry.flow}))\n                            .and(noSuccessfulHandlers(q.INSTANCE_ID, entry.handlerKind))\n                            .and(count(tx, q.INSTANCE_ID, entry.handlerKind).lessThan(entry.maxTries))\n                            .and(noRunningHandlers(q.INSTANCE_ID)))\n                    .limit(maxEntries)\n                    .fetch(this::toEntry));\n        }\n\n        public List<ProcessKey> pollStalled(DSLContext tx, ProcessStatus[] statuses, Field<OffsetDateTime> cutOff, int maxEntries) {\n            ProcessQueue q = PROCESS_QUEUE.as(\"q\");\n            return tx.select(q.INSTANCE_ID, q.CREATED_AT)\n                    .from(q)\n                    .where(q.CURRENT_STATUS.in(Utils.toString(statuses))\n                            .and(q.LAST_UPDATED_AT.lessThan(cutOff)))\n                    .limit(maxEntries)\n                    .forUpdate()\n                    .skipLocked()\n                    .fetch(r -> new ProcessKey(r.value1(), r.value2()));\n        }\n\n        public List<TimedOutEntry> pollExpired(int maxEntries) {\n            ProcessQueue q = PROCESS_QUEUE.as(\"q\");\n\n            Field<?> maxAge = interval(\"1 second\").mul(q.TIMEOUT);\n\n            return txResult(tx -> tx.select(q.INSTANCE_ID, q.CREATED_AT, q.LAST_AGENT_ID, q.TIMEOUT)\n                    .from(q)\n                    .where(q.CURRENT_STATUS.eq(ProcessStatus.RUNNING.toString())\n                            .and(q.LAST_RUN_AT.plus(maxAge).lessOrEqual(currentOffsetDateTime())))\n                    .limit(maxEntries)\n                    .forUpdate()\n                    .skipLocked()\n                    .fetch(WatchdogDao::toExpiredEntry));\n        }\n\n        public List<TimedOutEntry> pollSuspendExpired(int maxEntries) {\n            ProcessQueue q = PROCESS_QUEUE.as(\"q\");\n\n            Field<?> maxAge = interval(\"1 second\").mul(q.SUSPEND_TIMEOUT);\n\n            return txResult(tx -> tx.select(q.INSTANCE_ID, q.CREATED_AT, q.LAST_AGENT_ID, q.SUSPEND_TIMEOUT)\n                    .from(q)\n                    .where(q.CURRENT_STATUS.eq(ProcessStatus.SUSPENDED.toString())\n                            .and(q.LAST_UPDATED_AT.plus(maxAge).lessOrEqual(currentOffsetDateTime())))\n                    .limit(maxEntries)\n                    .forUpdate()\n                    .skipLocked()\n                    .fetch(WatchdogDao::toExpiredEntry));\n        }\n\n        private Field<Number> count(DSLContext tx, Field<UUID> parentInstanceId, ProcessKind kind) {\n            return tx.selectCount()\n                    .from(PROCESS_QUEUE)\n                    .where(PROCESS_QUEUE.PARENT_INSTANCE_ID.eq(parentInstanceId)\n                            .and(PROCESS_QUEUE.PROCESS_KIND.eq(kind.toString())))\n                    .asField();\n        }\n\n        private Condition noSuccessfulHandlers(Field<UUID> parentInstanceId, ProcessKind kind) {\n            return notExists(selectOne().from(PROCESS_QUEUE)\n                    .where(PROCESS_QUEUE.PARENT_INSTANCE_ID.eq(parentInstanceId)\n                            .and(PROCESS_QUEUE.PROCESS_KIND.eq(kind.toString()))\n                            .and(PROCESS_QUEUE.CURRENT_STATUS.eq(ProcessStatus.FINISHED.toString()))));\n        }\n\n        private Condition noRunningHandlers(Field<UUID> parentInstanceId) {\n            return notExists(selectOne().from(PROCESS_QUEUE)\n                    .where(PROCESS_QUEUE.PARENT_INSTANCE_ID.eq(parentInstanceId)\n                            .and(PROCESS_QUEUE.CURRENT_STATUS.in(Utils.toString(ACTIVE_PROCESS_STATUSES)))\n                            .and(PROCESS_QUEUE.PROCESS_KIND.in(Utils.toString(SPECIAL_HANDLERS)))));\n        }\n\n        private ProcessEntry toEntry(Record5<UUID, OffsetDateTime, UUID, UUID, JSONB> r) {\n            ProcessKey processKey = new ProcessKey(r.get(PROCESS_QUEUE.INSTANCE_ID), r.get(PROCESS_QUEUE.CREATED_AT));\n            return new ProcessEntry(processKey,\n                    r.get(PROCESS_QUEUE.PROJECT_ID),\n                    r.get(PROCESS_QUEUE.INITIATOR_ID),\n                    objectMapper.fromJSONB(r.get(PROCESS_QUEUE.IMPORTS), Imports.class));\n        }\n\n        private static TimedOutEntry toExpiredEntry(Record4<UUID, OffsetDateTime, String, Long> r) {\n            ProcessKey processKey = new ProcessKey(r.value1(), r.value2());\n            return new TimedOutEntry(processKey, r.value3(), r.value4());\n        }\n    }\n\n    private static final class ProcessEntry {\n\n        private final ProcessKey processKey;\n        private final UUID projectId;\n        private final UUID initiatorId;\n        private final Imports imports;\n\n        private ProcessEntry(ProcessKey processKey, UUID projectId, UUID initiatorId, Imports imports) {\n            this.processKey = processKey;\n            this.projectId = projectId;\n            this.initiatorId = initiatorId;\n            this.imports = imports;\n        }\n    }\n\n    private static class TimedOutEntry {\n\n        private final ProcessKey processKey;\n        private final String agentId;\n        private final Long timeout;\n\n        private TimedOutEntry(ProcessKey processKey, String agentId, Long timeout) {\n            this.processKey = processKey;\n            this.agentId = agentId;\n            this.timeout = timeout;\n        }\n    }\n}"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/ProcessRequirementsEntry.java",
    "content": "package com.walmartlabs.concord.server.process.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.time.OffsetDateTime;\nimport java.util.Map;\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableProcessRequirementsEntry.class)\n@JsonDeserialize(as = ImmutableProcessRequirementsEntry.class)\npublic interface ProcessRequirementsEntry extends Serializable {\n\n    UUID instanceId();\n\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    OffsetDateTime createdAt();\n\n    @Nullable\n    Map<String, Object> requirements();\n\n    static ImmutableProcessRequirementsEntry.Builder builder() {\n        return ImmutableProcessRequirementsEntry.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/ProcessStatusListener.java",
    "content": "package com.walmartlabs.concord.server.process.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport org.jooq.DSLContext;\n\npublic interface ProcessStatusListener {\n\n    void onStatusChange(DSLContext tx, ProcessKey processKey, ProcessStatus status);\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/dispatcher/ConcurrentProcessFilter.java",
    "content": "package com.walmartlabs.concord.server.process.queue.dispatcher;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.collect.ImmutableSet;\nimport com.walmartlabs.concord.policyengine.CheckResult;\nimport com.walmartlabs.concord.policyengine.ConcurrentProcessRule;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.server.policy.PolicyManager;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueEntry;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueManager;\nimport com.walmartlabs.concord.server.process.waits.ProcessCompletionCondition;\nimport com.walmartlabs.concord.server.process.waits.ProcessWaitManager;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport org.jooq.DSLContext;\n\nimport javax.inject.Inject;\nimport java.util.*;\n\n/**\n * Handles \"max concurrent processes\" policy.\n * The process won't be scheduled for execution until the number of currently running\n * processes in the same project exceeds the configured value.\n */\npublic class ConcurrentProcessFilter extends WaitProcessFinishFilter {\n\n    private static final Set<ProcessStatus> FINAL_STATUSES = ImmutableSet.of(\n            ProcessStatus.SUSPENDED,\n            ProcessStatus.FINISHED,\n            ProcessStatus.FAILED,\n            ProcessStatus.CANCELLED,\n            ProcessStatus.TIMED_OUT);\n\n    private final ConcurrentProcessFilterDao dao;\n    private final PolicyManager policyManager;\n\n    @Inject\n    public ConcurrentProcessFilter(ProcessWaitManager processWaitManager, PolicyManager policyManager, ProcessQueueManager processQueueManager, ConcurrentProcessFilterDao dao) {\n        super(processWaitManager, processQueueManager);\n        this.policyManager = policyManager;\n        this.dao = dao;\n    }\n\n    @Override\n    public void cleanup() {\n        dao.cleanup();\n    }\n\n    @Override\n    protected List<UUID> findProcess(DSLContext tx, ProcessQueueEntry item, List<ProcessQueueEntry> startingProcesses) {\n        PolicyEngine pe = getPolicyEngine(item.orgId(), item.projectId(), item.initiatorId());\n        if (pe == null) {\n            return Collections.emptyList();\n        }\n\n        CheckResult<ConcurrentProcessRule, List<UUID>> result = pe.getConcurrentProcessPolicy().check(\n                () -> processesPerOrg(tx, item.orgId(), startingProcesses),\n                () -> processesPerProject(tx, item.projectId(), startingProcesses));\n\n        if (result.getDeny().isEmpty()) {\n            return Collections.emptyList();\n        }\n\n        return result.getDeny().get(0).getEntity();\n    }\n\n    @Override\n    protected String getReason() {\n        return \"max concurrent process limit exceeded\";\n    }\n\n    @Override\n    protected Set<ProcessStatus> getFinalStatuses() {\n        return FINAL_STATUSES;\n    }\n\n    @Override\n    protected ProcessCompletionCondition.CompleteCondition getCompleteCondition() {\n        return ProcessCompletionCondition.CompleteCondition.ONE_OF;\n    }\n\n    private PolicyEngine getPolicyEngine(UUID orgId, UUID prjId, UUID userId) {\n        if (prjId == null) {\n            return null;\n        }\n\n        return policyManager.get(orgId, prjId, userId);\n    }\n\n    private List<UUID> processesPerOrg(DSLContext tx, UUID orgId, List<ProcessQueueEntry> startingProcesses) {\n        if (orgId == null) {\n            return Collections.emptyList();\n        }\n\n        List<UUID> result = new ArrayList<>(dao.processesPerOrg(tx, orgId));\n        for (ProcessQueueEntry p : startingProcesses) {\n            if (orgId.equals(p.orgId())) {\n                result.add(p.key().getInstanceId());\n            }\n        }\n        return result;\n    }\n\n    private List<UUID> processesPerProject(DSLContext tx, UUID projectId, List<ProcessQueueEntry> startingProcesses) {\n        if (projectId == null) {\n            return Collections.emptyList();\n        }\n\n        List<UUID> result = new ArrayList<>(dao.processesPerProject(tx, projectId));\n        for (ProcessQueueEntry p : startingProcesses) {\n            if (projectId.equals(p.projectId())) {\n                result.add(p.key().getInstanceId());\n            }\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/dispatcher/ConcurrentProcessFilterDao.java",
    "content": "package com.walmartlabs.concord.server.process.queue.dispatcher;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.jooq.tables.ProcessQueue;\nimport com.walmartlabs.concord.server.jooq.tables.Projects;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport org.jooq.DSLContext;\nimport org.jooq.Record1;\n\nimport java.util.*;\n\npublic class ConcurrentProcessFilterDao {\n\n    private static final List<ProcessStatus> RUNNING_PROCESS_STATUSES = Arrays.asList(\n            ProcessStatus.STARTING,\n            ProcessStatus.RUNNING,\n            ProcessStatus.RESUMING);\n\n    private final Map<UUID, List<UUID>> perOrg = new HashMap<>();\n    private final Map<UUID, List<UUID>> perProject = new HashMap<>();\n\n    public List<UUID> processesPerOrg(DSLContext tx, UUID orgId) {\n        return perOrg.computeIfAbsent(orgId, id -> computeProcessesPerOrg(tx, id));\n    }\n\n    public List<UUID> processesPerProject(DSLContext tx, UUID projectId) {\n        return perProject.computeIfAbsent(projectId, id -> computeProcessesPerProject(tx, id));\n    }\n\n    public void cleanup() {\n        perOrg.clear();\n        perProject.clear();\n    }\n\n    private List<UUID> computeProcessesPerOrg(DSLContext tx, UUID orgId) {\n        ProcessQueue q = ProcessQueue.PROCESS_QUEUE.as(\"q\");\n        Projects p = Projects.PROJECTS.as(\"p\");\n        return tx.select(q.INSTANCE_ID)\n                .from(q)\n                .innerJoin(p).on(q.PROJECT_ID.eq(p.PROJECT_ID))\n                .where(p.ORG_ID.eq(orgId)\n                        .and(q.CURRENT_STATUS.in(RUNNING_PROCESS_STATUSES)))\n                .fetch(Record1::value1);\n    }\n\n    private static List<UUID> computeProcessesPerProject(DSLContext tx, UUID projectId) {\n        ProcessQueue q = ProcessQueue.PROCESS_QUEUE.as(\"q\");\n        return tx.select(q.INSTANCE_ID)\n                .from(q)\n                .where(q.PROJECT_ID.eq(projectId)\n                        .and(q.CURRENT_STATUS.in(RUNNING_PROCESS_STATUSES)))\n                .fetch(Record1::value1);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/dispatcher/Dispatcher.java",
    "content": "package com.walmartlabs.concord.server.process.queue.dispatcher;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.Histogram;\nimport com.codahale.metrics.MetricRegistry;\nimport com.codahale.metrics.Timer;\nimport com.walmartlabs.concord.common.Matcher;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.runtime.v2.model.ExclusiveMode;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.Locks;\nimport com.walmartlabs.concord.server.PeriodicTask;\nimport com.walmartlabs.concord.server.cfg.ProcessQueueConfiguration;\nimport com.walmartlabs.concord.server.jooq.tables.ProcessQueue;\nimport com.walmartlabs.concord.server.process.ImportsNormalizerFactory;\nimport com.walmartlabs.concord.server.process.SessionTokenCreator;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueEntry;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueManager;\nimport com.walmartlabs.concord.server.queueclient.message.MessageType;\nimport com.walmartlabs.concord.server.queueclient.message.ProcessRequest;\nimport com.walmartlabs.concord.server.queueclient.message.ProcessResponse;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.message.MessageChannel;\nimport com.walmartlabs.concord.server.message.MessageChannelManager;\nimport com.walmartlabs.concord.server.agent.websocket.WebSocketChannel;\nimport org.jooq.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\nimport java.util.*;\nimport java.util.concurrent.TimeUnit;\nimport java.util.regex.PatternSyntaxException;\n\nimport static com.walmartlabs.concord.server.jooq.tables.Organizations.ORGANIZATIONS;\nimport static com.walmartlabs.concord.server.jooq.tables.ProcessQueue.PROCESS_QUEUE;\nimport static com.walmartlabs.concord.server.jooq.tables.Projects.PROJECTS;\nimport static com.walmartlabs.concord.server.jooq.tables.Repositories.REPOSITORIES;\nimport static com.walmartlabs.concord.server.jooq.tables.Secrets.SECRETS;\nimport static com.walmartlabs.concord.server.metrics.MetricUtils.withTimer;\nimport static org.jooq.impl.DSL.*;\n\n/**\n * Dispatches processes to agents.\n */\npublic class Dispatcher extends PeriodicTask {\n\n    private static final Logger log = LoggerFactory.getLogger(Dispatcher.class);\n\n    private static final long ERROR_DELAY = TimeUnit.SECONDS.toMillis(30);\n    private static final long LOCK_KEY = 1552468327245L;\n\n    private final Locks locks;\n    private final DispatcherDao dao;\n    private final MessageChannelManager channelManager;\n    private final ProcessLogManager logManager;\n    private final ProcessQueueManager queueManager;\n    private final Set<Filter> filters;\n    private final ImportsNormalizerFactory importsNormalizerFactory;\n\n    private final int batchSize;\n\n    private final Histogram dispatchedCountHistogram;\n    private final Timer responseTimer;\n\n    private final SessionTokenCreator sessionTokenCreator;\n    private final RequirementsMatcherErrorHandler requirementsMatcherErrorHandler;\n\n    @Inject\n    public Dispatcher(Locks locks,\n                      DispatcherDao dao,\n                      MessageChannelManager channelManager,\n                      ProcessLogManager logManager,\n                      ProcessQueueManager queueManager,\n                      Set<Filter> filters,\n                      ImportsNormalizerFactory importsNormalizerFactory,\n                      ProcessQueueConfiguration cfg,\n                      MetricRegistry metricRegistry,\n                      SessionTokenCreator sessionTokenCreator) {\n\n        super(cfg.getDispatcherPollDelay().toMillis(), ERROR_DELAY);\n\n        this.locks = locks;\n        this.dao = dao;\n        this.channelManager = channelManager;\n        this.logManager = logManager;\n        this.queueManager = queueManager;\n        this.filters = filters;\n        this.importsNormalizerFactory = importsNormalizerFactory;\n        this.requirementsMatcherErrorHandler = new DefaultRequirementsMatcherErrorHandler(queueManager, logManager);\n\n        this.batchSize = cfg.getDispatcherBatchSize();\n        this.sessionTokenCreator = sessionTokenCreator;\n\n        this.dispatchedCountHistogram = metricRegistry.histogram(\"process-queue-dispatcher-dispatched-count\");\n        this.responseTimer = metricRegistry.timer(\"process-queue-dispatcher-response-timer\");\n    }\n\n    @Override\n    @WithTimer\n    protected boolean performTask() {\n        // TODO the WebSocketChannelManager business can be replaced with an async jax-rs endpoint and an \"inbox\" queue\n\n        // grab the requests w/o responses\n        Map<MessageChannel, ProcessRequest> requests = this.channelManager.getRequests(MessageType.PROCESS_REQUEST);\n        if (requests.isEmpty()) {\n            return false;\n        }\n\n        List<Request> l = requests.entrySet().stream()\n                .map(e -> new Request(e.getKey(), e.getValue()))\n                .toList();\n\n        // prepare all responses in a single transaction\n        // take a global lock to avoid races\n        List<Match> matches = dao.txResult(tx -> {\n            locks.lock(tx, LOCK_KEY);\n            try {\n                return match(tx, l);\n            } finally {\n                filters.forEach(Filter::cleanup);\n            }\n        });\n\n        dispatchedCountHistogram.update(matches.size());\n\n        // no matches, retry after a delay\n        if (matches.isEmpty()) {\n            return false;\n        }\n\n        // send all responses in parallel\n        withTimer(responseTimer, () -> matches.stream()\n                .parallel()\n                .forEach(this::sendResponse));\n\n        return true;\n    }\n\n    private List<Match> match(DSLContext tx, List<Request> requests) {\n        // we need it modifiable\n        List<Request> inbox = new ArrayList<>(requests);\n\n        int offset = 0;\n        List<Match> matches = new ArrayList<>();\n        while (true) {\n            // fetch the next few ENQUEUED processes from the DB\n            List<ProcessQueueEntry> candidates = dao.next(tx, offset, batchSize);\n            if (candidates.isEmpty()) {\n                break;\n            }\n\n            // filter out the candidates that shouldn't be dispatched at the moment (e.g. due to concurrency limits)\n            for (ProcessQueueEntry e : candidates) {\n                // find request/agent who can handle process\n                Request req = findRequest(e, inbox, tx, requirementsMatcherErrorHandler);\n\n                if (req == null) {\n                    continue;\n                }\n\n                // \"startingProcesses\" are the currently collected \"matches\"\n                // we keep them in a separate collection to simplify the filtering\n                List<ProcessQueueEntry> startingProcesses = matches.stream()\n                        .map(Match::response)\n                        .toList();\n\n                if (pass(tx, e, startingProcesses)) {\n                    matches.add(new Match(req, e));\n                    inbox.remove(req);\n\n                    if (inbox.isEmpty()) {\n                        break;\n                    }\n                }\n            }\n\n            if (inbox.isEmpty()) {\n                break;\n            }\n\n            offset += batchSize;\n        }\n\n        for (Match m : matches) {\n            ProcessQueueEntry candidate = m.response;\n\n            // mark the process as STARTING\n            String agentId = m.request.channel.getAgentId();\n            queueManager.updateAgentId(tx, candidate.key(), agentId, ProcessStatus.STARTING);\n        }\n\n        return matches;\n    }\n\n    static Request findRequest(ProcessQueueEntry candidate,\n                               List<Request> requests,\n                               DSLContext tx,\n                               RequirementsMatcherErrorHandler errHandler) {\n\n        return requests.stream()\n                .filter(req -> isRequestMatch(candidate, req, tx, errHandler))\n                .findFirst()\n                .orElse(null);\n    }\n\n    private static boolean isRequestMatch(ProcessQueueEntry candidate,\n                                          Request req,\n                                          DSLContext tx,\n                                          RequirementsMatcherErrorHandler errHandler) {\n\n        Map<String, Object> capabilities = req.request.getCapabilities();\n        Map<?, ?> requirements = getAgentRequirements(candidate);\n\n        try {\n            return requirements.isEmpty() || Matcher.matches(capabilities, requirements);\n        } catch (PatternSyntaxException pse) {\n            log.error(\"Invalid regex in requested agent capabilities for instanceId: {}\", candidate.key().getInstanceId());\n            errHandler.handleError(tx, candidate, pse);\n        } catch (Exception e) {\n            String errMsg = String.format(\"Error matching process requirements for instanceId: %s\", candidate.key().getInstanceId());\n            log.error(errMsg, e);\n            errHandler.handleError(tx, candidate, e);\n        }\n\n        return false;\n    }\n\n    private static Map<?, ?> getAgentRequirements(ProcessQueueEntry entry) {\n        Map<String, Object> requirements = entry.requirements();\n        if (requirements == null) {\n            return Collections.emptyMap();\n        }\n\n        Object agent = requirements.get(\"agent\");\n        if (agent instanceof Map<?, ?> agentMap) {\n            return agentMap;\n        }\n\n        return Collections.emptyMap();\n    }\n\n    private boolean pass(DSLContext tx, ProcessQueueEntry e, List<ProcessQueueEntry> startingProcesses) {\n        for (Filter f : filters) {\n            if (!f.apply(tx, e, startingProcesses)) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    private void sendResponse(Match match) {\n        long correlationId = match.request.request.getCorrelationId();\n        ProcessQueueEntry item = match.response;\n\n        try {\n            SecretReference secret = null;\n            if (item.repoId() != null) {\n                secret = dao.getSecretReference(item.repoId());\n            }\n\n            // backward compatibility with old process queue entries that are not normalized\n            Imports imports = importsNormalizerFactory.forProject(item.projectId())\n                    .normalize(item.imports());\n\n            ProcessResponse resp = new ProcessResponse(correlationId,\n                    sessionTokenCreator.create(item.key()),\n                    item.key().getInstanceId(),\n                    item.key().getCreatedAt(),\n                    secret != null ? secret.orgName : null,\n                    item.repoUrl(),\n                    item.repoPath(),\n                    item.commitId(),\n                    item.commitBranch(),\n                    secret != null ? secret.secretName : null,\n                    imports,\n                    item.requirements());\n\n            MessageChannel channel = match.request.channel;\n            if (!channelManager.sendMessage(channel.getChannelId(), resp)) {\n                log.warn(\"sendResponse ['{}'] -> failed\", correlationId);\n            }\n\n            // TODO a way to avoid instanceof here\n            String userAgent = channel instanceof WebSocketChannel ? ((WebSocketChannel) channel).getUserAgent() : null;\n            if (userAgent != null) {\n                logManager.info(item.key(), \"Acquired by: \" + userAgent);\n            }\n        } catch (Exception e) {\n            log.error(\"sendResponse ['{}'] -> failed (instanceId: {})\", correlationId, item.key().getInstanceId());\n        }\n    }\n\n    @SuppressWarnings(\"resource\")\n    public static class DispatcherDao extends AbstractDao {\n\n        private final ConcordObjectMapper objectMapper;\n        private final Histogram offsetHistogram;\n\n        @Inject\n        public DispatcherDao(@MainDB Configuration cfg,\n                             ConcordObjectMapper objectMapper,\n                             MetricRegistry metricRegistry) {\n\n            super(cfg);\n            this.objectMapper = objectMapper;\n            this.offsetHistogram = metricRegistry.histogram(\"process-queue-dispatcher-offset\");\n        }\n\n        @Override\n        protected <T> T txResult(TxResult<T> t) {\n            return super.txResult(t);\n        }\n\n        @WithTimer\n        public List<ProcessQueueEntry> next(DSLContext tx, int offset, int limit) {\n            offsetHistogram.update(offset);\n\n            ProcessQueue q = PROCESS_QUEUE.as(\"q\");\n\n            Field<UUID> orgIdField = select(PROJECTS.ORG_ID).from(PROJECTS).where(PROJECTS.PROJECT_ID.eq(q.PROJECT_ID)).asField();\n\n            SelectJoinStep<Record14<UUID, OffsetDateTime, UUID, UUID, UUID, UUID, String, String, String, UUID, JSONB, JSONB, JSONB, String>> s =\n                    tx.select(\n                                    q.INSTANCE_ID,\n                                    q.CREATED_AT,\n                                    q.PROJECT_ID,\n                                    orgIdField,\n                                    q.INITIATOR_ID,\n                                    q.PARENT_INSTANCE_ID,\n                                    q.REPO_PATH,\n                                    q.REPO_URL,\n                                    q.COMMIT_ID,\n                                    q.REPO_ID,\n                                    q.IMPORTS,\n                                    q.REQUIREMENTS,\n                                    q.EXCLUSIVE,\n                                    q.COMMIT_BRANCH)\n                            .from(q);\n\n            s.where(q.CURRENT_STATUS.eq(ProcessStatus.ENQUEUED.toString())\n                    .and(or(q.START_AT.isNull(),\n                            q.START_AT.le(currentOffsetDateTime()))));\n\n            return s.orderBy(q.LAST_UPDATED_AT)\n                    .offset(offset)\n                    .limit(limit)\n                    .forUpdate()\n                    .of(q)\n                    .skipLocked()\n                    .fetch(r -> ProcessQueueEntry.builder()\n                            .key(new ProcessKey(r.value1(), r.value2()))\n                            .projectId(r.value3())\n                            .orgId(r.value4())\n                            .initiatorId(r.value5())\n                            .parentInstanceId(r.value6())\n                            .repoPath(r.value7())\n                            .repoUrl(r.value8())\n                            .commitId(r.value9())\n                            .commitBranch(r.value14())\n                            .repoId(r.value10())\n                            .imports(objectMapper.fromJSONB(r.value11(), Imports.class))\n                            .requirements(objectMapper.fromJSONB(r.value12()))\n                            .exclusive(objectMapper.fromJSONB(r.value13(), ExclusiveMode.class))\n                            .build());\n        }\n\n        public SecretReference getSecretReference(UUID repoId) {\n            return dsl().select(ORGANIZATIONS.ORG_NAME, SECRETS.SECRET_NAME)\n                    .from(REPOSITORIES)\n                    .leftOuterJoin(SECRETS).on(REPOSITORIES.SECRET_ID.eq(SECRETS.SECRET_ID))\n                    .leftOuterJoin(ORGANIZATIONS).on(SECRETS.ORG_ID.eq(ORGANIZATIONS.ORG_ID))\n                    .where(REPOSITORIES.REPO_ID.eq(repoId))\n                    .fetchOne(r -> new SecretReference(r.value1(), r.value2()));\n        }\n    }\n\n    record Request(MessageChannel channel, ProcessRequest request) {\n    }\n\n    private record Match(Request request, ProcessQueueEntry response) {\n\n    }\n\n    private record SecretReference(String orgName, String secretName) {\n\n    }\n\n    /**\n     * Handles errors encountered while matching a process queue request with an\n     * agent. Typically, this is due to regular expression issues\n     * (e.g. non-compilable pattern), but may be something unexpected.\n     */\n    interface RequirementsMatcherErrorHandler {\n        void handleError(DSLContext tx, ProcessQueueEntry queueEntry, Exception e);\n    }\n\n    static class DefaultRequirementsMatcherErrorHandler implements RequirementsMatcherErrorHandler {\n\n        private final ProcessQueueManager queueManager;\n        private final ProcessLogManager logManager;\n\n        public DefaultRequirementsMatcherErrorHandler(ProcessQueueManager queueManager, ProcessLogManager logManager) {\n            this.queueManager = queueManager;\n            this.logManager = logManager;\n        }\n\n        @Override\n        public void handleError(DSLContext tx, ProcessQueueEntry queueEntry, Exception e) {\n            if (e instanceof PatternSyntaxException) {\n                logManager.error(queueEntry.key(), \"Invalid regex in requested agent capabilities.\");\n            } else {\n                logManager.error(queueEntry.key(), \"Error matching process request requirements.\");\n            }\n\n            queueManager.updateStatus(tx, queueEntry.key(), ProcessStatus.FAILED);\n        }\n    }\n\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/dispatcher/ExclusiveProcessFilter.java",
    "content": "package com.walmartlabs.concord.server.process.queue.dispatcher;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.v2.model.ExclusiveMode;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueEntry;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueManager;\nimport com.walmartlabs.concord.server.process.waits.ProcessWaitManager;\nimport org.jooq.DSLContext;\n\nimport javax.inject.Inject;\nimport java.util.*;\n\n/**\n * Handles \"exclusive\" processes.\n * Exclusive processes can't be executed when there is another process\n * running in the same project and group.\n */\npublic class ExclusiveProcessFilter extends WaitProcessFinishFilter {\n\n    private final ExclusiveProcessFilterDao dao;\n\n    @Inject\n    public ExclusiveProcessFilter(ProcessWaitManager processWaitManager, ProcessQueueManager processQueueManager, ExclusiveProcessFilterDao dao) {\n        super(processWaitManager, processQueueManager);\n        this.dao = dao;\n    }\n\n    @Override\n    public void cleanup() {\n        dao.cleanup();\n    }\n\n    @Override\n    protected List<UUID> findProcess(DSLContext tx, ProcessQueueEntry item, List<ProcessQueueEntry> startingProcesses) {\n        UUID projectId = item.projectId();\n        ExclusiveMode exclusive = item.exclusive();\n\n        if (projectId == null || exclusive == null) {\n            return Collections.emptyList();\n        }\n\n        boolean isWaitMode = exclusive.mode() == ExclusiveMode.Mode.wait;\n        if (!isWaitMode) {\n            return Collections.emptyList();\n        }\n\n        List<UUID> result = new ArrayList<>(dao.findProcess(tx, item, exclusive.group()));\n        for (ProcessQueueEntry p : startingProcesses) {\n            if (projectId.equals(p.projectId()) && groupEquals(exclusive, p.exclusive())) {\n                result.add(p.key().getInstanceId());\n            }\n        }\n        return result;\n    }\n\n    private static boolean groupEquals(ExclusiveMode a, ExclusiveMode b) {\n        if (a == null || b == null) {\n            return false;\n        }\n\n        return Objects.equals(a.group(), b.group());\n    }\n\n    @Override\n    protected String getReason() {\n        return \"exclusive process\";\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/dispatcher/ExclusiveProcessFilterDao.java",
    "content": "package com.walmartlabs.concord.server.process.queue.dispatcher;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.jooq.tables.ProcessQueue;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueEntry;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport org.immutables.value.Value;\nimport org.jooq.DSLContext;\nimport org.jooq.Record1;\nimport org.jooq.SelectConditionStep;\nimport org.jooq.SelectJoinStep;\n\nimport javax.annotation.Nullable;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.db.PgUtils.jsonbText;\nimport static com.walmartlabs.concord.server.jooq.tables.ProcessQueue.PROCESS_QUEUE;\nimport static org.jooq.impl.DSL.*;\n\npublic class ExclusiveProcessFilterDao {\n\n    private static final List<ProcessStatus> RUNNING_PROCESS_STATUSES = Arrays.asList(\n            ProcessStatus.STARTING,\n            ProcessStatus.SUSPENDED,\n            ProcessStatus.RUNNING,\n            ProcessStatus.RESUMING);\n\n    private final Map<CacheKey, List<UUID>> cache = new HashMap<>();\n\n    public void cleanup() {\n        cache.clear();\n    }\n\n    public List<UUID> findProcess(DSLContext tx, ProcessQueueEntry item, String group) {\n        return cache.computeIfAbsent(CacheKey.of(group, item.projectId(), item.parentInstanceId()),\n                key -> findProcess(tx, key.group(), key.projectId(), key.parentInstanceId()));\n    }\n\n    private List<UUID> findProcess(DSLContext tx, String group, UUID projectId, UUID parentInstanceId) {\n        ProcessQueue q = ProcessQueue.PROCESS_QUEUE.as(\"q\");\n        SelectConditionStep<Record1<UUID>> s = tx.select(q.INSTANCE_ID)\n                .from(q)\n                .where(q.PROJECT_ID.eq(projectId)\n                        .and(q.CURRENT_STATUS.in(RUNNING_PROCESS_STATUSES)\n                                .and(jsonbText(q.EXCLUSIVE, \"group\").eq(group))));\n\n        // parent's\n        if (parentInstanceId != null) {\n            SelectJoinStep<Record1<UUID>> parents = tx.withRecursive(\"parents\").as(\n                    select(PROCESS_QUEUE.INSTANCE_ID, PROCESS_QUEUE.PARENT_INSTANCE_ID).from(PROCESS_QUEUE)\n                            .where(PROCESS_QUEUE.INSTANCE_ID.eq(parentInstanceId))\n                            .unionAll(\n                                    select(PROCESS_QUEUE.INSTANCE_ID, PROCESS_QUEUE.PARENT_INSTANCE_ID)\n                                            .from(PROCESS_QUEUE)\n                                            .join(name(\"parents\"))\n                                            .on(PROCESS_QUEUE.INSTANCE_ID.eq(\n                                                    field(name(\"parents\", \"PARENT_INSTANCE_ID\"), UUID.class)))))\n                    .select(field(\"parents.INSTANCE_ID\", UUID.class))\n                    .from(name(\"parents\"));\n\n            s.and(q.INSTANCE_ID.notIn(parents));\n        }\n\n        return s.fetch(Record1::value1);\n    }\n\n    @Value.Immutable\n    interface CacheKey {\n\n        String group();\n\n        UUID projectId();\n\n        @Nullable\n        UUID parentInstanceId();\n\n        static CacheKey of(String group, UUID projectId, UUID parentInstanceId) {\n            return ImmutableCacheKey.builder()\n                    .group(group)\n                    .projectId(projectId)\n                    .parentInstanceId(parentInstanceId)\n                    .build();\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/dispatcher/Filter.java",
    "content": "package com.walmartlabs.concord.server.process.queue.dispatcher;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueEntry;\nimport org.jooq.DSLContext;\n\nimport java.util.List;\n\npublic interface Filter {\n\n    boolean apply(DSLContext tx, ProcessQueueEntry e, List<ProcessQueueEntry> startingProcesses);\n\n    void cleanup();\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/queue/dispatcher/WaitProcessFinishFilter.java",
    "content": "package com.walmartlabs.concord.server.process.queue.dispatcher;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.collect.ImmutableSet;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueEntry;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueManager;\nimport com.walmartlabs.concord.server.process.waits.ProcessCompletionCondition;\nimport com.walmartlabs.concord.server.process.waits.ProcessWaitManager;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport org.jooq.DSLContext;\n\nimport java.util.List;\nimport java.util.Set;\nimport java.util.UUID;\n\n/**\n * Base class for all filters that are implementing process wait conditions.\n */\npublic abstract class WaitProcessFinishFilter implements Filter {\n\n    private static final Set<ProcessStatus> FINAL_STATUSES = ImmutableSet.of(\n            ProcessStatus.FINISHED,\n            ProcessStatus.FAILED,\n            ProcessStatus.CANCELLED,\n            ProcessStatus.TIMED_OUT);\n\n    private final ProcessWaitManager processWaitManager;\n    private final ProcessQueueManager processQueueManager;\n\n    protected WaitProcessFinishFilter(ProcessWaitManager processWaitManager,\n                                      ProcessQueueManager processQueueManager) {\n\n        this.processWaitManager = processWaitManager;\n        this.processQueueManager = processQueueManager;\n    }\n\n    @Override\n    public boolean apply(DSLContext tx, ProcessQueueEntry e, List<ProcessQueueEntry> startingProcesses) {\n        List<UUID> processes = findProcess(tx, e, startingProcesses);\n        if (processes.isEmpty()) {\n            return true;\n        }\n\n        processWaitManager.addWait(tx, e.key(), ProcessCompletionCondition.builder()\n                .processes(processes)\n                .reason(getReason())\n                .finalStatuses(getFinalStatuses())\n                .completeCondition(getCompleteCondition())\n                .exclusive(true)\n                .build());\n\n        processQueueManager.updateStatus(tx, e.key(), ProcessStatus.WAITING);\n\n        return false;\n    }\n\n    protected abstract List<UUID> findProcess(DSLContext tx, ProcessQueueEntry item, List<ProcessQueueEntry> startingProcesses);\n\n    protected Set<ProcessStatus> getFinalStatuses() {\n        return FINAL_STATUSES;\n    }\n\n    protected ProcessCompletionCondition.CompleteCondition getCompleteCondition() {\n        return ProcessCompletionCondition.CompleteCondition.ALL;\n    }\n\n    /**\n     * @return the \"reason\" string, explaining why this particular wait condition was added.\n     */\n    protected abstract String getReason();\n\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/state/ProcessCheckpointDao.java",
    "content": "package com.walmartlabs.concord.server.process.state;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.process.ImmutableProcessCheckpointEntry;\nimport com.walmartlabs.concord.server.process.ProcessEntry.ProcessCheckpointEntry;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport org.jooq.Configuration;\nimport org.jooq.Record;\n\nimport javax.inject.Inject;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\nimport java.sql.Timestamp;\nimport java.util.Date;\nimport java.util.List;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.tables.ProcessCheckpoints.PROCESS_CHECKPOINTS;\n\npublic class ProcessCheckpointDao extends AbstractDao {\n\n    @Inject\n    public ProcessCheckpointDao(@MainDB Configuration cfg) {\n        super(cfg);\n    }\n\n    public List<ProcessCheckpointEntry> list(ProcessKey processKey) {\n        return txResult(tx -> tx.select()\n                .from(PROCESS_CHECKPOINTS)\n                .where(PROCESS_CHECKPOINTS.INSTANCE_ID.eq(processKey.getInstanceId())\n                        .and(PROCESS_CHECKPOINTS.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt())))\n                .fetch(ProcessCheckpointDao::toEntry));\n    }\n\n    public UUID getRecentId(ProcessKey processKey, String checkpointName) {\n        return txResult(tx -> tx.select(PROCESS_CHECKPOINTS.CHECKPOINT_ID)\n                .from(PROCESS_CHECKPOINTS)\n                .where(PROCESS_CHECKPOINTS.INSTANCE_ID.eq(processKey.getInstanceId())\n                        .and(PROCESS_CHECKPOINTS.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt()))\n                        .and(PROCESS_CHECKPOINTS.CHECKPOINT_NAME.eq(checkpointName)))\n                .orderBy(PROCESS_CHECKPOINTS.CHECKPOINT_DATE.desc())\n                .limit(1)\n                .fetchOne(PROCESS_CHECKPOINTS.CHECKPOINT_ID));\n    }\n\n    public void importCheckpoint(ProcessKey processKey, UUID checkpointId, UUID correlationId, String checkpointName, Path data) {\n        if (checkpointName.length() > PROCESS_CHECKPOINTS.CHECKPOINT_NAME.getDataType().length()) {\n            throw new ValidationErrorsException(\"Invalid checkpoint name: value too long. Actual: \" + checkpointName.length() + \", max: \" + PROCESS_CHECKPOINTS.CHECKPOINT_NAME.getDataType().length());\n        }\n        tx(tx -> {\n            String sql = tx.insertInto(PROCESS_CHECKPOINTS)\n                    .columns(PROCESS_CHECKPOINTS.INSTANCE_ID,\n                            PROCESS_CHECKPOINTS.INSTANCE_CREATED_AT,\n                            PROCESS_CHECKPOINTS.CHECKPOINT_ID,\n                            PROCESS_CHECKPOINTS.CHECKPOINT_NAME,\n                            PROCESS_CHECKPOINTS.CHECKPOINT_DATE,\n                            PROCESS_CHECKPOINTS.CHECKPOINT_DATA,\n                            PROCESS_CHECKPOINTS.CORRELATION_ID)\n                    .values((UUID) null, null, null, null, null, null, null)\n                    .getSQL();\n\n            tx.connection(conn -> {\n                try (PreparedStatement ps = conn.prepareStatement(sql)) {\n                    ps.setObject(1, processKey.getInstanceId());\n                    ps.setObject(2, processKey.getCreatedAt());\n                    ps.setObject(3, checkpointId);\n                    ps.setString(4, checkpointName);\n                    ps.setTimestamp(5, new Timestamp(new Date().getTime()));\n                    try (InputStream in = Files.newInputStream(data)) {\n                        ps.setBinaryStream(6, in);\n                    }\n                    ps.setObject(7, correlationId);\n\n                    ps.execute();\n                }\n            });\n        });\n    }\n\n    public String export(ProcessKey processKey, UUID checkpointId, Path dest) {\n        return txResult(tx -> {\n            String sql = tx.select(PROCESS_CHECKPOINTS.CHECKPOINT_DATA, PROCESS_CHECKPOINTS.CHECKPOINT_NAME)\n                    .from(PROCESS_CHECKPOINTS)\n                    .where(PROCESS_CHECKPOINTS.CHECKPOINT_ID.eq(checkpointId)\n                            .and(PROCESS_CHECKPOINTS.INSTANCE_ID.eq(processKey.getInstanceId())\n                                    .and(PROCESS_CHECKPOINTS.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt()))))\n                    .getSQL();\n\n            return tx.connectionResult(conn -> {\n                try (PreparedStatement ps = conn.prepareStatement(sql)) {\n                    ps.setObject(1, checkpointId);\n                    ps.setObject(2, processKey.getInstanceId());\n                    ps.setObject(3, processKey.getCreatedAt());\n\n                    try (ResultSet rs = ps.executeQuery()) {\n                        if (!rs.next()) {\n                            return null;\n                        }\n\n                        try (InputStream in = rs.getBinaryStream(1)) {\n                            Files.copy(in, dest, StandardCopyOption.REPLACE_EXISTING);\n                        }\n                        return rs.getString(2);\n                    }\n                }\n            });\n        });\n    }\n\n    private static ProcessCheckpointEntry toEntry(Record r) {\n        return ImmutableProcessCheckpointEntry.builder()\n                .id(r.get(PROCESS_CHECKPOINTS.CHECKPOINT_ID))\n                .name(r.get(PROCESS_CHECKPOINTS.CHECKPOINT_NAME))\n                .createdAt(r.get(PROCESS_CHECKPOINTS.CHECKPOINT_DATE))\n                .correlationId(r.get(PROCESS_CHECKPOINTS.CORRELATION_ID))\n                .build();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/state/ProcessCheckpointManager.java",
    "content": "package com.walmartlabs.concord.server.process.state;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.TemporaryPath;\nimport com.walmartlabs.concord.common.ZipUtils;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.org.project.ProjectAccessManager;\nimport com.walmartlabs.concord.server.process.OutVariablesUtils;\nimport com.walmartlabs.concord.server.process.ProcessEntry;\nimport com.walmartlabs.concord.server.process.ProcessEntry.ProcessCheckpointEntry;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueDao;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.sdk.Constants.Files.CHECKPOINT_META_FILE_NAME;\n\npublic class ProcessCheckpointManager {\n\n    private final ProcessCheckpointDao checkpointDao;\n    private final ProcessQueueDao queueDao;\n    private final ProcessStateManager stateManager;\n    private final ProjectAccessManager projectAccessManager;\n\n    @Inject\n    protected ProcessCheckpointManager(ProcessCheckpointDao checkpointDao,\n                                       ProcessQueueDao queueDao,\n                                       ProcessStateManager stateManager,\n                                       ProjectAccessManager projectAccessManager) {\n\n        this.checkpointDao = checkpointDao;\n        this.queueDao = queueDao;\n        this.stateManager = stateManager;\n        this.projectAccessManager = projectAccessManager;\n    }\n\n    public UUID getRecentCheckpointId(ProcessKey processKey, String checkpointName) {\n        return checkpointDao.getRecentId(processKey, checkpointName);\n    }\n\n    /**\n     * Import checkpoints data from the specified directory or a file.\n     *\n     * @param processKey     process key\n     * @param checkpointId   process checkpoint ID\n     * @param checkpointName process checkpoint name\n     * @param data           checkpoint data file\n     */\n    public void importCheckpoint(ProcessKey processKey, UUID checkpointId, UUID correlationId, String checkpointName, Path data) {\n        checkpointDao.importCheckpoint(processKey, checkpointId, correlationId, checkpointName, data);\n    }\n\n    /**\n     * Restore process to a saved checkpoint.\n     */\n    public CheckpointInfo restoreCheckpoint(ProcessKey processKey, UUID checkpointId) {\n        try (TemporaryPath checkpointArchive = PathUtils.tempFile(\"checkpoint\", \".zip\")) {\n\n            String checkpointName = export(processKey, checkpointId, checkpointArchive.path());\n            if (checkpointName == null) {\n                return null;\n            }\n\n            try (TemporaryPath extractedDir = PathUtils.tempDir(\"unzipped-checkpoint\")) {\n                ZipUtils.unzip(checkpointArchive.path(), extractedDir.path());\n\n                // TODO: only for v1 runtime\n                String eventName = readCheckpointEventName(extractedDir.path());\n\n                stateManager.tx(tx -> {\n                    stateManager.deleteDirectory(tx, processKey, Constants.Files.CONCORD_SYSTEM_DIR_NAME);\n                    stateManager.deleteDirectory(tx, processKey, Constants.Files.JOB_ATTACHMENTS_DIR_NAME);\n                    stateManager.importPath(tx, processKey, null, extractedDir.path(), (p, attrs) -> true);\n                });\n\n                Map<String, Object> out = OutVariablesUtils.read(extractedDir.path().resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME));\n                if (out.isEmpty()) {\n                    queueDao.removeMeta(processKey, \"out\");\n                } else {\n                    queueDao.updateMeta(processKey, Collections.singletonMap(\"out\", out));\n                }\n\n                return CheckpointInfo.of(checkpointName, eventName);\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(\"Restore checkpoint '\" + checkpointId + \"' error\", e);\n        }\n    }\n\n    /**\n     * List checkpoints of a given instanceId\n     */\n    public List<ProcessCheckpointEntry> list(ProcessKey processKey) {\n        return checkpointDao.list(processKey);\n    }\n\n    public void assertProcessAccess(ProcessEntry e) {\n        UserPrincipal p = UserPrincipal.assertCurrent();\n\n        UUID initiatorId = e.initiatorId();\n        if (p.getId().equals(initiatorId)) {\n            // process owners should be able to restore the process from a checkpoint\n            return;\n        }\n\n        if (Roles.isAdmin()) {\n            return;\n        }\n\n        UUID projectId = e.projectId();\n        if (projectId != null) {\n            projectAccessManager.assertAccess(projectId, ResourceAccessLevel.WRITER, false);\n            return;\n        }\n\n        throw new UnauthorizedException(\"The current user (\" + p.getUsername() + \") doesn't have \" +\n                \"the necessary permissions to restore the process using a checkpoint: \" + e.instanceId());\n    }\n\n    @Value.Immutable\n    public interface CheckpointInfo {\n\n        String name();\n\n        /**\n         * {@code null} in the \"concord-v2\" runtime.\n         */\n        @Nullable\n        String eventName();\n\n        static CheckpointInfo of(String name, String eventName) {\n            return ImmutableCheckpointInfo.builder()\n                    .name(name)\n                    .eventName(eventName)\n                    .build();\n        }\n    }\n\n    private String readCheckpointEventName(Path checkpointDir) throws IOException {\n        Path checkpoint = checkpointDir.resolve(CHECKPOINT_META_FILE_NAME);\n        if (!Files.exists(checkpoint)) {\n            return null;\n        }\n        String checkpointName = new String(Files.readAllBytes(checkpoint));\n        Files.delete(checkpoint);\n        return checkpointName;\n    }\n\n    private String export(ProcessKey processKey, UUID checkpointId, Path dest) {\n        return checkpointDao.export(processKey, checkpointId, dest);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/state/ProcessStateManager.java",
    "content": "package com.walmartlabs.concord.server.process.state;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.Posix;\nimport com.walmartlabs.concord.common.secret.SecretUtils;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.db.PgUtils;\nimport com.walmartlabs.concord.policyengine.CheckResult;\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.policyengine.StatePolicy;\nimport com.walmartlabs.concord.policyengine.StateRule;\nimport com.walmartlabs.concord.server.cfg.ProcessConfiguration;\nimport com.walmartlabs.concord.server.cfg.SecretStoreConfiguration;\nimport com.walmartlabs.concord.server.policy.PolicyException;\nimport com.walmartlabs.concord.server.policy.PolicyManager;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKeyCache;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport org.apache.commons.compress.archivers.zip.ZipArchiveEntry;\nimport org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;\nimport org.jooq.*;\nimport org.jooq.impl.DSL;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.nio.file.*;\nimport java.nio.file.attribute.BasicFileAttributes;\nimport java.nio.file.attribute.PosixFilePermission;\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.time.OffsetDateTime;\nimport java.util.*;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\nimport java.util.stream.Stream;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.PROCESS_INITIAL_STATE;\nimport static com.walmartlabs.concord.server.jooq.tables.ProcessQueue.PROCESS_QUEUE;\nimport static com.walmartlabs.concord.server.jooq.tables.ProcessState.PROCESS_STATE;\nimport static com.walmartlabs.concord.server.jooq.tables.Projects.PROJECTS;\nimport static org.jooq.impl.DSL.*;\n\npublic class ProcessStateManager extends AbstractDao {\n\n    private static final Logger log = LoggerFactory.getLogger(ProcessStateManager.class);\n\n    private static final String PATH_SEPARATOR = \"/\";\n    private static final int INSERT_BATCH_SIZE = 10;\n\n    private final SecretStoreConfiguration secretCfg;\n    private final PolicyManager policyManager;\n    private final ProcessLogManager logManager;\n    private final ProcessKeyCache processKeyCache;\n\n    private final Set<String> secureFiles;\n\n    @Inject\n    protected ProcessStateManager(@MainDB Configuration cfg,\n                                  SecretStoreConfiguration secretCfg,\n                                  ProcessConfiguration stateCfg,\n                                  PolicyManager policyManager,\n                                  ProcessLogManager logManager,\n                                  ProcessKeyCache processKeyCache) {\n        super(cfg);\n        this.secretCfg = secretCfg;\n        this.policyManager = policyManager;\n        this.logManager = logManager;\n        this.processKeyCache = processKeyCache;\n\n        this.secureFiles = Collections.unmodifiableSet(new HashSet<>(stateCfg.getSecureFiles()));\n    }\n\n    @Override\n    public void tx(Tx t) {\n        super.tx(t);\n    }\n\n    public <T> Optional<T> get(PartialProcessKey partialProcessKey, String path, Function<InputStream, Optional<T>> converter) {\n        ProcessKey processKey = processKeyCache.get(partialProcessKey.getInstanceId());\n        if (processKey == null) {\n            throw new IllegalStateException(\"Can't determine the process key of \" + partialProcessKey.getInstanceId());\n        }\n        return get(processKey, path, converter);\n    }\n\n    /**\n     * Fetches a single value specified by its path and applies a converter function.\n     */\n    public <T> Optional<T> get(ProcessKey processKey, String path, Function<InputStream, Optional<T>> converter) {\n        return get(dsl(), processKey, path, converter);\n    }\n\n    private <T> Optional<T> get(DSLContext tx, ProcessKey processKey, String path, Function<InputStream, Optional<T>> converter) {\n        return doGet(tx, CurrentProcessStateTable.INSTANCE, processKey, path, converter);\n    }\n\n    public <T> Optional<T> getInitial(ProcessKey processKey, String path, Function<InputStream, Optional<T>> converter) {\n        return getInitial(dsl(), processKey, path, converter);\n    }\n\n    private <T> Optional<T> getInitial(DSLContext tx, ProcessKey processKey, String path, Function<InputStream, Optional<T>> converter) {\n        return doGet(tx, InitialProcessStateTable.INSTANCE, processKey, path, converter);\n    }\n\n    private <T> Optional<T> doGet(DSLContext tx, ProcessStateTable table, ProcessKey processKey, String path, Function<InputStream, Optional<T>> converter) {\n        String sql = tx.select(table.IS_ENCRYPTED(), table.ITEM_DATA())\n                .from(table.table())\n                .where(table.INSTANCE_ID().eq((UUID) null)\n                        .and(table.INSTANCE_CREATED_AT().eq((OffsetDateTime) null))\n                        .and(table.ITEM_PATH().eq((String) null)))\n                .getSQL();\n\n        return tx.connectionResult(conn -> {\n            try (PreparedStatement ps = conn.prepareStatement(sql)) {\n                ps.setObject(1, processKey.getInstanceId());\n                ps.setObject(2, processKey.getCreatedAt());\n                ps.setString(3, path);\n\n                try (ResultSet rs = ps.executeQuery()) {\n                    if (!rs.next()) {\n                        return Optional.empty();\n                    }\n                    boolean encrypted = rs.getBoolean(1);\n                    try (InputStream in = rs.getBinaryStream(2);\n                         InputStream processed = encrypted ? decrypt(in) : in) {\n                        return converter.apply(processed);\n                    }\n                }\n            }\n        });\n    }\n\n    /**\n     * Fetches multiple values whose path begins with the specified value and applies a converter function\n     * to each value.\n     */\n    public <T> List<T> forEach(ProcessKey processKey, String path, Function<InputStream, Optional<T>> converter) {\n        DSLContext tx = dsl();\n\n        String sql = tx.select(PROCESS_STATE.IS_ENCRYPTED, PROCESS_STATE.ITEM_DATA)\n                .from(PROCESS_STATE)\n                .where(PROCESS_STATE.INSTANCE_ID.eq((UUID) null)\n                        .and(PROCESS_STATE.INSTANCE_CREATED_AT.eq((OffsetDateTime) null))\n                        .and(PROCESS_STATE.ITEM_PATH.startsWith((String) null)))\n                .getSQL();\n\n        return tx.connectionResult(conn -> {\n            try (PreparedStatement ps = conn.prepareStatement(sql)) {\n                ps.setObject(1, processKey.getInstanceId());\n                ps.setObject(2, processKey.getCreatedAt());\n                ps.setString(3, path);\n\n                List<T> result = new ArrayList<>();\n\n                try (ResultSet rs = ps.executeQuery()) {\n                    while (rs.next()) {\n                        boolean encrypted = rs.getBoolean(1);\n                        try (InputStream in = rs.getBinaryStream(2);\n                             InputStream processed = encrypted ? decrypt(in) : in) {\n                            Optional<T> o = converter.apply(processed);\n                            o.ifPresent(result::add);\n                        }\n                    }\n                }\n\n                return result;\n            }\n        });\n    }\n\n    /**\n     * Retrieves a list of resources whose path begins with the specified value.\n     */\n    public List<String> list(PartialProcessKey partialProcessKey, String path) {\n        ProcessKey processKey = processKeyCache.assertKey(partialProcessKey.getInstanceId());\n        return dsl().select(PROCESS_STATE.ITEM_PATH)\n                .from(PROCESS_STATE)\n                .where(PROCESS_STATE.INSTANCE_ID.eq(processKey.getInstanceId())\n                        .and(PROCESS_STATE.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt()))\n                        .and(PROCESS_STATE.ITEM_PATH.startsWith(path)))\n                .fetch(PROCESS_STATE.ITEM_PATH);\n    }\n\n    /**\n     * Finds all item paths that starts with the specified value.\n     */\n    public <T> Optional<T> findPath(ProcessKey processKey, String path, Function<Stream<String>, Optional<T>> converter) {\n        Stream<String> s = dsl().select(PROCESS_STATE.ITEM_PATH)\n                .from(PROCESS_STATE)\n                .where(PROCESS_STATE.INSTANCE_ID.eq(processKey.getInstanceId())\n                        .and(PROCESS_STATE.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt()))\n                        .and(PROCESS_STATE.ITEM_PATH.startsWith(path)))\n                .fetch(PROCESS_STATE.ITEM_PATH)\n                .stream();\n\n        return converter.apply(s);\n    }\n\n    public boolean exists(PartialProcessKey partialProcessKey, String path) {\n        ProcessKey processKey = processKeyCache.get(partialProcessKey.getInstanceId());\n        return exists(processKey, path);\n    }\n\n    /**\n     * Checks if a value exists.\n     */\n    public boolean exists(ProcessKey processKey, String path) {\n        return dsl().fetchExists(selectFrom(PROCESS_STATE)\n                .where(PROCESS_STATE.INSTANCE_ID.eq(processKey.getInstanceId())\n                        .and(PROCESS_STATE.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt()))\n                        .and(PROCESS_STATE.ITEM_PATH.startsWith(path))));\n    }\n\n    /**\n     * Removes a single value.\n     */\n    public void deleteFile(ProcessKey processKey, String path) {\n        tx(tx -> deleteFile(tx, processKey, path));\n    }\n\n    public void deleteFile(DSLContext tx, ProcessKey processKey, String path) {\n        tx.deleteFrom(PROCESS_STATE)\n                .where(PROCESS_STATE.INSTANCE_ID.eq(processKey.getInstanceId())\n                        .and(PROCESS_STATE.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt()))\n                        .and(PROCESS_STATE.ITEM_PATH.eq(path)))\n                .execute();\n    }\n\n    public void delete(ProcessKey processKey) {\n        tx(tx -> delete(tx, processKey));\n    }\n\n    public void delete(DSLContext tx, ProcessKey processKey) {\n        delete(tx, processKey.getInstanceId(), processKey.getCreatedAt());\n    }\n\n    /**\n     * Remove a directory and all content from it.\n     */\n    @WithTimer\n    public void deleteDirectory(DSLContext tx, ProcessKey processKey, String path) {\n        tx.deleteFrom(PROCESS_STATE)\n                .where(PROCESS_STATE.INSTANCE_ID.eq(processKey.getInstanceId())\n                        .and(PROCESS_STATE.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt())))\n                .and(PROCESS_STATE.ITEM_PATH.eq(path)\n                        .or(PROCESS_STATE.ITEM_PATH.startsWith(fixPath(path))))\n                .execute();\n    }\n\n    /**\n     * Replaces a single value.\n     */\n    public void replace(ProcessKey processKey, String path, byte[] data) {\n        tx(tx -> {\n            deleteDirectory(tx, processKey, path);\n            insert(tx, processKey, path, data);\n        });\n    }\n\n    /**\n     * Inserts a single value.\n     */\n    public void insert(DSLContext tx, ProcessKey processKey, String path, byte[] in) {\n        doInsert(tx, CurrentProcessStateTable.INSTANCE, processKey, path, in);\n    }\n\n    public void insertInitial(DSLContext tx, ProcessKey processKey, String path, byte[] in) {\n        doInsert(tx, InitialProcessStateTable.INSTANCE, processKey, path, in);\n    }\n\n    private void doInsert(DSLContext tx, ProcessStateTable table, ProcessKey processKey, String path, byte[] in) {\n        boolean needEncrypt = secureFiles.contains(path);\n        byte[] data = in;\n        if (needEncrypt) {\n            data = encrypt(in);\n        }\n        tx.insertInto(table.table())\n                .columns(table.INSTANCE_ID(), table.INSTANCE_CREATED_AT(), table.ITEM_PATH(), table.ITEM_DATA(), table.IS_ENCRYPTED())\n                .values(processKey.getInstanceId(), processKey.getCreatedAt(), path, data, needEncrypt)\n                .execute();\n    }\n\n    /**\n     * Imports data from the specified directory or a file replacing the existing data.\n     * If the filter function returns {@code false}, the matching file will be skipped.\n     */\n    public void replacePath(ProcessKey processKey, Path src, BiFunction<Path, BasicFileAttributes, Boolean> filter) {\n        UUID instanceId = processKey.getInstanceId();\n        OffsetDateTime instanceCreatedAt = processKey.getCreatedAt();\n\n        tx(tx -> {\n            delete(tx, instanceId, instanceCreatedAt);\n            importPath(tx, processKey, null, src, filter);\n        });\n    }\n\n    /**\n     * Imports data from the specified directory or a file.\n     */\n    @WithTimer\n    public void importPath(ProcessKey processKey, String path, Path src, BiFunction<Path, BasicFileAttributes, Boolean> filter) {\n        tx(tx -> importPath(tx, processKey, path, src, filter));\n    }\n\n    @WithTimer\n    public void importPath(DSLContext tx, ProcessKey processKey, String path, Path src, BiFunction<Path, BasicFileAttributes, Boolean> filter) {\n        doImportPath(tx, CurrentProcessStateTable.INSTANCE, processKey, path, src, filter);\n    }\n\n    @WithTimer\n    public void importPathInitial(DSLContext tx, ProcessKey processKey, String path, Path src, BiFunction<Path, BasicFileAttributes, Boolean> filter) {\n        doImportPath(tx, InitialProcessStateTable.INSTANCE, processKey, path, src, filter);\n    }\n\n    private void doImportPath(DSLContext tx, ProcessStateTable table, ProcessKey processKey, String path, Path src, BiFunction<Path, BasicFileAttributes, Boolean> filter) {\n        PolicyEngine policyEngine = assertPolicy(tx, processKey, src, filter);\n\n        String prefix = fixPath(path);\n\n        List<BatchItem> batch = new ArrayList<>();\n        try {\n            Files.walkFileTree(src, new SimpleFileVisitor<Path>() {\n                @Override\n                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {\n                    if (!filter.apply(file, attrs)) {\n                        return FileVisitResult.CONTINUE;\n                    }\n\n                    Path p = src.relativize(file);\n\n                    // can't import directories or symlinks\n                    // the caller shouldn't attempt to import anything but regular files\n                    if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) {\n                        throw new IllegalStateException(\"Can't import non-regular files into the process state: \" + p +\n                                \" This is most likely a bug.\");\n                    }\n\n                    String n = p.toString();\n                    if (prefix != null) {\n                        n = prefix + n;\n                    }\n\n                    Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(file);\n                    int unixMode = Posix.unixMode(permissions);\n                    boolean needsEncryption = secureFiles.contains(n);\n\n                    tx.deleteFrom(table.table()).where(table.INSTANCE_ID().eq(processKey.getInstanceId())\n                                    .and(table.INSTANCE_CREATED_AT().eq(processKey.getCreatedAt()))\n                                    .and(table.ITEM_PATH().eq(n)))\n                            .execute();\n\n                    batch.add(new BatchItem(n, file, unixMode, needsEncryption));\n                    if (batch.size() >= INSERT_BATCH_SIZE) {\n                        doInsert(tx, table, processKey.getInstanceId(), processKey.getCreatedAt(), batch);\n                        batch.clear();\n                    }\n\n                    return FileVisitResult.CONTINUE;\n                }\n            });\n\n            if (!batch.isEmpty()) {\n                doInsert(tx, table, processKey.getInstanceId(), processKey.getCreatedAt(), batch);\n            }\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n\n        assertPolicy(tx, processKey, policyEngine);\n    }\n\n    /**\n     * Exports all data of a process instance.\n     */\n    public boolean export(ProcessKey processKey, ItemConsumer consumer) {\n        DSLContext tx = dsl();\n\n        String sql = tx\n                .select(PROCESS_STATE.ITEM_PATH, PROCESS_STATE.UNIX_MODE, PROCESS_STATE.IS_ENCRYPTED, PROCESS_STATE.ITEM_DATA)\n                .from(PROCESS_STATE)\n                .where(PROCESS_STATE.INSTANCE_ID.eq((UUID) null).and(PROCESS_STATE.INSTANCE_CREATED_AT.eq((OffsetDateTime) null)))\n                .getSQL();\n\n        return tx.connectionResult(conn -> {\n            try (PreparedStatement ps = conn.prepareStatement(sql)) {\n                ps.setObject(1, processKey.getInstanceId());\n                ps.setObject(2, processKey.getCreatedAt());\n\n                boolean found = false;\n                try (ResultSet rs = ps.executeQuery()) {\n                    while (rs.next()) {\n                        found = true;\n\n                        String n = rs.getString(1);\n                        int unixMode = rs.getInt(2);\n                        boolean encrypted = rs.getBoolean(3);\n                        try (InputStream in = rs.getBinaryStream(4);\n                             InputStream processed = encrypted ? decrypt(in) : in) {\n                            consumer.accept(n, unixMode, processed);\n                        }\n                    }\n                }\n\n                return found;\n            }\n        });\n    }\n\n    /**\n     * Exports elements whose path begins with the specified value.\n     */\n    public boolean exportDirectory(ProcessKey processKey, String path, ItemConsumer consumer) {\n        return doExportDirectory(CurrentProcessStateTable.INSTANCE, processKey, path, consumer);\n    }\n\n    public boolean exportDirectoryInitial(ProcessKey processKey, String path, ItemConsumer consumer) {\n        return doExportDirectory(InitialProcessStateTable.INSTANCE, processKey, path, consumer);\n    }\n\n    public boolean doExportDirectory(ProcessStateTable table, ProcessKey processKey, String path, ItemConsumer consumer) {\n        String dir = fixPath(path);\n\n        DSLContext tx = dsl();\n\n        String sql = tx\n                .select(table.ITEM_PATH(), table.UNIX_MODE(), table.IS_ENCRYPTED(), table.ITEM_DATA())\n                .from(table.table())\n                .where(table.INSTANCE_ID().eq((UUID) null)\n                        .and(table.INSTANCE_CREATED_AT().eq((OffsetDateTime) null))\n                        .and(table.ITEM_PATH().startsWith((String) null)))\n                .getSQL();\n\n        return tx.connectionResult(conn -> {\n            try (PreparedStatement ps = conn.prepareStatement(sql)) {\n                ps.setObject(1, processKey.getInstanceId());\n                ps.setObject(2, processKey.getCreatedAt());\n                ps.setString(3, dir);\n\n                boolean found = false;\n                try (ResultSet rs = ps.executeQuery()) {\n                    while (rs.next()) {\n                        found = true;\n\n                        String n = relativize(dir, rs.getString(1));\n                        int unixMode = rs.getInt(2);\n                        boolean encrypted = rs.getBoolean(3);\n                        try (InputStream in = rs.getBinaryStream(4);\n                             InputStream processed = encrypted ? decrypt(in) : in) {\n                            consumer.accept(n, unixMode, processed);\n                        }\n                    }\n                }\n\n                return found;\n            }\n        });\n    }\n\n    /**\n     * Copies the data to the specified target directory.\n     *\n     * @param dst     target directory\n     * @param options optional copy options\n     */\n    public static ItemConsumer copyTo(Path dst, OpenOption... options) {\n        return new CopyConsumer(dst, null, options);\n    }\n\n    /**\n     * Copies the data to the specified target directory. Skips the ignored files.\n     *\n     * @param dst     target directory\n     * @param ignored name patterns or ignored files\n     * @param options optional copy options\n     */\n    public static ItemConsumer copyTo(Path dst, String[] ignored, OpenOption... options) {\n        return new CopyConsumer(dst, ignored, options);\n    }\n\n    /**\n     * Puts all elements into the specified ZIP archive stream.\n     *\n     * @param dst archive stream.\n     */\n    public static ItemConsumer zipTo(ZipArchiveOutputStream dst) {\n        return new ZipConsumer(dst);\n    }\n\n    public static ItemConsumer exclude(ItemConsumer delegate, String... patterns) {\n        return new FilteringConsumer(delegate, n -> Arrays.stream(patterns).noneMatch(n::matches));\n    }\n\n    /**\n     * Creates a path from the specified array of elements.\n     */\n    public static String path(String... elements) {\n        return String.join(PATH_SEPARATOR, elements);\n    }\n\n    private void delete(DSLContext tx, UUID instanceId, OffsetDateTime instanceCreatedAt) {\n        tx.deleteFrom(PROCESS_STATE)\n                .where(PROCESS_STATE.INSTANCE_ID.eq(instanceId)\n                        .and(PROCESS_STATE.INSTANCE_CREATED_AT.eq(instanceCreatedAt)))\n                .execute();\n    }\n\n    private void insert(DSLContext tx, UUID instanceId, OffsetDateTime instanceCreatedAt, Collection<BatchItem> batch) {\n        String sql = tx.insertInto(PROCESS_STATE)\n                .columns(PROCESS_STATE.INSTANCE_ID, PROCESS_STATE.INSTANCE_CREATED_AT, PROCESS_STATE.ITEM_PATH, PROCESS_STATE.UNIX_MODE, PROCESS_STATE.ITEM_DATA, PROCESS_STATE.IS_ENCRYPTED)\n                .values((UUID) null, null, null, null, null, null)\n                .getSQL();\n\n        List<InputStream> streams = new LinkedList<>();\n        try {\n            tx.connection(conn -> {\n                try (PreparedStatement ps = conn.prepareStatement(sql)) {\n                    for (BatchItem item : batch) {\n                        // INSTANCE_ID\n                        ps.setObject(1, instanceId);\n\n                        // INSTANCE_CREATED_AT\n                        ps.setObject(2, instanceCreatedAt);\n\n                        // ITEM_PATH\n                        ps.setString(3, item.itemPath);\n\n                        // UNIX_MODE\n                        ps.setInt(4, item.unixMode);\n\n                        InputStream in = Files.newInputStream(item.path);\n                        streams.add(in); // keep the streams open until the batch is committed\n\n                        if (item.needsEncryption) {\n                            in = encrypt(in);\n                        }\n\n                        // ITEM_DATA\n                        ps.setBinaryStream(5, in);\n\n                        // IS_ENCRYPTED\n                        ps.setBoolean(6, item.needsEncryption);\n\n                        ps.addBatch();\n                    }\n\n                    ps.executeBatch();\n                } catch (SQLException e) {\n                    throw new RuntimeException(e);\n                }\n            });\n        } finally {\n            streams.forEach(ProcessStateManager::closeSilently);\n        }\n    }\n\n    private void doInsert(DSLContext tx, ProcessStateTable table, UUID instanceId, OffsetDateTime instanceCreatedAt, Collection<BatchItem> batch) {\n        String sql = tx.insertInto(table.table())\n                .columns(table.INSTANCE_ID(), table.INSTANCE_CREATED_AT(), table.ITEM_PATH(), table.UNIX_MODE(), table.ITEM_DATA(), table.IS_ENCRYPTED())\n                .values((UUID) null, null, null, null, null, null)\n                .getSQL();\n\n        List<InputStream> streams = new LinkedList<>();\n        try {\n            tx.connection(conn -> {\n                try (PreparedStatement ps = conn.prepareStatement(sql)) {\n                    for (BatchItem item : batch) {\n                        // INSTANCE_ID\n                        ps.setObject(1, instanceId);\n\n                        // INSTANCE_CREATED_AT\n                        ps.setObject(2, instanceCreatedAt);\n\n                        // ITEM_PATH\n                        ps.setString(3, item.itemPath);\n\n                        // UNIX_MODE\n                        ps.setInt(4, item.unixMode);\n\n                        InputStream in = Files.newInputStream(item.path);\n                        streams.add(in); // keep the streams open until the batch is committed\n\n                        if (item.needsEncryption) {\n                            in = encrypt(in);\n                        }\n\n                        // ITEM_DATA\n                        ps.setBinaryStream(5, in);\n\n                        // IS_ENCRYPTED\n                        ps.setBoolean(6, item.needsEncryption);\n\n                        ps.addBatch();\n                    }\n\n                    ps.executeBatch();\n                } catch (SQLException e) {\n                    throw new RuntimeException(e);\n                }\n            });\n        } finally {\n            streams.forEach(ProcessStateManager::closeSilently);\n        }\n    }\n\n    private InputStream decrypt(InputStream in) {\n        return SecretUtils.decrypt(in, secretCfg.getServerPwd(), secretCfg.getSecretStoreSalt());\n    }\n\n    private InputStream encrypt(InputStream in) {\n        return SecretUtils.encrypt(in, secretCfg.getServerPwd(), secretCfg.getSecretStoreSalt());\n    }\n\n    private byte[] encrypt(byte[] in) {\n        return SecretUtils.encrypt(in, secretCfg.getServerPwd(), secretCfg.getSecretStoreSalt());\n    }\n\n    private static String fixPath(String p) {\n        if (p == null) {\n            return null;\n        }\n\n        if (!p.endsWith(PATH_SEPARATOR)) {\n            p = p + PATH_SEPARATOR;\n        }\n\n        return p;\n    }\n\n    private static String relativize(String parent, String child) {\n        int i = child.indexOf(parent);\n        if (i < 0) {\n            throw new IllegalArgumentException(\"Can't relativize '\" + child + \"' from '\" + parent + \"'\");\n        }\n        return child.substring(parent.length());\n    }\n\n    private PolicyEngine assertPolicy(DSLContext tx, ProcessKey processKey, Path src, BiFunction<Path, BasicFileAttributes, Boolean> filter) {\n        PolicyEngine pe = getPolicyEngine(tx, processKey);\n        if (pe == null) {\n            return null;\n        }\n\n        CheckResult<StateRule, Path> result;\n        try {\n            result = pe.getStatePolicy().check(src, filter);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n\n        result.getWarn().forEach(w -> logManager.warn(processKey, \"Potentially restricted state file '{}' (state policy: {})\", src.relativize(w.getEntity()), w.getRule().msg()));\n        result.getDeny().forEach(e -> logManager.error(processKey, \"State file '{}' is forbidden by the state policy {}\", src.relativize(e.getEntity()), e.getRule().msg()));\n\n        if (!result.getDeny().isEmpty()) {\n            throw new PolicyException(\"Found forbidden state files\");\n        }\n\n        return pe;\n    }\n\n    private void assertPolicy(DSLContext tx, ProcessKey processKey, PolicyEngine policyEngine) {\n        if (policyEngine == null) {\n            return;\n        }\n\n        CheckResult<StateRule, StatePolicy.StateStats> result = policyEngine.getStatePolicy().check(() -> getStateStats(tx, processKey));\n\n        result.getWarn().forEach(w -> logManager.warn(processKey, \"Potentially restricted state: '{}' (state policy: {})\", w.getMsg(), w.getRule().msg()));\n        result.getDeny().forEach(e -> logManager.error(processKey, \"State is forbidden: '{}' (state policy {})\", e.getMsg(), e.getRule().msg()));\n\n        if (!result.getDeny().isEmpty()) {\n            throw new PolicyException(\"Found forbidden state files\");\n        }\n    }\n\n    private static StatePolicy.StateStats getStateStats(DSLContext tx, ProcessKey processKey) {\n        return tx.select(DSL.sum(PgUtils.length(PROCESS_STATE.ITEM_DATA)), count(PROCESS_STATE.ITEM_DATA))\n                .from(PROCESS_STATE)\n                .where(PROCESS_STATE.INSTANCE_ID.eq(processKey.getInstanceId())\n                        .and(PROCESS_STATE.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt())))\n                .fetchOne(r -> new StatePolicy.StateStats(r.value1().longValue(), r.value2()));\n    }\n\n    private PolicyEngine getPolicyEngine(DSLContext tx, ProcessKey processKey) {\n        Field<UUID> orgId = select(PROJECTS.ORG_ID)\n                .from(PROJECTS)\n                .where(PROJECTS.PROJECT_ID.eq(PROCESS_QUEUE.PROJECT_ID))\n                .asField();\n\n        Map<String, UUID> info = tx.select(orgId, PROCESS_QUEUE.PROJECT_ID, PROCESS_QUEUE.INITIATOR_ID)\n                .from(PROCESS_QUEUE)\n                .where(PROCESS_QUEUE.INSTANCE_ID.eq(processKey.getInstanceId()))\n                .fetchOne(r -> {\n                    Map<String, UUID> result = new HashMap<>();\n                    result.put(\"orgId\", r.value1());\n                    result.put(\"prjId\", r.value2());\n                    result.put(\"userId\", r.value3());\n                    return result;\n                });\n        if (info == null) {\n            return null;\n        }\n\n        return policyManager.get(info.get(\"orgId\"), info.get(\"prjId\"), info.get(\"userId\"));\n    }\n\n    private static void closeSilently(AutoCloseable c) {\n        if (c == null) {\n            return;\n        }\n\n        try {\n            c.close();\n        } catch (Exception e) {\n            log.error(\"Error while closing a resource\", e);\n        }\n    }\n\n    public interface ItemConsumer {\n\n        void accept(String name, int unixMode, InputStream src);\n    }\n\n    public static final class CopyConsumer implements ItemConsumer {\n\n        private final Path dst;\n        private final String[] ignored;\n        private final OpenOption[] options;\n\n        private CopyConsumer(Path dst, String[] ignored, OpenOption[] options) { // NOSONAR\n            this.dst = dst;\n            this.ignored = ignored;\n            this.options = options;\n        }\n\n        @Override\n        public void accept(String name, int unixMode, InputStream src) {\n            if (ignored != null) {\n                for (String i : ignored) {\n                    if (name.matches(i)) {\n                        return;\n                    }\n                }\n            }\n\n            Path p = dst.resolve(name);\n\n            try {\n                Path parent = p.getParent();\n                if (parent != null && !Files.exists(parent)) {\n                    Files.createDirectories(parent);\n                }\n\n                try (OutputStream dst = Files.newOutputStream(p, options)) {\n                    src.transferTo(dst);\n                }\n\n                Files.setPosixFilePermissions(p, Posix.posix(unixMode));\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n    }\n\n    public static final class ZipConsumer implements ItemConsumer {\n\n        private final ZipArchiveOutputStream dst;\n\n        private ZipConsumer(ZipArchiveOutputStream dst) {\n            this.dst = dst;\n        }\n\n        @Override\n        public void accept(String name, int unixMode, InputStream src) {\n            ZipArchiveEntry entry = new ZipArchiveEntry(name);\n            entry.setUnixMode(unixMode);\n\n            try {\n                dst.putArchiveEntry(entry);\n                src.transferTo(dst);\n                dst.closeArchiveEntry();\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n    }\n\n    public static final class FilteringConsumer implements ItemConsumer {\n\n        private final ItemConsumer delegate;\n        private final Function<String, Boolean> checkFn;\n\n        public FilteringConsumer(ItemConsumer delegate, Function<String, Boolean> checkFn) {\n            this.delegate = delegate;\n            this.checkFn = checkFn;\n        }\n\n        @Override\n        public void accept(String name, int unixMode, InputStream src) {\n            if (checkFn.apply(name)) {\n                delegate.accept(name, unixMode, src);\n            }\n        }\n    }\n\n    private static final class BatchItem {\n\n        private final String itemPath;\n        private final Path path;\n        private final int unixMode;\n        private final boolean needsEncryption;\n\n        private BatchItem(String itemPath, Path path, int unixMode, boolean needsEncryption) {\n            this.itemPath = itemPath;\n            this.path = path;\n            this.unixMode = unixMode;\n            this.needsEncryption = needsEncryption;\n        }\n    }\n\n    private interface ProcessStateTable {\n\n        Table<?> table();\n\n        TableField<?, UUID> INSTANCE_ID();\n\n        TableField<?, OffsetDateTime> INSTANCE_CREATED_AT();\n\n        TableField<?, String> ITEM_PATH();\n\n        TableField<?, byte[]> ITEM_DATA();\n\n        TableField<?, Short> UNIX_MODE();\n\n        TableField<?, Boolean> IS_ENCRYPTED();\n    }\n\n\n    static class InitialProcessStateTable implements ProcessStateTable {\n\n        public static InitialProcessStateTable INSTANCE = new InitialProcessStateTable();\n\n        @Override\n        public Table<?> table() {\n            return PROCESS_INITIAL_STATE;\n        }\n\n        @Override\n        public TableField<?, UUID> INSTANCE_ID() {\n            return PROCESS_INITIAL_STATE.INSTANCE_ID;\n        }\n\n        @Override\n        public TableField<?, OffsetDateTime> INSTANCE_CREATED_AT() {\n            return PROCESS_INITIAL_STATE.INSTANCE_CREATED_AT;\n        }\n\n        @Override\n        public TableField<?, String> ITEM_PATH() {\n            return PROCESS_INITIAL_STATE.ITEM_PATH;\n        }\n\n        @Override\n        public TableField<?, byte[]> ITEM_DATA() {\n            return PROCESS_INITIAL_STATE.ITEM_DATA;\n        }\n\n        @Override\n        public TableField<?, Short> UNIX_MODE() {\n            return PROCESS_INITIAL_STATE.UNIX_MODE;\n        }\n\n        @Override\n        public TableField<?, Boolean> IS_ENCRYPTED() {\n            return PROCESS_INITIAL_STATE.IS_ENCRYPTED;\n        }\n    }\n\n    static class CurrentProcessStateTable implements ProcessStateTable {\n\n        public static CurrentProcessStateTable INSTANCE = new CurrentProcessStateTable();\n\n        @Override\n        public Table<?> table() {\n            return PROCESS_STATE;\n        }\n\n        @Override\n        public TableField<?, UUID> INSTANCE_ID() {\n            return PROCESS_STATE.INSTANCE_ID;\n        }\n\n        @Override\n        public TableField<?, OffsetDateTime> INSTANCE_CREATED_AT() {\n            return PROCESS_STATE.INSTANCE_CREATED_AT;\n        }\n\n        @Override\n        public TableField<?, String> ITEM_PATH() {\n            return PROCESS_STATE.ITEM_PATH;\n        }\n\n        @Override\n        public TableField<?, byte[]> ITEM_DATA() {\n            return PROCESS_STATE.ITEM_DATA;\n        }\n\n        @Override\n        public TableField<?, Short> UNIX_MODE() {\n            return PROCESS_STATE.UNIX_MODE;\n        }\n\n        @Override\n        public TableField<?, Boolean> IS_ENCRYPTED() {\n            return PROCESS_STATE.IS_ENCRYPTED;\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/waits/AbstractWaitCondition.java",
    "content": "package com.walmartlabs.concord.server.process.waits;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonSubTypes;\nimport com.fasterxml.jackson.annotation.JsonTypeInfo;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\n\n/**\n * The inheriting classes MUST override {@link #equals(Object)}) if they\n * provide additional fields.\n */\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonTypeInfo(\n        use = JsonTypeInfo.Id.NAME,\n        property = \"type\")\n@JsonSubTypes({\n        @JsonSubTypes.Type(value = ImmutableProcessCompletionCondition.class, name = \"PROCESS_COMPLETION\"),\n        @JsonSubTypes.Type(value = ImmutableProcessLockCondition.class, name = \"PROCESS_LOCK\"),\n        @JsonSubTypes.Type(value = ImmutableProcessSleepCondition.class, name = \"PROCESS_SLEEP\")\n})\npublic abstract class AbstractWaitCondition implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    public abstract WaitType type();\n\n    @Nullable\n    public abstract String reason();\n\n    public abstract boolean exclusive();\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/waits/ProcessCompletionCondition.java",
    "content": "package com.walmartlabs.concord.server.process.waits;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.google.common.collect.ImmutableSet;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.util.Set;\nimport java.util.UUID;\n\n@Value.Immutable\n@Value.Modifiable\n@JsonSerialize(as = ImmutableProcessCompletionCondition.class)\n@JsonDeserialize(as = ImmutableProcessCompletionCondition.class)\npublic abstract class ProcessCompletionCondition extends AbstractWaitCondition {\n\n    private static final long serialVersionUID = 1L;\n\n    private static final Set<ProcessStatus> DEFAULT_FINISHED_STATUSES = ImmutableSet.of(\n            ProcessStatus.FINISHED,\n            ProcessStatus.FAILED,\n            ProcessStatus.CANCELLED,\n            ProcessStatus.TIMED_OUT);\n\n    @Nullable\n    public abstract String resumeEvent();\n\n    public abstract Set<UUID> processes();\n\n    @Value.Default\n    public Set<ProcessStatus> finalStatuses() {\n        return DEFAULT_FINISHED_STATUSES;\n    }\n\n    @Value.Default\n    public CompleteCondition completeCondition() {\n        return CompleteCondition.ALL;\n    }\n\n    @Override\n    public WaitType type() {\n        return WaitType.PROCESS_COMPLETION;\n    }\n\n    @Value.Default\n    @Override\n    public boolean exclusive() {\n        return false;\n    }\n\n    public static ImmutableProcessCompletionCondition.Builder builder() {\n        return ImmutableProcessCompletionCondition.builder();\n    }\n\n    public enum CompleteCondition {\n        // all processes are finished\n        ALL,\n        // one of the processes is finished\n        ONE_OF\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/waits/ProcessLockCondition.java",
    "content": "package com.walmartlabs.concord.server.process.waits;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.server.jooq.enums.ProcessLockScope;\nimport com.walmartlabs.concord.server.process.locks.LockEntry;\nimport org.immutables.value.Value;\n\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonSerialize(as = ImmutableProcessLockCondition.class)\n@JsonDeserialize(as = ImmutableProcessLockCondition.class)\npublic abstract class ProcessLockCondition extends AbstractWaitCondition {\n\n    private static final long serialVersionUID = 1L;\n\n    public abstract UUID instanceId();\n\n    public abstract UUID orgId();\n\n    public abstract UUID projectId();\n\n    public abstract ProcessLockScope scope();\n\n    public abstract String name();\n\n    @Override\n    public WaitType type() {\n        return WaitType.PROCESS_LOCK;\n    }\n\n    @Override\n    public boolean exclusive() {\n        return false;\n    }\n\n    public static ImmutableProcessLockCondition.Builder builder() {\n        return ImmutableProcessLockCondition.builder();\n    }\n\n    public static ProcessLockCondition from(LockEntry e) {\n        return builder()\n                .instanceId(e.instanceId())\n                .orgId(e.orgId())\n                .projectId(e.projectId())\n                .scope(e.scope())\n                .name(e.name())\n                .build();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/waits/ProcessSleepCondition.java",
    "content": "package com.walmartlabs.concord.server.process.waits;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport java.util.Date;\n\n@Value.Immutable\n@JsonSerialize(as = ImmutableProcessSleepCondition.class)\n@JsonDeserialize(as = ImmutableProcessSleepCondition.class)\npublic abstract class ProcessSleepCondition extends AbstractWaitCondition {\n\n    private static final long serialVersionUID = 1L;\n\n    public abstract String resumeEvent();\n\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    public abstract Date until();\n\n    @Override\n    public WaitType type() {\n        return WaitType.PROCESS_SLEEP;\n    }\n\n    @Override\n    public boolean exclusive() {\n        return false;\n    }\n\n    public static ImmutableProcessSleepCondition.Builder builder() {\n        return ImmutableProcessSleepCondition.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/waits/ProcessWaitDao.java",
    "content": "package com.walmartlabs.concord.server.process.waits;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.process.ProcessEntry.ProcessWaitEntry;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.jooq.JSONB;\n\nimport javax.inject.Inject;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.db.PgUtils.jsonbAppend;\nimport static com.walmartlabs.concord.db.PgUtils.jsonbOrEmptyArray;\nimport static com.walmartlabs.concord.server.jooq.Tables.PROCESS_WAIT_CONDITIONS;\nimport static org.jooq.impl.DSL.field;\n\npublic class ProcessWaitDao extends AbstractDao {\n\n    public static final TypeReference<List<AbstractWaitCondition>> WAIT_LIST = new TypeReference<List<AbstractWaitCondition>>() {\n    };\n\n    private static final TypeReference<List<Map<String, Object>>> LIST_OF_MAP = new TypeReference<List<Map<String, Object>>>() {\n    };\n\n    private final ConcordObjectMapper objectMapper;\n\n    @Inject\n    public ProcessWaitDao(@MainDB Configuration cfg, ConcordObjectMapper objectMapper) {\n        super(cfg);\n        this.objectMapper = objectMapper;\n    }\n\n    @Override\n    public void tx(Tx t) {\n        super.tx(t);\n    }\n\n    @Override\n    public <T> T txResult(TxResult<T> t) {\n        return super.txResult(t);\n    }\n\n    public void addWait(DSLContext tx, ProcessKey processKey, AbstractWaitCondition wait) {\n        tx.update(PROCESS_WAIT_CONDITIONS)\n                .set(PROCESS_WAIT_CONDITIONS.VERSION, PROCESS_WAIT_CONDITIONS.VERSION.plus(1))\n                .set(PROCESS_WAIT_CONDITIONS.WAIT_CONDITIONS,\n                        jsonbAppend(jsonbOrEmptyArray(PROCESS_WAIT_CONDITIONS.WAIT_CONDITIONS), objectMapper.toJSONB(wait)))\n                .where(PROCESS_WAIT_CONDITIONS.INSTANCE_ID.eq(processKey.getInstanceId())\n                        .and(PROCESS_WAIT_CONDITIONS.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt())))\n                .execute();\n    }\n\n    public boolean setWait(DSLContext tx, ProcessKey processKey, List<AbstractWaitCondition> waits, boolean isWaiting, long version) {\n        if (waits != null && waits.isEmpty()) {\n            waits = null;\n        }\n\n        int rows = tx.update(PROCESS_WAIT_CONDITIONS)\n                .set(PROCESS_WAIT_CONDITIONS.WAIT_CONDITIONS, field(\"?::jsonb\", JSONB.class, objectMapper.toJSONB(waits, WAIT_LIST)))\n                .set(PROCESS_WAIT_CONDITIONS.IS_WAITING, isWaiting)\n                .set(PROCESS_WAIT_CONDITIONS.VERSION, PROCESS_WAIT_CONDITIONS.VERSION.plus(1))\n                .where(PROCESS_WAIT_CONDITIONS.INSTANCE_ID.eq(processKey.getInstanceId())\n                        .and(PROCESS_WAIT_CONDITIONS.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt())\n                                .and(PROCESS_WAIT_CONDITIONS.VERSION.eq(version))))\n                .execute();\n\n        return rows > 0;\n    }\n\n    public ProcessWaitEntry get(ProcessKey processKey) {\n        return txResult(tx -> tx.select(PROCESS_WAIT_CONDITIONS.IS_WAITING, PROCESS_WAIT_CONDITIONS.WAIT_CONDITIONS)\n                .from(PROCESS_WAIT_CONDITIONS)\n                .where(PROCESS_WAIT_CONDITIONS.INSTANCE_ID.eq(processKey.getInstanceId())\n                        .and(PROCESS_WAIT_CONDITIONS.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt())))\n                .fetchOne(r -> ProcessWaitEntry.of(r.value1(), objectMapper.fromJSONB(r.value2(), LIST_OF_MAP))));\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/waits/ProcessWaitHandler.java",
    "content": "package com.walmartlabs.concord.server.process.waits;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport org.immutables.value.Value;\nimport org.jooq.DSLContext;\n\nimport javax.annotation.Nullable;\nimport java.util.List;\n\npublic interface ProcessWaitHandler<T extends AbstractWaitCondition> {\n\n    WaitType getType();\n\n    List<Result<T>> processBatch(List<WaitConditionItem<T>> waits);\n\n    @Value.Immutable\n    interface WaitConditionItem<T extends AbstractWaitCondition> {\n\n        @Value.Parameter\n        ProcessKey processKey();\n\n        @Value.Parameter\n        int waitConditionId();\n\n        @Value.Parameter\n        T waitCondition();\n\n        static <T extends AbstractWaitCondition> WaitConditionItem<T> of(ProcessKey processKey, int waitConditionId, T waitCondition) {\n            return ImmutableWaitConditionItem.of(processKey, waitConditionId, waitCondition);\n        }\n    }\n\n    @Value.Immutable\n    interface Result<T extends AbstractWaitCondition> {\n\n        @Value.Parameter\n        ProcessKey processKey();\n\n        @Value.Parameter\n        int waitConditionId();\n\n        /**\n         * return null if the process doesn't have any wait conditions.\n         */\n        @Value.Parameter\n        @Nullable\n        T waitCondition();\n\n        /**\n         * resume event for resume process.\n         */\n        @Value.Parameter\n        @Nullable\n        String resumeEvent();\n\n        @Value.Parameter\n        @Nullable\n        Action action();\n\n        static <T extends AbstractWaitCondition> Result<T> of(ProcessKey processKey, int waitConditionId, T waitCondition) {\n            return ImmutableResult.of(processKey, waitConditionId, waitCondition, null, null);\n        }\n\n        static <T extends AbstractWaitCondition> Result<T> resume(WaitConditionItem<?> wait, String resumeEvent) {\n            return ImmutableResult.of(wait.processKey(), wait.waitConditionId(), null, resumeEvent, null);\n        }\n\n        static <T extends AbstractWaitCondition> Result<T> resume(ProcessKey processKey, int waitConditionId, String resumeEvent) {\n            return ImmutableResult.of(processKey, waitConditionId, null, resumeEvent, null);\n        }\n\n        static <T extends AbstractWaitCondition> Result<T> action(WaitConditionItem<?> wait, Action action) {\n            return ImmutableResult.of(wait.processKey(), wait.waitConditionId(), null, null, action);\n        }\n    }\n\n    interface Action {\n\n        void execute(DSLContext tx);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/waits/ProcessWaitManager.java",
    "content": "package com.walmartlabs.concord.server.process.waits;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.sdk.EventType;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.process.ProcessEntry.ProcessWaitEntry;\nimport com.walmartlabs.concord.server.process.event.NewProcessEvent;\nimport com.walmartlabs.concord.server.process.event.ProcessEventManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport org.jooq.DSLContext;\n\nimport javax.inject.Inject;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.stream.Collectors;\n\npublic class ProcessWaitManager {\n\n    private final ProcessWaitDao processWaitDao;\n    private final ConcordObjectMapper objectMapper;\n    private final ProcessEventManager eventManager;\n\n    @Inject\n    public ProcessWaitManager(ProcessWaitDao processWaitDao, ConcordObjectMapper objectMapper, ProcessEventManager eventManager) {\n        this.processWaitDao = processWaitDao;\n        this.objectMapper = objectMapper;\n        this.eventManager = eventManager;\n    }\n\n    public void tx(AbstractDao.Tx t) {\n        processWaitDao.tx(t);\n    }\n\n    public <T> T txResult(AbstractDao.TxResult<T> t) {\n        return processWaitDao.txResult(t);\n    }\n\n    public ProcessWaitEntry getWait(ProcessKey processKey) {\n        return processWaitDao.get(processKey);\n    }\n\n    /**\n     * @see #addWait(DSLContext, ProcessKey, AbstractWaitCondition)\n     */\n    public void addWait(ProcessKey processKey, AbstractWaitCondition wait) {\n        processWaitDao.tx(tx -> addWait(tx, processKey, wait));\n    }\n\n    /**\n     * Add the process' wait conditions. Adds a wait condition history event.\n     */\n    public void addWait(DSLContext tx, ProcessKey processKey, AbstractWaitCondition wait) {\n        processWaitDao.addWait(tx, processKey, wait);\n\n        eventManager.event(tx, Collections.singletonList(buildEvent(processKey, Collections.singletonList(wait), \"add\")));\n    }\n\n    /**\n     * Set the process' wait conditions. Adds a wait condition history event.\n     */\n    public boolean setWait(DSLContext tx, ProcessKey processKey, List<AbstractWaitCondition> waits, boolean isWaiting, long version) {\n        boolean updated = processWaitDao.setWait(tx, processKey, waits, isWaiting, version);\n        if (updated) {\n            eventManager.event(tx, buildEvent(processKey, waits, \"set\"));\n        }\n        return updated;\n    }\n\n    private NewProcessEvent buildEvent(ProcessKey processKey, List<AbstractWaitCondition> waits, String action) {\n        Map<String, Object> data = new HashMap<>();\n        if (waits != null && !waits.isEmpty()) {\n            data.put(\"waits\", waits.stream()\n                    .map(objectMapper::convertToMap)\n                    .collect(Collectors.toList()));\n        }\n        data.put(\"action\", action);\n        return NewProcessEvent.builder()\n                .processKey(processKey)\n                .eventType(EventType.PROCESS_WAIT.name())\n                .data(data)\n                .build();\n    }\n\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/waits/ProcessWaitWatchdog.java",
    "content": "package com.walmartlabs.concord.server.process.waits;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.Histogram;\nimport com.codahale.metrics.MetricRegistry;\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.cfg.ProcessWaitWatchdogConfiguration;\nimport com.walmartlabs.concord.server.jooq.tables.ProcessWaitConditions;\nimport com.walmartlabs.concord.server.process.*;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ScheduledTask;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport org.immutables.value.Value;\nimport org.jooq.Configuration;\nimport org.jooq.JSONB;\nimport org.jooq.Record5;\nimport org.jooq.SelectConditionStep;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.core.Response;\nimport java.io.IOException;\nimport java.time.OffsetDateTime;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.PROCESS_WAIT_CONDITIONS;\nimport static com.walmartlabs.concord.server.process.waits.ProcessWaitHandler.WaitConditionItem;\nimport static com.walmartlabs.concord.server.process.waits.ProcessWaitHandler.Result;\nimport static com.walmartlabs.concord.server.process.waits.ProcessWaitHandler.Action;\n\n/**\n * Takes care of processes with wait conditions.\n * E.g. waiting for other processes to finish, locking, etc.\n */\npublic class ProcessWaitWatchdog implements ScheduledTask {\n\n    private static final Logger log = LoggerFactory.getLogger(ProcessWaitWatchdog.class);\n\n    private final ProcessWaitWatchdogConfiguration cfg;\n    private final WatchdogDao dao;\n    private final ProcessWaitManager processWaitManager;\n    private final ProcessManager processManager;\n    private final PayloadManager payloadManager;\n    private final Map<WaitType, ProcessWaitHandler<AbstractWaitCondition>> processWaitHandlers;\n    private final Histogram waitItemsHistogram;\n\n    @Inject\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    public ProcessWaitWatchdog(ProcessWaitWatchdogConfiguration cfg,\n                               @MainDB Configuration dbCfg,\n                               ConcordObjectMapper objectMapper,\n                               ProcessWaitManager processWaitManager,\n                               ProcessManager processManager,\n                               PayloadManager payloadManager,\n                               Set<ProcessWaitHandler> handlers,\n                               MetricRegistry metricRegistry) {\n\n        this.cfg = cfg;\n        this.dao = new WatchdogDao(dbCfg, objectMapper);\n        this.processWaitManager = processWaitManager;\n        this.processManager = processManager;\n        this.payloadManager = payloadManager;\n        this.processWaitHandlers = new EnumMap<>(WaitType.class);\n\n        handlers.forEach(h -> this.processWaitHandlers.put(h.getType(), h));\n        this.waitItemsHistogram = metricRegistry.histogram(\"process-wait-watchdog-items\");\n    }\n\n    @Override\n    public String getId() {\n        return \"process-wait-watchdog\";\n    }\n\n    @Override\n    public long getIntervalInSec() {\n        return cfg.getPeriod().getSeconds();\n    }\n\n    @Override\n    @WithTimer\n    public void performTask() {\n        Long lastId = null;\n        while (true) {\n            List<WaitingProcess> processes = dao.nextWaitItems(lastId, cfg.getPollLimit());\n            waitItemsHistogram.update(processes.size());\n\n            if (processes.isEmpty()) {\n                return;\n            }\n\n            processWaits(processes);\n\n            lastId = processes.get(processes.size() - 1).id();\n\n            if (processes.size() < cfg.getPollLimit()) {\n                return;\n            }\n        }\n    }\n\n    @WithTimer\n    void processWaits(List<WaitingProcess> processes) {\n        Map<WaitType, List<WaitConditionItem<AbstractWaitCondition>>> batches = toBatches(processes);\n\n        Map<UUID, List<Result<AbstractWaitCondition>>> results = new HashMap<>();\n        for (var e : batches.entrySet()) {\n            List<Result<AbstractWaitCondition>> batchResult = processBatch(e.getKey(), e.getValue());\n            for (var r : batchResult) {\n                var resultsForProcess = results.computeIfAbsent(r.processKey().getInstanceId(), k -> new ArrayList<>());\n                resultsForProcess.add(r);\n            }\n        }\n\n        for (WaitingProcess p : processes) {\n            List<Result<AbstractWaitCondition>> resultForProcess = results.get(p.processKey().getInstanceId());\n            if (resultForProcess == null) {\n                continue;\n            }\n\n            Set<String> resumeEvents = resultForProcess.stream()\n                    .map(Result::resumeEvent)\n                    .filter(Objects::nonNull)\n                    .collect(Collectors.toSet());\n            List<Action> resultActions = resultForProcess.stream()\n                    .map(Result::action)\n                    .filter(Objects::nonNull)\n                    .toList();\n            List<AbstractWaitCondition> resultWaits = buildWaitConditions(p.waits(), resultForProcess);\n\n            if (p.waits().equals(resultWaits)) {\n                continue;\n            }\n\n            try {\n                boolean updated = processWaitManager.txResult(tx -> {\n                    // TODO: better way\n                    // Right now, we have only one result action, and it moves the process to enqueued status.\n                    // If the process moves to enqueued, it means it's no longer waiting for anything, so isWaiting should be false\n                    boolean isWaiting = !resultWaits.isEmpty() && resumeEvents.isEmpty() && resultActions.isEmpty();\n                    boolean up = processWaitManager.setWait(tx, p.processKey(), resultWaits, isWaiting, p.version());\n                    if (up) {\n                        resultActions.forEach(a -> a.execute(tx));\n                    }\n                    return up;\n                });\n\n                if (updated && !resumeEvents.isEmpty()) {\n                    log.info(\"processWaits ['{}', '{}', {}] -> resume\", p.processKey(), resultWaits, p.version());\n                    resumeProcess(p.processKey(), resumeEvents);\n                }\n            } catch (Exception e) {\n                log.info(\"processWaits ['{}'] -> error\", p, e);\n            }\n        }\n    }\n\n    @WithTimer\n    List<Result<AbstractWaitCondition>> processBatch(WaitType type, List<WaitConditionItem<AbstractWaitCondition>> waitConditions) {\n        ProcessWaitHandler<AbstractWaitCondition> handler = processWaitHandlers.get(type);\n        if (handler == null) {\n            log.warn(\"processBatch ['{}'] -> handler not found\", type);\n            return waitConditions.stream()\n                    .map(w -> Result.of(w.processKey(), w.waitConditionId(), null))\n                    .collect(Collectors.toList());\n        }\n\n        try {\n            return handler.processBatch(waitConditions);\n        } catch (Exception e) {\n            log.info(\"processHandler ['{}'] -> error\",type, e);\n            return List.of();\n        }\n    }\n\n    @WithTimer\n    void resumeProcess(ProcessKey key, Set<String> events) {\n        Payload payload;\n        try {\n            payload = payloadManager.createResumePayload(key, events, null);\n        } catch (ProcessException e) {\n            if (e.getStatus() == Response.Status.NOT_FOUND) {\n                log.info(\"resumeProcess ['{}'] -> can't resume: {}. killing process\", key, e.getMessage());\n                processManager.kill(key);\n                return;\n            }\n            throw e;\n        } catch (IOException e) {\n            throw new RuntimeException(\"Error creating a payload\", e);\n        }\n\n        processManager.resume(payload);\n        log.info(\"resumeProcess ['{}', '{}'] -> done\", key, events);\n    }\n\n    private static Map<WaitType, List<WaitConditionItem<AbstractWaitCondition>>> toBatches(List<WaitingProcess> processes) {\n        Map<WaitType, List<WaitConditionItem<AbstractWaitCondition>>> result = new HashMap<>();\n\n        for (WaitingProcess p : processes) {\n            boolean hasExclusive = p.waits().stream().anyMatch(AbstractWaitCondition::exclusive);\n            List<AbstractWaitCondition> waits = p.waits();\n            for (int i = 0; i < waits.size(); i++) {\n                AbstractWaitCondition w = waits.get(i);\n                if (!hasExclusive || w.exclusive()) {\n                    var r = result.computeIfAbsent(w.type(), k -> new ArrayList<>());\n                    r.add(WaitConditionItem.of(p.processKey(), i, w));\n                }\n            }\n        }\n        return result;\n    }\n\n    private static List<AbstractWaitCondition> buildWaitConditions(List<AbstractWaitCondition> originalWaits,\n                                                                   List<Result<AbstractWaitCondition>> newWaits) {\n\n        List<AbstractWaitCondition> result = new ArrayList<>(originalWaits);\n        for (Result<AbstractWaitCondition> w : newWaits) {\n            result.set(w.waitConditionId(), w.waitCondition());\n        }\n        return result.stream()\n                .filter(Objects::nonNull)\n                .collect(Collectors.toList());\n    }\n\n    @Value.Immutable\n    interface WaitingProcess {\n\n        ProcessKey processKey();\n\n        long id();\n\n        List<AbstractWaitCondition> waits();\n\n        long version();\n\n        static ImmutableWaitingProcess.Builder builder() {\n            return ImmutableWaitingProcess.builder();\n        }\n    }\n\n    private static class WatchdogDao extends AbstractDao {\n\n        private static final TypeReference<List<AbstractWaitCondition>> WAIT_LIST = new TypeReference<List<AbstractWaitCondition>>() {\n        };\n\n        private final ConcordObjectMapper objectMapper;\n\n        private WatchdogDao(@MainDB Configuration cfg, ConcordObjectMapper objectMapper) {\n            super(cfg);\n\n            this.objectMapper = objectMapper;\n        }\n\n        @WithTimer\n        public List<WaitingProcess> nextWaitItems(Long lastId, int pollLimit) {\n            return txResult(tx -> {\n                ProcessWaitConditions w = PROCESS_WAIT_CONDITIONS.as(\"w\");\n\n                SelectConditionStep<Record5<UUID, OffsetDateTime, Long, JSONB, Long>> s = tx.select(\n                                w.INSTANCE_ID,\n                                w.INSTANCE_CREATED_AT,\n                                w.ID_SEQ,\n                                w.WAIT_CONDITIONS,\n                                w.VERSION)\n                        .from(w)\n                        .where(w.IS_WAITING.eq(true));\n\n                if (lastId != null) {\n                    s.and(w.ID_SEQ.greaterThan(lastId));\n                }\n\n                return s.orderBy(w.ID_SEQ)\n                        .limit(pollLimit)\n                        .fetch(r -> WaitingProcess.builder()\n                                .processKey(new ProcessKey(r.value1(), r.value2()))\n                                .id(r.value3())\n                                .waits(objectMapper.fromJSONB(r.value4(), WAIT_LIST))\n                                .version(r.value5())\n                                .build());\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/waits/WaitConditionUpdater.java",
    "content": "package com.walmartlabs.concord.server.process.waits;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.queue.ProcessStatusListener;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport org.jooq.DSLContext;\n\npublic class WaitConditionUpdater implements ProcessStatusListener {\n\n    @Override\n    public void onStatusChange(DSLContext tx, ProcessKey processKey, ProcessStatus status) {\n        switch (status) {\n            case SUSPENDED:\n            case FINISHED:\n            case FAILED:\n            case CANCELLED:\n            case TIMED_OUT:\n                updateWaitConditions(tx, processKey, status);\n                break;\n            default:\n                // do nothing\n        }\n    }\n\n    private static void updateWaitConditions(DSLContext tx, ProcessKey processKey, ProcessStatus status) {\n        String sql = \"\"\"\n            UPDATE process_wait_conditions\n            SET wait_conditions =\n                    (SELECT jsonb_agg(\n                        CASE\n                            WHEN obj->>'type' = 'PROCESS_COMPLETION' and obj->>'completeCondition' = 'ALL' and obj->>'processes' is not null\n                                THEN jsonb_set(obj, '{processes}', (obj->'processes') - ?)\n                            WHEN obj->>'type' = 'PROCESS_COMPLETION' and obj->>'completeCondition' = 'ONE_OF' and obj->>'processes' is not null and obj->'processes' ?? ?\n                                THEN jsonb_set(obj, '{processes}', '[]')\n                            ELSE obj\n                        END\n                     )\n                     FROM jsonb_array_elements(wait_conditions) obj),\n                version = version + 1\n            WHERE wait_conditions @> ?::jsonb;\n            \"\"\";\n\n        String jsonMatch = String.format(\"[{\\\"type\\\": \\\"PROCESS_COMPLETION\\\", \\\"finalStatuses\\\": [\\\"%s\\\"],  \\\"processes\\\": [\\\"%s\\\"]}]\", status.name(), processKey.getInstanceId().toString());\n\n        tx.execute(sql, processKey.getInstanceId().toString(), processKey.getInstanceId().toString(), jsonMatch);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/waits/WaitProcessFinishHandler.java",
    "content": "package com.walmartlabs.concord.server.process.waits;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.Histogram;\nimport com.codahale.metrics.MetricRegistry;\nimport com.walmartlabs.concord.db.AbstractDao;\n\nimport com.walmartlabs.concord.server.cfg.ProcessWaitWatchdogConfiguration;\nimport com.walmartlabs.concord.server.jooq.tables.ProcessQueue;\n\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueManager;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport org.jooq.Configuration;\nimport org.jooq.impl.EnumConverter;\n\nimport javax.inject.Inject;\nimport javax.inject.Singleton;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.jooq.tables.ProcessQueue.PROCESS_QUEUE;\nimport static com.walmartlabs.concord.server.process.waits.ProcessCompletionCondition.CompleteCondition;\nimport static org.jooq.impl.DSL.any;\n\n/**\n * Handles the processes that are waiting for other processes to finish.\n */\n@Singleton\npublic class WaitProcessFinishHandler implements ProcessWaitHandler<ProcessCompletionCondition> {\n\n    private final Dao dao;\n\n    private final int processLimitForStatusQuery;\n\n    private final ProcessQueueManager processQueueManager;\n\n    private final Histogram waitProcessesHistogram;\n\n\n    @Inject\n    public WaitProcessFinishHandler(Dao dao,\n                                    ProcessWaitWatchdogConfiguration cfg,\n                                    ProcessQueueManager processQueueManager,\n                                    MetricRegistry metricRegistry) {\n        this.dao = dao;\n        this.processLimitForStatusQuery = cfg.getProcessLimitForStatusQuery();\n        this.processQueueManager = processQueueManager;\n        this.waitProcessesHistogram = metricRegistry.histogram(\"wait-process-finish-histogram\");\n    }\n\n    @Override\n    public WaitType getType() {\n        return WaitType.PROCESS_COMPLETION;\n    }\n\n    @Override\n    @WithTimer\n    public List<Result<ProcessCompletionCondition>> processBatch(List<WaitConditionItem<ProcessCompletionCondition>> items) {\n        Set<UUID> allProcesses = items.stream()\n                .flatMap(i -> i.waitCondition().processes().stream())\n                .collect(Collectors.toSet());\n\n        waitProcessesHistogram.update(allProcesses.size());\n\n        List<WaitConditionItem<ProcessCompletionCondition>> modifiableItems = new ArrayList<>();\n        for (WaitConditionItem<ProcessCompletionCondition> item : items) {\n            modifiableItems.add(toModifiable(item));\n        }\n\n        List<Set<UUID>> processesBatch = split(allProcesses, processLimitForStatusQuery);\n        List<Result<ProcessCompletionCondition>> results = new ArrayList<>();\n        for (Set<UUID> processes : processesBatch) {\n            Map<UUID, ProcessStatus> statuses = dao.findStatuses(processes);\n\n            for (var it = modifiableItems.iterator(); it.hasNext(); ) {\n                WaitConditionItem<ProcessCompletionCondition> item = it.next();\n\n                filterFinished(item.waitCondition(), statuses);\n                if (item.waitCondition().processes().isEmpty()) {\n                    results.add(toResult(item));\n                    it.remove();\n                }\n            }\n\n            if (modifiableItems.isEmpty()) {\n                break;\n            }\n        }\n\n        for (WaitConditionItem<ProcessCompletionCondition> item : modifiableItems) {\n            results.add(toResult(item));\n        }\n\n        return results;\n    }\n\n    private Result<ProcessCompletionCondition> toResult(WaitConditionItem<ProcessCompletionCondition> item) {\n        Set<UUID> newAwaitProcesses = item.waitCondition().processes();\n        if (newAwaitProcesses.isEmpty()) {\n            if (item.waitCondition().resumeEvent() != null) {\n                return Result.resume(item, item.waitCondition().resumeEvent());\n            } else {\n                return Result.action(item, tx -> processQueueManager.updateExpectedStatus(tx, item.processKey(), ProcessStatus.WAITING, ProcessStatus.ENQUEUED));\n            }\n        }\n\n        return Result.of(item.processKey(), item.waitConditionId(),\n                ProcessCompletionCondition.builder().from(item.waitCondition())\n                        .processes(newAwaitProcesses)\n                        .build());\n    }\n\n    private static WaitConditionItem<ProcessCompletionCondition> toModifiable(WaitConditionItem<ProcessCompletionCondition> item) {\n        return WaitConditionItem.of(item.processKey(), item.waitConditionId(), ModifiableProcessCompletionCondition.create().from(item.waitCondition()));\n    }\n\n    private static List<Set<UUID>> split(Set<UUID> processes, int maxProcessesItems) {\n        List<Set<UUID>> subsets = new ArrayList<>();\n        Set<UUID> currentSet = new HashSet<>();\n\n        for (UUID element : processes) {\n            currentSet.add(element);\n            if (currentSet.size() >= maxProcessesItems) {\n                subsets.add(new HashSet<>(currentSet));\n                currentSet.clear();\n            }\n        }\n\n        if (!currentSet.isEmpty()) {\n            subsets.add(currentSet);\n        }\n\n        return subsets;\n    }\n\n    private static void filterFinished(ProcessCompletionCondition condition, Map<UUID, ProcessStatus> statuses) {\n        if (condition.processes() == null || condition.processes().isEmpty()) {\n            return;\n        }\n\n        Iterator<UUID> it = condition.processes().iterator();\n        while (it.hasNext()) {\n            UUID proc = it.next();\n            if (statuses.containsKey(proc)) {\n                ProcessStatus status = statuses.get(proc);\n                if (status == null || condition.finalStatuses().contains(status)) {\n                    if (condition.completeCondition() == CompleteCondition.ALL) {\n                        it.remove();\n                    } else if (condition.completeCondition() == CompleteCondition.ONE_OF) {\n                        condition.processes().clear();\n                        return;\n                    } else {\n                        throw new RuntimeException(\"Unknown wait complete condition: \" + condition.completeCondition());\n                    }\n                }\n            }\n        }\n    }\n\n    public static class Dao extends AbstractDao {\n\n        @Inject\n        private Dao(@MainDB Configuration cfg) {\n            super(cfg);\n        }\n\n        public Map<UUID, ProcessStatus> findStatuses(Set<UUID> processes) {\n            Map<UUID, ProcessStatus> result = txResult(tx -> {\n                ProcessQueue q = PROCESS_QUEUE.as(\"q\");\n                return tx.select(q.INSTANCE_ID, q.CURRENT_STATUS)\n                        .from(q)\n                        .where(q.INSTANCE_ID.equal(any(processes.toArray(UUID[]::new))))\n                        .fetchMap(q.INSTANCE_ID, r -> r.get(q.CURRENT_STATUS, new EnumConverter<>(String.class, ProcessStatus.class)));\n            });\n            for (UUID key : processes) {\n                result.putIfAbsent(key, null);\n            }\n            return result;\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/waits/WaitProcessLockHandler.java",
    "content": "package com.walmartlabs.concord.server.process.waits;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.locks.LockEntry;\nimport com.walmartlabs.concord.server.process.locks.ProcessLocksDao;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\n\nimport javax.inject.Inject;\nimport javax.inject.Singleton;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * Handles the processes that are waiting for locks. Resumes a suspended process\n * if the lock was acquired successfully.\n */\n@Singleton\npublic class WaitProcessLockHandler implements ProcessWaitHandler<ProcessLockCondition> {\n\n    private final ProcessLocksDao locksDao;\n\n    @Inject\n    public WaitProcessLockHandler(ProcessLocksDao locksDao) {\n        this.locksDao = locksDao;\n    }\n\n    @Override\n    public WaitType getType() {\n        return WaitType.PROCESS_LOCK;\n    }\n\n    @Override\n    @WithTimer\n    public List<Result<ProcessLockCondition>> processBatch(List<WaitConditionItem<ProcessLockCondition>> waits) {\n        return waits.stream()\n                .map(w -> process(w.processKey(), w.waitConditionId(), w.waitCondition()))\n                .collect(Collectors.toList());\n    }\n\n    @WithTimer\n    public Result<ProcessLockCondition> process(ProcessKey key, int waitConditionId, ProcessLockCondition wait) {\n        LockEntry lock = locksDao.tryLock(key, wait.orgId(), wait.projectId(), wait.scope(), wait.name());\n        if (lock.instanceId().equals(key.getInstanceId())) {\n            return Result.resume(key, waitConditionId, wait.name());\n        }\n\n        return Result.of(key, waitConditionId, ProcessLockCondition.from(lock));\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/waits/WaitProcessSleepHandler.java",
    "content": "package com.walmartlabs.concord.server.process.waits;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\n\nimport javax.inject.Singleton;\nimport java.util.Date;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * Handles the processes that are waiting for some timeout. Resumes a suspended process\n * if the timeout exceeded.\n */\n@Singleton\npublic class WaitProcessSleepHandler implements ProcessWaitHandler<ProcessSleepCondition> {\n\n    @Override\n    public WaitType getType() {\n        return WaitType.PROCESS_SLEEP;\n    }\n\n    @Override\n    public List<Result<ProcessSleepCondition>> processBatch(List<WaitConditionItem<ProcessSleepCondition>> waits) {\n        return waits.stream()\n                .map(w -> process(w.processKey(), w.waitConditionId(), w.waitCondition()))\n                .collect(Collectors.toList());\n    }\n\n    @WithTimer\n    public Result<ProcessSleepCondition> process(ProcessKey key, int waitConditionId, ProcessSleepCondition wait) {\n        if (wait.until().before(new Date())) {\n            return Result.resume(key, waitConditionId, wait.resumeEvent());\n        }\n\n        return Result.of(key, waitConditionId, wait);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/waits/WaitProcessStatusListener.java",
    "content": "package com.walmartlabs.concord.server.process.waits;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.queue.ProcessStatusListener;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport org.jooq.DSLContext;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.PROCESS_WAIT_CONDITIONS;\nimport static org.jooq.impl.DSL.*;\n\npublic class WaitProcessStatusListener implements ProcessStatusListener {\n\n    @Override\n    public void onStatusChange(DSLContext tx, ProcessKey processKey, ProcessStatus status) {\n        switch (status) {\n            case NEW:\n            case PREPARING:\n                init(tx, processKey);\n                break;\n            case WAITING:\n            case SUSPENDED:\n                waiting(tx, processKey);\n                break;\n            case FINISHED:\n            case FAILED:\n            case CANCELLED:\n            case TIMED_OUT:\n                clear(tx, processKey);\n                break;\n            default:\n                // do nothing\n        }\n    }\n\n    private static void init(DSLContext tx, ProcessKey processKey) {\n        tx.insertInto(PROCESS_WAIT_CONDITIONS, PROCESS_WAIT_CONDITIONS.INSTANCE_ID, PROCESS_WAIT_CONDITIONS.INSTANCE_CREATED_AT)\n                .select(\n                        select(value(processKey.getInstanceId()), value(processKey.getCreatedAt()))\n                                .whereNotExists(\n                                        selectOne()\n                                                .from(PROCESS_WAIT_CONDITIONS)\n                                                .where(PROCESS_WAIT_CONDITIONS.INSTANCE_ID.eq(processKey.getInstanceId())\n                                                        .and(PROCESS_WAIT_CONDITIONS.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt())))\n                                )\n                )\n                .execute();\n    }\n\n    private static void waiting(DSLContext tx, ProcessKey processKey) {\n        tx.update(PROCESS_WAIT_CONDITIONS)\n                .set(PROCESS_WAIT_CONDITIONS.IS_WAITING, true)\n                .set(PROCESS_WAIT_CONDITIONS.VERSION, PROCESS_WAIT_CONDITIONS.VERSION.plus(1))\n                .where(PROCESS_WAIT_CONDITIONS.INSTANCE_ID.eq(processKey.getInstanceId())\n                        .and(PROCESS_WAIT_CONDITIONS.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt())\n                                .and(PROCESS_WAIT_CONDITIONS.WAIT_CONDITIONS.isNotNull())))\n                .execute();\n    }\n\n    private static void clear(DSLContext tx, ProcessKey processKey) {\n        tx.update(PROCESS_WAIT_CONDITIONS)\n                .set(PROCESS_WAIT_CONDITIONS.IS_WAITING, false)\n                .setNull(PROCESS_WAIT_CONDITIONS.WAIT_CONDITIONS)\n                .set(PROCESS_WAIT_CONDITIONS.VERSION, PROCESS_WAIT_CONDITIONS.VERSION.plus(1))\n                .where(PROCESS_WAIT_CONDITIONS.INSTANCE_ID.eq(processKey.getInstanceId())\n                        .and(PROCESS_WAIT_CONDITIONS.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt())))\n                .execute();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/process/waits/WaitType.java",
    "content": "package com.walmartlabs.concord.server.process.waits;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic enum WaitType {\n\n    NONE,\n\n    /**\n     * Waiting for a specific process to complete.\n      */\n    PROCESS_COMPLETION,\n\n    /**\n     * Waiting for a (named) lock taken by another process.\n     */\n    PROCESS_LOCK,\n\n    /**\n     * Waiting for a specific timeout to resume process.\n     */\n    PROCESS_SLEEP\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/repository/ClasspathRepositoryProvider.java",
    "content": "package com.walmartlabs.concord.server.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.io.Resources;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.repository.*;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.net.URL;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.nio.file.StandardOpenOption;\nimport java.util.List;\n\npublic class ClasspathRepositoryProvider implements RepositoryProvider {\n\n    private static final String URL_PREFIX = \"classpath://\";\n\n    @Override\n    public boolean canHandle(String url) {\n        return url.startsWith(URL_PREFIX);\n    }\n\n    @Override\n    public FetchResult fetch(FetchRequest request) {\n        String repoUrl = request.url();\n        Path dst = request.destination();\n        URL resUrl = Resources.getResource(normalizeUrl(repoUrl));\n\n        try {\n            if (!Files.exists(dst)) {\n                Files.createDirectories(dst);\n            }\n\n            String fileName = repoUrl.substring(repoUrl.lastIndexOf('/') + 1);\n            Path dstFile = dst.resolve(fileName);\n\n            try (OutputStream out = Files.newOutputStream(dstFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {\n                Resources.copy(resUrl, out);\n            }\n            return null;\n        } catch (IOException e) {\n            throw new RepositoryException(\"Error while fetching a repository\", e);\n        }\n    }\n\n    @Override\n    public Snapshot export(Path src, Path dst, List<String> ignorePatterns) throws IOException {\n        PathUtils.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);\n        return Snapshot.includeAll();\n    }\n\n    private static String normalizeUrl(String url) {\n        return url.substring(URL_PREFIX.length());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/repository/InvalidRepositoryPathException.java",
    "content": "package com.walmartlabs.concord.server.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic class InvalidRepositoryPathException extends RuntimeException {\n\n    private static final long serialVersionUID = 1L;\n\n    public InvalidRepositoryPathException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/repository/RepositoryCacheCleanupTask.java",
    "content": "package com.walmartlabs.concord.server.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.PeriodicTask;\n\nimport javax.inject.Inject;\nimport java.util.concurrent.TimeUnit;\n\npublic class RepositoryCacheCleanupTask extends PeriodicTask {\n\n    private static final long ERROR_DELAY = TimeUnit.SECONDS.toMillis(30);\n\n    private final RepositoryManager repositoryManager;\n\n    @Inject\n    public RepositoryCacheCleanupTask(RepositoryManager repositoryManager) {\n        super(repositoryManager.cleanupInterval(), ERROR_DELAY);\n        this.repositoryManager = repositoryManager;\n    }\n\n    @Override\n    protected boolean performTask() throws Exception {\n        repositoryManager.cleanup();\n        return false;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/repository/RepositoryManager.java",
    "content": "package com.walmartlabs.concord.server.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.common.AuthTokenProvider;\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.dependencymanager.DependencyManager;\nimport com.walmartlabs.concord.repository.*;\nimport com.walmartlabs.concord.sdk.Secret;\nimport com.walmartlabs.concord.server.cfg.GitConfiguration;\nimport com.walmartlabs.concord.server.cfg.RepositoryConfiguration;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.org.project.RepositoryEntry;\nimport com.walmartlabs.concord.server.org.secret.SecretManager;\nimport com.walmartlabs.concord.server.org.secret.SecretManager.AccessScope;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.UUID;\nimport java.util.concurrent.Callable;\n\nimport static com.walmartlabs.concord.process.loader.ProjectLoaderUtils.isConcordFileExists;\n\npublic class RepositoryManager {\n\n    private static final Logger log = LoggerFactory.getLogger(RepositoryManager.class);\n\n    private final RepositoryProviders providers;\n    private final ProjectDao projectDao;\n    private final SecretManager secretManager;\n    private final RepositoryCache repositoryCache;\n    private final RepositoryConfiguration repoCfg;\n    private final GitConfiguration gitCfg;\n\n    @Inject\n    public RepositoryManager(ObjectMapper objectMapper,\n                             GitConfiguration gitCfg,\n                             RepositoryConfiguration repoCfg,\n                             ProjectDao projectDao,\n                             SecretManager secretManager,\n                             DependencyManager dependencyManager,\n                             AuthTokenProvider authProvider) throws IOException {\n\n        GitClientConfiguration gitCliCfg = GitClientConfiguration.builder()\n                .defaultOperationTimeout(gitCfg.getDefaultOperationTimeout())\n                .fetchTimeout(gitCfg.getFetchTimeout())\n                .httpLowSpeedLimit(gitCfg.getHttpLowSpeedLimit())\n                .httpLowSpeedTime(gitCfg.getHttpLowSpeedTime())\n                .sshTimeout(gitCfg.getSshTimeout())\n                .sshTimeoutRetryCount(gitCfg.getSshTimeoutRetryCount())\n                .maxGitCliOutputBytes(gitCfg.maxGitCliOutputBytes())\n                .allowedSchemes(new HashSet<>(gitCfg.getAllowedSchemes()))\n                .build();\n\n        this.gitCfg = gitCfg;\n        this.providers =\n                new RepositoryProviders(List.of(\n                        new ClasspathRepositoryProvider(),\n                        new MavenRepositoryProvider(dependencyManager),\n                        new GitCliRepositoryProvider(gitCliCfg, authProvider)\n                ));\n        this.secretManager = secretManager;\n        this.projectDao = projectDao;\n        this.repoCfg = repoCfg;\n\n        this.repositoryCache = new RepositoryCache(repoCfg.getCacheDir(),\n                repoCfg.getCacheInfoDir(),\n                repoCfg.getLockTimeout(),\n                repoCfg.getMaxAge(),\n                repoCfg.getLockCount(),\n                objectMapper);\n    }\n\n    public void testConnection(UUID orgId, UUID projectId, String uri, String branch, String commitId, String path, String secretName) {\n        Path tmpDir = null;\n        try {\n            tmpDir = PathUtils.createTempDir(\"repository\");\n\n            Secret secret = getSecret(orgId, projectId, secretName);\n\n            Repository repo = providers.fetch(\n                    FetchRequest.builder()\n                            .url(uri)\n                            .shallow(gitCfg.isShallowClone())\n                            .version(FetchRequest.Version.commitWithBranch(commitId, branch))\n                            .secret(secret)\n                            .destination(tmpDir)\n                            .build(), path);\n\n            if (repoCfg.isConcordFileValidationEnabled()) {\n                if (!isConcordFileExists(repo.path())) {\n                    throw new InvalidRepositoryPathException(\"Invalid repository path: `concord.yml` or `.concord.yml` is missing!\");\n                }\n            }\n        } catch (IOException e) {\n            log.error(\"testConnection ['{}', '{}', '{}', '{}', '{}'] -> error\", uri, branch, commitId, path, secretName, e);\n            throw new RepositoryException(\"Test connection error\", e);\n        } finally {\n            if (tmpDir != null) {\n                try {\n                    PathUtils.deleteRecursively(tmpDir);\n                } catch (IOException e) {\n                    log.warn(\"testConnection -> cleanup error: {}\", e.getMessage());\n                }\n            }\n        }\n    }\n\n    public Repository fetch(String url, FetchRequest.Version version, String path, Secret secret, boolean withCommitInfo) {\n        Path dest = repositoryCache.getPath(url);\n        return providers.fetch(\n                FetchRequest.builder()\n                        .url(url)\n                        .shallow(gitCfg.isShallowClone())\n                        .checkAlreadyFetched(gitCfg.isCheckAlreadyFetched())\n                        .version(version)\n                        .secret(secret)\n                        .destination(dest)\n                        .withCommitInfo(withCommitInfo)\n                        .build(), path);\n    }\n\n    public Repository fetch(String url, String branch, String commitId, String path, Secret secret, boolean withCommitInfo) {\n        String fetchedCommitId = commitId;\n        long start = System.currentTimeMillis();\n\n        Path dest = repositoryCache.getPath(url);\n        try {\n            Repository result = providers.fetch(\n                    FetchRequest.builder()\n                            .url(url)\n                            .shallow(gitCfg.isShallowClone())\n                            .checkAlreadyFetched(gitCfg.isCheckAlreadyFetched())\n                            .version(FetchRequest.Version.commitWithBranch(commitId, branch))\n                            .secret(secret)\n                            .destination(dest)\n                            .withCommitInfo(withCommitInfo)\n                            .build(), path);\n            fetchedCommitId = result.fetchResult() != null ? Objects.requireNonNull(result.fetchResult()).head() : null;\n            return result;\n        } finally {\n            log.info(\"fetch ['{}', '{}', '{}', '{}'] -> current commitId {}, done in {}ms\",\n                    url, branch, commitId, path, fetchedCommitId, (System.currentTimeMillis() - start));\n        }\n    }\n\n    public Repository fetch(UUID projectId, RepositoryEntry repository) {\n        return fetch(projectId, repository, false);\n    }\n\n    public Repository fetch(UUID projectId, RepositoryEntry repository, boolean withCommitInfo) {\n        UUID orgId = getOrgId(projectId);\n        Secret secret = getSecret(orgId, projectId, repository.getSecretName());\n        return fetch(repository.getUrl(), repository.getBranch(), repository.getCommitId(), repository.getPath(), secret, withCommitInfo);\n    }\n\n    public <T> T withLock(String repoUrl, Callable<T> f) {\n        long start = System.currentTimeMillis();\n        try {\n            return repositoryCache.withLock(repoUrl, f);\n        } finally {\n            log.info(\"withLock ['{}'] -> done in {}ms\", repoUrl, (System.currentTimeMillis() - start));\n        }\n    }\n\n    public void cleanup() {\n        repositoryCache.cleanup();\n    }\n\n    public long cleanupInterval() {\n        return repositoryCache.cleanupInterval();\n    }\n\n    private UUID getOrgId(UUID projectId) {\n        UUID orgId = projectDao.getOrgId(projectId);\n\n        if (orgId == null) {\n            log.warn(\"getOrgId ['{}'] -> can't determine the project's organization ID\", projectId);\n            return OrganizationManager.DEFAULT_ORG_ID;\n        }\n\n        return orgId;\n    }\n\n    private Secret getSecret(UUID orgId, UUID projectId, String secretName) {\n        if (secretName == null) {\n            return null;\n        }\n\n        SecretManager.DecryptedSecret s = secretManager.getSecret(AccessScope.project(projectId), orgId, secretName, null, null);\n        if (s == null) {\n            throw new RepositoryException(\"Secret not found: \" + secretName);\n        }\n\n        return s.getSecret();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/repository/RepositoryModule.java",
    "content": "package com.walmartlabs.concord.server.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.walmartlabs.concord.server.repository.listeners.ProcessDefinitionRefreshListener;\nimport com.walmartlabs.concord.server.repository.listeners.RepositoryRefreshListener;\nimport com.walmartlabs.concord.server.repository.listeners.TriggerRefreshListener;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\nimport static com.walmartlabs.concord.server.Utils.bindSingletonBackgroundTask;\n\npublic class RepositoryModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        bindSingletonBackgroundTask(binder, RepositoryCacheCleanupTask.class);\n\n        newSetBinder(binder, RepositoryRefreshListener.class).addBinding().to(ProcessDefinitionRefreshListener.class);\n        newSetBinder(binder, RepositoryRefreshListener.class).addBinding().to(TriggerRefreshListener.class);\n\n        binder.bind(RepositoryManager.class).in(SINGLETON);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/repository/RepositoryRefresher.java",
    "content": "package com.walmartlabs.concord.server.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.PathUtils;\nimport com.walmartlabs.concord.common.TemporaryPath;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.imports.ImportsListener;\nimport com.walmartlabs.concord.process.loader.DelegatingProjectLoader;\nimport com.walmartlabs.concord.process.loader.ImportsNormalizer;\nimport com.walmartlabs.concord.runtime.model.ProcessDefinition;\nimport com.walmartlabs.concord.repository.Repository;\nimport com.walmartlabs.concord.repository.RepositoryException;\nimport com.walmartlabs.concord.sdk.Secret;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.ResourceAccessLevel;\nimport com.walmartlabs.concord.server.org.project.*;\nimport com.walmartlabs.concord.server.org.secret.SecretManager;\nimport com.walmartlabs.concord.server.process.ImportsNormalizerFactory;\nimport com.walmartlabs.concord.server.repository.listeners.RepositoryRefreshListener;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\n\npublic class RepositoryRefresher extends AbstractDao {\n\n    private static final Logger log = LoggerFactory.getLogger(RepositoryRefresher.class);\n\n    private final Set<RepositoryRefreshListener> listeners;\n    private final OrganizationManager orgManager;\n    private final ProjectAccessManager projectAccessManager;\n    private final RepositoryManager repositoryManager;\n    private final RepositoryDao repositoryDao;\n    private final ProjectDao projectDao;\n    private final DelegatingProjectLoader projectLoader;\n    private final ImportsNormalizerFactory importsNormalizerFactory;\n    private final SecretManager secretManager;\n\n    @Inject\n    public RepositoryRefresher(@MainDB Configuration cfg,\n                               Set<RepositoryRefreshListener> listeners,\n                               OrganizationManager orgManager,\n                               ProjectAccessManager projectAccessManager,\n                               RepositoryManager repositoryManager,\n                               RepositoryDao repositoryDao,\n                               ProjectDao projectDao,\n                               DelegatingProjectLoader projectLoader,\n                               ImportsNormalizerFactory importsNormalizerFactory,\n                               SecretManager secretManager) {\n\n        super(cfg);\n\n        this.listeners = listeners;\n        this.orgManager = orgManager;\n        this.projectAccessManager = projectAccessManager;\n        this.repositoryManager = repositoryManager;\n        this.repositoryDao = repositoryDao;\n        this.projectDao = projectDao;\n        this.projectLoader = projectLoader;\n        this.importsNormalizerFactory = importsNormalizerFactory;\n        this.secretManager = secretManager;\n    }\n\n    public void refresh(List<UUID> repositoryIds) {\n        List<RepositoryEntry> repositories = repositoryIds.stream()\n                .map(repositoryDao::get)\n                .filter(Objects::nonNull)\n                .collect(Collectors.toList());\n\n        for (RepositoryEntry r : repositories) {\n            try {\n                refresh(r);\n            } catch (Exception e) {\n                log.warn(\"refresh ['{}'] -> error: {}\", r.getId(), e.getMessage());\n            }\n        }\n    }\n\n    private void refresh(RepositoryEntry r) {\n        ProjectEntry project = projectDao.get(r.getProjectId());\n        if (project == null) {\n            log.warn(\"refresh ['{}'] -> project not found\", r.getProjectId());\n            return;\n        }\n\n        refresh(project.getOrgName(), project.getName(), r.getName());\n    }\n\n    public void refresh(String orgName, String projectName, String repositoryName) {\n        UUID orgId = orgManager.assertAccess(orgName, true).getId();\n        ProjectEntry projectEntry = assertProject(orgId, projectName);\n        RepositoryEntry repositoryEntry = repositoryDao.get(projectEntry.getId(), repositoryName);\n        ProcessDefinition processDefinition = processDefinition(orgId, repositoryEntry.getProjectId(), repositoryEntry);\n        tx(tx -> refresh(tx, projectEntry.getId(), repositoryName , processDefinition));\n    }\n\n    public void refresh(DSLContext tx, UUID projectId, String repositoryName, ProcessDefinition pd) {\n        try {\n            RepositoryEntry repositoryEntry = repositoryDao.get(tx, projectId, repositoryName);\n            for (RepositoryRefreshListener l : listeners) {\n                l.onRefresh(tx, repositoryEntry, pd);\n            }\n        } catch (Exception e) {\n            String errorMessage = e.getCause() != null ? e.getCause().getMessage() : e.getMessage();\n            throw new ConcordApplicationException(\"Error while refreshing repository: \\n\" + errorMessage, e);\n        }\n    }\n\n    public ProcessDefinition processDefinition(UUID orgId, UUID projectId, RepositoryEntry repositoryEntry) {\n        if (repositoryEntry.getBranch() == null && repositoryEntry.getCommitId() == null){\n            throw new ValidationErrorsException(\"Branch or CommitId required\");\n        }\n\n        String branch;\n        if (repositoryEntry.getCommitId() != null) {\n            branch = null;\n        } else {\n            branch = repositoryEntry.getBranch();\n        }\n\n        try (TemporaryPath tmpRepoPath = PathUtils.tempDir(\"refreshRepo_\")) {\n            Path path = tmpRepoPath.path();\n\n            Secret secret = getSecret(orgId, projectId, repositoryEntry);\n\n            ImportsNormalizer normalizer = importsNormalizerFactory.forProject(projectId);\n\n            repositoryManager.withLock(repositoryEntry.getUrl(), () -> {\n                Repository repository = repositoryManager.fetch(repositoryEntry.getUrl(), branch, repositoryEntry.getCommitId(), repositoryEntry.getPath(), secret, false);\n                repository.export(path);\n                return null;\n            });\n\n            return projectLoader.loadProject(path, normalizer, ImportsListener.NOP_LISTENER)\n                    .projectDefinition();\n        } catch (Exception e) {\n            throw new ConcordApplicationException(\"Error while loading process definition: \\n\" + e.getMessage(), e);\n        }\n    }\n\n    private ProjectEntry assertProject(UUID orgId, String projectName) {\n        if (projectName == null) {\n            throw new ValidationErrorsException(\"Invalid project name\");\n        }\n\n        return projectAccessManager.assertAccess(orgId, null, projectName, ResourceAccessLevel.READER, true);\n    }\n\n    private Secret getSecret(UUID orgId, UUID projectId, RepositoryEntry repositoryEntry) {\n        if (repositoryEntry.getSecretId() == null && repositoryEntry.getSecretName() == null) {\n            return null;\n        }\n\n        String secretName = repositoryEntry.getSecretName();\n        if (secretName == null) {\n            secretName = secretManager.getSecretName(repositoryEntry.getSecretId());\n        }\n\n        if (secretName == null) {\n            return null;\n        }\n\n        SecretManager.DecryptedSecret s = secretManager.getSecret(SecretManager.AccessScope.project(projectId), orgId, secretName, null, null);\n        if (s == null) {\n            throw new RepositoryException(\"Secret not found: \" + secretName);\n        }\n\n        return s.getSecret();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/repository/RepositoryValidationResponse.java",
    "content": "package com.walmartlabs.concord.server.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.server.OperationResult;\n\nimport java.io.Serializable;\nimport java.util.List;\n\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\npublic class RepositoryValidationResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok;\n    private final OperationResult result;\n\n    private final List<String> errors;\n    private final List<String> warnings;\n\n    @JsonCreator\n    public RepositoryValidationResponse(@JsonProperty(\"ok\") boolean ok,\n                                        @JsonProperty(\"result\") OperationResult result,\n                                        @JsonProperty(\"errors\") List<String> errors,\n                                        @JsonProperty(\"warnings\") List<String> warnings) {\n        this.ok = ok;\n        this.result = result;\n        this.errors = errors;\n        this.warnings = warnings;\n    }\n\n    public OperationResult getResult() {\n        return result;\n    }\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    public List<String> getErrors() {\n        return errors;\n    }\n\n    public List<String> getWarnings() {\n        return warnings;\n    }\n\n    @Override\n    public String toString() {\n        return \"RepositoryValidationResponse{\" +\n                \"ok=\" + ok +\n                \", result=\" + result +\n                \", errors=\" + errors +\n                \", warnings=\" + warnings +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/repository/ServerAuthTokenProvider.java",
    "content": "package com.walmartlabs.concord.server.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.MetricRegistry;\nimport com.walmartlabs.concord.common.AuthTokenProvider;\nimport com.walmartlabs.concord.common.ExternalAuthToken;\nimport com.walmartlabs.concord.github.appinstallation.GitHubAppInstallation;\nimport com.walmartlabs.concord.sdk.Secret;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport java.net.URI;\nimport java.util.List;\nimport java.util.Optional;\n\n@Named\n@Singleton\npublic class ServerAuthTokenProvider implements AuthTokenProvider {\n\n    private final List<AuthTokenProvider> authTokenProviders;\n\n    @Inject\n    public ServerAuthTokenProvider(GitHubAppInstallation githubProvider,\n                                   OauthTokenProvider oauthTokenProvider,\n                                   MetricRegistry metricRegistry) {\n        this.authTokenProviders = List.of(githubProvider, oauthTokenProvider);\n\n        metricRegistry.gauge(\"github-token-cache-size\", () -> githubProvider::cacheSize);\n    }\n\n    @Override\n    public boolean supports(URI repo, @Nullable Secret secret) {\n        return authTokenProviders.stream()\n                .anyMatch(p -> p.supports(repo, secret));\n    }\n\n    @WithTimer\n    public Optional<ExternalAuthToken> getToken(URI repo, @Nullable Secret secret) {\n        for (var tokenProvider : authTokenProviders) {\n            if (tokenProvider.supports(repo, secret)) {\n                return tokenProvider.getToken(repo, secret);\n            }\n        }\n\n        return Optional.empty();\n    }\n\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/repository/listeners/ProcessDefinitionRefreshListener.java",
    "content": "package com.walmartlabs.concord.server.repository.listeners;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.ProcessDefinition;\nimport com.walmartlabs.concord.server.org.project.RepositoryDao;\nimport com.walmartlabs.concord.server.org.project.RepositoryEntry;\nimport org.jooq.DSLContext;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic class ProcessDefinitionRefreshListener implements RepositoryRefreshListener {\n\n    private static final Logger log = LoggerFactory.getLogger(ProcessDefinitionRefreshListener.class);\n\n    private final RepositoryDao repositoryDao;\n\n    @Inject\n    public ProcessDefinitionRefreshListener(RepositoryDao repositoryDao) {\n        this.repositoryDao = repositoryDao;\n    }\n\n    @Override\n    public void onRefresh(DSLContext tx, RepositoryEntry repo, ProcessDefinition pd) {\n        Set<String> pf = pd.publicFlows();\n        if (pf == null || pf.isEmpty()) {\n            // all flows are public when no public flows defined\n            pf = new HashSet<>(pd.flows().keySet());\n        }\n\n        Set<String> entryPoints = pd.flows().keySet()\n                .stream()\n                .filter(pf::contains)\n                .collect(Collectors.toSet());\n\n        List<String> profiles = new ArrayList<>(pd.profiles().keySet());\n\n        Map<String, Object> meta = new HashMap<>();\n        meta.put(\"entryPoints\", emptyToNull(entryPoints));\n        meta.put(\"profiles\", emptyToNull(profiles));\n        repositoryDao.updateMeta(tx, repo.getId(), meta);\n\n        log.info(\"onRefresh ['{}'] -> done ({})\", repo.getId(), entryPoints);\n    }\n\n    private static <E extends Collection<?>> E emptyToNull(E items) {\n        if (items.isEmpty()) {\n            return null;\n        }\n        return items;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/repository/listeners/RepositoryRefreshListener.java",
    "content": "package com.walmartlabs.concord.server.repository.listeners;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.ProcessDefinition;\nimport com.walmartlabs.concord.server.org.project.RepositoryEntry;\nimport org.jooq.DSLContext;\n\npublic interface RepositoryRefreshListener {\n\n    void onRefresh(DSLContext tx, RepositoryEntry repo, ProcessDefinition pd);\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/repository/listeners/TriggerRefreshListener.java",
    "content": "package com.walmartlabs.concord.server.repository.listeners;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.runtime.model.ProcessDefinition;\nimport com.walmartlabs.concord.server.org.project.ProjectValidator;\nimport com.walmartlabs.concord.server.org.project.RepositoryEntry;\nimport com.walmartlabs.concord.server.org.triggers.TriggerManager;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport org.jooq.DSLContext;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\n\npublic class TriggerRefreshListener implements RepositoryRefreshListener {\n\n    private static final Logger log = LoggerFactory.getLogger(TriggerRefreshListener.class);\n\n    private final TriggerManager triggerManager;\n\n    @Inject\n    public TriggerRefreshListener(TriggerManager triggerManager) {\n        this.triggerManager = triggerManager;\n    }\n\n    @Override\n    public void onRefresh(DSLContext tx, RepositoryEntry repo, ProcessDefinition pd) {\n        if (repo.isTriggersDisabled() || repo.isDisabled()) {\n            triggerManager.clearTriggers(tx, repo.getProjectId(), repo.getId());\n            return;\n        }\n\n        log.info(\"refresh ['{}'] -> triggers\", repo.getId());\n\n        ProjectValidator.Result validationResult = ProjectValidator.validate(pd);\n        if (!validationResult.isValid()) {\n            throw new ValidationErrorsException(String.join(\"\\n\", validationResult.getErrors()));\n        }\n\n        triggerManager.refresh(tx, repo.getProjectId(), repo.getId(), pd);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/role/RoleDao.java",
    "content": "package com.walmartlabs.concord.server.role;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.UuidGenerator;\nimport com.walmartlabs.concord.server.jooq.tables.RoleLdapGroups;\nimport com.walmartlabs.concord.server.user.RoleEntry;\nimport org.jooq.*;\n\nimport javax.inject.Inject;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.*;\nimport static com.walmartlabs.concord.server.jooq.tables.Roles.ROLES;\nimport static java.util.Objects.requireNonNull;\nimport static org.jooq.impl.DSL.*;\n\npublic class RoleDao extends AbstractDao {\n\n    private final UuidGenerator uuidGenerator;;\n\n    @Inject\n    protected RoleDao(@MainDB Configuration cfg, UuidGenerator uuidGenerator) {\n        super(cfg);\n        this.uuidGenerator = requireNonNull(uuidGenerator);\n    }\n\n    @Override\n    public void tx(Tx t) {\n        super.tx(t);\n    }\n\n    public RoleEntry get(UUID id) {\n        SelectConditionStep<Record1<String[]>> permissions = select(arrayAgg(PERMISSIONS.PERMISSION_NAME)).from(PERMISSIONS)\n                .where(PERMISSIONS.PERMISSION_ID.in(\n                        select(ROLE_PERMISSIONS.PERMISSION_ID).from(ROLE_PERMISSIONS)\n                                .where(ROLE_PERMISSIONS.ROLE_ID.in(ROLES.ROLE_ID))));\n\n        return dsl().select(ROLES.ROLE_ID, ROLES.ROLE_NAME, isnull(permissions.asField(), new String[]{}))\n                .from(ROLES)\n                .where(ROLES.ROLE_ID.in(id))\n                .fetchOne(e -> new RoleEntry(e.value1(), e.value2(), new HashSet<>(Arrays.asList(e.value3()))));\n    }\n\n    public UUID getId(String roleName) {\n        SelectConditionStep<Record1<UUID>> q = dsl().select(ROLES.ROLE_ID)\n                .from(ROLES)\n                .where(ROLES.ROLE_NAME.eq(roleName));\n\n        return q.fetchOne(ROLES.ROLE_ID);\n    }\n\n    public UUID insert(String roleName, Set<String> permissions) {\n        return txResult(tx -> insert(tx, roleName, permissions));\n    }\n\n    public UUID insert(DSLContext tx, String roleName, Set<String> permissions) {\n        UUID roleId = uuidGenerator.generate();\n\n        UUID effectiveRoleId = tx.insertInto(ROLES)\n                .columns(ROLES.ROLE_ID, ROLES.ROLE_NAME)\n                .values(value(roleId), value(roleName))\n                .returning(ROLES.ROLE_ID)\n                .fetchOne().getRoleId();\n\n        if (permissions != null) {\n            tx.insertInto(ROLE_PERMISSIONS).select(\n                    select(\n                            value(effectiveRoleId).as(ROLE_PERMISSIONS.ROLE_ID),\n                            PERMISSIONS.PERMISSION_ID.as(ROLE_PERMISSIONS.PERMISSION_ID)\n                    ).from(PERMISSIONS).where(PERMISSIONS.PERMISSION_NAME.in(permissions)))\n                    .execute();\n        }\n\n        return effectiveRoleId;\n    }\n\n    public void update(UUID roleId, String name, Set<String> permissions) {\n        tx(tx -> {\n\n            tx.update(ROLES)\n                    .set(ROLES.ROLE_NAME, name)\n                    .where(ROLES.ROLE_ID.eq(roleId))\n                    .execute();\n\n            if (permissions != null) {\n                tx.deleteFrom(ROLE_PERMISSIONS).where(ROLE_PERMISSIONS.ROLE_ID.eq(roleId)).execute();\n\n                tx.insertInto(ROLE_PERMISSIONS).select(\n                        select(\n                                value(roleId).as(ROLE_PERMISSIONS.ROLE_ID),\n                                PERMISSIONS.PERMISSION_ID.as(ROLE_PERMISSIONS.PERMISSION_ID)\n                        ).from(PERMISSIONS).where(PERMISSIONS.PERMISSION_NAME.in(permissions)))\n                        .execute();\n            }\n        });\n    }\n\n    public void delete(UUID roleId) {\n        tx(tx -> tx.deleteFrom(ROLES)\n                .where(ROLES.ROLE_ID.eq(roleId))\n                .execute());\n    }\n\n    public List<RoleEntry> list() {\n        SelectConditionStep<Record1<String[]>> permissions = select(arrayAgg(PERMISSIONS.PERMISSION_NAME)).from(PERMISSIONS)\n                .where(PERMISSIONS.PERMISSION_ID.in(\n                        select(ROLE_PERMISSIONS.PERMISSION_ID).from(ROLE_PERMISSIONS)\n                                .where(ROLE_PERMISSIONS.ROLE_ID.eq(ROLES.ROLE_ID))));\n\n\n        return dsl().select(ROLES.ROLE_ID, ROLES.ROLE_NAME, isnull(permissions.asField(), new String[]{}))\n                .from(ROLES)\n                .fetch(RoleDao::toEntry);\n    }\n\n    public void upsertLdapGroup(DSLContext tx, UUID roleId, String ldapGroup) {\n        tx.insertInto(ROLE_LDAP_GROUPS)\n                .columns(ROLE_LDAP_GROUPS.ROLE_ID, ROLE_LDAP_GROUPS.LDAP_GROUP)\n                .values(roleId, ldapGroup)\n                .onConflict(ROLE_LDAP_GROUPS.ROLE_ID, ROLE_LDAP_GROUPS.LDAP_GROUP)\n                .doNothing()\n                .execute();\n    }\n    \n    public void removeLdapGroups(DSLContext tx, UUID roleId) {\n        tx.deleteFrom(ROLE_LDAP_GROUPS)\n                .where(ROLE_LDAP_GROUPS.ROLE_ID.eq(roleId))\n                .execute();\n    }\n\n    public List<String> listLdapGroups(UUID roleId) {\n        RoleLdapGroups r = ROLE_LDAP_GROUPS.as(\"r\");\n        return txResult(tx -> tx.select(r.LDAP_GROUP)\n                .from(r)\n                .where(r.ROLE_ID.eq(roleId))\n                .orderBy(r.LDAP_GROUP)\n                .fetch(r.LDAP_GROUP));\n    }\n    \n    private static RoleEntry toEntry(Record3<UUID, String, String[]> e) {\n        return new RoleEntry(e.value1(), e.value2(), new HashSet<>(Arrays.asList(e.value3())));\n    }\n\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/role/RoleModule.java",
    "content": "package com.walmartlabs.concord.server.role;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\n\nimport static com.walmartlabs.concord.server.Utils.bindJaxRsResource;\n\npublic class RoleModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        bindJaxRsResource(binder, RoleResource.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/role/RoleOperationResponse.java",
    "content": "package com.walmartlabs.concord.server.role;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.server.OperationResult;\n\nimport java.io.Serializable;\nimport java.util.UUID;\n\npublic class RoleOperationResponse implements Serializable {\n    private static final long serialVersionUID = 1L;\n\n    private final UUID id;\n    private final OperationResult result;\n\n    @JsonCreator\n    public RoleOperationResponse(@JsonProperty(\"id\") UUID id,\n                                 @JsonProperty(\"result\") OperationResult result) {\n        this.id = id;\n        this.result = result;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public OperationResult getResult() {\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"RoleOperationResponse{\" +\n                \"id=\" + id +\n                \", result=\" + result +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/role/RoleResource.java",
    "content": "package com.walmartlabs.concord.server.role;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.server.GenericOperationResult;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.audit.AuditAction;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.audit.AuditObject;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport com.walmartlabs.concord.server.user.RoleEntry;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.validation.Valid;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.UUID;\n\nimport static javax.ws.rs.core.Response.Status;\n\n@Path(\"/api/v1/role\")\n@Tag(name = \"Roles\")\npublic class RoleResource implements Resource {\n\n    private final RoleDao roleDao;\n    private final AuditLog auditLog;\n\n    @Inject\n    public RoleResource(RoleDao roleDao, AuditLog auditLog) {\n        this.roleDao = roleDao;\n        this.auditLog = auditLog;\n    }\n\n    @GET\n    @Path(\"/{roleName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"Get a role's details\", operationId = \"getRole\")\n    public RoleEntry get(@PathParam(\"roleName\") String roleName) {\n        assertAdmin();\n\n        UUID id = roleDao.getId(roleName);\n        if (id == null) {\n            throw new ConcordApplicationException(\"Role not found: \" + roleName, Status.NOT_FOUND);\n        }\n\n        return roleDao.get(id);\n    }\n\n    @GET\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"List roles\", operationId = \"listRoles\")\n    public List<RoleEntry> list() {\n        assertAdmin();\n\n        return roleDao.list();\n    }\n\n    @POST\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Create or update a role\", operationId = \"createOrUpdateRole\")\n    public RoleOperationResponse createOrUpdate(@Valid RoleEntry entry) {\n        assertAdmin();\n\n        UUID id = entry.getId();\n        if (id == null && entry.getName() != null) {\n            id = roleDao.getId(entry.getName());\n        }\n\n        if (id == null) {\n            id = roleDao.insert(entry.getName(), entry.getPermissions());\n\n            auditLog.add(AuditObject.ROLE, AuditAction.CREATE)\n                    .field(\"roleId\", id)\n                    .field(\"name\", entry.getName())\n                    .field(\"permissions\", entry.getPermissions())\n                    .log();\n\n            return new RoleOperationResponse(id, OperationResult.CREATED);\n        } else {\n            roleDao.update(id, entry.getName(), entry.getPermissions());\n\n            auditLog.add(AuditObject.ROLE, AuditAction.UPDATE)\n                    .field(\"roleId\", id)\n                    .field(\"name\", entry.getName())\n                    .field(\"permissions\", entry.getPermissions())\n                    .log();\n\n            return new RoleOperationResponse(id, OperationResult.UPDATED);\n        }\n    }\n\n    @DELETE\n    @Path(\"/{roleName}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"Delete an existing role\", operationId = \"deleteRole\")\n    public GenericOperationResult delete(@PathParam(\"roleName\") @ConcordKey String roleName) {\n        assertAdmin();\n\n        UUID id = roleDao.getId(roleName);\n        if (id == null) {\n            throw new ConcordApplicationException(\"Role not found: \" + roleName, Status.NOT_FOUND);\n        }\n\n        roleDao.delete(id);\n\n        auditLog.add(AuditObject.ROLE, AuditAction.DELETE)\n                .field(\"roleId\", id)\n                .field(\"name\", roleName)\n                .log();\n\n        return new GenericOperationResult(OperationResult.DELETED);\n    }\n\n    /**\n     * Add LDAP groups to the specified role.\n     *\n     * @param roleName Name of the Role\n     * @param groups LDAP groups collection\n     * @return result\n     */\n    @PUT\n    @Path(\"/{roleName}/ldapGroups\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Add LDAP groups to a role\", operationId = \"addLdapGroupsToRole\")\n    public GenericOperationResult addLdapGroups(@PathParam(\"roleName\") @ConcordKey String roleName,\n                                                @QueryParam(\"replace\") @DefaultValue(\"false\") boolean replace,\n                                                @Valid Collection<String> groups) {\n        assertAdmin();\n\n        boolean isEmptyGroups = groups == null || groups.isEmpty();\n        if (isEmptyGroups && !replace) {\n            throw new ValidationErrorsException(\"Empty LDAP group list\");\n        }\n\n        UUID id = roleDao.getId(roleName);\n        if (id == null) {\n            throw new ConcordApplicationException(\"Role not found: \" + roleName, Status.NOT_FOUND);\n        }\n\n        roleDao.tx(tx -> {\n            if (replace) {\n                roleDao.removeLdapGroups(tx, id);\n            }\n\n            for (String g : groups) {\n                roleDao.upsertLdapGroup(tx, id, g);\n            }\n        });\n\n        auditLog.add(AuditObject.ROLE, AuditAction.UPDATE)\n                .field(\"roleId\", id)\n                .field(\"name\", roleName)\n                .field(\"action\", \"addLdapGroups\")\n                .field(\"groups\", groups)\n                .field(\"replace\", replace)\n                .log();\n\n        return new GenericOperationResult(OperationResult.UPDATED);\n    }\n\n    /**\n     * List LDAP groups of a role.\n     *\n     * @param roleName Name of a Role\n     * @return list of groups\n     */\n    @GET\n    @Path(\"/{roleName}/ldapGroups\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"List ldap groups of a role\", operationId = \"listLdapGroupsForRole\")\n    public List<String> listLdapGroups(@PathParam(\"roleName\") @ConcordKey String roleName) {\n        assertAdmin();\n\n        UUID id = roleDao.getId(roleName);\n        if (id == null) {\n            throw new ConcordApplicationException(\"Role not found: \" + roleName, Status.NOT_FOUND);\n        }\n        \n        return roleDao.listLdapGroups(id);\n    }\n    \n    private static void assertAdmin() {\n        if (!Roles.isAdmin()) {\n            throw new UnauthorizedException(\"Only admins can do that\");\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/sdk/process/CustomEnqueueProcessor.java",
    "content": "package com.walmartlabs.concord.server.sdk.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.Payload;\n\n/**\n * A custom processor that can be used to modify the payload before its state\n * is imported into the database.\n */\npublic interface CustomEnqueueProcessor {\n\n    /**\n     * Process any attachments before they are stored as files in the ${workDir}.\n     */\n    default Payload handleAttachments(Payload payload) {\n        return payload;\n    }\n\n    /**\n     * Process the process state before it is stored in the database.\n     */\n    default Payload handleState(Payload payload) {\n        return payload;\n    }\n\n    /**\n     * Process the process configuration after the merge rules are applied.\n     */\n    default Payload handleConfiguration(Payload payload) {\n        return payload;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/BasicAuthenticationHandler.java",
    "content": "package com.walmartlabs.concord.server.security;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.boot.filters.AuthenticationHandler;\nimport org.apache.shiro.authc.AuthenticationToken;\nimport org.apache.shiro.authc.UsernamePasswordToken;\nimport org.apache.shiro.subject.support.DefaultSubjectContext;\n\nimport javax.servlet.ServletRequest;\nimport javax.servlet.ServletResponse;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.ws.rs.core.HttpHeaders;\nimport java.util.Base64;\n\nimport static com.walmartlabs.concord.sdk.Constants.Headers.REMEMBER_ME_HEADER;\n\n/**\n * Handles basic authentication (username/password).\n *\n * @see SessionTokenAuthenticationHandler for a special case of session tokens passed as usernames.\n */\npublic class BasicAuthenticationHandler implements AuthenticationHandler {\n\n    private static final String BASIC_AUTH_PREFIX = \"Basic \";\n\n    @Override\n    public AuthenticationToken createToken(ServletRequest request, ServletResponse response) {\n        var req = (HttpServletRequest) request;\n\n        if (req.getHeader(HttpHeaders.AUTHORIZATION) == null) {\n            return null;\n        }\n\n        // check the 'remember me' status\n        var rememberMe = Boolean.parseBoolean(req.getHeader(REMEMBER_ME_HEADER));\n\n        var auth = req.getHeader(HttpHeaders.AUTHORIZATION);\n        if (auth == null || auth.isBlank()) {\n            return null;\n        }\n\n        if (!auth.startsWith(BASIC_AUTH_PREFIX)) {\n            return null;\n        }\n\n        auth = auth.substring(BASIC_AUTH_PREFIX.length());\n        auth = new String(Base64.getDecoder().decode(auth));\n\n        var idx = auth.indexOf(\":\");\n        if (idx < 0) {\n            // invalid auth header\n            return null;\n        }\n\n        if (idx + 1 == auth.length()) {\n            // no password\n            // it might be a session token that is passed as a username\n            // we let the SessionTokenAuthenticationHandler to handle it\n            return null;\n        }\n\n        var username = auth.substring(0, idx).trim();\n        var password = auth.substring(idx + 1);\n\n        // enable sessions\n        req.setAttribute(DefaultSubjectContext.SESSION_CREATION_ENABLED, true);\n\n        return new UsernamePasswordToken(username, password, rememberMe);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/GithubAuthenticatingFilter.java",
    "content": "package com.walmartlabs.concord.server.security;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.google.common.io.ByteStreams;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.cfg.GithubConfiguration;\nimport com.walmartlabs.concord.server.org.project.EncryptedProjectValueManager;\nimport com.walmartlabs.concord.server.security.github.GithubKey;\nimport org.apache.shiro.authc.AuthenticationToken;\nimport org.apache.shiro.authc.UsernamePasswordToken;\nimport org.apache.shiro.lang.codec.Hex;\nimport org.apache.shiro.lang.util.StringUtils;\nimport org.apache.shiro.web.filter.authc.AuthenticatingFilter;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.crypto.Mac;\nimport javax.crypto.spec.SecretKeySpec;\nimport javax.inject.Inject;\nimport javax.servlet.*;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletRequestWrapper;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.security.InvalidKeyException;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.util.Base64;\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic class GithubAuthenticatingFilter extends AuthenticatingFilter {\n\n    private static final Logger log = LoggerFactory.getLogger(GithubAuthenticatingFilter.class);\n\n    private static final String HMAC_SHA1_ALGORITHM = \"HmacSHA1\";\n    private static final String SIGNATURE_HEADER = \"X-Hub-Signature\";\n\n    public static final String HOOK_PROJECT_ID = \"hookProjectId\";\n    public static final String HOOK_REPO_TOKEN = \"hookRepoToken\";\n\n    private final GithubConfiguration cfg;\n    private final EncryptedProjectValueManager encryptedValueManager;\n    private final ObjectMapper objectMapper;\n\n    @Inject\n    public GithubAuthenticatingFilter(GithubConfiguration cfg, EncryptedProjectValueManager encryptedValueManager, ObjectMapper objectMapper) {\n        this.cfg = cfg;\n        this.encryptedValueManager = encryptedValueManager;\n        this.objectMapper = objectMapper;\n    }\n\n    @Override\n    public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {\n        HttpServletRequest req = (HttpServletRequest) request;\n        super.doFilterInternal(new CachingRequestWrapper(req), response, filterChain);\n    }\n\n    @Override\n    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {\n        CachingRequestWrapper req = (CachingRequestWrapper) request;\n\n        final byte[] payload = req.getPayload();\n\n        // support for hooks restricted to a specific repository\n        UUID projectId = getUUID(req, HOOK_PROJECT_ID);\n        String repoToken = getString(req, HOOK_REPO_TOKEN);\n        if (projectId != null && repoToken != null) {\n            return processSpecificRepo(projectId, repoToken, payload);\n        }\n\n        String h = req.getHeader(SIGNATURE_HEADER);\n        if (!StringUtils.hasText(h)) {\n            log.warn(\"createToken -> authorization header is missing. URI: '{}'\", req.getRequestURI());\n            return new UsernamePasswordToken();\n        }\n\n        String[] algDigest = h.split(\"=\");\n        if (algDigest.length != 2) {\n            log.warn(\"createToken -> invalid format of the authorization header. URI: '{}'\", req.getRequestURI());\n            return new UsernamePasswordToken();\n        }\n\n        if (!\"sha1\".equals(algDigest[0])) {\n            log.warn(\"createToken -> invalid algorithm of the authorization header '{}'. URI: '{}'\", algDigest[0], req.getRequestURI());\n            return new UsernamePasswordToken();\n        }\n\n        SecretKeySpec signingKey = new SecretKeySpec(cfg.getSecret().getBytes(), HMAC_SHA1_ALGORITHM);\n        try {\n            Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);\n            mac.init(signingKey);\n            byte[] digest = mac.doFinal(payload);\n            String digestHex = String.valueOf(Hex.encode(digest));\n\n            if (!algDigest[1].equals(digestHex)) {\n                log.error(\"createToken -> invalid auth digest. Expected: '{}', request: '{}'\", digestHex, algDigest[1]);\n                return new UsernamePasswordToken();\n            }\n\n            return new GithubKey(h, projectId, repoToken);\n        } catch (NoSuchAlgorithmException | InvalidKeyException e) {\n            log.error(\"createToken -> internal error\", e);\n            throw e;\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private AuthenticationToken processSpecificRepo(UUID projectId, String repoToken, byte[] rawPayload) throws IOException {\n        Map<String, Object> payload = objectMapper.readValue(rawPayload, ConcordObjectMapper.MAP_TYPE);\n\n        Map<String, Object> repo = (Map<String, Object>) payload.getOrDefault(\"repository\", Collections.emptyMap());\n        String repoFullName = (String) repo.get(\"full_name\");\n        if (repoFullName == null) {\n            log.error(\"processSpecificRepo -> repository name not found in payload\");\n            return new UsernamePasswordToken();\n        }\n\n        String[] orgRepo = repoFullName.split(\"/\");\n        if (orgRepo.length != 2) {\n            log.error(\"processSpecificRepo -> invalid repo name '{}', expected org/repo format\", repoFullName);\n            return new UsernamePasswordToken();\n        }\n\n        byte[] decodedHash = Base64.getDecoder().decode(repoToken);\n        byte[] decryptedHash = encryptedValueManager.decrypt(projectId, decodedHash);\n        String userHash = new String(decryptedHash, StandardCharsets.UTF_8);\n\n        String repoHash = hash(orgRepo[1]);\n        if (!repoHash.equals(userHash)) {\n            log.error(\"processSpecificRepo -> invalid repo token\");\n            return new UsernamePasswordToken();\n        }\n        return new GithubKey(null, projectId, repoToken);\n    }\n\n    @Override\n    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {\n        boolean loggedId = executeLogin(request, response);\n\n        if (!loggedId) {\n            HttpServletResponse resp = (HttpServletResponse) response;\n            resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);\n        }\n\n        return loggedId;\n    }\n\n    private static String getString(HttpServletRequest req, String k) {\n        String s = req.getParameter(k);\n        if (StringUtils.hasText(s)) {\n            return s;\n        }\n        return null;\n    }\n\n    private static UUID getUUID(HttpServletRequest req, String k) {\n        String s = getString(req, k);\n        if (s == null) {\n            return null;\n        }\n        return UUID.fromString(s);\n    }\n\n    private static String hash(String value) {\n        MessageDigest md;\n        try {\n            md = MessageDigest.getInstance(\"SHA-256\");\n        } catch (NoSuchAlgorithmException e) {\n            throw new RuntimeException(e);\n        }\n\n        byte[] ab = md.digest(value.getBytes(StandardCharsets.UTF_8));\n\n        return Base64.getEncoder().withoutPadding().encodeToString(ab);\n    }\n\n    static class CachingRequestWrapper extends HttpServletRequestWrapper {\n\n        private byte[] payload;\n\n        private CachingRequestWrapper(HttpServletRequest request) {\n            super(request);\n        }\n\n        private byte[] getPayload() throws IOException {\n            if (payload == null) {\n                payload = ByteStreams.toByteArray(getRequest().getInputStream());\n            }\n            return payload;\n        }\n\n        @Override\n        public ServletInputStream getInputStream() throws IOException {\n            final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(getPayload());\n            return new ServletInputStream() {\n\n                @Override\n                public int read() {\n                    return byteArrayInputStream.read();\n                }\n\n                @Override\n                public boolean isFinished() {\n                    return false;\n                }\n\n                @Override\n                public boolean isReady() {\n                    return true;\n                }\n\n                @Override\n                public void setReadListener(ReadListener listener) {\n\n                }\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/LocalRequestFilter.java",
    "content": "package com.walmartlabs.concord.server.security;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.security.AuthenticationException;\n\nimport javax.servlet.*;\nimport javax.servlet.http.HttpServletRequest;\nimport java.io.IOException;\nimport java.net.InetAddress;\nimport java.net.UnknownHostException;\n\npublic class LocalRequestFilter implements Filter {\n\n    @Override\n    public void init(FilterConfig filterConfig) {\n        // do nothing\n    }\n\n    @Override\n    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {\n        HttpServletRequest req = (HttpServletRequest) request;\n        if (isLocalRequest(req)) {\n            chain.doFilter(request, response);\n        } else {\n            throw new AuthenticationException(\"Only localhost requests are allowed\");\n        }\n    }\n\n    @Override\n    public void destroy() {\n        // do nothing\n    }\n\n\n    private static boolean isLocalRequest(HttpServletRequest request) throws UnknownHostException {\n        InetAddress addr = InetAddress.getByName(request.getRemoteAddr());\n        return addr.isAnyLocalAddress() || addr.isLoopbackAddress();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/Permission.java",
    "content": "package com.walmartlabs.concord.server.security;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic enum Permission {\n\n    /**\n     * Read-only access to the process queue for all organizations.\n     * <p>\n     * As in {@code com/walmartlabs/concord/server/db/v1.18.0.xml}\n     */\n    GET_PROCESS_QUEUE_ALL_ORGS(\"getProcessQueueAllOrgs\"),\n    /**\n     * Permission to create organizations\n     * <p>\n     * As in {@code com/walmartlabs/concord/server/db/v1.57.0.xml}\n     */\n    CREATE_ORG(\"createOrg\"),\n    /**\n     * Permission to update organizations\n     * <p>\n     * As in {@code com/walmartlabs/concord/server/db/v1.94.0.xml}\n     */\n    UPDATE_ORG(\"updateOrg\"),\n    /**\n     * Allows users to specify API key values when creating new API keys.\n     * <p>\n     * As in {@code com/walmartlabs/concord/server/db/v2.31.0.xml}\n     */\n    API_KEY_SPECIFY_VALUE(\"apiKeySpecifyValue\");\n\n    private final String key;\n\n    Permission(String key) {\n        this.key = key;\n    }\n\n    public String getKey() {\n        return key;\n    }\n\n    public boolean isPermitted() {\n        return SecurityUtils.isPermitted(this.getKey());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/Roles.java",
    "content": "package com.walmartlabs.concord.server.security;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic final class Roles {\n\n    /**\n     * Admins can access any resource and perform any action.\n     */\n    public static final String ADMIN = \"concordAdmin\";\n\n    /**\n     * System readers can access any resource.\n     */\n    public static final String SYSTEM_READER = \"concordSystemReader\";\n\n    /**\n     * System readers can modify any resource.\n     */\n    public static final String SYSTEM_WRITER = \"concordSystemWriter\";\n\n    public static boolean isAdmin() {\n        return SecurityUtils.hasRole(ADMIN);\n    }\n\n    public static boolean isGlobalReader() {\n        return SecurityUtils.hasRole(SYSTEM_READER);\n    }\n\n    public static boolean isGlobalWriter() {\n        return SecurityUtils.hasRole(SYSTEM_WRITER);\n    }\n\n    private Roles() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/SecurityModule.java",
    "content": "package com.walmartlabs.concord.server.security;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.walmartlabs.concord.server.boot.filters.AuthenticationHandler;\nimport com.walmartlabs.concord.server.security.apikey.ApiKeyAuthenticationHandler;\nimport com.walmartlabs.concord.server.security.apikey.ApiKeyRealm;\nimport com.walmartlabs.concord.server.security.github.GithubRealm;\nimport com.walmartlabs.concord.server.security.internal.InternalRealm;\nimport com.walmartlabs.concord.server.security.internal.LocalUserInfoProvider;\nimport com.walmartlabs.concord.server.security.ldap.*;\nimport com.walmartlabs.concord.server.security.sessionkey.SessionKeyRealm;\nimport com.walmartlabs.concord.server.user.UserInfoProvider;\nimport org.apache.shiro.realm.Realm;\nimport org.apache.shiro.realm.ldap.LdapContextFactory;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\nimport static com.walmartlabs.concord.server.Utils.*;\n\npublic class SecurityModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        binder.bind(UserSecurityContext.class);\n\n        newSetBinder(binder, AuthenticationHandler.class).addBinding().to(BasicAuthenticationHandler.class).in(SINGLETON);\n        newSetBinder(binder, AuthenticationHandler.class).addBinding().to(ApiKeyAuthenticationHandler.class).in(SINGLETON);\n        newSetBinder(binder, AuthenticationHandler.class).addBinding().to(SessionTokenAuthenticationHandler.class).in(SINGLETON);\n\n        newSetBinder(binder, Realm.class).addBinding().to(ApiKeyRealm.class);\n        newSetBinder(binder, Realm.class).addBinding().to(GithubRealm.class);\n        newSetBinder(binder, Realm.class).addBinding().to(InternalRealm.class);\n        newSetBinder(binder, Realm.class).addBinding().to(LdapRealm.class);\n        newSetBinder(binder, Realm.class).addBinding().to(SessionKeyRealm.class);\n\n        binder.bind(LdapManager.class).toProvider(LdapManagerProvider.class);\n        binder.bind(LdapContextFactory.class).toProvider(LdapContextFactoryProvider.class);\n\n        bindSingletonScheduledTask(binder, UserLdapGroupSynchronizer.class);\n\n        binder.bind(LocalUserInfoProvider.class).in(SINGLETON);\n        newSetBinder(binder, UserInfoProvider.class).addBinding().to(LocalUserInfoProvider.class);\n\n        binder.bind(LdapUserInfoProvider.class).in(SINGLETON);\n        newSetBinder(binder, UserInfoProvider.class).addBinding().to(LdapUserInfoProvider.class);\n\n        bindExceptionMapper(binder, UnauthorizedExceptionMapper.class);\n        bindExceptionMapper(binder, UnauthenticatedExceptionMapper.class);\n\n        bindJaxRsResource(binder, UserLdapGroupResource.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/SecurityUtils.java",
    "content": "package com.walmartlabs.concord.server.security;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.security.AuthenticationException;\nimport com.walmartlabs.concord.server.user.RoleEntry;\nimport com.walmartlabs.concord.server.user.UserEntry;\nimport org.apache.shiro.authz.AuthorizationInfo;\nimport org.apache.shiro.authz.SimpleAuthorizationInfo;\nimport org.apache.shiro.mgt.SecurityManager;\nimport org.apache.shiro.subject.PrincipalCollection;\nimport org.apache.shiro.subject.Subject;\nimport org.apache.shiro.util.ThreadContext;\n\nimport java.io.*;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Set;\n\n/**\n * Utility methods for working with Shiro's security context.\n * Should be the only place where Shiro's API is used directly except for\n * the security filters.\n */\npublic final class SecurityUtils {\n\n    public static void logout() {\n        Subject subject = getSubject();\n        if (subject != null) {\n            subject.logout();\n        }\n    }\n\n    public static boolean hasRole(String role) {\n        Subject s = getSubject();\n        return s.hasRole(role);\n    }\n\n    public static boolean isPermitted(String permission) {\n        Subject s = getSubject();\n        return s.isPermitted(permission);\n    }\n\n    public static Subject getSubject() {\n        Subject subject = ThreadContext.getSubject();\n        if (subject == null) {\n            subject = (new Subject.Builder()).buildSubject();\n            ThreadContext.bind(subject);\n        }\n        return subject;\n    }\n\n    public static <T> T getCurrent(Class<T> type) {\n        SecurityManager securityManager = ThreadContext.getSecurityManager();\n        if (securityManager == null) {\n            return null;\n        }\n\n        Subject subject = getSubject();\n        if (subject == null) {\n            return null;\n        }\n\n        PrincipalCollection principals = subject.getPrincipals();\n        if (principals == null) {\n            return null;\n        }\n\n        return principals.oneByType(type);\n    }\n\n    public static <T> T assertCurrent(Class<T> type) {\n        T p = getCurrent(type);\n        if (p == null) {\n            throw new AuthenticationException(\"Can't determine the current principal (\" + type.getName() + \")\");\n        }\n        return p;\n    }\n\n    public static byte[] serialize(PrincipalCollection data) {\n        ByteArrayOutputStream baos = new ByteArrayOutputStream();\n        try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {\n            oos.writeObject(data);\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n        return baos.toByteArray();\n    }\n\n    public static Optional<PrincipalCollection> deserialize(byte[] data) {\n        InputStream in = new ByteArrayInputStream(data);\n        return deserialize(in);\n    }\n\n    public static Optional<PrincipalCollection> deserialize(InputStream in) {\n        try (ObjectInputStream ois = new ObjectInputStream(in)) {\n            return Optional.of((PrincipalCollection) ois.readObject());\n        } catch (IOException | ClassNotFoundException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static AuthorizationInfo toAuthorizationInfo(PrincipalCollection principals) {\n        return toAuthorizationInfo(principals, null);\n    }\n\n    public static AuthorizationInfo toAuthorizationInfo(PrincipalCollection principals, List<String> extraRoles) {\n        SimpleAuthorizationInfo i = new SimpleAuthorizationInfo();\n\n        UserPrincipal p = principals.oneByType(UserPrincipal.class);\n        if (p == null) {\n            return i;\n        }\n\n        UserEntry u = p.getUser();\n        Set<RoleEntry> roles = u.getRoles();\n        if (roles != null) {\n            roles.forEach(r -> {\n                i.addRole(r.getName());\n\n                Set<String> permissions = r.getPermissions();\n                if (permissions != null) {\n                    permissions.forEach(i::addStringPermission);\n                }\n            });\n        }\n\n        if (extraRoles != null) {\n            extraRoles.forEach(i::addRole);\n        }\n\n        return i;\n    }\n\n    private SecurityUtils() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/SessionTokenAuthenticationHandler.java",
    "content": "package com.walmartlabs.concord.server.security;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.secret.SecretUtils;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.boot.filters.AuthenticationHandler;\nimport com.walmartlabs.concord.server.cfg.SecretStoreConfiguration;\nimport com.walmartlabs.concord.server.security.sessionkey.SessionKey;\nimport org.apache.shiro.authc.AuthenticationToken;\nimport org.apache.shiro.subject.support.DefaultSubjectContext;\n\nimport javax.inject.Inject;\nimport javax.servlet.ServletRequest;\nimport javax.servlet.ServletResponse;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.ws.rs.core.HttpHeaders;\nimport java.util.Base64;\nimport java.util.UUID;\n\n/**\n * Handles session tokens.\n */\npublic class SessionTokenAuthenticationHandler implements AuthenticationHandler {\n\n    private static final String BASIC_AUTH_PREFIX = \"Basic \";\n\n    private final SecretStoreConfiguration secretCfg;\n\n    @Inject\n    public SessionTokenAuthenticationHandler(SecretStoreConfiguration secretCfg) {\n        this.secretCfg = secretCfg;\n    }\n\n    @Override\n    public AuthenticationToken createToken(ServletRequest request, ServletResponse response) {\n        var req = (HttpServletRequest) request;\n\n        var encryptedToken = req.getHeader(Constants.Headers.SESSION_TOKEN);\n        if (encryptedToken == null) {\n            // handle the special case of session tokens passed as usernames\n            var auth = req.getHeader(HttpHeaders.AUTHORIZATION);\n            if (auth == null) {\n                // no session token header and no basic auth header, skip\n                return null;\n            }\n\n            if (!auth.startsWith(BASIC_AUTH_PREFIX)) {\n                return null;\n            }\n\n            auth = auth.substring(BASIC_AUTH_PREFIX.length());\n            auth = new String(Base64.getDecoder().decode(auth));\n\n            var idx = auth.indexOf(\":\");\n            if (idx < 0 || idx != auth.length() - 1) {\n                // invalid auth header or a non-empty password, skip\n                return null;\n            }\n\n            encryptedToken = auth.substring(0, idx);\n        }\n\n        var decryptedValue = decryptSessionKey(encryptedToken);\n        var token = new SessionKey(decryptedValue);\n\n        // explicitly disable sessions\n        req.setAttribute(DefaultSubjectContext.SESSION_CREATION_ENABLED, false);\n\n        return token;\n    }\n\n    private UUID decryptSessionKey(String h) {\n        var salt = secretCfg.getSecretStoreSalt();\n        var pwd = secretCfg.getServerPwd();\n        var ab = SecretUtils.decrypt(Base64.getDecoder().decode(h), pwd, salt);\n        return UUID.fromString(new String(ab));\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/UnauthenticatedExceptionMapper.java",
    "content": "package com.walmartlabs.concord.server.security;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.boot.resteasy.ExceptionMapperSupport;\nimport org.apache.shiro.authz.UnauthenticatedException;\n\nimport javax.ws.rs.core.HttpHeaders;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport javax.ws.rs.core.Response.Status;\nimport javax.ws.rs.ext.Provider;\n\n@Provider\npublic class UnauthenticatedExceptionMapper extends ExceptionMapperSupport<UnauthenticatedException> {\n\n    @Override\n    protected Response convert(UnauthenticatedException exception) {\n        return Response.status(Status.UNAUTHORIZED)\n                .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_TYPE)\n                .entity(exception.getMessage())\n                .build();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/UnauthorizedException.java",
    "content": "package com.walmartlabs.concord.server.security;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic class UnauthorizedException extends RuntimeException {\n\n    public UnauthorizedException(String message) {\n        super(message);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/UnauthorizedExceptionMapper.java",
    "content": "package com.walmartlabs.concord.server.security;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.boot.resteasy.ExceptionMapperSupport;\n\nimport javax.ws.rs.core.HttpHeaders;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport javax.ws.rs.core.Response.Status;\nimport javax.ws.rs.ext.Provider;\n\n@Provider\npublic class UnauthorizedExceptionMapper extends ExceptionMapperSupport<UnauthorizedException> {\n\n    @Override\n    protected Response convert(UnauthorizedException exception) {\n        return Response.status(Status.FORBIDDEN)\n                .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_TYPE)\n                .entity(exception.getMessage())\n                .build();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/UserPrincipal.java",
    "content": "package com.walmartlabs.concord.server.security;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.user.UserEntry;\nimport com.walmartlabs.concord.server.user.UserType;\n\nimport java.io.Serializable;\nimport java.util.UUID;\n\nimport static java.util.Objects.requireNonNull;\n\n/**\n * <b>Note:</b> this class is serialized when user principals are stored in\n * the process state. It must maintain backward compatibility.\n */\npublic class UserPrincipal implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    public static UserPrincipal getCurrent() {\n        return SecurityUtils.getCurrent(UserPrincipal.class);\n    }\n\n    public static UserPrincipal assertCurrent() {\n        return SecurityUtils.assertCurrent(UserPrincipal.class);\n    }\n\n    private final String realm;\n    private final UserEntry user;\n\n    public UserPrincipal(String realm, UserEntry user) {\n        this.realm = requireNonNull(realm);\n        this.user = requireNonNull(user);\n    }\n\n    public String getRealm() {\n        return realm;\n    }\n\n    public UserEntry getUser() {\n        return user;\n    }\n\n    public UUID getId() {\n        return user.getId();\n    }\n\n    public String getUsername() {\n        return user.getName();\n    }\n\n    public String getDomain() {\n        return user.getDomain();\n    }\n\n    public UserType getType() {\n        return user.getType();\n    }\n\n    @Override\n    public String toString() {\n        return \"UserPrincipal{\" +\n                \"realm='\" + realm + '\\'' +\n                \", user=\" + user +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/UserSecurityContext.java",
    "content": "package com.walmartlabs.concord.server.security;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.security.internal.InternalRealm;\nimport com.walmartlabs.concord.server.user.UserEntry;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport org.apache.shiro.mgt.SecurityManager;\nimport org.apache.shiro.subject.SimplePrincipalCollection;\nimport org.apache.shiro.subject.Subject;\nimport org.apache.shiro.util.ThreadContext;\n\nimport javax.inject.Inject;\nimport java.util.UUID;\nimport java.util.concurrent.Callable;\n\npublic class UserSecurityContext {\n\n    private final SecurityManager securityManager;\n    private final UserManager userManager;\n\n    @Inject\n    public UserSecurityContext(SecurityManager securityManager, UserManager userManager) {\n        this.securityManager = securityManager;\n        this.userManager = userManager;\n    }\n\n    /**\n     * Run the specified {@link Callable} as if it was started by another user.\n     */\n    public <T> T runAs(UUID userID, Callable<T> c) throws Exception {\n        UserEntry u = userManager.get(userID).orElse(null);\n        if (u == null) {\n            throw new UnauthorizedException(\"User '\" + userID + \"'not found\");\n        }\n\n        try {\n            ThreadContext.bind(securityManager);\n\n            SimplePrincipalCollection principals = new SimplePrincipalCollection();\n            principals.add(new UserPrincipal(InternalRealm.REALM_NAME, u), InternalRealm.REALM_NAME);\n\n            Subject subject = new Subject.Builder()\n                    .sessionCreationEnabled(false)\n                    .authenticated(true)\n                    .principals(principals)\n                    .buildSubject();\n\n            ThreadContext.bind(subject);\n\n            return c.call();\n        } finally {\n            ThreadContext.unbindSubject();\n            ThreadContext.unbindSecurityManager();\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKey.java",
    "content": "package com.walmartlabs.concord.server.security.apikey;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.apache.shiro.authc.RememberMeAuthenticationToken;\n\nimport java.util.UUID;\n\npublic class ApiKey implements RememberMeAuthenticationToken {\n\n    private static final long serialVersionUID = 1L;\n\n    private final UUID keyId;\n    private final UUID userId;\n    private final String key;\n    private final boolean rememberMe;\n\n    public ApiKey(UUID keyId, UUID userId, String key, boolean rememberMe) {\n        this.keyId = keyId;\n        this.userId = userId;\n        this.key = key;\n        this.rememberMe = rememberMe;\n    }\n\n    public UUID getKeyId() {\n        return keyId;\n    }\n\n    public UUID getUserId() {\n        return userId;\n    }\n\n    public String getKey() {\n        return key;\n    }\n\n    @Override\n    public Object getPrincipal() {\n        return getUserId();\n    }\n\n    @Override\n    public Object getCredentials() {\n        return getKey();\n    }\n\n    @Override\n    public boolean isRememberMe() {\n        return rememberMe;\n    }\n\n    @Override\n    public String toString() {\n        return \"ApiKey{\" +\n                \"keyId=\" + keyId +\n                \", userId=\" + userId +\n                \", rememberMe=\" + rememberMe +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyAuthenticationHandler.java",
    "content": "package com.walmartlabs.concord.server.security.apikey;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.boot.filters.AuthenticationHandler;\nimport org.apache.shiro.authc.AuthenticationToken;\nimport org.apache.shiro.subject.support.DefaultSubjectContext;\n\nimport javax.inject.Inject;\nimport javax.servlet.ServletRequest;\nimport javax.servlet.ServletResponse;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.ws.rs.core.HttpHeaders;\nimport java.util.Base64;\n\nimport static com.walmartlabs.concord.sdk.Constants.Headers.ENABLE_HTTP_SESSION;\nimport static com.walmartlabs.concord.sdk.Constants.Headers.REMEMBER_ME_HEADER;\nimport static java.util.Objects.requireNonNull;\n\n/**\n * Handles the regular Concord API key authentication.\n */\npublic class ApiKeyAuthenticationHandler implements AuthenticationHandler {\n\n    private static final String BASIC_AUTH_PREFIX = \"Basic \";\n    private static final String BEARER_AUTH_PREFIX = \"Bearer \";\n\n    private final ApiKeyDao dao;\n\n    @Inject\n    public ApiKeyAuthenticationHandler(ApiKeyDao dao) {\n        this.dao = requireNonNull(dao);\n    }\n\n    @Override\n    public AuthenticationToken createToken(ServletRequest request, ServletResponse response) {\n        var req = (HttpServletRequest) request;\n\n        var auth = req.getHeader(HttpHeaders.AUTHORIZATION);\n        if (auth == null || auth.isBlank()) {\n            return null;\n        }\n\n        if (auth.startsWith(BASIC_AUTH_PREFIX)) {\n            // not our case\n            return null;\n        }\n\n        // Concord API keys can be sent both as plain \"Authentication: <token>\"\n        // and as \"Authentication: Bearer <token>\"\n\n        if (auth.startsWith(BEARER_AUTH_PREFIX)) {\n            auth = auth.substring(BEARER_AUTH_PREFIX.length());\n        }\n\n        if (!isValidBase64(auth)) {\n            return null;\n        }\n\n        var apiKey = dao.find(auth);\n        if (apiKey == null) {\n            return null;\n        }\n\n        var enableSessions = Boolean.parseBoolean(req.getHeader(ENABLE_HTTP_SESSION));\n        req.setAttribute(DefaultSubjectContext.SESSION_CREATION_ENABLED, enableSessions);\n\n        var rememberMe = Boolean.parseBoolean(req.getHeader(REMEMBER_ME_HEADER));\n        return new ApiKey(apiKey.getId(), apiKey.getUserId(), auth, rememberMe);\n    }\n\n    private static boolean isValidBase64(String s) {\n        try {\n            Base64.getDecoder().decode(s);\n            return true;\n        } catch (IllegalArgumentException e) {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyCleaner.java",
    "content": "package com.walmartlabs.concord.server.security.apikey;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.cfg.ApiKeyConfiguration;\nimport com.walmartlabs.concord.server.sdk.ScheduledTask;\nimport org.jooq.Configuration;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.util.concurrent.TimeUnit;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.API_KEYS;\nimport static org.jooq.impl.DSL.currentOffsetDateTime;\n\npublic class ApiKeyCleaner implements ScheduledTask {\n\n    private static final Logger log = LoggerFactory.getLogger(ApiKeyCleaner.class);\n\n    private static final long CLEANUP_INTERVAL = TimeUnit.DAYS.toSeconds(1);\n\n    private final boolean enabled;\n    private final CleanerDao cleanerDao;\n\n    @Inject\n    public ApiKeyCleaner(ApiKeyConfiguration cfg, CleanerDao cleanerDao) {\n        this.enabled = cfg.isExpirationEnabled();\n        this.cleanerDao = cleanerDao;\n    }\n\n    @Override\n    public String getId() {\n        return \"api-key-cleanup\";\n    }\n\n    @Override\n    public long getIntervalInSec() {\n        return this.enabled ? CLEANUP_INTERVAL : 0;\n    }\n\n    @Override\n    public void performTask() {\n        cleanerDao.deleteExpiredKeys();\n    }\n\n    static class CleanerDao extends AbstractDao {\n\n        @Inject\n        protected CleanerDao(@MainDB Configuration cfg) {\n            super(cfg);\n        }\n\n        void deleteExpiredKeys() {\n            tx(tx -> {\n                int keys = tx.deleteFrom(API_KEYS)\n                        .where(API_KEYS.EXPIRED_AT.lessOrEqual(currentOffsetDateTime()))\n                        .execute();\n\n                log.info(\"deleteExpiredKeys -> removed {} key(s)\", keys);\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyDao.java",
    "content": "package com.walmartlabs.concord.server.security.apikey;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.UuidGenerator;\nimport org.jooq.Configuration;\nimport org.jooq.Record4;\n\nimport javax.inject.Inject;\nimport java.security.SecureRandom;\nimport java.time.OffsetDateTime;\nimport java.util.Base64;\nimport java.util.Base64.Encoder;\nimport java.util.List;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.tables.ApiKeys.API_KEYS;\nimport static com.walmartlabs.concord.server.security.apikey.ApiKeyUtils.hash;\nimport static java.util.Objects.requireNonNull;\nimport static org.jooq.impl.DSL.currentOffsetDateTime;\nimport static org.jooq.impl.DSL.selectFrom;\n\npublic class ApiKeyDao extends AbstractDao {\n\n    private final SecureRandom rnd;\n    private final UuidGenerator uuidGenerator;\n\n    @Inject\n    public ApiKeyDao(@MainDB Configuration cfg, SecureRandom rnd, UuidGenerator uuidGenerator) {\n        super(cfg);\n        this.rnd = requireNonNull(rnd);\n        this.uuidGenerator = requireNonNull(uuidGenerator);\n    }\n\n    public String newApiKey() {\n        byte[] ab = new byte[16];\n        rnd.nextBytes(ab);\n\n        Encoder e = Base64.getEncoder().withoutPadding();\n        return e.encodeToString(ab);\n    }\n\n    public UUID getId(UUID userId, String keyName) {\n        return dsl().select(API_KEYS.KEY_ID)\n                .from(API_KEYS)\n                .where(API_KEYS.USER_ID.eq(userId)\n                        .and(API_KEYS.KEY_NAME.eq(keyName)))\n                .fetchOne(API_KEYS.KEY_ID);\n    }\n\n    public List<ApiKeyEntry> list(UUID userId) {\n        return dsl().select(\n                API_KEYS.KEY_ID,\n                API_KEYS.USER_ID,\n                API_KEYS.KEY_NAME,\n                API_KEYS.EXPIRED_AT)\n                .from(API_KEYS)\n                .where(API_KEYS.USER_ID.eq(userId))\n                .orderBy(API_KEYS.KEY_NAME)\n                .fetch(ApiKeyDao::toEntry);\n    }\n\n    public UUID insert(UUID userId, String key, String name, OffsetDateTime expiredAt) {\n        UUID keyId = uuidGenerator.generate();\n        return txResult(tx -> tx.insertInto(API_KEYS)\n                .columns(API_KEYS.KEY_ID, API_KEYS.USER_ID, API_KEYS.API_KEY, API_KEYS.KEY_NAME, API_KEYS.EXPIRED_AT)\n                .values(keyId, userId, hash(key), name, expiredAt)\n                .returning(API_KEYS.KEY_ID)\n                .fetchOne()\n                .getKeyId());\n    }\n\n    public void update(UUID keyId, String key, OffsetDateTime expiredAt) {\n        tx(tx -> tx.update(API_KEYS)\n                .set(API_KEYS.API_KEY, hash(key))\n                .set(API_KEYS.EXPIRED_AT, expiredAt)\n                .where(API_KEYS.KEY_ID.eq(keyId))\n                .execute());\n    }\n\n    public void delete(UUID id) {\n        tx(tx -> tx.deleteFrom(API_KEYS)\n                .where(API_KEYS.KEY_ID.eq(id))\n                .execute());\n    }\n\n    public UUID getUserId(UUID id) {\n        return dsl().select(API_KEYS.USER_ID)\n                .from(API_KEYS)\n                .where(API_KEYS.KEY_ID.eq(id))\n                .fetchOne(API_KEYS.USER_ID);\n    }\n\n    public ApiKeyEntry find(String key) {\n        return dsl().select(API_KEYS.KEY_ID, API_KEYS.USER_ID, API_KEYS.KEY_NAME, API_KEYS.EXPIRED_AT)\n                .from(API_KEYS)\n                .where(API_KEYS.API_KEY.eq(hash(key))\n                        .and(API_KEYS.EXPIRED_AT.isNull()\n                                .or(API_KEYS.EXPIRED_AT.greaterThan(currentOffsetDateTime()))))\n                .fetchOne(ApiKeyDao::toEntry);\n    }\n\n    public int count(UUID userId) {\n        return dsl().fetchCount(selectFrom(API_KEYS).where(API_KEYS.USER_ID.eq(userId)));\n    }\n\n    private static ApiKeyEntry toEntry(Record4<UUID, UUID, String, OffsetDateTime> r) {\n        return new ApiKeyEntry(r.value1(), r.value2(), r.value3(), r.value4());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyEntry.java",
    "content": "package com.walmartlabs.concord.server.security.apikey;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\n\nimport java.io.Serializable;\nimport java.time.OffsetDateTime;\nimport java.util.UUID;\n\n@JsonInclude(Include.NON_NULL)\npublic class ApiKeyEntry implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final UUID id;\n\n    private final UUID userId;\n\n    @ConcordKey\n    private final String name;\n\n    private final OffsetDateTime expiredAt;\n\n    @JsonCreator\n    public ApiKeyEntry(@JsonProperty(\"id\") UUID id,\n                       @JsonProperty(\"userId\") UUID userId,\n                       @JsonProperty(\"name\") String name,\n                       @JsonProperty(\"expiredAt\") OffsetDateTime expiredAt) {\n\n        this.id = id;\n        this.userId = userId;\n        this.name = name;\n        this.expiredAt = expiredAt;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public UUID getUserId() {\n        return userId;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public OffsetDateTime getExpiredAt() {\n        return expiredAt;\n    }\n\n    @Override\n    public String toString() {\n        return \"ApiKeyEntry{\" +\n                \"id=\" + id +\n                \", userId=\" + userId +\n                \", name='\" + name + '\\'' +\n                \", expiredAt=\" + expiredAt +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyExpirationNotifier.java",
    "content": "package com.walmartlabs.concord.server.security.apikey;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.github.mustachejava.DefaultMustacheFactory;\nimport com.github.mustachejava.Mustache;\nimport com.github.mustachejava.MustacheFactory;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.cfg.ApiKeyConfiguration;\nimport com.walmartlabs.concord.server.sdk.ScheduledTask;\nimport com.walmartlabs.concord.server.user.UserDao;\nimport org.jooq.Configuration;\nimport org.jooq.Record4;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.io.StringWriter;\nimport java.text.SimpleDateFormat;\nimport java.time.OffsetDateTime;\nimport java.util.*;\nimport java.util.concurrent.TimeUnit;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.API_KEYS;\nimport static org.jooq.impl.DSL.currentOffsetDateTime;\nimport static org.jooq.impl.DSL.trunc;\n\npublic class ApiKeyExpirationNotifier implements ScheduledTask {\n\n    private static final Logger log = LoggerFactory.getLogger(ApiKeyExpirationNotifier.class);\n\n    private static final String EMAIL_SUBJECT = \"Your Concord API Token Is Expiring\";\n    private static final long POLL_INTERVAL = TimeUnit.DAYS.toSeconds(1);\n    private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat(\"yyyy-MM-dd\"));\n\n    private final ApiKeyConfiguration cfg;\n    private final ExpiredKeysDao dao;\n    private final UserDao userDao;\n    private final EmailNotifier notifier;\n\n    @Inject\n    public ApiKeyExpirationNotifier(ApiKeyConfiguration cfg,\n                                    ExpiredKeysDao dao,\n                                    UserDao userDao,\n                                    EmailNotifier notifier) {\n\n        this.cfg = cfg;\n        this.dao = dao;\n        this.userDao = userDao;\n        this.notifier = notifier;\n    }\n\n    @Override\n    public String getId() {\n        return \"api-key-expiration-notifier\";\n    }\n\n    @Override\n    public long getIntervalInSec() {\n        return cfg.isExpirationEnabled() ? POLL_INTERVAL : 0;\n    }\n\n    @Override\n    public void performTask() {\n        for (int days : cfg.getNotifyBeforeDays()) {\n            List<ApiKeyEntry> keys = dao.poll(days);\n\n            Map<UUID, List<ApiKeyEntry>> keysByUser = new HashMap<>();\n            keys.forEach(e -> keysByUser.computeIfAbsent(e.userId, k -> new ArrayList<>()).add(e));\n\n            for (Map.Entry<UUID, List<ApiKeyEntry>> e : keysByUser.entrySet()) {\n                UUID userId = e.getKey();\n                if (sendNotification(userId, days, e.getValue())) {\n                    List<UUID> keyIds = e.getValue().stream().map(k -> k.id).collect(Collectors.toList());\n                    dao.markNotified(keyIds, OffsetDateTime.now());\n                }\n            }\n\n            log.info(\"performTask -> {} keys, for days {}\", keys.size(), days);\n        }\n    }\n\n    private boolean sendNotification(UUID userId, int days, List<ApiKeyEntry> keys) {\n        String userEmail = userDao.getEmail(userId);\n        if (userEmail == null) {\n            log.info(\"sendNotification ['{}'] -> user email not found\", userId);\n            return true;\n        }\n\n        return notifier.send(userEmail, EMAIL_SUBJECT, getMessage(days, keys));\n    }\n\n    private String getMessage(int days, List<ApiKeyEntry> keys) {\n        try (InputStreamReader in = new InputStreamReader(this.getClass().getResourceAsStream(\"/com/walmartlabs/concord/server/email/api-key-expiration.mustache\"))) {\n            MustacheFactory mf = new DefaultMustacheFactory();\n            Mustache mustache = mf.compile(in, \"api-key-notifier\");\n\n            Map<String, Object> ctx = new HashMap<>();\n            ctx.put(\"days\", days);\n            ctx.put(\"keys\", keys.stream().map(e -> {\n                Map<String, String> r = new HashMap<>();\n                r.put(\"name\", e.getName());\n                r.put(\"expiredAt\", DATE_FORMAT.get().format(e.getExpiredAt()));\n                return r;\n            }).collect(Collectors.toList()));\n\n            StringWriter out = new StringWriter();\n            mustache.execute(out, ctx);\n            return out.toString();\n        } catch (IOException e) {\n            log.error(\"getMessage -> error\", e);\n            throw new RuntimeException(\"get message error: \" + e.getMessage());\n        }\n    }\n\n    private static class ExpiredKeysDao extends AbstractDao {\n\n        @Inject\n        public ExpiredKeysDao(@MainDB Configuration cfg) {\n            super(cfg);\n        }\n\n        public List<ApiKeyEntry> poll(int days) {\n            return txResult(tx ->\n                    tx.select(API_KEYS.KEY_ID,\n                            API_KEYS.KEY_NAME,\n                            API_KEYS.EXPIRED_AT,\n                            API_KEYS.USER_ID)\n                            .from(API_KEYS)\n                            .where(API_KEYS.EXPIRED_AT.isNotNull()\n                                    .and(currentOffsetDateTime().greaterOrEqual(trunc(API_KEYS.EXPIRED_AT).minus(days))\n                                            .and(API_KEYS.LAST_NOTIFIED_AT.isNull()\n                                                    .or(API_KEYS.LAST_NOTIFIED_AT.lessOrEqual(API_KEYS.EXPIRED_AT.minus(days))))))\n                            .fetch(this::toEntry));\n        }\n\n        public void markNotified(List<UUID> keyIds, OffsetDateTime date) {\n            tx(tx -> tx.update(API_KEYS)\n                    .set(API_KEYS.LAST_NOTIFIED_AT, date)\n                    .where(API_KEYS.KEY_ID.in(keyIds))\n                    .execute());\n        }\n\n        private ApiKeyEntry toEntry(Record4<UUID, String, OffsetDateTime, UUID> r) {\n            return new ApiKeyEntry(r.value1(), r.value2(), r.value3(), r.value4());\n        }\n    }\n\n    private static class ApiKeyEntry {\n\n        private final UUID id;\n        private final String name;\n        private final OffsetDateTime expiredAt;\n        private final UUID userId;\n\n        public ApiKeyEntry(UUID id, String name, OffsetDateTime expiredAt, UUID userId) {\n            this.id = id;\n            this.name = name;\n            this.expiredAt = expiredAt;\n            this.userId = userId;\n        }\n\n        public UUID getId() {\n            return id;\n        }\n\n        public String getName() {\n            return name;\n        }\n\n        public OffsetDateTime getExpiredAt() {\n            return expiredAt;\n        }\n\n        public UUID getUserId() {\n            return userId;\n        }\n\n        @Override\n        public String toString() {\n            return \"ApiKeyEntry{\" +\n                    \"id=\" + id +\n                    \", name='\" + name + '\\'' +\n                    \", expiredAt=\" + expiredAt +\n                    \", userId=\" + userId +\n                    '}';\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyManager.java",
    "content": "package com.walmartlabs.concord.server.security.apikey;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.db.PgUtils;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.audit.AuditAction;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.audit.AuditObject;\nimport com.walmartlabs.concord.server.cfg.ApiKeyConfiguration;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.Permission;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport com.walmartlabs.concord.server.user.UserType;\nimport org.jooq.exception.DataAccessException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\nimport java.util.List;\nimport java.util.UUID;\n\nimport static java.util.Objects.requireNonNull;\n\npublic class ApiKeyManager {\n\n    private static final Logger log = LoggerFactory.getLogger(ApiKeyManager.class);\n\n    private final ApiKeyDao apiKeyDao;\n    private final AuditLog auditLog;\n    private final ApiKeyConfiguration cfg;\n    private final UserManager userManager;\n\n    @Inject\n    public ApiKeyManager(ApiKeyConfiguration cfg,\n                         UserManager userManager,\n                         ApiKeyDao apiKeyDao,\n                         AuditLog auditLog) {\n\n        this.cfg = requireNonNull(cfg);\n        this.userManager = requireNonNull(userManager);\n        this.apiKeyDao = requireNonNull(apiKeyDao);\n        this.auditLog = requireNonNull(auditLog);\n    }\n\n    public CreateApiKeyResponse create(CreateApiKeyRequest req) {\n        String key = assertKeyValue(req);\n\n        UUID userId = assertUserId(req.getUserId());\n        if (userId == null) {\n            userId = assertUsername(req.getUsername(), req.getUserDomain(), req.getUserType());\n        }\n\n        if (userId == null) {\n            userId = UserPrincipal.assertCurrent().getId();\n        }\n\n        assertOwner(userId);\n\n        String name = trim(req.getName());\n        if (name == null || name.isEmpty()) {\n            // auto generate the name\n            name = \"key-\" + UUID.randomUUID();\n        } else {\n            if (!name.matches(ConcordKey.PATTERN)) {\n                throw new ValidationErrorsException(\"Invalid API key name. Must match \" + ConcordKey.PATTERN);\n            }\n\n            if (apiKeyDao.getId(userId, name) != null) {\n                throw new ValidationErrorsException(\"API key with name '\" + name + \"' already exists\");\n            }\n        }\n\n        return createApiKey(userId, name, key);\n    }\n\n    public CreateApiKeyResponse createOrUpdate(CreateApiKeyRequest req) {\n        String key = assertKeyValue(req);\n\n        UUID userId = assertUserId(req.getUserId());\n        if (userId == null) {\n            userId = assertUsername(req.getUsername(), req.getUserDomain(), req.getUserType());\n        }\n\n        if (userId == null) {\n            userId = UserPrincipal.assertCurrent().getId();\n        }\n\n        assertOwner(userId);\n\n        String name = trim(req.getName());\n        if (name == null || name.isEmpty()) {\n            // auto generate the name\n            name = \"key#\" + UUID.randomUUID();\n        }\n\n        UUID apiKeyId = apiKeyDao.getId(userId, name);\n        if (apiKeyId == null) {\n            return createApiKey(userId, name, key);\n        } else {\n            return updateApiKey(apiKeyId, userId, name, key);\n        }\n    }\n\n    public CreateApiKeyResponse createApiKey(UUID userId, String name, @Nullable String key) {\n        if (key == null) {\n            key = apiKeyDao.newApiKey();\n        }\n\n        OffsetDateTime expiredAt = null;\n        if (cfg.isExpirationEnabled()) {\n            expiredAt = OffsetDateTime.now().plusDays(cfg.getExpirationPeriod().toDays());\n        }\n\n        UUID id;\n        try {\n            id = apiKeyDao.insert(userId, key, name, expiredAt);\n        } catch (DataAccessException e) {\n            if (PgUtils.isUniqueViolationError(e)) {\n                log.warn(\"create ['{}'] -> duplicate name error: {}\", name, e.getMessage());\n                throw new ValidationErrorsException(\"Duplicate API key name: \" + name);\n            }\n\n            throw e;\n        }\n\n        auditLog.add(AuditObject.API_KEY, AuditAction.CREATE)\n                .field(\"id\", id)\n                .field(\"name\", name)\n                .field(\"expiredAt\", expiredAt)\n                .field(\"userId\", userId)\n                .log();\n\n        return new CreateApiKeyResponse(id, name, key, OperationResult.CREATED);\n    }\n\n    public CreateApiKeyResponse updateApiKey(UUID id, UUID userId, String name, @Nullable String key) {\n        if (key == null) {\n            key = apiKeyDao.newApiKey();\n        }\n\n        OffsetDateTime expiredAt = null;\n        if (cfg.isExpirationEnabled()) {\n            expiredAt = OffsetDateTime.now().plusDays(cfg.getExpirationPeriod().toDays());\n        }\n\n        apiKeyDao.update(id, key, expiredAt);\n\n        auditLog.add(AuditObject.API_KEY, AuditAction.UPDATE)\n                .field(\"id\", id)\n                .field(\"name\", name)\n                .field(\"expiredAt\", expiredAt)\n                .field(\"userId\", userId)\n                .log();\n\n        return new CreateApiKeyResponse(id, name, key, OperationResult.UPDATED);\n    }\n\n    public void deleteById(UUID id) {\n        UUID userId = apiKeyDao.getUserId(id);\n        if (userId == null) {\n            throw new ValidationErrorsException(\"API key not found: \" + id);\n        }\n\n        assertOwner(userId);\n\n        apiKeyDao.delete(id);\n\n        auditLog.add(AuditObject.API_KEY, AuditAction.DELETE)\n                .field(\"id\", id)\n                .log();\n    }\n\n    public List<ApiKeyEntry> list(@Nullable UUID userId) {\n        UUID effectiveUserId = userId;\n        if (effectiveUserId == null) {\n            effectiveUserId = UserPrincipal.assertCurrent().getId();\n        }\n\n        assertOwner(effectiveUserId);\n\n        return apiKeyDao.list(effectiveUserId);\n    }\n\n    private UUID assertUsername(String username, String domain, UserType type) {\n        if (username == null) {\n            return null;\n        }\n\n        if (type == null) {\n            type = UserPrincipal.assertCurrent().getType();\n        }\n\n        return userManager.getId(username, domain, type)\n                .orElseThrow(() -> new ConcordApplicationException(\"User not found: \" + username));\n    }\n\n    private UUID assertUserId(UUID userId) {\n        if (userId == null) {\n            return null;\n        }\n\n        if (userManager.get(userId).isEmpty()) {\n            throw new ValidationErrorsException(\"User not found: \" + userId);\n        }\n\n        return userId;\n    }\n\n    private static String assertKeyValue(CreateApiKeyRequest req) {\n        String key = req.getKey();\n\n        if (key != null && !Permission.API_KEY_SPECIFY_VALUE.isPermitted()) {\n            throw new UnauthorizedException(\"Not allowed to specify the API key value.\");\n        }\n\n        return key;\n    }\n\n    private static void assertOwner(UUID userId) {\n        if (Roles.isAdmin()) {\n            // admin users can manage other user's keys\n            return;\n        }\n\n        UserPrincipal p = UserPrincipal.assertCurrent();\n        if (!userId.equals(p.getId())) {\n            throw new UnauthorizedException(\"Operation is not permitted\");\n        }\n    }\n\n    private static String trim(String s) {\n        if (s == null) {\n            return null;\n        }\n\n        return s.trim();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyModule.java",
    "content": "package com.walmartlabs.concord.server.security.apikey;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.walmartlabs.concord.server.sdk.BackgroundTask;\nimport com.walmartlabs.concord.server.security.apikey.loader.ApiKeyLoader;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\nimport static com.walmartlabs.concord.server.Utils.bindJaxRsResource;\nimport static com.walmartlabs.concord.server.Utils.bindSingletonScheduledTask;\n\npublic class ApiKeyModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        binder.bind(ApiKeyLoader.class).in(SINGLETON);\n        newSetBinder(binder, BackgroundTask.class).addBinding().to(ApiKeyLoader.class);\n\n        bindJaxRsResource(binder, ApiKeyResource.class);\n        bindJaxRsResource(binder, ApiKeyResourceV2.class);\n\n        bindSingletonScheduledTask(binder, ApiKeyCleaner.class);\n        bindSingletonScheduledTask(binder, ApiKeyExpirationNotifier.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyRealm.java",
    "content": "package com.walmartlabs.concord.server.security.apikey;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.audit.AuditAction;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.audit.AuditObject;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.security.AuthenticationException;\nimport com.walmartlabs.concord.server.security.SecurityUtils;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.user.UserEntry;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport org.apache.shiro.authc.AuthenticationInfo;\nimport org.apache.shiro.authc.AuthenticationToken;\nimport org.apache.shiro.authc.SimpleAccount;\nimport org.apache.shiro.authz.AuthorizationInfo;\nimport org.apache.shiro.realm.AuthorizingRealm;\nimport org.apache.shiro.subject.PrincipalCollection;\n\nimport javax.inject.Inject;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class ApiKeyRealm extends AuthorizingRealm {\n\n    private static final String REALM_NAME = \"apikey\";\n\n    private final UserManager userManager;\n    private final AuditLog auditLog;\n\n    @Inject\n    public ApiKeyRealm(UserManager userManager, AuditLog auditLog) {\n        this.userManager = userManager;\n        this.auditLog = auditLog;\n    }\n\n    @Override\n    public boolean supports(AuthenticationToken token) {\n        return token instanceof ApiKey;\n    }\n\n    @Override\n    @WithTimer\n    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {\n        ApiKey t = (ApiKey) token;\n\n        UserEntry u = null;\n        if (t.getUserId() != null) {\n            u = userManager.get(t.getUserId()).orElse(null);\n            if (u == null) {\n                return null;\n            }\n\n            if (u.isDisabled()) {\n                throw new AuthenticationException(\"User account '\" + u.getName() + \"' is disabled\");\n            }\n        }\n\n        auditLog.add(AuditObject.SYSTEM, AuditAction.ACCESS)\n                .userId(u != null ? u.getId() : null)\n                .field(\"realm\", REALM_NAME)\n                .field(\"apiKeyId\", t.getKeyId())\n                .log();\n\n        List<Object> principals = new ArrayList<>();\n        if (u != null) {\n            principals.add(new UserPrincipal(REALM_NAME, u));\n        }\n        principals.add(t);\n\n        return new SimpleAccount(principals, t.getKey(), getName());\n    }\n\n    @Override\n    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {\n        ApiKey principal = principals.oneByType(ApiKey.class);\n        if (principal == null) {\n            return null;\n        }\n        return SecurityUtils.toAuthorizationInfo(principals);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyResource.java",
    "content": "package com.walmartlabs.concord.server.security.apikey;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.server.GenericOperationResult;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.Roles;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.apache.shiro.authz.AuthorizationException;\n\nimport javax.inject.Inject;\nimport javax.validation.Valid;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport java.util.List;\nimport java.util.UUID;\n\nimport static java.util.Objects.requireNonNull;\n\n@Path(\"/api/v1/apikey\")\n@Tag(name = \"API keys\")\npublic class ApiKeyResource implements Resource {\n\n    private final ApiKeyDao apiKeyDao;\n    private final ApiKeyManager apiKeyManager;\n\n    @Inject\n    public ApiKeyResource(ApiKeyDao apiKeyDao, ApiKeyManager apiKeyManager) {\n        this.apiKeyDao = requireNonNull(apiKeyDao);\n        this.apiKeyManager = requireNonNull(apiKeyManager);\n    }\n\n    @GET\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"List user api keys\", operationId = \"listUserApiKeys\")\n    public List<ApiKeyEntry> list(@QueryParam(\"userId\") UUID userId) {\n        return apiKeyManager.list(userId);\n    }\n\n    @POST\n    @Path(\"/{name}\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Create a new API key\", operationId = \"createApiKey\")\n    public CreateApiKeyResponse create(@PathParam(\"name\") @ConcordKey String name) {\n        assertAdmin();\n\n        if (apiKeyDao.getId(null, name) != null) {\n            throw new ValidationErrorsException(\"API Token with name '\" + name + \"' already exists\");\n        }\n\n        return apiKeyManager.createApiKey(null, name, null);\n    }\n\n    @POST\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Create a new API key\", operationId = \"createUserApiKey\")\n    public CreateApiKeyResponse create(@Valid CreateApiKeyRequest req) {\n        return apiKeyManager.create(req);\n    }\n\n    @DELETE\n    @Path(\"/{id}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Delete an existing API key\", operationId = \"deleteUserApiKeyById\")\n    public GenericOperationResult deleteKeyById(@PathParam(\"id\") UUID id) {\n        apiKeyManager.deleteById(id);\n        return new GenericOperationResult(OperationResult.DELETED);\n    }\n\n    private static void assertAdmin() {\n        if (!Roles.isAdmin()) {\n            throw new AuthorizationException(\"Only admins are allowed to create API keys without users\");\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyResourceV2.java",
    "content": "package com.walmartlabs.concord.server.security.apikey;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.validation.Valid;\nimport javax.ws.rs.Consumes;\nimport javax.ws.rs.POST;\nimport javax.ws.rs.Path;\nimport javax.ws.rs.Produces;\nimport javax.ws.rs.core.MediaType;\n\nimport static java.util.Objects.requireNonNull;\n\n@Path(\"/api/v2/apikey\")\n@Tag(name = \"API keys V2\")\npublic class ApiKeyResourceV2 implements Resource {\n\n    private final ApiKeyManager apiKeyManager;\n\n    @Inject\n    public ApiKeyResourceV2(ApiKeyManager apiKeyManager) {\n        this.apiKeyManager = requireNonNull(apiKeyManager);\n    }\n\n    @POST\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Create a new API key or update an existing one\", operationId = \"createOrUpdateUserApiKey\")\n    public CreateApiKeyResponse createOrUpdate(@Valid CreateApiKeyRequest req) {\n        return apiKeyManager.createOrUpdate(req);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/ApiKeyUtils.java",
    "content": "package com.walmartlabs.concord.server.security.apikey;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.util.Base64;\n\npublic class ApiKeyUtils {\n\n    // TODO salt?\n    public static String hash(String s) {\n        MessageDigest md;\n        try {\n            md = MessageDigest.getInstance(\"SHA-256\");\n        } catch (NoSuchAlgorithmException e) {\n            throw new RuntimeException(e);\n        }\n\n        byte[] ab = Base64.getDecoder().decode(s);\n        ab = md.digest(ab);\n\n        return Base64.getEncoder().withoutPadding().encodeToString(ab);\n    }\n\n    private ApiKeyUtils() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/CreateApiKeyRequest.java",
    "content": "package com.walmartlabs.concord.server.security.apikey;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.server.user.UserType;\n\nimport java.io.Serial;\nimport java.io.Serializable;\nimport java.util.UUID;\n\npublic class CreateApiKeyRequest implements Serializable {\n\n    @Serial\n    private static final long serialVersionUID = 1L;\n\n    private final UUID userId;\n    private final String username;\n    private final String userDomain;\n    private final UserType userType;\n    private final String name;\n    private final String key;\n\n    @JsonCreator\n    public CreateApiKeyRequest(@JsonProperty(\"userId\") UUID userId,\n                               @JsonProperty(\"username\") String username,\n                               @JsonProperty(\"userDomain\") String userDomain,\n                               @JsonProperty(\"userType\") UserType userType,\n                               @JsonProperty(\"name\") String name,\n                               @JsonProperty(\"key\") String key) {\n        this.userId = userId;\n        this.username = username;\n        this.userDomain = userDomain;\n        this.userType = userType;\n        this.name = name;\n        this.key = key;\n    }\n\n    public UUID getUserId() {\n        return userId;\n    }\n\n    public String getUsername() {\n        return username;\n    }\n\n    public String getUserDomain() {\n        return userDomain;\n    }\n\n    public UserType getUserType() {\n        return userType;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public String getKey() {\n        return key;\n    }\n\n    @Override\n    public String toString() {\n        return \"CreateApiKeyRequest{\" +\n                \"userId=\" + userId +\n                \", username='\" + username + '\\'' +\n                \", userDomain='\" + userDomain + '\\'' +\n                \", userType=\" + userType +\n                \", name='\" + name + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/CreateApiKeyResponse.java",
    "content": "package com.walmartlabs.concord.server.security.apikey;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.server.OperationResult;\n\nimport java.io.Serial;\nimport java.io.Serializable;\nimport java.util.UUID;\n\npublic class CreateApiKeyResponse implements Serializable {\n\n    @Serial\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n    private final UUID id;\n    private final String name;\n    private final String key;\n    private final OperationResult result;\n\n    @JsonCreator\n    public CreateApiKeyResponse(@JsonProperty(\"id\") UUID id,\n                                @JsonProperty(\"name\") String name,\n                                @JsonProperty(\"key\") String key,\n                                @JsonProperty(\"result\") OperationResult result) {\n        this.id = id;\n        this.name = name;\n        this.key = key;\n        this.result = result;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public String getKey() {\n        return key;\n    }\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    public OperationResult getResult() {\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"CreateApiKeyResponse{\" +\n                \"ok=\" + ok +\n                \", result='\" + result + '\\'' +\n                \", id=\" + id +\n                \", name=\" + name +\n                \", key='\" + key + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/EmailNotifier.java",
    "content": "package com.walmartlabs.concord.server.security.apikey;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.cfg.EmailNotifierConfiguration;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.mail.Message;\nimport javax.mail.Session;\nimport javax.mail.Transport;\nimport javax.mail.internet.InternetAddress;\nimport javax.mail.internet.MimeMessage;\nimport java.util.Properties;\n\npublic class EmailNotifier {\n\n    private static final Logger log = LoggerFactory.getLogger(EmailNotifier.class);\n\n    private final Properties mailProperties;\n    private final String from;\n    private final boolean enabled;\n\n    @Inject\n    public EmailNotifier(EmailNotifierConfiguration cfg) {\n        this.mailProperties = buildProperties(cfg);\n        this.from = cfg.getFrom();\n        this.enabled = cfg.isEnabled();\n    }\n\n    private Properties buildProperties(EmailNotifierConfiguration cfg) {\n        if (!cfg.isEnabled()) {\n            return new Properties();\n        }\n\n        Properties result = new Properties();\n        result.setProperty(\"mail.smtp.host\", cfg.getHost());\n        result.setProperty(\"mail.smtp.port\", Integer.toString(cfg.getPort()));\n        result.setProperty(\"mail.smtp.timeout\", Long.toString(cfg.getReadTimeout().toMillis()));\n        result.setProperty(\"mail.smtp.connectiontimeout\", Long.toString(cfg.getConnectTimeout().toMillis()));\n        return result;\n    }\n\n    public boolean send(String to, String subject, String text) {\n        if (!enabled) {\n            log.info(\"send ['{}', '{}'] -> email notifications are disabled\", to, subject);\n            return true;\n        }\n\n        try {\n            Session session = Session.getInstance(mailProperties);\n\n            Message message = new MimeMessage(session);\n            message.setFrom(new InternetAddress(from));\n            message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));\n            message.setSubject(subject);\n            message.setText(text);\n\n            Transport.send(message);\n            return true;\n        } catch (Exception e) {\n            log.error(\"send ['{}', '{}'] -> error\", to, subject, e);\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/loader/ApiKeyEntry.java",
    "content": "package com.walmartlabs.concord.server.security.apikey.loader;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.time.OffsetDateTime;\n\n@Value.Immutable\n@JsonSerialize(as = ImmutableApiKeyEntry.class)\n@JsonDeserialize(as = ImmutableApiKeyEntry.class)\npublic interface ApiKeyEntry extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    String username();\n\n    String keyName();\n\n    String value();\n\n    @Nullable\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    OffsetDateTime expiredAt();\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/loader/ApiKeyLoader.java",
    "content": "package com.walmartlabs.concord.server.security.apikey.loader;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.server.cfg.ApiKeyConfiguration;\nimport com.walmartlabs.concord.server.sdk.BackgroundTask;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\n\npublic class ApiKeyLoader implements BackgroundTask {\n\n    private static final Logger log = LoggerFactory.getLogger(ApiKeyLoader.class);\n\n    private static final TypeReference<List<ApiKeyEntry>> LIST_API_KEY_ENTRIES = new TypeReference<List<ApiKeyEntry>>() {\n    };\n\n    private final Path loadFrom;\n    private final ObjectMapper objectMapper;\n    private final ApiKeyLoaderDao dao;\n\n    @Inject\n    public ApiKeyLoader(ApiKeyConfiguration cfg, ObjectMapper objectMapper, ApiKeyLoaderDao dao) {\n        this.loadFrom = cfg.getLoadFrom();\n        this.objectMapper = objectMapper;\n        this.dao = dao;\n    }\n\n    @Override\n    public void start() {\n        if (this.loadFrom == null) {\n            return;\n        }\n\n        log.info(\"Loading user API keys from {}...\", this.loadFrom);\n        try (InputStream in = Files.newInputStream(this.loadFrom)) {\n            List<ApiKeyEntry> entries = objectMapper.readValue(in, LIST_API_KEY_ENTRIES);\n            dao.upsert(entries);\n        } catch (IOException e) {\n            log.error(\"Error while loading the API keys file: {}\", e.getMessage());\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/apikey/loader/ApiKeyLoaderDao.java",
    "content": "package com.walmartlabs.concord.server.security.apikey.loader;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\nimport java.util.List;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.tables.ApiKeys.API_KEYS;\nimport static com.walmartlabs.concord.server.jooq.tables.Users.USERS;\nimport static com.walmartlabs.concord.server.security.apikey.ApiKeyUtils.hash;\nimport static java.util.Objects.requireNonNull;\n\npublic class ApiKeyLoaderDao extends AbstractDao {\n\n    private static final Logger log = LoggerFactory.getLogger(ApiKeyLoaderDao.class);\n\n    @Inject\n    public ApiKeyLoaderDao(@MainDB Configuration cfg) {\n        super(cfg);\n    }\n\n    public void upsert(List<ApiKeyEntry> entries) {\n        tx(tx -> {\n            for (ApiKeyEntry entry : entries) {\n                String username = requireNonNull(entry.username(), \"'username' is null\");\n                String keyName = requireNonNull(entry.keyName(), \"'keyName' is null\");\n                String value = requireNonNull(entry.value(), \"'value' is null\");\n                OffsetDateTime expiredAt = entry.expiredAt();\n\n                UUID userId = getUserId(tx, username);\n                if (userId == null) {\n                    log.warn(\"User not found '{}', skipping...\", username);\n                    continue;\n                }\n\n                log.info(\"Updating API key '{}' for user '{}'...\", keyName, username);\n\n                UUID keyId = getKeyId(tx, userId, keyName);\n                if (keyId != null) {\n                    deleteKey(tx, keyId);\n                }\n\n                insertKey(tx, userId, keyName, value, expiredAt);\n            }\n        });\n    }\n\n    private static UUID getUserId(DSLContext tx, String username) {\n        List<UUID> uuids = tx.select(USERS.USER_ID).from(USERS)\n                .where(USERS.USERNAME.eq(username))\n                .fetch(USERS.USER_ID);\n\n        if (uuids.isEmpty()) {\n            return null;\n        }\n\n        if (uuids.size() != 1) {\n            throw new IllegalStateException(\"Non-unique username: \" + username);\n        }\n\n        return uuids.get(0);\n    }\n\n    private static UUID getKeyId(DSLContext tx, UUID userId, String keyName) {\n        return tx.select(API_KEYS.KEY_ID).from(API_KEYS)\n                .where(API_KEYS.USER_ID.eq(userId)\n                        .and(API_KEYS.KEY_NAME.eq(keyName)))\n                .fetchOne(API_KEYS.KEY_ID);\n    }\n\n    private static void deleteKey(DSLContext tx, UUID keyId) {\n        tx.deleteFrom(API_KEYS).where(API_KEYS.KEY_ID.eq(keyId)).execute();\n    }\n\n    private static void insertKey(DSLContext tx, UUID userId, String keyName, String value, OffsetDateTime expiredAt) {\n        tx.insertInto(API_KEYS)\n                .columns(API_KEYS.USER_ID, API_KEYS.API_KEY, API_KEYS.KEY_NAME, API_KEYS.EXPIRED_AT)\n                .values(userId, hash(value), keyName, expiredAt)\n                .execute();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/github/GithubKey.java",
    "content": "package com.walmartlabs.concord.server.security.github;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.security.SecurityUtils;\nimport org.apache.shiro.authc.AuthenticationToken;\n\nimport java.util.UUID;\n\npublic class GithubKey implements AuthenticationToken {\n\n    public static GithubKey getCurrent() {\n        return SecurityUtils.getCurrent(GithubKey.class);\n    }\n\n    private static final long serialVersionUID = 1L;\n\n    private final String key;\n    private final UUID projectId;\n    private final String repoToken;\n\n    public GithubKey(String key, UUID projectId, String repoToken) {\n        this.key = key;\n        this.projectId = projectId;\n        this.repoToken = repoToken;\n    }\n\n    public String getKey() {\n        return key;\n    }\n\n    public UUID getProjectId() {\n        return projectId;\n    }\n\n    public String getRepoToken() {\n        return repoToken;\n    }\n\n    @Override\n    public Object getPrincipal() {\n        return getKey();\n    }\n\n    @Override\n    public Object getCredentials() {\n        return key != null ? key : repoToken;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/github/GithubRealm.java",
    "content": "package com.walmartlabs.concord.server.security.github;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.security.AuthenticationException;\nimport com.walmartlabs.concord.server.security.SecurityUtils;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport org.apache.shiro.authc.AuthenticationInfo;\nimport org.apache.shiro.authc.AuthenticationToken;\nimport org.apache.shiro.authc.SimpleAccount;\nimport org.apache.shiro.authz.AuthorizationInfo;\nimport org.apache.shiro.realm.AuthorizingRealm;\nimport org.apache.shiro.subject.PrincipalCollection;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.util.Arrays;\nimport java.util.UUID;\n\npublic class GithubRealm extends AuthorizingRealm {\n\n    private static final Logger log = LoggerFactory.getLogger(GithubRealm.class);\n\n    private static final String REALM_NAME = \"github\";\n    private static final UUID USER_ID = UUID.fromString(\"acc17a02-b471-46af-9914-48cba3dd31ab\"); // as in v0.47.0.xml\n\n    private final UserManager userManager;\n\n    @Inject\n    public GithubRealm(UserManager userManager) {\n        this.userManager = userManager;\n    }\n\n    @Override\n    public boolean supports(AuthenticationToken token) {\n        return token instanceof GithubKey;\n    }\n\n    @Override\n    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {\n        GithubKey t = (GithubKey) token;\n\n        try {\n            return userManager.get(USER_ID)\n                    .map(u -> {\n                        UserPrincipal p = new UserPrincipal(REALM_NAME, u);\n                        return new SimpleAccount(Arrays.asList(p, t), t.getCredentials(), getName());\n                    })\n                    .orElse(null);\n        } catch (Exception e) {\n            log.error(\"doGetAuthenticationInfo -> error\", e);\n            throw e;\n        }\n    }\n\n    @Override\n    @WithTimer\n    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {\n        UserPrincipal p = principals.oneByType(UserPrincipal.class);\n        if (p == null || !REALM_NAME.equals(p.getRealm())) {\n            return null;\n        }\n\n        return SecurityUtils.toAuthorizationInfo(principals);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/internal/InternalRealm.java",
    "content": "package com.walmartlabs.concord.server.security.internal;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.security.AuthenticationException;\nimport com.walmartlabs.concord.server.security.SecurityUtils;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport org.apache.shiro.authc.AuthenticationInfo;\nimport org.apache.shiro.authc.AuthenticationToken;\nimport org.apache.shiro.authz.AuthorizationInfo;\nimport org.apache.shiro.realm.AuthorizingRealm;\nimport org.apache.shiro.subject.PrincipalCollection;\n\npublic class InternalRealm extends AuthorizingRealm {\n\n    public static final String REALM_NAME = \"internal\";\n\n    @Override\n    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {\n        // we don't need the authentication part\n        throw new IllegalStateException(\"Not implemented\");\n    }\n\n    @Override\n    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {\n        UserPrincipal p = principals.oneByType(UserPrincipal.class);\n        if (p == null || !REALM_NAME.equals(p.getRealm())) {\n            return null;\n        }\n\n        return SecurityUtils.toAuthorizationInfo(principals);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/internal/LocalUserInfoProvider.java",
    "content": "package com.walmartlabs.concord.server.security.internal;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.user.AbstractUserInfoProvider;\nimport com.walmartlabs.concord.server.user.UserDao;\nimport com.walmartlabs.concord.server.user.UserType;\n\nimport javax.inject.Inject;\nimport java.util.Set;\nimport java.util.UUID;\n\npublic class LocalUserInfoProvider extends AbstractUserInfoProvider {\n    \n    @Inject\n    public LocalUserInfoProvider(UserDao userDao) {\n        super(userDao);\n    }\n\n    @Override\n    public UserType getUserType() {\n        return UserType.LOCAL;\n    }\n\n    @Override\n    public UserInfo getInfo(UUID id, String username, String userDomain) {\n        return getInfo(id, username, userDomain, UserType.LOCAL);\n    }\n\n    @Override\n    public UUID create(String username, String domain, String displayName, String email, Set<String> roles) {\n        return create(username, domain, displayName, email, roles, UserType.LOCAL);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/ldap/CachingLdapManager.java",
    "content": "package com.walmartlabs.concord.server.security.ldap;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.cache.CacheBuilder;\nimport com.google.common.cache.CacheLoader;\nimport com.google.common.cache.LoadingCache;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nonnull;\nimport javax.annotation.Nullable;\nimport javax.naming.NamingException;\nimport java.time.Duration;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.concurrent.TimeUnit;\n\npublic class CachingLdapManager implements LdapManager {\n\n    private final LdapManager delegate;\n\n    private final LoadingCache<CacheKey, Optional<LdapPrincipal>> principalByName;\n    private final LoadingCache<String, Optional<LdapPrincipal>> principalByDn;\n    private final LoadingCache<String, Optional<LdapPrincipal>> principalByMail;\n\n    public CachingLdapManager(Duration cacheDuration,\n                              LdapManager delegate) {\n\n        this.delegate = delegate;\n        this.principalByName = CacheBuilder.newBuilder()\n                .expireAfterWrite(cacheDuration.toMillis(), TimeUnit.MILLISECONDS)\n                .concurrencyLevel(32)\n                .recordStats()\n                .build(new CacheLoader<CacheKey, Optional<LdapPrincipal>>() {\n                    @Override\n                    public Optional<LdapPrincipal> load(@Nonnull CacheKey key) throws Exception {\n                        return Optional.ofNullable(delegate.getPrincipal(key.userName(), key.domain()));\n                    }\n                });\n\n        this.principalByDn = CacheBuilder.newBuilder()\n                .expireAfterWrite(cacheDuration.toMillis(), TimeUnit.MILLISECONDS)\n                .concurrencyLevel(32)\n                .recordStats()\n                .build(new CacheLoader<String, Optional<LdapPrincipal>>() {\n                    @Override\n                    public Optional<LdapPrincipal> load(@Nonnull String key) throws Exception {\n                        return Optional.ofNullable(delegate.getPrincipalByDn(key));\n                    }\n                });\n\n        this.principalByMail = CacheBuilder.newBuilder()\n                .expireAfterWrite(cacheDuration.toMillis(), TimeUnit.MILLISECONDS)\n                .concurrencyLevel(32)\n                .recordStats()\n                .build(new CacheLoader<String, Optional<LdapPrincipal>>() {\n                    @Override\n                    public Optional<LdapPrincipal> load(String key) throws Exception {\n                        return Optional.ofNullable(delegate.getPrincipalByMail(key));\n                    }\n                });\n    }\n\n    @Override\n    public List<LdapGroupSearchResult> searchGroups(String filter) throws NamingException {\n        return delegate.searchGroups(filter);\n    }\n\n    @Override\n    public Set<String> getGroups(String username, String domain) throws NamingException {\n        return delegate.getGroups(username, domain);\n    }\n\n    @Override\n    public LdapPrincipal getPrincipal(String username, String domain) throws Exception {\n        return principalByName.get(CacheKey.of(username, domain)).orElse(null);\n    }\n\n    @Override\n    public LdapPrincipal getPrincipalByDn(String dn) throws Exception {\n        return principalByDn.get(dn).orElse(null);\n    }\n\n    @Override\n    public LdapPrincipal getPrincipalByMail(String email) throws Exception {\n        return principalByMail.get(email).orElse(null);\n    }\n\n    @Value.Immutable\n    interface CacheKey {\n\n        String userName();\n\n        @Nullable\n        String domain();\n\n        static CacheKey of(String userName, String domain) {\n            return ImmutableCacheKey.builder()\n                    .userName(userName)\n                    .domain(domain)\n                    .build();\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/ldap/ConcordDnsSrvLdapContextFactory.java",
    "content": "package com.walmartlabs.concord.server.security.ldap;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.cfg.LdapConfiguration;\nimport org.apache.shiro.realm.ldap.JndiLdapContextFactory;\nimport org.apache.shiro.realm.ldap.LdapContextFactory;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.naming.CommunicationException;\nimport javax.naming.Context;\nimport javax.naming.NamingEnumeration;\nimport javax.naming.NamingException;\nimport javax.naming.directory.Attribute;\nimport javax.naming.directory.Attributes;\nimport javax.naming.directory.DirContext;\nimport javax.naming.directory.InitialDirContext;\nimport javax.naming.ldap.LdapContext;\nimport java.util.Hashtable;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\n\npublic class ConcordDnsSrvLdapContextFactory implements LdapContextFactory {\n\n    private LdapContextFactory delegate;\n\n    private static final Logger log = LoggerFactory.getLogger(ConcordDnsSrvLdapContextFactory.class);\n\n    private static final String PROTOCOL = \"ldaps\";\n    private static final String PORT = \"3269\";\n    private static final int MAX_RETRY = 100;\n\n    private final LdapConfiguration cfg;\n    private final Lock mutex = new ReentrantLock();\n\n    private Iterator<String> ldapUrlIterator;\n\n    /*\n    1. check dnsSRV and refresh SRV list\n    2. pic one and create LDAP context\n    3. while getContext check for communication exception and update context with next URL in SRV list\n    4. on SRV list empty, call step 1.\n     */\n    public ConcordDnsSrvLdapContextFactory(LdapConfiguration cfg) {\n        this.cfg = cfg;\n        this.setLdapContextFactory(getNewContextInstance(resolveUrl()));\n    }\n\n    public String getCurrentLdapUrl() {\n        return ((JndiLdapContextFactory) this.delegate).getUrl();\n    }\n\n    public void setLdapContextFactory(LdapContextFactory ldapContextFactory) {\n        mutex.lock();\n        try {\n            this.delegate = ldapContextFactory;\n        } finally {\n            mutex.unlock();\n        }\n    }\n\n    public void setLdapUrlIterator(Iterator<String> ldapUrlIterator) {\n        mutex.lock();\n        try {\n            this.ldapUrlIterator = ldapUrlIterator;\n        } finally {\n            mutex.unlock();\n        }\n    }\n\n    @Override\n    public LdapContext getSystemLdapContext() {\n        try {\n            return withRetry(() -> this.delegate.getSystemLdapContext());\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public LdapContext getLdapContext(Object principal, Object credentials) {\n        try {\n            return withRetry(() -> this.delegate.getLdapContext(principal, credentials));\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private <T> T withRetry(Callable<T> f) throws Exception {\n        int tryCount = 0;\n        while (tryCount < MAX_RETRY) {\n            try {\n                return f.call();\n            } catch (CommunicationException e) {\n                handleCommunicationException(e);\n            }\n            tryCount++;\n        }\n        throw new IllegalStateException(\"Too deep, max retries limit reached with ldap url identifier\");\n    }\n\n    public void refreshSRVList() throws NamingException {\n        if (cfg.getDnsSRVName() != null) {\n            this.setLdapUrlIterator(getLdapServers(cfg.getDnsSRVName()).iterator());\n        }\n    }\n\n    private JndiLdapContextFactory getNewContextInstance(String ldapUrl) {\n        if (ldapUrl == null || ldapUrl.isEmpty()) {\n            log.error(\"LDAP Url is null or empty\");\n            throw new RuntimeException(\"An LDAP URL must be specified of the form ldap://<hostname>:<port>\");\n        }\n        JndiLdapContextFactory f = new JndiLdapContextFactory() {\n            @Override\n            protected LdapContext createLdapContext(Hashtable env) throws NamingException {\n                String url = getCurrentLdapUrl();\n                if (url != null && url.startsWith(PROTOCOL) && cfg.isTrustAllCertificates()) {\n                    env.put(\"java.naming.ldap.factory.socket\", TrustingSslSocketFactory.class.getName());\n                }\n\n                env.put(\"com.sun.jndi.ldap.read.timeout\", Long.toString(cfg.getConnectTimeout().toMillis()));\n                env.put(\"com.sun.jndi.ldap.connect.timeout\", Long.toString(cfg.getReadTimeout().toMillis()));\n\n                return super.createLdapContext(env);\n            }\n        };\n\n        f.setSystemUsername(cfg.getSystemUsername());\n        f.setSystemPassword(cfg.getSystemPassword());\n        f.setPoolingEnabled(true);\n        log.info(\"Connecting to ldap server: \" + ldapUrl);\n        f.setUrl(ldapUrl);\n        return f;\n    }\n\n    private List<String> getLdapServers(String dnsSRVName) throws NamingException {\n        CopyOnWriteArrayList<String> servers = new CopyOnWriteArrayList<>();\n        Hashtable<String, String> env = new Hashtable<>();\n        env.put(Context.INITIAL_CONTEXT_FACTORY, \"com.sun.jndi.dns.DnsContextFactory\");\n        env.put(Context.PROVIDER_URL, \"dns:\");\n        DirContext ctx = new InitialDirContext(env);\n        Attributes srvs = ctx.getAttributes(dnsSRVName, new String[]{\"SRV\"});\n        NamingEnumeration<? extends Attribute> srv = srvs.getAll();\n        while (srv.hasMore()) {\n            Attribute srvRecords = (Attribute) srv.next();\n            NamingEnumeration<?> srvRecord = srvRecords.getAll();\n            while (srvRecord.hasMore()) {\n                String attr = (String) srvRecord.next();\n                servers.add(PROTOCOL + \"://\" + removeLastCharIfDot(attr.split(\" \")[3]) + \":\" + PORT);\n            }\n        }\n        return servers;\n    }\n\n    private static String removeLastCharIfDot(String s) {\n        if (s == null || s.length() == 0 || s.charAt(s.length() - 1) != '.') {\n            return s;\n        }\n        return s.substring(0, s.length() - 1);\n    }\n\n    private String resolveUrl() {\n        if (cfg.getDnsSRVName() != null) {\n            String ldapUrl = getNextLdapUrl();\n            if (ldapUrl != null) {\n                return ldapUrl;\n            }\n        }\n        if (cfg.getUrl() != null) {\n            return cfg.getUrl();\n        }\n        return null;\n    }\n\n    private String getNextLdapUrl() {\n        mutex.lock();\n        try {\n            if (ldapUrlIterator != null && ldapUrlIterator.hasNext()) {\n                return ldapUrlIterator.next();\n            }\n            return null;\n        } finally {\n            mutex.unlock();\n        }\n    }\n\n    private void handleCommunicationException(Exception e) throws NamingException {\n        if (cfg.getDnsSRVName() == null) {\n            log.error(\"Failed to communicate with ldap server: \" + getCurrentLdapUrl());\n            throw new RuntimeException(e);\n        }\n\n        if (this.ldapUrlIterator != null && !this.ldapUrlIterator.hasNext()) {\n            this.refreshSRVList();\n        }\n\n        this.setLdapContextFactory(getNewContextInstance(getNextLdapUrl()));\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/ldap/ConcordLdapContextFactory.java",
    "content": "package com.walmartlabs.concord.server.security.ldap;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.cfg.LdapConfiguration;\nimport org.apache.shiro.realm.ldap.JndiLdapContextFactory;\nimport org.apache.shiro.realm.ldap.LdapContextFactory;\n\nimport javax.naming.NamingException;\nimport javax.naming.ldap.LdapContext;\nimport java.util.Hashtable;\n\npublic class ConcordLdapContextFactory implements LdapContextFactory {\n    private final LdapContextFactory delegate;\n\n    @SuppressWarnings(\"unchecked\")\n    public ConcordLdapContextFactory(LdapConfiguration cfg) {\n        JndiLdapContextFactory f = new JndiLdapContextFactory() {\n            @Override\n            protected LdapContext createLdapContext(Hashtable env) throws NamingException {\n                String url = cfg.getUrl();\n                if (url != null && url.startsWith(\"ldaps:\") && cfg.isTrustAllCertificates()) {\n                    env.put(\"java.naming.ldap.factory.socket\", TrustingSslSocketFactory.class.getName());\n                }\n\n                env.put(\"com.sun.jndi.ldap.read.timeout\", Long.toString(cfg.getConnectTimeout().toMillis()));\n                env.put(\"com.sun.jndi.ldap.connect.timeout\", Long.toString(cfg.getReadTimeout().toMillis()));\n                return super.createLdapContext(env);\n            }\n        };\n        f.setUrl(cfg.getUrl());\n        f.setSystemUsername(cfg.getSystemUsername());\n        f.setSystemPassword(cfg.getSystemPassword());\n        f.setPoolingEnabled(true);\n        this.delegate = f;\n    }\n\n    @Override\n    public LdapContext getSystemLdapContext() throws NamingException {\n        return delegate.getSystemLdapContext();\n    }\n\n    @Override\n    public LdapContext getLdapContext(Object principal, Object credentials) throws NamingException {\n        return delegate.getLdapContext(principal, credentials);\n    }\n\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/ldap/LdapContextFactoryProvider.java",
    "content": "package com.walmartlabs.concord.server.security.ldap;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.cfg.LdapConfiguration;\nimport org.apache.shiro.realm.ldap.LdapContextFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Provider;\nimport javax.naming.NamingException;\n\npublic class LdapContextFactoryProvider implements Provider<LdapContextFactory> {\n\n    private final LdapConfiguration cfg;\n\n    @Inject\n    public LdapContextFactoryProvider(LdapConfiguration cfg) {\n        this.cfg = cfg;\n    }\n\n    @Override\n    public LdapContextFactory get() {\n        if (cfg.getDnsSRVName() != null) {\n            ConcordDnsSrvLdapContextFactory factory = new ConcordDnsSrvLdapContextFactory(cfg);\n            try {\n                factory.refreshSRVList();\n            } catch (NamingException e) {\n                throw new RuntimeException(\"ConcordDnsSrvLdapContextFactory init error\", e);\n            }\n            return factory;\n        } else {\n            return new ConcordLdapContextFactory(cfg);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/ldap/LdapGroupDao.java",
    "content": "package com.walmartlabs.concord.server.security.ldap;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport org.jooq.*;\n\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\nimport java.util.Set;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.USERS;\nimport static com.walmartlabs.concord.server.jooq.Tables.USER_LDAP_GROUPS;\nimport static org.jooq.impl.DSL.currentOffsetDateTime;\nimport static org.jooq.impl.DSL.value;\n\npublic class LdapGroupDao extends AbstractDao {\n\n    @Inject\n    public LdapGroupDao(@MainDB Configuration cfg) {\n        super(cfg);\n    }\n\n    public void updateIfNeeded(UUID userId, Set<String> groups, Field<OffsetDateTime> cutOff) {\n        tx(tx -> {\n            Record1<Integer> r = tx.select(value(1)).from(USERS).where(USERS.USER_ID.eq(userId)\n                    .and(USERS.LAST_GROUP_SYNC_DT.isNull()\n                            .or(USERS.LAST_GROUP_SYNC_DT.lessThan(cutOff))))\n                    .forUpdate()\n                    .fetchOne();\n\n            if (r == null) {\n                return;\n            }\n\n            updateGroups(tx, userId, groups);\n            updateLastSyncTimestamp(tx, userId);\n        });\n    }\n\n    public void update(UUID userId, Set<String> groups) {\n        tx(tx -> {\n            updateGroups(tx, userId, groups);\n            updateLastSyncTimestamp(tx, userId);\n        });\n    }\n\n    private void updateGroups(DSLContext tx, UUID userId, Set<String> groups) {\n        tx.deleteFrom(USER_LDAP_GROUPS).where(USER_LDAP_GROUPS.USER_ID.eq(userId))\n                .execute();\n\n        if (groups.isEmpty()) {\n            return;\n        }\n\n        BatchBindStep q = tx.batch(tx.insertInto(USER_LDAP_GROUPS, USER_LDAP_GROUPS.USER_ID, USER_LDAP_GROUPS.LDAP_GROUP)\n                .values((UUID) null, null));\n\n        for (String g : groups) {\n            q.bind(value(userId), value(g));\n        }\n\n        q.execute();\n    }\n    \n    private void updateLastSyncTimestamp(DSLContext tx, UUID userId) {\n        tx.update(USERS).set(USERS.LAST_GROUP_SYNC_DT, currentOffsetDateTime())\n                .where(USERS.USER_ID.eq(userId))\n                .execute();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/ldap/LdapGroupManager.java",
    "content": "package com.walmartlabs.concord.server.security.ldap;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.PgUtils;\nimport com.walmartlabs.concord.server.cfg.LdapGroupSyncConfiguration;\nimport org.jooq.Field;\n\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\nimport java.util.Set;\nimport java.util.UUID;\n\npublic class LdapGroupManager {\n\n    private final LdapGroupSyncConfiguration syncCfg;\n    private final LdapGroupDao groupDao;\n\n    @Inject\n    public LdapGroupManager(LdapGroupSyncConfiguration syncCfg, LdapGroupDao groupDao) {\n        this.syncCfg = syncCfg;\n        this.groupDao = groupDao;\n    }\n\n    public void cacheLdapGroupsIfNeeded(UUID userId, Set<String> groups) {\n        Field<OffsetDateTime> cutoff = PgUtils.nowMinus(syncCfg.getMinAgeLogin());\n        groupDao.updateIfNeeded(userId, groups, cutoff);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/ldap/LdapGroupSearchResult.java",
    "content": "package com.walmartlabs.concord.server.security.ldap;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableLdapGroupSearchResult.class)\n@JsonDeserialize(as = ImmutableLdapGroupSearchResult.class)\npublic interface LdapGroupSearchResult extends Serializable {\n\n    String groupName();\n\n    String displayName();\n\n    static ImmutableLdapGroupSearchResult.Builder builder() {\n        return ImmutableLdapGroupSearchResult.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/ldap/LdapManager.java",
    "content": "package com.walmartlabs.concord.server.security.ldap;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.naming.NamingException;\nimport java.util.List;\nimport java.util.Set;\n\npublic interface LdapManager {\n\n    List<LdapGroupSearchResult> searchGroups(String filter) throws NamingException;\n\n    Set<String> getGroups(String username, String domain) throws NamingException;\n\n    LdapPrincipal getPrincipal(String username, String domain) throws Exception;\n\n    LdapPrincipal getPrincipalByDn(String dn) throws Exception;\n\n    LdapPrincipal getPrincipalByMail(String email) throws Exception;\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/ldap/LdapManagerImpl.java",
    "content": "package com.walmartlabs.concord.server.security.ldap;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.cfg.LdapConfiguration;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport org.apache.shiro.realm.ldap.LdapContextFactory;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.naming.NamingEnumeration;\nimport javax.naming.NamingException;\nimport javax.naming.directory.Attribute;\nimport javax.naming.directory.Attributes;\nimport javax.naming.directory.SearchControls;\nimport javax.naming.directory.SearchResult;\nimport javax.naming.ldap.LdapContext;\nimport java.util.*;\nimport java.util.function.Function;\n\npublic class LdapManagerImpl implements LdapManager {\n\n    private static final Logger log = LoggerFactory.getLogger(LdapManagerImpl.class);\n\n    private static final String MEMBER_OF_ATTR = \"memberOf\"; // TODO move to cfg\n    private static final String DISPLAY_NAME_ATTR = \"displayName\"; // TODO move to cfg\n\n    private final LdapConfiguration cfg;\n    private final LdapContextFactory ctxFactory;\n\n    public LdapManagerImpl(LdapConfiguration cfg,\n                           LdapContextFactory ctxFactory) {\n\n        this.cfg = cfg;\n        this.ctxFactory = ctxFactory;\n    }\n\n    @Override\n    public List<LdapGroupSearchResult> searchGroups(String filter) throws NamingException {\n        return search(filter, cfg.getGroupSearchFilter(), new String[]{cfg.getGroupNameProperty(), cfg.getGroupDisplayNameProperty()},\n                attrs -> {\n                    String groupName = attrs.get(cfg.getGroupNameProperty());\n                    if (groupName == null) {\n                        return null;\n                    }\n                    return LdapGroupSearchResult.builder()\n                            .groupName(groupName)\n                            .displayName(attrs.getOrDefault(cfg.getGroupDisplayNameProperty(), \"n/a\"))\n                            .build();\n                });\n    }\n\n    @Override\n    public Set<String> getGroups(String username, String domain) throws NamingException {\n        LdapContext ctx = null;\n        try {\n            ctx = ctxFactory.getSystemLdapContext();\n            return getGroups(ctx, username, domain);\n        } catch (Exception e) {\n            log.warn(\"getGroups ['{}'] -> error while retrieving LDAP data: {}\", username, e.getMessage(), e);\n            throw e;\n        } finally {\n            LdapUtils.closeContext(ctx);\n        }\n    }\n\n    private Set<String> getGroups(LdapContext ctx, String username, String domain) throws NamingException {\n        SearchControls ctls = new SearchControls();\n        ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);\n        ctls.setReturningAttributes(new String[]{MEMBER_OF_ATTR});\n\n        Object[] args = new Object[]{normalizeDomain(username, domain), username, domain};\n        NamingEnumeration<SearchResult> answer = ctx.search(cfg.getSearchBase(), cfg.getPrincipalSearchFilter(), args, ctls);\n        if (!answer.hasMoreElements()) {\n            return null;\n        }\n\n        Set<String> result = new HashSet<>();\n        while (answer.hasMoreElements()) {\n            SearchResult sr = answer.next();\n\n            Attributes attrs = sr.getAttributes();\n            if (attrs != null) {\n                NamingEnumeration<? extends Attribute> ae = attrs.getAll();\n                while (ae.hasMore()) {\n                    Attribute attr = ae.next();\n                    if (MEMBER_OF_ATTR.equals(attr.getID())) {\n                        result.addAll(LdapUtils.getAllAttributeValues(attr));\n                        break;\n                    }\n                }\n            }\n        }\n        return result;\n    }\n\n    @WithTimer\n    @Override\n    public LdapPrincipal getPrincipal(String username, String domain) throws NamingException {\n        LdapContext ctx = null;\n        try {\n            ctx = ctxFactory.getSystemLdapContext();\n            return getPrincipal(new SearchFn(ctx) {\n\n                @Override\n                public NamingEnumeration<SearchResult> lookup(SearchControls ctls) throws NamingException {\n                    Object[] args = new Object[]{normalizeDomain(username, domain), username, domain};\n                    return ctx.search(cfg.getSearchBase(), cfg.getPrincipalSearchFilter(), args, ctls);\n                }\n\n                @Override\n                public void handleNonUniqueResult() {\n                    log.error(\"getPrincipal ['{}', '{}'] -> non unique results\", username, domain);\n                    throw new RuntimeException(\"LDAP error, non unique result found for username: '\" + username + \"'. \" +\n                            \"Try using a fully-qualified username.\");\n                }\n            });\n        } catch (Exception e) {\n            log.warn(\"getPrincipal ['{}', '{}'] -> error while retrieving LDAP data: {}\", username, domain, e.getMessage(), e);\n            throw e;\n        } finally {\n            LdapUtils.closeContext(ctx);\n        }\n    }\n\n    @Override\n    public LdapPrincipal getPrincipalByDn(String dn) throws NamingException {\n        int idx = dn.indexOf(\",\");\n        if (idx < 0 || idx + 1 > dn.length()) {\n            throw new IllegalArgumentException(\"Invalid LDAP DN: \" + dn);\n        }\n\n        String searchDn = dn.substring(0, idx);\n        String baseDn = dn.substring(idx + 1);\n\n        LdapContext ctx = null;\n        try {\n            ctx = ctxFactory.getSystemLdapContext();\n            return getPrincipal(new SearchFn(ctx) {\n                @Override\n                public NamingEnumeration<SearchResult> lookup(SearchControls ctls) throws NamingException {\n                    return ctx.search(baseDn, searchDn, ctls);\n                }\n\n                @Override\n                public void handleNonUniqueResult() {\n                    log.error(\"getPrincipalByDn ['{}'] -> non unique results\", dn);\n                    throw new RuntimeException(\"LDAP error, non unique result found for DN: '\" + dn + \"'.\");\n                }\n            });\n        } catch (Exception e) {\n            log.warn(\"getPrincipalByDn ['{}'] -> error while retrieving LDAP data: {}\", dn, e.getMessage(), e);\n            throw e;\n        } finally {\n            LdapUtils.closeContext(ctx);\n        }\n    }\n\n    @Override\n    public LdapPrincipal getPrincipalByMail(String email) throws NamingException {\n        String searchDn = cfg.getSearchBase();\n        String mailAttr = \"(mail=\" + email + \")\";\n\n        LdapContext ctx = null;\n        try {\n            ctx = ctxFactory.getSystemLdapContext();\n            return getPrincipal(new SearchFn(ctx) {\n                @Override\n                public NamingEnumeration<SearchResult> lookup(SearchControls ctls) throws NamingException {\n                    return ctx.search(searchDn, mailAttr, ctls);\n                }\n\n                @Override\n                public void handleNonUniqueResult() {\n                    log.error(\"getPrincipalByMail ['{}'] -> non unique results\", email);\n                    throw new RuntimeException(\"LDAP error, non unique result found for email: '\" + email + \"'.\");\n                }\n            });\n        } catch (Exception e) {\n            log.warn(\"getPrincipalByMail ['{}'] -> error while retrieving LDAP data: {}\", email, e.getMessage(), e);\n            throw e;\n        } finally {\n            LdapUtils.closeContext(ctx);\n        }\n    }\n\n    private LdapPrincipal getPrincipal(SearchFn searchFn) throws NamingException {\n        SearchControls ctls = new SearchControls();\n        ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);\n        if (cfg.getReturningAttributes() != null && !cfg.getReturningAttributes().isEmpty()) {\n            ctls.setReturningAttributes(cfg.getReturningAttributes().toArray(new String[0]));\n        }\n\n        NamingEnumeration<SearchResult> answer = searchFn.lookup(ctls);\n        if (!answer.hasMoreElements()) {\n            return null;\n        }\n\n        LdapPrincipalBuilder b = new LdapPrincipalBuilder();\n\n        SearchResult sr = answer.next();\n        b.nameInNamespace(sr.getNameInNamespace());\n\n        Attributes attrs = sr.getAttributes();\n        if (attrs != null) {\n            NamingEnumeration<? extends Attribute> ae = attrs.getAll();\n            while (ae.hasMore()) {\n                Attribute attr = ae.next();\n                processAttribute(b, attr);\n            }\n        }\n\n        if (answer.hasMoreElements()) {\n            searchFn.handleNonUniqueResult();\n        }\n\n        return b.build();\n    }\n\n    private <E> List<E> search(String filter, String searchFilter, String[] returningAttributes, Function<Map<String, String>, E> converter) throws NamingException {\n        LdapContext ctx = null;\n        try {\n            ctx = ctxFactory.getSystemLdapContext();\n\n            SearchControls ctls = new SearchControls();\n            ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);\n            ctls.setReturningAttributes(returningAttributes);\n            ctls.setCountLimit(10);\n            Object[] args = new Object[]{filter};\n\n            NamingEnumeration<SearchResult> answer = ctx.search(cfg.getSearchBase(), searchFilter, args, ctls);\n            if (!answer.hasMoreElements()) {\n                return Collections.emptyList();\n            }\n\n            List<E> result = new ArrayList<>();\n            while (answer.hasMoreElements()) {\n                SearchResult sr = answer.next();\n                Attributes attrs = sr.getAttributes();\n                if (attrs != null) {\n                    NamingEnumeration<? extends Attribute> ae = attrs.getAll();\n                    Map<String, String> attributes = new HashMap<>();\n                    while (ae.hasMore()) {\n                        Attribute attr = ae.next();\n                        if (attr.size() == 0) {\n                            continue;\n                        }\n                        String id = attr.getID();\n                        attributes.put(id, attr.get().toString());\n                    }\n\n                    E item = converter.apply(attributes);\n                    if (item != null) {\n                        result.add(item);\n                    }\n                }\n            }\n            return result;\n        } finally {\n            LdapUtils.closeContext(ctx);\n        }\n    }\n\n    private void processAttribute(LdapPrincipalBuilder b, Attribute attr) throws NamingException {\n        String id = attr.getID();\n        Object v = attr.get();\n\n        boolean matched = false;\n        if (id.equals(cfg.getUserPrincipalNameProperty())) {\n            String upn = v.toString();\n            b.userPrincipalName(upn);\n            b.domain(getDomain(upn));\n            matched = true;\n        }\n\n        if (id.equals(cfg.getUsernameProperty())) {\n            String username = v.toString();\n            b.username(getUsername(username));\n            matched = true;\n        }\n\n        if (id.equals(cfg.getMailProperty())) {\n            b.email(v.toString());\n            b.addAttribute(id, v.toString());\n            matched = true;\n        }\n\n        if (matched) {\n            return;\n        }\n\n        switch (id) {\n            case MEMBER_OF_ATTR: {\n                Collection<String> names = LdapUtils.getAllAttributeValues(attr);\n                b.addGroups(names);\n                break;\n            }\n            case DISPLAY_NAME_ATTR: {\n                b.displayName(v.toString());\n                break;\n            }\n            default: {\n                boolean exclude = cfg.getExcludeAttributes().contains(id);\n                if (exclude) {\n                    return;\n                }\n                Set<String> exposedAttr = cfg.getExposeAttributes();\n                if (exposedAttr == null || exposedAttr.isEmpty() || exposedAttr.contains(id)) {\n                    Collection<String> values = LdapUtils.getAllAttributeValues(attr);\n                    if (values.size() == 1) {\n                        b.addAttribute(id, values.iterator().next());\n                    } else {\n                        b.addAttribute(id, values);\n                    }\n                }\n            }\n        }\n    }\n\n    private static String normalizeDomain(String username, String domain) {\n        if (domain == null) {\n            return username;\n        }\n\n        return username + \"@\" + domain;\n    }\n\n    private static String getDomain(String upn) {\n        int pos = upn.indexOf(\"@\");\n        if (pos > 0) {\n            return upn.substring(pos + 1);\n        }\n        return null;\n    }\n\n    private static String getUsername(String upn) {\n        int pos = upn.indexOf(\"@\");\n        if (pos > 0) {\n            return upn.substring(0, pos);\n        }\n        return upn;\n    }\n\n    private static abstract class SearchFn {\n\n        protected final LdapContext ctx;\n\n        protected SearchFn(LdapContext ctx) {\n            this.ctx = ctx;\n        }\n\n        public abstract NamingEnumeration<SearchResult> lookup(SearchControls ctls) throws NamingException;\n\n        public abstract void handleNonUniqueResult();\n    }\n\n    private static final class LdapPrincipalBuilder {\n\n        private String username;\n        private String domain;\n        private String nameInNamespace;\n        private String userPrincipalName;\n        private String displayName;\n        private String email;\n        private Set<String> groups;\n        private Map<String, Object> attributes;\n\n        public LdapPrincipalBuilder username(String username) {\n            this.username = username;\n            return this;\n        }\n\n        public LdapPrincipalBuilder domain(String domain) {\n            this.domain = domain;\n            return this;\n        }\n\n        public LdapPrincipalBuilder nameInNamespace(String nameInNamespace) {\n            this.nameInNamespace = nameInNamespace;\n            return this;\n        }\n\n        public LdapPrincipalBuilder userPrincipalName(String userPrincipalName) {\n            this.userPrincipalName = userPrincipalName;\n            return this;\n        }\n\n        public LdapPrincipalBuilder displayName(String displayName) {\n            this.displayName = displayName;\n            return this;\n        }\n\n        public LdapPrincipalBuilder email(String email) {\n            this.email = email;\n            return this;\n        }\n\n        public LdapPrincipalBuilder addGroups(Collection<String> names) {\n            if (groups == null) {\n                groups = new HashSet<>();\n            }\n            groups.addAll(names);\n            return this;\n        }\n\n        public LdapPrincipalBuilder addAttribute(String k, Object v) {\n            if (attributes == null) {\n                attributes = new HashMap<>();\n            }\n            attributes.put(k, v);\n            return this;\n        }\n\n        public LdapPrincipal build() {\n            if (groups == null) {\n                groups = Collections.emptySet();\n            }\n\n            if (attributes == null) {\n                attributes = Collections.emptyMap();\n            }\n\n            return new LdapPrincipal(normalize(username), normalize(domain), nameInNamespace, userPrincipalName, displayName, email, groups, attributes);\n        }\n\n        private static String normalize(String s) {\n            if (s == null) {\n                return null;\n            }\n\n            return s.trim().toLowerCase();\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/ldap/LdapManagerProvider.java",
    "content": "package com.walmartlabs.concord.server.security.ldap;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.cfg.LdapConfiguration;\nimport org.apache.shiro.realm.ldap.LdapContextFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Provider;\nimport java.time.Duration;\n\npublic class LdapManagerProvider implements Provider<LdapManager> {\n\n    private final LdapManager ldapManager;\n\n    @Inject\n    public LdapManagerProvider(LdapConfiguration cfg,\n                               LdapContextFactory ctxFactory) {\n\n        LdapManager manager = new LdapManagerImpl(cfg, ctxFactory);\n\n        Duration cacheDuration = cfg.getCacheDuration();\n        if (cacheDuration != null) {\n            this.ldapManager = new CachingLdapManager(cacheDuration, manager);\n        } else {\n            this.ldapManager = manager;\n        }\n    }\n\n    @Override\n    public LdapManager get() {\n        return ldapManager;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/ldap/LdapPrincipal.java",
    "content": "package com.walmartlabs.concord.server.security.ldap;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.security.SecurityUtils;\n\nimport java.io.Serializable;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * <b>Note:</b> this class is serialized when user principals are stored in\n * the process state. It must maintain backward compatibility.\n */\npublic class LdapPrincipal implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String username;\n    private final String domain;\n    private final String nameInNamespace;\n    private final String userPrincipalName;\n    private final String displayName;\n    private final String email;\n    private final Set<String> groups;\n    private final Map<String, Object> attributes;\n\n    public LdapPrincipal(String username,\n                         String domain,\n                         String nameInNamespace,\n                         String userPrincipalName,\n                         String displayName,\n                         String email,\n                         Set<String> groups,\n                         Map<String, Object> attributes) {\n\n        this.username = username;\n        this.domain = domain;\n        this.nameInNamespace = nameInNamespace;\n        this.userPrincipalName = userPrincipalName;\n        this.displayName = displayName;\n        this.email = email;\n        this.groups = groups;\n        this.attributes = attributes;\n    }\n\n    public static LdapPrincipal getCurrent() {\n        return SecurityUtils.getCurrent(LdapPrincipal.class);\n    }\n\n    public String getUsername() {\n        return username;\n    }\n\n    public String getDomain() {\n        return domain;\n    }\n\n    public String getNameInNamespace() {\n        return nameInNamespace;\n    }\n\n    public String getUserPrincipalName() {\n        return userPrincipalName;\n    }\n\n    public String getDisplayName() {\n        return displayName;\n    }\n\n    public String getEmail() {\n        return email;\n    }\n\n    public Set<String> getGroups() {\n        return groups;\n    }\n\n    public Map<String, Object> getAttributes() {\n        return attributes;\n    }\n\n    @Override\n    public String toString() {\n        return \"LdapPrincipal{\" +\n                \"username='\" + username + '\\'' +\n                \", domain='\" + domain + '\\'' +\n                \", nameInNamespace='\" + nameInNamespace + '\\'' +\n                \", userPrincipalName='\" + userPrincipalName + '\\'' +\n                \", displayName='\" + displayName + '\\'' +\n                \", email='\" + email + '\\'' +\n                \", groups=\" + groups +\n                \", attributes=\" + attributes +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/ldap/LdapRealm.java",
    "content": "package com.walmartlabs.concord.server.security.ldap;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.audit.AuditAction;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.audit.AuditObject;\nimport com.walmartlabs.concord.server.cfg.LdapConfiguration;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.security.SecurityUtils;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.user.UserEntry;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport com.walmartlabs.concord.server.user.UserType;\nimport org.apache.shiro.authc.*;\nimport org.apache.shiro.authz.AuthorizationInfo;\nimport org.apache.shiro.realm.ldap.AbstractLdapRealm;\nimport org.apache.shiro.realm.ldap.LdapContextFactory;\nimport org.apache.shiro.realm.ldap.LdapUtils;\nimport org.apache.shiro.subject.PrincipalCollection;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.naming.NamingException;\nimport javax.naming.ldap.LdapContext;\nimport java.util.Arrays;\nimport java.util.UUID;\n\npublic class LdapRealm extends AbstractLdapRealm {\n\n    private static final String REALM_NAME = \"ldap\";\n\n    private static final Logger log = LoggerFactory.getLogger(LdapRealm.class);\n\n    private final UserManager userManager;\n    private final LdapManager ldapManager;\n    private final LdapGroupManager ldapGroupManager;\n    private final LdapContextFactory ldapContextFactory;\n    private final AuditLog auditLog;\n\n    @Inject\n    public LdapRealm(LdapConfiguration cfg,\n                     UserManager userManager,\n                     LdapContextFactory ldapContextFactory,\n                     LdapManager ldapManager,\n                     LdapGroupManager ldapGroupManager,\n                     AuditLog auditLog) throws NamingException {\n\n        this.userManager = userManager;\n        this.ldapManager = ldapManager;\n        this.ldapGroupManager = ldapGroupManager;\n        this.auditLog = auditLog;\n        this.searchBase = cfg.getSearchBase();\n        this.systemUsername = cfg.getSystemUsername();\n        this.systemPassword = cfg.getSystemPassword();\n\n        setUrl(cfg.getSystemPassword() != null \n                ? (String) ldapContextFactory.getSystemLdapContext().getEnvironment().get(\"java.naming.provider.url\") \n                : cfg.getUrl());\n\n        setCachingEnabled(false);\n\n        setAuthenticationTokenClass(UsernamePasswordToken.class);\n\n        setCredentialsMatcher((token, info) -> {\n            SimpleAccount a = (SimpleAccount) info;\n\n            UsernamePasswordToken stored = (UsernamePasswordToken) a.getCredentials();\n            UsernamePasswordToken received = (UsernamePasswordToken) token;\n\n            return stored.getUsername().equals(received.getUsername()) &&\n                    Arrays.equals(stored.getPassword(), received.getPassword());\n        });\n\n        this.ldapContextFactory = ldapContextFactory;\n        setLdapContextFactory(ldapContextFactory);\n    }\n\n    @Override\n    @WithTimer\n    protected AuthenticationInfo queryForAuthenticationInfo(AuthenticationToken token, LdapContextFactory ldapContextFactory) throws NamingException {\n        if (this.url == null) {\n            return null;\n        }\n\n        UsernamePasswordToken t = (UsernamePasswordToken) token;\n\n        LdapPrincipal ldapPrincipal;\n        try {\n            ldapPrincipal = getPrincipal(t);\n        } catch (Exception e) {\n            throw new AuthenticationException(\"LDAP error while attempting to retrieve the user's principal: \" + t.getUsername(), e);\n        }\n\n        if (ldapPrincipal == null) {\n            throw new AuthenticationException(\"LDAP data not found: \" + t.getUsername());\n        }\n\n        // TODO merge getOrCreate+update operations into a single one (only for this use case)\n\n        UserEntry u = userManager.getOrCreate(ldapPrincipal.getUsername(), ldapPrincipal.getDomain(), UserType.LDAP)\n                .orElseThrow(() -> new ConcordApplicationException(\"User not found: \" + ldapPrincipal.getUsername()));\n\n        if (u.isDisabled()) {\n            throw new AuthenticationException(\"User account '\" + u.getName() + \"' is disabled\");\n        }\n\n        UUID userId = u.getId();\n\n        u = userManager.update(userId, ldapPrincipal.getDisplayName(), ldapPrincipal.getEmail(), UserType.LDAP, false, null)\n                .orElseThrow(() -> new RuntimeException(\"User record not found: \" + userId));\n\n        ldapGroupManager.cacheLdapGroupsIfNeeded(userId, ldapPrincipal.getGroups());\n\n        UserPrincipal userPrincipal = new UserPrincipal(REALM_NAME, u);\n\n        auditLog.add(AuditObject.SYSTEM, AuditAction.ACCESS)\n                .userId(userId)\n                .field(\"username\", u.getName())\n                .field(\"domain\", u.getDomain())\n                .field(\"realm\", REALM_NAME)\n                .log();\n\n        return new SimpleAccount(Arrays.asList(userPrincipal, t, ldapPrincipal), t, getName());\n    }\n\n    @SuppressWarnings(\"deprecation\")\n    private LdapPrincipal getPrincipal(UsernamePasswordToken t) throws Exception {\n        String username = t.getUsername();\n        char[] password = t.getPassword();\n        if (username == null || password == null) {\n            return null;\n        }\n\n        String[] usernameDomain = getUsernameDomain(t);\n        LdapPrincipal ldapPrincipal = ldapManager.getPrincipal(usernameDomain[0], usernameDomain[1]);\n        if (ldapPrincipal == null) {\n            throw new AuthenticationException(\"LDAP data not found: \" + username);\n        }\n\n        String principalName = ldapPrincipal.getUserPrincipalName();\n        if (principalName == null) {\n            principalName = ldapPrincipal.getNameInNamespace();\n        }\n\n        if (principalName == null) {\n            throw new NamingException(\"Can't determine the principal name of '\" + username + \"'\");\n        }\n\n        LdapContext ctx = null;\n        try {\n            ctx = ldapContextFactory.getLdapContext(principalName, new String(password));\n        } catch (Exception e) {\n            log.warn(\"queryForAuthenticationInfo -> '{}', failed: {}\", principalName, e.getMessage());\n            throw e;\n        } finally {\n            LdapUtils.closeContext(ctx);\n        }\n\n        return ldapPrincipal;\n    }\n\n    private String[] getUsernameDomain(UsernamePasswordToken t) {\n        String username = t.getUsername().trim();\n        int pos = username.indexOf(\"@\");\n        if (pos < 0) {\n            return new String[]{username, null};\n        }\n        return new String[]{username.substring(0, pos), username.substring(pos + 1)};\n    }\n\n    @Override\n    protected AuthorizationInfo queryForAuthorizationInfo(PrincipalCollection principals, LdapContextFactory ldapContextFactory) {\n        LdapPrincipal p = principals.oneByType(LdapPrincipal.class);\n        if (p == null) {\n            return null;\n        }\n\n        return SecurityUtils.toAuthorizationInfo(principals);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/ldap/LdapUserInfoProvider.java",
    "content": "package com.walmartlabs.concord.server.security.ldap;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.cfg.LdapConfiguration;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.user.UserDao;\nimport com.walmartlabs.concord.server.user.UserInfoProvider;\nimport com.walmartlabs.concord.server.user.UserType;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.util.Set;\nimport java.util.UUID;\n\npublic class LdapUserInfoProvider implements UserInfoProvider {\n\n    private static final Logger log = LoggerFactory.getLogger(LdapUserInfoProvider.class);\n\n    private final UserDao userDao;\n    private final LdapManager ldapManager;\n    private final LdapConfiguration cfg;\n\n    @Inject\n    public LdapUserInfoProvider(UserDao userDao,\n                                LdapManager ldapManager,\n                                LdapConfiguration cfg) {\n\n        this.userDao = userDao;\n        this.ldapManager = ldapManager;\n        this.cfg = cfg;\n    }\n\n    @Override\n    public UserType getUserType() {\n        return UserType.LDAP;\n    }\n\n    @Override\n    public UserInfo getInfo(UUID id, String username, String userDomain) {\n        try {\n            LdapPrincipal p = ldapManager.getPrincipal(username, userDomain);\n            return buildInfo(id, p);\n        } catch (Exception e) {\n            log.error(\"getInfo ['{}'] -> error\", username, e);\n            throw new ConcordApplicationException(\"Error while retrieving LDAP information for \" + username, e);\n        }\n    }\n\n    @Override\n    public UUID create(String username, String userDomain, String displayName, String email, Set<String> roles) {\n        if (!Roles.isAdmin() && !cfg.isAutoCreateUsers()) {\n            // unfortunately there's no easy way to throw a custom authentication error and keep the original message\n            // this will result in a 401 response with an empty body anyway\n            throw new ConcordApplicationException(\"Automatic creation of users is disabled.\");\n        }\n\n        UserInfo info = getInfo(null, username, userDomain);\n        if (info == null) {\n            throw new ConcordApplicationException(\"User '\" + username + \"' with domain '\" + userDomain + \"' not found in LDAP\");\n        }\n\n        return userDao.insertOrUpdate(info.username(), info.userDomain(), info.displayName(), info.email(), UserType.LDAP, roles);\n    }\n    \n    private static UserInfo buildInfo(UUID id, LdapPrincipal p) {\n        if (p == null) {\n            return null;\n        }\n\n        return UserInfo.builder()\n                .id(id)\n                .username(p.getUsername())\n                .userDomain(p.getDomain())\n                .displayName(p.getDisplayName())\n                .email(p.getEmail())\n                .groups(p.getGroups())\n                .attributes(p.getAttributes())\n                .build();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/ldap/LdapUtils.java",
    "content": "package com.walmartlabs.concord.server.security.ldap;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.naming.NamingEnumeration;\nimport javax.naming.NamingException;\nimport javax.naming.directory.Attribute;\nimport javax.naming.ldap.LdapContext;\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.Set;\n\npublic final class LdapUtils {\n\n    private static final Logger log = LoggerFactory.getLogger(LdapUtils.class);\n\n    public static Collection<String> getAllAttributeValues(Attribute attr) throws NamingException {\n        Set<String> values = new HashSet<>();\n        NamingEnumeration<?> ne = null;\n        try {\n            ne = attr.getAll();\n            while (ne.hasMore()) {\n                Object value = ne.next();\n                if (value instanceof String) {\n                    values.add((String) value);\n                }\n            }\n        } finally {\n            closeEnumeration(ne);\n        }\n\n        return values;\n    }\n\n    public static void closeContext(LdapContext ctx) {\n        try {\n            if (ctx != null) {\n                ctx.close();\n            }\n        } catch (NamingException e) {\n            log.error(\"closeContext -> error\", e);\n        }\n    }\n\n    public static void closeEnumeration(NamingEnumeration<?> ne) {\n        try {\n            if (ne != null) {\n                ne.close();\n            }\n        } catch (NamingException e) {\n            log.error(\"closeEnumeration -> {}, error\", ne, e);\n        }\n    }\n\n    private LdapUtils() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/ldap/SyncUserLdapGroupRequest.java",
    "content": "package com.walmartlabs.concord.server.security.ldap;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport javax.validation.constraints.NotNull;\nimport java.io.Serializable;\n\npublic class SyncUserLdapGroupRequest implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @NotNull\n    private final String username;\n\n    private final String userDomain;\n\n    @JsonCreator\n    public SyncUserLdapGroupRequest(@JsonProperty(\"username\") String username, @JsonProperty(\"userDomain\") String userDomain) {\n        this.username = username;\n        this.userDomain = userDomain;\n    }\n\n    public String getUsername() {\n        return username;\n    }\n\n    public String getUserDomain() {\n        return userDomain;\n    }\n\n    @Override\n    public String toString() {\n        return \"SyncUserLdapGroupRequest{\" +\n                \"username='\" + username + '\\'' +\n                \", userDomain='\" + userDomain + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/ldap/TrustingSslSocketFactory.java",
    "content": "package com.walmartlabs.concord.server.security.ldap;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.net.SocketFactory;\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.SSLSocketFactory;\nimport javax.net.ssl.TrustManager;\nimport javax.net.ssl.X509TrustManager;\nimport java.io.IOException;\nimport java.net.InetAddress;\nimport java.net.Socket;\nimport java.security.SecureRandom;\nimport java.security.cert.CertificateException;\nimport java.security.cert.X509Certificate;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\n\n/**\n * A socket factory that creates SSL sockets using {@link DummyTrustManager}.\n * It implements both connected and unconnected socket creation methods\n * to support, for example, connection timeout for LDAP connections.\n */\npublic class TrustingSslSocketFactory extends SocketFactory {\n\n    private static final Lock mutex = new ReentrantLock();\n\n    private final SSLSocketFactory delegate;\n\n    public static SocketFactory getDefault() {\n        mutex.lock();\n        try {\n            return new TrustingSslSocketFactory();\n        } finally {\n            mutex.unlock();\n        }\n    }\n\n    public TrustingSslSocketFactory() {\n        try {\n            SSLContext ctx = SSLContext.getInstance(\"TLS\");\n            ctx.init(null, new TrustManager[]{new DummyTrustManager()}, new SecureRandom());\n            delegate = ctx.getSocketFactory();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @Override\n    public Socket createSocket() throws IOException {\n        return delegate.createSocket();\n    }\n\n    @Override\n    public Socket createSocket(String s, int i) throws IOException {\n        return delegate.createSocket(s, i);\n    }\n\n    @Override\n    public Socket createSocket(String s, int i, InetAddress inetAddress, int i1) throws IOException {\n        return delegate.createSocket(s, i, inetAddress, i1);\n    }\n\n    @Override\n    public Socket createSocket(InetAddress inetAddress, int i) throws IOException {\n        return delegate.createSocket(inetAddress, i);\n    }\n\n    @Override\n    public Socket createSocket(InetAddress inetAddress, int i, InetAddress inetAddress1, int i1) throws IOException {\n        return delegate.createSocket(inetAddress, i, inetAddress1, i1);\n    }\n\n    private static class DummyTrustManager implements X509TrustManager {\n        public void checkClientTrusted(X509Certificate[] xcs, String string) throws CertificateException {  // NOSONAR\n        }\n\n        public void checkServerTrusted(X509Certificate[] xcs, String string) throws CertificateException { // NOSONAR\n        }\n\n        public X509Certificate[] getAcceptedIssuers() {\n            return new X509Certificate[0];\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/ldap/UserLdapGroupResource.java",
    "content": "package com.walmartlabs.concord.server.security.ldap;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.GenericOperationResult;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport com.walmartlabs.concord.server.user.UserInfoProvider;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport com.walmartlabs.concord.server.user.UserType;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.validation.Valid;\nimport javax.ws.rs.Consumes;\nimport javax.ws.rs.POST;\nimport javax.ws.rs.Path;\nimport javax.ws.rs.Produces;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport java.util.Collections;\nimport java.util.Set;\nimport java.util.UUID;\n\n@Path(\"/api/v1/userldapgroup\")\n@Tag(name = \"UserLdapGroup\")\npublic class UserLdapGroupResource implements Resource {\n\n    private final UserManager userManager;\n    private final LdapUserInfoProvider ldapUserInfoProvider;\n    private final LdapGroupDao ldapGroupsDao;\n    private final LdapManager ldapManager;\n\n    @Inject\n    public UserLdapGroupResource(UserManager userManager, LdapUserInfoProvider ldapUserInfoProvider, LdapGroupDao ldapGroupsDao, LdapManager ldapManager) {\n        this.userManager = userManager;\n        this.ldapUserInfoProvider = ldapUserInfoProvider;\n        this.ldapGroupsDao = ldapGroupsDao;\n        this.ldapManager = ldapManager;\n    }\n\n    /**\n     * Sync Ldap groups for a ldap user\n     *\n     * @param req user's data\n     * @return GenericOperationResult result\n     */\n    @POST\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Path(\"/sync\")\n    @Validate\n    @Operation(description = \"Sync ldap groups for a user\", operationId = \"syncLdapGroups\")\n    public GenericOperationResult sync(@Valid SyncUserLdapGroupRequest req) {\n        assertAdmin();\n\n        UUID id = userManager.getId(req.getUsername(), req.getUserDomain(), UserType.LDAP).orElse(null);\n        if (id == null) {\n            throw new ConcordApplicationException(\"User not found: \" + req.getUsername(), Response.Status.BAD_REQUEST);\n        }\n\n        UserInfoProvider.UserInfo info = ldapUserInfoProvider.getInfo(id, req.getUsername(), req.getUserDomain());\n        if (info == null) {\n            throw new ConcordApplicationException(\"User '\" + req.getUsername() + \"' with domain '\" + req.getUserDomain() + \"' not found in LDAP\", Response.Status.BAD_REQUEST);\n        }\n\n        try {\n            Set<String> groups = ldapManager.getGroups(req.getUsername(), req.getUserDomain());\n            if (groups == null) {\n                ldapGroupsDao.update(id, Collections.emptySet());\n            } else {\n                ldapGroupsDao.update(id, groups);\n            }\n        } catch (Exception e) {\n            throw new ConcordApplicationException(\"Failed to update groups for user '\" + req.getUsername() + \"' error -> '\" + e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR);\n        }\n\n        return new GenericOperationResult(OperationResult.UPDATED);\n    }\n\n    private static void assertAdmin() {\n        if (!Roles.isAdmin()) {\n            throw new UnauthorizedException(\"Only admins can do that\");\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/ldap/UserLdapGroupSynchronizer.java",
    "content": "package com.walmartlabs.concord.server.security.ldap;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.db.PgUtils;\nimport com.walmartlabs.concord.server.audit.ActionSource;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.cfg.LdapGroupSyncConfiguration;\nimport com.walmartlabs.concord.server.sdk.ScheduledTask;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport com.walmartlabs.concord.server.user.UserType;\nimport org.jooq.Configuration;\nimport org.jooq.Field;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.USERS;\nimport static org.jooq.impl.DSL.*;\n\n/**\n * Responsible for AD/LDAP group synchronization and enabling/disabling users\n * based on whether they have active groups or not.\n */\npublic class UserLdapGroupSynchronizer implements ScheduledTask {\n\n    private static final Logger log = LoggerFactory.getLogger(UserLdapGroupSynchronizer.class);\n\n    private static final String TASK_NAME = \"user-ldap-group-sync\";\n    private final Dao dao;\n    private final LdapGroupSyncConfiguration cfg;\n    private final LdapManager ldapManager;\n    private final LdapGroupDao ldapGroupsDao;\n    private final UserManager userManager;\n\n    @Inject\n    public UserLdapGroupSynchronizer(Dao dao, LdapGroupSyncConfiguration cfg,\n                                     LdapManager ldapManager,\n                                     LdapGroupDao ldapGroupsDao,\n                                     UserManager userManager) {\n\n        this.dao = dao;\n        this.cfg = cfg;\n        this.ldapManager = ldapManager;\n        this.userManager = userManager;\n        this.ldapGroupsDao = ldapGroupsDao;\n    }\n\n    @Override\n    public String getId() {\n        return TASK_NAME;\n    }\n\n    @Override\n    public long getIntervalInSec() {\n        return cfg.getInterval().getSeconds();\n    }\n\n    @Override\n    public void performTask() {\n        Field<OffsetDateTime> cutoff = PgUtils.nowMinus(cfg.getMinAgeSync());\n        Field<OffsetDateTime> disabledAge = cfg.getDisabledAge() != null ? PgUtils.nowMinus(cfg.getDisabledAge()) : null;\n\n        long usersCount = 0;\n        List<UserItem> users;\n        do {\n            users = dao.list(cfg.getFetchLimit(), cutoff, disabledAge);\n            users.forEach(this::processUser);\n            usersCount += users.size();\n        } while (users.size() >= cfg.getFetchLimit());\n\n        log.info(\"performTask -> done, {} user(s) synchronized\", usersCount);\n    }\n\n    private void processUser(UserItem u) {\n        try {\n            Set<String> groups = ldapManager.getGroups(u.username, u.domain);\n            if (groups == null) {\n                if (u.expired) {\n                    deleteUser(u.userId);\n                } else {\n                    ldapGroupsDao.update(u.userId, Collections.emptySet());\n                    disableUser(u.userId);\n                }\n            } else if (!u.permanentlyDisabled) {\n                enableUser(u.userId);\n                ldapGroupsDao.update(u.userId, groups);\n            }\n        } catch (Exception e) {\n            log.error(\"processUser ['{}'] -> error\", u.username, e);\n        }\n    }\n\n    private void enableUser(UUID userId) {\n        AuditLog.withActionSource(ActionSource.SYSTEM, Collections.singletonMap(\"task\", TASK_NAME),\n                () -> userManager.enable(userId));\n    }\n\n    private void disableUser(UUID userId) {\n        AuditLog.withActionSource(ActionSource.SYSTEM, Collections.singletonMap(\"task\", TASK_NAME),\n                () -> userManager.disable(userId));\n    }\n\n    private void deleteUser(UUID userId) {\n        AuditLog.withActionSource(ActionSource.SYSTEM, Collections.singletonMap(\"task\", TASK_NAME),\n                () -> userManager.delete(userId));\n    }\n\n    public static final class Dao extends AbstractDao {\n\n        @Inject\n        public Dao(@MainDB Configuration cfg) {\n            super(cfg);\n        }\n\n        List<UserItem> list(int limit, Field<OffsetDateTime> cutoff, Field<OffsetDateTime> disabledAge) {\n            Field<Boolean> expiredFiled = disabledAge == null ? inline(false) : field(nvl(USERS.DISABLED_DATE, currentOffsetDateTime()).lessThan(disabledAge));\n            return txResult(tx -> tx.select(USERS.USER_ID, USERS.USERNAME, USERS.DOMAIN, USERS.IS_PERMANENTLY_DISABLED, expiredFiled)\n                    .from(USERS)\n                    .where(USERS.USER_TYPE.eq(UserType.LDAP.name()))\n                    .and(USERS.LAST_GROUP_SYNC_DT.isNull().or(USERS.LAST_GROUP_SYNC_DT.lessThan(cutoff)))\n                    .limit(limit)\n                    .fetch(r -> new UserItem(r.value1(), r.value2(), r.value3(), r.value4(), r.value5())));\n        }\n    }\n\n    private static class UserItem {\n\n        private final UUID userId;\n        private final String username;\n        private final String domain;\n        private final boolean permanentlyDisabled;\n        private final boolean expired;\n\n        private UserItem(UUID userId, String username, String domain, boolean expired, boolean permanentlyDisabled) {\n            this.userId = userId;\n            this.username = username;\n            this.domain = domain;\n            this.expired = expired;\n            this.permanentlyDisabled = permanentlyDisabled;\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/rememberme/ConcordRememberMeManager.java",
    "content": "package com.walmartlabs.concord.server.security.rememberme;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.cfg.RememberMeConfiguration;\nimport com.walmartlabs.concord.server.security.SecurityUtils;\nimport com.walmartlabs.concord.server.security.apikey.ApiKey;\nimport org.apache.shiro.authc.UsernamePasswordToken;\nimport org.apache.shiro.lang.io.SerializationException;\nimport org.apache.shiro.lang.io.Serializer;\nimport org.apache.shiro.subject.PrincipalCollection;\nimport org.apache.shiro.subject.SimplePrincipalCollection;\nimport org.apache.shiro.subject.Subject;\nimport org.apache.shiro.web.mgt.CookieRememberMeManager;\nimport org.apache.shiro.web.util.WebUtils;\n\nimport javax.inject.Inject;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Optional;\n\n/**\n * Implementation of {@link org.apache.shiro.mgt.RememberMeManager}. Uses the DB to store session data.\n */\npublic class ConcordRememberMeManager extends CookieRememberMeManager {\n\n    @Inject\n    public ConcordRememberMeManager(RememberMeConfiguration cfg) {\n        byte[] cipherKey = cfg.getCipherKey();\n        if (cipherKey != null) {\n            if (cipherKey.length != 16 && cipherKey.length != 24 && cipherKey.length != 32) {\n                throw new IllegalArgumentException(\"Invalid rememberMe.cipherKey value: should be 16, 24 or 32 bytes length\");\n            }\n            setCipherKey(cipherKey);\n        }\n\n        int maxAge = (int) cfg.getRememberMeMaxAge().getSeconds();\n        getCookie().setMaxAge(maxAge);\n\n        setSerializer(new PrincipalCollectionSerializer());\n    }\n\n    @Override\n    protected void rememberIdentity(Subject subject, PrincipalCollection src) {\n        SimplePrincipalCollection dst = new SimplePrincipalCollection();\n\n        // keep only the specific types of principals to keep the cookie small\n        for (String realmName : src.getRealmNames()) {\n            Collection<?> principals = src.fromRealm(realmName);\n            for (Object p : principals) {\n                if (p instanceof UsernamePasswordToken || p instanceof ApiKey) {\n                    dst.add(p, realmName);\n                }\n            }\n        }\n\n        super.rememberIdentity(subject, dst);\n    }\n\n    @Override\n    protected void forgetIdentity(Subject subject) {\n        if (!WebUtils.isHttp(subject)) {\n            return;\n        }\n\n        // delete the \"remember me\" cookie only if it is present\n        var request = WebUtils.getHttpRequest(subject);\n        var rememberMeCookieName = getCookie().getName();\n\n        Optional.ofNullable(request.getCookies()).stream()\n                .flatMap(Arrays::stream)\n                .filter(cookie -> cookie.getName().equals(rememberMeCookieName))\n                .findFirst()\n                .ifPresent(cookie -> super.forgetIdentity(subject));\n    }\n\n    private static class PrincipalCollectionSerializer implements Serializer<PrincipalCollection> {\n        @Override\n        public byte[] serialize(PrincipalCollection principalCollection) throws SerializationException {\n            return SecurityUtils.serialize(principalCollection);\n        }\n\n        @Override\n        public PrincipalCollection deserialize(byte[] bytes) throws SerializationException {\n            return SecurityUtils.deserialize(bytes).orElse(null);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/sessionkey/SessionKey.java",
    "content": "package com.walmartlabs.concord.server.security.sessionkey;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.apache.shiro.authc.AuthenticationToken;\n\nimport java.util.UUID;\n\npublic class SessionKey implements AuthenticationToken {\n\n    private static final long serialVersionUID = 1L;\n\n    private final UUID instanceId;\n\n    public SessionKey(UUID instanceId) {\n        this.instanceId = instanceId;\n    }\n\n    public UUID getInstanceId() {\n        return instanceId;\n    }\n\n    @Override\n    public Object getPrincipal() {\n        return getInstanceId();\n    }\n\n    @Override\n    public Object getCredentials() {\n        return getInstanceId();\n    }\n\n    @Override\n    public String toString() {\n        return \"SessionKey{\" +\n                \"instanceId=\" + instanceId +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/sessionkey/SessionKeyPrincipal.java",
    "content": "package com.walmartlabs.concord.server.security.sessionkey;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.security.SecurityUtils;\n\npublic class SessionKeyPrincipal {\n\n    public static SessionKeyPrincipal getCurrent() {\n        return SecurityUtils.getCurrent(SessionKeyPrincipal.class);\n    }\n\n    private final PartialProcessKey processKey;\n\n    public SessionKeyPrincipal(PartialProcessKey processKey) {\n        this.processKey = processKey;\n    }\n\n    public PartialProcessKey getProcessKey() {\n        return processKey;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/security/sessionkey/SessionKeyRealm.java",
    "content": "package com.walmartlabs.concord.server.security.sessionkey;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.collect.ImmutableSet;\nimport com.walmartlabs.concord.server.process.ProcessSecurityContext;\nimport com.walmartlabs.concord.server.process.queue.ProcessInitiatorEntry;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueManager;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.security.AuthenticationException;\nimport org.apache.shiro.authc.AuthenticationInfo;\nimport org.apache.shiro.authc.AuthenticationToken;\nimport org.apache.shiro.authc.SimpleAccount;\nimport org.apache.shiro.authz.AuthorizationInfo;\nimport org.apache.shiro.authz.SimpleAuthorizationInfo;\nimport org.apache.shiro.realm.AuthorizingRealm;\nimport org.apache.shiro.subject.PrincipalCollection;\nimport org.apache.shiro.subject.SimplePrincipalCollection;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.util.Set;\n\npublic class SessionKeyRealm extends AuthorizingRealm {\n\n    private static final Logger log = LoggerFactory.getLogger(SessionKeyRealm.class);\n\n    public static final String REALM_NAME = \"sessionkey\";\n\n    private final ProcessSecurityContext processSecurityContext;\n    private final ProcessQueueManager processQueueManager;\n\n    private static final Set<ProcessStatus> FINISHED_STATUSES = ImmutableSet.of(\n            ProcessStatus.FINISHED,\n            ProcessStatus.FAILED,\n            ProcessStatus.CANCELLED,\n            ProcessStatus.TIMED_OUT);\n\n    @Inject\n    public SessionKeyRealm(ProcessSecurityContext processSecurityContext,\n                           ProcessQueueManager processQueueManager) {\n\n        this.processSecurityContext = processSecurityContext;\n        this.processQueueManager = processQueueManager;\n    }\n\n    @Override\n    public boolean supports(AuthenticationToken token) {\n        return token instanceof SessionKey;\n    }\n\n    @Override\n    @WithTimer\n    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {\n        SessionKey t = (SessionKey) token;\n        PartialProcessKey processKey = PartialProcessKey.from(t.getInstanceId());\n\n        try {\n            ProcessInitiatorEntry p = processQueueManager.getInitiator(processKey);\n            if (p == null) {\n                log.warn(\"doGetAuthenticationInfo -> process not found: {}\", t.getInstanceId());\n                return null;\n            }\n\n            if (p.initiatorId() == null) {\n                log.warn(\"doGetAuthenticationInfo -> initiator not found: {}\", t.getInstanceId());\n                return null;\n            }\n\n            if (isFinished(p)) {\n                log.warn(\"doGetAuthenticationInfo -> process is finished: {}\", t.getInstanceId());\n                return null;\n            }\n\n            PrincipalCollection principals = getPrincipals(processKey);\n            return new SimpleAccount(principals, t.getInstanceId(), getName());\n        } catch (Exception e) {\n            log.error(\"doGetAuthenticationInfo ['{}'] -> error\", t.getInstanceId(), e);\n            throw e;\n        }\n    }\n\n    private PrincipalCollection getPrincipals(PartialProcessKey processKey) {\n        PrincipalCollection principals = processSecurityContext.getPrincipals(processKey);\n\n        SessionKeyPrincipal p = principals.oneByType(SessionKeyPrincipal.class);\n        if (p != null) {\n            // should never happen, sessionkey principals shouldn't be stored in the process state\n            log.warn(\"getPrincipals ['{}'] -> unexpected principal: {}\", processKey, p.getProcessKey());\n            throw new AuthenticationException(\"Unexpected session principal: \" + p.getProcessKey());\n        }\n\n        SimplePrincipalCollection c = new SimplePrincipalCollection(principals);\n        c.add(new SessionKeyPrincipal(processKey), REALM_NAME);\n        return c;\n    }\n\n    @Override\n    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {\n        SessionKeyPrincipal p = principals.oneByType(SessionKeyPrincipal.class);\n        if (p == null) {\n            return null;\n        }\n        return new SimpleAuthorizationInfo();\n    }\n\n    private boolean isFinished(ProcessInitiatorEntry process) {\n        return FINISHED_STATUSES.contains(process.status());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/task/SchedulerDao.java",
    "content": "package com.walmartlabs.concord.server.task;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.db.PgUtils;\nimport com.walmartlabs.concord.server.jooq.enums.TaskStatusType;\nimport com.walmartlabs.concord.server.metrics.FailedTaskError;\nimport com.walmartlabs.concord.server.sdk.ScheduledTask;\nimport org.jooq.*;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.time.OffsetDateTime;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.TASKS;\nimport static org.jooq.impl.DSL.currentOffsetDateTime;\nimport static org.jooq.impl.DSL.value;\n\npublic final class SchedulerDao extends AbstractDao {\n\n    private static final Logger log = LoggerFactory.getLogger(SchedulerDao.class);\n\n    @Inject\n    public SchedulerDao(@MainDB Configuration cfg) {\n        super(cfg);\n    }\n\n    public List<String> poll() {\n        @SuppressWarnings(\"unchecked\")\n        Field<? extends Number> i = (Field<? extends Number>) PgUtils.interval(\"1 second\");\n\n        return txResult(tx -> {\n            List<String> ids = tx.select(TASKS.TASK_ID)\n                    .from(TASKS)\n                    .where(TASKS.TASK_INTERVAL.greaterThan(0L)\n                            .and(TASKS.TASK_STATUS.notEqual(TaskStatusType.RUNNING).or(TASKS.TASK_STATUS.isNull()))\n                            .and(TASKS.FINISHED_AT.isNull()\n                                    .or(TASKS.FINISHED_AT.plus(i.mul(TASKS.TASK_INTERVAL)).lessOrEqual(currentOffsetDateTime()))))\n                    .forUpdate()\n                    .skipLocked()\n                    .fetch(TASKS.TASK_ID);\n\n            if (ids.isEmpty()) {\n                return ids;\n            }\n\n            tx.update(TASKS)\n                    .set(TASKS.STARTED_AT, currentOffsetDateTime())\n                    .set(TASKS.TASK_STATUS, value(TaskStatusType.RUNNING))\n                    .set(TASKS.FINISHED_AT, (OffsetDateTime) null)\n                    .set(TASKS.LAST_UPDATED_AT, currentOffsetDateTime())\n                    .where(TASKS.TASK_ID.in(ids))\n                    .execute();\n\n            return ids;\n        });\n    }\n\n    public List<String> pollStalled(DSLContext tx, Field<OffsetDateTime> cutOff) {\n        return tx.select(TASKS.TASK_ID)\n                .from(TASKS)\n                .where(TASKS.LAST_UPDATED_AT.lessThan(cutOff)\n                        .and(TASKS.TASK_STATUS.eq(TaskStatusType.RUNNING))\n                        .and(TASKS.TASK_INTERVAL.greaterThan(0L)))\n                .forUpdate()\n                .skipLocked()\n                .fetch(TASKS.TASK_ID);\n    }\n\n    public void success(String taskId) {\n        tx(tx -> taskFinished(tx, taskId, TaskStatusType.OK, null));\n    }\n\n    public void error(String taskId, Exception e) {\n        tx(tx -> taskFinished(tx, taskId, TaskStatusType.ERROR, e));\n    }\n\n    public void stalled(DSLContext tx, String taskId) {\n        taskFinished(tx, taskId, TaskStatusType.STALLED, null);\n    }\n\n    public void updateTaskIntervals(Collection<ScheduledTask> tasks) {\n        tx(tx -> {\n            for (ScheduledTask task : tasks) {\n                String taskId = task.getId();\n\n                if (task.getIntervalInSec() <= 0) {\n                    log.warn(\"{} has period <= 0, the task will be disabled\", taskId);\n                }\n\n                tx.insertInto(TASKS, TASKS.TASK_ID, TASKS.TASK_INTERVAL)\n                        .values(taskId, task.getIntervalInSec())\n                        .onDuplicateKeyUpdate().set(TASKS.TASK_INTERVAL, task.getIntervalInSec())\n                        .execute();\n            }\n        });\n    }\n\n    public void updateRunning(Set<String> runningTasks) {\n        tx(tx -> tx.update(TASKS)\n                .set(TASKS.LAST_UPDATED_AT, currentOffsetDateTime())\n                .where(TASKS.TASK_ID.in(runningTasks))\n                .execute());\n    }\n\n    public void transaction(Tx t) {\n        tx(t);\n    }\n\n    private void taskFinished(DSLContext tx, String taskId, TaskStatusType status, Exception e) {\n        tx.update(TASKS)\n                .set(TASKS.FINISHED_AT, currentOffsetDateTime())\n                .set(TASKS.LAST_UPDATED_AT, currentOffsetDateTime())\n                .set(TASKS.TASK_STATUS, value(status))\n                .set(TASKS.LAST_ERROR_AT, e != null ? currentOffsetDateTime() : null)\n                .set(TASKS.LAST_ERROR, e != null ? value(toString(e)) : TASKS.LAST_ERROR)\n                .where(TASKS.TASK_ID.eq(taskId))\n                .execute();\n    }\n\n    public List<FailedTaskError> pollErrored() {\n        return txResult(tx -> {\n            Result<Record3<String, String, OffsetDateTime>> result = tx.select(TASKS.TASK_ID, TASKS.LAST_ERROR, TASKS.LAST_ERROR_AT)\n                    .from(TASKS)\n                    .where(TASKS.LAST_ERROR_AT.isNotNull()\n                            .and(TASKS.LAST_ERROR.isNotNull()))\n                    .fetch();\n            return result.stream()\n                    .map(x -> new FailedTaskError(x.component1(), x.component2(), x.component3()))\n                    .collect(Collectors.toList());\n        });\n    }\n\n    private static String toString(Exception exception) {\n        try (StringWriter sw = new StringWriter();\n             PrintWriter pw = new PrintWriter(sw)) {\n            exception.printStackTrace(pw);\n            return sw.toString();\n        } catch (IOException e) {\n            log.error(\"toString [{}]-> error: {}\", exception, e.getMessage());\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/task/TaskScheduler.java",
    "content": "package com.walmartlabs.concord.server.task;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.PeriodicTask;\nimport com.walmartlabs.concord.server.sdk.ScheduledTask;\nimport org.jooq.Field;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\nimport java.util.function.Consumer;\n\nimport static com.walmartlabs.concord.db.PgUtils.interval;\nimport static java.util.stream.Collectors.toMap;\nimport static org.jooq.impl.DSL.currentOffsetDateTime;\n\npublic class TaskScheduler extends PeriodicTask {\n\n    private static final Logger log = LoggerFactory.getLogger(TaskScheduler.class);\n\n    private static final long POLL_INTERVAL = TimeUnit.SECONDS.toMillis(1);\n    private static final long ERROR_DELAY = TimeUnit.SECONDS.toMillis(10);\n    private static final String MAX_STALLED_AGE = \"1 minute\";\n    private static final long STALLED_CHECK_INTERVAL = TimeUnit.SECONDS.toMillis(20);\n    private static final long RUNNING_UPDATE_INTERVAL = TimeUnit.SECONDS.toMillis(20);\n\n    private final ExecutorService executor;\n    private final SchedulerDao dao;\n    private final Map<String, ScheduledTask> tasks;\n    private final Set<String> runningTasks = new HashSet<>();\n\n    private final Lock mutex = new ReentrantLock();\n\n    private long lastUpdateDate;\n    private long lasStalledCheckDate;\n\n    @Inject\n    public TaskScheduler(Set<ScheduledTask> tasks, SchedulerDao dao) {\n        super(POLL_INTERVAL, ERROR_DELAY);\n\n        this.executor = Executors.newCachedThreadPool();\n        this.dao = dao;\n        this.tasks = tasks.stream().collect(toMap(ScheduledTask::getId, t -> t));\n\n        this.dao.updateTaskIntervals(tasks);\n    }\n\n    @Override\n    protected boolean performTask() {\n        if (lastUpdateDate + RUNNING_UPDATE_INTERVAL <= System.currentTimeMillis()) {\n            withRunningTasks();\n            lastUpdateDate = System.currentTimeMillis();\n        }\n\n        if (lasStalledCheckDate + STALLED_CHECK_INTERVAL <= System.currentTimeMillis()) {\n            failStalled();\n            lasStalledCheckDate = System.currentTimeMillis();\n        }\n\n        List<String> ids = dao.poll();\n        ids.forEach(this::startTask);\n\n        return false;\n    }\n\n    @Override\n    public void stop() {\n        super.stop();\n\n        executor.shutdown();\n\n        try {\n            if (executor.awaitTermination(5, TimeUnit.MINUTES)) {\n                log.info(\"stop -> done\");\n            } else {\n                log.info(\"stop -> timeout\");\n            }\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    private void startTask(String id) {\n        ScheduledTask task = tasks.get(id);\n        if (task == null) {\n            log.error(\"startTask -> task with id '{}' not found\", id);\n            return;\n        }\n\n        executor.submit(() -> {\n            withRunningTasks(tasks -> tasks.add(id));\n            try {\n                task.performTask();\n\n                dao.success(id);\n\n                log.debug(\"startTask ['{}'] -> done\", id);\n            } catch (Exception e) {\n                log.error(\"startTask ['{}'] -> error\", id, e);\n\n                dao.error(id, e);\n            } finally {\n                withRunningTasks(tasks -> tasks.remove(id));\n            }\n        });\n    }\n\n    private void withRunningTasks() {\n        Set<String> forUpdate = copyRunningTasks();\n        if (forUpdate.isEmpty()) {\n            return;\n        }\n\n        try {\n            dao.updateRunning(forUpdate);\n        } catch (Exception e) {\n            log.error(\"updateRunningTasks -> error: {}\", e.getMessage());\n        }\n    }\n\n    private void failStalled() {\n        Field<OffsetDateTime> cutOff = currentOffsetDateTime().minus(interval(MAX_STALLED_AGE));\n\n        try {\n            dao.transaction(tx -> {\n                List<String> ids = dao.pollStalled(tx, cutOff);\n                for (String id : ids) {\n                    dao.stalled(tx, id);\n                    log.info(\"failStalled -> marked as failed: {}\", id);\n                }\n            });\n        } catch (Exception e) {\n            log.error(\"failStalled -> error: {}\", e.getMessage());\n        }\n    }\n\n    private void withRunningTasks(Consumer<Set<String>> f) {\n        mutex.lock();\n        try {\n            f.accept(runningTasks);\n        } finally {\n            mutex.unlock();\n        }\n    }\n\n    private Set<String> copyRunningTasks() {\n        mutex.lock();\n        try {\n            return Set.copyOf(runningTasks);\n        } finally {\n            mutex.unlock();\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/task/TaskSchedulerModule.java",
    "content": "package com.walmartlabs.concord.server.task;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.walmartlabs.concord.server.sdk.BackgroundTask;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\n\npublic class TaskSchedulerModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        binder.bind(TaskScheduler.class).in(SINGLETON);\n        newSetBinder(binder, BackgroundTask.class).addBinding().to(TaskScheduler.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/template/TemplateAliasDao.java",
    "content": "package com.walmartlabs.concord.server.template;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.jooq.Record2;\n\nimport javax.inject.Inject;\nimport java.util.List;\nimport java.util.Optional;\n\nimport static com.walmartlabs.concord.server.jooq.tables.TemplateAliases.TEMPLATE_ALIASES;\n\npublic class TemplateAliasDao extends AbstractDao {\n\n    @Inject\n    public TemplateAliasDao(@MainDB Configuration cfg) {\n        super(cfg);\n    }\n\n    public boolean exists(String alias) {\n        DSLContext tx = dsl();\n        return tx.fetchExists(tx.selectFrom(TEMPLATE_ALIASES)\n                .where(TEMPLATE_ALIASES.TEMPLATE_ALIAS.eq(alias)));\n    }\n\n    public Optional<String> get(String alias) {\n        return dsl().select(TEMPLATE_ALIASES.TEMPLATE_URL)\n                .from(TEMPLATE_ALIASES)\n                .where(TEMPLATE_ALIASES.TEMPLATE_ALIAS.eq(alias))\n                .fetchOptional(TEMPLATE_ALIASES.TEMPLATE_URL);\n    }\n\n    public void insert(String alias, String url) {\n        tx(tx -> insert(tx, alias, url));\n    }\n\n    public void insert(DSLContext tx, String alias, String url) {\n        tx.insertInto(TEMPLATE_ALIASES)\n                .columns(TEMPLATE_ALIASES.TEMPLATE_ALIAS, TEMPLATE_ALIASES.TEMPLATE_URL)\n                .values(alias, url)\n                .execute();\n    }\n\n    public List<TemplateAliasEntry> list() {\n        return dsl().select(TEMPLATE_ALIASES.TEMPLATE_ALIAS, TEMPLATE_ALIASES.TEMPLATE_URL)\n                .from(TEMPLATE_ALIASES)\n                .fetch(TemplateAliasDao::toEntry);\n    }\n\n    public void delete(String alias) {\n        tx(tx -> delete(tx, alias));\n    }\n\n    public void delete(DSLContext tx, String alias) {\n        tx.deleteFrom(TEMPLATE_ALIASES)\n                .where(TEMPLATE_ALIASES.TEMPLATE_ALIAS.eq(alias))\n                .execute();\n    }\n\n    private static TemplateAliasEntry toEntry(Record2<String, String> r) {\n        return new TemplateAliasEntry(r.value1(), r.value2());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/template/TemplateAliasEntry.java",
    "content": "package com.walmartlabs.concord.server.template;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\n\nimport javax.validation.constraints.NotNull;\nimport javax.validation.constraints.Size;\nimport java.io.Serializable;\n\npublic class TemplateAliasEntry implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @NotNull\n    @ConcordKey\n    private final String alias;\n\n    @NotNull\n    @Size(max = 2048)\n    private final String url;\n\n    @JsonCreator\n    public TemplateAliasEntry(@JsonProperty(\"alias\") String alias,\n                              @JsonProperty(\"url\") String url) {\n        this.alias = alias;\n        this.url = url;\n    }\n\n    public String getAlias() {\n        return alias;\n    }\n\n    public String getUrl() {\n        return url;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/template/TemplateAliasResource.java",
    "content": "package com.walmartlabs.concord.server.template;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.validation.ConcordKey;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.jooq.Configuration;\n\nimport javax.inject.Inject;\nimport javax.validation.Valid;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport java.util.List;\n\n@Path(\"/api/v1/template/alias\")\n@Tag(name = \"TemplateAlias\")\npublic class TemplateAliasResource extends AbstractDao implements Resource {\n\n    private final TemplateAliasDao aliasDao;\n\n    @Inject\n    public TemplateAliasResource(@MainDB Configuration cfg, TemplateAliasDao aliasDao) {\n        super(cfg);\n        this.aliasDao = aliasDao;\n    }\n\n    @POST\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Create or update a template alias\", operationId = \"createOrUpdateTemplate\")\n    public TemplateAliasResponse createOrUpdate(@Valid TemplateAliasEntry request) {\n        assertAdmin();\n\n        tx(tx -> {\n            aliasDao.delete(request.getAlias());\n            aliasDao.insert(request.getAlias(), request.getUrl());\n        });\n\n        return new TemplateAliasResponse();\n    }\n\n    @GET\n    @Produces(MediaType.APPLICATION_JSON)\n    @Operation(description = \"List current template aliases\", operationId = \"listTemplates\")\n    public List<TemplateAliasEntry> list() {\n        return aliasDao.list();\n    }\n\n    @DELETE\n    @Path(\"/{alias}\")\n    @Operation(description = \"Delete existing template alias\", operationId = \"deleteTemplate\")\n    public TemplateAliasResponse delete(@PathParam(\"alias\") @ConcordKey String alias) {\n\n        assertAdmin();\n        aliasDao.delete(alias);\n        return new TemplateAliasResponse();\n    }\n\n    private static void assertAdmin() {\n        if (!Roles.isAdmin()) {\n            throw new UnauthorizedException(\"Only admins can do that\");\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/template/TemplateAliasResponse.java",
    "content": "package com.walmartlabs.concord.server.template;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\n\npublic class TemplateAliasResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n    private final boolean ok = true;\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    @Override\n    public String toString() {\n        return \"TemplateAliasResponse{\" +\n                \"ok=\" + ok +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/template/TemplateModule.java",
    "content": "package com.walmartlabs.concord.server.template;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\n\nimport static com.walmartlabs.concord.server.Utils.bindJaxRsResource;\n\npublic class TemplateModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        bindJaxRsResource(binder, TemplateAliasResource.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/user/AbstractUserInfoProvider.java",
    "content": "package com.walmartlabs.concord.server.user;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Set;\nimport java.util.UUID;\n\npublic abstract class AbstractUserInfoProvider implements UserInfoProvider{\n    \n    private final UserDao userDao;\n    \n    public AbstractUserInfoProvider(UserDao userDao){\n        this.userDao = userDao;\n    }\n    \n    protected UserInfo getInfo(UUID id, String username, String userDomain, UserType type) {\n        if (id == null) {\n            id = userDao.getId(username, userDomain, type);\n        }\n\n        if (id == null) {\n            return null;\n        }\n\n        UserEntry e = userDao.get(id);\n        if (e == null) {\n            return null;\n        }\n\n        return UserInfo.builder()\n                .id(id)\n                .username(e.getName())\n                .userDomain(userDomain)\n                .displayName(e.getDisplayName())\n                .email(e.getEmail())\n                .build();\n    }\n\n    protected UUID create(String username, String domain, String displayName, String email, Set<String> roles, UserType type) {\n        return userDao.insertOrUpdate(username, domain, displayName, email, type, roles);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/user/CreateUserRequest.java",
    "content": "package com.walmartlabs.concord.server.user;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport javax.validation.constraints.NotNull;\nimport javax.validation.constraints.Size;\nimport java.io.Serializable;\nimport java.util.Set;\n\npublic class CreateUserRequest implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @NotNull\n    @Size(max = UserEntry.MAX_USERNAME_LENGTH)\n    private final String username;\n\n    @Size(max = UserEntry.MAX_DOMAIN_LENGTH)\n    private final String userDomain;\n\n    @Size(max = UserEntry.MAX_DISPLAY_NAME_LENGTH)\n    private final String displayName;\n\n    @Size(max = UserEntry.MAX_EMAIL_LENGTH)\n    private final String email;\n\n    private final UserType type;\n\n    private final boolean disabled;\n\n    private final Set<String> roles;\n\n    @JsonCreator\n    public CreateUserRequest(@JsonProperty(\"username\") String username,\n                             @JsonProperty(\"userDomain\") String userDomain,\n                             @JsonProperty(\"displayName\") String displayName,\n                             @JsonProperty(\"email\") String email,\n                             @JsonProperty(\"userType\") UserType type,\n                             @JsonProperty(\"disabled\") boolean disabled,\n                             @JsonProperty(\"roles\") Set<String> roles) {\n\n        this.username = username;\n        this.userDomain = userDomain;\n        this.displayName = displayName;\n        this.email = email;\n        this.type = type;\n        this.disabled = disabled;\n        this.roles = roles;\n    }\n\n    public String getUsername() {\n        return username;\n    }\n\n    public String getUserDomain() {\n        return userDomain;\n    }\n\n    public String getDisplayName() {\n        return displayName;\n    }\n\n    public String getEmail() {\n        return email;\n    }\n\n    public UserType getType() {\n        return type;\n    }\n\n    public boolean isDisabled() {\n        return disabled;\n    }\n\n    public Set<String> getRoles() {\n        return roles;\n    }\n\n    @Override\n    public String toString() {\n        return \"CreateUserRequest{\" +\n                \"username='\" + username + '\\'' +\n                \", userDomain='\" + userDomain + '\\'' +\n                \", displayName='\" + displayName + '\\'' +\n                \", email='\" + email + '\\'' +\n                \", type=\" + type +\n                \", disabled=\" + disabled +\n                \", roles=\" + roles +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/user/CreateUserResponse.java",
    "content": "package com.walmartlabs.concord.server.user;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.server.OperationResult;\n\nimport java.io.Serializable;\nimport java.util.UUID;\n\npublic class CreateUserResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n    private final UUID id;\n    private final String username;\n    private final OperationResult result;\n\n    @JsonCreator\n    public CreateUserResponse(@JsonProperty(\"id\") UUID id,\n                              @JsonProperty(\"username\") String username,\n                              @JsonProperty(\"result\") OperationResult result) {\n\n        this.id = id;\n        this.username = username;\n        this.result = result;\n    }\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public String getUsername() {\n        return username;\n    }\n\n    public OperationResult getResult() {\n        return result;\n    }\n\n    @Override\n    public String toString() {\n        return \"CreateUserResponse{\" +\n                \"ok=\" + ok +\n                \", id=\" + id +\n                \", username='\" + username + '\\'' +\n                \", result=\" + result +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/user/DeleteUserResponse.java",
    "content": "package com.walmartlabs.concord.server.user;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\n\npublic class DeleteUserResponse implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final boolean ok = true;\n\n    public boolean isOk() {\n        return ok;\n    }\n\n    @Override\n    public String toString() {\n        return \"DeleteUserResponse{\" +\n                \"ok=\" + ok +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/user/RoleEntry.java",
    "content": "package com.walmartlabs.concord.server.user;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.common.validation.ConcordKey;\n\nimport java.io.Serializable;\nimport java.util.Set;\nimport java.util.UUID;\n\n@JsonInclude(JsonInclude.Include.NON_NULL)\npublic class RoleEntry implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final UUID id;\n\n    @ConcordKey\n    private final String name;\n\n    private final Set<String> permissions;\n\n    @JsonCreator\n    public RoleEntry(@JsonProperty(\"id\") UUID id,\n                     @JsonProperty(\"name\") String name,\n                     @JsonProperty(\"permissions\") Set<String> permissions) {\n\n        this.id = id;\n        this.name = name;\n        this.permissions = permissions;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public Set<String> getPermissions() {\n        return permissions;\n    }\n\n    @Override\n    public String toString() {\n        return \"RoleEntry{\" +\n                \"id=\" + id +\n                \", name='\" + name + '\\'' +\n                \", permissions=\" + permissions +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/user/UpdateUserRolesRequest.java",
    "content": "package com.walmartlabs.concord.server.user;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport javax.validation.constraints.NotNull;\nimport java.io.Serializable;\nimport java.util.Set;\n\npublic class UpdateUserRolesRequest implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @NotNull\n    private final Set<String> roles;\n\n    public UpdateUserRolesRequest(@JsonProperty(\"roles\") Set<String> roles) {\n        this.roles = roles;\n    }\n\n    public Set<String> getRoles() {\n        return roles;\n    }\n\n    @Override\n    public String toString() {\n        return \"UpdateUserRolesRequest{\" +\n                \"roles=\" + roles +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/user/User.java",
    "content": "package com.walmartlabs.concord.server.user;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\n\n@Value.Immutable\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableUser.class)\n@JsonDeserialize(as = ImmutableUser.class)\npublic interface User extends Serializable {\n\n    String username();\n\n    @Nullable\n    String domain();\n\n    UserType type();\n\n    static User of(String username, String domain, UserType type) {\n        return ImmutableUser.builder()\n                .username(username)\n                .domain(domain)\n                .type(type)\n                .build();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/user/UserDao.java",
    "content": "package com.walmartlabs.concord.server.user;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.UuidGenerator;\nimport com.walmartlabs.concord.server.jooq.Tables;\nimport com.walmartlabs.concord.server.jooq.tables.records.UsersRecord;\nimport com.walmartlabs.concord.server.org.OrganizationEntry;\nimport com.walmartlabs.concord.server.org.team.TeamRole;\nimport com.walmartlabs.concord.server.security.ldap.LdapGroupSearchResult;\nimport org.jooq.*;\n\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.*;\nimport static com.walmartlabs.concord.server.jooq.tables.Organizations.ORGANIZATIONS;\nimport static com.walmartlabs.concord.server.jooq.tables.Roles.ROLES;\nimport static com.walmartlabs.concord.server.jooq.tables.Teams.TEAMS;\nimport static com.walmartlabs.concord.server.jooq.tables.UserRoles.USER_ROLES;\nimport static com.walmartlabs.concord.server.jooq.tables.UserTeams.USER_TEAMS;\nimport static com.walmartlabs.concord.server.jooq.tables.Users.USERS;\nimport static java.util.Objects.requireNonNull;\nimport static org.jooq.impl.DSL.*;\n\npublic class UserDao extends AbstractDao {\n\n    private final UuidGenerator uuidGenerator;\n\n    @Inject\n    public UserDao(@MainDB Configuration cfg, UuidGenerator uuidGenerator) {\n        super(cfg);\n        this.uuidGenerator = requireNonNull(uuidGenerator);\n    }\n\n    @Override\n    protected <T> T txResult(TxResult<T> t) {\n        return super.txResult(t);\n    }\n\n    /**\n     * Inserts a new record or updates an existing one.\n     * Note that {@code username} and {@code domain} are case-insensitive and forced to lower case.\n     */\n    public UUID insertOrUpdate(String username, String domain, String displayName, String email, UserType type, Set<String> roles) {\n        UUID userId = uuidGenerator.generate();\n        return txResult(tx -> {\n            UUID effectiveUserId = tx.insertInto(USERS)\n                    .columns(USERS.USER_ID, USERS.USERNAME, USERS.DOMAIN, USERS.DISPLAY_NAME, USERS.USER_EMAIL, USERS.USER_TYPE)\n                    .values(userId, toLowerCase(username), toLowerCase(domain), displayName, email, type.toString())\n                    .onConflict(lower(USERS.USERNAME), lower(USERS.DOMAIN), field(USERS.USER_TYPE))\n                    .doUpdate()\n                    .set(USERS.DISPLAY_NAME, displayName)\n                    .set(USERS.USER_EMAIL, email)\n                    .returning(USERS.USER_ID)\n                    .fetchOne()\n                    .getUserId();\n\n            if (roles != null) {\n                updateRoles(tx, effectiveUserId, roles);\n            }\n\n            return effectiveUserId;\n        });\n    }\n\n    public void delete(UUID id) {\n        tx(tx -> tx.deleteFrom(USERS)\n                .where(USERS.USER_ID.eq(id))\n                .execute());\n    }\n\n    public void enable(UUID id) {\n        tx(tx -> tx.update(USERS)\n                .set(USERS.IS_DISABLED, inline(false))\n                .set(USERS.IS_PERMANENTLY_DISABLED, inline(false))\n                .setNull(USERS.DISABLED_DATE)\n                .where(USERS.USER_ID.eq(id))\n                .execute());\n    }\n\n    public void disable(UUID id, boolean permanent) {\n        tx(tx -> tx.update(USERS)\n                .set(USERS.IS_DISABLED, inline(true))\n                .set(Tables.USERS.IS_PERMANENTLY_DISABLED, permanent)\n                .set(USERS.DISABLED_DATE, currentOffsetDateTime())\n                .where(USERS.USER_ID.eq(id))\n                .execute());\n    }\n\n    public UserEntry update(UUID id, String displayName, String email, UserType userType, boolean isDisabled, Set<String> roles) {\n        tx(tx -> {\n            UpdateSetMoreStep<UsersRecord> q = tx.update(USERS)\n                    .set(USERS.IS_DISABLED, isDisabled);\n\n            if (isDisabled) {\n                q.set(USERS.DISABLED_DATE, currentOffsetDateTime());\n            } else {\n                q.setNull(USERS.DISABLED_DATE);\n            }\n\n            if (userType != null) {\n                q.set(USERS.USER_TYPE, userType.name());\n            }\n\n            if (displayName != null) {\n                q.set(USERS.DISPLAY_NAME, displayName);\n            }\n\n            if (email != null) {\n                q.set(USERS.USER_EMAIL, email);\n            }\n\n            q.where(USERS.USER_ID.eq(id))\n                    .execute();\n\n            if (roles != null) {\n                updateRoles(tx, id, roles);\n            }\n        });\n\n        return get(id);\n    }\n\n    // TODO add \"include\" option\n    public UserEntry get(UUID id) {\n        DSLContext tx = dsl();\n\n        Record9<UUID, String, String, String, String, String, Boolean, Boolean, OffsetDateTime> r =\n                tx.select(USERS.USER_ID, USERS.USER_TYPE, USERS.USERNAME, USERS.DOMAIN, USERS.DISPLAY_NAME, USERS.USER_EMAIL, USERS.IS_DISABLED, USERS.IS_PERMANENTLY_DISABLED, USERS.DISABLED_DATE)\n                        .from(USERS)\n                        .where(USERS.USER_ID.eq(id))\n                        .fetchOne();\n\n        if (r == null) {\n            return null;\n        }\n\n        return getUserInfo(tx, r);\n    }\n\n    public UserEntry getUserFromExternalMapping(String externalId) {\n        DSLContext tx = dsl();\n\n        Record9<UUID, String, String, String, String, String, Boolean, Boolean, OffsetDateTime> r =\n                tx.select(USERS.USER_ID, USERS.USER_TYPE, USERS.USERNAME, USERS.DOMAIN, USERS.DISPLAY_NAME, USERS.USER_EMAIL, USERS.IS_DISABLED, USERS.IS_PERMANENTLY_DISABLED, USERS.DISABLED_DATE)\n                        .from(USERS, EXTERNAL_APP_USERS)\n                        .where(USERS.USER_ID.eq(EXTERNAL_APP_USERS.USER_ID))\n                        .and(EXTERNAL_APP_USERS.EXTERNAL_USER_ID.eq(externalId))\n                        .fetchOne();\n\n        if (r == null) {\n            return null;\n        }\n\n        return getUserInfo(tx, r);\n    }\n\n    /**\n     * Creates a mapping from an external user to an existing Concord user. The\n     * external identifier may not be a singular, exact, ID for the system. The\n     * context(s) creating and retrieving the mapping must understand the construction\n     * of the identifier. Typically, it should be a formatting along the lines\n     * of <code>{external_system} + {actual_user_id}</code> to avoid clashing of\n     * IDs that are unique to the system but potentially duplicated across other\n     * external systems that have been integrated (e.g. multiple GitHub instances).\n     * @param userId existing Concord user ID\n     * @param externalId identifier for user in an external system\n     */\n    public void createExternalUserMapping(UUID userId, String externalId) {\n        tx(tx -> tx.insertInto(EXTERNAL_APP_USERS)\n                .columns(EXTERNAL_APP_USERS.EXTERNAL_USER_ID, EXTERNAL_APP_USERS.USER_ID)\n                .values(externalId, userId)\n                .onConflict(EXTERNAL_APP_USERS.EXTERNAL_USER_ID)\n                .doUpdate()\n                .set(EXTERNAL_APP_USERS.USER_ID, userId)\n                .execute());\n    }\n\n    /**\n     * Returns the ID of the specified user.\n     * Note that {@code username} and {@code domain} are case-insensitive and forced to lower case.\n     */\n    public UUID getId(String username, String domain, UserType type) {\n        SelectConditionStep<Record1<UUID>> q = dsl().select(USERS.USER_ID)\n                .from(USERS)\n                .where(USERS.USERNAME.eq(toLowerCase(username)));\n\n        if (domain != null) {\n            q.and(USERS.DOMAIN.eq(toLowerCase(domain)));\n        }\n\n        if (type != null) {\n            q.and(USERS.USER_TYPE.eq(type.toString()));\n        }\n\n        List<UUID> result = q.fetch(USERS.USER_ID);\n        if (result.isEmpty()) {\n            return null;\n        } else if (result.size() > 1) {\n            throw new IllegalArgumentException(\"Non unique results found for username: '\" + username + \"', domain: '\" + domain + \"', type: \" + type +\n                    \". Note that user and domain names are case-insensitive.\");\n        }\n\n        return result.get(0);\n    }\n\n    public String getUsername(UUID id) {\n        return dsl().select(USERS.USERNAME).from(USERS)\n                .where(USERS.USER_ID.eq(id))\n                .fetchOne(USERS.USERNAME);\n    }\n\n    public boolean existsById(UUID id) {\n        int cnt = dsl().fetchCount(selectFrom(USERS)\n                .where(USERS.USER_ID.eq(id)));\n\n        return cnt > 0;\n    }\n\n    public boolean isInOrganization(UUID userId, UUID orgId) {\n        return isInOrganization(dsl(), userId, orgId);\n    }\n\n    public boolean isInOrganization(DSLContext tx, UUID userId, UUID orgId) {\n        SelectConditionStep<Record1<UUID>> teamIds = select(TEAMS.TEAM_ID)\n                .from(TEAMS)\n                .where(TEAMS.ORG_ID.eq(orgId));\n\n        return tx.fetchExists(selectFrom(V_USER_TEAMS)\n                .where(V_USER_TEAMS.USER_ID.eq(userId)\n                        .and(V_USER_TEAMS.TEAM_ID.in(teamIds))));\n    }\n\n    public Set<UUID> getOrgIds(UUID userId) {\n        SelectConditionStep<Record1<UUID>> teamIds = select(V_USER_TEAMS.TEAM_ID)\n                .from(V_USER_TEAMS)\n                .where(V_USER_TEAMS.USER_ID.eq(userId));\n\n        return dsl().selectDistinct(TEAMS.ORG_ID)\n                .from(TEAMS)\n                .where(TEAMS.TEAM_ID.in(teamIds))\n                .fetchSet(TEAMS.ORG_ID);\n    }\n\n    public String getEmail(UUID id) {\n        return dsl().select(USERS.USER_EMAIL)\n                .from(USERS)\n                .where(USERS.USER_ID.eq(id))\n                .fetchOne(USERS.USER_EMAIL);\n    }\n\n    public void updateRoles(UUID id, Set<String> roles) {\n        tx(tx -> updateRoles(tx, id, roles));\n    }\n\n    public void updateRoles(DSLContext tx, UUID id, Set<String> roles) {\n        tx.deleteFrom(USER_ROLES).where(USER_ROLES.USER_ID.eq(id)).execute();\n\n        tx.insertInto(USER_ROLES).select(\n                select(\n                        value(id).as(USER_ROLES.USER_ID),\n                        ROLES.ROLE_ID.as(USER_ROLES.ROLE_ID)\n                ).from(ROLES).where(ROLES.ROLE_NAME.in(roles)))\n                .execute();\n    }\n\n    public Optional<Boolean> isDisabled(UUID userId) {\n        return dsl().select(USERS.IS_DISABLED)\n                .from(USERS)\n                .where(USERS.USER_ID.eq(userId))\n                .fetchOne(r -> Optional.ofNullable(r.get(USERS.IS_DISABLED)));\n    }\n\n    // TODO add \"include\" option\n    public List<UserEntry> list(String filter, int offset, int limit) {\n        return dsl().select(USERS.USER_ID, USERS.USERNAME, USERS.DOMAIN, USERS.USER_TYPE, USERS.DISPLAY_NAME, USERS.USER_EMAIL, USERS.IS_DISABLED, USERS.DISABLED_DATE, USERS.IS_PERMANENTLY_DISABLED)\n                .from(USERS)\n                .where(USERS.IS_DISABLED.isFalse())\n                .and(value(filter).isNotNull()\n                        .and(USERS.USERNAME.containsIgnoreCase(filter)\n                                .or(USERS.DISPLAY_NAME.containsIgnoreCase(filter))\n                                .or(USERS.USER_EMAIL.containsIgnoreCase(filter))))\n                .orderBy(USERS.DISPLAY_NAME, USERS.USERNAME)\n                .offset(offset)\n                .limit(limit)\n                .fetch(r -> new UserEntry(r.get(USERS.USER_ID),\n                        r.get(USERS.USERNAME),\n                        r.get(USERS.DOMAIN),\n                        r.get(USERS.DISPLAY_NAME),\n                        null,\n                        UserType.valueOf(r.get(USERS.USER_TYPE)),\n                        r.get(USERS.USER_EMAIL),\n                        null,\n                        r.get(USERS.IS_DISABLED),\n                        r.get(USERS.DISABLED_DATE),\n                        r.get(USERS.IS_PERMANENTLY_DISABLED)));\n    }\n\n    public List<LdapGroupSearchResult> searchLdapGroups(String filter) {\n        return dsl().selectDistinct(USER_LDAP_GROUPS.LDAP_GROUP)\n                .from(USER_LDAP_GROUPS)\n                .where(USER_LDAP_GROUPS.LDAP_GROUP.likeIgnoreCase(String.format(\"%%%s%%\", filter)))\n                .orderBy(USER_LDAP_GROUPS.LDAP_GROUP)\n                .limit(10)\n                .fetch(r -> LdapGroupSearchResult.builder()\n                        .displayName(r.get(USER_LDAP_GROUPS.LDAP_GROUP))\n                        .groupName(r.get(USER_LDAP_GROUPS.LDAP_GROUP))\n                        .build());\n    }\n\n    public List<UserTeam> listTeams(DSLContext tx, UUID userId) {\n        return tx.select(USER_TEAMS.TEAM_ID, USER_TEAMS.TEAM_ROLE)\n                .from(USER_TEAMS)\n                .where(USER_TEAMS.USER_ID.eq(userId))\n                .fetch(r -> UserTeam.of(r.get(USER_TEAMS.TEAM_ID), TeamRole.valueOf(r.get(USER_TEAMS.TEAM_ROLE))));\n    }\n\n    public void excludeFromTeams(DSLContext tx, UUID userId, List<UUID> teamIds) {\n        if (teamIds.isEmpty()) {\n            return;\n        }\n\n        tx.deleteFrom(USER_TEAMS)\n                .where(USER_TEAMS.USER_ID.eq(userId)\n                        .and(USER_TEAMS.TEAM_ID.in(teamIds)))\n                .execute();\n    }\n\n    private UserEntry getUserInfo(DSLContext tx, Record9<UUID, String, String, String, String, String, Boolean, Boolean, OffsetDateTime> r) {\n        // TODO join?\n        Field<String> orgNameField = select(ORGANIZATIONS.ORG_NAME)\n                .from(ORGANIZATIONS)\n                .where(ORGANIZATIONS.ORG_ID.eq(TEAMS.ORG_ID)).asField();\n\n        SelectConditionStep<Record1<UUID>> teamIds = select(V_USER_TEAMS.TEAM_ID)\n                .from(V_USER_TEAMS)\n                .where(V_USER_TEAMS.USER_ID.eq(r.get(USERS.USER_ID)));\n\n        List<OrganizationEntry> orgs = tx.selectDistinct(TEAMS.ORG_ID, orgNameField)\n                .from(TEAMS)\n                .where(TEAMS.TEAM_ID.in(teamIds))\n                .fetch(e -> new OrganizationEntry(e.value1(), e.value2(), null, null, null, null));\n\n        SelectConditionStep<Record1<String[]>> permissions = select(arrayAgg(PERMISSIONS.PERMISSION_NAME)).from(PERMISSIONS)\n                .where(PERMISSIONS.PERMISSION_ID.in(\n                        select(ROLE_PERMISSIONS.PERMISSION_ID).from(ROLE_PERMISSIONS)\n                                .where(ROLE_PERMISSIONS.ROLE_ID.in(ROLES.ROLE_ID))));\n\n        SelectConditionStep<Record1<UUID>> roleIds = select(V_USER_ROLES.ROLE_ID).from(V_USER_ROLES)\n                .where(V_USER_ROLES.USER_ID\n                        .eq(r.get(USERS.USER_ID)));\n\n        List<RoleEntry> roles = tx.select(ROLES.ROLE_ID, ROLES.ROLE_NAME,\n                isnull(permissions.asField(), new String[]{}))\n                .from(ROLES)\n                .where(ROLES.ROLE_ID.in(roleIds))\n                .fetch(e -> new RoleEntry(e.value1(), e.value2(), new HashSet<>(Arrays.asList(e.value3()))));\n\n        return new UserEntry(r.get(USERS.USER_ID),\n                r.get(USERS.USERNAME),\n                r.get(USERS.DOMAIN),\n                r.get(USERS.DISPLAY_NAME),\n                new HashSet<>(orgs),\n                UserType.valueOf(r.get(USERS.USER_TYPE)),\n                r.get(USERS.USER_EMAIL),\n                new HashSet<>(roles),\n                r.get(USERS.IS_DISABLED),\n                r.get(USERS.DISABLED_DATE),\n                r.get(USERS.IS_PERMANENTLY_DISABLED));\n    }\n\n    private static String toLowerCase(String s) {\n        if (s == null) {\n            return null;\n        }\n\n        return s.toLowerCase();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/user/UserEntry.java",
    "content": "package com.walmartlabs.concord.server.user;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.server.org.OrganizationEntry;\n\nimport javax.validation.constraints.Size;\nimport java.io.Serializable;\nimport java.time.OffsetDateTime;\nimport java.util.Set;\nimport java.util.UUID;\n\n@JsonInclude(Include.NON_NULL)\npublic class UserEntry implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    public static final int MAX_USERNAME_LENGTH = 128;\n    public static final int MAX_DOMAIN_LENGTH = 512;\n    public static final int MAX_DISPLAY_NAME_LENGTH = 1024;\n    public static final int MAX_EMAIL_LENGTH = 512;\n\n    private final UUID id;\n\n    @Size(max = MAX_USERNAME_LENGTH)\n    private final String name;\n\n    @Size(max = MAX_DOMAIN_LENGTH)\n    private final String domain;\n\n    @Size(max = MAX_DISPLAY_NAME_LENGTH)\n    private final String displayName;\n\n    private final Set<OrganizationEntry> orgs;\n\n    private final UserType type;\n\n    @Size(max = MAX_EMAIL_LENGTH)\n    private final String email;\n\n    private final Set<RoleEntry> roles;\n\n    private final boolean disabled;\n\n    private final boolean permanentlyDisabled;\n\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    private final OffsetDateTime disabledDate;\n\n    @JsonCreator\n    public UserEntry(@JsonProperty(\"id\") UUID id,\n                     @JsonProperty(\"name\") String name,\n                     @JsonProperty(\"domain\") String domain,\n                     @JsonProperty(\"displayName\") String displayName,\n                     @JsonProperty(\"orgs\") Set<OrganizationEntry> orgs,\n                     @JsonProperty(\"type\") UserType type,\n                     @JsonProperty(\"email\") String email,\n                     @JsonProperty(\"roles\") Set<RoleEntry> roles,\n                     @JsonProperty(\"disabled\") boolean disabled,\n                     @JsonProperty(\"disabledDate\") OffsetDateTime disabledDate,\n                     @JsonProperty(\"permanentlyDisabled\") boolean permanentlyDisabled) {\n\n        this.id = id;\n        this.name = name;\n        this.domain = domain;\n        this.displayName = displayName;\n        this.orgs = orgs;\n        this.type = type;\n        this.email = email;\n        this.roles = roles;\n        this.disabled = disabled;\n        this.disabledDate = disabledDate;\n        this.permanentlyDisabled = permanentlyDisabled;\n    }\n\n    public UUID getId() {\n        return id;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public String getDomain() {\n        return domain;\n    }\n\n    public String getDisplayName() {\n        return displayName;\n    }\n\n    public Set<OrganizationEntry> getOrgs() {\n        return orgs;\n    }\n\n    public UserType getType() {\n        return type;\n    }\n\n    public String getEmail() {\n        return email;\n    }\n\n    public Set<RoleEntry> getRoles() {\n        return roles;\n    }\n\n    public boolean isDisabled() {\n        return disabled;\n    }\n\n    public OffsetDateTime getDisabledDate() {\n        return disabledDate;\n    }\n\n    public boolean isPermanentlyDisabled() {\n        return permanentlyDisabled;\n    }\n\n    @Override\n    public String toString() {\n        return \"UserEntry{\" +\n                \"id=\" + id +\n                \", name='\" + name + '\\'' +\n                \", domain='\" + domain + '\\'' +\n                \", displayName='\" + displayName + '\\'' +\n                \", orgs=\" + orgs +\n                \", type=\" + type +\n                \", email='\" + email + '\\'' +\n                \", roles=\" + roles +\n                \", disabled=\" + disabled +\n                \", disabledDate=\" + disabledDate +\n                \", permanentlyDisabled=\" + permanentlyDisabled +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/user/UserInfoProvider.java",
    "content": "package com.walmartlabs.concord.server.user;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.UUID;\n\nimport static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY;\n\npublic interface UserInfoProvider {\n\n    UserType getUserType();\n\n    /**\n     * Returns data for the specified user.\n     * @param id user's ID, optional\n     * @param username user's name, mandatory\n     * @param userDomain user's domain, optional\n     * @return UserInfo user's info\n     */\n    UserInfo getInfo(UUID id, String username, String userDomain);\n\n    UUID create(String username, String domain, String displayName, String email, Set<String> roles);\n\n    @Value.Immutable\n    @JsonInclude(NON_EMPTY)\n    @JsonSerialize(as = ImmutableUserInfo.class)\n    @JsonDeserialize(as = ImmutableUserInfo.class)\n    interface UserInfo {\n\n        static ImmutableUserInfo.Builder builder() {\n            return ImmutableUserInfo.builder();\n        }\n\n        @Nullable\n        UUID id();\n\n        @Nullable\n        String username();\n\n        @Nullable\n        String userDomain();\n\n        @Nullable\n        String displayName();\n\n        @Nullable\n        String email();\n\n        @Nullable\n        Set<String> groups();\n\n        @Nullable\n        Map<String, Object> attributes();\n\n        @Nullable\n        String usernameSignature();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/user/UserManager.java",
    "content": "package com.walmartlabs.concord.server.user;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.audit.AuditAction;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.audit.AuditObject;\nimport com.walmartlabs.concord.server.org.project.DiffUtils;\nimport com.walmartlabs.concord.server.org.team.TeamDao;\nimport com.walmartlabs.concord.server.org.team.TeamManager;\nimport com.walmartlabs.concord.server.org.team.TeamRole;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.security.ldap.LdapGroupSearchResult;\nimport org.jooq.DSLContext;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.core.Response;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.server.user.UserInfoProvider.UserInfo;\n\npublic class UserManager {\n\n    private final UserDao userDao;\n    private final TeamDao teamDao;\n    private final AuditLog auditLog;\n    private final Map<UserType, UserInfoProvider> userInfoProviders;\n\n    private static final String SSO_REALM_NAME = \"sso\";\n\n    @Inject\n    public UserManager(UserDao userDao, TeamDao teamDao, AuditLog auditLog, Set<UserInfoProvider> providers) {\n        this.userDao = userDao;\n        this.teamDao = teamDao;\n        this.auditLog = auditLog;\n\n        this.userInfoProviders = new HashMap<>();\n        providers.forEach(p -> this.userInfoProviders.put(p.getUserType(), p));\n    }\n\n    public Optional<UserEntry> get(String username, String domain, UserType type) {\n        if (type == null) {\n            type = UserPrincipal.assertCurrent().getType();\n        }\n\n        UUID id = userDao.getId(username, domain, type);\n        if (id == null) {\n            return Optional.empty();\n        }\n\n        return Optional.of(userDao.get(id));\n    }\n\n    public Optional<UserEntry> getOrCreate(String username, String userDomain, UserType type) {\n        return getOrCreate(username, userDomain, type, null, null, null);\n    }\n\n    public Optional<UserEntry> getOrCreate(String username, String userDomain, UserType type, String displayName, String email, Set<String> roles) {\n        Optional<UserEntry> result = get(username, userDomain, type);\n        if (result.isPresent()) {\n            return result;\n        }\n\n        if (type == null) {\n            type = UserPrincipal.assertCurrent().getType();\n        }\n\n        UserInfoProvider provider = assertProvider(type);\n        UserInfo info = provider.getInfo(null, username, userDomain);\n        if (info != null) {\n            username = info.username();\n            userDomain = info.userDomain();\n            displayName = info.displayName();\n            email = info.email();\n            result = get(username, userDomain, type);\n            if (result.isPresent()) {\n                return result;\n            }\n        }\n\n        return Optional.of(create(username, userDomain, displayName, email, type, roles));\n    }\n\n    public Optional<UserEntry> get(UUID id) {\n        return Optional.ofNullable(userDao.get(id));\n    }\n\n    public Optional<UUID> getId(String username, String userDomain, UserType type) {\n        return Optional.ofNullable(userDao.getId(username, userDomain, type));\n    }\n\n    public Optional<UserEntry> getUserFromExternalMapping(String externalId) {\n        if (externalId == null) {\n            return Optional.empty();\n        }\n\n        return Optional.ofNullable(userDao.getUserFromExternalMapping(externalId));\n    }\n\n    public void createExternalUserMapping(UUID concordUserId, String externalId) {\n        if (externalId == null) {\n            return;\n        }\n\n        userDao.createExternalUserMapping(concordUserId, externalId);\n    }\n\n    public Optional<UserEntry> update(UUID userId, String displayName, String email, UserType userType, boolean isDisabled, Set<String> roles) {\n        UserEntry prevEntry = userDao.get(userId);\n        if (prevEntry == null) {\n            return Optional.empty();\n        }\n\n        if (prevEntry.isPermanentlyDisabled()) {\n            throw new ConcordApplicationException(\"User is permanently disabled\");\n        }\n\n        if (prevEntry.isPermanentlyDisabled() && isDisabled) {\n            return Optional.empty();\n        }\n\n        UserEntry newEntry = userDao.update(userId, displayName, email, userType, isDisabled, roles);\n        if (newEntry == null) {\n            return Optional.empty();\n        }\n\n        Map<String, Object> changes = DiffUtils.compare(prevEntry, newEntry);\n        // some callers (e.g. the LDAP realm) update user records regardless of whether there was\n        // any actual changes or not\n        // add an audit log record only if there was any changes\n        if (!changes.isEmpty()) {\n            auditLog.add(AuditObject.USER, AuditAction.UPDATE)\n                    .field(\"userId\", userId)\n                    .field(\"username\", prevEntry.getName())\n                    .field(\"changes\", changes)\n                    .log();\n        }\n\n        return Optional.of(newEntry);\n    }\n\n    public UserEntry create(String username, String domain, String displayName, String email, UserType type, Set<String> roles) {\n        if (type == null) {\n            type = UserPrincipal.assertCurrent().getType();\n        }\n\n        UserInfoProvider provider = assertProvider(type);\n        UUID id = provider.create(username, domain, displayName, email, roles);\n\n        // add the new user to the default org/team\n        UUID teamId = TeamManager.DEFAULT_ORG_TEAM_ID;\n        teamDao.upsertUser(teamId, id, TeamRole.MEMBER);\n\n        UserEntry e = userDao.get(id);\n        auditLog.add(AuditObject.USER, AuditAction.CREATE)\n                .field(\"userId\", id)\n                .field(\"username\", e.getName())\n                .changes(null, e)\n                .log();\n\n        return e;\n    }\n\n    public boolean isInOrganization(UUID orgId) {\n        return userDao.txResult(tx -> isInOrganization(tx, orgId));\n    }\n\n    public boolean isInOrganization(DSLContext tx, UUID orgId) {\n        UserPrincipal p = UserPrincipal.assertCurrent();\n        UUID userId = p.getId();\n        return userDao.isInOrganization(tx, userId, orgId);\n    }\n\n    public UserInfo getCurrentUserInfo() {\n        UserPrincipal u = UserPrincipal.getCurrent();\n        if (u == null) {\n            return null;\n        }\n        UserType type = assertSsoUserType(u, u.getType());\n        UserInfoProvider p = assertProvider(type);\n        return p.getInfo(u.getId(), u.getUsername(), u.getDomain());\n    }\n\n    public List<LdapGroupSearchResult> searchLdapGroups(String filter) {\n        return userDao.searchLdapGroups(filter);\n    }\n\n    public UserInfo getInfo(String username, String domain, UserType type) {\n        UserInfoProvider p = assertProvider(assertUserType(username, domain, type));\n        return p.getInfo(null, username, domain);\n    }\n\n    public void enable(UUID userId) {\n        UserEntry user = userDao.get(userId);\n\n        if (user == null) {\n            throw new ConcordApplicationException(\"User not found: \" + userId);\n        }\n\n        if (!user.isDisabled()) {\n            // the account is already enabled, nothing to do\n            return;\n        }\n\n        if (user.isPermanentlyDisabled()) {\n            throw new ConcordApplicationException(\"User is permanently disabled\");\n        }\n\n        userDao.enable(userId);\n\n        auditLog.add(AuditObject.USER, AuditAction.UPDATE)\n                .field(\"userId\", userId)\n                .changes(describeStatusChange(true, false), describeStatusChange(false, false))\n                .log();\n    }\n\n    public void disable(UUID userId) {\n        UserEntry user = userDao.get(userId);\n        if (user == null) {\n            throw new ConcordApplicationException(\"User not found: \" + userId, Response.Status.NOT_FOUND);\n        }\n\n        if (user.isDisabled() && user.getDisabledDate() != null) {\n            // the account is already disabled, nothing to do\n            return;\n        }\n\n        userDao.disable(userId, false);\n\n        auditLog.add(AuditObject.USER, AuditAction.UPDATE)\n                .field(\"userId\", userId)\n                .changes(describeStatusChange(user.isDisabled(), user.isPermanentlyDisabled()), describeStatusChange(true, false))\n                .log();\n    }\n\n    public void permanentlyDisable(UUID userId) {\n        UserEntry user = userDao.get(userId);\n        if (user == null) {\n            throw new ConcordApplicationException(\"User not found: \" + userId, Response.Status.NOT_FOUND);\n        }\n\n        if (user.isPermanentlyDisabled() && user.isDisabled() && user.getDisabledDate() != null) {\n            // nothing to do\n            return;\n        }\n\n        userDao.disable(userId, true);\n\n        auditLog.add(AuditObject.USER, AuditAction.UPDATE)\n                .field(\"userId\", userId)\n                .changes(describeStatusChange(user.isDisabled(), user.isPermanentlyDisabled()), describeStatusChange(true, true))\n                .log();\n    }\n\n    public void delete(UUID userId) {\n        UserEntry user = userDao.get(userId);\n        if (user == null) {\n            return;\n        }\n\n        userDao.delete(userId);\n\n        auditLog.add(AuditObject.USER, AuditAction.DELETE)\n                .field(\"userId\", userId)\n                .field(\"name\", user.getName())\n                .log();\n    }\n\n    private UserInfoProvider assertProvider(UserType type) {\n        UserInfoProvider p = userInfoProviders.get(type);\n        if (p == null) {\n            throw new ConcordApplicationException(\"Unknown user account type: \" + type);\n        }\n        return p;\n    }\n\n    private UserType assertUserType(String username, String domain, UserType type) {\n        /* Override LDAP to SSO type if current user loggedIn via SSO */\n        /*\n        1. LoggedIn via internal user realm -> domain = null\n        2. LoggedIn via ldap realm -> realm != SSO_REALM_NAME\n        3. LoggedIn via SSO -> @return UserType.SSO\n         */\n        if (username != null && domain != null) {\n            UserPrincipal u = UserPrincipal.getCurrent();\n            if (u != null && u.getUsername().equalsIgnoreCase(username) && u.getDomain().equalsIgnoreCase(domain)) {\n                return assertSsoUserType(u, type);\n            }\n        }\n        return type;\n    }\n\n    private UserType assertSsoUserType(UserPrincipal u, UserType type) {\n        if (u.getRealm().equals(SSO_REALM_NAME)) {\n            if (userInfoProviders.get(UserType.SSO) != null) {\n                return UserType.SSO;\n            }\n        }\n        return type;\n    }\n\n    /**\n     * {@link DiffUtils#compare(Object, Object)}\n     * doesn't work for top-level Maps. So we have to create a temporary bean with a single\n     * field in order to record the user account's status change using the existing\n     * {@link AuditLog.EntryBuilder#changes(Object, Object)} mechanism w/o pulling\n     * the {@link UserEntry} before and after the change.\n     */\n    private static StatusChange describeStatusChange(boolean disabled, boolean permanentlyDisabled) {\n        return new StatusChange(disabled, permanentlyDisabled);\n    }\n\n    private record StatusChange(boolean disabled, boolean permanentlyDisabled) {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/user/UserModule.java",
    "content": "package com.walmartlabs.concord.server.user;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\n\nimport static com.walmartlabs.concord.server.Utils.bindJaxRsResource;\n\npublic class UserModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        bindJaxRsResource(binder, UserResource.class);\n        bindJaxRsResource(binder, UserResourceV2.class);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/user/UserResource.java",
    "content": "package com.walmartlabs.concord.server.user;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.GenericOperationResult;\nimport com.walmartlabs.concord.server.OperationResult;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.Validate;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.validation.Valid;\nimport javax.validation.constraints.NotNull;\nimport javax.validation.constraints.Size;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response.Status;\nimport java.util.UUID;\n\n@Path(\"/api/v1/user\")\n@Tag(name = \"Users\")\npublic class UserResource implements Resource {\n\n    private final UserManager userManager;\n    private final UserDao userDao;\n\n    @Inject\n    public UserResource(UserManager userManager, UserDao userDao) {\n        this.userManager = userManager;\n        this.userDao = userDao;\n    }\n\n    /**\n     * Creates a new user or updated an existing one.\n     *\n     * @param req user's data\n     * @return\n     */\n    @POST\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Create a new user or update an existing one\", operationId = \"createOrUpdateUser\")\n    public CreateUserResponse createOrUpdate(@Valid CreateUserRequest req) {\n        assertAdmin();\n\n        String username = req.getUsername();\n        UserType type = assertUserType(req.getType());\n\n        UUID id = userManager.getId(username, req.getUserDomain(), type).orElse(null);\n        if (id == null) {\n            UserEntry e = userManager.create(username, req.getUserDomain(), req.getDisplayName(), req.getEmail(), req.getType(), req.getRoles());\n            return new CreateUserResponse(e.getId(), e.getName(), OperationResult.CREATED);\n        } else {\n            UserEntry e = userManager.update(id, req.getDisplayName(), req.getEmail(), req.getType(), req.isDisabled(), req.getRoles()).orElse(null);\n            if (e == null) {\n                throw new ConcordApplicationException(\"User not found: \" + id, Status.BAD_REQUEST);\n            }\n            return new CreateUserResponse(id, e.getName(), OperationResult.UPDATED);\n        }\n    }\n\n    /**\n     * Finds an existing user by username.\n     *\n     * @param username\n     * @return user details\n     */\n    @GET\n    @Path(\"/{username}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Find a user\")\n    public UserEntry findByUsername(@PathParam(\"username\") @Size(max = UserEntry.MAX_USERNAME_LENGTH) @NotNull String username) {\n        assertAdmin();\n\n        UUID id = userManager.getId(username, null, null)\n                .orElseThrow(() -> new ConcordApplicationException(\"User not found: \" + username, Status.NOT_FOUND));\n\n        return userDao.get(id);\n    }\n\n    /**\n     * Disables an existing user. Optionally allows permanent disabling of the user.\n     *\n     * @param id ID of user to disable\n     * @param permanent When <code>true</code>, user cannot be automatically re-enabled on login\n     * @return updated user details\n     */\n    @PUT\n    @Path(\"/{id}/disable\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Disable a user\")\n    public UserEntry disableUser(@PathParam(\"id\") UUID id, @QueryParam(\"permanent\") boolean permanent) {\n        assertAdmin();\n\n        if (permanent) {\n            userManager.permanentlyDisable(id);\n        } else {\n            userManager.disable(id);\n        }\n\n        return userDao.get(id);\n    }\n\n    /**\n     * Removes an existing user.\n     *\n     * @param id User's database ID\n     * @return\n     */\n    @DELETE\n    @Path(\"/{id}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    // TODO use usernames instead of IDs?\n    @Operation(description = \"Delete an existing user\", operationId = \"deleteUser\")\n    public DeleteUserResponse delete(@PathParam(\"id\") UUID id) {\n        assertAdmin();\n\n        if (!userDao.existsById(id)) {\n            throw new ValidationErrorsException(\"User not found: \" + id);\n        }\n\n        userManager.delete(id);\n        return new DeleteUserResponse();\n    }\n\n    @POST\n    @Path(\"/{username}/roles\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @Produces(MediaType.APPLICATION_JSON)\n    @Validate\n    @Operation(description = \"Update the list of roles for the existing user\")\n    public GenericOperationResult updateUserRoles(@PathParam(\"username\") @Size(max = UserEntry.MAX_USERNAME_LENGTH) String username,\n                                                  @Valid UpdateUserRolesRequest req) {\n        assertAdmin();\n\n        // TODO: type from request\n        UserType type = UserPrincipal.assertCurrent().getType();\n        // TODO: userDomain from request\n        String userDomain = null;\n        UUID id = userManager.getId(username, userDomain, type)\n                .orElseThrow(() -> new ConcordApplicationException(\"User not found: \" + username, Status.NOT_FOUND));\n\n        userDao.updateRoles(id, req.getRoles());\n        return new GenericOperationResult(OperationResult.UPDATED);\n    }\n\n    private static void assertAdmin() {\n        if (!Roles.isAdmin()) {\n            throw new UnauthorizedException(\"Only admins can do that\");\n        }\n    }\n\n    private static UserType assertUserType(UserType type) {\n        if (type != null && type.equals(UserType.SSO)) {\n            throw new ConcordApplicationException(\"User of type \"+type.name()+\" cannot be created\", Status.BAD_REQUEST);\n        }\n        \n        if (type != null) {\n            return type;\n        }\n        return UserPrincipal.assertCurrent().getType();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/user/UserResourceV2.java",
    "content": "package com.walmartlabs.concord.server.user;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.UnauthorizedException;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport java.util.List;\nimport java.util.UUID;\n\n@Path(\"/api/v2/user\")\n@Tag(name = \"UserV2\")\npublic class UserResourceV2 implements Resource {\n\n    private final UserDao userDao;\n\n    @Inject\n    public UserResourceV2(UserDao userDao) {\n        this.userDao = userDao;\n    }\n\n    @GET\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"Returns a list of existing active users matching the supplied filter\", operationId = \"listUsersWithFilter\")\n    public List<UserEntry> list(@QueryParam(\"offset\") @DefaultValue(\"0\") int offset,\n                                @QueryParam(\"limit\") @DefaultValue(\"10\") int limit,\n                                @QueryParam(\"filter\") String filter) {\n\n        if (offset < 0) {\n            throw new ValidationErrorsException(\"'offset' must be a positive number or zero\");\n        }\n\n        if (limit < 1) {\n            throw new ValidationErrorsException(\"'limit' must be a positive number\");\n        }\n\n        return userDao.list(filter, offset, limit);\n    }\n\n    @GET\n    @Path(\"/{id}\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    @Operation(description = \"Get an existing user\", operationId = \"getUser\")\n    public UserEntry get(@PathParam(\"id\") UUID id) {\n\n        UserPrincipal loggedIn = UserPrincipal.assertCurrent();\n\n        UUID authenticatedId = loggedIn.getId();\n\n        if (authenticatedId.equals(id) || Roles.isAdmin() || Roles.isGlobalReader()) {\n            return userDao.get(id);\n        }\n\n        throw new UnauthorizedException(\"Users can only view their own information or must have admin privileges.\");\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/user/UserTeam.java",
    "content": "package com.walmartlabs.concord.server.user;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.org.team.TeamRole;\nimport org.immutables.value.Value;\n\nimport java.util.UUID;\n\n@Value.Immutable\npublic interface UserTeam {\n\n    UUID teamId();\n\n    TeamRole role();\n\n    static UserTeam of(UUID teamId, TeamRole role) {\n        return UserTeam.builder()\n                .teamId(teamId)\n                .role(role)\n                .build();\n    }\n\n    static ImmutableUserTeam.Builder builder() {\n        return ImmutableUserTeam.builder();\n    }\n}\n"
  },
  {
    "path": "server/impl/src/main/java/com/walmartlabs/concord/server/user/UserType.java",
    "content": "package com.walmartlabs.concord.server.user;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic enum UserType {\n\n    LOCAL,\n    LDAP,\n    SSO\n}\n"
  },
  {
    "path": "server/impl/src/main/resources/com/walmartlabs/concord/server/console/badRequest.html",
    "content": "{{> header}}\n<div class=\"ui negative message\">\n    <div class=\"header\">Invalid request</div>\n    <p>{{message}}</p>\n</div>\n<p>\n    Check your request parameters and process configuration and try again.\n</p>\n{{> footer}}"
  },
  {
    "path": "server/impl/src/main/resources/com/walmartlabs/concord/server/console/footer.html",
    "content": "</div></div>\n<script>\n    $('.url.example .ui.embed').embed();\n</script>\n</body></html>"
  },
  {
    "path": "server/impl/src/main/resources/com/walmartlabs/concord/server/console/formNotFound.html",
    "content": "{{> header}}\n\n<h1>Form not found</h1>\n\n<p>\n    Form doesn't exist or already submitted.\n    Check <a href=\"/#/process/{{instanceId}}\" style=\"text-decoration: underline;\">the process status page</a> for more details.\n</p>\n\n{{> footer}}"
  },
  {
    "path": "server/impl/src/main/resources/com/walmartlabs/concord/server/console/genericError.html",
    "content": "{{> header}}\n\n<div class=\"ui negative message\">\n    <div class=\"header\">Unexpected Error - {{ statusCode }}</div>\n    <p>The application has encountered an unknown error. Check request URL and parameters.</p>\n</div>\n\n{{> footer}}\n"
  },
  {
    "path": "server/impl/src/main/resources/com/walmartlabs/concord/server/console/header.html",
    "content": "<!doctype html>\n<html>\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link rel=\"stylesheet\" href=\"/resources/console/semantic.min.css\"/>\n    <script src=\"/resources/console/jquery.min.js\"></script>\n    <script src=\"/resources/console/semantic.min.js\"></script>\n    <style>\n        body {\n            margin-top: 10px;\n        }\n    </style>\n    <title>Concord</title>\n</head>\n<body>\n<div class=\"ui grid\">\n    <div class=\"six wide column centered\">\n"
  },
  {
    "path": "server/impl/src/main/resources/com/walmartlabs/concord/server/console/inProgress.html",
    "content": "{{> header}}\n\n<div id=\"messageFinishedCtrl\" class=\"ui icon green message hidden\">\n    <p>Process is successfully executed. You can view details <a href=\"/#/process/{{instanceId}}\" style=\"text-decoration: underline;\">here</a></p>\n</div>\n<div id=\"messageErrorCtrl\" class=\"ui icon red message hidden\">\n    <p>Process is terminated with error(s). You can view details <a href=\"/#/process/{{instanceId}}\" style=\"text-decoration: underline;\">here</a></p>\n</div>\n\n\n<div id=\"loadingCtrl\" class=\"ui segment\">\n    <div class=\"ui relaxed divided list\">\n        {{#instanceId}}\n        <div class=\"item\">\n            <div class=\"content\">\n                <div class=\"header\">Process</div>\n                <div class=\"description\">{{instanceId}}</div>\n            </div>\n        </div>\n        {{/instanceId}}\n        {{#initiator}}\n        <div class=\"item\">\n            <div class=\"content\">\n                <div class=\"header\">Initiator</div>\n                <div class=\"description\">{{initiator}}</div>\n            </div>\n        </div>\n        {{/initiator}}\n    </div>\n\n    <h4><i id=\"loading-indicator\" class=\"spinner loading icon\"></i><span id=\"message\">{{status}}</span></h4>\n</div>\n\n\n<form id=\"nextForm\" action=\"/api/v1/org/{{instanceId}}/next\" method=\"POST\"></form>\n<script>\n\nvar checkStatusUrl = \"/api/v1/process/{{instanceId}}\";\nvar proceedToNextStepUrl = \"/api/v1/org/{{instanceId}}/next\";\n\nvar terminatingStatuses = [\"FINISHED\", \"CANCELLED\", \"FAILED\"];\nvar nextFormStatuses = [\"SUSPENDED\"];\nvar successStatuses= [\"FINISHED\"];\n\nvar retryDelayTime = 2000;\n\nfunction checkStatus(){\n    $.ajax({\n        url: checkStatusUrl\n    })\n    .then(function(response){\n        handleResponse(response);\n    });\n}\n\nfunction handleResponse(responseJson){\n    console.log(responseJson.status);\n\n    $(\"#message\").text(responseJson.status);\n\n    if(terminatingStatuses.includes(responseJson.status)){\n\n        hideInProgress(responseJson.status);\n        return;\n\n    }else if(nextFormStatuses.includes(responseJson.status)){\n\n        $('#nextForm').attr('action', proceedToNextStepUrl);\n        $(\"#nextForm\").submit();\n\n    }else{\n\n        setTimeout(checkStatus, retryDelayTime);\n    }\n}\n\nfunction hideInProgress(status){\n    $(\"#loading-indicator\").removeClass(\"loading\");\n\n    if (successStatuses.includes(status)){\n        $(\"#messageFinishedCtrl\").show();\n    }else{\n        $(\"#messageErrorCtrl\").show();\n    }\n}\n\n$(document).ready(function() {\n    checkStatus();\n});\n\n</script>\n\n{{> footer}}"
  },
  {
    "path": "server/impl/src/main/resources/com/walmartlabs/concord/server/console/processError.html",
    "content": "{{> header}}\n<div class=\"ui negative message\">\n    <div class=\"header\">Error</div>\n    <p>{{message}}</p>\n    {{#instanceId}}\n    <p><a href=\"/#/process/{{instanceId}}\">Check the status</a></p>\n    {{/instanceId}}\n</div>\n\n{{#stacktrace}}\n<div class=\"ui divider\"></div>\n\n<div class=\"ui segment\" style=\"font-family: monospace; white-space: pre-wrap; overflow-x: scroll\">\n    {{stacktrace}}\n</div>\n{{/stacktrace}}\n\n{{> footer}}"
  },
  {
    "path": "server/impl/src/main/resources/com/walmartlabs/concord/server/console/processFinished.html",
    "content": "{{> header}}\n<div class=\"ui message\">\n    <div class=\"header\">Process finished</div>\n    <p>\n        <a href=\"/#/process/{{instanceId}}\">Check the status</a>\n    </p>\n</div>\n{{> footer}}"
  },
  {
    "path": "server/impl/src/main/resources/com/walmartlabs/concord/server/email/api-key-expiration.mustache",
    "content": "The following Concord API Keys are about to expire in {{days}} days:\n{{#keys}}\n    '{{name}}' -  will expire at {{expiredAt}}\n{{/keys}}\n\nPlease replace the expired tokens using the REST API or Concord Console.\n"
  },
  {
    "path": "server/impl/src/main/resources/com/walmartlabs/concord/server/org/triggers/concord.yml",
    "content": "configuration:\n  runtime: \"concord-v2\"\n\nflows:\n  onChange:\n  - name: \"Repository Refresh\"\n    task: repositoryRefresh\n    in:\n      # TODO remove after agent version catch up with server\n      repositoryInfo: \"${event.repositoryInfo.stream().filter(r -> r.enabled).toList()}\"\n      event: \"${event}\"\n\ntriggers:\n  - concord:\n      version: 2\n      conditions:\n        event: \"repository.*\"\n      entryPoint: \"onChange\"\n\n  - github:\n      version: 2\n      entryPoint: \"onChange\"\n      conditions:\n        type: \"push\"\n        githubOrg: \".*\"\n        githubRepo: \".*\"\n        payload:\n          deleted: false # don't start for deleted refs\n        branch: \".*\"\n        repositoryInfo:\n          # trigger only for registered enabled repos (i.e. added to Concord)\n          - repository: \".*\"\n            enabled: true\n"
  },
  {
    "path": "server/impl/src/main/resources/com/walmartlabs/concord/server/selfcheck/concord.yml",
    "content": "flows:\n  default:\n  - log: \"OK\""
  },
  {
    "path": "server/impl/src/main/resources/logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] [%-5level] [%X{instanceId:-}] %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <logger name=\"com.walmartlabs.ollie\" level=\"WARN\"/>\n    <logger name=\"org.apache.shiro\" level=\"ERROR\"/>\n    <logger name=\"org.eclipse.jetty.server\" level=\"WARN\"/>\n    <logger name=\"org.javers.core\" level=\"WARN\"/>\n    <logger name=\"org.jooq\" level=\"WARN\"/>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n</configuration>\n"
  },
  {
    "path": "server/impl/src/main/resources/openapi-server-config.yaml",
    "content": "openAPI:\n  info:\n    version: '2.0'\n    title: Concord Server API\n    description: Concord Server API\n"
  },
  {
    "path": "server/impl/src/main/resources/security.json",
    "content": "{\n  \"api_key\": {\n    \"description\": \"API key\",\n    \"type\": \"apiKey\",\n    \"name\": \"Authorization\",\n    \"in\": \"header\"\n  },\n  \"session_key\": {\n    \"description\": \"Process session key\",\n    \"type\": \"apiKey\",\n    \"name\": \"X-Concord-SessionToken\",\n    \"in\": \"header\"\n  },\n  \"ldap\": {\n    \"description\": \"AD/LDAP username/password\",\n    \"type\": \"basic\",\n    \"name\": \"Authorization\",\n    \"in\": \"header\"\n  }\n}"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/AbstractDaoTest.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.MetricRegistry;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.DatabaseConfiguration;\nimport com.walmartlabs.concord.db.DatabaseModule;\nimport com.walmartlabs.concord.db.MainDBChangeLogProvider;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.jooq.impl.DSL;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\n\nimport javax.sql.DataSource;\nimport java.lang.reflect.Method;\nimport java.time.Duration;\nimport java.util.Base64;\nimport java.util.Collections;\nimport java.util.Map;\n\npublic abstract class AbstractDaoTest {\n\n    private final boolean migrateDb;\n\n    public AbstractDaoTest() {\n        this(true);\n    }\n\n    public AbstractDaoTest(boolean migrateDb) {\n        this.migrateDb = migrateDb;\n    }\n\n    private DataSource dataSource;\n    private Configuration cfg;\n\n    @BeforeEach\n    public void initDataSource() {\n        DatabaseConfiguration cfg = new DatabaseConfigurationImpl(\"jdbc:postgresql://localhost:5432/postgres\", \"postgres\", \"q1\", 3);\n\n        DatabaseModule db = new DatabaseModule(migrateDb);\n        this.dataSource = db.appDataSource(cfg, new MetricRegistry(), Collections.singleton(new MainDBChangeLogProvider()));\n\n        this.cfg = db.appJooqConfiguration(this.dataSource);\n    }\n\n    @AfterEach\n    public void closeDataSource() throws Exception {\n        Method m = dataSource.getClass().getMethod(\"close\");\n        m.invoke(dataSource);\n    }\n\n    protected void tx(AbstractDao.Tx t) {\n        DSL.using(cfg).transaction(cfg -> {\n            DSLContext tx = DSL.using(cfg);\n            t.run(tx);\n        });\n    }\n\n    protected Configuration getConfiguration() {\n        return cfg;\n    }\n\n    protected UuidGenerator getUuidGenerator() {\n        return new UuidGenerator();\n    }\n\n    private static final class DatabaseConfigurationImpl implements DatabaseConfiguration {\n\n        private final String url;\n        private final String username;\n        private final String password;\n        private final int maxPoolSize;\n\n        private DatabaseConfigurationImpl(String url, String username, String password, int maxPoolSize) {\n            this.url = url;\n            this.username = username;\n            this.password = password;\n            this.maxPoolSize = maxPoolSize;\n        }\n\n        @Override\n        public String url() {\n            return url;\n        }\n\n        @Override\n        public String username() {\n            return username;\n        }\n\n        @Override\n        public String password() {\n            return password;\n        }\n\n        @Override\n        public int maxPoolSize() {\n            return maxPoolSize;\n        }\n\n        @Override\n        public Duration maxLifetime() {\n            return Duration.ofSeconds(30);\n        }\n\n        @Override\n        public Map<String, Object> changeLogParameters() {\n            String fakeSecret = Base64.getEncoder().encodeToString(\"test\".getBytes());\n            return Map.of(\"createExtensionAvailable\", \"true\",\n                    \"defaultAdminToken\", fakeSecret,\n                    \"skipAdminTokenGeneration\", \"true\",\n                    \"defaultAgentToken\", fakeSecret,\n                    \"skipAgentTokenGeneration\", \"true\",\n                    \"secretStoreSalt\", fakeSecret,\n                    \"serverPassword\", fakeSecret);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/ConcordObjectMapperTest.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.common.ObjectMapperProvider;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nclass ConcordObjectMapperTest {\n\n    private static final ObjectMapper objectMapper = new ObjectMapperProvider().get();\n    private static final ConcordObjectMapper concordObjectMapper = new ConcordObjectMapper(objectMapper);\n\n    @Test\n    void testNul() {\n        // Postgres doesn't want a NUL character in JSONB, even if it's escaped.\n        var jsonB = concordObjectMapper.toJSONB(Map.of(\"aNul\", \">\\u0000<\"));\n\n        assertEquals(\"{\\\"aNul\\\":\\\"><\\\"}\", jsonB.toString());\n    }\n\n    @Test\n    void testSoh() {\n        // Other control characters are fine when escaped, e.g., SOH (Start of Heading, \\u0001)\n        var jsonB = concordObjectMapper.toJSONB(Map.of(\"aSoh\", \">\\u0001<\"));\n\n        assertEquals(\"{\\\"aSoh\\\":\\\">\\\\u0001<\\\"}\", jsonB.toString());\n    }\n\n    @Test\n    void testSimilarButNotControl() {\n        // Make sure we don't mess with similar but not control characters, e.g., \\u0000 preceded by a backslash\n        var jsonB = concordObjectMapper.toJSONB(Map.of(\"notNul\", \">\\\\u0000<\"));\n\n        assertEquals(\"{\\\"notNul\\\":\\\">\\\\\\\\u0000<\\\"}\", jsonB.toString());\n    }\n\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/ListenersTest.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.events.ProcessEvent;\nimport com.walmartlabs.concord.server.sdk.events.ProcessEventListener;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.OffsetDateTime;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Set;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class ListenersTest {\n\n    @Test\n    public void test() {\n        List<ProcessEvent> receivedEvents = new ArrayList<>();\n        Set<ProcessEventListener> processEventListeners = Collections.singleton(events -> {\n            synchronized (receivedEvents) {\n                receivedEvents.addAll(events);\n            }\n        });\n\n        Listeners listeners = new Listeners(processEventListeners, Collections.emptySet(), Collections.emptySet());\n        listeners.onProcessEvent(Collections.singletonList(ProcessEvent.builder()\n                .processKey(ProcessKey.random())\n                .eventSeq(0)\n                .eventDate(OffsetDateTime.now())\n                .eventType(\"TEST\")\n                .data(Collections.singletonMap(\"x\", 123))\n                .build()));\n\n        assertEquals(1, receivedEvents.size());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/OffsetDateTimeTest.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.DateTimeUtils;\nimport org.jooq.impl.DSL;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\nimport java.time.OffsetDateTime;\nimport java.util.UUID;\n\n@Disabled\npublic class OffsetDateTimeTest extends AbstractDaoTest {\n\n    public OffsetDateTimeTest() {\n        super(false);\n    }\n\n    @Test\n    public void test() throws Exception {\n        UUID instanceId = UUID.fromString(\"c686b84f-3d0d-4958-a156-9257db3efa13\");\n        OffsetDateTime createdAt = DateTimeUtils.fromIsoString(\"2020-07-16T21:30:53.907Z\");\n\n        DSL.using(getConfiguration()).connection(conn -> {\n            try (PreparedStatement ps = conn.prepareStatement(\"select * from process_state where instance_id = ? and instance_created_at = ?\")) {\n                ps.setObject(1, instanceId);\n                ps.setObject(2, createdAt);\n\n                try (ResultSet rs = ps.executeQuery()) {\n                    while (rs.next()) {\n                        String s = rs.getString(\"item_path\");\n                        System.out.println(s);\n                    }\n                }\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/TestObjectMapper.java",
    "content": "package com.walmartlabs.concord.server;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.common.ObjectMapperProvider;\n\npublic final class TestObjectMapper {\n\n    public static final ObjectMapper INSTANCE = createObjectMapper();\n\n    private static ObjectMapper createObjectMapper() {\n        return new ObjectMapperProvider().get();\n    }\n\n    private TestObjectMapper() {\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/events/ExpressionUtilsTest.java",
    "content": "package com.walmartlabs.concord.server.events;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class ExpressionUtilsTest {\n\n    @Test\n    public void simpleTest() {\n        Map<String, Object> event = new HashMap<>();\n        event.put(\"k\", \"v\");\n        event.put(\"commitMessage\", \"some text ${oops} moo\");\n\n        Map<String, Object> escaped = ExpressionUtils.escapeMap(event);\n        assertEquals(event.size(), escaped.size());\n        assertEquals(event.get(\"k\"), escaped.get(\"k\"));\n        assertEquals(\"some text \\\\${oops} moo\", escaped.get(\"commitMessage\"));\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/events/GithubEventInitiatorSupplierTest.java",
    "content": "package com.walmartlabs.concord.server.events;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2026 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.cache.LoadingCache;\nimport com.walmartlabs.concord.server.cfg.GithubConfiguration;\nimport com.walmartlabs.concord.server.events.GithubEventResource.GithubEventInitiatorSupplier;\nimport com.walmartlabs.concord.server.events.github.Payload;\nimport com.walmartlabs.concord.server.security.ldap.LdapManager;\nimport com.walmartlabs.concord.server.user.UserEntry;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport com.walmartlabs.concord.server.user.UserType;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\nimport java.time.OffsetDateTime;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.concurrent.ExecutionException;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.*;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.when;\n\n@ExtendWith(MockitoExtension.class)\nclass GithubEventInitiatorSupplierTest {\n\n    @Mock\n    private static GithubConfiguration githubCfg;\n\n    @Mock\n    private UserManager userManager;\n\n    @Mock\n    private LdapManager ldapManager;\n\n    @Mock\n    private LoadingCache<GithubEventResource.EmailCacheKey, Optional<String>> ghUserEmailCache;\n\n    private final UserEntry userEntry = new UserEntry(UUID.randomUUID(), \"mock-login\", null, null, Set.of(), UserType.LDAP, null, Set.of(), false, OffsetDateTime.now(), false);\n\n    private final Payload payload = Payload.from(\"push\", Map.of(\n            \"repository\", Map.of(\n                    \"full_name\", \"mock-org/mock-repo\",\n                    \"clone_url\", \"https://mock.local/mock-org/mock-repo.git\"\n            ),\n            \"sender\", Map.of(\n                    \"login\", \"mock-login\",\n                    \"node_id\", \"123\",\n                    \"url\", \"https://mock.local/mock-login\"\n            )\n    ));\n\n    @Test\n    void fallbackFoundAddedToMappingCache() throws ExecutionException {\n\n        // given: enableExternalUserIdMappingCache is enabled\n\n        when(githubCfg.isEnableExternalUserIdMappingCache()).thenReturn(true);\n        when(githubCfg.isUseSenderEmail()).thenReturn(true);\n\n        when(ghUserEmailCache.get(any(GithubEventResource.EmailCacheKey.class)))\n                .thenReturn(Optional.empty());\n\n        when(userManager.getOrCreate(\"mock-login\", null, UserType.LDAP))\n                .thenReturn(Optional.of(userEntry));\n\n        var initiatorSupplier = new GithubEventInitiatorSupplier(githubCfg, userManager, ldapManager, payload, ghUserEmailCache);\n\n\n        // when: get call finds user in fallback\n\n        initiatorSupplier.get();\n\n\n        // then: user found via fallback and user is added to cache\n\n        verify(userManager, times(1)).getUserFromExternalMapping(anyString());\n        verify(userManager, times(1)).getOrCreate(\"mock-login\", null, UserType.LDAP);\n        verify(userManager, times(1)).createExternalUserMapping(any(), any());\n        verifyNoMoreInteractions(userManager);\n    }\n\n    @Test\n    void userNotCreatedWhenFoundInMappingCache() {\n\n        // given: ExternalUserIdMappingCache is enabled and user exists in the db mapping cache\n\n        when(githubCfg.isEnableExternalUserIdMappingCache()).thenReturn(true);\n        when(userManager.getUserFromExternalMapping(any())).thenReturn(Optional.of(userEntry));\n\n        var initiatorSupplier = new GithubEventInitiatorSupplier(githubCfg, userManager, ldapManager, payload, ghUserEmailCache);\n\n\n        // when: get call finds user in db mapping cache\n\n        initiatorSupplier.get();\n\n\n        // then: cache lookup occurred, no mapping is created\n\n        verify(userManager, times(1)).getUserFromExternalMapping(any());\n        verify(userManager, times(0)).createExternalUserMapping(any(), any());\n        verifyNoMoreInteractions(userManager);\n    }\n\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/events/GithubUtilsTest.java",
    "content": "package com.walmartlabs.concord.server.events;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.events.github.GithubUtils;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class GithubUtilsTest {\n\n    @Test\n    public void testGetRefShortName() {\n        assertRefShortName(\"master\", \"master\");\n        assertRefShortName(\"refs/heads/master\", \"master\");\n        assertRefShortName(\"refs/tags/master\", \"master\");\n        assertRefShortName(\"refs/heads/feature/boo\", \"feature/boo\");\n        assertRefShortName(\"refs/remotes/boo/HEAD\", \"boo\");\n    }\n\n    @Test\n    public void testParse() {\n        assertRepositoryName(\"git@github.example.com:h1sammo/Anisble-GLS-AppInstall.git\", \"h1sammo/Anisble-GLS-AppInstall\");\n        assertRepositoryName(\"git+https://github.com/owner/name.git\", \"owner/name\");\n        assertRepositoryName(\"git://gh.pages.com/owner/name.git\", \"owner/name\");\n        assertRepositoryName(\"git://github.assemble.com/owner/name.git\", \"owner/name\");\n        assertRepositoryName(\"git://github.assemble.two.com/owner/name.git\", \"owner/name\");\n        assertRepositoryName(\"git://github.com/owner/name\", \"owner/name\");\n        assertRepositoryName(\"git@github.com/owner/name.git\", \"owner/name\");\n        assertRepositoryName(\"git@github.com:owner/name.git\", \"owner/name\");\n        assertRepositoryName(\"git@github.com:8080/owner/name.git\", \"owner/name\");\n        assertRepositoryName(\"github.com:owner/name.git\", \"owner/name\");\n        assertRepositoryName(\"github:owner/name\", \"owner/name\");\n        assertRepositoryName(\"http://github.com:8080/owner/name\", \"owner/name\");\n        assertRepositoryName(\"http://github.com/owner/name\", \"owner/name\");\n        assertRepositoryName(\"http://github.com/owner/name.git\", \"owner/name\");\n        assertRepositoryName(\"http://github.com/owner/name/tree\", \"owner/name\");\n        assertRepositoryName(\"http://github.com/owner/name/tree/master\", \"owner/name\");\n        assertRepositoryName(\"https://assemble@github.com/owner/name.git\", \"owner/name\");\n        assertRepositoryName(\"https://github.com/owner/name/blob/249b21a86400b38969cee3d5df6d2edf8813c137/README.md\", \"owner/name\");\n//        assertRepositoryName(\"git@github.com:owner/name.git#1.2.3\", \"owner/name\");\n    }\n\n    private static void assertRepositoryName(String url, String expected) {\n        String actual = GithubUtils.getRepositoryName(url);\n        assertEquals(expected, actual);\n    }\n\n    private static void assertRefShortName(String ref, String expected) {\n        String shortName = GithubUtils.getRefShortName(ref);\n        assertEquals(expected, shortName);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/events/github/PayloadTest.java",
    "content": "package com.walmartlabs.concord.server.events.github;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.org.triggers.TriggerEntry;\nimport com.walmartlabs.concord.server.org.triggers.TriggerEntryBuilder;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\nclass PayloadTest {\n\n    private static final String BASE_REPO = \"https://repo/base.git\";\n    private static final String FORK_REPO = \"https://repo/fork.git\";\n\n    @Test\n    void testSamePRRepo() {\n        Payload payload = createPRPayload(\"pull_request\", BASE_REPO);\n\n        assertFalse(payload.isPullRequestFromDifferentRepo());\n    }\n\n    @Test\n    void testDifferentPRRepo() {\n        Payload payload = createPRPayload(\"pull_request\", FORK_REPO);\n\n        assertTrue(payload.isPullRequestFromDifferentRepo());\n    }\n\n    @Test\n    void testSkipTriggerSamePRRepo() {\n        TriggerEntry t = generateTrigger(true);\n\n        boolean skip = GithubTriggerProcessor.skipTrigger(t, \"pull_request\", createPRPayload(\"pull_request\", BASE_REPO));\n        assertFalse(skip);\n    }\n\n    @Test\n    void testSkipTriggerDifferentPRRepo() {\n        TriggerEntry t = generateTrigger(true);\n\n        boolean skip = GithubTriggerProcessor.skipTrigger(t, \"pull_request\", createPRPayload(\"pull_request\", FORK_REPO));\n        assertTrue(skip);\n    }\n\n    @Test\n    void testSkipTriggerDifferentPRRepoNoUseEventCommitId() {\n        TriggerEntry t = generateTrigger(false);\n\n        boolean skip = GithubTriggerProcessor.skipTrigger(t, \"pull_request\", createPRPayload(\"pull_request\", FORK_REPO));\n        assertFalse(skip);\n    }\n\n    @Test\n    void testSkipTriggerPRReviewCommentDifferentPRRepo() {\n        TriggerEntry t = generateTrigger(true);\n\n        boolean skip = GithubTriggerProcessor.skipTrigger(t, \"pull_request_review_comment\", createPRPayload(\"pull_request_review_comment\", FORK_REPO));\n        assertTrue(skip);\n    }\n\n    @Test\n    void testSkipTriggerPush() {\n        TriggerEntry t = generateTrigger(true);\n        Payload p = Payload.from(\"push\", Map.of(\n                \"after\", \"123\",\n                \"before\", \"456\"\n        ));\n\n        boolean skip = GithubTriggerProcessor.skipTrigger(t, \"push\", p);\n        assertFalse(skip);\n    }\n\n    private static TriggerEntry generateTrigger(boolean useEventCommitId) {\n        return new TriggerEntryBuilder()\n                .eventSource(\"github\")\n                .id(UUID.randomUUID())\n                .orgId(UUID.randomUUID())\n                .orgName(\"mock-org\")\n                .projectId(UUID.randomUUID())\n                .projectName(\"mock-proj\")\n                .repositoryId(UUID.randomUUID())\n                .repositoryName(\"mock-repo\")\n                .cfg(Map.of(\"useEventCommitId\", useEventCommitId))\n                .build();\n    }\n\n    private static Payload createPRPayload(String eventName, String headUrl) {\n        return Payload.from(eventName, Map.of(\n                \"pull_request\", Map.of(\n                        \"base\", Map.of(\n                                \"repo\", Map.of(\n                                        \"clone_url\", BASE_REPO\n                                )\n                        ),\n                        \"head\", Map.of(\n                                \"repo\", Map.of(\n                                        \"clone_url\", headUrl\n                                )\n                        )\n                )\n        ));\n    }\n\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/org/jsonstore/JsonStorageQueryExecDaoTest.java",
    "content": "package com.walmartlabs.concord.server.org.jsonstore;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.AbstractDaoTest;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.TestObjectMapper;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.util.*;\n\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\n@Disabled(\"local DB connection required\")\npublic class JsonStorageQueryExecDaoTest extends AbstractDaoTest {\n\n    @Test\n    public void execQueryTest() throws Exception {\n        List<String> queries = parseQueries(\"queries.txt\");\n\n        JsonStoreQueryDao qd = mock(JsonStoreQueryDao.class);\n        JsonStoreQueryExecDao dao = new JsonStoreQueryExecDao(getConfiguration(), new ConcordObjectMapper(TestObjectMapper.INSTANCE), qd);\n\n        UUID storageId = UUID.randomUUID();\n        for(String sql : queries) {\n            String queryName = \"test\";\n            UUID queryId = UUID.randomUUID();\n            Map<String, Object> params = null;\n            if (sql.contains(\"?::jsonb\")) {\n                params = new HashMap<>();\n                params.put(\"k\", \"v\");\n            }\n            when(qd.get(eq(storageId), eq(queryName))).thenReturn(JsonStoreQueryEntry.builder()\n                    .id(queryId)\n                    .storeId(storageId)\n                    .name(queryName)\n                    .text(sql)\n                    .build());\n\n            List<Object> result = dao.exec(storageId, queryName, params);\n            assertNotNull(result);\n        }\n    }\n\n    private static List<String> parseQueries(String resourceName) throws IOException {\n        List<String> queries = new ArrayList<>();\n\n        try (InputStream in = JsonStorageQueryExecDaoTest.class.getResourceAsStream(resourceName);\n             BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {\n\n            int brk = 0;\n\n            StringBuilder sb = new StringBuilder();\n\n            String line;\n            while ((line = reader.readLine()) != null) {\n                if (line.trim().isEmpty()) {\n                    brk++;\n                    continue;\n                }\n\n                if (brk >= 2) {\n                    queries.add(sb.toString());\n                    sb = new StringBuilder();\n                }\n\n                sb.append(line).append(\" \");\n                brk = 0;\n            }\n        }\n        return queries;\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/org/project/DiffUtilsTest.java",
    "content": "package com.walmartlabs.concord.server.org.project;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.javers.core.metamodel.annotation.Id;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.UUID;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\n\npublic class DiffUtilsTest {\n\n    @Test\n    public void addTest() {\n        UUID personId = UUID.randomUUID();\n\n        Person left = PersonBuilder.Person().build();\n\n        Person right = PersonBuilder.Person()\n                .withId(personId)\n                .withName(\"Bravo\")\n                .build();\n\n        Map<String, Object> result = DiffUtils.compare(left, right);\n\n\n        assertNull(getProperty(result, \"prev\"));\n        assertEquals(personId, getProperty(result, \"new.id\"));\n        assertEquals(\"Bravo\", getProperty(result, \"new.name\"));\n    }\n\n    @Test\n    public void deleteTest() {\n        UUID personId = UUID.randomUUID();\n\n        Person left = PersonBuilder.Person()\n                .withId(personId)\n                .withName(\"Bravo\")\n                .build();\n        Person right = PersonBuilder.Person().build();\n\n        Map<String, Object> result = DiffUtils.compare(left, right);\n\n\n        assertNull(getProperty(result, \"new\"));\n        assertEquals(personId, getProperty(result, \"prev.id\"));\n        assertEquals(\"Bravo\", getProperty(result, \"prev.name\"));\n    }\n\n    @Test\n    public void propertyUpdateTest() {\n        UUID personId = UUID.randomUUID();\n\n        Person left = PersonBuilder.Person()\n                .withId(personId)\n                .withName(\"Alpha\")\n                .build();\n\n        Person right = PersonBuilder.Person()\n                .withId(personId)\n                .withName(\"Bravo\")\n                .build();\n\n        Map<String, Object> result = DiffUtils.compare(left, right);\n\n\n        assertNull(getProperty(result, \"prev.id\"));\n        assertNull(getProperty(result, \"new.id\"));\n\n        assertEquals(\"Alpha\", getProperty(result, \"prev.name\"));\n        assertEquals(\"Bravo\", getProperty(result, \"new.name\"));\n    }\n\n    @Test\n    public void propertyAddTest() {\n        UUID personId = UUID.randomUUID();\n\n        Person left = PersonBuilder.Person()\n                .withId(personId)\n                .build();\n\n        Person right = PersonBuilder.Person()\n                .withId(personId)\n                .withName(\"Bravo\")\n                .build();\n\n        Map<String, Object> result = DiffUtils.compare(left, right);\n\n\n        assertNull(getProperty(result, \"prev.id\"));\n        assertNull(getProperty(result, \"new.id\"));\n\n        assertNull(getProperty(result, \"prev.name\"));\n        assertEquals(\"Bravo\", getProperty(result, \"new.name\"));\n    }\n\n    @Test\n    public void propertyIdUpdateForEntityTest() {\n        UUID personId = UUID.randomUUID();\n        UUID personId2 = UUID.randomUUID();\n\n        PersonEntity left = PersonBuilder.Person()\n                .withId(personId)\n                .withName(\"Bravo\")\n                .buildEntity();\n\n        PersonEntity right = PersonBuilder.Person()\n                .withId(personId2)\n                .withName(\"Bravo\")\n                .buildEntity();\n\n        Map<String, Object> result = DiffUtils.compare(left, right);\n\n\n        assertEquals(personId, getProperty(result, \"prev.id\"));\n        assertEquals(personId2, getProperty(result, \"new.id\"));\n\n        assertEquals(\"Bravo\", getProperty(result, \"prev.name\"));\n        assertEquals(\"Bravo\", getProperty(result, \"new.name\"));\n    }\n\n    @Test\n    public void propertyIdUpdateForVOTest() {\n        UUID personId = UUID.randomUUID();\n        UUID personId2 = UUID.randomUUID();\n\n        Person left = PersonBuilder.Person()\n                .withId(personId)\n                .withName(\"Bravo\")\n                .build();\n\n        Person right = PersonBuilder.Person()\n                .withId(personId2)\n                .withName(\"Bravo\")\n                .build();\n\n        Map<String, Object> result = DiffUtils.compare(left, right);\n\n\n        assertEquals(personId, getProperty(result, \"prev.id\"));\n        assertEquals(personId2, getProperty(result, \"new.id\"));\n\n        assertNull(getProperty(result, \"prev.name\"));\n        assertNull(getProperty(result, \"new.name\"));\n    }\n\n    @Test\n    public void propertyDeleteTest() {\n        UUID personId = UUID.randomUUID();\n\n        Person left = PersonBuilder.Person()\n                .withId(personId)\n                .withName(\"Alpha\")\n                .build();\n\n        Person right = PersonBuilder.Person()\n                .withId(personId)\n                .build();\n\n        Map<String, Object> result = DiffUtils.compare(left, right);\n\n\n        assertNull(getProperty(result, \"prev.id\"));\n        assertNull(getProperty(result, \"new.id\"));\n\n        assertEquals(\"Alpha\", getProperty(result, \"prev.name\"));\n        assertNull(getProperty(result, \"new.name\"));\n    }\n\n    @Test\n    public void mapUpdateEntryTest() {\n        UUID personId = UUID.randomUUID();\n        UUID carId = UUID.randomUUID();\n\n        Person left = PersonBuilder.Person()\n                .withId(personId)\n                .withName(\"Alpha Bravo\")\n                .addCar(carId, \"Toyota\")\n                .build();\n\n        Person right = PersonBuilder.Person()\n                .withId(personId)\n                .withName(\"Alpha Bravo\")\n                .addCar(carId, \"Honda\")\n                .build();\n\n        Map<String, Object> result = DiffUtils.compare(left, right);\n\n\n        assertEquals(\"Toyota\", getProperty(result, \"prev.cars.\" + carId + \".make\"));\n        assertEquals(\"Honda\", getProperty(result, \"new.cars.\" + carId + \".make\"));\n\n        assertNull(getProperty(result, \"prev.cars.\" + carId + \".id\"));\n        assertNull(getProperty(result, \"new.cars.\" + carId + \".id\"));\n\n        assertNull(getProperty(result, \"prev.id\"));\n        assertNull(getProperty(result, \"prev.name\"));\n\n        assertNull(getProperty(result, \"new.id\"));\n        assertNull(getProperty(result, \"new.name\"));\n    }\n\n    @Test\n    public void mapAddEntryTest() {\n        UUID personId = UUID.randomUUID();\n        UUID carId1 = UUID.randomUUID();\n        UUID carId2 = UUID.randomUUID();\n\n        Person left = PersonBuilder.Person()\n                .withId(personId)\n                .withName(\"Alpha Bravo\")\n                .addCar(carId1, \"Toyota\")\n                .build();\n\n        Person right = PersonBuilder.Person()\n                .withId(personId)\n                .withName(\"Alpha Bravo\")\n                .addCar(carId1, \"Toyota\")\n                .addCar(carId2, \"Honda\")\n                .build();\n\n        Map<String, Object> result = DiffUtils.compare(left, right);\n\n        assertNull(getProperty(result, \"prev.id\"));\n        assertNull(getProperty(result, \"prev.name\"));\n\n        assertNull(getProperty(result, \"new.id\"));\n        assertNull(getProperty(result, \"new.name\"));\n\n        assertEquals(carId2, getProperty(result, \"new.cars.\" + carId2 + \".id\"));\n        assertNull(getProperty(result, \"prev.cars\"));\n    }\n\n    @Test\n    public void mapAddFirstEntryTest() {\n        UUID personId = UUID.randomUUID();\n        UUID carId1 = UUID.randomUUID();\n        UUID carId2 = UUID.randomUUID();\n\n        Person left = PersonBuilder.Person()\n                .withId(personId)\n                .withName(\"Alpha Bravo\")\n                .build();\n\n        Person right = PersonBuilder.Person()\n                .withId(personId)\n                .withName(\"Alpha Bravo\")\n                .addCar(carId1, \"Toyota\")\n                .addCar(carId2, \"Honda\")\n                .build();\n\n        Map<String, Object> result = DiffUtils.compare(left, right);\n\n        assertNull(getProperty(result, \"prev.id\"));\n        assertNull(getProperty(result, \"prev.name\"));\n\n        assertNull(getProperty(result, \"new.id\"));\n        assertNull(getProperty(result, \"new.name\"));\n\n        assertEquals(carId1, getProperty(result, \"new.cars.\" + carId1 + \".id\"));\n        assertEquals(carId2, getProperty(result, \"new.cars.\" + carId2 + \".id\"));\n        assertNull(getProperty(result, \"prev.cars\"));\n    }\n\n    @Test\n    public void mapDeleteEntryTest() {\n        UUID personId = UUID.randomUUID();\n        UUID carId1 = UUID.randomUUID();\n        UUID carId2 = UUID.randomUUID();\n\n        Person left = PersonBuilder.Person()\n                .withId(personId)\n                .withName(\"Alpha Bravo\")\n                .addCar(carId1, \"Toyota\")\n                .addCar(carId2, \"Honda\")\n                .build();\n\n        Person right = PersonBuilder.Person()\n                .withId(personId)\n                .withName(\"Alpha Bravo\")\n                .addCar(carId1, \"Toyota\")\n                .build();\n\n        Map<String, Object> result = DiffUtils.compare(left, right);\n\n        assertNull(getProperty(result, \"prev.id\"));\n        assertNull(getProperty(result, \"prev.name\"));\n\n        assertNull(getProperty(result, \"new.id\"));\n        assertNull(getProperty(result, \"new.name\"));\n\n        assertEquals(carId2, getProperty(result, \"prev.cars.\" + carId2 + \".id\"));\n        assertNull(getProperty(result, \"new.cars\"));\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private Object getProperty(Map<String, Object> map, String dottedPath) {\n        String[] path = dottedPath.split(\"\\\\.\");\n        Object currentObject = map;\n        for (String p : path) {\n            currentObject = ((Map<String, Object>) currentObject).get(p);\n            if (currentObject == null) {\n                return null;\n            }\n        }\n        return currentObject;\n    }\n\n    private static class Car {\n        UUID id;\n        String make;\n\n        Car(UUID id, String make) {\n            this.id = id;\n            this.make = make;\n        }\n\n        public UUID getId() {\n            return id;\n        }\n\n        public String getName() {\n            return make;\n        }\n\n    }\n\n    private static class Person {\n        UUID id;\n        String name;\n        Map<String, Car> cars;\n\n        Person(UUID id, String name, Map<String, Car> cars) {\n            this.id = id;\n            this.name = name;\n            this.cars = cars;\n        }\n\n        public UUID getId() {\n            return id;\n        }\n\n        public String getName() {\n            return name;\n        }\n    }\n\n    private static class PersonEntity {\n        @Id\n        UUID id;\n        String name;\n        Map<String, Car> cars;\n\n        PersonEntity(UUID id, String name, Map<String, Car> cars) {\n            this.id = id;\n            this.name = name;\n            this.cars = cars;\n        }\n\n        public UUID getId() {\n            return id;\n        }\n\n        public String getName() {\n            return name;\n        }\n    }\n\n    private static class PersonBuilder {\n        UUID id;\n        String name;\n        Map<String, Car> cars;\n\n        private PersonBuilder() {\n\n        }\n\n        static PersonBuilder Person() {\n            return new PersonBuilder();\n        }\n\n        public PersonBuilder withId(UUID id) {\n            this.id = id;\n            return this;\n        }\n\n        public PersonBuilder withName(String name) {\n            this.name = name;\n            return this;\n        }\n\n        public PersonBuilder addCar(UUID id, String make) {\n            if (Objects.isNull(cars)) {\n                this.cars = new HashMap<>();\n            }\n            this.cars.put(id.toString(), new Car(id, make));\n            return this;\n        }\n\n        public Person build() {\n            return new Person(id, name, cars);\n        }\n\n        public PersonEntity buildEntity() {\n            return new PersonEntity(id, name, cars);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/org/secret/PasswordCheckerTest.java",
    "content": "package com.walmartlabs.concord.server.org.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.user.UserEntry;\nimport org.apache.shiro.mgt.DefaultSecurityManager;\nimport org.apache.shiro.mgt.SecurityManager;\nimport org.apache.shiro.subject.SimplePrincipalCollection;\nimport org.apache.shiro.subject.Subject;\nimport org.apache.shiro.subject.SubjectContext;\nimport org.apache.shiro.subject.support.DefaultSubjectContext;\nimport org.apache.shiro.util.ThreadContext;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.UUID;\n\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.junit.jupiter.api.Assertions.fail;\n\npublic class PasswordCheckerTest {\n\n    private static final String USERNAME = \"vasia\";\n\n    @BeforeEach\n    public void bindUser() {\n        SecurityManager securityManager = new DefaultSecurityManager();\n        ThreadContext.bind(securityManager);\n\n        UserPrincipal p = new UserPrincipal(\"test\", new UserEntry(UUID.randomUUID(), USERNAME, null, null, null, null, null, null, false, null, false));\n        SubjectContext ctx = new DefaultSubjectContext();\n        ctx.setAuthenticated(true);\n        ctx.setPrincipals(new SimplePrincipalCollection(p, p.getRealm()));\n\n        Subject subject = securityManager.createSubject(ctx);\n        ThreadContext.bind(subject);\n    }\n\n    @AfterEach\n    public void unbindUser() {\n        ThreadContext.unbindSubject();\n    }\n\n    @Test\n    public void lengthTest() {\n        String password = \"aA3456\";\n\n        try {\n            PasswordChecker.check(password);\n            fail(\"exception expected\");\n        } catch (PasswordChecker.CheckerException e) {\n            assertTrue(e.getMessage().contains(\"seven (7) characters\"));\n        }\n    }\n\n    @Test\n    public void upperTest() {\n        String password = \"a234567\";\n\n        try {\n            PasswordChecker.check(password);\n            fail(\"exception expected\");\n        } catch (PasswordChecker.CheckerException e) {\n            assertTrue(e.getMessage().contains(\"UPPER character\"));\n        }\n    }\n\n    @Test\n    public void lowerTest() {\n        String password = \"A234567\";\n\n        try {\n            PasswordChecker.check(password);\n            fail(\"exception expected\");\n        } catch (PasswordChecker.CheckerException e) {\n            assertTrue(e.getMessage().contains(\"lowercase character\"));\n        }\n    }\n\n    @Test\n    public void digitTest() {\n        String password = \"AaAAAAA\";\n\n        try {\n            PasswordChecker.check(password);\n            fail(\"exception expected\");\n        } catch (PasswordChecker.CheckerException e) {\n            assertTrue(e.getMessage().contains(\"numeric character\"));\n        }\n    }\n\n    // Passwords may NOT contain three (3) consecutive characters from your user account name.\n    @Test\n    public void userTest() {\n        String password = \"Aa345678asi\";\n\n        try {\n            PasswordChecker.check(password);\n            fail(\"exception expected\");\n        } catch (PasswordChecker.CheckerException e) {\n            assertTrue(e.getMessage().contains(\"consecutive characters from your user account name\"));\n        }\n    }\n\n    @Test\n    public void validTest() throws PasswordChecker.CheckerException {\n        String password = \"Aa345678\";\n\n        PasswordChecker.check(password);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/org/triggers/CronUtilsTest.java",
    "content": "package com.walmartlabs.concord.server.org.triggers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport java.time.Duration;\nimport java.time.OffsetDateTime;\nimport java.time.ZoneId;\nimport java.time.format.DateTimeFormatter;\nimport java.util.concurrent.TimeUnit;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class CronUtilsTest {\n\n    @Test\n    public void test() {\n        OffsetDateTime now = OffsetDateTime.parse(\"2020-07-17T10:00:01-04:00\");\n\n        // ---\n\n        String spec = \"0 10 * * *\";\n        ZoneId zoneId = ZoneId.of(\"America/Toronto\");\n\n        OffsetDateTime next = CronUtils.nextExecution(now, spec, zoneId);\n        // next morning\n        assertEquals(\"2020-07-18T10:00:00-04:00\", DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(next));\n\n        // ---\n\n        spec = \"* * * * *\";\n        zoneId = ZoneId.of(\"UTC\");\n\n        // next minute\n        next = CronUtils.nextExecution(now, spec, zoneId);\n        assertEquals(\"2020-07-17T14:01:00Z\", DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(next));\n\n        // next after next\n        next = CronUtils.nextExecution(next, spec, zoneId);\n        assertEquals(\"2020-07-17T14:02:00Z\", DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(next));\n\n        Duration interval = Duration.between(next, CronUtils.nextExecution(next, spec, zoneId));\n        assertEquals(TimeUnit.MINUTES.toMillis(1), interval.toMillis());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/org/triggers/TriggerInternalIdCalculatorTest.java",
    "content": "package com.walmartlabs.concord.server.org.triggers;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport java.util.*;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotEquals;\n\npublic class TriggerInternalIdCalculatorTest {\n\n    @Test\n    public void testEmpty() {\n        String name = \"trigger\";\n        List<String> activeProfiles = List.of();\n        Map<String, Object> arguments = Map.of();\n        Map<String, Object> conditions = Map.of();\n        Map<String, Object> cfg = Map.of();\n\n        String id1 = TriggerInternalIdCalculator.getId(name, activeProfiles, arguments, conditions, cfg);\n        String id2 = TriggerInternalIdCalculator.getId(name, activeProfiles, arguments, conditions, cfg);\n        assertEquals(id1, id2);\n    }\n\n    @Test\n    public void test1() {\n        String name = \"trigger\";\n        List<String> activeProfiles = List.of();\n        Map<String, Object> arguments = Map.of();\n        Map<String, Object> conditions = Map.of();\n\n        Map<String, Object> cfg1 = Map.of(\n                \"a\", \"a-value\",\n                \"b\", Collections.singletonMap(\"k\", \"v\"),\n                \"c\", \"c-value\");\n\n        Map<String, Object> cfg2 = new LinkedHashMap<>();\n        cfg2.put(\"c\", \"c-value\");\n        cfg2.put(\"a\", \"a-value\");\n        cfg2.put(\"b\", Collections.singletonMap(\"k\", \"v\"));\n\n        String id1 = TriggerInternalIdCalculator.getId(name, activeProfiles, arguments, conditions, cfg1);\n        String id2 = TriggerInternalIdCalculator.getId(name, activeProfiles, arguments, conditions, cfg2);\n        assertEquals(id1, id2);\n    }\n\n    @Test\n    public void testArrayOfObjects() {\n        String name = \"trigger\";\n        List<String> activeProfiles = List.of();\n\n        Map<String, Object> arguments = Map.of(\n                \"listOfMaps\", List.of(Map.of(\"name\", \"one\"), Map.of(\"name\", \"two\")),\n                \"anotherProblem\", Arrays.asList(\"a\", 2, false));\n\n        Map<String, Object> conditions = Map.of();\n\n        Map<String, Object> configuration = Map.of(\n                \"name\", \"MyTrigger\",\n                \"entryPoint\", \"default\");\n\n        String id1 = TriggerInternalIdCalculator.getId(name, activeProfiles, arguments, conditions, configuration);\n        String id2 = TriggerInternalIdCalculator.getId(name, activeProfiles, arguments, conditions, configuration);\n        assertEquals(id1, id2);\n    }\n\n    @Test\n    public void testNotEquals() {\n        String name = \"trigger\";\n        List<String> activeProfiles = List.of();\n        Map<String, Object> arguments = Map.of();\n        Map<String, Object> conditions = Map.of();\n\n        Map<String, Object> cfg1 = Map.of(\n                \"a\", \"a-value\",\n                \"b\", Map.of(\"k\", \"v\"),\n                \"c\", \"c-value\");\n\n        Map<String, Object> cfg2 = Map.of(\n                \"c\", \"c-value\",\n                \"a\", \"a-value\",\n                \"boom\", Map.of(\"k\", \"v\"));\n\n        String id1 = TriggerInternalIdCalculator.getId(name, activeProfiles, arguments, conditions, cfg1);\n        String id2 = TriggerInternalIdCalculator.getId(name, activeProfiles, arguments, conditions, cfg2);\n        assertNotEquals(id1, id2);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/policy/PolicyCacheTest.java",
    "content": "package com.walmartlabs.concord.server.policy;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.collect.ImmutableMap;\nimport com.walmartlabs.concord.policyengine.EntityRule;\nimport com.walmartlabs.concord.policyengine.PolicyEngineRules;\nimport com.walmartlabs.concord.server.TestObjectMapper;\nimport com.walmartlabs.concord.server.cfg.PolicyCacheConfiguration;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.*;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.mock;\n\npublic class PolicyCacheTest {\n\n    @Test\n    public void allowNullValues() {\n        PolicyCache.Dao dao = mock(PolicyCache.Dao.class);\n        PolicyCache pc = new PolicyCache(TestObjectMapper.INSTANCE, new PolicyCacheConfiguration(), dao);\n\n        // ---\n        Map<String, Object> ruleParams = new HashMap<>();\n        ruleParams.put(\"nullValue\", null);\n        ruleParams.put(\"nullValue2\", null);\n        Map<String, Object> conditions = Collections.singletonMap(\"entity\", ImmutableMap.of(\"params\", ruleParams));\n\n        Map<String, Object> denyEntityRule = new HashMap<>();\n        denyEntityRule.put(\"msg\", \"test message\");\n        denyEntityRule.put(\"action\", \"create\");\n        denyEntityRule.put(\"entity\", \"trigger\");\n        denyEntityRule.put(\"conditions\", conditions);\n\n        Map<String, Object> rules = new HashMap<>();\n        rules.put(\"entity\", Collections.singletonMap(\"deny\", Collections.singletonList(denyEntityRule)));\n\n        List<PolicyCache.PolicyRules> policies = Collections.singletonList(\n                PolicyCache.PolicyRules.builder()\n                        .id(UUID.randomUUID())\n                        .name(\"test\")\n                        .rules(rules)\n                        .build());\n\n        // ---\n        Map<UUID, PolicyCache.Policy> merged = pc.mergePolicies(policies);\n        assertEquals(1, merged.size());\n\n        PolicyEngineRules actualRules = merged.values().iterator().next().rules();\n        assertEquals(1, actualRules.entityRules().getDeny().size());\n\n        EntityRule actualRule = actualRules.entityRules().getDeny().get(0);\n        assertEquals(conditions, actualRule.conditions());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/process/ImmutablesTest.java",
    "content": "package com.walmartlabs.concord.server.process;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.guava.GuavaModule;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.OffsetDateTime;\nimport java.util.UUID;\n\npublic class ImmutablesTest {\n\n    @Test\n    public void test() throws Exception {\n        ObjectMapper om = new ObjectMapper();\n        om.registerModule(new GuavaModule());\n        om.registerModule(new Jdk8Module());\n        om.registerModule(new JavaTimeModule());\n\n        ProcessEntry e = ImmutableProcessEntry.\n                builder()\n                .instanceId(UUID.randomUUID())\n                .kind(ProcessKind.DEFAULT)\n                .status(ProcessStatus.FINISHED)\n                .createdAt(OffsetDateTime.now())\n                .lastUpdatedAt(OffsetDateTime.now())\n                .disabled(false)\n                .build();\n\n        String s = om.writeValueAsString(e);\n        System.out.println(s);\n\n        ProcessEntry e2 = om.readValue(s, ProcessEntry.class);\n        System.out.println(e2);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/process/pipelines/processors/ConfigurationProcessorTest.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.policyengine.PolicyEngine;\nimport com.walmartlabs.concord.policyengine.PolicyEngineRules;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.org.OrganizationDao;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.org.project.ProjectEntry;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.stubbing.Answer;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.logging.LogManager;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\npublic class ConfigurationProcessorTest {\n\n    private ConfigurationProcessor p;\n    private ProjectDao projectDao;\n    private OrganizationDao orgDao;\n\n    @BeforeEach\n    public void init() {\n        projectDao = mock(ProjectDao.class);\n        orgDao = mock(OrganizationDao.class);\n        p = new ConfigurationProcessor(projectDao, orgDao, mock(ProcessLogManager.class));\n    }\n\n    @Test\n    public void testAllCfg() throws Exception {\n        Path workDir = Files.createTempDirectory(\"testAllCfg_workDir\");\n\n        UUID instanceId = UUID.randomUUID();\n        UUID orgId = UUID.randomUUID();\n        UUID prjId = UUID.randomUUID();\n\n        Map<String, Object> req = new HashMap<>();\n        req.put(\"a\", \"a-req\");\n        req.put(\"req\", \"req-value\");\n\n        Map<String, Object> orgCfg = new HashMap<>();\n        orgCfg.put(\"a\", \"a-org\");\n        orgCfg.put(\"org\", \"org-value\");\n\n        Map<String, Object> prjCfg = new HashMap<>();\n        prjCfg.put(\"a\", \"a-prj\");\n        prjCfg.put(\"project\", \"prj-value\");\n\n        ProjectEntry projectEntry = new ProjectEntry(prjId, null, null, null, null, null, prjCfg, null, null, null, null, null, null, null); // nom-nom\n\n        Map<String, Object> processCfgPolicy = new HashMap<>();\n        processCfgPolicy.put(\"a\", \"a-process-cfg-policy\");\n        processCfgPolicy.put(\"process-cfg-policy\", \"process-cfg-policy-value\");\n\n        Map<String, Object> defaultProcessCfgPolicy = new HashMap<>();\n        defaultProcessCfgPolicy.put(\"a\", \"default\");\n        defaultProcessCfgPolicy.put(\"process-cfg-policy\", \"default-2\");\n\n        PolicyEngineRules policy = PolicyEngineRules.builder()\n                .processCfg(processCfgPolicy)\n                .defaultProcessCfg(defaultProcessCfgPolicy)\n                .build();\n\n        // ---\n        when(orgDao.getConfiguration(eq(orgId))).thenReturn(orgCfg);\n        when(projectDao.get(eq(prjId))).thenReturn(projectEntry);\n\n        Payload payload = new Payload(ProcessKey.random());\n        payload = payload\n                .putHeader(Payload.CONFIGURATION, req)\n                .putHeader(Payload.ORGANIZATION_ID, orgId)\n                .putHeader(Payload.PROJECT_ID, prjId)\n                .putHeader(Payload.WORKSPACE_DIR, workDir)\n                .putHeader(Payload.POLICY, new PolicyEngine(\"test\", policy));\n\n        // ---\n        Map<String, Object> expected = new HashMap<>();\n        expected.put(\"activeProfiles\", Collections.singletonList(\"default\"));\n\n        // orgCfg < prjCfg < req < org-policy < prj-policy\n        expected.put(\"a\", \"a-process-cfg-policy\");\n        expected.put(\"org\", \"org-value\");\n        expected.put(\"project\", \"prj-value\");\n        expected.put(\"req\", \"req-value\");\n        expected.put(\"process-cfg-policy\", \"process-cfg-policy-value\");\n\n        Map<String, Object> result = process(payload);\n        result.remove(Constants.Request.ARGUMENTS_KEY); // don't care about arguments and other stuff here\n        result.remove(Constants.Request.PROCESS_INFO_KEY);\n        result.remove(Constants.Request.PROJECT_INFO_KEY);\n        assertEquals(expected, result);\n    }\n\n    @Test\n    public void testWithoutPolicy() throws Exception {\n        Path workDir = Files.createTempDirectory(\"testWithoutPoliy_workDir\");\n\n        UUID instanceId = UUID.randomUUID();\n        UUID orgId = UUID.randomUUID();\n        UUID prjId = UUID.randomUUID();\n\n        Map<String, Object> req = new HashMap<>();\n        req.put(\"a\", \"a-req\");\n        req.put(\"req\", \"req-value\");\n\n        Map<String, Object> orgCfg = new HashMap<>();\n        orgCfg.put(\"a\", \"a-org\");\n        orgCfg.put(\"org\", \"org-value\");\n\n        Map<String, Object> prjCfg = new HashMap<>();\n        prjCfg.put(\"a\", \"a-prj\");\n        prjCfg.put(\"project\", \"prj-value\");\n\n        ProjectEntry projectEntry = new ProjectEntry(prjId, null, null, null, null, null, prjCfg, null, null, null, null, null, null, null);\n\n        // ---\n        when(orgDao.getConfiguration(eq(orgId))).thenReturn(orgCfg);\n        when(projectDao.get(eq(prjId))).thenReturn(projectEntry);\n\n        Payload payload = new Payload(ProcessKey.random());\n        payload = payload\n                .putHeader(Payload.CONFIGURATION, req)\n                .putHeader(Payload.ORGANIZATION_ID, orgId)\n                .putHeader(Payload.PROJECT_ID, prjId)\n                .putHeader(Payload.WORKSPACE_DIR, workDir);\n\n        // ---\n        Map<String, Object> expected = new HashMap<>();\n        expected.put(\"activeProfiles\", Collections.singletonList(\"default\"));\n\n        // orgCfg < prjCfg < req\n        expected.put(\"a\", \"a-req\");\n        expected.put(\"org\", \"org-value\");\n        expected.put(\"project\", \"prj-value\");\n        expected.put(\"req\", \"req-value\");\n\n        Map<String, Object> result = process(payload);\n        result.remove(Constants.Request.ARGUMENTS_KEY); // don't care about arguments and other stuff here\n        result.remove(Constants.Request.PROCESS_INFO_KEY);\n        result.remove(Constants.Request.PROJECT_INFO_KEY);\n        assertEquals(expected, result);\n    }\n\n    private Map<String, Object> process(Payload payload) {\n        return processPayload(payload).getHeader(Payload.CONFIGURATION);\n    }\n\n    private Payload processPayload(Payload payload) {\n        Chain chain = mock(Chain.class);\n\n        when(chain.process(any())).thenAnswer((Answer<Payload>) invocation -> {\n            Object[] args = invocation.getArguments();\n            return (Payload) args[0];\n        });\n\n        return p.process(chain, payload);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/process/pipelines/processors/TemplateScriptProcessTest.java",
    "content": "package com.walmartlabs.concord.server.process.pipelines.processors;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.cfg.TemplatesConfiguration;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.keys.HeaderKey;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.io.TempDir;\nimport org.mockito.ArgumentMatchers;\n\nimport java.nio.file.Path;\n\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\nclass TemplateScriptProcessTest {\n\n    @TempDir\n    Path workspace;\n\n    private ProcessLogManager logManager;\n    private TemplatesConfiguration cfg;\n\n    private Chain chain;\n    private Payload payload;\n\n    @BeforeEach\n    void init() {\n        logManager = mock(ProcessLogManager.class);\n        cfg = mock(TemplatesConfiguration.class);\n        chain = mock(Chain.class);\n        payload = mock(Payload.class);\n    }\n\n    @Test\n    void testDisableScripting() {\n        when(cfg.isAllowScripting()).thenReturn(false);\n        var processor = new TemplateScriptProcessor(logManager, cfg);\n\n        processor.process(chain, payload);\n\n        verify(payload, times(0)).getHeader(any(), any());\n    }\n\n    @Test\n    void testEnableScripting() {\n        when(cfg.isAllowScripting()).thenReturn(true);\n        var headerMatcher = ArgumentMatchers.<HeaderKey<Path>>argThat(header ->\n                header.equals(Payload.WORKSPACE_DIR));\n        when(payload.getHeader(headerMatcher)).thenReturn(workspace);\n\n        var processor = new TemplateScriptProcessor(logManager, cfg);\n        processor.process(chain, payload);\n\n        verify(payload, times(1)).getHeader(Payload.WORKSPACE_DIR);\n    }\n\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/process/queue/FilterUtilsTest.java",
    "content": "package com.walmartlabs.concord.server.process.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport javax.ws.rs.core.MultivaluedHashMap;\nimport javax.ws.rs.core.UriInfo;\nimport java.util.Collections;\nimport java.util.List;\n\nimport static com.walmartlabs.concord.server.process.queue.ProcessFilter.FilterType.REGEXP_MATCH;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\npublic class FilterUtilsTest {\n\n    @Test\n    public void testRegularExpressionMatch() {\n        MultivaluedHashMap<String, String> m = new MultivaluedHashMap<>();\n        m.put(\"test.regexp\", Collections.singletonList(\"myValue\"));\n\n        UriInfo uriInfo = mock(UriInfo.class);\n        when(uriInfo.getQueryParameters()).thenReturn(m);\n\n        List<ProcessFilter.JsonFilter> filter =  FilterUtils.parseJson(\"test\", uriInfo);\n        assertEquals(1, filter.size());\n        assertEquals(REGEXP_MATCH, filter.get(0).type());\n        assertEquals(\"myValue\", filter.get(0).value());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/process/queue/ProcessKeyCacheTest.java",
    "content": "package com.walmartlabs.concord.server.process.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.AbstractDaoTest;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.TestObjectMapper;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.UUID;\n\nimport static org.junit.jupiter.api.Assertions.assertNull;\n\n@Disabled(\"requires a local DB instance\")\npublic class ProcessKeyCacheTest extends AbstractDaoTest {\n\n    @Test\n    public void testNotFound() {\n        ProcessQueueDao dao = new ProcessQueueDao(getConfiguration(), new ConcordObjectMapper(TestObjectMapper.INSTANCE));\n        ProcessKeyCache keyCache = new ProcessKeyCache(dao);\n\n        ProcessKey key = keyCache.get(UUID.randomUUID());\n        assertNull(key);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/process/queue/dispatcher/DispatcherTest.java",
    "content": "package com.walmartlabs.concord.server.process.queue.dispatcher;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.MetricRegistry;\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.process.loader.ImportsNormalizer;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.Locks;\nimport com.walmartlabs.concord.server.cfg.ProcessQueueConfiguration;\nimport com.walmartlabs.concord.server.message.MessageChannel;\nimport com.walmartlabs.concord.server.message.MessageChannelManager;\nimport com.walmartlabs.concord.server.process.ImportsNormalizerFactory;\nimport com.walmartlabs.concord.server.process.SessionTokenCreator;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueEntry;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueManager;\nimport com.walmartlabs.concord.server.queueclient.message.Message;\nimport com.walmartlabs.concord.server.queueclient.message.MessageType;\nimport com.walmartlabs.concord.server.queueclient.message.ProcessRequest;\nimport com.walmartlabs.concord.server.queueclient.message.ProcessResponse;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\nimport java.time.Duration;\nimport java.time.OffsetDateTime;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.UUID;\n\nimport static java.time.temporal.ChronoUnit.MICROS;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.ArgumentMatchers.any;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.times;\nimport static org.mockito.Mockito.verify;\nimport static org.mockito.Mockito.when;\n\n@ExtendWith(MockitoExtension.class)\nclass DispatcherTest {\n\n    private static List<Dispatcher.Request> requests;\n\n    @Mock\n    private Dispatcher.RequirementsMatcherErrorHandler errHandler;\n\n    @Mock\n    private DSLContext tx;\n\n    @Mock\n    private Locks locks;\n\n    @Mock\n    private ProcessQueueManager queueManager;\n\n    @Mock\n    private ProcessLogManager logManager;\n\n    @Mock\n    private ImportsNormalizerFactory importsNormalizerFactory;\n\n    @Mock\n    private SessionTokenCreator sessionTokenCreator;\n\n    @Mock\n    private ProcessQueueConfiguration cfg;\n\n    @BeforeAll\n    static void setup() {\n        requests = List.of(new Dispatcher.Request(null, processRequest(0)));\n    }\n\n    @Test\n    void testInvalidInvalidRegex() {\n        var candidate = generateCandidate(\"*invalidRegex*\");\n        assertNull(Dispatcher.findRequest(candidate, requests, tx, errHandler));\n        verify(errHandler, times(1)).handleError(any(), any(), any());\n    }\n\n    @Test\n    void testValid() {\n        var candidate = generateCandidate(\".*default.*\");\n        assertNotNull(Dispatcher.findRequest(candidate, requests, tx, errHandler));\n        verify(errHandler, times(0)).handleError(any(), any(), any());\n    }\n\n    @Test\n    void testResponseIncludesRequirementsInProcessResponse() {\n        UUID projectId = UUID.randomUUID();\n        OffsetDateTime createdAt = OffsetDateTime.now().truncatedTo(MICROS);\n        Map<String, Object> requirements = Map.of(\n                \"agent\", Map.of(\n                        \"flavor\", List.of(\".*default.*\")\n                ),\n                \"custom\", Map.of(\n                        \"foo\", \"bar\"\n                ));\n        Imports imports = Imports.builder().build();\n        ProcessQueueEntry candidate = ProcessQueueEntry.builder()\n                .key(new ProcessKey(UUID.randomUUID(), createdAt))\n                .projectId(projectId)\n                .repoUrl(\"repo-url\")\n                .repoPath(\"repo-path\")\n                .commitId(\"commit-id\")\n                .commitBranch(\"repo-branch\")\n                .imports(imports)\n                .requirements(requirements)\n                .build();\n\n        TestDispatcherDao dao = new TestDispatcherDao(tx, candidate);\n        MessageChannelManager channelManager = new MessageChannelManager();\n        TestMessageChannel channel = new TestMessageChannel(\"channel-1\", \"agent-1\", processRequest(321));\n        channelManager.add(channel);\n\n        ImportsNormalizer normalizer = value -> value;\n        when(importsNormalizerFactory.forProject(projectId)).thenReturn(normalizer);\n        when(sessionTokenCreator.create(candidate.key())).thenReturn(\"session-token\");\n        when(cfg.getDispatcherPollDelay()).thenReturn(Duration.ofSeconds(1));\n        when(cfg.getDispatcherBatchSize()).thenReturn(10);\n\n        Dispatcher dispatcher = new Dispatcher(locks, dao, channelManager, logManager, queueManager, Collections.emptySet(),\n                importsNormalizerFactory, cfg, new MetricRegistry(), sessionTokenCreator);\n\n        assertTrue(dispatcher.performTask());\n\n        verify(queueManager, times(1)).updateAgentId(eq(tx), eq(candidate.key()), eq(\"agent-1\"), eq(ProcessStatus.STARTING));\n\n        Message response = channel.response();\n        ProcessResponse processResponse = assertInstanceOf(ProcessResponse.class, response);\n        assertEquals(requirements, processResponse.getRequirements());\n        assertEquals(\"session-token\", processResponse.getSessionToken());\n        assertEquals(imports, processResponse.getImports());\n    }\n\n    private static ProcessQueueEntry generateCandidate(String flavor) {\n        return ProcessQueueEntry.builder()\n                .requirements(Map.of(\n                        \"agent\", Map.of(\n                                \"flavor\", List.of(flavor)\n                        )\n                ))\n                .key(ProcessKey.random())\n                .build();\n    }\n\n    private static ProcessRequest processRequest(long correlationId) {\n        ProcessRequest request = new ProcessRequest(Map.of(\"flavor\", \"default\"));\n        request.setCorrelationId(correlationId);\n        return request;\n    }\n\n    private static class TestDispatcherDao extends Dispatcher.DispatcherDao {\n\n        private final DSLContext tx;\n        private final List<ProcessQueueEntry> candidates;\n        private boolean fetched;\n\n        private TestDispatcherDao(DSLContext tx, ProcessQueueEntry candidate) {\n            super(mock(Configuration.class), mock(ConcordObjectMapper.class), new MetricRegistry());\n            this.tx = tx;\n            this.candidates = List.of(candidate);\n        }\n\n        @Override\n        protected <T> T txResult(TxResult<T> t) {\n            try {\n                return t.run(tx);\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n        }\n\n        @Override\n        public List<ProcessQueueEntry> next(DSLContext tx, int offset, int limit) {\n            if (fetched) {\n                return List.of();\n            }\n\n            fetched = true;\n            return candidates;\n        }\n    }\n\n    private static class TestMessageChannel implements MessageChannel {\n\n        private final String channelId;\n        private final String agentId;\n        private ProcessRequest request;\n        private Message response;\n\n        private TestMessageChannel(String channelId, String agentId, ProcessRequest request) {\n            this.channelId = channelId;\n            this.agentId = agentId;\n            this.request = request;\n        }\n\n        @Override\n        public String getChannelId() {\n            return channelId;\n        }\n\n        @Override\n        public String getAgentId() {\n            return agentId;\n        }\n\n        @Override\n        public boolean offerMessage(Message msg) {\n            this.response = msg;\n            return true;\n        }\n\n        @Override\n        public Optional<Message> getMessage(MessageType messageType) {\n            if (request != null && request.getMessageType() == messageType) {\n                ProcessRequest current = request;\n                request = null;\n                return Optional.of(current);\n            }\n\n            return Optional.empty();\n        }\n\n        @Override\n        public void close() {\n        }\n\n        private Message response() {\n            return response;\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/process/state/ProcessStateManagerTest.java",
    "content": "package com.walmartlabs.concord.server.process.state;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.google.common.base.Charsets;\nimport com.walmartlabs.concord.common.DateTimeUtils;\nimport com.walmartlabs.concord.sdk.Constants;\nimport com.walmartlabs.concord.server.AbstractDaoTest;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.cfg.ProcessConfiguration;\nimport com.walmartlabs.concord.server.cfg.SecretStoreConfiguration;\nimport com.walmartlabs.concord.server.policy.PolicyManager;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.process.queue.ProcessKeyCache;\nimport com.walmartlabs.concord.server.process.queue.ProcessQueueDao;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.time.Duration;\nimport java.time.temporal.ChronoUnit;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.server.process.state.ProcessStateManager.copyTo;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.mock;\n\n@Disabled(\"requires a local DB instance\")\npublic class ProcessStateManagerTest extends AbstractDaoTest {\n\n    @Test\n    public void testUpdateState() throws Exception {\n        ProcessKey processKey = ProcessKey.random();\n\n        Path baseDir = Files.createTempDirectory(\"testImport\");\n\n        writeTempFile(baseDir.resolve(\"file-1\"), \"123\".getBytes());\n        writeTempFile(baseDir.resolve(\"file-2\"), \"456\".getBytes());\n\n        //\n        ProcessKeyCache processKeyCache = new ProcessKeyCache(new ProcessQueueDao(getConfiguration(), new ConcordObjectMapper(new ObjectMapper())));\n        ProcessConfiguration stateCfg = new ProcessConfiguration(Duration.of(24, ChronoUnit.HOURS), Collections.singletonList(Constants.Files.CONFIGURATION_FILE_NAME));\n        ProcessStateManager stateManager = new ProcessStateManager(getConfiguration(), mock(SecretStoreConfiguration.class), stateCfg, mock(PolicyManager.class), mock(ProcessLogManager.class), processKeyCache);\n        stateManager.importPath(processKey, null, baseDir, (p, attrs) -> true);\n\n        Path tmpDir = Files.createTempDirectory(\"testExport\");\n\n        boolean result = stateManager.export(processKey, copyTo(tmpDir));\n        assertTrue(result);\n        assertFileContent(\"123\", tmpDir.resolve(\"file-1\"));\n        assertFileContent(\"456\", tmpDir.resolve(\"file-2\"));\n\n        // --- update\n\n        writeTempFile(baseDir.resolve(\"file-1\"), \"123-up\".getBytes());\n\n        stateManager.importPath(processKey, null, baseDir, (p, attrs) -> true);\n\n        result = stateManager.export(processKey, copyTo(tmpDir));\n        assertTrue(result);\n        assertFileContent(\"123-up\", tmpDir.resolve(\"file-1\"));\n        assertFileContent(\"456\", tmpDir.resolve(\"file-2\"));\n    }\n\n    @Test\n    public void testLargeImport() throws Exception {\n        ProcessKey processKey = ProcessKey.random();\n\n        int files = 100;\n        int chunkSize = 1024 * 1024;\n        int fileSize = 10 * chunkSize;\n\n        byte[] ab = new byte[chunkSize];\n        Arrays.fill(ab, (byte) 0);\n\n        Path baseDir = Files.createTempDirectory(\"test\");\n        for (int i = 0; i < files; i++) {\n            Path p = baseDir.resolve(\"file\" + i);\n            try (OutputStream out = Files.newOutputStream(p, StandardOpenOption.CREATE)) {\n                for (int j = 0; j < fileSize; j += chunkSize) {\n                    out.write(ab);\n                }\n            }\n        }\n\n        ProcessKeyCache processKeyCache = new ProcessKeyCache(new ProcessQueueDao(getConfiguration(), new ConcordObjectMapper(new ObjectMapper())));\n        ProcessConfiguration stateCfg = new ProcessConfiguration(Duration.of(24, ChronoUnit.HOURS), Collections.singletonList(Constants.Files.CONFIGURATION_FILE_NAME));\n        ProcessStateManager stateManager = new ProcessStateManager(getConfiguration(), mock(SecretStoreConfiguration.class), stateCfg, mock(PolicyManager.class), mock(ProcessLogManager.class), processKeyCache);\n        stateManager.importPath(processKey, \"/\", baseDir, (p, attrs) -> true);\n    }\n\n    @Test\n    @Disabled(\"Used to reproduce the timestamp truncation issue when PG rounds up nanos while JOOQ truncates\")\n    public void testCreatedAtNanoTruncate() throws Exception {\n        String createdAt = \"2023-03-26T20:00:37.000000500Z\";\n        ProcessKey processKey = new ProcessKey(UUID.randomUUID(), DateTimeUtils.fromIsoString(createdAt));\n\n        Path baseDir = Files.createTempDirectory(\"testImport\");\n\n        writeTempFile(baseDir.resolve(\"file-1\"), \"123\".getBytes());\n        writeTempFile(baseDir.resolve(\"file-2\"), \"456\".getBytes());\n\n        SecretStoreConfiguration secretCfg = null;\n        ProcessConfiguration stateCfg = new ProcessConfiguration(null, Collections.emptyList());\n        ProcessKeyCache processKeyCache = null;\n\n        ProcessStateManager stateManager = new ProcessStateManager(getConfiguration(), secretCfg, stateCfg, null, mock(ProcessLogManager.class), processKeyCache);\n\n        // ---\n        stateManager.tx(tx -> {\n            stateManager.insert(tx, processKey, \"_initial/payload.json\", \"fake-payload\".getBytes());\n            stateManager.importPath(tx, processKey, \"_initial/attachments/\", baseDir, (path, basicFileAttributes) -> true);\n        });\n\n        stateManager.replace(processKey, \".concord/current_user\", \"principals\".getBytes());\n\n        // ---\n        List<String> exported = new ArrayList<>();\n        boolean result = stateManager.export(processKey, (name, unixMode, src) -> exported.add(name));\n        assertTrue(result);\n        // should be 4, but 2 because of createdAt rounded in postgresql JDBC and just truncated in jooq.\n        /**\n         *  see {@link org.postgresql.jdbc.TimestampUtils#toString(OffsetDateTime)}\n         *  and\n         *  {@link org.jooq.impl.DefaultBinding.format(OffsetDateTime val, SQLDialect family)\n          */\n        assertEquals(4, exported.size());\n    }\n\n    private static void assertFileContent(String expected, Path f) throws IOException {\n        String str = com.google.common.io.Files.asCharSource(f.toFile(), Charsets.UTF_8).read();\n        assertEquals(expected, str);\n    }\n\n    private static void writeTempFile(Path p, byte[] ab) throws IOException {\n        try (OutputStream out = Files.newOutputStream(p, StandardOpenOption.CREATE)) {\n            out.write(ab);\n        }\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/process/waits/WaitConditionSerializeTest.java",
    "content": "package com.walmartlabs.concord.server.process.waits;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.TestObjectMapper;\nimport com.walmartlabs.concord.server.jooq.enums.ProcessLockScope;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.UUID;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class WaitConditionSerializeTest {\n\n    private final ConcordObjectMapper objectMapper = new ConcordObjectMapper(TestObjectMapper.INSTANCE);\n\n    @Test\n    public void testProcessLockCondition() throws Exception {\n        ProcessLockCondition c = ProcessLockCondition.builder()\n                .instanceId(UUID.fromString(\"e7c43983-95b5-4426-acef-6682b8c1dabe\"))\n                .orgId(UUID.fromString(\"64468dec-6279-49b6-b2ee-0540db5953e9\"))\n                .projectId(UUID.fromString(\"8e9d1b26-eb23-465b-9862-e64037e4d2e9\"))\n                .scope(ProcessLockScope.PROJECT)\n                .name(\"test\")\n                .build();\n\n        String json = objectMapper.toString(c);\n        System.out.println(json);\n\n        AbstractWaitCondition cc = objectMapper.fromString(json, AbstractWaitCondition.class);\n        assertEquals(c, cc);\n    }\n\n    @Test\n    public void testProcessCompletionCondition() throws Exception {\n        ProcessCompletionCondition c = ProcessCompletionCondition.builder()\n                .processes(Collections.singletonList(UUID.fromString(\"0c443946-0f2c-4685-a9f0-2bc0b735b7ae\")))\n                .reason(\"test-reason\")\n                .exclusive(true)\n                .build();\n\n        String json = objectMapper.toString(c);\n        System.out.println(json);\n\n        AbstractWaitCondition cc = objectMapper.fromString(json, AbstractWaitCondition.class);\n        assertEquals(c, cc);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/process/waits/WaitProcessFinishHandlerTest.java",
    "content": "package com.walmartlabs.concord.server.process.waits;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.MetricRegistry;\nimport com.walmartlabs.concord.server.cfg.ProcessWaitWatchdogConfiguration;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessStatus;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.stubbing.Answer;\n\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.process.waits.ProcessWaitHandler.WaitConditionItem;\nimport static org.junit.jupiter.api.Assertions.*;\nimport static org.mockito.ArgumentMatchers.anySet;\nimport static com.walmartlabs.concord.server.process.waits.ProcessWaitHandler.*;\nimport static org.mockito.Mockito.*;\n\npublic class WaitProcessFinishHandlerTest {\n\n    private WaitProcessFinishHandler handler;\n    private WaitProcessFinishHandler.Dao dao;\n\n    @BeforeEach\n    public void setUp() {\n        var cfg = mock(ProcessWaitWatchdogConfiguration.class);\n        when(cfg.getProcessLimitForStatusQuery()).thenReturn(2);\n\n        dao = mock(WaitProcessFinishHandler.Dao.class);\n        handler = new WaitProcessFinishHandler(dao, cfg, null, new MetricRegistry());\n    }\n\n    @Test\n    public void testEmptyInput() {\n        List<WaitConditionItem<ProcessCompletionCondition>> items = new ArrayList<>();\n        List<Result<ProcessCompletionCondition>> results = handler.processBatch(items);\n        assertTrue(results.isEmpty());\n    }\n\n    @Test\n    public void testAllProcessesFinished() {\n        // ---\n        UUID p1 = UUID.randomUUID();\n        UUID p2 = UUID.randomUUID();\n        UUID p3 = UUID.randomUUID();\n\n        List<WaitConditionItem<ProcessCompletionCondition>> items = new ArrayList<>();\n        items.add(WaitConditionItem.of(ProcessKey.random(), 0, ProcessCompletionCondition.builder().resumeEvent(\"test-resume\").addProcesses(p1, p2).build()));\n        items.add(WaitConditionItem.of(ProcessKey.random(), 1, ProcessCompletionCondition.builder().addProcesses(p1, p2, p3).build()));\n\n        when(dao.findStatuses(anySet())).thenAnswer(\n                (Answer<Map<UUID, ProcessStatus>>) invocation -> {\n                    Set<UUID> processes = invocation.getArgument(0);\n                    return processes.stream()\n                            .collect(Collectors.toMap(\n                                    p -> p,\n                                    p -> ProcessStatus.FINISHED\n                            ));\n                }\n        );\n\n        List<Result<ProcessCompletionCondition>> results = handler.processBatch(items);\n\n        assertEquals(items.size(), results.size(), \"All items should be processed\");\n        assertNull(results.get(0).waitCondition());\n        assertNull(results.get(1).waitCondition());\n\n        verify(dao, times(2)).findStatuses(anySet());\n    }\n\n    @Test\n    public void testNotALLProcessesFinished() {\n        // ---\n        UUID p1 = UUID.randomUUID();\n        UUID p2 = UUID.randomUUID();\n        UUID p3 = UUID.randomUUID();\n\n        List<WaitConditionItem<ProcessCompletionCondition>> items = new ArrayList<>();\n        items.add(WaitConditionItem.of(ProcessKey.random(), 0, ProcessCompletionCondition.builder().resumeEvent(\"test-resume\").addProcesses(p1, p2).build()));\n        items.add(WaitConditionItem.of(ProcessKey.random(), 1, ProcessCompletionCondition.builder().addProcesses(p1, p2, p3).resumeEvent(\"my-event\").exclusive(true).addFinalStatuses(ProcessStatus.FINISHED).build()));\n\n        when(dao.findStatuses(anySet())).thenAnswer(\n                (Answer<Map<UUID, ProcessStatus>>) invocation -> {\n                    Set<UUID> processes = invocation.getArgument(0);\n                    return processes.stream()\n                            .collect(Collectors.toMap(\n                                    p -> p,\n                                    p -> p == p3 ? ProcessStatus.RUNNING : ProcessStatus.FINISHED\n                            ));\n                }\n        );\n\n        List<Result<ProcessCompletionCondition>> results = handler.processBatch(items);\n\n        assertEquals(items.size(), results.size(), \"All items should be processed\");\n\n        assertNull(results.get(0).waitCondition());\n\n        assertNotNull(results.get(1).waitCondition());\n        assertEquals(Set.of(p3), results.get(1).waitCondition().processes());\n        assertEquals(\"my-event\", results.get(1).waitCondition().resumeEvent());\n        assertTrue(results.get(1).waitCondition().exclusive());\n        assertEquals(Set.of(ProcessStatus.FINISHED), results.get(1).waitCondition().finalStatuses());\n\n        verify(dao, times(2)).findStatuses(anySet());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/repository/ServerAuthTokenProviderTest.java",
    "content": "package com.walmartlabs.concord.server.repository;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.MetricRegistry;\nimport com.walmartlabs.concord.common.AuthTokenProvider;\nimport com.walmartlabs.concord.common.ExternalAuthToken;\nimport com.walmartlabs.concord.github.appinstallation.GitHubAppInstallation;\nimport org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.extension.ExtendWith;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\n\nimport java.net.URI;\nimport java.time.OffsetDateTime;\nimport java.util.Optional;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertInstanceOf;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\nimport static org.mockito.Mockito.any;\nimport static org.mockito.Mockito.when;\n\n@ExtendWith(MockitoExtension.class)\nclass ServerAuthTokenProviderTest {\n\n    @Mock\n    private MetricRegistry metricRegistry;\n\n    @Mock\n    GitHubAppInstallation ghApp;\n\n    @Mock\n    AuthTokenProvider.OauthTokenProvider oauthTokenProvider;\n\n    @Test\n    void testGitHubApp() {\n        when(ghApp.getToken(any(), any())).\n                thenReturn(Optional.of(ExternalAuthToken.SimpleToken.builder()\n                        .token(\"gh-installation-token\")\n                        .expiresAt(OffsetDateTime.now().plusMinutes(60))\n                        .build()));\n        when(ghApp.supports(any(), any())).thenReturn(true);\n\n        var provider = new ServerAuthTokenProvider(ghApp, oauthTokenProvider, metricRegistry);\n\n        // --\n\n        assertTrue(provider.supports(URI.create(\"https://github.local/owner/repo.git\"), null));\n        var o = provider.getToken(URI.create(\"https://github.local/owner/repo.git\"), null);\n\n        // --\n\n        assertTrue(o.isPresent());\n        var result = assertInstanceOf(ExternalAuthToken.class, o.get());\n        assertEquals(\"gh-installation-token\", result.token());\n    }\n\n    @Test\n    void testOauth() {\n        when(oauthTokenProvider.supports(any(), any())).thenReturn(true);\n        when(oauthTokenProvider.getToken(any(), any()))\n                .thenReturn(Optional.of(ExternalAuthToken.StaticToken.builder()\n                        .token(\"oauth-token\")\n                        .build()));\n\n        var provider = new ServerAuthTokenProvider(ghApp, oauthTokenProvider, metricRegistry);\n\n        assertTrue(provider.supports(URI.create(\"https://github.local/owner/repo.git\"), null));\n        var o = provider.getToken(URI.create(\"https://github.local/owner/repo.git\"), null);\n\n        assertTrue(o.isPresent());\n        var result = assertInstanceOf(ExternalAuthToken.class, o.get());\n        assertEquals(\"oauth-token\", result.token());\n    }\n\n    @Test\n    void testNoAuth() {\n        when(ghApp.supports(any(), any())).thenReturn(false);\n        when(oauthTokenProvider.supports(any(), any())).thenReturn(false);\n\n        var provider = new ServerAuthTokenProvider(ghApp, oauthTokenProvider, metricRegistry);\n\n        assertFalse(provider.supports(URI.create(\"https://github.local/owner/repo.git\"), null));\n        var o = provider.getToken(URI.create(\"https://github.local/owner/repo.git\"), null);\n\n        assertFalse(o.isPresent());\n    }\n\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/security/secret/SecretDaoTest.java",
    "content": "package com.walmartlabs.concord.server.security.secret;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.secret.HashAlgorithm;\nimport com.walmartlabs.concord.common.secret.SecretEncryptedByType;\nimport com.walmartlabs.concord.common.secret.SecretUtils;\nimport com.walmartlabs.concord.server.AbstractDaoTest;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.TestObjectMapper;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.org.project.RepositoryDao;\nimport com.walmartlabs.concord.server.org.project.RepositoryEntry;\nimport com.walmartlabs.concord.server.org.secret.SecretDao;\nimport com.walmartlabs.concord.server.org.secret.SecretType;\nimport com.walmartlabs.concord.server.org.secret.SecretVisibility;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.*;\n\nimport static com.walmartlabs.concord.server.org.secret.SecretDao.InsertMode.INSERT;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\nimport static org.junit.jupiter.api.Assertions.assertNull;\n\n@Disabled(\"requires a local DB instance\")\npublic class SecretDaoTest extends AbstractDaoTest {\n\n    @Test\n    public void testOnCascade() {\n        UUID orgId = OrganizationManager.DEFAULT_ORG_ID;\n\n        String projectName = \"project#\" + System.currentTimeMillis();\n\n        ProjectDao projectDao = new ProjectDao(getConfiguration(), new ConcordObjectMapper(TestObjectMapper.INSTANCE), getUuidGenerator());\n        UUID projectId = projectDao.insert(orgId, projectName, \"test\", null, null, null, null, new byte[0], null, null, null);\n\n        String secretName = \"secret#\" + System.currentTimeMillis();\n        SecretDao secretDao = new SecretDao(getConfiguration(), getUuidGenerator());\n        byte[] secretSalt = SecretUtils.generateSalt(16);\n        UUID secretId = secretDao.insert(orgId, secretName, null, SecretType.KEY_PAIR, SecretEncryptedByType.SERVER_KEY, \"concord\", SecretVisibility.PUBLIC, secretSalt, HashAlgorithm.SHA256, INSERT);\n        secretDao.updateData(secretId, new byte[]{0, 1, 2});\n        secretDao.update(secretId, secretName, UUID.fromString(\"4b9d496a-c3a0-4e1b-804c-ac3fccddcb27\"), null, new byte[0], null, orgId, HashAlgorithm.SHA256);\n\n\n        String repoName = \"repo#\" + System.currentTimeMillis();\n        RepositoryDao repositoryDao = new RepositoryDao(getConfiguration(), new ConcordObjectMapper(TestObjectMapper.INSTANCE), getUuidGenerator());\n        UUID repoId = repositoryDao.insert(projectId, repoName, \"n/a\", null, null, null, secretId, false, null, false);\n\n        // ---\n\n        secretDao.delete(secretId);\n\n        // ---\n\n        RepositoryEntry r = repositoryDao.get(projectId, repoId);\n        assertNotNull(r);\n        assertNull(r.getSecretName());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/tasks/SchedulerDaoTest.java",
    "content": "package com.walmartlabs.concord.server.tasks;\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.AbstractDaoTest;\nimport com.walmartlabs.concord.server.metrics.FailedTaskError;\nimport com.walmartlabs.concord.server.task.SchedulerDao;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\n@Disabled(\"requires a local DB instance\")\npublic class SchedulerDaoTest extends AbstractDaoTest {\n\n    private SchedulerDao schedulerDao;\n\n    @BeforeEach\n    public void setUp() {\n        schedulerDao = new SchedulerDao(getConfiguration());\n    }\n\n    @Test\n    public void testPollErrored() {\n        List<FailedTaskError> failedTaskErrors = schedulerDao.pollErrored();\n        assertEquals(0, failedTaskErrors.size());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/template/ConfigurationUtilsTest.java",
    "content": "package com.walmartlabs.concord.server.template;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.common.ConfigurationUtils;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class ConfigurationUtilsTest {\n\n    @Test\n    @SuppressWarnings(\"unchecked\")\n    public void test() throws Exception {\n        String json = \"{ \\\"smtp\\\": { \\\"host\\\": \\\"localhost\\\" }, \\\"ssl\\\": false }\";\n\n        ObjectMapper om = new ObjectMapper();\n        Map<String, Object> m = om.readValue(json, Map.class);\n\n        assertEquals(\"localhost\", ConfigurationUtils.get(m, \"smtp\", \"host\"));\n        assertEquals(false, ConfigurationUtils.get(m, \"ssl\"));\n\n        // ---\n\n        Map<String, Object> n = new HashMap<>();\n        n.put(\"host\", \"127.0.0.1\");\n\n        ConfigurationUtils.merge(m, n, \"smtp\");\n\n        assertEquals(\"127.0.0.1\", ConfigurationUtils.get(m, \"smtp\", \"host\"));\n\n        // ---\n\n        String newJson = \"{ \\\"smtp\\\": { \\\"host\\\": \\\"10.11.12.13\\\" } }\";\n        Map<String, Object> nn = om.readValue(newJson, Map.class);\n\n        ConfigurationUtils.merge(m, nn);\n\n        assertEquals(\"10.11.12.13\", ConfigurationUtils.get(m, \"smtp\", \"host\"));\n        assertEquals(false, ConfigurationUtils.get(m, \"ssl\"));\n    }\n\n    @Test\n    @SuppressWarnings(\"unchecked\")\n    public void testDeepMerge() throws Exception {\n        ObjectMapper om = new ObjectMapper();\n\n        String cfgJson = \"{ \\\"arguments\\\": { \\\"smtpParams\\\": { \\\"host\\\": \\\"localhost\\\" } } }\";\n        Map<String, Object> cfg = om.readValue(cfgJson, Map.class);\n\n        String requestJson = \"{ \\\"arguments\\\": { \\\"mail\\\": \\\"hello\\\" } }\";\n        Map<String, Object> request = om.readValue(requestJson, Map.class);\n\n        // ---\n\n        Map<String, Object> m = ConfigurationUtils.deepMerge(cfg, request);\n        assertEquals(\"localhost\", ConfigurationUtils.get(m, \"arguments\", \"smtpParams\", \"host\"));\n        assertEquals(\"hello\", ConfigurationUtils.get(m, \"arguments\", \"mail\"));\n    }\n\n    @Test\n    public void testDeepMergeEmptyLeftSide() {\n        Map<String, Object> a = Collections.emptyMap();\n\n        Map<String, Object> b = new HashMap<>();\n        b.put(\"test\", 123);\n\n        a = ConfigurationUtils.deepMerge(a, b);\n        assertEquals(123, a.get(\"test\"));\n    }\n\n    @Test\n    public void testMerge() {\n        Map<String, Object> a = new HashMap<>();\n        a.put(\"x\", 123);\n\n        Map<String, Object> b = new HashMap<>();\n        b.put(\"y\", 234);\n\n        ConfigurationUtils.merge(a, b);\n        assertEquals(234, a.get(\"y\"));\n    }\n\n    @Test\n    @SuppressWarnings(\"unchecked\")\n    public void testToNested() {\n        String k = \"a.b.c\";\n        Object v = 123;\n\n        Map<String, Object> m = ConfigurationUtils.toNested(k, v);\n        assertEquals(1, m.size());\n\n        Map<String, Object> m1 = (Map<String, Object>) m.get(\"a\");\n        assertNotNull(m1);\n\n        Map<String, Object> m2 = (Map<String, Object>) m1.get(\"b\");\n        assertNotNull(m2);\n\n        Object vv = m2.get(\"c\");\n        assertEquals(v, vv);\n    }\n\n    @Test\n    public void testToNestedMinimal() {\n        String k = \"a\";\n        Object v = 123;\n\n        Map<String, Object> m = ConfigurationUtils.toNested(k, v);\n        assertEquals(1, m.size());\n\n        Object vv = m.get(\"a\");\n        assertEquals(v, vv);\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/template/ProjectDaoTest.java",
    "content": "package com.walmartlabs.concord.server.template;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.collect.ImmutableMap;\nimport com.walmartlabs.concord.server.AbstractDaoTest;\nimport com.walmartlabs.concord.server.ConcordObjectMapper;\nimport com.walmartlabs.concord.server.TestObjectMapper;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.org.project.ProjectEntry;\nimport com.walmartlabs.concord.server.org.project.ProjectVisibility;\nimport com.walmartlabs.concord.server.org.project.RepositoryDao;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.tables.Projects.PROJECTS;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\n\n@Disabled(\"requires a local DB instance\")\npublic class ProjectDaoTest extends AbstractDaoTest {\n\n    private ProjectDao projectDao;\n    private RepositoryDao repositoryDao;\n\n    @BeforeEach\n    public void setUp() {\n        repositoryDao = new RepositoryDao(getConfiguration(), new ConcordObjectMapper(TestObjectMapper.INSTANCE), getUuidGenerator());\n        projectDao = new ProjectDao(getConfiguration(), new ConcordObjectMapper(TestObjectMapper.INSTANCE), getUuidGenerator());\n    }\n\n    @Test\n    public void testInsertDelete() {\n        UUID orgId = OrganizationManager.DEFAULT_ORG_ID;\n\n        Map<String, Object> cfg = ImmutableMap.of(\"a\", \"a-v\");\n        String projectName = \"project#\" + System.currentTimeMillis();\n        UUID projectId = projectDao.insert(orgId, projectName, \"test\", null, cfg, null, null, new byte[0], null, null, null);\n\n        // ---\n        Map<String, Object> actualCfg = projectDao.getConfiguration(projectId);\n        assertEquals(cfg, actualCfg);\n\n        // ---\n\n        String repoName = \"repo#\" + System.currentTimeMillis();\n        String repoUrl = \"n/a\";\n        repositoryDao.insert(projectId, repoName, repoUrl, null, null, null, null, false, null, false);\n\n        // ---\n        Map<String, Object> newCfg1 = ImmutableMap.of(\"a1\", \"a1-v\");\n        tx(tx -> projectDao.updateCfg(tx, projectId, newCfg1));\n\n        actualCfg = projectDao.getConfiguration(projectId);\n        assertEquals(newCfg1, actualCfg);\n\n        // ---\n        Map<String, Object> newCfg2 = ImmutableMap.of(\"a2\", \"a2-v\");\n        tx(tx -> projectDao.update(tx, orgId, projectId, ProjectVisibility.PRIVATE, projectName, \"new-description\", newCfg2, null, null, null, null, null));\n\n        actualCfg = projectDao.getConfiguration(projectId);\n        assertEquals(newCfg2, actualCfg);\n\n        // ---\n        String v = (String) projectDao.getConfigurationValue(projectId, \"a2\");\n        assertEquals(\"a2-v\", v);\n\n        // ---\n        projectDao.delete(projectId);\n\n        // ---\n\n        assertNull(projectDao.getId(orgId, projectName));\n        assertNull(repositoryDao.getId(projectId, repoName));\n    }\n\n    @Test\n    public void testList() {\n        UUID orgId = OrganizationManager.DEFAULT_ORG_ID;\n\n        assertEquals(0, projectDao.list(null, null, PROJECTS.PROJECT_NAME, true, 0, -1, null).size());\n\n        // ---\n\n        String aName = \"aProject#\" + System.currentTimeMillis();\n        String bName = \"bProject#\" + System.currentTimeMillis();\n        String cName = \"cProject#\" + System.currentTimeMillis();\n\n        projectDao.insert(orgId, aName, \"test\", null, null, null, null, new byte[0], null, null, null);\n        projectDao.insert(orgId, bName, \"test\", null, null, null, null, new byte[0], null, null, null);\n        projectDao.insert(orgId, cName, \"test\", null, null, null, null, new byte[0], null, null, null);\n\n        // ---\n\n        List<ProjectEntry> l = projectDao.list(null, null, PROJECTS.PROJECT_NAME, false, 0, -1, null);\n        assertEquals(3, l.size());\n\n        ProjectEntry a = l.get(2);\n        assertEquals(aName, a.getName());\n\n        ProjectEntry b = l.get(1);\n        assertEquals(bName, b.getName());\n\n        ProjectEntry c = l.get(0);\n        assertEquals(cName, c.getName());\n\n        // ---\n\n        List<ProjectEntry> l2 = projectDao.list(null, null, PROJECTS.PROJECT_NAME, false, 1, -1, null);\n        assertEquals(2, l2.size());\n\n        ProjectEntry a2 = l2.get(1);\n        assertEquals(aName, a2.getName());\n\n        ProjectEntry b2 = l2.get(0);\n        assertEquals(bName, b2.getName());\n\n        // ---\n\n        List<ProjectEntry> l3 = projectDao.list(null, null, PROJECTS.PROJECT_NAME, false, 0, -1, \"cProject\");\n        assertEquals(1, l3.size());\n\n        ProjectEntry c3 = l3.get(0);\n        assertEquals(cName, c3.getName());\n\n        // ---\n\n        List<ProjectEntry> l4 = projectDao.list(null, null, PROJECTS.PROJECT_NAME, false, 0, 1, null);\n        assertEquals(1, l4.size());\n\n        ProjectEntry c4 = l4.get(0);\n        assertEquals(cName, c4.getName());\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/template/kv/KvDaoTest.java",
    "content": "package com.walmartlabs.concord.server.template.kv;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.AbstractDaoTest;\nimport com.walmartlabs.concord.server.Locks;\nimport com.walmartlabs.concord.server.cfg.LockingConfiguration;\nimport com.walmartlabs.concord.server.org.project.KvDao;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.Duration;\nimport java.util.UUID;\nimport java.util.concurrent.atomic.AtomicLong;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertTimeout;\n\n@Disabled(\"requires a local DB instance\")\npublic class KvDaoTest extends AbstractDaoTest {\n\n    @Test\n    public void test() throws Exception {\n        assertTimeout(Duration.ofMillis(10000), () -> {\n            KvDao kvDao = new KvDao(getConfiguration(), new Locks(new LockingConfiguration(8)));\n\n            UUID projectId = UUID.randomUUID();\n            String key = \"key_\" + System.currentTimeMillis();\n\n            int threads = 3;\n            int iterations = 50;\n\n            AtomicLong counter = new AtomicLong(0);\n\n            Runnable r = () -> {\n                for (int i = 0; i < iterations; i++) {\n                    kvDao.inc(projectId, key);\n                    counter.incrementAndGet();\n                }\n            };\n\n            Thread[] workes = new Thread[threads];\n            for (int i = 0; i < threads; i++) {\n                workes[i] = new Thread(r);\n                workes[i].start();\n            }\n\n            for (Thread w : workes) {\n                w.join();\n            }\n\n            Long total = counter.get();\n            assertEquals(total, kvDao.getLong(projectId, key));\n        });\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/java/com/walmartlabs/concord/server/user/UserDaoTest.java",
    "content": "package com.walmartlabs.concord.server.user;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.AbstractDaoTest;\nimport com.walmartlabs.concord.server.SecureRandomProvider;\nimport com.walmartlabs.concord.server.security.apikey.ApiKeyDao;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Disabled;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.OffsetDateTime;\nimport java.util.Base64;\nimport java.util.List;\nimport java.util.UUID;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNull;\n\n@Disabled(\"requires a local DB instance\")\npublic class UserDaoTest extends AbstractDaoTest {\n\n    private UserDao userDao;\n    private ApiKeyDao apiKeyDao;\n\n    @BeforeEach\n    public void setUp() {\n        userDao = new UserDao(getConfiguration(), getUuidGenerator());\n        apiKeyDao = new ApiKeyDao(getConfiguration(), new SecureRandomProvider().get(), getUuidGenerator());\n    }\n\n    @Test\n    public void testInsertListDelete() {\n        String username = \"user#\" + System.currentTimeMillis();\n\n        UUID userId = userDao.insertOrUpdate(username, null, null, null, UserType.LOCAL, null);\n\n        String s = \"key#\" + System.currentTimeMillis();\n        String name = \"name#\" + System.currentTimeMillis();\n        String apiKey = Base64.getEncoder().encodeToString(s.getBytes());\n        apiKeyDao.insert(userId, apiKey, name, OffsetDateTime.now());\n\n        // ---\n\n        List<UserEntry> result = userDao.list(username, 0, 1);\n        assertEquals(1, result.size());\n        assertEquals(userId, result.get(0).getId());\n\n        // ---\n\n        userDao.delete(userId);\n\n        // ---\n\n        assertNull(userDao.get(userId));\n        assertNull(apiKeyDao.find(apiKey));\n    }\n}\n"
  },
  {
    "path": "server/impl/src/test/resources/com/walmartlabs/concord/server/instance/example.yml",
    "content": "main:\n- expr: ${log.info(\"test\", \"Hello!\")}"
  },
  {
    "path": "server/impl/src/test/resources/com/walmartlabs/concord/server/org/jsonstore/queries.txt",
    "content": "select\n  cast(json_build_object(\n    'host', a.item_data->'host',\n    'zone', a.item_data->'zone',\n    'profile', a.item_data->'profile',\n    'apiServer', a.item_data->'apiServer',\n    'ingress', a.item_data->'ingress',\n    'k8sVersion', a.item_data->'k8sVersion'\n  ) as varchar)\nfrom inventory_data a\nwhere\n  item_data @> ?::jsonb\n\n\nselect\n  cast(json_build_object(\n    'host', a.item_path,\n    'name', a.item_data->'name',\n    'clusterTokenRef', a.item_data->'clusterTokenRef',\n    'manifest', a.item_data->'manifest'\n  ) as varchar)\nfrom inventory_data a\nwhere\n  item_data @> ?::jsonb\n\n\nselect\n  cast(json_build_object(\n    'host', a.item_path,\n    'workload_config', a.item_data->'workload_config',\n    'manifest', a.item_data->'manifest'\n  ) as varchar)\nfrom inventory_data a\nwhere\n  item_data->'workload_config' @> ?::jsonb\n\n\nselect\n  cast(json_build_object(\n    'hostname', a.item_data->'hostname',\n    'address', a.item_data->'address',\n    'kind', a.item_data->'kind',\n    'profile', a.item_data->'profile',\n    'zone', a.item_data->'zone',\n    'clusterInventoryRef', a.item_data->'clusterInventoryRef'\n  ) as varchar)\nfrom inventory_data a\nwhere\n  item_data @> ?::jsonb\n\n\nselect\n  cast(json_build_object(\n    'host', a.item_data->'host',\n    'ansible_host', a.item_data->'ip',\n    'ooInstanceName', a.item_data->'ooInstanceName',\n    'type', a.item_data->'type',\n    'profile', a.item_data->'profile',\n    'zone', a.item_data->'zone',\n    'clusterInventoryRef', a.item_data->'clusterInventoryRef'\n  ) as varchar)\nfrom inventory_data a\nwhere\n  item_data @> ?::jsonb\n\n\nselect cast(a.item_data as varchar) from\n  inventory_data a\nwhere\n  item_data @> ?::jsonb\n\n\nselect cast(a.item_data as varchar) from\n  inventory_data a\nwhere\n  item_data @> ?::jsonb\n\n\nselect cast(a.item_data as varchar) from\n  inventory_data a\nwhere\n  item_data @> ?::jsonb\n\n\nselect cast(?::jsonb as varchar)\n\n\nselect cast(json_build_object('host', a.item_data->'gls'->'k8s', 'env', 'dev') as varchar) from inventory_data a where item_data @> ?::jsonb\n\n\nselect a.item_data->>'server_name' as server_name  from inventory_data a;\n\n\nselect cast( json_build_object('server_name', a.item_data->>'server_name') as varchar) from inventory_data a\n"
  },
  {
    "path": "server/impl/src/test/resources/com/walmartlabs/concord/server/process/attachmentTest/dir1/test1.txt",
    "content": "1"
  },
  {
    "path": "server/impl/src/test/resources/com/walmartlabs/concord/server/process/attachmentTest/dir2/test2.txt",
    "content": "2"
  },
  {
    "path": "server/impl/src/test/resources/com/walmartlabs/concord/server/process/attachmentTest/test0.txt",
    "content": "0"
  },
  {
    "path": "server/liquibase-ext/README.md",
    "content": "# Liquibase Extensions\n\nThis module contains Concord-specific [Liquibase](https://www.liquibase.org/) extensions. "
  },
  {
    "path": "server/liquibase-ext/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.server</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>liquibase-ext</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.liquibase</groupId>\n            <artifactId>liquibase-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n        </dependency>\n    </dependencies>\n</project>\n"
  },
  {
    "path": "server/liquibase-ext/src/main/java/com/walmartlabs/concord/server/liquibase/ext/ApiTokenCreator.java",
    "content": "package com.walmartlabs.concord.server.liquibase.ext;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport liquibase.change.custom.CustomSqlChange;\nimport liquibase.change.custom.CustomSqlRollback;\nimport liquibase.database.Database;\nimport liquibase.exception.ValidationErrors;\nimport liquibase.resource.ResourceAccessor;\nimport liquibase.statement.SqlStatement;\nimport liquibase.statement.core.DeleteStatement;\nimport liquibase.statement.core.InsertStatement;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.SecureRandom;\nimport java.util.Base64;\n\n/**\n * Generates a new API token for a given user.\n */\npublic class ApiTokenCreator implements CustomSqlChange, CustomSqlRollback {\n\n    private static final Logger log = LoggerFactory.getLogger(ApiTokenCreator.class);\n\n    private static final String DEFAULT_KEY_NAME = \"autogenerated\";\n\n    private String token;\n    private String userId;\n    private String username;\n    private String skip;\n    private String keyName;\n\n    public void setUserId(String userId) {\n        this.userId = userId;\n    }\n\n    public void setToken(String token) {\n        this.token = token;\n    }\n\n    public void setUsername(String username) {\n        this.username = username;\n    }\n\n    public void setSkip(String skip) {\n        this.skip = skip;\n    }\n\n    public void setKeyName(String keyName) {\n        this.keyName = keyName;\n    }\n\n    @Override\n    public SqlStatement[] generateStatements(Database database) {\n        if (this.token == null) {\n            log.info(\"API token generation is disabled for userId={}, username={}, skipping...\", userId, username);\n            return new SqlStatement[0];\n        }\n\n        return new SqlStatement[]{\n                new InsertStatement(null, null, \"API_KEYS\")\n                        .addColumnValue(\"API_KEY\", hash(token))\n                        .addColumnValue(\"USER_ID\", userId)\n                        .addColumnValue(\"KEY_NAME\", getKeyName())\n        };\n    }\n\n    @Override\n    public SqlStatement[] generateRollbackStatements(Database database) {\n        if (this.token == null) {\n            return new SqlStatement[0];\n        }\n\n        if (userId != null) {\n            return new SqlStatement[]{\n                    new DeleteStatement(null, null, \"API_KEYS\")\n                            .setWhere(\"USER_ID=? and KEY_NAME=?\")\n                            .addWhereParameter(userId)\n                            .addWhereParameter(getKeyName())\n            };\n        } else {\n            return new SqlStatement[]{\n                    new DeleteStatement(null, null, \"API_KEYS\")\n                            .setWhere(\"KEY_NAME=?\")\n                            .addWhereParameter(getKeyName())\n            };\n        }\n    }\n\n    @Override\n    public String getConfirmationMessage() {\n        if (this.token == null) {\n            return null;\n        }\n\n        System.out.println(\">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\");\n        System.out.println(\">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\");\n        System.out.println(\">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\");\n        System.out.println();\n        if (username == null) {\n            System.out.println(\"API token created (without a user): \" + token);\n        } else {\n            System.out.println(\"API token created for user '\" + username + \"': \" + token);\n        }\n        System.out.println();\n        System.out.println(\"(don't forget to remove it in production)\");\n        System.out.println();\n        System.out.println(\">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\");\n        System.out.println(\">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\");\n        System.out.println(\">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\");\n\n        return null;\n    }\n\n    @Override\n    public void setUp() {\n        if (this.skip.equals(\"true\")) {\n            this.token = null;\n            return;\n        }\n\n        if (this.token == null || this.token.trim().isEmpty()) {\n            this.token = newApiKey();\n        }\n    }\n\n    @Override\n    public void setFileOpener(ResourceAccessor resourceAccessor) {\n    }\n\n    @Override\n    public ValidationErrors validate(Database database) {\n        return null;\n    }\n\n    private String getKeyName() {\n        if (this.keyName != null) {\n            return this.keyName;\n        }\n        return DEFAULT_KEY_NAME;\n    }\n\n    private static String newApiKey() {\n        try {\n            byte[] ab = new byte[16];\n            SecureRandom.getInstance(\"NativePRNGNonBlocking\").nextBytes(ab);\n\n            Base64.Encoder e = Base64.getEncoder().withoutPadding();\n            return e.encodeToString(ab);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static String hash(String s) {\n        MessageDigest md;\n        try {\n            md = MessageDigest.getInstance(\"SHA-256\");\n        } catch (NoSuchAlgorithmException e) {\n            throw new RuntimeException(e);\n        }\n\n        byte[] ab = Base64.getDecoder().decode(s);\n        ab = md.digest(ab);\n\n        return Base64.getEncoder().withoutPadding().encodeToString(ab);\n    }\n}\n"
  },
  {
    "path": "server/liquibase-ext/src/main/java/com/walmartlabs/concord/server/liquibase/ext/NoopLockService.java",
    "content": "package com.walmartlabs.concord.server.liquibase.ext;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport liquibase.database.Database;\nimport liquibase.exception.DatabaseException;\nimport liquibase.lockservice.DatabaseChangeLogLock;\nimport liquibase.lockservice.LockService;\n\npublic class NoopLockService implements LockService {\n\n    @Override\n    public boolean supports(Database database) {\n        return true;\n    }\n\n    @Override\n    public void init() throws DatabaseException {\n    }\n\n    @Override\n    public void setDatabase(Database database) {\n    }\n\n    @Override\n    public void setChangeLogLockWaitTime(long changeLogLockWaitTime) {\n    }\n\n    @Override\n    public void setChangeLogLockRecheckTime(long changeLogLockRecheckTime) {\n    }\n\n    @Override\n    public boolean hasChangeLogLock() {\n        return false;\n    }\n\n    @Override\n    public void waitForLock() {\n    }\n\n    @Override\n    public boolean acquireLock() {\n        return false;\n    }\n\n    @Override\n    public void releaseLock() {\n    }\n\n    @Override\n    public DatabaseChangeLogLock[] listLocks() {\n        return new DatabaseChangeLogLock[0];\n    }\n\n    @Override\n    public void forceReleaseLock() {\n    }\n\n    @Override\n    public void reset() {\n    }\n\n    @Override\n    public int getPriority() {\n        return PRIORITY_DATABASE;\n    }\n\n    @Override\n    public void destroy() throws DatabaseException {\n\n    }\n}\n"
  },
  {
    "path": "server/liquibase-ext/src/main/java/com/walmartlabs/concord/server/liquibase/ext/migration/SecretsHashMigrationTask.java",
    "content": "package com.walmartlabs.concord.server.liquibase.ext.migration;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2022 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.secret.HashAlgorithm;\nimport com.walmartlabs.concord.common.secret.SecretEncryptedByType;\nimport com.walmartlabs.concord.common.secret.SecretUtils;\nimport liquibase.change.custom.CustomTaskChange;\nimport liquibase.database.Database;\nimport liquibase.database.jvm.JdbcConnection;\nimport liquibase.exception.CustomChangeException;\nimport liquibase.exception.SetupException;\nimport liquibase.exception.ValidationErrors;\nimport liquibase.resource.ResourceAccessor;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\nimport java.util.Base64;\n\npublic class SecretsHashMigrationTask implements CustomTaskChange {\n\n    private static final Logger log = LoggerFactory.getLogger(SecretsHashMigrationTask.class);\n\n    private static final int BATCH_SIZE = 100;\n\n    private String serverPassword;\n\n    public String getServerPassword() {\n        return serverPassword;\n    }\n\n    public void setServerPassword(String serverPassword) {\n        this.serverPassword = serverPassword;\n    }\n\n    @Override\n    public void execute(Database database) throws CustomChangeException {\n        log.info(\"Starting migration task for secretsHash\");\n        try {\n            JdbcConnection con = (JdbcConnection) database.getConnection();\n            byte[] serverPwd = Base64.getDecoder().decode(serverPassword);\n            con.setAutoCommit(false);\n            while (true) {\n                try (PreparedStatement psSelect = con.prepareStatement(\"SELECT secret_id, secret_salt, secret_data FROM secrets WHERE hash_algorithm = ? AND encrypted_by = ? LIMIT ? FOR UPDATE\")) {\n                    psSelect.setString(1, HashAlgorithm.LEGACY_MD5.getName());\n                    psSelect.setString(2, SecretEncryptedByType.SERVER_KEY.toString());\n                    psSelect.setInt(3, BATCH_SIZE);\n                    int count = 0;\n                    try (ResultSet resultSet = psSelect.executeQuery();\n                         PreparedStatement psUpdated = con.prepareStatement(\"UPDATE secrets SET secret_data = ?, hash_algorithm = ? WHERE secret_id = ?::uuid\")) {\n                        while (resultSet.next()) {\n                            String secretId = resultSet.getString(1);\n                            byte[] secretSalt = resultSet.getBytes(2);\n                            byte[] encryptedData = resultSet.getBytes(3);\n                            byte[] decryptedData = SecretUtils.decrypt(encryptedData, serverPwd, secretSalt, HashAlgorithm.LEGACY_MD5);\n                            byte[] newlyEncryptedData = SecretUtils.encrypt(decryptedData, serverPwd, secretSalt, HashAlgorithm.SHA256);\n                            psUpdated.setBytes(1, newlyEncryptedData);\n                            psUpdated.setString(2, HashAlgorithm.SHA256.getName());\n                            psUpdated.setObject(3, secretId);\n                            psUpdated.addBatch();\n                            count++;\n                        }\n                        if (count > 0) {\n                            psUpdated.executeBatch();\n                            con.commit();\n                        }\n                        log.info(\"Committing {} records with new hash algorithm\", count);\n                        if (count < BATCH_SIZE) {\n                            break;\n                        }\n                    }\n                }\n            }\n        } catch (Exception e) {\n            log.error(\"Exception in executing secretsHashMigrationTask, message : {}\", e.getMessage());\n            throw new CustomChangeException(e);\n        }\n        log.info(\"Successfully completed migrating password less secrets with SHA256 hash algorithm\");\n    }\n\n    @Override\n    public String getConfirmationMessage() {\n        return null;\n    }\n\n    @Override\n    public void setUp() throws SetupException {\n    }\n\n    @Override\n    public void setFileOpener(ResourceAccessor resourceAccessor) {\n    }\n\n    @Override\n    public ValidationErrors validate(Database database) {\n        return null;\n    }\n}\n"
  },
  {
    "path": "server/plugins/README.md",
    "content": "# Server-side Plugins\n\nTo use those plugins they must be included into the Server's classpath. See the [dist](../dist) module. "
  },
  {
    "path": "server/plugins/ansible/client2/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.server.plugins.ansible</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-ansible-plugin-client2</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-client2</artifactId>\n        </dependency>\n\n        <!-- JDK9+ compatibility -->\n        <dependency>\n            <groupId>javax.annotation</groupId>\n            <artifactId>javax.annotation-api</artifactId>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.openapitools</groupId>\n                <artifactId>openapi-generator-maven-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>generate</goal>\n                        </goals>\n                        <configuration>\n                            <inputSpec>${project.basedir}/../impl/target/classes/com/walmartlabs/concord/server/plugins/ansible/swagger/swagger.yaml</inputSpec>\n                            <generatorName>java</generatorName>\n                            <apiPackage>com.walmartlabs.concord.client2</apiPackage>\n                            <modelPackage>com.walmartlabs.concord.client2</modelPackage>\n                            <packageName>com.walmartlabs.concord.client2</packageName>\n                            <invokerPackage>com.walmartlabs.concord.client2</invokerPackage>\n                            <configOptions>\n                                <sourceFolder>src/gen/java/main</sourceFolder>\n                                <dateLibrary>java8</dateLibrary>\n                                <serializableModel>true</serializableModel>\n                                <openApiNullable>false</openApiNullable>\n                                <supportUrlQuery>false</supportUrlQuery>\n                            </configOptions>\n                            <skipValidateSpec>false</skipValidateSpec>\n                            <library>native</library>\n                            <generateApiTests>false</generateApiTests>\n                            <generateModelTests>false</generateModelTests>\n                            <generateApiDocumentation>false</generateApiDocumentation>\n                            <generateModelDocumentation>false</generateModelDocumentation>\n                            <generateSupportingFiles>true</generateSupportingFiles>\n                            <supportingFilesToGenerate>ApiClient.java,ApiResponse.java,ApiException.java,Pair.java</supportingFilesToGenerate>\n                            <templateDirectory>../../../../client2/src/main/template</templateDirectory>\n                            <cleanupOutput>true</cleanupOutput>\n                            <typeMappings>string+binary=InputStream</typeMappings>\n                            <importMappings>InputStream=java.io.InputStream</importMappings>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "server/plugins/ansible/db/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.server.plugins.ansible</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-ansible-plugin-db</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <properties>\n        <db.baseDir>${project.build.directory}/db</db.baseDir>\n        <db.changeLogPath>com/walmartlabs/concord/server/plugins/ansible/db/liquibase.xml</db.changeLogPath>\n        <db.host>localhost</db.host>\n        <db.username>postgres</db.username>\n        <db.password>q1</db.password>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server-db</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.postgresql</groupId>\n            <artifactId>postgresql</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.inject</groupId>\n            <artifactId>guice</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.zaxxer</groupId>\n            <artifactId>HikariCP</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.sisu</groupId>\n            <artifactId>org.eclipse.sisu.inject</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.jooq</groupId>\n            <artifactId>jooq</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.liquibase</groupId>\n            <artifactId>liquibase-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.dropwizard.metrics</groupId>\n            <artifactId>metrics-core</artifactId>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>build-helper-maven-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>reserve-ports</id>\n                        <phase>generate-sources</phase>\n                        <goals>\n                            <goal>reserve-network-port</goal>\n                        </goals>\n                        <configuration>\n                            <portNames>\n                                <portName>db.port</portName>\n                            </portNames>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>io.fabric8</groupId>\n                <artifactId>docker-maven-plugin</artifactId>\n                <extensions>true</extensions>\n                <executions>\n                    <execution>\n                        <id>start</id>\n                        <phase>generate-sources</phase>\n                        <goals>\n                            <goal>start</goal>\n                        </goals>\n                    </execution>\n                    <execution>\n                        <id>stop</id>\n                        <phase>prepare-package</phase>\n                        <goals>\n                            <goal>stop</goal>\n                        </goals>\n                    </execution>\n                </executions>\n                <configuration>\n                    <images>\n                        <image>\n                            <name>${db.image}</name>\n                            <alias>db</alias>\n                            <run>\n                                <ports>\n                                    <port>${db.port}:5432</port>\n                                </ports>\n                                <env>\n                                    <POSTGRES_PASSWORD>${db.password}</POSTGRES_PASSWORD>\n                                </env>\n                                <wait>\n                                    <log>(?s).*ready for start up.*ready to accept connections.*</log>\n                                    <time>60000</time>\n                                </wait>\n                            </run>\n                        </image>\n                    </images>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.liquibase</groupId>\n                <artifactId>liquibase-maven-plugin</artifactId>\n                <version>${liquibase.version}</version>\n                <executions>\n                    <execution>\n                        <phase>generate-sources</phase>\n                        <goals>\n                            <goal>update</goal>\n                        </goals>\n                    </execution>\n                </executions>\n                <configuration>\n                    <changeLogFile>src/main/resources/${db.changeLogPath}</changeLogFile>\n                    <driver>org.postgresql.Driver</driver>\n                    <url>jdbc:postgresql://${db.host}:${db.port}/postgres</url>\n                    <username>${db.username}</username>\n                    <password>${db.password}</password>\n                    <promptOnNonLocalDatabase>false</promptOnNonLocalDatabase>\n                    <expressionVariables>\n                        <superuserAvailable>true</superuserAvailable>\n                        <createExtensionAvailable>true</createExtensionAvailable>\n                    </expressionVariables>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.jooq</groupId>\n                <artifactId>jooq-codegen-maven</artifactId>\n                <executions>\n                    <execution>\n                        <phase>generate-sources</phase>\n                        <goals>\n                            <goal>generate</goal>\n                        </goals>\n                    </execution>\n                </executions>\n                <configuration>\n                    <logging>WARN</logging>\n                    <jdbc>\n                        <driver>org.postgresql.Driver</driver>\n                        <url>jdbc:postgresql://${db.host}:${db.port}/postgres</url>\n                        <user>${db.username}</user>\n                        <password>${db.password}</password>\n                    </jdbc>\n                    <generator>\n                        <database>\n                            <name>org.jooq.meta.postgres.PostgresDatabase</name>\n                            <inputSchema>public</inputSchema>\n                            <includes>.*</includes>\n                            <excludes>DATABASECHANGELOG.*</excludes>\n                        </database>\n                        <target>\n                            <packageName>com.walmartlabs.concord.server.plugins.ansible.jooq</packageName>\n                            <directory>target/generated-sources/jooq</directory>\n                        </target>\n                        <generate>\n                            <deprecationOnUnknownTypes>false</deprecationOnUnknownTypes>\n                        </generate>\n                    </generator>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>cobertura-maven-plugin</artifactId>\n                <configuration>\n                    <skip>true</skip>\n                    <check />\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n    <profiles>\n        <profile>\n            <id>looper</id>\n            <activation>\n                <activeByDefault>false</activeByDefault>\n            </activation>\n            <properties>\n                <db.host>${env.DB_CONTAINER_NAME}</db.host>\n                <db.port>5432</db.port>\n            </properties>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.codehaus.mojo</groupId>\n                        <artifactId>build-helper-maven-plugin</artifactId>\n                        <executions>\n                            <execution>\n                                <id>reserve-ports</id>\n                                <phase>none</phase>\n                            </execution>\n                        </executions>\n                    </plugin>\n                    <plugin>\n                        <groupId>io.fabric8</groupId>\n                        <artifactId>docker-maven-plugin</artifactId>\n                        <extensions>true</extensions>\n                        <configuration>\n                            <skip>true</skip>\n                        </configuration>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n    </profiles>\n</project>\n"
  },
  {
    "path": "server/plugins/ansible/db/src/main/java/com/walmartlabs/concord/server/plugins/ansible/db/AnsibleDBChangeLogProvider.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible.db;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.DatabaseChangeLogProvider;\nimport com.walmartlabs.concord.db.MainDB;\n\n@MainDB\npublic class AnsibleDBChangeLogProvider implements DatabaseChangeLogProvider {\n\n    @Override\n    public String getChangeLogPath() {\n        return \"com/walmartlabs/concord/server/plugins/ansible/db/liquibase.xml\";\n    }\n\n    @Override\n    public String toString() {\n        return \"ansible-db\";\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/db/src/main/java/com/walmartlabs/concord/server/plugins/ansible/db/DatabaseModule.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible.db;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.walmartlabs.concord.db.DatabaseChangeLogProvider;\n\nimport javax.inject.Named;\n\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\n\n@Named\npublic class DatabaseModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        newSetBinder(binder, DatabaseChangeLogProvider.class).addBinding().to(AnsibleDBChangeLogProvider.class);\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/db/src/main/resources/com/walmartlabs/concord/server/plugins/ansible/db/liquibase.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"ansible-10000\" author=\"ybrigo@gmail.com\">\n        <validCheckSum>7:a6629219e3ee8ccedc56a698d1dc226f</validCheckSum>\n\n        <!-- migration from pre-1.18.0 -->\n        <preConditions onFail=\"MARK_RAN\">\n            <not>\n                <and>\n                    <changeSetExecuted id=\"99100\" author=\"ybrigo@gmail.com\" changeLogFile=\"com/walmartlabs/concord/server/db/v0.99.0.xml\"/>\n                    <changeSetExecuted id=\"1110000\" author=\"ybrigo@gmail.com\" changeLogFile=\"com/walmartlabs/concord/server/db/v1.11.0.xml\"/>\n                </and>\n            </not>\n        </preConditions>\n        <createTable tableName=\"ANSIBLE_HOSTS\">\n            <column name=\"INSTANCE_ID\" type=\"uuid\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"INSTANCE_CREATED_AT\" type=\"timestamp\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"HOST\" type=\"varchar(1024)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"HOST_GROUP\" type=\"varchar(1024)\">\n                <constraints primaryKey=\"true\" nullable=\"false\"/>\n            </column>\n            <column name=\"EVENT_SEQ\" type=\"bigint\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"STATUS\" type=\"varchar(32)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"DURATION\" type=\"bigint\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <!-- removed in 1.36.1+\n            <column name=\"RETRY_COUNT\" type=\"int\" defaultValue=\"0\">\n                <constraints nullable=\"false\"/>\n            </column>\n            -->\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"ansible-10001\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"ANSIBLE_PLAYBOOK_STATS\">\n            <column name=\"INSTANCE_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"INSTANCE_CREATED_AT\" type=\"timestamp\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"PLAYBOOK_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"NAME\" type=\"varchar(1024)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"STARTED_AT\" type=\"timestamp\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"HOST_COUNT\" type=\"int\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"PLAY_COUNT\" type=\"int\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"TOTAL_WORK\" type=\"int\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"ansible-10002\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"ANSIBLE_PLAYBOOK_RESULT\">\n            <column name=\"INSTANCE_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"INSTANCE_CREATED_AT\" type=\"timestamp\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"PLAYBOOK_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"STATUS\" type=\"varchar(64)\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"ansible-10003\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"ANSIBLE_PLAY_STATS\">\n            <column name=\"INSTANCE_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"INSTANCE_CREATED_AT\" type=\"timestamp\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"PLAYBOOK_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"PLAY_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"PLAY_NAME\" type=\"varchar(1024)\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"PLAY_ORDER\" type=\"int\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"HOST_COUNT\" type=\"bigint\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"TASK_COUNT\" type=\"int\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"FINISHED_TASK_COUNT\" type=\"bigint\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"ansible-10004\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"ANSIBLE_TASK_STATS\">\n            <column name=\"INSTANCE_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"INSTANCE_CREATED_AT\" type=\"timestamp\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"PLAYBOOK_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"PLAY_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"TASK_ID\" type=\"uuid\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"TASK_NAME\" type=\"varchar(1024)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"TASK_ORDER\" type=\"bigint\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"TASK_TYPE\" type=\"varchar(64)\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"OK_COUNT\" type=\"bigint\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"FAILED_COUNT\" type=\"bigint\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"UNREACHABLE_COUNT\" type=\"bigint\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"SKIPPED_COUNT\" type=\"bigint\">\n                <constraints nullable=\"false\"/>\n            </column>\n            <column name=\"RUNNING_COUNT\" type=\"bigint\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"ansible-10005\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"ANSIBLE_HOSTS\">\n            <column name=\"PLAYBOOK_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <changeSet id=\"ansible-10006\" author=\"ybrigo@gmail.com\">\n        <createIndex tableName=\"ANSIBLE_PLAYBOOK_STATS\" indexName=\"IDX_A_PLAYBOOK_STATS\">\n            <column name=\"INSTANCE_ID\"/>\n            <column name=\"INSTANCE_CREATED_AT\"/>\n        </createIndex>\n\n        <createIndex tableName=\"ANSIBLE_PLAYBOOK_RESULT\" indexName=\"IDX_A_PLAYBOOK_RESULT\">\n            <column name=\"INSTANCE_ID\"/>\n            <column name=\"INSTANCE_CREATED_AT\"/>\n            <column name=\"PLAYBOOK_ID\"/>\n        </createIndex>\n\n        <createIndex tableName=\"ANSIBLE_PLAY_STATS\" indexName=\"IDX_A_PLAY_STATS\">\n            <column name=\"INSTANCE_ID\"/>\n            <column name=\"INSTANCE_CREATED_AT\"/>\n            <column name=\"PLAYBOOK_ID\"/>\n        </createIndex>\n\n        <createIndex tableName=\"ANSIBLE_TASK_STATS\" indexName=\"IDX_A_TASK_STATS\">\n            <column name=\"INSTANCE_ID\"/>\n            <column name=\"INSTANCE_CREATED_AT\"/>\n            <column name=\"PLAY_ID\"/>\n        </createIndex>\n    </changeSet>\n\n    <changeSet id=\"ansible-10007\" author=\"ybrigo@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <sqlCheck expectedResult=\"0\">\n                select count(*)\n                from pg_inherits\n                join pg_class parent ON pg_inherits.inhparent = parent.oid\n                join pg_class child ON pg_inherits.inhrelid = child.oid\n                join pg_namespace nmsp_parent ON nmsp_parent.oid = parent.relnamespace\n                join pg_namespace nmsp_child ON nmsp_child.oid = child.relnamespace\n                where parent.relname = 'ansible_hosts'\n            </sqlCheck>\n        </preConditions>\n\n        <dropPrimaryKey tableName=\"ANSIBLE_HOSTS\"/>\n        <addPrimaryKey tableName=\"ANSIBLE_HOSTS\" columnNames=\"INSTANCE_ID, INSTANCE_CREATED_AT, HOST, HOST_GROUP, PLAYBOOK_ID\"/>\n    </changeSet>\n\n    <changeSet id=\"ansible-10008\" author=\"ybrigo@gmail.com\">\n        <addColumn tableName=\"ANSIBLE_PLAYBOOK_STATS\">\n            <column name=\"RETRY_NUM\" type=\"int\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </addColumn>\n    </changeSet>\n\n    <!-- see server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.58.0.xml -->\n    <changeSet id=\"ansible-1580000\" author=\"ibodrov@gmail.com\">\n        <createProcedure dbms=\"postgresql\">\n            create or replace function ts_to_tstz(t text)\n                returns bool as $$\n            declare\n                v_cnt numeric;\n            begin\n                v_cnt := 0;\n\n                update pg_attribute\n                    set atttypid = 'timestamp with time zone'::regtype\n                from pg_class\n                where attrelid = pg_class.oid\n                    and relnamespace = current_schema()::regnamespace\n                    and atttypid = 'timestamp'::regtype\n                    and relname ilike t;\n\n                get diagnostics v_cnt = row_count;\n                if v_cnt = 0 then\n                    raise warning 'Relation not found (or is already converted): %', t;\n                end if;\n\n                update pg_index\n                    set indclass = array_to_string(array_replace(indclass::oid[], 3128::oid, 3127::oid), ' ')::oidvector\n                from pg_class\n                where indrelid = pg_class.oid\n                    and relnamespace = current_schema()::regnamespace\n                    and indclass::oid[] @> ARRAY[3128::oid]\n                    and relname ilike t;\n\n                return v_cnt > 0;\n            end;\n            $$ language plpgsql\n        </createProcedure>\n    </changeSet>\n\n    <!-- tables that might be partitioned -->\n\n    <!-- ANSIBLE_HOSTS -->\n    <changeSet id=\"ansible-1580100\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('ansible_hosts%')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"ansible-1580100-a\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <sql>\n            alter table ANSIBLE_HOSTS\n                alter column INSTANCE_CREATED_AT type timestamptz using INSTANCE_CREATED_AT at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- ANSIBLE_PLAY_STATS -->\n    <changeSet id=\"ansible-1580110\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('ansible_play_stats%')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"ansible-1580110-a\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <sql>\n            alter table ANSIBLE_PLAY_STATS\n                alter column INSTANCE_CREATED_AT type timestamptz using INSTANCE_CREATED_AT at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- ANSIBLE_PLAYBOOK_RESULT -->\n    <changeSet id=\"ansible-1580120\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('ansible_playbook_result%')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"ansible-1580120-a\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <sql>\n            alter table ANSIBLE_PLAYBOOK_RESULT\n                alter column INSTANCE_CREATED_AT type timestamptz using INSTANCE_CREATED_AT at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- ANSIBLE_PLAYBOOK_STATS -->\n    <changeSet id=\"ansible-1580130\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('ansible_playbook_stats%')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"ansible-1580130-a\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <sql>\n            alter table ANSIBLE_PLAYBOOK_STATS\n                alter column INSTANCE_CREATED_AT type timestamptz using INSTANCE_CREATED_AT at time zone 'UTC',\n                alter column STARTED_AT type timestamptz using STARTED_AT at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- ANSIBLE_TASK_STATS -->\n    <changeSet id=\"ansible-1580140\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('ansible_task_stats%')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"ansible-1580140-a\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <sql>\n            alter table ANSIBLE_TASK_STATS\n                alter column INSTANCE_CREATED_AT type timestamptz using INSTANCE_CREATED_AT at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- indices -->\n\n    <changeSet id=\"ansible-1580200\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('idx_a_playbook_stats')\n        </sql>\n        <sql>\n            select ts_to_tstz('idx_a_playbook_result')\n        </sql>\n        <sql>\n            select ts_to_tstz('idx_a_play_stats')\n        </sql>\n        <sql>\n            select ts_to_tstz('idx_a_task_stats')\n        </sql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/plugins/ansible/impl/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.server.plugins.ansible</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-ansible-plugin</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server.plugins.ansible</groupId>\n            <artifactId>concord-ansible-plugin-db</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server-sdk</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server-db</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.typesafe</groupId>\n            <artifactId>config</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-config</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.jboss.spec.javax.ws.rs</groupId>\n            <artifactId>jboss-jaxrs-api_2.1_spec</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.jooq</groupId>\n            <artifactId>jooq</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.inject</groupId>\n            <artifactId>guice</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.swagger.core.v3</groupId>\n            <artifactId>swagger-annotations</artifactId>\n            <version>2.2.15</version>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-core</artifactId>\n        </dependency>\n\n        <!-- Immutables -->\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-guava</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jdk8</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.errorprone</groupId>\n            <artifactId>error_prone_annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server-impl</artifactId>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n\n            <plugin>\n                <groupId>io.swagger.core.v3</groupId>\n                <artifactId>swagger-maven-plugin</artifactId>\n                <configuration>\n                    <outputFileName>swagger</outputFileName>\n                    <outputPath>${project.build.directory}/classes/com/walmartlabs/concord/server/plugins/ansible/swagger</outputPath>\n                    <outputFormat>YAML</outputFormat>\n                    <readAllResources>false</readAllResources>\n                    <resourcePackages>\n                        <package>com.walmartlabs.concord.server.plugins.ansible</package>\n                    </resourcePackages>\n                    <configurationFilePath>${project.build.directory}/classes/openapi-ansible-config.yaml</configurationFilePath>\n                </configuration>\n                <executions>\n                    <execution>\n                        <phase>compile</phase>\n                        <goals>\n                            <goal>resolve</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/AbstractAnsibleEvent.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.MapUtils;\n\nimport java.time.OffsetDateTime;\nimport java.util.UUID;\n\npublic abstract class AbstractAnsibleEvent {\n\n    protected final EventProcessor.Event e;\n\n    protected AbstractAnsibleEvent(EventProcessor.Event e) {\n        this.e = e;\n    }\n\n    public UUID instanceId() {\n        return e.instanceId();\n    }\n\n    public OffsetDateTime instanceCreatedAt() {\n        return e.instanceCreatedAt();\n    }\n\n    public long eventSeq() {\n        return e.eventSeq();\n    }\n\n    public UUID playbookId() {\n        UUID result = MapUtils.getUUID(e.payload(), \"playbookId\");\n        if (result != null) {\n            return result;\n        }\n        result = MapUtils.getUUID(e.payload(), \"parentCorrelationId\");\n        if (result != null) {\n            return result;\n        }\n        return e.instanceId();\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/AbstractEventProcessor.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.ScheduledTask;\nimport org.jooq.DSLContext;\n\nimport java.util.List;\n\n// TODO: move to plugins sdk?\npublic abstract class AbstractEventProcessor<E extends AbstractEventProcessor.Event> implements ScheduledTask {\n\n    private final String processorName;\n    private final EventMarkerDao eventMarkerDao;\n    private final int fetchLimit;\n\n    protected AbstractEventProcessor(String processorName, EventMarkerDao eventMarkerDao, int fetchLimit) {\n        this.processorName = processorName;\n        this.eventMarkerDao = eventMarkerDao;\n        this.fetchLimit = fetchLimit;\n    }\n\n    @Override\n    public void performTask() {\n        int processedEvents = 0;\n\n        do {\n            EventMarkerDao.EventMarker m = eventMarkerDao.get(processorName);\n            processedEvents = process(m, fetchLimit);\n        } while (processedEvents >= fetchLimit);\n    }\n\n    private int process(EventMarkerDao.EventMarker m, int fetchLimit) {\n        return eventMarkerDao.txResult(tx -> {\n            List<E> events = processEvents(tx, m, fetchLimit);\n            if (events.isEmpty()) {\n                eventMarkerDao.update(tx, processorName, m.maxEventSeq());\n                return 0;\n            }\n\n            E lastEvent = events.get(events.size() - 1);\n            eventMarkerDao.update(tx, processorName, lastEvent.eventSeq());\n\n            return events.size();\n        });\n    }\n\n    protected abstract List<E> processEvents(DSLContext tx, EventMarkerDao.EventMarker m, int fetchLimit);\n\n    public interface Event {\n        long eventSeq();\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/AnsibleEvent.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.MapUtils;\n\nimport java.util.UUID;\n\npublic class AnsibleEvent extends AbstractAnsibleEvent {\n\n    public static AnsibleEvent from(EventProcessor.Event e) {\n        if (!e.eventType().equals(Constants.ANSIBLE_EVENT_TYPE)) {\n            return null;\n        }\n\n        return new AnsibleEvent(e);\n    }\n\n    private AnsibleEvent(EventProcessor.Event e) {\n        super(e);\n    }\n\n    public String host() {\n        return MapUtils.assertString(e.payload(), \"host\");\n    }\n\n    public String hostGroup() {\n        return MapUtils.getString(e.payload(), \"hostGroup\", \"-\");\n    }\n\n    public UUID getPlayId() {\n        return MapUtils.getUUID(e.payload(), \"playId\");\n    }\n\n    public String getStatus() {\n        return MapUtils.getString(e.payload(), \"status\");\n    }\n\n    public UUID getTaskId() {\n        return MapUtils.getUUID(e.payload(), \"taskId\");\n    }\n\n    public String getTaskName() {\n        return MapUtils.assertString(e.payload(), \"task\");\n    }\n\n    public String getAction() {\n        return MapUtils.getString(e.payload(), \"action\");\n    }\n\n    public boolean isSetupTask() {\n        return \"gather_facts\".equals(getAction()) || \"setup\".equals(getAction());\n    }\n\n    public boolean isHandler() {\n        return MapUtils.getBoolean(e.payload(), \"isHandler\", false);\n    }\n\n    public long duration() {\n        return MapUtils.getNumber(e.payload(), \"duration\", 0L).longValue();\n    }\n\n    public boolean ignoreErrors() {\n        Object value = e.payload().get(\"ignore_errors\");\n        if (value instanceof Boolean) {\n            return (boolean) value;\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/AnsibleEventsConfiguration.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport java.io.Serializable;\nimport java.time.Duration;\n\n@Named\n@Singleton\npublic class AnsibleEventsConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"ansibleEvents.period\")\n    private Duration period;\n\n    @Inject\n    @Config(\"ansibleEvents.fetchLimit\")\n    private int fetchLimit;\n\n    public Duration getPeriod() {\n        return period;\n    }\n\n    public int getFetchLimit() {\n        return fetchLimit;\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/AnsibleHostProcessor.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.StringUtils;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport org.immutables.value.Value;\nimport org.jooq.CaseValueStep;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.jooq.Field;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLException;\nimport java.time.OffsetDateTime;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.server.plugins.ansible.jooq.tables.AnsibleHosts.ANSIBLE_HOSTS;\nimport static org.jooq.impl.DSL.*;\n\n@Named\n@Singleton\npublic class AnsibleHostProcessor implements EventProcessor {\n\n    private final Dao dao;\n\n    @Inject\n    public AnsibleHostProcessor(Dao dao) {\n        this.dao = dao;\n    }\n\n    @Override\n    public void process(DSLContext tx, List<Event> events) {\n        List<AnsibleEvent> eventItems = new ArrayList<>();\n        for (Event e : events) {\n            AnsibleEvent event = AnsibleEvent.from(e);\n            if (event == null) {\n                continue;\n            }\n\n            eventItems.add(event);\n        }\n\n        List<HostItem> result = combineEvents(eventItems);\n        dao.insert(tx, result);\n    }\n\n    private static List<HostItem> combineEvents(List<AnsibleEvent> events) {\n        Map<HostItem.Key, HostItem> result = new HashMap<>();\n        for (AnsibleEvent e : events) {\n            result.compute(HostItem.Key.from(e),\n                    (k, v) -> (v == null) ? HostItem.from(e) : combine(v, e));\n        }\n        return new ArrayList<>(result.values());\n    }\n\n    private static HostItem combine(HostItem hostItem, AnsibleEvent newEvent) {\n        long duration = hostItem.duration() + newEvent.duration();\n\n        HostStatus status = getHostStatus(newEvent);\n        long eventSeq = newEvent.eventSeq();\n        if (status.weight() <= hostItem.status().weight()) {\n            status = hostItem.status();\n            eventSeq = hostItem.eventSeq();\n        }\n\n        return ImmutableHostItem.builder()\n                .from(hostItem)\n                .duration(duration)\n                .status(status)\n                .eventSeq(eventSeq)\n                .build();\n    }\n\n    @Named\n    public static class Dao extends AbstractDao {\n\n        @Inject\n        public Dao(@MainDB Configuration cfg) {\n            super(cfg);\n        }\n\n        @Override\n        public <T> T txResult(TxResult<T> t) {\n            return super.txResult(t);\n        }\n\n        public void insert(DSLContext tx, List<HostItem> items) {\n            DbUtils.upsert(tx, items, Dao::update, Dao::insert);\n        }\n\n        private static int[] update(DSLContext tx, Connection conn, List<HostItem> hosts) throws SQLException {\n            Field<Integer> currentStatusWeight = decodeStatus(choose(ANSIBLE_HOSTS.STATUS));\n            Field<Integer> newStatusWeight = decodeStatus(choose(value((String) null)));\n\n            String update = tx.update(ANSIBLE_HOSTS)\n                    .set(ANSIBLE_HOSTS.DURATION, ANSIBLE_HOSTS.DURATION.plus(value((Integer) null)))\n                    .set(ANSIBLE_HOSTS.STATUS, when(currentStatusWeight.greaterThan(newStatusWeight), ANSIBLE_HOSTS.STATUS).otherwise(value((String) null)))\n                    .set(ANSIBLE_HOSTS.EVENT_SEQ, when(currentStatusWeight.greaterThan(newStatusWeight), ANSIBLE_HOSTS.EVENT_SEQ).otherwise(value((Long) null)))\n                    .where(ANSIBLE_HOSTS.INSTANCE_ID.eq(value((UUID) null))\n                            .and(ANSIBLE_HOSTS.INSTANCE_CREATED_AT.eq(value((OffsetDateTime) null))\n                                    .and(ANSIBLE_HOSTS.HOST.eq(value((String) null))\n                                            .and(ANSIBLE_HOSTS.HOST_GROUP.eq(value((String) null))\n                                                    .and(ANSIBLE_HOSTS.PLAYBOOK_ID.eq((UUID)null))))))\n                    .getSQL();\n\n            try (PreparedStatement ps = conn.prepareStatement(update)) {\n                for (HostItem h : hosts) {\n                    // set duration\n                    ps.setLong(1, h.duration());\n\n                    // set status\n                    ps.setString(2, h.status().name());\n                    ps.setString(3, h.status().name());\n\n                    // set event seq\n                    ps.setString(4, h.status().name());\n                    ps.setLong(5, h.eventSeq());\n\n                    ps.setObject(6, h.key().instanceId());\n                    ps.setObject(7, h.key().instanceCreatedAt());\n                    ps.setString(8, StringUtils.abbreviate(h.key().host(), ANSIBLE_HOSTS.HOST.getDataType().length()));\n                    ps.setString(9, StringUtils.abbreviate(h.key().hostGroup(), ANSIBLE_HOSTS.HOST_GROUP.getDataType().length()));\n                    ps.setObject(10, h.key().playbookId());\n\n                    ps.addBatch();\n                }\n                return ps.executeBatch();\n            }\n        }\n\n        private static void insert(DSLContext tx, Connection conn, List<HostItem> hosts) throws SQLException {\n            String insert = tx.insertInto(ANSIBLE_HOSTS)\n                    .columns(ANSIBLE_HOSTS.INSTANCE_ID,\n                            ANSIBLE_HOSTS.INSTANCE_CREATED_AT,\n                            ANSIBLE_HOSTS.PLAYBOOK_ID,\n                            ANSIBLE_HOSTS.HOST,\n                            ANSIBLE_HOSTS.HOST_GROUP,\n                            ANSIBLE_HOSTS.STATUS,\n                            ANSIBLE_HOSTS.DURATION,\n                            ANSIBLE_HOSTS.EVENT_SEQ)\n                    .values(value((UUID) null), null, null, null, null, null, null, null)\n                    .getSQL();\n\n            try (PreparedStatement ps = conn.prepareStatement(insert)) {\n                for (HostItem h : hosts) {\n                    ps.setObject(1, h.key().instanceId());\n                    ps.setObject(2, h.key().instanceCreatedAt());\n                    ps.setObject(3, h.key().playbookId());\n                    ps.setString(4, StringUtils.abbreviate(h.key().host(), ANSIBLE_HOSTS.HOST.getDataType().length()));\n                    ps.setString(5, StringUtils.abbreviate(h.key().hostGroup(), ANSIBLE_HOSTS.HOST_GROUP.getDataType().length()));\n                    ps.setString(6, h.status().name());\n                    ps.setLong(7, h.duration());\n                    ps.setLong(8, h.eventSeq());\n\n                    ps.addBatch();\n                }\n                ps.executeBatch();\n            }\n        }\n\n        private static Field<Integer> decodeStatus(CaseValueStep<String> choose) {\n            return choose\n                    .when(inline(HostStatus.FAILED.name()), inline(HostStatus.FAILED.weight()))\n                    .when(inline(HostStatus.UNREACHABLE.name()), inline(HostStatus.UNREACHABLE.weight()))\n                    .when(inline(HostStatus.OK.name()), inline(HostStatus.OK.weight()))\n                    .otherwise(inline(HostStatus.OK.weight));\n        }\n    }\n\n    private static HostStatus getHostStatus(AnsibleEvent e) {\n        boolean ignoreErrors = e.ignoreErrors();\n        String status = e.getStatus();\n        if (ignoreErrors && HostStatus.FAILED.name().equals(status)) {\n            return HostStatus.OK;\n        }\n        return HostStatus.of(status);\n    }\n\n    public enum HostStatus {\n\n        OK(2),\n        UNREACHABLE(4),\n        FAILED(5);\n\n        private final int weight;\n\n        HostStatus(int weight) {\n            this.weight = weight;\n        }\n\n        public int weight() {\n            return weight;\n        }\n\n        public static HostStatus of(String status) {\n            for (HostStatus s : values()) {\n                if (s.name().equals(status)) {\n                    return s;\n                }\n            }\n            return OK;\n        }\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    public interface HostItem {\n\n        @Value.Immutable\n        @Value.Style(jdkOnly = true)\n        interface Key {\n\n            @Value.Parameter\n            UUID instanceId();\n\n            @Value.Parameter\n            OffsetDateTime instanceCreatedAt();\n\n            @Value.Parameter\n            UUID playbookId();\n\n            @Value.Parameter\n            String host();\n\n            @Value.Parameter\n            String hostGroup();\n\n            static Key from(AnsibleEvent e) {\n                return ImmutableKey.of(e.instanceId(), e.instanceCreatedAt(), e.playbookId(), e.host(), e.hostGroup());\n            }\n        }\n\n        Key key();\n\n        HostStatus status();\n\n        long duration();\n\n        long eventSeq();\n\n        static HostItem from(AnsibleEvent event) {\n            return ImmutableHostItem.builder()\n                    .key(Key.from(event))\n                    .status(getHostStatus(event))\n                    .duration(event.duration())\n                    .eventSeq(event.eventSeq())\n                    .build();\n        }\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/AnsibleModule.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.walmartlabs.concord.db.DatabaseChangeLogProvider;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.plugins.ansible.db.AnsibleDBChangeLogProvider;\nimport com.walmartlabs.concord.server.plugins.ansible.queue.InventoryProcessor;\nimport com.walmartlabs.concord.server.plugins.ansible.queue.PrivateKeyProcessor;\nimport com.walmartlabs.concord.server.sdk.ScheduledTask;\nimport com.walmartlabs.concord.server.sdk.process.CustomEnqueueProcessor;\n\nimport javax.inject.Named;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\n\n@Named\npublic class AnsibleModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        binder.bind(DatabaseChangeLogProvider.class).annotatedWith(MainDB.class).to(AnsibleDBChangeLogProvider.class);\n        binder.bind(EventFetcher.class).in(SINGLETON);\n        newSetBinder(binder, ScheduledTask.class).addBinding().to(EventFetcher.class);\n        newSetBinder(binder, CustomEnqueueProcessor.class).addBinding().to(PrivateKeyProcessor.class);\n        newSetBinder(binder, CustomEnqueueProcessor.class).addBinding().to(InventoryProcessor.class);\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/Constants.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic final class Constants {\n\n    public static final String ANSIBLE_EVENT_TYPE = \"ANSIBLE\";\n\n    public static final String ANSIBLE_PLAYBOOK_INFO = \"ANSIBLE_PLAYBOOK_INFO\";\n\n    public static final String ANSIBLE_PLAYBOOK_RESULT = \"ANSIBLE_PLAYBOOK_RESULT\";\n\n    private Constants() {\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/DbUtils.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.jooq.DSLContext;\n\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic final class DbUtils {\n\n    /**\n     * Do not use it as generic \"INSERT ... ON CONFLICT UPDATE\" solution.\n     * It works only until there's only one \"inserter\".\n     */\n    public static <E> void upsert(DSLContext tx, List<E> items,\n                                  Update<E> update, Insert<E> insert) {\n\n        if (items.isEmpty()) {\n            return;\n        }\n\n        tx.connection(conn -> {\n            int[] updated = update.call(tx, conn, items);\n            List<E> forInsert = new ArrayList<>();\n            for (int i = 0; i < updated.length; i++) {\n                if (updated[i] < 1) {\n                    forInsert.add(items.get(i));\n                }\n            }\n            if (!forInsert.isEmpty()) {\n                insert.call(tx, conn, forInsert);\n            }\n        });\n    }\n\n    @FunctionalInterface\n    public interface Update<E> {\n\n        int[] call(DSLContext tx, Connection conn, List<E> items) throws SQLException;\n    }\n\n    @FunctionalInterface\n    public interface Insert<E> {\n\n        void call(DSLContext tx, Connection conn, List<E> items) throws SQLException;\n    }\n\n    private DbUtils() {\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/EventDao.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport org.jooq.Configuration;\nimport org.jooq.Record4;\nimport org.jooq.SelectConditionStep;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.time.OffsetDateTime;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.PROCESS_EVENTS;\nimport static org.jooq.impl.DSL.field;\nimport static org.jooq.impl.DSL.inline;\n\n@Named\npublic class EventDao extends AbstractDao {\n\n    @Inject\n    public EventDao(@MainDB Configuration cfg) {\n        super(cfg);\n    }\n\n    public List<ProcessEventEntry> list(ProcessKey processKey, Map<String, String> dataFilter) {\n        SelectConditionStep<Record4<UUID, String, OffsetDateTime, String>> q = dsl()\n                .select(PROCESS_EVENTS.EVENT_ID,\n                        PROCESS_EVENTS.EVENT_TYPE,\n                        PROCESS_EVENTS.EVENT_DATE,\n                        PROCESS_EVENTS.EVENT_DATA.cast(String.class))\n                .from(PROCESS_EVENTS)\n                .where(PROCESS_EVENTS.INSTANCE_ID.eq(processKey.getInstanceId())\n                        .and(PROCESS_EVENTS.INSTANCE_CREATED_AT.eq(processKey.getCreatedAt())));\n\n        q.and(PROCESS_EVENTS.EVENT_TYPE.eq(Constants.ANSIBLE_EVENT_TYPE));\n\n        if (dataFilter != null) {\n            dataFilter.forEach((k, v) -> {\n                q.and(field(\"{0}->>{1}\", Object.class, PROCESS_EVENTS.EVENT_DATA, inline(k)).eq(v));\n            });\n        }\n\n        return q.orderBy(PROCESS_EVENTS.EVENT_DATE)\n                .fetch(EventDao::toEntry);\n    }\n\n    private static ProcessEventEntry toEntry(Record4<UUID, String, OffsetDateTime, String> r) {\n        return ProcessEventEntry.builder()\n                .id(r.value1())\n                .eventType(r.value2())\n                .eventDate(r.value3())\n                .data(r.value4())\n                .build();\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/EventFetcher.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.jooq.tables.ProcessEvents;\nimport org.jooq.*;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.io.IOException;\nimport java.time.OffsetDateTime;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.PROCESS_EVENTS;\nimport static org.jooq.impl.DSL.*;\n\npublic class EventFetcher extends AbstractEventProcessor<EventProcessor.Event> {\n\n    private static final String PROCESSOR_NAME = \"ansible-event-processor\";\n\n    private final AnsibleEventsConfiguration cfg;\n    private final AnsibleEventDao dao;\n    private final List<EventProcessor> processors;\n\n    @Inject\n    public EventFetcher(AnsibleEventsConfiguration cfg,\n                        EventMarkerDao eventMarkerDao,\n                        AnsibleEventDao dao,\n                        List<EventProcessor> processors) {\n        super(PROCESSOR_NAME, eventMarkerDao, cfg.getFetchLimit());\n        this.cfg = cfg;\n        this.dao = dao;\n        this.processors = processors;\n    }\n\n    @Override\n    public String getId() {\n        return \"ansible-event-processor\";\n    }\n\n    @Override\n    public long getIntervalInSec() {\n        return cfg.getPeriod().getSeconds();\n    }\n\n    @Override\n    protected List<EventProcessor.Event> processEvents(DSLContext tx, EventMarkerDao.EventMarker marker, int fetchLimit) {\n        List<EventProcessor.Event> events = dao.list(tx, marker, fetchLimit);\n        if (events.isEmpty()) {\n            return Collections.emptyList();\n        }\n\n        for (EventProcessor p : processors) {\n            p.process(tx, events);\n        }\n\n        return events;\n    }\n\n    @Named\n    public static class AnsibleEventDao extends AbstractDao {\n\n        private final ObjectMapper objectMapper;\n\n        @Inject\n        public AnsibleEventDao(@MainDB Configuration cfg, ObjectMapper objectMapper) {\n            super(cfg);\n            this.objectMapper = objectMapper;\n        }\n\n        @Override\n        public <T> T txResult(TxResult<T> t) {\n            return super.txResult(t);\n        }\n\n        private static SelectConditionStep<Record1<JSONB>> payloadField(DSLContext tx, String... keys) {\n            return tx.select(function(\"jsonb_object_agg\", JSONB.class, field(\"key\"), field(\"value\")))\n                    .from(table(\"jsonb_each(pe.EVENT_DATA)\"))\n                    .where(field(\"key\").in(Arrays.asList(keys)));\n        }\n\n        public List<EventProcessor.Event> list(DSLContext tx, EventMarkerDao.EventMarker marker, int count) {\n            ProcessEvents pe = PROCESS_EVENTS.as(\"pe\");\n\n            SelectConditionStep<Record6<UUID, OffsetDateTime, Long, OffsetDateTime, String, JSONB>> q = tx.select(\n                            pe.INSTANCE_ID,\n                            pe.INSTANCE_CREATED_AT,\n                            pe.EVENT_SEQ,\n                            pe.EVENT_DATE,\n                            pe.EVENT_TYPE,\n                            when(pe.EVENT_TYPE.eq(Constants.ANSIBLE_EVENT_TYPE), payloadField(tx, \"host\", \"hostGroup\", \"status\", \"duration\", \"ignore_errors\", \"currentRetryCount\", \"hostStatus\", \"playId\", \"playbookId\", \"parentCorrelationId\", \"action\", \"isHandler\", \"taskId\", \"task\"))\n                                    .when(pe.EVENT_TYPE.eq(Constants.ANSIBLE_PLAYBOOK_INFO), payloadField(tx, \"plays\", \"playbookId\", \"playbook\", \"uniqueHosts\", \"totalWork\", \"parentCorrelationId\", \"currentRetryCount\"))\n                                    .when(pe.EVENT_TYPE.eq(Constants.ANSIBLE_PLAYBOOK_RESULT), payloadField(tx, \"playbookId\", \"status\", \"parentCorrelationId\")))\n                    .from(pe)\n                    .where(pe.EVENT_TYPE.in(Constants.ANSIBLE_EVENT_TYPE, Constants.ANSIBLE_PLAYBOOK_INFO, Constants.ANSIBLE_PLAYBOOK_RESULT)\n                            .and(pe.EVENT_SEQ.greaterThan(marker.eventSeq())));\n\n            return q.orderBy(pe.EVENT_SEQ)\n                    .limit(count)\n                    .fetch(r -> ImmutableEvent.builder()\n                            .instanceId(r.value1())\n                            .instanceCreatedAt(r.value2())\n                            .eventSeq(r.value3())\n                            .eventDate(r.value4())\n                            .eventType(r.value5())\n                            .payload(deserialize(r.value6()))\n                            .build());\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        private Map<String, Object> deserialize(JSONB o) {\n            if (o == null) {\n                return null;\n            }\n\n            try {\n                return objectMapper.readValue(o.toString(), Map.class);\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/EventMarkerDao.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.jooq.tables.EventProcessorMarker;\nimport org.immutables.value.Value;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.jooq.Record1;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.EVENT_PROCESSOR_MARKER;\nimport static com.walmartlabs.concord.server.jooq.Tables.PROCESS_EVENTS;\nimport static org.jooq.impl.DSL.max;\nimport static org.jooq.impl.DSL.value;\n\n// TODO: move to plugins sdk?\n@Named\npublic class EventMarkerDao extends AbstractDao {\n\n    @Inject\n    public EventMarkerDao(@MainDB Configuration cfg) {\n        super(cfg);\n    }\n\n    @Override\n    public void tx(Tx t) {\n        super.tx(t);\n    }\n\n    @Override\n    public <T> T txResult(TxResult<T> t) {\n        return super.txResult(t);\n    }\n\n    public EventMarker get(String processorName) {\n        EventProcessorMarker m = EVENT_PROCESSOR_MARKER.as(\"m\");\n\n        Long currentEventSeq = txResult(tx -> tx.select(m.EVENT_SEQ)\n                .from(m)\n                .where(m.PROCESSOR_NAME.eq(processorName))\n                .fetchOne(m.EVENT_SEQ));\n\n        Long maxEventSeq = txResult(tx -> tx.select(max(PROCESS_EVENTS.EVENT_SEQ))\n                .from(PROCESS_EVENTS)\n                .fetchOne(Record1::value1));\n\n        return EventMarker.builder()\n                .eventSeq(currentEventSeq != null ? currentEventSeq : -1)\n                .maxEventSeq(maxEventSeq != null ? maxEventSeq : -1)\n                .build();\n    }\n\n    public void update(DSLContext tx, String processorName, long eventSeq) {\n        EventProcessorMarker m = EVENT_PROCESSOR_MARKER.as(\"m\");\n        tx.insertInto(m)\n                .columns(m.PROCESSOR_NAME, m.EVENT_SEQ)\n                .values(value(processorName), value(eventSeq))\n                .onDuplicateKeyUpdate()\n                .set(m.EVENT_SEQ, eventSeq)\n                .where(m.PROCESSOR_NAME.eq(processorName))\n                .execute();\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    public interface EventMarker {\n\n        long eventSeq();\n\n        long maxEventSeq();\n\n        static ImmutableEventMarker.Builder builder() {\n            return ImmutableEventMarker.builder();\n        }\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/EventProcessor.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\nimport org.jooq.DSLContext;\n\nimport java.time.OffsetDateTime;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic interface EventProcessor {\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface Event extends AbstractEventProcessor.Event {\n\n        UUID instanceId();\n\n        OffsetDateTime instanceCreatedAt();\n\n        OffsetDateTime eventDate();\n\n        String eventType();\n\n        long eventSeq();\n\n        Map<String, Object> payload();\n    }\n\n    void process(DSLContext tx, List<Event> events);\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/PlayInfoProcessor.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.StringUtils;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.plugins.ansible.jooq.tables.AnsiblePlayStats;\nimport org.immutables.value.Value;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLException;\nimport java.time.OffsetDateTime;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.plugins.ansible.jooq.Tables.ANSIBLE_PLAY_STATS;\nimport static org.jooq.impl.DSL.inline;\nimport static org.jooq.impl.DSL.value;\n\n@Named\n@Singleton\npublic class PlayInfoProcessor implements EventProcessor {\n\n    private static final Set<String> FINISHED_TASK_STATUSES = new HashSet<>(Arrays.asList(\"SKIPPED\", \"OK\", \"UNREACHABLE\", \"FAILED\"));\n\n    private final Dao dao;\n\n    @Inject\n    public PlayInfoProcessor(Dao dao) {\n        this.dao = dao;\n    }\n\n    @Override\n    public void process(DSLContext tx, List<Event> events) {\n        Map<PlayInfoKey, Long> finishedTasks = collectFinished(events);\n\n        List<PlayInfoItem> plays = new ArrayList<>();\n        for (Event e : events) {\n            PlaybookInfoEvent p = PlaybookInfoEvent.from(e);\n            if (p == null) {\n                continue;\n            }\n\n            List<PlaybookInfoEvent.Play> eventPlays = p.getPlays();\n            for (int i = 0; i < eventPlays.size(); i++) {\n                PlaybookInfoEvent.Play play = eventPlays.get(i);\n\n                PlayInfoKey key = PlayInfoKey.builder()\n                        .instanceId(e.instanceId())\n                        .instanceCreatedAt(e.instanceCreatedAt())\n                        .playbookId(p.playbookId())\n                        .playId(play.getId())\n                        .build();\n                Long finishedCount = finishedTasks.remove(key);\n\n                String playName = StringUtils.abbreviate(play.getName(), ANSIBLE_PLAY_STATS.PLAY_NAME.getDataType().length());\n                if (playName == null) {\n                    // ignore invalid data (i.e. data produced by old ansible-task versions)\n                    continue;\n                }\n\n                plays.add(ImmutablePlayInfoItem.builder()\n                        .key(key)\n                        .playName(playName)\n                        .playOrder(i)\n                        .hostCount(play.getHostCount())\n                        .taskCount(play.getTaskCount())\n                        .finishedCount(finishedCount != null ? finishedCount : 0)\n                        .build());\n            }\n        }\n\n        dao.insert(tx, plays);\n        dao.insertFinished(tx, finishedTasks.entrySet().stream().map(e -> PlayInfoFinishedItem.of(e.getKey(), e.getValue())).collect(Collectors.toList()));\n    }\n\n    private static Map<PlayInfoKey, Long> collectFinished(List<Event> events) {\n        Map<PlayInfoKey, Long> result = new HashMap<>();\n        for (Event e : events) {\n            AnsibleEvent event = AnsibleEvent.from(e);\n            if (event == null) {\n                continue;\n            }\n\n            UUID playId = event.getPlayId();\n            if (playId == null) {\n                // event from old plugin\n                continue;\n            }\n\n            if (ignore(event)) {\n                continue;\n            }\n\n            if (isFinished(event)) {\n                PlayInfoKey key = PlayInfoKey.builder()\n                        .instanceId(e.instanceId())\n                        .instanceCreatedAt(e.instanceCreatedAt())\n                        .playbookId(event.playbookId())\n                        .playId(playId)\n                        .build();\n\n                result.compute(key, (k, v) -> (v == null) ? 1 : v + 1);\n            }\n        }\n        return result;\n    }\n\n    private static boolean ignore(AnsibleEvent e) {\n        String action = e.getAction();\n        if (\"gather_facts\".equals(action)) {\n            return true;\n        }\n\n        return e.isHandler();\n    }\n\n    private static boolean isFinished(AnsibleEvent e) {\n        String status = e.getStatus();\n        if (status == null) {\n            return false;\n        }\n\n        return FINISHED_TASK_STATUSES.contains(status);\n    }\n\n    @Named\n    public static class Dao extends AbstractDao {\n\n        @Inject\n        public Dao(@MainDB Configuration cfg) {\n            super(cfg);\n        }\n\n        public void insertFinished(DSLContext tx, List<PlayInfoFinishedItem> items) {\n            if (items.isEmpty()) {\n                return;\n            }\n\n            DbUtils.upsert(tx, items, Dao::updateFinishedItems, Dao::insertFinishedItems);\n        }\n\n        public void insert(DSLContext tx, List<PlayInfoItem> plays) {\n            if (plays.isEmpty()) {\n                return;\n            }\n\n            DbUtils.upsert(tx, plays, Dao::update, Dao::insert);\n        }\n\n        private static int[] updateFinishedItems(DSLContext tx, Connection conn, List<PlayInfoFinishedItem> items) throws SQLException {\n            AnsiblePlayStats a = ANSIBLE_PLAY_STATS;\n            String update = tx.update(a)\n                    .set(a.FINISHED_TASK_COUNT, a.FINISHED_TASK_COUNT.plus(value((Long) null)))\n                    .where(a.INSTANCE_ID.eq(value((UUID) null))\n                            .and(a.INSTANCE_CREATED_AT.eq(value((OffsetDateTime) null))\n                                    .and(a.PLAY_ID.eq(value((UUID) null)))))\n                    .getSQL();\n\n            try (PreparedStatement ps = conn.prepareStatement(update)) {\n                for (PlayInfoFinishedItem p : items) {\n                    ps.setLong(1, p.finishedCount());\n                    ps.setObject(2, p.key().instanceId());\n                    ps.setObject(3, p.key().instanceCreatedAt());\n                    ps.setObject(4, p.key().playId());\n\n                    ps.addBatch();\n                }\n                return ps.executeBatch();\n            }\n        }\n\n        private static void insertFinishedItems(DSLContext tx, Connection conn, List<PlayInfoFinishedItem> items) throws SQLException {\n            String insert = tx.insertInto(ANSIBLE_PLAY_STATS)\n                    .columns(ANSIBLE_PLAY_STATS.INSTANCE_ID,\n                            ANSIBLE_PLAY_STATS.INSTANCE_CREATED_AT,\n                            ANSIBLE_PLAY_STATS.PLAYBOOK_ID,\n                            ANSIBLE_PLAY_STATS.PLAY_ID,\n                            ANSIBLE_PLAY_STATS.PLAY_ORDER,\n                            ANSIBLE_PLAY_STATS.HOST_COUNT,\n                            ANSIBLE_PLAY_STATS.TASK_COUNT,\n                            ANSIBLE_PLAY_STATS.FINISHED_TASK_COUNT)\n                    .values(value((UUID) null), null, null, null, inline(0), inline(0L), inline(0), null)\n                    .getSQL();\n\n            try (PreparedStatement ps = conn.prepareStatement(insert)) {\n                for (PlayInfoFinishedItem p : items) {\n                    ps.setObject(1, p.key().instanceId());\n                    ps.setObject(2, p.key().instanceCreatedAt());\n                    ps.setObject(3, p.key().playbookId());\n                    ps.setObject(4, p.key().playId());\n                    ps.setLong(5, p.finishedCount());\n\n                    ps.addBatch();\n                }\n                ps.executeBatch();\n            }\n        }\n\n        private static int[] update(DSLContext tx, Connection conn, List<PlayInfoItem> plays) throws SQLException {\n            AnsiblePlayStats a = ANSIBLE_PLAY_STATS;\n            String update = tx.update(a)\n                    .set(a.PLAY_NAME, value((String) null))\n                    .set(a.PLAY_ORDER, value((Integer) null))\n                    .set(a.TASK_COUNT, value((Integer) null))\n                    .set(a.HOST_COUNT, value((Long) null))\n                    .set(a.FINISHED_TASK_COUNT, a.FINISHED_TASK_COUNT.plus(value((Long) null)))\n                    .where(a.INSTANCE_ID.eq(value((UUID) null))\n                            .and(a.INSTANCE_CREATED_AT.eq(value((OffsetDateTime) null))\n                                    .and(a.PLAY_ID.eq(value((UUID) null)))))\n                    .getSQL();\n\n            try (PreparedStatement ps = conn.prepareStatement(update)) {\n                for (PlayInfoItem p : plays) {\n                    ps.setString(1, p.playName());\n                    ps.setInt(2, p.playOrder());\n                    ps.setInt(3, p.taskCount());\n                    ps.setLong(4, p.hostCount());\n                    ps.setLong(5, p.finishedCount());\n                    ps.setObject(6, p.key().instanceId());\n                    ps.setObject(7, p.key().instanceCreatedAt());\n                    ps.setObject(8, p.key().playId());\n\n                    ps.addBatch();\n                }\n                return ps.executeBatch();\n            }\n        }\n\n        private static void insert(DSLContext tx, Connection conn, List<PlayInfoItem> plays) throws SQLException {\n            String insert = tx.insertInto(ANSIBLE_PLAY_STATS)\n                    .columns(ANSIBLE_PLAY_STATS.INSTANCE_ID,\n                            ANSIBLE_PLAY_STATS.INSTANCE_CREATED_AT,\n                            ANSIBLE_PLAY_STATS.PLAYBOOK_ID,\n                            ANSIBLE_PLAY_STATS.PLAY_ID,\n                            ANSIBLE_PLAY_STATS.PLAY_NAME,\n                            ANSIBLE_PLAY_STATS.PLAY_ORDER,\n                            ANSIBLE_PLAY_STATS.HOST_COUNT,\n                            ANSIBLE_PLAY_STATS.TASK_COUNT,\n                            ANSIBLE_PLAY_STATS.FINISHED_TASK_COUNT)\n                    .values(value((UUID) null), null, null, null, null, null, null, null, null)\n                    .getSQL();\n\n            try (PreparedStatement ps = conn.prepareStatement(insert)) {\n                for (PlayInfoItem p : plays) {\n                    ps.setObject(1, p.key().instanceId());\n                    ps.setObject(2, p.key().instanceCreatedAt());\n                    ps.setObject(3, p.key().playbookId());\n                    ps.setObject(4, p.key().playId());\n                    ps.setString(5, p.playName());\n                    ps.setInt(6, p.playOrder());\n                    ps.setLong(7, p.hostCount());\n                    ps.setInt(8, p.taskCount());\n                    ps.setLong(9, p.finishedCount());\n\n                    ps.addBatch();\n                }\n                ps.executeBatch();\n            }\n        }\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface PlayInfoKey {\n\n        UUID instanceId();\n\n        OffsetDateTime instanceCreatedAt();\n\n        UUID playbookId();\n\n        UUID playId();\n\n        static ImmutablePlayInfoKey.Builder builder() {\n            return ImmutablePlayInfoKey.builder();\n        }\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface PlayInfoFinishedItem {\n\n        PlayInfoKey key();\n\n        long finishedCount();\n\n        static PlayInfoFinishedItem of(PlayInfoKey key, long count) {\n            return ImmutablePlayInfoFinishedItem.builder()\n                    .key(key)\n                    .finishedCount(count)\n                    .build();\n        }\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface PlayInfoItem {\n\n        PlayInfoKey key();\n\n        String playName();\n\n        int playOrder();\n\n        long hostCount();\n\n        int taskCount();\n\n        long finishedCount();\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/PlaybookInfoEvent.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.MapUtils;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\n\npublic class PlaybookInfoEvent extends AbstractAnsibleEvent {\n\n    public static PlaybookInfoEvent from(EventProcessor.Event e) {\n        if (!e.eventType().equals(Constants.ANSIBLE_PLAYBOOK_INFO)) {\n            return null;\n        }\n\n        return new PlaybookInfoEvent(e);\n    }\n\n    private PlaybookInfoEvent(EventProcessor.Event e) {\n        super(e);\n    }\n\n    public String getPlaybookName() {\n        return MapUtils.assertString(e.payload(), \"playbook\");\n    }\n\n    public int getHostsCount() {\n        return MapUtils.assertInt(e.payload(), \"uniqueHosts\");\n    }\n\n    public List<Play> getPlays() {\n        List<Map<String, Object>> plays = MapUtils.assertList(e.payload(), \"plays\");\n        return plays.stream()\n                .map(Play::from)\n                .collect(Collectors.toList());\n    }\n\n    public long getTotalWork() {\n        return MapUtils.assertNumber(e.payload(), \"totalWork\").longValue();\n    }\n\n    public Integer retryCount() {\n        Object v = e.payload().get(\"currentRetryCount\");\n\n        if (v instanceof Number) {\n            return ((Number) v).intValue();\n        }\n\n        if (v instanceof String) {\n            return Integer.parseInt((String)v);\n        }\n\n        return null;\n    }\n\n    public String getStatus() {\n        return MapUtils.assertString(e.payload(), \"status\");\n    }\n\n    public static class Play {\n\n        private final Map<String, Object> params;\n\n        private Play(Map<String, Object> params) {\n            this.params = params;\n        }\n\n        public static Play from(Map<String, Object> p) {\n            return new Play(p);\n        }\n\n        public UUID getId() {\n            return MapUtils.assertUUID(params, \"id\");\n        }\n\n        public String getName() {\n            return MapUtils.getString(params, \"play\");\n        }\n\n        public long getHostCount() {\n            return MapUtils.assertNumber(params, \"hosts\").longValue();\n        }\n\n        public int getTaskCount() {\n            return getTasks().size();\n        }\n\n        public List<Map<String, Object>> getTasks() {\n            return MapUtils.assertList(params, \"tasks\");\n        }\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/PlaybookInfoProcessor.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.StringUtils;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.plugins.ansible.jooq.tables.AnsiblePlaybookStats;\nimport org.immutables.value.Value;\nimport org.jooq.*;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport java.sql.PreparedStatement;\nimport java.time.OffsetDateTime;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.plugins.ansible.jooq.Tables.ANSIBLE_PLAYBOOK_STATS;\nimport static org.jooq.impl.DSL.value;\n\n@Named\n@Singleton\npublic class PlaybookInfoProcessor implements EventProcessor {\n\n    private final Dao dao;\n\n    @Inject\n    public PlaybookInfoProcessor(Dao dao) {\n        this.dao = dao;\n    }\n\n    @Override\n    public void process(DSLContext tx, List<Event> events) {\n        List<PlaybookInfo> playbooks = new ArrayList<>();\n        for (Event e : events) {\n            PlaybookInfoEvent p = PlaybookInfoEvent.from(e);\n            if (p == null) {\n                continue;\n            }\n\n            playbooks.add(ImmutablePlaybookInfo.builder()\n                    .instanceId(e.instanceId())\n                    .instanceCreatedAt(e.instanceCreatedAt())\n                    .playbookId(p.playbookId())\n                    .name(p.getPlaybookName())\n                    .startedAt(e.eventDate())\n                    .hostCount(p.getHostsCount())\n                    .playCount(p.getPlays().size())\n                    .totalWork(p.getTotalWork())\n                    .currentRetryCount(p.retryCount())\n                    .build());\n        }\n\n        dao.insert(tx, playbooks);\n    }\n\n    @Named\n    public static class Dao extends AbstractDao {\n\n        @Inject\n        public Dao(@MainDB Configuration cfg) {\n            super(cfg);\n        }\n\n        public void insert(DSLContext tx, List<PlaybookInfo> items) {\n            if (items.isEmpty()) {\n                return;\n            }\n\n            AnsiblePlaybookStats a = ANSIBLE_PLAYBOOK_STATS.as(\"a\");\n\n            SelectConditionStep<Record1<Integer>> exists = tx.selectOne()\n                    .from(a)\n                    .where(a.INSTANCE_ID.eq(value((UUID) null))\n                            .and(a.INSTANCE_CREATED_AT.eq(value((OffsetDateTime) null))\n                                    .and(a.PLAYBOOK_ID.eq(value((UUID) null)))));\n\n            SelectConditionStep<Record9<UUID, OffsetDateTime, UUID, String, OffsetDateTime, Integer, Integer, Integer, Integer>> values =\n                    tx.select(value((UUID) null),\n                            value((OffsetDateTime) null),\n                            value((UUID) null),\n                            value((String) null),\n                            value((OffsetDateTime) null),\n                            value((Integer) null),\n                            value((Integer) null),\n                            value((Integer) null),\n                            value((Integer) null))\n                            .whereNotExists(exists);\n\n            String insert = tx.insertInto(a)\n                    .columns(a.INSTANCE_ID,\n                            a.INSTANCE_CREATED_AT,\n                            a.PLAYBOOK_ID,\n                            a.NAME,\n                            a.STARTED_AT,\n                            a.HOST_COUNT,\n                            a.PLAY_COUNT,\n                            a.TOTAL_WORK,\n                            a.RETRY_NUM)\n                    .select(values)\n                    .getSQL();\n\n            tx.connection(conn -> {\n                try (PreparedStatement ps = conn.prepareStatement(insert)) {\n                    for (PlaybookInfo p : items) {\n                        ps.setObject(1, p.instanceId());\n                        ps.setObject(2, p.instanceCreatedAt());\n                        ps.setObject(3, p.playbookId());\n                        ps.setString(4, StringUtils.abbreviate(p.name(), ANSIBLE_PLAYBOOK_STATS.NAME.getDataType().length()));\n                        ps.setObject(5, p.startedAt());\n                        ps.setLong(6, p.hostCount());\n                        ps.setInt(7, p.playCount());\n                        ps.setLong(8, p.totalWork());\n                        ps.setObject(9, p.currentRetryCount());\n\n                        ps.setObject(10, p.instanceId());\n                        ps.setObject(11, p.instanceCreatedAt());\n                        ps.setObject(12, p.playbookId());\n\n                        ps.addBatch();\n                    }\n                    ps.executeBatch();\n                }\n            });\n        }\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface PlaybookInfo {\n\n        UUID instanceId();\n\n        OffsetDateTime instanceCreatedAt();\n\n        UUID playbookId();\n\n        String name();\n\n        OffsetDateTime startedAt();\n\n        long hostCount();\n\n        int playCount();\n\n        long totalWork();\n\n        @Nullable\n        Integer currentRetryCount();\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/PlaybookResultEvent.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.sdk.MapUtils;\n\npublic class PlaybookResultEvent extends AbstractAnsibleEvent {\n\n    public static PlaybookResultEvent from(EventProcessor.Event e) {\n        if (!e.eventType().equals(Constants.ANSIBLE_PLAYBOOK_RESULT)) {\n            return null;\n        }\n\n        return new PlaybookResultEvent(e);\n    }\n\n    private PlaybookResultEvent(EventProcessor.Event e) {\n        super(e);\n    }\n\n    public String getStatus() {\n        return MapUtils.assertString(e.payload(), \"status\");\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/PlaybookResultProcessor.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.plugins.ansible.jooq.tables.AnsiblePlaybookResult;\nimport org.immutables.value.Value;\nimport org.jooq.*;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport java.sql.PreparedStatement;\nimport java.time.OffsetDateTime;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.plugins.ansible.jooq.Tables.ANSIBLE_PLAYBOOK_RESULT;\nimport static org.jooq.impl.DSL.value;\n\n@Named\n@Singleton\npublic class PlaybookResultProcessor implements EventProcessor {\n\n    private final Dao dao;\n\n    @Inject\n    public PlaybookResultProcessor(Dao dao) {\n        this.dao = dao;\n    }\n\n    @Override\n    public void process(DSLContext tx, List<Event> events) {\n        List<PlaybookResult> results = new ArrayList<>();\n        for (Event e : events) {\n            PlaybookResultEvent p = PlaybookResultEvent.from(e);\n            if (p == null) {\n                continue;\n            }\n\n            results.add(ImmutablePlaybookResult.builder()\n                    .instanceId(e.instanceId())\n                    .instanceCreatedAt(e.instanceCreatedAt())\n                    .playbookId(p.playbookId())\n                    .status(p.getStatus())\n                    .build());\n        }\n\n        dao.insert(tx, results);\n    }\n\n    @Named\n    public static class Dao extends AbstractDao {\n\n        @Inject\n        public Dao(@MainDB Configuration cfg) {\n            super(cfg);\n        }\n\n        public void insert(DSLContext tx, List<PlaybookResult> items) {\n            if (items.isEmpty()) {\n                return;\n            }\n\n            AnsiblePlaybookResult a = ANSIBLE_PLAYBOOK_RESULT.as(\"a\");\n\n            SelectConditionStep<Record1<Integer>> exists = tx.selectOne()\n                    .from(a)\n                    .where(a.INSTANCE_ID.eq(value((UUID) null))\n                            .and(a.INSTANCE_CREATED_AT.eq(value((OffsetDateTime) null))\n                                    .and(a.PLAYBOOK_ID.eq(value((UUID) null)))));\n\n            SelectConditionStep<Record4<UUID, OffsetDateTime, UUID, String>> values =\n                    tx.select(value((UUID) null),\n                            value((OffsetDateTime) null),\n                            value((UUID) null),\n                            value((String) null))\n                            .whereNotExists(exists);\n\n            String insert = tx.insertInto(a)\n                    .columns(a.INSTANCE_ID,\n                            a.INSTANCE_CREATED_AT,\n                            a.PLAYBOOK_ID,\n                            a.STATUS)\n                    .select(values)\n                    .getSQL();\n\n            tx.connection(conn -> {\n                try (PreparedStatement ps = conn.prepareStatement(insert)) {\n                    for (PlaybookResult p : items) {\n                        ps.setObject(1, p.instanceId());\n                        ps.setObject(2, p.instanceCreatedAt());\n                        ps.setObject(3, p.playbookId());\n                        ps.setString(4, p.status());\n\n                        ps.setObject(5, p.instanceId());\n                        ps.setObject(6, p.instanceCreatedAt());\n                        ps.setObject(7, p.playbookId());\n\n                        ps.addBatch();\n                    }\n                    ps.executeBatch();\n                }\n            });\n        }\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface PlaybookResult {\n\n        UUID instanceId();\n\n        OffsetDateTime instanceCreatedAt();\n\n        UUID playbookId();\n\n        String status();\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/ProcessAnsibleResource.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.plugins.ansible.jooq.tables.AnsibleHosts;\nimport com.walmartlabs.concord.server.plugins.ansible.jooq.tables.AnsiblePlayStats;\nimport com.walmartlabs.concord.server.plugins.ansible.jooq.tables.AnsiblePlaybookStats;\nimport com.walmartlabs.concord.server.plugins.ansible.jooq.tables.AnsibleTaskStats;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKeyCache;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\nimport org.immutables.value.Value;\nimport org.jooq.*;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport java.math.BigDecimal;\nimport java.time.OffsetDateTime;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.plugins.ansible.ProcessAnsibleResource.AnsibleHostStatus.FAILED;\nimport static com.walmartlabs.concord.server.plugins.ansible.ProcessAnsibleResource.AnsibleHostStatus.UNREACHABLE;\nimport static com.walmartlabs.concord.server.plugins.ansible.jooq.Tables.*;\nimport static com.walmartlabs.concord.server.plugins.ansible.jooq.tables.AnsibleTaskStats.ANSIBLE_TASK_STATS;\nimport static org.jooq.impl.DSL.*;\n\n@Named\n@Singleton\n@Path(\"/api/v1/process\")\n@Tag(name = \"Ansible Process\")\npublic class ProcessAnsibleResource implements Resource {\n\n    private final ProcessKeyCache processKeyCache;\n    private final EventDao eventDao;\n    private final AnsibleDao ansibleDao;\n\n    @Inject\n    public ProcessAnsibleResource(ProcessKeyCache processKeyCache, EventDao eventDao, AnsibleDao ansibleDao) {\n        this.processKeyCache = processKeyCache;\n        this.eventDao = eventDao;\n        this.ansibleDao = ansibleDao;\n    }\n\n    @GET\n    @Operation(description = \"List Ansible playbooks of a specific process\", operationId = \"listPlaybooks\")\n    @Path(\"/{processInstanceId}/ansible/playbooks\")\n    @Produces(MediaType.APPLICATION_JSON)\n    public List<PlaybookEntry> listPlaybooks(@PathParam(\"processInstanceId\") UUID processInstanceId) {\n        ProcessKey key = processKeyCache.get(processInstanceId);\n        if (key == null) {\n            return Collections.emptyList();\n        }\n\n        return ansibleDao.listPlaybooks(key.getInstanceId(), key.getCreatedAt());\n    }\n\n    @GET\n    @Operation(description = \"List Ansible plays of a specific process\", operationId = \"listPlays\")\n    @Path(\"/{processInstanceId}/ansible/{playbookId}/plays\")\n    @Produces(MediaType.APPLICATION_JSON)\n    public List<PlayInfo> listPlays(@PathParam(\"processInstanceId\") UUID processInstanceId,\n                                    @PathParam(\"playbookId\") UUID playbookId) {\n        ProcessKey key = processKeyCache.get(processInstanceId);\n        if (key == null) {\n            return Collections.emptyList();\n        }\n\n        return ansibleDao.listPlays(key.getInstanceId(), key.getCreatedAt(), playbookId);\n    }\n\n    @GET\n    @Operation(description = \"List Ansible plays of a specific process\", operationId = \"listTasks\")\n    @Path(\"/{processInstanceId}/ansible/tasks\")\n    @Produces(MediaType.APPLICATION_JSON)\n    public List<TaskInfo> listTasks(@PathParam(\"processInstanceId\") UUID processInstanceId,\n                                    @QueryParam(\"playId\") UUID playId) {\n        ProcessKey key = processKeyCache.get(processInstanceId);\n        if (key == null) {\n            return Collections.emptyList();\n        }\n\n        return ansibleDao.listTasks(key.getInstanceId(), key.getCreatedAt(), playId);\n    }\n\n\n    /**\n     * Lists Ansible hosts of a specific process.\n     */\n    @GET\n    @Operation(description = \"List Ansible hosts of a specific process\", operationId = \"listAnsibleHosts\")\n    @Path(\"/{processInstanceId}/ansible/hosts\")\n    @Produces(MediaType.APPLICATION_JSON)\n    public AnsibleHostListResponse list(@PathParam(\"processInstanceId\") UUID processInstanceId,\n                                        @QueryParam(\"host\") String host,\n                                        @QueryParam(\"hostGroup\") String hostGroup,\n                                        @QueryParam(\"status\") AnsibleHostStatus status,\n                                        @QueryParam(\"statuses\") List<AnsibleHostStatus> statuses,\n                                        @QueryParam(\"playbookId\") UUID playbookId,\n                                        @QueryParam(\"limit\") @DefaultValue(\"30\") int limit,\n                                        @QueryParam(\"offset\") @DefaultValue(\"0\") int offset,\n                                        @QueryParam(\"sortField\") SortField sortField,\n                                        @QueryParam(\"sortBy\") SortBy sortBy) {\n\n        ProcessKey key = processKeyCache.get(processInstanceId);\n        if (key == null) {\n            return null;\n        }\n\n        if (status != null) {\n            if (statuses == null) {\n                statuses = new ArrayList<>();\n            }\n            statuses.add(status);\n        }\n\n        List<AnsibleHostEntry> hosts = ansibleDao.list(key.getInstanceId(), key.getCreatedAt(), host, hostGroup, statuses, playbookId, limit, offset, sortField, sortBy);\n        List<String> hostGroups = ansibleDao.listHostGroups(key.getInstanceId(), key.getCreatedAt(), playbookId);\n        return ImmutableAnsibleHostListResponse.builder()\n                .items(hosts)\n                .hostGroups(hostGroups)\n                .build();\n    }\n\n    /**\n     * Lists Ansible events of a specific process.\n     */\n    @GET\n    @Operation(description = \"List Ansible events of a specific process\", operationId = \"listEvents\")\n    @Path(\"/{processInstanceId}/ansible/events\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @WithTimer\n    public List<ProcessEventEntry> listEvents(@PathParam(\"processInstanceId\") UUID processInstanceId,\n                                              @QueryParam(\"host\") String host,\n                                              @QueryParam(\"hostGroup\") String hostGroup,\n                                              @QueryParam(\"status\") String status,\n                                              @QueryParam(\"playbookId\") UUID playbookId) {\n\n        ProcessKey key = processKeyCache.get(processInstanceId);\n        if (key == null) {\n            return Collections.emptyList();\n        }\n\n        Map<String, String> eventFilter = new HashMap<>();\n        if (host != null) {\n            eventFilter.put(\"host\", host);\n        }\n        if (hostGroup != null) {\n            eventFilter.put(\"hostGroup\", hostGroup);\n        }\n        if (status != null) {\n            eventFilter.put(\"status\", status);\n        }\n        if (playbookId != null) {\n            eventFilter.put(\"playbookId\", playbookId.toString());\n        }\n\n        return eventDao.list(key, eventFilter);\n    }\n\n    @Named\n    public static class AnsibleDao extends AbstractDao {\n\n        @Inject\n        protected AnsibleDao(@MainDB Configuration cfg) {\n            super(cfg);\n        }\n\n        public List<PlayInfo> listPlays(UUID instanceId, OffsetDateTime instanceCreatedAt, UUID playbookId) {\n            return txResult(tx -> {\n                AnsiblePlayStats a = ANSIBLE_PLAY_STATS.as(\"a\");\n                AnsibleTaskStats s = ANSIBLE_TASK_STATS.as(\"s\");\n\n                return tx.select(\n                        a.PLAY_ID,\n                        a.PLAY_NAME,\n                        a.PLAY_ORDER,\n                        a.HOST_COUNT,\n                        a.TASK_COUNT,\n                        a.FINISHED_TASK_COUNT,\n                        sum(s.OK_COUNT).as(\"ok\"),\n                        sum(s.FAILED_COUNT).as(\"failed\"),\n                        sum(s.UNREACHABLE_COUNT).as(\"unreachable\"),\n                        sum(s.SKIPPED_COUNT).as(\"skipped\"),\n                        sum(s.RUNNING_COUNT).as(\"running\"))\n                        .from(a)\n                        .leftJoin(s).on(s.INSTANCE_ID.eq(instanceId)\n                                .and(s.INSTANCE_CREATED_AT.eq(instanceCreatedAt)\n                                        .and(s.PLAY_ID.eq(a.PLAY_ID))))\n                        .where(a.INSTANCE_ID.eq(instanceId)\n                                .and(a.INSTANCE_CREATED_AT.eq(instanceCreatedAt)\n                                        .and(a.PLAYBOOK_ID.eq(playbookId))))\n                        .groupBy(a.PLAY_ID, a.PLAY_NAME, a.PLAY_ORDER, a.HOST_COUNT, a.TASK_COUNT, a.FINISHED_TASK_COUNT)\n                        .fetch(r -> ImmutablePlayInfo.builder()\n                                .playId(r.get(a.PLAY_ID))\n                                .playName(r.get(a.PLAY_NAME) != null ? r.get(a.PLAY_NAME) : \"n/a\")\n                                .playOrder(r.get(a.PLAY_ORDER))\n                                .hostCount(r.get(a.HOST_COUNT))\n                                .taskCount(r.get(a.TASK_COUNT))\n                                .putAllTaskStats(getTaskStats(r))\n                                .finishedTaskCount(r.get(a.FINISHED_TASK_COUNT))\n                                .build());\n            });\n        }\n\n        private Map<String, Long> getTaskStats(Record11<UUID, String, Integer, Long, Integer, Long, BigDecimal, BigDecimal, BigDecimal, BigDecimal, BigDecimal> r) {\n            Map<String, Long> result = new HashMap<>();\n            result.put(\"ok\", getTaskCount(r, \"ok\"));\n            result.put(\"failed\", getTaskCount(r, \"failed\"));\n            result.put(\"unreachable\", getTaskCount(r, \"unreachable\"));\n            result.put(\"skipped\", getTaskCount(r, \"skipped\"));\n            result.put(\"running\", getTaskCount(r, \"running\"));\n            return result;\n        }\n\n        private static Long getTaskCount(Record11<UUID, String, Integer, Long, Integer, Long, BigDecimal, BigDecimal, BigDecimal, BigDecimal, BigDecimal> r, String key) {\n            Object v = r.get(key);\n            if (v == null) {\n                return 0L;\n            }\n\n            long result = ((BigDecimal)v).longValue();\n            if (result < 0) {\n                return 0L;\n            }\n\n            return result;\n        }\n\n        public List<TaskInfo> listTasks(UUID instanceId, OffsetDateTime instanceCreatedAt, UUID playId) {\n            return txResult(tx -> {\n                AnsibleTaskStats a = ANSIBLE_TASK_STATS.as(\"a\");\n                return tx.select(a.TASK_NAME, a.TASK_ORDER, a.TASK_TYPE, a.OK_COUNT, a.FAILED_COUNT, a.UNREACHABLE_COUNT, a.SKIPPED_COUNT, a.RUNNING_COUNT)\n                        .from(a)\n                        .where(a.INSTANCE_ID.eq(instanceId)\n                                .and(a.INSTANCE_CREATED_AT.eq(instanceCreatedAt)\n                                        .and(a.PLAY_ID.eq(playId))))\n                        .fetch(r -> ImmutableTaskInfo.builder()\n                                .taskName(r.get(a.TASK_NAME))\n                                .taskOrder(r.get(a.TASK_ORDER))\n                                .type(TaskType.valueOf(r.get(a.TASK_TYPE)))\n                                .okCount(r.get(a.OK_COUNT))\n                                .failedCount(r.get(a.FAILED_COUNT))\n                                .unreachableCount(r.get(a.UNREACHABLE_COUNT))\n                                .skippedCount(r.get(a.SKIPPED_COUNT))\n                                .runningCount(r.get(a.RUNNING_COUNT) < 0 ? 0 : r.get(a.RUNNING_COUNT))\n                                .build());\n            });\n        }\n\n        public List<String> listHostGroups(UUID instanceId, OffsetDateTime instanceCreatedAt, UUID playbookId) {\n            return txResult(tx -> {\n                AnsibleHosts a = ANSIBLE_HOSTS.as(\"a\");\n                SelectConditionStep<Record1<String>> q = tx.selectDistinct(a.HOST_GROUP)\n                        .from(a)\n                        .where(a.INSTANCE_ID.eq(instanceId)\n                                .and(a.INSTANCE_CREATED_AT.eq(instanceCreatedAt)));\n\n                if (playbookId != null) {\n                    q = q.and(a.PLAYBOOK_ID.eq(playbookId));\n                }\n\n                return q.fetch(Record1::value1);\n            });\n        }\n\n        public List<AnsibleHostEntry> list(UUID instanceId, OffsetDateTime instanceCreatedAt,\n                                           String host, String hostGroup,\n                                           List<AnsibleHostStatus> statuses,\n                                           UUID playbookId,\n                                           int limit, int offset, SortField sortField, SortBy sortBy) {\n            \n            Field<String> orderField = assertSortField(sortField);\n            \n            return txResult(tx -> {\n                AnsibleHosts a = ANSIBLE_HOSTS.as(\"a\");\n                SelectConditionStep<Record4<String, String, Long, String>> q =\n                        tx.select(\n                                a.HOST,\n                                a.HOST_GROUP,\n                                a.DURATION,\n                                a.STATUS)\n                                .from(a)\n                                .where(a.INSTANCE_ID.eq(instanceId)\n                                        .and(a.INSTANCE_CREATED_AT.eq(instanceCreatedAt)));\n\n                if (host != null) {\n                    q.and(a.HOST.contains(host));\n                }\n\n                if (hostGroup != null) {\n                    q.and(a.HOST_GROUP.eq(hostGroup));\n                }\n\n                if (statuses != null && !statuses.isEmpty()) {\n                    q.and(a.STATUS.in(statuses.stream().map(Enum::name).collect(Collectors.toList())));\n                }\n\n                if (playbookId != null) {\n                    q.and(a.PLAYBOOK_ID.eq(playbookId));\n                }\n                \n                if (orderField != null) {\n                    if (sortBy != null && sortBy.equals(SortBy.DESC)) {\n                        q.orderBy(orderField.desc());\n                    }\n                    q.orderBy(orderField.asc());\n                }\n                else {\n                    q.orderBy(a.HOST);\n                }\n                \n                return q.limit(limit)\n                        .offset(offset)\n                        .fetch(AnsibleDao::toHostEntity);\n            });\n        }\n\n        private static Field<String> assertSortField(SortField sortField) { \n            if (sortField == null) {\n                return null;\n            }\n            \n            Field<String> orderField = (Field<String>) ANSIBLE_HOSTS.as(\"a\").field(sortField.name().toLowerCase());\n            if (orderField == null) {\n                throw new ValidationErrorsException(\"Invalid sort field: \" + sortField.name());\n            }\n            \n            return orderField;\n        }\n\n        public List<PlaybookEntry> listPlaybooks(UUID instanceId, OffsetDateTime createdAt) {\n            return txResult(tx -> {\n                AnsiblePlaybookStats p = ANSIBLE_PLAYBOOK_STATS.as(\"p\");\n\n                Field<Integer> failedHosts = tx.select(count()).from(ANSIBLE_HOSTS).where(ANSIBLE_HOSTS.STATUS.in(FAILED.name(), UNREACHABLE.name())\n                        .and(ANSIBLE_HOSTS.INSTANCE_ID.eq(instanceId)\n                                .and(ANSIBLE_HOSTS.INSTANCE_CREATED_AT.eq(createdAt)\n                                        .and(ANSIBLE_HOSTS.PLAYBOOK_ID.eq(p.PLAYBOOK_ID)))))\n                        .asField(\"failedHostsCount\");\n\n                AnsiblePlayStats s = ANSIBLE_PLAY_STATS;\n                Field<Long> totalPlayWork = s.HOST_COUNT.mul(s.TASK_COUNT);\n                Field<BigDecimal> finishedTasks = tx.select(sum(\n                        when(s.FINISHED_TASK_COUNT.greaterThan(totalPlayWork), totalPlayWork)\n                                .otherwise(s.FINISHED_TASK_COUNT)))\n                        .from(s)\n                        .where(s.INSTANCE_ID.eq(instanceId)\n                                .and(s.INSTANCE_CREATED_AT.eq(createdAt)\n                                        .and(s.PLAYBOOK_ID.eq(p.PLAYBOOK_ID))))\n                        .asField(\"finishedTasksCount\");\n\n                Field<BigDecimal> failedTasks = tx.select(sum(ANSIBLE_TASK_STATS.FAILED_COUNT))\n                        .from(ANSIBLE_TASK_STATS)\n                        .where(ANSIBLE_TASK_STATS.INSTANCE_ID.eq(instanceId)\n                                .and(ANSIBLE_TASK_STATS.INSTANCE_CREATED_AT.eq(createdAt)\n                                        .and(ANSIBLE_TASK_STATS.PLAYBOOK_ID.eq(p.PLAYBOOK_ID))))\n                        .asField(\"failedTasksCount\");\n\n                Field<String> playbookStatus = tx.select(ANSIBLE_PLAYBOOK_RESULT.STATUS)\n                        .from(ANSIBLE_PLAYBOOK_RESULT)\n                        .where(ANSIBLE_PLAYBOOK_RESULT.INSTANCE_ID.eq(instanceId)\n                                .and(ANSIBLE_PLAYBOOK_RESULT.INSTANCE_CREATED_AT.eq(createdAt)\n                                        .and(ANSIBLE_PLAYBOOK_RESULT.PLAYBOOK_ID.eq(p.PLAYBOOK_ID))))\n                        .asField(\"playbookResult\");\n\n                return tx.select(p.PLAYBOOK_ID,\n                        p.NAME,\n                        p.STARTED_AT,\n                        p.HOST_COUNT,\n                        failedHosts,\n                        p.PLAY_COUNT,\n                        failedTasks,\n                        finishedTasks,\n                        playbookStatus,\n                        p.TOTAL_WORK,\n                        p.RETRY_NUM)\n                        .from(p)\n                        .where(p.INSTANCE_CREATED_AT.eq(createdAt)\n                                .and(p.INSTANCE_ID.eq(instanceId)))\n                        .fetch(AnsibleDao::toPlaybookEntry);\n            });\n        }\n\n        private static PlaybookEntry toPlaybookEntry(Record11<UUID, String, OffsetDateTime, Integer, Integer, Integer, BigDecimal, BigDecimal, String, Integer, Integer> r) {\n            long totalWork = r.value10().longValue();\n            long finishedCount = r.value8().longValue();\n            PlaybookStatus status = r.value9() == null ? PlaybookStatus.RUNNING : PlaybookStatus.valueOf(r.value9());\n            int progress;\n            if (status == PlaybookStatus.OK || status == PlaybookStatus.FAILED || totalWork == 0) {\n                progress = 100;\n            } else {\n                progress = (int) (100 * finishedCount / totalWork);\n            }\n\n            return ImmutablePlaybookEntry.builder()\n                    .id(r.value1())\n                    .name(r.value2())\n                    .startedAt(r.value3())\n                    .hostsCount(r.value4())\n                    .failedHostsCount(r.value5() == null ? 0 : r.value5())\n                    .playsCount(r.value6())\n                    .failedTasksCount(r.value7() == null ? 0 : r.value7().longValue())\n                    .progress(progress)\n                    .status(status)\n                    .retryNum(r.value11())\n                    .build();\n        }\n\n        private static AnsibleHostEntry toHostEntity(Record4<String, String, Long, String> r) {\n            return ImmutableAnsibleHostEntry.builder()\n                    .host(r.value1())\n                    .hostGroup(r.value2())\n                    .duration(r.value3())\n                    .status(AnsibleHostStatus.valueOf(r.value4()))\n                    .build();\n        }\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    @JsonInclude(JsonInclude.Include.NON_NULL)\n    @JsonSerialize(as = ImmutableAnsibleHostEntry.class)\n    @JsonDeserialize(as = ImmutableAnsibleHostEntry.class)\n    public interface AnsibleHostEntry {\n\n        String host();\n\n        String hostGroup();\n\n        AnsibleHostStatus status();\n\n        long duration();\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    @JsonInclude(JsonInclude.Include.NON_NULL)\n    @JsonSerialize(as = ImmutableAnsibleHostListResponse.class)\n    @JsonDeserialize(as = ImmutableAnsibleHostListResponse.class)\n    public interface AnsibleHostListResponse {\n\n        List<String> hostGroups();\n\n        List<AnsibleHostEntry> items();\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    @JsonInclude(JsonInclude.Include.NON_NULL)\n    @JsonSerialize(as = ImmutablePlaybookEntry.class)\n    @JsonDeserialize(as = ImmutablePlaybookEntry.class)\n    public interface PlaybookEntry {\n\n        UUID id();\n\n        String name();\n\n        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n        OffsetDateTime startedAt();\n\n        long hostsCount();\n\n        long failedHostsCount();\n\n        int playsCount();\n\n        long failedTasksCount();\n\n        int progress();\n\n        PlaybookStatus status();\n\n        @Nullable\n        Integer retryNum();\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    @JsonInclude(JsonInclude.Include.NON_NULL)\n    @JsonSerialize(as = ImmutablePlayInfo.class)\n    @JsonDeserialize(as = ImmutablePlayInfo.class)\n    public interface PlayInfo {\n\n        UUID playId();\n\n        String playName();\n\n        int playOrder();\n\n        long hostCount();\n\n        int taskCount();\n\n        Map<String, Long> taskStats();\n\n        long finishedTaskCount();\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    @JsonInclude(JsonInclude.Include.NON_NULL)\n    @JsonSerialize(as = ImmutableTaskInfo.class)\n    @JsonDeserialize(as = ImmutableTaskInfo.class)\n    public interface TaskInfo {\n\n        String taskName();\n\n        TaskType type();\n\n        long taskOrder();\n\n        long okCount();\n\n        long failedCount();\n\n        long unreachableCount();\n\n        long skippedCount();\n\n        long runningCount();\n    }\n\n    public enum AnsibleHostStatus {\n        RUNNING,\n        CHANGED,\n        FAILED,\n        OK,\n        SKIPPED,\n        UNREACHABLE\n    }\n\n    public enum PlaybookStatus {\n        RUNNING,\n        OK,\n        FAILED\n    }\n\n    public enum TaskType {\n        TASK,\n        SETUP,\n        HANDLER\n    }\n    \n    public enum SortField {\n        HOST,\n        DURATION,\n        STATUS,\n        HOST_GROUP\n    }\n    \n    public enum SortBy {\n        ASC,\n        DESC\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/ProcessEventEntry.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonRawValue;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.time.OffsetDateTime;\nimport java.util.UUID;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_NULL)\n@JsonSerialize(as = ImmutableProcessEventEntry.class)\n@JsonDeserialize(as = ImmutableProcessEventEntry.class)\npublic interface ProcessEventEntry extends Serializable {\n\n    UUID id();\n\n    String eventType();\n\n    @Nullable\n    @JsonRawValue\n    Object data();\n\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    OffsetDateTime eventDate();\n\n    static ImmutableProcessEventEntry.Builder builder() {\n        return ImmutableProcessEventEntry.builder();\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/TaskInfoProcessor.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.StringUtils;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.sdk.MapUtils;\nimport com.walmartlabs.concord.server.plugins.ansible.jooq.tables.AnsibleTaskStats;\nimport org.immutables.value.Value;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLException;\nimport java.time.OffsetDateTime;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\nimport static com.walmartlabs.concord.server.plugins.ansible.jooq.Tables.ANSIBLE_TASK_STATS;\nimport static org.jooq.impl.DSL.*;\n\n@Named\n@Singleton\npublic class TaskInfoProcessor implements EventProcessor {\n\n    private final Dao dao;\n\n    @Inject\n    public TaskInfoProcessor(Dao dao) {\n        this.dao = dao;\n    }\n\n    @Override\n    public void process(DSLContext tx, List<Event> events) {\n        Map<TaskInfoKey, TaskDetails> stats = new HashMap<>();\n\n        processTasksFromPlays(events, stats);\n\n        for (Event e : events) {\n            AnsibleEvent a = AnsibleEvent.from(e);\n            if (a == null) {\n                continue;\n            }\n\n            if (ignore(a)) {\n                continue;\n            }\n\n            TaskInfoKey key = ImmutableTaskInfoKey.builder()\n                    .instanceId(e.instanceId())\n                    .instanceCreatedAt(e.instanceCreatedAt())\n                    .playbookId(a.playbookId())\n                    .playId(a.getPlayId())\n                    .taskId(a.getTaskId())\n                    .build();\n\n            String taskName = Optional.ofNullable(a.getTaskName())\n                    .map(name -> StringUtils.abbreviate(name, ANSIBLE_TASK_STATS.TASK_NAME.getDataType().length()))\n                    .orElse(\"[task name not found]\");\n            String status = a.getStatus();\n            long taskOrder = a.isSetupTask() ? -1 : e.eventSeq();\n\n            stats.compute(key, (k, v) -> (v == null) ? TaskDetails.builder()\n                    .taskName(taskName)\n                    .stats(new TaskStats().inc(status))\n                    .order(taskOrder)\n                    .type(getTaskType(a))\n                    .build()\n                    : v.inc(status));\n        }\n\n        List<TaskInfoItem> items = stats.entrySet().stream()\n                .map(v -> ImmutableTaskInfoItem.of(v.getKey(), v.getValue()))\n                .collect(Collectors.toList());\n\n        dao.insert(tx, items);\n    }\n\n    private void processTasksFromPlays(List<Event> events, Map<TaskInfoKey, TaskDetails> stats) {\n        for (Event e : events) {\n            PlaybookInfoEvent p = PlaybookInfoEvent.from(e);\n            if (p == null) {\n                continue;\n            }\n\n            for (PlaybookInfoEvent.Play play : p.getPlays()) {\n                List<Map<String, Object>> tasks = play.getTasks();\n                for (int i = 0; i < tasks.size(); i++) {\n                    Map<String, Object> t = tasks.get(i);\n                    TaskInfoKey key = ImmutableTaskInfoKey.builder()\n                            .instanceId(e.instanceId())\n                            .instanceCreatedAt(e.instanceCreatedAt())\n                            .playbookId(p.playbookId())\n                            .playId(play.getId())\n                            .taskId(MapUtils.assertUUID(t, \"id\"))\n                            .build();\n\n                    stats.put(key, TaskDetails.builder()\n                            .taskName(MapUtils.assertString(t, \"task\"))\n                            .stats(new TaskStats())\n                            .order(i)\n                            .overwriteOrder(true)\n                            .type(\"TASK\")\n                            .build());\n                }\n            }\n        }\n    }\n\n    private static String getTaskType(AnsibleEvent e) {\n        if (e.isSetupTask()) {\n            return \"SETUP\";\n        } else if (e.isHandler()) {\n            return \"HANDLER\";\n        } else {\n            return \"TASK\";\n        }\n    }\n\n    private static boolean ignore(AnsibleEvent e) {\n        UUID taskId = e.getTaskId();\n        if (taskId == null) {\n            return true;\n        }\n        String status = e.getStatus();\n        if (status == null) {\n            return true;\n        }\n        return false;\n    }\n\n    @Named\n    public static class Dao extends AbstractDao {\n\n        @Inject\n        public Dao(@MainDB Configuration cfg) {\n            super(cfg);\n        }\n\n        public void insert(DSLContext tx, List<TaskInfoItem> items) {\n            if (items.isEmpty()) {\n                return;\n            }\n\n            DbUtils.upsert(tx, items, Dao::update, Dao::insert);\n        }\n\n        private static int[] update(DSLContext tx, Connection conn, List<TaskInfoItem> items) throws SQLException {\n            AnsibleTaskStats a = ANSIBLE_TASK_STATS;\n            String update = tx.update(a)\n                    .set(a.OK_COUNT, a.OK_COUNT.plus(value((Long) null)))\n                    .set(a.FAILED_COUNT, a.FAILED_COUNT.plus(value((Long) null)))\n                    .set(a.UNREACHABLE_COUNT, a.UNREACHABLE_COUNT.plus(value((Long) null)))\n                    .set(a.SKIPPED_COUNT, a.SKIPPED_COUNT.plus(value((Long) null)))\n                    .set(a.RUNNING_COUNT, a.RUNNING_COUNT.plus(value((Long) null)))\n                    .set(a.TASK_ORDER, when(value((Boolean) null).eq(inline(true)), value((Long) null)).otherwise(a.TASK_ORDER))\n                    .where(a.INSTANCE_ID.eq(value((UUID) null))\n                            .and(a.INSTANCE_CREATED_AT.eq(value((OffsetDateTime) null))\n                                    .and(a.PLAY_ID.eq(value((UUID) null))\n                                            .and(a.TASK_ID.eq(value((UUID) null))))))\n                    .getSQL();\n\n            try (PreparedStatement ps = conn.prepareStatement(update)) {\n                for (TaskInfoItem i : items) {\n                    ps.setInt(1, i.details().stats().okCount);\n                    ps.setInt(2, i.details().stats().failedCount);\n                    ps.setInt(3, i.details().stats().unreachableCount);\n                    ps.setInt(4, i.details().stats().skippedCount);\n                    ps.setInt(5, i.details().stats().runningCount);\n                    ps.setBoolean(6, i.details().overwriteOrder());\n                    ps.setLong(7, i.details().order());\n\n                    ps.setObject(8, i.key().instanceId());\n                    ps.setObject(9, i.key().instanceCreatedAt());\n                    ps.setObject(10, i.key().playId());\n                    ps.setObject(11, i.key().taskId());\n\n                    ps.addBatch();\n                }\n                return ps.executeBatch();\n            }\n        }\n\n        private static void insert(DSLContext tx, Connection conn, List<TaskInfoItem> items) throws SQLException {\n            AnsibleTaskStats a = ANSIBLE_TASK_STATS;\n\n            String insert = tx.insertInto(a)\n                    .columns(a.INSTANCE_ID,\n                            a.INSTANCE_CREATED_AT,\n                            a.PLAYBOOK_ID,\n                            a.PLAY_ID,\n                            a.TASK_ID,\n                            a.TASK_NAME,\n                            a.TASK_ORDER,\n                            a.OK_COUNT,\n                            a.FAILED_COUNT,\n                            a.UNREACHABLE_COUNT,\n                            a.SKIPPED_COUNT,\n                            a.RUNNING_COUNT,\n                            a.TASK_TYPE)\n                    .values(value((UUID) null), null, null, null, null, null, null, null, null, null, null, null, null)\n                    .getSQL();\n\n            try (PreparedStatement ps = conn.prepareStatement(insert)) {\n                for (TaskInfoItem i : items) {\n                    ps.setObject(1, i.key().instanceId());\n                    ps.setObject(2, i.key().instanceCreatedAt());\n                    ps.setObject(3, i.key().playbookId());\n                    ps.setObject(4, i.key().playId());\n                    ps.setObject(5, i.key().taskId());\n                    ps.setString(6, StringUtils.abbreviate(i.details().taskName(), a.TASK_NAME.getDataType().length()));\n                    ps.setLong(7, i.details().order());\n                    ps.setInt(8, i.details().stats().okCount);\n                    ps.setLong(9, i.details().stats().failedCount);\n                    ps.setLong(10, i.details().stats().unreachableCount);\n                    ps.setLong(11, i.details().stats().skippedCount);\n                    ps.setLong(12, i.details().stats().runningCount);\n                    ps.setString(13, i.details().type());\n\n                    ps.addBatch();\n                }\n                ps.executeBatch();\n            }\n        }\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface TaskInfoKey {\n\n        UUID instanceId();\n\n        OffsetDateTime instanceCreatedAt();\n\n        UUID playbookId();\n\n        UUID playId();\n\n        UUID taskId();\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface TaskInfoItem {\n\n        @Value.Parameter\n        TaskInfoKey key();\n\n        @Value.Parameter\n        TaskDetails details();\n    }\n\n    @Value.Immutable\n    @Value.Style(jdkOnly = true)\n    interface TaskDetails {\n\n        String taskName();\n\n        TaskStats stats();\n\n        String type();\n\n        long order();\n\n        @Value.Default\n        default boolean overwriteOrder() {\n            return false;\n        }\n\n        static ImmutableTaskDetails.Builder builder() {\n            return ImmutableTaskDetails.builder();\n        }\n\n        default TaskDetails inc(String status) {\n            return TaskDetails.builder().from(this)\n                    .stats(stats().inc(status))\n                    .build();\n        }\n    }\n\n    static class TaskStats {\n\n        private int okCount;\n        private int failedCount;\n        private int unreachableCount;\n        private int skippedCount;\n        private int runningCount;\n\n        public TaskStats inc(String status) {\n            switch (status) {\n                case \"RUNNING\": {\n                    runningCount++;\n                    return this;\n                }\n                case \"SKIPPED\": {\n                    skippedCount++;\n                    runningCount--;\n                    return this;\n                }\n                case \"OK\": {\n                    okCount++;\n                    runningCount--;\n                    return this;\n                }\n                case \"CHANGED\": {\n                    runningCount--;\n                    return this;\n                }\n                case \"UNREACHABLE\": {\n                    unreachableCount++;\n                    runningCount--;\n                    return this;\n                }\n                case \"FAILED\": {\n                    failedCount++;\n                    runningCount--;\n                    return this;\n                }\n            }\n            return this;\n        }\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/queue/AnsibleConfigurationConstants.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\n@Deprecated\npublic final class AnsibleConfigurationConstants {\n\n    public static final String GROUP_KEY = \"ansible\";\n    public static final String PRIVATE_KEYS = \"privateKeys\";\n    public static final String SECRET_KEY = \"secret\";\n    public static final String REPOSITORY_KEY = \"repository\";\n\n    private AnsibleConfigurationConstants() {\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/queue/InventoryProcessor.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.keys.AttachmentKey;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.process.CustomEnqueueProcessor;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\n/**\n * Support for external Ansible inventories.\n * <p>\n * This processor takes an inventory file from a request and stores it in a request's workspace\n * for {@code com.walmartlabs.concord.plugins.ansible.RunPlaybookTask2} to pick it up later.\n */\n@Deprecated\npublic class InventoryProcessor implements CustomEnqueueProcessor {\n\n    public static final AttachmentKey INVENTORY_FILE = AttachmentKey.register(\"inventory\");\n    public static final AttachmentKey DYNAMIC_INVENTORY_FILE = AttachmentKey.register(\"dynamicInventory\");\n\n    private static final String INVENTORY_FILE_NAME = \"_inventory\";\n    private static final String DYNAMIC_INVENTORY_FILE_NAME = \"_dynamicInventory\";\n\n    private final ProcessLogManager logManager;\n\n    @Inject\n    public InventoryProcessor(ProcessLogManager logManager) {\n        this.logManager = logManager;\n    }\n\n    @Override\n    public Payload handleAttachments(Payload payload) {\n        if (!copy(payload, INVENTORY_FILE, INVENTORY_FILE_NAME)) {\n            if (!copy(payload, DYNAMIC_INVENTORY_FILE, DYNAMIC_INVENTORY_FILE_NAME)) {\n                return payload;\n            }\n        }\n\n        deprecationWarning(payload.getProcessKey());\n\n        payload = payload.removeAttachment(INVENTORY_FILE)\n                .removeAttachment(DYNAMIC_INVENTORY_FILE);\n\n        return payload;\n    }\n\n    private boolean copy(Payload payload, AttachmentKey src, String dstName) {\n        ProcessKey processKey = payload.getProcessKey();\n        Path workspace = payload.getHeader(Payload.WORKSPACE_DIR);\n\n        Path p = payload.getAttachment(src);\n        if (p == null) {\n            return false;\n        }\n\n        Path dst = workspace.resolve(dstName);\n        try {\n            Files.copy(p, dst);\n        } catch (IOException e) {\n            logManager.error(processKey, \"Error while copying an inventory file: \" + p, e);\n            throw new ProcessException(processKey, \"Error while copying an inventory file: \" + p, e);\n        }\n\n        return true;\n    }\n\n    private void deprecationWarning(ProcessKey processKey) {\n        String msg = \".. WARNING ............................................................................\\n\" +\n                \" 'inventory' and 'dynamicInventory' request parameters are deprecated.\\n\" +\n                \" Please use 'inventoryFile' and 'dynamicInventoryFile' parameters of the Ansible task.\\n\" +\n                \".......................................................................................\\n\";\n        logManager.log(processKey, msg);\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/java/com/walmartlabs/concord/server/plugins/ansible/queue/PrivateKeyProcessor.java",
    "content": "package com.walmartlabs.concord.server.plugins.ansible.queue;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.secret.KeyPair;\nimport com.walmartlabs.concord.server.org.OrganizationManager;\nimport com.walmartlabs.concord.server.org.project.ProjectDao;\nimport com.walmartlabs.concord.server.org.secret.SecretManager;\nimport com.walmartlabs.concord.server.process.Payload;\nimport com.walmartlabs.concord.server.process.ProcessException;\nimport com.walmartlabs.concord.server.process.logs.ProcessLogManager;\nimport com.walmartlabs.concord.server.process.pipelines.processors.RepositoryProcessor;\nimport com.walmartlabs.concord.server.process.pipelines.processors.RepositoryProcessor.RepositoryInfo;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.process.CustomEnqueueProcessor;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardOpenOption;\nimport java.util.Collection;\nimport java.util.Map;\nimport java.util.UUID;\n\n@Deprecated\npublic class PrivateKeyProcessor implements CustomEnqueueProcessor {\n\n    private static final Logger log = LoggerFactory.getLogger(PrivateKeyProcessor.class);\n\n    private static final String PRIVATE_KEY_FILE_NAME = \"_privateKey\";\n\n    private final ProcessLogManager logManager;\n    private final SecretManager secretManager;\n    private final ProjectDao projectDao;\n\n    @Inject\n    public PrivateKeyProcessor(ProcessLogManager logManager, SecretManager secretManager, ProjectDao projectDao) {\n        this.logManager = logManager;\n        this.secretManager = secretManager;\n        this.projectDao = projectDao;\n    }\n\n    @Override\n    @SuppressWarnings(\"unchecked\")\n    public Payload handleState(Payload payload) {\n        ProcessKey processKey = payload.getProcessKey();\n        Map<String, Object> cfg = payload.getHeader(Payload.CONFIGURATION);\n\n        Map<String, Object> ansibleCfg = (Map<String, Object>) cfg.get(AnsibleConfigurationConstants.GROUP_KEY);\n        if (ansibleCfg == null) {\n            return payload;\n        }\n\n        Collection<Map<String, Object>> keys = (Collection<Map<String, Object>>) ansibleCfg.get(AnsibleConfigurationConstants.PRIVATE_KEYS);\n        if (keys == null) {\n            return payload;\n        }\n\n        deprecationWarning(processKey);\n\n        String secret = findMatchingSecret(payload, keys);\n        if (secret == null) {\n            logManager.error(processKey, \"No matching secrets found\");\n            throw new ProcessException(processKey, \"No matching secrets found\");\n        }\n\n        UUID orgId = getOrgId(payload);\n        KeyPair keyPair = secretManager.getKeyPair(SecretManager.AccessScope.internal(), orgId, secret, null);\n        if (keyPair == null) {\n            logManager.error(processKey, \"Secret not found: \" + secret);\n            throw new ProcessException(processKey, \"Secret not found: \" + secret);\n        }\n\n        if (keyPair.getPrivateKey() == null) {\n            logManager.error(processKey, \"Private key not found: \" + secret);\n            throw new ProcessException(processKey, \"Private key not found: \" + secret);\n        }\n\n        Path workspace = payload.getHeader(Payload.WORKSPACE_DIR);\n        Path dst = workspace.resolve(PRIVATE_KEY_FILE_NAME);\n\n        try {\n            Files.write(dst, keyPair.getPrivateKey(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);\n        } catch (IOException e) {\n            logManager.error(processKey, \"Error while copying a private key: \" + dst, e);\n            throw new ProcessException(processKey, \"Error while copying a private key: \" + dst, e);\n        }\n\n        log.info(\"process ['{}'] -> done\", processKey);\n        return payload;\n    }\n\n    private UUID getOrgId(Payload p) {\n        UUID projectId = p.getHeader(Payload.PROJECT_ID);\n\n        UUID orgId = null;\n        if (projectId != null) {\n            orgId = projectDao.getOrgId(projectId);\n        }\n\n        if (orgId == null) {\n            orgId = OrganizationManager.DEFAULT_ORG_ID;\n        }\n\n        return orgId;\n    }\n\n    private void deprecationWarning(ProcessKey processKey) {\n        String msg = \".. WARNING ............................................................................\\n\" +\n                \" 'configuration.ansible.privateKeys' is deprecated.\\n\" +\n                \" Please use 'privateKey' parameter of the Ansible task.\\n\" +\n                \".......................................................................................\\n\";\n        logManager.log(processKey, msg);\n    }\n\n    private static String findMatchingSecret(Payload payload, Collection<Map<String, Object>> items) {\n        RepositoryInfo info = payload.getHeader(RepositoryProcessor.REPOSITORY_INFO_KEY);\n        String repoName = info != null ? info.getName() : \"\";\n\n        for (Map<String, Object> i : items) {\n            String secret = (String) i.get(AnsibleConfigurationConstants.SECRET_KEY);\n            if (secret == null || secret.trim().isEmpty()) {\n                continue;\n            }\n\n            String repo = (String) i.get(AnsibleConfigurationConstants.REPOSITORY_KEY);\n            if (repo != null && repoName.matches(repo)) {\n                return secret;\n            }\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "server/plugins/ansible/impl/src/main/resources/openapi-ansible-config.yaml",
    "content": "openAPI:\n  info:\n    version: '2.0'\n    title: Concord Ansible API\n    description: Concord Ansible API\n"
  },
  {
    "path": "server/plugins/ansible/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.server.plugins</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <groupId>com.walmartlabs.concord.server.plugins.ansible</groupId>\n    <artifactId>parent</artifactId>\n    <packaging>pom</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <modules>\n        <module>db</module>\n        <module>impl</module>\n        <module>client2</module>\n    </modules>\n</project>\n\n"
  },
  {
    "path": "server/plugins/kafka-event-sink/README.md",
    "content": "# Kafka Sink\n\nA Kafka-based sink for process events, process logs and audit log events.\n\n## Usage\n\nMust be included into the server's [dist](../../dist) module.\n\n## Configuration\n\n```\n# concord-server.conf\nconcord-server {\n    eventSink {\n        kafka {\n            enabled = true\n            bootstrapServers = \"localhost:9092\"\n            processEventsTopic = \"process_events\"\n            processLogsTopic = \"process_logs\"\n            auditLogTopic = \"audit_log\"\n        }\n    }\n}\n```"
  },
  {
    "path": "server/plugins/kafka-event-sink/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.server.plugins</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>kafka-event-sink</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server-sdk</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.kafka</groupId>\n            <artifactId>kafka-clients</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-config</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n\n"
  },
  {
    "path": "server/plugins/kafka-event-sink/src/main/java/com/walmartlabs/concord/server/plugins/eventsink/kafka/KafkaConnector.java",
    "content": "package com.walmartlabs.concord.server.plugins.eventsink.kafka;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.BackgroundTask;\nimport org.apache.kafka.clients.producer.KafkaProducer;\nimport org.apache.kafka.clients.producer.ProducerConfig;\nimport org.apache.kafka.clients.producer.ProducerRecord;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.net.InetAddress;\nimport java.net.UnknownHostException;\nimport java.util.Properties;\n\npublic class KafkaConnector implements BackgroundTask {\n\n    private static final Logger log = LoggerFactory.getLogger(KafkaConnector.class);\n\n    private final KafkaEventSinkConfiguration cfg;\n    private final boolean enabled;\n\n    private KafkaProducer<String, String> producer;\n\n    @Inject\n    public KafkaConnector(KafkaEventSinkConfiguration cfg) {\n        this.cfg = cfg;\n        this.enabled = cfg.getEnabled() != null ? cfg.getEnabled() : false;\n    }\n\n    @Override\n    public void start() {\n        if (!enabled) {\n            return;\n        }\n\n        String clientId = cfg.getClientId();\n        if (clientId == null) {\n            try {\n                clientId = InetAddress.getLocalHost().getHostName();\n            } catch (UnknownHostException e) {\n                throw new RuntimeException(\"Error while starting the Kafka connector: \" + e.getMessage(), e);\n            }\n        }\n\n        String bootstrapServers = cfg.getBootstrapServers();\n        if (bootstrapServers == null) {\n            throw new IllegalArgumentException(\"bootstrapServers value is required\");\n        }\n\n        try {\n            Properties props = new Properties();\n            props.put(ProducerConfig.CLIENT_ID_CONFIG, clientId);\n            props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);\n            props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, \"org.apache.kafka.common.serialization.StringSerializer\");\n            props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, \"org.apache.kafka.common.serialization.StringSerializer\");\n            producer = new KafkaProducer<>(props);\n        } catch (Exception e) {\n            log.warn(\"start -> error creating a Kafka producer: {}\", e.getMessage(), e);\n            throw new RuntimeException(e);\n        }\n\n        log.info(\"started the Kafka connector using {}...\", bootstrapServers);\n    }\n\n    @Override\n    public void stop() {\n        if (!enabled) {\n            return;\n        }\n\n        if (producer != null) {\n            producer.close();\n        }\n    }\n\n    public void send(String topic, String key, String value) {\n        if (!enabled || producer == null || topic == null) {\n            return;\n        }\n\n        producer.send(new ProducerRecord<>(topic, key, value));\n    }\n}\n"
  },
  {
    "path": "server/plugins/kafka-event-sink/src/main/java/com/walmartlabs/concord/server/plugins/eventsink/kafka/KafkaEventSink.java",
    "content": "package com.walmartlabs.concord.server.plugins.eventsink.kafka;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.server.sdk.audit.AuditEvent;\nimport com.walmartlabs.concord.server.sdk.audit.AuditLogListener;\nimport com.walmartlabs.concord.server.sdk.events.ProcessEvent;\nimport com.walmartlabs.concord.server.sdk.events.ProcessEventListener;\nimport com.walmartlabs.concord.server.sdk.log.ProcessLogEntry;\nimport com.walmartlabs.concord.server.sdk.log.ProcessLogListener;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.util.Collections;\nimport java.util.List;\n\npublic class KafkaEventSink implements ProcessEventListener, ProcessLogListener, AuditLogListener {\n\n    private static final Logger log = LoggerFactory.getLogger(KafkaEventSink.class);\n\n    private final KafkaEventSinkConfiguration cfg;\n    private final KafkaConnector connector;\n\n    private final ObjectMapper objectMapper = new ObjectMapper();\n\n    @Inject\n    public KafkaEventSink(KafkaEventSinkConfiguration cfg, KafkaConnector connector) {\n        this.cfg = cfg;\n        this.connector = connector;\n    }\n\n    @Override\n    public void onEvent(AuditEvent event) {\n        String k = Long.toString(event.entrySeq());\n        try {\n            String v = objectMapper.writeValueAsString(event);\n            connector.send(cfg.getAuditLogTopic(), k, v);\n        } catch (Exception e) {\n            log.warn(\"onEvent [{}] -> error while sending an audit log event: {}\", k, e.getMessage());\n        }\n    }\n\n    @Override\n    public void onEvents(List<ProcessEvent> events) {\n        for (ProcessEvent ev : events) {\n            String k = Long.toString(ev.eventSeq());\n            try {\n                String v = objectMapper.writeValueAsString(ev);\n                connector.send(cfg.getProcessEventsTopic(), k, v);\n            } catch (Exception e) {\n                log.warn(\"onEvents [{}] -> error while sending an event: {}\", k, e.getMessage());\n            }\n        }\n    }\n\n    @Override\n    public void onAppend(ProcessLogEntry entry) {\n        String k = entry.processKey().getInstanceId().toString();\n        try {\n            String v = objectMapper.writeValueAsString(Collections.singletonMap(\"msg\", new String(entry.msg())));\n            connector.send(cfg.getProcessLogsTopic(), k, v);\n        } catch (Exception e) {\n            log.warn(\"onAppend [{}] -> error while sending a log entry: {}\", k, e.getMessage());\n        }\n    }\n\n    @Override\n    public String toString() {\n        return \"KafkaEventSink -> \" + cfg.getBootstrapServers();\n    }\n}\n"
  },
  {
    "path": "server/plugins/kafka-event-sink/src/main/java/com/walmartlabs/concord/server/plugins/eventsink/kafka/KafkaEventSinkConfiguration.java",
    "content": "package com.walmartlabs.concord.server.plugins.eventsink.kafka;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport java.io.Serializable;\n\npublic class KafkaEventSinkConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Nullable\n    @Config(\"eventSink.kafka.enabled\")\n    private Boolean enabled;\n\n    @Inject\n    @Nullable\n    @Config(\"eventSink.kafka.clientId\")\n    private String clientId;\n\n    @Inject\n    @Nullable\n    @Config(\"eventSink.kafka.bootstrapServers\")\n    private String bootstrapServers;\n\n    @Inject\n    @Nullable\n    @Config(\"eventSink.kafka.processEventsTopic\")\n    private String processEventsTopic;\n\n    @Inject\n    @Nullable\n    @Config(\"eventSink.kafka.processLogsTopic\")\n    private String processLogsTopic;\n\n    @Inject\n    @Nullable\n    @Config(\"eventSink.kafka.auditLogTopic\")\n    private String auditLogTopic;\n\n    @Nullable\n    public Boolean getEnabled() {\n        return enabled;\n    }\n\n    @Nullable\n    public String getClientId() {\n        return clientId;\n    }\n\n    @Nullable\n    public String getBootstrapServers() {\n        return bootstrapServers;\n    }\n\n    @Nullable\n    public String getProcessEventsTopic() {\n        return processEventsTopic;\n    }\n\n    @Nullable\n    public String getProcessLogsTopic() {\n        return processLogsTopic;\n    }\n\n    @Nullable\n    public String getAuditLogTopic() {\n        return auditLogTopic;\n    }\n}\n"
  },
  {
    "path": "server/plugins/kafka-event-sink/src/main/java/com/walmartlabs/concord/server/plugins/eventsink/kafka/KafkaModule.java",
    "content": "package com.walmartlabs.concord.server.plugins.eventsink.kafka;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.walmartlabs.concord.server.sdk.BackgroundTask;\nimport com.walmartlabs.concord.server.sdk.audit.AuditLogListener;\nimport com.walmartlabs.concord.server.sdk.events.ProcessEventListener;\nimport com.walmartlabs.concord.server.sdk.log.ProcessLogListener;\n\nimport javax.inject.Named;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\n\n@Named\npublic class KafkaModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        binder.bind(KafkaEventSinkConfiguration.class).in(SINGLETON);\n\n        binder.bind(KafkaEventSink.class).in(SINGLETON);\n        newSetBinder(binder, ProcessEventListener.class).addBinding().to(KafkaEventSink.class);\n        newSetBinder(binder, ProcessLogListener.class).addBinding().to(KafkaEventSink.class);\n        newSetBinder(binder, AuditLogListener.class).addBinding().to(KafkaEventSink.class);\n\n        binder.bind(KafkaConnector.class).in(SINGLETON);\n        newSetBinder(binder, BackgroundTask.class).addBinding().to(KafkaConnector.class);\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/client2/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.server.plugins.noderoster</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-noderoster-plugin-client2</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-client2</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n        </dependency>\n\n        <!-- JDK9+ compatibility -->\n        <dependency>\n            <groupId>javax.annotation</groupId>\n            <artifactId>javax.annotation-api</artifactId>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.openapitools</groupId>\n                <artifactId>openapi-generator-maven-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>generate</goal>\n                        </goals>\n                        <configuration>\n                            <inputSpec>${project.basedir}/../impl/target/classes/com/walmartlabs/concord/server/plugins/noderoster/swagger/swagger.yaml</inputSpec>\n                            <generatorName>java</generatorName>\n                            <apiPackage>com.walmartlabs.concord.client2</apiPackage>\n                            <modelPackage>com.walmartlabs.concord.client2</modelPackage>\n                            <packageName>com.walmartlabs.concord.client2</packageName>\n                            <invokerPackage>com.walmartlabs.concord.client2</invokerPackage>\n                            <configOptions>\n                                <sourceFolder>src/gen/java/main</sourceFolder>\n                                <dateLibrary>java8</dateLibrary>\n                                <serializableModel>true</serializableModel>\n                                <openApiNullable>false</openApiNullable>\n                                <supportUrlQuery>false</supportUrlQuery>\n                            </configOptions>\n                            <skipValidateSpec>false</skipValidateSpec>\n                            <library>native</library>\n                            <generateApiTests>false</generateApiTests>\n                            <generateModelTests>false</generateModelTests>\n                            <generateApiDocumentation>false</generateApiDocumentation>\n                            <generateModelDocumentation>false</generateModelDocumentation>\n                            <generateSupportingFiles>true</generateSupportingFiles>\n                            <supportingFilesToGenerate>ApiClient.java,ApiResponse.java,ApiException.java,Pair.java</supportingFilesToGenerate>\n                            <templateDirectory>../../../../client2/src/main/template</templateDirectory>\n                            <cleanupOutput>true</cleanupOutput>\n                            <typeMappings>string+binary=InputStream</typeMappings>\n                            <importMappings>InputStream=java.io.InputStream</importMappings>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "server/plugins/noderoster/db/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.server.plugins.noderoster</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-noderoster-plugin-db</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <properties>\n        <db.baseDir>${project.build.directory}/db</db.baseDir>\n        <db.changeLogPath>com/walmartlabs/concord/server/plugins/noderoster/db/liquibase.xml</db.changeLogPath>\n        <db.host>localhost</db.host>\n        <db.username>postgres</db.username>\n        <db.password>q1</db.password>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server-db</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.postgresql</groupId>\n            <artifactId>postgresql</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.inject</groupId>\n            <artifactId>guice</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.zaxxer</groupId>\n            <artifactId>HikariCP</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.sisu</groupId>\n            <artifactId>org.eclipse.sisu.inject</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.jooq</groupId>\n            <artifactId>jooq</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.liquibase</groupId>\n            <artifactId>liquibase-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.dropwizard.metrics</groupId>\n            <artifactId>metrics-core</artifactId>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>build-helper-maven-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <id>reserve-ports</id>\n                        <phase>generate-sources</phase>\n                        <goals>\n                            <goal>reserve-network-port</goal>\n                        </goals>\n                        <configuration>\n                            <portNames>\n                                <portName>db.port</portName>\n                            </portNames>\n                        </configuration>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>io.fabric8</groupId>\n                <artifactId>docker-maven-plugin</artifactId>\n                <extensions>true</extensions>\n                <executions>\n                    <execution>\n                        <id>start</id>\n                        <phase>generate-sources</phase>\n                        <goals>\n                            <goal>start</goal>\n                        </goals>\n                    </execution>\n                    <execution>\n                        <id>stop</id>\n                        <phase>prepare-package</phase>\n                        <goals>\n                            <goal>stop</goal>\n                        </goals>\n                    </execution>\n                </executions>\n                <configuration>\n                    <images>\n                        <image>\n                            <name>${db.image}</name>\n                            <alias>db</alias>\n                            <run>\n                                <ports>\n                                    <port>${db.port}:5432</port>\n                                </ports>\n                                <env>\n                                    <POSTGRES_PASSWORD>${db.password}</POSTGRES_PASSWORD>\n                                </env>\n                                <wait>\n                                    <log>(?s).*ready for start up.*ready to accept connections.*</log>\n                                    <time>60000</time>\n                                </wait>\n                            </run>\n                        </image>\n                    </images>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.liquibase</groupId>\n                <artifactId>liquibase-maven-plugin</artifactId>\n                <version>${liquibase.version}</version>\n                <executions>\n                    <execution>\n                        <phase>generate-sources</phase>\n                        <goals>\n                            <goal>update</goal>\n                        </goals>\n                    </execution>\n                </executions>\n                <configuration>\n                    <changeLogFile>src/main/resources/${db.changeLogPath}</changeLogFile>\n                    <driver>org.postgresql.Driver</driver>\n                    <url>jdbc:postgresql://${db.host}:${db.port}/postgres</url>\n                    <username>${db.username}</username>\n                    <password>${db.password}</password>\n                    <promptOnNonLocalDatabase>false</promptOnNonLocalDatabase>\n                    <expressionVariables>\n                        <superuserAvailable>true</superuserAvailable>\n                        <createExtensionAvailable>true</createExtensionAvailable>\n                    </expressionVariables>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.jooq</groupId>\n                <artifactId>jooq-codegen-maven</artifactId>\n                <executions>\n                    <execution>\n                        <phase>generate-sources</phase>\n                        <goals>\n                            <goal>generate</goal>\n                        </goals>\n                    </execution>\n                </executions>\n                <configuration>\n                    <logging>WARN</logging>\n                    <jdbc>\n                        <driver>org.postgresql.Driver</driver>\n                        <url>jdbc:postgresql://${db.host}:${db.port}/postgres</url>\n                        <user>${db.username}</user>\n                        <password>${db.password}</password>\n                    </jdbc>\n                    <generator>\n                        <database>\n                            <name>org.jooq.meta.postgres.PostgresDatabase</name>\n                            <inputSchema>public</inputSchema>\n                            <includes>.*</includes>\n                            <excludes>DATABASECHANGELOG.*</excludes>\n                        </database>\n                        <target>\n                            <packageName>com.walmartlabs.concord.server.plugins.noderoster.jooq</packageName>\n                            <directory>target/generated-sources/jooq</directory>\n                        </target>\n                        <generate>\n                            <deprecationOnUnknownTypes>false</deprecationOnUnknownTypes>\n                        </generate>\n                    </generator>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.codehaus.mojo</groupId>\n                <artifactId>cobertura-maven-plugin</artifactId>\n                <configuration>\n                    <skip>true</skip>\n                    <check />\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n    <profiles>\n        <profile>\n            <id>looper</id>\n            <activation>\n                <activeByDefault>false</activeByDefault>\n            </activation>\n            <properties>\n                <db.host>${env.DB_CONTAINER_NAME}</db.host>\n                <db.port>5432</db.port>\n            </properties>\n            <build>\n                <plugins>\n                    <plugin>\n                        <groupId>org.codehaus.mojo</groupId>\n                        <artifactId>build-helper-maven-plugin</artifactId>\n                        <executions>\n                            <execution>\n                                <id>reserve-ports</id>\n                                <phase>none</phase>\n                            </execution>\n                        </executions>\n                    </plugin>\n                    <plugin>\n                        <groupId>io.fabric8</groupId>\n                        <artifactId>docker-maven-plugin</artifactId>\n                        <extensions>true</extensions>\n                        <configuration>\n                            <skip>true</skip>\n                        </configuration>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n    </profiles>\n</project>"
  },
  {
    "path": "server/plugins/noderoster/db/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/db/DatabaseModule.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster.db;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.MetricRegistry;\nimport com.google.inject.AbstractModule;\nimport com.google.inject.Provides;\nimport com.walmartlabs.concord.db.DataSourceUtils;\nimport com.walmartlabs.concord.db.DatabaseChangeLogProvider;\nimport com.walmartlabs.concord.db.DatabaseConfiguration;\nimport com.walmartlabs.concord.db.MainDB;\nimport org.jooq.Configuration;\n\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport javax.sql.DataSource;\n\n@Named\npublic class DatabaseModule extends AbstractModule {\n\n    @Override\n    protected void configure() {\n        bind(DatabaseChangeLogProvider.class).annotatedWith(NodeRosterDB.class).to(NodeRosterDBChangeLogProvider.class);\n    }\n\n    @Provides\n    @NodeRosterDB\n    @Singleton\n    public DataSource dataSource(@NodeRosterDB DatabaseConfiguration cfg,\n                                 MetricRegistry metricRegistry,\n                                 @NodeRosterDB DatabaseChangeLogProvider changeLogProvider,\n                                 @MainDB DatabaseConfiguration mainCfg) {\n\n        DataSourceUtils.migrateDb(mainCfg, changeLogProvider);\n        return DataSourceUtils.createDataSource(cfg, \"noderoster\", cfg.username(), cfg.password(), metricRegistry);\n    }\n\n    @Provides\n    @NodeRosterDB\n    @Singleton\n    public Configuration jooqConfiguration(@NodeRosterDB DataSource ds) {\n        return DataSourceUtils.createJooqConfiguration(ds);\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/db/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/db/NodeRosterDB.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster.db;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.inject.Qualifier;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n@Retention(RetentionPolicy.RUNTIME)\n@Qualifier\npublic @interface NodeRosterDB {\n}\n"
  },
  {
    "path": "server/plugins/noderoster/db/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/db/NodeRosterDBChangeLogProvider.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster.db;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.DatabaseChangeLogProvider;\n\n@NodeRosterDB\npublic class NodeRosterDBChangeLogProvider implements DatabaseChangeLogProvider {\n\n    @Override\n    public String getChangeLogPath() {\n        return \"com/walmartlabs/concord/server/plugins/noderoster/db/liquibase.xml\";\n    }\n\n    @Override\n    public String toString() {\n        return \"noderoster-db\";\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/db/src/main/resources/com/walmartlabs/concord/server/plugins/noderoster/db/liquibase.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<databaseChangeLog\n        xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.3.xsd\">\n\n    <changeSet id=\"noderoster-10000\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"createExtensionAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"noderoster-10020\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"NODE_ROSTER_HOSTS\">\n            <column name=\"HOST_ID\" type=\"uuid\" defaultValueComputed=\"uuid_generate_v1()\">\n                <constraints nullable=\"false\" primaryKey=\"true\"/>\n            </column>\n            <column name=\"NORMALIZED_HOSTNAME\" type=\"varchar(2048)\">\n                <constraints nullable=\"false\" unique=\"true\"/>\n            </column>\n            <column name=\"CREATED_AT\" type=\"timestamp\" defaultValueComputed=\"current_timestamp\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"noderoster-10030\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"NODE_ROSTER_PROCESS_HOSTS\">\n            <column name=\"INSTANCE_ID\" type=\"uuid\">\n                <constraints nullable=\"false\" primaryKey=\"true\"/>\n            </column>\n            <column name=\"INSTANCE_CREATED_AT\" type=\"timestamp\">\n                <constraints nullable=\"false\" primaryKey=\"true\"/>\n            </column>\n            <column name=\"HOST_ID\" type=\"uuid\">\n                <constraints nullable=\"false\" primaryKey=\"true\"/>\n            </column>\n            <column name=\"INITIATOR\" type=\"varchar(64)\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"INITIATOR_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n            <column name=\"PROJECT_ID\" type=\"uuid\">\n                <constraints nullable=\"true\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"noderoster-10040\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"NODE_ROSTER_HOST_FACTS\">\n            <column name=\"INSTANCE_ID\" type=\"uuid\">\n                <constraints nullable=\"false\" primaryKey=\"true\"/>\n            </column>\n            <column name=\"INSTANCE_CREATED_AT\" type=\"timestamp\">\n                <constraints nullable=\"false\" primaryKey=\"true\"/>\n            </column>\n            <column name=\"HOST_ID\" type=\"uuid\">\n                <constraints nullable=\"false\" primaryKey=\"true\"/>\n            </column>\n            <column name=\"FACTS\" type=\"jsonb\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"noderoster-10050\" author=\"ybrigo@gmail.com\">\n        <createTable tableName=\"NODE_ROSTER_HOST_ARTIFACTS\">\n            <column name=\"INSTANCE_ID\" type=\"uuid\">\n                <constraints nullable=\"false\" primaryKey=\"true\"/>\n            </column>\n            <column name=\"INSTANCE_CREATED_AT\" type=\"timestamp\">\n                <constraints nullable=\"false\" primaryKey=\"true\"/>\n            </column>\n            <column name=\"HOST_ID\" type=\"uuid\">\n                <constraints nullable=\"false\" primaryKey=\"true\"/>\n            </column>\n            <column name=\"ARTIFACT_URL\" type=\"varchar(1024)\">\n                <constraints nullable=\"false\"/>\n            </column>\n        </createTable>\n    </changeSet>\n\n    <changeSet id=\"noderoster-10060\" author=\"ibodrov@gmail.com\">\n        <addColumn tableName=\"NODE_ROSTER_HOST_FACTS\">\n            <!-- a process can produce multiple gather_facts events for each host -->\n            <column name=\"SEQ_ID\" type=\"serial\"/>\n        </addColumn>\n\n        <dropPrimaryKey tableName=\"NODE_ROSTER_HOST_FACTS\"/>\n\n        <createIndex tableName=\"NODE_ROSTER_HOST_FACTS\" indexName=\"IDX_NR_HF_HOST\">\n            <column name=\"HOST_ID\"/>\n        </createIndex>\n    </changeSet>\n\n    <changeSet id=\"noderoster-10070\" author=\"ybrigo@gmail.com\" runInTransaction=\"false\">\n        <!-- skip if the table is partitioned -->\n        <preConditions onFail=\"MARK_RAN\">\n            <sqlCheck expectedResult=\"0\">\n                select count(*)\n                from pg_inherits\n                join pg_class parent ON pg_inherits.inhparent = parent.oid\n                join pg_class child ON pg_inherits.inhrelid = child.oid\n                join pg_namespace nmsp_parent ON nmsp_parent.oid = parent.relnamespace\n                join pg_namespace nmsp_child ON nmsp_child.oid = child.relnamespace\n                where parent.relname = 'node_roster_host_artifacts'\n            </sqlCheck>\n        </preConditions>\n\n        <dropPrimaryKey tableName=\"NODE_ROSTER_HOST_ARTIFACTS\"/>\n        <createIndex tableName=\"NODE_ROSTER_HOST_ARTIFACTS\" indexName=\"IDX_NR_HA_HOST\">\n            <column name=\"HOST_ID\"/>\n        </createIndex>\n    </changeSet>\n\n    <!-- see server/db/src/main/resources/com/walmartlabs/concord/server/db/v1.58.0.xml -->\n    <changeSet id=\"noderoster-1580000\" author=\"ibodrov@gmail.com\">\n        <createProcedure dbms=\"postgresql\">\n            create or replace function ts_to_tstz(t text)\n                returns bool as $$\n            declare\n                v_cnt numeric;\n            begin\n                v_cnt := 0;\n\n                update pg_attribute\n                    set atttypid = 'timestamp with time zone'::regtype\n                from pg_class\n                where attrelid = pg_class.oid\n                    and relnamespace = current_schema()::regnamespace\n                    and atttypid = 'timestamp'::regtype\n                    and relname ilike t;\n\n                get diagnostics v_cnt = row_count;\n                if v_cnt = 0 then\n                    raise warning 'Relation not found (or is already converted): %', t;\n                end if;\n\n                update pg_index\n                    set indclass = array_to_string(array_replace(indclass::oid[], 3128::oid, 3127::oid), ' ')::oidvector\n                from pg_class\n                where indrelid = pg_class.oid\n                    and relnamespace = current_schema()::regnamespace\n                    and indclass::oid[] @> ARRAY[3128::oid]\n                    and relname ilike t;\n\n                return v_cnt > 0;\n            end;\n            $$ language plpgsql\n        </createProcedure>\n    </changeSet>\n\n    <!-- non-partitioned tables -->\n\n    <!-- NODE_ROSTER_HOSTS -->\n    <changeSet id=\"noderoster-1580100\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('node_roster_hosts')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"noderoster-1580100-a\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <sql>\n            alter table NODE_ROSTER_HOSTS\n                alter column CREATED_AT type timestamptz using CREATED_AT at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- tables that might be partitioned -->\n\n    <!-- NODE_ROSTER_HOST_ARTIFACTS -->\n    <changeSet id=\"noderoster-1580110\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('node_roster_host_artifacts%')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"noderoster-1580110-a\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <sql>\n            alter table NODE_ROSTER_HOST_ARTIFACTS\n                alter column INSTANCE_CREATED_AT type timestamptz using INSTANCE_CREATED_AT at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- NODE_ROSTER_HOST_FACTS -->\n    <changeSet id=\"noderoster-1580120\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('node_roster_host_facts%')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"noderoster-1580120-a\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <sql>\n            alter table NODE_ROSTER_HOST_FACTS\n                alter column INSTANCE_CREATED_AT type timestamptz using INSTANCE_CREATED_AT at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- NODE_ROSTER_PROCESS_HOSTS -->\n    <changeSet id=\"noderoster-1580130\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('node_roster_process_hosts%')\n        </sql>\n    </changeSet>\n\n    <changeSet id=\"noderoster-1580130-a\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"false\"/>\n        </preConditions>\n\n        <sql>\n            alter table NODE_ROSTER_PROCESS_HOSTS\n                alter column INSTANCE_CREATED_AT type timestamptz using INSTANCE_CREATED_AT at time zone 'UTC'\n        </sql>\n    </changeSet>\n\n    <!-- indices -->\n\n    <changeSet id=\"noderoster-1580200\" author=\"ibodrov@gmail.com\">\n        <preConditions onFail=\"MARK_RAN\">\n            <changeLogPropertyDefined property=\"superuserAvailable\" value=\"true\"/>\n        </preConditions>\n\n        <sql>\n            select ts_to_tstz('pk_node_roster_process_hosts')\n        </sql>\n    </changeSet>\n</databaseChangeLog>\n"
  },
  {
    "path": "server/plugins/noderoster/impl/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.server.plugins.noderoster</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-noderoster-plugin</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-config</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.walmartlabs.concord.server.plugins.noderoster</groupId>\n            <artifactId>concord-noderoster-plugin-db</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server-db</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server-sdk</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.guava</groupId>\n            <artifactId>guava</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.inject</groupId>\n            <artifactId>guice</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.inject.extensions</groupId>\n            <artifactId>guice-servlet</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>javax.validation</groupId>\n            <artifactId>validation-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.hibernate.validator</groupId>\n            <artifactId>hibernate-validator</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.jboss.resteasy</groupId>\n            <artifactId>resteasy-jackson2-provider</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.jaxrs</groupId>\n            <artifactId>jackson-jaxrs-base</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.module</groupId>\n            <artifactId>jackson-module-jaxb-annotations</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.jboss.resteasy</groupId>\n            <artifactId>resteasy-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.jboss.resteasy</groupId>\n            <artifactId>resteasy-jaxb-provider</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>javax.servlet</groupId>\n            <artifactId>javax.servlet-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty</groupId>\n            <artifactId>jetty-server</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty.ee8</groupId>\n            <artifactId>jetty-ee8-servlets</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty</groupId>\n            <artifactId>jetty-util</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty</groupId>\n            <artifactId>jetty-rewrite</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.sisu</groupId>\n            <artifactId>org.eclipse.sisu.inject</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.jboss.resteasy</groupId>\n            <artifactId>resteasy-multipart-provider</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.jboss.spec.javax.ws.rs</groupId>\n            <artifactId>jboss-jaxrs-api_2.1_spec</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.jooq</groupId>\n            <artifactId>jooq</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>ch.qos.logback</groupId>\n            <artifactId>logback-classic</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.postgresql</groupId>\n            <artifactId>postgresql</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.glassfish</groupId>\n            <artifactId>javax.el</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.dataformat</groupId>\n            <artifactId>jackson-dataformat-yaml</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.typesafe</groupId>\n            <artifactId>config</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.swagger.core.v3</groupId>\n            <artifactId>swagger-annotations</artifactId>\n            <version>2.2.15</version>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>io.dropwizard.metrics</groupId>\n            <artifactId>metrics-core</artifactId>\n        </dependency>\n\n        <!-- immutables -->\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-guava</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jdk8</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.errorprone</groupId>\n            <artifactId>error_prone_annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n            <plugin>\n                <groupId>io.swagger.core.v3</groupId>\n                <artifactId>swagger-maven-plugin</artifactId>\n                <configuration>\n                    <outputFileName>swagger</outputFileName>\n                    <outputPath>${project.build.directory}/classes/com/walmartlabs/concord/server/plugins/noderoster/swagger</outputPath>\n                    <outputFormat>YAML</outputFormat>\n                    <readAllResources>false</readAllResources>\n                    <resourcePackages>\n                        <package>com.walmartlabs.concord.server.plugins.noderoster</package>\n                    </resourcePackages>\n                    <configurationFilePath>${project.build.directory}/classes/openapi-noderoster-config.yaml</configurationFilePath>\n                </configuration>\n                <executions>\n                    <execution>\n                        <phase>compile</phase>\n                        <goals>\n                            <goal>resolve</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/ArtifactEntry.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonSerialize(as = ImmutableArtifactEntry.class)\n@JsonDeserialize(as = ImmutableArtifactEntry.class)\npublic interface ArtifactEntry {\n\n    @Value.Parameter\n    String url();\n\n    @Value.Parameter\n    UUID processInstanceId();\n\n    static ArtifactEntry of(String url, UUID processInstanceId) {\n        return ImmutableArtifactEntry.of(url, processInstanceId);\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/ArtifactsResource.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.plugins.noderoster.dao.ArtifactsDao;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.UUID;\n\n@Named\n@Singleton\n@Path(\"/api/v1/noderoster/artifacts\")\n@Tag(name = \"Node Roster Artifacts\")\npublic class ArtifactsResource implements Resource {\n\n    private static final String DEFAULT_LIMIT = \"30\";\n    private static final String DEFAULT_OFFSET = \"0\";\n\n    private final HostManager hosts;\n    private final ArtifactsDao artifactsDao;\n\n    @Inject\n    public ArtifactsResource(HostManager hosts, ArtifactsDao artifactsDao) {\n        this.hosts = hosts;\n        this.artifactsDao = artifactsDao;\n    }\n\n    @GET\n    @Path(\"/\")\n    @Operation(description = \"List artifacts deployed on a host\", operationId = \"listHostArtifacts\")\n    @Produces(MediaType.APPLICATION_JSON)\n    public List<ArtifactEntry> list(@QueryParam(\"hostId\") UUID hostId,\n                                    @QueryParam(\"hostName\") String hostName,\n                                    @QueryParam(\"filter\") String filter,\n                                    @QueryParam(\"limit\") @DefaultValue(DEFAULT_LIMIT) int limit,\n                                    @QueryParam(\"offset\") @DefaultValue(DEFAULT_OFFSET) int offset) {\n\n        if (hostName == null && hostId == null) {\n            throw new ValidationErrorsException(\"A 'hostName' or 'hostId' value is required\");\n        }\n\n        assertLimitAndOffset(limit, offset);\n\n        UUID effectiveHostId = hosts.getId(hostId, hostName);\n\n        if (effectiveHostId == null) {\n            return Collections.emptyList();\n        }\n\n        return artifactsDao.getArtifacts(effectiveHostId, limit, offset, filter);\n    }\n\n    private static void assertLimitAndOffset(int limit, int offset) {\n        if (limit <= 0) {\n            throw new ValidationErrorsException(\"'limit' must be a positive number\");\n        }\n\n        if (offset < 0) {\n            throw new ValidationErrorsException(\"'offset' must be equal or more than zero\");\n        }\n    }\n}"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/FactsResource.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.plugins.noderoster.dao.HostsDao;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.media.Content;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport io.swagger.v3.oas.annotations.responses.ApiResponse;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport javax.ws.rs.GET;\nimport javax.ws.rs.Path;\nimport javax.ws.rs.Produces;\nimport javax.ws.rs.QueryParam;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport java.util.UUID;\n\n@Named\n@Singleton\n@Path(\"/api/v1/noderoster/facts\")\n@Tag(name = \"Node Roster Facts\")\npublic class FactsResource implements Resource {\n\n    private final HostManager hostManager;\n    private final HostsDao hostsDao;\n\n    @Inject\n    public FactsResource(HostManager hostManager, HostsDao hostsDao) {\n        this.hostManager = hostManager;\n        this.hostsDao = hostsDao;\n    }\n\n    @GET\n    @Path(\"/last\")\n    @Operation(description = \"Get last known Ansible facts for a host\")\n    @Produces(MediaType.APPLICATION_JSON)\n    @ApiResponse(description = \"Facts content\",\n            content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(type = \"object\"))\n    )\n    public Response getFacts(@QueryParam(\"hostId\") UUID hostId,\n                             @QueryParam(\"hostName\") String hostName) {\n\n        if (hostName == null && hostId == null) {\n            throw new ValidationErrorsException(\"A 'hostName' or 'hostId' value is required\");\n        }\n\n        UUID effectiveHostId = hostManager.getId(hostId, hostName);\n        if (effectiveHostId == null) {\n            return Response.ok().entity(\"{}\").build();\n        }\n\n        String result = hostsDao.getLastFacts(effectiveHostId);\n        if (result == null) {\n            return Response.ok().entity(\"{}\").build();\n        }\n\n        // return the raw JSON string, no need to parse it just to serialize it back\n        return Response.ok()\n                .entity(result)\n                .build();\n    }\n}"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/HostEntry.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.time.OffsetDateTime;\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonSerialize(as = ImmutableHostEntry.class)\n@JsonDeserialize(as = ImmutableHostEntry.class)\npublic interface HostEntry {\n\n    UUID id();\n\n    String name();\n\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    OffsetDateTime createdAt();\n\n    @Nullable\n    String artifactUrl();\n\n    static ImmutableHostEntry.Builder builder() {\n        return ImmutableHostEntry.builder();\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/HostFilter.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.util.UUID;\n\n@Value.Immutable\npublic interface HostFilter {\n\n    @Nullable\n    String host();\n\n    @Nullable\n    String artifact();\n\n    @Nullable\n    UUID processInstanceId();\n\n    static ImmutableHostFilter.Builder builder() {\n        return ImmutableHostFilter.builder();\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/HostManager.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.cache.CacheBuilder;\nimport com.google.common.cache.CacheLoader;\nimport com.google.common.cache.LoadingCache;\nimport com.walmartlabs.concord.server.plugins.noderoster.cfg.NodeRosterEventsConfiguration;\nimport com.walmartlabs.concord.server.plugins.noderoster.dao.HostsDao;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\n\nimport javax.annotation.Nonnull;\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport java.util.Optional;\nimport java.util.UUID;\n\n@Named\n@Singleton\npublic class HostManager {\n\n    private final HostsDao dao;\n    private final HostNormalizer hostNormalizer;\n\n    private final LoadingCache<String, Optional<UUID>> hostCache;\n\n    @Inject\n    public HostManager(HostsDao dao, HostNormalizer hostNormalizer, NodeRosterEventsConfiguration cfg) {\n        this.dao = dao;\n        this.hostNormalizer = hostNormalizer;\n\n        this.hostCache = CacheBuilder.newBuilder()\n                .expireAfterAccess(cfg.getHostCacheDuration())\n                .maximumSize(cfg.getHostCacheSize())\n                .recordStats()\n                .build(new CacheLoader<>() {\n                    @Override\n                    public @Nonnull Optional<UUID> load(@Nonnull String host) {\n                        UUID id = findHost(host);\n                        return Optional.ofNullable(id);\n                    }\n                });\n    }\n\n    @WithTimer\n    public UUID getId(UUID hostId, String host) {\n        if (hostId != null) {\n            return hostId;\n        }\n\n        try {\n            return hostCache.get(host)\n                    .orElse(null);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @WithTimer\n    public UUID getOrCreate(String host) {\n        try {\n            Optional<UUID> knownHost = hostCache.get(host);\n\n            if (knownHost.isPresent()) {\n                return knownHost.get();\n            }\n\n            // insert in db and update cache\n            UUID id = dao.insert(hostNormalizer.normalize(host));\n            hostCache.put(host, Optional.of(id));\n\n            return id;\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private UUID findHost(String host) {\n        String normalizedHost = hostNormalizer.normalize(host);\n        return dao.getId(normalizedHost);\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/HostNormalizer.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.StringUtils;\n\nimport javax.inject.Named;\n\nimport static com.walmartlabs.concord.server.plugins.noderoster.jooq.tables.NodeRosterHosts.NODE_ROSTER_HOSTS;\n\n@Named\npublic class HostNormalizer {\n\n    public String normalize(String host) {\n        // TODO implement, add metrics\n        return StringUtils.abbreviate(host.toLowerCase(), NODE_ROSTER_HOSTS.NORMALIZED_HOSTNAME.getDataType().length());\n    }\n}"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/HostsDataInclude.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic enum HostsDataInclude {\n\n    ARTIFACTS(\"artifacts\");\n\n    private final String value;\n\n    HostsDataInclude(String value) {\n        this.value = value;\n    }\n\n    public static HostsDataInclude fromString(String str) {\n        for (HostsDataInclude v : values()) {\n            if (v.value.equalsIgnoreCase(str)) {\n                return v;\n            }\n        }\n        throw new IllegalArgumentException(str + \" not found\");\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/HostsResource.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.plugins.noderoster.dao.HostsDao;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.UUID;\n\n@Named\n@Singleton\n@Path(\"/api/v1/noderoster/hosts\")\n@Tag(name = \"Node Roster Hosts\")\npublic class HostsResource implements Resource {\n\n    private static final String DEFAULT_LIMIT = \"30\";\n    private static final String DEFAULT_OFFSET = \"0\";\n\n    private final HostsDao hostsDao;\n\n    @Inject\n    public HostsResource(HostsDao hostsDao) {\n        this.hostsDao = hostsDao;\n    }\n\n    @GET\n    @Path(\"/\")\n    @Operation(description = \"List all known hosts\", operationId = \"listKnownHosts\")\n    @Produces(MediaType.APPLICATION_JSON)\n    public List<HostEntry> list(@QueryParam(\"host\") String host,\n                                @QueryParam(\"artifact\") String artifact,\n                                @QueryParam(\"processInstanceId\") UUID processInstanceId,\n                                @QueryParam(\"include\") Set<HostsDataInclude> includes,\n                                @QueryParam(\"limit\") @DefaultValue(DEFAULT_LIMIT) int limit,\n                                @QueryParam(\"offset\") @DefaultValue(DEFAULT_OFFSET) int offset) {\n\n        assertLimitAndOffset(limit, offset);\n\n        HostFilter filter = HostFilter.builder()\n                .host(host)\n                .artifact(artifact)\n                .processInstanceId(processInstanceId)\n                .build();\n\n        return hostsDao.list(filter, includes, limit, offset);\n    }\n\n    @GET\n    @Path(\"/{hostId}\")\n    @Operation(description = \"Get a host\", operationId = \"getHost\")\n    @Produces(MediaType.APPLICATION_JSON)\n    public HostEntry get(@PathParam(\"hostId\") UUID hostId) {\n        return hostsDao.get(hostId);\n    }\n\n    private static void assertLimitAndOffset(int limit, int offset) {\n        if (limit <= 0) {\n            throw new ValidationErrorsException(\"'limit' must be a positive number\");\n        }\n\n        if (offset < 0) {\n            throw new ValidationErrorsException(\"'offset' must be equal or more than zero\");\n        }\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/NodeRosterModule.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.walmartlabs.concord.server.plugins.noderoster.processor.AnsibleEventsProcessor;\nimport com.walmartlabs.concord.server.sdk.ScheduledTask;\n\nimport javax.inject.Named;\n\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\n\n@Named\npublic class NodeRosterModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        newSetBinder(binder, ScheduledTask.class).addBinding().to(AnsibleEventsProcessor.class);\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/ProcessEntry.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonInclude.Include;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.time.OffsetDateTime;\nimport java.util.UUID;\n\n@Value.Immutable\n@JsonInclude(Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableProcessEntry.class)\n@JsonDeserialize(as = ImmutableProcessEntry.class)\npublic interface ProcessEntry extends Serializable {\n\n    UUID instanceId();\n\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    OffsetDateTime createdAt();\n\n    @Nullable\n    UUID projectId();\n\n    @Nullable\n    String projectName();\n\n    @Nullable\n    UUID initiatorId();\n\n    @Nullable\n    String initiator();\n\n    static ImmutableProcessEntry.Builder builder() {\n        return ImmutableProcessEntry.builder();\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/ProcessesResource.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.plugins.noderoster.dao.HostsDao;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.sdk.validation.ValidationErrorsException;\nimport io.swagger.v3.oas.annotations.Operation;\nimport io.swagger.v3.oas.annotations.tags.Tag;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.MediaType;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.UUID;\n\n@Named\n@Singleton\n@Path(\"/api/v1/noderoster/processes\")\n@Tag(name = \"Node Roster Processes\")\npublic class ProcessesResource implements Resource {\n\n    private static final String DEFAULT_LIMIT = \"30\";\n    private static final String DEFAULT_OFFSET = \"0\";\n\n    private final HostManager hosts;\n    private final HostsDao hostsDao;\n\n    @Inject\n    public ProcessesResource(HostManager hosts, HostsDao hostsDao) {\n        this.hosts = hosts;\n        this.hostsDao = hostsDao;\n    }\n\n    @GET\n    @Path(\"/\")\n    @Operation(description = \"Get all known hosts\", operationId = \"listHosts\")\n    @Produces(MediaType.APPLICATION_JSON)\n    public List<ProcessEntry> list(@QueryParam(\"hostId\") UUID hostId,\n                                   @QueryParam(\"hostName\") String hostName,\n                                   @QueryParam(\"limit\") @DefaultValue(DEFAULT_LIMIT) int limit,\n                                   @QueryParam(\"offset\") @DefaultValue(DEFAULT_OFFSET) int offset) {\n\n        if (hostName == null && hostId == null) {\n            throw new ValidationErrorsException(\"A 'hostName' or 'hostId' value is required\");\n        }\n\n        assertLimitAndOffset(limit, offset);\n\n        UUID effectiveHostId = hosts.getId(hostId, hostName);\n\n        if (effectiveHostId == null) {\n            return Collections.emptyList();\n        }\n\n        return hostsDao.listProcesses(effectiveHostId, limit, offset);\n    }\n\n    private static void assertLimitAndOffset(int limit, int offset) {\n        if (limit <= 0) {\n            throw new ValidationErrorsException(\"'limit' must be a positive number\");\n        }\n\n        if (offset < 0) {\n            throw new ValidationErrorsException(\"'offset' must be equal or more than zero\");\n        }\n    }\n}"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/cfg/NodeRosterDBConfiguration.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.DatabaseConfiguration;\nimport com.walmartlabs.concord.server.plugins.noderoster.db.NodeRosterDB;\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport java.time.Duration;\n\n@Named\n@NodeRosterDB\n@Singleton\npublic class NodeRosterDBConfiguration implements DatabaseConfiguration {\n\n    @Inject\n    @Config(\"noderoster.db.url\")\n    private String url;\n\n    @Inject\n    @Config(\"noderoster.db.username\")\n    private String username;\n\n    @Inject\n    @Config(\"noderoster.db.password\")\n    private String password;\n\n    @Inject\n    @Config(\"noderoster.db.maxPoolSize\")\n    private int maxPoolSize;\n\n    @Override\n    public String url() {\n        return url;\n    }\n\n    @Override\n    public String username() {\n        return username;\n    }\n\n    @Override\n    public String password() {\n        return password;\n    }\n\n    @Override\n    public int maxPoolSize() {\n        return maxPoolSize;\n    }\n\n    @Override\n    public Duration maxLifetime() {\n        return Duration.ofSeconds(0);\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/cfg/NodeRosterEventsConfiguration.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster.cfg;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport java.io.Serializable;\nimport java.time.Duration;\nimport java.time.Instant;\n\n@Named\n@Singleton\npublic class NodeRosterEventsConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"noderoster.events.period\")\n    private Duration period;\n\n    @Inject\n    @Config(\"noderoster.events.fetchLimit\")\n    private int fetchLimit;\n\n    @Inject\n    @Config(\"noderoster.events.hostCacheSize\")\n    private long hostCacheSize;\n\n    @Inject\n    @Config(\"noderoster.events.hostCacheDuration\")\n    private Duration hostCacheDuration;\n\n    private final Instant startTimestamp;\n\n    @Inject\n    public NodeRosterEventsConfiguration(@Config(\"noderoster.events.startTimestamp\") @Nullable String startTimestamp) {\n        this.startTimestamp = startTimestamp != null ? Instant.parse(startTimestamp) : null;\n    }\n\n    public Duration getPeriod() {\n        return period;\n    }\n\n    public int getFetchLimit() {\n        return fetchLimit;\n    }\n\n    public long getHostCacheSize() {\n        return hostCacheSize;\n    }\n\n    public Duration getHostCacheDuration() {\n        return hostCacheDuration;\n    }\n\n    @Nullable\n    public Instant getStartTimestamp() {\n        return startTimestamp;\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/dao/ArtifactsDao.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster.dao;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.server.plugins.noderoster.ArtifactEntry;\nimport com.walmartlabs.concord.server.plugins.noderoster.db.NodeRosterDB;\nimport com.walmartlabs.concord.server.plugins.noderoster.jooq.tables.NodeRosterHostArtifacts;\nimport org.jooq.Configuration;\nimport org.jooq.Record2;\nimport org.jooq.SelectJoinStep;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport java.util.List;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.plugins.noderoster.jooq.tables.NodeRosterHostArtifacts.NODE_ROSTER_HOST_ARTIFACTS;\n\n@Named\n@Singleton\npublic class ArtifactsDao extends AbstractDao {\n\n    @Inject\n    public ArtifactsDao(@NodeRosterDB Configuration cfg) {\n        super(cfg);\n    }\n\n    public List<ArtifactEntry> getArtifacts(UUID hostId, int limit, int offset, String filter) {\n        return txResult(tx -> {\n            NodeRosterHostArtifacts a = NODE_ROSTER_HOST_ARTIFACTS.as(\"a\");\n\n            SelectJoinStep<Record2<String, UUID>> q = tx.select(a.ARTIFACT_URL, a.INSTANCE_ID)\n                    .from(a);\n\n            if (filter != null) {\n                q.where(a.ARTIFACT_URL.likeRegex(filter));\n            }\n\n            return q.where(a.HOST_ID.eq(hostId))\n                    .orderBy(a.INSTANCE_CREATED_AT.desc())\n                    .limit(limit)\n                    .offset(offset)\n                    .fetch(r -> ArtifactEntry.of(r.get(a.ARTIFACT_URL), r.get(a.INSTANCE_ID)));\n        });\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/dao/HostsDao.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster.dao;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.server.plugins.noderoster.HostEntry;\nimport com.walmartlabs.concord.server.plugins.noderoster.HostFilter;\nimport com.walmartlabs.concord.server.plugins.noderoster.HostsDataInclude;\nimport com.walmartlabs.concord.server.plugins.noderoster.ProcessEntry;\nimport com.walmartlabs.concord.server.plugins.noderoster.db.NodeRosterDB;\nimport com.walmartlabs.concord.server.plugins.noderoster.jooq.tables.NodeRosterHostArtifacts;\nimport com.walmartlabs.concord.server.plugins.noderoster.jooq.tables.NodeRosterHosts;\nimport com.walmartlabs.concord.server.plugins.noderoster.jooq.tables.NodeRosterProcessHosts;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.ProcessKeyCache;\nimport org.jooq.Record;\nimport org.jooq.*;\n\nimport javax.inject.Inject;\nimport java.time.OffsetDateTime;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.PROJECTS;\nimport static com.walmartlabs.concord.server.plugins.noderoster.jooq.tables.NodeRosterHostArtifacts.NODE_ROSTER_HOST_ARTIFACTS;\nimport static com.walmartlabs.concord.server.plugins.noderoster.jooq.tables.NodeRosterHostFacts.NODE_ROSTER_HOST_FACTS;\nimport static com.walmartlabs.concord.server.plugins.noderoster.jooq.tables.NodeRosterHosts.NODE_ROSTER_HOSTS;\nimport static com.walmartlabs.concord.server.plugins.noderoster.jooq.tables.NodeRosterProcessHosts.NODE_ROSTER_PROCESS_HOSTS;\nimport static org.jooq.impl.DSL.select;\n\npublic class HostsDao extends AbstractDao {\n\n    private final ProcessKeyCache processKeyCache;\n\n    @Inject\n    public HostsDao(@NodeRosterDB Configuration cfg, ProcessKeyCache processKeyCache) {\n        super(cfg);\n        this.processKeyCache = processKeyCache;\n    }\n\n    public HostEntry get(UUID id) {\n        return txResult(tx -> tx.select(NODE_ROSTER_HOSTS.HOST_ID, NODE_ROSTER_HOSTS.NORMALIZED_HOSTNAME, NODE_ROSTER_HOSTS.CREATED_AT)\n                .from(NODE_ROSTER_HOSTS)\n                .where(NODE_ROSTER_HOSTS.HOST_ID.eq(id))\n                .fetchOne(HostsDao::toHostEntry));\n    }\n\n    public List<HostEntry> list(HostFilter filter, Set<HostsDataInclude> includes, int limit, int offset) {\n        NodeRosterHosts nrh = NODE_ROSTER_HOSTS.as(\"nrh\");\n        NodeRosterProcessHosts nrph = NODE_ROSTER_PROCESS_HOSTS.as(\"nrph\");\n        NodeRosterHostArtifacts nrha = NODE_ROSTER_HOST_ARTIFACTS.as(\"nrha\");\n\n        SelectQuery<Record> query = dsl().selectQuery();\n        query.addSelect(nrh.HOST_ID, nrh.NORMALIZED_HOSTNAME, nrh.CREATED_AT);\n        query.addFrom(nrh);\n\n        if (filter.host() != null) {\n            query.addConditions(nrh.NORMALIZED_HOSTNAME.containsIgnoreCase(filter.host()));\n        }\n\n        ProcessKey key = null;\n        if (filter.processInstanceId() != null) {\n            key = processKeyCache.get(filter.processInstanceId());\n            if (key == null) {\n                return Collections.emptyList();\n            }\n\n            query.addFrom(nrph);\n\n            query.addConditions(nrh.HOST_ID.eq(nrph.HOST_ID)\n                    .and(nrph.INSTANCE_ID.eq(key.getInstanceId())\n                            .and(nrph.INSTANCE_CREATED_AT.eq(key.getCreatedAt()))));\n        }\n\n        if ((includes != null && includes.contains(HostsDataInclude.ARTIFACTS))\n                || filter.artifact() != null) {\n            query.addSelect(nrha.ARTIFACT_URL);\n            query.addFrom(nrha);\n            query.addConditions(nrh.HOST_ID.eq(nrha.HOST_ID));\n        }\n\n        if (filter.artifact() != null) {\n            query.addConditions(nrha.ARTIFACT_URL.likeRegex(filter.artifact()));\n\n            if (key != null) {\n                query.addConditions(nrha.INSTANCE_ID.eq(key.getInstanceId())\n                        .and(nrha.INSTANCE_CREATED_AT.eq(key.getCreatedAt())));\n            }\n        }\n\n        query.addOrderBy(nrh.CREATED_AT.desc());\n        query.addLimit(limit);\n        query.addOffset(offset);\n\n        return query.fetch(HostsDao::toHostEntry);\n    }\n\n    public List<ProcessEntry> listProcesses(UUID hostId, int limit, int offset) {\n        NodeRosterProcessHosts h = NODE_ROSTER_PROCESS_HOSTS.as(\"h\");\n\n        Field<String> projectNameField = select(PROJECTS.PROJECT_NAME)\n                .from(PROJECTS)\n                .where(PROJECTS.PROJECT_ID.eq(h.PROJECT_ID)).asField();\n\n        SelectJoinStep<Record6<UUID, OffsetDateTime, UUID, String, UUID, String>> q = dsl().select(\n                h.INSTANCE_ID, h.INSTANCE_CREATED_AT,\n                h.INITIATOR_ID, h.INITIATOR,\n                h.PROJECT_ID, projectNameField)\n                .from(h);\n\n        return q.where(h.HOST_ID.eq(hostId))\n                .orderBy(h.INSTANCE_CREATED_AT.desc())\n                .limit(limit)\n                .offset(offset)\n                .fetch(HostsDao::toProcessEntry);\n    }\n\n    public UUID getId(String host) {\n        return txResult(tx -> tx.select(NODE_ROSTER_HOSTS.HOST_ID)\n                .from(NODE_ROSTER_HOSTS)\n                .where(NODE_ROSTER_HOSTS.NORMALIZED_HOSTNAME.eq(host))\n                .fetchOne(NODE_ROSTER_HOSTS.HOST_ID));\n    }\n\n    public UUID insert(String host) {\n        return txResult(tx -> tx.insertInto(NODE_ROSTER_HOSTS, NODE_ROSTER_HOSTS.NORMALIZED_HOSTNAME)\n                .values(host)\n                .returning(NODE_ROSTER_HOSTS.HOST_ID)\n                .fetchOne()\n                .getHostId());\n    }\n\n    public String getLastFacts(UUID hostId) {\n        return txResult(tx -> tx.select(NODE_ROSTER_HOST_FACTS.FACTS.cast(String.class))\n                .from(NODE_ROSTER_HOST_FACTS)\n                .where(NODE_ROSTER_HOST_FACTS.HOST_ID.eq(hostId))\n                .orderBy(NODE_ROSTER_HOST_FACTS.SEQ_ID.desc())\n                .limit(1)\n                .fetchOne(Record1::value1));\n    }\n\n    private static <E> E getOrNull(Record r, Field<E> field) {\n        Field<?> f = r.field(field);\n        if (f == null) {\n            return null;\n        }\n\n        return r.get(field);\n    }\n\n    private static HostEntry toHostEntry(Record r) {\n        return HostEntry.builder()\n                .id(r.getValue(NODE_ROSTER_HOSTS.HOST_ID))\n                .name(r.getValue(NODE_ROSTER_HOSTS.NORMALIZED_HOSTNAME))\n                .createdAt(r.getValue(NODE_ROSTER_HOSTS.CREATED_AT))\n                .artifactUrl(getOrNull(r, NODE_ROSTER_HOST_ARTIFACTS.ARTIFACT_URL))\n                .build();\n    }\n\n    private static ProcessEntry toProcessEntry(Record6<UUID, OffsetDateTime, UUID, String, UUID, String> r) {\n        return ProcessEntry.builder()\n                .instanceId(r.value1())\n                .createdAt(r.value2())\n                .initiatorId(r.value3())\n                .initiator(r.value4())\n                .projectId(r.value5())\n                .projectName(r.value6())\n                .build();\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/processor/AbstractEventProcessor.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster.processor;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.ScheduledTask;\nimport org.jooq.DSLContext;\n\nimport java.util.List;\n\npublic abstract class AbstractEventProcessor<E extends AbstractEventProcessor.Event> implements ScheduledTask {\n\n    private final String processorName;\n    private final EventMarkerDao eventMarkerDao;\n    private final int fetchLimit;\n\n    protected AbstractEventProcessor(String processorName, EventMarkerDao eventMarkerDao, int fetchLimit) {\n        this.processorName = processorName;\n        this.eventMarkerDao = eventMarkerDao;\n        this.fetchLimit = fetchLimit;\n    }\n\n    @Override\n    public void performTask() {\n        int processedEvents;\n        do {\n            EventMarkerDao.EventMarker m = eventMarkerDao.get(processorName);\n            processedEvents = process(m, fetchLimit);\n        } while (processedEvents >= fetchLimit);\n    }\n\n    private int process(EventMarkerDao.EventMarker m, int fetchLimit) {\n        return eventMarkerDao.txResult(tx -> {\n            List<E> events = processEvents(tx, m, fetchLimit);\n            if (events.isEmpty()) {\n                eventMarkerDao.update(tx, processorName, m.maxEventSeq());\n                return 0;\n            }\n\n            E lastEvent = events.get(events.size() - 1);\n            eventMarkerDao.update(tx, processorName, lastEvent.eventSeq());\n\n            return events.size();\n        });\n    }\n\n    protected abstract List<E> processEvents(DSLContext tx, EventMarkerDao.EventMarker m, int fetchLimit);\n\n    public interface Event {\n        long eventSeq();\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/processor/AnsibleEvent.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster.processor;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.time.OffsetDateTime;\nimport java.util.UUID;\n\n@Value.Immutable\npublic interface AnsibleEvent extends AbstractEventProcessor.Event {\n\n    UUID id();\n\n    @Override\n    long eventSeq();\n\n    UUID instanceId();\n\n    OffsetDateTime instanceCreatedAt();\n\n    OffsetDateTime eventDate();\n\n    EventData data();\n\n    @Nullable\n    String initiator();\n\n    @Nullable\n    UUID initiatorId();\n\n    @Nullable\n    UUID projectId();\n\n    static ImmutableAnsibleEvent.Builder builder() {\n        return ImmutableAnsibleEvent.builder();\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/processor/AnsibleEventsProcessor.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster.processor;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.jooq.tables.ProcessEvents;\nimport com.walmartlabs.concord.server.jooq.tables.ProcessQueue;\nimport com.walmartlabs.concord.server.plugins.noderoster.cfg.NodeRosterEventsConfiguration;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport org.jooq.*;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.io.IOException;\nimport java.time.Instant;\nimport java.time.OffsetDateTime;\nimport java.time.ZoneId;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.*;\nimport static com.walmartlabs.concord.server.plugins.noderoster.processor.EventMarkerDao.EventMarker;\nimport static org.jooq.impl.DSL.function;\n\n/**\n * Scans the process_events table for new Ansible events and hands\n * the data off to individual processors.\n */\npublic class AnsibleEventsProcessor extends AbstractEventProcessor<AnsibleEvent> {\n\n    private static final String NAME = \"noderoster/ansible-events-processor\";\n\n    private final EventsDao eventsDao;\n    private final List<Processor> processors;\n\n    private final long interval;\n    private final OffsetDateTime startTimestamp;\n\n    @Inject\n    public AnsibleEventsProcessor(NodeRosterEventsConfiguration eventsCfg,\n                                  EventMarkerDao eventMarkerDao,\n                                  EventsDao eventsDao,\n                                  Map<String, Processor> processors) {\n\n        super(NAME, eventMarkerDao, eventsCfg.getFetchLimit());\n\n        this.eventsDao = eventsDao;\n        this.processors = new ArrayList<>(processors.values());\n\n        this.interval = eventsCfg.getPeriod().getSeconds();\n\n        Instant startTimestamp = eventsCfg.getStartTimestamp();\n        this.startTimestamp = startTimestamp != null ? OffsetDateTime.ofInstant(startTimestamp, ZoneId.systemDefault()) : null;\n    }\n\n    @Override\n    public String getId() {\n        return \"noderoster/ansible-events-processor\";\n    }\n\n    @Override\n    public long getIntervalInSec() {\n        return interval;\n    }\n\n    @Override\n    protected List<AnsibleEvent> processEvents(DSLContext tx, EventMarker marker, int fetchLimit) {\n        List<AnsibleEvent> events = eventsDao.list(marker, startTimestamp, fetchLimit);\n        if (events.isEmpty()) {\n            return Collections.emptyList();\n        }\n\n        for (Processor p : processors) {\n            p.process(events);\n        }\n        return events;\n    }\n\n    @Named\n    public static class EventsDao extends AbstractDao {\n\n        private final ObjectMapper objectMapper;\n\n        @Inject\n        public EventsDao(@MainDB Configuration cfg) {\n            super(cfg);\n\n            this.objectMapper = new ObjectMapper();\n        }\n\n        @WithTimer\n        public List<AnsibleEvent> list(EventMarker marker, OffsetDateTime startTimestamp, int count) {\n            return txResult(tx -> {\n                ProcessQueue pq = PROCESS_QUEUE.as(\"pq\");\n                ProcessEvents pe = PROCESS_EVENTS.as(\"pe\");\n                Field<Object> eventData = function(\"jsonb_strip_nulls\", Object.class, pe.EVENT_DATA);\n\n                SelectConditionStep<Record6<UUID, Long, UUID, OffsetDateTime, OffsetDateTime, Object>> selectEventsSubQuery =\n                        tx.select(pe.EVENT_ID,\n                                pe.EVENT_SEQ,\n                                pe.INSTANCE_ID,\n                                pe.INSTANCE_CREATED_AT,\n                                pe.EVENT_DATE,\n                                eventData.as(\"event_data\"))\n                            .from(pe)\n                            .where(pe.EVENT_TYPE.eq(\"ANSIBLE\"))\n                            .and(pe.EVENT_SEQ.greaterThan(marker.eventSeq()));\n\n                if (startTimestamp != null) {\n                    selectEventsSubQuery.and(pe.INSTANCE_CREATED_AT.greaterOrEqual(startTimestamp));\n                }\n\n                Table<Record6<UUID, Long, UUID, OffsetDateTime, OffsetDateTime, Object>> eventsAlias = selectEventsSubQuery\n                        .orderBy(pe.EVENT_SEQ)\n                        .limit(count)\n                        .asTable(\"events_batch\");\n\n                SelectOnConditionStep<Record9<UUID, Long, UUID, OffsetDateTime, OffsetDateTime, Object, String, UUID, UUID>> s = tx.select(\n                                eventsAlias.field(\"event_id\", UUID.class),\n                                eventsAlias.field(\"event_seq\", Long.class),\n                                eventsAlias.field(\"instance_id\", UUID.class),\n                                eventsAlias.field(\"instance_created_at\", OffsetDateTime.class),\n                                eventsAlias.field(\"event_date\", OffsetDateTime.class),\n                                eventsAlias.field(\"event_data\", Object.class),\n                                USERS.USERNAME,\n                                pq.INITIATOR_ID,\n                                pq.PROJECT_ID\n                        )\n                        .from(eventsAlias)\n                        .leftOuterJoin(pq)\n                                .on(pq.INSTANCE_ID.eq(eventsAlias.field(\"instance_id\", UUID.class)))\n                        .leftOuterJoin(USERS).on(USERS.USER_ID.eq(pq.INITIATOR_ID));\n\n                return s.fetch(this::toEntity);\n            });\n        }\n\n        private AnsibleEvent toEntity(Record9<UUID, Long, UUID, OffsetDateTime, OffsetDateTime, Object, String, UUID, UUID> r) {\n            return AnsibleEvent.builder()\n                    .id(r.value1())\n                    .eventSeq(r.value2())\n                    .instanceId(r.value3())\n                    .instanceCreatedAt(r.value4())\n                    .eventDate(r.value5())\n                    .data(new EventData(deserialize(r.value6())))\n                    .initiator(r.value7())\n                    .initiatorId(r.value8())\n                    .projectId(r.value9())\n                    .build();\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        private Map<String, Object> deserialize(Object ab) {\n            try {\n                return objectMapper.readValue(String.valueOf(ab), Map.class);\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/processor/EventData.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster.processor;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.Map;\n\npublic class EventData {\n\n    private final Map<String, Object> data;\n\n    public String getAction() {\n        return getString(\"action\");\n    }\n\n    public String getTask() {\n        return getString(\"task\");\n    }\n\n    public boolean isOk() {\n        return \"ok\".equalsIgnoreCase(getString(\"status\"));\n    }\n\n    public boolean isPostEvent() {\n        return \"post\".equals(getString(\"phase\"));\n    }\n\n    public String getHost() {\n        return getString(\"host\");\n    }\n\n    public String getString(String param) {\n        Object o = data.get(param);\n        if (o instanceof String) {\n            return (String) data.get(param);\n        }\n        return null;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public Map<String, Object> getMap(String param) {\n        Object o = data.get(param);\n        if (o instanceof Map) {\n            return (Map<String, Object>) data.get(param);\n        }\n        return null;\n    }\n\n    public EventData(Map<String, Object> data) {\n        this.data = data;\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/processor/EventMarkerDao.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster.processor;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.db.MainDB;\nimport com.walmartlabs.concord.server.jooq.Tables;\nimport com.walmartlabs.concord.server.jooq.tables.EventProcessorMarker;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport org.immutables.value.Value;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.jooq.Record1;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\n\nimport static com.walmartlabs.concord.server.jooq.Tables.PROCESS_EVENTS;\nimport static org.jooq.impl.DSL.max;\nimport static org.jooq.impl.DSL.value;\n\n// TODO: move to plugins sdk?\n@Named\npublic class EventMarkerDao extends AbstractDao {\n\n    @Inject\n    public EventMarkerDao(@MainDB Configuration cfg) {\n        super(cfg);\n    }\n\n    @Override\n    public void tx(Tx t) {\n        super.tx(t);\n    }\n\n    @Override\n    public <T> T txResult(TxResult<T> t) {\n        return super.txResult(t);\n    }\n\n    @WithTimer\n    public EventMarker get(String processorName) {\n        EventProcessorMarker m = Tables.EVENT_PROCESSOR_MARKER.as(\"m\");\n\n        Long currentEventSeq = txResult(tx -> tx.select(m.EVENT_SEQ)\n                .from(m)\n                .where(m.PROCESSOR_NAME.eq(processorName))\n                .fetchOne(m.EVENT_SEQ));\n\n        Long maxEventSeq = txResult(tx -> tx.select(max(PROCESS_EVENTS.EVENT_SEQ))\n                .from(PROCESS_EVENTS)\n                .fetchOne(Record1::value1));\n\n        return EventMarker.builder()\n                .eventSeq(currentEventSeq != null ? currentEventSeq : -1)\n                .maxEventSeq(maxEventSeq != null ? maxEventSeq : -1)\n                .build();\n    }\n\n    @WithTimer\n    public void update(DSLContext tx, String processorName, long eventSeq) {\n        EventProcessorMarker m = Tables.EVENT_PROCESSOR_MARKER.as(\"m\");\n        tx.insertInto(m)\n                .columns(m.PROCESSOR_NAME, m.EVENT_SEQ)\n                .values(value(processorName), value(eventSeq))\n                .onDuplicateKeyUpdate()\n                .set(m.EVENT_SEQ, eventSeq)\n                .where(m.PROCESSOR_NAME.eq(processorName))\n                .execute();\n    }\n\n    @Value.Immutable\n    public interface EventMarker {\n\n        long eventSeq();\n\n        long maxEventSeq();\n\n        static ImmutableEventMarker.Builder builder() {\n            return ImmutableEventMarker.builder();\n        }\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/processor/HostArtifactsProcessor.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster.processor;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.StringUtils;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.server.plugins.noderoster.HostManager;\nimport com.walmartlabs.concord.server.plugins.noderoster.db.NodeRosterDB;\nimport com.walmartlabs.concord.server.plugins.noderoster.jooq.tables.NodeRosterHostArtifacts;\nimport com.walmartlabs.concord.server.plugins.noderoster.jooq.tables.records.NodeRosterHostArtifactsRecord;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport org.immutables.value.Value;\nimport org.jooq.BatchBindStep;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.jooq.Table;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.time.OffsetDateTime;\nimport java.util.*;\nimport java.util.function.Function;\n\nimport static com.walmartlabs.concord.server.plugins.noderoster.jooq.tables.NodeRosterHostArtifacts.NODE_ROSTER_HOST_ARTIFACTS;\nimport static org.jooq.impl.DSL.value;\n\n/**\n * Collect \"get_url\", \"maven_artifact\" and other related events, extracts\n * the artifact data and saved in the DB.\n */\n@Named\npublic class HostArtifactsProcessor implements Processor {\n\n    private static final Logger log = LoggerFactory.getLogger(HostArtifactsProcessor.class);\n\n    private final Dao dao;\n    private final HostManager hosts;\n\n    @Inject\n    public HostArtifactsProcessor(Dao dao, HostManager hosts) {\n        this.dao = dao;\n        this.hosts = hosts;\n    }\n\n    @Override\n    @WithTimer\n    public void process(List<AnsibleEvent> events) {\n        List<HostArtifactItem> items = new ArrayList<>();\n\n        for (AnsibleEvent e : events) {\n            String host = e.data().getHost();\n            Set<String> urls = getArtifactUrl(e.data());\n            if (host != null && urls != null) {\n                for (String url : urls) {\n                    items.add(HostArtifactItem.builder()\n                            .instanceId(e.instanceId())\n                            .instanceCreatedAt(e.instanceCreatedAt())\n                            .host(hosts.getOrCreate(host))\n                            .artifactUrl(url)\n                            .build());\n                }\n            }\n        }\n\n        if (!items.isEmpty()) {\n            dao.insert(items);\n        }\n\n        log.info(\"process -> events: {}, items: {}\", events.size(), items.size());\n    }\n\n    private static Set<String> getArtifactUrl(EventData eventData) {\n        if (!eventData.isPostEvent()) {\n            return null;\n        }\n\n        if (!eventData.isOk()) {\n            return null;\n        }\n\n        String action = eventData.getAction();\n        if (action == null) {\n            return null;\n        }\n\n        switch (action) {\n            case \"maven_artifact\": {\n                return fromMavenArtifact(eventData);\n            }\n            case \"get_url\":\n                return fromGetUrl(eventData);\n            case \"uri\": {\n                return fromUri(eventData);\n            }\n        }\n\n        return null;\n    }\n\n    // /$groupId[0]/../$groupId[n]/$artifactId/$version/$artifactId-$version-$classifier.$extension\n    private static Set<String> fromMavenArtifact(EventData eventData) {\n        return processResult(eventData, result -> {\n            String repoUrl = (String) result.get(\"repository_url\");\n            String groupId = (String) result.get(\"group_id\");\n            String artifactId = (String) result.get(\"artifact_id\");\n            String extension = (String) result.get(\"extension\");\n            String classifier = (String) result.get(\"classifier\");\n            String version = (String) result.get(\"version\");\n\n            if (repoUrl == null || groupId == null || artifactId == null || version == null) {\n                return null;\n            }\n\n            String name = artifactId + \"-\" + version;\n            if (classifier != null && !classifier.isEmpty()) {\n                name += \"-\" + classifier;\n            }\n            name += \".\" + extension;\n\n            return normalizeUrl(repoUrl) + \"/\" + groupId.replaceAll(\"\\\\.\", \"/\") + \"/\" + artifactId + \"/\" + version + \"/\" + name;\n        });\n    }\n\n    private static Set<String> fromGetUrl(EventData eventData) {\n        return processResult(eventData, r -> {\n            Object url = r.get(\"url\");\n            if (url instanceof String) {\n                return (String) url;\n            }\n            return null;\n        });\n    }\n\n\n    private static Set<String> fromUri(EventData eventData) {\n        return processResult(eventData, r -> {\n            if (r.get(\"path\") == null) {\n                return null;\n            }\n            Object url = r.get(\"url\");\n            if (url instanceof String) {\n                return (String) url;\n            }\n            return null;\n        });\n    }\n\n    private static Set<String> processResult(EventData eventData, Function<Map<String, Object>, String> processor) {\n        Map<String, Object> result = eventData.getMap(\"result\");\n        if (result == null) {\n            return Collections.emptySet();\n        }\n\n        String singleResult = processor.apply(result);\n        if (singleResult != null) {\n            return Collections.singleton(singleResult);\n        }\n\n        return processResults(result, processor);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Set<String> processResults(Map<String, Object> result, Function<Map<String, Object>, String> processor) {\n        Object resultsObject = result.get(\"results\");\n        if (!(resultsObject instanceof Collection)) {\n            return Collections.emptySet();\n        }\n\n        Set<String> processed = new HashSet<>();\n        Collection<Map<String, Object>> results = (Collection<Map<String, Object>>) resultsObject;\n        for (Object ro : results) {\n            if (ro instanceof Map) {\n                Map<String, Object> r = (Map<String, Object>) ro;\n                String value = processor.apply(r);\n                if (value != null) {\n                    processed.add(value);\n                }\n            }\n        }\n        return processed;\n    }\n\n    private static String normalizeUrl(String url) {\n        if (url.endsWith(\"/\")) {\n            return url.substring(0, url.length() - 1);\n        }\n        return url;\n    }\n\n    @Named\n    public static class Dao extends AbstractDao {\n\n        private final HostArtifactPartitioner partitioner;\n\n        @Inject\n        public Dao(@NodeRosterDB Configuration cfg, HostArtifactPartitioner partitioner) {\n            super(cfg);\n            this.partitioner = partitioner;\n        }\n\n        @WithTimer\n        public void insert(List<HostArtifactItem> items) {\n            tx(tx -> insert(tx, items));\n        }\n\n        private void insert(DSLContext tx, List<HostArtifactItem> items) {\n            NodeRosterHostArtifacts h = NODE_ROSTER_HOST_ARTIFACTS.as(\"ha\");\n\n            Map<Table<NodeRosterHostArtifactsRecord>, Collection<HostArtifactItem>> tblItems = partitioner.process(items);\n            for (Map.Entry<Table<NodeRosterHostArtifactsRecord>, Collection<HostArtifactItem>> e : tblItems.entrySet()) {\n                BatchBindStep q = tx.batch(tx.insertInto(e.getKey().as(\"ha\"), h.INSTANCE_ID, h.INSTANCE_CREATED_AT, h.HOST_ID, h.ARTIFACT_URL)\n                        .values((UUID) null, null, null, null)\n                        .onConflictDoNothing());\n\n                for (HostArtifactItem i : e.getValue()) {\n                    q.bind(value(i.instanceId()), value(i.instanceCreatedAt()), value(i.host()), value(StringUtils.abbreviate(i.artifactUrl(), h.ARTIFACT_URL.getDataType().length())));\n                }\n\n                q.execute();\n            }\n        }\n    }\n\n    @Named\n    public static class HostArtifactPartitioner extends Partitioner<HostArtifactItem, NodeRosterHostArtifactsRecord> {\n\n        public HostArtifactPartitioner() {\n            super(NODE_ROSTER_HOST_ARTIFACTS, HostArtifactItem::instanceCreatedAt);\n        }\n    }\n\n    @Value.Immutable\n    interface HostArtifactItem {\n\n        UUID host();\n\n        UUID instanceId();\n\n        OffsetDateTime instanceCreatedAt();\n\n        String artifactUrl();\n\n        static ImmutableHostArtifactItem.Builder builder() {\n            return ImmutableHostArtifactItem.builder();\n        }\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/processor/HostFactsProcessor.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster.processor;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.server.plugins.noderoster.HostManager;\nimport com.walmartlabs.concord.server.plugins.noderoster.db.NodeRosterDB;\nimport com.walmartlabs.concord.server.plugins.noderoster.jooq.tables.NodeRosterHostFacts;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport org.immutables.value.Value;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.jooq.JSONB;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.io.IOException;\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLException;\nimport java.time.OffsetDateTime;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static com.walmartlabs.concord.server.plugins.noderoster.jooq.tables.NodeRosterHostFacts.NODE_ROSTER_HOST_FACTS;\nimport static org.jooq.impl.DSL.value;\n\n/**\n * Collects facts received from \"gather_facts\" steps and saves them in the DB.\n */\n@Named\npublic class HostFactsProcessor implements Processor {\n\n    private static final Logger log = LoggerFactory.getLogger(HostFactsProcessor.class);\n\n    private final Dao dao;\n    private final HostManager hosts;\n\n    @Inject\n    public HostFactsProcessor(Dao dao, HostManager hosts) {\n        this.dao = dao;\n        this.hosts = hosts;\n    }\n\n    @Override\n    @WithTimer\n    public void process(List<AnsibleEvent> events) {\n        List<HostFactsItem> items = new ArrayList<>();\n\n        for (AnsibleEvent e : events) {\n            String host = e.data().getHost();\n            Map<String, Object> facts = getFacts(e.data());\n            if (host != null && facts != null) {\n                items.add(HostFactsItem.builder()\n                        .instanceId(e.instanceId())\n                        .instanceCreatedAt(e.instanceCreatedAt())\n                        .host(hosts.getOrCreate(host))\n                        .facts(facts)\n                        .build());\n            }\n        }\n\n        if (!items.isEmpty()) {\n            dao.insert(items);\n        }\n\n        log.info(\"process -> events: {}, items: {}\", events.size(), items.size());\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Map<String, Object> getFacts(EventData eventData) {\n        if (!eventData.isPostEvent()) {\n            return null;\n        }\n\n        String action = eventData.getAction();\n        String task = eventData.getTask();\n        if (\"setup\".equals(action) || \"Gathering Facts\".equals(task)) {\n            Map<String, Object> result = eventData.getMap(\"result\");\n            if (result == null) {\n                return null;\n            }\n\n            return (Map<String, Object>) result.get(\"ansible_facts\");\n        }\n        return null;\n    }\n\n    @Named\n    public static class Dao extends AbstractDao {\n\n        private final ObjectMapper objectMapper;\n\n        @Inject\n        public Dao(@NodeRosterDB Configuration cfg) {\n            super(cfg);\n            this.objectMapper = new ObjectMapper();\n        }\n\n        @WithTimer\n        public void insert(List<HostFactsItem> items) {\n            tx(tx -> insert(tx, items));\n        }\n\n        private void insert(DSLContext tx, List<HostFactsItem> items) {\n            tx.connection(conn -> {\n                int[] updated = update(tx, conn, items);\n\n                List<HostFactsItem> itemsForInsert = new ArrayList<>();\n                for (int i = 0; i < updated.length; i++) {\n                    if (updated[i] < 1) {\n                        itemsForInsert.add(items.get(i));\n                    }\n                }\n\n                if (!itemsForInsert.isEmpty()) {\n                    insert(tx, conn, itemsForInsert);\n                }\n\n                log.info(\"insert -> updated: {}, inserted: {}\",\n                        items.size() - itemsForInsert.size(), itemsForInsert.size());\n            });\n        }\n\n        @WithTimer\n        protected int[] update(DSLContext tx, Connection conn, List<HostFactsItem> items) throws SQLException {\n            NodeRosterHostFacts f = NODE_ROSTER_HOST_FACTS.as(\"f\");\n\n            String update = tx.update(f)\n                    .set(f.FACTS, (JSONB) null)\n                    .where(f.INSTANCE_ID.eq(value((UUID) null))\n                            .and(f.INSTANCE_CREATED_AT.eq(value((OffsetDateTime) null))\n                                    .and(f.HOST_ID.eq(value((UUID) null)))))\n                    .getSQL();\n\n            try (PreparedStatement ps = conn.prepareStatement(update)) {\n                for (HostFactsItem i : items) {\n                    ps.setString(1, serialize(i.facts()));\n                    ps.setObject(2, i.instanceId());\n                    ps.setObject(3, i.instanceCreatedAt());\n                    ps.setObject(4, i.host());\n\n                    ps.addBatch();\n                }\n                return ps.executeBatch();\n            }\n        }\n\n        @WithTimer\n        protected void insert(DSLContext tx, Connection conn, List<HostFactsItem> items) throws SQLException {\n            NodeRosterHostFacts f = NODE_ROSTER_HOST_FACTS.as(\"f\");\n\n            String insert = tx.insertInto(f)\n                    .columns(f.HOST_ID,\n                            f.INSTANCE_ID,\n                            f.INSTANCE_CREATED_AT,\n                            f.FACTS)\n                    .values(value((UUID) null), null, null, null)\n                    .getSQL();\n\n            try (PreparedStatement ps = conn.prepareStatement(insert)) {\n                for (HostFactsItem i : items) {\n                    ps.setObject(1, i.host());\n                    ps.setObject(2, i.instanceId());\n                    ps.setObject(3, i.instanceCreatedAt());\n                    ps.setString(4, serialize(i.facts()));\n\n                    ps.addBatch();\n                }\n                ps.executeBatch();\n            }\n        }\n\n        private String serialize(Map<String, Object> m) {\n            if (m == null) {\n                return null;\n            }\n\n            try {\n                return objectMapper.writeValueAsString(m);\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n        }\n    }\n\n    @Value.Immutable\n    interface HostFactsItem {\n\n        UUID host();\n\n        UUID instanceId();\n\n        OffsetDateTime instanceCreatedAt();\n\n        Map<String, Object> facts();\n\n        static ImmutableHostFactsItem.Builder builder() {\n            return ImmutableHostFactsItem.builder();\n        }\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/processor/Partitioner.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster.processor;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.jooq.Record;\nimport org.jooq.Table;\nimport org.jooq.impl.DSL;\n\nimport java.time.OffsetDateTime;\nimport java.time.format.DateTimeFormatter;\nimport java.util.*;\nimport java.util.function.Function;\n\n/**\n * Handles partitioned tables. Assumes {@code table_yyyyMMdd} format\n * of partition names.\n */\npublic class Partitioner<E, R extends Record> {\n\n    // TODO make configurable\n    private static final DateTimeFormatter PARTITION_DATE_FORMAT = DateTimeFormatter.ofPattern(\"yyyyMMdd\");\n\n    private final Table<R> table;\n    private final Function<E, OffsetDateTime> keyGetter;\n\n    private final boolean enabled;\n\n    public Partitioner(Table<R> table, Function<E, OffsetDateTime> keyGetter) {\n        this.table = table;\n        this.keyGetter = keyGetter;\n        this.enabled = \"true\".equals(System.getenv(\"NODE_ROSTER_PARTITIONING_ENABLED\"));\n    }\n\n    public Map<Table<R>, Collection<E>> process(Collection<E> items) {\n        if (!enabled) {\n            return Collections.singletonMap(table, items);\n        }\n\n        Map<Table<R>, Collection<E>> result = new HashMap<>();\n        for (E i : items) {\n            OffsetDateTime itemKey = keyGetter.apply(i);\n            String partitionId = partitionId(itemKey);\n            Table<R> t = table(table, partitionId);\n\n            result.computeIfAbsent(t, recordTable -> new ArrayList<>())\n                    .add(i);\n        }\n\n        return result;\n    }\n\n    private static String partitionId(OffsetDateTime itemKey) {\n        return PARTITION_DATE_FORMAT.format(itemKey);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static <E extends Record> Table<E> table(Table<?> table, String partitionId) {\n        return (Table<E>) DSL.table(DSL.name(table.getName() + \"_\" + partitionId));\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/processor/ProcessHostsProcessor.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster.processor;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.common.StringUtils;\nimport com.walmartlabs.concord.db.AbstractDao;\nimport com.walmartlabs.concord.server.plugins.noderoster.HostManager;\nimport com.walmartlabs.concord.server.plugins.noderoster.db.NodeRosterDB;\nimport com.walmartlabs.concord.server.plugins.noderoster.jooq.tables.NodeRosterProcessHosts;\nimport com.walmartlabs.concord.server.plugins.noderoster.jooq.tables.records.NodeRosterProcessHostsRecord;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport org.immutables.value.Value;\nimport org.jooq.BatchBindStep;\nimport org.jooq.Configuration;\nimport org.jooq.DSLContext;\nimport org.jooq.Table;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport java.time.OffsetDateTime;\nimport java.util.*;\n\nimport static com.walmartlabs.concord.server.plugins.noderoster.jooq.tables.NodeRosterProcessHosts.NODE_ROSTER_PROCESS_HOSTS;\nimport static org.jooq.impl.DSL.value;\n\n/**\n * Saves all hosts found in events of a particular process.\n */\n@Named\npublic class ProcessHostsProcessor implements Processor {\n\n    private static final Logger log = LoggerFactory.getLogger(ProcessHostsProcessor.class);\n\n    private final Dao dao;\n    private final HostManager hosts;\n\n    @Inject\n    public ProcessHostsProcessor(Dao dao, HostManager hosts) {\n        this.dao = dao;\n        this.hosts = hosts;\n    }\n\n    @Override\n    @WithTimer\n    public void process(List<AnsibleEvent> events) {\n        Set<ProcessHostItem> items = new HashSet<>();\n\n        for (AnsibleEvent e : events) {\n            String host = e.data().getHost();\n            if (host != null) {\n                items.add(ProcessHostItem.builder()\n                        .instanceId(e.instanceId())\n                        .instanceCreatedAt(e.instanceCreatedAt())\n                        .host(hosts.getOrCreate(host))\n                        .initiator(e.initiator())\n                        .initiatorId(e.initiatorId())\n                        .projectId(e.projectId())\n                        .build());\n            }\n        }\n\n        if (!items.isEmpty()) {\n            dao.insert(items);\n        }\n\n        log.info(\"process -> events: {}, items: {}\", events.size(), items.size());\n    }\n\n    @Named\n    public static class Dao extends AbstractDao {\n\n        private final ProcessHostsPartitioner partitioner;\n\n        @Inject\n        public Dao(@NodeRosterDB Configuration cfg, ProcessHostsPartitioner partitioner) {\n            super(cfg);\n            this.partitioner = partitioner;\n        }\n\n        @WithTimer\n        public void insert(Set<ProcessHostItem> items) {\n            tx(tx -> insert(tx, items));\n        }\n\n        private void insert(DSLContext tx, Set<ProcessHostItem> items) {\n            NodeRosterProcessHosts h = NODE_ROSTER_PROCESS_HOSTS.as(\"ph\");\n\n            Map<Table<NodeRosterProcessHostsRecord>, Collection<ProcessHostItem>> tblItems = partitioner.process(items);\n            for (Map.Entry<Table<NodeRosterProcessHostsRecord>, Collection<ProcessHostItem>> e : tblItems.entrySet()) {\n                BatchBindStep q = tx.batch(tx.insertInto(e.getKey().as(\"ph\"),\n                        h.INSTANCE_ID,\n                        h.INSTANCE_CREATED_AT,\n                        h.HOST_ID,\n                        h.INITIATOR,\n                        h.INITIATOR_ID,\n                        h.PROJECT_ID)\n                        .values((UUID) null, null, null, null, null, null)\n                        .onConflictDoNothing());\n\n                for (ProcessHostItem i : e.getValue()) {\n                    q.bind(value(i.instanceId()),\n                            value(i.instanceCreatedAt()),\n                            value(i.host()),\n                            value(StringUtils.abbreviate(i.initiator(), h.INITIATOR.getDataType().length())),\n                            value(i.initiatorId()),\n                            value(i.projectId()));\n                }\n\n                q.execute();\n            }\n        }\n    }\n\n    @Named\n    public static class ProcessHostsPartitioner extends Partitioner<ProcessHostItem, NodeRosterProcessHostsRecord> {\n\n        public ProcessHostsPartitioner() {\n            super(NODE_ROSTER_PROCESS_HOSTS, ProcessHostItem::instanceCreatedAt);\n        }\n    }\n\n    @Value.Immutable\n    public abstract static class ProcessHostItem {\n\n        public abstract UUID instanceId();\n\n        public abstract OffsetDateTime instanceCreatedAt();\n\n        public abstract UUID host();\n\n        @Nullable\n        public abstract String initiator();\n\n        @Nullable\n        public abstract UUID initiatorId();\n\n        @Nullable\n        public abstract UUID projectId();\n\n        @Override\n        public boolean equals(Object o) {\n            if (this == o) return true;\n            if (o == null || getClass() != o.getClass()) return false;\n            ProcessHostItem that = (ProcessHostItem) o;\n            return Objects.equals(instanceId(), that.instanceId()) &&\n                    Objects.equals(instanceCreatedAt(), that.instanceCreatedAt()) &&\n                    Objects.equals(host(), that.host());\n        }\n\n        @Override\n        public int hashCode() {\n            return Objects.hash(instanceId(), instanceCreatedAt(), host());\n        }\n\n        public static ImmutableProcessHostItem.Builder builder() {\n            return ImmutableProcessHostItem.builder();\n        }\n    }\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/java/com/walmartlabs/concord/server/plugins/noderoster/processor/Processor.java",
    "content": "package com.walmartlabs.concord.server.plugins.noderoster.processor;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.List;\n\npublic interface Processor {\n\n    void process(List<AnsibleEvent> events);\n}\n"
  },
  {
    "path": "server/plugins/noderoster/impl/src/main/resources/openapi-noderoster-config.yaml",
    "content": "openAPI:\n  info:\n    version: '2.0'\n    title: Concord Node Roster API\n    description: Concord Node Roster API\n"
  },
  {
    "path": "server/plugins/noderoster/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.server.plugins</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <groupId>com.walmartlabs.concord.server.plugins.noderoster</groupId>\n    <artifactId>parent</artifactId>\n    <packaging>pom</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <modules>\n        <module>db</module>\n        <module>impl</module>\n        <module>client2</module>\n    </modules>\n</project>\n"
  },
  {
    "path": "server/plugins/oidc/README.md",
    "content": "# OpenID Connect Integration\n\nAdds support for authentication using OpenID Connect.\n\n## Usage\n\nAdd the necessary parameters to the Server's configuration file. Example for [Okta](https://www.okta.com/):\n\n```\nconcord-server {\n    oidc {\n        enabled = true\n        clientId = \"********************\"\n        secret = \"****************************************\"\n        discoveryUri = \"https://myorg.okta.com/oauth2/default/.well-known/openid-configuration\"\n        urlBase = \"http://localhost:8001\"\n        afterLoginUrl = \"http://localhost:8001\"\n        afterLogoutUrl = \"http://localhost:8001/#/logout/done\"\n        roles = [\n            \"concordAdmin\"\n        ]\n    }\n}\n```\n\nFor running in development mode (i.e. on `localhost`), callback URLs must be\nin the form of\n\n```\nhttp://localhost:8001/api/service/oidc/callback?client_name=oidc\n```\n\nNote the `client_name=oidc` query parameter, it is required by the plugin and\nmust be present in the provider's configuration.\n\nThe plugin uses the following scopes: `openid`, `profile`, `email`, `groups`.\nWhich may or may not be enabled by default in the provider's configuration.\n\nOkta, for example, does not provide the `groups` scope by default. You can\nadd it in the \"Security\" -> \"API\" -> \"Authorization Servers\" -> your_server ->\n\"Scope\" section.\n\n### Interactive Login\n\nConfigure the Concord Console to use custom logout/login URLs:\n- create a `cfg.js` (use [the default file](../../../console2/public/cfg.js) as an example):\n  ```javascript\n  window.concord = {\n      loginUrl: '/api/service/oidc/auth',\n      logoutUrl: '/api/service/oidc/logout'\n  };\n  ```\n- mount in into the Server's container:\n  ```\n  docker run ... -v /path/to/cfg.js:/opt/concord/console/cfg.js:ro walmartlabs/concord-server\n  ```\n\nWhen accessing the Concord Console you should be redirected to the OpenID Connect\nprovider's sign-in page.\n\n### API Tokens\n\n- get an access token from the ID provider. For Okta, you can use\n[the implicit flow](https://developer.okta.com/docs/guides/implement-implicit/use-flow/).\nThe token must allow `openid`, `email` and `profile` scopes;\n- call a Concord API endpoint using the token:\n  ```\n  curl -ik -H 'Authorization: Bearer TOKEN' http://localhost:8001/api/service/console/whoami\n  ```\n"
  },
  {
    "path": "server/plugins/oidc/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.server.plugins</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>oidc</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server-impl</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.shiro</groupId>\n            <artifactId>shiro-core</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.shiro</groupId>\n            <artifactId>shiro-web</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.servlet</groupId>\n            <artifactId>javax.servlet-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.inject</groupId>\n            <artifactId>guice</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.sisu</groupId>\n            <artifactId>org.eclipse.sisu.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-config</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.typesafe</groupId>\n            <artifactId>config</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-core</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "server/plugins/oidc/src/main/java/com/walmartlabs/concord/server/plugins/oidc/OidcAuthFilter.java",
    "content": "package com.walmartlabs.concord.server.plugins.oidc;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.inject.Inject;\nimport javax.servlet.*;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.util.UUID;\n\n// TODO can be implemented as a JAX-RS resource\npublic class OidcAuthFilter implements Filter {\n\n    public static final String URL = \"/api/service/oidc/auth\";\n    private static final String SESSION_STATE_KEY = \"OIDC_STATE\";\n    private static final String SESSION_REDIRECT_KEY = \"OIDC_REDIRECT_URL\";\n\n    private final PluginConfiguration pluginConfig;\n    private final OidcService oidcService;\n\n    @Inject\n    public OidcAuthFilter(PluginConfiguration pluginConfig, OidcService oidcService) {\n        this.pluginConfig = pluginConfig;\n        this.oidcService = oidcService;\n    }\n\n    @Override\n    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException {\n        var req = (HttpServletRequest) request;\n        var resp = (HttpServletResponse) response;\n\n        if (!pluginConfig.isEnabled()) {\n            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, \"OIDC disabled\");\n            return;\n        }\n\n        var redirectUrl = req.getParameter(\"from\");\n        var state = UUID.randomUUID().toString();\n        var callbackUrl = pluginConfig.getUrlBase() + OidcCallbackFilter.URL + \"?client_name=oidc\";\n\n        var session = req.getSession(true);\n        session.setAttribute(SESSION_STATE_KEY, state);\n        session.setAttribute(SESSION_REDIRECT_KEY, redirectUrl);\n\n        var authUrl = oidcService.buildAuthorizationUrl(callbackUrl, state);\n        resp.sendRedirect(authUrl);\n    }\n\n    @Override\n    public void init(FilterConfig filterConfig) {\n    }\n\n    @Override\n    public void destroy() {\n    }\n}\n"
  },
  {
    "path": "server/plugins/oidc/src/main/java/com/walmartlabs/concord/server/plugins/oidc/OidcAuthenticationHandler.java",
    "content": "package com.walmartlabs.concord.server.plugins.oidc;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.boot.filters.AuthenticationHandler;\nimport org.apache.shiro.authc.AuthenticationToken;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.servlet.ServletRequest;\nimport javax.servlet.ServletResponse;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\npublic class OidcAuthenticationHandler implements AuthenticationHandler {\n\n    private static final Logger log = LoggerFactory.getLogger(OidcAuthenticationHandler.class);\n    private static final String FORM_URL_PATTERN = \"/forms/.*\";\n\n    private static final String AUTHORIZATION_HEADER = \"Authorization\";\n    private static final String HEADER_PREFIX = \"Bearer\";\n    private static final String SESSION_PROFILE_KEY = \"OIDC_USER_PROFILE\";\n\n    private final PluginConfiguration cfg;\n    private final OidcService oidcService;\n\n    @Inject\n    public OidcAuthenticationHandler(PluginConfiguration cfg, OidcService oidcService) {\n        this.cfg = cfg;\n        this.oidcService = oidcService;\n    }\n\n    @Override\n    public AuthenticationToken createToken(ServletRequest request, ServletResponse response) {\n        if (!cfg.isEnabled()) {\n            return null;\n        }\n\n        var req = (HttpServletRequest) request;\n\n        var header = req.getHeader(AUTHORIZATION_HEADER);\n        if (header == null) {\n            var session = req.getSession(false);\n            if (session == null) {\n                return null;\n            }\n\n            var profile = (UserProfile) session.getAttribute(SESSION_PROFILE_KEY);\n            return new OidcToken(profile);\n        }\n\n        var as = header.split(\" \");\n        if (as.length != 2 || !as[0].equals(HEADER_PREFIX)) {\n            return null;\n        }\n\n        var accessToken = as[1].trim();\n        try {\n            var profile = oidcService.validateToken(accessToken);\n            return new OidcToken(profile);\n        } catch (IOException e) {\n            log.warn(\"Token validation failed: {}\", e.getMessage());\n            return null;\n        }\n    }\n\n    @Override\n    public boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {\n        if (!cfg.isEnabled()) {\n            return false;\n        }\n\n        var req = (HttpServletRequest) request;\n        var resp = (HttpServletResponse) response;\n\n        if (req.getRequestURI().matches(FORM_URL_PATTERN)) {\n            resp.sendRedirect(resp.encodeRedirectURL(OidcAuthFilter.URL + \"?from=\" + req.getRequestURL()));\n            return true;\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "server/plugins/oidc/src/main/java/com/walmartlabs/concord/server/plugins/oidc/OidcCallbackFilter.java",
    "content": "package com.walmartlabs.concord.server.plugins.oidc;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.servlet.Filter;\nimport javax.servlet.FilterChain;\nimport javax.servlet.ServletRequest;\nimport javax.servlet.ServletResponse;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport javax.servlet.http.HttpSession;\nimport java.io.IOException;\n\npublic class OidcCallbackFilter implements Filter {\n\n    private static final Logger log = LoggerFactory.getLogger(OidcCallbackFilter.class);\n\n    public static final String URL = \"/api/service/oidc/callback\";\n    private static final String SESSION_STATE_KEY = \"OIDC_STATE\";\n    private static final String SESSION_REDIRECT_KEY = \"OIDC_REDIRECT_URL\";\n    private static final String SESSION_PROFILE_KEY = \"OIDC_USER_PROFILE\";\n\n    private final PluginConfiguration cfg;\n    private final OidcService oidcService;\n\n    @Inject\n    public OidcCallbackFilter(PluginConfiguration cfg, OidcService oidcService) {\n        this.cfg = cfg;\n        this.oidcService = oidcService;\n    }\n\n    @Override\n    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException {\n        var req = (HttpServletRequest) request;\n        var resp = (HttpServletResponse) response;\n\n        if (!cfg.isEnabled()) {\n            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, \"OIDC disabled\");\n            return;\n        }\n\n        var session = req.getSession(false);\n        if (session == null) {\n            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, \"No session\");\n            return;\n        }\n\n        var postLoginUrl = (String) session.getAttribute(SESSION_REDIRECT_KEY);\n        if (postLoginUrl == null || postLoginUrl.trim().isEmpty()) {\n            postLoginUrl = cfg.getAfterLoginUrl();\n        }\n\n        var error = req.getParameter(\"error\");\n        if (error != null) {\n            var derivedError = \"unknown\";\n            if (\"access_denied\".equals(error)) {\n                derivedError = \"oidc_access_denied\";\n            }\n            resp.sendRedirect(resp.encodeRedirectURL(cfg.getOnErrorUrl() + \"?from=\" + postLoginUrl + \"&error=\" + derivedError));\n            return;\n        }\n\n        var code = req.getParameter(\"code\");\n        var state = req.getParameter(\"state\");\n        var expectedState = (String) session.getAttribute(SESSION_STATE_KEY);\n\n        if (code == null || state == null || !state.equals(expectedState)) {\n            log.warn(\"Invalid callback parameters: code={}, state={}, expectedState={}\", code != null, state, expectedState);\n            invalidateOrWarn(session);\n            resp.sendRedirect(resp.encodeRedirectURL(OidcAuthFilter.URL + \"?from=\" + postLoginUrl));\n            return;\n        }\n\n        try {\n            var redirectUri = cfg.getUrlBase() + URL + \"?client_name=oidc\";\n            var profile = oidcService.exchangeCodeForProfile(code, redirectUri);\n\n            session.setAttribute(SESSION_PROFILE_KEY, profile);\n            session.removeAttribute(SESSION_STATE_KEY);\n            session.removeAttribute(SESSION_REDIRECT_KEY);\n\n            resp.sendRedirect(resp.encodeRedirectURL(postLoginUrl));\n\n        } catch (Exception e) {\n            log.warn(\"OIDC callback error\", e);\n            invalidateOrWarn(session);\n            resp.sendRedirect(resp.encodeRedirectURL(OidcAuthFilter.URL + \"?from=\" + postLoginUrl));\n        }\n    }\n\n    private static void invalidateOrWarn(HttpSession session) {\n        try {\n            session.invalidate();\n        } catch (Exception e) {\n            log.warn(\"Unable to invalidate the session\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "server/plugins/oidc/src/main/java/com/walmartlabs/concord/server/plugins/oidc/OidcFilterChainConfigurator.java",
    "content": "package com.walmartlabs.concord.server.plugins.oidc;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.boot.FilterChainConfigurator;\nimport org.apache.shiro.web.filter.mgt.FilterChainManager;\n\nimport javax.inject.Inject;\nimport java.util.Objects;\n\nimport static java.util.Objects.requireNonNull;\n\npublic class OidcFilterChainConfigurator implements FilterChainConfigurator {\n\n    private final OidcAuthFilter authFilter;\n    private final OidcCallbackFilter callbackFilter;\n    private final OidcLogoutFilter logoutFilter;\n\n    @Inject\n    public OidcFilterChainConfigurator(OidcAuthFilter authFilter,\n                                       OidcCallbackFilter callbackFilter,\n                                       OidcLogoutFilter logoutFilter) {\n\n        this.authFilter = requireNonNull(authFilter);\n        this.callbackFilter = requireNonNull(callbackFilter);\n        this.logoutFilter = requireNonNull(logoutFilter);\n    }\n\n    @Override\n    public void configure(FilterChainManager manager) {\n        manager.addFilter(\"oidcAuth\", authFilter);\n        manager.createChain(OidcAuthFilter.URL, \"oidcAuth\");\n\n        manager.addFilter(\"oidcCallback\", callbackFilter);\n        manager.createChain(OidcCallbackFilter.URL, \"oidcCallback\");\n\n        manager.addFilter(\"oidcLogout\", logoutFilter);\n        manager.createChain(OidcLogoutFilter.URL, \"oidcLogout\");\n    }\n}\n"
  },
  {
    "path": "server/plugins/oidc/src/main/java/com/walmartlabs/concord/server/plugins/oidc/OidcLogoutFilter.java",
    "content": "package com.walmartlabs.concord.server.plugins.oidc;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.inject.Inject;\nimport javax.servlet.*;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.util.Optional;\n\npublic class OidcLogoutFilter implements Filter {\n\n    public static final String URL = \"/api/service/oidc/logout\";\n\n    private final PluginConfiguration cfg;\n\n    @Inject\n    public OidcLogoutFilter(PluginConfiguration cfg) {\n        this.cfg = cfg;\n    }\n\n    @Override\n    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {\n        var req = (HttpServletRequest) request;\n        var resp = (HttpServletResponse) response;\n\n        var session = req.getSession(false);\n        if (session != null) {\n            session.invalidate();\n        }\n\n        var afterLogoutUrl = Optional.ofNullable(req.getParameter(\"from\")).orElse(cfg.getAfterLogoutUrl());\n        resp.sendRedirect(afterLogoutUrl);\n    }\n}\n"
  },
  {
    "path": "server/plugins/oidc/src/main/java/com/walmartlabs/concord/server/plugins/oidc/OidcPluginModule.java",
    "content": "package com.walmartlabs.concord.server.plugins.oidc;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.AbstractModule;\nimport com.walmartlabs.concord.server.boot.FilterChainConfigurator;\nimport com.walmartlabs.concord.server.boot.filters.AuthenticationHandler;\nimport org.apache.shiro.realm.Realm;\n\nimport javax.inject.Named;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\n\n@Named\npublic class OidcPluginModule extends AbstractModule {\n\n    @Override\n    protected void configure() {\n        bind(OidcService.class).in(SINGLETON);\n        newSetBinder(binder(), AuthenticationHandler.class).addBinding().to(OidcAuthenticationHandler.class);\n        newSetBinder(binder(), FilterChainConfigurator.class).addBinding().to(OidcFilterChainConfigurator.class);\n        newSetBinder(binder(), Realm.class).addBinding().to(OidcRealm.class);\n    }\n}\n"
  },
  {
    "path": "server/plugins/oidc/src/main/java/com/walmartlabs/concord/server/plugins/oidc/OidcRealm.java",
    "content": "package com.walmartlabs.concord.server.plugins.oidc;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.annotations.VisibleForTesting;\nimport com.walmartlabs.concord.common.Matcher;\nimport com.walmartlabs.concord.server.org.team.TeamDao;\nimport com.walmartlabs.concord.server.org.team.TeamRole;\nimport com.walmartlabs.concord.server.plugins.oidc.PluginConfiguration.TeamMapping;\nimport com.walmartlabs.concord.server.role.RoleDao;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.security.AuthenticationException;\nimport com.walmartlabs.concord.server.security.SecurityUtils;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.user.*;\nimport org.apache.shiro.authc.AuthenticationInfo;\nimport org.apache.shiro.authc.AuthenticationToken;\nimport org.apache.shiro.authc.SimpleAccount;\nimport org.apache.shiro.authc.credential.CredentialsMatcher;\nimport org.apache.shiro.authz.AuthorizationInfo;\nimport org.apache.shiro.realm.AuthorizingRealm;\nimport org.apache.shiro.subject.PrincipalCollection;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.util.*;\nimport java.util.stream.Collectors;\n\npublic class OidcRealm extends AuthorizingRealm {\n\n    private static final Logger log = LoggerFactory.getLogger(OidcRealm.class);\n    private static final String REALM_NAME = \"oidc\";\n\n    private final UserManager userManager;\n    private final UserDao userDao;\n    private final TeamDao teamDao;\n\n    private final Map<String, List<PluginConfiguration.Source>> roleMapping;\n    private final Map<UUID, TeamMapping> teamMapping;\n\n    @Inject\n    public OidcRealm(PluginConfiguration cfg,\n                     UserManager userManager,\n                     UserDao userDao,\n                     RoleDao roleDao,\n                     TeamDao teamDao) {\n\n        this.userManager = userManager;\n        this.userDao = userDao;\n        this.teamDao = teamDao;\n\n        this.roleMapping = validateRoleMapping(cfg.getRoleMapping(), roleDao);\n        this.teamMapping = validateTeamMapping(cfg.getTeamMapping(), teamDao);\n\n        setCredentialsMatcher(new OidcCredentialsMatcher());\n    }\n\n    @Override\n    public boolean supports(AuthenticationToken token) {\n        return token instanceof OidcToken;\n    }\n\n    @Override\n    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {\n        OidcToken t = (OidcToken) token;\n\n        UserProfile profile = t.getProfile();\n\n        // TODO replace getOrCreate+update with a single method?\n\n        String username = profile.email().toLowerCase();\n        UserEntry u = userManager.getOrCreate(username, null, UserType.LOCAL)\n                .orElseThrow(() -> new ConcordApplicationException(\"User not found: \" + profile.email()));\n        UUID userId = u.getId();\n\n        userManager.update(userId, profile.displayName(), profile.email(), null, false, null);\n\n        Set<UUID> newTeams = new HashSet<>();\n        teamDao.tx(tx -> {\n            List<UserTeam> currentTeams = userDao.listTeams(tx, userId);\n\n            for (Map.Entry<UUID, TeamMapping> e : teamMapping.entrySet()) {\n                UUID teamId = e.getKey();\n                TeamMapping mapping = e.getValue();\n                if (match(profile, mapping.sources())) {\n                    if (!hasTeam(teamId, mapping.role(), currentTeams)) {\n                        teamDao.upsertUser(tx, teamId, userId, mapping.role());\n                    }\n                    newTeams.add(teamId);\n                }\n            }\n\n            List<UUID> toRemove = currentTeams.stream()\n                    .map(UserTeam::teamId)\n                    .filter(o -> !newTeams.contains(o))\n                    .collect(Collectors.toList());\n            userDao.excludeFromTeams(tx, userId, toRemove);\n        });\n\n        UserPrincipal userPrincipal = new UserPrincipal(REALM_NAME, u);\n        return new SimpleAccount(Arrays.asList(userPrincipal, t), t, getName());\n    }\n\n    @Override\n    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {\n        UserPrincipal p = principals.oneByType(UserPrincipal.class);\n        if (p == null || !REALM_NAME.equals(p.getRealm())) {\n            return null;\n        }\n\n        OidcToken token = principals.oneByType(OidcToken.class);\n\n        List<String> roles = new ArrayList<>();\n        for (Map.Entry<String, List<PluginConfiguration.Source>> e : roleMapping.entrySet()) {\n            String roleName = e.getKey();\n            List<PluginConfiguration.Source> sources = e.getValue();\n\n            if (match(token.getProfile(), sources)) {\n                roles.add(roleName);\n            }\n        }\n        return SecurityUtils.toAuthorizationInfo(principals, roles);\n    }\n\n    static boolean match(UserProfile profile, List<PluginConfiguration.Source> sources) {\n        for (PluginConfiguration.Source source : sources) {\n            String attr = source.attribute();\n            String pattern = source.pattern();\n            Object attrValue = profile.getAttribute(attr);\n            if (Matcher.matches(attrValue, pattern)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    private static boolean hasTeam(UUID teamId, TeamRole role, List<UserTeam> teams) {\n        return teams.stream().anyMatch(ut -> ut.teamId() == teamId && ut.role() == role);\n    }\n\n    @VisibleForTesting\n    static Map<String, List<PluginConfiguration.Source>> validateRoleMapping(Map<String, List<PluginConfiguration.Source>> input, RoleDao roleDao) {\n        for (Map.Entry<String, List<PluginConfiguration.Source>> entry : input.entrySet()) {\n            String roleName = entry.getKey();\n            if (roleDao.getId(roleName) == null) {\n                log.warn(\"validateRoleMapping -> possibly invalid OIDC role mapping for roleName={}, role not found. It will still be used during user authorization.\", roleName);\n            }\n        }\n        return input;\n    }\n\n    @VisibleForTesting\n    static Map<UUID, TeamMapping> validateTeamMapping(Map<UUID, TeamMapping> input, TeamDao teamDao) {\n        Map<UUID, TeamMapping> output = new HashMap<>();\n\n        for (Map.Entry<UUID, TeamMapping> entry : input.entrySet()) {\n            boolean valid = true;\n\n            UUID teamId = entry.getKey();\n            TeamMapping teamMapping = entry.getValue();\n\n            if (teamDao.get(teamId) == null) {\n                log.warn(\"validateTeamMapping -> invalid OIDC team mapping, teamId={} doesn't exist\", teamId);\n                valid = false;\n            }\n\n            for (PluginConfiguration.Source src : teamMapping.sources()) {\n                if (src.attribute() == null || src.attribute().isBlank()) {\n                    log.warn(\"validateTeamMapping -> invalid OIDC team mapping for teamId={}, empty source attribute name in {} mapping\", teamId, src);\n                    valid = false;\n                }\n            }\n\n            if (valid) {\n                output.put(teamId, teamMapping);\n            } else {\n                log.warn(\"validateTeamMapping -> removing invalid teamId={} to mapping={}. It will not be considered during user authorization.\", teamId, teamMapping);\n            }\n        }\n\n        return output;\n    }\n\n    static class OidcCredentialsMatcher implements CredentialsMatcher {\n\n        @Override\n        public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {\n            SimpleAccount account = (SimpleAccount) info;\n\n            OidcToken stored = (OidcToken) account.getCredentials();\n            OidcToken received = (OidcToken) token;\n\n            String a = stored.getProfile().accessToken();\n            String b = received.getProfile().accessToken();\n\n            return a != null && a.equals(b);\n        }\n    }\n}\n"
  },
  {
    "path": "server/plugins/oidc/src/main/java/com/walmartlabs/concord/server/plugins/oidc/OidcService.java",
    "content": "package com.walmartlabs.concord.server.plugins.oidc;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpRequest.BodyPublishers;\nimport java.net.http.HttpResponse.BodyHandlers;\nimport java.time.Duration;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static java.util.Objects.requireNonNull;\n\npublic class OidcService {\n\n    private final PluginConfiguration cfg;\n    private final HttpClient httpClient;\n    private final ObjectMapper objectMapper;\n    private volatile DiscoveryDocument discoveryDocument;\n\n    @Inject\n    public OidcService(PluginConfiguration cfg) {\n        this.cfg = requireNonNull(cfg);\n        this.httpClient = HttpClient.newBuilder()\n                .connectTimeout(Duration.ofSeconds(30))\n                .build();\n        this.objectMapper = new ObjectMapper();\n    }\n\n    public String buildAuthorizationUrl(String redirectUri, String state) {\n        var discovery = getDiscoveryDocument();\n        var scopes = cfg.getScopes() != null ? String.join(\" \", cfg.getScopes()) : \"openid email profile\";\n        return discovery.authorizationEndpoint() + \"?\" +\n               \"response_type=code\" +\n               \"&client_id=\" + urlEncode(cfg.getClientId()) +\n               \"&redirect_uri=\" + urlEncode(redirectUri) +\n               \"&scope=\" + urlEncode(scopes) +\n               \"&state=\" + urlEncode(state);\n    }\n\n    public UserProfile exchangeCodeForProfile(String code, String redirectUri) throws IOException {\n        var discovery = getDiscoveryDocument();\n\n        var tokenRequest = \"grant_type=authorization_code\" +\n                           \"&code=\" + urlEncode(code) +\n                           \"&redirect_uri=\" + urlEncode(redirectUri) +\n                           \"&client_id=\" + urlEncode(cfg.getClientId()) +\n                           \"&client_secret=\" + urlEncode(cfg.getSecret());\n\n        var request = HttpRequest.newBuilder()\n                .uri(URI.create(discovery.tokenEndpoint()))\n                .header(\"Content-Type\", \"application/x-www-form-urlencoded\")\n                .POST(BodyPublishers.ofString(tokenRequest))\n                .build();\n\n        try {\n            var response = httpClient.send(request, BodyHandlers.ofString());\n            if (response.statusCode() != 200) {\n                throw new IOException(\"Token exchange failed: \" + response.statusCode() + \" \" + response.body());\n            }\n\n            var tokenResponse = objectMapper.readTree(response.body());\n            var accessToken = tokenResponse.get(\"access_token\").asText();\n            if (accessToken == null) {\n                throw new IOException(\"No access token in response\");\n            }\n\n            return getUserInfo(accessToken, discovery.userinfoEndpoint());\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n            throw new IOException(\"Request interrupted\", e);\n        }\n    }\n\n    public UserProfile validateToken(String accessToken) throws IOException {\n        var discovery = getDiscoveryDocument();\n        return getUserInfo(accessToken, discovery.userinfoEndpoint());\n    }\n\n    private UserProfile getUserInfo(String accessToken, String userinfoEndpoint) throws IOException {\n        var request = HttpRequest.newBuilder()\n                .uri(URI.create(userinfoEndpoint))\n                .header(\"Authorization\", \"Bearer \" + accessToken)\n                .GET()\n                .build();\n\n        try {\n            var response = httpClient.send(request, BodyHandlers.ofString());\n            if (response.statusCode() != 200) {\n                throw new IOException(\"UserInfo request failed: \" + response.statusCode() + \" \" + response.body());\n            }\n\n            return UserProfileConverter.convert(objectMapper, response.body(), accessToken);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n            throw new IOException(\"Request interrupted\", e);\n        }\n    }\n\n    private DiscoveryDocument getDiscoveryDocument() {\n        if (discoveryDocument == null) {\n            synchronized (this) {\n                if (discoveryDocument == null) {\n                    discoveryDocument = fetchDiscoveryDocument();\n                }\n            }\n        }\n        return discoveryDocument;\n    }\n\n    private DiscoveryDocument fetchDiscoveryDocument() {\n        try {\n            var request = HttpRequest.newBuilder()\n                    .uri(URI.create(cfg.getDiscoveryUri()))\n                    .GET()\n                    .build();\n\n            var response = httpClient.send(request, BodyHandlers.ofString());\n            if (response.statusCode() != 200) {\n                throw new RuntimeException(\"Discovery document fetch failed: \" + response.statusCode());\n            }\n\n            var discovery = objectMapper.readTree(response.body());\n            return new DiscoveryDocument(\n                    discovery.get(\"authorization_endpoint\").asText(),\n                    discovery.get(\"token_endpoint\").asText(),\n                    discovery.get(\"userinfo_endpoint\").asText()\n            );\n        } catch (IOException | InterruptedException e) {\n            throw new RuntimeException(\"Failed to fetch OIDC discovery document\", e);\n        }\n    }\n\n    private static String urlEncode(String value) {\n        return URLEncoder.encode(value, UTF_8);\n    }\n\n    private record DiscoveryDocument(\n            String authorizationEndpoint,\n            String tokenEndpoint,\n            String userinfoEndpoint\n    ) {\n    }\n}\n"
  },
  {
    "path": "server/plugins/oidc/src/main/java/com/walmartlabs/concord/server/plugins/oidc/OidcToken.java",
    "content": "package com.walmartlabs.concord.server.plugins.oidc;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.apache.shiro.authc.AuthenticationToken;\n\nimport java.io.Serial;\nimport java.io.Serializable;\n\npublic class OidcToken implements AuthenticationToken, Serializable {\n\n    @Serial\n    private static final long serialVersionUID = 1L;\n\n    private final UserProfile profile;\n\n    public OidcToken(UserProfile profile) {\n        this.profile = profile;\n    }\n\n    public UserProfile getProfile() {\n        return profile;\n    }\n\n    @Override\n    public Object getPrincipal() {\n        return profile.id();\n    }\n\n    @Override\n    public Object getCredentials() {\n        return profile;\n    }\n\n    @Override\n    public String toString() {\n        return \"OidcToken{\" +\n                \"profile=\" + profile +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/plugins/oidc/src/main/java/com/walmartlabs/concord/server/plugins/oidc/PluginConfiguration.java",
    "content": "package com.walmartlabs.concord.server.plugins.oidc;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.typesafe.config.ConfigException;\nimport com.walmartlabs.concord.server.org.team.TeamRole;\nimport com.walmartlabs.concord.config.Config;\nimport org.eclipse.sisu.Nullable;\n\nimport javax.inject.Inject;\nimport java.util.*;\n\npublic class PluginConfiguration {\n\n    @Inject\n    @Config(\"oidc.enabled\")\n    private boolean enabled;\n\n    @Inject\n    @Config(\"oidc.clientId\")\n    private String clientId;\n\n    @Inject\n    @Config(\"oidc.secret\")\n    private String secret;\n\n    @Inject\n    @Config(\"oidc.discoveryUri\")\n    private String discoveryUri;\n\n    @Inject\n    @Config(\"oidc.urlBase\")\n    private String urlBase;\n\n    @Inject\n    @Config(\"oidc.afterLoginUrl\")\n    private String afterLoginUrl;\n\n    @Inject\n    @Config(\"oidc.afterLogoutUrl\")\n    private String afterLogoutUrl;\n\n    @Inject\n    @Config(\"oidc.onErrorUrl\")\n    private String onErrorUrl;\n\n    @Inject\n    @Nullable\n    @Config(\"oidc.scopes\")\n    private List<String> scopes;\n\n    private final Map<UUID, TeamMapping> teamMapping;\n\n    private final Map<String, List<Source>> roleMapping;\n\n    @Inject\n    public PluginConfiguration(@Config(\"oidc.teamMapping\") Map<String, Map<String, Object>> teamMapping,\n                               @Config(\"oidc.roleMapping\") Map<String, Map<String, Object>> roleMapping) {\n        this.teamMapping = toTeamMapping(teamMapping);\n        this.roleMapping = toRoleMapping(roleMapping);\n    }\n\n    public boolean isEnabled() {\n        return enabled;\n    }\n\n    public String getClientId() {\n        return clientId;\n    }\n\n    public String getSecret() {\n        return secret;\n    }\n\n    public String getDiscoveryUri() {\n        return discoveryUri;\n    }\n\n    public String getUrlBase() {\n        return urlBase;\n    }\n\n    public String getAfterLoginUrl() {\n        return afterLoginUrl;\n    }\n\n    public String getAfterLogoutUrl() {\n        return afterLogoutUrl;\n    }\n\n    public String getOnErrorUrl() {\n        return onErrorUrl;\n    }\n\n    public List<String> getScopes() {\n        return scopes;\n    }\n\n    public Map<UUID, TeamMapping> getTeamMapping() {\n        return teamMapping;\n    }\n\n    public Map<String, List<Source>> getRoleMapping() {\n        return roleMapping;\n    }\n\n    private static Map<UUID, TeamMapping> toTeamMapping(Map<String, Map<String, Object>> mapping) {\n        Map<UUID, TeamMapping> m = new HashMap<>();\n        for (Map.Entry<String, Map<String, Object>> e : mapping.entrySet()) {\n            String path = \"oidc.teamMapping.\" + e.getKey();\n            m.put(toUUID(path, e.getKey()), new TeamMapping(getSources(path, e.getValue()), getTeamRole(path, \"role\", e.getValue(), TeamRole.MEMBER)));\n        }\n        return m;\n    }\n\n    private static Map<String, List<Source>> toRoleMapping(Map<String, Map<String, Object>> mapping) {\n        Map<String, List<Source>> m = new HashMap<>();\n        for (Map.Entry<String, Map<String, Object>> e : mapping.entrySet()) {\n            String path = \"oidc.roleMapping.\" + e.getKey();\n            m.put(e.getKey(), getSources(path, e.getValue()));\n        }\n        return m;\n    }\n\n    private static UUID toUUID(String path, String value) {\n        try {\n            return UUID.fromString(value);\n        } catch (Exception e) {\n            throw new RuntimeException(path + \" invalid format '\" + value + \"'. expected UUID\");\n        }\n    }\n\n    private static List<Source> getSources(String path, Map<String, Object> m) {\n        List<String> raw = assertList(path, \"source\", m);\n        List<Source> result = new ArrayList<>(raw.size());\n        for (String r : raw) {\n            String[] sourcePattern = r.split(\"\\\\.\", 2);\n            if (sourcePattern.length != 2) {\n                throw new RuntimeException(path + \".source invalid format '\" + r + \"'. expected source.pattern\");\n            }\n            result.add(new Source(sourcePattern[0], sourcePattern[1]));\n        }\n        return result;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static List<String> assertList(String path, String key, Map<String, Object> m) {\n        Object raw = m.get(key);\n        if (raw == null) {\n            throw new ConfigException.Missing(path + \".\" + key);\n        }\n\n        if (!(raw instanceof List)) {\n            throw new RuntimeException(path + \".\" + key + \" has type \" + raw.getClass() + \" rather than list\");\n        }\n        return (List<String>) raw;\n    }\n\n    private static String getString(String path, String key, Map<String, Object> m) {\n        Object raw = m.get(key);\n        if (raw == null) {\n            return null;\n        }\n\n        if (!(raw instanceof String)) {\n            throw new RuntimeException(path + \".\" + key + \" has type \" + raw.getClass() + \" rather than string\");\n        }\n        return (String) raw;\n    }\n\n    private static TeamRole getTeamRole(String path, String key, Map<String, Object> m, TeamRole defaultRole) {\n        String value = getString(path, key, m);\n        if (value == null) {\n            return defaultRole;\n        }\n\n        return asTeamRole(path + \".\" + key, value);\n    }\n\n    private static TeamRole asTeamRole(String path, String value) {\n        try {\n            return TeamRole.valueOf(value.trim().toUpperCase());\n        } catch (IllegalArgumentException e) {\n            throw new RuntimeException(path + \" unknown role: '\" + value + \"'. Available roles: \" + Arrays.toString(TeamRole.values()));\n        }\n    }\n\n    public record Source(String attribute, String pattern) {\n    }\n\n    public record TeamMapping(List<Source> sources, TeamRole role) {\n    }\n}\n"
  },
  {
    "path": "server/plugins/oidc/src/main/java/com/walmartlabs/concord/server/plugins/oidc/UserProfile.java",
    "content": "package com.walmartlabs.concord.server.plugins.oidc;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serial;\nimport java.io.Serializable;\nimport java.util.Map;\n\npublic record UserProfile(\n        String id,\n        String email,\n        String displayName,\n        String accessToken,\n        Map<String, Object> attributes) implements Serializable {\n\n    @Serial\n    private static final long serialVersionUID = 1L;\n\n    public Object getAttribute(String name) {\n        return attributes != null ? attributes.get(name) : null;\n    }\n}\n"
  },
  {
    "path": "server/plugins/oidc/src/main/java/com/walmartlabs/concord/server/plugins/oidc/UserProfileConverter.java",
    "content": "package com.walmartlabs.concord.server.plugins.oidc;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\n\npublic final class UserProfileConverter {\n\n    public static UserProfile convert(ObjectMapper objectMapper, String json, String accessToken) throws JsonProcessingException {\n        var userInfo = objectMapper.readTree(json);\n        var id = userInfo.get(\"sub\").asText();\n        var email = userInfo.get(\"email\").asText();\n        var displayName = userInfo.get(\"name\").asText(email);\n        return new UserProfile(id, email, displayName, accessToken, objectMapper.convertValue(userInfo, new TypeReference<>() {\n        }));\n    }\n\n    private UserProfileConverter() {\n    }\n}\n"
  },
  {
    "path": "server/plugins/oidc/src/test/java/com/walmartlabs/concord/server/plugins/oidc/OidcRealmTest.java",
    "content": "package com.walmartlabs.concord.server.plugins.oidc;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.server.org.team.TeamDao;\nimport com.walmartlabs.concord.server.org.team.TeamEntry;\nimport com.walmartlabs.concord.server.org.team.TeamRole;\nimport com.walmartlabs.concord.server.plugins.oidc.PluginConfiguration.Source;\nimport com.walmartlabs.concord.server.plugins.oidc.PluginConfiguration.TeamMapping;\nimport com.walmartlabs.concord.server.role.RoleDao;\nimport com.walmartlabs.concord.server.security.SecurityUtils;\nimport org.apache.shiro.subject.SimplePrincipalCollection;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static io.smallrye.common.constraint.Assert.*;\nimport static org.mockito.ArgumentMatchers.eq;\nimport static org.mockito.Mockito.mock;\nimport static org.mockito.Mockito.when;\n\npublic class OidcRealmTest {\n\n    @Test\n    public void invalidRolesAreAllowed() {\n        var roleDao = mock(RoleDao.class);\n        when(roleDao.getId(eq(\"roleBaz\"))).thenReturn(UUID.randomUUID());\n\n        var input = Map.of(\n                \"roleFoo\", List.of(new Source(\"groups\", \".*\")),\n                \"roleBar\", List.of(new Source(\"groups\", \".*\")),\n                \"roleBaz\", List.of(new Source(\"groups\", \".*\")));\n\n        var output = OidcRealm.validateRoleMapping(input, roleDao);\n        assertTrue(output.containsKey(\"roleFoo\"));\n        assertTrue(output.containsKey(\"roleBar\"));\n        assertTrue(output.containsKey(\"roleBaz\"));\n    }\n\n    @Test\n    public void invalidTeamsAreIgnored() {\n        var teamIdFoo = UUID.randomUUID();\n        var teamIdBar = UUID.randomUUID();\n        var teamIdBaz = UUID.randomUUID();\n\n        var teamDao = mock(TeamDao.class);\n        when(teamDao.get(eq(teamIdBar))).thenReturn(new TeamEntry(teamIdBar, UUID.randomUUID(), \"bar\", \"bar\", \"bar\"));\n\n        var input = Map.of(\n                teamIdFoo, new TeamMapping(List.of(new Source(\"group\", \".*\")), TeamRole.MEMBER),\n                teamIdBar, new TeamMapping(List.of(new Source(\"group\", \".*\")), TeamRole.MEMBER),\n                teamIdBaz, new TeamMapping(List.of(new Source(\"group\", \".*\")), TeamRole.MEMBER));\n\n        var output = OidcRealm.validateTeamMapping(input, teamDao);\n        assertFalse(output.containsKey(teamIdFoo));\n        assertTrue(output.containsKey(teamIdBar));\n        assertFalse(output.containsKey(teamIdBaz));\n    }\n\n    @Test\n    public void testMapping() throws Exception {\n        var profile = \"\"\"\n                {\n                  \"sub\": \"1234567890\",\n                  \"email\": \"user@example.com\",\n                  \"name\": \"Test User\",\n                  \"groups\": [\n                    \"admins\",\n                    \"dev\",\n                    \"testers\"\n                  ]\n                }\"\"\";\n\n        var userProfile = UserProfileConverter.convert(new ObjectMapper(), profile, \"accessToken\");\n\n        assertTrue(OidcRealm.match(userProfile, List.of(new Source(\"groups\", \".*admins.*\"))));\n        assertTrue(OidcRealm.match(userProfile, List.of(new Source(\"groups\", \"dev\"))));\n        assertTrue(OidcRealm.match(userProfile, List.of(new Source(\"groups\", \"test.*\"))));\n        assertTrue(OidcRealm.match(userProfile, List.of(new Source(\"groups\", \".*\"))));\n\n        assertFalse(OidcRealm.match(userProfile, List.of(new Source(\"groups\", \".*superadmins.*\"))));\n    }\n\n    @Test\n    public void testSerialization() throws Exception {\n        var profile = \"\"\"\n                {\n                  \"sub\": \"1234567890\",\n                  \"email\": \"user@example.com\",\n                  \"name\": \"Test User\",\n                  \"groups\": [\n                    \"admins\",\n                    \"dev\",\n                    \"testers\"\n                  ]\n                }\"\"\";\n\n        var userProfile = UserProfileConverter.convert(new ObjectMapper(), profile, \"accessToken\");\n\n        var spc = new SimplePrincipalCollection();\n        spc.add(new OidcToken(userProfile), \"oidc\");\n        var bytes = SecurityUtils.serialize(spc);\n        assertNotNull(bytes);\n    }\n}\n"
  },
  {
    "path": "server/plugins/oneops/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.server.plugins</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-oneops-plugin</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-common</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server-impl</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server-db</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.sisu</groupId>\n            <artifactId>org.eclipse.sisu.inject</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-config</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.jboss.spec.javax.ws.rs</groupId>\n            <artifactId>jboss-jaxrs-api_2.1_spec</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.jooq</groupId>\n            <artifactId>jooq</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "server/plugins/oneops/src/main/java/com/walmartlabs/concord/server/plugins/oneops/Constants.java",
    "content": "package com.walmartlabs.concord.server.plugins.oneops;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic final class Constants {\n\n    public static final String EVENT = \"oneops\";\n\n    public Constants() {\n    }\n}\n"
  },
  {
    "path": "server/plugins/oneops/src/main/java/com/walmartlabs/concord/server/plugins/oneops/OneOpsConfiguration.java",
    "content": "package com.walmartlabs.concord.server.plugins.oneops;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\n\npublic class OneOpsConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"oneops.logEvents\")\n    private boolean logEvents;\n\n    public boolean isLogEvents() {\n        return logEvents;\n    }\n}\n"
  },
  {
    "path": "server/plugins/oneops/src/main/java/com/walmartlabs/concord/server/plugins/oneops/OneOpsEventResource.java",
    "content": "package com.walmartlabs.concord.server.plugins.oneops;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.walmartlabs.concord.server.audit.AuditAction;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.audit.AuditObject;\nimport com.walmartlabs.concord.server.events.Event;\nimport com.walmartlabs.concord.server.events.EventInitiatorSupplier;\nimport com.walmartlabs.concord.server.events.TriggerEventInitiatorResolver;\nimport com.walmartlabs.concord.server.events.TriggerProcessExecutor;\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.sdk.PartialProcessKey;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.rest.Resource;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\nimport javax.ws.rs.Consumes;\nimport javax.ws.rs.POST;\nimport javax.ws.rs.Path;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport javax.ws.rs.core.Response.Status;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.walmartlabs.concord.common.MemoSupplier.memo;\n\n/**\n * Handles external OneOps events.\n * At the moment supports only VM replacement events. Other event types might\n * require additional support code in {@link OneOpsTriggerProcessor}.\n */\n@Named\n@Singleton\n@Path(\"/api/v1/events\")\npublic class OneOpsEventResource implements Resource {\n\n    private static final Logger log = LoggerFactory.getLogger(OneOpsEventResource.class);\n\n    private static final String EVENT_SOURCE = \"oneops\";\n\n    private final ObjectMapper objectMapper;\n    private final TriggerProcessExecutor executor;\n    private final UserManager userManager;\n    private final TriggerEventInitiatorResolver initiatorResolver;\n    private final List<OneOpsTriggerProcessor> processors;\n    private final AuditLog auditLog;\n    private final OneOpsConfiguration cfg;\n\n    @Inject\n    public OneOpsEventResource(ObjectMapper objectMapper,\n                               TriggerProcessExecutor executor,\n                               UserManager userManager,\n                               TriggerEventInitiatorResolver initiatorResolver,\n                               List<OneOpsTriggerProcessor> processors,\n                               AuditLog auditLog,\n                               OneOpsConfiguration cfg) {\n        this.objectMapper = objectMapper;\n        this.executor = executor;\n        this.userManager = userManager;\n        this.initiatorResolver = initiatorResolver;\n        this.processors = processors;\n        this.auditLog = auditLog;\n        this.cfg = cfg;\n    }\n\n    @POST\n    @Path(\"/oneops\")\n    @Consumes(MediaType.APPLICATION_JSON)\n    @WithTimer\n    public Response event(Map<String, Object> event) {\n        if (executor.isDisabled(EVENT_SOURCE)) {\n            log.warn(\"event ['{}'] disabled\", EVENT_SOURCE);\n            return Response.ok().build();\n        }\n\n        if (event == null || event.isEmpty()) {\n            return Response.status(Status.BAD_REQUEST).build();\n        }\n\n        if (cfg.isLogEvents()) {\n            auditLog.add(AuditObject.EXTERNAL_EVENT, AuditAction.ACCESS)\n                    .field(\"source\", EVENT_SOURCE)\n                    .field(\"eventId\", String.valueOf(event.get(\"cmsId\")))\n                    .field(\"payload\", event)\n                    .log();\n        }\n\n        List<OneOpsTriggerProcessor.Result> results = new ArrayList<>();\n        processors.forEach(p -> p.process(event, results));\n\n        for (OneOpsTriggerProcessor.Result result : results) {\n            Event e = Event.builder()\n                    .id(String.valueOf(event.get(\"cmsId\")))\n                    .name(EVENT_SOURCE)\n                    .attributes(result.event())\n                    // \"author\" property is set by OneOpsTriggerProcessor\n                    // TODO replace with a custom Supplier<UserEntry> that directly looks at the event's \"createdBy\"\n                    .initiator(memo(new EventInitiatorSupplier(\"author\", userManager, result.event())))\n                    .build();\n\n            List<PartialProcessKey> processKeys = executor.execute(e, initiatorResolver, result.triggers());\n            log.info(\"event ['{}'] -> done, {} processes started\", e.id(), processKeys.size());\n        }\n\n        return Response.ok().build();\n    }\n\n    @POST\n    @Path(\"/oneops\")\n    @Consumes(MediaType.WILDCARD)\n    @WithTimer\n    @SuppressWarnings(\"unchecked\")\n    public Response event(InputStream in) {\n        Map<String, Object> m;\n        try {\n            m = objectMapper.readValue(in, Map.class);\n        } catch (IOException e) {\n            throw new ConcordApplicationException(\"Error while reading JSON data: \" + e.getMessage(), e);\n        }\n\n        return event(m);\n    }\n}\n"
  },
  {
    "path": "server/plugins/oneops/src/main/java/com/walmartlabs/concord/server/plugins/oneops/OneOpsTriggerProcessor.java",
    "content": "package com.walmartlabs.concord.server.plugins.oneops;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.events.DefaultEventFilter;\nimport com.walmartlabs.concord.server.org.triggers.TriggerEntry;\nimport com.walmartlabs.concord.server.org.triggers.TriggersDao;\n\nimport java.util.*;\n\npublic abstract class OneOpsTriggerProcessor {\n\n    private static final String ORG_KEY = \"org\";\n    private static final String ASM_KEY = \"asm\";\n    private static final String ENV_KEY = \"env\";\n    private static final String PLATFORM_KEY = \"platform\";\n    private static final String STATE_KEY = \"state\";\n    private static final String TYPE_KEY = \"type\";\n    private static final String COMPONENT_KEY = \"component\";\n    private static final String SOURCE_KEY = \"source\";\n    private static final String SUBJECT_KEY = \"subject\";\n    private static final String DEPLOYMENT_STATE_KEY = \"deploymentState\";\n    private static final String IPS_KEY = \"ips\";\n    private static final String AUTHOR_KEY = \"author\";\n    private static final String EVENT_SOURCE = \"oneops\";\n\n    private final TriggersDao triggersDao;\n    private final int version;\n\n    public OneOpsTriggerProcessor(TriggersDao triggersDao, int version) {\n        this.triggersDao = triggersDao;\n        this.version = version;\n    }\n\n    public void process(Map<String, Object> event, List<Result> result) {\n        List<TriggerEntry> triggers = triggersDao.list(EVENT_SOURCE, version);\n\n        Map<String, Object> triggerConditions = buildConditions(event);\n        enrichTriggerConditions(triggerConditions, version);\n        Map<String, Object> triggerEvent = buildTriggerEvent(event, triggerConditions);\n\n        for (TriggerEntry triggerEntry : triggers) {\n            if (DefaultEventFilter.filter(triggerConditions, triggerEntry)) {\n                result.add(Result.from(triggerEvent, triggerEntry));\n            }\n        }\n    }\n\n    private static void enrichTriggerConditions(Map<String, Object> triggerConditions, Integer version) {\n        triggerConditions.put(\"version\", version);\n    }\n\n    private static Map<String, Object> buildTriggerEvent(Map<String, Object> event,\n                                                         Map<String, Object> conditions) {\n\n        Map<String, Object> result = new HashMap<>(conditions);\n        result.put(\"payload\", event);\n        return result;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Map<String, Object> buildConditions(Map<String, Object> event) {\n        Map<String, Object> cis = getCis(event);\n        Map<String, Object> payload = (Map<String, Object>) event.get(\"payload\");\n\n        Map<String, Object> result = new HashMap<>();\n        result.put(STATE_KEY, get(\"ciState\", cis));\n        result.put(COMPONENT_KEY, get(\"ciClassName\", cis));\n        result.put(TYPE_KEY, get(\"type\", event));\n        result.put(SOURCE_KEY, get(\"source\", event));\n        result.put(SUBJECT_KEY, get(\"subject\", event));\n        result.put(DEPLOYMENT_STATE_KEY, get(\"deploymentState\", payload));\n\n        result.put(IPS_KEY, getIPs(event));\n        result.put(AUTHOR_KEY, payload.get(\"createdBy\"));\n\n        // example: /testing/twst/localtest/bom/sts/1\n        //          / org   /asm /env      /.../platform/...\n        String[] nsPath = getNsPath(event);\n        if (nsPath != null) {\n            addKey(ORG_KEY, nsPath, 0, result);\n            addKey(ASM_KEY, nsPath, 1, result);\n            addKey(ENV_KEY, nsPath, 2, result);\n            // ignore bom\n            addKey(PLATFORM_KEY, nsPath, 4, result);\n        }\n\n        return result;\n    }\n\n    private static String get(String key, Map<String, Object> event) {\n        return String.valueOf(event.get(key));\n    }\n\n    private static String[] getNsPath(Map<String, Object> event) {\n        Map<String, Object> cis = getCis(event);\n        if (cis == null) {\n            return null;\n        }\n\n        String nsPath = (String) cis.get(\"nsPath\");\n        if (nsPath == null) {\n            return new String[0];\n        }\n\n        if (nsPath.startsWith(\"/\")) {\n            nsPath = nsPath.substring(1);\n        }\n\n        return nsPath.split(\"/\");\n    }\n\n    private static Map<String, Object> getCis(Map<String, Object> event) {\n        List<Map<String, Object>> cisItems = getCisItems(event);\n        if (cisItems.isEmpty()) {\n            return Collections.emptyMap();\n        }\n        return cisItems.get(0);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static List<Map<String, Object>> getCisItems(Map<String, Object> event) {\n        Object cisElement = event.get(\"cis\");\n        if (cisElement == null) {\n            return Collections.emptyList();\n        }\n\n        if (cisElement instanceof List) {\n            return (List<Map<String, Object>>) cisElement;\n        }\n\n        return Collections.emptyList();\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private static Set<String> getIPs(Map<String, Object> event) {\n        List<Map<String, Object>> cisItems = getCisItems(event);\n        if (cisItems.isEmpty()) {\n            return Collections.emptySet();\n        }\n\n        Set<String> result = new HashSet<>();\n        for (Map<String, Object> i : cisItems) {\n            Object attrs = i.get(\"ciAttributes\");\n            if (attrs == null) {\n                continue;\n            }\n\n            Map<String, Object> m = (Map<String, Object>) attrs;\n            Object v = m.get(\"public_ip\");\n            if (v == null) {\n                continue;\n            }\n\n            result.add(v.toString());\n        }\n        return result;\n    }\n\n    private static void addKey(String key, String[] values, int valueIndex, Map<String, Object> result) {\n        if (values.length > valueIndex) {\n            result.put(key, values[valueIndex]);\n        }\n    }\n\n    public static class Result {\n\n        private final Map<String, Object> event;\n\n        private final List<TriggerEntry> triggers;\n\n        public Result(Map<String, Object> event, List<TriggerEntry> triggers) {\n            this.event = event;\n            this.triggers = triggers;\n        }\n\n        public Map<String, Object> event() {\n            return event;\n        }\n\n        public List<TriggerEntry> triggers() {\n            return triggers;\n        }\n\n        static Result from(Map<String, Object> event, TriggerEntry trigger) {\n            return new OneOpsTriggerProcessor.Result(event, Collections.singletonList(trigger));\n        }\n    }\n}"
  },
  {
    "path": "server/plugins/oneops/src/main/java/com/walmartlabs/concord/server/plugins/oneops/OneOpsTriggerV1Processor.java",
    "content": "package com.walmartlabs.concord.server.plugins.oneops;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.org.triggers.TriggersDao;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\n\n/**\n * Prepares trigger conditions for OneOps v1 triggers.\n * The actual implementation is the same as for v2 (see {@link OneOpsTriggerV2Processor}.\n * The only reason it is split into two different @Named classes is to keep\n * the resource's code as close to GitHub's as possible.\n */\n@Named\n@Singleton\n@Deprecated\npublic class OneOpsTriggerV1Processor extends OneOpsTriggerProcessor {\n\n    @Inject\n    public OneOpsTriggerV1Processor(TriggersDao triggersDao) {\n        super(triggersDao, 1);\n    }\n}\n"
  },
  {
    "path": "server/plugins/oneops/src/main/java/com/walmartlabs/concord/server/plugins/oneops/OneOpsTriggerV2Processor.java",
    "content": "package com.walmartlabs.concord.server.plugins.oneops;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.org.triggers.TriggersDao;\n\nimport javax.inject.Inject;\nimport javax.inject.Named;\nimport javax.inject.Singleton;\n\n/**\n * Prepares trigger conditions for OneOps v2 triggers.\n * The actual implementation is the same as for v1 (see {@link OneOpsTriggerV1Processor}.\n * The only reason it is split into two different @Named classes is to keep\n * the resource's code as close to GitHub's as possible.\n */\n@Named\n@Singleton\npublic class OneOpsTriggerV2Processor extends OneOpsTriggerProcessor {\n\n    @Inject\n    public OneOpsTriggerV2Processor(TriggersDao triggersDao) {\n        super(triggersDao, 2);\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/README.md",
    "content": "# PingFederate SSO plugin\n\nAdds support for authentication using [PingFederate](https://www.pingidentity.com/en/software/pingfederate.html)\n\n## Usage\n\nMust be included into the server's [dist](../../dist) module.\n\nAdd the necessary parameters to the Server's configuration file. Example:\n\n```\nconcord-server {\n    sso {        \n        pfed {\n             enabled = true\n        }\n        authEndpointUrl = \"https://pfed.example.com/as/authorization.oauth2\"\n        tokenEndpointUrl = \"https://pfed.example.com/as/token.oauth2\"\n        logoutEndpointUrl = \"https://pfed.example.com/as/revoke_token.oauth2\"\n        redirectUrl = \"https://concord.example.com/api/service/sso/redirect\"\n        userInfoEndpointUrl = \"https://pfed.example.com/idp/userinfo.openid\"\n        tokenSigningKeyUrl = \"https://pfed.example.com/pf/JWKS\"\n        clientId = \"**************************\"\n        clientSecret = \"*****************************\"\n        tokenServiceReadTimeout = 10000\n        tokenServiceConnectTimeout = 10000\n        validateNonce = false\n        tokenSignatureValidation = true # Option to enable or disable token signature validation\n        tokenSigningKey = null  # providing direct key here overrides the remote signing key\n        autoCreateUsers = false\n    }\n}\n```\n\nClick [here](https://docs.pingidentity.com/bundle/developer/page/erq1601508087286.html#developer-OpenIDConnect10DeveloperGuide-7)\nfor developer guide to implement OpenID Connect with PingFederate\n\n### Interactive Login\n\nConfigure the Concord Console to use custom logout/login URLs:\n\n- create a `cfg.js` (use [the default file](../../../console2/public/cfg.js) as an example):\n  ```javascript\n  window.concord = {\n      loginUrl: '/api/service/sso/auth',\n      logoutUrl: '/api/service/sso/logout'\n  };\n  ```\n- mount in into the Server's container:\n  ```\n  docker run ... -v /path/to/cfg.js:/opt/concord/console/cfg.js:ro walmartlabs/concord-server\n  ```\n\nWhen accessing the Concord Console you should be redirected to the pingfederate sign-in page.\n"
  },
  {
    "path": "server/plugins/pfed-sso/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>parent</artifactId>\n        <groupId>com.walmartlabs.concord.server.plugins</groupId>\n        <version>2.40.1-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>pfed-sso</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.nimbusds</groupId>\n            <artifactId>nimbus-jose-jwt</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>net.minidev</groupId>\n            <artifactId>json-smart</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server-impl</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.inject</groupId>\n            <artifactId>guice</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.sisu</groupId>\n            <artifactId>org.eclipse.sisu.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.servlet</groupId>\n            <artifactId>javax.servlet-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.shiro</groupId>\n            <artifactId>shiro-core</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.shiro</groupId>\n            <artifactId>shiro-web</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty</groupId>\n            <artifactId>jetty-util</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.httpcomponents</groupId>\n            <artifactId>httpcore</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.errorprone</groupId>\n            <artifactId>error_prone_annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-config</artifactId>\n            <scope>provided</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/AbstractHttpFilter.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.servlet.*;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\npublic abstract class AbstractHttpFilter implements Filter {\n\n    @Override\n    public void init(FilterConfig filterConfig) {\n        // do nothing\n    }\n\n    @Override\n    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {\n        HttpServletResponse resp = (HttpServletResponse) response;\n        HttpServletRequest req = (HttpServletRequest) request;\n\n        doFilter(req, resp, chain);\n    }\n\n    @Override\n    public void destroy() {\n        // do nothing\n    }\n\n    protected abstract void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException;\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/JwkHelper.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.nimbusds.jose.JOSEException;\nimport com.nimbusds.jose.jwk.ECKey;\nimport com.nimbusds.jose.jwk.OctetSequenceKey;\nimport com.nimbusds.jose.jwk.RSAKey;\nimport net.minidev.json.JSONObject;\n\nimport java.nio.charset.StandardCharsets;\nimport java.security.KeyPair;\nimport java.text.ParseException;\n\npublic final class JwkHelper {\n\n    /**\n     * Build the secret from the JWK JSON.\n     *\n     * @param json json\n     * @return secret\n     */\n    public static String buildSecretFromJwk(JSONObject json) {\n        try {\n            OctetSequenceKey octetSequenceKey = OctetSequenceKey.parse(json);\n            return new String(octetSequenceKey.toByteArray(), StandardCharsets.UTF_8);\n        } catch (ParseException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    /**\n     * Build the RSA key pair from the JWK JSON.\n     *\n     * @param json json\n     * @return key pair\n     */\n    public static KeyPair buildRSAKeyPairFromJwk(JSONObject json) {\n        try {\n            RSAKey rsaKey = RSAKey.parse(json);\n            return rsaKey.toKeyPair();\n        } catch (JOSEException | ParseException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    /**\n     * Build the EC key pair from the JWK JSON.\n     *\n     * @param json json\n     * @return key pair\n     */\n    public static KeyPair buildECKeyPairFromJwk(JSONObject json) {\n        try {\n            ECKey ecKey = ECKey.parse(json);\n            return ecKey.toKeyPair();\n        } catch (JOSEException | ParseException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/JwtAuthenticator.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.nimbusds.jose.JOSEException;\nimport com.nimbusds.jwt.*;\nimport com.walmartlabs.concord.server.plugins.pfedsso.encryption.EncryptionConfiguration;\nimport com.walmartlabs.concord.server.plugins.pfedsso.encryption.EncryptionConfigurationFactory;\nimport com.walmartlabs.concord.server.plugins.pfedsso.signature.SignatureConfiguration;\nimport com.walmartlabs.concord.server.plugins.pfedsso.signature.SignatureConfigurationFactory;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport java.io.IOException;\nimport java.text.ParseException;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.List;\n\n\npublic class JwtAuthenticator {\n\n    private static final Logger log = LoggerFactory.getLogger(JwtAuthenticator.class);\n\n    private final EncryptionConfiguration encryptionConfiguration;\n    private final SignatureConfiguration signatureConfiguration;\n    private final SsoClient ssoClient;\n    private final SsoConfiguration cfg;\n\n    private enum ALGOTYPE {\n        RSA(\"RS256\"),\n        EC(\"ES256\");\n\n        private final String algorithm;\n\n        ALGOTYPE(String algorithm) {\n            this.algorithm = algorithm;\n        }\n\n        public String getAlgorithm() {\n            return this.algorithm;\n        }\n    }\n\n    @Inject\n    public JwtAuthenticator(SsoConfiguration cfg, SsoClient ssoClient) {\n        this.encryptionConfiguration = EncryptionConfigurationFactory.create(cfg.getTokenEncryptionKey());\n        this.signatureConfiguration = SignatureConfigurationFactory.create(cfg.getTokenSigningKey());\n        this.ssoClient = ssoClient;\n        this.cfg = cfg;\n    }\n\n    /**\n     * Check is the JWT valid\n     *\n     * @param token the JWT\n     * @return <code>true</code> if token valid and not expired\n     */\n    public boolean isTokenValid(String token, boolean restrictOnClientId) {\n        return isTokenValid(token, null, restrictOnClientId);\n    }\n\n    /**\n     * Check is the JWT valid\n     *\n     * @param token JWT\n     * @param nonce nonce\n     * @return <code>true</code> if token valid, correct nonce and not expired\n     */\n    public boolean isTokenValid(String token, String nonce, boolean restrictOnClientId) {\n        try {\n            Map<String, Object> claims = validateTokenAndGetClaims(token);\n            if (claims == null) {\n                return false;\n            }\n\n            if (restrictOnClientId) {\n                List<String> allowedClientIds = cfg.getAllowedClientIds();\n                String clientId = (String) claims.get(\"client_id\");\n                if(!allowedClientIds.contains(clientId)) {\n                    log.warn(\"isTokenValid ['{}', '{}'] -> clientId not in allowed list for bearer tokens\", token, clientId);\n                    return false;\n                }\n            }\n\n            if (nonce == null) {\n                return true;\n            }\n\n            String claimsNonce = (String) claims.get(\"nonce\");\n            if (claimsNonce == null) {\n                log.error(\"isTokenValid ['{}', '{}'] -> claims without nonce\", token, nonce);\n                return false;\n            }\n\n            return nonce.equals(claimsNonce);\n        } catch (Exception e) {\n            log.error(\"isTokenValid ['{}', '{}'] -> error\", token, nonce, e);\n            return false;\n        }\n    }\n\n    public Map<String, Object> validateTokenAndGetClaims(String token) {\n        try {\n            JWT jwt = validateToken(token);\n            if (jwt == null) {\n                return null;\n            }\n\n            return createClaims(jwt);\n        } catch (Exception e) {\n            log.error(\"validateTokenAndGetClaims ['{}'] -> error\", token, e);\n            return null;\n        }\n    }\n\n    private JWT validateToken(String token) throws ParseException, JOSEException, IOException {\n        JWT jwt = JWTParser.parse(token);\n\n        if (jwt instanceof PlainJWT) {\n            if (signatureConfiguration == null) {\n                log.debug(\"validateToken ['{}'] -> JWT is not signed and no signature configuration\", token);\n                return jwt;\n            } else {\n                log.error(\"validateToken ['{}'] -> non-signed JWT and signature configuration\", token);\n                return null;\n            }\n        }\n\n        SignedJWT signedJWT = null;\n        if (jwt instanceof SignedJWT) {\n            signedJWT = (SignedJWT) jwt;\n        }\n\n        if (jwt instanceof EncryptedJWT) {\n            if (encryptionConfiguration == null) {\n                log.error(\"validateToken ['{}'] -> JWT is encrypted and no encryption configuration\", token);\n                return null;\n            }\n\n            final EncryptedJWT encryptedJWT = (EncryptedJWT) jwt;\n\n            encryptionConfiguration.decrypt(encryptedJWT);\n            signedJWT = encryptedJWT.getPayload().toSignedJWT();\n            if (signedJWT != null) {\n                jwt = signedJWT;\n            }\n        }\n\n        if (signedJWT != null && cfg.isTokenSignatureValidation()) {\n            if (signatureConfiguration == null) {\n                String kid = signedJWT.getHeader().getKeyID();\n                String alg = signedJWT.getHeader().getAlgorithm().getName();\n                if (kid == null || alg == null) {\n                    log.error(\"validateToken ['{}'] -> JWT is signed and no signature configuration\", token);\n                    return null;\n                }\n                return getSigningKeyFromServerAndVerify(signedJWT, kid, alg);\n            }\n\n            boolean verified = signatureConfiguration.verify(signedJWT);\n            if (!verified) {\n                return null;\n            }\n        }\n        return jwt;\n    }\n\n    private JWT getSigningKeyFromServerAndVerify(SignedJWT signedJWT, String kid, String alg) throws IOException, JOSEException {\n        String tokenSigningKey = getMatchedSigningKey(ssoClient.getTokenSigningKey(), kid, alg);\n        if (tokenSigningKey == null) {\n            log.error(\"validateToken ['{}'] -> JWT is signed and no signature configuration found from remote\", signedJWT);\n            return null;\n        }\n        SignatureConfiguration signatureConfiguration = SignatureConfigurationFactory.create(tokenSigningKey);\n        return signatureConfiguration.verify(signedJWT) ? signedJWT : null;\n    }\n\n    private String getMatchedSigningKey(String tokenSigningKeys, String kid, String alg) {\n        if (tokenSigningKeys == null)\n            return null;\n        JsonObject mapping = JsonParser.parseString(tokenSigningKeys).getAsJsonObject();\n        JsonElement keysJson = mapping.get(\"keys\");\n        JsonArray keysArray = keysJson.getAsJsonArray();\n        for (JsonElement jsonElement : keysArray) {\n            String algorithm;\n            String jsonKid = jsonElement.getAsJsonObject().get(\"kid\").getAsString();\n            String algoType = jsonElement.getAsJsonObject().get(\"kty\").getAsString();\n            try {\n                algorithm = ALGOTYPE.valueOf(algoType).getAlgorithm();\n            } catch (IllegalArgumentException e) {\n                log.warn(\"Algorithm for type: {} not found...\", algoType);\n                continue;\n            }\n            if (alg.equals(algorithm)\n                    && jsonKid.equals(kid)) {\n                return jsonElement.toString();\n            }\n        }\n        return null;\n    }\n\n    private Map<String, Object> createClaims(JWT jwt) throws ParseException {\n        JWTClaimsSet claimSet = jwt.getJWTClaimsSet();\n\n        Date expTime = claimSet.getExpirationTime();\n        if (expTime != null) {\n            Date now = new Date();\n            if (expTime.before(now)) {\n                log.debug(\"createClaims ['{}'] -> JWT expired\", jwt);\n                return null;\n            }\n        }\n\n        return new HashMap<>(claimSet.getClaims());\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/PluginModule.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.walmartlabs.concord.server.boot.FilterChainConfigurator;\nimport com.walmartlabs.concord.server.boot.filters.AuthenticationHandler;\nimport com.walmartlabs.concord.server.user.UserInfoProvider;\nimport org.apache.shiro.realm.Realm;\n\nimport javax.inject.Named;\n\nimport static com.google.inject.Scopes.SINGLETON;\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\n\n@Named\npublic class PluginModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        newSetBinder(binder, UserInfoProvider.class).addBinding().to(SsoUserInfoProvider.class);\n        newSetBinder(binder, FilterChainConfigurator.class).addBinding().to(SsoFilterChainConfigurator.class);\n        newSetBinder(binder, AuthenticationHandler.class).addBinding().to(SsoHandler.class);\n        newSetBinder(binder, Realm.class).addBinding().to(SsoRealm.class);\n\n        binder.bind(JwtAuthenticator.class).in(SINGLETON);\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/RedirectHelper.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.eclipse.jetty.util.URIUtil;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.net.URL;\n\npublic class RedirectHelper {\n\n    private static final Logger log = LoggerFactory.getLogger(RedirectHelper.class);\n\n    private final String redirectHost;\n\n    @Inject\n    public RedirectHelper(SsoConfiguration cfg) {\n        this.redirectHost = getBaseUrl(cfg.getRedirectUrl());\n    }\n\n    public void sendRedirect(HttpServletResponse response, String location) throws IOException {\n        response.sendRedirect(response.encodeRedirectURL(normalizeLocation(location)));\n    }\n\n    public void redirectToLoginOnError(HttpServletResponse response, String destination, String errorMsg) throws IOException {\n        log.warn(\"Error during sso login -> '{}'\", errorMsg);\n        sendRedirect(response, String.format(\"/#/login?from=%s\", getPathName(destination.trim())));\n    }\n\n    private String normalizeLocation(String location) {\n        if (URIUtil.hasScheme(location)) {\n            return location;\n        }\n\n        return concatPath(this.redirectHost, location);\n    }\n\n    private static String concatPath(String host, String location) {\n        if (host == null) {\n            return location;\n        }\n\n        if (location.startsWith(\"/\")) {\n            return host + location;\n        } else {\n            return host + \"/\" + location;\n        }\n    }\n\n    private static String getBaseUrl(String s) {\n        if (s == null || s.trim().isEmpty()) {\n            return null;\n        }\n\n        URL url;\n        try {\n            url = new URL(s);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n\n        if (url.getPort() == -1) {\n            return url.getProtocol() + \"://\" + url.getHost();\n        } else {\n            return url.getProtocol() + \"://\" + url.getHost() + \":\" + url.getPort();\n        }\n    }\n\n    private String getPathName(String s) {\n        if (s == null || s.trim().isEmpty()) {\n            return null;\n        }\n\n        if (s.startsWith(\"/#\")) {\n            return s.replaceFirst(\"^/#\", \"\");\n        }\n\n        if (s.startsWith(\"#\")) {\n            return s.replaceFirst(\"^#\", \"\");\n        }\n\n        return s;\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/SsoAuthFilter.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.servlet.FilterChain;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport javax.servlet.http.HttpSession;\nimport java.io.IOException;\nimport java.math.BigInteger;\nimport java.util.concurrent.ThreadLocalRandom;\n\npublic class SsoAuthFilter extends AbstractHttpFilter {\n\n    private static final Logger log = LoggerFactory.getLogger(SsoAuthFilter.class);\n\n    private final SsoConfiguration cfg;\n    private final JwtAuthenticator jwtAuthenticator;\n    private final RedirectHelper redirectHelper;\n\n    @Inject\n    public SsoAuthFilter(SsoConfiguration cfg, JwtAuthenticator jwtAuthenticator, RedirectHelper redirectHelper) {\n        this.cfg = cfg;\n        this.jwtAuthenticator = jwtAuthenticator;\n        this.redirectHelper = redirectHelper;\n    }\n\n    @Override\n    public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException {\n        if (!cfg.isEnabled()) {\n            response.sendError(HttpServletResponse.SC_BAD_REQUEST, \"sso disabled\");\n            return;\n        }\n\n        String from = request.getParameter(\"from\");\n        if (from == null || from.trim().isEmpty()) {\n            from = \"/\";\n        }\n        \n        // do not redirect if refresh token is not null as it is invalid after one use. Unfortunately no way to validate RefreshToken\n        // TODO: self-refresh using refresh token\n        String token = SsoCookies.getTokenCookie(request);\n        String refreshToken = SsoCookies.getRefreshCookie(request);\n        \n        if (token != null) {\n            if (refreshToken == null){\n                boolean isValid = jwtAuthenticator.isTokenValid(token, false);\n                if (isValid) {\n                    log.info(\"doFilter -> found valid token in cookies, redirect to '{}'\", from);\n                    redirectHelper.sendRedirect(response, from);\n                    return;\n                }\n            }\n            SsoCookies.removeTokenCookie(response);\n        }\n\n        if (refreshToken != null) {\n            SsoCookies.removeRefreshTokenCookie(response);\n        }\n\n        SsoCookies.addFromCookie(from, response);\n\n        String nonce = generateNonce();\n        String state = generateState();\n        if (cfg.isValidateNonce()) {\n            HttpSession session = request.getSession();\n            session.setAttribute(\"ssoNonce\", nonce);\n            session.setAttribute(\"ssoState\", state);\n        }\n\n        redirectHelper.sendRedirect(response, getAuthzUrl(nonce, state));\n    }\n\n    private String getAuthzUrl(String nonce, String state) {\n        return String.format(\"%s?response_type=code\" +\n                        \"&scope=openid+profile+full\" +\n                        \"&client_id=%s\" +\n                        \"&redirect_uri=%s\" +\n                        \"&nonce=%s\" +\n                        \"&state=%s\",\n                cfg.getAuthEndPointUrl(), cfg.getClientId(), cfg.getRedirectUrl(), nonce, state);\n    }\n\n    private static String generateNonce() {\n        return new BigInteger(50, ThreadLocalRandom.current()).toString(16);\n    }\n\n    private static String generateState() {\n        return new BigInteger(50, ThreadLocalRandom.current()).toString(16);\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/SsoCallbackFilter.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport javax.inject.Inject;\nimport javax.servlet.FilterChain;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\nimport static com.walmartlabs.concord.server.plugins.pfedsso.SsoCookies.REFRESH_TOKEN_COOKIE;\nimport static com.walmartlabs.concord.server.plugins.pfedsso.SsoCookies.TOKEN_COOKIE;\n\npublic class SsoCallbackFilter extends AbstractHttpFilter {\n\n    private final SsoConfiguration cfg;\n    private final SsoClient ssoClient;\n    private final RedirectHelper redirectHelper;\n\n    @Inject\n    public SsoCallbackFilter(SsoConfiguration cfg, SsoClient ssoClient, RedirectHelper redirectHelper) {\n        this.cfg = cfg;\n        this.ssoClient = ssoClient;\n        this.redirectHelper = redirectHelper;\n    }\n\n    @Override\n    public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException {\n        if (!cfg.isEnabled()) {\n            response.sendError(HttpServletResponse.SC_BAD_REQUEST, \"sso disabled\");\n            return;\n        }\n\n        String redirectUrl = SsoCookies.getFromCookie(request);\n        if (redirectUrl == null || redirectUrl.trim().isEmpty()) {\n            redirectUrl = \"/\";\n        }\n\n        String code = request.getParameter(\"code\");\n        if (code == null || code.trim().isEmpty()) {\n            redirectHelper.redirectToLoginOnError(response, redirectUrl, \"code param missing\");\n            return;\n        }\n\n        String state = request.getParameter(\"state\");\n        if (state == null || state.trim().isEmpty()) {\n            redirectHelper.redirectToLoginOnError(response, redirectUrl, \"state param missing\");\n            return;\n        }\n\n        SsoClient.Token token;\n        try {\n            token = ssoClient.getToken(code, cfg.getRedirectUrl());\n            SsoCookies.addCookie(TOKEN_COOKIE, token.idToken(), token.expiresIn(), response);\n            SsoCookies.addCookie(REFRESH_TOKEN_COOKIE, token.refreshToken(), token.expiresIn(), response);\n        } catch (Exception exception) {\n            redirectHelper.redirectToLoginOnError(response, redirectUrl, exception.getMessage());\n            return;\n        }\n\n        redirectHelper.sendRedirect(response, redirectUrl);\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/SsoClient.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.apache.http.HttpHeaders;\nimport org.immutables.value.Value;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.annotation.Nullable;\nimport javax.inject.Inject;\nimport java.io.BufferedReader;\nimport java.io.DataOutputStream;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Base64;\nimport java.util.Set;\n\npublic class SsoClient {\n\n    private static final Logger log = LoggerFactory.getLogger(SsoClient.class);\n\n    private static final String USER_AGENT_HEADER = \"Mozilla/5.0\";\n    private static final String ACCEPT_LANG_HEADER = \"en-US,en;q=0.5\";\n    private static final String CONTENT_TYPE_HEADER = \"application/x-www-form-urlencoded\";\n    private static final String CHARSET_HEADER = \"utf-8\";\n    private static final String TOKEN_REQUEST = \"code=%s&redirect_uri=%s&grant_type=authorization_code&client_id=%s\";\n    private static final String REVOKE_TOKEN_REQUEST = \"token=%s&token_type_hint=refresh_token&client_id=%s\";\n    private static final String TOKEN_BY_REFRESHER_REQUEST = \"refresh_token=%s&grant_type=refresh_token&client_id=%s\";\n\n    private final SsoConfiguration cfg;\n    private final ObjectMapper objectMapper = new ObjectMapper();\n\n    @Inject\n    public SsoClient(SsoConfiguration cfg) {\n        this.cfg = cfg;\n    }\n\n    public Token getToken(String authCode, String clientRedirectURI) throws IOException {\n        String urlParameters = String.format(TOKEN_REQUEST, authCode, clientRedirectURI, cfg.getClientId());\n        return getToken(urlParameters);\n    }\n\n    public Token getTokenByRefreshToken(String refreshToken) throws IOException {\n        String urlParameters = String.format(TOKEN_BY_REFRESHER_REQUEST, refreshToken, cfg.getClientId());\n        return getToken(urlParameters);\n    }\n\n    public void revokeToken(String refreshToken) throws IOException {\n        HttpURLConnection con = null;\n        try {\n            URL url = new URL(cfg.getLogoutEndpointUrl());\n            con = (HttpURLConnection) url.openConnection();\n            String urlParameters = String.format(REVOKE_TOKEN_REQUEST, refreshToken, cfg.getClientId());\n            postRequest(con, urlParameters);\n            int responseCode = con.getResponseCode();\n            if (responseCode != 200) {\n                log.error(\"refreshToken ['{}'] -> error response code {}\", refreshToken, responseCode);\n                throw new IOException(\"Invalid server response code: \" + responseCode);\n            }\n\n        } finally {\n            if (con != null) {\n                con.disconnect();\n            }\n        }\n    }\n\n    public String getTokenSigningKey() throws IOException {\n        if (cfg.getTokenSigningKeyUrl() == null) {\n            return null;\n        }\n        HttpURLConnection con = null;\n        try {\n            URL url = new URL(cfg.getTokenSigningKeyUrl());\n            con = (HttpURLConnection) url.openConnection();\n            con.setRequestMethod(\"GET\");\n            con.setConnectTimeout((int) cfg.getTokenServiceConnectTimeout().toMillis());\n            con.setReadTimeout((int) cfg.getTokenServiceReadTimeout().toMillis());\n            con.setDoOutput(true);\n            int responseCode = con.getResponseCode();\n            if (responseCode != 200) {\n                log.error(\"getTokenSigningKey ['{}'] -> error response code {}\", cfg.getTokenSigningKeyUrl(), responseCode);\n                throw new IOException(\"Invalid server response code: \" + responseCode);\n            }\n            BufferedReader in = new BufferedReader(\n                    new InputStreamReader(con.getInputStream()));\n            String inputLine;\n            StringBuilder content = new StringBuilder();\n            while ((inputLine = in.readLine()) != null) {\n                content.append(inputLine);\n            }\n            in.close();\n            return content.toString();\n        } finally {\n            if (con != null) {\n                con.disconnect();\n            }\n        }\n    }\n\n    public Profile getUserProfileByRefreshToken(String refreshToken) throws IOException {\n        Token token = getTokenByRefreshToken(refreshToken);\n        return getProfile(token.accessToken());\n    }\n\n    private Token getToken(String urlParameters) throws IOException {\n        HttpURLConnection con = null;\n        try {\n            URL url = new URL(cfg.getTokenEndPointUrl());\n            con = (HttpURLConnection) url.openConnection();\n            postRequest(con, urlParameters);\n            int responseCode = con.getResponseCode();\n            if (responseCode != 200) {\n                log.error(\"getToken ['{}'] -> error response code {}\", urlParameters, responseCode);\n                throw new IOException(\"Invalid server response code: \" + responseCode);\n            }\n\n            try (BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8))) {\n                return objectMapper.readValue(in, Token.class);\n            }\n        } finally {\n            if (con != null) {\n                con.disconnect();\n            }\n        }\n    }\n\n    private void postRequest(HttpURLConnection con, String urlParameters) throws IOException {\n        String clientIdAndSecret = String.format(\"%s:%s\", cfg.getClientId(), cfg.getClientSecret());\n        String authzHeaderValue = String.format(\"Basic %s\", Base64.getEncoder().encodeToString(clientIdAndSecret.getBytes()));\n\n        con.setRequestProperty(HttpHeaders.AUTHORIZATION, authzHeaderValue);\n        con.setRequestProperty(HttpHeaders.USER_AGENT, USER_AGENT_HEADER);\n        con.setRequestProperty(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE_HEADER);\n        con.setRequestProperty(HttpHeaders.ACCEPT_LANGUAGE, ACCEPT_LANG_HEADER);\n        con.setRequestProperty(HttpHeaders.ACCEPT_CHARSET, CHARSET_HEADER);\n\n        con.setRequestMethod(\"POST\");\n        con.setConnectTimeout((int) cfg.getTokenServiceConnectTimeout().toMillis());\n        con.setReadTimeout((int) cfg.getTokenServiceReadTimeout().toMillis());\n        con.setDoOutput(true);\n\n        byte[] postData = urlParameters.getBytes(StandardCharsets.UTF_8);\n        int postDataLength = postData.length;\n        con.setRequestProperty(HttpHeaders.CONTENT_LENGTH, Integer.toString(postDataLength));\n\n        try (DataOutputStream wr = new DataOutputStream(con.getOutputStream())) {\n            wr.write(postData);\n            wr.flush();\n        }\n    }\n\n    public Profile getProfile(String accessToken) throws IOException {\n        if (cfg.getUserInfoEndpointUrl() == null) {\n            return null;\n        }\n        HttpURLConnection con = null;\n        try {\n            URL url = new URL(cfg.getUserInfoEndpointUrl());\n            con = (HttpURLConnection) url.openConnection();\n            String authzHeaderValue = String.format(\"Bearer %s\", accessToken);\n            con.setRequestProperty(HttpHeaders.AUTHORIZATION, authzHeaderValue);\n            con.setRequestProperty(HttpHeaders.CONTENT_TYPE, CONTENT_TYPE_HEADER);\n            con.setRequestMethod(\"GET\");\n            con.setConnectTimeout((int) cfg.getTokenServiceConnectTimeout().toMillis());\n            con.setReadTimeout((int) cfg.getTokenServiceReadTimeout().toMillis());\n            con.setDoOutput(true);\n            int responseCode = con.getResponseCode();\n            if (responseCode != 200) {\n                log.error(\"getProfile ['{}'] -> error response code {}\", cfg.getUserInfoEndpointUrl(), responseCode);\n                throw new IOException(\"Invalid server response code: \" + responseCode);\n            }\n            try (BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8))) {\n                return objectMapper.readValue(in, Profile.class);\n            }\n        } finally {\n            if (con != null) {\n                con.disconnect();\n            }\n        }\n    }\n\n    @Value.Immutable\n    @JsonInclude(JsonInclude.Include.NON_EMPTY)\n    @JsonSerialize(as = ImmutableToken.class)\n    @JsonDeserialize(as = ImmutableToken.class)\n    public interface Token {\n\n        @JsonProperty(\"access_token\")\n        @Nullable\n        String accessToken();\n\n        @JsonProperty(\"token_type\")\n        String tokenType();\n\n        @JsonProperty(\"expires_in\")\n        @Nullable\n        Integer expiresIn();\n\n        @JsonProperty(\"id_token\")\n        @Nullable\n        String idToken();\n\n        @JsonProperty(\"refresh_token\")\n        String refreshToken();\n    }\n\n    @Value.Immutable\n    @JsonInclude(JsonInclude.Include.NON_EMPTY)\n    @JsonSerialize(as = ImmutableProfile.class)\n    @JsonDeserialize(as = ImmutableProfile.class)\n    @JsonIgnoreProperties(ignoreUnknown = true)\n    public interface Profile {\n\n        @JsonProperty(\"sub\")\n        String sub();\n\n        @JsonProperty(\"sAMAccountName\")\n        String userId();\n\n        @JsonProperty(\"mail\")\n        String mail();\n\n        @JsonProperty(\"displayName\")\n        @Nullable\n        String displayName();\n\n        @JsonProperty(\"userPrincipalName\")\n        @Nullable\n        String userPrincipalName();\n\n        @JsonProperty(\"distinguishedName\")\n        @Nullable\n        String nameInNamespace();\n\n        @JsonProperty(\"memberOf\")\n        @Nullable\n        Set<String> groups();\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/SsoConfiguration.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.config.Config;\nimport org.eclipse.sisu.Nullable;\n\nimport javax.inject.Inject;\nimport java.io.Serializable;\nimport java.time.Duration;\nimport java.util.List;\n\npublic class SsoConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    @Inject\n    @Config(\"sso.pfed.enabled\")\n    private boolean enabled;\n\n    @Inject\n    @Config(\"sso.pfed.priority\")\n    private int priority;\n\n    @Inject\n    @Config(\"sso.authEndpointUrl\")\n    private String authEndPointUrl;\n\n    @Inject\n    @Config(\"sso.tokenEndpointUrl\")\n    private String tokenEndPointUrl;\n\n    @Inject\n    @Config(\"sso.logoutEndpointUrl\")\n    private String logoutEndpointUrl;\n\n    @Inject\n    @Config(\"sso.redirectUrl\")\n    private String redirectUrl;\n\n    @Inject\n    @Config(\"sso.clientId\")\n    private String clientId;\n\n    @Inject\n    @Config(\"sso.clientSecret\")\n    private String clientSecret;\n\n    @Inject\n    @Config(\"sso.pfed.bearerToken.enableBearerTokens\")\n    private boolean enableBearerTokens;\n\n    @Inject\n    @Config(\"sso.pfed.bearerToken.allowAllClientIds\")\n    private boolean allowAllClientIds;\n\n    @Inject\n    @Nullable\n    @Config(\"sso.tokenSigningKey\")\n    private String tokenSigningKey;\n\n    @Inject\n    @Nullable\n    @Config(\"sso.tokenEncryptionKey\")\n    private String tokenEncryptionKey;\n\n    @Inject\n    @Config(\"sso.tokenServiceReadTimeout\")\n    private Duration tokenServiceReadTimeout;\n\n    @Inject\n    @Config(\"sso.tokenServiceConnectTimeout\")\n    private Duration tokenServiceConnectTimeout;\n\n    @Inject\n    @Config(\"sso.validateNonce\")\n    private boolean validateNonce;\n\n    @Inject\n    @Nullable\n    @Config(\"sso.tokenSigningKeyUrl\")\n    private String tokenSigningKeyUrl;\n\n    @Inject\n    @Config(\"sso.tokenSignatureValidation\")\n    private boolean tokenSignatureValidation;\n\n    @Inject\n    @Nullable\n    @Config(\"sso.userInfoEndpointUrl\")\n    private String userInfoEndpointUrl;\n\n    @Inject\n    @Config(\"sso.autoCreateUsers\")\n    private boolean autoCreateUsers;\n\n    @Inject\n    @Config(\"sso.pfed.bearerToken.allowedClientIds\")\n    private List<String> allowedClientIds;\n\n    public boolean isAutoCreateUsers() {\n        return autoCreateUsers;\n    }\n\n    public boolean isEnabled() {\n        return enabled;\n    }\n\n    public String getAuthEndPointUrl() {\n        return authEndPointUrl;\n    }\n\n    public String getTokenEndPointUrl() {\n        return tokenEndPointUrl;\n    }\n\n    public String getLogoutEndpointUrl() {\n        return logoutEndpointUrl;\n    }\n\n    public String getRedirectUrl() {\n        return redirectUrl;\n    }\n\n    public String getClientId() {\n        return clientId;\n    }\n\n    public String getClientSecret() {\n        return clientSecret;\n    }\n\n    public boolean getEnableBearerTokens() {\n        return enableBearerTokens;\n    }\n\n    public boolean getAllowAllClientIds() {\n        return allowAllClientIds;\n    }\n\n    public String getTokenEncryptionKey() {\n        return tokenEncryptionKey;\n    }\n\n    public String getTokenSigningKey() {\n        return tokenSigningKey;\n    }\n\n    public Duration getTokenServiceReadTimeout() {\n        return tokenServiceReadTimeout;\n    }\n\n    public Duration getTokenServiceConnectTimeout() {\n        return tokenServiceConnectTimeout;\n    }\n\n    public boolean isValidateNonce() {\n        return validateNonce;\n    }\n\n    public int getPriority() {\n        return priority;\n    }\n\n    public String getTokenSigningKeyUrl() {\n        return tokenSigningKeyUrl;\n    }\n\n    public boolean isTokenSignatureValidation() {\n        return tokenSignatureValidation;\n    }\n\n    public String getUserInfoEndpointUrl() {\n        return userInfoEndpointUrl;\n    }\n\n    public List<String> getAllowedClientIds() {\n        return allowedClientIds;\n    }\n\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/SsoCookies.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.servlet.http.Cookie;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.util.Arrays;\n\npublic final class SsoCookies {\n\n    public static final String TOKEN_COOKIE = \"ssoToken\";\n    public static final String REFRESH_TOKEN_COOKIE = \"refreshToken\";\n    public static final String POST_LOGIN_URL_COOKIE = \"postLoginUrl\";\n\n    /**\n     * Return the SSO token\n     *\n     * @param request from which SSO token cookie will get\n     * @return SSO token or <code>null</code> if no SSO token cookie present\n     */\n    public static String getTokenCookie(HttpServletRequest request) {\n        return getCookie(TOKEN_COOKIE, request);\n    }\n\n    /**\n     * Return the Refresh token\n     *\n     * @param request from which Refresh token cookie will get\n     * @return SSO token or <code>null</code> if no SSO token cookie present\n     */\n    public static String getRefreshCookie(HttpServletRequest request) {\n        return getCookie(REFRESH_TOKEN_COOKIE, request);\n    }\n\n    /**\n     * Store SSO token in response cookies\n     *\n     * @param name      Cookie name\n     * @param token     SSO token\n     * @param expiresIn SSO token expiration time in seconds\n     * @param response  response\n     */\n    public static void addCookie(String name, String token, Integer expiresIn, HttpServletResponse response) {\n        Cookie cookie = new Cookie(name, token);\n        cookie.setMaxAge(expiresIn);\n        cookie.setPath(\"/\");\n        cookie.setHttpOnly(true);\n        cookie.setSecure(true);\n\n        response.addCookie(cookie);\n    }\n\n    /**\n     * Remove SSO token cookie from response\n     *\n     * @param response response from which SSO token cookie will be removed\n     */\n    public static void removeTokenCookie(HttpServletResponse response) {\n        remove(TOKEN_COOKIE, response);\n    }\n\n    /**\n     * Remove Refresh token cookie from response\n     *\n     * @param response response from which Refresh token cookie will be removed\n     */\n    public static void removeRefreshTokenCookie(HttpServletResponse response) {\n        remove(REFRESH_TOKEN_COOKIE, response);\n    }\n\n    /**\n     * Store post successful redirect url in response cookies\n     *\n     * @param url      url to be redirected to\n     * @param response response\n     */\n    public static void addFromCookie(String url, HttpServletResponse response) {\n        Cookie cookie = new Cookie(POST_LOGIN_URL_COOKIE, url);\n        cookie.setMaxAge(Integer.MAX_VALUE);\n        cookie.setPath(\"/\");\n        cookie.setHttpOnly(true);\n\n        response.addCookie(cookie);\n    }\n\n    /**\n     * Return the url to be redirected post successful authentication by SSO\n     *\n     * @param request from which url cookie will get\n     * @return redirect url or <code>null</code> if no redirect login cookie present\n     */\n    public static String getFromCookie(HttpServletRequest request) {\n        return getCookie(POST_LOGIN_URL_COOKIE, request);\n    }\n\n    /**\n     * Clear All Cookies related to SSO\n     *\n     * @param response response from which SSO cookies will be removed\n     * @return response.\n     */\n    public static HttpServletResponse clear(HttpServletResponse response) {\n        remove(TOKEN_COOKIE, response);\n        remove(POST_LOGIN_URL_COOKIE, response);\n        remove(REFRESH_TOKEN_COOKIE, response);\n        return response;\n    }\n\n    private static String getCookie(String name, HttpServletRequest request) {\n        Cookie[] cookies = request.getCookies();\n        if (cookies == null) {\n            return null;\n        }\n\n        return Arrays.stream(cookies)\n                .filter(c -> name.equals(c.getName()))\n                .findFirst()\n                .map(Cookie::getValue)\n                .orElse(null);\n    }\n\n    private static void remove(String name, HttpServletResponse resp) {\n        resp.addCookie(expiredCookie(name));\n    }\n\n    private static Cookie expiredCookie(String name) {\n        Cookie cookie = new Cookie(name, \"\");\n        cookie.setMaxAge(0);\n        cookie.setPath(\"/\");\n        return cookie;\n    }\n\n    private SsoCookies() {\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/SsoFilterChainConfigurator.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.boot.FilterChainConfigurator;\nimport org.apache.shiro.web.filter.mgt.FilterChainManager;\n\nimport javax.inject.Inject;\n\npublic class SsoFilterChainConfigurator implements FilterChainConfigurator {\n\n    private final SsoAuthFilter ssoAuthFilter;\n    private final SsoCallbackFilter ssoCallbackFilter;\n    private final SsoLogoutFilter ssoLogoutFilter;\n    private final SsoConfiguration ssoConfiguration;\n\n    @Inject\n    public SsoFilterChainConfigurator(SsoAuthFilter ssoAuthFilter,\n                                      SsoCallbackFilter ssoCallbackFilter,\n                                      SsoLogoutFilter ssoLogoutFilter,\n                                      SsoConfiguration ssoConfiguration) {\n\n        this.ssoAuthFilter = ssoAuthFilter;\n        this.ssoCallbackFilter = ssoCallbackFilter;\n        this.ssoLogoutFilter = ssoLogoutFilter;\n        this.ssoConfiguration = ssoConfiguration;\n    }\n\n    @Override\n    public void configure(FilterChainManager manager) {\n        manager.addFilter(\"ssoAuth\", ssoAuthFilter);\n        manager.createChain(\"/api/service/sso/auth\", \"ssoAuth\");\n\n        manager.addFilter(\"ssoCallback\", ssoCallbackFilter);\n        manager.createChain(\"/api/service/sso/redirect\", \"ssoCallback\");\n\n        manager.addFilter(\"ssoLogout\", ssoLogoutFilter);\n        manager.createChain(\"/api/service/sso/logout\", \"ssoLogout\");\n    }\n\n    @Override\n    public int priority() {\n        int priority = ssoConfiguration.getPriority();\n        if (!ssoConfiguration.isEnabled())\n            return priority + 1;\n        return priority;\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/SsoHandler.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.boot.filters.AuthenticationHandler;\nimport org.apache.shiro.authc.AuthenticationToken;\n\nimport javax.inject.Inject;\nimport javax.servlet.ServletRequest;\nimport javax.servlet.ServletResponse;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\npublic class SsoHandler implements AuthenticationHandler {\n\n    private final SsoConfiguration cfg;\n    private final JwtAuthenticator jwtAuthenticator;\n    private final RedirectHelper redirectHelper;\n    private final SsoClient ssoClient;\n\n    private static final String FORM_URL_PATTERN = \"/forms/.*\";\n\n    @Inject\n    public SsoHandler(SsoConfiguration cfg, JwtAuthenticator jwtAuthenticator, RedirectHelper redirectHelper, SsoClient ssoClient) {\n        this.cfg = cfg;\n        this.jwtAuthenticator = jwtAuthenticator;\n        this.redirectHelper = redirectHelper;\n        this.ssoClient = ssoClient;\n    }\n\n    @Override\n    public AuthenticationToken createToken(ServletRequest request, ServletResponse response) {\n        if (!cfg.isEnabled()) {\n            return null;\n        }\n\n        HttpServletRequest req = (HttpServletRequest) request;\n\n        String bearerToken = cfg.getEnableBearerTokens() ? extractTokenFromRequest(req) : null;\n        String token = bearerToken != null ? bearerToken : SsoCookies.getTokenCookie(req);\n\n        if (token == null) {\n            return null;\n        }\n\n        boolean restrictOnClientId = (bearerToken != null) && (!cfg.getAllowAllClientIds());\n\n        if (!jwtAuthenticator.isTokenValid(token, restrictOnClientId)) {\n            return null;\n        }\n\n        try {\n            SsoClient.Profile profile = bearerToken != null ? ssoClient.getProfile(bearerToken) :\n                    ssoClient.getUserProfileByRefreshToken(SsoCookies.getRefreshCookie(req));\n\n            if (profile == null) {\n                return null;\n            }\n\n            String[] as = parseDomain(profile.sub());\n\n            return new SsoToken(as[0], as[1], profile.displayName(), profile.mail(), profile.userPrincipalName(), profile.nameInNamespace(), profile.groups());\n\n        } catch (IOException e) {\n            return null;\n        }\n    }\n\n    @Override\n    public boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {\n        if (!cfg.isEnabled()) {\n            return false;\n        }\n\n        HttpServletRequest req = (HttpServletRequest) request;\n        HttpServletResponse resp = (HttpServletResponse) response;\n\n        String p = req.getRequestURI();\n        if (p.matches(FORM_URL_PATTERN)) {\n            redirectHelper.sendRedirect(resp, \"/api/service/sso/auth?from=\" + p);\n            return true;\n        }\n\n        return false;\n    }\n\n    private String[] parseDomain(String s) {\n        s = s.trim();\n\n        int pos = s.indexOf(\"@\");\n        if (pos < 0) {\n            return new String[]{s, null};\n        }\n\n        String username = s.substring(0, pos);\n        String domain = s.substring(pos + 1);\n        return new String[]{username, domain};\n    }\n\n    private String extractTokenFromRequest(HttpServletRequest request) {\n        final String value = request.getHeader(\"Authorization\");\n\n        if (value == null || !value.toLowerCase().startsWith(\"bearer\")) {\n            return null;\n        }\n\n        String[] parts = value.split(\" \");\n\n        if (parts.length < 2) {\n            return null;\n        }\n\n        return parts[1].trim();\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/SsoLogoutFilter.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\nimport com.walmartlabs.concord.server.security.SecurityUtils;\nimport org.apache.shiro.subject.Subject;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.inject.Inject;\nimport javax.servlet.FilterChain;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\npublic class SsoLogoutFilter extends AbstractHttpFilter {\n\n    private static final Logger log = LoggerFactory.getLogger(SsoLogoutFilter.class);\n\n    private final SsoConfiguration cfg;\n    private final RedirectHelper redirectHelper;\n    private final SsoClient ssoClient;\n\n    @Inject\n    public SsoLogoutFilter(SsoConfiguration cfg, RedirectHelper redirectHelper, SsoClient ssoClient) {\n        this.cfg = cfg;\n        this.redirectHelper = redirectHelper;\n        this.ssoClient = ssoClient;\n    }\n\n    @Override\n    public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException {\n        if (!cfg.isEnabled()) {\n            response.sendError(HttpServletResponse.SC_BAD_REQUEST, \"SSO is disabled, the logout URL is not available\");\n            return;\n        }\n\n        String initParam = request.getParameter(\"init\");\n        boolean isInit = initParam != null && !initParam.trim().isEmpty();\n        if (isInit) {\n            try {\n                ssoClient.revokeToken(SsoCookies.getRefreshCookie(request));\n            } catch (Exception e) {\n                log.warn(\"Error in revoking sso token during logout -> '{}'\", e.getMessage(), e);\n            }\n        }\n        SsoCookies.clear(response);\n        Subject subject = SecurityUtils.getSubject();\n        subject.logout();\n\n        redirectHelper.sendRedirect(response, \"/#/logout/done\");\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/SsoRealm.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.audit.AuditAction;\nimport com.walmartlabs.concord.server.audit.AuditLog;\nimport com.walmartlabs.concord.server.audit.AuditObject;\nimport com.walmartlabs.concord.server.sdk.metrics.WithTimer;\nimport com.walmartlabs.concord.server.sdk.security.AuthenticationException;\nimport com.walmartlabs.concord.server.security.SecurityUtils;\nimport com.walmartlabs.concord.server.security.UserPrincipal;\nimport com.walmartlabs.concord.server.security.ldap.LdapPrincipal;\nimport com.walmartlabs.concord.server.user.UserEntry;\nimport com.walmartlabs.concord.server.user.UserManager;\nimport com.walmartlabs.concord.server.user.UserType;\nimport org.apache.shiro.authc.AuthenticationInfo;\nimport org.apache.shiro.authc.AuthenticationToken;\nimport org.apache.shiro.authc.SimpleAccount;\nimport org.apache.shiro.authz.AuthorizationInfo;\nimport org.apache.shiro.realm.AuthorizingRealm;\nimport org.apache.shiro.subject.PrincipalCollection;\n\nimport javax.inject.Inject;\nimport java.util.Arrays;\nimport java.util.Collections;\n\npublic class SsoRealm extends AuthorizingRealm {\n\n    public static final String REALM_NAME = \"sso\";\n\n    private final UserManager userManager;\n    private final AuditLog auditLog;\n\n    @Inject\n    public SsoRealm(UserManager userManager, AuditLog auditLog) {\n        this.userManager = userManager;\n        this.auditLog = auditLog;\n    }\n\n    @Override\n    public boolean supports(AuthenticationToken token) {\n        return token instanceof SsoToken;\n    }\n\n    @Override\n    @WithTimer\n    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {\n        SsoToken t = (SsoToken) token;\n\n        if (t.getUsername() == null) {\n            return null;\n        }\n\n        UserEntry u = userManager.get(t.getUsername(), t.getDomain(), UserType.LDAP)\n                .orElse(null);\n        if (u == null) {\n            u = userManager.create(t.getUsername(), t.getDomain(), t.getDisplayName(), t.getMail(), UserType.SSO, null);\n        }\n\n        if (u.isPermanentlyDisabled()) {\n            return null;\n        }\n\n        // we consider the account active if the authentication was successful\n        userManager.enable(u.getId());\n\n        auditLog.add(AuditObject.SYSTEM, AuditAction.ACCESS)\n                .userId(u.getId())\n                .field(\"username\", u.getName())\n                .field(\"userDomain\", u.getDomain())\n                .field(\"realm\", REALM_NAME)\n                .log();\n\n        UserPrincipal userPrincipal = new UserPrincipal(REALM_NAME, u);\n\n        LdapPrincipal ldapPrincipal = new LdapPrincipal(t.getUsername(), t.getDomain(), t.getNameInNamespace(), t.getUserPrincipalName(), t.getDisplayName(), t.getMail(), t.getGroups(), Collections.singletonMap(\"mail\", t.getMail()));\n\n        return new SimpleAccount(Arrays.asList(userPrincipal, t, ldapPrincipal), t.getCredentials(), getName());\n    }\n\n    @Override\n    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {\n        UserPrincipal p = principals.oneByType(UserPrincipal.class);\n        if (p == null || !REALM_NAME.equals(p.getRealm())) {\n            return null;\n        }\n\n        return SecurityUtils.toAuthorizationInfo(principals);\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/SsoToken.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.apache.shiro.authc.AuthenticationToken;\n\nimport java.util.Set;\n\npublic class SsoToken implements AuthenticationToken {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String username;\n    private final String domain;\n    private final String displayName;\n    private final String mail;\n    private final String userPrincipalName;\n    private final String nameInNamespace;\n    private final Set<String> groups;\n\n    public SsoToken(String username, String domain, String displayName, String mail, String userPrincipalName, String nameInNamespace, Set<String> groups) {\n        this.username = username;\n        this.domain = domain;\n        this.displayName = displayName;\n        this.mail = mail;\n        this.userPrincipalName = userPrincipalName;\n        this.nameInNamespace = nameInNamespace;\n        this.groups = groups;\n    }\n\n    public String getUsername() {\n        return username;\n    }\n\n    public String getDomain() {\n        return domain;\n    }\n\n    public String getDisplayName() {\n        return displayName;\n    }\n\n    public String getMail() {\n        return mail;\n    }\n\n    public String getNameInNamespace() {\n        return nameInNamespace;\n    }\n\n    public String getUserPrincipalName() {\n        return userPrincipalName;\n    }\n\n    public Set<String> getGroups() {\n        return groups;\n    }\n\n    @Override\n    public Object getPrincipal() {\n        return username + \"@\" + domain;\n    }\n\n    @Override\n    public Object getCredentials() {\n        return getPrincipal();\n    }\n\n    @Override\n    public String toString() {\n        return \"SsoToken{\" +\n                \"username='\" + username + '\\'' +\n                \", domain='\" + domain + '\\'' +\n                \", displayName='\" + displayName + '\\'' +\n                \", mail='\" + mail + '\\'' +\n                \", userPrincipalName='\" + userPrincipalName + '\\'' +\n                \", nameInNamespace='\" + nameInNamespace + '\\'' +\n                \", groups=\" + groups +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/SsoUserInfoProvider.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2021 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\nimport com.walmartlabs.concord.server.security.Roles;\nimport com.walmartlabs.concord.server.security.ldap.LdapPrincipal;\nimport com.walmartlabs.concord.server.user.AbstractUserInfoProvider;\nimport com.walmartlabs.concord.server.user.UserDao;\nimport com.walmartlabs.concord.server.user.UserType;\n\nimport javax.inject.Inject;\nimport java.util.Set;\nimport java.util.UUID;\n\npublic class SsoUserInfoProvider extends AbstractUserInfoProvider {\n\n    private final SsoConfiguration cfg;\n\n    @Inject\n    public SsoUserInfoProvider(UserDao userDao, SsoConfiguration ssoConfiguration) {\n        super(userDao);\n        this.cfg = ssoConfiguration;\n    }\n\n    @Override\n    public UserType getUserType() {\n        return UserType.SSO;\n    }\n\n    @Override\n    public UserInfo getInfo(UUID id, String username, String userDomain) {\n        // return if ldap principal exists as part of sso\n        LdapPrincipal ldapPrincipal = LdapPrincipal.getCurrent();\n        if (ldapPrincipal != null && ldapPrincipal.getUsername().equalsIgnoreCase(username) && ldapPrincipal.getDomain().equalsIgnoreCase(userDomain)) {\n            return UserInfo.builder()\n                    .id(id)\n                    .username(ldapPrincipal.getUsername())\n                    .userDomain(ldapPrincipal.getDomain())\n                    .displayName(ldapPrincipal.getDisplayName())\n                    .email(ldapPrincipal.getEmail())\n                    .groups(ldapPrincipal.getGroups())\n                    .attributes(ldapPrincipal.getAttributes())\n                    .build();\n        }\n\n        return getInfo(id, username, userDomain, UserType.LDAP);\n    }\n\n    @Override\n    public UUID create(String username, String domain, String displayName, String email, Set<String> roles) {\n        if (!Roles.isAdmin() && !cfg.isAutoCreateUsers()) {\n            // unfortunately there's no easy way to throw a custom authentication error and keep the original message\n            // this will result in a 401 response with an empty body anyway\n            throw new ConcordApplicationException(\"Automatic creation of users is disabled.\");\n        }\n        return create(username, domain, displayName, email, roles, UserType.LDAP);\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/encryption/AbstractEncryptionConfiguration.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso.encryption;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.nimbusds.jose.EncryptionMethod;\nimport com.nimbusds.jose.JOSEException;\nimport com.nimbusds.jose.JWEAlgorithm;\nimport com.nimbusds.jose.JWEDecrypter;\nimport com.nimbusds.jwt.EncryptedJWT;\n\npublic abstract class AbstractEncryptionConfiguration implements EncryptionConfiguration {\n\n    protected JWEAlgorithm algorithm;\n\n    protected EncryptionMethod method;\n\n    @Override\n    public void decrypt(final EncryptedJWT encryptedJWT) throws JOSEException {\n        encryptedJWT.decrypt(buildDecrypter());\n    }\n\n    protected abstract JWEDecrypter buildDecrypter();\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/encryption/ECEncryptionConfiguration.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso.encryption;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.nimbusds.jose.JOSEException;\nimport com.nimbusds.jose.JWEDecrypter;\nimport com.nimbusds.jose.crypto.ECDHDecrypter;\n\nimport java.security.KeyPair;\nimport java.security.interfaces.ECPrivateKey;\nimport java.security.interfaces.ECPublicKey;\n\npublic class ECEncryptionConfiguration extends AbstractEncryptionConfiguration {\n\n    private final ECPublicKey publicKey;\n\n    private final ECPrivateKey privateKey;\n\n    public ECEncryptionConfiguration(final KeyPair keyPair) {\n        this.privateKey = (ECPrivateKey) keyPair.getPrivate();\n        this.publicKey = (ECPublicKey) keyPair.getPublic();\n    }\n\n    @Override\n    protected JWEDecrypter buildDecrypter() {\n        try {\n            return new ECDHDecrypter(this.privateKey);\n        } catch (final JOSEException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public ECPublicKey getPublicKey() {\n        return publicKey;\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/encryption/EncryptionConfiguration.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso.encryption;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.nimbusds.jose.JOSEException;\nimport com.nimbusds.jwt.EncryptedJWT;\n\npublic interface EncryptionConfiguration {\n\n    /**\n     * Decrypt an encrypted JWT.\n     *\n     * @param encryptedJWT encrypted JWT\n     * @throws JOSEException exception\n     */\n    void decrypt(EncryptedJWT encryptedJWT) throws JOSEException;\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/encryption/EncryptionConfigurationFactory.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso.encryption;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.nimbusds.jose.jwk.KeyType;\nimport com.walmartlabs.concord.server.plugins.pfedsso.JwkHelper;\nimport net.minidev.json.JSONObject;\n\nimport java.security.KeyPair;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\npublic class EncryptionConfigurationFactory {\n\n    private static final ObjectMapper objectMapper = new ObjectMapper();\n\n    public static EncryptionConfiguration create(String cfg) {\n        if (cfg == null) {\n            return null;\n        }\n\n        try {\n            JSONObject json = objectMapper.readValue(cfg, JSONObject.class);\n            KeyType kty = KeyType.parse(json.getAsString(\"kty\"));\n            if (KeyType.EC.equals(kty)) {\n                KeyPair key = JwkHelper.buildECKeyPairFromJwk(json);\n                return new ECEncryptionConfiguration(key);\n            } else if (KeyType.RSA.equals(kty)) {\n                KeyPair key = JwkHelper.buildRSAKeyPairFromJwk(json);\n                return new RSAEncryptionConfiguration(key);\n            } else if (KeyType.OCT.equals(kty)) {\n                String secret = JwkHelper.buildSecretFromJwk(json);\n                return new SecretEncryptionConfiguration(secret.getBytes(UTF_8));\n            } else {\n                throw new RuntimeException(\"unknown key type: \" + kty);\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/encryption/RSAEncryptionConfiguration.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso.encryption;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.nimbusds.jose.JWEDecrypter;\nimport com.nimbusds.jose.crypto.RSADecrypter;\n\nimport java.security.KeyPair;\nimport java.security.interfaces.RSAPrivateKey;\nimport java.security.interfaces.RSAPublicKey;\n\npublic class RSAEncryptionConfiguration extends AbstractEncryptionConfiguration {\n\n    private final RSAPublicKey publicKey;\n\n    private final RSAPrivateKey privateKey;\n\n    public RSAEncryptionConfiguration(final KeyPair keyPair) {\n        this.privateKey = (RSAPrivateKey) keyPair.getPrivate();\n        this.publicKey = (RSAPublicKey) keyPair.getPublic();\n    }\n\n    @Override\n    protected JWEDecrypter buildDecrypter() {\n        return new RSADecrypter(this.privateKey);\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/encryption/SecretEncryptionConfiguration.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso.encryption;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.nimbusds.jose.JWEDecrypter;\nimport com.nimbusds.jose.KeyLengthException;\nimport com.nimbusds.jose.crypto.AESDecrypter;\nimport com.nimbusds.jose.crypto.DirectDecrypter;\n\nimport java.util.Arrays;\n\npublic class SecretEncryptionConfiguration extends AbstractEncryptionConfiguration {\n\n    private final byte[] secret;\n\n    public SecretEncryptionConfiguration(byte[] secret) {\n        this.secret = Arrays.copyOf(secret, secret.length);\n    }\n\n    @Override\n    protected JWEDecrypter buildDecrypter() {\n        try {\n            if (DirectDecrypter.SUPPORTED_ALGORITHMS.contains(algorithm)) {\n                return new DirectDecrypter(this.secret);\n            } else {\n                return new AESDecrypter(this.secret);\n            }\n        } catch (KeyLengthException e) {\n            throw new RuntimeException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/signature/ECSignatureConfiguration.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso.signature;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.nimbusds.jose.JOSEException;\nimport com.nimbusds.jose.JWSVerifier;\nimport com.nimbusds.jose.crypto.ECDSAVerifier;\nimport com.nimbusds.jwt.SignedJWT;\n\nimport java.security.KeyPair;\nimport java.security.interfaces.ECPrivateKey;\nimport java.security.interfaces.ECPublicKey;\n\npublic class ECSignatureConfiguration implements SignatureConfiguration {\n\n    private final ECPublicKey publicKey;\n\n    private final ECPrivateKey privateKey;\n\n    public ECSignatureConfiguration(KeyPair keyPair) {\n        this.privateKey = (ECPrivateKey) keyPair.getPrivate();\n        this.publicKey = (ECPublicKey) keyPair.getPublic();\n    }\n\n    @Override\n    public boolean verify(final SignedJWT jwt) throws JOSEException {\n        final JWSVerifier verifier = new ECDSAVerifier(this.publicKey);\n        return jwt.verify(verifier);\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/signature/RSASignatureConfiguration.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso.signature;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.nimbusds.jose.JOSEException;\nimport com.nimbusds.jose.JWSVerifier;\nimport com.nimbusds.jose.crypto.RSASSAVerifier;\nimport com.nimbusds.jwt.SignedJWT;\n\nimport java.security.KeyPair;\nimport java.security.interfaces.RSAPrivateKey;\nimport java.security.interfaces.RSAPublicKey;\n\npublic class RSASignatureConfiguration implements SignatureConfiguration {\n\n    private final RSAPrivateKey privateKey;\n    private final RSAPublicKey publicKey;\n\n    public RSASignatureConfiguration(KeyPair keyPair) {\n        this.privateKey = (RSAPrivateKey) keyPair.getPrivate();\n        this.publicKey = (RSAPublicKey) keyPair.getPublic();\n    }\n\n    @Override\n    public boolean verify(final SignedJWT jwt) throws JOSEException {\n        final JWSVerifier verifier = new RSASSAVerifier(this.publicKey);\n        return jwt.verify(verifier);\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/signature/SecretSignatureConfiguration.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso.signature;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.nimbusds.jose.JOSEException;\nimport com.nimbusds.jose.JWSVerifier;\nimport com.nimbusds.jose.crypto.MACVerifier;\nimport com.nimbusds.jwt.SignedJWT;\n\nimport java.util.Arrays;\n\npublic class SecretSignatureConfiguration implements SignatureConfiguration {\n\n    private final byte[] secret;\n\n    public SecretSignatureConfiguration(byte[] secret) {\n        this.secret = Arrays.copyOf(secret, secret.length);\n    }\n\n    @Override\n    public boolean verify(final SignedJWT jwt) throws JOSEException {\n        final JWSVerifier verifier = new MACVerifier(this.secret);\n        return jwt.verify(verifier);\n    }\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/signature/SignatureConfiguration.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso.signature;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.nimbusds.jose.JOSEException;\nimport com.nimbusds.jwt.SignedJWT;\n\npublic interface SignatureConfiguration {\n\n    /**\n     * Verify a signed JWT.\n     *\n     * @param jwt signed JWT\n     * @return <code>true</code> if signature verified\n     * @throws JOSEException exception\n     */\n    boolean verify(SignedJWT jwt) throws JOSEException;\n}\n"
  },
  {
    "path": "server/plugins/pfed-sso/src/main/java/com/walmartlabs/concord/server/plugins/pfedsso/signature/SignatureConfigurationFactory.java",
    "content": "package com.walmartlabs.concord.server.plugins.pfedsso.signature;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.nimbusds.jose.jwk.KeyType;\nimport com.walmartlabs.concord.server.plugins.pfedsso.JwkHelper;\nimport net.minidev.json.JSONObject;\n\nimport java.security.KeyPair;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\n\npublic class SignatureConfigurationFactory {\n\n    private static final ObjectMapper objectMapper = new ObjectMapper();\n\n    public static SignatureConfiguration create(String cfg) {\n        if (cfg == null) {\n            return null;\n        }\n\n        try {\n            JSONObject json = objectMapper.readValue(cfg, JSONObject.class);\n            KeyType kty = KeyType.parse(json.getAsString(\"kty\"));\n            if (KeyType.EC.equals(kty)) {\n                KeyPair key = JwkHelper.buildECKeyPairFromJwk(json);\n                return new ECSignatureConfiguration(key);\n            } else if (KeyType.RSA.equals(kty)) {\n                KeyPair key = JwkHelper.buildRSAKeyPairFromJwk(json);\n                return new RSASignatureConfiguration(key);\n            } else if (KeyType.OCT.equals(kty)) {\n                String secret = JwkHelper.buildSecretFromJwk(json);\n                return new SecretSignatureConfiguration(secret.getBytes(UTF_8));\n            } else {\n                throw new RuntimeException(\"unknown key type: \" + kty);\n            }\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n\n    }\n}\n"
  },
  {
    "path": "server/plugins/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.server</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <groupId>com.walmartlabs.concord.server.plugins</groupId>\n    <artifactId>parent</artifactId>\n    <packaging>pom</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <modules>\n        <module>ansible</module>\n        <module>kafka-event-sink</module>\n        <module>noderoster</module>\n        <module>oidc</module>\n        <module>oneops</module>\n        <module>pfed-sso</module>\n        <module>webapp</module>\n    </modules>\n</project>\n"
  },
  {
    "path": "server/plugins/webapp/README.md",
    "content": "# webapp\n\nA concord-server plugin to serve SPAs.\n"
  },
  {
    "path": "server/plugins/webapp/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <artifactId>parent</artifactId>\n        <groupId>com.walmartlabs.concord.server.plugins</groupId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>webapp</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord.server</groupId>\n            <artifactId>concord-server-sdk</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.inject</groupId>\n            <artifactId>javax.inject</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.inject</groupId>\n            <artifactId>guice</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty.ee8</groupId>\n            <artifactId>jetty-ee8-servlet</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.jboss.spec.javax.ws.rs</groupId>\n            <artifactId>jboss-jaxrs-api_2.1_spec</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>javax.annotation</groupId>\n            <artifactId>javax.annotation-api</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "server/plugins/webapp/src/main/java/ca/ibodrov/concord/webapp/ExcludedPrefixes.java",
    "content": "package ca.ibodrov.concord.webapp;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.annotations.VisibleForTesting;\nimport com.walmartlabs.concord.server.sdk.rest.ApiDescriptor;\nimport org.eclipse.jetty.ee8.servlet.ServletHolder;\n\nimport javax.inject.Inject;\nimport javax.servlet.annotation.WebServlet;\nimport javax.servlet.http.HttpServlet;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.regex.Pattern;\nimport java.util.stream.Stream;\n\nimport static java.util.Comparator.reverseOrder;\nimport static java.util.Objects.requireNonNull;\n\n/**\n * Collects all URI prefixes that should NOT be handled by {@link WebappFilter}.\n */\npublic class ExcludedPrefixes {\n\n    private final List<Pattern> prefixes;\n\n    @Inject\n    public ExcludedPrefixes(Set<ApiDescriptor> apiDescriptors,\n                            Set<HttpServlet> servlets,\n                            Set<ServletHolder> servletHolders) {\n        this(collectPrefixes(apiDescriptors, servlets, servletHolders));\n    }\n\n    @VisibleForTesting\n    ExcludedPrefixes(Stream<String> prefixes) {\n        this.prefixes = prefixes.map(prefix -> {\n                    if (prefix.endsWith(\"/*\")) {\n                        prefix = Pattern.quote(prefix.substring(0, prefix.length() - 1));\n                        return prefix + \".*\";\n                    }\n                    return prefix;\n                })\n                .sorted(reverseOrder())\n                .map(Pattern::compile)\n                .toList();\n    }\n\n    public boolean matches(String path) {\n        return prefixes.stream().anyMatch(prefix -> prefix.matcher(path).matches());\n    }\n\n    private static Stream<String> collectPrefixes(Set<ApiDescriptor> apiDescriptors,\n                                                  Set<HttpServlet> servlets,\n                                                  Set<ServletHolder> servletHolders) {\n        var apiPrefixes = requireNonNull(apiDescriptors).stream().flatMap(descriptor -> Arrays.stream(descriptor.paths()));\n        var servletPrefixes = webServletAnnotated(requireNonNull(servlets));\n        var servletHolderPrefixes = webServletAnnotated(requireNonNull(servletHolders));\n        return Stream.of(apiPrefixes, servletPrefixes, servletHolderPrefixes).flatMap(p -> p);\n    }\n\n    private static Stream<String> webServletAnnotated(Set<?> instances) {\n        return instances.stream()\n                .flatMap(i -> Stream.ofNullable(i.getClass().getAnnotation(WebServlet.class)))\n                .flatMap(a -> Arrays.stream(Optional.ofNullable(a.value())\n                        .filter(values -> values.length != 0)\n                        .orElse(a.urlPatterns())));\n    }\n}\n"
  },
  {
    "path": "server/plugins/webapp/src/main/java/ca/ibodrov/concord/webapp/StaticResource.java",
    "content": "package ca.ibodrov.concord.webapp;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nrecord StaticResource(String path, String contentType, String eTag) {\n}\n"
  },
  {
    "path": "server/plugins/webapp/src/main/java/ca/ibodrov/concord/webapp/WebappFilter.java",
    "content": "package ca.ibodrov.concord.webapp;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.cache.CacheBuilder;\nimport com.google.common.cache.CacheLoader;\nimport com.google.common.cache.LoadingCache;\nimport com.google.common.collect.ImmutableMap;\n\nimport javax.annotation.Priority;\nimport javax.inject.Inject;\nimport javax.servlet.FilterChain;\nimport javax.servlet.ServletException;\nimport javax.servlet.annotation.WebFilter;\nimport javax.servlet.http.HttpFilter;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport javax.ws.rs.WebApplicationException;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.net.URL;\nimport java.util.*;\nimport java.util.stream.Stream;\n\nimport static java.util.Comparator.comparing;\nimport static java.util.Objects.requireNonNull;\nimport static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;\n\n/**\n * @implNote The filter should be applied after all other filters.\n */\n@WebFilter(\"/*\")\n@Priority(Integer.MAX_VALUE - 1000)\npublic class WebappFilter extends HttpFilter {\n\n    private final WebappCollection webapps;\n    private final ExcludedPrefixes excludedPrefixes;\n\n    @Inject\n    public WebappFilter(ExcludedPrefixes excludedPrefixes) {\n        this.webapps = loadWebapps();\n        this.excludedPrefixes = requireNonNull(excludedPrefixes);\n    }\n\n    @Override\n    protected void doFilter(HttpServletRequest req, HttpServletResponse resp, FilterChain chain)\n            throws ServletException, IOException {\n\n        var uri = req.getRequestURI();\n        if (excludedPrefixes.matches(uri)) {\n            chain.doFilter(req, resp);\n            return;\n        }\n\n        var webapp = webapps.stream().filter(w -> uri.startsWith(w.path())).findFirst();\n        if (webapp.isPresent()) {\n            doGet(webapp.get(), req, resp);\n        } else {\n            chain.doFilter(req, resp);\n        }\n    }\n\n    private void doGet(Webapp webapp, HttpServletRequest req, HttpServletResponse resp) {\n        var path = req.getRequestURI();\n\n        if (!path.startsWith(webapp.path())) {\n            throw new RuntimeException(\"Unexpected path: \" + path);\n        }\n\n        path = path.length() > webapp.path().length() ? path.substring(webapp.path().length()) : \"\";\n\n        if (path.startsWith(\"/\")) {\n            path = path.substring(1);\n        }\n\n        if (path.isEmpty() || path.equals(\"/\")) {\n            path = \"index.html\";\n        }\n\n        var resource = Optional.ofNullable(webapp.resources().get(path))\n                .orElseGet(() -> webapp.resources().get(webapp.indexHtmlRelativePath()));\n\n        resp.setHeader(\"Content-Type\", resource.contentType());\n        resp.setHeader(\"ETag\", resource.eTag());\n\n        var ifNoneMatch = req.getHeader(\"If-None-Match\");\n        if (resource.eTag().equals(ifNoneMatch)) {\n            resp.setStatus(304);\n            return;\n        }\n\n        var content = webapp.getContent(resource);\n        try {\n            resp.setStatus(200);\n            resp.getOutputStream().write(content);\n        } catch (IOException e) {\n            throw new WebApplicationException(INTERNAL_SERVER_ERROR);\n        }\n    }\n\n    private static WebappCollection loadWebapps() {\n        var result = new ArrayList<Webapp>();\n        var classLoader = WebappPluginModule.class.getClassLoader();\n        try {\n            classLoader.getResources(\"META-INF/concord/webapp.properties\")\n                    .asIterator()\n                    .forEachRemaining(source -> {\n                        var webapp = new Webapp(source);\n                        result.add(webapp);\n                    });\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n        return new WebappCollection(result);\n    }\n\n    private static final class Webapp {\n\n        private static final int CACHE_SIZE = 1000;\n\n        private final String path;\n        private final Map<String, StaticResource> resources;\n        private final String indexHtmlRelativePath;\n        private final LoadingCache<String, byte[]> contentCache;\n\n        private Webapp(URL propertiesSource) {\n            var props = new Properties();\n            try (var in = propertiesSource.openStream()) {\n                props.load(in);\n            } catch (IOException e) {\n                throw new RuntimeException(e);\n            }\n\n            this.path = assertString(propertiesSource, props, \"path\");\n            this.resources = loadResources(assertString(propertiesSource, props, \"checksumsFileResourcePath\"));\n            this.indexHtmlRelativePath = assertString(propertiesSource, props, \"indexHtmlRelativePath\");\n\n            var resourceRoot = assertString(propertiesSource, props, \"resourceRoot\");\n            this.contentCache = CacheBuilder.newBuilder()\n                    .maximumSize(CACHE_SIZE)\n                    .build(new CacheLoader<>() {\n                        @Override\n                        public byte[] load(String key) throws Exception {\n                            var filePath = resourceRoot + key;\n                            try (var in = WebappFilter.class.getClassLoader().getResourceAsStream(filePath)) {\n                                if (in == null) {\n                                    throw new IllegalStateException(\"Resource not found: \" + filePath);\n                                }\n                                return in.readAllBytes();\n                            }\n                        }\n                    });\n        }\n\n        public String path() {\n            return path;\n        }\n\n        public Map<String, StaticResource> resources() {\n            return resources;\n        }\n\n        public String indexHtmlRelativePath() {\n            return indexHtmlRelativePath;\n        }\n\n        public byte[] getContent(StaticResource resource) {\n            return contentCache.getUnchecked(resource.path());\n        }\n    }\n\n    private static class WebappCollection {\n\n        // must be sorted, longest prefixes first\n        private final List<Webapp> webapps;\n\n        public WebappCollection(Collection<Webapp> webapps) {\n            this.webapps = webapps.stream()\n                    .sorted(comparing(Webapp::path).reversed())\n                    .toList();\n        }\n\n        public Stream<Webapp> stream() {\n            return webapps.stream();\n        }\n    }\n\n    private static Map<String, StaticResource> loadResources(String file) {\n        var resources = ImmutableMap.<String, StaticResource>builder();\n\n        var cl = WebappFilter.class.getClassLoader();\n        try (var in = cl.getResourceAsStream(file)) {\n            if (in == null) {\n                throw new RuntimeException(file + \" file not found. Classpath or build issues?\");\n            }\n\n            try (var reader = new BufferedReader(new InputStreamReader(in))) {\n                String line;\n                while ((line = reader.readLine()) != null) {\n                    line = line.trim();\n\n                    if (line.startsWith(\"#\")) {\n                        continue;\n                    }\n\n                    var items = line.split(\",\");\n                    if (items.length != 2) {\n                        throw new RuntimeException(file + \" file, invalid line: \" + line);\n                    }\n\n                    var path = items[0];\n                    var eTag = items[1];\n                    var contentType = getContentType(path)\n                            .orElseThrow(() -> new RuntimeException(\"Can't determine Content-Type for \" + path));\n\n                    resources.put(path, new StaticResource(path, contentType, eTag));\n                }\n            }\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n\n        return resources.build();\n    }\n\n    private static String assertString(URL source, Properties props, String key) {\n        var value = props.getProperty(key);\n        if (value == null) {\n            throw new RuntimeException(\"Missing required property: %s (in %s)\".formatted(key, source));\n        }\n        return value;\n    }\n\n    private static Optional<String> getContentType(String fileName) {\n        var extIdx = fileName.lastIndexOf('.');\n        if (extIdx < 2 || extIdx >= fileName.length() - 1) {\n            return Optional.empty();\n        }\n        var ext = fileName.substring(extIdx + 1);\n        return Optional.ofNullable(switch (ext) {\n            case \"css\" -> \"text/css\";\n            case \"eot\" -> \"application/vnd.ms-fontobject\";\n            case \"gif\" -> \"image/gif\";\n            case \"html\" -> \"text/html\";\n            case \"jpg\", \"jpeg\" -> \"image/jpeg\";\n            case \"js\" -> \"text/javascript\";\n            case \"json\" -> \"text/json\";\n            case \"map\" -> \"application/json\";\n            case \"png\" -> \"image/png\";\n            case \"svg\" -> \"image/svg+xml\";\n            case \"ttf\" -> \"font/ttf\";\n            case \"txt\" -> \"text/plain\";\n            case \"webp\" -> \"image/webp\";\n            case \"woff\" -> \"font/woff\";\n            case \"woff2\" -> \"font/woff2\";\n            default -> null;\n        });\n    }\n}\n"
  },
  {
    "path": "server/plugins/webapp/src/main/java/ca/ibodrov/concord/webapp/WebappPluginModule.java",
    "content": "package ca.ibodrov.concord.webapp;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.inject.Binder;\nimport com.google.inject.Module;\nimport com.walmartlabs.concord.server.sdk.rest.ApiDescriptor;\nimport org.eclipse.jetty.ee8.servlet.ServletHolder;\n\nimport javax.inject.Named;\nimport javax.servlet.Filter;\nimport javax.servlet.http.HttpServlet;\n\nimport static com.google.inject.multibindings.Multibinder.newSetBinder;\n\n@Named\npublic class WebappPluginModule implements Module {\n\n    @Override\n    public void configure(Binder binder) {\n        newSetBinder(binder, ApiDescriptor.class);\n        newSetBinder(binder, HttpServlet.class);\n        newSetBinder(binder, ServletHolder.class);\n\n        newSetBinder(binder, Filter.class).addBinding().to(WebappFilter.class);\n    }\n}\n"
  },
  {
    "path": "server/plugins/webapp/src/test/java/ca/ibodrov/concord/webapp/ExcludedPrefixesTest.java",
    "content": "package ca.ibodrov.concord.webapp;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2025 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport java.util.stream.Stream;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\npublic class ExcludedPrefixesTest {\n\n    @Test\n    public void matchWorksAsIntended() {\n        var excludedPrefixes = new ExcludedPrefixes(Stream.of(\"/\", \"/api/*\", \"/foo/*\", \"/bar\"));\n\n        assertTrue(excludedPrefixes.matches(\"/\"));\n        assertTrue(excludedPrefixes.matches(\"/api/foobar\"));\n        assertTrue(excludedPrefixes.matches(\"/foo/bar\"));\n        assertTrue(excludedPrefixes.matches(\"/bar\"));\n\n        assertFalse(excludedPrefixes.matches(\"/test\"));\n        assertFalse(excludedPrefixes.matches(\"/api\"));\n        assertFalse(excludedPrefixes.matches(\"/foo\"));\n        assertFalse(excludedPrefixes.matches(\"/baz\"));\n    }\n}\n"
  },
  {
    "path": "server/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <groupId>com.walmartlabs.concord.server</groupId>\n    <artifactId>parent</artifactId>\n    <packaging>pom</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <modules>\n        <module>sdk</module>\n        <module>liquibase-ext</module>\n        <module>db</module>\n        <module>impl</module>\n        <module>queue-client</module>\n        <module>plugins</module>\n        <module>dist</module>\n    </modules>\n</project>\n"
  },
  {
    "path": "server/queue-client/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.server</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-queue-client</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.walmartlabs.concord</groupId>\n            <artifactId>concord-imports</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty.websocket</groupId>\n            <artifactId>jetty-websocket-jetty-client</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty.ee8.websocket</groupId>\n            <artifactId>jetty-ee8-websocket-jetty-client</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty.websocket</groupId>\n            <artifactId>jetty-websocket-jetty-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty.websocket</groupId>\n            <artifactId>jetty-websocket-jetty-common</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty</groupId>\n            <artifactId>jetty-util</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.guava</groupId>\n            <artifactId>guava</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.jboss.spec.javax.ws.rs</groupId>\n            <artifactId>jboss-jaxrs-api_2.1_spec</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-guava</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jdk8</artifactId>\n        </dependency>\n\n        <!-- Immutables -->\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jsr310</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.errorprone</groupId>\n            <artifactId>error_prone_annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>sisu-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n"
  },
  {
    "path": "server/queue-client/src/main/java/com/walmartlabs/concord/server/queueclient/MessageSerializer.java",
    "content": "package com.walmartlabs.concord.server.queueclient;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.guava.GuavaModule;\nimport com.fasterxml.jackson.datatype.jdk8.Jdk8Module;\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\nimport com.google.common.annotations.VisibleForTesting;\nimport com.walmartlabs.concord.server.queueclient.message.Message;\nimport com.walmartlabs.concord.server.queueclient.message.MessageType;\n\nimport java.util.Map;\n\npublic final class MessageSerializer {\n\n    @VisibleForTesting\n    static final ObjectMapper objectMapper = createObjectMapper();\n\n    @SuppressWarnings(\"unchecked\")\n    public static <E extends Message> E deserialize(String msg) {\n        try {\n            Map<String, Object> m = objectMapper.readValue(msg, Map.class);\n            String messageType = (String) m.get(\"messageType\");\n            if (messageType == null) {\n                throw new IllegalStateException(\"messageType not found: \" + m);\n            }\n            return (E) objectMapper.convertValue(m, MessageType.valueOf(messageType).getClazz());\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static String serialize(Message msg) {\n        try {\n            return objectMapper.writeValueAsString(msg);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    private static ObjectMapper createObjectMapper() {\n        ObjectMapper om = new ObjectMapper();\n        om.registerModule(new GuavaModule());\n        om.registerModule(new Jdk8Module());\n        om.registerModule(new JavaTimeModule());\n        return om;\n    }\n\n    private MessageSerializer() {\n    }\n}\n"
  },
  {
    "path": "server/queue-client/src/main/java/com/walmartlabs/concord/server/queueclient/QueueClient.java",
    "content": "package com.walmartlabs.concord.server.queueclient;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.google.common.util.concurrent.SettableFuture;\nimport com.walmartlabs.concord.server.queueclient.message.Message;\nimport com.walmartlabs.concord.server.queueclient.message.MessageType;\nimport org.eclipse.jetty.client.HttpClient;\nimport org.eclipse.jetty.ee8.websocket.api.Session;\nimport org.eclipse.jetty.ee8.websocket.api.WebSocketListener;\nimport org.eclipse.jetty.ee8.websocket.api.WebSocketPingPongListener;\nimport org.eclipse.jetty.ee8.websocket.client.ClientUpgradeRequest;\nimport org.eclipse.jetty.ee8.websocket.client.WebSocketClient;\nimport org.eclipse.jetty.util.ssl.SslContextFactory;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.ws.rs.core.HttpHeaders;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.ClosedChannelException;\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.concurrent.atomic.AtomicReference;\n\npublic class QueueClient {\n\n    public static final String AGENT_UA = \"X-Concord-Agent\";\n    public static final String AGENT_ID = \"X-Concord-Agent-Id\";\n\n    private static final Logger log = LoggerFactory.getLogger(QueueClient.class);\n\n    private final Set<MessageType> ignoreRequests;\n    private final List<RequestEntry> requests;\n\n    private final Worker worker;\n    private Thread workerThread;\n    private boolean onMaintenanceMode;\n\n    public QueueClient(QueueClientConfiguration cfg) throws URISyntaxException {\n        this.ignoreRequests = new HashSet<>();\n        this.requests = new ArrayList<>();\n\n        this.worker = new Worker(cfg, requests);\n    }\n\n    public void start() {\n        this.workerThread = new Thread(worker, \"queue-client\");\n        this.workerThread.start();\n    }\n\n    public void stop() {\n        if (workerThread == null) {\n            return;\n        }\n\n        workerThread.interrupt();\n        workerThread = null;\n    }\n\n    public void maintenanceMode() {\n        if (workerThread == null) {\n            return;\n        }\n\n        synchronized (requests) {\n            if (onMaintenanceMode) {\n                return;\n            }\n            ignoreRequests.add(MessageType.PROCESS_REQUEST);\n            worker.disconnect();\n            onMaintenanceMode = true;\n        }\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public <E extends Message> Future<E> request(Message request) {\n        SettableFuture<Message> f = SettableFuture.create();\n        synchronized (requests) {\n            if (ignoreRequests.contains(request.getMessageType())) {\n                f.set(null);\n            } else {\n                requests.add(new RequestEntry(request, f));\n            }\n        }\n        return (Future<E>) f;\n    }\n\n    private static final class Worker implements Runnable, WebSocketListener, WebSocketPingPongListener {\n\n        private enum State {\n            CONNECTING,\n            CONNECTED,\n            DISCONNECTING\n        }\n\n        private final AtomicLong requestIdGenerator = new AtomicLong();\n\n        private final String agentId;\n        private final String userAgent;\n        private final String apiToken;\n        private final URI[] destUris;\n        private final Map<Long, RequestEntry> awaitResponses;\n        private final List<RequestEntry> requests;\n        private final long pingInterval;\n        private final long maxNoActivityPeriod;\n        private final long connectTimeout;\n\n        private final long processRequestDelay;\n        private final long reconnectDelay;\n\n        private WebSocketClient client;\n        private CompletableFuture<Void> closeFuture;\n\n        private long lastRequestTimestamp;\n        private long lastResponseTimestamp;\n\n        private final AtomicReference<State> state;\n\n        public Worker(QueueClientConfiguration cfg, List<RequestEntry> requests) throws URISyntaxException {\n            this.agentId = cfg.getAgentId();\n            this.userAgent = cfg.getUserAgent();\n            this.apiToken = cfg.getApiKey();\n\n            this.requests = requests;\n            this.destUris = toURIs(cfg.getAddresses());\n            this.awaitResponses = new ConcurrentHashMap<>();\n            this.pingInterval = cfg.getPingInterval();\n            this.maxNoActivityPeriod = cfg.getMaxNoActivityPeriod();\n            this.connectTimeout = cfg.getConnectTimeout();\n\n            this.processRequestDelay = cfg.getProcessRequestDelay();\n            this.reconnectDelay = cfg.getReconnectDelay();\n\n            this.state = new AtomicReference<>(State.CONNECTING);\n        }\n\n        private void mainLoop() {\n            int destUriIndex = 0;\n\n            Session session = null;\n            while (!Thread.currentThread().isInterrupted()) {\n                try {\n                    switch (state.get()) {\n                        case CONNECTING: {\n                            session = connect(destUris[destUriIndex]);\n                            state.set(State.CONNECTED);\n                            lastRequestTimestamp = System.currentTimeMillis();\n                            lastResponseTimestamp = lastRequestTimestamp;\n                            log.info(\"connect ['{}'] -> done\", destUris[destUriIndex]);\n                            break;\n                        }\n                        case CONNECTED: {\n                            processRequests(session);\n                            processPing(session);\n                            sleep(processRequestDelay);\n                            break;\n                        }\n                        case DISCONNECTING: {\n                            close(session);\n                            session = null;\n                            state.set(State.CONNECTING);\n                            sleep(reconnectDelay);\n                            break;\n                        }\n                    }\n                } catch (Exception e) {\n                    log.error(\"mainLoop -> error\", e);\n                    state.set(State.DISCONNECTING);\n\n                    if (++destUriIndex >= destUris.length) {\n                        destUriIndex = 0;\n                    }\n                }\n            }\n\n            if (session != null) {\n                close(session);\n            }\n        }\n\n        @Override\n        public void run() {\n            mainLoop();\n\n            log.info(\"run -> finished\");\n        }\n\n        @Override\n        public void onWebSocketPong(ByteBuffer payload) {\n            this.lastResponseTimestamp = System.currentTimeMillis();\n        }\n\n        @Override\n        public void onWebSocketPing(ByteBuffer payload) {\n            // we don't expect pings\n        }\n\n        @Override\n        public void onWebSocketBinary(byte[] payload, int offset, int len) {\n            log.warn(\"onWebSocketBinary [{}, '{}'] -> ignored\", offset, len);\n        }\n\n        @Override\n        public void onWebSocketText(String message) {\n            this.lastResponseTimestamp = System.currentTimeMillis();\n\n            Message response = MessageSerializer.deserialize(message);\n            RequestEntry request = awaitResponses.remove(response.getCorrelationId());\n            if (request == null) {\n                log.error(\"onWebSocketText ['{}'] -> request not found\", message);\n                return;\n            }\n            request.onResponse(response);\n            log.debug(\"onWebSocketText ['{}'] -> done\", message);\n        }\n\n        @Override\n        public void onWebSocketClose(int statusCode, String reason) {\n            state.set(State.DISCONNECTING);\n            closeFuture.complete(null);\n            log.info(\"onWebSocketClose [{}, '{}'] -> done\", statusCode, reason);\n        }\n\n        @Override\n        public void onWebSocketConnect(Session session) {\n            log.info(\"onWebSocketConnect ['{}'] -> done\", session);\n        }\n\n        @Override\n        public void onWebSocketError(Throwable cause) {\n            if (cause instanceof ClosedChannelException) {\n                log.warn(\"onWebSocketError -> closed channel\");\n            } else {\n                log.error(\"onWebSocketError -> '{}'\", cause.getMessage());\n            }\n        }\n\n        public void disconnect() {\n            state.set(State.DISCONNECTING);\n        }\n\n        private Session connect(URI destUri) throws Exception {\n            this.client = createWebSocketClient(connectTimeout);\n            this.client.start();\n\n            closeFuture = new CompletableFuture<>();\n\n            ClientUpgradeRequest request = new ClientUpgradeRequest();\n            request.setHeader(AGENT_ID, agentId);\n            request.setHeader(AGENT_UA, userAgent);\n            request.setHeader(HttpHeaders.AUTHORIZATION, apiToken);\n            return client.connect(this, destUri, request).get(connectTimeout, TimeUnit.MILLISECONDS);\n        }\n\n        private boolean send(Session session, Message message) {\n            if (!session.isOpen()) {\n                return false;\n            }\n\n            message.setCorrelationId(requestIdGenerator.incrementAndGet());\n            try {\n                session.getRemote().sendString(MessageSerializer.serialize(message));\n\n                lastRequestTimestamp = System.currentTimeMillis();\n\n                log.info(\"send ['{}'] -> done\", message);\n                return true;\n            } catch (Exception e) {\n                log.error(\"send ['{}'] -> error\", message, e);\n                return false;\n            }\n        }\n\n        private void processRequests(Session session) {\n            RequestEntry e = nextRequest();\n            if (e == null) {\n                return;\n            }\n\n            boolean sent = send(session, e.request);\n            if (!sent) {\n                e.cancel();\n                state.set(State.DISCONNECTING);\n                return;\n            }\n\n            awaitResponses.put(e.getCorrelationId(), e);\n        }\n\n        private void processPing(Session session) throws IOException {\n            long now = System.currentTimeMillis();\n\n            if (now - lastRequestTimestamp >= pingInterval) {\n                session.getRemote().sendPing(ByteBuffer.wrap(\"ping\".getBytes()));\n            }\n\n            if (now - lastResponseTimestamp >= maxNoActivityPeriod) {\n                log.warn(\"processPing -> no response for more than {}ms...\", maxNoActivityPeriod);\n                disconnect();\n            }\n        }\n\n        private void close(Session session) {\n            try {\n                if (session != null && session.isOpen()) {\n                    session.close();\n\n                    closeFuture.get(5, TimeUnit.SECONDS);\n                    closeFuture = null;\n                }\n            } catch (TimeoutException e) {\n                log.warn(\"close -> error: timeout waiting for close\");\n            } catch (Exception e) {\n                log.warn(\"close -> error\", e);\n            }\n\n            try {\n                this.client.stop();\n                this.client.destroy();\n            } catch (InterruptedException e) {\n                // ignore, we're stopping anyway\n            } catch (Exception e) {\n                log.warn(\"stop -> error: {}\", e.getMessage());\n            }\n\n            synchronized (this.requests) {\n                this.requests.forEach(RequestEntry::cancel);\n                this.requests.clear();\n\n                this.awaitResponses.values().forEach(RequestEntry::cancel);\n                this.awaitResponses.clear();\n            }\n\n            log.info(\"close -> done\");\n        }\n\n        private RequestEntry nextRequest() {\n            synchronized (requests) {\n                Iterator<RequestEntry> it = requests.iterator();\n                while (it.hasNext()) {\n                    RequestEntry request = it.next();\n                    if (!alreadySent(request.request.getMessageType())) {\n                        it.remove();\n                        return request;\n                    }\n                }\n            }\n\n            return null;\n        }\n\n        private boolean alreadySent(MessageType requestType) {\n            return awaitResponses.values().stream()\n                    .anyMatch(ar -> ar.request.getMessageType() == requestType);\n        }\n\n        private void sleep(long ms) {\n            try {\n                Thread.sleep(ms);\n            } catch (InterruptedException e) {\n                Thread.currentThread().interrupt();\n            }\n        }\n\n        private static URI[] toURIs(String[] as) throws URISyntaxException {\n            URI[] result = new URI[as.length];\n            for (int i = 0; i < as.length; i++) {\n                result[i] = new URI(as[i]);\n            }\n            return result;\n        }\n    }\n\n    private static class RequestEntry {\n\n        private final Message request;\n        private final SettableFuture<Message> future;\n\n        public RequestEntry(Message request, SettableFuture<Message> future) {\n            this.request = request;\n            this.future = future;\n        }\n\n        public void onResponse(Message response) {\n            future.set(response);\n        }\n\n        public void cancel() {\n            future.set(null);\n        }\n\n        public Long getCorrelationId() {\n            return request.getCorrelationId();\n        }\n    }\n\n    private static WebSocketClient createWebSocketClient(long connectTimeout) {\n        SslContextFactory.Client scf = new SslContextFactory.Client();\n        scf.setValidateCerts(false);\n        scf.setValidatePeerCerts(false);\n        scf.setTrustAll(true);\n\n        HttpClient httpClient = new HttpClient();\n        httpClient.setSslContextFactory(scf);\n\n        WebSocketClient c = new WebSocketClient(httpClient);\n        c.setStopAtShutdown(false);\n        c.setConnectTimeout(connectTimeout);\n        return c;\n    }\n}\n"
  },
  {
    "path": "server/queue-client/src/main/java/com/walmartlabs/concord/server/queueclient/QueueClientConfiguration.java",
    "content": "package com.walmartlabs.concord.server.queueclient;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\n\npublic class QueueClientConfiguration implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String[] addresses;\n    private final String agentId;\n    private final String apiKey;\n    private final String userAgent;\n    private final long connectTimeout;\n    private final long pingInterval;\n    private final long maxNoActivityPeriod;\n\n    private final long processRequestDelay;\n    private final long reconnectDelay;\n\n    private QueueClientConfiguration(Builder b) {\n        this.addresses = b.addresses;\n        this.agentId = b.agentId;\n        this.apiKey = b.apiKey;\n        this.userAgent = b.userAgent;\n        this.connectTimeout = b.connectTimeout;\n        this.pingInterval = b.pingInterval;\n        this.maxNoActivityPeriod = b.maxNoActivityPeriod;\n        this.processRequestDelay = b.processRequestDelay;\n        this.reconnectDelay = b.reconnectDelay;\n    }\n\n    public String[] getAddresses() {\n        return addresses;\n    }\n\n    public String getAgentId() {\n        return agentId;\n    }\n\n    public String getApiKey() {\n        return apiKey;\n    }\n\n    public String getUserAgent() {\n        return userAgent;\n    }\n\n    public long getConnectTimeout() {\n        return connectTimeout;\n    }\n\n    public long getPingInterval() {\n        return pingInterval;\n    }\n\n    public long getMaxNoActivityPeriod() {\n        return maxNoActivityPeriod;\n    }\n\n    public long getProcessRequestDelay() {\n        return processRequestDelay;\n    }\n\n    public long getReconnectDelay() {\n        return reconnectDelay;\n    }\n\n    public static class Builder {\n\n        private final String[] addresses;\n\n        private String agentId;\n        private String apiKey;\n        private String userAgent;\n        private long connectTimeout = 30000;\n        private long pingInterval = 10000;\n        private long maxNoActivityPeriod = 30000;\n        private long processRequestDelay = 1000;\n        private long reconnectDelay = 10000;\n\n        public Builder(String[] addresses) {\n            this.addresses = addresses;\n        }\n\n        public Builder agentId(String agentId) {\n            this.agentId = agentId;\n            return this;\n        }\n\n        public Builder apiKey(String apiKey) {\n            this.apiKey = apiKey;\n            return this;\n        }\n\n        public Builder userAgent(String userAgent) {\n            this.userAgent = userAgent;\n            return this;\n        }\n\n        public Builder connectTimeout(long connectTimeout) {\n            this.connectTimeout = connectTimeout;\n            return this;\n        }\n\n        public Builder pingInterval(long pingInterval) {\n            this.pingInterval = pingInterval;\n            return this;\n        }\n\n        public Builder maxNoActivityPeriod(long maxNoActivityPeriod) {\n            this.maxNoActivityPeriod = maxNoActivityPeriod;\n            return this;\n        }\n\n        public Builder processRequestDelay(long processRequestDelay) {\n            this.processRequestDelay = processRequestDelay;\n            return this;\n        }\n\n        public Builder reconnectDelay(long reconnectDelay) {\n            this.reconnectDelay = reconnectDelay;\n            return this;\n        }\n\n        public QueueClientConfiguration build() {\n            return new QueueClientConfiguration(this);\n        }\n    }\n}\n"
  },
  {
    "path": "server/queue-client/src/main/java/com/walmartlabs/concord/server/queueclient/message/CommandRequest.java",
    "content": "package com.walmartlabs.concord.server.queueclient.message;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.util.UUID;\n\npublic class CommandRequest extends Message {\n\n    private final UUID agentId;\n\n    @JsonCreator\n    public CommandRequest(\n            @JsonProperty(\"agentId\") UUID agentId) {\n        super(MessageType.COMMAND_REQUEST);\n        this.agentId = agentId;\n    }\n\n    public UUID getAgentId() {\n        return agentId;\n    }\n\n    @Override\n    public String toString() {\n        return \"CommandRequest{\" +\n                \"correlationId='\" + getCorrelationId() + \"', \" +\n                \"agentId='\" + agentId + \"'\" +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/queue-client/src/main/java/com/walmartlabs/concord/server/queueclient/message/CommandResponse.java",
    "content": "package com.walmartlabs.concord.server.queueclient.message;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.util.Map;\n\npublic class CommandResponse extends Message {\n\n    public enum CommandType {\n        CANCEL_JOB\n    }\n\n    public static CommandResponse cancel(long correlationId, Map<String, Object> payload) {\n        return new CommandResponse(correlationId, CommandType.CANCEL_JOB, payload);\n    }\n\n    private final CommandType type;\n\n    private final Map<String, Object> payload;\n\n    @JsonCreator\n    public CommandResponse(\n            @JsonProperty(\"correlationId\") long correlationId,\n            @JsonProperty(\"type\") CommandType type,\n            @JsonProperty(\"payload\") Map<String, Object> payload) {\n        super(MessageType.COMMAND_RESPONSE);\n        setCorrelationId(correlationId);\n        this.type = type;\n        this.payload = payload;\n    }\n\n    public CommandType getType() {\n        return type;\n    }\n\n    public Map<String, Object> getPayload() {\n        return payload;\n    }\n\n    @Override\n    public String toString() {\n        return \"CommandRequest{\" +\n                \"correlationId='\" + getCorrelationId() + \"', \" +\n                \"type='\" + type + \"', \" +\n                \"payload='\" + payload + \"'\" +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/queue-client/src/main/java/com/walmartlabs/concord/server/queueclient/message/Message.java",
    "content": "package com.walmartlabs.concord.server.queueclient.message;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport static com.fasterxml.jackson.annotation.JsonInclude.Include;\n\n@JsonInclude(Include.NON_NULL)\n@JsonIgnoreProperties(ignoreUnknown=true)\npublic abstract class Message {\n\n    private final MessageType messageType;\n\n    private long correlationId;\n\n    @JsonCreator\n    protected Message(\n            @JsonProperty(\"messageType\") MessageType messageType) {\n        this.messageType = messageType;\n    }\n\n    public MessageType getMessageType() {\n        return messageType;\n    }\n\n    public void setCorrelationId(long correlationId) {\n        this.correlationId = correlationId;\n    }\n\n    public long getCorrelationId() {\n        return correlationId;\n    }\n}\n"
  },
  {
    "path": "server/queue-client/src/main/java/com/walmartlabs/concord/server/queueclient/message/MessageType.java",
    "content": "package com.walmartlabs.concord.server.queueclient.message;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic enum MessageType {\n\n    COMMAND_REQUEST(CommandRequest.class),\n    COMMAND_RESPONSE(CommandResponse.class),\n    PROCESS_REQUEST(ProcessRequest.class),\n    PROCESS_RESPONSE(ProcessResponse.class);\n\n    private final Class<? extends Message> clazz;\n\n    MessageType(Class<? extends Message> clazz) {\n        this.clazz = clazz;\n    }\n\n    public Class<? extends Message> getClazz() {\n        return clazz;\n    }\n}\n"
  },
  {
    "path": "server/queue-client/src/main/java/com/walmartlabs/concord/server/queueclient/message/ProcessRequest.java",
    "content": "package com.walmartlabs.concord.server.queueclient.message;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.util.Map;\n\npublic class ProcessRequest extends Message {\n\n    private final Map<String, Object> capabilities;\n\n    @JsonCreator\n    public ProcessRequest(\n            @JsonProperty(\"capabilities\") Map<String, Object> capabilities) {\n        super(MessageType.PROCESS_REQUEST);\n        this.capabilities = capabilities;\n    }\n\n    public Map<String, Object> getCapabilities() {\n        return capabilities;\n    }\n\n    @Override\n    public String toString() {\n        return \"ProcessRequest{\" +\n                \"correlationId='\" + getCorrelationId() + \"', \" +\n                \"capabilities='\" + capabilities + \"'\" +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/queue-client/src/main/java/com/walmartlabs/concord/server/queueclient/message/ProcessResponse.java",
    "content": "package com.walmartlabs.concord.server.queueclient.message;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonFormat;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.imports.Imports;\n\nimport java.time.OffsetDateTime;\nimport java.util.Map;\nimport java.util.UUID;\n\npublic class ProcessResponse extends Message {\n\n    private final String sessionToken;\n    private final UUID processId;\n    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = \"yyyy-MM-dd'T'HH:mm:ss.SSSX\")\n    private final OffsetDateTime processCreatedAt;\n    private final String orgName; // TODO rename to secretOrgName\n    private final String repoUrl;\n    private final String repoPath;\n    private final String commitId;\n    private final String repoBranch;\n    private final String secretName;\n    private final Imports imports;\n    private final Map<String, Object> requirements;\n\n    @JsonCreator\n    public ProcessResponse(\n            @JsonProperty(\"correlationId\") long correlationId,\n            @JsonProperty(\"sessionToken\") String sessionToken,\n            @JsonProperty(\"processId\") UUID processId,\n            @JsonProperty(\"processCreatedAt\") OffsetDateTime processCreatedAt,\n            @JsonProperty(\"orgName\") String orgName,\n            @JsonProperty(\"repoUrl\") String repoUrl,\n            @JsonProperty(\"repoPath\") String repoPath,\n            @JsonProperty(\"commitId\") String commitId,\n            @JsonProperty(\"repoBranch\") String repoBranch,\n            @JsonProperty(\"secretName\") String secretName,\n            @JsonProperty(\"imports\") Imports imports,\n            @JsonProperty(\"requirements\") Map<String, Object> requirements) {\n\n        super(MessageType.PROCESS_RESPONSE);\n\n        setCorrelationId(correlationId);\n        this.sessionToken = sessionToken;\n        this.processId = processId;\n        this.processCreatedAt = processCreatedAt;\n        this.orgName = orgName;\n        this.repoUrl = repoUrl;\n        this.repoPath = repoPath;\n        this.commitId = commitId;\n        this.repoBranch = repoBranch;\n        this.secretName = secretName;\n        this.imports = imports;\n        this.requirements = requirements;\n    }\n\n    public String getSessionToken() {\n        return sessionToken;\n    }\n\n    public UUID getProcessId() {\n        return processId;\n    }\n\n    public OffsetDateTime getProcessCreatedAt() {\n        return processCreatedAt;\n    }\n\n    public String getOrgName() {\n        return orgName;\n    }\n\n    public String getRepoUrl() {\n        return repoUrl;\n    }\n\n    public String getRepoPath() {\n        return repoPath;\n    }\n\n    public String getRepoBranch() {\n        return repoBranch;\n    }\n\n    public String getCommitId() {\n        return commitId;\n    }\n\n    public String getSecretName() {\n        return secretName;\n    }\n\n    public Imports getImports() {\n        return imports;\n    }\n\n    public Map<String, Object> getRequirements() {\n        return requirements;\n    }\n\n    @Override\n    public String toString() {\n        return \"ProcessResponse{\" +\n                \"sessionToken='***'\" +\n                \", processId=\" + processId +\n                \", processCreatedAt=\" + processCreatedAt + '\\'' +\n                \", orgName='\" + orgName + '\\'' +\n                \", repoUrl='\" + repoUrl + '\\'' +\n                \", repoPath='\" + repoPath + '\\'' +\n                \", commitId='\" + commitId + '\\'' +\n                \", repoBranch='\" + repoBranch + '\\'' +\n                \", secretName='\" + secretName + '\\'' +\n                \", imports=\" + imports +\n                \", requirements=\" + requirements +\n                '}';\n    }\n}\n"
  },
  {
    "path": "server/queue-client/src/test/java/com/walmartlabs/concord/server/queueclient/MessageSerializerTest.java",
    "content": "package com.walmartlabs.concord.server.queueclient;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport com.walmartlabs.concord.imports.Import;\nimport com.walmartlabs.concord.imports.Import.SecretDefinition;\nimport com.walmartlabs.concord.imports.Imports;\nimport com.walmartlabs.concord.server.queueclient.message.*;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.OffsetDateTime;\nimport java.time.ZoneId;\nimport java.time.temporal.ChronoUnit;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertNotNull;\n\npublic class MessageSerializerTest {\n\n    @Test\n    public void testCommandRequest() {\n        CommandRequest r = new CommandRequest(UUID.randomUUID());\n        r.setCorrelationId(123);\n\n        String rSerialized = MessageSerializer.serialize(r);\n        assertNotNull(rSerialized);\n\n        CommandRequest rDeserialized = MessageSerializer.deserialize(rSerialized);\n        assertEquals(r.getMessageType(), MessageType.COMMAND_REQUEST);\n        assertEquals(r.getAgentId(), rDeserialized.getAgentId());\n        assertEquals(r.getCorrelationId(), rDeserialized.getCorrelationId());\n    }\n\n    @Test\n    public void testCommandResponse() {\n        CommandResponse r = new CommandResponse(123, CommandResponse.CommandType.CANCEL_JOB, null);\n\n        String rSerialized = MessageSerializer.serialize(r);\n        assertNotNull(rSerialized);\n\n        CommandResponse rDeserialized = MessageSerializer.deserialize(rSerialized);\n        assertEquals(r.getMessageType(), MessageType.COMMAND_RESPONSE);\n        assertEquals(r.getType(), rDeserialized.getType());\n        assertEquals(r.getPayload(), rDeserialized.getPayload());\n        assertEquals(r.getCorrelationId(), rDeserialized.getCorrelationId());\n    }\n\n    @Test\n    public void testProcessRequest() {\n        ProcessRequest r = new ProcessRequest(Collections.singletonMap(\"k\", \"v\"));\n        r.setCorrelationId(123);\n\n        String rSerialized = MessageSerializer.serialize(r);\n        assertNotNull(rSerialized);\n\n        ProcessRequest rDeserialized = MessageSerializer.deserialize(rSerialized);\n        assertEquals(r.getMessageType(), MessageType.PROCESS_REQUEST);\n        assertEquals(r.getCapabilities(), rDeserialized.getCapabilities());\n        assertEquals(r.getCorrelationId(), rDeserialized.getCorrelationId());\n    }\n\n    @Test\n    public void testUnknownProperties() {\n        String str = \"{\\\"sessionToken\\\":\\\"123123\\\", \\\"correlationId\\\":123, \\\"processId\\\":\\\"b26a60c6-b54e-4f4d-bf0a-abafb908bf76\\\", \\\"messageType\\\":\\\"PROCESS_RESPONSE\\\", \\\"unknown\\\":\\\"value\\\"}\";\n        ProcessResponse rDeserialized = MessageSerializer.deserialize(str);\n        assertEquals(123, rDeserialized.getCorrelationId());\n    }\n\n    @Test\n    public void testLegacyProcessResponseDeserializesNewerServerPayload() throws Exception {\n        String str = \"{\\\"sessionToken\\\":\\\"123123\\\", \\\"correlationId\\\":123, \\\"processId\\\":\\\"b26a60c6-b54e-4f4d-bf0a-abafb908bf76\\\", \\\"messageType\\\":\\\"PROCESS_RESPONSE\\\", \\\"requirements\\\":{\\\"agent\\\":{\\\"flavor\\\":[\\\"default\\\"]}}}\";\n\n        @SuppressWarnings(\"unchecked\")\n        Map<String, Object> payload = MessageSerializer.objectMapper.readValue(str, Map.class);\n        LegacyProcessResponse rDeserialized = MessageSerializer.objectMapper.convertValue(payload, LegacyProcessResponse.class);\n\n        assertEquals(123, rDeserialized.getCorrelationId());\n        assertEquals(\"123123\", rDeserialized.getSessionToken());\n        assertEquals(UUID.fromString(\"b26a60c6-b54e-4f4d-bf0a-abafb908bf76\"), rDeserialized.getProcessId());\n    }\n\n    @Test\n    public void testProcessResponse() {\n        SecretDefinition secret = SecretDefinition.builder()\n                .org(\"secret-org\")\n                .name(\"secret-name\")\n                .password(\"secret-password\")\n                .build();\n\n        Import item = Import.GitDefinition.builder()\n                .url(\"http://url\")\n                .version(\"master\")\n                .dest(\"concord\")\n                .path(\"path1\")\n                .secret(secret)\n                .build();\n\n        Imports imports = Imports.of(Collections.singletonList(item));\n        Map<String, Object> requirements = Map.of(\n                \"agent\", Map.of(\n                        \"flavor\", List.of(\"default\")\n                ),\n                \"custom\", Map.of(\n                        \"foo\", \"bar\"\n                ));\n\n        OffsetDateTime createdAt = OffsetDateTime.now(ZoneId.of(\"UTC\")).minusMinutes(10).truncatedTo(ChronoUnit.MILLIS);\n        ProcessResponse r = new ProcessResponse(123, \"sesion-token\", UUID.randomUUID(), createdAt, \"org-name\", \"repo-url\", \"repo-path\", \"commit-id\", \"repo-branch\", \"secret-name\", imports, requirements);\n\n        String rSerialized = MessageSerializer.serialize(r);\n        assertNotNull(rSerialized);\n\n        ProcessResponse rDeserialized = MessageSerializer.deserialize(rSerialized);\n        assertEquals(MessageType.PROCESS_RESPONSE, r.getMessageType());\n        assertEquals(r.getSessionToken(), rDeserialized.getSessionToken());\n        assertEquals(r.getProcessId(), rDeserialized.getProcessId());\n        assertEquals(createdAt, rDeserialized.getProcessCreatedAt());\n        assertEquals(r.getCorrelationId(), rDeserialized.getCorrelationId());\n        assertEquals(\"repo-branch\", rDeserialized.getRepoBranch());\n        assertEquals(requirements, rDeserialized.getRequirements());\n    }\n\n    private static class LegacyProcessResponse extends Message {\n\n        private final String sessionToken;\n        private final UUID processId;\n\n        @JsonCreator\n        private LegacyProcessResponse(@JsonProperty(\"correlationId\") long correlationId,\n                                      @JsonProperty(\"sessionToken\") String sessionToken,\n                                      @JsonProperty(\"processId\") UUID processId) {\n\n            super(MessageType.PROCESS_RESPONSE);\n            setCorrelationId(correlationId);\n            this.sessionToken = sessionToken;\n            this.processId = processId;\n        }\n\n        public String getSessionToken() {\n            return sessionToken;\n        }\n\n        public UUID getProcessId() {\n            return processId;\n        }\n    }\n}\n"
  },
  {
    "path": "server/sdk/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>com.walmartlabs.concord.server</groupId>\n        <artifactId>parent</artifactId>\n        <version>2.40.1-SNAPSHOT</version>\n        <relativePath>../pom.xml</relativePath>\n    </parent>\n\n    <artifactId>concord-server-sdk</artifactId>\n    <packaging>jar</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.jboss.spec.javax.ws.rs</groupId>\n            <artifactId>jboss-jaxrs-api_2.1_spec</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>io.dropwizard.metrics</groupId>\n            <artifactId>metrics-core</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-annotations</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.core</groupId>\n            <artifactId>jackson-databind</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <!-- Immutables -->\n        <dependency>\n            <groupId>org.immutables</groupId>\n            <artifactId>value</artifactId>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.google.code.findbugs</groupId>\n            <artifactId>jsr305</artifactId>\n            <scope>provided</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter-api</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>com.fasterxml.jackson.datatype</groupId>\n            <artifactId>jackson-datatype-jsr310</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n</project>\n\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/AllowNulls.java",
    "content": "package com.walmartlabs.concord.server.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Target;\n\n@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.TYPE_USE})\npublic @interface AllowNulls {}"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/BackgroundTask.java",
    "content": "package com.walmartlabs.concord.server.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface BackgroundTask {\n\n    default void start() {\n    }\n\n    default void stop() {\n    }\n}"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/ConcordApplicationException.java",
    "content": "package com.walmartlabs.concord.server.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport javax.ws.rs.WebApplicationException;\nimport javax.ws.rs.core.MediaType;\nimport javax.ws.rs.core.Response;\nimport java.io.Serial;\n\npublic class ConcordApplicationException extends WebApplicationException {\n\n    @Serial\n    private static final long serialVersionUID = 1L;\n\n    public ConcordApplicationException(Response resp) {\n        super(resp);\n    }\n\n    public ConcordApplicationException(Throwable cause) {\n        this(cause.getMessage(), cause);\n    }\n\n    public ConcordApplicationException(String message) {\n        this(message, (Throwable) null);\n    }\n\n    public ConcordApplicationException(String message, Throwable cause) {\n        super(message, cause, Response.status(Response.Status.INTERNAL_SERVER_ERROR)\n                .entity(message)\n                .type(MediaType.TEXT_PLAIN_TYPE)\n                .build());\n    }\n\n    public ConcordApplicationException(String message, Response.Status status) {\n        super(message, null, Response.status(status)\n                .entity(message)\n                .type(MediaType.TEXT_PLAIN_TYPE)\n                .build());\n    }\n\n    public ConcordApplicationException(String message, Throwable cause, Response.Status status) throws IllegalArgumentException {\n        super(message, cause, Response.status(status)\n                .entity(message)\n                .type(MediaType.TEXT_PLAIN_TYPE)\n                .build());\n    }\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/PartialProcessKey.java",
    "content": "package com.walmartlabs.concord.server.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.io.Serializable;\nimport java.util.Objects;\nimport java.util.UUID;\n\n/**\n * Partial key of a process. Contains only the ID and can be used to\n * start a new process or to retrieve the complete process key {@link ProcessKey}.\n */\npublic class PartialProcessKey implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    /**\n     * Creates a partial process key from a known instance ID.\n     */\n    public static PartialProcessKey from(UUID instanceId) {\n        return new PartialProcessKey(instanceId);\n    }\n\n    /**\n     * Creates a new unique partial process key.\n     */\n    public static PartialProcessKey create() {\n        return PartialProcessKey.from(UUID.randomUUID());\n    }\n\n    private final UUID instanceId;\n\n    protected PartialProcessKey(UUID instanceId) {\n        this.instanceId = instanceId;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        PartialProcessKey that = (PartialProcessKey) o;\n        return Objects.equals(instanceId, that.instanceId);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(instanceId);\n    }\n\n    public boolean partOf(PartialProcessKey p) {\n        return instanceId.equals(p.getInstanceId());\n    }\n\n    public UUID getInstanceId() {\n        return instanceId;\n    }\n\n    @Override\n    public String toString() {\n        return instanceId.toString();\n    }\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/ProcessKey.java",
    "content": "package com.walmartlabs.concord.server.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonCreator;\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\nimport java.time.OffsetDateTime;\nimport java.time.temporal.ChronoUnit;\nimport java.util.Objects;\nimport java.util.UUID;\n\n/**\n * Composite key of a process. Identifies process resources in partitioned tables.\n * @apiNote the \"createdAt\" portion of the key must be truncated to microseconds.\n */\npublic class ProcessKey extends PartialProcessKey {\n\n    private static final long serialVersionUID = 1L;\n\n    private final OffsetDateTime createdAt;\n\n    public static ProcessKey random() {\n        return new ProcessKey(UUID.randomUUID(), OffsetDateTime.now().truncatedTo(ChronoUnit.MICROS));\n    }\n\n    public ProcessKey(PartialProcessKey part, OffsetDateTime createdAt) {\n        this(part.getInstanceId(), createdAt);\n    }\n\n    @JsonCreator\n    public ProcessKey(@JsonProperty(\"instanceId\") UUID instanceId,\n                      @JsonProperty(\"createdAt\") OffsetDateTime createdAt) {\n\n        super(instanceId);\n\n        if (createdAt.getNano() != createdAt.truncatedTo(ChronoUnit.MICROS).getNano()) {\n            throw new IllegalArgumentException(\"The process' createdAt must be truncated to microseconds: \" + createdAt);\n        }\n\n        this.createdAt = createdAt;\n    }\n\n    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n        if (!super.equals(o)) return false;\n        ProcessKey that = (ProcessKey) o;\n        return Objects.equals(createdAt, that.createdAt);\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(super.hashCode(), createdAt);\n    }\n\n    public OffsetDateTime getCreatedAt() {\n        return createdAt;\n    }\n\n    public String toString() {\n        return getInstanceId().toString();\n    }\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/ProcessKeyCache.java",
    "content": "package com.walmartlabs.concord.server.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.UUID;\n\npublic interface ProcessKeyCache {\n\n    ProcessKey assertKey(UUID instanceId);\n\n    ProcessKey get(UUID instanceId);\n\n    long hitCount();\n\n    long missCount();\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/ProcessStatus.java",
    "content": "package com.walmartlabs.concord.server.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n\npublic enum ProcessStatus {\n    NEW,\n    PREPARING,\n    ENQUEUED,\n    WAITING,\n    STARTING,\n    RUNNING,\n    SUSPENDED,\n    RESUMING,\n    FINISHED,\n    FAILED,\n    CANCELLED,\n    TIMED_OUT\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/Range.java",
    "content": "package com.walmartlabs.concord.server.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport org.immutables.value.Value;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableRange.class)\n@JsonDeserialize(as = ImmutableRange.class)\npublic interface Range {\n\n    Mode lowerMode();\n\n    int lower();\n\n    int upper();\n\n    Mode upperMode();\n\n    static ImmutableRange.Builder builder() {\n        return ImmutableRange.builder();\n    }\n\n    enum Mode {\n        INCLUSIVE,\n        EXCLUSIVE\n    }\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/ScheduledTask.java",
    "content": "package com.walmartlabs.concord.server.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface ScheduledTask {\n\n    String getId();\n\n    long getIntervalInSec();\n\n    void performTask() throws Exception;\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/audit/AuditEvent.java",
    "content": "package com.walmartlabs.concord.server.sdk.audit;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.server.sdk.AllowNulls;\nimport org.immutables.value.Value;\n\nimport javax.annotation.Nullable;\nimport java.io.Serializable;\nimport java.time.OffsetDateTime;\nimport java.util.Map;\nimport java.util.UUID;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableAuditEvent.class)\n@JsonDeserialize(as = ImmutableAuditEvent.class)\npublic interface AuditEvent extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    long entrySeq();\n\n    OffsetDateTime entryDate();\n\n    @Nullable\n    UUID userId();\n\n    String object();\n\n    String action();\n\n    @AllowNulls\n    Map<String, Object> details();\n\n    static ImmutableAuditEvent.Builder builder() {\n        return ImmutableAuditEvent.builder();\n    }\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/audit/AuditLogListener.java",
    "content": "package com.walmartlabs.concord.server.sdk.audit;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface AuditLogListener {\n\n    void onEvent(AuditEvent event);\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/events/ProcessEvent.java",
    "content": "package com.walmartlabs.concord.server.sdk.events;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.server.sdk.AllowNulls;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\nimport java.time.OffsetDateTime;\nimport java.util.Map;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableProcessEvent.class)\n@JsonDeserialize(as = ImmutableProcessEvent.class)\npublic interface ProcessEvent extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    long eventSeq();\n\n    ProcessKey processKey();\n\n    String eventType();\n\n    OffsetDateTime eventDate();\n\n    @AllowNulls\n    Map<String, Object> data();\n\n    static ImmutableProcessEvent.Builder builder() {\n        return ImmutableProcessEvent.builder();\n    }\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/events/ProcessEventListener.java",
    "content": "package com.walmartlabs.concord.server.sdk.events;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.List;\n\npublic interface ProcessEventListener {\n\n    void onEvents(List<ProcessEvent> events);\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/log/ProcessLogEntry.java",
    "content": "package com.walmartlabs.concord.server.sdk.log;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2020 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.databind.annotation.JsonDeserialize;\nimport com.fasterxml.jackson.databind.annotation.JsonSerialize;\nimport com.walmartlabs.concord.server.sdk.ProcessKey;\nimport com.walmartlabs.concord.server.sdk.Range;\nimport org.immutables.value.Value;\n\nimport java.io.Serializable;\n\n@Value.Immutable\n@Value.Style(jdkOnly = true)\n@JsonInclude(JsonInclude.Include.NON_EMPTY)\n@JsonSerialize(as = ImmutableProcessLogEntry.class)\n@JsonDeserialize(as = ImmutableProcessLogEntry.class)\npublic interface ProcessLogEntry extends Serializable {\n\n    long serialVersionUID = 1L;\n\n    ProcessKey processKey();\n\n    Range range();\n\n    byte[] msg();\n\n    static ImmutableProcessLogEntry.Builder builder() {\n        return ImmutableProcessLogEntry.builder();\n    }\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/log/ProcessLogListener.java",
    "content": "package com.walmartlabs.concord.server.sdk.log;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2019 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\npublic interface ProcessLogListener {\n\n    void onAppend(ProcessLogEntry entry);\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/metrics/GaugeProvider.java",
    "content": "package com.walmartlabs.concord.server.sdk.metrics;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.codahale.metrics.Gauge;\n\npublic interface GaugeProvider<T> {\n\n    String name();\n\n    Gauge<T> gauge();\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/metrics/InjectCounter.java",
    "content": "package com.walmartlabs.concord.server.sdk.metrics;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.FIELD)\npublic @interface InjectCounter {\n\n    String value() default \"\";\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/metrics/InjectMeter.java",
    "content": "package com.walmartlabs.concord.server.sdk.metrics;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.FIELD)\npublic @interface InjectMeter {\n\n    String value() default \"\";\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/metrics/WithTimer.java",
    "content": "package com.walmartlabs.concord.server.sdk.metrics;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2018 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Retention(RetentionPolicy.RUNTIME)\n@Target(ElementType.METHOD)\npublic @interface WithTimer {\n\n    String suffix() default \"\";\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/rest/ApiDescriptor.java",
    "content": "package com.walmartlabs.concord.server.sdk.rest;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n/**\n * Declares a list of prefixes that should be served as JAX-RS resources.\n */\npublic interface ApiDescriptor {\n\n    /**\n     * Path patterns of the API. E.g. {@code \"/api/*\"} or {@code \"/events/foo\"}.\n     */\n    String[] paths();\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/rest/Component.java",
    "content": "package com.walmartlabs.concord.server.sdk.rest;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n/**\n * Marker for JAX-RS components.\n */\npublic interface Component {\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/rest/Resource.java",
    "content": "package com.walmartlabs.concord.server.sdk.rest;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\n/**\n * Marker for JAX-RS resources.\n */\npublic interface Resource extends Component {\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/security/AuthenticationException.java",
    "content": "package com.walmartlabs.concord.server.sdk.security;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.walmartlabs.concord.server.sdk.ConcordApplicationException;\n\nimport static javax.ws.rs.core.Response.Status.UNAUTHORIZED;\n\npublic class AuthenticationException extends ConcordApplicationException {\n\n    public AuthenticationException(String message, Throwable cause) {\n        super(message, cause, UNAUTHORIZED);\n    }\n\n    public AuthenticationException(String message) {\n        super(message, UNAUTHORIZED);\n    }\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/validation/Validate.java",
    "content": "package com.walmartlabs.concord.server.sdk.validation;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Validate arguments and/or return values.\n */\n@Target({ElementType.METHOD})\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface Validate {\n\n    Class<?>[] groups() default {};\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/validation/ValidationErrorXO.java",
    "content": "package com.walmartlabs.concord.server.sdk.validation;\n\n/*\n * Copyright (c) 2007-2014 Sonatype, Inc. All rights reserved.\n *\n * This program is licensed to you under the Apache License Version 2.0,\n * and you may not use this file except in compliance with the Apache License Version 2.0.\n * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the Apache License Version 2.0 is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.\n *\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n/**\n * Based on the original {@link org.sonatype.siesta.ValidationErrorsXO}.\n */\npublic class ValidationErrorXO {\n    /**\n     * Denotes that validation does not applies to a specific value.\n     */\n    public static final String GENERIC = \"*\";\n\n    /**\n     * Identifies the value value that is failing validation. A value of \"*\" denotes that validation\n     * does not applies to a specific value.\n     * <p>\n     * E.g. \"name\".\n     */\n    @JsonProperty\n    private String id;\n\n    /**\n     * Description of failing validation.\n     * <p>\n     * E.g. \"Name cannot be null\".\n     */\n    @JsonProperty\n    private String message;\n\n    public ValidationErrorXO() {\n        this.id = GENERIC;\n    }\n\n    /**\n     * Creates a validation error that does not applies to a specific value.\n     *\n     * @param message validation description\n     */\n    public ValidationErrorXO(String message) {\n        this(GENERIC, message);\n    }\n\n    /**\n     * Creates a validation error for a specific value.\n     *\n     * @param id      identifier of value failing validation.\n     * @param message validation description\n     */\n    public ValidationErrorXO(String id, final String message) {\n        this.id = id == null ? GENERIC : id;\n        this.message = message;\n    }\n\n    /**\n     * @return identifier of value failing validation (never null).  A value of \"*\" denotes that validation does\n     * not applies to a specific value.\n     */\n    public String getId() {\n        return id;\n    }\n\n    /**\n     * @param id of value failing validation\n     */\n    public void setId(String id) {\n        this.id = id == null ? GENERIC : id;\n    }\n\n    /**\n     * @param id of value failing validation\n     * @return itself, for fluent api usage\n     */\n    public ValidationErrorXO withId(String id) {\n        setId(id);\n        return this;\n    }\n\n    /**\n     * @return validation description\n     */\n    public String getMessage() {\n        return message;\n    }\n\n    /**\n     * @param message validation description\n     */\n    public void setMessage(String message) {\n        this.message = message;\n    }\n\n    /**\n     * @param message validation description\n     * @return itself, for fluent api usage\n     */\n    public ValidationErrorXO withMessage(String message) {\n        this.message = message;\n        return this;\n    }\n\n    @Override\n    public String toString() {\n        return getClass().getSimpleName() + \"{\" +\n               \"id='\" + id + '\\'' +\n               \", message='\" + message + '\\'' +\n               '}';\n    }\n}\n"
  },
  {
    "path": "server/sdk/src/main/java/com/walmartlabs/concord/server/sdk/validation/ValidationErrorsException.java",
    "content": "package com.walmartlabs.concord.server.sdk.validation;\n\n/*\n * Copyright (c) 2007-2014 Sonatype, Inc. All rights reserved.\n *\n * This program is licensed to you under the Apache License Version 2.0,\n * and you may not use this file except in compliance with the Apache License Version 2.0.\n * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the Apache License Version 2.0 is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.\n *\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2024 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport static java.util.Objects.requireNonNull;\n\n/**\n * Based on the original {@link org.sonatype.siesta.server.validation.ValidationErrorsException}.\n */\npublic class ValidationErrorsException extends RuntimeException {\n    private final List<ValidationErrorXO> errors = new ArrayList<ValidationErrorXO>();\n\n    public ValidationErrorsException() {\n        super();\n    }\n\n    public ValidationErrorsException(final String message) {\n        errors.add(new ValidationErrorXO(message));\n    }\n\n    public ValidationErrorsException(final String id, final String message) {\n        errors.add(new ValidationErrorXO(id, message));\n    }\n\n    public ValidationErrorsException withError(final String message) {\n        errors.add(new ValidationErrorXO(message));\n        return this;\n    }\n\n    public ValidationErrorsException withError(final String id, final String message) {\n        errors.add(new ValidationErrorXO(id, message));\n        return this;\n    }\n\n    public ValidationErrorsException withErrors(final ValidationErrorXO... validationErrors) {\n        errors.addAll(Arrays.asList(requireNonNull(validationErrors)));\n        return this;\n    }\n\n    public ValidationErrorsException withErrors(final List<ValidationErrorXO> validationErrors) {\n        errors.addAll(requireNonNull(validationErrors));\n        return this;\n    }\n\n    public List<ValidationErrorXO> getValidationErrors() {\n        return errors;\n    }\n\n    public boolean hasValidationErrors() {\n        return !errors.isEmpty();\n    }\n\n    @Override\n    public String getMessage() {\n        StringBuilder sb = new StringBuilder();\n        for (ValidationErrorXO error : errors) {\n            if (!sb.isEmpty()) {\n                sb.append(\", \");\n            }\n            sb.append(error.getMessage());\n        }\n        return sb.isEmpty() ? \"(No validation errors)\" : sb.toString();\n    }\n}\n"
  },
  {
    "path": "server/sdk/src/test/java/com/walmartlabs/concord/server/sdk/ProcessKeyTest.java",
    "content": "package com.walmartlabs.concord.server.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * \n *      http://www.apache.org/licenses/LICENSE-2.0\n * \n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport org.junit.jupiter.api.Test;\n\nimport java.time.OffsetDateTime;\nimport java.time.temporal.ChronoField;\nimport java.util.UUID;\n\nimport static org.junit.jupiter.api.Assertions.fail;\n\npublic class ProcessKeyTest {\n\n    @Test\n    public void testNanos() {\n        try {\n            new ProcessKey(ProcessKey.from(UUID.randomUUID()), OffsetDateTime.now()\n                    .with(ChronoField.NANO_OF_SECOND, 1));\n            fail(\"must fail\");\n        } catch (IllegalArgumentException e) {\n        }\n\n        try {\n            new ProcessKey(ProcessKey.from(UUID.randomUUID()), OffsetDateTime.now()\n                    .with(ChronoField.NANO_OF_SECOND, 499));\n            fail(\"must fail\");\n        } catch (IllegalArgumentException e) {\n        }\n\n        try {\n            new ProcessKey(ProcessKey.from(UUID.randomUUID()), OffsetDateTime.now()\n                    .with(ChronoField.NANO_OF_SECOND, 999));\n            fail(\"must fail\");\n        } catch (IllegalArgumentException e) {\n        }\n\n        new ProcessKey(ProcessKey.from(UUID.randomUUID()), OffsetDateTime.now()\n                .with(ChronoField.NANO_OF_SECOND, 0));\n    }\n}\n"
  },
  {
    "path": "server/sdk/src/test/java/com/walmartlabs/concord/server/sdk/SerializationTest.java",
    "content": "package com.walmartlabs.concord.server.sdk;\n\n/*-\n * *****\n * Concord\n * -----\n * Copyright (C) 2017 - 2023 Walmart Inc.\n * -----\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n * =====\n */\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;\nimport com.walmartlabs.concord.server.sdk.audit.AuditEvent;\nimport com.walmartlabs.concord.server.sdk.events.ProcessEvent;\nimport com.walmartlabs.concord.server.sdk.log.ProcessLogEntry;\nimport org.junit.jupiter.api.Test;\n\nimport java.time.OffsetDateTime;\nimport java.util.Collections;\nimport java.util.UUID;\n\nimport static org.junit.jupiter.api.Assertions.assertArrayEquals;\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\npublic class SerializationTest {\n\n    @Test\n    public void testProcessEvent() throws Exception {\n        ObjectMapper om = new ObjectMapper();\n        om.registerModule(new JavaTimeModule());\n\n        ProcessEvent ev = ProcessEvent.builder()\n                .eventSeq(123)\n                .processKey(ProcessKey.random())\n                .eventType(\"TEST\")\n                .eventDate(OffsetDateTime.now())\n                .data(Collections.singletonMap(\"x\", \"abc\"))\n                .build();\n\n        String s = om.writeValueAsString(ev);\n        ev = om.readValue(s, ProcessEvent.class);\n\n        assertEquals(\"TEST\", ev.eventType());\n    }\n\n    @Test\n    public void testProcessLog() throws Exception {\n        ObjectMapper om = new ObjectMapper();\n        om.registerModule(new JavaTimeModule());\n\n        ProcessLogEntry le = ProcessLogEntry.builder()\n                .processKey(ProcessKey.random())\n                .msg(\"hello\".getBytes())\n                .range(Range.builder()\n                        .lowerMode(Range.Mode.INCLUSIVE).lower(0)\n                        .upperMode(Range.Mode.EXCLUSIVE).upper(5)\n                        .build())\n                .build();\n\n        String s = om.writeValueAsString(le);\n        le = om.readValue(s, ProcessLogEntry.class);\n\n        assertArrayEquals(\"hello\".getBytes(), le.msg());\n    }\n\n    @Test\n    public void testAuditEvent() throws Exception {\n        ObjectMapper om = new ObjectMapper();\n        om.registerModule(new JavaTimeModule());\n\n        AuditEvent ae = AuditEvent.builder()\n                .entrySeq(0)\n                .entryDate(OffsetDateTime.now())\n                .userId(UUID.randomUUID())\n                .object(\"TEST\")\n                .action(\"DOING A TEST\")\n                .details(Collections.singletonMap(\"x\", \"abc\"))\n                .build();\n\n        String s = om.writeValueAsString(ae);\n        ae = om.readValue(s, AuditEvent.class);\n\n        assertEquals(\"DOING A TEST\", ae.action());\n    }\n}\n"
  },
  {
    "path": "targetplatform/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>com.walmartlabs.concord</groupId>\n    <artifactId>concord-targetplatform</artifactId>\n    <version>2.40.1-SNAPSHOT</version>\n\n    <packaging>pom</packaging>\n\n    <name>${project.groupId}:${project.artifactId}</name>\n    <description>Concord: Orchestrate More. Live Better.</description>\n    <url>https://concord.walmartlabs.com/</url>\n\n    <licenses>\n        <license>\n            <name>Apache License, Version 2.0</name>\n            <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>\n            <distribution>repo</distribution>\n        </license>\n    </licenses>\n\n    <developers>\n        <developer>\n            <id>team</id>\n            <name>Concord Development Team</name>\n            <roles>\n                <role>Project Lead</role>\n            </roles>\n            <timezone>-5</timezone>\n        </developer>\n    </developers>\n\n    <properties>\n        <scm.connection>scm:git:https://github.com/walmartlabs/concord.git</scm.connection>\n\n        <activation.api.version>1.2.2</activation.api.version>\n        <aopalliance.version>1.0</aopalliance.version>\n        <aws.version>1.11.475</aws.version>\n        <bouncycastle.version>1.84</bouncycastle.version>\n        <bpm.version>1.0.3</bpm.version>\n        <bytebuddy.version>1.14.9</bytebuddy.version>\n        <commons.beanutils.version>1.11.0</commons.beanutils.version>\n        <commons.codec.version>1.19.0</commons.codec.version>\n        <commons.collections.version>3.2.2</commons.collections.version>\n        <commons.compress.version>1.28.0</commons.compress.version>\n        <commons.email.version>1.5</commons.email.version>\n        <commons.io.version>2.20.0</commons.io.version>\n        <commons.lang.version>3.18.0</commons.lang.version>\n        <commons.text.version>1.12.0</commons.text.version>\n        <commons.validator.version>1.10.0</commons.validator.version>\n        <config.version>1.4.2</config.version>\n        <cronutils.version>9.2.0</cronutils.version>\n        <errorproneannotations.version>2.28.0</errorproneannotations.version>\n        <glassfish.el.version>3.0.1-b12</glassfish.el.version>\n        <graalvm.version>22.0.0.2</graalvm.version>\n        <greenmail.version>1.6.5</greenmail.version>\n        <groovy.version>5.0.4</groovy.version>\n        <guava.version>33.3.1-jre</guava.version>\n        <guice.version>5.1.0</guice.version>\n        <hibernate.validator.version>6.2.5.Final</hibernate.validator.version>\n        <hikaricp.version>6.0.0</hikaricp.version>\n        <httpclient.version>4.5.13</httpclient.version>\n        <httpcore.version>4.4.13</httpcore.version>\n        <httpmime.version>4.5.11</httpmime.version>\n        <immutables.version>2.9.3</immutables.version>\n        <jackson.annotations.version>2.21</jackson.annotations.version>\n        <jackson.databind.version>2.21.1</jackson.databind.version>\n        <jackson.jsonschema.version>1.0.39</jackson.jsonschema.version>\n        <jackson.version>2.21.1</jackson.version>\n        <jansi.version>2.4.2</jansi.version>\n        <javax.activation.version>1.2.0</javax.activation.version>\n        <javax.annotations.api.version>1.3.2</javax.annotations.api.version>\n        <javax.inject.version>1</javax.inject.version>\n        <javax.mail.version>1.6.2</javax.mail.version>\n        <javax.validation.version>2.0.1.Final</javax.validation.version>\n        <javax.ws.rs.version>2.0.1.Final</javax.ws.rs.version>\n        <javers.version>6.6.2</javers.version>\n        <jboss.logging>3.4.2.Final</jboss.logging>\n        <jcl-over-slf4j.version>1.7.36</jcl-over-slf4j.version>\n        <jetbrain.annotations.version>23.0.0</jetbrain.annotations.version>\n        <jetty.version>12.0.33</jetty.version>\n        <jgit.version>6.10.1.202505221210-r</jgit.version>\n        <jna.version>5.18.1</jna.version>\n        <jooq.version>3.14.0</jooq.version>\n        <jsch.version>2.27.3</jsch.version>\n        <json.schema.validator.version>1.5.4</json.schema.validator.version>\n        <json.smart.version>2.5.2</json.smart.version>\n        <jsr305.version>3.0.2</jsr305.version>\n        <junit.version>5.9.1</junit.version>\n        <kafka.client.version>3.9.2</kafka.client.version>\n        <kerby.version>2.0.1</kerby.version>\n        <kubernetes.client.version>7.0.1</kubernetes.client.version>\n        <launcher.version>0.128</launcher.version>\n        <liquibase.version>4.29.2</liquibase.version>\n        <logback.version>1.5.25</logback.version>\n        <maven.annotation.version>3.5</maven.annotation.version>\n        <maven.api.version>3.0.5</maven.api.version>\n        <maven.artifact.version>3.8.4</maven.artifact.version>\n        <maven.prj.version>2.2.1</maven.prj.version>\n        <maven.resolver.provider.version>3.8.4</maven.resolver.provider.version>\n        <maven.model.builder.version>3.8.4</maven.model.builder.version>\n        <maven.resolver.version>1.9.20</maven.resolver.version>\n        <maven.resolver.version.concord>0.0.4</maven.resolver.version.concord>\n        <metrics.version>4.2.25</metrics.version>\n        <mockito.version>4.9.0</mockito.version>\n        <mustache.version>0.9.10</mustache.version>\n        <nimbus.jwt.version>10.5</nimbus.jwt.version>\n        <parc.version>0.1.2</parc.version>\n        <picocli.version>4.7.0</picocli.version>\n        <postgres.version>42.7.3</postgres.version>\n        <prometheus.simpleclient.version>0.16.0</prometheus.simpleclient.version>\n        <reactive.streams.version>1.0.3</reactive.streams.version>\n        <reflections.version>0.9.11</reflections.version>\n        <resteasy.version>4.7.9.Final</resteasy.version>\n        <selenium.version>4.27.0</selenium.version>\n        <servlet.version>4.0.1</servlet.version>\n        <shiro.version>2.1.0</shiro.version>\n        <siesta.version>2.3.2</siesta.version>\n        <sisu.version>0.9.0.M2</sisu.version>\n        <slf4j.version>2.0.17</slf4j.version>\n        <snakeyaml.version>2.5</snakeyaml.version>\n        <sshd.version>2.15.0</sshd.version>\n        <sysoutslf4j.version>1.0.2</sysoutslf4j.version>\n        <testcontainers.concord.version>2.0.4</testcontainers.concord.version>\n        <testcontainers.version>2.0.3</testcontainers.version>\n        <threetenbp.version>1.3.5</threetenbp.version>\n        <uuid.generator.version>5.1.0</uuid.generator.version>\n        <wiremock.version>3.10.0</wiremock.version>\n    </properties>\n\n    <dependencyManagement>\n        <dependencies>\n            <!-- Concord -->\n            <dependency>\n                <groupId>com.walmartlabs.concord</groupId>\n                <artifactId>concord-client2</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord</groupId>\n                <artifactId>concord-config</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord</groupId>\n                <artifactId>concord-common</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord</groupId>\n                <artifactId>concord-github-app-installation</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord</groupId>\n                <artifactId>concord-server</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord</groupId>\n                <artifactId>concord-agent</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord</groupId>\n                <artifactId>concord-imports</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord</groupId>\n                <artifactId>concord-forms</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.runtime</groupId>\n                <artifactId>concord-runtime-model</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.runtime.v1</groupId>\n                <artifactId>concord-runtime-model-v1</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.runtime</groupId>\n                <artifactId>concord-runtime-loader</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord</groupId>\n                <artifactId>concord-sdk</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.server</groupId>\n                <artifactId>concord-queue-client</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord</groupId>\n                <artifactId>concord-dependency-manager</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord</groupId>\n                <artifactId>concord-repository</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.runtime.v1</groupId>\n                <artifactId>concord-runtime-impl-v1</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.runtime</groupId>\n                <artifactId>concord-runtime-common</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n                <artifactId>concord-runtime-model-v2</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n                <artifactId>concord-runtime-vm-v2</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n                <artifactId>concord-runtime-sdk-v2</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n                <artifactId>concord-runner-v2</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.runtime.v2</groupId>\n                <artifactId>concord-runner-v2-test</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.it</groupId>\n                <artifactId>concord-common-it</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord</groupId>\n                <artifactId>concord-policy-engine</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.server</groupId>\n                <artifactId>concord-server-sdk</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.server</groupId>\n                <artifactId>liquibase-ext</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.server</groupId>\n                <artifactId>concord-server-db</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.server</groupId>\n                <artifactId>concord-server</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n                <artifactId>ansible-tasks</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n                <artifactId>log-tasks</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n                <artifactId>locale-tasks</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n                <artifactId>kv-tasks</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n                <artifactId>misc-tasks</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n                <artifactId>resource-tasks</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n                <artifactId>sleep-tasks</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n                <artifactId>variables-tasks</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n                <artifactId>crypto-tasks</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n                <artifactId>docker-tasks</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n                <artifactId>throw-tasks</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n                <artifactId>lock-tasks</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.plugins.basic</groupId>\n                <artifactId>ansible-template</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.server</groupId>\n                <artifactId>concord-server-impl</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.server.plugins.ansible</groupId>\n                <artifactId>concord-ansible-plugin</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.server.plugins</groupId>\n                <artifactId>concord-oneops-plugin</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord</groupId>\n                <artifactId>concord-console2</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.server.plugins</groupId>\n                <artifactId>webapp</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <!-- deprecated -->\n            <dependency>\n                <groupId>com.walmartlabs.concord.server.plugins.ansible</groupId>\n                <artifactId>concord-ansible-plugin-client</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.server.plugins.ansible</groupId>\n                <artifactId>concord-ansible-plugin-client2</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.server.plugins.ansible</groupId>\n                <artifactId>concord-ansible-plugin-db</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.server.plugins</groupId>\n                <artifactId>kafka-event-sink</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.server.plugins.noderoster</groupId>\n                <artifactId>concord-noderoster-plugin</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.server.plugins.noderoster</groupId>\n                <artifactId>concord-noderoster-plugin-db</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.server.plugins.noderoster</groupId>\n                <artifactId>concord-noderoster-plugin-client</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.server.plugins.noderoster</groupId>\n                <artifactId>concord-noderoster-plugin-client2</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.server.plugins</groupId>\n                <artifactId>pfed-sso</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.walmartlabs.concord.server.plugins</groupId>\n                <artifactId>oidc</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.jetty</groupId>\n                <artifactId>jetty-rewrite</artifactId>\n                <version>${jetty.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.maven</groupId>\n                <artifactId>maven-artifact</artifactId>\n                <version>${maven.artifact.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.maven.resolver</groupId>\n                <artifactId>maven-resolver-api</artifactId>\n                <version>${maven.resolver.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.maven.resolver</groupId>\n                <artifactId>maven-resolver-spi</artifactId>\n                <version>${maven.resolver.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.maven.resolver</groupId>\n                <artifactId>maven-resolver-util</artifactId>\n                <version>${maven.resolver.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.maven.resolver</groupId>\n                <artifactId>maven-resolver-impl</artifactId>\n                <version>${maven.resolver.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.maven.resolver</groupId>\n                <artifactId>maven-resolver-connector-basic</artifactId>\n                <version>${maven.resolver.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.maven.resolver</groupId>\n                <artifactId>maven-resolver-transport-file</artifactId>\n                <version>${maven.resolver.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>ca.ibodrov.concord.maven</groupId>\n                <artifactId>concord-maven-resolver-transport-http</artifactId>\n                <version>${maven.resolver.version.concord}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.maven</groupId>\n                <artifactId>maven-resolver-provider</artifactId>\n                <version>${maven.resolver.provider.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.maven</groupId>\n                <artifactId>maven-model-builder</artifactId>\n                <version>${maven.model.builder.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.slf4j</groupId>\n                <artifactId>jul-to-slf4j</artifactId>\n                <version>${slf4j.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>io.dropwizard.metrics</groupId>\n                <artifactId>metrics-jetty12</artifactId>\n                <version>${metrics.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>io.prometheus</groupId>\n                <artifactId>simpleclient</artifactId>\n                <version>${prometheus.simpleclient.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>io.prometheus</groupId>\n                <artifactId>simpleclient_dropwizard</artifactId>\n                <version>${prometheus.simpleclient.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>io.prometheus</groupId>\n                <artifactId>simpleclient_servlet</artifactId>\n                <version>${prometheus.simpleclient.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>io.prometheus</groupId>\n                <artifactId>simpleclient_hotspot</artifactId>\n                <version>${prometheus.simpleclient.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>ch.qos.logback</groupId>\n                <artifactId>logback-classic</artifactId>\n                <version>${logback.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>ch.qos.logback</groupId>\n                <artifactId>logback-core</artifactId>\n                <version>${logback.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.fasterxml.jackson.core</groupId>\n                <artifactId>jackson-annotations</artifactId>\n                <version>${jackson.annotations.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.fasterxml.jackson.core</groupId>\n                <artifactId>jackson-core</artifactId>\n                <version>${jackson.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.fasterxml.jackson.core</groupId>\n                <artifactId>jackson-databind</artifactId>\n                <version>${jackson.databind.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.fasterxml.jackson.dataformat</groupId>\n                <artifactId>jackson-dataformat-yaml</artifactId>\n                <version>${jackson.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.fasterxml.jackson.jaxrs</groupId>\n                <artifactId>jackson-jaxrs-json-provider</artifactId>\n                <version>${jackson.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.fasterxml.jackson.jaxrs</groupId>\n                <artifactId>jackson-jaxrs-base</artifactId>\n                <version>${jackson.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.fasterxml.jackson.module</groupId>\n                <artifactId>jackson-module-jaxb-annotations</artifactId>\n                <version>${jackson.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.fasterxml.uuid</groupId>\n                <artifactId>java-uuid-generator</artifactId>\n                <version>${uuid.generator.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.wiremock</groupId>\n                <artifactId>wiremock-jetty12</artifactId>\n                <version>${wiremock.version}</version>\n                <exclusions>\n                    <exclusion>\n                        <groupId>commons-codec</groupId>\n                        <artifactId>commons-codec</artifactId>\n                    </exclusion>\n                    <exclusion>\n                        <groupId>org.ow2.asm</groupId>\n                        <artifactId>asm</artifactId>\n                    </exclusion>\n                    <exclusion>\n                        <groupId>org.eclipse.jetty</groupId>\n                        <artifactId>jetty-webapp</artifactId>\n                    </exclusion>\n                    <exclusion>\n                        <groupId>org.eclipse.jetty</groupId>\n                        <artifactId>jetty-alpn-client</artifactId>\n                    </exclusion>\n                    <exclusion>\n                        <groupId>org.slf4j</groupId>\n                        <artifactId>slf4j-api</artifactId>\n                    </exclusion>\n                </exclusions>\n            </dependency>\n            <dependency>\n                <groupId>com.google.guava</groupId>\n                <artifactId>guava</artifactId>\n                <version>${guava.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.google.inject</groupId>\n                <artifactId>guice</artifactId>\n                <version>${guice.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.google.inject.extensions</groupId>\n                <artifactId>guice-servlet</artifactId>\n                <version>${guice.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.graalvm.js</groupId>\n                <artifactId>js</artifactId>\n                <version>${graalvm.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.graalvm.js</groupId>\n                <artifactId>js-scriptengine</artifactId>\n                <version>${graalvm.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.graalvm.sdk</groupId>\n                <artifactId>graal-sdk</artifactId>\n                <version>${graalvm.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.github.mwiede</groupId>\n                <artifactId>jsch</artifactId>\n                <version>${jsch.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>io.dropwizard.metrics</groupId>\n                <artifactId>metrics-core</artifactId>\n                <version>${metrics.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>io.dropwizard.metrics</groupId>\n                <artifactId>metrics-graphite</artifactId>\n                <version>${metrics.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>io.dropwizard.metrics</groupId>\n                <artifactId>metrics-jvm</artifactId>\n                <version>${metrics.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>io.takari.bpm</groupId>\n                <artifactId>activiti-compat</artifactId>\n                <version>${bpm.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>io.takari.bpm</groupId>\n                <artifactId>bpm-engine-api</artifactId>\n                <version>${bpm.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>io.takari.bpm</groupId>\n                <artifactId>bpm-engine-impl</artifactId>\n                <version>${bpm.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>io.takari.bpm</groupId>\n                <artifactId>testkit</artifactId>\n                <version>${bpm.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>javax.inject</groupId>\n                <artifactId>javax.inject</artifactId>\n                <version>${javax.inject.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>javax.servlet</groupId>\n                <artifactId>javax.servlet-api</artifactId>\n                <version>${servlet.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>javax.validation</groupId>\n                <artifactId>validation-api</artifactId>\n                <version>${javax.validation.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.hibernate.validator</groupId>\n                <artifactId>hibernate-validator</artifactId>\n                <version>${hibernate.validator.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.junit.jupiter</groupId>\n                <artifactId>junit-jupiter-api</artifactId>\n                <version>${junit.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.junit.jupiter</groupId>\n                <artifactId>junit-jupiter-params</artifactId>\n                <version>${junit.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.junit.jupiter</groupId>\n                <artifactId>junit-jupiter-engine</artifactId>\n                <version>${junit.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>commons-beanutils</groupId>\n                <artifactId>commons-beanutils</artifactId>\n                <version>${commons.beanutils.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.commons</groupId>\n                <artifactId>commons-email</artifactId>\n                <version>${commons.email.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>commons-validator</groupId>\n                <artifactId>commons-validator</artifactId>\n                <version>${commons.validator.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.commons</groupId>\n                <artifactId>commons-text</artifactId>\n                <version>${commons.text.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.httpcomponents</groupId>\n                <artifactId>httpclient</artifactId>\n                <version>${httpclient.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.httpcomponents</groupId>\n                <artifactId>httpmime</artifactId>\n                <version>${httpmime.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.shiro</groupId>\n                <artifactId>shiro-core</artifactId>\n                <version>${shiro.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.shiro</groupId>\n                <artifactId>shiro-guice</artifactId>\n                <version>${shiro.version}</version>\n                <exclusions>\n                    <exclusion>\n                        <groupId>com.google.inject</groupId>\n                        <artifactId>guice</artifactId>\n                    </exclusion>\n                    <exclusion>\n                        <groupId>com.google.inject.extensions</groupId>\n                        <artifactId>guice-multibindings</artifactId>\n                    </exclusion>\n                    <exclusion>\n                        <groupId>com.google.inject.extensions</groupId>\n                        <artifactId>guice-assistedinject</artifactId>\n                    </exclusion>\n                </exclusions>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.shiro</groupId>\n                <artifactId>shiro-web</artifactId>\n                <version>${shiro.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.sshd</groupId>\n                <artifactId>sshd-core</artifactId>\n                <version>${sshd.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.sshd</groupId>\n                <artifactId>sshd-git</artifactId>\n                <version>${sshd.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.sshd</groupId>\n                <artifactId>sshd-common</artifactId>\n                <version>${sshd.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.groovy</groupId>\n                <artifactId>groovy-all</artifactId>\n                <version>${groovy.version}</version>\n                <type>pom</type>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.groovy</groupId>\n                <artifactId>groovy</artifactId>\n                <version>${groovy.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.jetty</groupId>\n                <artifactId>jetty-server</artifactId>\n                <version>${jetty.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.jetty.ee8</groupId>\n                <artifactId>jetty-ee8-servlet</artifactId>\n                <version>${jetty.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.jetty.ee8</groupId>\n                <artifactId>jetty-ee8-servlets</artifactId>\n                <version>${jetty.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.jetty.ee10</groupId>\n                <artifactId>jetty-ee10-servlet</artifactId>\n                <version>${jetty.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.jetty</groupId>\n                <artifactId>jetty-util</artifactId>\n                <version>${jetty.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.jetty</groupId>\n                <artifactId>jetty-client</artifactId>\n                <version>${jetty.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.jetty.ee8</groupId>\n                <artifactId>jetty-ee8-webapp</artifactId>\n                <version>${jetty.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.jetty.ee10</groupId>\n                <artifactId>jetty-ee10-webapp</artifactId>\n                <version>${jetty.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.jetty</groupId>\n                <artifactId>jetty-jmx</artifactId>\n                <version>${jetty.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.jetty</groupId>\n                <artifactId>jetty-deploy</artifactId>\n                <version>${jetty.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.jetty</groupId>\n                <artifactId>jetty-security</artifactId>\n                <version>${jetty.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.jetty</groupId>\n                <artifactId>jetty-http</artifactId>\n                <version>${jetty.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.jetty.websocket</groupId>\n                <artifactId>jetty-websocket-jetty-api</artifactId>\n                <version>${jetty.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.jetty.ee8.websocket</groupId>\n                <artifactId>jetty-ee8-websocket-javax-server</artifactId>\n                <version>${jetty.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.jetty.ee8.websocket</groupId>\n                <artifactId>jetty-ee8-websocket-jetty-server</artifactId>\n                <version>${jetty.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.jetty.websocket</groupId>\n                <artifactId>jetty-websocket-jetty-common</artifactId>\n                <version>${jetty.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.jetty.websocket</groupId>\n                <artifactId>jetty-websocket-jetty-server</artifactId>\n                <version>${jetty.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.jetty.websocket</groupId>\n                <artifactId>jetty-websocket-jetty-client</artifactId>\n                <version>${jetty.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.jetty.ee8.websocket</groupId>\n                <artifactId>jetty-ee8-websocket-jetty-client</artifactId>\n                <version>${jetty.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.jgit</groupId>\n                <artifactId>org.eclipse.jgit</artifactId>\n                <version>${jgit.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.eclipse.sisu</groupId>\n                <artifactId>org.eclipse.sisu.inject</artifactId>\n                <version>${sisu.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.jboss.resteasy</groupId>\n                <artifactId>resteasy-jackson2-provider</artifactId>\n                <version>${resteasy.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.jboss.resteasy</groupId>\n                <artifactId>resteasy-core</artifactId>\n                <version>${resteasy.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.jboss.resteasy</groupId>\n                <artifactId>resteasy-multipart-provider</artifactId>\n                <version>${resteasy.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.jboss.resteasy</groupId>\n                <artifactId>resteasy-jaxb-provider</artifactId>\n                <version>${resteasy.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.jboss.spec.javax.ws.rs</groupId>\n                <artifactId>jboss-jaxrs-api_2.1_spec</artifactId>\n                <version>${javax.ws.rs.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.jooq</groupId>\n                <artifactId>jooq</artifactId>\n                <version>${jooq.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.liquibase</groupId>\n                <artifactId>liquibase-core</artifactId>\n                <version>${liquibase.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.mockito</groupId>\n                <artifactId>mockito-core</artifactId>\n                <version>${mockito.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.mockito</groupId>\n                <artifactId>mockito-junit-jupiter</artifactId>\n                <version>${mockito.version}</version>\n                <scope>test</scope>\n            </dependency>\n            <dependency>\n                <groupId>org.slf4j</groupId>\n                <artifactId>slf4j-api</artifactId>\n                <version>${slf4j.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.postgresql</groupId>\n                <artifactId>postgresql</artifactId>\n                <version>${postgres.version}</version>\n                <exclusions>\n                    <exclusion>\n                        <groupId>org.checkerframework</groupId>\n                        <artifactId>checker-qual</artifactId>\n                    </exclusion>\n                </exclusions>\n            </dependency>\n            <dependency>\n                <groupId>com.icegreen</groupId>\n                <artifactId>greenmail</artifactId>\n                <version>${greenmail.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.icegreen</groupId>\n                <artifactId>greenmail-junit5</artifactId>\n                <version>${greenmail.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.sun.mail</groupId>\n                <artifactId>javax.mail</artifactId>\n                <version>${javax.mail.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>io.takari</groupId>\n                <artifactId>parc</artifactId>\n                <version>${parc.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.commons</groupId>\n                <artifactId>commons-lang3</artifactId>\n                <version>${commons.lang.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.github.spullara.mustache.java</groupId>\n                <artifactId>compiler</artifactId>\n                <version>${mustache.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.zaxxer</groupId>\n                <artifactId>HikariCP</artifactId>\n                <version>${hikaricp.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.httpcomponents</groupId>\n                <artifactId>httpcore</artifactId>\n                <version>${httpcore.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>aopalliance</groupId>\n                <artifactId>aopalliance</artifactId>\n                <version>${aopalliance.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.glassfish</groupId>\n                <artifactId>javax.el</artifactId>\n                <version>${glassfish.el.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.commons</groupId>\n                <artifactId>commons-compress</artifactId>\n                <version>${commons.compress.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>commons-io</groupId>\n                <artifactId>commons-io</artifactId>\n                <version>${commons.io.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.cronutils</groupId>\n                <artifactId>cron-utils</artifactId>\n                <version>${cronutils.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.typesafe</groupId>\n                <artifactId>config</artifactId>\n                <version>${config.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.javers</groupId>\n                <artifactId>javers-core</artifactId>\n                <version>${javers.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>javax.xml.bind</groupId>\n                <artifactId>jaxb-api</artifactId>\n                <version>2.3.1</version>\n            </dependency>\n            <dependency>\n                <groupId>com.sun.xml.bind</groupId>\n                <artifactId>jaxb-impl</artifactId>\n                <version>2.3.2</version>\n            </dependency>\n            <dependency>\n                <groupId>org.immutables</groupId>\n                <artifactId>value</artifactId>\n                <version>${immutables.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.immutables</groupId>\n                <artifactId>builder</artifactId>\n                <version>${immutables.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.immutables</groupId>\n                <artifactId>serial</artifactId>\n                <version>${immutables.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.fasterxml.jackson.datatype</groupId>\n                <artifactId>jackson-datatype-guava</artifactId>\n                <version>${jackson.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.fasterxml.jackson.datatype</groupId>\n                <artifactId>jackson-datatype-jdk8</artifactId>\n                <version>${jackson.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.fasterxml.jackson.datatype</groupId>\n                <artifactId>jackson-datatype-jsr310</artifactId>\n                <version>${jackson.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.google.code.findbugs</groupId>\n                <artifactId>jsr305</artifactId>\n                <version>${jsr305.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.google.errorprone</groupId>\n                <artifactId>error_prone_annotations</artifactId>\n                <version>${errorproneannotations.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.nimbusds</groupId>\n                <artifactId>nimbus-jose-jwt</artifactId>\n                <version>${nimbus.jwt.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>net.minidev</groupId>\n                <artifactId>json-smart</artifactId>\n                <version>${json.smart.version}</version>\n                <exclusions>\n                    <exclusion>\n                        <groupId>org.ow2.asm</groupId>\n                        <artifactId>asm</artifactId>\n                    </exclusion>\n                </exclusions>\n            </dependency>\n            <dependency>\n                <groupId>org.seleniumhq.selenium</groupId>\n                <artifactId>selenium-api</artifactId>\n                <version>${selenium.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.seleniumhq.selenium</groupId>\n                <artifactId>selenium-java</artifactId>\n                <version>${selenium.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.seleniumhq.selenium</groupId>\n                <artifactId>selenium-remote-driver</artifactId>\n                <version>${selenium.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.seleniumhq.selenium</groupId>\n                <artifactId>selenium-chrome-driver</artifactId>\n                <version>${selenium.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.seleniumhq.selenium</groupId>\n                <artifactId>selenium-chromium-driver</artifactId>\n                <version>${selenium.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.seleniumhq.selenium</groupId>\n                <artifactId>selenium-support</artifactId>\n                <version>${selenium.version}</version>\n            </dependency>\n            <!-- k8s stuff -->\n            <dependency>\n                <groupId>io.fabric8</groupId>\n                <artifactId>kubernetes-client</artifactId>\n                <version>${kubernetes.client.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>io.fabric8</groupId>\n                <artifactId>kubernetes-model-core</artifactId>\n                <version>${kubernetes.client.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>io.fabric8</groupId>\n                <artifactId>kubernetes-model-apiextensions</artifactId>\n                <version>${kubernetes.client.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>io.fabric8</groupId>\n                <artifactId>kubernetes-model-common</artifactId>\n                <version>${kubernetes.client.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.bouncycastle</groupId>\n                <artifactId>bcprov-jdk18on</artifactId>\n                <version>${bouncycastle.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.bouncycastle</groupId>\n                <artifactId>bcpkix-jdk18on</artifactId>\n                <version>${bouncycastle.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.bouncycastle</groupId>\n                <artifactId>bcutil-jdk18on</artifactId>\n                <version>${bouncycastle.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>info.picocli</groupId>\n                <artifactId>picocli</artifactId>\n                <version>${picocli.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.kafka</groupId>\n                <artifactId>kafka-clients</artifactId>\n                <version>${kafka.client.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>ca.ibodrov.concord</groupId>\n                <artifactId>testcontainers-concord-core</artifactId>\n                <version>${testcontainers.concord.version}</version>\n                <exclusions>\n                    <exclusion>\n                        <groupId>com.google.code.gson</groupId>\n                        <artifactId>gson</artifactId>\n                    </exclusion>\n                </exclusions>\n            </dependency>\n            <dependency>\n                <groupId>ca.ibodrov.concord</groupId>\n                <artifactId>testcontainers-concord-junit5</artifactId>\n                <version>${testcontainers.concord.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.testcontainers</groupId>\n                <artifactId>testcontainers</artifactId>\n                <version>${testcontainers.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.testcontainers</groupId>\n                <artifactId>testcontainers-postgresql</artifactId>\n                <version>${testcontainers.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>uk.org.lidalia</groupId>\n                <artifactId>sysout-over-slf4j</artifactId>\n                <version>${sysoutslf4j.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>javax.annotation</groupId>\n                <artifactId>javax.annotation-api</artifactId>\n                <version>${javax.annotations.api.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.kjetland</groupId>\n                <artifactId>mbknor-jackson-jsonschema_2.12</artifactId>\n                <version>${jackson.jsonschema.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.networknt</groupId>\n                <artifactId>json-schema-validator</artifactId>\n                <version>${json.schema.validator.version}</version>\n            </dependency>\n            <!-- Apache kerby stuff -->\n            <dependency>\n                <groupId>org.apache.kerby</groupId>\n                <artifactId>kerb-client-api-all</artifactId>\n                <version>${kerby.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.kerby</groupId>\n                <artifactId>kerby-util</artifactId>\n                <version>${kerby.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.kerby</groupId>\n                <artifactId>kerb-core</artifactId>\n                <version>${kerby.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.kerby</groupId>\n                <artifactId>kerb-client</artifactId>\n                <version>${kerby.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.kerby</groupId>\n                <artifactId>kerb-util</artifactId>\n                <version>${kerby.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.yaml</groupId>\n                <artifactId>snakeyaml</artifactId>\n                <version>${snakeyaml.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.sun.activation</groupId>\n                <artifactId>javax.activation</artifactId>\n                <version>${javax.activation.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.slf4j</groupId>\n                <artifactId>jcl-over-slf4j</artifactId>\n                <version>${jcl-over-slf4j.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.jetbrains</groupId>\n                <artifactId>annotations</artifactId>\n                <version>${jetbrain.annotations.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>net.java.dev.jna</groupId>\n                <artifactId>jna</artifactId>\n                <version>${jna.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.jboss.logging</groupId>\n                <artifactId>jboss-logging</artifactId>\n                <version>${jboss.logging}</version>\n            </dependency>\n            <dependency>\n                <groupId>net.bytebuddy</groupId>\n                <artifactId>byte-buddy</artifactId>\n                <version>${bytebuddy.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.reflections</groupId>\n                <artifactId>reflections</artifactId>\n                <version>${reflections.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>commons-codec</groupId>\n                <artifactId>commons-codec</artifactId>\n                <version>${commons.codec.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.reactivestreams</groupId>\n                <artifactId>reactive-streams</artifactId>\n                <version>${reactive.streams.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>jakarta.activation</groupId>\n                <artifactId>jakarta.activation-api</artifactId>\n                <version>${activation.api.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>org.fusesource.jansi</groupId>\n                <artifactId>jansi</artifactId>\n                <version>${jansi.version}</version>\n            </dependency>\n        </dependencies>\n    </dependencyManagement>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-gpg-plugin</artifactId>\n                <version>3.1.0</version>\n                <executions>\n                    <execution>\n                        <id>sign-artifacts</id>\n                        <phase>verify</phase>\n                        <goals>\n                            <goal>sign</goal>\n                        </goals>\n                    </execution>\n                </executions>\n                <configuration>\n                    <skip>true</skip>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-enforcer-plugin</artifactId>\n                <version>3.3.0</version>\n                <configuration>\n                    <skip>true</skip>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n    <scm>\n        <url>${scm.connection}</url>\n        <developerConnection>${scm.connection}</developerConnection>\n        <tag>HEAD</tag>\n    </scm>\n\n    <distributionManagement>\n        <repository>\n            <id>sonatype.releases</id>\n            <name>Sonatype Releases</name>\n            <url>https://oss.sonatype.org/service/local/staging/deploy/maven2</url>\n        </repository>\n        <snapshotRepository>\n            <id>sonatype.snapshots</id>\n            <name>Sonatype Snapshots</name>\n            <url>https://oss.sonatype.org/content/repositories/snapshots</url>\n        </snapshotRepository>\n    </distributionManagement>\n\n    <profiles>\n        <profile>\n            <id>concord-release</id>\n            <build>\n                <plugins>\n                    <plugin>\n                        <artifactId>maven-gpg-plugin</artifactId>\n                        <configuration>\n                            <skip>false</skip>\n                        </configuration>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n        <profile>\n            <id>looper</id>\n\n            <build>\n                <plugins>\n                    <plugin>\n                        <artifactId>maven-gpg-plugin</artifactId>\n                        <configuration>\n                            <skip>true</skip>\n                        </configuration>\n                    </plugin>\n                    <plugin>\n                        <artifactId>maven-release-plugin</artifactId>\n                        <configuration>\n                            <pushChanges>true</pushChanges>\n                        </configuration>\n                    </plugin>\n                </plugins>\n            </build>\n        </profile>\n        <profile>\n            <id>walmart</id>\n            <distributionManagement>\n                <repository>\n                    <id>${public-release.serverId}</id>\n                    <url>${public-release.url}</url>\n                </repository>\n\n                <snapshotRepository>\n                    <id>${public-snapshot.serverId}</id>\n                    <url>${public-snapshot.url}</url>\n                </snapshotRepository>\n\n                <site>\n                    <id>${site.id}</id>\n                    <url>${site.url}</url>\n                </site>\n            </distributionManagement>\n        </profile>\n    </profiles>\n</project>\n"
  },
  {
    "path": "vagrant/README.md",
    "content": "# Running Concord in Vagrant\n\n## Prerequisites\n\n- Linux or OSX;\n- Java 8 and Maven 3.5.0+;\n- Vagrant and Virtualbox. Optionally install `vagrant-vbguest`\n  plugin to automatically update VBox guest additions:\n  `vagrant plugin install vagrant-vbguest`\n- Docker, up and running on the host machine. Necessary\n  to build and export Concord images;\n- (optionally) locally-built Concord images:\n  `mvn clean install -Pdocker -DskipTests`\n\n## Usage\n\nStart the VM with `vagrant up`. By default it uses\n`walmartlabs/concord-*` images from Docker Hub.\nTo use another prefix run:\n```\nIMAGE_PREFIX=myimages vagrant up\n```\n\nTo use locally-built images run:\n```\nUSE_LOCAL_IMAGES=true vagrant up\n```\n\nThe API will be available on port `18001`:\n```\n$ curl http://localhost:18001/api/v1/server/ping\n{\n  \"ok\" : true\n}\n```\n\nThe Console will be available on port `18001` as well:\nhttp://localhost:18001/ The default user is `myuser` with password\n`q1`.\n\nThe examples can be started using `localhost:18001` as the API\nendpoint:\n```\n$ cd ../examples/hello_world\n$ ./run.sh localhost:18001\nUsername: myuser\nEnter host password for user 'myuser': q1\n{\n  \"instanceId\" : \"...\",\n  \"ok\" : true\n}\n```\n\nThe VM can be stopped with `vagrant halt` or suspended\nwith `vagrant suspend`.\n\n## Updating the Local Images\n\n1. build new Concord images;\n2. run `USE_LOCAL_IMAGES=true vagrant provision`.\n\n## TODO\n\n- use persistent volumes for the database.\n"
  },
  {
    "path": "vagrant/Vagrantfile",
    "content": "# -*- mode: ruby -*-\n# vi: set ft=ruby :\n\nimage_prefix = ENV['IMAGE_PREFIX'] || \"walmartlabs\"\nuse_local_images = ENV['USE_LOCAL_IMAGES'] || false\n\nVagrant.configure(\"2\") do |config|\n  config.vm.provider \"virtualbox\" do |v|\n    v.memory = 3072\n    v.cpus = 2\n  end\n\n  config.vm.box = \"centos/7\"\n\n  config.vm.network \"forwarded_port\", guest: 8001, host: 18001\n  config.vm.network \"forwarded_port\", guest: 8080, host: 18080\n\n  config.vm.provision \"shell\",\n    inline: \"yum install -y ansible\"\n\n  config.vm.provision \"ansible\" do |ansible|\n    ansible.playbook = \"playbook.yml\"\n    ansible.extra_vars = {\n        image_prefix: image_prefix,\n        use_local_images: use_local_images,\n    }\n  end\nend\n"
  },
  {
    "path": "vagrant/agent.conf",
    "content": "concord-agent {\n\n    # server connection settinss\n    server {\n        apiBaseUrl = \"http://server:8001\"\n        websocketUrl = \"ws://server:8001/websocket\"\n\n        apiKey = \"cTJxMnEycTI=\"\n    }\n}\n"
  },
  {
    "path": "vagrant/playbook.yml",
    "content": "---\n- hosts: all\n  tasks:\n  - name: Install the necessary packages\n    become: true\n    yum:\n      name:\n      - docker\n      - python-docker-py\n      state: present\n\n  - name: Start the Docker daemon\n    become: true\n    systemd:\n      name: docker\n      state: started\n      enabled: true\n\n  - name: Get local images digests\n    delegate_to: localhost\n    shell: docker image inspect --format '{{ '{{' }}.Id{{ '}}' }}' walmartlabs/concord-server walmartlabs/concord-agent\n    register: local_image_ids\n    when: use_local_images\n\n  - name: Get target images digests (if exist)\n    become: true\n    shell: docker image inspect --format '{{ '{{' }}.Id{{ '}}' }}' walmartlabs/concord-server walmartlabs/concord-agent\n    register: target_image_ids\n    no_log: true\n    ignore_errors: yes\n    when: use_local_images\n    \n  - name: Export local Docker images\n    delegate_to: localhost\n    shell: docker save -o /tmp/images.tar walmartlabs/concord-server walmartlabs/concord-agent\n    when: use_local_images and (local_image_ids.stdout != target_image_ids.stdout)\n\n  - name: Copy Docker images to the VM\n    copy:\n      src: /tmp/images.tar\n      dest: /tmp/images.tar\n    when: use_local_images and (local_image_ids.stdout != target_image_ids.stdout)\n\n  - name: Remove the local images archive\n    delegate_to: localhost\n    file:\n      path: /tmp/images.tar\n      state: absent\n    when: use_local_images and (local_image_ids.stdout != target_image_ids.stdout)\n\n  - name: Import Docker images\n    become: true\n    shell: docker load -i /tmp/images.tar\n    when: use_local_images and (local_image_ids.stdout != target_image_ids.stdout)\n\n  - name: Remove the images archive\n    file:\n      path: /tmp/images.tar\n      state: absent\n    when: use_local_images\n\n  - name: Create the conf directory\n    become: true\n    file:\n      path: /opt/concord/conf\n      state: directory\n\n  - name: Copy the configuration files\n    become: true\n    copy:\n      src: \"{{ item.src }}\"\n      dest: \"{{ item.dest }}\"\n    loop:\n    - { src: \"server.conf\", dest: \"/opt/concord/conf/server.conf\" }\n    - { src: \"agent.conf\", dest: \"/opt/concord/conf/agent.conf\" }\n\n  - name: Start the database\n    become: true\n    docker_container:\n      name: db\n      image: library/postgres:14\n      env:\n        POSTGRES_PASSWORD: \"q1\"\n      restart_policy: always\n      state: started\n      ports:\n      - 5432:5432\n\n  - name: Wait for the DB to start\n    wait_for:\n      port: 5432\n      delay: 3\n      timeout: 30\n\n  - name: Start the OpenLDAP server\n    become: true\n    docker_container:\n      name: oldap\n      image: osixia/openldap\n      restart_policy: always\n      state: started\n\n  - name: Start the Concord Server\n    become: true\n    docker_container:\n      name: server\n      image: \"{{ image_prefix }}/concord-server\"\n      restart: true\n      restart_policy: always\n      volumes:\n      - /opt/concord/conf:/opt/concord/conf:ro\n      links:\n      - db\n      - oldap\n      ports:\n      - 8001:8001\n      env:\n        DB_URL: jdbc:postgresql://db:5432/postgres\n        CONCORD_CFG_FILE: /opt/concord/conf/server.conf\n\n  - name: Start the Concord Agent\n    become: true\n    docker_container:\n      name: agent\n      image: \"{{ image_prefix }}/concord-agent\"\n      restart: true\n      restart_policy: always\n      links:\n      - server\n      volumes:\n        - /opt/concord/conf:/opt/concord/conf:ro\n      env:\n        CONCORD_CFG_FILE: /opt/concord/conf/agent.conf\n        CONCORD_DOCKER_LOCAL_MODE: \"false\"\n\n  - name: Copy the LDAP user file\n    copy:\n      src: user.ldif\n      dest: /tmp/user.ldif\n\n  - name: Create the LDAP user\n    become: true\n    shell: cat /tmp/user.ldif | docker exec -i oldap ldapadd -x -D \"cn=admin,dc=example,dc=org\" -w admin\n    ignore_errors: yes\n\n  - name: Wait for the API\n    uri:\n      url: http://127.0.0.1:8001/api/v1/server/ping\n      status_code: 200\n    register: result\n    until: result.status == 200\n    retries: 20\n    delay: 3\n"
  },
  {
    "path": "vagrant/server.conf",
    "content": "concord-server {\n    db {\n        appPassword = \"q1\"\n        inventoryPassword = \"q1\"\n\n        changeLogParameters {\n            defaultAgentToken = \"cTJxMnEycTI=\"  # base64 of 'q2q2q2q2'\n        }\n    }\n\n    secretStore {\n        # base64 of 'vagrant'\n        serverPassword = \"dmFncmFudA==\"\n        secretStoreSalt = \"dmFncmFudA==\"\n        projectSecretSalt = \"dmFncmFudA==\"\n    }\n\n    ldap {\n        url = \"ldap://oldap:389\"\n        searchBase = \"dc=example,dc=org\"\n        principalSearchFilter = \"(cn={0})\"\n        userSearchFilter = \"(cn=*{0}*)\"\n        usernameProperty = \"cn\"\n        userPrincipalNameProperty = \"\"\n        systemUsername = \"cn=admin,dc=example,dc=org\"\n        systemPassword = \"admin\"\n        returningAttributes = [\"*\", \"memberOf\"]\n    }\n}\n"
  },
  {
    "path": "vagrant/user.ldif",
    "content": "dn: cn=myuser,dc=example,dc=org\ncn: myuser\nobjectClass: top\nobjectClass: organizationalRole\nobjectClass: simpleSecurityObject\nobjectClass: mailAccount\nuserPassword: {SSHA}FZxXb9WXU8yO5VgJYCU8Z+pbVzCJisNX\nmail: myuser@example.org\n"
  }
]